タグ別アーカイブ: Google

GoogleのService Account認証に潜む invalid_grant 問題

| 2件の返信

Appleのイベントは見ましたか?
Spring forward.のことです。
いやぁ、まさかあんなサプライズがあるとは。。。
どうも、DiceK Mikamiです。

さて、今回は(Appleとは全然関係なく)Google API使うにあたって利用することになるService Account認証に潜む罠に関して取り上げてみたいと思います。
続きを読む

JavaでOpenID ConnectのBasic Clientを実装してみた

| コメントをどうぞ

5月も中頃となり、気候は穏やかな毎日ですね。
あーあ、ずっと春ならいいのに。
どうもDiceK Mikamiです。

今回は、あのGoogleも対応を発表したOpenID Connectのサンプル実装をご紹介します。
これであなたもOpenID Connecterだ!

1. OpenID Connectとは

まずは、OpenID Connectについて軽くおさらいをしておきましょう。
OpenID Connectについて調べてみますと、、、

OpenID Connectは、ユーザーがパーソナルデータのコントロールを行うことを目的とした認証プロトコルです。
Webサービスごとにアイデンティティを使い分けて、利用者のプライバシー強化を実現する技術として標準化が進められてきました。

ごめん。なに言ってるかさっぱりわからねぇ。
複数のサービスでIDを同じにしておけば、ユーザーとしてはログインの手間が省けますし、ID情報をセキュアなOpenID Providerが管理しているためパスワードなどの秘匿情報の流出を防ぐことができます。
さらにユーザー属性がサービスによって異なるからそれを標準化して、様々なサービスで属性情報を共通化しようと。
でも、これはOpenID自体の利点であって、OpenID Connectになったから出てきたメリットではありません。

では、なにがいいの?
OpenID ConnectはOAuth2.0ベースで仕様が策定されています。
これによってWebアプリケーションだけでなく、さまざまなアプリケーションと連携できるようになりました。

既存アプリケーションでは、OAuth2.0本来の仕様を利用して認証を行っているものもありましたが、OAuth2.0は認可プロトコルのため、実際のところはあまり好ましい実装ではありませんでした。
なので、OpenIDという認証プロトコルとOAuth2.0という認可プロトコルを結合させて、本来あるべき認証と認可の仕組みを提供しようという考えなんですね。
また、それによってプロバイダから提供されるID Tokenなるユーザー属性によってID連携を行うことができます。

実際のところ、すでにこうした実装がされているアプリにとってはなんも嬉しくないんですけど。。。
何でもありません。

OpenID Connectは、OAuth2.0の拡張にて実装が可能となっています。
今回のサンプルでは、Basic Client Profile(OAuth 2.0 Authorization Code Grantの拡張)を実装しますので、そのフローを以下にてご紹介します。

openidconnect00

うん。よく見るOAuth2.0のフローですね。
さてさて、次項からOpenID Connectのサンプル実装の解説に入っていきます。

2. 検証環境

  • JDK: 1.7.0_51

3. 下準備

OpenID Connectの実装、、、の前に、クライアントIDとクライアントシークレットを取得しておきましょう。
今回はGoogleをOPとしますので、Google Developers Consoleからアプリを作成しておきます。

Google Developers Consoleにアクセスしたら、CREATE PROJECTをクリックします。

openidconnect01

左ペインよりAPIs & authをクリックします。

openidconnect02

APIs & authからCredentialsを選択し、右ペインのOAuthカテゴリ内にあるCREATE NEW CLIENT IDをクリックします。

openidconnect03

Create Client IDモーダルApplication typeWeb applicationを選択します。
Authorized JavaScript originsには、認可するサイトのオリジンを記述します。(後述するサンプルを利用する場合、http://localhost:4567を指定してください)
Authorized redirect URIには、コールバックURLを記述します。(後述するサンプルを利用する場合、http://localhost:4567/oauth2callbackを指定してください)
設定したらCreate Client IDボタンをクリックしましょう。

openidconnect04

最後に作成されたアプリのClient IDClient secretを覚えておきましょう。

openidconnect05

4. 実装

それではようやく実装に入っていきましょう。
今回は、Spark Framework(like a Sinatra framework for Ruby)とGoogle謹製のgoogle-api-java-clientを使います。
(サンプルコードはGitHubにアップロードしてあります。GitHub corestrike/OpenIDConnectSample
仕組みとしてはOAuth2.0と変わりませんので、その順序で実装していきます。
最初に、Authorization Codeを取得します。

App.java

// OpenID Connectのエントリーポイント
post(new Route("/oauth") {
   @Override
   public Object handle(Request req, Response res){
       // クライアントID
       String clientId = req.queryMap().get("client_id").value();
       // クライアントシークレット
       String clientSecret = req.queryMap().get("client_secret").value();

       if(clientId.isEmpty() || clientSecret.isEmpty()){
           res.status(400);
           return "Bad Request!";
       }else{
           setCLIENT_ID(clientId);
           setCLIENT_SECRET(clientSecret);
           String authUrl = getCodeUrl();
           res.redirect(authUrl);
       }

       return null;
   }
});

// Authorization Codeを取得するURLを生成
private static String getCodeUrl() {
    AuthorizationCodeRequestUrl codeUrl
        = new AuthorizationCodeRequestUrl(AUTHORIZATION_SERVER_URL, CLIENT_ID);
    // スコープにopenidを入れる必要がある
    codeUrl.setScopes(Arrays.asList("openid email profile"));
    codeUrl.setResponseTypes(Arrays.asList("code"));
    codeUrl.setRedirectUri(REDIRECT_URL);
    codeUrl.setState(STATE);

    /* RefreshTokenを返却する場合
    codeUrl.set("access_type", "offline");
    */

    return codeUrl.build();
}

Authorization Codeを取得する際のスコープにopenidを含めるのがポイントです。
これを含めていると最終的にユーザー属性を含んでいるクレームがAccessTokenと同時に返却されてくるようになります。
Authorization Codeを取得するURLは、Googleの場合、https://accounts.google.com/o/oauth2/authになります。

App.java

// コールバックメソッド
// 取得したトークンを分解してブラウザに返却する
get(new FreeMarkerRoute("/oauth2callback") {
    @Override
    public Object handle(Request req, Response res){
        // 本来はここでstateチェックをする

        // Tokenを取得
        String code = req.queryMap("code").value();
        TokenResponse tr = getTokenUrl(code);

        // ID TokenはJWTで返却されるのでそれを分解する
        String[] jwt = ((String)tr.get("id_token")).split(".");
        byte[] jwtHeader = Base64.decodeBase64(jwt[0]);
        byte[] jwtClaim = Base64.decodeBase64(jwt[1]);
        byte[] jwtSigniture = Base64.decodeBase64(jwt[2]);

        // 本来はここでSignを検証する

        Map<String, Object> attributes = new HashMap<>();
        attributes.put("accesstoken", tr.getAccessToken());
        attributes.put("refreshtoken",
                 tr.getRefreshToken() == null ? "null" : tr.getRefreshToken());
        attributes.put("tokentype", tr.getTokenType());
        attributes.put("expire", tr.getExpiresInSeconds());
        attributes.put("jwtheader", new String(jwtHeader));
        attributes.put("jwtclaim", new String(jwtClaim));
        attributes.put("jwtsign", new String(jwtSigniture));

        try{
            JsonParser jsonParser
                          = JSON_FACTORY.createJsonParser(new String(jwtClaim));
            while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
                String name = jsonParser.getCurrentName();
                if(name != null) {
                    jsonParser.nextToken();

                    switch (name){
                        case "iss":
                            attributes.put("iss", jsonParser.getText());
                            break;
                        case "sub":
                            attributes.put("sub", jsonParser.getText());
                            break;
                        case "azp":
                            attributes.put("azp", jsonParser.getText());
                            break;
                        case "email":
                            attributes.put("email", jsonParser.getText());
                            break;
                        case "at_hash":
                            attributes.put("at_hash", jsonParser.getText());
                            break;
                        case "email_verified":
                            attributes.put("email_verified", jsonParser.getText());
                            break;
                        case "aud":
                            attributes.put("aud", jsonParser.getText());
                            break;
                        case "iat":
                            attributes.put("iat", jsonParser.getText());
                            break;
                        case "exp":
                            attributes.put("exp", jsonParser.getText());
                            break;
                    }
                }
            }
        }catch (IOException e){
            e.printStackTrace();
        }

        return modelAndView(attributes, "callback.ftl");
    }
});

// Authorization Codeを利用して、Tokenを取得
private static TokenResponse getTokenUrl(String code) {
    AuthorizationCodeTokenRequest tokenUrl = new AuthorizationCodeTokenRequest(
            HTTP_TRANSPORT,
            JSON_FACTORY,
            new GenericUrl(TOKEN_SERVER_URL),
            code
    );
    tokenUrl.setGrantType("authorization_code");
    tokenUrl.setRedirectUri(REDIRECT_URL);
    tokenUrl.set("client_id", CLIENT_ID);
    tokenUrl.set("client_secret", CLIENT_SECRET);

    TokenResponse tr = null;
    try {
        tr = tokenUrl.execute();
    } catch (IOException e) {
        e.printStackTrace();
    }

    return tr;
}

Tokenの取得は、Authorization URLにPost送信します。
Token取得のためのURLは、https://accounts.google.com/o/oauth2/tokenになります。
サンプルでは実装していませんが、実運用するためにはStateのチェックや返却されたID Tokenの正当性検証などを実装する必要があります。
ID TokenはJWTにて返却されるため、JWT解析を行う必要があります。
そのあたりに関しては、本稿では割愛します。(参考文献にあるJWT関連のページは大変分かりやすいと思います)

これだけで実装は完了です。
簡単ですよね?
すでにOAuth2.0クライアントを実現しているアプリケーションなどでは、スコープを少し変更するだけでOKですよね。
(まぁ、JWT解析とか正当性検証とか必要ですけど。。。

OpenID Connectは様々なプロバイダで実装が進んでいます。
これからのID連携の一つとして実装パターンを覚えておいて損はないかと思います。
それでは、また次回。

参考文献

Using OAuth 2.0 for Login (OpenID Connect)
Google Developers Console
OpenID Connect Basic Client Implementer’s Guide 1.0 – draft 33
Spark – A small web framework for Java
google-api-java-client
hiyosi’s blog – JWTについて簡単にまとめてみた
GitHub corestrike/OpenIDConnectSample