Skip to main content
Star OpenZiti on GitHub Star

OIDC Authentication

The OpenZiti controller is an OpenID Connect (OIDC) provider. It exposes a standards-compliant OIDC server that clients use to authenticate and receive security tokens. These tokens are then used to authorize subsequent API calls and service access.

OIDC authentication uses the Authorization Code Flow with PKCE (Proof Key for Code Exchange), which is the recommended flow for native and mobile clients. All OpenZiti SDKs and tunnelers implement this flow automatically. The PKCE flow is designed for public clients that cannot safely store a client secret.

Upon successful authentication, the controller issues three tokens:

  • Access Token - a short-lived JWT used to authorize all API requests
  • ID Token - a JWT containing identity information about the authenticated user
  • Refresh Token - a longer-lived token used to obtain a new access token when the current one expires

The access token replaces the legacy zt-session opaque token as the primary credential for API requests. Both mechanisms remain supported. See Using Tokens for details.

OIDC Endpoints

The controller exposes a standard OIDC discovery document that clients use to locate all authentication endpoints. The discovery document is available at:

GET /.well-known/openid-configuration

The OIDC provider path prefix is /oidc. All OIDC protocol endpoints (authorization, token, JWKS, etc.) are exposed under this prefix and are listed in the discovery document. The discovery document also appears at /oidc/.well-known/openid-configuration for compatibility with clients that include the path prefix when constructing the discovery URL.

Key endpoints returned by the discovery document include:

EndpointPathPurpose
Authorization/oidc/authorizationInitiates the PKCE authentication flow
Token/oidc/tokenExchanges a code for tokens, also used for token refresh
JWKS/oidc/keysReturns the public keys used to verify tokens
End Session/oidc/end_sessionTerminates the OIDC session (logout)
Userinfo/oidc/userinfoReturns claims about the authenticated identity

Authentication-specific login endpoints are exposed under /oidc/login/ and are described in the Authentication Flow section.

Configuration

Default configuration

OIDC is enabled automatically whenever the edge section is present in the controller configuration. By default, the controller co-locates the OIDC API on the same web listener as the edge-client API. No explicit configuration is required for basic operation.

Token durations

Token lifetimes are configured in the edge.oidc section of the controller configuration file. The defaults are suitable for most deployments:

edge:
oidc:
# (optional, default 30m) Lifetime of issued access tokens. Minimum 1m.
# Must be at least 1 minute shorter than refreshTokenDuration.
accessTokenDuration: 30m

# (optional, default 30m) Lifetime of issued ID tokens. Minimum 1m.
idTokenDuration: 30m

# (optional, default 24h) Lifetime of issued refresh tokens.
# Must be at least 1 minute longer than accessTokenDuration.
refreshTokenDuration: 24h

Shortening accessTokenDuration reduces the window during which a stolen access token is usable. Shortening refreshTokenDuration forces clients to re-authenticate more frequently. When refreshTokenDuration is set, it must be at least one minute longer than accessTokenDuration. The controller enforces this and adjusts values that violate the constraint.

OIDC bind point

The edge-oidc binding is the API binding label for the OIDC server. By default, the controller automatically adds the edge-oidc binding to any web listener that hosts edge-client, even when it is not explicitly listed. This automatic co-location is the recommended approach for most deployments.

To configure the OIDC binding explicitly, for example to specify allowed redirect URIs, add it to the apis list of the appropriate web listener:

web:
- name: public-api
bindPoints:
- interface: 0.0.0.0:443
address: controller.example.com:443
apis:
- binding: edge-client
- binding: edge-oidc
options:
redirectURIs:
- "http://localhost:*/auth/callback"
- "http://127.0.0.1:*/auth/callback"

The redirectURIs option controls which callback URLs the OIDC provider will accept. Wildcard * is supported in the port position to allow clients that listen on a dynamically assigned local port (common in native application flows). The defaults permit localhost callbacks. Production deployments should review this list.

Disabling auto-binding

To suppress the automatic co-location of edge-oidc with edge-client and require explicit configuration, set disableOidcAutoBinding to true:

edge:
api:
# (optional, default false) When true, the OIDC API is only bound where explicitly configured
# in the web section. The default false automatically adds edge-oidc to every listener
# that hosts edge-client.
disableOidcAutoBinding: true

With disableOidcAutoBinding: true, OIDC is only available on listeners where edge-oidc is explicitly listed as an API binding.

Separate OIDC bind points

The OIDC endpoint can be served from a different network interface or port than the edge-client or edge-management APIs. This is useful when the OIDC authorization flow must be reachable from a browser (requiring a hostname with a valid TLS certificate) while the other APIs are on a separate internal interface.

To bind the OIDC API independently, add a named server to the web section that hosts only edge-oidc:

web:
- name: internal-apis
bindPoints:
- interface: 10.0.0.1:443
address: controller.internal:443
apis:
- binding: edge-management
- binding: edge-client
- binding: edge-oidc
options:
redirectURIs:
- "http://localhost:*/auth/callback"

- name: public-oidc
bindPoints:
- interface: 0.0.0.0:8443
address: auth.example.com:8443
apis:
- binding: edge-oidc
options:
redirectURIs:
- "https://app.example.com/auth/callback"

In multi-bind-point deployments, the OIDC issuer URL is derived from the bind point address that the client connects to. Each bind point issues tokens with an iss claim that reflects the address used for that connection. Clients must use the same address for token verification as they used for authentication.

note

When disableOidcAutoBinding is false (the default) and edge-oidc is listed explicitly in at least one listener, auto-binding is suppressed and the explicit configuration is used as-is.

Authentication flow

Authentication proceeds in two phases: primary authentication establishes the identity. Secondary authentication (if required) satisfies additional factors such as TOTP or a required external JWT.

Step 1 - Discover endpoints

Clients retrieve the OIDC discovery document to learn the authorization and token endpoint URLs:

GET /.well-known/openid-configuration

Step 2 - Initiate Authorization

The client constructs a PKCE authorization request. The controller's OIDC client ID is openziti.

GET /oidc/authorization
?response_type=code
&client_id=openziti
&redirect_uri=http%3A%2F%2Flocalhost%3A20314%2Fauth%2Fcallback
&scope=openid
&code_challenge=<base64url-sha256-of-verifier>
&code_challenge_method=S256
&method=<auth-method>

The optional method parameter hints at the intended primary authentication method (password, cert, ext-jwt). If omitted, the controller selects the method based on whether a client TLS certificate was presented. The authorization endpoint responds with a redirect to the appropriate login URL.

Step 3 - Submit Primary credentials

The controller redirects the client to a login URL of the form:

/oidc/login/<method>?authRequestID=<id>

The authRequestID value identifies the in-progress authentication request. The client POSTs credentials to the same URL. See Primary Authentication for method-specific request formats.

Step 4 - Satisfy Secondary factors (if required)

If the identity's Authentication Policy requires secondary factors, the login endpoint returns HTTP 200 with a JSON body listing the outstanding Authentication Queries rather than redirecting to the callback. The client satisfies each query and resubmits. See Secondary Authentication for details.

Step 5 - Exchange code for tokens

Once all authentication factors are satisfied, the controller redirects to the client's redirect_uri with an authorization code. The client exchanges the code for tokens:

POST /oidc/token

grant_type=authorization_code
&code=<authorization-code>
&redirect_uri=<redirect-uri>
&client_id=openziti
&code_verifier=<pkce-verifier>

Content-Type: application/x-www-form-urlencoded

A successful response:

{
"access_token": "eyJhbGci...",
"id_token": "eyJhbGci...",
"refresh_token": "...",
"token_type": "Bearer",
"expires_in": 1800
}

Primary Authentication

Primary Authentication establishes which Identity is authenticating. The method is determined by the login URL path and the credentials submitted. All login endpoints accept either application/json or application/x-www-form-urlencoded request bodies.

All requests include the authRequestID from the authorization redirect, either as a query parameter (GET) or in the request body (POST).

Username / Password (UPDB)

POST /oidc/login/username

{
"authRequestId": "<auth-request-id>",
"username": "my-identity",
"password": "my-password"
}

Username/password is the simplest primary method. It is disabled by default in production-oriented Authentication Policies in favor of certificate or JWT authentication.

Client Certificate

POST /oidc/login/cert

{
"authRequestId": "<auth-request-id>"
}

Certificate authentication requires that the HTTP connection to the controller use a client TLS certificate associated with the target identity. The body is empty. The controller reads the certificate from the TLS handshake. The certificate must be issued by the OpenZiti PKI or a registered and enabled 3rd Party CA.

External JWT (ext-jwt)

POST /oidc/login/ext-jwt

HTTP Header: Authorization: Bearer <jwt-from-external-provider>

{
"authRequestId": "<auth-request-id>"
}

External JWT authentication requires a valid JWT from a configured External JWT Signer in the Authorization header. The controller validates the JWT's signature, expiration, issuer, and audience against the matching External JWT Signer configuration, then maps the configured claim to an identity.

Secondary Authentication

Secondary authentication is triggered when the identity's Authentication Policy requires additional factors beyond the primary credential. Outstanding secondary factors are represented as Authentication Queries on the in-progress auth request.

When secondary factors remain unsatisfied, the login endpoint returns HTTP 200 with the pending queries instead of redirecting to the callback URL:

{
"authQueries": [
{
"typeId": "MFA",
"format": "alphaNumeric",
"httpMethod": "POST",
"httpUrl": "/oidc/login/totp",
"minLength": 6,
"maxLength": 6,
"provider": "ziti"
}
]
}

Clients can also fetch outstanding auth queries at any point during an in-progress authentication:

GET /oidc/login/auth-queries?id=<auth-request-id>

Response headers during secondary auth

When TOTP is required, the controller sets the totp-required header on the response:

totp-required: true

Clients can use this header to detect that TOTP is required without parsing the authQueries body.

TOTP (Time-Based One-Time Password)

If the Authentication Policy's secondary.requireTotp is true, the client must provide a TOTP code generated by an authenticator application (Google Authenticator, Authy, Microsoft Authenticator, etc.) after primary Authentication succeeds.

POST /oidc/login/totp

{
"id": "<auth-request-id>",
"code": "123456"
}

On success, the controller redirects to the callback URL with the authorization code. On failure, the response returns HTTP 400 with an error indicating the code was invalid.

TOTP Enrollment during an OIDC flow

If the identity's Authentication Policy requires TOTP but the identity has not yet enrolled in TOTP, enrollment can be completed mid-OIDC-flow. The auth request remains active while enrollment is completed.

Start enrollment:

POST /oidc/login/totp/enroll

{
"authRequestId": "<auth-request-id>"
}

Response:

{
"isVerified": false,
"provisioningUrl": "otpauth://totp/my-identity?issuer=controller.example.com&secret=JBSWY3DPEHPK3PXP",
"recoveryCodes": ["abc123", "def456", "..."]
}

The provisioningUrl is an otpauth:// URI that authenticator applications understand, typically displayed as a QR code. The recovery codes are one-time-use backup codes.

Verify enrollment:

POST /oidc/login/totp/enroll/verify

{
"authRequestId": "<auth-request-id>",
"code": "123456"
}

A successful verification confirms that the authenticator application is correctly configured. The controller saves the TOTP configuration and from this point forward, the identity's Authentication Policy will require a TOTP code on every authentication.

Cancel enrollment:

DELETE /oidc/login/totp/enroll

{
"authRequestId": "<auth-request-id>"
}

This abandons the in-progress enrollment and allows a fresh enrollment to be started.

External JWT as a secondary factor

When the Authentication Policy's secondary.requireExtJwt is set to an External JWT Signer ID, every API request must include a valid JWT from that signer in addition to the primary access token. During an OIDC flow, the controller checks for the secondary JWT at the time of primary authentication.

The secondary JWT is provided in the Authorization header alongside the primary credential during the login request:

POST /oidc/login/ext-jwt or POST /oidc/login/username

HTTP Header: Authorization: Bearer <secondary-jwt>

The controller evaluates the secondary JWT against the required External JWT Signer. If the JWT is valid, the secondary factor is satisfied and authentication proceeds to the callback. If it is missing, expired, or invalid, authentication is blocked and the applicable WWW-Authenticate challenge is returned.

After the OIDC flow is complete and an access token has been issued, the secondary JWT requirement is re-evaluated on every subsequent API request (see WWW-Authenticate Headers).

Partial Authentication

An OIDC authentication request is partially authenticated when primary credentials have been accepted but one or more secondary factors remain outstanding. The authorization code has not yet been issued, so no access token exists yet. The client is still in the middle of the OIDC login flow.

During partial authentication, only the OIDC login endpoints are accessible. These include:

ActionEndpoint
List outstanding auth queriesGET /oidc/login/auth-queries?id=<authRequestId>
Submit a TOTP codePOST /oidc/login/totp
Start TOTP enrollmentPOST /oidc/login/totp/enroll
Verify and complete TOTP enrollmentPOST /oidc/login/totp/enroll/verify
Cancel TOTP enrollmentDELETE /oidc/login/totp/enroll

The primary authentication endpoints (/oidc/login/username, /oidc/login/cert, /oidc/login/ext-jwt) may be retried with corrected credentials if the initial attempt failed, but the authorization code is only issued once all secondary factors are satisfied and the controller redirects to the callback URL.

No Edge Client or Edge Management API calls can be made during partial authentication in OIDC. There is no token to present until the full OIDC flow completes.

Tokens

Access Token

The access token is a short-lived JWT that authorizes API requests. It is used as a Bearer token in the Authorization header:

Authorization: Bearer <access-token>

The access token's sub claim contains the OpenZiti Identity ID. The token also carries OpenZiti-specific custom claims (prefixed z_) used by the controller to reconstruct session context without a database lookup on every request. Access tokens expire after edge.oidc.accessTokenDuration (default 30 minutes).

Selected custom claims in the access token:

ClaimDescription
z_asidAPI session ID associated with this token
z_iaWhether the identity is an administrator
z_ctConfiguration type IDs requested during authentication
z_tToken type (a for access)
z_iceWhether the API session certificate is extendable

ID Token

The ID token is a standard OIDC JWT that contains identity information about the authenticated user. It is returned alongside the access token but is not used to authorize API calls. SDKs may use the ID token to display user information or to confirm the identity of the authenticated user.

Refresh Token

The refresh token is an opaque token (not a JWT) that can be exchanged for a new access token without requiring the user to re-authenticate. It expires after edge.oidc.refreshTokenDuration (default 24 hours).

Using tokens

OIDC Bearer token (current)

Include the access token in the Authorization header for all API calls:

Authorization: Bearer <access-token>

This applies to both the edge-client API and the edge-management API.

Legacy zt-session token

The legacy zt-session authentication mechanism remains supported for backward compatibility. Clients that authenticate via the legacy POST /edge/client/v1/authenticate or POST /edge/management/v1/authenticate endpoints receive an opaque zt-session token. This token is included in requests using the zt-session header:

zt-session: <token>

New clients and SDKs should use OIDC bearer tokens. The legacy zt-session mechanism may be deprecated in a future release.

Refreshing an Access Token

When an access token expires, use the refresh token to obtain a new one without re-authentication. This requires the offline_access scope to have been requested during the initial authorization.

POST /oidc/token

grant_type=refresh_token
&refresh_token=<refresh-token>
&client_id=openziti

Content-Type: application/x-www-form-urlencoded

A successful response returns a new set of tokens:

{
"access_token": "eyJhbGci...",
"id_token": "eyJhbGci...",
"refresh_token": "...",
"token_type": "Bearer",
"expires_in": 1800
}

The old refresh token is invalidated when a new one is issued. Store the new refresh token for future refreshes.

When the refresh token itself expires, the client must initiate a full authentication flow again.

Response headers

The controller returns several headers on every authenticated API response that describe the session lifetime:

HeaderDescription
expiration-secondsSeconds remaining until the current API session expires
expires-atAbsolute datetime at which the current API session expires

For OIDC-based sessions, "session expiry" is the expiry of the access token. Clients can monitor these headers to proactively refresh the access token before it expires.

WWW-Authenticate headers

On HTTP 401 Unauthorized responses, the controller includes one or more WWW-Authenticate headers that describe exactly what token is missing, expired, or invalid. Clients can parse these headers to determine the corrective action required.

The header follows RFC 6750 (Bearer Token Usage) with extended fields for OpenZiti-specific authentication schemes.

No token provided

When a request is made without any supported security token, the response includes challenges for both the OIDC bearer scheme and the legacy zt-session scheme:

WWW-Authenticate: zt-session realm="zt-session" error="missing" error_description="no matching token was provided",
Bearer realm="openziti-oidc" error="missing" error_description="no matching token was provided"

OIDC Access Token errors

When a Bearer token is provided but is invalid or expired:

WWW-Authenticate: Bearer realm="openziti-oidc" error="expired" error_description="token expired"
WWW-Authenticate: Bearer realm="openziti-oidc" error="invalid" error_description="token is invalid"

External JWT errors (primary)

When the Authentication Policy requires a primary External JWT signer and it is absent, expired, or invalid, the challenge includes the signer's ID and issuer to help clients identify which IdP to authenticate against:

WWW-Authenticate: Bearer realm="openziti-primary-ext-jwt"
error="missing"
error_description="no matching token was provided"
id="<ext-jwt-signer-id>"
issuer="<ext-jwt-signer-issuer>"

External JWT errors (secondary factor)

When a secondary External JWT requirement is not satisfied on an ongoing API request:

WWW-Authenticate: Bearer realm="openziti-secondary-ext-jwt"
error="expired"
error_description="token expired"
id="<ext-jwt-signer-id>"
issuer="<ext-jwt-signer-issuer>"

The id field contains the ID of the External JWT Signer that must be satisfied. The issuer field contains the expected JWT issuer, which clients can use to initiate the correct OIDC flow with the appropriate identity provider.

Error values summary

error valueMeaning
missingNo token of the required type was provided
invalidA token was provided but failed validation (bad signature, wrong audience, etc.)
expiredA token was provided but has passed its expiration time

Realm values summary

realm valueMeaning
openziti-oidcThe OpenZiti OIDC access token (Bearer)
zt-sessionThe legacy zt-session opaque token
openziti-primary-ext-jwtA primary External JWT Signer requirement
openziti-secondary-ext-jwtA secondary (ongoing) External JWT Signer requirement