AmazonAPIGatewayのLambdaAuthorizer(Node.js)でJWTを検証する(LambdaをServerlessFrameworkでデプロイする)

# はじめに

AWSでAPIを構築する際、必ずといっていいほど登場するのがAmazonAPIGatewayですよね。
APIGatewayの認証ってどうされてますか？

* 別に必要ないからしてない 
* APIKeyを使っている
* CognitoをAuthorizerにしてトークンの検証をする
* LambdaAuthorizerでトークンの検証をする

など色々なパターンがあると思います。今回は最後に書いた「LambdaAuthorizerでトークンの検証をする」について説明して、その構成をServerlessFrameworkでデプロイするところまでを書いていきます。

## 登場人物

### JSON Web Token (JWT)

JSONをベースとしたトークン。
「ヘッダー / ペイロード / 署名」といった3つのセクションから構成されていて、署名はヘッダーとペイロードのハッシュされた組み合わせです。
`hogehoge.foofoofoo.barbarbarbar` のような文字列です。

### ServerlessFramework

Serverlessなアーキテクチャを構築するためのフレームワーク。AWSだけじゃなくAzureやGCPなど他のクラウドサービスでも利用することができる。
詳しくは [公式ページ](https://serverless.com/) で確認してみてください。

## 構成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95875/649cdaa2-83fc-70b7-eaa1-48effd6d071a.png)

## Lambda Authorizer

JWTの検証をLambda内で行っていきます。
今回はNode.js(TypeScript)での例を書いています。

```typescript:authorizer.ts
import { Handler, Context, Callback, CustomAuthorizerEvent } from "aws-lambda"
import * as jsonwebtoken from "jsonwebtoken"
import jwkToPem from "jwk-to-pem"
import jwk from "./jwk"
const pem = jwkToPem(jwk as any)

export const handler: Handler = async (
  event: CustomAuthorizerEvent,
  _context: Context,
  callback: Callback
): Promise<any> => {
  console.log(JSON.stringify(event))

  jsonwebtoken.verify(
    event.authorizationToken,
    pem,
    {
      algorithms: ["RS256"]
    },
    (err: jsonwebtoken.VerifyErrors, decodedToken: object | string) => {
      if (err) {
        callback(null, {
          principalId: 1,
          policyDocument: {
            Version: "2012-10-17",
            Statement: [
              {
                Action: "execute-api:Invoke",
                Effect: "Deny",
                Resource: event.methodArn
              }
            ]
          },
          context: {
            messagge: "Custom Error Message"
          }
        })
      } else {
        console.log(decodedToken)
        callback(null, {
          principalId: 1,
          policyDocument: {
            Version: "2012-10-17",
            Statement: [
              {
                Action: "execute-api:Invoke",
                Effect: "Allow",
                Resource: event.methodArn
              }
            ]
          },
          context: {
            messagge: "Custom Allow Message"
          }
        })
      }
    }
  )
}
```

### 解説

#### JWT検証で使うモジュールをインストール

```terminal
$ yarn add jsonwebtoken jwk-to-pem
$ yarn add -D @types/jsonwebtoken @types/jwk-to-pem
```

`jsonwebtoken` がJWTトークンの検証をするライブラリです。
`jwt-to-pem` はJWTトークンを検証するための JSON Web Key (JWK) をpem形式に変換するライブラリです。

#### JWT検証

`jsonwebtoken` の `verify` というメソッドを使って検証をします。

```typescript
jsonwebtoken.verify(
  event.authorizationToken, //  requestHeader内のToken
  pem, // JWKから生成したpem
  { algorithms: ["RS256"] },
  (err: jsonwebtoken.VerifyErrors, decodedToken: object | string) =>  {}
)
```

#### ( Cognitoを使っている場合のJWK取得 )

Cognitoが発行したJWTの検証に使うJWKは `https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json` から取得する事ができます。
そのJSONを何かしらの方法でimportしてください。今回の例はTSファイルにしてimportしています。
また上記のURLからJWKを取得すると2つKeyが入っていますが、最初の一つを利用すればいいです。

```typescript:jwk.ts
export default {
  alg: "RS256",
  e: "XXXX",
  kid: "xxxxxxxxxxxxxx",
  kty: "RSA",
  n: "xxxxxxxxxxxxxx",
  use: "sig"
}
```

#### 検証後の処理

CallbackでAPIGatewayにレスポンスを返して行きます。
Statement内のEffectが `Allow` か `Deny` かでその後の処理を判断させます。

```typescript
// 成功時
callback(null, {
  principalId: 1,
  policyDocument: {
    Version: "2012-10-17",
    Statement: [
      {
        Action: "execute-api:Invoke",
        Effect: "Allow",
        Resource: event.methodArn
      }
    ]
  },
  context: {
    messagge: "Custom Allow Message"
  }
})

// 失敗時
callback(null, {
  principalId: 1,
  policyDocument: {
    Version: "2012-10-17",
    Statement: [
      {
        Action: "execute-api:Invoke",
        Effect: "Deny",
        Resource: event.methodArn
      }
    ]
  },
  context: {
    messagge: "Custom Error Message"
  }
})
```

## ServerlessFrameworkでデプロイする

### プロジェクト作成

LambdaのソースコードをTypeScriptで書いているので、TypeScript用のテンプレートを使ってプロジェクトを作成します。

```terminal
$ npx -p serverless sls create -t aws-nodejs-typescript -p example-serverless-ts
```

### serverless.yml

AuthorizerをつけたいAPIGatewayをイベントとするfunctionの中に `authorizer` を追加していきます。

* `name` はAuthorizerとなるLambdaです。同じyml内で定義している場合はこの `name` で参照することができます。外部でデプロイされているLambdaをAuthorizerにしたい場合は、 `arn` に対してそのLambdaのARNを指定します。
* `identitySource` はTokenをどこを参照すれば習得できるのかという設定です。この場合はリクエストヘッダー内の `Authorization` を参照するようになっています。
* `type` はAuthorizerとなるLambdaのeventに渡される値の設定です。単純にTokenを取得したいだけなら `token` がおすすめです。リクエストの内容をすべて取得したい場合は `request` を指定します。

```yml
authorizer:
  name: authorizer
  identitySource: method.request.header.Authorization
  type: token
```

全体としてはこんな感じになります。

```yml:serverless.yml
service:
  name: lambdda-node-ts

plugins:
  - serverless-webpack

provider:
  name: aws
  runtime: nodejs10.x
  region: ap-northeast-1

functions:
  authorizer:
    handler: authorizer.handler
  index:
    handler: index.handler
    events:
      - http:
          method: get
          path: hello
          cors: true
          authorizer:
            name: authorizer
            identitySource: method.request.header.Authorization
            type: token
```

あとは `sls deploy` でデプロイすれば作業は完了です。

## さいごに

今回はLambdaAuthorizerを紹介しました。
トークンの検証にLamdbaを使うことで、トークンの検証以外にもAPIに対するリクエストのログを取ることができたり、怪しいリクエストが来た場合にメール送信などカスタマイズすることができます。
Lambdaを挟むことでかなりできることの幅が広がります！
ではまた！！