AXIAM PHP SDK

OidcClient
in package

FinalYes

The OIDC / SSO relying-party engine (CONTRACT.md §12) behind {@see \Axiam\Sdk\AxiamClient}'s nine public `oidc*`/`introspect`/`revoke`/`sso*` methods.

Per the T1 TypeScript reference's own judgment call 1 ("Host object"): unlike the TypeScript SDK (whose CI forbids Node-only crypto from leaking into a browser bundle, forcing a separate OidcClient), THIS SDK has no such constraint — the canonical §12.2 method names live directly on AxiamClient, exactly as the plan's T8 item says. This class is the internal engine AxiamClient composes (mirroring how it already composes AuthzDispatcher for §1 authz) — never part of the public API surface itself.

Everything below is reuse, not reimplementation (§12 forbids forking):

  • transport, §4 cookie jar, §5 tenant header, §6 TLS → the $http Guzzle client (AxiamClient's OWN $plainHttp — AuthMiddleware only, so a 401 from /oauth2/introspect//oauth2/revoke structurally can never reach RefreshMiddleware and therefore never enters the §9 guard, §12.3 rule 3);
  • §9 single-flight refresh → Session::refreshGuard();
  • §12.4 rules 1–2 (alg/kid/signature) → JwksVerifier::verifyIdTokenSignature(), the SAME verifier instance the §10 middleware already uses for AXIAM's own access tokens (AXIAM's OIDC provider and its own auth server are the SAME origin, so there is exactly one JWKS to trust — no separate per-jwks_uri verifier map is needed here, unlike a generic multi-issuer RP);
  • §12.4 rules 3–6 (issuer/audience/time/nonce) → IdTokenValidator;
  • §7/§12.5 redaction → Sensitive.

Table of Contents

Constants

ACCESS_TOKEN_TYPE  : mixed = 'urn:ietf:params:oauth:token-type:access_token'
The `actor_token_type` this SDK sends, and the `subject_token_type` it sends when the caller names none — an AXIAM-issued access token (§15.1).
BACKCHANNEL_LOGOUT_EVENT  : mixed = 'http://schemas.openid.net/event/backchannel-lo...
The `events` member that distinguishes a logout token from an ID token (OIDC Back-Channel Logout 1.0 §2.4).
DEFAULT_POLL_INTERVAL_SECONDS  : mixed = 5
Polling interval used when the authorization response omits `interval` (RFC 8628 §3.2, §14.2 rule 2). An SDK MUST NOT hard-code a faster floor.
DEVICE_CODE_GRANT_TYPE  : mixed = 'urn:ietf:params:oauth:grant-type:device_code'
`grant_type` of the device access-token request (RFC 8628 §3.4).
DISCOVERY_PATH  : mixed = '/.well-known/openid-configuration'
JWT_TOKEN_TYPE  : mixed = 'urn:ietf:params:oauth:token-type:jwt'
A JWT from a trusted external issuer — the cross-domain exchange of §15.7.
MAX_LOGOUT_TOKEN_AGE_SECONDS  : mixed = 300
Maximum accepted age for a logout token's `iat`, in seconds. AXIAM issues them with a 120 s lifetime; this bound is the same order and stops a token captured from a mis-configured RP being replayed days later.
MIN_DISCOVERY_TTL_SECONDS  : mixed = 300
Minimum — and default — discovery-cache TTL, in seconds. CONTRACT.md §12.3 rule 6 sets a floor of 5 minutes; a smaller configured value is raised to it.
RREG_PATH  : mixed = '/uma2/rreg/resource_set'
FedAuthz §2.2 resource registration endpoint.
SLOW_DOWN_INCREMENT_SECONDS  : mixed = 5
Seconds added to the polling interval on each `slow_down` (§14.2 rule 1). The increase is permanent and cumulative.
SSO_CALLBACK_PATH  : mixed = '/api/v1/auth/federation/oidc/callback'
SSO_HANDOFF_PATH  : mixed = '/api/v1/auth/federation/handoff'
Path of the handoff-code redemption endpoint (contract 1.38).
SSO_OAUTH2_CALLBACK_PATH  : mixed = '/api/v1/auth/federation/oauth2/callback'
Path of the plain-OAuth2 federation step-2 (callback) endpoint (contract 1.38).
SSO_OAUTH2_START_PATH  : mixed = '/api/v1/auth/federation/oauth2/start'
Path of the plain-OAuth2 federation step-1 endpoint (contract 1.38).
SSO_PROVIDERS_PATH  : mixed = '/api/v1/auth/federation/providers'
Path of the public provider-listing endpoint (contract 1.38).
SSO_START_PATH  : mixed = '/api/v1/auth/federation/oidc/start'
TOKEN_EXCHANGE_GRANT_TYPE  : mixed = 'urn:ietf:params:oauth:grant-type:token-exchange'
`grant_type` of an RFC 8693 exchange.
UMA_CLAIM_TOKEN_FORMAT  : mixed = 'urn:ietf:params:oauth:token-type:access_token'
The only `claim_token_format` AXIAM implements. §20.2 rule 2 makes the `claim_token` itself required rather than defaulted; the *format* has one value, so the SDK supplies it.
UMA_PROTECTION_SCOPE  : mixed = 'uma_protection'
The scope a PAT must carry (§20.2 rule 1) — for callers minting one.
UMA_TICKET_GRANT_TYPE  : mixed = 'urn:ietf:params:oauth:grant-type:uma-ticket'
`grant_type` of the UMA 2.0 ticket grant (CONTRACT.md §20.1).

Methods

__construct()  : mixed
deviceAuthorize()  : DeviceAuthorization
`POST /oauth2/device_authorization` (§14.1) — start the device grant and obtain the code pair.
deviceLogin()  : OidcTokenSet
The composed §14.3 helper: start the grant, hand the caller the user code, poll to completion.
devicePoll()  : OidcTokenSet
`POST /oauth2/token` with the device-code grant (§14.1) — **one** poll attempt.
introspect()  : IntrospectionResult
`POST /oauth2/introspect` (RFC 7662, §12.1) — ask the server whether a token is active and, if so, for its metadata.
loginClientCredentials()  : OidcTokenSet
`POST /oauth2/token` with `grant_type=client_credentials` (§12.1) — service-account machine-to-machine login.
logoutUrl()  : string
Build the RP-initiated logout URL to redirect the user agent to (§12.7.2).
normalizeOrigin()  : string
Normalize a URL to its cache key: lowercased scheme and host with the port always explicit (§12.3 rule 6). `https://IAM.example.com/` and `https://iam.example.com:443/x` therefore share one key, while `http://iam.example.com` gets its own.
oidcBegin()  : AuthorizationRequest
Build an authorization request (§12.1) — **pure local computation, no network I/O**.
oidcDiscover()  : OidcConfiguration
`GET /.well-known/openid-configuration` (§12.1) — fetch the OIDC discovery document, cached per origin with a ≥5-minute TTL and single-flight de-duplication of concurrent callers (§12.3 rule 6).
oidcDiscoverAsync()  : PromiseInterface
Async entry point exercised directly by tests wanting to prove single-flight de-duplication under Guzzle's async interface — mirrors {@see JwksVerifier}'s own `ensureFreshAsync()`/`ensureFresh()` split.
oidcExchange()  : OidcTokenSet
`POST /oauth2/token` with `grant_type=authorization_code` (§12.1) — exchange an authorization code for a token set, validating the returned ID token in full before returning.
oidcPar()  : PushedAuthorizationRequest
`POST /oauth2/par` (CONTRACT.md §26.1) — push the authorization request over the back channel and get an opaque handle to redirect with.
oidcRefresh()  : OidcTokenSet
`POST /oauth2/token` with `grant_type=refresh_token` (§12.1) — refresh an {@see OidcTokenSet}, governed by a §9-conformant single-flight refresh guard.
revoke()  : void
`POST /oauth2/revoke` (RFC 7009, §12.1) — revoke an access or refresh token.
ssoComplete()  : SsoCompleteResult
`POST /api/v1/auth/federation/oidc/callback` (§12.1) — step 2 of upstream SSO: consumes the single-use `state`, provisions or links the user, and establishes the session.
ssoCompleteHandoff()  : SsoCompleteResult
`POST /api/v1/auth/federation/handoff` (§12.1) — redeem the single-use code the SAML and Apple flows deliver.
ssoCompleteOauth2()  : SsoCompleteResult
`POST /api/v1/auth/federation/oauth2/callback` (§12.1) — step 2 of a plain-OAuth2 login.
ssoProviders()  : FederationProviderList
`GET /api/v1/auth/federation/providers` (§12.1) — which "Sign in with X" buttons to render for a workspace.
ssoStart()  : SsoStartResult
`POST /api/v1/auth/federation/oidc/start` (§12.1) — step 1 of first-time SSO against an **upstream** IdP. No JWT required.
ssoStartOauth2()  : SsoStartResult
`POST /api/v1/auth/federation/oauth2/start` (§12.1) — step 1 of a login through a **plain-OAuth2** upstream (GitHub, Facebook, `generic_oauth2`).
tokenExchange()  : ExchangedToken
`POST /oauth2/token` with the RFC 8693 grant (§15.1) — exchange a token for a **narrower** one.
umaChallengeHeader()  : string
Format a `WWW-Authenticate: UMA` header (§20.3, emit half) — pure local computation, for a resource server that has just minted a ticket.
umaDeleteResource()  : void
`DELETE /uma2/rreg/resource_set/{id}` (§20.1) — deregister a resource set.
umaExchangeTicket()  : RequestingPartyToken
`POST /oauth2/token` with the UMA ticket grant (§20.1) — redeem a permission ticket for a Requesting Party Token.
umaListResources()  : array<int, string>
`GET /uma2/rreg/resource_set` (§20.1) — the ids **this client** registered.
umaParseChallenge()  : UmaChallenge|null
Parse a `WWW-Authenticate: UMA …` header (§20.3) — pure local computation.
umaReadResource()  : ResourceSet
`GET /uma2/rreg/resource_set/{id}` (§20.1) — read a registered resource set.
umaRegisterResource()  : ResourceSet
`POST /uma2/rreg/resource_set` (§20.1) — register a resource set.
umaRequestTicket()  : Sensitive
`POST /uma2/perm` (§20.1) — mint a permission ticket for the pairs a caller lacks.
umaUpdateResource()  : ResourceSet
`PUT /uma2/rreg/resource_set/{id}` (§20.1) — replace a resource set's state.
verifyLogoutToken()  : VerifiedLogoutToken
Verify a back-channel logout token the OP POSTed to this application's `backchannel_logout_uri` (§12.7.3).

Constants

ACCESS_TOKEN_TYPE

The `actor_token_type` this SDK sends, and the `subject_token_type` it sends when the caller names none — an AXIAM-issued access token (§15.1).

public mixed ACCESS_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token'

BACKCHANNEL_LOGOUT_EVENT

The `events` member that distinguishes a logout token from an ID token (OIDC Back-Channel Logout 1.0 §2.4).

public mixed BACKCHANNEL_LOGOUT_EVENT = 'http://schemas.openid.net/event/backchannel-logout'

DEFAULT_POLL_INTERVAL_SECONDS

Polling interval used when the authorization response omits `interval` (RFC 8628 §3.2, §14.2 rule 2). An SDK MUST NOT hard-code a faster floor.

public mixed DEFAULT_POLL_INTERVAL_SECONDS = 5

DEVICE_CODE_GRANT_TYPE

`grant_type` of the device access-token request (RFC 8628 §3.4).

public mixed DEVICE_CODE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code'

DISCOVERY_PATH

public mixed DISCOVERY_PATH = '/.well-known/openid-configuration'

JWT_TOKEN_TYPE

A JWT from a trusted external issuer — the cross-domain exchange of §15.7.

public mixed JWT_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:jwt'

Pass it as tokenExchange's $subjectTokenType to exchange a partner IdP's token. AXIAM also accepts ACCESS_TOKEN_TYPE for an external issuer, and refuses refresh and ID token types by name.

MAX_LOGOUT_TOKEN_AGE_SECONDS

Maximum accepted age for a logout token's `iat`, in seconds. AXIAM issues them with a 120 s lifetime; this bound is the same order and stops a token captured from a mis-configured RP being replayed days later.

public mixed MAX_LOGOUT_TOKEN_AGE_SECONDS = 300

MIN_DISCOVERY_TTL_SECONDS

Minimum — and default — discovery-cache TTL, in seconds. CONTRACT.md §12.3 rule 6 sets a floor of 5 minutes; a smaller configured value is raised to it.

public mixed MIN_DISCOVERY_TTL_SECONDS = 300

RREG_PATH

FedAuthz §2.2 resource registration endpoint.

public mixed RREG_PATH = '/uma2/rreg/resource_set'

SLOW_DOWN_INCREMENT_SECONDS

Seconds added to the polling interval on each `slow_down` (§14.2 rule 1). The increase is permanent and cumulative.

public mixed SLOW_DOWN_INCREMENT_SECONDS = 5

SSO_CALLBACK_PATH

public mixed SSO_CALLBACK_PATH = '/api/v1/auth/federation/oidc/callback'

SSO_HANDOFF_PATH

Path of the handoff-code redemption endpoint (contract 1.38).

public mixed SSO_HANDOFF_PATH = '/api/v1/auth/federation/handoff'

SSO_OAUTH2_CALLBACK_PATH

Path of the plain-OAuth2 federation step-2 (callback) endpoint (contract 1.38).

public mixed SSO_OAUTH2_CALLBACK_PATH = '/api/v1/auth/federation/oauth2/callback'

SSO_OAUTH2_START_PATH

Path of the plain-OAuth2 federation step-1 endpoint (contract 1.38).

public mixed SSO_OAUTH2_START_PATH = '/api/v1/auth/federation/oauth2/start'

SSO_PROVIDERS_PATH

Path of the public provider-listing endpoint (contract 1.38).

public mixed SSO_PROVIDERS_PATH = '/api/v1/auth/federation/providers'

SSO_START_PATH

public mixed SSO_START_PATH = '/api/v1/auth/federation/oidc/start'

TOKEN_EXCHANGE_GRANT_TYPE

`grant_type` of an RFC 8693 exchange.

public mixed TOKEN_EXCHANGE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange'

UMA_CLAIM_TOKEN_FORMAT

The only `claim_token_format` AXIAM implements. §20.2 rule 2 makes the `claim_token` itself required rather than defaulted; the *format* has one value, so the SDK supplies it.

public mixed UMA_CLAIM_TOKEN_FORMAT = 'urn:ietf:params:oauth:token-type:access_token'

UMA_PROTECTION_SCOPE

The scope a PAT must carry (§20.2 rule 1) — for callers minting one.

public mixed UMA_PROTECTION_SCOPE = 'uma_protection'

UMA_TICKET_GRANT_TYPE

`grant_type` of the UMA 2.0 ticket grant (CONTRACT.md §20.1).

public mixed UMA_TICKET_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:uma-ticket'

Methods

__construct()

public __construct(Client $http, string $baseUrl, Session $session, JwksVerifier $jwksVerifier[, string|null $clientId = null ][, Sensitive|null $clientSecret = null ][, string|null $tenantId = null ][, string|null $orgId = null ][, string|null $orgSlug = null ][, string|null $tenantSlugForSso = null ][, int $discoveryTtlSeconds = self::MIN_DISCOVERY_TTL_SECONDS ][, int $clockSkewSec = IdTokenValidator::MAX_CLOCK_SKEW_SEC ]) : mixed
Parameters
$http : Client

AuthMiddleware ONLY — never RefreshMiddleware (§12.3 rule 3/rule 4).

$baseUrl : string

The AXIAM server base URL this engine's client is bound to.

$session : Session

The shared session (cookie jar, CSRF, §9 refresh guard).

$jwksVerifier : JwksVerifier

The SAME verifier the §10 middleware uses for AXIAM's own access tokens.

$clientId : string|null = null

The relying party's OAuth2 client_id.

$clientSecret : Sensitive|null = null

The confidential client's client_secret, already Sensitive-wrapped.

$tenantId : string|null = null

Client-level tenant_id UUID fallback (§12.3 rule 4).

$orgId : string|null = null

Client-level organization UUID fallback for ssoStart (§5.1).

$orgSlug : string|null = null

Client-level organization slug fallback for ssoStart (§5.1).

$tenantSlugForSso : string|null = null

Client-level tenant slug fallback for ssoStart (§5.1).

$discoveryTtlSeconds : int = self::MIN_DISCOVERY_TTL_SECONDS

Discovery-cache TTL, floored to self::MIN_DISCOVERY_TTL_SECONDS.

$clockSkewSec : int = IdTokenValidator::MAX_CLOCK_SKEW_SEC

Permitted ID-token clock skew, clamped to IdTokenValidator::MAX_CLOCK_SKEW_SEC.

deviceAuthorize()

`POST /oauth2/device_authorization` (§14.1) — start the device grant and obtain the code pair.

public deviceAuthorize([string|null $scope = null ][, string|null $tenantId = null ][, OidcConfiguration|null $configuration = null ]) : DeviceAuthorization

Unauthenticated by design. A device that cannot show a browser also cannot hold a client secret, so this never sends client_secret and never refuses a client built without one (§14.1).

Parameters
$scope : string|null = null
$tenantId : string|null = null
$configuration : OidcConfiguration|null = null
Tags
throws
AuthError

when the discovery document advertises no device_authorization_endpoint. The URL is never built by concatenation onto the issuer: that works against AXIAM and breaks against every other OP the same code is pointed at.

Return values
DeviceAuthorization

deviceLogin()

The composed §14.3 helper: start the grant, hand the caller the user code, poll to completion.

public deviceLogin(callable(DeviceAuthorization): void $onUserCode[, string|null $scope = null ][, string|null $tenantId = null ][, OidcConfiguration|null $configuration = null ][, bool $adoptAsCredential = false ][, callable(int): void|null $sleep = null ]) : OidcTokenSet

$onUserCode is called with the DeviceAuthorization before the first poll — §14.3 rule 2 requires the caller to have had the chance to display the code before polling begins. The SDK never prints it: what the device does with it (screen, QR code, e-ink panel) is the application's decision.

Per §14.3 rule 4 (contract 1.7 errata) the token set is returned; whether it is adopted is $adoptAsCredential, the same opt-in flag loginClientCredentials uses in this SDK.

Polling follows §14.2: the interval comes from the response; slow_down adds 5 s permanently; authorization_pending loops; access_denied and expired_token raise distinct errors; polling stops at expiresIn even if the server has not yet said expired_token. A 5xx or transport failure mid-poll is not terminal (rule 6) — a server restart must not lose a grant the user has already approved.

Parameters
$onUserCode : callable(DeviceAuthorization): void

Invoked before the first poll.

$scope : string|null = null
$tenantId : string|null = null
$configuration : OidcConfiguration|null = null
$adoptAsCredential : bool = false
$sleep : callable(int): void|null = null

Injectable sleeper, so tests can assert the §14.2 arithmetic without spending it in wall-clock time. Defaults to sleep().

Return values
OidcTokenSet

devicePoll()

`POST /oauth2/token` with the device-code grant (§14.1) — **one** poll attempt.

public devicePoll(Sensitive|string $deviceCode[, string|null $tenantId = null ][, OidcConfiguration|null $configuration = null ]) : OidcTokenSet

The raw single call, so an application driving its own loop (a UI rendering a countdown, say) can. All five RFC 8628 §3.5 answers surface as OAuthProtocolErrorauthorization_pending and slow_down included — so a hand-rolled loop sees exactly what deviceLogin sees. Most callers want deviceLogin.

Parameters
$deviceCode : Sensitive|string
$tenantId : string|null = null
$configuration : OidcConfiguration|null = null
Return values
OidcTokenSet

introspect()

`POST /oauth2/introspect` (RFC 7662, §12.1) — ask the server whether a token is active and, if so, for its metadata.

public introspect(Sensitive|string $token[, string|null $tokenTypeHint = null ][, string|null $tenantId = null ][, OidcConfiguration|null $configuration = null ]) : IntrospectionResult

Requires confidential-client credentials (§12.1 note 4). A 401 here is a client-credential failure surfaced as OAuthProtocolError; it never enters the §9 refresh guard — this method's $http transport carries NO RefreshMiddleware at all, so there is structurally no guard for it to enter (§12.3 rule 3).

Parameters
$token : Sensitive|string
$tokenTypeHint : string|null = null
$tenantId : string|null = null
$configuration : OidcConfiguration|null = null
Tags
throws
AuthError

when no clientSecret is configured.

Return values
IntrospectionResult

loginClientCredentials()

`POST /oauth2/token` with `grant_type=client_credentials` (§12.1) — service-account machine-to-machine login.

public loginClientCredentials([string|null $scope = null ][, string|null $tenantId = null ][, OidcConfiguration|null $configuration = null ][, bool $adoptAsCredential = false ]) : OidcTokenSet

Requests no openid scope, so the response carries no id_token. Pass $adoptAsCredential: true to additionally use the returned access token as this session's bearer credential for subsequent REST calls (§12.1, a MAY).

Parameters
$scope : string|null = null
$tenantId : string|null = null
$configuration : OidcConfiguration|null = null
$adoptAsCredential : bool = false
Tags
throws
AuthError

when no clientSecret is configured — this grant cannot be performed by a public client.

Return values
OidcTokenSet

logoutUrl()

Build the RP-initiated logout URL to redirect the user agent to (§12.7.2).

public logoutUrl(Sensitive|string $idToken[, string|null $postLogoutRedirectUri = null ][, string|null $state = null ][, OidcConfiguration|null $configuration = null ]) : string

Performs no network I/O beyond the discovery fetch the SDK caches anyway, and does not clear this client's own session: whether the local session ends is the application's decision — a backend holding a service-account session must not lose it because a user logged out.

end_session_endpoint is read from discovery and never synthesised from the issuer (rule 1). $postLogoutRedirectUri is passed through unvalidated against any local list (rule 3): the allow-list lives in the client's server-side registration, and a client-side copy would drift and reject a URI an operator had just registered.

Parameters
$idToken : Sensitive|string
$postLogoutRedirectUri : string|null = null
$state : string|null = null
$configuration : OidcConfiguration|null = null
Tags
throws
AuthError

when the discovery document advertises no end_session_endpoint.

Return values
string

normalizeOrigin()

Normalize a URL to its cache key: lowercased scheme and host with the port always explicit (§12.3 rule 6). `https://IAM.example.com/` and `https://iam.example.com:443/x` therefore share one key, while `http://iam.example.com` gets its own.

public static normalizeOrigin(string $url) : string
Parameters
$url : string
Return values
string

oidcBegin()

Build an authorization request (§12.1) — **pure local computation, no network I/O**.

public oidcBegin(OidcConfiguration $configuration, string $redirectUri[, string|array<int, string> $scope = null ][, array<string, string> $extraParams = [] ]) : AuthorizationRequest

Generates a 32-byte CSPRNG state and nonce (base64url, unpadded) and a fresh PKCE verifier/challenge pair using S256 onlyplain is not implemented anywhere in this SDK. The URL is built from the discovery document's authorization_endpoint with exactly the eight parameters §12.1 rule 5 mandates, plus any $extraParams the caller adds.

Nothing is stored: persist the returned state, nonce and codeVerifier yourself (§12.3 rule 1).

Parameters
$configuration : OidcConfiguration
$redirectUri : string
$scope : string|array<int, string> = null
$extraParams : array<string, string> = []
Tags
throws
InvalidArgumentException

when $extraParams tries to override one of the eight SDK-owned parameters — a programming error, caught at call time.

Return values
AuthorizationRequest

oidcDiscover()

`GET /.well-known/openid-configuration` (§12.1) — fetch the OIDC discovery document, cached per origin with a ≥5-minute TTL and single-flight de-duplication of concurrent callers (§12.3 rule 6).

public oidcDiscover() : OidcConfiguration
Return values
OidcConfiguration

oidcDiscoverAsync()

Async entry point exercised directly by tests wanting to prove single-flight de-duplication under Guzzle's async interface — mirrors {@see JwksVerifier}'s own `ensureFreshAsync()`/`ensureFresh()` split.

public oidcDiscoverAsync() : PromiseInterface
Return values
PromiseInterface

oidcExchange()

`POST /oauth2/token` with `grant_type=authorization_code` (§12.1) — exchange an authorization code for a token set, validating the returned ID token in full before returning.

public oidcExchange(string $code, Sensitive|string $codeVerifier, string $redirectUri, string $nonce[, string|null $tenantId = null ][, OidcConfiguration|null $configuration = null ]) : OidcTokenSet

The $nonce argument is mandatory: this grant always requests the openid scope, so §12.4 rule 6 always applies. If any §12.4 rule fails, the whole token set is discarded and AuthError is raised with the matching reason code — the access and refresh tokens from the same response are never returned (§12.4 rule 7).

Parameters
$code : string
$codeVerifier : Sensitive|string
$redirectUri : string
$nonce : string
$tenantId : string|null = null
$configuration : OidcConfiguration|null = null
Return values
OidcTokenSet

oidcPar()

`POST /oauth2/par` (CONTRACT.md §26.1) — push the authorization request over the back channel and get an opaque handle to redirect with.

public oidcPar(AuthorizationRequest $request, string $redirectUri[, OidcConfiguration|null $configuration = null ][, string|array<int, string>|null $scope = null ][, string|null $tenantId = null ]) : PushedAuthorizationRequest

PAR moves the authorization request off the browser. Instead of putting scope, redirect_uri, state and the PKCE challenge into a URL the user agent carries, the client POSTs them straight to AXIAM over an authenticated channel and puts an opaque request_uri in the redirect. What travels through the browser is then a random string that cannot be edited into meaning something else.

Required for a FAPI 2.0 client: profile: "fapi2" refuses a registration that does not set require_par, so such a client cannot authorize any other way (§21.1).

Not retried on a 5xx or a transport failure — it is a POST that creates server state, so it falls outside §16.2's read-only eligibility exactly as self::oidcExchange() does. The safe recovery is a fresh push, which costs one round trip and cannot double-consume anything (§26.2 rule 4).

Parameters
$request : AuthorizationRequest
$redirectUri : string
$configuration : OidcConfiguration|null = null
$scope : string|array<int, string>|null = null
$tenantId : string|null = null
Tags
throws
AuthError

client-side, with no wire call, when the discovery document advertises no PAR endpoint — §12.7.2 rule 1's discipline: never synthesise the URL from the issuer.

Return values
PushedAuthorizationRequest

oidcRefresh()

`POST /oauth2/token` with `grant_type=refresh_token` (§12.1) — refresh an {@see OidcTokenSet}, governed by a §9-conformant single-flight refresh guard.

public oidcRefresh(Sensitive|string $refreshToken[, string|null $scope = null ][, string|null $tenantId = null ][, OidcConfiguration|null $configuration = null ]) : OidcTokenSet

This is a distinct operation from AxiamClient::refresh(), which drives the cookie/opaque-token session path at POST /api/v1/auth/refresh. The two are never merged or aliased and neither falls back to the other (§12.1). They DO share Session's single §9 guard slot, tagged with Session::REFRESH_KIND_OIDC, so at most one refresh of either kind is ever in flight for a session:

  • If the guard is busy with ANOTHER oidcRefresh call (same kind), this call does not issue its own wire call — it awaits and reuses that one leader's outcome (CONTRACT.md §9 rule 2, F-06). Refresh tokens are single-use with rotation, so a second wire call here would replay an already-consumed token and fail invalid_grant; sharing the result is the whole point of the guard.
  • If the guard is busy with a cookie-session refresh (different kind, which cannot produce an OidcTokenSet), this call retries (bounded, 3 attempts) once that refresh settles, rather than returning a stale/foreign result.

An id_token in the response is validated against §12.4 rules 1–5 and 7; rule 6 (nonce) is skipped, since OIDC Core §12.2 does not require a nonce in a refresh-issued ID token.

Parameters
$refreshToken : Sensitive|string
$scope : string|null = null
$tenantId : string|null = null
$configuration : OidcConfiguration|null = null
Return values
OidcTokenSet

revoke()

`POST /oauth2/revoke` (RFC 7009, §12.1) — revoke an access or refresh token.

public revoke(Sensitive|string $token[, string|null $tokenTypeHint = null ][, string|null $tenantId = null ][, OidcConfiguration|null $configuration = null ]) : void

Returns nothing.

Per RFC 7009 the server answers 200 for unknown, expired and already-revoked tokens alike, so revocation is idempotent: any 200 is success and no error is raised for a token the server has never seen. Only a 401 (client authentication failed) is an error, surfaced as OAuthProtocolError (§12.1 note 5, §12.3 rule 3). A 5xx stays a NetworkError — it never becomes "success" just because RFC 7009 treats an unknown token as success.

Parameters
$token : Sensitive|string
$tokenTypeHint : string|null = null
$tenantId : string|null = null
$configuration : OidcConfiguration|null = null
Tags
throws
AuthError

when no clientSecret is configured.

ssoComplete()

`POST /api/v1/auth/federation/oidc/callback` (§12.1) — step 2 of upstream SSO: consumes the single-use `state`, provisions or links the user, and establishes the session.

public ssoComplete(string $state, string $code) : SsoCompleteResult

The session arrives as Set-Cookie, not in the response body (§12.1 note 6) — $this->http shares AxiamClient's §4 cookie jar, so the session is captured automatically. On success the §3 CSRF token freshly set by the server is captured too, exactly as login() does.

§12.4 does not apply here — no ID token ever reaches the SDK on the federation path.

Parameters
$state : string
$code : string
Return values
SsoCompleteResult

ssoCompleteHandoff()

`POST /api/v1/auth/federation/handoff` (§12.1) — redeem the single-use code the SAML and Apple flows deliver.

public ssoCompleteHandoff(string $code) : SsoCompleteResult

Those two protocols return cross-site, so the server cannot set SameSite=Strict session cookies on that response. It instead redirects the browser to the SPA's callback URL with a FederationProviderList::HANDOFF_QUERY_PARAM query parameter; this call posts that code back same-origin, and this response is the one that carries the cookies (§12.1 note 12).

The code is gone either way. It is valid for FederationProviderList::HANDOFF_CODE_TTL_SECONDS seconds and redeemable once. Redeem it from the same origin, immediately, and never retry a failed redemption: a 401 is terminal, and this makes exactly one wire call so that it cannot become a retry by accident. Unknown, expired and already-redeemed all answer the same 401, deliberately.

Parameters
$code : string
Return values
SsoCompleteResult

ssoCompleteOauth2()

`POST /api/v1/auth/federation/oauth2/callback` (§12.1) — step 2 of a plain-OAuth2 login.

public ssoCompleteOauth2(string $state, string $code) : SsoCompleteResult

The session arrives as Set-Cookie (§12.1 note 6) through the same §4 cookie jar self::ssoComplete() uses, and the §3 CSRF token is captured the same way.

§12.4 does not apply: an OAuth2 provider issues no ID token, so there is nothing to validate — the server authenticated the user by calling a configured userinfo endpoint with the access token it had just received (§12.1 note 11).

Parameters
$state : string
$code : string
Return values
SsoCompleteResult

ssoProviders()

`GET /api/v1/auth/federation/providers` (§12.1) — which "Sign in with X" buttons to render for a workspace.

public ssoProviders([string|null $orgId = null ][, string|null $orgSlug = null ][, string|null $tenantId = null ][, string|null $tenantSlug = null ]) : FederationProviderList

The identifiers travel as query parameters; this is a GET and sends no body. The neighbouring start operations take the same four in a JSON body, and the two are one copy-paste apart.

An empty list is a success. An unknown organization, a known one with nothing configured, and a request naming no workspace at all all answer 200 with an empty providers array (§12.1 note 9). Every one of them comes back as an ordinary result and nothing here synthesises a not-found: the endpoint is deliberately shaped so it cannot be used to enumerate organization or tenant slugs, and an SDK that reintroduced the distinction would reintroduce the oracle. A caller learns it named the workspace wrongly at the start operations, where every failure is a uniform 401.

For the same reason this is the one federation operation that does not throw client-side when no workspace resolves.

Parameters
$orgId : string|null = null
$orgSlug : string|null = null
$tenantId : string|null = null
$tenantSlug : string|null = null
Return values
FederationProviderList

ssoStart()

`POST /api/v1/auth/federation/oidc/start` (§12.1) — step 1 of first-time SSO against an **upstream** IdP. No JWT required.

public ssoStart(string $federationConfigId, string $redirectUri[, string|null $tenantId = null ][, string|null $tenantSlug = null ][, string|null $orgId = null ][, string|null $orgSlug = null ]) : SsoStartResult

One tenant form ($tenantId or $tenantSlug) and one org form ($orgId or $orgSlug) must be resolvable, from the arguments or from the client's construction options (§5.1). Redirect the browser to the returned authorizeUrl and round-trip state back into self::ssoComplete() unmodified — the server keeps the nonce to itself (§12.1 note 7).

Parameters
$federationConfigId : string
$redirectUri : string
$tenantId : string|null = null
$tenantSlug : string|null = null
$orgId : string|null = null
$orgSlug : string|null = null
Tags
throws
AuthError

client-side, without a wire call, when tenant or org context cannot be resolved.

Return values
SsoStartResult

ssoStartOauth2()

`POST /api/v1/auth/federation/oauth2/start` (§12.1) — step 1 of a login through a **plain-OAuth2** upstream (GitHub, Facebook, `generic_oauth2`).

public ssoStartOauth2(string $federationConfigId, string $redirectUri[, string|null $tenantId = null ][, string|null $tenantSlug = null ][, string|null $orgId = null ][, string|null $orgSlug = null ]) : SsoStartResult

Call this, rather than self::ssoStart(), exactly when the provider's protocol is FederationProvider::PROTOCOL_OAUTH2 (§12.1 note 10). The server refuses a mismatch with 400 rather than accepting it silently, so a client that assumes OIDC fails on every GitHub button.

PKCE is mandatory on this path and is generated and stored server-side; nothing about it appears in the request or the response (§12.1 note 11).

A 400 here can mean the $redirectUri is not on an origin the deployment accepts (§12.1 rule 12a). §2's 400 row makes that a NetworkError — this taxonomy's configuration/programming-error member, as distinct from the AuthError a 401 gets. It is not retried.

Parameters
$federationConfigId : string
$redirectUri : string
$tenantId : string|null = null
$tenantSlug : string|null = null
$orgId : string|null = null
$orgSlug : string|null = null
Tags
throws
AuthError

client-side, without a wire call, when tenant or org context cannot be resolved.

Return values
SsoStartResult

tokenExchange()

`POST /oauth2/token` with the RFC 8693 grant (§15.1) — exchange a token for a **narrower** one.

public tokenExchange(Sensitive|string $subjectToken, string $subjectTokenType[, Sensitive|string|null $actorToken = null ][, array<int, string>|null $scopes = null ][, string|null $audience = null ][, string|null $resource = null ][, string|null $tenantId = null ][, OidcConfiguration|null $configuration = null ]) : ExchangedToken

The exchanging client authenticates (client_secret_post): unlike §14's device, this is a confidential service, so a client with no secret fails here client-side, with no wire call.

What this method deliberately does not do:

  • No default $actorToken (§15.2 rule 1). Passing null asks for impersonation; the SDK will not quietly reuse the client's own session token as the actor and turn that into a delegation.
  • No retry or downgrade on unauthorized_client (rule 2) — a registration fact an operator must fix.
  • No auto-narrowing on invalid_scope (rule 3). The server refuses instead of silently narrowing precisely so the caller finds out here.
  • No adoption (rule 5), and no flag to enable it — a MUST NOT, where loginClientCredentials adoption is a MAY.

A cross-tenant subject token answers invalid_grant, identically to an expired one. The SDK does not try to tell them apart (§15.3): the server collapses them because distinguishing them is a tenant-enumeration signal.

Parameters
$subjectToken : Sensitive|string
$subjectTokenType : string

What kind of token $subjectToken is. Required (§15.1), with no default — a default would be this SDK choosing which kind of credential you hold, which is what §15.7 forbids. Pass ACCESS_TOKEN_TYPE for the same-domain exchange, or JWT_TOKEN_TYPE for a trusted external issuer's JWT (§15.7). It sits second, matching §15.1's canonical order: it was last while it was optional, to spare positional callers, and making it required breaks them anyway — so it may as well be where the contract puts it. The SDK never reads $subjectToken to decide it: which kind of token you hold is something only you know, AXIAM refuses refresh and ID token types by name, and the SDK will not retry a refusal as a different type.

$actorToken : Sensitive|string|null = null
$scopes : array<int, string>|null = null

Omitted from the body when null or empty.

$audience : string|null = null
$resource : string|null = null
$tenantId : string|null = null
$configuration : OidcConfiguration|null = null
Tags
throws
AuthError

when no clientSecret is configured.

Return values
ExchangedToken

umaChallengeHeader()

Format a `WWW-Authenticate: UMA` header (§20.3, emit half) — pure local computation, for a resource server that has just minted a ticket.

public umaChallengeHeader(string $realm, string $asUri, Sensitive|string $ticket) : string
Parameters
$realm : string
$asUri : string
$ticket : Sensitive|string
Return values
string

umaDeleteResource()

`DELETE /uma2/rreg/resource_set/{id}` (§20.1) — deregister a resource set.

public umaDeleteResource(Sensitive|string $pat, string $id) : void
Parameters
$pat : Sensitive|string
$id : string

umaExchangeTicket()

`POST /oauth2/token` with the UMA ticket grant (§20.1) — redeem a permission ticket for a Requesting Party Token.

public umaExchangeTicket(Sensitive|string $ticket, Sensitive|string $claimToken[, string|null $tenantId = null ][, OidcConfiguration|null $configuration = null ]) : RequestingPartyToken

Unlike the Protection API above, this is a token-endpoint grant: the client authenticates through the form body (client_secret_post), so a client with no secret fails here client-side with no wire call.

What this method deliberately does not do:

  • No retry, ever (§20.2 rule 6) — not on 5xx, not on a timeout, not on invalid_grant. This is the one documented exception to §16, and it is a security rule rather than a performance one: the ticket is consumed before the request is evaluated, so a failed exchange has already spent it, and a retry is a second redemption — exactly the concurrent redemption a server whose storage engine this SDK cannot attest may admit twice (ilpanich/axiam#302). This SDK gets that for free: no /oauth2/* call passes through RetryPolicy, and the transport this class holds carries no RefreshMiddleware.
  • No defaulted $claimToken (rule 2). It is the only channel that names the requesting party; defaulting it to the resource server's own PAT would mint an RPT for the resource server rather than for the user.
  • No auto-narrowing on access_denied (rule 3). A partial grant is refused whole, and whether two-of-three permissions is useful is the application's judgment, not this SDK's.
  • No adoption (rule 4). The RPT is the requesting party's token; adopting it would re-privilege every later call this resource server makes as that user.
  • No refresh token (rule 5) — the grant issues none, and RequestingPartyToken has nowhere to put one. Re-run the grant with a new ticket to get a fresh RPT.

The four ticket refusals — unknown, expired, already used, minted by another client — all arrive as one invalid_grant, and this SDK does not guess which (§20.4): the server collapses them because telling them apart lets a caller probe for live ticket handles.

Parameters
$ticket : Sensitive|string
$claimToken : Sensitive|string
$tenantId : string|null = null
$configuration : OidcConfiguration|null = null
Tags
throws
AuthError

when no clientSecret is configured.

Return values
RequestingPartyToken

umaListResources()

`GET /uma2/rreg/resource_set` (§20.1) — the ids **this client** registered.

public umaListResources(Sensitive|string $pat) : array<int, string>

Not the tenant's resource tree: the server scopes the listing to the registering client, so a PAT is not an enumeration handle.

Parameters
$pat : Sensitive|string
Return values
array<int, string>

umaParseChallenge()

Parse a `WWW-Authenticate: UMA …` header (§20.3) — pure local computation.

public umaParseChallenge(string $header) : UmaChallenge|null

Performs no exchange, deliberately: the as_uri names an authorization server this client has not necessarily chosen to trust. See UmaChallenge::parse().

Parameters
$header : string
Return values
UmaChallenge|null

umaRegisterResource()

`POST /uma2/rreg/resource_set` (§20.1) — register a resource set.

public umaRegisterResource(Sensitive|string $pat, string $name[, string|null $type = null ][, array<int, string> $resourceScopes = [] ]) : ResourceSet

The returned $id is the AXIAM resource id, directly usable as the $resourceId of a later umaRequestTicket call and anywhere else this SDK takes a resource id.

Parameters
$pat : Sensitive|string

The Protection API Token — a client-credentials token carrying the uma_protection scope (§20.2 rule 1). This SDK never falls back to the session's own token when one is not passed.

$name : string
$type : string|null = null
$resourceScopes : array<int, string> = []
Return values
ResourceSet

umaRequestTicket()

`POST /uma2/perm` (§20.1) — mint a permission ticket for the pairs a caller lacks.

public umaRequestTicket(Sensitive|string $pat, array<int, RequestedPermission$permissions) : Sensitive

The ticket comes back wrapped: for its 60-second life it is the credential that converts into an RPT, and a short lifetime is not the same as a harmless one (§20.6).

Parameters
$pat : Sensitive|string
$permissions : array<int, RequestedPermission>
Return values
Sensitive

umaUpdateResource()

`PUT /uma2/rreg/resource_set/{id}` (§20.1) — replace a resource set's state.

public umaUpdateResource(Sensitive|string $pat, string $id, string $name[, string|null $type = null ][, array<int, string> $resourceScopes = [] ]) : ResourceSet

$resourceScopes replaces the declared list; it does not merge with it (§20.2 rule 8). This method deliberately performs no read-modify-write: folding the current scopes into the payload as a convenience would make removing a scope impossible through this SDK.

Parameters
$pat : Sensitive|string
$id : string
$name : string
$type : string|null = null
$resourceScopes : array<int, string> = []
Return values
ResourceSet

verifyLogoutToken()

Verify a back-channel logout token the OP POSTed to this application's `backchannel_logout_uri` (§12.7.3).

public verifyLogoutToken(string $logoutToken[, OidcConfiguration|null $configuration = null ]) : VerifiedLogoutToken

Every check exists because skipping it has a name:

  1. Signature, through the same §12.4 JWKS verifier the ID-token path uses — no second key-fetching path — which already pins EdDSA and requires a kid.
  2. iss/aud: a token minted for another RP is not accepted here.
  3. events carries the back-channel-logout key. This is what distinguishes a logout token from an ID token; skipping it means accepting a replayed ID token as a logout instruction.
  4. nonce is absent. Back-Channel Logout 1.0 §2.4 forbids it, and its presence is the documented signature of an ID token being replayed. Rejected, not ignored.
  5. At least one of sid/sub — a token naming neither identifies nothing.
  6. exp in the future, iat recent.
Parameters
$logoutToken : string
$configuration : OidcConfiguration|null = null
Tags
throws
AuthError

on any failed check.

Return values
VerifiedLogoutToken

The sid/sub/jti the token names — never a bare bool, because the RP has to know which session to end. Dedup on jti yourself: delivery is at-least-once, and an SDK-side guard would have no durable store and would silently drop a real second logout after a restart.

On this page

Search results