39
44

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 5 years have passed since last update.

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

Last updated at Posted at 2018-06-16

前回は、AWS Cognitoを使って、GoogleアカウントとLINEアカウントからログインできるようにCognitoユーザープールを設定しました。
AWS CognitoにGoogleとLINEアカウントを連携させる

今度は、サインイン状態を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

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

image.png

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

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の管理コンソールを開きます。

image.png

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

image.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)


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

まず、auth.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です。


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

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

image.png

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

image.png

仕掛けは、

<BODY onload="proc_load()">

の部分と、

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トークンを取得できました。早速中身を見てみます。
以下のサイトが有用です。

image.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がそれを行っています。

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

	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

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

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

動作確認

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

image.png

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

image.png

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

image.png

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

image.png

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

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

39
44
34

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
39
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?