Auth0 + Angularで認証機能をお手軽に実装しよう

  • 31
    いいね
  • 0
    コメント

Angular Advent Calendar 2016 の5日目です。ちきさんです。

今回のGitHubリポジトリは ovrmrw/qiita-advent-calendar-2016-angular-1 です。オンラインデモは こちら です。


Auth0 を使って認証機能を組み込んだAngularアプリをお手軽に作る方法をご紹介します。本当に簡単です。1時間ぐらいで出来ますよ。

「Auth0って何?」「なんでAuth0が必要なの?」という皆様には Why Auth0 の動画をオススメ致します。これを観れば誰でも使いたくなる。

それではさっそく始めましょう。

まずは angular-cli をインストールしますね。

$ npm i -g angular-cli

そしてng newして新しいプロジェクトを作ります。僕の非力なノートPCではお風呂に入れるぐらいの時間がかかります。

$ ng new my-awesome-project

さてここから先はAuth0の 公式ドキュメント にある程度従って進めていきましょう。
今回RxJSは一切用いません。とてもチャレンジングなプログラミングです。

auth0-lock, angular2-jwt

Auth0を利用するのに必要なライブラリをインストールします。

$ npm i -S auth0-lock angular2-jwt
$ npm i -D @types/auth0-lock

auth.service.ts

Auth0をハンドリングするサービスを作成します。公式ドキュメントにほぼ沿った形です。

src/lib/auth.service.ts
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { tokenNotExpired } from 'angular2-jwt';
import Auth0Lock from 'auth0-lock';


const AUTH0_CLIENT_ID = 'your-auth0-client-id';
const AUTH0_DOMAIN = 'your-auth0-domain';
const auth0Options = {
  auth: {
    redirect: false
  },
  autoclose: true
};


@Injectable()
export class AuthService {
  // Configure Auth0
  lock = new Auth0Lock(AUTH0_CLIENT_ID, AUTH0_DOMAIN, auth0Options);

  // Store profile object in auth class
  userProfile: Object;


  constructor(
    private router: Router,
  ) {
    // Set userProfile attribute of already saved profile
    this.userProfile = JSON.parse(localStorage.getItem('profile'));

    // Add callback for the Lock `authenticated` event
    this.lock.on('authenticated', (authResult) => {
      localStorage.setItem('id_token', authResult.idToken);

      // Fetch profile information
      this.lock.getProfile(authResult.idToken, (error, profile) => {
        if (error) {
          // Handle error
          alert(error);
          return;
        }

        localStorage.setItem('profile', JSON.stringify(profile));
        this.userProfile = profile;

        this.router.navigate(['/secret']);
      });
    });
  }


  login() {
    // Call the show method to display the widget.
    this.lock.show();
  };


  authenticated() {
    // Check if there's an unexpired JWT
    // This searches for an item in localStorage with key == 'id_token'
    return tokenNotExpired();
  };


  logout() {
    // Remove token and profile from localStorage
    localStorage.removeItem('id_token');
    localStorage.removeItem('profile');
    this.userProfile = undefined;

    this.router.navigate(['/welcome']);
  };
}
  • constructor
    • Lockウィジェットを使って認証されたときの処理を記述しておく。ログインしたらlocalStorageにトークンやプロフィールを書き込む。
  • loginメソッド
    • ログイン処理を行う。といってもLockウィジェットを起動するだけ。
  • authenticatedメソッド
    • tokenNotExpired()を実行してトークンが有効かどうかを確認する。現在ログインしているかどうかを判断できる。
  • logoutメソッド
    • ログイン時に書き込んでいたlocalStorageの情報を消したりWelcomeページに遷移させたり。

auth.guard.ts

RoutingのGuardを作っておきます。Auth0の公式ドキュメントにはありません。
Guardのメリットは認証されていない場合にルーティングを拒否できることです。

src/lib/auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthService } from './auth.service';


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


  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    if (this.auth.authenticated()) {
      return true;
    } else {
      this.router.navigate(['/welcome']);
      alert('You are not signed in.');
      return false;
    }
  }

}
  • canActivateメソッド
    • ログイン状態であればtrueを返す。ページ遷移が許可される。
    • ログアウト状態であればfalseを返す。ページ遷移がブロックされる。ついでにWelcomeページに飛ばしている。

lib.module.ts

ある程度のマイライブラリの集合でNgModuleを作っておきます。その方が後々メンテしやすくなります。

src/lib/lib.module.ts
import { NgModule } from '@angular/core';
import { AUTH_PROVIDERS } from 'angular2-jwt';

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


@NgModule({
  providers: [
    AUTH_PROVIDERS,
    AuthService,
    AuthGuard,
  ],
})
export class LibModule { }

app.routes.ts

Routingの定義を作ります。先程作成したAuthGuardクラスはここに登場します。

src/app/app.routes.ts
import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { WelcomeComponent } from './welcome.component';
import { SecretComponent } from './secret.component';
import { AuthGuard } from '../lib';


const appRoutes: Routes = [
  {
    path: '',
    redirectTo: '/welcome',
    pathMatch: 'full'
  },
  {
    path: 'welcome',
    component: WelcomeComponent
  },
  {
    path: 'secret',
    component: SecretComponent,
    canActivate: [AuthGuard]
  }
];


export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);

WelcomeページとSecretページがあり、SecretページはAuthGuardで守られている、ということだけわかればOKです。

app.component.ts

トップレベルのComponentです。主にナビゲーションを管理します。
ログイン状態に応じてログインボタンが変化することがわかるでしょうか。

src/lib/app.componen.ts
import { Component } from '@angular/core';
import { AuthService } from '../lib';


@Component({
  selector: 'app-root',
  template: `  
    <nav class="navbar navbar-light bg-faded">
      <a class="navbar-brand" href="#">(Auth0 - Angular 2)</a>
      <ul class="nav navbar-nav">
        <li class="nav-item" routerLinkActive="active">
          <a class="nav-link" [routerLink]="['/welcome']">Welcome</a>
        </li>
        <li class="nav-item" routerLinkActive="active">
          <a class="nav-link" [routerLink]="['/secret']">Secret</a>
        </li>
      </ul>
      <div class="float-xs-right">
        <button class="btn btn-outline-primary btn-margin" (click)="auth.login()" *ngIf="!auth.authenticated()">Log In</button>
        <button class="btn btn-outline-danger btn-margin" (click)="auth.logout()" *ngIf="auth.authenticated()">Log Out</button>
      </div>
    </nav>
    <router-outlet></router-outlet>
  `,
})
export class AppComponent {
  constructor(
    private auth: AuthService,
  ) { }
}

ログインボタンの記述は公式ドキュメントと同じです。AuthServiceクラスのメソッドであるlogin()とかauthenticated()をtemplateから直接コールしているところがキモいのですが、そこは敢えてそのままにしています。直すのもめんどくさかった。

この点に関しては はちさん(@armorik83)アンチパターンであると注意を促しています。

そもそも外部クラスのメソッドをtemplateで実行することで状態を取得するということ自体がイケてないです。ここは普通ならRxJSでリアクティブにするところですね。

welcome.component.ts

Welcomeページを用意します。ログアウトすると強制的にここに飛ばされます。

src/app/welcome.component.ts
import { Component } from '@angular/core';
import { AuthService } from '../lib';


@Component({
  selector: 'app-welcome',
  template: `
    <h2>Welcome Page</h2>
    <div>{{loginState()}}</div>
  `
})
export class WelcomeComponent {
  constructor(
    private auth: AuthService,
  ) { }


  loginState(): string {
    if (this.auth.authenticated()) {
      return 'ログインしています。';
    } else {
      return 'ログインしていません。';
    }
  }
}

secret.component.ts

Secretページを用意します。RoutingのGuardが効いているため、ログインしていないと表示できません。
内容はAuth0が取得したユーザープロフィールを表示するだけの簡単なページです。

src/app/secret.component.ts
import { Component } from '@angular/core';
import { AuthService } from '../lib';


@Component({
  selector: 'app-secret',
  template: `
    <h2>Secret Page</h2>
    <hr />
    <pre>{{auth.userProfile | json}}</pre>
  `
})
export class SecretComponent {
  constructor(
    private auth: AuthService,
  ) { }
}

app.module.ts

自作したものを色々追加して最終的なapp.module.tsはこのようになりました。

src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { routing } from './app.routes';
import { AppComponent } from './app.component';
import { WelcomeComponent } from './welcome.component';
import { SecretComponent } from './secret.component';

import { LibModule } from '../lib';


@NgModule({
  declarations: [
    AppComponent,
    SecretComponent,
    WelcomeComponent,
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    routing,
    LibModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

angular-cliのおかげでコーディング作業はこれぐらいです。

実際に オンラインデモ を動かしてみるとログアウトしている状態ではSecretページに遷移できないことがわかるかと思います。

認証に関する処理はほとんど書きませんでしたが、このように簡単に認証機能を組み込めるAuth0はとても便利だなと思います。皆さんも是非試してみてください。

今年11月に発表した 「Auth0を使ってサインインを一括管理したりAzure Functionsをセキュアにしたりした話」 も併せてどうぞ。

明日は @mstssk さんです。

(僕がAuth0をよく触るようになったいきさつを Microservices Advent Calendar 2016 5日目 で紹介しています。)