カテゴリー
【Angularユーザーのための認証API自作講座①】AWS Cognitoでセキュアなユーザー認証を自力で構築する
※ 当ページには【広告/PR】を含む場合があります。
2021/06/24
2022/08/10

認証ありのECサイトを自分の手で一から構築するのは中々に骨の折れる作業で、Auth0などのサードパーティ製のAPI方式認証サービスを使うほうが圧倒的にハードルが低くなります。
とはいえ、サイトの規模が大きくなってくるとサービスの利用料がエンタープライズ版への移行が望ましく、それなりの維持費用が発生してくるようになります。
そこで今回は、管理費を圧迫しつつあったAuth0のようなユーザー認証管理サービスの利用を諦めて、独自のユーザー認証システムを一念発起してCognitoへマイグレーションする際に行うべき作業をまとめてみました。
Auth0の料金が割高に感じた時のCognito
Auth0の料金プランに関しては
それ以上のMAUではEnterpriseプランに移行する必要があります。
仮にEssentialプランで月1万MAUが利用した料金は
$228
対して、
例えば、7万MAUであった月の請求額は、
$0.00550 * (70000 - 50000) = $110 ~ 12000円程度
であり、
もちろんCognito自体はユーザー管理の基本機能しかなく、他の必要なマイクロサービスも自作しないといけませんので、システムの構築をする労力を考えるとAuth0を使い続ける方が楽ではあるのですが、あとは使う側の予算と相談かと思います。
Cognitoオーソライザ付きのAPI Gatewayによるユーザー認証
API Gatewayには内部でユーザー認証を行うためのオーソライザーを追加して利用することができます。 このオーソライザーにはLambdaタイプか、Cognitoタイプが指定して利用できます。
前述したAuth0などのようなサードパーティ製のユーザー認証APIなどを指定する場合にはLambdaタイプでカスタムオーソライザーを作成して使うことになりますが、AWSネイティブのCognitoは別に区別されているようです。
ということで、今回はCognitoオーソライザの話に限定したお話をしていきます。 この構成を組んで何が嬉しいかというと、以下の概念図のようにAPIがクライアントからのリクエストヘッダーの情報を元にユーザー管理ができるバックエンドが簡単に構築できるわけです。
1133x721

さて、
1. Cognitoコンソール > ユーザープールを作成
2. API Gatewayコンソール > ユーザープールからAPI Gatewayオーソライザーを作成
3. API Gatewayコンソール > 選択したAPIメソッドでオーソライザーを有効にする
4. ユーザープールを有効化したAPIメソッドを呼び出すために、APIクライアントで次のタスクを実行:
4.1. AWS CLIまたはAPIを使用して、ユーザープールにユーザーをサインインさせ、ID・アクセストークンを取得
4.2. クライアントから、デプロイされたAPI Gateway APIを呼び出して、Authorizationヘッダーに適切なトークンを指定する
という内容で順次説明していきます。
Cognitoの設定手順
まずは手動でCognitoのダッシュボードからユーザー管理サービスをセットアップしていきます。
Cognitoでユーザープールの作成
ユーザープール
まずはCognitoのダッシュボードのトップから
[ユーザープールの管理] > [ユーザープールを作成する]
1200x925

ここではプール名を
testusers
とりあえず新規作成するプールはデフォルト設定のまま生成しますので、
[デフォルトを確認する] > [プールの作成]
ユーザープールが作成されると以下の画面がでますので、
1000x940

新規作成したプールID値とARN値を何処かに控えておきましょう。
次にアプリクライアントを追加してみます。
先程のユーザープールで、左側のペインから
[アプリクライアント] > [アプリクライアントの追加]
myAuthApp
1224x1560

動作テスト程度ですのでクライアントシークレット機能は不要として、
クライアントシークレットを生成
Cognitoによる認証にはIDトークンを使用したいので、
認証用の管理 API のユーザー名パスワード認証を有効にする
他はデフォルトのまま利用します。
設定が終わると、
[アプリクライアントの作成]
ユーザープールにユーザーを追加する
まだこのままではプールに一人もユーザーを登録していないので、動作確認用に適当に一つユーザーを追加してみます。
対象のユーザープールの左側メニューから
[ユーザーとグループ] > [ユーザー]タブ > [ユーザーの作成]
795x1400

ここで適当な名前でユーザー・
tacokin-test
仮パスワード
電話番号
Eメール
今回はEメールでの認証にして、
Eメールを検証済みにしましすか?
また、
この新規ユーザーに招待を送信しますか?
仮パスワード
さて、このままでは肝心のログインシステムができていない状態で、後段の作業に何かと都合が悪い状態です。
そこで、動作確認だけの救済措置として、
AWS-CLIのインストール方法はここでは割愛させていただきますが、
まずはちゃんとユーザープールができているかを確認するために、
$ aws cognito-idp list-users --user-pool-id [ユーザープールID]
{
"Users": [
{
"Username": "tacokin-test",
"Attributes": [
{
"Name": "sub",
"Value": "****************************"
},
{
"Name": "email_verified",
"Value": "true"
},
{
"Name": "email",
"Value": "****************************"
}
],
"UserCreateDate": xxxxxxxxxxxxxxxxx,
"UserLastModifiedDate": xxxxxxxxxxxxxxxxx,
"Enabled": true,
"UserStatus": "FORCE_CHANGE_PASSWORD"
}
]
}
というレスポンスが返っていれば正しく認識されています。
もし、
$ aws cognito-idp list-users --user-pool-id [ユーザープールID]
An error occurred (ResourceNotFoundException) when calling the ListUsers operation: User pool ap-northeast-1_******* does not exist.
というエラーが発生する場合には、現在のCLIで指定しているリージョン名が間違っている場合があります。 この例でいうと、
AWS_DEFAULT_REGION="ap-northeast-1"
では、
$ aws cognito-idp admin-initiate-auth \
--user-pool-id [ユーザープールID値] \
--client-id [アプリクライアントID] \
--auth-flow ADMIN_NO_SRP_AUTH \
--auth-parameters USERNAME=tacokin-test,PASSWORD=[仮パスワード]
#👇レスポンス
{
"ChallengeName": "NEW_PASSWORD_REQUIRED",
"Session": "A...中略...Y",
"ChallengeParameters": {
"USER_ID_FOR_SRP": "tacokin-test",
"requiredAttributes": "[]",
"userAttributes": "{\"email_verified\":\"true\",\"email\":\"**************\"}"
}
}
※パスワードは仮パスワードで置き換えてお読みください。
ここで、
--user-pool-id
--client-id
返ってきたJSONレスポンスをみると、
NEW_PASSWORD_REQUIRED
Session
ではこのセッション値を使って、
$ aws cognito-idp admin-respond-to-auth-challenge \
--user-pool-id [ユーザープールID] \
--client-id [アプリクライアントID] \
--challenge-name NEW_PASSWORD_REQUIRED \
--challenge-responses 'NEW_PASSWORD=[新しいパスワード],USERNAME=tacokin-test' \
--session [先程のセッション値]
#👇成功した際のレスポンス
{
"ChallengeParameters": {},
"AuthenticationResult": {
"AccessToken": "e...中略...A",
"ExpiresIn": 3600,
"TokenType": "Bearer",
"RefreshToken": "e...中略...A",
"IdToken": "e...中略...g"
}
}
※セッションは数分で有効期限切れになりますので、切れたら再び新しいセッションでやり直す必要があります。
パスワードの再設定が終わると、各種トークンがサーバから返されます。 なおデフォルトでは60分間ログイン状態が保持されるようです。
再びユーザーの状態を確認してみますと、
$ aws cognito-idp list-users --user-pool-id [ユーザープールID]
{
"Users": [
{
"Username": "tacokin-test",
"Attributes": [
#...中略
#👇ユーザー登録が完了している
"UserStatus": "CONFIRMED"
}
]
}
のようになれば準備が完了です。
API Gatewayの設定
ここではもっとも簡単なAPIの例であるPetStoreをインポートして、先程のCognitoをオーソライザとして設定する手順を説明していきます。
API Gatewayの作成
Cognitoオーソライザーを試してみるためのAPIGatewayインスタンスをダッシュボードから新規作成していきます。
まず先程設定したCognitoと同じリージョンであることを確認してから、API Gatewayダッシュボードのトップに移動し、
[APIを作成] > [APIタイプを選択] > [インポート] > プロトコルを選択: [REST] > 新しいAPIの作成: [APIの例]
800x1461

このサンプルはGETとPOSTが簡単に学習できる単純なものですが、そのままデプロイして使ってみるのにはうってつけです。
デプロイは
[リソース] > ルートのリソースを選択 > [アクション] > [APIのデプロイ]
876x1354

デプロイにはステージが必要ですので、ここでは開発ステージということで
dev
できたばかりのAPIエンドポイントを以下Curlで叩いてみますと、
$ curl --include https://*************.execute-api.ap-northeast-1.amazonaws.com/dev
HTTP/2 200
date: Wed, 23 Jun 2021 08:54:55 GMT
content-type: text/html
content-length: 1308
x-amzn-requestid: ac9f2768-fadb-4c1c-b193-caede1078e14
x-amz-apigw-id: BXuC_EKmtjMFg0g=
<html>
<head>
<style>
body {
color: #333;
font-family: Sans-serif;
max-width: 800px;
margin: auto;
}
</style>
</head>
<body>
<h1>Welcome to your Pet Store API</h1>
<p>
You have successfully deployed your first API. You are seeing this HTML page because the <code>GET</code> method to the root resource of your API returns this content as a Mock integration.
</p>
<p>
The Pet Store API contains the <code>/pets</code> and <code>/pets/{petId}</code> resources. By making a <a href="/dev/pets/" target="_blank"><code>GET</code> request</a> to <code>/pets</code> you can retrieve a list of Pets in your API. If you are looking for a specific pet, for example the pet with ID 1, you can make a <a href="/dev/pets/1" target="_blank"><code>GET</code> request</a> to <code>/pets/1</code>.
</p>
<p>
You can use a REST client such as <a href="https://www.getpostman.com/" target="_blank">Postman</a> to test the <code>POST</code> methods in your API to create a new pet. Use the sample body below to send the <code>POST</code> request:
</p>
<pre>
{
"type" : "cat",
"price" : 123.11
}
</pre>
</body>
</html>
きちんとしたコンテンツが返されたら成功です。
Cognitoオーソライザの設定
ここからはいよいよ本題であった
Cognitoオーソライザ
API GatewayのダッシュボードからAPIを選択肢、
[オーソライザー] > [新しいオーソライザーの作成]
542x1097

すると
オーソライザーの作成
myCognitoAuthorizer
Cognito
Cognitoユーザープールには上の節で設定した
testusers
またリクエストヘッダの
Authorization
以上を確認し、
[作成]
オーソライザーの有効化
オーソライザーを作成しただけではAPIの何処のメソッドにも紐付いていないため、そのままですと無効の状態です。
手始めにルート(
/
/
GET # 👈このメソッドにオーソライザを設定
/pets
GET
OPTIONS
POST
/{petId}
GET
OPTIONS
まず対象のAPI Gatewayインスタンスから
[リソース] > '/'以下のGETを選択 > [メソッドの実行]ブロック > [メソッドリクエスト] > 許可: [myCognitoAuthorizer]
1000x976

設定を反映させると、メソッドリクエストの許可に上節で作成したCognitoオーソライザーが設定されていればOKです。
設定変更したリソースは再びデプロイすることで、このオーソライザーが有効化できていると思います。
確認のためAPIエンドポイントにアクセスすると、
$ curl --include https://**************.execute-api.ap-northeast-1.amazonaws.com/dev
HTTP/2 401
date: Wed, 23 Jun 2021 14:42:22 GMT
content-type: application/json
content-length: 26
x-amzn-requestid: 3adfb5c1-d2a0-457b-bd7a-057cef8ec6dd
x-amzn-errortype: UnauthorizedException
x-amz-apigw-id: BYg8RHKTtjMFpZA=
{"message":"Unauthorized"}
という感じにトークンなしでは弾かれるようになりました。
ここで注意が必要なのは、ルート(
/
/pets
% curl --include https://*******.execute-api.ap-northeast-1.amazonaws.com/dev/pets
HTTP/2 200
date: Wed, 23 Jun 2021 14:45:05 GMT
content-type: application/json
content-length: 184
x-amzn-requestid: 154ff613-b414-43fd-9f5e-18529a369a3e
access-control-allow-origin: *
x-amz-apigw-id: BYhVuHswNjMFUQA=
x-amzn-trace-id: Root=1-60d348f1-1ac19a8e65df38a428e2d095
[
{
"id": 1,
"type": "dog",
"price": 249.99
},
{
"id": 2,
"type": "cat",
"price": 124.99
},
{
"id": 3,
"type": "fish",
"price": 0.99
}
]
...ハイ、余裕でGETできる状態です。 ということでオーソライザーはアクセス制限したい全てのメソッドに個別に設定しないといけません。
もしサイトの全てのページにユーザー認証を入れたい場合には、この例でいくと、
/
GET # 👈このメソッドにオーソライザを設定
/pets
GET # 👈このメソッドにオーソライザを設定
OPTIONS
POST # 👈このメソッドにオーソライザを設定
/{petId}
GET # 👈このメソッドにオーソライザを設定
OPTIONS
とうふうに全てのGETとPOST等にオーソライザーを仕込まないといけないことに注意してください。
ユーザーのサインインとトークンの取得
正式なログイン機能を作っていない段階ですので、まずは
cognito-idp admin-initiate-auth
$ aws cognito-idp admin-initiate-auth \
--user-pool-id '[ユーザープールID値]' \
--client-id '[アプリクライアントID]' \
--auth-flow ADMIN_NO_SRP_AUTH \
--auth-parameters 'USERNAME=tacokin-test,PASSWORD=[ユーザーパスワード]'
#👇有効期限付き各種トークンがレスポンスとなる
{
"ChallengeParameters": {},
"AuthenticationResult": {
"AccessToken": "ey...Q",
"ExpiresIn": 3600,
"TokenType": "Bearer",
"RefreshToken": "ey...A",
"IdToken": "ey...Q"
}
}
この生レスポンスから
IdToken
Authorization
$ curl --include https://*******.execute-api.ap-northeast-1.amazonaws.com/dev \
-H 'Authorization:[IDトークン]'
#👇きちんとログイン出来ている(レスポンス200OK)
HTTP/2 200
date: Wed, 23 Jun 2021 16:24:31 GMT
content-type: text/html
content-length: 1308
x-amzn-requestid: d5fc257f-513c-4c2d-abae-ff5d662d53d9
x-amz-apigw-id: BYv54ERHNjMF6IA=
<html>
...中略
</body>
</html>
これでCognitoユーザープールを使ってAPI Gatewayにユーザ認証の機能を付与することが出来ました。
ついでにトークンがない場合のレスポンスも改めて見てみると、
$ curl --include https://*******.execute-api.ap-northeast-1.amazonaws.com/dev
HTTP/2 401 date: Wed, 23 Jun 2021 16:27:38 GMTcontent-type: application/jsoncontent-length: 26
x-amzn-requestid: 70bd3137-5f6e-4cb0-a08d-78fb69a5f351
x-amzn-errortype: UnauthorizedException
x-amz-apigw-id: BYwXKHtRNjMFRhQ=
というように、レスポンスステータス401が返ってきます。
またトークンがあっても間違っていた時のレスポンスは、
$ curl --include https://********.execute-api.ap-northeast-1.amazonaws.com/dev \
-H "Authorization:hogehoge"
HTTP/2 403
date: Wed, 23 Jun 2021 16:27:06 GMT
content-type: application/json
content-length: 27
x-amzn-requestid: bfe17578-4445-443c-ae16-b51d93e04200
x-amzn-errortype: AccessDeniedException
x-amz-apigw-id: BYwSGHGYtjMFZpA=
でステータスコード403が返ってきていることも確認できます。
Angularにログイン機能を統合してみる
ここまででコマンドラインで行ったことをJavascriptのライブラリを使ってAngularアプリとして統合してみます。
引き続きPetStoreのサンプルAPIをベースに、Angularアプリを作ってみます。
途中ライブラリのビルドでコンパイルオプションでnodeタイプが必要となってきますので、プロジェクトの
tsconfig.app.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "es2015",
"types": ["node"] //👈aws-sdkで必要
},
"exclude": [
"test.ts",
"**/*.spec.ts"
]
}
不足していれば追加しておきましょう。
ログイン用のコンポーネント作成
今回はユーザー名とパスワードを入力して、ログイン/ログアウトするだけのアプリを作成します。
Angularプロジェクトの適当なフォルダに
login
$ yarn ng g component components/login --module=app
CREATE src/app/components/login/login.component.css (0 bytes)
CREATE src/app/components/login/login.component.html (20 bytes)
CREATE src/app/components/login/login.component.spec.ts (619 bytes)
CREATE src/app/components/login/login.component.ts (271 bytes)
UPDATE src/app/app.module.ts (4864 bytes)
Done in 1.11s.
CSSスタイル付けは省略しますが、
login.component.ts
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-login',
template: `
<div class="login-wrapper">
<div class="login-input">USER: <input type="text" placeholder="ユーザー名"></div>
<div class="login-input">PASSWORD: <input type="text" placeholder="パスワード"></div>
<button class="login-button" type="button">ログイン</button>
<div class="login-state">
お知らせ:{{loginInfo}}
</div>
</div>
`,
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
loginInfo: string;
constructor() {}
ngOnInit(): void {
this.loginInfo = '現在、ログインしていません。';
}
}
プロジェクトにこのコンポーネントを組み込んでビルドすると、以下のようなログインを試すだけのアプリになります。
467x320

ではここからログインのための具体的な機能を付け加えておきます。
Cognitoをjs/tsで操作するサービス
javascript(typescript)クライアントからCognitoを操作するのに必要なnpmパッケージをインストールします。
$ yarn add -S aws-sdk amazon-cognito-identity-js
ではプロジェクトの適当な場所にCognitoの操作をまとめたサービスを
cognito.service.ts
$ yarn ng g service services/cognito
CREATE src/app/services/cognito.service.spec.ts (362 bytes)
CREATE src/app/services/cognito.service.ts (136 bytes)
Done in 0.91s.
ここでcognito.service.tsを以下のような内容にします。
import { Injectable } from '@angular/core';
import { CognitoUserPool, CognitoUser, AuthenticationDetails } from 'amazon-cognito-identity-js';
import * as AWS from 'aws-sdk';
@Injectable({
providedIn: 'root'
})
export class CognitoService {
cognitoCreds: AWS.CognitoIdentityCredentials;
private userPool: CognitoUserPool;
constructor() {
AWS.config.region = 'ap-northeast-1';//👈AWSサービスを配置したリージョンを指定
this.userPool = new CognitoUserPool({
UserPoolId: '[ユーザープールID]', //👈ユーザープールIDに書きかえ
ClientId: '[アプリクライアントID]'//👈アプリクライアントIDに書きかえ
});
}
//ログイン
login(username: string, password: string): Promise<any> {
const cognitoUser = new CognitoUser({
Username: username,
Pool: this.userPool,
Storage: localStorage
});
const authenticationDetails = new AuthenticationDetails({
Username: username,
Password: password,
});
return new Promise((resolve, reject) => {
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: (result) => {
alert('Login has done!');
let msg = `Id token: ${result.getIdToken().getJwtToken()}\n`;
msg += `Access token: ${result.getAccessToken().getJwtToken()}\n`;
msg += `Refresh token: ${result.getRefreshToken().getToken()}`;
console.log(msg);
resolve(msg);
},
onFailure: (err) => {
alert(err.message);
reject(err);
}
});
});
}
//ログイン状態確認
isAuthenticated(): Promise<any> {
const cognitoUser = this.userPool.getCurrentUser();
return new Promise((resolve, reject) => {
cognitoUser === null && resolve(cognitoUser);
cognitoUser.getSession((err: any, session: any) => {
err ? reject(err) : (!session.isValid() ? reject(session) : resolve(session));
});
});
}
//IDトークン取得
getCurrentUserIdToken(): any {
const cognitoUser = this.userPool.getCurrentUser();
let rslt = null;
cognitoUser != null && cognitoUser.getSession((err: any, session: any) => {
if (err) {
alert(err);
} else {
rslt = session.getIdToken().getJwtToken();
}
});
return rslt;
}
//ログアウト
logout() {
alert('Logout');
const currentUser = this.userPool.getCurrentUser();
currentUser && currentUser.signOut();
}
}
このサービスを
login.component.ts
import { Component, OnInit } from '@angular/core';
import { CognitoService } from '../../services/cognito.service';
@Component({
selector: 'app-login',
template: `
<div class="login-wrapper">
<div class="login-input">USER: <input type="text" placeholder="ユーザー名" id="username" name="username" #username></div>
<div class="login-input">PASSWORD: <input type="password" placeholder="パスワード" id="password" name="password" #password></div>
<button class="login-button" type="button" (click)="login(username.value, password.value)">ログイン</button>
<div class="login-state">
お知らせ: {{loginInfo}}
</div>
</div>
`,
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
loginInfo: string;
constructor(
private cognito: CognitoService
) {}
ngOnInit(): void {
this.loginInfo = '現在、ログインしていません。';
}
async login(username: string, password: string): Promise<any> {
try {
const result = await this.cognito.login(username, password);
this.loginInfo = JSON.stringify(result);
} catch(e) {
console.log(e);
}
}
}
これでアプリを立ち上げて、ユーザー名とパスワードを打ち込んでログインしてみると、
1020x693

IDトークンなどの認証情報が取得できました。
ログイン・ログアウトを切り替える
ここまででログインができていることが確認できましたが、出来れば一つのボタンでログイン状況に応じてログイン・ログアウトを行いたいので、更に
login.component.ts
import { Component, OnInit } from '@angular/core';
import { CognitoService } from '../../services/cognito.service';
@Component({
selector: 'app-login',
template: `
<div class="login-wrapper">
<div class="login-input">USER: <input type="text" placeholder="ユーザー名" id="username" name="username" #username></div>
<div class="login-input">PASSWORD: <input type="password" placeholder="パスワード" id="password" name="password" #password></div>
<button class="login-button" type="button" (click)="login(username.value, password.value)">{{buttonTitle}}</button>
<div class="login-state">
お知らせ: {{loginInfo}}
</div>
</div>
`,
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
loginInfo: string;
buttonTitle: string;
constructor(
private cognito: CognitoService
) {}
ngOnInit(): void {
this.buttonTitle = 'ログイン'
this.loginInfo = '現在、ログインしていません。';
}
async login(username: string, password: string): Promise<any> {
try {
const isLogin = await this.cognito.isAuthenticated();
console.log(isLogin);
if(isLogin === null) {
const result = await this.cognito.login(username, password);
this.loginInfo = JSON.stringify(result);
this.buttonTitle = 'ログアウト';
} else {
this.cognito.logout();
this.buttonTitle = 'ログイン';
this.loginInfo = 'ログアウトしました。';
}
} catch(e) {
if(e === null) {
this.cognito.logout();
this.buttonTitle = 'ログイン';
this.loginInfo = 'セッションの有効期限切れです。';
}
}
}
}
ここでのポイントはcognito.service.tsの
isAuthenticated
1000x435

IDトークンを使ってリソースにアクセス
ログイン/ログアウトの機能が正常に動作していることが確認できましたので、最後に取得したJWTトークンをリクエストヘッダのAuthorizationプロパティに仕込んで、リソースにアクセスしてみます。
CORSの有効化
最近のブラウザではCORS周りのセキュリティ強化を背景に、Angularアプリ開発での定石であるローカルから
http://localhost:4200
最終的なプロダクトではlocalhostサーバーは使わないので考慮しなくても宜しいのですが、今回に限って言えば、
http://localhost:4200
そこでAPI GatewayをCORS対応に再設定する必要があります。
API GatewayのダッシュボードからAPIを選択して、
[リソース] > /petsを選択 > [アクション] > [CORSの有効化]
1146x1176

ここで
CORSの有効化
Access-Control-Allow-Headers
'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,access-control-allow-origin,access-control-allow-headers,x-content-type-options'
あとはすべてデフォルトでOKです。 CORSを有効化させたあとは、変更したリソースをデプロイし直すことを忘れずに行う必要があります。
Httpインターセプターでリクエストヘッダを置き換える
AngularアプリでHTTPリクエストヘッダを書き換えるなどの操作は
HttpInterceptorクラス
ということで
get-pet-interceptor.service.ts
$ yarn ng g service interceptors/getPetInterceptor
早速この
get-pet-interceptor.service.ts
import { Injectable } from '@angular/core';
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpClient
} from '@angular/common/http';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { Observable } from 'rxjs';
import { CognitoService } from '../services/cognito.service';
export class Pet {
id: number;
type: string;
price: number;
}
@Injectable({
providedIn: 'root'
})
export class GetPetService {
//👇/petsにアクセスする
private Url = 'https://**********/.execute-api.ap-northeast-1.amazonaws.com/dev/pets';
constructor(
private http: HttpClient
) { }
getPets(): Observable<Pet[]> {
return this.http.get<Pet[]>(this.url_);
}
}
///👇Authorizationヘッダ用のインターセプター
@Injectable({
providedIn: 'root'
})
export class GetPetInterceptor implements HttpInterceptor {
constructor(
private cognito: CognitoService
) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
//👇現在のIDトークンを取得
const authHeader = this.cognito.getCurrentUserIdToken();
//👇オリジナルのリクエストヘッダーを複製し、IDトークンを追加したものに差替え
const authReq = req.clone({
headers: req.headers.set('Authorization', authHeader)
});
//👇変形したリクエストとして送信側へ流す
return next.handle(authReq);
}
}
export const GET_PET_PROVIDER = {
provide: HTTP_INTERCEPTORS,
useClass: GetPetInterceptor,
multi: true
};
HTTPインターセプタを有効にするためには、
app.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
//...
//....中略
//👇インターセプタ用のプロバイダインスタンス
import { GET_PET_PROVIDER } from './interceptors/get-pet-interceptor.service';
//....中略
@NgModule({
declarations: [
//....中略
],
providers: [
//....
//👇HTTPリクエストの度にバックグラウンドで実行される
GET_PET_PROVIDER
],
//....以下略
最終的には
yarn serve
三度、
login.component.ts
import { Component, OnInit } from '@angular/core';
import { take, tap } from 'rxjs/operators';
import { CognitoService } from '../../services/cognito.service';
import { GetPetService } from '../../interceptors/get-pet-interceptor.service';
@Component({
selector: 'app-login',
template: `
<div class="login-wrapper">
<div class="login-input">USER: <input type="text" placeholder="ユーザー名" id="username" name="username" #username></div>
<div class="login-input">PASSWORD: <input type="password" placeholder="パスワード" id="password" name="password" #password></div>
<button class="login-button" type="button" (click)="login(username.value, password.value)">{{buttonTitle}}</button>
<div class="login-state">
<p>お知らせ:</p>
{{loginInfo}}
</div>
</div>
`,
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
loginInfo: string;
buttonTitle: string;
constructor(
private cognito: CognitoService,
private getpet: GetPetService
) {}
ngOnInit(): void {
this.buttonTitle = 'ログイン'
this.loginInfo = '現在、ログインしていません。';
}
async login(username: string, password: string): Promise<any> {
try {
const isLogin = await this.cognito.isAuthenticated();
console.log(isLogin);
if(isLogin === null) {
const result = await this.cognito.login(username, password);
this.getpet.getPets().pipe(take(1)).subscribe((res: any[]) => {
this.loginInfo = '';
for (const item of res) {
this.loginInfo += `${JSON.stringify(item)}\n`;
}
});
this.buttonTitle = 'ログアウト';
} else {
this.cognito.logout();
this.buttonTitle = 'ログイン';
this.loginInfo = 'ログアウトしました。';
}
} catch(e) {
if(e === null) {
this.cognito.logout();
this.buttonTitle = 'ログイン';
this.loginInfo = 'セッションの有効期限切れです。';
}
}
}
}
きちんとログインできたと同時に
/pets
363x314

まとめ
今回は少し長めのテクニカル解説になってしまいましたが、最後まで見ていただきまして有難うございました。
この記事の内容で、AngularとAWS API GatewayとCognitoを連携させるまでの概要が一通り網羅されたように思います。
最終的にはウェブショップなどのもう少し実用的なアプリにしてみたい気はしますが、また時間があるときに実践的な話を解説していきたいと思います。
参考サイト
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー