AWS Cognitoのエンドポイントを使いこなす

OpenID Connectでは、以下の4つのアクセス権限付与フローが定義されています。

1. Authorization Code Grant
2. Implicit Grant
3. Resource Owner Password Credentials Grant
4. Client Credentials Grant

これらは、AWS Cognitoにある以下の5つのエンドポイントを組み合わせて実現します。

- 認証エンドポイント (/oauth2/authorize)
 - ユーザーをサインインさせます
- トークンエンドポイント (/oauth2/token)
 - ユーザーのトークンを取得します。
- ログインエンドポイント (/login)
 - ユーザーをサインインさせます。ログインページにロードされ、ユーザーにクライアントに設定されている認証オプションを提示します。
- ログアウトエンドポイント (/logout)
 - ユーザーをサインアウトさせます。
- USERINFOエンドポイント (/oauth2/userInfo)
 - 認証されたユーザーに関する情報を返します。

[参考情報]
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-userpools-server-contract-reference.html

今回は、Chromeを使って実際にOpenID Connectのアクセス権限付与フローをやってみて、これらエンドポイントを使いこなしてみたいと思います。

（※ 3のResource Owner Password Credentials Grantは除きます）


# Cognitoユーザプールの準備

AWS Cognitoにユーザプールとアプリクライアントが設定されている前提です。
まだの方は、以下を参考に作成しておいてください。

[AWS CognitoにGoogleとLINEアカウントを連携させる](https://qiita.com/poruruba/items/189945dc64edfe1f2464)

（さらに、Client Credentials Grantを試す場合）
[AWS CognitoでClient Credentials Grantを使ってみる](https://qiita.com/poruruba/items/3681f1e1f8e72fcaadfa)

上記が完了していれば、以下の条件が整っているはずです。

- ユーザプールで、Cognitoユーザープールのユーザ認証に加えて、Googleアカウント・LINEアカウントの認証ができる。
- 許可されているOAuthフローが「Authorization code grant」と「Implicit grant」と「Client credentials」がそれぞれ有効なアプリクライアントが計3つ作られている。
- アプリの統合のところにある、Amazon Cognito のドメインが使用できる。

# テスト用のWebアプリの準備

今回もまたまた以下で作成した環境を流用します。

[AWS CognitoにGoogleとLINEアカウントを連携させる](https://qiita.com/poruruba/items/189945dc64edfe1f2464)

最低限必要なのは、以下のファイルです。今回、oidc.htmlとstart_oidc.jsの2つのファイルを作成します。

```
.
├── cert
│   ├── server.crt
│   ├── server.csr
│   └── server.key
├── index.js
├── node_modules
├── package.json
└── public
     ├── oidc.html
     └── js
          └── start_oidc.js
```

それぞれのソースコードは以下の通りです。
以下の部分は、みなさんの環境に合わせて書き換える必要がありますので補足します。（ちょっと多くてすみません）

- 【アプリクライアントID１】：Implicit grantを有効にしたアプリクライアントのアプリクライアントID
- 【アプリクライアントID２】：Authorization code grantを有効にしたアプリクライアントのアプリクライアントID
- 【アプリクライアントID３】：Client credentialsを有効にしたアプリクライアントのアプリクライアントID
- 【コールバックURL】：各アプリクライアントに設定したコールバックURL
- 【ドメイン名】：ユーザープールに設定したドメインのプレフィックス
- 【アプリクライアントID２のアプリクライアントシークレット】：アプリクライアントID２のクライアントシークレット
- 【アプリクライアントID３のアプリクライアントシークレット】：アプリクライアントID３のクライアントシークレット

```html:oidc.html
<html lang="ja">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">	 
    <meta http-equiv="Content-Security-Policy" content="default-src * 'unsafe-inline'; style-src * 'unsafe-inline'; media-src * blob:; img-src *; script-src * 'unsafe-inline';">
    <title>OpenID Connectテスト</title>	 
</head>
<body onload="proc_load()">
    <h1><a href="">OpenID Connectテスト</a></h1>
    <div id="hashs"></div>
    <div id="searchs"></div>
    <div id="message"></div>
    <div id="content"></div>

    <h2>ログインエンドポイント</h2>
    <a href="https://【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/login?response_type=token&client_id=【アプリクライアントID１】&scope=openid%20email&redirect_uri=【コールバックURL】">Response_Type=token</a><br>
    <a href="https:// 【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/login?response_type=code&client_id=【アプリクライアントID２】&scope=openid%20email&redirect_uri=【コールバックURL】">Response_Type=code</a><br>
    <h2>ログアウトエンドポイント</h2>
    <a href="https:// 【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/logout?client_id=【アプリクライアントID１】&logout_uri=【サインアウトURL】"> Response_Type=token</a><br>
    <a href="https:// 【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/logout?client_id=【アプリクライアントID２】&logout_uri=【サインアウトURL】"> Response_Type=code</a><br>

    <h2>認証エンドポイント</h2>
    <label>Implicit Grant</label><br>
    <a href="https:// 【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize?response_type=token&client_id=【アプリクライアントID１】&scope=openid%20email&redirect_uri=【コールバックURL】">Response_Type=token</a><br>
    <a href="https:// 【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize?identity_provider=COGNITO&response_type=token&client_id=【アプリクライアントID１】&scope=openid%20email&redirect_uri=【コールバックURL】">Response_Type=token(Cognito)</a><br>
    <a href="https:// 【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize?identity_provider=Google&response_type=token&client_id=【アプリクライアントID１】&scope=openid%20email&redirect_uri=【コールバックURL】">Response_Type=token(Google)</a><br>
    <a href="https:// 【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize?identity_provider=LINE&response_type=token&client_id=【アプリクライアントID１】&scope=openid%20email&redirect_uri=【コールバックURL】">Response_Type=token(LINE)</a><br>
    <label>Authorization Code Grant</label><br>
    <a href="https:// 【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize?response_type=code&client_id=【アプリクライアントID２】&scope=openid%20email&redirect_uri=【コールバックURL】">Response_Type=code</a><br>
    <a href="https:// 【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize?identity_provider=COGNITO&response_type=code&client_id=【アプリクライアントID２】&scope=openid%20email&redirect_uri=【コールバックURL】">Response_Type=code(Cognito)</a><br>
    <a href="https:// 【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize?identity_provider=Google&response_type=code&client_id=【アプリクライアントID２】&scope=openid%20email&redirect_uri=【コールバックURL】">Response_Type=code(Google)</a><br>
    <a href="https:// 【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize?identity_provider=LINE&response_type=code&client_id=【アプリクライアントID２】&scope=openid%20email&redirect_uri=【コールバックURL】">Response_Type=code(LINE)</a><br>

    <h2>トークンエンドポイント</h2>
    <label>Authorization Code Grant (Grant_Type=authorization_code)</label><br>
	<input type="text" id="code" size="36">
    <button id="token_call" onclick="token_call()">token呼び出し</button><br>
    <label>Client Credentials Grant (Grant_Type=client_credentials)</label><br>
    <button id="client_call" onclick="client_call()">client呼び出し</button>

    <h2>USERINFOエンドポイント</h2>
    <button id="userinfo_call" onclick="userinfo_call()">userinfo呼び出し</button>

    <script src="js/start_oidc.js"></script>
</body>
</html>
```

```js:start_oidc.js
const Authorization_UserPoolClientId = 【アプリクライアントID２】;
const Authorization_UserPoolClientSecret =【アプリクライアントID２のアプリクライアントシークレット】;
const Client_UserPoolClientId =【アプリクライアントID３】;
const Client_UserPoolClientSecret = 【アプリクライアントID３のアプリクライアントシークレット】;

var elem = document.getElementById("message");
var content = document.getElementById("content");
var hashs = {};
var searchs = {};

function userinfo_call(){
    var bearer = 'Bearer ' + hashs.access_token;
    return do_post('https://【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/oauth2/userInfo', [], bearer )
	.then(result =>{
        if( result.error ){
            elem.innerText = 'userinfo_call Error:' + result.error;
            return;
        }
		elem.innerText = 'userinfo_call OK';
		content.innerText = JSON.stringify(result);
	})
	.catch(error=>{
		elem.innerText = 'userinfo_call Error:' + error;
	});
}

function token_call(){
    var code = document.getElementById("code");
    var params = {
        grant_type: 'authorization_code',
        client_id: Authorization_UserPoolClientId,
        redirect_uri: 【コールバックURL】,
        code: code.value
    };

    var basic = 'Basic ' + btoa(Authorization_UserPoolClientId + ':' + Authorization_UserPoolClientSecret);
    return do_post_urlencode('https:// 【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/oauth2/token', params, basic)
	.then(result =>{
        if( result.error ){
            elem.innerText = 'token_call Error:' + result.error;
            return;
        }
		elem.innerText = 'token_call OK';
		content.innerText = JSON.stringify(result);
	})
	.catch(error=>{
		elem.innerText = 'token_call Error:' + error;
	});
}

function client_call(){
    var basic = 'Basic ' + btoa(Client_UserPoolClientId + ':' + Client_UserPoolClientSecret);
    
    var params = {
        grant_type: 'client_credentials',
        scope: 'test_resource/test'
    };
    
	return do_post_urlencode('https:// 【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/oauth2/token', params, basic)
	.then(result =>{
        if( result.error ){
            elem.innerText = 'client_call Error:' + result.error;
            return;
        }
		elem.innerText = 'client_call OK';
		content.innerText = JSON.stringify(result);
	})
	.catch(error=>{
		elem.innerText = 'client_call Error:' + error;
	});
}

function do_post(url, body, authorize ) {
	const headers = new Headers( { "Content-type" : "application/json", 'Authorization': authorize } );
	return fetch(url, {
		method : 'POST',
		body : JSON.stringify(body),
		headers: headers
	})
	.then((response) => {
		return response.json();
	});
}

function do_post_urlencode(url, params, authorize) {
    var data = new URLSearchParams();
    for( var name in params )
        data.append(name, params[name]);
    
    const headers = new Headers( { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : authorize } );
	return fetch(url, {
		method : 'POST',
		body : data,
		headers: headers
	})
	.then((response) => {
		return response.json();
	});
}

function proc_load() {
    var content = document.getElementById("hashs");
    hashs = parse_url_vars(location.hash);
    content.innerText = 'hashs=' + JSON.stringify(hashs);

    content = document.getElementById("searchs");
    searchs = parse_url_vars(location.search);
    content.innerText = 'querys=' + JSON.stringify(searchs);
}

function parse_url_vars(param){
	if( param.length < 1 )
		return {};
	
	var hash = param;
	if( hash.slice(0, 1) == '#' || hash.slice(0, 1) == '?' )
		hash = hash.slice(1);
	var hashs  = hash.split('&');
	var vars = {};
	for( var i = 0 ; i < hashs.length ; i++ ){
		var array = hashs[i].split('=');
		vars[array[0]] = array[1];
	}
	
	return vars;
}
```

最後に、それぞれのアプリクライアントのアプリクライアントの設定において、コールバックURL、サインアウトURLに、上記のHTMLのoidc.htmlのURLを指定しておきます。
１つだけ注意です。今回は単なるテストなので端折っていますが、本番では、CSRF攻撃対策のためにきちんとstateを使うようにして下さい。

それでは、oidc.htmlをChromeブラウザで開きましょう。次から、操り方を示します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/3706a137-3eb2-d44c-590b-63db651db55d.png)

リダイレクトが多用されていますので、そのやり取りが見えるよう、キーボードの[F12]キーを押して、DevToolsを表示させておきましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/570662b6-381b-e4fb-295a-497df6cfe4e1.png)


# Implicit Grantの場合

まずは、Implicit Grantの場合のフローを流してみます。

以下の流れに沿って、ブラウザを操作します。

1. ログインエンドポイントのリンク「Response_Type=token」をクリックします。
2. ログイン画面からログインします。
3. 以下のように、hashs=XXXXXにトークンが取得できているのがわかります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/0672d780-c0b2-ef8a-2ce9-9f40a7da33c3.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/ce18ae60-c3c7-e8de-fa2e-d70d189d2d37.png)

実際には、以下の流れになっています。

![Implicit Grant.png](https://qiita-image-store.s3.amazonaws.com/0/261826/833775a8-38f4-ccbd-e80a-b0c49256efe1.png)

1. ログインエンドポイントにアクセスすると、Cognitoが用意しているログイン画面が表示されます。
2. どのアカウントを使うかによって、フローが多少異なります。

2.1 [Cognitoユーザープールの認証の場合]
2.2.1 ユーザID/パスワードを入力します。
2.1.2 フラグメント識別子にID/アクセストークンがあるリダイレクト要求が返ってきます。

2.2 [LINEの認証の場合]
2.2.1 認証エンドポイントにアクセスし、LINEで認証することをCognitoに伝えます。
2.2.2 LINEにリダイレクトされます。
2.2.3 LINEからユーザID/パスワードの入力画面、Scope許可入力画面が表示されます。
2.2.4 入力が終わると、認可コード付きでidpresponseエンドポイントにリダイレクトされてきます。(LINE Developer Consoleで指定したリダイレクト設定のURLです)
2.2.5ここでおそらくCognitoは、認可コードを使ってLINEからトークンを取得していると思われます。
2.2.6 フラグメント識別子に、ID/アクセストークンがあるリダイレクト要求が返ってきます。

2.3 [Googleの認証の場合]
2.3.1 認証エンドポイントにアクセスし、Googleで認証することをCognitoに伝えます。
2.3.2 Googleにリダイレクトされます。
2.3.3 GoogleからユーザID/パスワードの入力画面、Scope許可入力画面が表示されます。
2.3.4 入力が終わると、認可コード付きでidpresponseエンドポイントにリダイレクトされてきます。(Google Developer Consoleで指定した認証済みのリダイレクトURIです)
2.3.5 ここでおそらくCognitoは、認可コードを使ってGoogleからトークンを取得していると思われます。
2.3.6 フラグメント識別子に、ID/アクセストークンがあるリダイレクト要求が返ってきます。

3.アプリサーバにリダイレクトされて、リダイレクト先のアプリページが取得されます。
4.アプリページは、Javascript等で、フラグメント識別子からIDトークンやアクセストークンを取得できます。

上記の手順では、ログインエンドポイントからログインしました。（このログインエンドポイントは、システムブラウザ経由で行うためのものらしいです。。。）
同様なことが認証エンドポイント(Response_Type=token)からも同様なことができます。さらに、Response_Type=token(LINE)やResponse_Type=token(Google)のリンクを選択することで、直接外部のソーシャルアカウントのログイン画面から入ることもできます。

次のために、いったんログアウトしておきます。
ログアウトエンドポイントの「Response_Type=token」を選択します。

# Authorization Code Grantの場合

今度は、Authorization Code Grantの場合を見てみましょう。

1. ログインエンドポイントのリンク「Response_Type=code」をクリックします。
2. ログイン画面からログインします。
3. 成功すると、認可コードが、「querys={"code":"XXXXXXXXXX”}」のように表示されます。
4. 認可コードをトークンエンドポイントの入力フィールドの所にコピペして、「token呼び出し」ボタンを押下します。
5. 以下のように、トークンがtoken_call OKの下に表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/0e089dae-cce6-4ba6-4f60-842b082d34e3.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/5f67d92e-1f2e-9251-64a7-3ba272bc7569.png)

それでは、詳細なフローを追っていきます。

![Authorization Code Grant.png](https://qiita-image-store.s3.amazonaws.com/0/261826/6b969ce4-2225-1dc1-9a0a-9fe02e689c93.png)

1. ログインエンドポイントにアクセスすると、Cognitoが用意しているログイン画面が表示されます。
2. どのアカウントを使うかによって、フローが多少異なります。

2.2 [Cognitoユーザープールの認証の場合]
2.2.1 ユーザID/パスワードを入力します。
2.2.2 QueryStringに認可コードがあるリダイレクト要求が返ってきます。

2.3 [LINEの認証の場合]
2.3.1 認証エンドポイントにアクセスし、LINEで認証することをCognitoに伝えます。
2.3.2 LINEにリダイレクトされます。
2.3.3 LINEからユーザID/パスワードの入力画面、Scope許可入力画面が表示されます。
2.3.4 入力が終わると、認可コード付きでidpresponseエンドポイントにリダイレクトされてきます。(LINE Developer Consoleで指定したリダイレクト設定のURLです)
2.3.5 ここでおそらくCognitoは、認可コードを使ってLINEからトークンを取得していると思われます。
2.3.6 QueryStringに認可コードがあるリダイレクト要求が返ってきます。

2.4 [Googleの認証の場合]
2.4.1 認証エンドポイントにアクセスし、Googleで認証することをCognitoに伝えます。
2.4.2 Googleにリダイレクトされます。
2.4.3 GoogleからユーザID/パスワードの入力画面、Scope許可入力画面が表示されます。
2.4.4 入力が終わると、認可コード付きでidpresponseエンドポイントにリダイレクトされてきます。(Google Developer Consoleで指定した認証済みのリダイレクトURIです)
2.4.5 ここでおそらくCognitoは、認可コードを使ってGoogleからトークンを取得していると思われます。
2.4.6 QueryStringに認可コードがあるリダイレクト要求が返ってきます。

3.アプリサーバにリダイレクトされて、リダイレクト先のアプリページが取得されます。
4.アプリサーバは、認可コードを使ってCognitoのトークンエンドポイントにアクセスし、トークンを取得します。

１点補足があります。
シーケンス図上では、アプリサーバが認可コードを使ってトークンを取得していますが、実装上では、アプリであるブラウザから取得しています。アプリサーバが正しいです。理由はこの実装のままではアプリクライアントシークレットがばれてしまうからです。本番では直して下さい。

Implicit Grantの時と同様に、ログインエンドポイントからだけでなく、認証エンドポイントからも同様のことができます。

# Client Credentials Grantの場合

最後に、Client Credentials Grantの場合を見てみます。
これは単純です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/e4a0f10d-ce6a-ffe1-d609-65e1f8a11bfa.png)
 
１点補足があります。
シーケンス図上では、アプリサーバがトークンを取得していますが、実装上では、アプリであるブラウザから取得しています。アプリサーバが正しいです。理由はこの実装のままではアプリクライアントシークレットがばれてしまうからです。本番では直して下さい。

# USERINFOの取得

OpenID Connectのアクセス権限付与フローには含まれないのですが、AWS Cognitoでエンドポイントが定義されているので、それも使ってみます。

認証が完了している状態で、「userinfo呼び出し」ボタンを押下すると、ログインユーザの情報を取得することができます。scope=openid emaiとしていたので、メールアドレスが取得できています。
Response_Type=tokenで認証した後でないと失敗してしまいますが、実装内容を確認していただければ、Response_Type=codeのときでも実装を改造することで成功するはずです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/9eba58bb-f92a-ba79-d164-095bd078a06d.png)

いかがでしたでしょうか。
OpenID Connectを解説しているページはたくさんありますが、なかなか具体的なイメージが付きにくかったと思います。実際に動かしてみると、もう少しわかった気になれたのではないでしょうか。

# 他のIdpのエンドポイント

以上説明したエンドポイントの仕様は、OpenID Connectで仕様が定義されていて、ほぼそれに準拠しています。(login/logout除く)
これらのエンドポイントのアドレスは、OpenID ConnectのDiscoveryというエンドポイント( /.well-known/openid-configuration )で知ることができます。

(参考情報)
https://openid.net/specs/openid-connect-discovery-1_0.html

Cognitoだけでなく、他のIdpもエンドポイントの情報を知ることができます。以下に主なものを挙げておきます。

* Congito
https://cognito-idp.[リージョン].amazonaws.com/[プールID]/.well-known/openid-configuration

* Google
https://accounts.google.com/.well-known/openid-configuration

* LINE
https://access.line.me/.well-known/openid-configuration

* Yahoo
https://auth.login.yahoo.co.jp/yconnect/v2/.well-known/openid-configuration

* Microsoft Azure
https://login.microsoftonline.com/common/.well-known/openid-configuration

* Salesforce
https://login.salesforce.com/.well-known/openid-configuration

# 参考：Cognitoが返すトークンの内容

IDトークンの内容
・iss
・sub
・aud
・exp
・iat

以下、Optional
・token_use
・at_hash
・nonce
・auth_time
・cognito:username
・cognito:groups
・identities
・email

その他

アクセストークンの中身
・iss
・sub
・exp
・iat

以下、Optional
・token_use
・scope
・auth_time
・client_id
・username
・cognito:groups
・version
・jti

# 補足

CognitoがバックエンドでGoogleと何をやり取りしているか、詳しく知りたい？
であれば、以下を参考に、自分でOpenID Connectサーバを立ち上げて、Cognitoと連携してみましょう。どんなリクエストがCognitoからきているかわかります。

　[AWS CognitoとなんちゃってOpenID ConnectサーバをIDフェデレーションする](https://qiita.com/poruruba/items/6ea55dd2befb92df6628)

以上です。