前回は、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
右上の「関数の作成」ボタンを押下します。
今回は、ほぼ何も処理をしないシンプルな関数を用意します。
「一から作成」を選択し、適当な名前を入力します。例えば、「test-cognito-lambda」とします。
ランタイムは、Node.js 8.10 としました。
ロールは既存のロールを選択してもよいですし、新規に作成してもよいです。
例えば、ロールとして「テンプレートから新しいロールを作成」とし、ポリシーテンプレートは「シンプルなマイクロサービスのアクセス権限」を選択します。必要最低限のアクセス権限といえます。
それでは非常に単純な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の管理コンソールを開きます。
「APIの作成」ボタンを押下します。
適当な名前を入力します。この名前は、外部に公開する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は以下でした。
今度の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();
});
}
で改めてサインインのリンクをクリックします。
今回も期待通りのサインイン画面がでてきました。
サインインが成功すると、以下のように、id_token=XXXXXX という長ーい文字列が表示されたのではないでしょうか。これがIDトークンです。
仕掛けは、
<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トークンを取得できました。早速中身を見てみます。
以下のサイトが有用です。
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関数を作ります。中身は同じでよいです。
以下のように出来上がりました。
さらに、POSTを選択します。
メソッドリクエストを選択します。
ここで、設定→認証のところで、さきほど作成したオーソライザー「test-cognito-authorizer」を選択し、チェックボタンを押下します。
忘れずに、APIのデプロイをしておきます。
動作確認
では早速、このリソースにアクセスしてみます。
auth.htmlをブラウザで開きます。
サインイン状態だと思うので、サインアウトのURLをクリックして、いったんサインインしていない状態にします。
いざ、secret呼び出しボタンを押下してみます。
エラーとなりました。サインインしていない状態なので、エラーとなるのは正しい動作です。
では、サインインURLをリンクしてサインイン状態にします。
この状態で、secret呼び出しボタンを押下すると、無事に成功しました。
Lambda側で受け取った引数eventを見ると、新しくevent.requestContext.authorizerというのが増えているかと思います。
この中に、IDトークンの中身で見た、”aud”、"cognito:groups"、”token_use”、”email”、”cognito:username”があるのがわかります。
以上です。(ちょっと絵が少なくて説明文が多すぎましたかね。。。)