LambdaでCognito認証(ユーザー認可)

#はじめに

SDKをローカルに持ってきてゴニョるサンプルは検索に引っかかるのですが、
クラウド側(Lambda関数内部)で完結するサンプルが見つからない...
よし、ならば投稿してしまえ。

[トップ](https://qiita.com/minmax/items/a36b081c073eff4a6533)
├[ユーザー作成](https://qiita.com/minmax/items/8c2aa57b76e09b8192ed)
├[ユーザー確認](https://qiita.com/minmax/items/ed4f81e61c2617d5c6cf)
├[ユーザー認証](https://qiita.com/minmax/items/12e65cd51d85f419faa5)
└__ユーザー認可 ←イマココ__

#注意事項
本稿は、[こちらの記事](https://aws.amazon.com/jp/premiumsupport/knowledge-center/decode-verify-cognito-json-token/)をLambda関数で実現することを目的としています。
**兄弟記事とは毛色が違いますので、予めご了承ください。**

+ [Decode and verify Amazon Cognito JWT tokens (Amazon Cognito JWT トークンを復号して検証する)](https://github.com/awslabs/aws-support-tools/tree/master/Cognito/decode-verify-jwt)を、JavaScriptで改変したプログラムを使用します。
+ npmを使用してライブラリを入手し、Lambdaにアップロードする手順が含まれます。
+ Lambda関数でAPI Gateway用のオーソライザーの作成方法を解説する記事ではありません。
+ API Gatewayでオーソライザーの設定方法を解説する記事でもありません。[→こちらをご覧ください。](https://qiita.com/minmax/items/f8cbd00da12960678d7b)


#ユーザー認可 (Authorization)
通知されたIDトークンが有効かどうかを判断します。
![認可のパターン](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546599/bfecfd26-53d7-eedc-4b9e-ea09b2ce0691.png)
通常、ユーザー認可はAPI Gatewayのオーソライザーに任せればいいのですが、諸事情によりAPI Gatewayに任せられない場合は、Lambdaに担当させる必要があります。

本稿では、Lambda関数でユーザー認可する方法を解説します。

##ドキュメント
__AWSJavaScriptSDKは使用しません。__
[JSON ウェブトークンの検証](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html)

##npm
ユーザー認可に使用する外部ライブラリを入手します。

###外部ライブラリ
npmで入手します。
入手した外部ライブラリはLambdaにアップロードします。

```
npm i jwk-to-pem --save
npm i jsonwebtoken --save
```

##Cognito
Cognitoのユーザープールから、ユーザー認可に使用する「公開キー」と「アプリクライアントID」を入手します。

###公開キー
公開キーはこちらにあります。
https://cognito-idp.{リージョン}.amazonaws.com/{プールID}/.well-known/jwks.json
![JWT](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546599/1098cbd5-19e3-a939-0e5e-e54b5bae8b1a.png)
プールIDは、Cognitoのユーザープールの「全般設定」のページから確認できます。
![プールID.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546599/bf1073e3-518a-c725-2827-b953adb2905f.png)

###アプリクライアントID
アプリクライアントIDは、Cognitoのユーザープールの「全般設定>アプリクライアント」のページから確認できます。
![アプリクライアントID](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546599/1e0344f6-cc8b-b8ce-7e27-0eb8ec23e3ad.png)


##Lambda
関数を一から作成します。

###環境設定
最終的には次のような環境にします。
![lambda環境設定_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546599/11ff10a3-6e8d-ad7f-1b75-a11e087504ab.png)

###ソースコード
↓適当な例外をthrowしています。
アプリクライアントIDや発行者(issuer)の文字列は、Lambdaの環境変数に設定すると、プログラムの見通しが良くなると思います。

```javascript:authorizer.js
'use strict';

// ライブラリ
const jwkToPem = require('./node_modules/jwk-to-pem/src/jwk-to-pem.js');
const jsonwebtoken = require('./node_modules/jsonwebtoken');
// 公開キー。 https://cognito-idp.{リージョン}.amazonaws.com/{プールID}/.well-known/jwks.json と同じもの
const jwks = require('./jwks.json');
// JWT形式判定用の正規表現
const jwtRe = /(?<header>.+)\.(?<payload>.+)\.(?<signature>.+)/;

/**
 * オーソライザー (トークンを検証する)
 * @param {string} token IDトークン
 * @returns {Object} ペイロード
 */
module.exports = token => {

  // 入力はJWT形式の文字列か?
  const match = jwtRe.exec(token);
  if (!match) throw 'AuthError';

  // ヘッダー情報を取得
  const header = JSON.parse(Buffer.from(match.groups.header, 'base64').toString());

  // 公開キーから使用するキーを選ぶ
  const jwk = jwks.keys.find(k => k.kid == header.kid);
  if (!jwk) throw 'AuthError';

  // 外部ライブラリを使って署名を確認する
  const pem = jwkToPem(jwk);
  const claim = jsonwebtoken.verify(token, pem);

  // 有効期限の確認
  const now = new Date() / 1000;
  if (now > claim.exp || now < claim.auth_time) throw 'AuthError';

  // 利用者(audience)の確認
  if (claim.aud !== '{アプリクライアントID}')  throw 'AuthError';

  // 発行者(issuer)の確認
  if (claim.iss !== 'https://cognito-idp.{リージョン}.amazonaws.com/{プールID}') throw 'AuthError';

  // トークン種別の確認
  // IDトークンを使う場合の claim.token_use は 'id'
  // アクセストークンを使う場合の claim.token_use は 'access'
  if (claim.token_use !== 'id') throw 'AuthError';

  // ハレて認可された
  return claim;
};
```

↓例外をcatchしていませんが、そのあたりを含めて適当に改変してください。

```javascript:index.js
'use strict';

// オーソライザー
const authorizer = require('./authorizer.js');

/**
 * メイン
 * @param {Object} event プロキシ統合の情報
 * @returns {Promise<HTTPResponse>} HTTPレスポンス
 */
exports.handler = async event => {

  // event情報からIDトークンを取得
  const token = event.headers['Authorization']; // HTTPヘッダーのAuthorizationにIDトークンがある場合の例

  // IDトークンをオーソライザーに投入
  const claim = authorizer(token);

  // 例外が発生しなければ認可成功

  // HTTPレスポンスを返して終了
  return {
    statusCode: 200,
    body: 'Authorization succeeded.',
  };
};
```