OpenID ConnectをYahoo! ID連携を使って試してみる

## 概要
OpenID Connectの勉強も兼ねて、[Yahoo!ID連携](https://developer.yahoo.co.jp/yconnect/v2/)を利用してOpenID Connectの処理フローの確認を行いました。
この記事では、OpenID Connectの概要とYahoo!ID連携での検証結果をまとめています。

**(11/09 追記)
OIDCの利用用途について誤りがありましたので、修正させていただきました。**

## OpenID Connect(OIDC)とは?
### OIDCについて
OpenID Connect(OIDC)は**認証**の用途で利用される**IDトークン**を発行するための仕様です。

OIDCのフローでは、json形式のメッセージをREST APIを用いてやりとりします。
OIDCのDesign Goalとして「OIDCの利用・実装が容易であること」を明記しており、xmlベースのメッセージをHTTP/SOAPでやりとりするSAMLと比較しても実装が容易になっています。

(参考：[OpenID Connect FAQ and Q&As](https://openid.net/connect/faq/))
>It uses straightforward REST/JSON message flows with a design goal of
 **“making simple things simple and complicated things possible”**

>It’s uniquely easy **for developers to integrate**, 
compared to any preceding Identity protocol.

企業内の認証基盤として利用されるSAMLとの比較に関しては、
[Technical Comparison: OpenID and SAML](http://identitymeme.org/doc/draft-hodges-saml-openid-compare.html)にまとめられています。

### OAuth 2.0 + Authentication(認証) = OpenID Connect?
OIDC自体は認証用途を目的としたプロトコルですが、**OAuth2.0**という**認可**を目的としたプロトコルがベースになっています。
>**OpenID Connect** is an interoperable **authentication protocol** 
based on the **OAuth 2.0** family of specifications.

~~正確にいうと、OIDCは**OAuthで「権限を委譲する相手」を認証する用途**で利用されます。~~

**(11/09 追記)**
OIDCの用途は[OpenID FoudationのHP](https://openid.net/connect/)に下記のように記載されています。
>It allows Clients to **verify the identity of the End-User** based on the authentication performed by an Authorization Server, as well as to **obtain basic profile information about the End-User** in an interoperable and REST-like manner.

この部分の記載から、OIDCの用途として**エンドユーザーに関するプロファイル情報を取得して(クライアント側で)「エンドユーザーの検証を行うこと」**が定義されていることがわかります。

もう一点重要なポイントとして、OIDCのフローでは**「自分自身に対してトークンが払い出されたこと」を検証すること**ができます。

OAuth2.0では、アプリなどにアクセストークンを払い出すことで権限移譲を実現しますが、[「単なる OAuth 2.0 を認証に使うと、車が通れるほどのどでかいセキュリティー・ホールができる」](https://www.sakimura.org/2012/02/1487/)にあるように、**「アクセストークンを払い出した相手=アクセストークンの利用者」かどうか**をOAuth2.0で検証することはできません。

OIDCではIDトークンの中の`aud`(audience)を検証することで、トークンが払い出されるクライアント側の検証を行うことができます。
(audienceの検証は、[OIDCの仕様として必須](http://openid-foundation-japan.github.io/openid-connect-core-1_0.ja.html#IDTokenValidation)となっています。)

OpenID ConnectとOAuthの関係性については下記の点がポイントになります。

- OpenID Connect(OIDC)は**認証**の用途で利用されるプロトコル
- OIDCを利用すると、トークンを払い出す**クライアント側の検証**も可能である

## OIDCの各フローについて
OIDCでは、下記の3種類のフローが定義されています。
この後で検証する「Yahoo!ID連携」は、全てのフローに対応しています。

- 認可コードフロー (Authorization Code Flow)
- インプリシットフロー (Implicit Flow)
- ハイブリッドフロー (Hybrid Flow)

(参考: [openid-connect-core-1_0 (3.Authentication) ](https://openid.net/specs/openid-connect-core-1_0.html#Authentication))
>Authentication can follow one of three paths: 
the **Authorization Code Flow** (response_type=code), 
the **Implicit Flow** (response_type=id_token token or response_type=id_token), 
or the **Hybrid Flow** (using other Response Type values defined in OAuth 2.0 Multiple Response Type Encoding Practices [OAuth.Responses]). 

Yahoo!ID連携の利用説明欄では、上記の3フローについて下記のような説明を記載しています。
> 1.認証機能を利用したWebアプリケーション 
 『**Authorization Codeフロー**』 または『**Hybridフロー**』で実装する 
> 2.認証機能を利用したクライアントアプリケーション(iOS/Android)
 『**Implicitフロー**』または『**Hybridフロー**』で実装する 

OIDCは一般的なサーバーサイドのアプリケーションだけでなく、
クライアントサイド(iOS/Android)のアプリ(=ネイティブアプリ)にも対応しています。

実装時には、どのアプリ種別で実装するかによって利用すべきフローは異なります。
今回はアプリ種別と認証フローの関係性について、認可コードフローとインプリシットフローを例に説明します。

### フローの比較(認可コードフロー)
認可コードフローでは、大まかに下記の流れでアプリを認証します。

- ユーザーの認証情報入力+アプリへの情報提供の同意
- 認可コードの取得
- 認可コードを用いたIDトークンの取得
- IDトークンの検証(=アプリ自体の認証)
![authorization_flow_serverside.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170549/6a25c509-17ba-8a17-e9b9-7bcefd0195aa.png)

認可コードを利用して各種トークン(今回はIDトークン)を取得しているため処理には時間がかかりますが、**IDトークンをブラウザ(ユーザー側)に返さない**という点でセキュリティ面で優れています。

ただし、上記の内容は**サーバーサイドアプリで認可コードフローを実装した**前提の説明であり、クライアントアプリでの実装では(スマホ内の)アプリに返しているためセキュリティ強度は高くありません。

### フローの比較(インプリシットフロー)
![implicit_flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170549/1c8c07c9-7ad2-2f6d-61d6-09fa4764404f.png)
インプリシットフローでは、大まかに下記の流れでアプリを認証します。

- ユーザーの認証情報入力+アプリへの情報提供の同意
- **IDトークンの取得**
- IDトークンの検証(=アプリ自体の認証)

認可コードフローと異なり直接トークンを取得しているため、処理速度の点では優れています。
ただし、インプリシットフローの場合は**リダイレクト先のURLにトークンを含めて**IDトークンを取得しているので、認可コードフローと比較してセキュリティ強度は低くなります。

そのため、
・**IDトークンの更新を[インプリシットフローの仕様として禁止](https://openid.net/specs/openid-connect-core-1_0.html#Authentication)**したり、
・一定時間でセッションを破棄するなどして**セッションを適切に管理する**
などを行うことで、セキュリティ強度を高めています。

参考: [OpenID ConnectとAndroidアプリのログインサイクル](https://www.slideshare.net/kura_lab/openid-connect-and-login-cycle-for-android-apps) 

<br>認証フローを使い分ける際には、下記の観点で各フローを理解することが必要になります。

- 実装するアプリの種別
- 処理速度とセキュリティ強度のトレードオフ

### ハイブリッドフローについて
![hybrid_flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170549/6492b0b1-d59a-0bb4-5465-8e92641747ea.png)

ハイブリッドフローでは、**クライアントサイド・サーバーサイドの両方**が登場し、下記のような流れでアプリを認証します。

- ユーザーの認証情報入力+アプリへの情報提供の同意
- 認可コード+IDトークンの取得(**クライアントサイド**)
- IDトークンの検証(**クライアントサイド**)
- **認可コードの受け渡し(クライアントサイド->サーバーサイド)**
- 認可コードを用いたIDトークンの取得(**サーバーサイド**)
- IDトークンの検証(**サーバーサイド**)

[openid-connect-core-1_0で記載されているハイブリッドフローの認証フロー](https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth)と比較すると、クライアントサイド・サーバーサイドのアプリが「Client」として一括りにされていますが、トークン自体は2回取得しているのがわかります。


>The Hybrid Flow follows the following steps:
    1.Client prepares an Authentication Request containing the desired request parameters.
    2.Client sends the request to the Authorization Server.
    3.Authorization Server Authenticates the End-User.
    4.Authorization Server obtains End-User Consent/Authorization.
    **5.Authorization Server sends the End-User back to the Client with an Authorization Code and, depending on the Response Type, one or more additional parameters.
(認可コード+指定したトークンの取得)
    6.Client requests a response using the Authorization Code at the Token Endpoint.
(認可コードを用いたトークンのリクエスト)
    7.Client receives a response that contains an ID Token and Access Token in the response body.
(ID・アクセストークンの受け取り)
    8.Client validates the ID Token and retrieves the End-User's Subject Identifier.
(IDトークンの検証)**

このフローではIDトークンの取得と検証を2回実施しています。
検証を2回行うことで、クライアントサイド・サーバーサイドの両方を認証することができています。

トークンの取得に関して、同じトークンを同じように使うのではなく
・初回取得時のトークンは一部の種類のデータを含めない
・2回目取得時のトークンの保持期間を初回取得時のトークンより長くする
といったように差異を設けることで、セキュリティ的に強固にすることが可能です。

## Yahoo!ID連携について
ここでは検証に利用した[Yahoo!ID連携](https://developer.yahoo.co.jp/yconnect/v2/)について説明します。
Yahoo! JAPANでは、デベロッパー向けのサービスとして様々なWeb API ( ex.ショッピングAPI、YOLP (地図情報)API )を提供しています。
(参考：https://developer.yahoo.co.jp/sitemap/)

上記のAPI群の利用時に、APIを利用した安全性の高いデータ取得処理を実現する認証・認可のサービスとしてYahoo!ID連携サービスを提供しています。

OIDCの標準仕様に適合しているかをテストするプログラム(OpenID Connect Certificationプログラム)にも合格しており、フローの説明も充実しているため、今回はこちらを選択しました。
(詳細は、[Yahoo! JAPANさんのTech Blog (2016/12/13)](https://techblog.yahoo.co.jp/advent-calendar-2016/openid_certified/) に記載されています。)

利用前の準備として、ユーザーの登録とアプリケーションの登録が必要です。
>利用までのステップ
 1.ガイドラインを確認しましょう
    https://openid.net/certification/ 
 2.Yahoo! JAPAN IDを取得しましょう 
 3.Client IDを登録しましょう 

Client IDの登録では、アプリケーションの種類とアプリの基本情報を登録します。
![スクリーンショット 2019-09-14 18.39.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170549/c6a1cc1c-7dcb-1876-210b-2a27ef4a5258.png)
![スクリーンショット 2019-09-14 18.39.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170549/fe2da44d-4d23-85ab-9632-44cba51502cd.png)
また、一度アプリを登録した後に、「デベロッパーネットワークトップ > アプリケーションの管理」のアプリ情報の編集ボタンを押下し、**コールバックURL**を登録します。

今回は、検証用に下記のように登録します。
(1. 認可コードフロー検証用のアプリ設定)
![スクリーンショット 2019-09-15 16.08.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170549/e5ce7d2b-1930-aac6-5ec0-4a9f728e3d44.png)

(2. インプリシットフロー検証用のアプリ設定)
![スクリーンショット 2019-09-15 16.10.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170549/f71fe47a-eabe-f1e7-32a5-f78608ee2a8e.png)

上記の登録が終われば、検証の準備は完了です。

## Yahoo!ID連携を利用したフローの検証
今回は、認可コードフローとインプリシットフローを検証します。
フローの簡単な確認なので、Pythonでスクリプトを作成して認可コードやトークンが取得できているかどうかを確認します。
(※本来のフローで必要な「IDトークンの検証」は検証途中のため、後日追記予定です。)

実行環境は下記の通りです。

- ブラウザ: Safari ver12.1.2 (14607.3.9)
- OS: macOS Mojave 10.14.6
- Python: ver3.6.5


### 検証1: 認可コードフロー
初めに下記のスクリプトを実行して、認可エンドポイント( https://auth.login.yahoo.co.jp/yconnect/v2/authorization )にリクエストを投げます。
リクエストのパラメータには、response_typeとして**code(認可コード)**を指定します。
今回は、必ずユーザーへの同意確認画面を表示するために、追加可能なパラメータの一つであるpromptとして**consent(同意画面)**を指定しています。

指定したパラメータは下記の通りです。

- response_type: code(認可コード)
- client_id: アプリ登録時に表示されるアプリID
- scope: openid profile(ユーザープロファイル)
- prompt: consent(同意画面)
- redirect_uri: アプリ登録時のリダイレクト先URL

```python:get_auth_code.py
# coding: utf-8
import configparser
import requests
import random
import webbrowser

def main():
    config = configparser.SafeConfigParser()
    config.read('./config/auth_config.ini')

    client_id = config.get('general', 'client_id')

    #1. get authorization code
    # set parameters
    data = {
        'response_type': 'code', #認可コード取得時にはcodeをセット
        'client_id': client_id,
        'redirect_uri': 'http://localhost:8080/hello/test',
        'scope': 'openid profile', #ユーザーのプロファイル情報も要求
        'prompt': 'consent'
    }

    # send request
    response = requests.post(\
                'https://auth.login.yahoo.co.jp/yconnect/v2/authorization',\
                data=data,\
                allow_redirects=False\
               )

    # check response
    print(response.status_code)
    print(response.headers)

    # display consent screen in browser
    webbrowser.open(response.headers['Location'])


if __name__ == '__main__':
    main()
```

上記のリクエストに対して、認可エンドポイントは302を返して認証画面(Locationヘッダに記載されているURL)にリダイレクトさせます。

```
Status: 302

{'Date': 'Sun, 15 Sep 2019 07:17:55 GMT',
 'P3P': 'policyref="http://privacy.yahoo.co.jp/w3c/p3p_jp.xml",
 CP="CAO DSP COR CUR ADM DEV TAI PSA PSD IVAi IVDi CONi TELo OTPi OUR DELi SAMi OTRi UNRi PUBi IND PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA POL HEA PRE GOV"',
'Strict-Transport-Security': 'max-age=15552000; includeSubDomains',
'Expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
'Cache-Control': 'private, no-store, no-cache, must-revalidate',
'Pragma': 'no-cache',
'Location': 'https://login.yahoo.co.jp/config/login?.src=yconnectv2&.done=https%3A%2F%2Fauth.login.yahoo.co.jp%2Fyconnect%2Fv2%2Fauthorization%3F.scrumb%3D0%26from%3Dlogin%26session%3D5uWRpF3i%26display%3Dpage&ckey=dj00aiZpPVJRNDl3UkdCZjR4MCZzPWNvbnN1bWVyc2VjcmV0Jng9MWU-&auth_lv=pin&.display=page',
'Content-Length': '0', 
'Content-Type': 'text/html;charset=UTF-8', 
'Age': '0', 
'Connection': 'close', 
'Server': 'ATS'}
```


リダイレクト先の認証画面でテスト用のユーザーでログインすると、情報提供の同意画面が表示されます。
同意画面では、事前に登録したアプリ名とURLが表示されます。
また、リクエスト時に取得したい情報を指定するパラメータ(scope)として**profile**を指定したため、姓名などのユーザー情報の取得に関する同意確認も行います。
![スクリーンショット 2019-09-15 16.19.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170549/09a43677-ea93-b277-7147-62056f27ebd5.png)


「同意してはじめる」を押下すると、指定したリダイレクト先のURLにアクセスします。
(今回はリダイレクト先は用意していないため、下記のようなエラー画面が表示されます。)
![スクリーンショット 2019-09-15 16.35.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170549/2e6bea3e-620a-70de-e7a5-362056b8b1d8.png)

URL内に認可コード(code=**poafeor6**)が埋め込まれていることがわかります。

次に、認可コードを利用して、トークンエンドポイントに対してリクエストを送ります。
トークンエンドポイントに対するリクエストに関しては、下記の2つのパターンが利用できます。

- リクエストヘッダー(Authorizationヘッダー)を利用するパターン
- ヘッダーを利用せずに全てパラメータとして渡すパターン

今回はリクエストヘッダー(Authorizationヘッダー)を利用するパターンを検証します。
登録済のClient IDとClient Secretを「:」(コロン)で連結して、Base64エンコードを行ったものをAuthorizationヘッダーで送ります。

他のパラメータは下記のように指定します。

- grant_type: authorization_code(固定文字列)
- redirect_uri: アプリ登録時のリダイレクト先URL
- code: **poafeor6(=認可コード)**

```python:get_id_token.py
# coding: utf-8
import configparser
import requests
import pprint
import base64

def main():
    config = configparser.SafeConfigParser()
    config.read('./config/auth_config.ini')

    client_id = config.get('general', 'client_id')
    client_secret = config.get('general', 'secret')


    #2. get access code
    #TODO: set authorization_code
    authorization_code = 'poafeor6'

    # set headers
    str_for_basic_auth = base64.b64encode('{}:{}'.format(client_id, client_secret).encode('utf-8'))
    headers = {
        'Authorization': 'Basic '+ str_for_basic_auth.decode('utf-8')
    }
    print(headers)

    # set other parameters
    data = {
        'grant_type': 'authorization_code',
        'redirect_uri': 'http://localhost:8080/hello/test/authcode',
        'code': authorization_code,
    }

    # send request
    response = requests.post(\
                'https://auth.login.yahoo.co.jp/yconnect/v2/token',\
                data=data,\
                headers=headers,\
                allow_redirects=False
                )

    # check response
    print(response.status_code)
    pprint.pprint(response.json())

if __name__ == '__main__':
    main()
```

認可コードがexpireされていない場合は、上記のリクエストに対してid_tokenがボディに詰められた状態でレスポンスが返ってきます。

```
Status: 200

{'access_token': 'bWNcLSs...',
 'expires_in': 3600,
 'id_token': 'eyJ0e...',
 'refresh_token': 'lSDM...',
 'token_type': 'Bearer'}
```

### 検証2: インプリシットフロー
インプリシットフローでは、認可エンドポイントにトークン要求のリクエストを直接投げます。
指定したパラメータは下記の通りです。

- **response_type**: id_token token(前者がIDトークン、後者がアクセストークン)
- client_id: アプリ登録時に表示されるアプリID
- redirect_uri: アプリ登録時のリダイレクト先URL
- scope: openid profile(ユーザープロファイル)
- **nonce**: n-0S6_WzA2Mj(任意の文字列で可)
- prompt: consent(同意画面)


```python:implicit_flow.py
# coding: utf-8
import configparser
import requests
import random
import webbrowser

def main():
    config = configparser.SafeConfigParser()
    config.read('./config/impli_config.ini')

    client_id = config.get('general', 'client_id')

    #1. get tokens
    # set parameters
    data = {
        'response_type': 'id_token token',
        'client_id': client_id,
        'redirect_uri': 'http://localhost:8080/hello/test/implicit',
        'scope': 'openid profile',
        'nonce': 'n-0S6_WzA2Mj',
        'prompt': 'consent'
    }

    # send request
    response = requests.post('https://auth.login.yahoo.co.jp/yconnect/v2/authorization', \
                data=data,\
                allow_redirects=False)

    # check response
    print(response.status_code)
    print(response.headers)

    # display consent screen in browser
    webbrowser.open(response.headers['Location'])

if __name__ == '__main__':
    main()
```

ここでは、response_typeとnonceについて確認します。
インプリシットフローでは直接トークンを要求するため、response_typeで**取得したいトークンの種類**を指定します。
>response_type: id_token token(前者がIDトークン、後者がアクセストークン)

OIDCがOAuth2.0をベースにしていることに最初の方で触れましたが、パラメータで「**token**」を渡すとOAuth2.0で利用される**アクセストークン**を返してしまうため注意が必要です。

また、Yahoo!ID連携でのインプリシットフローの実行時には**nonce**というリプレイアタック対策用のパラメータが必須となっています。
nonceを認証サーバー側に送ることで、悪意のあるユーザー(A)が他人のIDトークンを不正に取得して利用しようとしても、Aのセッションデータはnonceを持っていないため、nonceの値が不一致となりIDトークンの不正利用を防止できるという仕組みになっています。

(参考: [OpenID Connectで使われるnonceパラメーターについて ](https://tech-lab.sios.jp/archives/13087))

上記のリクエストに対して、認可エンドポイントは302を返して認証画面(Locationヘッダに記載されているURL)にリダイレクトさせます。

```
Status: 302

{'Date': 'Sun, 15 Sep 2019 08:22:53 GMT', 
'P3P': 'policyref="http://privacy.yahoo.co.jp/w3c/p3p_jp.xml", 
CP="CAO DSP COR CUR ADM DEV TAI PSA PSD IVAi IVDi CONi TELo OTPi OUR DELi SAMi OTRi UNRi PUBi IND PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA POL HEA PRE GOV"', 'Strict-Transport-Security': 'max-age=15552000; includeSubDomains', 
'Expires': 'Thu, 01 Jan 1970 00:00:00 GMT', 
'Cache-Control': 'private, no-store, no-cache, must-revalidate', 
'Pragma': 'no-cache', 
'Location': 'https://login.yahoo.co.jp/config/login?.src=yconnectv2&.done=https%3A%2F%2Fauth.login.yahoo.co.jp%2Fyconnect%2Fv2%2Fauthorization%3F.scrumb%3D0%26from%3Dlogin%26session%3Da9CXHUX3%26display%3Dpage&ckey=dj00aiZpPTBDNXh1NmhSOFpvZSZzPWNvbnN1bWVyc2VjcmV0Jng9NzY-&auth_lv=pin&.display=page', 
'Content-Length': '0', 
'Content-Type': 'text/html;charset=UTF-8', 
'Age': '0', 
'Connection': 'close', 
'Server': 'ATS'}
```

認可コードフローと同様に、リダイレクト先の認証画面でテスト用のユーザーでログインすると、情報提供の同意画面が表示されます。
![スクリーンショット 2019-09-15 17.23.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170549/68aeadcc-4ef0-4eaf-29b4-fd5a852887cb.png)

「同意してはじめる」を押下すると、指定したリダイレクト先のURLにアクセスします。
認可コードフローの場合はURLにレスポンスのボディにトークンが含まれていましたが、インプリシットフローの場合は**response_typeで指定した種類のトークン**がURLに埋め込まれます。
![スクリーンショット 2019-09-15 17.57.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170549/0c7c43e0-cd4d-49b5-7a34-86efc4f09a59.png)

```
(リダイレクト先のURL)
http://localhost:8080/hello/test/implicit
#access_token=vbKq_0k...
&token_type=Bearer
&expires_in=3600
&id_token=eyJ0eXAiOiJKV1Q...
```

## 最後に
自身の勉強も兼ねて、OIDCの概要についてまとめました。
IDトークン内のclaimsに関する説明など詳細を省いている部分もあるので、「IDトークンの検証処理」の検証も兼ねて追記してまとめようと思います。

## 参考リンク
https://developer.yahoo.co.jp/yconnect/v2/
https://openid.net/specs/openid-connect-core-1_0.html