ApiGatewayのアクセス制御をしたい場合は、Cognitoかlambdaオーソライザーを使うことで実現できます。公式にもドキュメントがありますが、ざっくりしたした理解だと図のような感じです。
ApiGatewayのアクセス制御は例えばログインしたユーザーしかapiを呼ぶことできないようにしたい場合などに使用します。このような場合、簡単な実装方法はCognitoを使用することです。
使用しているCognitoのUserPoolを指定するだけで実装できちゃいます。AWS CDKなどを使用している場合は以下のような感じで実装できます。
// CDK(Typescript)の場合
const userPool = new cognito.UserPool(this, 'UserPool');
const auth = new apigateway.CognitoUserPoolsAuthorizer(this, 'booksAuthorizer', {
cognitoUserPools: [userPool]
});
declare const books: apigateway.Resource;
books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), {
authorizer: auth,
authorizationType: apigateway.AuthorizationType.COGNITO,
});
Cognitoは認証に成功するとフロントにトークンを返し、そのトークンを使ってApiGatewayにアクセスすることができます。ただし、この方法には問題があります。
CookieにHttpOnly属性
を設定できないということです。
実装する要件によっては、CookieにHttpOnly属性
を設定したい場合があります。このほかにもカスタムで何かを設定したい場合があってもAuthorizationType.COGNITO
だと難しいです。その場合はlambdaオーソライザーを使用することで実現できます。
lambdaオーソライザーと言っても実際にはlambda関数を使用してアクセス制御を行うことです。なのでかなり柔軟にアクセス制御を行うことができます。今回は認証にはCognitoを使用し、アクセス制御にlambdaオーソライザーを使用する方法を紹介します。
今回の要件は認証を受けたapiを呼び出すことです。まずはapi gatewayとlambdaを紐付けます。
lambdaはapi gatewayを呼び出すためその権限を持っている必要があります。
そして、今回はCookieにトークンをセットするので、method.request.header.Cookie
をidentitySourceに指定しています。
CookieにはhttpOnly属性
を設定するためフロントでCookieのトークンを取得することはできないのでapi gatewayにヘッダーにCookieをセットする必要があります。api gatewayのrequestParameters
に"integration.request.header.Cookie": "method.request.header.Cookie",
を設定します。
これで紐付けされたlambdaオーソライザーにCookieが渡されるので、あとはlambda関数で認証を行い、アクセス制御を行います。
// CDK(Typescript)の場合
// lambdaAuthorizerのロールを作成
const lambdaAuthorizerRole = new Role(this, "LambdaAuthorizerRole", {
roleName: LambdaAuthorizerRole,
assumedBy: new ServicePrincipal("apigateway.amazonaws.com"),
description: Lambda Authorizer Role,
});
// lambdaAuhorizerを設定
const lambdaAuthorizer = new TokenAuthorizer(this, "LambdaAuthorizer", {
handler: authorizerFunc,←lambda関数
identitySource: "method.request.header.Cookie",
assumeRole: lambdaAuthorizerRole,
});
// このようにlambdaAuthorizerを指定することでapi gatewayにアクセス制御をかけることができる
ApiEndPoint.addMethod(
"POST",
new LambdaIntegration(Lambda),
{
authorizer: lambdaAuthorizer,
authorizationType: AuthorizationType.CUSTOM,
}
);
これでapi gatewayとlambdaが紐付けられました。次はlambda関数を作成します。
lambda関数は以下のような感じで実装できます。ここではGetUserCommand
を使用してユーザー情報を取得しています。
GetUserCommand
のparamsにはAccessToken
が必要です。単純にAccessTokenだけをGetUserCommand
に渡して終わりみたいな処理をしてしまっていると他で発行されてたAccessTokenでもこの認証を通り抜けてしまうのでセキュリティ上よろしくないです。
なので、きちんとAccessTokenを検証する必要があります。有名どころのjsonwebtokenを使用する方法もありますが、ここではAWSLabsが提供しているaws-jwt-verifyを使用しています。
import {
CognitoIdentityProvider,
GetUserCommand,
GetUserCommandInput,
GetUserCommandOutput,
} from "@aws-sdk/client-cognito-identity-provider";
import { CognitoJwtVerifier } from "aws-jwt-verify";
import { apiGatewayTokenAuthorizerEvent } from "aws-lambda";
async function handler(event: apiGatewayTokenAuthorizerEvent) {
// Get the cookies
const cookies = event.authorizationToken || "";
const userPoolId = process.env.USER_POOL_ID;
const clientId = process.env.CLIENT_ID;
const client = new CognitoIdentityProvider({});
// accessTokenを取得
const accessToken = cookies
.split(";")
.find((row) => row.trim().startsWith("accessToken"))
?.split("=")[1];
// 値の取得ができなかった場合
if (!accessToken || !userPoolId || !clientId) {
return {
principalId: "user",
policyDocument: {
Version: "2012-10-17",
Statement: [
{
Action: "execute-api:Invoke",
Effect: "Deny",
Resource: "*",
},
],
},
};
}
try {
// accessTokenの検証
const verify = CognitoJwtVerifier.create({
userPoolId: userPoolId,
tokenUse: "access",
clientId: clientId,
});
const payload = await verify.verify(accessToken);
console.log("Token is valid. Payload:", payload);
// ユーザー情報の取得
const params: GetUserCommandInput = {
AccessToken: accessToken,
};
const command = new GetUserCommand(params);
const response: GetUserCommandOutput = await client.send(command);
const emailAttribute = response.UserAttributes?.find(
({ Name }) => Name === "email",
);
const email = emailAttribute?.Value;
return {
principalId: "user",
policyDocument: {
Version: "2012-10-17",
Statement: [
{
Action: "execute-api:Invoke",
Effect: "Allow",
Resource: "*",
},
],
},
context: {
email: email,
},
};
} catch (error) {
console.error("Error:", error);
return {
principalId: "user",
policyDocument: {
Version: "2012-10-17",
Statement: [
{
Action: "execute-api:Invoke",
Effect: "Deny",
Resource: "*",
},
],
},
};
}
}
export { handler };
今回のケースはlambdaオーソライザーでcognitoのGetUserCommand
を使用していますが、lambdaオーソライザーにはGetUserCommand
の権限は付与していません。ですが、GetUserCommand
は問題なく動作します。
なぜなら、GetUserCammand
はAccessTokenのパラメータからユーザーを取得するだけで、特にUserPoolなどの情報や認証情報は不要です。
最初はなんで動いているのか不思議で、アレコレIAMロールの設定を見たりしてました。
要はAccessTokenはきちんと検証しておきましょうということです。
lambdaオーソライザーで認証が通った場合は以下のように返してますが、Resource
には*
を指定しています。
return {
principalId: "user",
policyDocument: {
Version: "2012-10-17",
Statement: [
{
Action: "execute-api:Invoke",
Effect: "Allow",
Resource: "*",
},
],
},
context: {
email: email,
},
};
最初は、api GatewayのARN
を指定する必要があるのではないかと思い、api GatewayのARN
を指定していましたがベストプラクティスに従い最小限のポリシーを付与しようと思い、テストで誤ったapi GatewayのARN
を指定してみましたが、あら不思議、動作しました。
あれ、これってセキュリティ的に問題なのでは?と思い。また、アレコレ見てましたが、原因がわかりました。
lambdaオーソライザーとapi gatewayを紐づけるときにapiを指定しているので、Resource
に誤ったapi GatewayのARN
を指定しても動作するのです。
ApiEndPoint.addMethod(
"POST",
new LambdaIntegration(Lambda),
{
authorizer: lambdaAuthorizer,
authorizationType: AuthorizationType.CUSTOM,
}
);
よくよく考えれば、この記述だと指定したapiに対してのみauthorizer
が適用されるので、Resource
に*
を指定しても問題ないですね。