Passer au contenu principal
Ce document fait partie du scénario d’architecture SPA + API et explique comment implémenter la SPA avec Angular 2. Veuillez consulter le scénario pour obtenir de l’information sur la solution mise en œuvre. Le code source complet de l’implémentation Angular 2 de la SPA se trouve dans ce dépôt GitHub.

Étape 1. Configuration

Votre application aura besoin de certaines informations de configuration. Avant de poursuivre l’implémentation, créez une interface AuthConfig qui contiendra différentes valeurs de configuration. Placez cette interface dans un fichier nommé auth0-variables.ts.
interface AuthConfig {
  clientID: string;
  domain: string;
  callbackURL: string;
  apiUrl: string;
}

export const AUTH_CONFIG: AuthConfig = {
  clientID: '',
  domain: '',
  callbackURL: 'http://localhost:4200/callback',
  apiUrl: ''
};

Étape 2. Autorisez l’utilisateur

Créer un service d’authentification

La meilleure façon de gérer et de coordonner les tâches nécessaires à l’authentification de l’utilisateur est de créer un service réutilisable. Une fois ce service en place, vous pourrez appeler ses méthodes à partir de toute votre application. Une instance de l’objet WebAuth de auth0.js peut être créée dans ce service.
import { Injectable } from '@angular/core';
import { AUTH_CONFIG } from './auth0-variables';
import { Router } from '@angular/router';
import 'rxjs/add/operator/filter';
import auth0 from 'auth0-js';

@Injectable()
export class AuthService {

  userProfile: any;
  requestedScopes: string = 'openid profile read:timesheets create:timesheets';

  auth0 = new auth0.WebAuth({
    clientID: AUTH_CONFIG.clientID,
    domain: AUTH_CONFIG.domain,
    responseType: 'token id_token',
    audience: AUTH_CONFIG.apiUrl,
    redirectUri: AUTH_CONFIG.callbackURL,
    scope: this.requestedScopes
  });

  constructor(public router: Router) {}

  public login(): void {
    this.auth0.authorize();
  }

  public handleAuthentication(): void {
    this.auth0.parseHash((err, authResult) => {
      if (authResult && authResult.accessToken && authResult.idToken) {
        window.location.hash = '';
        this.setSession(authResult);
        this.router.navigate(['/home']);
      } else if (err) {
        this.router.navigate(['/home']);
        console.log(err);
        alert('Error: <%= "${err.error}" %>. Check the console for further details.');
      }
    });
  }

  private setSession(authResult): void {
    // Définir l'heure d'expiration du jeton d'accès
    const expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime());

    // Si le paramètre scope de authResult contient une valeur,
    // l'utiliser pour définir les scopes dans la session de l'utilisateur. Sinon,
    // utiliser les scopes demandés. Si aucun scope n'a été demandé,
    // ne rien définir
    const scopes = authResult.scope || this.requestedScopes || '';

    localStorage.setItem('access_token', authResult.accessToken);
    localStorage.setItem('id_token', authResult.idToken);
    localStorage.setItem('expires_at', expiresAt);
    localStorage.setItem('scopes', JSON.stringify(scopes));
  }

  public logout(): void {
    // Supprimer les jetons et l'heure d'expiration du localStorage
    localStorage.removeItem('access_token');
    localStorage.removeItem('id_token');
    localStorage.removeItem('expires_at');
    localStorage.removeItem('scopes');
    // Revenir à la route d'accueil
    this.router.navigate(['/']);
  }

  public isAuthenticated(): boolean {
    // Vérifier si l'heure actuelle est postérieure à
    // l'heure d'expiration du jeton d'accès
    const expiresAt = JSON.parse(localStorage.getItem('expires_at'));
    return new Date().getTime() < expiresAt;
  }

  public userHasScopes(scopes: Array<string>): boolean {
    const grantedScopes = JSON.parse(localStorage.getItem('scopes')).split(' ');
    return scopes.every(scope => grantedScopes.includes(scope));
  }
}
Le service comprend plusieurs méthodes pour gérer l’authentification.
  • login : appelle authorize d’auth0.js, ce qui déclenche
  • handleAuthentication : recherche un résultat d’authentification dans le fragment de l’URL et le traite avec la méthode parseHash d’auth0.js
  • setSession : définit le , l’ de l’utilisateur, ainsi que l’heure d’expiration du Jeton d’accès
  • logout : supprime les jetons de l’utilisateur du stockage du navigateur isAuthenticated : vérifie si l’heure d’expiration du Jeton d’accès est dépassée

Traiter le résultat de l’authentification

Lorsqu’un utilisateur s’authentifie avec Universal Login, puis est redirigé vers votre application, ses informations d’authentification se trouvent dans un fragment de hachage de l’URL. La méthode handleAuthentication dans AuthService sert à traiter ce hachage. Appelez handleAuthentication dans le composant racine de votre application afin que le fragment de hachage d’authentification soit traité au chargement initial de l’application après la redirection de l’utilisateur.
// src/app/app.component.ts

import { Component } from '@angular/core';
import { AuthService } from './auth/auth.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

export class AppComponent {

  constructor(public auth: AuthService) {
    auth.handleAuthentication();
  }
}

Ajouter le composant Callback

L’utilisation d’Universal Login signifie que les utilisateurs sont redirigés vers une page hébergée par Auth0, à l’extérieur de votre application. Une fois authentifiés avec succès, ils sont redirigés vers votre application, où une session côté client est établie. Vous pouvez choisir de rediriger les utilisateurs vers n’importe quelle URL de votre application; toutefois, il est recommandé de créer une route de rappel dédiée qui servira de point de retour central après une authentification réussie. Le fait d’avoir une seule route de rappel présente deux avantages principaux :
  • Cela évite d’avoir à autoriser plusieurs URL de rappel, parfois inconnues
  • Cela fournit un emplacement où afficher un indicateur de chargement pendant que votre application établit la session côté client de l’utilisateur
Créez un composant nommé CallbackComponent et ajoutez-y un indicateur de chargement.
<!-- app/callback/callback.html -->

<div class="loading">
  <img src="/assets/loading.svg" alt="loading">
</div>
Cet exemple suppose qu’un indicateur de chargement est disponible dans un répertoire assets. Consultez l’exemple téléchargeable pour voir comment cela fonctionne. Après l’authentification, les utilisateurs seront brièvement dirigés vers la route /callback, où un indicateur de chargement s’affichera. Pendant ce temps, leur session côté client sera établie, puis ils seront redirigés vers la route /home.

Étape 3. Obtenir le profil utilisateur

Extraire les informations du jeton

Cette section explique comment récupérer les informations de l’utilisateur à l’aide du Jeton d’accès et du point de terminaison /userinfo. Vous pouvez aussi simplement décoder l’ID Token à l’aide d’une bibliothèque (assurez-vous d’abord de le valider). Le résultat sera le même. Si vous avez besoin d’informations supplémentaires sur l’utilisateur, envisagez d’utiliser notre Management API.
Pour obtenir le profil de l’utilisateur, mettez à jour la classe AuthService existante. Ajoutez une fonction getProfile qui extrait le Jeton d’accès de l’utilisateur du stockage local, puis transmet cet appel à la fonction userInfo pour récupérer ses informations.
// Le code existant de la classe AuthService est omis dans cet exemple de code par souci de brièveté
@Injectable()
export class AuthService {
  public getProfile(cb): void {
    const accessToken = localStorage.getItem('access_token');
    if (!accessToken) {
      throw new Error('Access Token must exist to fetch profile');
    }

    const self = this;
    this.auth0.client.userInfo(accessToken, (err, profile) => {
      if (profile) {
        self.userProfile = profile;
      }
      cb(err, profile);
    });
  }
}
Vous pouvez maintenant simplement appeler cette fonction dans n’importe quel service où vous souhaitez récupérer et afficher des informations sur l’utilisateur. Par exemple, vous pouvez choisir de créer un nouveau composant pour afficher les informations du profil de l’utilisateur :
import { Component, OnInit } from '@angular/core';
import { AuthService } from './../auth/auth.service';

@Component({
  selector: 'app-profile',
  templateUrl: './profile.component.html',
  styleUrls: ['./profile.component.css']
})
export class ProfileComponent implements OnInit {

  profile: any;

  constructor(public auth: AuthService) { }

  ngOnInit() {
    if (this.auth.userProfile) {
      this.profile = this.auth.userProfile;
    } else {
      this.auth.getProfile((err, profile) => {
        this.profile = profile;
      });
    }
  }
}
Le modèle de ce composant se présente comme suit :
<div class="panel panel-default profile-area">
  <div class="panel-heading">
    <h3>Profile</h3>
  </div>
  <div class="panel-body">
    <img src="{{profile?.picture}}" class="avatar" alt="avatar">
    <div>
      <label><i class="glyphicon glyphicon-user"></i> Nickname</label>
      <h3 class="nickname">{{ profile?.nickname }}</h3>
    </div>
    <pre class="full-profile">{{ profile | json }}</pre>
  </div>
</div>

Étape 4. Afficher les éléments de l’interface utilisateur de manière conditionnelle selon les scopes

Pendant le processus d’autorisation, nous avons déjà stocké dans le stockage local les scopes réellement accordés à l’utilisateur. Si le scope renvoyé dans authResult n’est pas vide, cela signifie que l’utilisateur s’est vu accorder un ensemble de scopes différent de celui demandé initialement. Nous devons donc utiliser authResult.scope pour déterminer les scopes accordés à l’utilisateur. Si le scope renvoyé dans authResult est vide, cela signifie que l’utilisateur a obtenu tous les scopes demandés. Nous pouvons donc utiliser les scopes demandés pour déterminer les scopes accordés à l’utilisateur. Voici le code que nous avons écrit plus tôt pour la fonction setSession afin d’effectuer cette vérification :
private setSession(authResult): void {
  // Définir l'heure d'expiration du jeton d'accès
  const expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime());

  // Si le paramètre `scope` de authResult contient une valeur,
  // l'utiliser pour définir les scopes dans la session de l'utilisateur. Sinon,
  // utiliser les scopes tels que demandés. Si aucun scope n'a été demandé,
  // ne rien définir
  const scopes = authResult.scope || this.requestedScopes || '';

  localStorage.setItem('access_token', authResult.accessToken);
  localStorage.setItem('id_token', authResult.idToken);
  localStorage.setItem('expires_at', expiresAt);
  localStorage.setItem('scopes', JSON.stringify(scopes));
  this.scheduleRenewal();
}
Ensuite, nous devons ajouter à la classe AuthService une fonction que nous pourrons appeler pour déterminer si un utilisateur a obtenu un scope précis :
@Injectable()
export class AuthService {
  // certains éléments du code ont été omis par souci de concision

  public userHasScopes(scopes: Array<string>): boolean {
    const grantedScopes = JSON.parse(localStorage.getItem('scopes')).split(' ');
    return scopes.every(scope => grantedScopes.includes(scope));
  }
}
Vous pouvez appeler cette méthode pour déterminer si un élément précis de l’interface utilisateur doit être affiché ou non. Par exemple, nous voulons afficher le lien Approuver les feuilles de temps seulement si l’utilisateur possède le scope approve:timesheets. Notez, dans le code ci-dessous, que nous avons ajouté un appel à la fonction userHasScopes pour déterminer si ce lien doit être affiché ou non.
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <div class="navbar-header">
      <a class="navbar-brand" href="#">Timesheet System</a>
    </div>
    <div class="navbar-collapse collapse">
      <ul class="nav navbar-nav">
        <li><a routerLink="/">Home</a></li>
        <li><a *ngIf="auth.isAuthenticated()" routerLink="/profile">My Profile</a></li>
        <li><a *ngIf="auth.isAuthenticated()" routerLink="/timesheets">My Timesheets</a></li>
        <li><a *ngIf="auth.isAuthenticated() && auth.userHasScopes(['approve:timesheets'])" routerLink="/approval">Approve Timesheets</a></li>
      </ul>
      <ul class="nav navbar-nav navbar-right">
        <li><a *ngIf="!auth.isAuthenticated()" href="javascript:void(0)" (click)="auth.login()">Log In</a></li>
        <li><a *ngIf="auth.isAuthenticated()" href="javascript:void(0)" (click)="auth.logout()">Log Out</a></li>
      </ul>
    </div>
  </div>
</nav>

<main class="container">
  <router-outlet></router-outlet>
</main>

Protéger une route

Nous devrions aussi protéger une route pour empêcher qu’un utilisateur y accède s’il n’a pas obtenu les scopes requis. Pour ce faire, nous pouvons ajouter une nouvelle classe de service ScopeGuardService :
import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot } from '@angular/router';
import { AuthService } from './auth.service';

@Injectable()
export class ScopeGuardService implements CanActivate {

  constructor(public auth: AuthService, public router: Router) {}

  canActivate(route: ActivatedRouteSnapshot): boolean {

    const scopes = (route.data as any).expectedScopes;

    if (!this.auth.isAuthenticated() || !this.auth.userHasScopes(scopes)) {
      this.router.navigate(['']);
      return false;
    }
    return true;
  }

}
Utilisez-le ensuite lors de la configuration des routes pour déterminer si une route peut être activée. Notez l’utilisation du nouveau ScopeGuardService dans la définition de la route approval ci-dessous :
// app.routes.ts

import { Routes, CanActivate } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';
import { CallbackComponent } from './callback/callback.component';
import { AuthGuardService as AuthGuard } from './auth/auth-guard.service';
import { ScopeGuardService as ScopeGuard } from './auth/scope-guard.service';
import { TimesheetListComponent } from './timesheet-list/timesheet-list.component';
import { TimesheetAddComponent } from './timesheet-add/timesheet-add.component';
import { ApprovalComponent } from './approval/approval.component';

export const ROUTES: Routes = [
  { path: '', component: HomeComponent },
  { path: 'profile', component: ProfileComponent, canActivate: [AuthGuard] },
  { path: 'callback', component: CallbackComponent },
  { path: 'timesheets/add', component: TimesheetAddComponent, canActivate: [AuthGuard] },
  { path: 'timesheets', component: TimesheetListComponent, canActivate: [AuthGuard] },
  { path: 'approval', component: ApprovalComponent, canActivate: [ScopeGuard], data: { expectedScopes: ['approve:timesheets']} },
  { path: '**', redirectTo: '' }
];

Étape 5. Appeler l’API

Le module angular2-jwt peut servir à ajouter automatiquement des aux requêtes envoyées à votre API. Pour ce faire, il fournit une classe AuthHttp qui encapsule la classe Http d’Angular. Installez angular2-jwt :
# installation avec npm
npm install --save angular2-jwt

# installation avec yarn
yarn add angular2-jwt
Créez une fonction factory contenant certaines valeurs de configuration pour angular2-jwt, puis ajoutez-la au tableau providers du @NgModule de votre application. Cette fonction factory doit inclure une fonction tokenGetter qui récupère le access_token du stockage local.
import { Http, RequestOptions } from '@angular/http';
import { AuthHttp, AuthConfig } from 'angular2-jwt';

export function authHttpServiceFactory(http: Http, options: RequestOptions) {
  return new AuthHttp(new AuthConfig({
    tokenGetter: (() => localStorage.getItem('access_token'))
  }), http, options);
}

@NgModule({
  declarations: [...],
  imports: [...],
  providers: [
    AuthService,
    {
      provide: AuthHttp,
      useFactory: authHttpServiceFactory,
      deps: [Http, RequestOptions]
    }
  ],
  bootstrap: [...]
})
Après avoir configuré angular2-jwt, vous pouvez utiliser la classe AuthHttp pour effectuer des appels sécurisés à votre API depuis n’importe où dans l’application. Pour ce faire, injectez AuthHttp dans tout composant ou service où vous en avez besoin et utilisez-la comme vous le feriez avec la classe Http standard d’Angular.
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { AuthHttp } from 'angular2-jwt';
import 'rxjs/add/operator/map';
import { NewTimesheetModel } from '../models/new-timesheet-model';

@Injectable()
export class TimesheetsService {

  constructor(public authHttp: AuthHttp) { }

  addTimesheet(model: NewTimesheetModel) {
    return this.authHttp.post('http://localhost:8080/timesheets', JSON.stringify(model));
  }

  getAllTimesheets() {
    return this.authHttp.get('http://localhost:8080/timesheets')
      .map(res => res.json())
  }
}

Étape 6. Renouvelez le jeton d’accès

Pour renouveler le jeton d’accès de l’utilisateur, vous devez mettre à jour la SPA Angular. Ajoutez une méthode à AuthService qui appelle la méthode checkSession d’auth0.js. Si le renouvellement réussit, utilisez la méthode setSession existante pour enregistrer les nouveaux jetons dans le stockage local.
public renewToken() {
  this.auth0.checkSession({
    audience: AUTH_CONFIG.apiUrl
  }, (err, result) => {
    if (!err) {
      this.setSession(result);
    }
  });
}
Dans la classe AuthService, ajoutez une méthode appelée scheduleRenewal pour planifier le moment où l’authentification doit être renouvelée silencieusement. Dans l’exemple ci-dessous, cette opération est configurée pour se produire 30 secondes avant l’expiration réelle du jeton. Ajoutez également une méthode appelée unscheduleRenewal pour se désabonner de l’Observable.
public scheduleRenewal() {
  if (!this.isAuthenticated()) return;

  const expiresAt = JSON.parse(window.localStorage.getItem('expires_at'));

  const source = Observable.of(expiresAt).flatMap(
    expiresAt => {

      const now = Date.now();

      // Utiliser le délai dans un minuteur pour
      // exécuter le renouvellement au bon moment
      var refreshAt = expiresAt - (1000 * 30); // Renouveler 30 secondes avant l'expiration
      return Observable.timer(Math.max(1, refreshAt - now));
    });

  // Une fois le délai ci-dessus écoulé,
  // obtenir un nouveau JWT et planifier
  // des renouvellements supplémentaires
  this.refreshSubscription = source.subscribe(() => {
    this.renewToken();
  });
}

public unscheduleRenewal() {
  if (!this.refreshSubscription) return;
  this.refreshSubscription.unsubscribe();
}
Enfin, vous devez démarrer le renouvellement planifié. Pour ce faire, appelez scheduleRenewal dans votre AppComponent au chargement de la page. Cela se produira après chaque flux d’authentification, que l’utilisateur ouvre explicitement une session ou qu’une authentification silencieuse ait lieu.

Rotation des jetons d’actualisation

Les récentes avancées en matière de contrôles de confidentialité dans les navigateurs nuisent à l’expérience utilisateur en bloquant l’accès aux témoins tiers. Auth0 recommande d’utiliser la rotation des jetons d’actualisation, qui offre une méthode sécurisée pour utiliser des jetons d’actualisation dans les SPA tout en donnant aux utilisateurs finaux un accès fluide aux ressources, sans les perturbations de l’expérience utilisateur causées par des technologies de confidentialité des navigateurs comme ITP.