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).
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 :
| Élément | Exemple Angular | Exemple React |
|---|---|---|
Fournisseur OAuth2 du royaume ref | scopes openid, profile, photolibrary.read ; clients autorisés à passer outre le consentement | identique (le même fournisseur sert aux deux) |
Client de l’application (type Public, PKCE exigé, consentement implicite) | angular-openid-client | react-openid-client |
| URI de redirection et URI de redirection après déconnexion | http://localhost:4200 | http://localhost:3000 |
Client de l’API (type Confidential, scope am-introspect-all-tokens) | photos-api | react-photos-api |
Sous-configuration CORS (méthodes GET, POST, OPTIONS ; en-têtes authorization, content-type) | origine http://localhost:4200 | origine 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 :
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) :
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: truecomplè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 scopeprofile(name,given_name,family_name) viennent de userinfo.onSigninCallbacks’exécute après l’échange du code : il retirecodeetstatede 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.tsxefface la session locale : l’application affiche de nouveau Login au lieu de garder un jeton inutilisable.
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() :
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 lecode_verifierque 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 surpost_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.
const response = await fetch(`${config.apiBaseUrl}/fakealbums${path}`, {
method: body === undefined ? 'GET' : 'POST',
headers: { Authorization: `Bearer ${accessToken}`, ... },
body: body === undefined ? undefined : JSON.stringify(body),
})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_tokenLa 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.
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 :
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 :
cd tosiam-samples/tosiam-react/frontend
npm install
npm run dev # http://localhost:3000cd tosiam-samples/tosiam-react/backend
./mvnw spring-boot:run # http://localhost:8082Ouvrez http://localhost:3000 et cliquez sur Login.
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.
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.
Dans les outils de développement du navigateur, l’onglet Réseau montre les échanges :
| Requête | Ce qu’elle porte |
|---|---|
GET /tosiam/oauth2/ref/authorize | response_type=code, code_challenge, code_challenge_method=S256, state |
POST /tosiam/oauth2/ref/access_token | grant_type=authorization_code, code, code_verifier, client_id ; la réponse contient access_token, id_token et refresh_token |
GET /tosiam/oauth2/ref/userinfo | Authorization: Bearer <jeton d'accès> ; la réponse contient name, given_name, family_name |
GET localhost:8082/fakealbums/albums | Authorization: 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#
| Symptôme | Cause 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 use | Une 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 navigateur | Origine absente de la configuration CORS, ou Activation des restrictions CORS désactivée |
Page d’erreur invalid_client « Client authentication failed » au clic sur Login | Client 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ée | Le 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 redirection | L’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 sessionHttpOnly. - Build de production :
npm run buildproduit des fichiers statiques dansfrontend/dist, à servir par n’importe quel serveur web. Les variablesVITE_…sont lues au moment du build : définissez-les avantnpm run build. Le serveur doit renvoyerindex.htmlpour toute adresse inconnue (sinon/photos/2rechargé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#
- Application Angular avec OpenID Connect : la même application, avec la configuration détaillée dans la console.
- Enregistrer un client : tous les réglages d’un client OAuth2.
- Jetons : jetons opaques ou JWT, introspection et révocation.
Mis à jour le