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.
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 :
- Le client génère un
code_verifieraléatoire et en dérive uncode_challenge(SHA-256). - L'utilisateur s'authentifie sur le provider (Keycloak, Auth0, Azure AD…).
- Le provider retourne un
authorization_codecourt-vivant. - 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
loadDiscoveryDocumentAndTryLogin()récupère automatiquement les endpoints OIDC via/.well-known/openid-configuration.- 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()
]
};
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
- Refresh token — échange direct de token, nécessite que le provider l'accorde (
offline_accessscope). - 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: trueen 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
localStoragesi du contenu tiers (pub, analytics) est présent sur la page. - Valider la signature et l'expiration du
id_tokencô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.