AWS Cognitoにサインインしないと見れないLambdaを作る 

前回は、AWS Cognitoを使って、GoogleアカウントとLINEアカウントからログインできるようにCognitoユーザープールを設定しました。
[AWS CognitoにGoogleとLINEアカウントを連携させる](https://qiita.com/poruruba/items/189945dc64edfe1f2464)

今度は、サインイン状態をAWS API Gateway経由でLambdaに引き継いでみます。
つまりは、サインインしていないと表示できないページを作ります。


#とりあえずのLambda関数を作成

とりあえず、Lambdaを作成してから、後で、それを呼び出すようにAPI Gatewayのエンドポイントを作成します。

Lambdaの管理コンソールを開きます。
https://ap-northeast-1.console.aws.amazon.com/lambda/home?region=ap-northeast-1

右上の「関数の作成」ボタンを押下します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/f72950a4-254f-f282-5297-cbc9eaaf22cc.png)

今回は、ほぼ何も処理をしないシンプルな関数を用意します。
「一から作成」を選択し、適当な名前を入力します。例えば、「test-cognito-lambda」とします。
ランタイムは、Node.js 8.10 としました。
ロールは既存のロールを選択してもよいですし、新規に作成してもよいです。
例えば、ロールとして「テンプレートから新しいロールを作成」とし、ポリシーテンプレートは「シンプルなマイクロサービスのアクセス権限」を選択します。必要最低限のアクセス権限といえます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/200be8c4-2efb-e5c6-5f22-b54fd2b29732.png)

それでは非常に単純なLambda関数を作ってみます。
API Gatewayからは、Lambda関数のexports.handlerに、引数(event, context, callback)が付いた形で呼び出されます。
今回の実装は、単純に引き渡されるeventとcontextの中身をそのままクライアントに返すようにしてみます。
以下のようなコードにしてください。

```js
exports.handler = async (event, context, callback) => {
    console.log('Lambda called OK!!');

    const response = new Response();
    response.set_body({
        "event" : event,
        "context" : context
    });
    callback(null, response);
};

class Response{
    constructor(){
        this.statusCode = 200;
        this.headers = {'Access-Control-Allow-Origin' : '*', 'Content-Type' : 'application/json; charset=utf-8'};
        this.body = "";
    }

    set_error(error){
        this.body = JSON.stringify({"err": error});
    }

    set_body(content){
        this.body = JSON.stringify(content);        
    }
    
    get_body(){
        return JSON.parse(this.body);
    }
}
```

# API Gateway関数の作成

外部からWeb API呼び出しを受け付けるAPI Gatewayのエンドポイントを作成します。
API Gatewayの管理コンソールを開きます。

https://ap-northeast-1.console.aws.amazon.com/apigateway/home?region=ap-northeast-1

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/e97a5e59-3b8f-1b58-38ac-ce5d07e49e01.png)

「APIの作成」ボタンを押下します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/bd0453be-5110-5a7d-be2c-05ddf853bd3c.png)

適当な名前を入力します。この名前は、外部に公開するWeb APIのURL名には関係ないので、本当に適当で大丈夫です。
例えば、「新しいAPI」、API名として「TestApi」とします。

まずは、特に階層は作らず、ルートにメソッド「GET」を作成します。もちろんPOSTでも構いません。
チェックマークの小さな丸いボタンを押さないと、適用されないので、忘れずに押します。
Lambdaにつなぐので、統合タイプに「Lambda関数」、Lambdaプロキシ統合の使用のチェックをOn、Lambda関数に先ほど作成した「test-cognito-lambda」を入力します。
Lambdaプロキシの統合は、Lambdaを使うのであれば、Onにしたほうが楽ちんです。
ついでに「アクション」→「CORSの有効化」を選択して、以降の選択ではデフォルトのまま進めます。
以上で、ルートの下に、「GET」と「OPTIONS」ができているかと思います。

最後に、デプロイすることで、このWeb APIが外部に公開されます。
「アクション」→「APIのデプロイ」を選択し、デプロイされるステージとして[新しいステージ]を選択し、ステージ名を例えば「prod」としてデプロイを行います。

デプロイが完了するとステージの画面に自動的に切り替わります。
青帯で囲まれた「URLの呼び出し」にあるURLがこのAPI GatewayのAPIにつながっています。

> https://[RestApi-Id].execute-api.ap-northeast-1.amazonaws.com/[ステージ名]

では早速、上記URLをブラウザで表示しみます。
たくさんの要素を持ったJSONが表示されました。
最上位は2つの要素からなっており、eventとcontextです。Lambdaの実装の中で、この名前でそれぞれの変数の内容が返るようにしていたためです。

このページには認証制限はかけていないため、認証されていなくても、アクセス可能です。

# API GatewayのAPIにオーソライザを設定

さきほどは、認証状態にかかわらず、誰でも参照できるAPI GatewayのAPIを作成しました。
これから、認証された状態でなければ、参照できないAPIを作成します。

API Gatewayでは、誰が認証状態で呼び出されたのかを判別するために、IDトークンを要求します。
IDトークンは、OpenID Connect用語ですが、サインインユーザが誰なのかを認証プロバイダが証明するためのトークンです。

まずはAPI GatewayのAPIにオーソライザーを作成します。
オーソライザーは、どうやってIDトークンを受け取り、どのアプリ向けのIDトークンを受け付けるのかを指定します。

新しいオーソライザーの作成ボタンを押下します。
名前には、適当な名前を付けます。たとえば、「test-cognito-authorizer」とします。
タイプは、今回の目的の対象であるCognitoを選択します。
Cognitoユーザープールの入力のところには、対象のCognitoユーザプールの名前（例えば「test-pool」）を指定します。
そして、トークンのソースに「Authorization」、トークンの検証にはどのアプリ向けのIDトークンを受け取るのかを指定します。もし複数のアプリから接続を受けるなど、接続元のアプリを特定しない場合にはトークンの検証には指定せず、Lambda側で判別します。
IDトークンには、認証プロバイダがどのアプリ向けに発行したのかどうか示すaudというフィールドがあります。Cognitoとの連携では、認証プロバイダから見てログイン連携先であったCognitoユーザプールのアプリクライアントの「アプリクライアントID」に該当します。

ちょっと横道にそれて、IDトークンの中身を見てみて、audフィールドやその他フィールドを確認してみます。
前回作成したnode.jsのアプリケーションフォルダで、いくつかファイルを追加します。(auth.htmlとstart.js)

<pre>
.
├── cert
│   ├── server.crt
│   ├── server.csr
│   └── server.key
├── index.js
├── node_modules
├── package.json
└── public
     ├── auth.html
     └── js
         └── start.js
</pre>

まず、auth.htmlです。

```html

<HTML lang="ja">
<HEAD>
	<META http-equiv="Content-Type" content="text/html; charset=utf-8">	 
	<META http-equiv="Content-Security-Policy" content="default-src * 'unsafe-inline'; style-src * 'unsafe-inline'; media-src * blob:; img-src *; script-src * 'unsafe-inline';">
	<TITLE>テスト</TITLE>	 
</HEAD>
<BODY onload="proc_load()">
	<H1><a href="">テスト</a></H1>

	<a href="https://＜ドメイン名＞.auth.ap-northeast-1.amazoncognito.com/login?response_type=token&client_id=＜アプリクライアントID＞&scope=openid%20email&redirect_uri=＜リダイレクト先URL＞
">サインイン</a>
	<a href="https://＜ドメイン名＞.auth.ap-northeast-1.amazoncognito.com/logout?client_id=＜アプリクライアントID＞&logout_uri=＜リダイレクト先URL">サインアウト</a>
	<BUTTON id="secret_call" onclick="secret_call()">secret呼び出し</BUTTON>
	<DIV id="message"></DIV>
	<DIV id="content"></DIV>
	
	<SCRIPT src="js/start.js"></SCRIPT>
</BODY>
</HTML>

```

start.jsは後述します。

リダイレクト先URLと付記していますが、今回のサンプルではこのページ自体をリダイレクト先とします。
で、このサインインURLには見覚えがありますでしょか。
前回のCognitoへのサインインのURLは以下でした。

> https://＜ドメイン名＞.auth.ap-northeast-1.amazoncognito.com/login?response_type=code&client_id=＜アプリクライアントID＞&redirect_uri=<リダイレクト先URL＞

今度のURLでは、response_type=tokenとし、scope=openid%20emailというのが増えています。
これまでは認証プロバイダから認可コードを取得していました。その認可コードからIDトークンを取得する順番なのですが、Implicit GrantではいきなりIDトークンを取得することができます。（もちろん、response_type=codeとして以前のトークンを取得するやり方でも構いません。）
openidはIDトークンを、emailはEメールアドレスが認証プロバイダから欲しい、ということを示しています。

Web管理コンソールのCognitoの設定画面から、アプリクライアントの設定でImplicit Grantがまだ有効にしていないようでしたら、有効にしてください。
ちなみに、サインインのリダイレクト先URLは、「Cognitoユーザープール」→「アプリの統合」→対象アプリクライアント→コールバックURLと一致させます。おそらくこのhtmlはまだ設定していないと思いますので、「,(カンマ)」区切りで追加するか、置き換えます。
また、サインアウトのリダイレクト先URLは、「Cognitoユーザープール」→「アプリの統合」→対象アプリクライアント→サインアウトURLのことです。これもまだ設定していないと思いますので、「,(カンマ)」区切りで追加するか、置き換えます。

詳細は以下を参照してください。
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/login-endpoint.html

それでは、サインインURLのリンクをクリックしてみます。。。。とその前に、start.jsも使いますので、とりあえず、盲目的にファイルを作成しておきます。
start.jsです。

```js

var hashs = {};

function proc_load() {
	hashs = parse_url_vars(location.hash);

	var elem = document.getElementById("message");
	elem.innerText = 'id_token=' + hashs.id_token;
}

function parse_url_vars(param){
	if( param.length < 1 )
		return {};
	
	var hash = param;
	if( hash.slice(0, 1) == '#' || hash.slice(0, 1) == '?' )
		hash = hash.slice(1);
	var hashs  = hash.split('&');
	var vars = {};
	for( var i = 0 ; i < hashs.length ; i++ ){
		var array = hashs[i].split('=');
		vars[array[0]] = array[1];
	}
	
	return vars;
}

function secret_call(){
	var elem = document.getElementById("message");
	var content = document.getElementById("content");

	return do_post('https://[RestApi-Id].execute-api.ap-northeast-1.amazonaws.com/prod/secret', [], hashs.id_token)
	.then(result =>{
		elem.innerText = 'secret_call OK';
		content.innerText = JSON.stringify(result);
	})
	.catch(error=>{
		elem.innerText = 'secret_call Error:' + error;
	});
}

function do_post(url, body, idtoken) {
	const headers = new Headers( { "Content-type" : "application/json", 'Authorization': idtoken } );
	return fetch(url, {
		method : 'POST',
		body : JSON.stringify(body),
		headers: headers
	})
	.then((response) => {
		return response.json();
	});
}

```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/947d13fc-709a-b190-9fab-4657778f8b05.png)

で改めてサインインのリンクをクリックします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/ef7030e1-9cd3-19c0-c311-cbd78b888da5.png)

今回も期待通りのサインイン画面がでてきました。
サインインが成功すると、以下のように、id_token=XXXXXX という長ーい文字列が表示されたのではないでしょうか。これがIDトークンです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/0481df9a-79cc-306c-5723-2d889b506d52.png)

仕掛けは、

```html
<BODY onload="proc_load()">
```
の部分と、

```js
function proc_load() {
	hashs = parse_url_vars(location.hash);

	var elem = document.getElementById("message");
	elem.innerText = 'id_token=' + hashs.id_token;
}
```

の部分です。

proc_load()は、onloadに指定されているため、本ページがロードされたときに呼び出されます。
では、いったい何をしているかというと、フラグメント識別子からid_tokenを取り出しているように見えます。見えますというより、その通りです。

無事に、Cognitoによるサインイン認証が完了すると、リダイレクト先URLにリダイレクトされます。そう、同じ本ページを再度読み直しています。ただし、ブラウザのURL入力欄を見ていただけるとわかりますが、auth.htmlの後に、#マークに続けて、非常に長い文字列があります。
まさに、この中に、id_token(IDトークン)があるわけです。
ではなぜ、Javascriptでlocation.hashからIDトークンを取り出しているかというと、実は#マーク以降はフラグメント識別子と言って、リダイレクト先URLのサーバには通知されずに、ブラウザだけに保持されます。
そこで、リダイレクトURLが呼び出されて戻ってきたときに、proc_load()を呼び出して、フラグメント識別子からIDトークンを取り出したわけです。

これでやっと、IDトークンを取得できました。早速中身を見てみます。
以下のサイトが有用です。

https://jwt.io/

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/a04750ee-e04b-a93d-f4d0-0329a78e132f.png)

IDトークンは、JSON Web Token(JWT)でエンコードされており、こちらのサイトでは、JWTに関して有用な情報やツール類を提供してくれています。

少し下の方に行くと、「Debugger」というのがあり、Encodedのテキストエリアに、さきほど取得したIDトークン(id_token=以降の文字列)を張り付けてみます。

この中で注目すべきは、Decodedの、”aud”、"cognito:groups"、”token_use”、”email”、”cognito:username”です。それぞれ以下の内容になっているはずです。

+ aud：CognitoのアプリクライアントID
+ cognito:groups:[CognitoのプールID]
+ token_use："id"
+ email：サインインユーザのEメールアドレス
+ cognito:username：サインインユーザのユーザ名

このaudがまさにAPI Gatewayのオーソライザーで指定したトークンの検証であり、これと一致しているかどうかで、API Gatewayは間違ったIDトークンを扱わないようにしています。

ちなみに、Cognitoの検証用公開鍵は以下から取得できます。

> https://cognito-idp.[リージョン名].amazonaws.com/[ユーザープールID]/.well-known/jwks.json

ついでに、secret_call()とdo_post()も見てみます。
secret_callは、これから認証付き呼び出し対象に設定しようとしているAPI GatewayのAPIを呼び出す関数です。実際には、POST呼び出しをしており、do_postがそれを行っています。

大事なのは以下の部分です。

```js
	const headers = new Headers( { "Content-type" : "application/json", 'Authorization': idtoken } );
```

呼び出し時のヘッダーに、Authorizationとしてフラグメント識別子で取得したid_tokenを渡しています。
API Gatewayのオーソライザーのトークンのソースで、Authorizationと指定したのは、このヘッダでAuthorizationを指定したかったためです。


# API GatewayのAPIに、認証必須を指定

それでは、API Gateway APIに認証必須条件を指定します。

まずは対象のリソースを作成します。今回は、「/secret」という名前にしてみます。
メソッドは、「POST」を選択します。
とび先のLambda関数は、せっかくサインイン専用ページにするので、「test-cognito-lambda」とは別に「test-cognito-secret-lambda」というLambda関数を作ります。中身は同じでよいです。
以下のように出来上がりました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/7cfd650a-9368-bc4c-b420-a7282a62dd13.png)

さらに、POSTを選択します。
メソッドリクエストを選択します。

ここで、設定→認証のところで、さきほど作成したオーソライザー「test-cognito-authorizer」を選択し、チェックボタンを押下します。
忘れずに、APIのデプロイをしておきます。

# 動作確認

では早速、このリソースにアクセスしてみます。
auth.htmlをブラウザで開きます。
サインイン状態だと思うので、サインアウトのURLをクリックして、いったんサインインしていない状態にします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/c99541d3-3f7a-a723-f335-521b7954691e.png)

いざ、secret呼び出しボタンを押下してみます。
エラーとなりました。サインインしていない状態なので、エラーとなるのは正しい動作です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/bed46fa8-6bc2-4527-95ca-f49591dd354e.png)

では、サインインURLをリンクしてサインイン状態にします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/3bef4653-9a3e-7892-22e8-ecb3e65e5c35.png)

この状態で、secret呼び出しボタンを押下すると、無事に成功しました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/0a7e9b28-b775-1c84-e457-070f47bbecd9.png)

Lambda側で受け取った引数eventを見ると、新しくevent.requestContext.authorizerというのが増えているかと思います。
この中に、IDトークンの中身で見た、”aud”、"cognito:groups"、”token_use”、”email”、”cognito:username”があるのがわかります。

以上です。（ちょっと絵が少なくて説明文が多すぎましたかね。。。）