OpenID Connect体験



OpenID Connectを学習するために、実際に[Yahoo! ID連携サービス](http://developer.yahoo.co.jp/yconnect/)でOpenID Connectを体験してみました。

# 前置き

## 体験してみた理由

OpenID Connectを利用したID連携アプリケーションを作って見ようと思ったのですが、知識も無く、仕様も難しく、どこから手をつけて良いのかわかりませんでした。そこで、実際にOpenID Connectのフローを一つ一つ手で追ってみて、理解を深めようと考えました。その手順を記事にします。

## OpenID Connectとはなにか

OpenID Connectは、OAuth2.0というアクセストークンを利用したセキュアなサービスの利用ができる**認可**の仕組みの上で**認証**が出来るようにしたものです。以下にOpenID Connectを勉強する上で、参考にしたサイトを記載します。

[OpenID Connect 入門](http://www.slideshare.net/kura_lab/openid-connect-id)

# 前準備

## 事前に必要なコト

Yahoo! JAPANへのアプリケーションの登録が必要です。Yahoo! JAPANにアプリケーションを登録するとアプリケーションID(以下client_idとします)とシークレット(以下client_secretとします)が手に入ります。また、アプリケーションを登録するためには、Yahoo! JAPAN IDが必要です。持っていない場合は作る必要があります。以下の設定でアプリケーションを登録しました。

|設定項目|設定値|
|:--|:----|
|アプリケーションの種類|サーバーサイド|
|コールバックURL|http://example.com|

### 参考URL

[Yahoo! JAPAN IDの登録ページ](https://account.edit.yahoo.co.jp/registration?.src=ydn&.done=http%3A%2F%2Fdeveloper.yahoo.co.jp%2Fyconnect%2F)

[アプリケーションIDの登録ページ](https://e.developer.yahoo.co.jp/register)


## 事前に必要なモノ

* ブラウザ
* 下記のコマンドが実行できるコマンドラインインターフェース
    * tr
    * curl
    * openssl
    * base64
    * date

実行環境

* ブラウザ
  * Google Chrome バージョン 45.0.2454.85 m
* OS
  * Ubuntu 14.04 64bit



# 実際にOpenID Connectしてみる

## STEP1 Authorization codeの取得

このステップではID連携したいアプリケーションがYahoo! JAPANからAuthorization codeを取得する流れを確認しました。このステップでは、Yahoo! JAPANへのログインとID連携の同意が必要であるためブラウザを利用します。以下のURLをブラウザで開きます。

```
https://auth.login.yahoo.co.jp/yconnect/v1/authorization?
response_type=code+id_token
&client_id={client_id}
&redirect_uri=http://example.com
&state=xyz
&scope=openid+profile
&nonce=ef3a091928d5491624c0ac54d697124422705092
```

|パラメータ|必須|説明|
|:--|:--|:----|
|response_type|○|「code」と「id_token」を指定します|
|client_id|○|事前に準備したclient_idの値を入れます|
|redirect_uri|○|アプリケーションIDを登録した時に、コールバックURLに入力したURLを指定します。今回は「[http://example.com](http://example.com)」を指定します|
|state||CSRF対策のランダム文字列を指定します|
|scope||OpenID Connectのリクエストを送る場合は、scopeに必ず「openid」の値を入れる必要があります<br>「openid」の値以外に、取得したい属性情報をscopeで指定します<br>・「openid」のみをscopeに指定すると、ユーザー識別子のみ取得できます<br>・「profile」をscopeに指定すると、姓名・生年・性別が取得できます<br>・「email」をscopeに指定すると、メールアドレスと確認済みフラグを取得できます<br>・「address」をscopeに指定すると、ユーザー登録住所情報が取得できます|
|nonce|id_tokenを取得する際は必須|リプレイアタック対策のパラメーターです。リクエストごとにランダムな文字列を指定します|
|display||ユーザーのUIをpage(PC用UI)、touch(スマートフォン用UI)、wap(フィーチャーフォン用UI)、inapp(ネイティブアプリ用UI)～選択できます。デフォルト値はpageです|
|bail||Yahoo! JAPANが独自に用意したパラメータです。bail=1 を指定すると同意画面で「同意しない」ボタンをクリックした際にcodeパラメーターを付加せずにredirect_uriへリダイレクトさせます。指定しない場合はYahoo!トップへ遷移します|


成功するとログイン/同意画面が表示されます。

* Yahoo! JAPANにログインしていない場合はログインしてください
    * すでにログインしている場合は省略されます
* 同意画面では「同意してはじめる」を選択してください
    * 一度同意すると、次回以降同意画面は省略されます

ログイン/同意が完了する(もしくは事前にログイン、同意している)と、以下のようなURLで「[http://example.com](http://example.com)」にリダイレクトされます。

```
http://example.com/?code=kaeasavt&state=xyz
```

* code=kaeasavtの部分がAuthorization Codeです
    * このcodeの値を保存します
* state=xyzはAuthorization Requestの時に設定したパラメータの値です。リクエスト時の値「xyz」と一致していることを確認します
    * 本来は、リクエスト時にstateの値をセッションに保存し、レスポンスのstateの値を比較します。一致しなければ処理を中断します

## STEP2 Access Token、ID Tokenの取得

このステップではID連携したいアプリケーションが、Step1で取得したcodeをyahoo! JAPANに渡して、Access TokenとID Tokenを取得する流れを確認しました。

Tokenを取得する際は、HTTPリクエストのヘッダーやデータにTokenリクエストに必要な値を指定する必要があるため、コマンドラインインターフェースでcurlコマンドを利用しました。

以下のコマンドを実行して、Tokenを取得します。

```
curl -H 'Authorization: Basic {basicAuth}' -d \
"grant_type=authorization_code&code={code}&redirect_uri=http://example.com" \
https://auth.login.yahoo.co.jp/yconnect/v1/token
```

|パラメータ|必須|説明|
|:--|:--|:----|
|grant_type|○|authorization_code という固定文字列を指定します|
|code|○|認可コードを指定します<br>一度リクエストした認可コードは使用できなくなります|
|redirect_uri|○|STEP1で送ったリクエストのredirect_uriで指定したURLを入力します|

{code}にはSTEP1で取得したcodeの値を入れます。

{basicAuth}には、client_idとclient_secretを「:」（コロン）でつなぎ、Base64でエンコードした値を入れます。

以下のコマンドで{basicAuth}に入れる値を作ります。

```
echo -n "{client_id}:{client_secret}" | base64
```

{client_id}と{client_secret}は事前に用意したclient_idとclient_secretの値を入れます。

成功すると以下のようなレスポンスが表示されます。

```json
{
"access_token":"gCDvPtVgo..z2kTSWEnQs_.YzGtTdmMBWNLGJD0JE3El4v68N3a0EVhbdhmUqoqZsQ0H8samPleQPiFg6PPuFRjWipUpZGq0566RcWrUdsWNPUBCO9AL5tgb6mLS5gzArQvbkPEwSRJFJgAX1SlX1dlu.6mlEEWs5kQiZlZ1SnuTYarkn5o47nC_Kqip6aF7DFBz.alcj4dVaOc9OMPrZw1ioTIzKpNka2UnulvVDhL7KVm00nKcbxUKhoLPl_E4JDGe2ttEg.RStTfQer5oHpR5RSf_5BCpPSampleZx9MYntNVXRHBCczChWJKa3Kck1HlnN3CI9uhdwcD1xx6R5kgi4EMttDoSguU4LxtXiqC24_fAQuHdtz1D9IAf6eeE4xHF_1AfJDkzbyDLWvWVUAbNpiErvKFPM8O8efd9bb74.8WMe49cdK5AaUSEdZg7Hu5H6V5C.KBMs768i.Piv98iXQl0G8vXKmS4rZwrcADVYocokB9VFzlRZruwJFXtWpyW5gvS3.5lSshiN4SampleTT5qsMlrKsSETIj.KNnuPQEDUPWIVpHjLTr1hudsaMPleK8M1atMOGAKr65RdZbd7ItwshOEA--",
"token_type":"bearer",
"expires_in":"3600",
"refresh_token":"AIHv2FUi6ygUPgm0dPSaMPleDztwSX_xd0XZ9ZSiYdWWo5Lcvic-",
"id_token":"eyJ0eXAiOiJKV1QiLSampleiOiJIUzI1NiJ9.eyJpc3MiOiJodHRwxzpcL1wvYXV0aC3sb2dSampleWhvby5jby5qCCIsInVzZXJfaWQiOiJTT09HSUgzV1lCNUpDWFpFSUtUWUdLSFk0USIsImF1ZCI6ImRqMHphaSample5UVNrbE9aMkZWU21wWVJYWnpQV052Ym5OMWJXVnljMlZqY21WMEpuZzlPRGMtIiwiaWF0IjoxNDQSampleDk5LCJleHAiOjE0NDQwMjU5NjUsIm5vbmLlIjhiZWYzYTA5MTkyOGQ1NDkxNjI0YzBjYzU0FDY5NzEyNDQyMjcwNTA3MiJ9.24VcO1TsAmplEqcFK9bOR3tzaoQjDQSY_ufGfMHI1sQ"
}
```

## STEP3 IDトークンの正当性の検証手順

このステップでは、ID連携したいアプリケーションが受け取ったIDトークンが改ざんされていないかどうかを検証する流れを確認しました。

### IDトークンの内容を検証する

STEP2で取得したid_tokenは以下のようにピリオド(.)で区切られた3つ文字列により構成されています。

```
id_token: "{ヘッダー部}.{ペイロード部}.{シグネチャー部}"
```


{ヘッダー部}を以下のコマンドでbase64 デコードすると、内容を確認することが出来ます。

```
echo "{ヘッダー部}" | base64 -d
```

出力結果

```json
{
"typ":"JWT",
"alg":"HS256"
}
```

JWTであることと、HS256は署名生成アルゴリズムとしてHMAC-SHA256を利用していることがわかります。

{ペイロード部}も同様に、base64デコードし、内容を確認します。

```json
{
"iss":"https:¥/¥/auth.login.yahoo.co.jp",
"user_id":"TAHOGSH35SAMPLEZE4PNLMKHE0Q",
"aud":"dj0zaiZpPW9QSklOZ2FVSampleZzPWNvbnN1bWVyc2VjcmV0Jng9ODc-",
"iat":1441606899,
"exp":1444025965,
"nonce":"ef3a091928d5491624c0ac54d697124422705092"
}
```


|パラメータ|内容|検証方法|
|:--|:--|:----|
|iss|IDトークンの発行元|[https://auth.login.yahoo.co.jp](https://auth.login.yahoo.co.jp)と一致すること確認<br>また、ペイロードの内容はjson形式であり、スラッシュ(/)はバックスラッシュ(\\)でエスケープされます|
|user_id|ユーザー識別子|他の検証が全てとおることにより、このIDが正しい物だと検証できます|
|aud|IDトークンの発行対象のアプリケーションID|発行時のアプリケーションID(client_id)と一致することを確認|
|iat|IDトークンの発行時刻のUNIXタイムスタンプ|「（現在時刻のタイムスタンプ値）-（600秒）」以上であることを確認|
|exp|IDトークンの有効期限の時刻のUNIXタイムスタンプ|現在時刻のタイムスタンプ値より大きいことを確認|
|nonce|Authorizationリクエスト時に送信した値|リクエストした時のnonceの値と一致することを確認|

現在時刻のタイムスタンプ値は以下のコマンドで確認します。

```
date +%s
```


### 署名を検証する

YConnectではJWTの署名生成アルゴリズムとしてHMAC-SHA256が利用されています。そのため、検証の手順もHMAC-SHA256に基づいた検証をする必要があります。

以下のコマンドを実行し、上記の{ヘッダー部}と{ペイロード部}をピリオド(.)で繋ぎ、client_secretを使ってHMAC-SHA256でハッシュ化します。

```
echo -n "{ヘッダー部}.{ペイロード部}" | openssl dgst -binary -sha256 -hmac "{client_secret}" > hs256_sign.txt
```

* ハッシュ化するときはバイナリ表現で出力します

ハッシュ化した結果をurl safe base64エンコードします。

```
base64 hs256_sign.txt | tr -d '¥n' | tr -d '=' | tr '/+' '_-'
```

出力された結果と{シグネチャー部}が一致することを確認


## STEP4 属性情報の取得

このステップではID連携したいアプリケーションが認証したユーザの属性情報(名前、メールアドレス、住所等)をAccess Tokenを使って取得する流れを確認しました。

UserInfoAPIにAccess Tokenを渡すと、属性情報を得ることができます。

コマンドラインインターフェースで以下のコマンドを実行します。

```
curl -H 'Authorization: Bearer {access_token}' https://userinfo.yahooapis.jp/yconnect/v1/attribute?schema=openid
```

{access_token}に、STEP2で取得したAccess Tokenの値を入れます。

成功すると以下のようなレスポンスが表示されます。

```json
{
"user_id":"TAHOGSH35SAMPLEZE4PNLMKHE0Q",
"name":"山田太郎",
"given_name":"太郎",
"given_name#ja-Kana-JP":"タロウ",
"given_name#ja-Hani-JP":"太郎",
"family_name":"山田",
"family_name#ja-Kana-JP":"ヤマダ",
"family_name#ja-Hani-JP":"山田",
"gender":"male",
"birthday":"1989",
"locale":"ja-JP"
}
```

STEP1でscopeに指定した値で、返却される属性情報が変わります。

# まとめ

## はまったこと

* STEP3の署名を検証する時にけっこうはまってしまいました。はまってしまった原因は、HMAC-SHA256でハッシュ化するときに**Hex文字列**で出力した値をbase64でエンコードしていたため、シグネチャーの値と一致しませんでした。正しくは**バイナリ形式**で出力した値をbase64でエンコードします
 
## 感想

* 最初は仕様を読んで理解しようとしましたが、実際に手でフローを追ってみると、理解が深まりました
* 今回試した部分を実装するだけでID連携が可能となるので、自分でパスワード管理するよりもコストが少なく済むことを実感しました