こんにちは!
APIGateway は、AWS での RestAPI 開発でデファクトスタンダードな製品です。既存システムのクラウドリフトアップから新規開発問わずどんなユースケースでも導入が可能で、AWS で RestAPI 開発する際には真っ先に導入が検討される製品です。サーバーレスな API 開発から、プロキシー的な位置付けで導入を検討されるお客様も多くいらっしゃいます。
そんな RestAPI 開発の製品として魅力的な API Gateway ですが、私たちが思う APIGateway の真価は、その幅広いセキュリティ機能にあると感じています。
本記事では、CognitoUserPool、CognitoIDPool、IAM Role、APIGateway を使用したコードを一切書かないサーバーレスなAPI認可を実現する方法を解説します。(少々上級者向けの内容となっております…)
本記事は、AWS のセキュリティに関する基本的な知識を要する方向けの内容となっています。以下の言葉についてピンとこない方は、AWS の製品 HP などで仕様確認の上ご覧ください。
ネイティブの CloudFormation や SAM のコードは紹介しません。ServerlessFramework3 を用いた CloudFormation のコードを紹介します。(とは言いつつ、基本的には通常の CloudFormation ですので引用しやすいかと思います)
本記事で紹介する認可の APIGateway のワークフローは以下になります。(詳細は後述)
上記を実現するにあたり各 AWS 製品の細かいテクニックを使用しますが、その中心にいるのはIAMロールです。最終的なゴールは、各ユーザーが適切な IAM ロールの認可トークンを得ることです。
それでは、CloudFormation のソースコードを交えて実装方法を解説していきます。
本記事のサンプルでは、IAM ロールを用いて以下のような API に対する認可を実現していきます。以下のサンプルAPI は、シンプルに従業員と管理者、それぞれが呼び出せる API として理解してもらえたらと思います。
GET /staff
GET /admin
管理者向けの IAM ロールと従業員向けの IAM ロールを作成し、上記 API の認可を実現します。IAM ロールのCloudFormation は以下です。
# 以下は管理者向けのロール
AuthenticatedAdminRole:
Type: AWS::IAM::Role
Properties:
RoleName: "example-congito-apigateway-AuthenticatedAdminRole"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Action: sts:AssumeRoleWithWebIdentity
Effect: Allow
Principal:
Federated: cognito-identity.amazonaws.com
Condition:
StringEquals:
cognito-identity.amazonaws.com:aud: !Ref CognitoIDPool
ForAnyValue:StringLike:
cognito-identity.amazonaws.com:amr: authenticated
Policies:
- PolicyName: "policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: APIGateway
Effect: Allow
Action:
- execute-api:Invoke
Resource:
- !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGatewayRestApi}/*/GET/admin*
# 以下は従業員向けのロール
AuthenticatedStaffRole:
Type: AWS::IAM::Role
Properties:
RoleName: "example-congito-apigateway-AuthenticatedStaffRole"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Action: sts:AssumeRoleWithWebIdentity
Effect: Allow
Principal:
Federated: cognito-identity.amazonaws.com
Condition:
StringEquals:
cognito-identity.amazonaws.com:aud: !Ref CognitoIDPool
ForAnyValue:StringLike:
cognito-identity.amazonaws.com:amr: authenticated
Policies:
- PolicyName: "policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: APIGateway
Effect: Allow
Action:
- execute-api:Invoke
Resource:
- !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGatewayRestApi}/*/GET/staff*
# 以下はCognitoIDプール向けのロール
CognitoIdentityPoolAuthenticatedRole:
Type: AWS::IAM::Role
UpdateReplacePolicy: Retain
Properties:
RoleName: "example-congito-apigateway-CognitoIdentityPoolAuthenticatedRole"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Action: sts:AssumeRoleWithWebIdentity
Effect: Allow
Principal:
Federated: cognito-identity.amazonaws.com
Condition:
StringEquals:
cognito-identity.amazonaws.com:aud: !Ref CognitoIDPool
ForAnyValue:StringLike:
cognito-identity.amazonaws.com:amr: authenticated
Policies:
- PolicyName: "example-congito-apigateway-CognitoIdentityPoolAuthenticatedRole-policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: Cognito
Effect: Allow
Action:
- cognito-identity:GetCredentialsForIdentity
- cognito-identity:GetId
Resource: "*"
CognitoUserPool の作成では、CognitoUserPool にユーザープールグループを設定するのがポイントです。各ユーザープールグループに対して IAM ロールの ARN を指定しますが、これを後述の CognitoIDPool が AssumeRole 対象のIAM ロールとして使用します。
CognitoUserPool:
Type: "AWS::Cognito::UserPool"
Properties:
AccountRecoverySetting:
RecoveryMechanisms:
- Name: "verified_email"
Priority: 2
AutoVerifiedAttributes:
- "email"
AliasAttributes:
- "email"
- "preferred_username"
Policies:
PasswordPolicy:
MinimumLength: 8 # 10文字以上
RequireLowercase: false # 小文字必須
RequireNumbers: false # 数字必須
RequireSymbols: false # 記号必須
RequireUppercase: false # 大文字必須
UserPoolName: "example-congito-apigateway-UserPool"
CognitoUserPoolClient:
Type: "AWS::Cognito::UserPoolClient"
Properties:
CallbackURLs:
- "http://localhost:3000"
ClientName: "example-congito-apigateway-UserPoolClient"
DefaultRedirectURI: "http://localhost:3000"
ExplicitAuthFlows:
- ALLOW_USER_SRP_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
- ALLOW_ADMIN_USER_PASSWORD_AUTH
GenerateSecret: false
LogoutURLs:
- "http://localhost:3000"
PreventUserExistenceErrors: ENABLED
ReadAttributes:
- email
RefreshTokenValidity: 10
SupportedIdentityProviders:
- COGNITO
UserPoolId: !Ref CognitoUserPool
UserPoolAdministoratorGroup:
Type: "AWS::Cognito::UserPoolGroup"
Properties:
Description: "管理者"
GroupName: Admin
Precedence: 1
UserPoolId: !Ref CognitoUserPool
RoleArn: !GetAtt AuthenticatedAdminRole.Arn
UserPoolStaffGroup:
Type: "AWS::Cognito::UserPoolGroup"
Properties:
Description: "一般"
GroupName: Staff
Precedence: 2
UserPoolId: !Ref CognitoUserPool
RoleArn: !GetAtt AuthenticatedStaffRole.Arn
以下の箇所がユーザープールグループの設定箇所です。先ほど作成した従業員と管理者の IAM ロールが設定されていると思います。これにより、認証トークンに対してcognito:preferred_role
が付与され、後述の CognitoIDPool がAssumeRole する対象の IAM ロールとして認識できます。(詳細は後述)
UserPoolAdministoratorGroup:
Type: "AWS::Cognito::UserPoolGroup"
Properties:
Description: "管理者"
GroupName: Admin
Precedence: 1
UserPoolId: !Ref CognitoUserPool
RoleArn: !GetAtt AuthenticatedAdminRole.Arn
UserPoolStaffGroup:
Type: "AWS::Cognito::UserPoolGroup"
Properties:
Description: "一般"
GroupName: Staff
Precedence: 2
UserPoolId: !Ref CognitoUserPool
RoleArn: !GetAtt AuthenticatedStaffRole.Arn
それでは、CognitoIDPool を作成します。CognitoIDPool は CognitoUserPool で認証したトークンを使用して認可トークンを得ることができます。
CognitoIDPool:
Type: AWS::Cognito::IdentityPool
DependsOn:
- CognitoUserPool
- CognitoUserPoolClient
Properties:
IdentityPoolName: "example-congito-apigateway-IDPool"
AllowUnauthenticatedIdentities: false
CognitoIdentityProviders:
- ClientId: !Ref CognitoUserPoolClient
ProviderName: !Sub cognito-idp.${AWS::Region}.amazonaws.com/${CognitoUserPool}
IdentityPoolRoleAttachment:
Type: AWS::Cognito::IdentityPoolRoleAttachment
DependsOn:
- CognitoIDPool
- CognitoUserPool
- CognitoUserPoolClient
- AuthenticatedStaffRole
Properties:
IdentityPoolId: !Ref CognitoIDPool
Roles:
"authenticated": !GetAtt CognitoIdentityPoolAuthenticatedRole.Arn
RoleMappings:
CognitoUserPool:
AmbiguousRoleResolution: AuthenticatedRole
IdentityProvider: !Sub cognito-idp.${AWS::Region}.amazonaws.com/${CognitoUserPool}:${CognitoUserPoolClient}
Type: Token
RoleMappings の箇所を見てください。先ほど作成した CognitoUserPool の Web クライアントを認証プロバイダーに指定し、Type を Token に指定しています。ここで、TypeをTokenにすることが重要です。TypeをTokenにすることで、認証トークンの内容から自動的にAssumeRoleするIAMロールを判定することができます。
RoleMappings:
CognitoUserPool:
AmbiguousRoleResolution: AuthenticatedRole
IdentityProvider: !Sub cognito-idp.${AWS::Region}.amazonaws.com/${CognitoUserPool}:${CognitoUserPoolClient}
Type: Token
TypeをTokenに指定することで、CognitoUserPoolGroupで指定されているロール(cognito:roles and cognito:preferred_role)を使用してロールをマッピングすることが可能です。これにより、完全サーバーレスでIAMロールを使用したAPI認可を実現できます。
結論、取得できません。
APIGateway の認証タイプが IAM 認証になっている場合は、フロントエンドは CognitoIDPool から取得した STS トークンを使用したシグネチャーを生成し、APIGateway へリクエストを行います。つまり、認証トークンを API が知る術がないということです。
IAM 認証時においても、リクエストユーザーを特定できる情報を取得したいケースは多いです。以下は、APIGatewayの認証タイプが IAM 認証の時に、Lambda で取得できる Identity の情報です。実装時に参考にしてください!
{
"identity":
{
"cognitoIdentityPoolId": "ap-northeast-1:xxxxx-xxxxx-xxxxx",
"accountId": "643022257822",
"cognitoIdentityId": "ap-northeast-1:xxxxx-xxxxx-xxxxx",
"caller": "xxxxx-xxxxx-xxxxx:CognitoIdentityCredentials",
"sourceIp": "xxx.xxx.xxx.xxx",
"principalOrgId": "xxxxx-xxxxx-xxxxx",
"accessKey": "XXXXXXXXXXXX",
"cognitoAuthenticationType": "authenticated",
"cognitoAuthenticationProvider": "cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_6zELGQFml,cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_6zELGQFml:CognitoSignIn:{ cognito user pool sub }",
"userArn": "arn:aws:sts::643022257822:assumed-role/example-congito-apigateway-AuthenticatedAdminRole/CognitoIdentityCredentials",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"user": "xxxxx-xxxxx-xxxxx:CognitoIdentityCredentials",
},
}
cognitoAuthenticationProvider の「:」の後に CognitoUserPool の Sub が設定されています。これにより、IAM 認証時においてもユーザーの特定が可能となります。
本記事では、各 AWS 製品のメリットを存分に活かした APIGateway のサーバーレスな IAM 認証実装方法を解説しました。
もちろん、Lambda コードで愚直に認可処理を実行することも可能なので、プロジェクトに合わせて認可処理の実現方法を検討してみてください。
AWSサーバーレス、フロントエンドの開発はお気軽にお問い合わせください。