1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

cdkv2を使ってLambdaオーソライザ―を作ってHTTPAPIでcognitoのグループを使った認可を試したメモ

Last updated at Posted at 2022-08-07

概要

前回は、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を作成する。
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ユーザープールでグループ毎にリソース制御を行う

aws doc グループ

参考

manage-cognito-users-with-aws-cli

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?