【NextAuth.js/認可】IAM認証されたAPI GatewayにIdTokenを使ってアクセスする
はじめに
ウェブアプリケーションにおいて、ユーザーごとの権限を変えたい時ってありますよね?例えば、特定の権限を持ったユーザーにのみ特定のAPIを叩けるようにしたい、などです。
そんな時に役立つのがCognitoのIDプールです。Cognitoのユーザープールは認証を、IDプールは認可を担当しています。詳しい概念などは以下を参照してください。
やりたいこと
とあるウェブアプリケーションへサインインしたユーザーごとに叩けるAPIを制限することです。この時、APIのエンドポイントにはAPI Gatewayを利用しています。
対象読者
- Cognitoを使っている
- サインインしたユーザーごとに権限を付与したい
- 特にアクセスするAPIにIAM制限を付与したい
動作イメージ
CognitoのIDプールを使って、ユーザーに応じた一時クレデンシャルキーを発行します。この一時クレデンシャルキーはCognitoのグループに紐づくIAM Roleから選択されます。IAM Roleにはadminとuserが存在し、実行できるエンドポイントのパスが異なります。
adminで/adminへアクセス可能な一時キーを取得しアクセス
adminロールは、ユーザープールのadminグループに紐づいています。加えてadminロールは/admin
のエンドポイントのみアクセス可能なため、adminユーザーは/admin
のみアクセスできるというわけです。
また、adminとほぼ同じですが、userの認可付与取得の動きは次の通りです。
userで/userへアクセス可能な一時キーを取得しアクセス
デプロイ環境
- macOS: 13.5
- Next.js: 13.4
- AWS SDK: 3.413
- AWS CDK: 2.96.2
コード
コードは以下のリポジトリにおいてあります。本記事では紹介しないAPI Gatewayの構築なども可能です。
インフラ
Cognitoグループに紐づくIAM Roleを作成します。ここで作成した権限をサインインユーザーは引き受けることが可能です。そのため実行可能にしたいAPI Gatewayをリソース単位で指定しておきます。
以下はadmin
に関するコードのみですが、user
に関してもほぼ同様のコードになります。
(前略)
const adminOnlyApiGwResource = (
accountId: string,
apigwRestApiId: string,
): string[] => {
return [
`arn:aws:execute-api:ap-northeast-1:${accountId}:${apigwRestApiId}/v1/GET/admin`,
];
};
(中略)
// IdentityPoolからassumeできるIAM Role
const federatedPrincipal = new aws_iam.FederatedPrincipal(
"cognito-identity.amazonaws.com",
{
StringEquals: {
"cognito-identity.amazonaws.com:aud": params.idPool.idPoolId,
},
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "authenticated",
},
},
"sts:AssumeRoleWithWebIdentity",
);
const adminRole = new aws_iam.Role(this, "admin-role", {
roleName: `${prefix}-api-gateway-admin-role`,
assumedBy: federatedPrincipal,
inlinePolicies: {
executeApi: new aws_iam.PolicyDocument({
statements: [
new aws_iam.PolicyStatement({
effect: aws_iam.Effect.ALLOW,
resources: adminOnlyApiGwResource(
accountId,
params.idPool.apigwRestApiId,
),
actions: ["execute-api:Invoke"],
}),
],
}),
},
});
(後略)
Cognitoグループを作成し、上で作成したAdmin用のIAM Roleを紐付けます。これでサインインユーザーがadmin
という名前を含むCognitoグループに属している場合にIDプールを通してこのIAM Roleの一時キーを発行するという設定です。
(前略)
// CognitoのAdminグループ作成
new aws_cognito.CfnUserPoolGroup(this, "admin-group", {
userPoolId: userPool.userPoolId,
description: "description",
groupName: "admin",
precedence: 0,
roleArn: adminRole.roleArn,
});
const adminRMR: RoleMappingRule = {
claim: "cognito:groups",
claimValue: "admin",
mappedRole: adminRole,
matchType: RoleMappingMatchType.CONTAINS,
};
(後略)
最後にIDプールを作成します。上で作成したroleMappingRule
を指定しています。
(前略)
new IdentityPool(this, "identity-pool", {
identityPoolName: `${prefix}-identity-pool`,
allowUnauthenticatedIdentities: false,
authenticatedRole: userRole,
authenticationProviders: {
userPools: [
new UserPoolAuthenticationProvider({
userPool,
userPoolClient: privateClient,
}),
],
},
roleMappings: [
{
providerUrl: IdentityPoolProviderUrl.userPool(
`cognito-idp.ap-northeast-1.amazonaws.com/${userPool.userPoolId}:${privateClient.userPoolClientId}`,
),
useToken: false,
mappingKey: "userpool",
resolveAmbiguousRoles: false,
rules: [adminRMR, userRMR],
},
],
});
(後略)
フロントエンド
Next.jsのAPI Routesに以下の2つの関数を作成します。
1つ目の関数は、IdTokenからAWSのcredentialsを取得する関数です。この時に、サインイン中のユーザーが所属しているCognitoグループに紐づくIAM RoleからIDプールを使って一時クレデンシャルキーを取得します。
import type { NextApiRequest, NextApiResponse } from "next";
import axios from "axios";
import qs from "query-string";
import {
GetIdCommandInput,
GetIdCommand,
GetCredentialsForIdentityCommandInput,
GetCredentialsForIdentityCommandOutput,
GetCredentialsForIdentityCommand,
CognitoIdentityClient,
} from "@aws-sdk/client-cognito-identity";
import { SignatureV4 } from "@aws-sdk/signature-v4";
import { HttpRequest } from "@aws-sdk/protocol-http";
import { QueryParameterBag } from "@aws-sdk/types";
import { Sha256 } from "@aws-crypto/sha256-universal";
const getCredentialsFromIdToken = async (
idToken: string,
): Promise<GetCredentialsForIdentityCommandOutput> => {
const client = new CognitoIdentityClient({
region: process.env.COGNITO_REGION,
});
const loginsKey = `cognito-idp.${process.env.COGNITO_REGION}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}`;
const getIdCommandInput: GetIdCommandInput = {
AccountId: process.env.ACCOUNT_ID,
IdentityPoolId: process.env.COGNITO_IDENTITY_POOL_ID,
Logins: { [loginsKey]: idToken },
};
const identityId = await client.send(new GetIdCommand(getIdCommandInput));
const getCredentialsForIdentityCommandInput: GetCredentialsForIdentityCommandInput =
{
IdentityId: identityId.IdentityId,
Logins: { [loginsKey]: idToken },
};
return await client.send(
new GetCredentialsForIdentityCommand(getCredentialsForIdentityCommandInput),
);
};
(後略)
2つ目の関数は、取得したCredentialsからSigV4署名ヘッダを作成する関数です。この関数で得られたヘッダを付与してAPI Gatewayのエンドポイントへアクセスします。そうすることで、IAM認証をかけることができます。
(前略)
const getSignedHeaders = async (
credentials: GetCredentialsForIdentityCommandOutput,
apiUrl: URL,
query?: QueryParameterBag,
) => {
const signatureV4 = new SignatureV4({
service: "execute-api",
region: process.env.COGNITO_REGION,
credentials: {
accessKeyId: credentials.Credentials?.AccessKeyId || "",
secretAccessKey: credentials.Credentials?.SecretKey || "",
sessionToken: credentials.Credentials?.SessionToken || "",
},
sha256: Sha256,
});
console.log(`${apiUrl.hostname}, ${apiUrl.pathname}`);
const httpRequest = new HttpRequest({
headers: {
"content-type": "application/json",
host: apiUrl.hostname,
},
hostname: apiUrl.hostname,
method: "GET",
path: apiUrl.pathname,
query,
});
const signedRequest = await signatureV4.sign(httpRequest);
return signedRequest.headers;
};
(後略)
使い方としては、idToken
を付与してcredentials
を取得し、credentials
と実行したいAPI GatewayのURL
を指定するだけです。
呼び出しに必要な箇所だけを抜き出したコードは以下のようになっています。
import axios from "axios";
const credentials = await getCredentialsFromIdToken(
idToken,
);
const signedHeaders = await getSignedHeaders(
credentials,
new URL(`${process.env.BACKEND_API_ENDPOINT}/v1/user`),
);
const BackendApiClient = axios.create({
baseURL: `${process.env.BACKEND_API_ENDPOINT}/v1`,
});
const options = {
method: "GET",
headers: signedHeaders,
url: "/user",
};
const result = await BackendApiClient(options)
動作確認
Cognitoでadmin@example.com
, user@example.com
のユーザーをコンソールから作成します。その後に、それぞれのユーザーでログインして各種APIを実行してみます。
admin
ユーザーから試してみます。わかりにくいですが、/admin
へのアクセスは成功し、/user
へは失敗しています。
adminで/adminへアクセス:成功
adminで/userへアクセス:失敗
次にuser
ユーザーを試してみます。わかりにくいですが、/admin
へのアクセスは失敗し、/user
へは成功しています。
userで/adminへアクセス:失敗
userで/userへアクセス:成功
以下のリポジトリからも、Cognito/API Gatewayをデプロイすれば実行・確認できます。
おわりに
CognitoのIDプールを使って、IAM制限されたAPI Gatewayへアクセスしてみました。誰かの参考になれば幸いです。
参考記事
Discussion