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

## 概要
[前回](https://qiita.com/hibohiboo/items/5d20423ec6ba96e0f521)は、CognitoのJWTトークンによる認証を使い、ログインができるようになった。
今回は、Cognitoのグループ機能を使い、取得できるAPIの認可を与えることを試す。

![](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/images/custom-auth-workflow.png)

[ソースコード](https://github.com/hibohiboo/aws-cdk-v2/tree/id/6)

## 環境

+ 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`を作成した。

```bash
#!/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 \
```

```bash
#!/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 シンプル](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-lambda-authorizer.html)とする。

```ts: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
[サンプル](https://github.com/awslabs/aws-jwt-verify#api-gateway-lambda-authorizer---rest)の通りに作成。
グループは環境変数で指定するようにした。

```ts: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`から取得可能

```ts:
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')}`,
    })
  };
};
```

### クライアント側
* グループを追加し、認可の判定をできるようにする

```diff: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を取得する](https://wp-kyoto.net/get-cognito-user-pools-user-group-from-amplify-js/)
[Cognitoのユーザーグループを使ったAPI GatewayのエンドポイントへのRBAC](https://fogsbane.dev/ja/q/62bd3a460237b3fd258dc28d4547a5ca8f50485d04a9dfe168b674f40c4edc78) ... 解決法1を採用([github](https://github.com/awslabs/aws-jwt-verify#api-gateway-lambda-authorizer---rest))
[lambda認証を使用する](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html)
[API Gateway の Lambda オーソライザーをやってみた](https://qiita.com/sugimount-a/items/0079a79b94e442204d6f)

## 調査
[v1とv2でオーソライザの返すべき値がことなるので注意](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-lambda-authorizer.html)
[Lambdaオーソライザーのトークンベースとリクエストパラメータベースの挙動を比べて、どちらを選択するべきか考えてみた](https://dev.classmethod.jp/articles/lambda-authorizer-toke-request/)
[Cognitoユーザープールグループの扱い方・IAMロールについてを解説します](https://www.ragate.co.jp/blog/articles/4432)

[Cognitoユーザープールでグループ毎にリソース制御を行う](https://tech.opst.co.jp/2021/08/23/cognito%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%83%97%E3%83%BC%E3%83%AB%E3%81%A7%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E6%AF%8E%E3%81%AB%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E5%88%B6%E5%BE%A1%E3%82%92/)

[aws doc グループ](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-user-groups.html)


## 参考
[manage-cognito-users-with-aws-cli](https://mseeeen.msen.jp/manage-cognito-users-with-aws-cli/)