Skip to main content
Version: Next

OIDC

Nauthilus provides a built-in OpenID Connect provider with:

  • Authorization Code
  • Device Authorization
  • Client Credentials

The current configuration root is:

  • identity.oidc

Endpoints

  • GET /.well-known/openid-configuration
  • GET /oidc/authorize
  • POST /oidc/token
  • POST /oidc/introspect
  • GET /oidc/userinfo
  • GET /oidc/jwks
  • GET /oidc/logout
  • POST /oidc/device
  • GET /oidc/device/verify

Optional legacy GET support for /oidc/token is controlled by:

  • identity.oidc.tokens.token_endpoint_allow_get

Client Authentication Metadata

OIDC discovery publishes token and introspection client-auth metadata separately:

MetadataValues
token_endpoint_auth_methods_supportedConfigured through identity.oidc.token_endpoint_auth_methods_supported; defaults to client_secret_post, client_secret_basic, private_key_jwt, and none.
token_endpoint_auth_signing_alg_values_supportedRS256 and EdDSA when private_key_jwt is advertised for the token endpoint.
introspection_endpoint_auth_methods_supportedclient_secret_post, client_secret_basic, and private_key_jwt.
introspection_endpoint_auth_signing_alg_values_supportedRS256 and EdDSA.

The introspection endpoint intentionally does not advertise or accept none client authentication.

For private_key_jwt, the JWT aud claim must be the exact endpoint URL. Use issuer + "/oidc/token" for token requests and issuer + "/oidc/introspect" for introspection requests. A token-endpoint assertion is not reusable for introspection.

CORS for Discovery

OIDC discovery endpoints are often fetched directly by browser-based clients. Configure cross-origin behavior under runtime.servers.http.cors, not directly below runtime.http:

runtime:
servers:
http:
cors:
enabled: true
policies:
- name: "oidc_discovery"
enabled: true
path_prefixes:
- "/.well-known/"
allow_origins:
- "https://app.example.com"
allow_methods:
- "GET"
- "OPTIONS"
allow_headers:
- "Authorization"
- "Content-Type"
expose_headers: []
allow_credentials: false
max_age: 600

Policies are evaluated in order. The first enabled policy whose path_prefixes entry matches the request path controls the response.

If Nauthilus is behind a reverse proxy or identity sidecar that filters response headers, forward at least Vary and the Access-Control-* response headers. Otherwise the browser can report a missing Access-Control-Allow-Origin header even though Nauthilus emitted it.

Example

identity:
oidc:
enabled: true
issuer: "https://idp.example.com"
signing_keys:
- id: "main"
key_file: "/etc/nauthilus/keys/oidc.pem"
algorithm: "RS256"
active: true
custom_scopes:
- name: "tenant"
description: "Tenant information"
claims:
- name: "tenant_id"
type: "string"
scopes_supported:
- "openid"
- "profile"
- "email"
- "groups"
- "offline_access"
response_types_supported:
- "code"
subject_types_supported:
- "public"
id_token_signing_alg_values_supported:
- "RS256"
token_endpoint_auth_methods_supported:
- "client_secret_basic"
- "client_secret_post"
- "private_key_jwt"
- "none"
code_challenge_methods_supported:
- "S256"
claims_supported:
- "sub"
- "name"
- "email"
- "preferred_username"
- "groups"
consent:
ttl: 720h
mode: "all_or_nothing"
tokens:
default_access_token_lifetime: 1h
default_refresh_token_lifetime: 720h
revoke_refresh_token: true
token_endpoint_allow_get: false
logout:
front_channel_supported: true
front_channel_session_supported: false
back_channel_supported: true
back_channel_session_supported: false
device_flow:
code_expiry: 10m
polling_interval: 5
user_code_length: 8
clients:
- name: "Example Web App"
client_id: "example-web"
client_secret: "change-me"
redirect_uris:
- "https://app.example.com/callback"
scopes:
- "openid"
- "profile"
- "email"
- "offline_access"
grant_types:
- "authorization_code"
token_endpoint_auth_method: "client_secret_basic"
required_scopes:
- "openid"
optional_scopes:
- "profile"
- "email"
require_mfa:
- "totp"
supported_mfa:
- "totp"
- "webauthn"
required_mfa_level: 2

PKCE

  • only S256 is supported
  • public clients must use PKCE
  • public means no client_secret or token_endpoint_auth_method: none

Client Types and Grant Boundaries

Nauthilus enforces the security properties of each client type at configuration and request time:

  • client_credentials requires confidential client authentication. A public client configured for this grant is rejected during configuration validation.
  • A client_credentials request must not request openid. Service tokens do not represent an end user and do not receive an ID token or UserInfo claims; such a request fails with invalid_scope.
  • Confidential device-authorization clients must authenticate before Nauthilus allocates device-flow state.
  • When a request selects private_key_jwt, a failed assertion does not fall back to a client secret.
  • Public Authorization Code clients must use PKCE with S256.

Client Credentials access tokens are bound to the Nauthilus backchannel resource. JWT tokens carry the issuer-owned claims token_type=access_token and aud=nauthilus:backchannel; opaque tokens resolve to the same claims during server validation. Backchannel authorization rejects ID tokens and access tokens for another audience.

Do not include openid in the scope list of a machine-to-machine client:

identity:
oidc:
clients:
- name: "Operations Client"
client_id: "operations-client"
client_secret: "change-me"
grant_types:
- "client_credentials"
token_endpoint_auth_method: "client_secret_basic"
scopes:
- "nauthilus:authenticate"
- "nauthilus:admin"
- "nauthilus:security"

Redirect URI Validation

Exact matching is the default for identity.oidc.clients[].redirect_uris. Controlled exceptions are available for native clients and explicitly configured wildcard patterns:

  • A trailing * is allowed only at the end of a configured URI and not after a query string.
  • Wildcard matching preserves parsed scheme, host, port, and path-segment boundaries. A prefix for https://app.example/callback/* cannot match another host or /callback-evil.
  • Query and fragment components of the requested URI are ignored only while evaluating a permitted trailing wildcard.
  • A configured * matches HTTP and HTTPS redirect URIs and is strongly discouraged in production.
  • HTTP loopback redirects for 127.0.0.1, localhost, and ::1 may use a dynamic port.
  • Wildcards are disabled for requested URIs with user-info or unsafe path traversal segments, including encoded forms.
  • post_logout_redirect_uri is always checked exactly against post_logout_redirect_uris.

Prefer exact redirect URIs. Use a loopback exception only for native applications, and keep wildcard patterns within a dedicated callback path.

Token and Claim Boundaries

UserInfo requires an access token whose granted scopes contain openid. ID tokens are identity assertions, not API credentials, and introspection reports them as inactive. Successful access-token introspection includes token_type: "Bearer".

Custom ID-token and access-token claim mappings cannot override issuer-owned protocol and security claims such as iss, sub, aud, exp, iat, scope, client_id, nonce, auth_time, or token_type. Keep application claims in distinct names instead of relying on collisions with standard claims.

OIDC back-channel logout calls do not follow HTTP redirects. Configure backchannel_logout_uri as the final endpoint; a 3xx response is a failed delivery.

MFA Assurance

Each browser client may configure require_mfa, supported_mfa, required_mfa_level, and local mfa_policy.levels. Enrollment, offered challenges, and the strength of the current proof are evaluated separately. See MFA Assurance Policy for the complete model.

Backchannel Bearer Access

If you want to use OIDC-issued Bearer tokens for /api/v1/* backchannel calls, enable:

auth:
backchannel:
oidc_bearer:
enabled: true

Related request-header propagation for subject sources/logging lives at:

  • auth.request.headers.oidc_cid

Clients that validate opaque OIDC bearer tokens through /oidc/introspect can authenticate with client secrets or private_key_jwt. For private_key_jwt, use the introspection endpoint URL as the client-assertion audience.

Every Bearer-authenticated backchannel request also needs nauthilus:authenticate, even when the route has an additional administrative scope. See REST API for the route matrix and Basic-authentication behavior.

Built-in Scope Families

The reserved nauthilus:* scopes are used for backchannel administration, authority RPCs, and MFA/WebAuthn operations. They are granted through identity.oidc.clients[].scopes like any other allowed scope.

ScopePurpose
nauthilus:adminFull administrative access to backchannel API surfaces that accept admin bearer tokens.
nauthilus:securitySecurity-related backchannel access such as brute-force and metrics controls.
nauthilus:authenticatePassword authentication through backchannel or gRPC authority APIs.
nauthilus:lookup_identityTrusted identity lookup and remote user resolution.
nauthilus:list_accountsAccount listing.
nauthilus:mfa_readRead public MFA state.
nauthilus:mfa_verifyVerify TOTP or recovery-code material.
nauthilus:mfa_writeRegister, update, or delete TOTP and recovery-code state.
nauthilus:webauthn_readRead public WebAuthn credential descriptors.
nauthilus:webauthn_writeSave, update, or delete WebAuthn credentials.
nauthilus:attribute_readRelease identity attributes to a trusted authority caller.

For split edge/authority deployments, configure the edge service-principal client with only the scopes needed by its remote backend allowed_operations.