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

🏷️ Front-end 📅 14/04/2026 01:05:00 👤 Mezgani said
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 PKCE : flow recommandé, sécurité token et bonnes pratiques front.

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.