Front-end

Angular Auth moderne : OAuth2, OIDC et PKCE côté SPA

Angular Oauth2 Oidc Pkce Authentification
Angular Auth moderne : OAuth2, OIDC et PKCE côté SPA

Implémentez une authentification Angular moderne avec OAuth2/OIDC et le flow PKCE : sécurisez les tokens, gérez le refresh et suivez les bonnes pratiques.

Pourquoi OAuth2/OIDC avec PKCE

L'implicit flow OAuth2 historique exposait le token d'accès directement dans l'URL — une surface d'attaque réelle sur les SPAs. Le flow Authorization Code + PKCE (Proof Key for Code Exchange) résout ce problème en introduisant un code verifier connu uniquement du client.

OIDC (OpenID Connect) étend OAuth2 pour standardiser l'authentification : l'id_token JWT contient l'identité de l'utilisateur, l'access_token autorise les appels API.

Rule of thumb: En 2024+, toute SPA Angular doit utiliser response_type=code + PKCE. Le flow implicit (response_type=token) est déprécié par la RFC 9700.

Le flow se déroule en 4 étapes :

  1. Le client génère un code_verifier aléatoire et en dérive un code_challenge (SHA-256).
  2. L'utilisateur s'authentifie sur le provider (Keycloak, Auth0, Azure AD…).
  3. Le provider retourne un authorization_code court-vivant.
  4. Le client échange le code contre tokens en envoyant le code_verifier — le provider vérifie la cohérence.

Installation et configuration

La librairie de référence pour Angular est angular-oauth2-oidc. Elle gère le flow PKCE, le stockage des tokens et le refresh automatique.

npm install angular-oauth2-oidc

Configuration dans app.config.ts (Angular 17+ standalone) :

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideOAuthClient } from 'angular-oauth2-oidc';

export const appConfig: ApplicationConfig = {
    providers: [
        provideHttpClient(),
        provideOAuthClient()
    ]
};

Définir la configuration OIDC dans un service dédié :

import { Injectable } from '@angular/core';
import { OAuthService, AuthConfig } from 'angular-oauth2-oidc';
import { Router } from '@angular/router';

const authConfig: AuthConfig = {
    issuer: 'https://auth.exemple.com/realms/mon-app',
    redirectUri: window.location.origin,
    clientId: 'angular-spa',
    responseType: 'code',         // Authorization Code
    scope: 'openid profile email',
    useSilentRefresh: false,
    showDebugInformation: false,
    requireHttps: true
};

@Injectable({ providedIn: 'root' })
export class AuthService {
    constructor(private oauthService: OAuthService, private router: Router) {}

    async init(): Promise<void> {
        this.oauthService.configure(authConfig);
        this.oauthService.setupAutomaticSilentRefresh();
        await this.oauthService.loadDiscoveryDocumentAndTryLogin();
    }

    login(): void {
        this.oauthService.initCodeFlow(); // déclenche PKCE
    }

    logout(): void {
        this.oauthService.logOut();
    }

    get isAuthenticated(): boolean {
        return this.oauthService.hasValidAccessToken();
    }

    get accessToken(): string {
        return this.oauthService.getAccessToken();
    }
}

Discovery document

  1. loadDiscoveryDocumentAndTryLogin() récupère automatiquement les endpoints OIDC via /.well-known/openid-configuration.
  2. Si le provider n'expose pas ce document, utilise loadDiscoveryDocumentAndLogin() avec un objet manuel.

Intercepteur HTTP automatique

Pour attacher le Bearer token à chaque requête API sans répétition, on crée un intercepteur fonctionnel Angular 15+.

import { HttpInterceptorFn, HttpRequest, HttpHandlerFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { OAuthService } from 'angular-oauth2-oidc';

export const authInterceptor: HttpInterceptorFn = (
    req: HttpRequest<unknown>,
    next: HttpHandlerFn
) => {
    const oauthService = inject(OAuthService);
    const token = oauthService.getAccessToken();

    // N'attache le token que pour les appels vers l'API métier
    if (token && req.url.startsWith('https://api.exemple.com')) {
        const authReq = req.clone({
            setHeaders: { Authorization: `Bearer ${token}` }
        });
        return next(authReq);
    }

    return next(req);
};

Enregistrement dans app.config.ts :

import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './auth.interceptor';

export const appConfig: ApplicationConfig = {
    providers: [
        provideHttpClient(withInterceptors([authInterceptor])),
        provideOAuthClient()
    ]
};
Sécurité: Filtre toujours l'URL de destination avant d'attacher le token. Envoyer un Bearer à un domaine tiers expose l'utilisateur à un vol de token.

Protection des routes avec AuthGuard

Angular 15+ privilégie les guards fonctionnels. On vérifie l'état d'authentification et on redirige vers le login si nécessaire.

import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';

export const authGuard: CanActivateFn = () => {
    const auth = inject(AuthService);
    const router = inject(Router);

    if (auth.isAuthenticated) {
        return true;
    }

    auth.login(); // redirige vers le provider OIDC
    return false;
};

Application aux routes protégées :

import { Routes } from '@angular/router';
import { authGuard } from './auth.guard';

export const routes: Routes = [
    { path: '', loadComponent: () => import('./home/home.component') },
    {
        path: 'dashboard',
        loadComponent: () => import('./dashboard/dashboard.component'),
        canActivate: [authGuard]     // <-- protégé
    },
    { path: 'callback', redirectTo: '' }
];

Refresh token et expiration

Un access_token a une durée de vie courte (5–60 min). La librairie peut renouveler automatiquement via un refresh token ou un silent refresh (iframe).

// Dans AuthService.init() — active le refresh automatique
this.oauthService.setupAutomaticSilentRefresh();

// Écouter les événements de token pour réagir aux expirations
import { OAuthEvent } from 'angular-oauth2-oidc';

this.oauthService.events.subscribe((event: OAuthEvent) => {
    if (event.type === 'token_expires') {
        console.warn('Token expire bientôt');
    }
    if (event.type === 'session_terminated') {
        this.router.navigate(['/']);
    }
});

Refresh token vs Silent refresh

  1. Refresh token — échange direct de token, nécessite que le provider l'accorde (offline_access scope).
  2. Silent refresh — recharge le token via une iframe invisible, fonctionne si la session provider est encore active. Plus adapté aux SPAs sans CORS complexe.

Checklist sécurité

  • Utiliser response_type=code + PKCE — ne jamais utiliser l'implicit flow.
  • Activer requireHttps: true en production — bloquer tout échange de token sur HTTP.
  • Limiter le scope au strict nécessaire (openid profile email) sans demander de permissions inutiles.
  • Ne jamais stocker le token dans localStorage si du contenu tiers (pub, analytics) est présent sur la page.
  • Valider la signature et l'expiration du id_token côté serveur avant toute action sensible.
  • Filtrer l'URL cible dans l'intercepteur pour n'envoyer le Bearer qu'à votre propre API.
  • Ajouter une politique CSP (Content-Security-Policy) pour bloquer les injections XSS.
  • Auditer les scopes et rotations de clés du provider au moins une fois par trimestre.

Questions fréquentes

PKCE (Proof Key for Code Exchange) est une extension OAuth2 qui sécurise le flux d'autorisation pour les clients publics (SPA, mobile). Il génère un `code_verifier` et un `code_challenge` pour prévenir les attaques d'interception de code.

`angular-oauth2-oidc` est la librairie la plus populaire. Elle supporte PKCE, la découverte automatique OpenID Connect, le refresh token silencieux, et s'intègre facilement avec Keycloak, Auth0, Azure AD.

Créez un `AuthGuard` qui vérifie `this.oauthService.hasValidAccessToken()`. Si faux, redirigez vers `this.oauthService.initCodeFlow()`. Le guard s'applique sur les routes via `canActivate: [AuthGuard]` dans le routeur.

Utilisez `DefaultOAuthInterceptor` de `angular-oauth2-oidc` ou créez un intercepteur HTTP qui ajoute `Authorization: Bearer ${this.oauthService.getAccessToken()}` aux headers de chaque requête vers votre API.

Partager