AWS Amplify+Angular6+Cognitoでログインページを作ってみる ~フロントエンド編①~

本ブログではAWS Amplify+Angular6+Cognitoでログインページを作っていきます。また、ついでに、API GatewayにもCognitoで認証をかけて、Cognitoでログインしているユーザのみ利用できるようにしていきます。
2018.06.16

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

どうも!大阪オフィスの西村祐二です。

本ブログは下記の続きになります。

AWS Amplify+Angular6+Cognitoでログインページを作ってみる ~バックエンド編~

ゴールとして下記動画のようなサイトを構築していきます。

いよいよですが、Angular6とAWS-Amplifyを使ってフロントエンド部分をやっていきます。今回はフロントエンド部分の設定周りと、外部と通信するところのロジック部分の実装になります。

基本的な実装部分やAWS-Amplifyの実装部分は下記ブログを参考にさせていただきました。m(_ _)m

AWS Amplify + AngularでサーバーレスSPAの認証をするサンプル

Angularでログインページ作成

環境

  • Angular
Angular CLI: 6.0.8
Node: 8.11.3
OS: darwin x64
Angular: 6.0.5
... animations, common, compiler, compiler-cli, core, forms
... http, language-service, platform-browser
... platform-browser-dynamic, router

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.6.8
@angular-devkit/build-angular     0.6.8
@angular-devkit/build-optimizer   0.6.8
@angular-devkit/core              0.6.8
@angular-devkit/schematics        0.6.8
@angular/cli                      6.0.8
@ngtools/webpack                  6.0.8
@schematics/angular               0.6.8
@schematics/update                0.6.8
rxjs                              6.2.1
typescript                        2.7.2
webpack                           4.8.3
  • AWS Amplify: 0.4.4

  • Bootstrap: 4.1.1

Angular CLIインストール

AngularのCLIをインストールします。

$ npm install -g @angular/cli@latest

雛形作成

CLIを使って、雛形を作成します。

$ ng new test-amplify --routing

$ cd test-amplify

パッケージインストール

今回使うパッケージをインストールします。

  • AWS-Amplify
$ npm install --save aws-amplify
  • Bootstrap4
$ npm install --save bootstrap && npm install --save jquery popper.js

環境設定

▼aws-sdkを使うためにnodeを定義します。

tsconfig.app.json

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "module": "es2015",
    "types": [
      "node"
    ]
  },
  "exclude": [
    "src/test.ts",
    "**/*.spec.ts"
  ]
}

▼Cognitoの設定、API Gatewayのエンドポイントの情報を設定ファイルに記述します。

src/environments/environment.ts

export const environment = {
  production: false,
  amplify: {
    // AWS Amplify(Auth)の設定
    Auth: {
      region: 'ap-northeast-1',
      userPoolId: 'ap-northeast-1_xxxxxxxxxx',
      userPoolWebClientId: 'xxxxxxxxxxxxxxxxx'
    }
  },
  // API Gatewayのエンドポイントの設定
  apiBaseUrl: 'https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/stg',
  // Localstorageの設定
  localstorageBaseKey: 'CognitoIdentityServiceProvider.<userPoolWebClientIdの値>.'
};

今回、特にCognito IDプールは使わないので、設定していません。AWS-AmplifyのAPIクラスを利用する場合はCognito IDプールを設定する必要があるので注意ください。今回はAWS-AmplifyのAPIクラスは利用せず、AngularのRxJSのメソッドを使ってHTTPリクエストなどを行っていきます。

ログイン後にトークンやユーザ情報をローカルストレージから取得するため、ローカルストレージの設定を行っています。ログイン時に保存されるキーの値として、CognitoIdentityServiceProvider.<userPoolWebClientIdの値>.<属性>として保存されています。

ちなみに、ローカルストレージの確認方法として、Chromeの場合はデベロッパーツールのApplicationから確認できます。

▼UIにBootstrap4を利用するので、ファイルを読み込みます。

angular.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "test-amplify": {
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "prefix": "app",
      "schematics": {},
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/test-amplify",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.app.json",
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.css",
              "node_modules/bootstrap/dist/css/bootstrap.min.css"
            ],
            "scripts": [
              "node_modules/jquery/dist/jquery.slim.min.js",
              "node_modules/popper.js/dist/umd/popper.min.js",
              "node_modules/bootstrap/dist/js/bootstrap.min.js"
            ]
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true
            }
          }
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "test-amplify:build"
          },
          "configurations": {
            "production": {
              "browserTarget": "test-amplify:build:production"
            }
          }
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "test-amplify:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "main": "src/test.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.spec.json",
            "karmaConfig": "src/karma.conf.js",
            "styles": [
              "src/styles.css",
              "node_modules/bootstrap/dist/css/bootstrap.min.css"
            ],
            "scripts": [
              "node_modules/jquery/dist/jquery.slim.min.js",
              "node_modules/popper.js/dist/umd/popper.min.js",
              "node_modules/bootstrap/dist/js/bootstrap.min.js"
            ],
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ]
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": [
              "src/tsconfig.app.json",
              "src/tsconfig.spec.json"
            ],
            "exclude": [
              "**/node_modules/**"
            ]
          }
        }
      }
    },
    "test-amplify-e2e": {
      "root": "e2e/",
      "projectType": "application",
      "architect": {
        "e2e": {
          "builder": "@angular-devkit/build-angular:protractor",
          "options": {
            "protractorConfig": "e2e/protractor.conf.js",
            "devServerTarget": "test-amplify:serve"
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": "e2e/tsconfig.e2e.json",
            "exclude": [
              "**/node_modules/**"
            ]
          }
        }
      }
    }
  },
  "defaultProject": "test-amplify"
}

AuthService

Amazon Cognitoと通信する認証系の処理をまとめたサービスを作成します。

$ ng g service auth/auth

コンストラクタでAmplify.configure()に設定ファイルを渡し初期化しています。

AWS AmplifyのAuthモジュールの機能をハンドリングするメソッドを実装していきます。

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Observable, BehaviorSubject, from, of } from 'rxjs';
import { map, tap, catchError } from 'rxjs/operators';

import Amplify, { Auth } from 'aws-amplify';
import { environment } from './../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  loggedIn: BehaviorSubject<boolean>;
  password: String;
  constructor(private router: Router) {
    Amplify.configure(environment.amplify);
    this.loggedIn = new BehaviorSubject<boolean>(false);
  }

  /** サインアップ */
  public signUp(email, password): Observable<any> {
    this.password = password;
    return from(Auth.signUp(email, password, email));
  }

  /** 検証 */
  public confirmSignUp(email, code): Observable<any> {
    return from(Auth.confirmSignUp(email, code));
  }

  /** ログイン */
  public signIn(email, password): Observable<any> {
    return from(Auth.signIn(email, password)).pipe(
      tap(() => this.loggedIn.next(true))
    );
  }

  /** ログインユーザ情報の取得 */
  public getData(): Observable<any> {
    return from(Auth.currentAuthenticatedUser());
  }

  /** idtokenを取得 */
  public getIdToken(): string {
    return Auth.currentSession()['__zone_symbol__value']['idToken']['jwtToken'];
  }


  /** ログイン状態の取得 */
  public isAuthenticated(): Observable<boolean> {
    return from(Auth.currentAuthenticatedUser()).pipe(
      map(result => {
        this.loggedIn.next(true);
        return true;
      }),
      catchError(error => {
        this.loggedIn.next(false);
        return of(false);
      })
    );
  }

  /** ログアウト */
  public signOut() {
    from(Auth.signOut()).subscribe(
      result => {
        this.loggedIn.next(false);
        this.router.navigate(['/login']);
      },
      error => console.log(error)
    );
  }
}
  • loggedIn(BehaviorSubject)
    • グローバルなナビゲーションの表示を切り替えるために使っています。つまり、ログイン状態だったら、ナビバーのログイン画面へのリンクを非表示にしたり制御するための設定です。
  • password: String
    • サインアップが完了したあと、ログインした後のページに遷移させたいために、キャッシュするようにしています。他に良い方法があれば教えていただけると助かります。
  • signUp()、confirmSignUp()、signIn()、getData()
    • AWS AmplifyのAuthモジュールのメソッドが返すPromiseをRxJSのfrom(旧fromPromise)でPromiseからObservableに変換しています。
  • getIdToken()
    • API GatewayにはCognito認証をかけており、APIをリクエストするためにはログイン時に発行されるjwtトークンが必要になります。そのトークンを取得するメソッドになります。取得するために、Auth.currentAuthenticatedUser()からString型でトークンだけを返すようにしています。
  • isAuthenticated()
    • ログイン状態にある場合にはAuth.currentAuthenticatedUser()がPromiseのユーザー情報オブジェクトを返すため、それをBooleanのObservableに変換しています。後ほど実装するGuardで使うメソッドではTruefalseBooleanで返す必要があるためです。
  • signOut()
    • Auth.signOut()を呼びつつ、ログイン画面に遷移させます。

AuthGuard

AngularのGuardでは、ルーティング前にセッションチェックなどの処理を行って、未ログインの場合、ログインページにルーティングするなどの制御を行うことができます。

$ ng g guard auth/auth

今回は、CanActivateの機能を使って、ログイン状態にない場合に、login画面に画面遷移させるGuardを作成します。 CanActivateはPromiseまたは、Observableの非同期処理を行った結果のbooleanを戻り値に取ることが出来るので、 先ほどのAuthServiceで作成したisAuthenticated()メソッドの結果を使っています。

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable, of } from 'rxjs';
import { map, tap, catchError } from 'rxjs/operators';
import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  constructor(private router: Router, private auth: AuthService) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> {
    return this.auth.isAuthenticated().pipe(
      tap(loggedIn => {
        if (!loggedIn) {
          this.router.navigate(['/login']);
        }
      })
    );
  }
}

PetService

API GatewayのAPIに対してリクエストして、ペット情報一覧を取得します。

$ ng g service pet/pet
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from '../../environments/environment';

import { Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class PetService {
  private Url = environment.apiBaseUrl + '/pets';

  constructor(private http: HttpClient) {}

  public getPets(token: string): Observable<any> {
    const httpOptions = {
      headers: { Authorization: token }
    };
    return this.http.get<any>(this.Url, httpOptions).pipe(
      tap(users => users),
      catchError(this.handleError('getFile', []))
    );
  }

  private handleError<T>(operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {
      console.error(error); // log to console instead

      this.log(`${operation} failed: ${error.message}`);

      return of(result as T);
    };
  }

  private log(message: string) {
    console.log('petService: ' + message);
  }
}
  • getPets(token: string)
    • APIリクエストをする処理を書いています。APIにはCognito認証設定がされているので、その際に必要となるトークンをヘッダーに付与してリクエストするようにしています。

HTTPリクエストの箇所は公式サイトのチュートリアルがとても参考になります。

私がはじめたころは見つけられなかったのですが、日本語のサイトもあるのでスムーズに勉強できるかと思います。翻訳してくださった方ほんと感謝です。

https://angular.jp/tutorial/toh-pt6

AppRoutingModule

ルーティングの定義を行います。 プロジェクト作成時に--routingオプションを指定しているので、src/app/app-routing.module.tsが作成されているかと思います。

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './component/login/login.component';
import { SignupComponent } from './component/signup/signup.component';
import { HomeComponent } from './component/home/home.component';
import { PetComponent } from './component/pet/pet.component';

import { AuthGuard } from './auth/auth.guard';

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
  { path: 'login', component: LoginComponent },
  { path: 'signup', component: SignupComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

HomeComponentLoginComponentSignupComponentの3つのページがあり、HomeComponentはログインしたユーザーのみが閲覧できるようにするため、AuthGuardでルーティングまえに検証するようにしています。ペット情報の取得と一覧表示を行うPetComponentもありますが、こちらは、HomeComponentの子コンポーネントとします。

※各ページのコンポーネントについては次のブログで紹介します。少々お待ちください。

さいごに

いかがだったでしょうか。

Angular6とAWS-Amplifyを使ってフロントエンド部分の設定周りと、Amazon Cognito、API Gatewayと通信するところのロジック部分の実装を行いました。

次は画面を構成するコンポーネント部分の実装やっていきます。

参考サイト

AWS Amplify + AngularでサーバーレスSPAの認証をするサンプル