Register a user from a native application
This tutorial creates an active, email-verified Identity account with a password. Registration does not return OAuth tokens and does not create a login session. After account creation, start a new authentication interaction to sign the user in.
Use this API when the native product must render registration screens itself. If hosted Identity screens are acceptable, AppAuth hosted login is the shorter integration: registration, verification, MFA and consent stay in the system browser and the app handles only the OAuth callback.
What the three registration steps prove
Registration separates three pieces of authority:
- The interaction binds the operation to one tenant, native client, redirect URI, requested scopes, resource, state and PKCE challenge.
- The email code proves temporary control of the mailbox.
- The registration grant authorizes one account creation for five minutes. It is not an access token and cannot call an API.
Before you start
You need:
- a tenant whose registration mode is
SELF_SERVICE; - working transactional email delivery;
- a public
NATIVEclient with native authentication enabled; - an exact native redirect URI registered on that client;
- a fresh PKCE verifier, S256 challenge,
stateandnonce.
Use openid email as the basic OIDC scope. Add offline_access only when the app can protect and rotate a refresh token in platform secure storage.
1. Create the authorization interaction
The registration must be attached to the authorization request the app intends to complete.
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"Expected response
{
"interactionId": "4H1nKJ2wzY8tqU7sN5x9bR0cP3mL6aDfE2gV1iQoXkM"
}Keep the interaction ID with the original verifier, state and nonce. If the app loses any of them, discard the flow and start again.
2. Request the email code
Submit the interaction ID and the email entered by the user:
ISSUER='https://auth.example.com'
INTERACTION_ID='4H1nKJ2wzY8tqU7sN5x9bR0cP3mL6aDfE2gV1iQoXkM'
curl --fail-with-body --silent --show-error \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Accept-Language: en' \
--data-urlencode "interaction_id=$INTERACTION_ID" \
--data-urlencode 'email=ada@example.com' \
"$ISSUER/oauth2/native/registration/request"Expected response — 202 Accepted
{
"retryAfterSeconds": 60,
"expiresInSeconds": 600
}Disable the resend action for retryAfterSeconds. The code is valid for expiresInSeconds.
The request response deliberately does not reveal whether the address is already registered. An existing address receives the same shape and timing policy, but no new registration challenge is created. If the user cannot complete registration, offer sign-in and password recovery without claiming that the account exists.
3. Verify the six-digit code
curl --fail-with-body --silent --show-error \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "interaction_id=$INTERACTION_ID" \
--data-urlencode 'email=ada@example.com' \
--data-urlencode 'code=482193' \
"$ISSUER/oauth2/native/registration/verify"Expected response — 200 OK
{
"registrationGrant": "rg_L1Kz4u9Jv...",
"expiresInSeconds": 300
}Keep the grant only in memory. It expires after five minutes and is useful only with the same tenant registration flow.
An incorrect, expired or exhausted code returns 401 invalid_code. Let the user retry the code while the challenge is live. After repeated failures or expiry, return to the request step.
The email code is not accepted by the completion endpoint. Only the short-lived registration grant crosses from mailbox verification into account creation.
4. Create the account
Collect the password only after email verification, then submit it with the grant:
REGISTRATION_GRANT='rg_L1Kz4u9Jv...'
curl --fail-with-body --silent --show-error \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "interaction_id=$INTERACTION_ID" \
--data-urlencode "registration_grant=$REGISTRATION_GRANT" \
--data-urlencode 'password=correct horse battery staple' \
"$ISSUER/oauth2/native/registration/complete"Success is 201 Created with an empty body. At this point Identity has created an active subject, marked its email verified and stored the password credential. The registration grant is consumed.
Handle completion failures as state transitions:
| Status and error | What happened | Next action |
|---|---|---|
400 invalid_password | the password does not satisfy policy | keep the live grant and ask for another password |
401 invalid_grant | the grant is malformed, expired, consumed or belongs to another flow | discard it and restart registration |
409 registration_conflict | another flow created the account first | discard registration state and offer sign-in |
403 registration_closed | tenant self-service registration is disabled | stop and use the product’s controlled invitation path |
429 too_many_attempts | the source exceeded the attempt limit | honor Retry-After before another attempt |
503 temporarily_unavailable | email delivery or required registration state is unavailable | preserve only non-sensitive UI input and retry later |
Do not retry account creation after a network timeout with a new flow immediately. First attempt sign-in: the original request may have succeeded even if the response was lost.
5. Sign in the new account
Account creation deliberately stops before authentication. Clear the registration grant and code, generate new state, nonce and PKCE material, then create a new interaction. Submit the user’s email and password to POST /oauth2/native/password.
Follow native sign-in with MFA from that point. The result is either an MFA challenge or an authorization code. Exchange the code at /oauth2/token.
Verify the finished use case
The implementation is complete when all of these checks pass:
- requesting a code returns
202for both new and already registered addresses; - a wrong code never produces a registration grant;
- a valid code produces a five-minute grant;
- a weak password leaves the grant usable for another compliant password attempt;
- a successful completion returns
201and reusing its grant returns401 invalid_grant; - the newly created account can authenticate in a new interaction;
- the callback state and issuer match the values stored before registration;
- product data is bound only after token validation by exact
(iss, sub).
See native authentication responses and errors for the complete wire contract.