TutorielGuides

Application React avec OpenID Connect

Sur cette page

Ce tutoriel connecte une application React à 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. C’est le jumeau du tutoriel Application Angular avec OpenID Connect : même flux par code d’autorisation avec PKCE, même client public (sans secret), même API, avec les outils de l’écosystème React.

Le code complet est dans le dépôt tosiam-samples, dossier tosiam-react : une application React 19 construite avec Vite (frontend, bibliothèque react-oidc-context, elle-même fondée sur oidc-client-ts) et une API d’albums photo Spring Boot 3.5 (backend).

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

Étape 1 : configurer TOSIAM#

La configuration est celle des étapes 1 à 4 du tutoriel Angular, avec les noms et l’origine de l’application React :

Faites défiler le tableau
ÉlémentExemple AngularExemple React
Fournisseur OAuth2 du royaume refscopes openid, profile, photolibrary.read ; clients autorisés à passer outre le consentementidentique (le même fournisseur sert aux deux)
Client de l’application (type Public, PKCE exigé, consentement implicite)angular-openid-clientreact-openid-client
URI de redirection et URI de redirection après déconnexionhttp://localhost:4200http://localhost:3000
Client de l’API (type Confidential, scope am-introspect-all-tokens)photos-apireact-photos-api
Sous-configuration CORS (méthodes GET, POST, OPTIONS ; en-têtes authorization, content-type)origine http://localhost:4200origine http://localhost:3000

Comme pour Angular, la méthode d’authentification du client de l’application au point d’accès des jetons est none à partir de la version 3.38.0, ou client_secret_post avec une version antérieure (les scripts de l’exemple utilisent client_secret_post, accepté par toutes les versions). Le client n’a que le type de réponse code : le flux implicite (token) n’a plus de raison d’être dans une application récente.

Étape 2 : l’application React#

Les adresses sont regroupées dans frontend/src/config.ts. Chacune peut être remplacée par une variable VITE_…, par exemple dans un fichier .env.local :

typescript· frontend/src/config.ts
export const config = {
  issuer: import.meta.env.VITE_ISSUER ?? 'http://localhost:8080/tosiam/oauth2/ref',  // émetteur du royaume ref
  clientId: import.meta.env.VITE_CLIENT_ID ?? 'react-openid-client',                // client public, sans secret
  apiBaseUrl: import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8082',         // API qui reçoit le jeton
}

frontend/src/main.tsx entoure l’application du fournisseur d’authentification de react-oidc-context (extrait) :

typescript· frontend/src/main.tsx
const oidcConfig: AuthProviderProps = {
  authority: config.issuer,
  client_id: config.clientId,
  redirect_uri: window.location.origin,
  post_logout_redirect_uri: window.location.origin,
  scope: 'openid profile photolibrary.read',
  loadUserInfo: true,
  onSigninCallback: () => {
    window.history.replaceState({}, document.title, window.location.pathname)
  },
}

createRoot(document.getElementById('root')!).render(
  <AuthProvider {...oidcConfig}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </AuthProvider>,
)
  • La bibliothèque lit le document de découverte de l’émetteur (/.well-known/openid-configuration), utilise le flux par code et ajoute PKCE (code_challenge_method=S256) sans réglage : il n’y a ni secret ni option PKCE à donner.
  • loadUserInfo: true complète le profil par le point userinfo. Le jeton d’identité identifie l’utilisateur (sub) mais, avec le réglage par défaut du fournisseur, ne contient pas son nom : les informations du scope profile (name, given_name, family_name) viennent de userinfo.
  • onSigninCallback s’exécute après l’échange du code : il retire code et state de l’adresse. Sans lui, un rechargement de la page rejouerait un code déjà utilisé.
  • Le jeton d’accès est renouvelé avant son expiration avec le refresh token (automaticSilentRenew, actif par défaut). Si ce renouvellement échoue (refresh token expiré ou révoqué, TOSIAM injoignable), App.tsx efface la session locale : l’application affiche de nouveau Login au lieu de garder un jeton inutilisable.
typescript· frontend/src/App.tsx
useEffect(() => {
  const dropSession = () => void auth.removeUser()
  const unsubscribeExpired = auth.events.addAccessTokenExpired(dropSession)
  const unsubscribeRenewError = auth.events.addSilentRenewError(dropSession)
  return () => { unsubscribeExpired(); unsubscribeRenewError() }
}, [auth.events, auth.removeUser])

Le composant principal, App.tsx, affiche le bouton de connexion ou de déconnexion avec le crochet useAuth() :

typescript· frontend/src/App.tsx
const auth = useAuth()

{auth.isAuthenticated ? (
  <button onClick={() => void auth.signoutRedirect()}>Logout</button>
) : (
  <button onClick={() => void auth.signinRedirect()}>Login</button>
)}
  • signinRedirect() envoie le navigateur vers le point d’autorisation de TOSIAM ; au retour, AuthProvider échange le code contre les jetons avec le code_verifier que la bibliothèque a gardé dans le stockage local du navigateur pendant l’aller-retour.
  • signoutRedirect() passe par le point de fin de session de TOSIAM (/connect/endSession) avec le jeton d’identité, puis revient sur post_logout_redirect_uri.

La page d’accueil salue l’utilisateur et appelle l’API avec le jeton d’accès, sans intercepteur : frontend/src/api.ts ajoute l’en-tête lui-même.

typescript· frontend/src/api.ts
const response = await fetch(`${config.apiBaseUrl}/fakealbums${path}`, {
  method: body === undefined ? 'GET' : 'POST',
  headers: { Authorization: `Bearer ${accessToken}`, ... },
  body: body === undefined ? undefined : JSON.stringify(body),
})
typescript· frontend/src/pages/Home.tsx
const profile = auth.user?.profile
const fullName = [profile?.given_name, profile?.family_name].filter(Boolean).join(' ')
const userName = profile?.name || fullName || profile?.sub
getAlbums(accessToken)   // accessToken = auth.user?.access_token

La page des photos d’un album (/photos/:id) est réservée aux utilisateurs connectés. Sa garde attend la fin de isLoading : au rechargement de la page, la bibliothèque relit d’abord la session enregistrée, et rediriger avant ferait perdre l’adresse.

typescript· frontend/src/RequireAuth.tsx
export default function RequireAuth({ children }: { children: ReactNode }) {
  const auth = useAuth()
  if (auth.isLoading) return <div>Chargement…</div>
  return auth.isAuthenticated ? children : <Navigate to="/" replace />
}

Étape 3 : l’API Spring Boot#

L’API est la même que dans le tutoriel Angular (étape 6) : un serveur de ressources Spring Security qui envoie chaque jeton au point d’introspection et exige le scope photolibrary.read. Seuls changent le port, l’origine autorisée et le client d’introspection :

properties· backend/src/main/resources/application.properties
server.port: 8082
app.cors.allowed-origin: http://localhost:3000
app.images.base-url: http://localhost:8082
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri: http://localhost:8080/tosiam/oauth2/ref/introspect
spring.security.oauth2.resourceserver.opaquetoken.client-id: react-photos-api
spring.security.oauth2.resourceserver.opaquetoken.client-secret: ${PHOTOS_API_SECRET:react-photos-api-secret}

Étape 4 : lancer et tester#

Dans deux terminaux :

bash
cd tosiam-samples/tosiam-react/frontend
npm install
npm run dev             # http://localhost:3000
bash
cd tosiam-samples/tosiam-react/backend
./mvnw spring-boot:run  # http://localhost:8082

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

Page d'accueil de l'application React, bouton Login Page d'accueil de l'application React, 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, par exemple dduck / Donald-Duck-2026 si vous avez lancé up.sh.

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, l’adresse ne porte plus de code ; la page salue l’utilisateur par son nom et affiche les albums renvoyés par l’API. Un clic sur un album affiche ses photos, et la page des photos reste accessible après un rechargement.

Application React connectée : bouton Logout et quatre albums photo Montagnes, Voitures, Animaux, Culinaires Application React 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, 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:8082/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
npm run dev refuse de démarrer (version de Node.js)Version de Node.js trop ancienne pour Vite 8 : il faut 22.12 ou plus
Port 3000 is already in useUne autre application occupe le port 3000 : l’application ne change pas de port, puisque l’URI de redirection du client est http://localhost:3000
Erreur CORS dans la console du navigateurOrigine absente de la configuration CORS, ou Activation des restrictions CORS désactivée
Page d’erreur invalid_client « Client authentication failed » au clic sur LoginClient react-openid-client absent du royaume ref (QuickStart réinitialisée par exemple) : refaites l’étape 1, ou relancez up.sh
invalid_client « Invalid authentication method for accessing this endpoint »Méthode d’authentification du client autre que none (ou client_secret_post avant la version 3.38.0)
« Impossible de charger les albums (API injoignable) »API arrêtée, ou CORS de l’API qui n’autorise pas http://localhost:3000
« Impossible de charger les albums (erreur HTTP 401 : jeton refusé, reconnectez-vous) »Identifiants react-photos-api incorrects, ou scope am-introspect-all-tokens manquant : l’introspection renvoie active: false
« Impossible de charger les albums (erreur HTTP 403) »Jeton sans le scope photolibrary.read
« Échec de la connexion : Failed to fetch » au retour de TOSIAM, l’adresse garde ?code=…Appel refusé par le CORS de TOSIAM : en-tête authorization absent de la configuration CORS (appel userinfo, qui fait partie de la connexion avec loadUserInfo: true), ou origine absente. Corrigez la configuration, puis ouvrez http://localhost:3000 sans paramètres (recharger l’adresse avec ?code=… donne « No matching state found in storage »)
Votre session a expiré ou n’a pas pu être renouveléeLe jeton d’accès a expiré et le refresh token n’a pas permis de le renouveler (expiré, révoqué, ou TOSIAM injoignable) : l’application a effacé la session locale, cliquez sur Login
L’application est ouverte sur http://127.0.0.1:3000 et TOSIAM refuse la redirectionL’URI de redirection déclarée est http://localhost:3000 : utilisez localhost

En production#

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

  • HTTPS partout : TOSIAM, l’application et l’API.
  • Les jetons vivent dans le navigateur (stockage de session par défaut d’oidc-client-ts), 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.
  • Build de production : npm run build produit des fichiers statiques dans frontend/dist, à servir par n’importe quel serveur web. Les variables VITE_… sont lues au moment du build : définissez-les avant npm run build. Le serveur doit renvoyer index.html pour toute adresse inconnue (sinon /photos/2 rechargée donne une erreur 404).
  • Introspection ou JWT, consentement, secret de l’API : mêmes remarques que pour le tutoriel Angular.

Pour aller plus loin#

Mis à jour le