Contract
Issuer and OIDC profile
The issuer, discovery, Authorization Code with PKCE S256, exact redirects, state and nonce, scope openid profile email, access tokens addressed to the client itself.
Issuer and discovery
Section titled “Issuer and discovery”https://id.institutourupema.com.br/realms/urupema| Item | Value |
|---|---|
| Issuer | permanent; the iss of every token |
| Discovery | https://id.institutourupema.com.br/realms/urupema/.well-known/openid-configuration; read it at boot; its issuer must equal the configured one |
| JWKS | the jwks_uri from discovery; no pinned key; follow rotation |
Endpoint paths belong to the engine and change with it; discovery is the contract. See Endpoints.
Authorization Code only. The implicit flow (response_type=token), the password grant, client credentials, device and CIBA grants and token exchange are disabled on every managed client.
PKCE S256, mandatory, also for confidential clients (RFC 9700). A request without code_challenge, or with plain, is refused.
Exact redirects. No wildcards; https, or http only on localhost and 127.0.0.1. A redirect_uri that differs from a registered one by one character gets an IdP error page, not a return to the app.
Contract scopes only. openid profile email. Any other scope (phone, roles, offline_access, address…) returns invalid_scope.
Access tokens with a destination. The access token’s aud is the client itself, with no engine roles. An access token serves only the client that requested it.
Single-use state, bound to the request. Generate one per login, keep it on the server, compare it on the callback, then discard it.
nonce and ID token validation. Send a nonce; on the callback validate the ID token: signature against the JWKS, iss equals the issuer, aud contains your client_id, exp in the future, nonce equals the one sent.
The client secret stays on the server. Never in the browser, the repository or the logs. An app that runs only in a browser or device is a public client and has no secret.
The ID token is not API authorization. It identifies who signed in. An API validates the access token: signature, iss, exp, aud. See API: validate the token.
Authorization request
Section titled “Authorization request”Line-broken for reading; the endpoint is authorization_endpoint from discovery.
GET <authorization_endpoint> ?client_id=my-app &response_type=code &scope=openid%20profile%20email &redirect_uri=https%3A%2F%2Fmy-app.institutourupema.com.br%2Fcallback &state=Q2hhbmdlTWU... ← random, single use &nonce=bm9uY2UtMTIz... ← random, comes back inside the ID token &code_challenge=E9Melhoa2Ow... ← BASE64URL(SHA-256(code_verifier)) &code_challenge_method=S256The callback URL also carries iss (RFC 9207, authorization_response_iss_parameter_supported). openid-client checks it; a library that ignores it is safe if it validates the ID token’s iss.
Code exchange at token_endpoint: grant_type=authorization_code, the code, the same redirect_uri and the code_verifier; a confidential client authenticates with its secret (client_secret_basic or client_secret_post). A code works once.
Refused requests
Section titled “Refused requests”| Request | The IdP answers |
|---|---|
no code_challenge | back to the app with error=invalid_request |
code_challenge_method=plain | back to the app with error=invalid_request |
scope=openid phone (or roles, offline_access, address) | back to the app with error=invalid_scope |
response_type=token (implicit) | back to the app with error=unauthorized_client |
a redirect_uri not registered exactly | an IdP error page (HTTP 400); never back to the app |
grant_type=password at the token endpoint | HTTP 400, unauthorized_client |
Symptoms and fixes: Errors.