タグ別アーカイブ: OpenID

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

GrailsでOpenIDのチュートリアル

| コメントをどうぞ

最近はコード書くよりRedmineとにらめっこしています。
こんにちわ、DiceK Mikamiです。

Grailsの2.2.0がリリースされましたね。
今回は2.2.0を使って、OpenID実装のチュートリアルを実践してみたいと思います。
 
 

検証環境

  • Java: JDK1.7.0
  • Grails: 2.2.02.2.0 RC2

 

プラグインのインストール

OpenID環境の実装には、Spring Security OpenIDプラグインを利用します。
このプラグインはSpring Security Coreを中核としたOpenID用プラグインです。
さて、ここで一つ大問題があります。
なんと現在(2013/02/07)、Spring Security CoreがV2.2.0で動きません。
ちゃんとチュートリアル通りに動かしても以下のようなエラーが返ってきます。

grailsopenid02
なんでしょうか。
私の環境が悪いのでしょうか。
それとも日々の行いが悪いからでしょうか。
へこみます。
一体私が何をしたというのだ!

気を取り直して、ここでは2.2.0 RC2を使うことにします。(なぜかRC版では動くのです)
さっそくコマンドプロンプトからアプリ作成します。

$ grails create-app openidsample
$ cd openidsample

次に、プラグインをインストールしていきましょう。
と、いきたいところですが、しばしお待ちください。
チュートリアルでは、サクッとプラグインをインストールしていますが、この通りにやっても動きません。
ここはチュートリアルに掲載されていない重要な作業です!
プラグインの中では、Google Guice2.0を利用しているのですが、セントラルリポジトリにありません。
インストールページには、BootConfig.groovyにリポジトリを追加しろと記載されていますが、残念なことにこんなことでは動かないのです。
理由は不明ですが、どうやらリポジトリのURLが2.0のものではないようです。
仕方ないので、自前でGoogle Guice2.0を用意しましょう。
guice-2.0.jarというファイルが必要になりますので、「 guice-2.0.jar download 」とかで検索すればダウンロードサイトがヒットすると思います。
そこからダウンロードしてください。
ダウンロードしたguice-2.0.jarを以下の画像にあるフォルダに入れておきます。

grailsopenid01

このフォルダに入れておくとローカルライブラリとしてGrailsが勝手に処理してくれます。
ここまでの作業が終わりましたら、プラグインをインストールします。

$ grails install-plugin spring-security-openid

spring-security-openidはspring-security-coreに依存しているんで、勝手にそちらもインストールしてくれます。
余談ですが、私の環境ではインストールしてくれませんでした。ので、別途spring-security-coreをインストールしました。嫌われているのでしょうか。
 
 

雛形の作成

Grailsは最近の流行りよろしくほとんどがコマンドで作成できます。
もう障害はないので、雛形をバリバリ作っていきます。

$ grails s2-quickstart com.openidsample User Role
$ grails s2-init-openid
$ grails s2-create-persistent-token com.openidsample.PersistentLogin
$ grails s2-create-openid com.openidsample.OpenID

ここで作成されるファイル群は、サーバ側のアカウント管理、OpenIDとの紐付け、トークン管理を行うクラスです。
 
 

雛形の改良

単純にコマンドだけでは、OpenIDとの紐付けは行ってくれませんので、OpenID用にカスタマイズする必要があります。

grails-app/domain/com/openidsample/User.groovy

package com.openidtest

class User {
    String username
    String password
    boolean enabled
    boolean accountExpired
    boolean accountLocked
    boolean passwordExpired

    // hasManyを追加する
    static hasMany = [openIds: OpenID]

    static constraints = {
        username blank: false, unique: true
        password blank: false
    }

    static mapping = {
        password column: '`password`'
    }

    ー後略ー
}

次に、BootStrap.configでロールなどを追加しておきます。

grails-app/conf/BootStrap.groovy

import com.openidsample.Role
import com.openidsample.User
import com.openidsample.UserRole

class BootStrap {
    def springSecurityService

    def init = { servletContext ->
        String password = 'password'

        def roleAdmin = new Role(authority: 'ROLE_ADMIN').save()
        def roleUser = new Role(authority: 'ROLE_USER').save()

        def user = new User(username: 'user', password: password, enabled: true).save()
        def admin = new User(username: 'admin', password: password, enabled: true).save()

        UserRole.create user, roleUser
        UserRole.create admin, roleUser
        UserRole.create admin, roleAdmin, true
    }
}

一緒にユーザーも作っています。
これはローカルサーバーのユーザーです。
次にURLマッピングも若干変更します。
これはSpring Security Coreが作成したログイン画面ではなく、OpenID用のログイン画面に遷移させるために行います。

grails-app/conf/UrlMappings.groovy

class UrlMappings {
    static mappings = {
        "/login/auth" { 
            controller = 'openId'
            action = 'auth' 
        }

        "/login/openIdCreateAccount" {
            controller = 'openId'
            action = 'createAccount'
        }

    ー後略ー
    }
}

 

テスト用ファイルの作成

次にテスト用にセキュアなページを作っておきます。

$ grails create-controller secure

内容は簡単にロールによって表示ページを分けるようにします。

grails-app/controllers/openidsample/SecureController.groovy

package openidsample

import grails.plugins.springsecurity.Secured

class SecureController {
    // 管理ユーザー用
    @Secured(['ROLE_ADMIN'])
    def admins = {
        render 'Logged in with ROLE_ADMIN'
    }

    // 一般ユーザー用
    @Secured(['ROLE_USER'])
    def users = {
        render 'Logged in with ROLE_USER'
    }
}

 

実験

ここまで作成しましたら、あとは実験してみます。

$ grails run-app

起動しましたら、 http://localhost:8080/openidsample/secure/users にブラウザからアクセスしてみましょう。

grailsopenid03

ログインできていないので、上画面のようなログインページが表示されると思います。
今回はGoogleを利用しようと思いますので、テキストボックスにGoogleのOpenID用URLを入れてログインしましょう。

Google OpenID用URL: https://www.google.com/accounts/o8/id

指示の通り操作を行うと、ローカルサーバーのトップ画面に戻ってきます.
途中でアカウントを作るように促されますが、これはローカルサーバーのアカウントとの紐付けを行うためのものです。
再度 http://localhost:8080/openidsample/secure/users にブラウザからアクセスします。
以下のような画面が表示されれば成功です。

grailsopenid04
ひとまずチュートリアルとしてはこのような感じとなります。
認証サーバーのURLは固定ですのでそれをボタンにしたり、紐づけたアカウントに管理権限をつけたりと言った改造を施せば簡単にOpenID連携をしたアプリを構築できると思います。
プラグインがもう少し安定すれば実践でも使っていけるでしょう。
 
 

参考URL

http://grails-plugins.github.com/grails-spring-security-openid/