Skip to Content
API referenceNative authentication

Native authentication API

The native adapter is available only to clients registered with native authentication enabled. It begins with the same Authorization Code with PKCE request as a browser flow and returns a one-time authorization code. Exchange that code at /oauth2/token with the original PKCE verifier.

How the native flow fits together

The interaction is the root of every native operation. It preserves the OAuth client, exact redirect URI, scopes, resource, state, nonce and PKCE challenge while the app proves a user identity.

Registration and password recovery are credential-management branches attached to an interaction. Their grants cannot call APIs and cannot be exchanged for OAuth tokens. Only a successful authentication proof returns an authorization code.

All request bodies on this page use application/x-www-form-urlencoded. All paths are relative to the tenant issuer.

Security boundary

Call these routes from native networking code. A request carrying Origin, Cookie or Sec-Fetch-Site is rejected with 403 browser_request_forbidden so a website cannot use this surface as a cross-origin credential API.

1. Start an interaction

Send POST /oauth2/native/interactions with the authorization parameters.

ISSUER='https://auth.example.com'
curl --fail --silent --show-error \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'client_id=my-native-client' \
  --data-urlencode 'redirect_uri=com.example.app:/oauth/callback' \
  --data-urlencode 'response_type=code' \
  --data-urlencode 'scope=openid email offline_access' \
  --data-urlencode 'state=OPAQUE_STATE' \
  --data-urlencode 'code_challenge=PKCE_CODE_CHALLENGE' \
  --data-urlencode 'code_challenge_method=S256' \
  --data-urlencode 'nonce=OPAQUE_NONCE' \
  "$ISSUER/oauth2/native/interactions"

Response — 201 Created

{ "interactionId": "4H1nKJ2wzY8tqU7sN5x9bR0cP3mL6aDfE2gV1iQoXkM" }

The interaction is bound to the tenant, client, callback, requested scopes, resource, state and PKCE challenge. Pass interactionId as interaction_id to exactly one proof flow.

2. Prove the user identity

RouteRequired fields
POST /oauth2/native/passwordinteraction_id, email, password
POST /oauth2/native/socialinteraction_id, provider, id_token, nonce, audience
POST /oauth2/native/email/requestinteraction_id, email
POST /oauth2/native/email/verifyinteraction_id, email, code
POST /oauth2/native/mfainteraction_id, challenge_token, method, code

Authorization code — 200 OK

Password, social, email-code or MFA proof completes with:

{ "code": "nac_2PM8c2sYV7fR3D...", "state": "af0ifjsldkj", "issuer": "https://auth.example.com", "redirectUri": "com.example.app:/oauth/callback", "replacementRecoveryCode": null }
FieldMeaning
codeone-time authorization code to exchange at /oauth2/token
stateoriginal request value; compare it with the value stored by the app
issuertenant issuer that produced the result
redirectUriregistered callback bound to the code
replacementRecoveryCodenew recovery code when a recovery code was consumed; otherwise null

Store a non-null replacementRecoveryCode before discarding the response.

MFA challenge — 200 OK

A correct primary proof can require a second factor:

{ "status": "mfa_required", "challengeToken": "mf_9vTY2rE7aB..." }

Call POST /oauth2/native/mfa with that token. method is totp for an authenticator code or recovery for a recovery code. A challenge response is a successful intermediate state, not an error.

Email code request — 202 Accepted

{ "retryAfterSeconds": 60, "expiresInSeconds": 600 }

Wait at least retryAfterSeconds before requesting another code. Submit the six-digit code before expiresInSeconds elapses. The response is deliberately identical when no eligible account exists.

Registration

Registration remains bound to the original interaction.

The request step creates only a ten-minute email challenge and returns the same timer response when the address is already registered. This prevents the endpoint from becoming an account-discovery API. The verify step exchanges a correct six-digit code for a one-use, five-minute registrationGrant. No account exists before that mailbox proof succeeds.

Request a code

POST /oauth2/native/registration/request requires interaction_id and email.

Response — 202 Accepted

{ "retryAfterSeconds": 60, "expiresInSeconds": 600 }

Verify the code

POST /oauth2/native/registration/verify requires interaction_id, email and code.

Response — 200 OK

{ "registrationGrant": "rg_L1Kz4u9Jv...", "expiresInSeconds": 300 }

An incorrect or expired code returns 401 invalid_code.

Create the account

POST /oauth2/native/registration/complete requires interaction_id, registration_grant and password.

A successful request returns 201 Created with an empty body. 401 invalid_grant means the grant is invalid, expired or already consumed. 400 invalid_password means the password does not satisfy the tenant policy.

Completion consumes the grant and creates an active subject whose email is verified. It does not authenticate the user. Start a fresh interaction and use password sign-in to obtain an authorization code.

Use the native registration tutorial for the complete state transitions, retry behavior and runnable requests.

Password recovery

Recovery also remains bound to the original interaction.

Request a code

POST /oauth2/native/password/recovery/request requires interaction_id and email.

Response — 202 Accepted

{ "retryAfterSeconds": 60, "expiresInSeconds": 600 }

The response does not disclose whether the email belongs to an eligible account.

Verify the code

POST /oauth2/native/password/recovery/verify requires interaction_id, email and code.

Response — 200 OK

{ "resetGrant": "pg_h8fQ2WmK6...", "expiresInSeconds": 300 }

An incorrect or expired code returns 401 invalid_code.

Replace the password

POST /oauth2/native/password/recovery/complete requires interaction_id, reset_grant and new_password.

A successful request returns 204 No Content. 401 invalid_grant means the grant is invalid, expired or already consumed. 400 invalid_new_password means the replacement does not satisfy policy or cannot replace the current password.

Errors

Every JSON error has the form:

{ "error": "invalid_interaction" }
StatusErrorMeaning and client action
400invalid_requestfix the content type, duplicated fields, missing values or malformed values
400invalid_interactiondiscard local flow state and start a new interaction
400invalid_clientuse a client registered for this tenant
400invalid_scoperequest only scopes registered for the client and resource
400invalid_targetuse a resource URI available to the client
401invalid_credentialsthe password or upstream identity proof was rejected
401invalid_codethe email verification code is invalid or expired
401invalid_grantthe registration or reset grant is invalid, expired or consumed
403browser_request_forbiddenremove browser-only headers and use native networking code
403native_authentication_disabledthis OAuth client is not enabled for native authentication
403provider_unavailablethe requested social provider is not configured for the tenant
403registration_closedself-service registration is disabled for the tenant
409browser_requiredcomplete this authorization in the browser
409account_link_requireduse the account-linking browser flow before retrying social sign-in
409email_verification_requiredverify the upstream email before it can identify an account
409registration_conflictanother flow created the account; restart sign-in
429too_many_attemptswait for the Retry-After delay before retrying
503temporarily_unavailabledelivery or a required authentication dependency is unavailable
404unknown_tenantthe request issuer does not identify an active tenant