AWS CognitoでClient Credentials Grantを使ってみる

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

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

よく１、2を使っているのでなじみがあると思います。AWS Cognitoにもその選択肢があります。
3はあまりなじみがないとは思いますが、私もよく知りません。
4は、ユーザログインとは関係ないのですが、AWS Cognitoに選択肢があり、気になったので今回はこれを使ってみます。

Client Credentials Grantは、ユーザの同意を必要としないリソースにアクセスする際に利用されるそうです。
もともと、AWS Cognitoは、モバイルアプリケーション向けにユーザ登録・ログイン機能を提供するものです。一方で、このClient Credentials Grantは、ユーザは関係なく、モバイルアプリケーションやサーバを認証するものです。ちょっとAWS Cognitoには似つかわしいような気がしますが、せっかくある機能なので使ってみたいと思います。

# AWS Cognitoにリソースサーバを設定する

いきなりリソースサーバを設定する、としているのですが、おそらく、なんで？、と思われたのではないでしょうか。
AWS Cognitoの仕様のようで、なんでもよいから、リソースサーバを設定しないといけないようです。
（確かに、Client Credentials Grantは、何らかのリソースにアクセスするものではあるのですが。。。）

AWS Cognitoの管理コンソールから、ユーザプールIDを選択し、アプリ統合のリソースサーバーを選択します。

適当に、リソースサーバを設定します。（適当に、でいいんです。）
例えば、名前を「Test Resource」、識別子を「test_resource」とし、スコープとして名前「test」、説明として「Test」を追加します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/6ad2c8e9-9905-e756-b72d-59dd78c861e0.png)

とりあえず、これでよいのです。


# アプリクライアントの設定に、Client Credentialsを設定する

それでは、準備ができたところで、認証を行う対象のアプリを作成します。

認証を行う対象は、ユーザではなくアプリケーションです。
アプリケーションとはいっても、秘匿のシークレットを使って正しく認証を行うため、攻撃される可能性があるクライアントアプリではなく主にサーバアプリ(あるいはネイティブアプリ？)となるかと思います。

先に、全般設定のアプリクライアントからアプリクライアントを追加します。
例えば、アプリクライアント名として「test-server」とします。このとき、チェックボックス「クライアントシークレットを生成」をOnにしておきます。
作成が完了すると、アプリクライアントIDとアプリクライアントのシークレットが生成されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/66ffd201-2a71-5da3-0c57-62f6797eb80a.png)

次に、アプリクライアントの設定を行います。
アプリの統合からアプリクライアントの設定を選択します。

アプリクライアント 「test-server」を見てみます。
まだ何も設定されていないと思いますが、

> 許可されているカスタムスコープ
> □ test_resource/test

という見たこともないものが増えているのがわかります。そう、さきほどリソースサーバを追加したのが現れているのです。選択したいのですが、まだ選択可能な状態ではありません。
許可されているOAuthフローからClient credentialsを選択してみましょう。そうすると、「許可されているカスタムスコープ」が選択可能な状態になるので、「test_resource/test」を選択します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/adba0961-c7c8-12d5-833e-89a4976e4d8f.png)

実は、Authorization code grantやImplicit grantでは、「許可されているOAuthスコープ」だけでなく「許可されているカスタムスコープ」も選択できるのですが、Client credentialsでは「許可されているカスタムスコープ」しか選択することができません。しかも、いずれかのスコープを選択しないといけないため、Client credentialsを選択したいときには何らかの「許可されているカスタムスコープ」すなわちリソースサーバを登録する必要があるというわけです。
有効なIDプロバイダのところは、選択は必須ではありません。Client Credentials Grantではユーザ認証しないからです。

AWS CLIから設定する方のために、CLIでの実行方法も載せておきます。

```
aws cognito-idp update-user-pool-client --user-pool-id 【プールID】 --client-id 【アプリクライアントID】 --allowed-o-auth-flows [\"client_credentials\"] --allowed-o-auth-scopes [\"test_resource/test\"] --allowed-o-auth-flows-user-pool-client
```

[参考情報]
https://docs.aws.amazon.com/cli/latest/reference/cognito-idp/update-user-pool-client.html


# 実際にClient Credeitalsのトークンを取得してみる

それでは、実際に、Client Credentials Grantの認証フローでトークンを取得してみます。
取得には、Postmanを使いました。

Chromeのウェブストアからインストールできます。
https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop

※私の環境では古いpostmanを使っていまして、少々画面が現状と違ってます。基本的な考えは同じだと思いますので、すみませんが、読み替えてください。

起動すると以下のような画面が表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/013aea0a-2413-3b58-f6e8-abcb9c149a5a.png)

それでは、１つずつ入力していきます。
まず、トークンの取得先のURLですが、以下になります。OpenID Connect的にはトークンエンドポイントといいます。

> https://【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/oauth2/token

※ここでいうドメイン名とは、Cognitoのアプリの統合の所で指定したドメイン名のことです。

[参考情報]
　https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/token-endpoint.html

メソッドとして、「POST」を選択します。
次に、Bodyを選択し、x-www-form-urlencodedを選択します。
Headerを選択します。
Content-Typeとして、application/ x-www-form-urlencoded が自動的に入力されているかと思います。
ここに、Authorizationを指定するのですが、先に準備が必要です。

bashなどのTerminalから以下を入力します。
※以下はLinuxのTerminalを想定しています。Windowsではbase64というコマンドはないはず。。。

```
echo -n "【アプリクライアントID】:【アプリクライアントのシークレット】" | base64
```

何をしているのかというと、「コロン(:)」をアプリクライアントIDとアプリクライアントのシークレットで挟んだものをBase64エンコードしています。
Base64でエンコードされた文字列が出力されますので、それを控えておきます。途中に改行が入っているのでそれは除いておきます。これをHeaderのAuthorizationに指定するのです。

Postmanに戻ります。
HeaderにAuthorizationを追加します。

値は以下の通りです。

> Basic 【Base64でエンコードされた文字列】

もう一度Bodyに戻って、Key-Valueを入力していきます。
まず、grant_typeです。Valueには、今回取得しようとしている「client_credentials」を入力します。
次にscopeです。ここには、アプリクライアントの設定で指定した許可されているカスタムスコープの「test_resource/test」を入力します。

準備ができました。「Send」ボタンを押下してみましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/f29f013a-5c21-9d7b-3262-ed0ad598f40d.png)

レスポンスに何やらアクセストークンらしきものが取得されているようです。
JSON形式で、

* access_token：XXXXXXXXXXXXXXX
* expires_in：3600
* token_type：”Bearer”

が取得できているようです。

早速、以下のサイトでアクセストークンを調べてみます。

https://jwt.io

```
{
  "kid": "XXXXXXXXXXXXXXXXXXXXXXXX",
  "alg": "RS256"
}
{
  "sub": 【アプリクライアントID】,
  "token_use": "access",
  "scope": "test_resource/test",
  "auth_time": XXXXXXXX,
  "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/【プールID】",
  "exp": XXXXXXX,
  "iat": XXXXXXX,
  "version": 2,
  "jti": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  "client_id": 【アプリクライアントID】
}
```

issとsubとscopeを見ると、AWS CognitoがアプリクライアントIDを認証し、test_resource/testにアクセス許可を与えているのがわかります。


# API Gatewayで呼び出し元認証に使ってみる（準備編）

せっかくトークンを作ったので、実際に使ってみたく、API GatewayのAPI呼び出しに使ってみます。

前々回、API GatewayのAPI呼び出しで、AWS Cognito連携させました。今回はそれを再利用してみます。

[AWS Cognitoにサインインしないと見れないLambdaを作る](https://qiita.com/poruruba/items/86668c9c63844e1afd32)

今回は、API呼び出し時のヘッダAuthorizationに指定するのはIDトークンではなく、アクセストークンを入力とするので、ちょっとAPI Gatewayの設定の仕方が異なります。順に説明します。

[参考情報]
　https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-enable-cognito-user-pool.html

API Gatewayの管理コンソールから、まずリソースを作成します。
例えば、リソース名として「access」とします。「API Gateway CORSを有効にする」にチェックを入れておきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/4b5fc6cf-6a0b-6c40-e3e8-351d814a0de1.png)

次に、作成したリソース「/access」に、メソッド「POST」を追加します。
統合タイプとして「Lambda関数」、チェックボックス「Lambdaプロキシ統合の使用」をOnにし、Lambda関数として適当なLambda関数を選択します。例えば、以前に作成した「test-cognito-lambda」を選択しておきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/de68ebb8-2aa2-d07d-4548-0c0e56ee6546.png)

次に、メソッドリクエストを設定するのですが、その前に、オーソライザの再編集をしておきます。
以前、AWS Cognitoと連携するために、以下のようなオーソライザを作成しました。

> 名前：test-cognito-authorizer
> タイプ：Cognito
> Cognitoユーザプール：【プールID】
> トークンのソース：Authorization
> トークンの検証：【アプリクライアントID】

トークンの検証としてアプリクライアントIDを指定して、接続要求元を限定するようにしていました。これはこれでよいのですが、接続元の検証はLambda実装の中で行うとして、今回は「なし(無指定)」にします。（アクセストークンにはaudクレームがないんだよね。。。）
（トークン検証の所を空白にして保存しても反映されないので、スペースを入力して保存すると、「なし(無指定)」にできます。）

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/675e977e-9ca1-1bcc-26f6-8bd842cb593b.png)

それでは、メソッドリクエストの設定に戻ります。
設定→認証のところには、先ほど再編集した「test-cognito-authorizer」を選択します。小さなチェックボタンを押下して一旦コミットします。
すると、「許可されているOAuthスコープ」という入力が増えます。ここに、AWS Cognitoのアプリクライアントの設定で選択した許可されているカスタムスコープ「test_resource/test」を入力します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/3980c26d-e07b-32e2-13cc-23bebe280150.png)

これで準備が整いましたので、APIのデプロイを行っておきます。


# API Gatewayで呼び出し元認証に使ってみる（実行編）

API GatewayのAPI呼び出しには、またpostmanを使います。

URLには、API GatewayのURLに「/access」を加えたものを入力します。

> https://【RestApi-Id】.execute-api.ap-northeast-1.amazonaws.com/【ステージ名】/access

メソッドは「POST」にします。
Bodyを選択し、rawおよびJSON(application/json)を選択します。
Headerを選択すると、Content-Typeとしてapplication/jsonが自動的に設定されているかと思います。
ここでまずは、トークンを指定せずに呼び出してエラーとなることを確認しましょう。
「Send」ボタンを押下してみてください。

以下のようなレスポンスが表示されうまくいかなかったことがわかります。

```
{
    "message": "Unauthorized"
}
```

では、ヘッダAuthorizationを入力していきます。Valueとして、postmanを使ってトークンエンドポイントから取得したアクセストークンを指定します。ここまで来る間に、トークンの有効期限が切れているかもしれません。もう一度postmanから実行して再取得します。

AuthorizationのValueに再生成したアクセストークンを設定して、いざ「Send」ボタンを押下しましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/fddbb81b-ef82-4a7e-d4bf-e128d05726d0.png)

無事成功しました。（よかったー）

取得したレスポンスのJSONにおいて、

> event.requestContext.authorizer.claims

のところに、指定したアクセストークンが取得されているのがわかります。


いかがでしょうか。
AWS Cognitoはいろんな機能が汎用的に組み合わさっていて、全体像がつかめないと、なかなか使いこなせないのではないかと思います。
この記事が、AWS Cognitoの理解と駆使する助けになれば幸いです。

以上です。