流行りのOpenID Connectを使ったSSOを自前で作ってみた(RP編)


はじめに

前回はOpenAMをインストールして認証サーバ(IDP)を作成しました。

今回はWEBアプリ側(RP)でOpenID Connectを使った認証処理を実装していきます。

 

準備

まずOpenID Connectを使う上で、WEBアプリから認証サーバにアクセスする

URLを確認しましょう。

http://認証サーバ:8080/openam/.well-known/openid-configuration

のようなURLにブラウザでアクセスするとJSONの文字列が返却されます。

ここにどんなURLが用意されているか記載されています。

OpenAMでは以下のものが提供されているのがわかります。

 

認可エンドポイント
/oauth2/authorize
アプリケーションから認証してくれ!と依頼をかけるURL

アクセストークンエンドポイント
/oauth2/access_token
認証OK後、許可証(アクセストークン)の取得と更新をするポイント

ユーザ情報エンドポイント
/oauth2/userinfo
ユーザ情報を取得するためのエンドポイント→許可証(アクセストークン)が必要

Registrationエンドポイント
/oauth2/connect/register
新しいユーザを登録するためのエンドポイント

ログアウトエンドポイント
/oauth2/connect/endSession

セッション確認エンドポイント
/oauth2/connect/checkSession

 

これらのURLへ、WEBアプリ側のサーブレットからHTTP通信とか

リダイレクトをかけることで認証サーバとやりとりするんですね。

google様のAPIを使ったことがある人は理解しやすいかと思います。

 

今回JAVAで実装しますが、PHPだろうがなんだろうができますので

環境に合わせて試してみてください。

 

なお、今回はOpenID Connectでいうcode flowという方式で認証します。

もう一つimplicit flowってのもあるのですが今回は使いません。

違いはcode flowだとアプリ内に認証サーバにアクセスするパスワードを

セットしておく方式。

implicit flowはパスワードセットしなくていいけど1つ手順が増える方式。

webサーバ内にパスワード書いとく分には漏洩しない(だろう)ので

code flowでOK。

スマートフォンアプリとかだとパスワードを端末にばらまくので

(悪いやつがいると)漏洩するのでimplicit flow。

みたいな考え方なのかと理解してまっす。

 

実装

とりあえず、JAVAのなんらかwebアプリケーションがあって

インターセプターやらプロキシやらで、

ログイン必要画面のアクセス時にログインチェック用メソッドが

呼び出される感じになってることを前提とします笑

 

設定値を作っておく

認証サーバにアクセスするURLとかの情報は

設定ファイルにでも書いておきましょう。

今回はハードコードしてやってやりましたよ。

  private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
  private static final JsonFactory JSON_FACTORY = new JacksonFactory();

  // OAuth2 Parameters
  private static String CLIENT_ID = "前回OAuth2.0エージェント作った時の名前";
  private static String CLIENT_SECRET = "同じくパスワード";
  private static final String SSO_HOST = "http://sso.tryerror.net:8081/openam/";
  private static final String SCOPE = "openid email profile phone address"; // require "openid" at least

  // openAM endpoint url
  private static final String AUTHORIZATION_SERVER_URL = SSO_HOST + "oauth2/authorize";
  private static final String TOKEN_SERVER_URL = SSO_HOST + "oauth2/access_token";
  private static final String PROFILE_SERVER_URL = SSO_HOST + "oauth2/userinfo";

  // redirect url from openAM
  // 認証サーバからリダイレクトで戻って来るURL。
  // 結果を受け取るサーブレットがcallされるようにセットしてね。
  // (事前にOpenAMのOAuthエージェントに設定したURLじゃないとだめ)
  private static final String REDIRECT_URL = "http://localhost:8080/webservice/OAuthLogin/oauth2Callback";

前回作成したOAuthエージェントでセットした内容を記述していますね。

SCOPEは何の情報にアクセスするかを指定します。

この例だとopenid(認証させてね)、email(emailアドレス見ちゃうよ)、profile(名前見ちゃうよ)、

phone(電話番号もね)、address(住所も見ちゃう!)

となってます。

こんな感じでユーザ情報のカラムに当たる項目をscopeにセットします。

 

まずは自サーバのセッションを確認

さてログインチェックが来た際、まず通常のWEBのように

自分自身の認証セッションがあるか確認してあるならOKにしましょう。

というのもいつもいつも認証サーバに聞きに行く必要もないので。

一度認証したら自分のアプリ内セッションに認証OKと記録しておきましょう。

 

で、認証していない場合。やっと認証サーバに伺いを立てます。

 

 

認証サーバに伺いを立てる

方法は簡単です。上述した認可エンドポイントに

おもむろにリダイレクトすればOKです。

リダイレクトする際、URLにいくつかパラメータをつけてやります。

scopeとか認証サーバから戻って来るURLとか。

この辺りはライブラリがいろいろあるのでそれを使うと便利です。

 

今回は認証サーバとのやりとりを「Google APIs Client Library for java」

使って実装してみます。

そんなに難しい処理ではないので自作してもよいのですが

ここはgoogle様に従って使ってみます。

 

「Google APIs Client Library for java」はココから一式ダウンロードできます。

ダウンロードしたものを解凍し、WEBアプリケーションのlibとかに配置して

クラスパスを通します。

今回必要だったのは以下です。

google-oauth-client-1.19.0.jar
google-http-client-jackson2-1.19.0.jar
google-http-client-1.19.0.jar
jackson-core-2.1.3.jar
(言語、環境に合わせて読み替えてください)

 

そして以下のような感じでまずはURLを作成します。

  public static String getUrlforCodeFlowAuth() {
     AuthorizationCodeRequestUrl codeUrl
         = new AuthorizationCodeRequestUrl(AUTHORIZATION_SERVER_URL, CLIENT_ID);
     codeUrl.setScopes(Arrays.asList(SCOPE));
     codeUrl.setResponseTypes(Arrays.asList("code"));
     codeUrl.setRedirectUri(REDIRECT_URL);

     return codeUrl.build();
  }

んでリダイレクト!

response.sendRedirect(OAuthUtil.getUrlforCodeFlowAuth());

するとブラウザにはOpenAMのログイン画面が・・・でます

(OpenAMログイン中だと認証OKとなりスルーするので、一度ログアウトしておいてください)

ID/PASSを入力すると

redirect pathに設定したURLにリダイレクトで戻ってきます。

ので受け取りメソッドを用意しておきます。

  public String oauth2Callback() {

    // OpenAM が "code"という名前のパラメータをくれるので受け取る。
    String code = ((String[])request.getParameterMap().get("code"))[0];

    //TODO: codeがない場合の処理(直接たたいたとか)

    // もらったcodeを使ってアクセストークンを取りに行くメソッドをたたく。
    String accesstoken = OAuthUtil.getAccessToken(code);

    // アクセストークンをセッションにでもつっこんでおくか。
    commonDto.setAccessToken(accesstoken);

    // アクセストークンを使ってユーザ情報を取ってくるぞ
    UserDto user = OAuthUtil.getProfileData(accesstoken);

    // 取ってきたユーザ情報をセッションに入れちゃう。
    Beans.copy(user, userDto).execute();
    userDto.setUserid(user.getSub());
    userDto.setUsername(user.getName());

    // 認証OK後の画面に遷移するぞ!

  }

コメントに書いときましたが、大体の流れはわかるでしょうか。

OpenAMからリダイレクトで戻る

パラメタでcodeというものをくれる。codeを使ってアクセストークンをOpenAMからもらう。

もらったアクセストークンを使ってユーザ情報をOpenAMからもらう。

 

なーんか何回もやりとりしてめんどくさいと思ったでしょう。

でも、そういうもんです。

 

書き忘れていたcodeを使ってアクセストークンを取得するサンプルです。

  public static String getAccessToken(String code) {

    // tokenを取得
    TokenResponse tr = getTokenResponse(code);

    return tr.getAccessToken();
  }


  private static TokenResponse getTokenResponse(String code) {
    // アクセストークンをもらうためのURLを作る
    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;
  }

そしてユーザ情報を取得するサンプルです。

  public static UserDto getProfileData(String accessToken) {
    // profile情報をJSONで作るメソッドをキック
    String data = getProfileJson(accessToken);

    Gson gson = new Gson();
    UserDto user = gson.fromJson(data, UserDto.class);
    return user;
  }

  private static String getProfileJson(String accessToken) {
    try {

      // profile取得用エンドポイントにアクセスするURLを作る
      URL url = new URL(PROFILE_SERVER_URL + "?access_token=" + accessToken);

      // HTTP通信するぞ
      HttpURLConnection connection = (HttpURLConnection) url.openConnection();
      connection.setDoOutput(true);
      // この辺は決まった形式。アクセストークンもこんな感じでOpenAMに渡す。
      connection.setRequestProperty("Authorization", String.format("Basic %s", accessToken));
      connection.setRequestProperty("Content-Type", "application/json");
      connection.setRequestProperty("Accept", "*/*");
      connection.setRequestMethod("POST");
      connection.connect();

      int responseCode = connection.getResponseCode();
      //System.out.println("\nSending 'POST' request to URL : " + url);
      //System.out.println("Response Code : " + responseCode);

      switch(responseCode) {
        case 200:
        case 201:
          // 戻ってくるのはJSON文字列。全部読み取って返すとか。
          InputStream content = (InputStream) connection.getInputStream();
          BufferedReader in = new BufferedReader(new InputStreamReader(content));
          String line;
          StringBuilder sb = new StringBuilder();

          while ((line = in.readLine()) != null) {
              //System.out.println("\n line : " + line);
              sb.append(line+"\n");
          }
          in.close();
          return sb.toString();
        default:
          return "";
      }

    } catch (Exception e) {
      // TODO: handle exception
      e.printStackTrace();
      return "";
    }
  }

どんどんコメントが減っていきましたね。

まぁ雰囲気だけつかんでください。

ユーザ情報を取得するサンプルはgoogle様ライブラリを使わず、

自分でHTTP通信するパターンで書いてみました。

この方がわかりやすいという人もいるかもしれませんね。

 

こんな感じのやりとりが正常に完了すれば認証OKかつユーザ情報取得済

となります。

あとはユーザ情報のsessionがあるかどうかとかで以降の認証を

行えばOKですね。

 

ざっと説明とコードを記載しました。

実は認証サーバとのやりとりの中で、

アクセストークンの有効期限をチェックするとか

やりとりが本物かどうかstateを仕込んでチェックするとか

といった考慮も必要になるのですが今回は割愛しています。

 

次回はopenAMのログイン画面をカスタマイズしたりする方法を

ご紹介しようかと思います。

 


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です