🐈

Cognito API ハンズオンをやってみた

2024/01/25に公開

以下のハンズオンを行ってみました。

https://dcj71ciaiav4i.cloudfront.net/591796E0-D127-11EB-A6A5-FB83B2BAF6EE/

ハンズオンの概要

Cognito の基礎から理解するため、直接 API の操作を行うハンズオン

はじめに

  • 実際のアプリ開発では SDK を使って呼び出すことが多いが、構成によっては API を直接呼び出すケースもある
  • Cognito のサービス
    • ユーザープール
      • ユーザを登録して認証
      • JWT形式のトークンを発行し、アプリのAPIへのアクセスに利用できる
    • IDプール
      • Cognitoユーザプールor外部IDプロバイダでのログインに基づき、AWS一時クレデンシャルを発行
      • ユーザは、AWSのAPIにアクセスできるようになる
  • ユーザプールで提供するAPI
    • Cognito Identity Provider API
      • 非AdminAPIをブラウザのクライアントから直接呼び出したり
      • AdminAPIをサーバからIAM権限を使って呼び出したり
    • Cognito Auth API
      • OAuth/OIDCの仕様をベースとしたAPIで、Hosted UIも利用する
      • Authorization Code Grant
        • トークン発行前に認可コードを使ったステップがある
      • Implicit Grant
        • 認可コードなしですぐにトークンが発行される
  • IDプールで提供するAPI
    • Cognito Federated Identities API

AWS サービスの準備

作成するリソース

  • Cognito ユーザープール
    • Public アプリケーションクライアント
      • Cognito Identity Provider API の非 Admin API呼び出しに利用
    • Confidential アプリケーションクライアント
      • Cognito Auth API の Authorization Code Grant の呼び出しに利用
  • API Gateway(REST API)
    • Cognito の認証後に得られたJWTトークンでアクセス

Cognito ユーザープールの作成

まずは Cognito ユーザープールを作成します。
なお、ハンズオンはコンソールが旧画面なのでやることは同じですが若干手順が異なります。

認証プロバイダーはデフォルトの Cognito ユーザープールのみ選択し、サインインオプションにはEメールを選択。

パスワードポリシーはデフォルト、多要素認証は「MFAなし」、ユーザーアカウントの復旧もデフォルトに設定します。

セルフサービスのサインアップもデフォルト、属性検証とユーザーアカウントの確認もデフォルト、必須の属性もデフォルト(追加の必須属性なし)にします。

メッセージ配信のEメールは「CognitoでEメールを送信」に変更し、そのほかはデフォルトのままにします。

以下は少し設定が多いので設定箇所をまとめました。
Hosted UI を有効化して、パブリッククライアントを作成しています。

項目 設定
ユーザープール名 Cognito-API-Handson
Cognito のホストされた UI を使用 有効化
Cognito ドメイン handson
アプリケーションタイプ パブリッククライアント
アプリケーションクライアント名 PublicClient
クライアントシークレット 生成しない
許可されているコールバック URL http://localhost/
認証フロー ALLOW_USER_SRP_AUTH, ALLOW_USER_PASSWORD_AUTH

基本的にパスワードの認証フローは USER_SRP_AUTH が推奨されますが、今後のハンズオンで利用する curl で SRP を利用することが難しいので ALLOW_USER_PASSWORD_AUTH を追加しています。




これでユーザープールとパブリックなアプリケーションクライアントを作成したので、次は Confidential アプリケーションクライアントを作成します。

Confidential アプリケーションクライアントは、Auth API の Authorization Code Grant 呼び出しで利用します。

項目 設定
アプリケーションタイプ 秘密クライアント
アプリケーションクライアント名 ConfidentialClient
クライアントシークレット 生成する
認証フロー なし
許可されているコールバック URL http://localhost/
OpenID Connect のスコープ OpenID

API Gateway の作成

API の作成

REST API の API Gateway(エンドポイントタイプ:リージョン) を作成します。
メソッドを作成し、メソッドタイプは「ANY」、統合タイプは「Mock」にします。
ANY メソッドを選択し、統合レスポンス200のマッピングテンプレートで「application/json」のテンプレートに、テンプレートを生成「メソッドリクエストのパススルー」を選択して、リクエストされた内容をそのまま返すようにします。
その後、一旦APIをデプロイします。
デプロイ後、APIのURLをブラウザに貼り付けるとリクエスト時の内容がブラウザに表示されることを確認します。

Cognito オーソライザーの設定

今のままではCognitoの認証なしでAPIにアクセスできるため、APIにCognitoオーソライザーを作成して設定します。

まず、Cognitoオーソライザーを作成します。

次に、メソッドのメソッドリクエストからCognitoオーソライザーを設定します。

設定後、APIをデプロイします。
その後、ブラウザからAPIのURLにアクセスすると、リクエストされた内容ではなく「{"message":"Unauthorized"}」が返却され、アクセスが拒否されたことがわかりました。

Cognito Identity Provider API と API Gateway を呼び出す

Cognito Identity Provider API の利用するAPIは4つとも非Admin APIであり、通常はモバイルアプリやSPA(Single Page Applicaiton)から直接呼び出しで使うAPI。

ハンズオンではAPIの理解が進むようにcurlかPostmanを利用するが、今回はcurlを利用する。

ユーザ登録 SignUp

  • ユーザ登録を行うAPI
  • IAMによる認証不要
  • ブラウザやモバイルアプリからユーザ認証される前に呼び出される想定のAPI
  • 主なAPIのパラメータ
    • SecretHashはアプリクライアントシークレットが設定されているクライアントの場合に必要
{
  "ClientId": "string",
  "SecretHash": "string",
  "Username": "strint",
  "Password": "sring"
}

https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_SignUp.html

ユーザ登録確認 ConfirmSignUp

  • SignUp後は仮登録状態
  • IAMによる認証不要
  • 確認を行う必要がある
    • ユーザ自身がメールやSMSで確認コードで確認(ConfirmSignUp)
    • 管理者が確認操作(AdminConfirmSignUp)
  • 主なAPIのパラメータ
{
  "ClientId": "string",
  "SecretHash": "string",
  "Username": "strint",
  "ConfirmationCode": "sring"
}

https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ConfirmSignUp.html

ユーザ認証 InitiateAuth

  • IAM認証不要
  • 使用する認証フローやMFA有無により利用方法が異なる
  • 1回のAPI呼び出しで完了せず、追加でRespontToAuthChallenge APIを呼び出さないといけないケースがある
  • このハンズオンでは簡単に呼び出せる認証フローUSER_PASSWORD_AUTHを利用してMFAもないため1回の呼び出しで完結
  • 主なAPIパラメータ
{
  "ClientId": "string",
  "AuthFlow": "USER_PASSWORD_AUTH",
  "AuthParameters": {
	  "USERNAME": "strint",
	  "PASSWORD": "sring"
  }
}

https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_InitiateAuth.html

レスポンスがあると、おおよそ以下の結果が返ってくる。

{
	"AuthenticationResult": {
		"AccessToken": "?????",
		"IdToken": "?????",
		"RefreshToken": "?????",
		"TokenType": "Bearer",
		"ExpiresIn": 3600
	},
	"ChallengeParameters": {}
}

トークンはそれぞれヘッダ、ペイロード、署名の3つが含まれ、それぞれがBASE64でエンコードされ、ピリオドで連結されている。
AccessToken,IdTokenのペイロードはJSONとなっており、認証情報の内容が確認できる。

なお、InitiateAuthの1回の呼び出しで認証が完結しない場合は、ChallengeParameters
内でRespondToAuthChallenge APIで送るべき情報が指定される。

REST API アクセス

  • InitiateAuth呼び出しで得られたIDトークンをAuthorizationヘッダの値として送り、拒否されないか確認する

ユーザ削除 DeleteUser

  • IAM認証不要
  • アクセストークンを利用してリクエスト
  • ユーザ自身を削除するものなので、リクエストパラメーターにはAccessTokenのみでいい
{
	"AccessToken": "string"
}

https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_DeleteUser.html

API 共通

Cognito Identity Provider API のエンドポイント

https://cognito-idp.<region>.amazonaws.com/

APIのHTTPリクエスト時、ボディに加えて以下のヘッダ情報が必要

ヘッダ項目
Content-Type application/x-amz-json-1.1
X-Amz-Target AWSCognitoIdentityProviderService.<呼び出すAPI名>

curl で SingUp

ハンズオンでは、上記記載の通り Content-Type などを記載する必要があるとありますが、なぜ必要なのか理解していないので、あえて上記の Content-Type や X-Amz-Target なしでリクエストを行うとどうなるか確認してみます。

まず、両方のヘッダーなしだと、サーバが要求されたオペレーションが理解できないと返ってきます。
なお、<app-client-id>にはアプリクライアントのIDを、<mail-address> には自身のメールアドレスを、<password>にはパスワードを入力します。

curl -v --location \
--request POST 'https://cognito-idp.ap-northeast-1.amazonaws.com/'  \
--data-raw '
{
    "ClientId": "<app-client-id>",
    "Username": "<mail-address>",
    "Password": "<password>"
}
'
{"code":"BadRequest","message":"The server did not understand the operation that was requested.","type":"client"}%  

次に、X-Amx-Targetのみつけてみます。
両方のヘッダーがない時と同じ内容です。

$ curl --location \
--request POST 'https://cognito-idp.ap-northeast-1.amazonaws.com/' \
--header 'X-Amz-Target: AWSCognitoIdentityProviderService.SignUp' \
--data-raw '
{
    "ClientId": "<app-client-id>",
    "Username": "<mail-address>",
    "Password": "<password>"
}
'
{"code":"BadRequest","message":"The server did not understand the operation that was requested.","type":"client"}

次に、Content-Typeのみつけてみます。
エラーメッセージが変わりました。
オペレーションが不明だそうです。

$ curl --location \
--request POST 'https://cognito-idp.ap-northeast-1.amazonaws.com/' \
--header 'Content-Type: application/x-amz-json-1.1' \
--data-raw '
{
    "ClientId": "<app-client-id>",
    "Username": "<mail-address>",
    "Password": "<password>"
}
'
{"__type":"UnknownOperationException"}% 

CognitoのSignUpのドキュメントを確認しましたが、ヘッダーをつけることについては記載がありませんでした。
以下のre:Postによると、X-Amz-Targetヘッダーに関する公式ドキュメントはなく、プレーンなHTTPリクエストの内容を確認するにはAWS SDKをデバッグして構文を見つける必要があるとのことです。

https://repost.aws/questions/QU-I5HG3dMRHedBKftHyjOew/about-undocumented-x-amz-target-header-in-amazon-cognito-user-pools-apis

なお、Trasfer Familyのドキュメントではヘッダーについて記載がありました。

https://docs.aws.amazon.com/ja_jp/transfer/latest/userguide/making-api-requests.html

ハンズオンに戻り、お作法のヘッダーを付与して成功するリクエストを実行します。

$ curl --location \
--request POST 'https://cognito-idp.ap-northeast-1.amazonaws.com/' \
--header 'Content-Type: application/x-amz-json-1.1' \
--header 'X-Amz-Target: AWSCognitoIdentityProviderService.SignUp' \
--data-raw '
{
    "ClientId": "<app-client-id>",
    "Username": "<mail-address>",
    "Password": "<password>"
}
'

{
	"CodeDeliveryDetails":{
		"AttributeName":"email",
		"DeliveryMedium":"EMAIL",
		"Destination":"***@***"
	},
	"UserConfirmed":false,
	"UserSub":"xxx"
}% 

これでステータスが Unconfirmed のユーザが登録できました。
Cognito ユーザープールから、登録したユーザが確認できます。

curl で confirmSignUp

登録したユーザはまだ使えないので、確認操作を行います。
ユーザ登録時のメールアドレスにコードが届いているので、メモします。
そのコードを、以下の ConfirmationCode に入力し、実行します。

$ curl --location \
--request POST 'https://cognito-idp.ap-northeast-1.amazonaws.com/' \
--header 'Content-Type: application/x-amz-json-1.1' \
--header 'X-Amz-Target: AWSCognitoIdentityProviderService.ConfirmSignUp' \
--data-raw '
{
	"ClientId": "<app-client-id>",
	"Username": "<mail-address>",
	"ConfirmationCode": "<code>"
}
'

{}

その後、Cognitoユーザープールを確認すれば、ユーザーが Confirmed になっていることが分かります。

curl で InitiateAuth

ユーザの確認までできたので、ユーザ認証を行ってトークンをゲットします。

curl --location \
--request POST 'https://cognito-idp.ap-northeast-1.amazonaws.com/' \
--header 'Content-Type: application/x-amz-json-1.1' \
--header 'X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth' \
--data-raw '
{
    "ClientId": "xxx",
    "AuthFlow": "USER_PASSWORD_AUTH",
    "AuthParameters":
    {
        "USERNAME": "xxx@xxx",
        "PASSWORD": "xxx"
    }
}
'
	
{
	"AuthenticationResult":{
		"AccessToken":"xxx.xxx.xxx",
		"ExpiresIn":3600,
		"IdToken":"xxx.xxx.xxx",
		"RefreshToken":"xxx",
		"TokenType":"Bearer"
	},
	"ChallengeParameters":{}
}%  

トークンの中身は、以下のサイトから確認できます。

https://jwt.io/

アクセストークンの中身
ヘッダー

{
  "kid": "xxx",
  "alg": "RS256"
}

ペイロード
ユーザにとってユーザ名であるメールアドレスはアクセストークンに含まれていない

{
  "sub": "xxx",
  "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_xxx",
  "client_id": "xxx",
  "origin_jti": "xxx",
  "event_id": "xxx",
  "token_use": "access",
  "scope": "aws.cognito.signin.user.admin",
  "auth_time": 1705413408,
  "exp": 1705417007,
  "iat": 1705413408,
  "jti": "xxx",
  "username": "xxx"
}	

IDトークンの中身
ヘッダー

{
  "kid": "xxx",
  "alg": "RS256"
}

ペイロード
アクセストークンと重なる内容もあるが、メールアドレスなどのユーザの属性も含まれている。

{
  "sub": "xxx",
  "email_verified": true,
  "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_xxx",
  "cognito:username": "xxx",
  "origin_jti": "xxx",
  "aud": "xx
  "event_id": "xxx",
  "token_use": "id",
  "auth_time": 1705413408,
  "exp": 1705417007,
  "iat": 1705413408,
  "jti": "xxx",
  "email": "xxx@xxx"
}	

curl で REST API にアクセス

InitiateAuth の呼び出し結果からIDトークンを入手し、curlを実行します。

curl --location \
--request POST '<API_URL>' \
--header 'Authorization: <id_token>'

上記では省略していますが、認証拒否されずに正常にアクセスできました。
なお、API GatewayでMock機能を使うのではなく、実際にLambda関数を呼び出した場合、Lambda関数からどのユーザがアクセスしたか確認できます。
また、SignUpのAPIは追加のユーザ属性を設定してユーザを作成でき、設定された属性はIDトークンに反映されます。
IDトークンに反映されるので、Lambda関数からそれらの情報にアクセスできます。

curl で DeleteUser

DeleteUser の呼び出しにはアクセストークンが必要です。
アクセストークンを利用してユーザを削除します。

$ curl --location \
--request POST 'https://cognito-idp.ap-northeast-1.amazonaws.com/' \
--header 'Content-Type: application/x-amz-json-1.1' \
--header 'X-Amz-Target: AWSCognitoIdentityProviderService.DeleteUser' \
--data-raw '
{
   "AccessToken": "<access_token>"
}
'

Cognito ユーザプールを確認すると、ユーザが削除されていることが確認できます。

Auth API の Authorization Code Grant の呼び出し

Authorization Code Grant フローでの呼び出しを Confidential クライアントから行います。

認可エンドポイントをブラウザで開く

以下URLをブラウザで開くと、

https://<domain-name>.auth.ap-northeast-1.amazoncognito.com/authorize?client_id=<app-client-id>&response_type=code&redirect_uri=http://localhost/

以下URLにリダイレクトされて、HostedUIが表示されます。

https://<domain-name>.auth.ap-northeast-1.amazoncognito.com/login?client_id=<app-client-id>&response_type=code&redirect_uri=http://localhost/

https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/authorization-endpoint.html

認証エンドポイントは、ホストされている UI または IdP サインインページのどちらかにリダイレクトされます。

https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/login-endpoint.html

ログインエンドポイントは認証サーバーであり、認可エンドポイント からのリダイレクト先です。

ユーザーを登録して確認コードを入力すると、アプリケーションのURL(今回は http://localhost/)にリダイレクトされます。
その際、URLの末尾にはcode=xxxと認可コードが付与されます。

トークンエンドポイントからトークンを得る

サーバサイドアプリの代わりに、curlでトークンエンドポイントに認可コードを送ります。

curl --location \
--request POST 'https://<domain-name>.auth.ap-northeast-1.amazoncognito.com/token' \
-u <app-client-id>:<app-client-secret> \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=<app-client-id>' \
--data-urlencode 'redirect_uri=http://localhost/' \
--data-urlencode 'code=<auth-code>'

成功すると、IDトークンなどが返されます。

{
    "id_token": "?????.?????.?????",
    "access_token": "?????.?????.?????",
    "refresh_token": "?????????????????",
    "expires_in": 3600,
    "token_type": "Bearer"
}

おわりに

Cognitoを初めて利用する際には良いハンズオンだと思いました。

Discussion