AWSでデバイス認可フローの実装を確認する

# 1. はじめに

ChatBot は、ユーザーとの対話を自動化する技術として、カスタマーサポートや社内問い合わせシステム、パーソナルアシスタントなど、さまざまな用途で利用されている。特に OpenAI の Realtime API を活用することで、より リアルタイムかつ自然な対話 を実現できるようになった。

一方で、ChatBot を運用する際に重要となるのが ユーザー認証 だ。特に スマートデバイスや IoT 端末、組み込み機器 などでは、パスワード入力が難しく、従来の認証方法が使いにくいケースがある。このような課題を解決する手段として、デバイス認可フロー（OAuth 2.0 Device Authorization Grant）が有効です。

[前回の記事では Amazon ECS 上で OpenAI Realtime API を活用した ChatBot に Cognito も用いたユーザー認証を実装した](https://qiita.com/rairaii/items/4f83515762390cdbaf3e)が、今回はさらにデバイス認可フローを導入する方法 について確認する。これにより、ユーザーは スマートフォンやPCを使って簡単に認証を行い、ChatBot へのアクセスが可能になる ため、利便性とセキュリティの両立が実現できる。

**前回の記事:**

https://qiita.com/rairaii/items/4f83515762390cdbaf3e


# 2. AWSでデバイス認可フローの実装する例を確かめる

[Amazon Cognito と AWS Lambda を使って OAuth 2.0 デバイスフローを実装する](https://aws.amazon.com/jp/blogs/security/implement-oauth-2-0-device-grant-flow-by-using-amazon-cognito-and-aws-lambda/)のサンプルを用いてデバイス認可フローの実装方法について確認する。


## (1) デバイス認可フローの概要

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/cc709ac3-f785-4115-9fec-d5d8001258ce.png)


## (2) スタックの作成

**[ステップ 2: CloudFormation テンプレートを使って実装をデプロイする]** の **[Launch Stack]** からスタックの作成を開始する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/80a9eacc-26c3-41da-a640-bd7d482d551a.png)

## (3) スタックのクイック作成

**スタックのクイック作成** でパラメータを設定する

| パラメータ名 | 説明 |
| :---: | :--- | 
| スタック名|  任意の名称 |
|ACertificateARN| [ACM](https://us-east-1.console.aws.amazon.com/acm/home?region=us-east-1#/certificates/80877647-499f-4c1a-93b2-688757eaa1d6)で作成した証明書のARN|
|ACognitoDomain| [Route53](https://us-east-1.console.aws.amazon.com/route53/v2/hostedzones?region=us-east-1)でレコードを作成したドメインで任意の名称。親ドメインのIPアドレスが設定されていなければエラーになるので、親ドメインのタイプAに 127.0.0.1 などのダミーを設定しておく|
|ACredsEmail| 自分が受け取れる任意の email アドレス|
|AFullyQualifiedDomainName (FQDN)| テストサービスを行うドメイン名 |
|ResultTokenSet| ID+ACCESS+REFRESH に変更すると ID_TOKEN も取得可能|


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/40549ca4-c186-4b27-95c7-25e73b57e055.png)



**AWS CloudFormationによってIAMリソースが...** のチェックマークを入れて **スタックの作成** を選択する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/e98589e2-fa74-4316-ab5a-984da3d14bb7.png)

###トラブルシューティング

Amazon Cognito カスタムドメインを作成するには、親ドメインに DNS A レコードが必要である。この設定がないと **Cognito** のカスタムドメインの作成に失敗する。ダミーで良いので **親ドメインに DNS A レコード** を設定すれば解決する。

https://repost.aws/ja/knowledge-center/cognito-custom-domain-errors

## (4) 設定を完了させる

- [CloudFormation コンソール](https://us-east-1.console.aws.amazon.com/cloudformation/) 内の Stacks ページで、作成したスタックをクリック
- 出力タブを開く
- **ALBCNAMEForDNSConfiguration** を確認する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/5fc17b65-aa50-4085-82b4-69fb3b06f5e7.png)


| **キー**                          | **値 (一部マスク済み)**                                  | **説明**                                                  |
|----------------------------------|-------------------------------------------------|-------------------------------------------------|
| **ALBCNAMEForDNSConfiguration**  | `***--Devic-**********.us-east-1.elb.amazonaws.com` | DNS を ALB のエンドポイントに向けるための CNAME |
| **DeviceCognitoClientClientID**  | `78n4qk*************`                             | 認可サーバーとやり取りするためにシミュレートされたデバイスが使用する App クライアント ID |
| **DeviceCognitoClientClientSecret** | `15r0jp1g**********************************`  | 認可サーバーとやり取りするためにシミュレートされたデバイスが使用する App クライアントシークレット |
| **TestEndPointForDevice**        | `https://**********/token` | シミュレートされたデバイスがリクエストを行うための HTTPS エンドポイント |
| **TestEndPointForUser**          | `https://**********/device` | ユーザーがリクエストを行うための HTTPS エンドポイント |
| **UserPassword**                 | `登録されたメールアドレスに送信`| テスト用 Cognito ユーザーのパスワード |
| **UserUserName**                 | `*********@**********.com` | テスト用 Cognito ユーザーのユーザー名 |

### ALB の DNS を設定

スタックが構築された後、利用する FQDN に対する DNS ゾーン内の DNS CNAME エントリが Application Load Balancer (ALB) の DNS 名を指すようにして設定を完了させる

1. **ALBCNAMEForDNSConfiguration** キーの値をコピー
1. [Route53](https://us-east-1.console.aws.amazon.com/route53/v2/hostedzones?region=us-east-1)でDNS ゾーン内の CNAME DNS エントリをこの値に設定する。Route 53 で DNS ゾーン内に Application Load Balancer を指す CNAME エントリを作成する

設定後に30秒ほど待つと TLS を使って作成したドメイン名でALBにアクセスできる


### IDトークン を取得する設定

[Lamdaのコンソール](https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions)で自動作成された関数 **DeviceGrant-token** を選択する

**[設定]** タブで環境変数を **編集する**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/e5ec3df3-53a6-4b23-b15e-3228f0aa14d9.png)

**RESULT_TOKEN_SET** を追加する。**ID+ACCESS+REFRESH** を指定することで、アクセストークンだけでなくIDトークンを同時に取得する

| 環境変数 | 値 |
| :--: | :--: |
| RESULT_TOKEN_SET | ID+ACCESS+REFRESH |


# 3. デバイス認可フローでトークンを取得する

トークン取得の全体像を確認する。図の黄色の長方形の箇所が **curl** コマンドや **ウェブブラウザ** でサーバーにアクセスする箇所になる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/3bc6f608-f45e-4b33-95d8-c316a27042dc.png)



[CloudFormation コンソール](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks?filteringText=&filteringStatus=active&viewNested=true) 内の Stacks ページの **[出力]** で確認できる次の２つのキー使って確認を進める

| **キー**                         | **値 (一部マスク済み)**                   |
|----------------------------------|----------------------------------|
| **DeviceCognitoClientClientID**  | `2m5a9b*************`           |
| **DeviceCognitoClientClientSecret** | `2m5a9b*************` |



json形式のアウトプットを読みやすくするために、あらかじめ **jq** コマンドを用意しておく

```: Macの場合
brew install jq
```

## トークンの取得

### (1) パラメータを設定する
```bash: 
CLIENT_ID=2m5a9b*************
CLIENT_SECRET_ID=2m5a9b*************
ALB=<ALBのDNA名>
```

### (2) デバイスコードとユーザーコードを取得する

```bash: 
CODE=`curl  -s -X POST "https://${ALB}/token?client_id=${CLIENT_ID}" \
     -H "User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)" \
     -H "Authorization: Basic $(echo -n ${CLIENT_ID}:${CLIENT_SECRET_ID} | base64)" | jq`
echo $CODE
DEVICE_CODE=`echo $CODE | jq -r .device_code`
echo $DEVICE_CODE
```

```bash: 出力例
{
    "device_code": "wLGgw9XnE**********",
    "user_code": "J2MVP***",
    "verification_uri": "https://**********/device",
    "verification_uri_complete": "https://**********/device?code=J2MVP***&authorize=true",
    "interval": 5,
    "expires_in": 1800
}
```


### **(3) 認証URLを開き、ユーザーコードを入力してサインインする**

1.	表示された認証URLを開く
1.	ユーザーコードを入力
1.	Amazon Cognito へのログイン


ブラウザで、 verification_uri にアクセスします。

ログインしていない場合にはユーザー名とパスワードでログイン

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/659c33b0-5d1b-4b25-80ff-4c1fcc1e78a1.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/808d1c3c-97bf-4a47-a3dd-80a306bd071f.png)

ここでエラーが表示される場合には今回のサイトに関連するブラウザのCookieをすべて削除してから再度アクセスしてログインし直せば良い・

再度 verification_uri にブラウザでアクセスして user_code を入力し Authorize を選択する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/4f861c20-760f-47bb-a5b5-0b1339503943.png)

成功すると下記の表示がされる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/044d441f-0523-4d25-8d1c-b3a134d2d9e1.png)


### **(4) トークン を取得する**


```bash
TOKENS=`curl -s -X POST "https://${ALB}/token?client_id=${CLIENT_ID}&device_code=${DEVICE_CODE}&grant_type=urn:ietf:params:oauth:grant-type:device_code" \
  -H "User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)" \
  -H "Host: ${ALB}" \
  -H "Authorization: Basic $(echo -n ${CLIENT_ID}:${CLIENT_SECRET_ID} | base64)" | jq`
echo $TOKENS
```
```bash:アウトプット例
{
  "id_token": "eyJ***************************",
  "access_token": "eyJ***************************",
  "refresh_token": "eyJ***************************",
  "expires_in": 3600
}
```

IDトークン、アクセストークンとリフレッシュトークンの取得を確認した。
**IDトークン を取得する設定** を忘れると IDトークンは取得できないので注意


### **(5) id_token と access_token を環境変数に設定しておく**


```bash
ID_TOKEN=`echo $TOKENS | jq -r .id_token`
ACCESS_TOKEN=`echo $TOKENS | jq -r .access_token`
```


# 4. トークンを使ってサービスにアクセスする

テスト用に **Hello World アプリ** を用意して、そのサービスにトークンを使ってアクセスする

## (1) Hello World Lamda 関数 を作成

[Lamda](https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions)のコンソールで **[関数を作成]** する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/5921b0f3-36c3-4737-93b8-9ba3bf612ce3.png)

次の設定値で関数を作成する

| 設定 | 設定値 |
| :--: | :--: |
| 関数の作成 | 一から作成 |
| 関数名 | helloworld |
| ランタイム | Python 3.9 |
| アーキテクチャ| arm64 |

**[関数の作成]** を選択し作成する


## (2) API Gateway の作成

[API Gateway のコンソール](https://us-east-1.console.aws.amazon.com/apigateway)からREST APIを作成する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/29ef4fb8-9e8f-4365-8c75-d7ec537a7a1c.png)

### a. オーソライザーの設定

**[オーソライザー]**  -「**[オーソライザーの作成]** を選択する


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/06afee05-0b36-4bec-bb4b-462070244a42.png)

次の設定値でオーソライザーを作成する

| 項目                 | 設定値                             |
| -------------------- | ---------------------------------- |
| オーソライザー名     | 任意の名称                         |
| オーソライザーのタイプ | Cognito                           |
| Cognito ユーザープール   | 自動作成されたユーザープール       |
| トークンのソース     | Authorization                      |


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/8fe41b77-79e5-4d2c-808a-a8f95f35e189.png)


### b. Hello World Lamdaを割り当てる

作成したAPIの [リソース] のページで **[メソッド作成]** を選択する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/64411434-b9e8-4516-b5fb-c9eb6ab936f3.png)

次の設定値でメソッドを作成する

| 項目                   | 設定値                               |
| ---------------------- | ------------------------------------ |
| メソッドタイプ         | ANY                                  |
| 統合タイプ            | Lambda                               |
| Lambda 関数           | 先ほど作成したLambda関数             |



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/63e7a121-4e8f-4d5b-9ef9-10f2216a0bc4.png)



### c. メソッドリクエストの設定

作成したAPIの [リソース] ページで　**メソッドリクエストの設定** を **[編集]** する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/072fcc64-65fb-4ae8-b06e-b2b182c8d862.png)

次の設定値でメソッドリクエストの設定 を行う

| 項目                   | 設定値                                         |
| ---------------------- | -------------------------------------------- |
| 認可                  | **オーソライザーの設定** で作成したオーソライザー名 |
| リクエストバリデーター | クエリ文字列パラメータ、およびヘッダーを検証   |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/0c4a1e4c-12ff-4a87-aa6a-ee12108a9c85.png)





**[保存]** ボタンを選択し編集を保存する


### d. APIのデプロイ

**[APIをデプロイ]** ボタンを選択してAPIをデプロイする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/180cb9c7-bb89-428b-9ee1-96c6318cbbdc.png)

ステージ名は **prod** として **デプロイ** を選択する


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/630e7ada-e595-4047-8998-853575be78e1.png)


デプロイしたAPIにアクセスするURL (URLを呼び出すのURL) を **ステージ** の **prod** のページで確認する

![スクリーンショット 2025-03-02 12.26.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/b89a4400-fab1-4209-9ae4-4192af784a6c.png)


## (3) トークンを使ってAPIにアクセスする

先に確認したデプロイしたAPIにアクセスするURLを通してトークンを使って認証を確かめた。IDトークンで認証されるが、アクセストークンでは認証に失敗することを確認した。

### **トークンなし の場合**

認証に失敗する

```bash
curl  -s  -X  GET  "https://example.com/prod"
```
```bash:出力例
{"message":"Unauthorized"}%    
```

### **アクセストークン の場合**

認証に失敗する

```bash
curl  -s  -X  GET -H "Authorization: Bearer ${ACCESS_TOKEN}" "https://example.com/prod"
```
```bash:出力例
{"message":"Unauthorized"}%  
```

### **IDトークン の場合**

認証に成功して Hello World Lamda関数が呼び出されたことが確認できる

```bash
curl  -s  -X  GET -H "Authorization: Bearer ${ID_TOKEN}" 
"https://example.com/prod"
```
```bash:出力例
{"statusCode": 200, "body": "\"Hello from Lambda!\""}%                                  
```



# 参考資料

https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow/call-your-api-using-the-device-authorization-flow

https://repost.aws/ja/knowledge-center/cognito-custom-domain-errors


https://dev.classmethod.jp/articles/amazon-cognito-hosted-ui-customize-domain/

https://qiita.com/zumax/items/2d7eb1ffa49441eb988a

https://dev.classmethod.jp/articles/api-gateway-request-validation/