はじめに
こんにちは、iimonでエンジニアをしているhogeです。
普段通り生活していたら、ふと思い立ったかのように API Gateway + Cognito構成のサーバレスのアプリケーションに認可を組み込む方法が気になることってありますよね。
AWSの最近のアップデートでAmazon Verified Permissionsを使って、Amazon Cognitoグループに対して簡単に認可を設定できるようになったようだったので、試してみたいと思います。
Amazon Verified Permissionsとは
簡単に説明すると、ポリシーを事前に登録することで、アプリケーションの認可をアプリケーションの外側で行うことができるサービスです。
https://aws.amazon.com/verified-permissions/
ポリシーはCedarというAWSのOSSのポリシー言語で記述します。
permit( principal == User::"alice", action in [Action::"view", Action::"edit", Action::"delete"], resource == Photo::"VacationPhoto94.jpg" );
例えば上記のポリシーは、aliceがvacationPhoto94.jpg という写真を表示、編集、削除することを許可します。
IAMポリシーのように主に以下の3つの要素で許可・拒否していくかを書いていくことになります。
- principal
- Alice
- action
- view,edit,deny,delete
- resource
- VacationPhoto94.jpg
API Gateway + Cognitoでバックエンドを作成する
API GatewayのREST APIでリソースを作成
mockで以下のリソースを作成しました。
/admin/hoge : ANY
curl https://gd4l55l53d.execute-api.ap-northeast-1.amazonaws.com/test/admin/hoge { "message":"hoge" }
/public/fuga : ANY
curl https://gd4l55l53d.execute-api.ap-northeast-1.amazonaws.com/test/public/fuga { "message":"fuga" }
Cognitoにユーザプールを作成する
- admin、developersの2つのグループを作成
- test1,test2の2つのユーザを作成
- test1をadminグループに追加
- test2をdevelopersグループに追加
Amazon Verified Permissionsでポリシーストアを作成する
作成する際の起動オプションに「API Gateway とID プロバイダーによるセットアップ」があるので、これを選択する
作成したAPI Gatewayとステージを選択して「APIをインポート」を押す 自動的にAPI Gatewayのメソッド・リソースを検出し、CederのアクションAPI Gatewayのリソースをマッピングしてくれる cognitoのユーザプールを選択します(cognito以外のidpも選択できるが、グループ単位で認可を行う機能はcognitoでしか使えない) ユーザグループを選択肢、マッピングしたCederアクションに対して許可するか拒否するかを設定する。この設定がポリシードキュメントに反映される。
以下のように設定
- adminは全てのAPIを許可
- developersはpublic/以下のみ許可
この設定でポリシーストアを作成すると、cloudfortmationによってLambdaのAuthorizerなどが生成されます。
動作確認
cognitoの認証で認証されていないユーザはAPIへアクセスできなくなった(401)
curl -i https://gd4l55l53d.execute-api.ap-northeast-1.amazonaws.com/test/public/fuga HTTP/2 401 date: Tue, 11 Jun 2024 01:55:45 GMT content-type: application/json content-length: 26 x-amzn-requestid: 374f8fab-14fc-4d49-a349-ae77bfe2846d x-amzn-errortype: UnauthorizedException x-amz-apigw-id: ZLg5PG9YtjMENIg= {"message":"Unauthorized"}%
cognitoのアクセストークンを取得
test1token=`aws cognito-idp initiate-auth --auth-flow USER_PASSWORD_AUTH --client-id $client_id --auth-parameters USERNAME=test1,PASSWORD=$PASSWORD | jq -r '.AuthenticationResult.AccessToken'` test2token=`aws cognito-idp initiate-auth --auth-flow USER_PASSWORD_AUTH --client-id $client_id --auth-parameters USERNAME=test2,PASSWORD=$PASSWORD | jq -r '.AuthenticationResult.AccessToken'`
認証されたユーザにアクセスできるようになったので、どちらのグループからも許可されているpublic/
のエンドポイントにアクセスしてみる
どちらもアクセスできる
curl -i -H "Authorization:$test1token" \ https://gd4l55l53d.execute-api.ap-northeast-1.amazonaws.com/test/public/fuga HTTP/2 200 date: Tue, 11 Jun 2024 02:30:35 GMT content-type: application/json content-length: 24 x-amzn-requestid: 39f11321-c4df-418e-adf0-b049451c1e3c x-amz-apigw-id: ZLl_iH_RNjMEZ9A= x-amzn-trace-id: Root=1-6667b6c9-5cdeb5fa07cd04460fbf5359;Parent=4951c4ec2b145d6f;Sampled=0;lineage=06faa94e:0 { "message":"fuga" }%
curl -i -H "Authorization:$test2token" \ https://gd4l55l53d.execute-api.ap-northeast-1.amazonaws.com/test/public/fuga HTTP/2 200 date: Tue, 11 Jun 2024 02:31:44 GMT content-type: application/json content-length: 24 x-amzn-requestid: 60a42197-2ecc-4b6e-a6cb-f1ce0c222df7 x-amz-apigw-id: ZLmKlHj0tjMEMUg= x-amzn-trace-id: Root=1-6667b710-31d98cfd2750715f74a2ab5f;Parent=7ee3c071aa80beb3;Sampled=0;lineage=06faa94e:0 { "message":"fuga" }%
次にtest1の所属しているadminグループのみアクセスが許可されている/admin/hoge
にアクセスしてみる
test1はアクセスできる
curl -i -H "Authorization:$test1token" \ https://gd4l55l53d.execute-api.ap-northeast-1.amazonaws.com/test/admin/hoge HTTP/2 200 date: Tue, 11 Jun 2024 02:33:01 GMT content-type: application/json content-length: 24 x-amzn-requestid: 27976cdb-f2ed-4326-a9d5-26c2b06dfb88 x-amz-apigw-id: ZLmWgGmntjMEB0w= x-amzn-trace-id: Root=1-6667b75c-4a3bbe422850c44e6fa1382d;Parent=213d9b02594070b1;Sampled=0;lineage=06faa94e:0 { "message":"hoge" }%
test2は認可が通らず403エラーが返される
curl -i -H "Authorization:$test2token" \ https://gd4l55l53d.execute-api.ap-northeast-1.amazonaws.com/test/admin/hoge HTTP/2 403 date: Tue, 11 Jun 2024 02:33:20 GMT content-type: application/json content-length: 82 x-amzn-requestid: 9856853a-6fd9-4ad3-9de9-9e7fc880d26f x-amzn-errortype: AccessDeniedException x-amz-apigw-id: ZLmZoHMztjMEvBQ= {"Message":"User is not authorized to access this resource with an explicit deny"}%
上記のようにAmazon Verified Permissionsで設定した通りの期待した通りの挙動になりました!
Verified Permissionsのセットアップで作成されたリソースを確認する
今回の構成では以下のリソースが作成されます
cognito,apigw,backendのリソースは自分で作っていますが、認可部分のAuthorizerとVerified Permissionsのドキュメントは自動で生成されています。
verified permissions
以下を検証するverified permissionsのCederのドキュメントが作成されている
permit( principal in test::UserGroup::"ap-northeast-1_Ltf4Zs6sc|developers", action in [ test::Action::"get /public/fuga", test::Action::"put /public/fuga", kterasihblogtest::Action::"delete /public/fuga", test::Action::"post /public/fuga", test::Action::"patch /public/fuga" ], resource );
permit ( principal in test::UserGroup::"ap-northeast-1_Ltf4Zs6sc|admin", action in [test::Action::"delete /admin/hoge", test::Action::"delete /public/fuga", test::Action::"get /admin/hoge", test::Action::"get /public/fuga", test::Action::"patch /admin/hoge", test::Action::"patch /public/fuga", test::Action::"post /admin/hoge", test::Action::"post /public/fuga", test::Action::"put /admin/hoge", test::Action::"put /public/fuga" ], resource );
Lambda Authorizer
API Gatewayを確認すると、認可の部分にLambda Authorizerが関連付けられている lambdaを確認すると以下の環境変数が自動的に設定されている
ENDPOINT NAMESPACE POLICY_STORE_ID TOKEN_TYPE
Authorizer側ではロジックを持たず、verified permissionsにリクエストして認可結果を返す、汎用的なものになっている
コード
const { VerifiedPermissions } = require('@aws-sdk/client-verifiedpermissions'); const policyStoreId = process.env.POLICY_STORE_ID; const namespace = process.env.NAMESPACE; const tokenType = process.env.TOKEN_TYPE; const resourceType = `${namespace}::Application`; const resourceId = namespace; const actionType = `${namespace}::Action`; const verifiedpermissions = !!process.env.ENDPOINT ? new VerifiedPermissions({ endpoint: `https://${process.env.ENDPOINT}ford.${process.env.AWS_REGION}.amazonaws.com`, }) : new VerifiedPermissions(); function getContextMap(event) { const hasPathParameters = Object.keys(event.pathParameters).length > 0; const hasQueryString = Object.keys(event.queryStringParameters).length > 0; if (!hasPathParameters && !hasQueryString) { return undefined; } const pathParametersObj = !hasPathParameters ? {} : { pathParameters: { // transform regular map into smithy format record: Object.keys(event.pathParameters).reduce((acc, pathParamKey) => { return { ...acc, [pathParamKey]: { string: event.pathParameters[pathParamKey] } } }, {}), } }; const queryStringObj = !hasQueryString ? {} : { queryStringParameters: { // transform regular map into smithy format record: Object.keys(event.queryStringParameters).reduce((acc, queryParamKey) => { return { ...acc, [queryParamKey]: { string: event.queryStringParameters[queryParamKey] } } }, {}), } }; return { contextMap: { ...queryStringObj, ...pathParametersObj, } }; } async function handler(event, context) { // https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html // > Header names and query parameters are processed in a case-sensitive way. // https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2 // > header field names MUST be converted to lowercase prior to their encoding in HTTP/2 // curl defaults to HTTP/2 let bearerToken = event.headers?.Authorization || event.headers?.authorization; if (bearerToken?.toLowerCase().startsWith('bearer ')) { // per https://www.rfc-editor.org/rfc/rfc6750#section-2.1 "Authorization" header should contain: // "Bearer" 1*SP b64token // however, match behavior of COGNITO_USER_POOLS authorizer allowing "Bearer" to be optional bearerToken = bearerToken.split(' ')[1]; } try { const parsedToken = JSON.parse(Buffer.from(bearerToken.split('.')[1], 'base64').toString()); const actionId = `${event.requestContext.httpMethod.toLowerCase()} ${event.requestContext.resourcePath}`; const input = { [tokenType]: bearerToken, policyStoreId: policyStoreId, action: { actionType: actionType, actionId: actionId, }, resource: { entityType: resourceType, entityId: resourceId }, context: getContextMap(event), }; const authResponse = await verifiedpermissions.isAuthorizedWithToken(input); console.log('Decision from AVP:', authResponse.decision); let principalId = `${parsedToken.iss.split('/')[3]}|${parsedToken.sub}`; if (authResponse.principal) { const principalEidObj = authResponse.principal; principalId = `${principalEidObj.entityType}::"${principalEidObj.entityId}"`; } return { principalId, policyDocument: { Version: '2012-10-17', Statement: [ { Action: 'execute-api:Invoke', Effect: authResponse.decision.toUpperCase() === 'ALLOW' ? 'Allow' : 'Deny', Resource: event.methodArn } ] }, context: { actionId, } } } catch (e) { console.log('Error: ', e); return { principalId: '', policyDocument: { Version: '2012-10-17', Statement: [ { Action: 'execute-api:Invoke', Effect: 'Deny', Resource: event.methodArn } ] }, context: {} } } } module.exports = { handler, };
APIリソースを追加する場合
/admin/miyo
リソースを追加しました
/admin/miyo にauthorizerを追加します
どちらのユーザからも認可が失敗してアクセスができなくなります
curl -i -H "Authorization:$test1token" \ https://gd4l55l53d.execute-api.ap-northeast-1.amazonaws.com/test/admin/miyo HTTP/2 403 date: Tue, 11 Jun 2024 04:32:42 GMT content-type: application/json content-length: 82 x-amzn-requestid: 36f7cc77-9ce3-4ff6-8dbc-bec8fb87bb38 x-amzn-errortype: AccessDeniedException x-amz-apigw-id: ZL34vG52tjMElEg= {"Message":"User is not authorized to access this resource with an explicit deny"}% curl -i -H "Authorization:$test1token" \ https://gd4l55l53d.execute-api.ap-northeast-1.amazonaws.com/test/admin/miyo HTTP/2 403 date: Tue, 11 Jun 2024 04:32:47 GMT content-type: application/json content-length: 82 x-amzn-requestid: e21d1357-607a-4cc6-a7b2-1da70baa7883 x-amzn-errortype: AccessDeniedException x-amz-apigw-id: ZL35ZGzwNjMEQcQ= {"Message":"User is not authorized to access this resource with an explicit deny"}%
スキーマを更新する ポリシードキュメントを更新してadminグループにadmin/miyoアクションを追加
permit ( principal in test::UserGroup::"ap-northeast-1_Ltf4Zs6sc|admin", action in [test::Action::"delete /admin/hoge", test::Action::"delete /public/fuga", test::Action::"get /admin/hoge", test::Action::"get /public/fuga", test::Action::"patch /admin/hoge", test::Action::"patch /public/fuga", test::Action::"post /admin/hoge", test::Action::"post /public/fuga", test::Action::"put /admin/hoge", test::Action::"put /public/fuga", test::Action::"get /admin/miyo" //追加 ], resource );
adminグループからはアクセスできたが、developerグループのユーザからはアクセスできなくなった
curl -i -H "Authorization:$test1token" \ https://gd4l55l53d.execute-api.ap-northeast-1.amazonaws.com/test/admin/miyo HTTP/2 200 date: Tue, 11 Jun 2024 04:43:54 GMT content-type: application/json content-length: 4 x-amzn-requestid: 63028081-1794-495f-88c1-6af177e2081a x-amz-apigw-id: ZL5hsGkbNjMEiIg= x-amzn-trace-id: Root=1-6667d60a-1719edd27c3047a0246aab6b;Parent=27a8de5298edadea;Sampled=0;lineage=06faa94e:0 miyo% curl -i -H "Authorization:$test2token" \ https://gd4l55l53d.execute-api.ap-northeast-1.amazonaws.com/test/admin/miyo HTTP/2 403 date: Tue, 11 Jun 2024 04:43:49 GMT content-type: application/json content-length: 82 x-amzn-requestid: e2a30755-2fde-4fb1-b1c0-83b7dfd0cd9a x-amzn-errortype: AccessDeniedException x-amz-apigw-id: ZL5gvHZktjMEYnQ= {"Message":"User is not authorized to access this resource with an explicit deny"}%
verified permissionsを触ってみた感想
以下のユースケースなら効果的に使えそうだと個人的には思いました
基本的な認可の管理
- ユーザやグループに対して具体的な操作(読み取り、書き込み、削除)などの権限を付与するような基本的な認可の管理
- 複雑な条件が必要な認可の場合はアプリケーション内で認可を行ったほうが良さそう
- 例:ユーザの状態や外部システムからのデータに基づく動的な認可
RBAC(ロールベースアクセス制御)
- 開発者、管理者、エンドユーザなどの異なるロールに対するアクセスレベルの設定
条件付きポリシー
- 特定のIPの範囲、デバイスタイプ、時間帯、ユーザの情報などの情報を指定して条件付きポリシーを書くのは得意そう
permit( principal, action == Action::"view", resource ) when {resource.client_ip == "222.222.222.222" && principal.location == "USA"};
ちなみに料金はけっこうお高い
1 か月あたり 最初の 4,000 万件のリクエスト | 承認リクエスト1件あたり0.00015ドル |
---|---|
次の 1 か月あたり 6,000 万件のリクエスト | オーソリゼーションリクエスト1件あたり0.000075ドル |
毎月 1 億件を超えるリクエスト | オーソリゼーションリクエスト1件あたり0.00004ドル |
ポリシー管理リクエスト | ポリシー管理リクエスト1件あたり0.00004ドル |
月に150万リクエストの場合、150 万 * 0.00015 USD = 225USDになる
さいごに
複雑な認可要件がある場合、対応できるかはわからないですが、アプリケーションのアクセスコントロールをポリシーで容易に実装できるのはとても便利だと思いました。特にVerified Permissionsの自動セットアップ機能は既存のサーバレスアプリケーションに認可を追加したい場合には採用しやすいと思いました。
弊社では現在エンジニアを募集しております!
この記事を読んで少しでも弊社に興味を持っていただけましたら是非カジュアル面談でお話ししましょう!!お話しできるのを楽しみにしています!! Wantedly / Green