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.
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
| Route | Required fields |
|---|---|
POST /oauth2/native/password | interaction_id, email, password |
POST /oauth2/native/social | interaction_id, provider, id_token, nonce, audience |
POST /oauth2/native/email/request | interaction_id, email |
POST /oauth2/native/email/verify | interaction_id, email, code |
POST /oauth2/native/mfa | interaction_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
}| Field | Meaning |
|---|---|
code | one-time authorization code to exchange at /oauth2/token |
state | original request value; compare it with the value stored by the app |
issuer | tenant issuer that produced the result |
redirectUri | registered callback bound to the code |
replacementRecoveryCode | new 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"
}| Status | Error | Meaning and client action |
|---|---|---|
400 | invalid_request | fix the content type, duplicated fields, missing values or malformed values |
400 | invalid_interaction | discard local flow state and start a new interaction |
400 | invalid_client | use a client registered for this tenant |
400 | invalid_scope | request only scopes registered for the client and resource |
400 | invalid_target | use a resource URI available to the client |
401 | invalid_credentials | the password or upstream identity proof was rejected |
401 | invalid_code | the email verification code is invalid or expired |
401 | invalid_grant | the registration or reset grant is invalid, expired or consumed |
403 | browser_request_forbidden | remove browser-only headers and use native networking code |
403 | native_authentication_disabled | this OAuth client is not enabled for native authentication |
403 | provider_unavailable | the requested social provider is not configured for the tenant |
403 | registration_closed | self-service registration is disabled for the tenant |
409 | browser_required | complete this authorization in the browser |
409 | account_link_required | use the account-linking browser flow before retrying social sign-in |
409 | email_verification_required | verify the upstream email before it can identify an account |
409 | registration_conflict | another flow created the account; restart sign-in |
429 | too_many_attempts | wait for the Retry-After delay before retrying |
503 | temporarily_unavailable | delivery or a required authentication dependency is unavailable |
404 | unknown_tenant | the request issuer does not identify an active tenant |