AXIAM PHP SDK

AxiamClient
in package

FinalYes

The AXIAM PHP SDK's public REST entry point (CONTRACT.md §1–§9, SC#1).

tenant is a REQUIRED, non-nullable-defaulted constructor parameter (D-13, §5) — there is no overload or default that lets a caller omit it; AXIAM is multi-tenant and there is no default tenant. login($email, $password) returns a typed LoginResult, never a raw array/stdClass (D-09); a two-phase MFA flow is completed via self::verifyMfa(). checkAccess/can/batchCheck delegate to AuthzDispatcher — this class never hand-rolls REST/gRPC transport selection (D-03). self::verifyLocallyOrFallback() is the seam the Laravel/Symfony framework bridges (a later plan) call: local JwksVerifier verification first, falling back to the reactive single-flight refresh path (D-02, D-06).

Composition, not reimplementation: this class wires together the already-built wave-2/3 pieces — Session (CookieJar + CSRF + single-flight refresh promise, D-06), AuthMiddleware/RefreshMiddleware (the HandlerStack auth/refresh mechanism), JwksVerifier (local EdDSA/JWKS verification), and AuthzDispatcher (REST-default, gRPC-when-available authz, D-03) — it does not reimplement any of their internal mechanisms.

Two Guzzle clients share ONE CookieJar (§4):

  • $plainHttp carries ONLY AuthMiddleware (tenant/auth/CSRF header injection, no 401-triggered refresh-and-retry). This is the client handed to Session's own constructor for its internal /api/v1/auth/refresh POST (per Session's own doc comment: the refresh call itself must never be able to recursively re-enter the single-flight guard), and is also used for login()/verifyMfa()/logout() — a failed login/logout attempt (401/403) must surface as its own clear error, not trigger an unrelated token-refresh attempt first.
  • $authzHttp carries BOTH AuthMiddleware and RefreshMiddleware — the full production stack — and is the client AuthzRestClient (and therefore AuthzDispatcher's REST path) sends every authz request through, so a 401 on an authz call transparently triggers the shared single-flight refresh-and-retry-once (D-06).

§6/D-12: the Guzzle verify option is ALWAYS true (strict TLS, system trust roots) unless $customCa (a CA bundle FILE PATH) is supplied, in which case verify is set to that path — the ONLY escape hatch. There is no code path in this class that sets verify to false or any other TLS-bypass value.

§6.1 (mTLS): supplying $clientCert + $clientKey (both PEM strings) makes this client present an X.509 client identity for mutual TLS on BOTH transports — the REST Guzzle clients (via cert/ssl_key) and any gRPC channel (via \Grpc\ChannelCredentials::createSsl(rootCerts, privateKey, certChain)). This is strictly ADDITIVE to §6: presenting a client certificate NEVER relaxes server verification — verify is untouched by this code path (contract rule §6.1.2). The private key is secret (§7): it is held behind Sensitive, written only to a 0600 temp file consumed by cURL, and never appears in any debug/log/exception output. Both PEM strings must be supplied together; supplying exactly one is a construction-time InvalidArgumentException.

Table of Contents

Methods

__construct()  : mixed
__destruct()  : mixed
§6.1: cleans up the `0600` temp files backing the client-certificate identity when this client is destroyed, so no PEM material (least of all the private key) outlives the object on disk. A no-op when mTLS was not configured.
actingTenant()  : self
Switches this client to act on `$tenantId` — CONTRACT.md §5.2 rule 1 (contract 1.51). Every subsequent `/api/v1` request this client makes carries `X-Axiam-Tenant: $tenantId`, until {@see self::clearActingTenant()} is called or another `actingTenant()` replaces it.
actingTenantId()  : string|null
The tenant this client currently acts on, or `null` when it acts on its own tenant.
audit()  : AuditApi
Append-only audit log, read-only by construction.
authenticateDevice()  : DeviceToken
`authenticateDevice` — `POST /api/v1/auth/device` (CONTRACT.md §6.1 rules 6-10, contract 1.51): the mTLS device login. The client presents the X.509 identity certificate configured via `$clientCert`/`$clientKey`, and the server authenticates it, no request body, no tenant/credential needed beforehand.
batchCheck()  : array<int, bool>
`batchCheck` — results preserve input order (CONTRACT.md §1).
caCertificates()  : CaCertificatesApi
Organization CAs and the per-tenant signing CAs chained beneath them.
can()  : bool
`can` — the browser/UI-scenario alias for {@see self::checkAccess()} (CONTRACT.md §1 note). Argument order is `(action, resource)` — matching {@see self::checkAccess()} and every other AXIAM SDK's `can`/`Can` (D-09/SDK-Q09; this was previously reversed relative to the rest of the SDK family).
certificates()  : CertificatesApi
End-entity X.509 certificates -- the ones issued to users, services and IoT devices.
checkAccess()  : bool
`checkAccess` — delegates to {@see AuthzDispatcher} (REST default, gRPC when available).
checkAccessDecision()  : AccessDecision
`checkAccess`, returning the **full** decision including CONTRACT.md §11 rule 9's `reason_code` — see {@see \Axiam\Sdk\Rest\AccessDecision}'s own docblock for why that distinction matters. Delegates to {@see AuthzDispatcher} on whichever transport {@see self::checkAccess()} itself would use (REST default, gRPC when available), so the two never disagree about which transport ran.
cibaAwait()  : OidcTokenSet
Poll for a CIBA request's outcome until it is decided or expires, honouring `interval` and `slow_down` (CONTRACT.md §33.7). Returns the token set without adopting it. See {@see OidcEngine::cibaAwait()}, including the ping-mode fallback.
cibaHandlePing()  : Sensitive
Verify a CIBA ping's bearer and return the `auth_req_id` it names (CONTRACT.md §33.1) — pure, no I/O. Answer the ping, then {@see self::cibaPoll()}. See {@see OidcEngine::cibaHandlePing()}.
cibaInitiate()  : CibaInitiateResponse
`POST /oauth2/bc-authorize` (CONTRACT.md §33.1) — ask AXIAM to authenticate a user on another device. **Never retried**, and a success proves nothing about the user; see {@see OidcEngine::cibaInitiate()}.
cibaPoll()  : OidcTokenSet
One CIBA token request (CONTRACT.md §33.1). `access_denied` and `expired_token` are {@see \Axiam\Sdk\Core\OAuthProtocolError}s told apart by `isAccessDenied()` / `isExpiredToken()`. **Store the tokens first**: a request is redeemed once. See {@see OidcEngine::cibaPoll()}.
clearActingTenant()  : self
Stops acting on another tenant: no further request sends `X-Axiam-Tenant`, and this client acts on its own tenant again (CONTRACT.md §5.2 rule 1's "a way to clear it").
close()  : void
Releases this client's local resources (CONTRACT.md §18).
confirmPasswordReset()  : void
`POST /api/v1/auth/reset/confirm` (CONTRACT.md §25.1) — set the new password.
debugClientCertOptions()  : array{cert: string|null, ssl_key: string|null}
Test-only seam (not part of the public API contract, mirroring {@see self::debugVerifyOption()}): exposes the §6.1 client-identity options (`cert` = certificate-chain file, `ssl_key` = private-key file) actually configured on this client's authz Guzzle transport, so tests can assert the mTLS wiring without performing a live TLS handshake. Both entries are `null` when mTLS was not configured. The values are FILE PATHS, never the PEM bytes — this seam never surfaces the private key itself.
debugVerifyOption()  : string|bool
deleteClientRegistration()  : void
`DELETE registration_client_uri` (RFC 7592 §2.3, CONTRACT.md §28.12) — delete this client's registration. A `204` returns normally.
deviceAuthorize()  : DeviceAuthorization
`POST /oauth2/device_authorization` (CONTRACT.md §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 (before the first poll), poll to completion.
devicePoll()  : OidcTokenSet
`POST /oauth2/token` with the device-code grant (CONTRACT.md §14.1) — **one** poll attempt, for an application driving its own loop. Most callers want {@see self::deviceLogin()}.
directory()  : DirectoryApi
A tenant's LDAP / Active Directory identity source (CONTRACT §30): its configuration, account linking, and the sync job's status.
emailConfig()  : EmailConfigApi
Transactional-mail transport, configurable at organization level and overridable per tenant.
expectedAudience()  : string|null
The `$expectedAudience` this client was constructed with (CONTRACT.md §10.1 rule 6), or `null` when no audience check is configured.
federation()  : FederationApi
Upstream IdP configuration and the per-user links it produces.
getUserInfo()  : UserInfo
`getUserInfo` — the gRPC-ONLY OIDC-style userinfo operation (CONTRACT.md §1.1, contract 1.3): returns the authenticated caller's identity claims from `axiam.v1.UserInfoService/GetUserInfo`, the low-latency counterpart of the server's REST `GET /oauth2/userinfo`. Delegates to {@see AuthzDispatcher} — this class never hand-rolls the gRPC transport (D-03).
groups()  : GroupsApi
Named collections of users. Roles assigned to a group are inherited by every member.
introspect()  : IntrospectionResult
`POST /oauth2/introspect` (RFC 7662, CONTRACT.md §12.1) — ask the server whether a token is active and, if so, for its metadata. Requires confidential-client credentials (§12.1 note 4). A `401` here is a client-credential failure surfaced as {@see \Axiam\Sdk\Core\OAuthProtocolError} and NEVER enters the §9 refresh guard (§12.3 rule 3).
introspectToken()  : TokenIntrospection
`introspectToken` — `axiam.v1.TokenService/IntrospectToken` (CONTRACT.md §1.1.1/§10.3, contract 1.51). The RFC 7662 set, plus the confirmation. See {@see self::validateToken()} for the shared rules this method follows exactly; the only difference is the richer RFC-7662-shaped return value.
login()  : LoginResult
`POST /api/v1/auth/login` (CONTRACT.md §1). Returns a typed {@see LoginResult} — an MFA challenge (HTTP 202) is an expected outcome, not an exception: callers MUST check {@see LoginResult::$mfaRequired} before assuming a session was established (SC#1).
loginClientCredentials()  : OidcTokenSet
`POST /oauth2/token` with `grant_type=client_credentials` (CONTRACT.md §12.1) — service-account machine-to-machine login. Requests no `openid` scope, so the response carries no `id_token`. Pass `$adoptAsCredential: true` to additionally adopt the returned access token as this client's bearer credential for subsequent same-origin REST calls (§12.1, an opt-in MAY) — the token is held behind {@see Sensitive} inside {@see Session} and is NEVER sent to `/oauth2/*`.
loginOpaque()  : LoginResult
`POST /api/v1/auth/opaque/login/start` followed by `/finish` — OPAQUE login, RFC 9807 (CONTRACT.md §23).
logout()  : void
`POST /api/v1/auth/logout` (CONTRACT.md §1) and clears local session state: the shared cookie jar (§4) and the captured CSRF token (§3). The session id comes from the current access token's `jti` claim (unverified decode — an operational hint only, never an authorization decision, mirroring the Python/C# sibling SDKs).
logoutUrl()  : string
Build the RP-initiated logout URL to redirect the user agent to (CONTRACT.md §12.7.2). Does **not** clear this client's own session.
management()  : ManagementApi
The CONTRACT.md §27 management surface: 190 operations across 28 namespaces.
mfaConfirm()  : bool
`POST /api/v1/auth/mfa/confirm` (CONTRACT.md §25.1) — activate the factor {@see self::mfaEnroll()} offered. Returns whether MFA is now enabled.
mfaEnroll()  : MfaEnrollment
`POST /api/v1/auth/mfa/enroll` (CONTRACT.md §25.1) — start voluntary TOTP enrolment for the signed-in user.
mfaSetupConfirm()  : LoginResult
`POST /api/v1/auth/mfa/setup/confirm` (CONTRACT.md §25.1) — finish forced enrolment and, with it, the login that was interrupted.
mfaSetupEnroll()  : MfaEnrollment
`POST /api/v1/auth/mfa/setup/enroll` (CONTRACT.md §25.1) — start the enrolment a {@see self::login()} demanded.
network()  : NetworkApi
A tenant's network segments and the RADIUS reply profile each owns, and the tenant's `Session-Timeout` cap (CONTRACT §36, contract 1.62). Every write is conditional on the `version` the caller read; the decision route FreeRADIUS calls is not SDK surface.
notificationRules()  : NotificationRulesApi
Which events raise a notification, and to whom.
oauth2Clients()  : Oauth2ClientsApi
Registered OAuth2/OIDC clients -- the registration half of what §12, §21 and §26 then speak to.
oidcBegin()  : AuthorizationRequest
Build an authorization request (CONTRACT.md §12.1) — **pure local computation, no network I/O**. Generates a `state`/`nonce` (CSPRNG, ≥128 bits) and a fresh PKCE verifier/challenge pair (**S256 only**), and builds `$configuration`'s `authorization_endpoint` into a redirect URL with exactly the eight SDK-owned query parameters plus any `$extraParams` supplied.
oidcDiscover()  : OidcConfiguration
`GET /.well-known/openid-configuration` (CONTRACT.md §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). The document's own `issuer` is authoritative for ID-token validation (§12.4 rule 3) and may legitimately differ from `$baseUrl` behind a proxy — never treated as an error (§12.3 rule 6).
oidcExchange()  : OidcTokenSet
`POST /oauth2/token` with `grant_type=authorization_code` (CONTRACT.md §12.1) — exchange an authorization code for a token set, validating the returned ID token in full (§12.4) before returning. `$nonce` is MANDATORY: this grant always requests `openid`, so §12.4 rule 6 always applies. On ANY §12.4 failure the whole token set is discarded and {@see AuthError} is raised with the matching reason code (§12.4 rule 7) — `getReason()` returns one of `invalid_alg`, `unknown_kid`, `invalid_signature`, `invalid_issuer`, `invalid_audience`, `token_expired`, `nonce_mismatch`.
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` (CONTRACT.md §12.1) — refresh an {@see OidcTokenSet} under the SAME §9 single-flight guard {@see self::refresh()} uses. A **distinct operation** from {@see self::refresh()} (the cookie/opaque-token session path) — the two are never merged, aliased, or fall back to one another, but they share ONE guard slot: a concurrent `oidcRefresh()` call finding the guard busy with a cookie-session refresh retries (bounded) rather than returning a stale token set.
opaqueAvailable()  : bool
Whether this installation can perform OPAQUE (§23.2).
opaqueEnrollment()  : OpaqueEnrollment
Builds a registration record for `$password`, to send with any request that sets one: `POST /api/v1/users`, `/auth/password/change`, `/auth/reset/confirm` and `/admin/bootstrap`.
opaqueEnrollmentForSelf()  : OpaqueEnrollment
Builds a registration record for the **caller's own** new password, sealed against the tenant the caller's account lives in.
organizations()  : OrganizationsApi
Organizations an SDK client may read and configure. Creation and deletion are outside the SDK boundary (§27.0).
passwordResetContext()  : PasswordResetContext
`GET /api/v1/auth/reset/context` (CONTRACT.md §25.1) — the OPAQUE policy for the account a reset token belongs to.
permissions()  : PermissionsApi
Permissions -- an action on a resource, optionally narrowed by a scope.
pgpKeys()  : PgpKeysApi
OpenPGP keys used for audit signing and encrypted data export.
platform()  : PlatformApi
Deployment-level probes and FIDO metadata state. Unauthenticated where the server leaves them so.
privacy()  : PrivacyApi
GDPR self-service: the authenticated account's own export and erasure. Scoped to the caller, never to another user.
reactors()  : ReactorsApi
Registration of §22 AMQP extension actors -- the admin surface §22.9 describes, which no SDK could previously reach.
readClientRegistration()  : ClientRegistration
`GET registration_client_uri` (RFC 7592 §2.1, CONTRACT.md §28.12) — read this client's own registration, as a client that registered itself through `POST /oauth2/register` (RFC 7591).
refresh()  : void
`POST /api/v1/auth/refresh` (CONTRACT.md §1), routed through {@see Session}'s single-flight guard (§9, D-06) — the SAME mechanism {@see RefreshMiddleware} triggers reactively on a `401`. A failure surfaces as {@see AuthError} with no retry (§9.3).
requestPasswordReset()  : void
`POST /api/v1/auth/reset` (CONTRACT.md §25.1) — ask for a reset mail.
resendOwnVerification()  : void
`POST /api/v1/users/me/resend-verification` (CONTRACT.md §25.1, §25.7) — resend the **signed-in caller's own** verification mail, and say what happened.
resendVerification()  : void
`POST /api/v1/auth/resend-verification` (CONTRACT.md §25.1) — the **unauthenticated** resend, for a caller with no session.
resolvedOrgId()  : string|null
The organization UUID §27 routes substitute for `{org_id}`, or `null` when the client was constructed without one (§27.4 rule 3).
resolvedTenantId()  : string|null
The tenant UUID §27 routes substitute for `{tenant_id}`, or `null` when the client was constructed without one (§27.4 rule 3).
resources()  : ResourcesApi
The resource hierarchy role assignments cascade down.
revoke()  : void
`POST /oauth2/revoke` (RFC 7009, CONTRACT.md §12.1) — revoke an access or refresh token. Returns nothing. Per RFC 7009 the server answers `200` for an unknown, expired, or already-revoked token too, so this call is **idempotent**: only a `401` (client authentication failed) is an error, surfaced as {@see \Axiam\Sdk\Core\OAuthProtocolError} (§12.1 note 5). A `5xx` still raises {@see NetworkError}.
roles()  : RolesApi
Roles, their permission sets, and their assignment to users and groups.
saml()  : SamlApi
A tenant's SAML 2.0 identity provider (CONTRACT §29): service providers, SP-metadata import, and the IdP signing-credential lifecycle.
scimTargets()  : ScimTargetsApi
A tenant's outbound SCIM targets (CONTRACT §31): the downstream SCIM 2.0 service providers AXIAM pushes users and groups to.
scimTokens()  : ScimTokensApi
Bearer tokens for the SCIM 2.0 provisioning endpoint.
scopes()  : ScopesApi
Sub-resource granularity, always addressed under their resource.
serviceAccounts()  : ServiceAccountsApi
Machine identities, their secrets, and the certificate a device-bound one authenticates with.
settings()  : SettingsApi
Effective settings, and the organization/tenant layers they resolve from.
ssf()  : SsfApi
A tenant's Shared Signals Framework streams (CONTRACT §32): which receiver gets which CAEP and RISC security events.
ssfReceiver()  : SsfReceiver
An SSF receiver — the helper a relying party uses to verify the Security Event Tokens AXIAM pushes to it and to poll a poll stream (CONTRACT.md §32.7). See {@see \Axiam\Sdk\Ssf\SsfReceiver::verifySet()} and {@see \Axiam\Sdk\Ssf\SsfReceiver::poll()}.
ssoComplete()  : SsoCompleteResult
`POST /api/v1/auth/federation/oidc/callback` (CONTRACT.md §12.1) — step 2 of upstream SSO: consumes the single-use `$state`, provisions or links the user, and establishes the session. The session arrives as `Set-Cookie` — not in the response body (§12.1 note 6) — so it is captured automatically via this client's shared §4 cookie jar; the freshly-issued §3 CSRF token is captured too, exactly as {@see self::login()} does. §12.4 does not apply here: no ID token ever reaches the SDK on the federation path.
ssoCompleteHandoff()  : SsoCompleteResult
`POST /api/v1/auth/federation/handoff` (CONTRACT.md §12.1, contract 1.38) — redeem the single-use `axiam_handoff` code the SAML and Apple flows deliver.
ssoCompleteOauth2()  : SsoCompleteResult
`POST /api/v1/auth/federation/oauth2/callback` (CONTRACT.md §12.1, contract 1.38) — step 2 of a plain-OAuth2 login. Same `Set-Cookie` session and same §3 CSRF capture as {@see self::ssoComplete()}. §12.4 does not apply: an `OAuth2` provider issues no ID token at all (§12.1 note 11).
ssoProviders()  : FederationProviderList
`GET /api/v1/auth/federation/providers` (CONTRACT.md §12.1, contract 1.38) — which "Sign in with X" buttons to render for a workspace. The identifiers travel as **query** parameters; this is a `GET` and sends no body.
ssoStart()  : SsoStartResult
`POST /api/v1/auth/federation/oidc/start` (CONTRACT.md §12.1) — step 1 of first-time SSO against an **upstream** IdP. No JWT required. One tenant form (`$tenantId`/`$tenantSlug`) and one org form (`$orgId`/`$orgSlug`) must be resolvable, from the arguments or from this client's construction options (§5.1) — an unresolvable one raises {@see AuthError} client-side, with no wire call. Redirect the browser to the returned `authorizeUrl` and round-trip `state` back into {@see self::ssoComplete()} unmodified; the server keeps the nonce to itself (§12.1 note 7).
ssoStartOauth2()  : SsoStartResult
`POST /api/v1/auth/federation/oauth2/start` (CONTRACT.md §12.1, contract 1.38) — step 1 of a login through a **plain-OAuth2** upstream (GitHub, Facebook, `generic_oauth2`). Call this, rather than {@see self::ssoStart()}, exactly when the provider's `protocol` is `OAuth2` (§12.1 note 10).
tenants()  : TenantsApi
Tenants within an organization -- the isolation boundary every other namespace is scoped to.
tokenExchange()  : ExchangedToken
`POST /oauth2/token` with the RFC 8693 grant (CONTRACT.md §15.1) — exchange a token for a **narrower** one.
umaChallengeHeader()  : string
Format a `WWW-Authenticate: UMA` header (CONTRACT.md §20.3, emit half).
umaDeleteResource()  : void
`DELETE /uma2/rreg/resource_set/{id}` (CONTRACT.md §20.1).
umaExchangeTicket()  : RequestingPartyToken
The UMA ticket grant (CONTRACT.md §20.1) — redeem a permission ticket for an RPT.
umaListResources()  : array<int, string>
`GET /uma2/rreg/resource_set` (CONTRACT.md §20.1) — the ids this client registered.
umaParseChallenge()  : UmaChallenge|null
Parse a `WWW-Authenticate: UMA …` header (CONTRACT.md §20.3) — local computation, and deliberately **no** exchange of the ticket it finds.
umaReadResource()  : ResourceSet
`GET /uma2/rreg/resource_set/{id}` (CONTRACT.md §20.1).
umaRegisterResource()  : ResourceSet
`POST /uma2/rreg/resource_set` (CONTRACT.md §20.1) — register a UMA resource set.
umaRequestTicket()  : Sensitive
`POST /uma2/perm` (CONTRACT.md §20.1) — mint a permission ticket.
umaUpdateResource()  : ResourceSet
`PUT /uma2/rreg/resource_set/{id}` (CONTRACT.md §20.1) — replace a resource set.
updateClientRegistration()  : ClientRegistration
`PUT registration_client_uri` (RFC 7592 §2.2, CONTRACT.md §28.12) — **replace** this client's registration, and receive a **rotated** registration access token.
users()  : UsersApi
Users within the client's tenant, and the administrative side of their second factor and lockout state.
validateToken()  : TokenValidation
`validateToken` — `axiam.v1.TokenService/ValidateToken` (CONTRACT.md §1.1.1/§10.3, contract 1.51). Signature + expiry, plus the confirmation (`cnf`) a resource server validating over gRPC needs in order not to accept a sender-constrained token as a bearer token (§10.1 rule 9).
verifyEmail()  : void
`POST /api/v1/auth/verify-email` (CONTRACT.md §25.1).
verifyLocally()  : array<string, mixed>|null
Verify an INBOUND caller's token and nothing else — the seam every request guard must use (CONTRACT.md §10.1 rule 8). Delegates straight to {@see JwksVerifier::verify()}, which applies the full §10.1 minimum local-verification set INCLUDING rule 9 (contract 1.51), and returns `null` on any failure with **no fallback to another credential**.
verifyLocallyOrFallback()  : array<string, mixed>|null
Local-first JWT verification with a reactive-refresh fallback (D-02) — for the SDK's own OUTBOUND calls, where the token being verified is this client's own and refreshing it is the intended recovery. Tries {@see JwksVerifier::verify()} first (no network call on the happy path); if that fails (expired/unknown-kid/invalid token), attempts the shared single-flight refresh (§9, D-06) and re-verifies the FRESH access token. Returns `null` — never unverified claims — on any failure.
verifyLogoutToken()  : VerifiedLogoutToken
Verify a back-channel logout token the OP pushed to this application's `backchannel_logout_uri` (CONTRACT.md §12.7.3).
verifyMfa()  : LoginResult
`POST /api/v1/auth/mfa/verify` (CONTRACT.md §1) — completes the two-phase flow started by {@see self::login()} when {@see LoginResult::$mfaRequired} was `true`. `$challengeToken` is the `Sensitive`-wrapped value from that `LoginResult` (D-11: never a raw string on the public surface).
verifyWithProofs()  : array<string, mixed>|null
{@see self::verifyLocally()}, applying CONTRACT.md §10.1 rule 9 against `$proofs` — evidence YOUR connection established for THIS request, never a value taken from a caller-settable request header (§10.1 rule 9 detail 2: e.g.
webauthnAuthenticateFinish()  : WebauthnLoginResult
`POST /api/v1/auth/webauthn/authenticate/finish` (CONTRACT.md §24.1).
webauthnAuthenticateStart()  : WebauthnChallenge
`POST /api/v1/auth/webauthn/authenticate/start` (CONTRACT.md §24.1) — begin the **second-factor** ceremony.
webauthnDiscoverableFinish()  : WebauthnLoginResult
`POST /api/v1/auth/webauthn/authenticate/discoverable/finish` (CONTRACT.md §24.1).
webauthnDiscoverableStart()  : WebauthnChallenge
`POST /api/v1/auth/webauthn/authenticate/discoverable/start` (CONTRACT.md §24.1) — begin the usernameless ceremony.
webauthnPolicy()  : WebauthnPolicyApi
Per-tenant attestation policy governing the §24 ceremonies, and the compliance report over it.
webauthnRegisterFinish()  : WebauthnCredential
`POST /api/v1/auth/webauthn/register/finish` (CONTRACT.md §24.1) — hand the authenticator's answer back and store the credential.
webauthnRegisterStart()  : WebauthnChallenge
`POST /api/v1/auth/webauthn/register/start` (CONTRACT.md §24.1) — begin enrolling a passkey for the signed-in user.
webauthnSetupRegisterFinish()  : LoginResult
`POST /api/v1/auth/webauthn/setup/register/finish` (CONTRACT.md §24.1, contract 1.45) — finish enrolling the passkey or security key {@see self::webauthnSetupRegisterStart()} offered, and with it the login that was interrupted.
webauthnSetupRegisterStart()  : WebauthnChallenge
`POST /api/v1/auth/webauthn/setup/register/start` (CONTRACT.md §24.1, contract 1.45) — begin enrolling a passkey or security key as the FIRST factor of a forced login enrolment.
webhooks()  : WebhooksApi
Outbound event notifications. Delivery signatures are verified with the §13 helper, which this namespace configures.

Methods

__construct()

public __construct(string $baseUrl, string $tenant[, string|null $orgSlug = null ][, string|null $orgId = null ][, string|null $customCa = null ][, string|null $clientCert = null ][, string|null $clientKey = null ][, LoggerInterface|null $logger = null ][, bool|null $restOnly = null ][, int $cacheTtlSeconds = 300 ][, string|null $grpcTarget = null ][, callable|null $transportHandler = null ][, string|null $oidcClientId = null ][, Sensitive|string|null $oidcClientSecret = null ][, string|null $oidcTenantId = null ][, string|null $expectedIssuer = null ][, string|null $expectedAudience = null ][, bool $retryEnabled = true ][, float $decisionMemoTtlMs = 0.0 ][, callable|null $telemetryHook = null ][, string|null $actingTenant = null ]) : mixed
Parameters
$baseUrl : string

The AXIAM server's base URL (e.g. https://api.axiam.example).

$tenant : string

The tenant slug — REQUIRED, no nullable default anywhere on this signature (D-13, §5). There is no default tenant; constructing this client without one is a compile-time (missing required argument) error, and an empty string is rejected at runtime as a backstop.

$orgSlug : string|null = null

Organization slug — mutually exclusive with $orgId. The real login/refresh handlers require an org identifier beyond CONTRACT.md §5's tenant-only minimum (mirrors the Python/C# sibling SDKs' org_slug/org_id constructor options).

$orgId : string|null = null

Organization UUID — mutually exclusive with $orgSlug.

$customCa : string|null = null

A CA bundle FILE PATH (PEM-encoded) — the ONLY TLS escape hatch (§6/D-12). Never pass a value here to disable TLS verification; there is no such option on this class.

$clientCert : string|null = null

§6.1 (mTLS): the client's X.509 identity certificate CHAIN as a PEM STRING (not a path). When supplied together with $clientKey, this client presents that certificate for mutual TLS on both the REST and gRPC transports. Purely additive — server verification is never relaxed (§6.1.2). Must be a PEM value; a non-PEM string is rejected at construction. null (default) leaves the default bearer-cookie behavior unchanged (§6.1.5).

$clientKey : string|null = null

§6.1/§7 (mTLS): the PEM STRING of the private key matching $clientCert (PKCS#8 or PKCS#1). Secret material — it is held behind Sensitive and never logged, displayed, or exposed via a getter. $clientCert and $clientKey are all-or-nothing: supplying exactly one throws InvalidArgumentException.

$logger : LoggerInterface|null = null

Injectable logger (D-15: diagnostic-only — status codes and operation names, NEVER a token/credential value). Defaults to a silent NullLogger.

$restOnly : bool|null = null

Force REST-only authz transport. null (default) resolves to true when $grpcTarget is not supplied (there would be nothing to connect the gRPC transport to) and false otherwise — an explicit true/false always wins. REST authz ALWAYS works regardless of this setting (D-03).

$cacheTtlSeconds : int = 300

JwksVerifier's local JWKS TTL cache lifetime.

$grpcTarget : string|null = null

gRPC target host:port (e.g. api.axiam.example:9443), required only to actually use the gRPC authz transport.

$transportHandler : callable|null = null

Test-only seam (NOT part of the public API contract, trailing/optional so it never affects SC#1's "tenant is required" reflection check): a raw Guzzle handler (e.g. GuzzleHttp\Handler\MockHandler) used as the base handler for both internal HandlerStacks instead of Guzzle's default cURL/stream handler. Mirrors the C# sibling SDK's CreateForTesting internal seam, adapted to Guzzle's own documented HandlerStack::create($mockHandler) testing idiom (docs.guzzlephp.org/en/stable/testing.html) — never used by production code.

$oidcClientId : string|null = null

CONTRACT.md §12: the relying party's OAuth2 client_id, used by every oidc*/introspect/revoke operation and matched against an ID token's aud/azp (§12.4 rule 4). Required only by callers that use the §12 OIDC/SSO helpers — omitting it leaves the §1–§11 surface completely unaffected, and a §12 call without one raises AuthError before any wire call.

$oidcClientSecret : Sensitive|string|null = null

CONTRACT.md §12: the confidential client's client_secret, held behind Sensitive (§12.5). Omit for a public client — introspect/revoke/loginClientCredentials REQUIRE it (§12.1 note 4) and raise AuthError when it is absent; oidcExchange/ oidcRefresh omit it from the form body entirely when absent, per §12.1's "MUST omit rather than send empty/null" rule.

$oidcTenantId : string|null = null

CONTRACT.md §12.3 rule 4: the tenant UUID used as the default ?tenant_id= query parameter on /oauth2/* calls when a call does not supply one explicitly. $tenant above is a SLUG (§5's X-Tenant-ID header value) and is never accepted where the wire contract requires a UUID — a §12 call with neither this nor a per-call tenantId raises AuthError client-side, with no wire call.

$expectedIssuer : string|null = null

CONTRACT.md §10.1 rule 5: the iss value local token verification requires. CONDITIONAL and unset by default — null means no issuer check is performed at all; once supplied, a token whose iss differs (or which carries no iss) is rejected. There is no default value and no hardcoded AXIAM issuer anywhere in this SDK.

$expectedAudience : string|null = null

CONTRACT.md §10.1 rule 6: the aud value local token verification requires. CONDITIONAL and unset by default — null means no audience check at all; once supplied, a token whose aud does not contain it (including one with no aud) is rejected. An app guarding a user-facing resource server should generally expect axiam:user; it is not defaulted, because a service-to-service guard legitimately expects a different audience.

$retryEnabled : bool = true
$decisionMemoTtlMs : float = 0.0
$telemetryHook : callable|null = null
$actingTenant : string|null = null

CONTRACT.md §5.2 rule 1 (contract 1.51): the construction-time form of the acting-tenant switch — the tenant UUID this client acts on from its very first request, sent as X-Axiam-Tenant on every /api/v1 request while set. Meaningful only for an organization-level* principal (§5.2); nothing is gated here — construction precedes the login that would reveal whether the principal really is one, and a service account never receives a login result at all — only the value's shape (a UUID) is checked. null (the default) sends no such header, byte- for-byte what every client sent before 1.51. Distinct from $tenant above (§5 rule 2's X-Tenant-ID, sent unconditionally and never read by the server as a tenant switch). See self::actingTenant() for the on-client form and its gating.

__destruct()

§6.1: cleans up the `0600` temp files backing the client-certificate identity when this client is destroyed, so no PEM material (least of all the private key) outlives the object on disk. A no-op when mTLS was not configured.

public __destruct() : mixed

actingTenant()

Switches this client to act on `$tenantId` — CONTRACT.md §5.2 rule 1 (contract 1.51). Every subsequent `/api/v1` request this client makes carries `X-Axiam-Tenant: $tenantId`, until {@see self::clearActingTenant()} is called or another `actingTenant()` replaces it.

public actingTenant(string $tenantId) : self

Meaningful only for an organization-level principal; see the $actingTenant constructor parameter for what the header does and does not reach (not X-Tenant-ID, not a {tenant_id} management path, not gRPC — REST-only, §5.2 rule 1's own text).

Mutates this client and returns it (self), unlike the reference implementation's per-handle isolation (a new object sharing the session, leaving the original untouched). §5.2 rule 1 requires only that the header be sendable and clearable — it does not require handle isolation — and PHP's request lifecycle has no concurrent tasks sharing one client the way a long-lived process does, so the simpler, shared-state form is what this port ships. Two concurrent PHP requests never share one AxiamClient instance in the first place. Recorded here, in the README and in the CHANGELOG as a deliberate, documented difference (C-12).

Parameters
$tenantId : string
Tags
throws
NetworkError

if $tenantId is not a UUID. The server silently ignores an X-Axiam-Tenant value that does not parse and answers for the caller's own tenant instead — reporting success about the wrong tenant — so this is refused client-side, with no wire call.

AuthzError

when this client holds a login result (§5.2.2) that reported organizationLevel: false (an ordinary tenant principal is a principal of exactly one tenant, and the server answers 403 to anything else), or that reported reachableTenantIds not containing $tenantId (§5.2.3 rule 4). A client holding no such result — a service account from client credentials or the device login, an injected token, a session completed without a user object — has nothing to gate on: the header is sent as asked, and the server's 403 is the answer. An organization-level service account is a supported design, and the server honours the header for one on the same terms as for a user.

Return values
self

actingTenantId()

The tenant this client currently acts on, or `null` when it acts on its own tenant.

public actingTenantId() : string|null
Return values
string|null

authenticateDevice()

`authenticateDevice` — `POST /api/v1/auth/device` (CONTRACT.md §6.1 rules 6-10, contract 1.51): the mTLS device login. The client presents the X.509 identity certificate configured via `$clientCert`/`$clientKey`, and the server authenticates it, no request body, no tenant/credential needed beforehand.

public authenticateDevice() : DeviceToken

Reachable only when this client was built with a client certificate (§6.1 rule 7). On a client built without one, this raises AuthError client-side, with ZERO wire calls — without a certificate the server would answer 401 anyway, so going to the wire gains nothing and turns a configuration mistake into an authentication failure.

The request carries no credential from an earlier session. $plainHttp shares this client's ONE cookie jar (§4) with every other REST call, and AuthMiddleware decorates EVERY same-origin request with Authorization: Bearer from Session::accessToken() (which itself prefers a cookie-sourced token) and, on this POST, X-CSRF-Token — so withholding the Cookie header alone is not enough: a cookie left from an earlier login()/verifyMfa() session would still ride this call as a stale Authorization: Bearer (the same defect independently found and fixed in the C# port, ilpanich/axiam-csharp-sdk#96). The request is therefore sent with BOTH Guzzle's per-request cookies => false override AND AuthMiddleware::NO_SESSION_CREDENTIALS_OPTION (the same option self::postWithoutSessionCredentials() uses for §24.1's setup/register/* pair) rather than by clearing the jar: neither the cookie, the stale bearer nor a stale X-CSRF-Token reaches this endpoint, while X-Tenant-ID and (when set) X-Axiam-Tenant still do — CONTRACT.md §5.2 rule 1/§5.2.2 rule 4 require both on every /api/v1 request regardless of session credentials, and NO_SESSION_CREDENTIALS_OPTION does not touch either. The prior session must otherwise survive a refusal intact (see below).

Session state changes ONLY after a 200 with a well-formed body — immediately before adopting the token via Session::adoptBearerCredential(). Only then: the shared cookie jar is cleared (Session::accessToken() prefers a cookie-sourced token over an adopted one, §12.1's login_client_credentials-as-credential-source precedent, so a leftover cookie would otherwise silently outrank the device token and every subsequent call would run as that earlier session's principal — the exact theft-adjacent scenario §6.1 rule 9 exists to close); the decision memo is cleared (§17.1 rule 9: the subject changed); and the §5.2 acting-tenant gate is reset to unknown (a device holds no LoginUserInfo). A REFUSED or malformed device login leaves the jar, the gate and the memo exactly as they were — a caller who was mid-session when this call was refused keeps a working session, matching how a refused SSO completion elsewhere in this port changes nothing.

Every refusal is 401 (§6.1 rule 8: an unknown, untrusted, expired, revoked or unbound certificate, and a Server-type certificate, all answer 401 authentication_failed), mapped to AuthError with the server's message. This is the login: a later 401 on this credential is returned to the caller as-is, never sent through the §9 refresh guard — there is no refresh token to spend (§6.1 rule 6), and the guard would fail before the wire anyway. A 429 (the route's per-client-IP rate limit, server default 60/min) is a NetworkError, not an AuthError — §16's ordinary mapping — and this call is attempted exactly once, like every login.

The token is certificate-bound (§6.1 rule 9, cnf.x5t#S256) when AXIAM itself terminated the handshake. A REST request without the same certificate is 401; a gRPC call is UNAUTHENTICATED on a listener that requests client certificates, and refused on every call on one that does not (the server default). §6.1 rule 4 already applies this client's certificate to BOTH transports, which is what makes that hold.

Return values
DeviceToken

batchCheck()

`batchCheck` — results preserve input order (CONTRACT.md §1).

public batchCheck(array<int, array{action: string, resourceId: string, scope?: string|null}> $checks) : array<int, bool>
Parameters
$checks : array<int, array{action: string, resourceId: string, scope?: string|null}>
Return values
array<int, bool>

can()

`can` — the browser/UI-scenario alias for {@see self::checkAccess()} (CONTRACT.md §1 note). Argument order is `(action, resource)` — matching {@see self::checkAccess()} and every other AXIAM SDK's `can`/`Can` (D-09/SDK-Q09; this was previously reversed relative to the rest of the SDK family).

public can(string $action, string $resource) : bool
Parameters
$action : string
$resource : string
Return values
bool

checkAccess()

`checkAccess` — delegates to {@see AuthzDispatcher} (REST default, gRPC when available).

public checkAccess(string $action, string $resourceId[, string|null $scope = null ][, string|null $subjectId = null ]) : bool
Parameters
$action : string
$resourceId : string
$scope : string|null = null
$subjectId : string|null = null

Additive, optional (CONTRACT.md §11.2.2 — declarative authorization helpers): when given, the check is evaluated for THIS subject (a UUID) rather than whichever identity this client's own session represents. This matters for a framework bridge sharing ONE AxiamClient instance (typically authenticated as a service account, or not authenticated at all) to authorize each inbound HTTP request's OWN end user: passing subjectId: $endUserId here checks the end user's permissions, never the shared client's. null (the default) preserves this method's pre-§11 behavior exactly.

Return values
bool

checkAccessDecision()

`checkAccess`, returning the **full** decision including CONTRACT.md §11 rule 9's `reason_code` — see {@see \Axiam\Sdk\Rest\AccessDecision}'s own docblock for why that distinction matters. Delegates to {@see AuthzDispatcher} on whichever transport {@see self::checkAccess()} itself would use (REST default, gRPC when available), so the two never disagree about which transport ran.

public checkAccessDecision(string $action, string $resourceId[, string|null $scope = null ][, string|null $subjectId = null ]) : AccessDecision
Parameters
$action : string
$resourceId : string
$scope : string|null = null
$subjectId : string|null = null

See self::checkAccess()'s own docblock.

Return values
AccessDecision

cibaAwait()

Poll for a CIBA request's outcome until it is decided or expires, honouring `interval` and `slow_down` (CONTRACT.md §33.7). Returns the token set without adopting it. See {@see OidcEngine::cibaAwait()}, including the ping-mode fallback.

public cibaAwait(CibaInitiateResponse $initiated[, string|null $tenantId = null ][, OidcConfiguration|null $configuration = null ][, CibaClock|null $clock = null ]) : OidcTokenSet
Parameters
$initiated : CibaInitiateResponse
$tenantId : string|null = null
$configuration : OidcConfiguration|null = null
$clock : CibaClock|null = null

The clock to wait on (tests).

Return values
OidcTokenSet

cibaHandlePing()

Verify a CIBA ping's bearer and return the `auth_req_id` it names (CONTRACT.md §33.1) — pure, no I/O. Answer the ping, then {@see self::cibaPoll()}. See {@see OidcEngine::cibaHandlePing()}.

public cibaHandlePing(array<string|int, string|array<int, string>> $headers, string $body, Sensitive $expectedToken) : Sensitive
Parameters
$headers : array<string|int, string|array<int, string>>

The ping request's headers.

$body : string
$expectedToken : Sensitive
Tags
throws
AuthError

when the Authorization header is not exactly the expected bearer.

ValidationError

when the body is not a ping body.

Return values
Sensitive

cibaInitiate()

`POST /oauth2/bc-authorize` (CONTRACT.md §33.1) — ask AXIAM to authenticate a user on another device. **Never retried**, and a success proves nothing about the user; see {@see OidcEngine::cibaInitiate()}.

public cibaInitiate(CibaInitiateRequest $request[, string|null $tenantId = null ][, OidcConfiguration|null $configuration = null ][, CibaClock|null $clock = null ]) : CibaInitiateResponse

The client authenticates with its oidcClientSecret, or — a tls_client_auth client — with its §6.1 certificate alone; a client with neither is refused locally.

Parameters
$request : CibaInitiateRequest
$tenantId : string|null = null
$configuration : OidcConfiguration|null = null
$clock : CibaClock|null = null

Stamps the response's receivedAt (tests).

Tags
throws
AuthError

locally, for a client with no credential or a server without CIBA.

OAuthProtocolError

for the server's refusals.

Return values
CibaInitiateResponse

cibaPoll()

One CIBA token request (CONTRACT.md §33.1). `access_denied` and `expired_token` are {@see \Axiam\Sdk\Core\OAuthProtocolError}s told apart by `isAccessDenied()` / `isExpiredToken()`. **Store the tokens first**: a request is redeemed once. See {@see OidcEngine::cibaPoll()}.

public cibaPoll(Sensitive $authReqId[, string|null $tenantId = null ][, OidcConfiguration|null $configuration = null ]) : OidcTokenSet
Parameters
$authReqId : Sensitive
$tenantId : string|null = null
$configuration : OidcConfiguration|null = null
Return values
OidcTokenSet

clearActingTenant()

Stops acting on another tenant: no further request sends `X-Axiam-Tenant`, and this client acts on its own tenant again (CONTRACT.md §5.2 rule 1's "a way to clear it").

public clearActingTenant() : self
Return values
self

close()

Releases this client's local resources (CONTRACT.md §18).

public close() : void

Idempotent — calling it twice is not an error. Cleanup runs from error paths, and an error path that itself throws hides the original failure.

This does not log out. §18.1 rule 5: shutting down a client releases local resources and never reaches the network. The server-side session deliberately outlives the client object, which is what lets a process restart and resume; a close() that logged out would silently end every user's session on each deploy. Call AxiamClient::logout() first if ending the session is what you want.

After this returns, every operation on this client throws a NetworkError rather than silently reconnecting.

debugClientCertOptions()

Test-only seam (not part of the public API contract, mirroring {@see self::debugVerifyOption()}): exposes the §6.1 client-identity options (`cert` = certificate-chain file, `ssl_key` = private-key file) actually configured on this client's authz Guzzle transport, so tests can assert the mTLS wiring without performing a live TLS handshake. Both entries are `null` when mTLS was not configured. The values are FILE PATHS, never the PEM bytes — this seam never surfaces the private key itself.

public debugClientCertOptions() : array{cert: string|null, ssl_key: string|null}
Return values
array{cert: string|null, ssl_key: string|null}

debugVerifyOption()

public debugVerifyOption() : string|bool
Return values
string|bool —

The Guzzle verify option: true, or a CA bundle path (never false).

deleteClientRegistration()

`DELETE registration_client_uri` (RFC 7592 §2.3, CONTRACT.md §28.12) — delete this client's registration. A `204` returns normally.

public deleteClientRegistration(string $registrationClientUri, Sensitive $registrationAccessToken) : void

Never retried: a retry after a lost 204 would read 401 and report a successful deletion as a failure.

Parameters
$registrationClientUri : string
$registrationAccessToken : Sensitive
Tags
throws
ValidationError

for a URI at another origin (local).

OAuthProtocolError

for an answer carrying error.

deviceAuthorize()

`POST /oauth2/device_authorization` (CONTRACT.md §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

The client authenticates the way it is configured to (contract 1.62, §14.1 rule 2). A client built with an oidcClientSecret sends it as client_secret in the form body — never Authorization: Basic, whatever it is registered for (§12.1 rule 3) — and a client with a §6.1 certificate presents it on the connection. A public client (the usual device, which cannot hold a secret) is built with neither, sends client_id and no credential, and is never refused here; the server refuses a public client that sends one. A failed client authentication is 401 invalid_client, an OAuthProtocolError.

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.

Return values
DeviceAuthorization

deviceLogin()

The composed §14.3 helper: start the grant, hand the caller the user code (before the first poll), 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 ][, callable(): float|null $now = null ][, callable(float): void|null $retrySleep = null ]) : OidcTokenSet

Returns the token set; $adoptAsCredential is the same opt-in flag self::loginClientCredentials() uses (§14.3 rule 4, contract 1.7).

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, for tests.

$now : callable(): float|null = null

Injectable clock in seconds, for tests.

$retrySleep : callable(float): void|null = null

Injectable sleeper (milliseconds) for the §16 waits inside a poll.

Return values
OidcTokenSet

devicePoll()

`POST /oauth2/token` with the device-code grant (CONTRACT.md §14.1) — **one** poll attempt, for an application driving its own loop. Most callers want {@see self::deviceLogin()}.

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

Every poll sends client_id and authenticates the client as self::deviceAuthorize() does (contract 1.62): the configured client_secret in the body, or the certificate on the connection, or nothing for a public client. A 401 invalid_client is an OAuthProtocolError, terminal and never retried. This SDK does not generate DPoP proofs (§21.9), so a client registered for DPoP is refused that way.

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

directory()

A tenant's LDAP / Active Directory identity source (CONTRACT §30): its configuration, account linking, and the sync job's status.

public directory() : DirectoryApi
Return values
DirectoryApi

expectedAudience()

The `$expectedAudience` this client was constructed with (CONTRACT.md §10.1 rule 6), or `null` when no audience check is configured.

public expectedAudience() : string|null

Public so a §10 guard built around this client can read back its own audience configuration without a second, independently-configured copy — McpGuardOptions::build() (CONTRACT.md §28.5 rule 2) is the caller.

Return values
string|null

getUserInfo()

`getUserInfo` — the gRPC-ONLY OIDC-style userinfo operation (CONTRACT.md §1.1, contract 1.3): returns the authenticated caller's identity claims from `axiam.v1.UserInfoService/GetUserInfo`, the low-latency counterpart of the server's REST `GET /oauth2/userinfo`. Delegates to {@see AuthzDispatcher} — this class never hand-rolls the gRPC transport (D-03).

public getUserInfo() : UserInfo

Identity is derived server-side from the current bearer token; the request is empty. sub/tenantId/orgId are always populated on the returned UserInfo; email is present only with the "email" token scope and preferredUsername only with "profile" (the server gates them exactly as the REST endpoint does). Requires a prior successful self::login() — calling it with no token raises AuthError before any wire call (§1.1.3) — and, being gRPC-only (§1.1.6), requires the grpc PECL extension plus a configured grpcTarget; there is NO REST fallback, so on a REST-only runtime it raises NetworkError rather than degrading. A gRPC UNAUTHENTICATED response drives the shared single-flight refresh (§9) and retries once (§1.1.4).

Return values
UserInfo

groups()

Named collections of users. Roles assigned to a group are inherited by every member.

public groups() : GroupsApi
Return values
GroupsApi

introspect()

`POST /oauth2/introspect` (RFC 7662, CONTRACT.md §12.1) — ask the server whether a token is active and, if so, for its metadata. Requires confidential-client credentials (§12.1 note 4). A `401` here is a client-credential failure surfaced as {@see \Axiam\Sdk\Core\OAuthProtocolError} and NEVER enters the §9 refresh guard (§12.3 rule 3).

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

The token to introspect — accepts the wrapped or bare form.

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

when no oidcClientSecret was configured.

Return values
IntrospectionResult

introspectToken()

`introspectToken` — `axiam.v1.TokenService/IntrospectToken` (CONTRACT.md §1.1.1/§10.3, contract 1.51). The RFC 7662 set, plus the confirmation. See {@see self::validateToken()} for the shared rules this method follows exactly; the only difference is the richer RFC-7662-shaped return value.

public introspectToken(Sensitive|string $inspectedAccessToken) : TokenIntrospection
Parameters
$inspectedAccessToken : Sensitive|string
Return values
TokenIntrospection

login()

`POST /api/v1/auth/login` (CONTRACT.md §1). Returns a typed {@see LoginResult} — an MFA challenge (HTTP 202) is an expected outcome, not an exception: callers MUST check {@see LoginResult::$mfaRequired} before assuming a session was established (SC#1).

public login(string $email, string $password) : LoginResult
Parameters
$email : string
$password : string
Return values
LoginResult

loginClientCredentials()

`POST /oauth2/token` with `grant_type=client_credentials` (CONTRACT.md §12.1) — service-account machine-to-machine login. Requests no `openid` scope, so the response carries no `id_token`. Pass `$adoptAsCredential: true` to additionally adopt the returned access token as this client's bearer credential for subsequent same-origin REST calls (§12.1, an opt-in MAY) — the token is held behind {@see Sensitive} inside {@see Session} and is NEVER sent to `/oauth2/*`.

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

when no oidcClientSecret was configured — this grant cannot be performed by a public client (§12.1 note 4).

Return values
OidcTokenSet

loginOpaque()

`POST /api/v1/auth/opaque/login/start` followed by `/finish` — OPAQUE login, RFC 9807 (CONTRACT.md §23).

public loginOpaque(string $usernameOrEmail, string $password) : LoginResult

A sibling of self::login(), not a replacement. It takes the same arguments and returns the same LoginResult, MFA branch included, so an application can switch a tenant to OPAQUE without touching its own code.

What this does that login does not. The password never leaves this process. What crosses the wire is a blinded group element and a MAC, neither useful without the account's registration record and the tenant's OPRF seed — so a TLS-terminating proxy, an accidentally verbose request log, or a heap dump on the server cannot capture a plaintext password, because the server never has one. It also means a stolen record database is not offline-crackable on its own, which is the pre-computation resistance SRP could not offer. It does not protect against a compromised AXIAM server.

PHP is no longer doubly conditional. The SRP client this replaces needed a bignum extension and a tenant on pbkdf2_sha256, because no PHP runtime offers Argon2id with a caller-supplied salt — so the AXIAM default was, for PHP, unreachable. The key stretching now happens inside libaxiam_opaque_ffi, so the only remaining condition is that the library and ext-ffi are present, which self::opaqueAvailable() reports.

One round trip, and no server-proof step. SRP had to guess a group before the server named one and restart the exchange if it guessed wrong; KE1 does not depend on the key-stretching function. And where the old §23.3 rule 6 had to mandate an M2 check in capitals — because skipping it kept only half the protocol — RFC 9807's AKE authenticates the server during the handshake, so opening KE2 is the proof that it holds the record.

Zeroization. PHP strings are immutable and the runtime copies them freely, so this SDK cannot clear the password — §23.3 rule 8 requires saying so rather than implying a guarantee it cannot keep.

A failed KE2 is not always the end (§23.4 rule 7, contract 1.29). Nothing is ever sent to login/finish when the envelope does not open, but what happens next depends on the mode the login/start response named, and on nothing else. Under "optional" this method retries over self::login() with the same credentials and returns that call's outcome — its success, or its error. Under "required", and for a response that carried no mode at all (a server older than the field), the failure is an AuthError and there is no retry. optional is the state a tenant lives in for the whole of a migration: every account has no registration record the moment OPAQUE is enabled and acquires one only when its password is next set, so treating the failed exchange as final would lock out every user of the tenant. See OpaqueMode for why mode is not downgrade protection.

Parameters
$usernameOrEmail : string
$password : string
Tags
throws
NetworkError

if the tenant has OPAQUE disabled (the endpoint answers 404 — a property of the tenant, not of any user), if ext-ffi or libaxiam_opaque_ffi is absent, or if the server names a key-stretching function this SDK cannot ask for. Deliberately not AuthError: reporting a configuration gap as a credential failure would send a user off to reset a password that works, and would stop a caller falling back to self::login()

AuthError

for a wrong password, an account that does not exist, and a server that does not hold the record — indistinguishable by design. Nothing is sent to login/finish in that case (§23.4 rule 7). Raised under mode: "required" and under a response with no mode; under "optional" the exchange is retried over self::login() first and this carries that call's failure instead

Return values
LoginResult

logout()

`POST /api/v1/auth/logout` (CONTRACT.md §1) and clears local session state: the shared cookie jar (§4) and the captured CSRF token (§3). The session id comes from the current access token's `jti` claim (unverified decode — an operational hint only, never an authorization decision, mirroring the Python/C# sibling SDKs).

public logout() : void

logoutUrl()

Build the RP-initiated logout URL to redirect the user agent to (CONTRACT.md §12.7.2). Does **not** clear this client's own session.

public logoutUrl(Sensitive|string $idToken[, string|null $postLogoutRedirectUri = null ][, string|null $state = null ][, OidcConfiguration|null $configuration = null ]) : string
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

management()

The CONTRACT.md §27 management surface: 190 operations across 28 namespaces.

public management() : ManagementApi

$client->management()->users()->listItems(). Built on the same Guzzle client that carries AuthMiddleware and RefreshMiddleware, so §27.8's "the generated layer sits on the SDK's existing request path" holds by construction rather than by convention.

Memoised: the returned object holds only the transport and the client's default scope, and handing back a new one per call would make management() !== management() for no benefit. This is not a §27.4 rule 10 violation — that rule forbids caching RESPONSES, and nothing here caches one.

Return values
ManagementApi

mfaConfirm()

`POST /api/v1/auth/mfa/confirm` (CONTRACT.md §25.1) — activate the factor {@see self::mfaEnroll()} offered. Returns whether MFA is now enabled.

public mfaConfirm(string $totpCode) : bool
Parameters
$totpCode : string
Return values
bool

mfaEnroll()

`POST /api/v1/auth/mfa/enroll` (CONTRACT.md §25.1) — start voluntary TOTP enrolment for the signed-in user.

public mfaEnroll() : MfaEnrollment

Changes nothing about the current session. In particular it does not clear the §17 decision memo: the subject has not changed, and discarding a warm memo on an unrelated profile action costs a round trip on every check that follows (§25.2 rule 3).

Return values
MfaEnrollment

mfaSetupConfirm()

`POST /api/v1/auth/mfa/setup/confirm` (CONTRACT.md §25.1) — finish forced enrolment and, with it, the login that was interrupted.

public mfaSetupConfirm(Sensitive|string $setupToken, string $totpCode) : LoginResult

Adopts credentials exactly as self::login() does, because it is the completion of a login (§25.2 rule 2) — including capturing the session's first CSRF token.

Parameters
$setupToken : Sensitive|string
$totpCode : string
Return values
LoginResult

mfaSetupEnroll()

`POST /api/v1/auth/mfa/setup/enroll` (CONTRACT.md §25.1) — start the enrolment a {@see self::login()} demanded.

public mfaSetupEnroll(Sensitive|string $setupToken) : MfaEnrollment

Reached when login() returns LoginResult::$mfaSetupRequired: the tenant requires MFA and this account has none. There is no session yet — the setup token is the credential.

Parameters
$setupToken : Sensitive|string
Return values
MfaEnrollment

network()

A tenant's network segments and the RADIUS reply profile each owns, and the tenant's `Session-Timeout` cap (CONTRACT §36, contract 1.62). Every write is conditional on the `version` the caller read; the decision route FreeRADIUS calls is not SDK surface.

public network() : NetworkApi
Return values
NetworkApi

oidcBegin()

Build an authorization request (CONTRACT.md §12.1) — **pure local computation, no network I/O**. Generates a `state`/`nonce` (CSPRNG, ≥128 bits) and a fresh PKCE verifier/challenge pair (**S256 only**), and builds `$configuration`'s `authorization_endpoint` into a redirect URL with exactly the eight SDK-owned query parameters plus any `$extraParams` supplied.

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

Nothing is stored (§12.3 rule 1): persist the returned state, nonce and codeVerifier yourself (e.g. in your own HTTP session, or via MemoryOidcStateStore) and pass nonce/codeVerifier back into self::oidcExchange() when the authorization code arrives.

Parameters
$configuration : OidcConfiguration
$redirectUri : string
$scope : string|array<int, string>|null = null

openid is added automatically when absent (§12.1 rule 4). Defaults to openid.

$extraParams : array<string, string> = []

Extra authorization-request parameters (e.g. prompt, login_hint). Throws InvalidArgumentException if one tries to override an SDK-owned parameter (§12.1 rule 5).

Return values
AuthorizationRequest

oidcDiscover()

`GET /.well-known/openid-configuration` (CONTRACT.md §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). The document's own `issuer` is authoritative for ID-token validation (§12.4 rule 3) and may legitimately differ from `$baseUrl` behind a proxy — never treated as an error (§12.3 rule 6).

public oidcDiscover() : OidcConfiguration
Return values
OidcConfiguration

oidcExchange()

`POST /oauth2/token` with `grant_type=authorization_code` (CONTRACT.md §12.1) — exchange an authorization code for a token set, validating the returned ID token in full (§12.4) before returning. `$nonce` is MANDATORY: this grant always requests `openid`, so §12.4 rule 6 always applies. On ANY §12.4 failure the whole token set is discarded and {@see AuthError} is raised with the matching reason code (§12.4 rule 7) — `getReason()` returns one of `invalid_alg`, `unknown_kid`, `invalid_signature`, `invalid_issuer`, `invalid_audience`, `token_expired`, `nonce_mismatch`.

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

The verifier from the matching AuthorizationRequest — accepts the wrapped or bare form.

$redirectUri : string
$nonce : string
$tenantId : string|null = null

Tenant UUID for the required ?tenant_id= query parameter (§12.3 rule 4). Falls back to the client's oidcTenantId.

$configuration : OidcConfiguration|null = null

A pre-fetched discovery document, to avoid re-reading the (cached) one. Fetched via self::oidcDiscover() when omitted.

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 ][, string|null $dpopJkt = 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 and puts an opaque request_uri in the redirect, so what travels through the browser is a random string that cannot be edited into meaning something else.

Required for a FAPI 2.0 client (§21.1). Not retried, being a POST that creates server state (§26.2 rule 4).

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

RFC 9449 §10.1 dpop_jkt — the caller-computed JWK SHA-256 thumbprint of the DPoP key, pushed so the authorization code is bound to that key at issue time. Omitted from the form when null. This SDK verifies DPoP proofs but does not generate them (CONTRACT.md §21.9), so the thumbprint comes from the application, not from here.

Return values
PushedAuthorizationRequest

oidcRefresh()

`POST /oauth2/token` with `grant_type=refresh_token` (CONTRACT.md §12.1) — refresh an {@see OidcTokenSet} under the SAME §9 single-flight guard {@see self::refresh()} uses. A **distinct operation** from {@see self::refresh()} (the cookie/opaque-token session path) — the two are never merged, aliased, or fall back to one another, but they share ONE guard slot: a concurrent `oidcRefresh()` call finding the guard busy with a cookie-session refresh retries (bounded) rather than returning a stale token set.

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

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

Parameters
$refreshToken : Sensitive|string

The refresh token to redeem — accepts the wrapped or bare form.

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

opaqueAvailable()

Whether this installation can perform OPAQUE (§23.2).

public opaqueAvailable() : bool

PHP remains the AXIAM SDK language where this genuinely answers false — but for a different and simpler reason than before. The SRP equivalent was false when neither ext-gmp nor ext-bcmath was present, and even a true there did not promise every tenant would work: an argon2id tenant was refused at login time, because no PHP runtime offers Argon2id with a caller-supplied salt. That second condition is gone. The key stretching happens inside libaxiam_opaque_ffi, so a true here means every tenant works.

Return values
bool

opaqueEnrollment()

Builds a registration record for `$password`, to send with any request that sets one: `POST /api/v1/users`, `/auth/password/change`, `/auth/reset/confirm` and `/admin/bootstrap`.

public opaqueEnrollment(string $password) : OpaqueEnrollment

The server cannot build this — it never sees the plaintext — so it has to arrive with the request or not at all.

Unlike the srpEnrollment it replaces this performs I/O: one register/start round trip. OPAQUE's envelope is sealed under the server's oblivious PRF, so there is no offline computation that produces a valid record.

Note the parameters that are gone. There is no $identity: the SRP version required the account's USERNAME, and an email there produced a verifier no login could ever satisfy, whereas a record binds to a credential identifier the server chooses. And there is no $group or $params, because those come from the register/start response — a caller cannot pick a cost the server will not honour.

Parameters
$password : string
Tags
throws
NetworkError

if the tenant has OPAQUE disabled, if ext-ffi or libaxiam_opaque_ffi is absent, or if the server names a key-stretching function this SDK cannot ask for

Return values
OpaqueEnrollment

opaqueEnrollmentForSelf()

Builds a registration record for the **caller's own** new password, sealed against the tenant the caller's account lives in.

public opaqueEnrollmentForSelf(string $password) : OpaqueEnrollment

CONTRACT.md §5.2.2 rule 2. POST /auth/password/change and the record that accompanies it are about the account, not about whatever tenant the client is currently pointed at, and a record sealed against the acting tenant is refused with "the OPAQUE session was issued for a different tenant".

The distinction only bites for an organization-level principal that has selected another tenant to act on; for everyone else the two tenants are the same value and this behaves identically to self::opaqueEnrollment(). It is still the method to call for a self-service password change, because which principal is signed in is not something the call site usually knows.

Parameters
$password : string
Tags
throws
NetworkError

when no login has completed on this client yet — the principal tenant is reported by the login response, so there is nothing to seal against before then — and on the same terms as self::opaqueEnrollment() otherwise.

Return values
OpaqueEnrollment

passwordResetContext()

`GET /api/v1/auth/reset/context` (CONTRACT.md §25.1) — the OPAQUE policy for the account a reset token belongs to.

public passwordResetContext(Sensitive|string $token) : PasswordResetContext

Call this before self::confirmPasswordReset() on any tenant that might have §23 enabled: the client has to build a registration record, and building one needs parameters it cannot know before it has a token to ask with. Sending a plaintext password to a tenant in opaque_mode: required is refused, and refused late (§25.4 rule 1).

A 404 means unknown, expired or already-consumed, deliberately without distinguishing them; this SDK does not distinguish them either (§25.4 rule 3).

Parameters
$token : Sensitive|string
Return values
PasswordResetContext

platform()

Deployment-level probes and FIDO metadata state. Unauthenticated where the server leaves them so.

public platform() : PlatformApi
Return values
PlatformApi

privacy()

GDPR self-service: the authenticated account's own export and erasure. Scoped to the caller, never to another user.

public privacy() : PrivacyApi
Return values
PrivacyApi

reactors()

Registration of §22 AMQP extension actors -- the admin surface §22.9 describes, which no SDK could previously reach.

public reactors() : ReactorsApi
Return values
ReactorsApi

readClientRegistration()

`GET registration_client_uri` (RFC 7592 §2.1, CONTRACT.md §28.12) — read this client's own registration, as a client that registered itself through `POST /oauth2/register` (RFC 7591).

public readClientRegistration(string $registrationClientUri, Sensitive $registrationAccessToken) : ClientRegistration

The result carries neither the token nor the client secret (the server never returns them on a read), but it does carry every member an update needs — so the usual update is "read, change a field, update".

$registrationClientUri is used verbatim, query included, and only when its scheme, host and port are this client's base URL's (and https, unless the base URL is http on a loopback host); any other URI is refused locally, before any request. The token travels as Authorization: Bearer and nothing of this client's session goes with it. Retried per §16 on a transport failure, a 5xx, 408 or 429 — never on another 4xx.

Parameters
$registrationClientUri : string
$registrationAccessToken : Sensitive
Tags
throws
ValidationError

for a URI at another origin — local, with no request sent.

OAuthProtocolError

for an answer carrying error, at any status: 401 invalid_token is an unknown client, a wrong or rotated-away token, another tenant's client or a client with no token — the server never says which — and it never refreshes this client's session (§28.12.2 rule 3).

Return values
ClientRegistration

refresh()

`POST /api/v1/auth/refresh` (CONTRACT.md §1), routed through {@see Session}'s single-flight guard (§9, D-06) — the SAME mechanism {@see RefreshMiddleware} triggers reactively on a `401`. A failure surfaces as {@see AuthError} with no retry (§9.3).

public refresh() : void

requestPasswordReset()

`POST /api/v1/auth/reset` (CONTRACT.md §25.1) — ask for a reset mail.

public requestPasswordReset(PasswordResetRequest $request) : void

Returns normally whether or not the address exists, and this SDK exposes no way to tell the two apart. That is not an omission to improve on: a client that surfaced a "no such user" state — even one inferred from timing — would turn the endpoint into the account-enumeration oracle its uniform response exists to prevent (§25.4).

Parameters
$request : PasswordResetRequest

resendOwnVerification()

`POST /api/v1/users/me/resend-verification` (CONTRACT.md §25.1, §25.7) — resend the **signed-in caller's own** verification mail, and say what happened.

public resendOwnVerification() : void

Takes no address. The server reads it off the caller's own record, and this signature deliberately offers no way to name a different one: a parameter here would let an authenticated session mail an arbitrary address.

Unlike self::resendVerification() this reports the outcome, because the caller is signed in to the account it is asking about and none of the outcomes tells it anything it did not already bring with it:

  • returns — a token was minted and the mail enqueued. Delivery is asynchronous and can still fail at the provider; a queue that accepts everything in front of a provider that rejects it looks exactly like this succeeding (§25.7 rule 3).
  • 409 — already verified, or the account is in a state that must not be sent a live token. Raised as the §2 mapping of 409.
  • 429 — the daily resend limit. Raised as the §2 mapping of 429.

§25.7 rule 2 forbids falling back to the unauthenticated endpoint on either failure, and this SDK does not: that fallback turns both back into a silent success and restores the bug this operation exists to fix, with an extra round trip.

Tags
throws
AuthError

when there is no session — raised client-side, with no wire call.

AuthzError

on the 409: already verified, or an account state that must not be sent a live token.

NetworkError

on the 429: the daily resend limit is reached.

resendVerification()

`POST /api/v1/auth/resend-verification` (CONTRACT.md §25.1) — the **unauthenticated** resend, for a caller with no session.

public resendVerification(string $email, string $tenantId) : void

Returns normally whatever the outcome. The address may not exist, may already be verified, or may be over the daily limit, and the server answers identically in every case because it takes an address from an anonymous caller: anything else is an oracle for which addresses have accounts (§25.4).

A caller that is signed in wants self::resendOwnVerification(), which says what happened. §25.7 rule 2 forbids routing either of these to the other, and this SDK does not.

Parameters
$email : string
$tenantId : string

resolvedOrgId()

The organization UUID §27 routes substitute for `{org_id}`, or `null` when the client was constructed without one (§27.4 rule 3).

public resolvedOrgId() : string|null

Public because §27 has routes where {org_id} names the entity being ADMINISTERED rather than the calling context — the signing CAs under caCertificates — and those take it as an ordinary argument. Without this accessor a caller had no way to pass the same identifier the implicit routes use.

Return values
string|null

resolvedTenantId()

The tenant UUID §27 routes substitute for `{tenant_id}`, or `null` when the client was constructed without one (§27.4 rule 3).

public resolvedTenantId() : string|null

This is the UUID, never the $tenant SLUG the client is constructed with: §5's X-Tenant-ID header takes the slug, but a {tenant_id} path segment takes the UUID, and the two are not interchangeable.

Return values
string|null

revoke()

`POST /oauth2/revoke` (RFC 7009, CONTRACT.md §12.1) — revoke an access or refresh token. Returns nothing. Per RFC 7009 the server answers `200` for an unknown, expired, or already-revoked token too, so this call is **idempotent**: only a `401` (client authentication failed) is an error, surfaced as {@see \Axiam\Sdk\Core\OAuthProtocolError} (§12.1 note 5). A `5xx` still raises {@see NetworkError}.

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

The token to revoke — accepts the wrapped or bare form.

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

when no oidcClientSecret was configured.

saml()

A tenant's SAML 2.0 identity provider (CONTRACT §29): service providers, SP-metadata import, and the IdP signing-credential lifecycle.

public saml() : SamlApi
Return values
SamlApi

scimTargets()

A tenant's outbound SCIM targets (CONTRACT §31): the downstream SCIM 2.0 service providers AXIAM pushes users and groups to.

public scimTargets() : ScimTargetsApi
Return values
ScimTargetsApi

ssf()

A tenant's Shared Signals Framework streams (CONTRACT §32): which receiver gets which CAEP and RISC security events.

public ssf() : SsfApi
Return values
SsfApi

ssfReceiver()

An SSF receiver — the helper a relying party uses to verify the Security Event Tokens AXIAM pushes to it and to poll a poll stream (CONTRACT.md §32.7). See {@see \Axiam\Sdk\Ssf\SsfReceiver::verifySet()} and {@see \Axiam\Sdk\Ssf\SsfReceiver::poll()}.

public ssfReceiver(string $issuer, string $audience[, string|null $jwksUri = null ][, string|null $discoveryUrl = null ][, callable(): Array|null $accessTokenProvider = null ][, int $replayWindowSeconds = SsfReceiver::MIN_REPLAY_WINDOW_SECONDS ][, ReplayStore|null $replayStore = null ]) : SsfReceiver

The receiver fetches the JWKS and polls over this client's session-free transport: this client's §6 TLS policy and §6.1 identity, but no cookies, no session token and no redirects. poll() calls {base URL}/ssf/v1/poll/{stream_id}.

Parameters
$issuer : string

The transmitter's issuer — the tenant's issuer, e.g. https://iam.example/t/{tenant_id} — compared with iss exactly.

$audience : string

This receiver's audience: the stream's audience.

$jwksUri : string|null = null

The JWKS (AXIAM: {issuer}/oauth2/jwks); exactly one of this and $discoveryUrl.

$discoveryUrl : string|null = null

The SSF configuration document; its jwks_uri is used and its issuer must be $issuer.

$accessTokenProvider : callable(): Array|null = null

The bearer poll() presents: a client-credentials token carrying ssf.manage. null for push only.

$replayWindowSeconds : int = SsfReceiver::MIN_REPLAY_WINDOW_SECONDS

At least, and by default, seven days.

$replayStore : ReplayStore|null = null

Shared store for several processes; in-memory when omitted.

Tags
throws
ValidationError

locally, for a replay window under seven days, an empty issuer or audience, or not exactly one key source.

Return values
SsfReceiver

ssoComplete()

`POST /api/v1/auth/federation/oidc/callback` (CONTRACT.md §12.1) — step 2 of upstream SSO: consumes the single-use `$state`, provisions or links the user, and establishes the session. The session arrives as `Set-Cookie` — not in the response body (§12.1 note 6) — so it is captured automatically via this client's shared §4 cookie jar; the freshly-issued §3 CSRF token is captured too, exactly as {@see self::login()} does. §12.4 does not apply here: no ID token ever reaches the SDK on the federation path.

public ssoComplete(string $state, string $code) : SsoCompleteResult
Parameters
$state : string
$code : string
Return values
SsoCompleteResult

ssoCompleteHandoff()

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

public ssoCompleteHandoff(string $code) : SsoCompleteResult

Valid 60 s 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 (§12.1 note 12).

Parameters
$code : string
Return values
SsoCompleteResult

ssoCompleteOauth2()

`POST /api/v1/auth/federation/oauth2/callback` (CONTRACT.md §12.1, contract 1.38) — step 2 of a plain-OAuth2 login. Same `Set-Cookie` session and same §3 CSRF capture as {@see self::ssoComplete()}. §12.4 does not apply: an `OAuth2` provider issues no ID token at all (§12.1 note 11).

public ssoCompleteOauth2(string $state, string $code) : SsoCompleteResult
Parameters
$state : string
$code : string
Return values
SsoCompleteResult

ssoProviders()

`GET /api/v1/auth/federation/providers` (CONTRACT.md §12.1, contract 1.38) — which "Sign in with X" buttons to render for a workspace. The identifiers travel as **query** parameters; this is a `GET` and sends no body.

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

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 array (§12.1 note 9), precisely so the endpoint cannot be used to enumerate organization or tenant slugs. 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` (CONTRACT.md §12.1) — step 1 of first-time SSO against an **upstream** IdP. No JWT required. One tenant form (`$tenantId`/`$tenantSlug`) and one org form (`$orgId`/`$orgSlug`) must be resolvable, from the arguments or from this client's construction options (§5.1) — an unresolvable one raises {@see AuthError} client-side, with no wire call. Redirect the browser to the returned `authorizeUrl` and round-trip `state` back into {@see self::ssoComplete()} unmodified; the server keeps the nonce to itself (§12.1 note 7).

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

ssoStartOauth2()

`POST /api/v1/auth/federation/oauth2/start` (CONTRACT.md §12.1, contract 1.38) — step 1 of a login through a **plain-OAuth2** upstream (GitHub, Facebook, `generic_oauth2`). Call this, rather than {@see self::ssoStart()}, exactly when the provider's `protocol` is `OAuth2` (§12.1 note 10).

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

PKCE is mandatory on this path and is generated and held server-side (§12.1 note 11). A 400 can mean the $redirectUri is not on an origin the deployment accepts (§12.1 rule 12a) and surfaces as NetworkError; 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
Return values
SsoStartResult

tenants()

Tenants within an organization -- the isolation boundary every other namespace is scoped to.

public tenants() : TenantsApi
Return values
TenantsApi

tokenExchange()

`POST /oauth2/token` with the RFC 8693 grant (CONTRACT.md §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

Requires confidential-client credentials. Never defaults $actorToken, never auto-narrows after invalid_scope, never adopts the result.

An $actorToken (delegation) must have been issued to this same client (§15.2 rule 9, contract 1.60): obtain it from the client's own client_credentials grant, for example $client->loginClientCredentials()->accessToken kept apart from the client's session (adoptAsCredential left false), and pass it. Any other actor token is answered 400 invalid_request (actor_token was not issued to the exchanging client), which surfaces unchanged: no retry, no rewrite into an impersonation.

Parameters
$subjectToken : Sensitive|string
$subjectTokenType : string

What kind of token $subjectToken is. Required (§15.1): OidcClient::ACCESS_TOKEN_TYPE for an AXIAM access token, or OidcClient::JWT_TOKEN_TYPE for a trusted external issuer's JWT (§15.7). Never inferred from the token.

$actorToken : Sensitive|string|null = null
$scopes : array<int, string>|null = null
$audience : string|null = null
$resource : string|null = null
$tenantId : string|null = null
$configuration : OidcConfiguration|null = null
Tags
throws
AuthError

when no oidcClientSecret was configured.

Return values
ExchangedToken

umaChallengeHeader()

Format a `WWW-Authenticate: UMA` header (CONTRACT.md §20.3, emit half).

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}` (CONTRACT.md §20.1).

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

umaExchangeTicket()

The UMA ticket grant (CONTRACT.md §20.1) — redeem a permission ticket for an RPT.

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

Never retried (§20.2 rule 6, the one documented exception to §16): a ticket is spent whether or not the exchange succeeded, so a retry is a second redemption. A failure surfaces; request a new ticket. The result is never adopted as this client's credentials and carries no refresh token.

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

when no oidcClientSecret was configured.

Return values
RequestingPartyToken

umaListResources()

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

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

umaParseChallenge()

Parse a `WWW-Authenticate: UMA …` header (CONTRACT.md §20.3) — local computation, and deliberately **no** exchange of the ticket it finds.

public umaParseChallenge(string $header) : UmaChallenge|null
Parameters
$header : string
Return values
UmaChallenge|null

umaRegisterResource()

`POST /uma2/rreg/resource_set` (CONTRACT.md §20.1) — register a UMA 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, usable directly as a RequestedPermission's $resourceId.

Parameters
$pat : Sensitive|string

A client-credentials token carrying uma_protection (§20.2 rule 1) — never this client's session token.

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

umaUpdateResource()

`PUT /uma2/rreg/resource_set/{id}` (CONTRACT.md §20.1) — replace a resource set.

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

$resourceScopes replaces the declared list rather than merging with it (§20.2 rule 8), so omitting a scope removes it.

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

updateClientRegistration()

`PUT registration_client_uri` (RFC 7592 §2.2, CONTRACT.md §28.12) — **replace** this client's registration, and receive a **rotated** registration access token.

public updateClientRegistration(string $registrationClientUri, Sensitive $registrationAccessToken, ClientRegistration $metadata) : ClientRegistration

$metadata is the whole registration: a member it omits is a member the server deletes. Start from self::readClientRegistration()'s result, which carries every member (jwks / jwks_uri and any member this SDK does not model included), and change what you mean to change. The SDK sets client_id to $metadata->clientId and never sends registration_access_token, registration_client_uri, client_secret_expires_at, client_id_issued_at or client_secret.

Persist the returned registrationAccessToken before doing anything else. From the moment the server answers it is the only valid token: the one you presented is dead for every operation.

Never retried — not on a transport error, not on a 5xx. An update that reached the server and lost its response has already rotated the token, and repeating it with the old one is a 401 that locks you out of your own registration. On a lost answer, read the registration with the token you hold: a 401 means the update landed.

Parameters
$registrationClientUri : string
$registrationAccessToken : Sensitive
$metadata : ClientRegistration
Tags
throws
ValidationError

for a URI at another origin (local).

OAuthProtocolError

for invalid_client_metadata, invalid_redirect_uri, invalid_request or invalid_token; a refusal does not rotate the token.

Return values
ClientRegistration

users()

Users within the client's tenant, and the administrative side of their second factor and lockout state.

public users() : UsersApi
Return values
UsersApi

validateToken()

`validateToken` — `axiam.v1.TokenService/ValidateToken` (CONTRACT.md §1.1.1/§10.3, contract 1.51). Signature + expiry, plus the confirmation (`cnf`) a resource server validating over gRPC needs in order not to accept a sender-constrained token as a bearer token (§10.1 rule 9).

public validateToken(Sensitive|string $inspectedAccessToken) : TokenValidation

$inspectedAccessToken is the token being asked about — a DIFFERENT credential from this client's OWN session token, which authenticates the RPC itself. The two are never confused: there is no default that falls one back to the other (§1.1.1 rule 1). $inspectedAccessToken is secret material and SHOULD be passed as Sensitive.

$result->valid is NOT permission to proceed — read $result->status(), or call $result->verifyPossession($proofs) with the proofs YOUR connection established. Requires a prior successful self::login() on THIS client (the caller's own credential) — with none, raises AuthError before any wire call — and, being gRPC-only, requires the grpc PECL extension plus a configured grpcTarget; there is NO REST substitution (§1.1.1 rule 7; POST /oauth2/introspect is a different operation, RFC 7662 §2.1 client authentication, not this one). A gRPC UNAUTHENTICATED on the caller's own credential drives the shared single-flight refresh (§9) and retries once.

Parameters
$inspectedAccessToken : Sensitive|string
Return values
TokenValidation

verifyEmail()

`POST /api/v1/auth/verify-email` (CONTRACT.md §25.1).

public verifyEmail(Sensitive|string $token, string $tenantId) : void

Unauthenticated: a user whose address is unverified may have no session at all. $tenantId is a body field here — this is not an /oauth2 endpoint, so §12.1 rule 2's query-parameter convention does not reach it.

Parameters
$token : Sensitive|string
$tenantId : string

verifyLocally()

Verify an INBOUND caller's token and nothing else — the seam every request guard must use (CONTRACT.md §10.1 rule 8). Delegates straight to {@see JwksVerifier::verify()}, which applies the full §10.1 minimum local-verification set INCLUDING rule 9 (contract 1.51), and returns `null` on any failure with **no fallback to another credential**.

public verifyLocally(string $token, string $tenant) : array<string, mixed>|null

Rule 9 (contract 1.51): this method has no transport to ask for a peer certificate or a verified DPoP proof, so a token carrying cnf is REFUSED here unconditionally — accepting one without evidence would be exactly the bound-to-bearer downgrade rule 9 exists to prevent. Before this, a certificate- or DPoP-bound token reached this method with its cnf claim intact and unchecked, so a device token lifted off a device (or a DPoP-bound token replayed without its proof) was admitted here as an ordinary bearer credential — the same defect found independently in the Rust, TypeScript, Go, Python and C# ports. A guard that DOES have evidence to offer (an mTLS-terminating listener, a verified DPoP proof) calls self::verifyWithProofs() instead.

This is deliberately the only verification entry point offered to the framework bridges. Its sibling self::verifyLocallyOrFallback() substitutes this client's own session when verification fails, which is correct for the SDK's outbound calls and an authentication bypass in a request guard — see SEC-085.

Parameters
$token : string
$tenant : string
Return values
array<string, mixed>|null —

Verified claims of the SUPPLIED token, or null.

verifyLocallyOrFallback()

Local-first JWT verification with a reactive-refresh fallback (D-02) — for the SDK's own OUTBOUND calls, where the token being verified is this client's own and refreshing it is the intended recovery. Tries {@see JwksVerifier::verify()} first (no network call on the happy path); if that fails (expired/unknown-kid/invalid token), attempts the shared single-flight refresh (§9, D-06) and re-verifies the FRESH access token. Returns `null` — never unverified claims — on any failure.

public verifyLocallyOrFallback(string $token, string $tenant) : array<string, mixed>|null

⚠ Never call this from a request guard. The fallback re-verifies a different credential — this client's own session, typically a service account — so a caller presenting an expired, foreign-tenant or forged token would be admitted under the application's own identity (SEC-085). Request guards must call self::verifyLocally(), which decides on the caller's credential alone.

Parameters
$token : string
$tenant : string
Return values
array<string, mixed>|null —

Verified claims, or null.

verifyLogoutToken()

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

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

Returns 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.

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

on any failed check.

Return values
VerifiedLogoutToken

verifyMfa()

`POST /api/v1/auth/mfa/verify` (CONTRACT.md §1) — completes the two-phase flow started by {@see self::login()} when {@see LoginResult::$mfaRequired} was `true`. `$challengeToken` is the `Sensitive`-wrapped value from that `LoginResult` (D-11: never a raw string on the public surface).

public verifyMfa(Sensitive $challengeToken, string $totpCode) : LoginResult
Parameters
$challengeToken : Sensitive
$totpCode : string
Return values
LoginResult

verifyWithProofs()

{@see self::verifyLocally()}, applying CONTRACT.md §10.1 rule 9 against `$proofs` — evidence YOUR connection established for THIS request, never a value taken from a caller-settable request header (§10.1 rule 9 detail 2: e.g.

public verifyWithProofs(string $token, string $tenant, PresentedProofs $proofs) : array<string, mixed>|null

$_SERVER['SSL_CLIENT_CERT']/the PSR-7 server params as set by the TLS-terminating web server, or an already-verified DPoP proof's jkt). An unbound token is accepted with or without proofs, exactly as self::verifyLocally().

The request guard to use when the deployment DOES have connection evidence to offer — an mTLS-terminating listener fronting a resource server that must accept device tokens, for instance. self::verifyLocally() stays the entry point for everything else, and always evaluates as though no evidence were available.

Parameters
$token : string
$tenant : string
$proofs : PresentedProofs
Return values
array<string, mixed>|null —

Verified claims, or null — including when a bound token's cnf is not satisfied by $proofs.

webauthnAuthenticateFinish()

`POST /api/v1/auth/webauthn/authenticate/finish` (CONTRACT.md §24.1).

public webauthnAuthenticateFinish(Sensitive|string $stateToken, string $response) : WebauthnLoginResult

On success the client is signed in: the server sets the same cookie triple POST /api/v1/auth/login sets, and the §17 decision memo is cleared because the subject changed (§24.3).

Parameters
$stateToken : Sensitive|string
$response : string
Return values
WebauthnLoginResult

webauthnAuthenticateStart()

`POST /api/v1/auth/webauthn/authenticate/start` (CONTRACT.md §24.1) — begin the **second-factor** ceremony.

public webauthnAuthenticateStart(Sensitive|string $challengeToken) : WebauthnChallenge

Continues a self::login() that answered mfaRequired with "webauthn" among its available methods; $challengeToken is that login's token. A different flow from self::webauthnDiscoverableStart(), not the same one with a flag (§24.2) — which is why the token is required here and absent there.

Parameters
$challengeToken : Sensitive|string
Return values
WebauthnChallenge

webauthnDiscoverableFinish()

`POST /api/v1/auth/webauthn/authenticate/discoverable/finish` (CONTRACT.md §24.1).

public webauthnDiscoverableFinish(Sensitive|string $stateToken, string $response) : WebauthnLoginResult

Adopts credentials exactly as self::webauthnAuthenticateFinish() does.

Parameters
$stateToken : Sensitive|string
$response : string
Return values
WebauthnLoginResult

webauthnDiscoverableStart()

`POST /api/v1/auth/webauthn/authenticate/discoverable/start` (CONTRACT.md §24.1) — begin the usernameless ceremony.

public webauthnDiscoverableStart([WebauthnWorkspace|null $workspace = null ]) : WebauthnChallenge

A primary factor: nothing precedes it, allowCredentials comes back empty, and the assertion itself identifies the user. Pass null for $workspace to have it filled from this client's own configured identity.

Unlike authenticate/finish, discoverable/finish fires the login.post_auth reactor hook (§22.5) — the former continues a login already gated at its password step, and this one has no such step.

Parameters
$workspace : WebauthnWorkspace|null = null
Return values
WebauthnChallenge

webauthnRegisterFinish()

`POST /api/v1/auth/webauthn/register/finish` (CONTRACT.md §24.1) — hand the authenticator's answer back and store the credential.

public webauthnRegisterFinish(Sensitive|string $stateToken, string $credentialName, string $response) : WebauthnCredential

$response is the platform's own response JSON, verbatim (§24.6a rule 2): credential.toJSON() from a browser, or registrationResponseJson from Android's Credential Manager relayed by a mobile client. It reaches the wire byte for byte, because re-encoding a signed buffer is three chances to corrupt it in service of nothing.

Parameters
$stateToken : Sensitive|string
$credentialName : string
$response : string
Return values
WebauthnCredential

webauthnRegisterStart()

`POST /api/v1/auth/webauthn/register/start` (CONTRACT.md §24.1) — begin enrolling a passkey for the signed-in user.

public webauthnRegisterStart() : WebauthnChallenge

Requires a session, and refuses client-side with no wire call when there is none — the shape §1.1 rule 3 requires of getUserInfo.

The returned options are the server's, untouched (§24.0). A 503 here means the tenant's attestation policy needs FIDO metadata the server cannot reach: a configuration state, not a transient one, and §24.4 rule 2 deliberately does not retry it.

Return values
WebauthnChallenge

webauthnSetupRegisterFinish()

`POST /api/v1/auth/webauthn/setup/register/finish` (CONTRACT.md §24.1, contract 1.45) — finish enrolling the passkey or security key {@see self::webauthnSetupRegisterStart()} offered, and with it the login that was interrupted.

public webauthnSetupRegisterFinish(Sensitive|string $setupToken, Sensitive|string $stateToken, string $credentialName, string $response) : LoginResult

Adopts credentials exactly as self::mfaSetupConfirm() does (CONTRACT.md §25.2 rule 2): both mfa_setup_confirm and webauthn_setup_register_finish are the completion of a login, both answer LoginSuccessResponse, and §24.3's five adoption rules apply to both — including capturing the session's first CSRF token and clearing the §17 decision memo. A caller who chose a passkey over TOTP must end up in the exact same signed-in state as one who did not.

$response is the platform's own response JSON, verbatim (§24.6a rule 2), exactly as self::webauthnRegisterFinish() treats it.

Like self::webauthnSetupRegisterStart(), this takes no session and MUST NOT be given one — the setup token is the only credential this pair accepts.

Parameters
$setupToken : Sensitive|string
$stateToken : Sensitive|string
$credentialName : string
$response : string
Return values
LoginResult

webauthnSetupRegisterStart()

`POST /api/v1/auth/webauthn/setup/register/start` (CONTRACT.md §24.1, contract 1.45) — begin enrolling a passkey or security key as the FIRST factor of a forced login enrolment.

public webauthnSetupRegisterStart(Sensitive|string $setupToken) : WebauthnChallenge

Reached exactly where self::mfaSetupEnroll() is: login() returned LoginResult::$mfaSetupRequired, and $setupToken is that result's $setupToken. There is no session — the setup token is the credential, and unlike self::webauthnRegisterStart() this call takes no session and MUST NOT be given one: an SDK that attached the caller's own session here would hand the server a second credential §24.1 does not ask for and does not want.

A 503 here means the tenant's attestation policy needs FIDO metadata the server cannot reach — a configuration state, not a transient one, and (mirroring self::webauthnRegisterStart()) this is never retried.

Parameters
$setupToken : Sensitive|string
Return values
WebauthnChallenge

webhooks()

Outbound event notifications. Delivery signatures are verified with the §13 helper, which this namespace configures.

public webhooks() : WebhooksApi
Return values
WebhooksApi
On this page

Search results