はじめに
こんにちは、江口です。
前回は人事選考の自動化を作成するにあたり、認証基盤をFlask JWTでOAuth2を自作しました。
が、
認証認可の処理はAWS Cognitoに丸々載せ替えることもできます。
Cognitoに載せ換えることで、前回ほどの労力・工数はかなり削減されます。
ということで、今回はCognito + APIGatewayと時々aws-amplify(with vue)のハンズオンです。
また、今回はFacebookやTwitterなどのThirdPartyは取り扱いません。
目指す構成は以下です。
構成図
- Cognitoに認証を任せ、AccessTokenを取得する
- ※フロントはvue x aws-amplifyの構成です。
- 取得したAccessTokenをAuthorizationHeaderに付与し、APIGatewayをコールする
- APIGateway(lambda)では、tokenの検証と検証を通過すれば、バックに構える各APIにproxyする
- 各APIはクライアントにJSONを返す
- ※各APIはAPIGatewayからのリクエストのみを許可します。
認証はCognitoに任せ、取得したtokenをheaderに仕込んだまま、
APIGatewayもコールしますが、実質APIとしてJSONを返すのはバックに構えるAPIサーバになります。
これにより、Lambdaの処理の中で、URLパターンを見て「/hoge」ならこのAPIに、「/fuga」ならこのAPIに。と、処理を各APIに移譲できます。
認証は共通でCognitoを使うが、サービスは複数ぶら下がることができるという狙いです。
上記が最終形ですが、二回に分けて紹介しようと考えているため、
今回は以下の構成までをハンズオンします。
- Cognitoに認証を任せ、AccessTokenを取得する
- ※フロントはvue x aws-amplifyの構成です。今回はlocalhostです
- 取得したAccessTokenをAuthorizationHeaderに付与し、APIGateayをコールする
- APIGateway(lambda)では、tokenの検証と検証を通過すれば、クライアントにテスト用のJSONを返す
つまり、オリジナルのAPIへのproxy部分は今回は作りません。
あくまで、CognitoとAPIGatewayの連携に焦点を当てたサンプルを作成します。
用語について
今回触れていく用語について簡単に紹介します。
Cognito
認証・認可をサポートしてくれます。
今回は下記のUserPoolsとFederated Identitiesとセットで使います。
APIGateway
APIです。
今回はLambdaと連携してJSONを返します。
オーソライザと呼ばれる機能を使い、Cognitoで認証を通過したアクセスのみ、
APIへのアクセスを認可するようにしています。
認証に通過したユーザーはtokenを取得して持っているはずなので、tokenをAuthorizationHeaderにセットしてAPIにアクセスすることでAPIGatewayにアクセスできるようになります。
aws-amplify
Cognitoを簡単に利用できるようになるjsライブラリです。
これまではCognitoIdentity SDKとAWS SDKを組み合わせて使う必要がありましたが、
amplifyのみで完結できるようになりました。
▼Github
github.com
Try
Cognito - UserPool -
まずはCognitoのUserPool設定からやっていきます。
これがないと始まりません。
AWSマネージメントコンソールにsighnInし、「Cognito」を開き、
以下の「ユーザープールの管理」を押下します。
押下したら、新規でユーザープールを作成しますので、
「tutorial」と命名し、「デフォルトを確認する」を選択します。
デフォルト設定のプレビューが表示されます。
こちらはあとで編集できますので、とりあえず「プールの作成」を押下し、作成を完了します。
完了したらダッシュボードに遷移すると思いますので、
「アプリクライアント」の「アプリクライアントID」の値をコピーしておいてください。
「アプリクライアントの設定」のを押下し、以下になるようにチェックを入れます。
また、注意点として、「アプリクライアント」の「クライアントシークレットの生成」はチェックが外れていることを確認してください。
これだけで、まずは簡単に器ができました!!
最後にUserPoolを利用する側のクライアントの設定をちょろっといじります。
callback urlでerrorが出る場合は、一旦サンプルなので「http://localhost:9999/test.php」とでも入れておいてください。
ログイン成功後にaccess_tokenがハッシュ文字列として付与されたURLにリダイレクトされますが、今回は使いません。
「許可されているOAuthフロー」で「Implicit grant」を選択しましたが、
「Authorization code grant」との違いはこちらを参考にしてください。
ユーザープールのアプリクライアントの設定 - Amazon Cognito
今回は「Implicit grant」 = 「認証エンドポイントから直接AccessTokenを取得」する方法にしました。
Cognito - Federated Identities -
続いて、Identityを作成します。
ヘッダーに「ユーザープール | フェデレーティッドアイデンティティ」とメニューがあるので、「フェデレーティッドアイデンティティ」を押下します。
押下したら、こんな画面に移るので、IDプール名には適当なnameを入力し、
「認証プロバイダー」の「Cognito」タブ「ユーザープールID」に先ほど作成したUserPoolのIDを記入し、アプリクライアントIDに先ほどコピーしたIDを記入します。
記入したら、「プールの作成」を押下します。
※この時エラーが出ることがありますが、時間を置いてから押すことで無事に作成されました。なんでしょうかね。
これで完了です。
続いてIAMの設定画面に移りますので、デフォルトのまま、AUTHロールとUNAUTHロールを新規作成します。
これでIDプールの作成は完了です。
Cognitoの確認
Cognitoはログインページがデフォルトで用意されるので、試しに表示して見ます。
以下のようにURLを組み立ててアクセスしてみると...
"https://YOUR_DOMAIN/oauth2/authorize?response_type=token&client_id=[YOUR_CLIENT_ID]&redirect_uri=http://localhost:9999/test.php"
このようなデフォルトUIが表示されます。
実際にログインすると、AccessTokenが帰ります。
試しに、localhost:9999でphp -aでlocalServerを立ち上げて見ます。
# vi test.php
<?php
$str = $_SERVER["REQUEST_URI"];
preg_match('/#(.+)$/', $str, $res);
var_dump(parse_url($str));
中身は適当でいいです。
作成したらlocalhostを立ち上げます。
# php -S localhost:9999
立ち上げたら、先ほどのURLにアクセスして見ますと...
http://localhost:9999/test.php#id_token=eyJra...[省略]&access_token=....[省略]&expires_in=3600&token_type=Bearer
このような形でTokenが取得できるかと思います!!
APIGatewayの用意
認証基盤は用意できたので、続いてプロキシとしてのAPIGateWayを用意します。
ですが、今回はプロキシではなく、「Cognitoの認証を通過したTokenを持ったリクエスト時にのみ、JSONを200Statusで返す」まで作って見ます。
まず、こちらを参考に、以下のようなlambda関数を一つ作成してください。
・関数名「cognito-tutorial-lambda」
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": 'Hello from Lambda!'
}
私はPythonが好きなのでpython36を選んでますが、ここは何でも構いません。
続いて、APIGatewayに遷移し、新しいAPIの作成で適当にテスト用のAPIを作成します。
「新しいAPI」で「APIの作成」を押下します。
以下のようにドロップダウンメニューから「リソースの作成」でtestディレクトリを作成します。
ディレクトリが作成できたら、「メソッドの作成」からGETメソッドを作成し、統合タイプを「Lambda関数」にし、先ほど作成したlambda関数名を入力して作成してください。
これでGETでJSONを取得するAPIが一つできました。
最後に/testを選択した状態でドロップダウンより「CORSの有効化」を行います。
CORSの設定として、今回は「Access-Control-Allow-Origin」ヘッダに「http://localhost:8080」を設定しました。
これで、あとで紹介するフロントでresponse bodyを受け取れるようになります。
(※ Access-Control-Allow-Originに指定されたドメイン以外からのリクエストだと、ブラウザがresponseを取得しません。)
APIGatewayAPIにオーソライザーを新規追加する
しつこいですが、Cognitoで認証を通過したユーザーだけがAPIにアクセスできます。
それ以外のユーザーは401ないしは403となります。
ちゃっちゃかやっていきます。
先ほど作成したAPIのメニューから「オーソライザ」を選択し、「新しいオーソライザを作成」を押します。
上図のような設定になるように、作成すれば完了です。
オーソライザをAPIのアクセス制限に設定する
追加したオーソライザをAPIに当てていきます。
先ほど作成した「GET: /test」エンドポイントを選択し、「メソッドリクエスト」をクリックすると、以下のような設定画面に遷移します。
「認証」に先ほど追加したオーソライザを選択します。
画面キャプチャから漏れてしまいましたが、「HTTTPリクエストヘッダー」の開き、「Authorization」を追加、必須に指定します!
これで準備は完了です。
フロント(vue / aws-amplify)
フロントで行う処理は以下です。
- Cognitoログインフォームを表示
- APIGatewayにdeployしたAPIをaxios経由でコールする
です。
今回は以下の技術構成でフロントを構成してます。
- vue
- JavaScript フレームワーク
- axios
- 非同期通信で使用
- aws-amplify
- Cognitoを利用したWebアプリケーションを開発するための包括的ライブラリ
- vue-router
- URLとVueコンポーネントのマッピング。ルーティングに使用
環境構築
- npm install -g vue/cli
- vue-init web pack cognito-tutorial
- npm install(以下のpackage.jsonを参考)
- aws-exports.jsをsrc下に手動で作成
{
"name": "cognito-tutorial",
"version": "1.0.0",
"description": "A Vue.js project",
"author": "eguchi <eguchi@asial.co.jp>",
"private": true,
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"unit": "jest --config test/unit/jest.conf.js --coverage",
"e2e": "node test/e2e/runner.js",
"test": "npm run unit && npm run e2e",
"lint": "eslint --ext .js,.vue src test/unit test/e2e/specs",
"build": "node build/build.js"
},
"dependencies": {
"@vue/eslint-config-prettier": "^3.0.3",
"amplify": "0.0.11",
"aws-amplify": "^1.0.11",
"axios": "^0.18.0",
"vue": "^2.5.2",
"vue-router": "^3.0.1"
},
"devDependencies": {
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-eslint": "^8.2.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-jest": "^21.0.2",
"babel-loader": "^7.1.1",
"babel-plugin-dynamic-import-node": "^1.2.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"chalk": "^2.0.1",
"chromedriver": "^2.27.2",
"copy-webpack-plugin": "^4.0.1",
"cross-spawn": "^5.0.1",
"css-loader": "^0.28.0",
"eslint": "^4.15.0",
"eslint-config-standard": "^10.2.1",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.7.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.0.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"jest": "^22.0.4",
"jest-serializer-vue": "^0.3.0",
"nightwatch": "^0.9.12",
"node-notifier": "^5.1.2",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"portfinder": "^1.0.13",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.0.8",
"postcss-url": "^7.2.1",
"rimraf": "^2.6.0",
"selenium-server": "^3.0.1",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"uglifyjs-webpack-plugin": "^1.1.1",
"url-loader": "^0.5.8",
"vue-jest": "^1.0.2",
"vue-loader": "^13.3.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.5.2",
"webpack": "^3.6.0",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.0"
},
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
<||
src/aws-exports.jsの中身は以下のようにします。
各自で作成した値を埋め込むだけです。
>|javascript|
import Amplify from 'aws-amplify'
Amplify.configure({
Auth: {
// フェデレーションアイデンティティのID
identityPoolId: 'ap-northeast-1:hogee',
// リージョン
region: 'ap-northeast-1',
// ユーザープールのID
userPoolId: 'ap-northeast-hogeeeet',
// ユーザープールのウェブクライアントID
userPoolWebClientId: 'fugaaaa',
mandatorySignIn: true
}
})
[aws-exports.js参考]
https://aws-amplify.github.io/amplify-js/media/authentication_guide.html
webpackサンプルを作成すると、最初から「HelloWorld.vue」ページコンポーネントがあると思いますので、これを書き換えちゃいます。
こんな感じにしてください。
<template>
<div class="page">
<div class="login-form">
<p>ログイン</p>
<p>{{ status }}</p>
<p>{{ message_text }} </p>
<ul class="login-form">
<li>
<label>ユーザー名</label>
<input
v-model="userInfo.username"
type="text">
</li>
<li>
<label>パスワード</label>
<input
v-model="userInfo.password"
type="password">
</li>
</ul>
<button
class="btn btn-primary"
@click="signIn">ログイン</button>
<button
class="btn btn-primary"
@click="signOut">ログアウト</button>
<br>
<router-link :to="{ name: 'ApiGatewayTest'}">APIGateway連携テスト</router-link>
</div>
</div>
</template>
<script>
import Amplify, { Auth } from 'aws-amplify'
const awsExports = require('@/aws-exports').default
Amplify.configure(awsExports)
export default {
data() {
return {
status: '',
userInfo: {
username: '',
password: ''
},
message_text: '',
url: ''
}
},
created() {
this.checkLogin()
},
mounted() {
console.log(this.userInfo)
},
methods: {
checkLogin() {
Auth.currentSession()
.then(data => {
console.log(data)
this.status = 'ログインしています'
})
.catch(err => {
console.log(err)
this.status = 'ログインしていません'
})
},
signIn() {
Auth.signIn(this.userInfo.username, this.userInfo.password)
.then(data => {
this.message_text = 'ログインしました'
this.status = 'こんにちは、' + data.username + 'さん'
})
.catch(err => {
console.log(err)
this.message_text = 'ログインできませんでした'
})
this.checkLogin()
},
signOut() {
Auth.signOut()
.then(data => {
console.log(data)
this.message_text = 'ログアウトしました'
})
.catch(err => {
console.log(err)
this.message_text = 'ログアウトできませんでした'
})
this.checkLogin()
}
}
}
</script>
<style scoped>
.login-form {
list-style: none;
}
</style>
これだけで、Cognitoへのログイン処理ができますが、
<router-link :to="{ name: 'ApiGatewayTest'}">APIGateway連携テスト</router-link>
とあるように、APIGatewayのAPIを呼び出すために、ルーティングとコンポーネントを追加します。
▼routing
routingはrouter/index.jsを編集します。
以下のようにします。
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import ApiGatewayTest from '@/components/ApiGatewayTest'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
},
{
path: '/apigateway-test',
name: 'ApiGatewayTest',
component: ApiGatewayTest
}
]
})
これで「/apigateway-test」でApiGatewayTest.vueが呼ばれます。
src/components/ApiGatewayTest.vueを以下のように作成します。
<template>
<div class="apigateway-test">
<div class="contents">
<p>{{ status }}</p>
<p class="message">{{ message }}</p>
</div>
</div>
</template>
<script>
import { Auth } from 'aws-amplify'
import * as api from '@/libs/api/api'
export default {
data() {
return {
status: '',
message: 'empty'
}
},
created() {
Auth.currentSession()
.then(data => {
console.log(data)
this.status = 'ログインしています'
})
.catch(err => {
console.log(err)
this.status = 'ログインしていません'
})
},
mounted() {
this.test()
},
methods: {
async test() {
const { data: { body } } = await api.getTest()
this.message = body
}
}
}
</script>
<style scoped>
.message {
font-weight: bold;
font-size: 24px;
}
</style>
次に非同期APIアクセス処理を追記します。
src/libs/apiでディレクトリを作成し、api.jsを以下のように作成します。
import axios from 'axios'
// API系は自分のAPI使ってね
const API_BASE_URL = 'https://hogeeeeeeee-api.ap-northeast-1.amazonaws.com/'
const API_ENV = 'stg'
const apiClient = axios.create({
baseURL: API_BASE_URL
})
apiClient.interceptors.request.use(async config => {
config.headers['Authorization'] = localStorage.getItem(
// 各自の環境で生成されたlocalStorageのidTokenのkey名を使う
'CognitoIdentityServiceProvider.hogeeeeee.eguchi.idToken'
)
return config
})
export async function getTest() {
return apiClient.get(`${API_BASE_URL}${API_ENV}/test`)
}
API_BASE_URLとlocalStorageのkey名は各自で入れ替えてください。
API_ENVはAPIGatewayのステージが該当します。
最終的なURLイメージとしては以下のようなエンドポイントになります。
「https://hogeeee-api.ap-northeast-1.amazonaws.com/stg/test」
amplifyを使うと、ログイン成功時にlocalStorageにCredential情報が自動で詰められるので、その中でも以下のキャプチャにあるような「IdToken」をAuthorizationヘッダに使用してください。
これで完了です。
全体的な流れのまとめとしては、、、
1. npm run devでlocalサーバ開始
2. 「http://localhost:8080」にアクセスする
3. ユーザー名とパスワードを入力して、ログインをおす
ログインしました。となります。この時、localStorageにもCredentialsが格納されてます。確認してみてください。
4. 「APIGateway連携テスト」をクリックして遷移する
遷移すると、ApiGatewayTest.vueが呼ばれます。
ApiGatewayTest.vueではmountedの中で
mounted() {
this.test()
},
methods: {
async test() {
const { data: { body } } = await api.getTest()
this.message = body
}
}
のように、APIをコールしてます。
APIからJSONを取得し、
this.message = body
でdataのmessageに値を詰めてます。
これが
<p class="message">{{ message }}</p>
で画面に表示されます。なお、messageのdefault値は「'empty'」です。
amplifyのAPIクラスに置き換えてみる
上記のやり方でも機能実現は可能です。
ですが、自分でlocalStorageからkeyを指定して、axiosでheaderにセットして〜とか管理しなくても、
実はamplifyを使って書き換えることも可能です。
この場合、localStorageのkeyを意識しないで済みます。
「src/aws-exports.js」と「src/libs/api/api.js」と「ApiGatewayTest.vue」をそれぞれ以下のように書き換えて見ましょう
▼src/aws-exports.js
※各自の環境に書き換えてください。
import Amplify from 'aws-amplify'
Amplify.configure({
Auth: {
// フェデレーションアイデンティティのID
identityPoolId: 'ap-northeast-1:3dce04b6-45c8-49c7-9b53-654ad7cbd2aa',
// リージョン
region: 'ap-northeast-1',
// ユーザープールのID
userPoolId: 'ap-northeast-1_KAmmkV74r',
// ユーザープールのウェブクライアントID
userPoolWebClientId: '4enc6qmrc2isf2phv5hoag17t',
mandatorySignIn: true
},
API: {
endpoints: [
{
name: 'cognito-tutorial-api',
endpoint: 'https://milaadw0od.execute-api.ap-northeast-1.amazonaws.com'
}
]
}
})
▼src/libs/api/api.js
import { Auth, API } from 'aws-amplify'
const API_NAME = 'cognito-tutorial-api'
export async function getTest() {
const user = await Auth.currentAuthenticatedUser()
const token = user.signInUserSession.idToken.jwtToken
return API.get(API_NAME, '/stg/test', {
headers: {
Authorization: token
}
})
}
▼ApiGatewayTest.vue
methods: {
async test() {
const { body } = await api.getTest()
this.message = body
}
}
最後に...
※環境構築がうまくいかなった人は、完成形をGithubに上げてありますので、設定やエンドポイントを書き換えて使ってみてください。
github.com
まとめ
今回は
- Cognito UserPoolの作成
- Cognito FederatedIdentityの作成
- APIGatewayの作成
- vue / aws-amplifyを使ったフロント処理
まで行いました。
次回は、今回APIとして使ったLambdaをプロキシとして使用し、
後続のオリジナルAPIに処理を繋ぐという処理をやっていきたいと思います。
長文・駄文でしたが、ありがとうございました。
以上です。