Angular向けのAuth0 SDKを試してみる #Auth0 #Angular

Auth0公式SDKを使ってAngularにAuth0の認証を組み込む方法をご紹介します。
2020.10.30

こんにちは、クラスメソッドの岡です。

Auth0ではSPA用のSDK(@auth0/auth0-spa-js)が提供されていますが、いつの間にかAngular専用の SDK, auth0-angularがリリースされていたので早速試してみました!

ちなみにReact用のSDKもこちらの記事で紹介されています。

React向けの新しいAuth0 SDKが登場してました

React SDK と同様に、内部的には auth0-spa-js を利用していますが、さらに簡易的な記述でログインなどの認証周りの実装が可能になります。

動作確認環境

  • Angular: 10.1.5
  • @auth0/auth0-angular: 1.0.0

試してみる

セットアップ

まずはAngularアプリを作成してライブラリをインストールします。

$ ng new sample-auth0-angular
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? Stylus

$ ng add @auth0/auth0-angular
$ npm install --save -D @types/auth0-angular

ログイン機能の実装

今回は一番実装コストが低いユニバーサルログインの機能を使います。

$ ng g component components/pages/login

environment.ts

export const environment = {
  production: false,
  AUTH0_DOMAIN: 'your-tenant-name.us.auth0.com',
  AUTH0_CLIENT_ID: 'tCJlygg99ZrRik5M7rVh0VFDsfFW', // Auth0上に作成したSINGLE PAGE APPLICATIONのClient ID
  API_URL: 'https://example.com/v1', // APIのエンドポイント
  AUDIENCE: 'https://example.com/v1', // Auth0上に作成したCUSTOM APIのaudience
};

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AuthModule } from '@auth0/auth0-angular';
import { AppRoutingModule } from './app-routing.module';
import { environment } from './../environments/environment';
import { AppComponent } from './app.component';
import { LoginComponent } from './components/pages/login/login.component';

@NgModule({
  declarations: [AppComponent, LoginComponent],
  imports: [
    BrowserModule,
    AppRoutingModule,
    AuthModule.forRoot({
      domain: environment.AUTH0_DOMAIN,
      clientId: environment.AUTH0_CLIENT_ID,
      audience: environment.AUDIENCE,
      redirectUri: `${window.location.origin}`,
    }),
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

AuthModuleのインポート文を追加してAuthModule.forRoot()にAuth0のドメインと事前にAuth0上に作成したSINGLE PAGE APPLICATIONのClient IDを追加します。

login.component.html

<button (click)="login()">Log in</button>

ユニバーサルログインの画面に遷移させるためにボタンのみ設置しておきます。

login.component.ts

import { Component, OnInit } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.styl'],
})
export class LoginComponent implements OnInit {
  constructor(public auth: AuthService) {}

  ngOnInit(): void {}

  login(): void {
    // Call this to redirect the user to the login page
    this.auth.loginWithRedirect();
  }
}

ボタンからの動線に login() を追加して AuthServiceloginWithRedirect を呼び出せるようにしておきます。 これだけでログイン機能は完成です!

AuthGuardの実装

ログインができるようになったら、ログイン後の遷移先を用意します。

$ ng g component components/pages/home

ログインしたユーザーしか表示できないようroutingに AuthGuard を追加します。

app-routing.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@auth0/auth0-angular';
import { LoginComponent } from './components/pages/login/login.component';
import { HomeComponent } from './components/pages/home/home.component';

const routes: Routes = [
  { path: '', component: HomeComponent, canActivate: [AuthGuard] },
  { path: 'login', component: LoginComponent },
];

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

home.component.html

<ul *ngIf="auth.user$ | async as user">
    <li>{{ user.name }}</li>
    <li>{{ user.email }}</li>
</ul>

<button *ngIf="auth.isAuthenticated$ | async" (click)="logout()">
    Log out
</button>

ユーザー名の表示とログアウトボタンのみ設置しておきます。

home.component.ts

import { Component, OnInit } from '@angular/core'
import { AuthService } from '@auth0/auth0-angular'


@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.styl'],
})
export class HomeComponent implements OnInit {
  userInfo: {[key: string]: any} = {}
  constructor(public auth: AuthService) {}

  ngOnInit(): void {
    this.auth.user$.subscribe(user => {
      this.userInfo = user
    })
  }

  logout(): void {
    this.auth.logout({ returnTo: window.location.origin });
  }
}

APIのリクエスト

APIの呼び出し時にヘッダーにアクセストークンを付与します。 これはauth0-angularの AuthHttpInterceptor をprovidersに追加する事で自動化してくれます。

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AuthModule, AuthHttpInterceptor } from '@auth0/auth0-angular';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { environment } from './../environments/environment';
// Components
import { AppComponent } from './app.component';
import { LoginComponent } from './components/pages/login/login.component';
import { HomeComponent } from './components/pages/home/home.component';

@NgModule({
  declarations: [AppComponent, LoginComponent, HomeComponent],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    AuthModule.forRoot({
      domain: environment.AUTH0_DOMAIN,
      clientId: environment.AUTH0_CLIENT_ID,
      useRefreshTokens: true,
      audience: environment.AUDIENCE,
      redirectUri: `${window.location.origin}`,
      httpInterceptor: {
        allowedList: [
          {
            uri: `${environment.API_URL}/*`,
            tokenOptions: {
              audience: environment.AUDIENCE,
            },
          },
        ],
      },
    }),
  ],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthHttpInterceptor, multi: true },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

あとはangular標準のHttpClientModuleでAPIを実行するだけです。

簡易的に動作確認するためAPI実行のボタンを設置します。

home.component.html

<button (click)="request()">API実行</button>

<ul *ngIf="auth.user$ | async as user">
    <li>{{ user.name }}</li>
    <li>{{ user.email }}</li>
</ul>

<button *ngIf="auth.isAuthenticated$ | async" (click)="logout()">
    Log out
</button>

HttpClientでgetリクエストを実行します。

home.component.ts

import { Component, OnInit } from '@angular/core'
import { ApiService } from 'src/app/services/api.service'
import { AuthService } from '@auth0/auth0-angular'
import { HttpClient } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { environment } from 'src/environments/environment';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.styl'],
})
export class HomeComponent implements OnInit {
  userInfo: {[key: string]: any} = {}
  constructor(private api: HttpClient, public auth: AuthService) {}

  ngOnInit(): void {
    this.auth.user$.subscribe(user => {
      this.userInfo = user
    })
  }

  logout(): void {
    // Call this to log the user out of the application
    this.auth.logout({ returnTo: window.location.origin });
  }

  request(): void {
    const url = `${environment.API_URL}/items`;
    this.http.get(url).subscribe(result => console.log(result));
  }

}

これで、内部的にgetTokenSilentlyを呼び出してトークンを取得し、ヘッダーに挿入まで自動的にやってくれます。非常に便利ですね!

認証フローは Authorization Code + PKCE

上記で紹介したSDKを使った場合、認証フローとしてはAuthorization Code + PKCEが用いられます。
上記のコードのみだと、OAuth2.0のどの認証フローを使うかは、ほぼ意識せずにも実装ができますが、以下の公式ドキュメントが非常にわかりやすくまとまっているので一読をお勧めします。

Auth0社筒井様のPKCEで実際にアクセストークンを取得するまでの動作を解説している以下のブログも非常にわかりやすいです。

Auth0を利用してOAuth 2.0のPKCEを理解する

参考

@auth0/auth0-angular
Angular Tutorials