概要
前回は、CognitoのJWTトークンによる認証を使い、ログインができるようになった。
今回は、Cognitoのグループ機能を使い、取得できるAPIの認可を与えることを試す。
環境
- Windows10
- gitbash
- aws-cli/2.7.21 Python/3.9.11 Windows/10 exe/AMD64 prompt/off
- cdk v2.35
- node v16.16.0
準備
cognitoのグループを作成し、グループに前回作成したユーザを追加する。
今回はgroup_1
を作成した。
#!/bin/bash
groupName=${1:-'group0'}
COGNITO_USER_POOL_ID=ap-northeast-1_xxx
# https://docs.aws.amazon.com/cli/latest/reference/cognito-idp/index.html#cli-aws-cognito-idp
aws cognito-idp create-group \
--group-name $groupName \
--user-pool-id $COGNITO_USER_POOL_ID \
#!/bin/bash
groupName=${1:-'group0'}
userName=${2:-'user@example.com'}
COGNITO_USER_POOL_ID=ap-northeast-1_xxx
aws cognito-idp admin-add-user-to-group \
--group-name $groupName \
--user-pool-id $COGNITO_USER_POOL_ID \
--username $userName
実装
CDK
- 認可用のlambdaを作成する。
- 形式はv2 シンプルとする。
cdk/lib/LambdaWithCognitoStack.ts
// cognitoグループを使った認可
const verifyGroup1LambdaAuthHandler = new NodejsFunction(
this,
`verify-group-1-lambda`,
{
runtime: lambda.Runtime.NODEJS_14_X,
entry: '../src/handler/authorizer/apiGatewayV2SimpleAuthorizer.ts',
functionName: 'apiGatewayV2SimpleAuthorizer',
description: 'Cognitoのグループをみた認可',
environment: {
COGNITO_UER_POOL_ID: userPool.userPoolId,
COGNITO_CLIENT_ID: userPoolClient.userPoolClientId,
COGNITO_USER_GROUP: 'group_1'
}
},
)
const lambdaAuthorizerOptions = {
responseTypes: [authz.HttpLambdaResponseType.SIMPLE]
}
const lambdaAuthorizer = new authz.HttpLambdaAuthorizer(
`${props.projectId}-lambdaAuthorizer`,
verifyGroup1LambdaAuthHandler,
lambdaAuthorizerOptions,
)
httpApi.addRoutes({
methods: [apigw.HttpMethod.GET],
path: '/group1-hello',
integration: new intg.HttpLambdaIntegration(
`${props.projectId}-group1-hello`,
handlerWithLambda,
),
authorizer: lambdaAuthorizer,
})
認可用Lambda
サンプルの通りに作成。
グループは環境変数で指定するようにした。
src/handler/authorizer/apiGatewayV2SimpleAuthorizer.ts
import { APIGatewayRequestSimpleAuthorizerHandlerV2WithContext } from 'aws-lambda';
import { CognitoJwtVerifier } from 'aws-jwt-verify';
const envList = [
'COGNITO_UER_POOL_ID',
'COGNITO_CLIENT_ID',
'COGNITO_USER_GROUP'
] as const
for (const key of envList) {
if (!process.env[key]) throw new Error(`environment missing. please add ${key} to environmenet`)
}
const processEnv = process.env as Record<typeof envList[number], string>
const verifier = CognitoJwtVerifier.create({
userPoolId: processEnv.COGNITO_UER_POOL_ID, // mandatory, can't be overridden upon calling verify
tokenUse: "id", // needs to be specified here or upon calling verify
clientId: processEnv.COGNITO_CLIENT_ID, // needs to be specified here or upon calling verify
groups: processEnv.COGNITO_USER_GROUP, // optional
});
export type AuthorizedCognitoContext = { cognitoUsername: string, email: string, congnitoUserId: string }
type AuthorizedCognitoContextAllOptional = Partial<AuthorizedCognitoContext>
export const handler: APIGatewayRequestSimpleAuthorizerHandlerV2WithContext<AuthorizedCognitoContextAllOptional> = async (event) => {
console.log("request:", JSON.stringify(event, undefined, 2));
const ret = {
isAuthorized: false,
context: {}
};
if (!event.headers?.authorization) return ret;
const jwt = event.headers.authorization;
let payload = null
try {
payload = await verifier.verify(jwt);
console.log("Access allowed. JWT payload:");
return {
isAuthorized: true,
context: {
cognitoUsername: `${payload['cognito:username']}`,
email: `${payload.email}`,
congnitoUserId: `${payload.sub}`
}
};
} catch (err) {
console.error("Access forbidden:", err);
}
return ret;
};
認証後のLambda
- authorizerのcontextで渡した値
context:{xxx:値}
は、event.requestContext.authorizer.xxx
から取得可能
import { APIGatewayProxyHandlerV2WithLambdaAuthorizer } from 'aws-lambda';
import { format } from 'date-fns';
import { formatDate } from '@/common/index';
import { corsHeaders } from '@/domain/http/const';
import { AuthorizedCognitoContext } from '../authorizer/apiGatewayV2SimpleAuthorizer';
export const handler: APIGatewayProxyHandlerV2WithLambdaAuthorizer<AuthorizedCognitoContext> = async (event) => {
console.log(`test:event -> ${JSON.stringify(event)}`)
const datetime = formatDate(new Date(event.requestContext.timeEpoch));
return {
'statusCode': 200,
headers: corsHeaders,
'body': JSON.stringify({
message: `hello world ${event.requestContext.authorizer.lambda.cognitoUsername}. ${datetime} = ${event.requestContext.timeEpoch}. now: ${format(new Date(), 'yyyy-MM-dd HH:mm:ss.SSS')}`,
})
};
};
クライアント側
- グループを追加し、認可の判定をできるようにする
client/src/store/slices/auth.ts
import {
createAsyncThunk,
createSlice,
SerializedError,
} from '@reduxjs/toolkit'
import { Auth } from 'aws-amplify'
interface AuthState {
username?: string
authenticated: boolean
error?: SerializedError
+ groups: string[]
}
const initialState: AuthState = {
username: undefined,
authenticated: false,
error: undefined,
+ groups: [],
}
+ type IdTokenPayload = {
+ aud: string //"17e4qspqct1l2cuh7s73h13e7s"
+ auth_time: number
+ event_id: string //'a778712b-1e5d-475c-9ab3-edae87faad0c'
+ exp: number
+ iat: number
+ iss: string // 'https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_xxxxxx'
+ jti: string //'2284a3cf-1f21-42fc-943e-3837dfbf24e4'
+ origin_jti: string // 'b31b301f-1df4-1aaa-a01b-33270cf3b839'
+ sub: string // '15949eae-bc0c-459a-af0c-3eca21b4226e'
+ token_use: string // 'id'
+ email: string
+ 'cognito:username': string // '15949eae-bc0c-459a-af0c-3eca21b4226e'
+ 'cognito:groups'?: string[]
+ }
+ const getGropus = async () => {
+ const session = await Auth.currentSession()
+ const token = session.getIdToken()
+ const payload = token.payload as IdTokenPayload
+ const groups = payload['cognito:groups']
+ return groups || []
+ }
export const initUser = createAsyncThunk<AuthState>(
'initUser',
async (req, thunkAPI) => {
try {
const result = await Auth.currentAuthenticatedUser()
+ const groups = await getGropus()
return { username: result.username, groups, authenticated: true }
} catch (error: any) {
return thunkAPI.rejectWithValue({ error: error.message })
}
},
)
export const signIn = createAsyncThunk<
AuthState,
{ username: string; password: string }
>('signIn', async ({ username, password }, thunkAPI) => {
try {
const result = await Auth.signIn(username, password)
+ const groups = await getGropus()
return { username: result.username, groups, authenticated: true }
} catch (error: any) {
return thunkAPI.rejectWithValue({ error: error.message })
}
})
export const signOut = createAsyncThunk('logout', async (_, thunkAPI) => {
try {
await Auth.signOut()
} catch (error: any) {
return thunkAPI.rejectWithValue({ error: error.message })
}
})
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(signIn.fulfilled, (state, action) => {
state.authenticated = true
state.username = action.payload.username
+ state.groups = action.payload.groups
})
builder.addCase(signIn.rejected, (state, action) => {
return { ...initialState, error: action.error }
})
builder.addCase(signOut.fulfilled, () => {
return { ...initialState }
})
builder.addCase(signOut.rejected, (state, action) => {
return { ...initialState, error: action.error }
})
builder.addCase(initUser.fulfilled, (state, action) => {
state.authenticated = true
state.username = action.payload.username
+ state.groups = action.payload.groups
})
},
})
参照
Amplify SDKで、ログイン中ユーザーのCognito User PoolsでのGroupを取得する
Cognitoのユーザーグループを使ったAPI GatewayのエンドポイントへのRBAC ... 解決法1を採用(github)
lambda認証を使用する
API Gateway の Lambda オーソライザーをやってみた
調査
v1とv2でオーソライザの返すべき値がことなるので注意
Lambdaオーソライザーのトークンベースとリクエストパラメータベースの挙動を比べて、どちらを選択するべきか考えてみた
Cognitoユーザープールグループの扱い方・IAMロールについてを解説します
Cognitoユーザープールでグループ毎にリソース制御を行う