カテゴリー
【Angularユーザーのための認証API自作講座①】AWS Cognitoでセキュアなユーザー認証を自力で構築する
※ 当ページには【広告/PR】を含む場合があります。
2021/06/24
2022/08/10
Auth0の料金が割高に感じた時のCognito
Essential($0.023/MAU)
Professional($0.24/MAU)
$228
月間ユーザ数/MAU | MAU単価 |
---|---|
0〜5万 | 無料 |
5〜10万 | $0.00550 |
10〜100万 | $0.00460 |
100〜1000万 | $0.00325 |
1000万以上 | $0.00250 |
$0.00550 * (70000 - 50000) = $110 ~ 12000円程度
Cognitoオーソライザ付きのAPI Gatewayによるユーザー認証
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でユーザープールの作成
ユーザープール
[ユーザープールの管理] > [ユーザープールを作成する]
testusers
[デフォルトを確認する] > [プールの作成]
[アプリクライアント] > [アプリクライアントの追加]
myAuthApp
クライアントシークレットを生成
認証用の管理 API のユーザー名パスワード認証を有効にする
[アプリクライアントの作成]
ユーザープールにユーザーを追加する
[ユーザーとグループ] > [ユーザー]タブ > [ユーザーの作成]
tacokin-test
仮パスワード
電話番号
Eメール
Eメールを検証済みにしましすか?
この新規ユーザーに招待を送信しますか?
仮パスワード
$ 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.
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
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"
}
}
$ aws cognito-idp list-users --user-pool-id [ユーザープールID]
{
"Users": [
{
"Username": "tacokin-test",
"Attributes": [
#...中略
#👇ユーザー登録が完了している
"UserStatus": "CONFIRMED"
}
]
}
API Gatewayの設定
API Gatewayの作成
[APIを作成] > [APIタイプを選択] > [インポート] > プロトコルを選択: [REST] > 新しいAPIの作成: [APIの例]
[リソース] > ルートのリソースを選択 > [アクション] > [APIのデプロイ]
dev
$ 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オーソライザ
[オーソライザー] > [新しいオーソライザーの作成]
オーソライザーの作成
myCognitoAuthorizer
Cognito
testusers
Authorization
[作成]
オーソライザーの有効化
/
/
GET # 👈このメソッドにオーソライザを設定
/pets
GET
OPTIONS
POST
/{petId}
GET
OPTIONS
[リソース] > '/'以下のGETを選択 > [メソッドの実行]ブロック > [メソッドリクエスト] > 許可: [myCognitoAuthorizer]
$ 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 # 👈このメソッドにオーソライザを設定
/pets
GET # 👈このメソッドにオーソライザを設定
OPTIONS
POST # 👈このメソッドにオーソライザを設定
/{petId}
GET # 👈このメソッドにオーソライザを設定
OPTIONS
ユーザーのサインインとトークンの取得
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>
$ 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=
$ 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=
Angularにログイン機能を統合してみる
tsconfig.app.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "es2015",
"types": ["node"] //👈aws-sdkで必要
},
"exclude": [
"test.ts",
"**/*.spec.ts"
]
}
ログイン用のコンポーネント作成
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.
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 = '現在、ログインしていません。';
}
}
Cognitoをjs/tsで操作するサービス
$ yarn add -S aws-sdk amazon-cognito-identity-js
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.
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);
}
}
}
ログイン・ログアウトを切り替える
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 = 'セッションの有効期限切れです。';
}
}
}
}
isAuthenticated
IDトークンを使ってリソースにアクセス
CORSの有効化
http://localhost:4200
http://localhost:4200
[リソース] > /petsを選択 > [アクション] > [CORSの有効化]
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'
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
};
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
まとめ
参考サイト
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー