Google+ Sign-in / Moment API

Google+ PlatformにサインインしてAPIを呼び出すところまでやってみたのでまとめます。
[こちら](https://developers.google.com/+/web/signin/server-side-flow)を参考に、ちょっと分からなかったところを補いつつ、[Moment API](https://developers.google.com/+/api/latest/moments)でApp Activityを送信するところまでやってみます。環境は、[前回の記事](http://qiita.com/nabe/items/2f2b00d7b076895729b5)で構築したものとなります。

# 準備その１：Client情報の登録
まずはGoogle+のClient IDとClient Srcretを取得します。公式ドキュメントのStep 1に従えばいいので省略します。

# 準備その２：必要なjarを配置
今回はMaven2で依存するjarを管理しているので、pom.xmlに設定を追加しておきます。WEB-INF/lib/にコピーも忘れずにしておきます。

```xml
  <repositories>
    <repository>
      <id>google-api-services</id>
      <url>http://google-api-client-libraries.appspot.com/mavenrepo</url>
    </repository>
  </repositories>

 ...

    <dependency>
      <groupid>com.google.api-client</groupid>
      <artifactid>google-api-client</artifactid>
      <version>1.13.2-beta</version>
    </dependency>
    <dependency>
      <groupid>com.google.api-client</groupid>
      <artifactid>google-api-client-servlet</artifactid>
      <version>1.13.0-beta</version>
    </dependency>
    <dependency>
      <groupid>com.google.api-client</groupid>
      <artifactid>google-api-client-appengine</artifactid>
      <version>1.13.2-beta</version>
    </dependency>
    <dependency>
      <groupid>com.google.apis</groupid>
      <artifactid>google-api-services-plus</artifactid>
      <version>v1-rev57-1.13.2-beta</version>
    </dependency>
    <dependency>
      <groupid>com.google.apis</groupid>
      <artifactid>google-api-services-oauth2</artifactid>
      <version>v2-rev9-1.7.2-beta</version>
    </dependency>
```

# 実装その１：CSRF対策
サインインボタンを表示するControllerを作成します。
この後説明するAccess Tokenの保存処理が不正に呼び出されないように、ランダムなパラメータを生成してセッションに保存しておきます。
同時に、Access Tokenの保存処理を呼び出すJavascriptにパラメータを渡せるように、リクエストにセットしておきます。

```java
    @Override
    public Navigation run() throws Exception {
        String token = new BigInteger(130, new SecureRandom()).toString(32);
        sessionScope("validateToken", token);
        requestScope("validateToken", token);
        return forward("test.vm");
    }
```

# 実装その２：Javascriptライブラリの読み込み
Oauth2のAuthorization Code取得まではJavascriptでやるようなので、test.vmでGoogle+ライブラリを読み込んでおきます。
公式ドキュメントのStep 3のコピペでOKです。

# 実装その３：サインインボタンの設置
test.vmにサインインボタンを設置します。ここがポイントなのですが、
**Moment APIを使う場合、data-requestvisibleactionsが必須になります。**おそらくAPIの認可が必要ということなのでしょう。見事にハマったorz
他に指定できるパラメータは[こちら](https://developers.google.com/+/web/signin/)に載っているのでご参照ください。

```html
<div id="signinButton">
    <span class="g-signin"
        data-scope="https://www.googleapis.com/auth/plus.login"
        data-clientid="あなたのClient ID"
        data-redirecturi="postmessage"
        data-accesstype="offline"
        data-cookiepolicy="single_host_origin"
        data-callback="signInCallback"
        data-requestvisibleactions="http://schemas.google.com/AddActivity">
    </span>
</div>
```

# 実装その４：callback関数の実装
上記サインインボタンに設定したcallback関数を実装します。この中でAuthorization Codeを元にAccess Tokenを取得するリクエストを送信します。パラメータには、CSRF対策用のパラメータも設定します。

```js
function signInCallback(authResult) {
    if (authResult['code']) {

        // Hide the sign-in button now that the user is authorized, for example:
        $('#signinButton').attr('style', 'display: none');

        // Send the code to the server
        jQuery.ajax({
            type: 'POST',
            url: '/api/storetoken',
            success: function(result) {
                
            },
            data: {
                code: authResult['code'],
                validateToken: '$validateToken',
            }
        });
    } else if (authResult['error']) {
        alert('Google+ Authorication Error');
    }
}
```

# 実装その５：Access Tokenの保存
JavascriptのPOSTを受けるControllerを実装します。まずはCSRF対策でパラメータチェックします。
その後、Authorization CodeとClient ID、Client Secretを使ってAccess Tokenを取得し、セッションに保存します。

```java
    @Override
    public Navigation run() throws Exception {
        if (!validateToken()) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            Map<string string=""> data = new HashMap<string string="">();
            data.put("state", "403");
            return json(data);
        }

        String clientId = "あなたのClient ID";
        String clientSecret = "あなたのClient Secret";
        String code = asString("code");

        HttpTransport transport = new NetHttpTransport();
        JsonFactory jsonFactory = new JacksonFactory();
        GoogleTokenResponse tokenResponse = new GoogleAuthorizationCodeTokenRequest(transport, jsonFactory,
            clientId, clientSecret, code, "postmessage"
        ).execute();
        GoogleCredential credential = new GoogleCredential.Builder()
            .setJsonFactory(jsonFactory)
            .setTransport(transport)
            .setClientSecrets(clientId, clientSecret).build()
            .setFromTokenResponse(tokenResponse);
        Oauth2 oauth2 = new Oauth2.Builder(transport, jsonFactory, credential).build();
        Tokeninfo tokenInfo = oauth2.tokeninfo().setAccessToken(credential.getAccessToken()).execute();
        if (tokenInfo.containsKey("error")) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            Map<string string=""> data = new HashMap<string string="">();
            data.put("state", "403");
            return json(data);
        }
        if (!tokenInfo.getIssuedTo().equals(clientId)) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            Map<string string=""> data = new HashMap<string string="">();
            data.put("state", "403");
            return json(data);
        }

        sessionScope("gPlusId", tokenInfo.getUserId());
        sessionScope("gAccessToken", credential.getAccessToken());

        Map<string string=""> data = new HashMap<string string="">();
        data.put("state", "200");
        return json(data);
    }

    protected boolean validateToken() {
        String specified = requestScope("validateToken");
        String token = sessionScope("validateToken");
        if (token.equals(specified)) {
            return true;
        }
        return false;
    }

    protected Navigation json(Object data) {
        try {
            JsonFactory jsonFactory = new JacksonFactory();
            JsonGenerator generator = jsonFactory.createJsonGenerator(response.getWriter());
            generator.serialize(data);
            generator.flush();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return null;
    }
```

# 実装その６：Moment API 呼び出し
最後に、取得したAccess Tokenを使ってMoment APIを呼び出すControllerを実装します。

```java
    @Override
    public Navigation run() throws Exception {
        String gPlusId = sessionScope("gPlusId");
        String gAccessToken = sessionScope("gAccessToken");

        String clientId = "あなたのClient ID";
        String clientSecret = "あなたのClient Secret";
        String applicationName = "あなたのアプリ名";

        HttpTransport transport = new NetHttpTransport();
        JsonFactory jsonFactory = new JacksonFactory();
        GoogleCredential credential = new GoogleCredential.Builder()
            .setJsonFactory(jsonFactory)
            .setTransport(transport)
            .setClientSecrets(clientId, clientSecret).build()
            .setAccessToken(gAccessToken);

        Plus plus = new Plus.Builder(transport, jsonFactory, credential).setApplicationName(applicationName).build();

        Moment moment = new Moment();
        moment.setType("http://schemas.google.com/AddActivity");
        ItemScope itemScope = new ItemScope();
        itemScope.setId("target-id-1"); 
        itemScope.setType("http://schemas.google.com/AddActivity");
        itemScope.setName("The Google+ Platform");
        itemScope.setDescription("hogehoge!");
        moment.setTarget(itemScope);
        Moment momentResult = plus.moments().insert("me", "vault", moment).execute();

        MomentsFeed moments = plus.moments().list("me", "vault").execute();

        return redirect("/");
    }
```

自分の英語力不足なのか、公式ドキュメントからは分からなくてハマったところも多かったですが（Oauth2クラスとかTokenInfoクラスを使うのにどのjarが必要か、とか）、なんとか出来ました。
Client IDなどを設定ファイルにしたりなどリファクタリングもまだまだ必要ですが。
道具は揃ったので、これをどう使っていくか考えてみようと思ってます！