Service à service avec client_credentials
Sur cette page
Quand aucun utilisateur n’intervient (un traitement planifié, un microservice qui en appelle un autre), le service s’authentifie lui-même auprès de TOSIAM avec le grant client_credentials : il présente son identifiant et son secret, et reçoit un jeton d’accès à son nom. Ce tutoriel configure deux services de ce type et une API qui valide leurs jetons localement : les jetons sont des JWT signés, que l’API vérifie avec les clés publiques de TOSIAM, sans l’interroger à chaque requête.
Le code est dans le dépôt tosiam-samples, dossier tosiam-client-credentials : une API d’inventaire Spring Boot 3.5 (api) et un service Spring Boot sans serveur web (client) qui l’appelle toutes les 30 secondes au nom de deux clients.
service-commandes ─┐ ┌─▶ TOSIAM (royaume ref) : ① jeton (client_credentials)
service-rapports ─┴─ client (Spring Boot) ────────────────┤
└─▶ API d'inventaire (localhost:8083) : ② jeton JWT
API ──③ clés publiques (jwks_uri), gardées en cache──▶ TOSIAMÉtape 1 : les clients des services#
Chaque service est un client OAuth2 confidentiel du royaume ref : il tourne sur un serveur et peut garder un secret. Dans Clients, créez deux clients :
| Réglage | service-commandes | service-rapports |
|---|---|---|
| Type de client | Confidential | Confidential |
| Secret (démonstration) | service-commandes-secret | service-rapports-secret |
| Type d’autorisation | Client Credentials seulement | Client Credentials seulement |
| Scopes | inventaire.lecture, inventaire.ecriture | inventaire.lecture |
| Méthode d’authentification au point d’accès des jetons | client_secret_basic | client_secret_basic |
| Émettre les jetons d’accès sous forme de JWT (onglet Avancé) | activé | activé |
Les scopes de chaque client disent ce que son service a le droit de faire : service-rapports pourra lire l’inventaire, pas le modifier.
Pour service-commandes : dans Clients, Créer un agent ouvre l’assistant. À l’étape Identité, donnez l’identifiant service-commandes ; aucune URI de redirection n’est nécessaire, aucun navigateur n’intervient. À l’étape Sécurité :
client_secret_basic (2), seul type d’autorisation Client Credentials (3) et scopes (4).- Type de client :
Confidentiel. - Mot de passe de l’agent : le secret du service. La méthode proposée,
client_secret_basic, convient : le service envoie son identifiant et son secret dans l’en-têteAuthorization. - Types d’autorisation : décochez Authorization Code et Refresh Token, cochez Client Credentials.
- Scopes : retirez
openidetprofile, ajoutezinventaire.lectureetinventaire.ecriture.
Créez le client, puis vérifiez sa fiche. Onglet Général, le type de client et les scopes :
Onglet Avancé, activez Émettre les jetons d’accès sous forme de JWT et vérifiez que Client Credentials est le seul type d’autorisation coché :
Enregistrez, puis créez service-rapports de la même façon, avec le seul scope inventaire.lecture.
Sans l’option Émettre les jetons d’accès sous forme de JWT (isAccessTokenJwtEnabled), TOSIAM délivre des jetons opaques, que l’API devrait faire vérifier par le point d’introspection (comme dans le tutoriel Angular). Avec elle, le jeton est un JWT signé avec l’algorithme de signature des jetons du fournisseur s’il est asymétrique, et en RS256 s’il est symétrique (HS256, la valeur par défaut) : l’API peut alors le vérifier avec les clés publiques du royaume.
Étape 2 : obtenir un jeton#
Le service appelle le point des jetons du royaume, authentifié par son identifiant et son secret (en-tête HTTP Basic pour client_secret_basic) :
curl -s -u service-rapports:service-rapports-secret \
-d grant_type=client_credentials -d scope=inventaire.lecture \
http://localhost:8080/tosiam/oauth2/ref/access_token{"access_token":"eyAidHlwIjogImF0K2p3dCIs…","token_type":"Bearer","expires_in":3599,"scope":"inventaire.lecture"}Pas de jeton d’identité ni de refresh token : il n’y a pas d’utilisateur, et le service redemandera simplement un jeton à l’expiration du premier. Décodé, le jeton d’accès donne :
{ "typ": "at+jwt", "token_format": "stateful", "kid": "K2k+448beikyJVPOO8yRpD16QjA=", "alg": "RS256" }{
"sub": "service-rapports",
"aud": "service-rapports",
"client_id": "service-rapports",
"scope": "inventaire.lecture",
"iss": "http://localhost:8080/tosiam/oauth2/ref",
"iat": 1791485669,
"exp": 1791489269,
"jti": "c6b996aa-e5ae-471e-9758-23755670fd06"
}typ: at+jwtmarque un jeton d’accès JWT (RFC 9068) ;kiddésigne la clé de signature, publiée à l’adressejwks_uridu royaume (/oauth2/ref/connect/jwk_uri).sub,audetclient_idvalent tous l’identifiant du client : sans utilisateur, le sujet du jeton est le service lui-même. TOSIAM met l’identifiant du client dansaud, pas celui de l’API.token_format: statefuletjti: le jeton reste enregistré dans TOSIAM, sous l’identifiantjti. Il peut donc être révoqué et introspecté, même si l’API ne le fait pas (voir l’étape 6).
Étape 3 : l’API valide le jeton#
L’API est un serveur de ressources Spring Security en mode JWT. api/src/main/resources/application.yml donne l’émetteur, l’adresse des clés publiques et la liste des clients autorisés :
server:
port: 8083
app:
tosiam:
issuer: http://localhost:8080/tosiam/oauth2/ref
jwk-set-uri: http://localhost:8080/tosiam/oauth2/ref/connect/jwk_uri
allowed-clients: service-commandes,service-rapportsSecurityConfig.java construit le décodeur et les règles d’accès :
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
// Algorithmes asymétriques (par défaut : RS256 seulement)
.jwsAlgorithms(algorithms -> algorithms.addAll(List.of(
SignatureAlgorithm.RS256, SignatureAlgorithm.RS384, SignatureAlgorithm.RS512,
SignatureAlgorithm.ES256, SignatureAlgorithm.ES384, SignatureAlgorithm.ES512,
SignatureAlgorithm.PS256, SignatureAlgorithm.PS384, SignatureAlgorithm.PS512)))
// TOSIAM marque ses jetons d'accès JWT du type at+jwt (RFC 9068)
.jwtProcessorCustomizer(processor -> processor.setJWSTypeVerifier(
new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("at+jwt"), JOSEObjectType.JWT, null)))
.build();
decoder.setJwtValidator(tosiamValidator(issuer, allowedClients));
static OAuth2TokenValidator<Jwt> tosiamValidator(String issuer, List<String> allowedClients) {
OAuth2TokenValidator<Jwt> allowedClient = new JwtClaimValidator<List<String>>(JwtClaimNames.AUD,
audience -> audience != null && audience.stream().anyMatch(allowedClients::contains));
return new DelegatingOAuth2TokenValidator<>(JwtValidators.createDefaultWithIssuer(issuer), allowedClient);
}http.authorizeHttpRequests(authz -> authz
.requestMatchers(HttpMethod.GET, "/inventaire/**").hasAuthority("SCOPE_inventaire.lecture")
.requestMatchers(HttpMethod.POST, "/inventaire/**").hasAuthority("SCOPE_inventaire.ecriture")
.anyRequest().denyAll())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));Pour chaque requête, l’API vérifie, sans appeler TOSIAM :
- la signature, avec la clé
kiddu jeu de clés publié par TOSIAM et un algorithme asymétrique. Le jeu de clés est téléchargé à la première requête puis gardé en cache cinq minutes ; une clé inconnue (nouvelle clé de signature) provoque un nouveau téléchargement. Par défaut, Spring Security n’accepte queRS256: l’exemple accepte aussi les autres algorithmes asymétriques (RS384,ES256,PS256…), pour le cas où le fournisseur signe avec l’un d’eux ; - le type
at+jwt: le vérificateur par défaut de Nimbus n’accepte queJWTou l’absence de type, d’où le réglage explicite ; - l’émetteur (
iss) et l’expiration (exp) ; - le client appelant : comme TOSIAM met l’identifiant du client dans
aud, l’API compareaudà la liste des clients autorisés. Un jeton délivré à un autre client du royaume, par exemple l’application Angular, est refusé (401) ; - le scope : Spring Security transforme la revendication
scopeen autorisationsSCOPE_…. Un jeton sans le bon scope donne403.
Étape 4 : le service client#
Le service utilise le client OAuth2 de Spring Security. client/src/main/resources/application.yml déclare les deux enregistrements :
spring:
main:
web-application-type: none
security:
oauth2:
client:
registration:
service-commandes:
provider: tosiam
client-id: service-commandes
client-secret: ${SERVICE_COMMANDES_SECRET:service-commandes-secret}
client-authentication-method: client_secret_basic
authorization-grant-type: client_credentials
scope: inventaire.lecture,inventaire.ecriture
service-rapports:
# … même forme, scope inventaire.lecture
provider:
tosiam:
token-uri: http://localhost:8080/tosiam/oauth2/ref/access_tokenSeule l’adresse du point des jetons est donnée (token-uri), sans découverte au démarrage : le service démarre même si TOSIAM est arrêté, et réessaie au cycle suivant.
OAuth2ClientConfig.java crée le gestionnaire des jetons et le RestClient de l’API :
var manager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(registrations, authorizedClients);
manager.setAuthorizedClientProvider(OAuth2AuthorizedClientProviderBuilder.builder().clientCredentials().build());
RestClient inventaire = builder.baseUrl(apiBaseUrl)
.requestInterceptor(new OAuth2ClientHttpRequestInterceptor(manager))
.build();Chaque appel désigne le client à utiliser ; l’intercepteur obtient le jeton (ou reprend celui en cache) et ajoute l’en-tête Authorization: Bearer … :
List<Map<String, Object>> articles = inventaire.get().uri("/inventaire/articles")
.attributes(clientRegistrationId("service-rapports"))
.retrieve()
.body(new ParameterizedTypeReference<>() { });Le gestionnaire garde le jeton de chaque client et en redemande un une minute avant son expiration (marge d’horloge par défaut de Spring Security) : le service n’appelle pas TOSIAM à chaque requête.
Étape 5 : lancer et tester#
Dans deux terminaux, l’API puis le service :
cd tosiam-samples/tosiam-client-credentials/api
./mvnw spring-boot:run # http://localhost:8083cd tosiam-samples/tosiam-client-credentials/client
./mvnw spring-boot:runLe journal du service montre les deux clients :
service-commandes : nouveau jeton d'accès de TOSIAM (scopes [inventaire.lecture, inventaire.ecriture], expire à 22:00:27)
service-commandes : 2 articles en stock
service-commandes : article ajouté {id=3, nom=Câble USB-C, quantite=10}
service-rapports : nouveau jeton d'accès de TOSIAM (scopes [inventaire.lecture], expire à 22:00:27)
service-rapports : 3 articles en stock
service-rapports : l'API répond 403 ForbiddenTrente secondes plus tard, les appels recommencent sans nouvelle ligne « nouveau jeton » : chaque service réutilise son jeton. Le message vient du gestionnaire des jetons, qui ne l’écrit que lorsque TOSIAM en délivre un :
manager.setAuthorizationSuccessHandler((client, principal, attributes) -> {
log.info("{} : nouveau jeton d'accès de TOSIAM (scopes {}, expire à {})", …);
authorizedClients.saveAuthorizedClient(client, principal); // comportement par défaut : garder le jeton
});Les mêmes vérifications à la main, avec le jeton de l’étape 2 dans la variable TOKEN :
| Requête | Réponse |
|---|---|
GET /inventaire/articles sans jeton | 401 |
GET /inventaire/articles avec le jeton de service-rapports | 200 et la liste des articles |
POST /inventaire/articles avec le jeton de service-rapports | 403 (pas de scope inventaire.ecriture) |
POST /inventaire/articles avec un jeton de service-commandes | 201 et l’article créé |
| Jeton altéré (un caractère de la signature changé) | 401 |
Étape 6 : révocation et validation locale#
Valider localement a un prix : l’API ne sait pas qu’un jeton a été révoqué. Sur la QuickStart :
curl -s -u service-rapports:service-rapports-secret -d "token=$TOKEN" \
http://localhost:8080/tosiam/oauth2/ref/token/revoke # 200 : jeton révoqué
curl -s -u service-rapports:service-rapports-secret -d "token=$TOKEN" \
http://localhost:8080/tosiam/oauth2/ref/introspect # {"active":false}
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN" \
http://localhost:8083/inventaire/articles # 200 : toujours acceptéTOSIAM sait que le jeton n’est plus valide, mais l’API, qui ne lui pose pas la question, l’accepte jusqu’à son expiration, plus la minute de tolérance d’horloge de Spring Security. Deux façons de réduire ce délai :
- des jetons courts : réglez la durée de vie des jetons d’accès du fournisseur (une heure par défaut) au délai de révocation acceptable ; les services redemandent des jetons plus souvent ;
- l’introspection pour les opérations sensibles : l’API interroge TOSIAM (
/oauth2/ref/introspect) et obtient l’état réel du jeton, au prix d’un appel par requête (voir le tutoriel Angular).
En cas de problème#
| Symptôme | Cause probable |
|---|---|
invalid_client au point des jetons | Identifiant ou secret faux, client absent du royaume ref, ou méthode d’authentification du client différente de client_secret_basic |
L’API répond 401 avec un jeton tout juste obtenu | Jeton opaque (option Émettre les jetons d’accès sous forme de JWT désactivée), client absent de allowed-clients, ou émetteur différent de app.tosiam.issuer |
L’API répond 401, l’en-tête WWW-Authenticate contient « JOSE header typ (type) at+jwt not allowed » | Vérificateur de type par défaut de Nimbus : at+jwt n’est pas accepté sans le réglage de l’étape 3 |
L’API répond 403 | Jeton sans le scope de l’opération : inventaire.lecture pour lire, inventaire.ecriture pour ajouter |
[invalid_token_response] … Connection refused dans le journal du service | TOSIAM arrêté : le service réessaie au cycle suivant |
I/O error on GET request for "http://localhost:8083/…" dans le journal du service | API arrêtée : le service réessaie au cycle suivant |
L’API répond 401 et son journal contient une erreur d’entrée-sortie sur jwk_uri | TOSIAM injoignable : passé les cinq minutes de cache, l’API ne peut plus relire les clés publiques. Elle reprend seule quand TOSIAM revient |
L’API répond 401 : « Another algorithm expected » | Algorithme de signature absent de la liste du décodeur (étape 3) |
| Un jeton révoqué est encore accepté | Comportement attendu de la validation locale (étape 6) |
En production#
- Secrets : passez-les par des variables d’environnement ou un coffre de secrets, jamais dans le dépôt. Pour aller plus loin, remplacez le secret par une assertion signée (
private_key_jwt) ou par un certificat client (mTLS), voir Enregistrer un client. - Un client par service, avec les seuls scopes dont il a besoin : un secret volé ne donne que les droits de ce service.
- Durée de vie des jetons choisie en fonction du délai de révocation acceptable (étape 6).
- HTTPS entre les services, l’API et TOSIAM.
- Rotation des clés : l’API recharge le jeu de clés quand elle rencontre un
kidinconnu ; gardez l’ancienne clé publiée le temps que les jetons qu’elle a signés expirent.
Pour aller plus loin#
- Les grant types :
client_credentialset les autres flux de TOSIAM. - Jetons : jetons opaques ou JWT, introspection et révocation.
- Application Angular avec OpenID Connect : une API qui vérifie les jetons par introspection.
- Application web Spring Boot avec OpenID Connect : un client confidentiel avec un utilisateur, qui se connecte avec
oauth2Login.
Mis à jour le