Obexal Docs

Docs/Authentification/OIDC et OAuth 2.1

Fournisseur OIDC et OAuth 2.1

Comment Obexal implémente OpenID Connect et OAuth 2.1 : découverte, JWKS, flux supportés et non supportés, scopes, claims et types de clients.

Obexal est un fournisseur OpenID Connect conforme aux standards, bâti sur OAuth 2.1. Chaque application que vous déclarez (app web, SPA, service backend ou agent IA) est un client OAuth, et toute bibliothèque OIDC standard peut s'y intégrer : aucun SDK propriétaire à embarquer.

Découverte et JWKS

Le document de découverte est la source de vérité des endpoints et des capacités :

curl https://accounts.obexal.com/.well-known/openid-configuration
{
  "issuer": "https://accounts.obexal.com",
  "authorization_endpoint": "https://accounts.obexal.com/oauth/authorize",
  "pushed_authorization_request_endpoint": "https://accounts.obexal.com/oauth/par",
  "token_endpoint": "https://accounts.obexal.com/oauth/token",
  "userinfo_endpoint": "https://accounts.obexal.com/oauth/userinfo",
  "jwks_uri": "https://accounts.obexal.com/.well-known/jwks.json",
  "revocation_endpoint": "https://accounts.obexal.com/oauth/revoke",
  "introspection_endpoint": "https://accounts.obexal.com/oauth/introspect",
  "end_session_endpoint": "https://accounts.obexal.com/oauth/logout",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token", "client_credentials", "urn:ietf:params:oauth:grant-type:token-exchange"],
  "code_challenge_methods_supported": ["S256"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "scopes_supported": ["openid", "profile", "email"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post", "private_key_jwt"]
}

Le JWKS servi sur /.well-known/jwks.json ne contient que des clés publiques. Il publie la clé de signature active ainsi que les clés retirées non encore expirées : les jetons émis avant une rotation de clé restent donc vérifiables.

Note

Les exemples utilisent accounts.obexal.com, le domaine par défaut. Si votre organisation utilise un domaine personnalisé, ce domaine devient l'issuer : remplacez-le partout.

Flux supportés

  • Authorization code avec PKCE : le seul flux interactif. PKCE est obligatoire pour tous les clients, publics comme confidentiels ; code_challenge_method doit valoir S256 (plain est refusé, tout comme l'absence de challenge).
  • Client credentials : identité machine pour les services backend et les agents IA. Clients confidentiels uniquement ; le jeton porte un sub égal au client_id, sans ID token ni refresh token.
  • Refresh token : rotatif, avec détection de rejeu. Voir Valider les jetons.
  • Token exchange (RFC 8693) : délégation attribuable pour les agents IA. Voir Délégation par Token Exchange.

Volontairement non supportés : le flux implicite (response_type autre que code), le grant resource owner password et le grant device authorization. Tout autre grant_type reçoit l'erreur standard unsupported_grant_type. Les endpoints OAuth renvoient des erreurs conformes au protocole, de la forme {"error": "...", "error_description": "..."}.

Types de clients

TypeAuthentification au token endpointUsage typique
Publicnone (pas de secret, PKCE seul)SPA, app mobile, CLI
Confidentielclient_secret_basic, client_secret_post ou private_key_jwtApp web côté serveur, service, agent IA

Vous déclarez les clients dans la console d'administration ou via POST /v1/applications. Le secret d'un client confidentiel n'est renvoyé qu'une seule fois, à la création ; seule son empreinte SHA-256 est stockée. Les redirect URIs doivent être des URL absolues http(s), comparées à l'identique, sans joker.

Scopes et claims

Les scopes standard sont openid (obligatoire), profile et email.

L'access token est un JWT (en-tête typ: at+jwt, signé RS256) portant les claims iss, sub, aud (le client_id par défaut), client_id, exp, iat, jti, scope et tenant. Les jetons délégués ajoutent un claim act (l'agent qui agit), et les jetons liés par DPoP ajoutent cnf.jkt.

L'ID token (en-tête typ: JWT) porte iss, sub, aud, exp, iat, auth_time et nonce (s'il est fourni), plus :

  • groups : les noms des groupes de l'utilisateur, omis s'il n'appartient à aucun groupe.
  • email et email_verified avec le scope email.
  • given_name, family_name, name et locale avec le scope profile, quand ils sont renseignés dans le profil d'annuaire.

Le flux authorization code, pas à pas

Générez un couple PKCE côté client :

VERIFIER=$(openssl rand -base64 60 | tr '+/' '-_' | tr -d '=\n')
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -binary -sha256 | openssl base64 | tr '+/' '-_' | tr -d '=\n')

Envoyez le navigateur de l'utilisateur vers l'endpoint d'autorisation. Avec une session Obexal valide, la réponse est un 302 vers votre redirect_uri ; sans session, l'utilisateur passe d'abord par la page de connexion hébergée puis revient ici.

curl -sS -i -b cookies.txt "https://accounts.obexal.com/oauth/authorize?response_type=code&client_id=app_a1b2c3&redirect_uri=https%3A%2F%2Fapp.example.eu%2Fcallback&scope=openid%20profile%20email&state=xyz&nonce=n-123&code_challenge=$CHALLENGE&code_challenge_method=S256"
# 302 Location: https://app.example.eu/callback?code=<CODE>&state=xyz

Échangez le code (usage unique ; un rejeu renvoie invalid_grant) :

curl -sS -X POST https://accounts.obexal.com/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code' \
  -d 'code=<CODE>' \
  -d 'redirect_uri=https://app.example.eu/callback' \
  -d 'client_id=app_a1b2c3' \
  -d "code_verifier=$VERIFIER"
{
  "access_token": "<JWT RS256>",
  "token_type": "Bearer",
  "expires_in": 600,
  "refresh_token": "<opaque>",
  "id_token": "<JWT>",
  "scope": "openid profile email"
}

Un client confidentiel s'authentifie en plus de PKCE, par exemple avec -u "$CLIENT_ID:$CLIENT_SECRET" (HTTP Basic).

Identité machine : client_credentials

curl -sS -X POST https://accounts.obexal.com/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d 'grant_type=client_credentials' \
  -d 'scope=read'
# 200 -> {"access_token":"<JWT>","token_type":"Bearer","expires_in":600,"scope":"read"}

Le scope demandé doit être un sous-ensemble des scopes déclarés du client.

Pour aller plus loin