TutorielGuides

Application Angular avec OpenID Connect

Sur cette page

Ce tutoriel connecte une application Angular à TOSIAM avec OpenID Connect, puis lui fait appeler une API Spring Boot protégée par les jetons d’accès que TOSIAM délivre. Il suit les recommandations actuelles pour une application qui tourne dans le navigateur : flux par code d’autorisation avec PKCE, client public (sans secret), déconnexion par le point de fin de session OpenID Connect.

Le code complet est dans le dépôt tosiam-samples, dossier tosiam-angular : une application Angular 22 (frontend, bibliothèque angular-oauth2-oidc) et une API d’albums photo Spring Boot 3.5 (backend).

texte
Navigateur ── Angular (localhost:4200) ──① connexion, code + PKCE──▶ TOSIAM (localhost:8080, royaume ref)
                      │                                                   ▲
                      └──② jeton d'accès──▶ API (localhost:8081) ──③ introspection (photos-api)

Étape 1 : le fournisseur OAuth2 du royaume#

Dans le royaume ref, ajoutez le service OAuth2 Provider (voir Fournisseur OAuth2). Son émetteur (issuer) est http://localhost:8080/tosiam/oauth2/ref : l’application lit tout le reste (points d’accès, clés, déconnexion) dans le document de découverte http://localhost:8080/tosiam/oauth2/ref/.well-known/openid-configuration.

Deux réglages du fournisseur servent à ce tutoriel :

  • Scopes acceptés (supportedScopes) : déclarez les scopes avec leur description, affichée à l’utilisateur quand son consentement est demandé, par exemple openid|Identifiant de connexion, profile|Nom et adresse e-mail et photolibrary.read|Lecture de vos albums photo.
  • Autoriser les clients à passer outre le consentement (clientsCanSkipConsent) : à activer pour que l’application, qui appartient au même éditeur que l’API, ne demande pas de consentement (étape suivante).
Réglage du fournisseur OAuth2 du royaume ref : Autoriser les clients à passer outre le consentement activé Réglage du fournisseur OAuth2 du royaume ref : Autoriser les clients à passer outre le consentement activé
Le fournisseur autorise les clients à ne pas demander de consentement.

Étape 2 : le client de l’application#

Dans Clients, Créer un agent ouvre l’assistant. Donnez l’identifiant angular-openid-client et l’URI de redirection http://localhost:4200, l’adresse de l’application.

Assistant de création d'agent : identifiant angular-openid-client et URI de redirection http://localhost:4200 Assistant de création d'agent : identifiant angular-openid-client et URI de redirection http://localhost:4200
Identifiant du client (1) et URI de redirection (2).

L’assistant demande un mot de passe à l’étape Sécurité : saisissez-en un quelconque, il ne servira pas. Gardez les types d’autorisation proposés, Authorization Code et Refresh Token, puis créez le client. Complétez ensuite sa fiche.

Onglet Général :

Fiche du client, onglet Général : type de client Public, URI de redirection, scopes openid, profile, photolibrary.read Fiche du client, onglet Général : type de client Public, URI de redirection, scopes openid, profile, photolibrary.read
Type de client Public (1), URI de redirection (2) et scopes (3).
  • Type de client : Public. Une application qui s’exécute dans le navigateur ne peut pas garder de secret : son code d’autorisation est protégé par PKCE.
  • Scope(s) : openid, profile et photolibrary.read, le scope qu’exige l’API.

Onglet Avancé :

Fiche du client, onglet Avancé : méthode d'authentification client_secret_post, code verifier requis, consentement implicite activés Fiche du client, onglet Avancé : méthode d'authentification client_secret_post, code verifier requis, consentement implicite activés
Méthode d’authentification (1), PKCE obligatoire (2) et consentement implicite (3).
  • Types de réponse : gardez seulement code.
  • Méthode d’authentification au point d’accès des jetons : client_secret_post. Un client public n’envoie que son identifiant, mais TOSIAM vérifie la méthode déclarée pour les clients OpenID Connect, et c’est sous cette méthode qu’il range une requête qui ne porte que client_id.
  • Paramètre Code verifier requis : activé. Toute demande d’autorisation devra porter un code_challenge PKCE.
  • Consentement implicite : activé.

Onglet OpenID Connect : URI de redirection après déconnexion http://localhost:4200. Onglet Signature et chiffrement : algorithme de signature du jeton d’identité RS256, vérifiable avec les clés publiques du fournisseur.

Étape 3 : le client de l’API#

L’API reçoit des jetons d’accès opaques et demande à TOSIAM s’ils sont valides, au point d’introspection. Cette requête doit être authentifiée, et le client public de l’application n’a pas de secret : l’API a donc son propre client.

Créez un second client photos-api avec un secret (par exemple photos-api-secret pour ce test), de type Confidential, avec le seul scope am-introspect-all-tokens. Sans ce scope, un client ne peut introspecter que les jetons émis à son propre nom ; avec lui, il peut vérifier ceux de l’application.

Étape 4 : autoriser les appels depuis le navigateur (CORS)#

L’application appelle TOSIAM depuis le navigateur (document de découverte, point d’accès des jetons) : TOSIAM doit accepter son origine. Dans Services globaux › CORS configuration :

  1. activez Activation des restrictions CORS ;
  2. sous Sous-configurations, Nouvelle configuration : origine http://localhost:4200, méthodes GET, POST et OPTIONS, en-têtes authorization et content-type, configuration active.
Configuration CORS angular : origine http://localhost:4200, méthodes POST GET OPTIONS, en-têtes authorization content-type, configuration active Configuration CORS angular : origine http://localhost:4200, méthodes POST GET OPTIONS, en-têtes authorization content-type, configuration active
Origine de l’application (1), méthodes (2), en-têtes (3), activation (4).

La modification s’applique sans redémarrage. Pour vérifier :

bash
curl -s -o /dev/null -D - -X OPTIONS http://localhost:8080/tosiam/oauth2/ref/access_token \
     -H 'Origin: http://localhost:4200' -H 'Access-Control-Request-Method: POST' \
  | grep -i access-control

La réponse doit contenir Access-Control-Allow-Origin: http://localhost:4200.

Étape 5 : l’application Angular#

Les adresses sont regroupées dans frontend/src/environments/environment.ts (et environment.development.ts, utilisé par ng serve) :

typescript
export const environment = {
  issuer: 'http://localhost:8080/tosiam/oauth2/ref',  // émetteur du royaume ref
  clientId: 'angular-openid-client',                   // client public, sans secret
  apiBaseUrl: 'http://localhost:8081',                 // API qui reçoit le jeton d'accès
};

La configuration OpenID Connect, frontend/src/app/sso.config.ts, s’en sert :

typescript
export const authConfig: AuthConfig = {
  issuer: environment.issuer,
  redirectUri: window.location.origin,
  postLogoutRedirectUri: window.location.origin,
  clientId: environment.clientId,
  responseType: 'code',
  scope: 'openid profile photolibrary.read',
  oidc: true,
  requireHttps: 'remoteOnly',
  showDebugInformation: false,
};

Pas de secret, et PKCE n’a pas besoin d’être demandé : la bibliothèque l’utilise par défaut avec le flux par code.

frontend/src/app/app.config.ts déclare le client OAuth et l’API qui doit recevoir le jeton :

typescript
provideHttpClient(withInterceptorsFromDi()),
provideOAuthClient({
  resourceServer: {
    allowedUrls: [environment.apiBaseUrl],
    sendAccessToken: true,
  },
}),

L’intercepteur de la bibliothèque ajoute alors Authorization: Bearer <jeton d'accès> à chaque requête vers apiBaseUrl : le service photos.service.ts ne fait que des appels HttpClient ordinaires. withInterceptorsFromDi() est indispensable : sans lui, l’intercepteur n’est jamais appelé et l’API répond 401.

Le composant principal (app.component.ts) démarre le client :

typescript
this.oauthService.configure(authConfig);
this.oauthService.setupAutomaticSilentRefresh();           // renouvelle le jeton avec le refresh token
this.oauthService.loadDiscoveryDocumentAndTryLogin();      // échange le code au retour de TOSIAM
  • login() appelle initLoginFlow() : redirection vers TOSIAM avec code_challenge et code_challenge_method=S256.
  • Au retour, loadDiscoveryDocumentAndTryLogin() échange le code contre les jetons, avec le code_verifier gardé dans le navigateur.
  • logout() appelle logOut() : redirection vers le point de fin de session de TOSIAM (/connect/endSession) avec le jeton d’identité, puis retour sur postLogoutRedirectUri.

La page d’accueil salue l’utilisateur par son nom. Le jeton d’identité identifie l’utilisateur (sub) mais ne contient pas son nom : comme le prévoit OpenID Connect quand un jeton d’accès est délivré, les informations du scope profile se lisent au point userinfo, avec ce jeton :

typescript
this.oauthService.loadUserProfile().then(profile => {
  const info = (profile as { info?: Record<string, any> }).info;
  this.userName.set(info?.['name'] || [info?.['given_name'], info?.['family_name']].filter(Boolean).join(' '));
});

Étape 6 : l’API Spring Boot#

L’API est un serveur de ressources Spring Security (Spring Boot 3.5). backend/src/main/resources/application.properties désigne le point d’introspection du royaume et les identifiants du client photos-api :

properties
server.port: 8081
app.cors.allowed-origin: http://localhost:4200
app.images.base-url: http://localhost:8081
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri: http://localhost:8080/tosiam/oauth2/ref/introspect
spring.security.oauth2.resourceserver.opaquetoken.client-id: photos-api
spring.security.oauth2.resourceserver.opaquetoken.client-secret: ${PHOTOS_API_SECRET:photos-api-secret}

La configuration de sécurité exige le scope photolibrary.read sur toute l’API :

java
http.cors(Customizer.withDefaults())
    .authorizeHttpRequests(authz -> authz
        .requestMatchers("/montagnes/**", "/voitures/**", "/animaux/**", "/culinaires/**").permitAll()
        .requestMatchers("/fakealbums/**").hasAuthority("SCOPE_photolibrary.read")
        .anyRequest().authenticated())
    .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
    .oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(Customizer.withDefaults()));

Spring Security envoie chaque jeton reçu au point d’introspection et convertit les scopes de la réponse en autorisations SCOPE_…. Un jeton expiré, révoqué ou sans le scope donne 401 ou 403.

Étape 7 : lancer et tester#

Dans deux terminaux :

bash
cd tosiam-samples/tosiam-angular/frontend
npm install
npx ng serve            # http://localhost:4200
bash
cd tosiam-samples/tosiam-angular/backend
./mvnw spring-boot:run  # http://localhost:8081

Ouvrez http://localhost:4200 et cliquez sur Login.

Page d'accueil de l'application Angular, bouton Login Page d'accueil de l'application Angular, bouton Login
L’application avant connexion.

L’application redirige vers TOSIAM, qui affiche sa page de connexion pour le royaume ref. Connectez-vous avec un utilisateur du royaume.

Page de connexion TOSIAM : nom d'utilisateur et mot de passe Page de connexion TOSIAM : nom d'utilisateur et mot de passe
La page de connexion de TOSIAM.

De retour dans l’application, la page salue l’utilisateur par son nom et affiche les albums renvoyés par l’API.

Application Angular connectée : bouton Logout et quatre albums photo Montagnes, Voitures, Animaux, Culinaires Application Angular connectée : bouton Logout et quatre albums photo Montagnes, Voitures, Animaux, Culinaires
Les albums renvoyés par l’API protégée.

Dans les outils de développement du navigateur, l’onglet Réseau montre les échanges :

Faites défiler le tableau
RequêteCe qu’elle porte
GET /tosiam/oauth2/ref/authorizeresponse_type=code, code_challenge, code_challenge_method=S256, nonce, state
POST /tosiam/oauth2/ref/access_tokengrant_type=authorization_code, code, code_verifier, client_id ; la réponse contient access_token, id_token et refresh_token
GET /tosiam/oauth2/ref/userinfoAuthorization: Bearer <jeton d'accès> ; la réponse contient name, given_name, family_name
GET localhost:8081/fakealbums/albumsAuthorization: Bearer <jeton d'accès>
GET /tosiam/oauth2/ref/connect/endSessionÀ la déconnexion : id_token_hint et post_logout_redirect_uri

Après Logout, cliquer de nouveau sur Login redemande l’identifiant et le mot de passe : la session TOSIAM est bien fermée.

En cas de problème#

Faites défiler le tableau
SymptômeCause probable
ng serve refuse de démarrer (« Node.js version … detected »)Version de Node.js trop ancienne pour Angular 22 : il faut 24.15 ou plus
Erreur CORS dans la console du navigateurOrigine absente de la configuration CORS, ou Activation des restrictions CORS désactivée
invalid_client « Invalid authentication method for accessing this endpoint »Méthode d’authentification du client autre que client_secret_post
Erreur 500 dès la redirection vers TOSIAMPKCE obligatoire, mais demande sans code_challenge (disablePKCE: true dans la configuration Angular)
Erreur 500 après Allow sur l’écran de consentementConsentement implicite non activé (voir l’encadré de l’étape 2)
L’API répond 401Identifiants photos-api incorrects, ou scope am-introspect-all-tokens manquant : l’introspection renvoie active: false. Ou jeton absent de la requête : withInterceptorsFromDi() manque dans app.config.ts
Pas de nom dans le message d’accueilAppel userinfo refusé : en-tête authorization absent de la configuration CORS
L’API répond 403Jeton sans le scope photolibrary.read

En production#

Ce tutoriel tourne en local. Avant une mise en production :

  • HTTPS partout : TOSIAM, l’application et l’API. requireHttps: 'remoteOnly' refuse déjà un émetteur en HTTP hors de localhost.
  • Les jetons vivent dans le navigateur (stockage de session par défaut), où un script injecté (XSS) pourrait les lire. Pour une application sensible, les recommandations actuelles (brouillon IETF OAuth 2.0 for Browser-Based Applications) préfèrent un back-end pour le front-end (BFF) : un petit serveur fait le flux OAuth, garde les jetons et ne donne au navigateur qu’un cookie de session HttpOnly.
  • Introspection ou JWT : l’API interroge TOSIAM à chaque requête. Pour un trafic important, elle peut valider localement des jetons d’accès JWT avec les clés publiques du fournisseur (jwks_uri), à condition de signer en RS256 (voir Fournisseur OAuth2) ; un jeton révoqué reste alors accepté jusqu’à son expiration.
  • Consentement : le consentement implicite n’a de sens que pour vos propres applications. Une application tierce doit demander le consentement de l’utilisateur.
  • Secrets : passez le secret de photos-api par la variable d’environnement PHOTOS_API_SECRET, pas dans le fichier de configuration.

Pour aller plus loin#

Mis à jour le