Skip to content

OAuth / OIDC (OpenID Connect)

OAuth is an authentication framework that involes lots of standards and supports many different use cases.

OIDC is based on OAuth 2.0 for more specific use cases.

As far as I can tell there are two main use cases:

  • login at a website (SP / OIDC Client) using a centralized account via an IdP (OP)
  • authorize a “native application” (email client, cloud-sync daemon, CLI applications, …) to access a certain resource (“scope”) owned by your account

An IdP is identified by its “issuer URL”; the metadata (JSON) for clients (“how to use the IdP”) should be located at ${issuer}/.well-known/openid-configuration.

The are different login flows:

  • Authorization Code Flow (using response type code)
  • Implicit Flow (using response type id_token or id_token token)
  • Hybrid Flow (using any response type that include code and at least one of id_token or token)

Only Authorization Code Flow should be used.

Service (RP) specific applications might use OAuth as well - say a synchronization of local files to a cloud service; in those cases the service (as “Identity Provider”) should have specific documentation how to use the API, including authentication and OAuth (and perhaps how to request tokens that only have restricted access). This document is not about such uses. The service may also use OIDC to authenticate users with an OP, which IS what this document is about.

Clients

A client is identified by a client_id; a client uses this client_id and a client_secret (empty for “Public Clients”, “Confidential Clients” require a proper secret) to authenticate requests with OP endpoints.

In the OIDC Context each SP is a client.

Authentication can be done via (see https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#token-endpoint-auth-method):

client_secret_basic

Using client_id and client_secret to build an HTTP Authorization: Basic ... header.

client_secret_post

Passing client_id and client_secret (might be omitted if empty) as additional parameters to POST data.

Prefer using client_secret_basic instead.

client_secret_jwt, private_key_jwt

See https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication

A client registration usually should include:

  • client_id and client_secret (both usually assigned by the OP)
  • redirect_uris: allowed redirect_uri values for authorization_endpoint
  • if client-initiated logout with redirect is desired: list of allowed post_logout_redirect_uris
  • the client should be given the OP issuer URL
    • clients might be unable/unwilling to read metadata from the OP based on the issuer URL; you might have to copy various fields to the client configuration manually
  • OP should/might impose additional restrictions:
    • allowed scopes
    • allowed response_types in login flow (allow only code for Authorization Code Flow)
    • allowed grant_types in token endpoint: authorization_code required for Authorization Code Flow, refresh_token if desired
    • token_endpoint auth methods: client_secret_basic
  • optionally a service description / homepage URL
  • optionally register JWKs (JSON Web Keys) to encrypt JWTs from OP to client (or sign JWTs from client to OP)

An OP might support dynamic registration of clients: (RFC 7591 OAuth 2.0 Dynamic Client Registration Protocol)[https://datatracker.ietf.org/doc/html/rfc7591].

Note

I recommend not allowing dynamic registration; use either public clients or require explicit registration through a known contact (e.g. registered user on your platform).

Tokens and codes

authorization_code

Actually only named code in protocol fields. Can be used (once) to retrieve id_token and access_token (and possibly a refresh_token) with the token_endpoint.

Requested by response type code; usually expires quickly (e.g. after 10-60 seconds).

access_token

HTTP “bearer” code that can be used to “act” on behalf of the user with target service (e.g. accessing the userinfo_endpoint).

Access is usually limited to certain (requested and granted) scopes and the client the token is bound to.

Requested by response type token.

id_token

Contains basic user information (“claims”) as signed JWT.

Requested by response type id_token or returned by token_endpoint.

When the token is received from token_endpoint the signature isn’t relevant (doesn’t need to be verified). Only when passed through a response type including “id_token” the signature must be verified using the keys located at jwks_uri from the metadata.

refresh_token

Can be used to refresh access_token and refresh_token with the token_endpoint.

Claims

Claims are user attributes and might be returned either in the id_token or from the userinfo_endpoint.

Also see https://openid.net/specs/openid-connect-core-1_0.html#Claims and https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims.

Important claims:

iss

OP issuer URL.

Unclear whether present in userinfo or only id_token.

sub

long-term stable identifier for user. A user is uniquely identified by the OP issuer and this field.

Often randomly generated (and stored) by the OP for a specific client (for privacy reasons), but can also use the same value across multiple clients.

Always present (or might require scope openid, which is required anyway).

name

Full name of user for presentation (scope profile).

preferred_username

Preferred account name; not necessarily unique, but can be if enforced by OP (scope profile).

email

E-mail address (scope email).

email_verified

Whether OP asserts the email address is valid (scope email).

sid:

Session identifier for use with Front-/Back-Channel Logouts

Endpoints at OP

Also see metadata overview: https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#authorization-server-metadata.

authorization_endpoint

Browser endpoint to start login flow with (using a normal redirect / “link open” -> HTTP GET).

Query Parameters are:

  • response_type: should pass code to use Authorization Code Flow (supported values: response_types_supported in metadata)
  • scope: space-separated list of scopes, should include at least openid (supported scopes: scopes_supported in metadata)
  • client_id
  • redirect_uri: where to go (redirect -> HTTP GET) once login finished
  • optional state: forwarded to redirect_uri, should be used to bind login flow to session at client
  • optional nonce: gets included in ID tokens created from this flow (another way to bind login flow to session)
  • optional PCKE (should be used with public clients, not needed with proper client_secrets):
    • code_challenge: (SHA256) hash of a random code_verifier
    • code_challenge_method: should use S256 (supported methods: code_challenge_methods_supported in metadata)
    • server associates code_challenge and code_challenge_method with authorization_code created from this flow
    • client remembers code_verifier to use with token_endpoint and returned authorization_code

Instead of sending all those parameters via the browser redirection a client can use PAR (if supported by the OP); see pushed_authorization_request_endpoint below; then the query parameters are only:

  • client_id
  • request_uri as returned by PAR

pushed_authorization_request_endpoint (PAR)

See PAR: RFC 9126 OAuth 2.0 Pushed Authorization Requests.

Instead of passing parameters to the authorization_endpoint a login flow can be started using this endpoint; parameters are the same with the following exceptions:

  • request_uri not allowed
  • needs client authentication, e.g. by passing client_secret

Todo

unclear if client_secret_basic should work too, not just the client_secret_post variant

As redirect_uri is passed through a trusted channel the OP might allow more or simply any values instead of checking against the set of allowed redirect_uris.

end_session_endpoint

Browser endpoint to trigger logout flow; see https://openid.net/specs/openid-connect-rpinitiated-1_0.html.

Note

client usually should pass id_token of user to logout as id_token_hint; only this client session (if verified!) should be automatically closed; even then the OP might ask for confirmation first. (An OP might trust certain clients to trigger a full logout.)

No GET-request / cross-site request should logout a user without confirmation by the user on the OP site.

The OP might ask whether other client sessions should be closed too and whether a full logout should be triggered.

token_endpoint

API endpoint for clients (HTTP basic authentication with $client_id:$client_secret) to:

  • convert authorization_code to access_token, id_token and refresh_token:
    • POST form-data with:
      • grant_type: authorization_code
      • code: authorization_code from login flow
      • redirect_uri: the redirect_uri used in the request to authorization_endpoint
      • when PCKE was used: code_verifier
    • returning refresh_token probably depends on a certain scope
    • see https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
  • use refresh_token to refresh access_token and refresh_token

For public clients (no/empty client_secret) the request is sent without HTTP authorization, and client_id is passed as additional parameter instead (RFC 6749 §2.3.1. Client Password).

Other grant types (grant_types_supported in metadata):

client_credentials

Privileged client requesting access tokens, see RFC 6749 §4.4. Client Credentials Grant.

password

Get access_token by sending username and password,see RFC 6749 §4.3. Resource Owner Password Credentials Grant.

urn:ietf:params:oauth:grant-type:device_code

See Device Authorization below.

urn:openid:params:grant-type:ciba

See CIBA below.

userinfo_endpoint

API endpoint for clients to get more data (“claims”) for an access_token; uses access_token as bearer authentication.

The id_token might not contain all claims a client is interested in (especially when limited in size due to being passed through URL query parameters); this endpoint returns all data (“claims”) the client is allowed to see.

pushed_authorization_request_endpoint

registration_endpoint

For use with (RFC 7591 OAuth 2.0 Dynamic Client Registration Protocol)[https://datatracker.ietf.org/doc/html/rfc7591].

revocation_endpoint

A client can revoke access_tokens and refresh_tokens using the endpoint; see RFC 7009 OAuth 2.0 Token Revocation.

introspection_endpoint

Tokens (access_tokens and refresh_tokens) can be inspected using this endpoint; usually a client authentication should be required (and only tokens associated with client should be usable).

See RFC 7662 OAuth 2.0 Token Introspection.

device_authorization_endpoint

See Device Authorization below.

backchannel_authentication_endpoint

See CIBA below.

Session concepts

There aren’t many descriptions on how sessions are handled.

  • access_tokens should have a fixed (and short, “use once” style) lifetime, but an explicit logout or revokation must invalidate them - i.e. userinfo_endpoint won’t work anymore and introspection_endpoint should return active=False.
  • refresh_tokens have a longer lifetime; again an explicit logout or revokation must invalidate them.
  • with scope offline_access tokens should outlive the normal OP browser session (see https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess).
  • when the OP browser session ends tokens should be invalidated too (expect for the refresh_tokens)
    • this depends on the OP implementation though
  • explicit logouts should trigger Front- and Back-Channel Logouts
    • probably should use the sid claim, at least for Back-Channel Logouts targetting specific sessions or Front-Channel Logouts.
  • password resets (or accounts getting disabled) should trigger logouts; removing other credentials (MFA tokens) should trigger logouts of related sessions (or simply all sessions)

Front-Channel Logout

See https://openid.net/specs/openid-connect-frontchannel-1_0.html.

Front-Channel uses the browser to tell the client to end the session; this can be used when the OP can’t talk to the client directly (e.g. due to firewall reasons), but the browser can.

Note

I’d avoid this method completely. If an client implements it, it really should require the iss and sid parameters and only end the matching session.

Back-Channel Logout

See https://openid.net/specs/openid-connect-backchannel-1_0.html.

This way an OP can end client sessions. It involves a JWT-signed “Logout Token”, which the client must verify using keys located at jwks_uri in the metadata.

Logins without redirect from browser

A client without a browser (or perhaps running on a device where you wouldn’t want to trigger a normal login and ask for the password) can try other forms of authentication; they’ll require a second device to complete the authentication.

Device Authorization

See RFC 8628 OAuth 2.0 Device Authorization Grant.

The client uses the device_authorization_endpoint from the metadata to start authentication.

The response includes verification_uri and user_code that should be displayed to the user (to complete the authentication on some other device).

As an alternative the response might include a verification_uri_complete (combining verification_uri and user_code) to be communicated in a machine-readable way (e.g. a QR code).

The response also includes a device_code that the client uses to poll the token_endpoint combined with grant_type=urn:ietf:params:oauth:grant-type:device_code.

CIBA: Client-Initiated Backchannel Authentication Flow

See https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html

The client uses the backchannel_authentication_endpoint from the metadata to start authentication; it should include a binding_message, which both client and OP display.

The response for backchannel_authentication_endpoint includes an auth_req_id that can be used with grant_type=urn:openid:params:grant-type:ciba at the token_endpoint (unless CIBA is used in “Push Mode”).

In “Ping Mode” or “Push Mode” the OP notifies the client when the authentication is done, in “Poll Mode” the client polls.

Note

The OP should reject duplicated (even similar) binding_messages; the user must be able to identify mismatches in case of an attack (e.g. by someone watching the screen).

Note

It seems the user will need to find the OP “dashboard” to grant the authentication request some other way (maybe from an email notification, …).