- Authentication Flow API
- State and Branching
- HTTP API
- Reference on input and output
- type: signup; step.type: identification
- type: signup; step.type: verify
- type: signup; step.type: create_authenticator
- type: signup; step.type: view_recovery_code
- type: signup; step.type: prompt_create_passkey
- type: login; step.type: identify
- type: login; step.type: authenticate
- type: login; step.type: change_password
- type: login; step.type: prompt_create_passkey
- type: signup_login; step.type: identify
- type: account_recovery; step.type: identify
- type: account_recovery; step.type: select_destination
- type: account_recovery; step.type: verify_account_recovery_code
- type: account_recovery; step.type: reset_password
Authentication Flow API is a HTTP API to create and run an authentication flow. It is the same API that powers that the default UI of Authgear. With Authentication Flow API, you can build your own UI while preserving the capability of running complicated authentication flow as the default UI does.
An authentication flow has a constant ID that never changes. When an authentication flow is created, it has one state. A state of an authentication flow is identified by its unique state token. A particular state of authentication flow reacts to an input, and produce a new state. You keep track of the latest state token and feed an input to it to obtain another state token. In doing this you move forward in the authentication flow.
In some steps in an authentication flow, you can take any one branch to continue. For example, your project may be configured to let the end-user to sign in with email address or phone number. In this case, there are two branches. Assume the current state is StateA. You pass an input to StateA to select the email address branch, you get a new state StateB with the email address branch selected. If the end-user changes their mind and taps the back button, we have to allow them to select phone number. This can be done by passing an input to StateA to select the phone number branch, resulting in a new state StateC. What if the end-user changes their mind again? All you need to do is to pass an input to StateA to select the email address branch, and get a new state StateB’. StateB and StateB’ are equal in their contents, only the state tokens are different.
As long as you associate the state token with the navigation, you can easily build multi-step UI.
- On the web where the History API is usually used to implement navigation, you can store the state ID in the
state
of a history entry. - On iOS where UIViewController usually represents a screen, you can store the state ID as a property of the view controller.
- On Android where Activity or Fragment usually represents a screen, you can store the state token as a property of the Activity or the Fragment, and implement onSaveInstanceState and onRestoreInstanceState to ensure the state token is persisted.
Authentication Flow API always returns a JSON response of the same shape.
{
"result": {
"state_token": "authflowstate_blahblahblah",
"type": "login",
"name": "default",
"action": {
"type": "authenticate",
"authentication": "primary_oob_otp_email",
"data": {}
}
}
}
state_token
: The token that refers to this particular state of an authentication flow.id
: The ID of the authentication flow. It is a constant for a particular authentication flow.type
: The type of the authentication flow. Possible values aresignup
: The flow to sign up as a new user.login
: The flow to sign in as a new user.signup_login
: This flow will either becomesignup
orlogin
depending on the input. If the end-user enters an existing login ID, then the flow will becomeslogin
, otherwise, it issignup
.
name
: The name of the authentication flow. See Create an authentication flowaction
: An object containing information about the current action.action.type
: The type of step. See Reference on input and outputaction.authentication
: The taken authentication branch.action.identification
: The taken identification branch.action.data
: An object containing action-specific data. See Reference on input and output
{
"result": {
"state_token": "authflowstate_blahblahblah",
"type": "login",
"name": "default",
"action": {
"type": "finished",
"data": {
"finish_redirect_uri": "https://myapp.authgear.cloud/..."
}
}
}
}
action.type
: When the flow has finished, the value isfinished
.action.type.data.finish_redirect_uri
: When the flow has finished, you must redirect to this URI to return the control back to Authgear.
{
"error": {
"name": "Unauthorized",
"reason": "InvalidCredentials",
"message": "invalid credentials",
"code": 401,
"info": {}
}
}
reason
: You use this string to distinguish between different errors. Do NOT usemessage
as it could change anytime.info
: An object containing extra information about the error. It can be absent (i.e. notnull
, but absent)
POST /api/v1/authentication_flows
Content-Type: application/json
{
"type": "login",
"name": "default"
}
Create an authentication flow by specifying the type
and the name
. Use the name default
to refer to the generated flow according to your project configuration. This is the same flow that the default UI runs.
POST /api/v1/authentication_flows/states/input
Content-Type: application/json
{
"state_token": "{{ STATE_TOKEN }}"
"input": {}
}
POST /api/v1/authentication_flows/states/input
Content-Type: application/json
{
"state_token": "{{ STATE_TOKEN }}"
"batch_input": [{}, {}]
}
Pass an input to a state of an authentication flow by specifying state_token
and input
. See Reference on input and output for details on input
.
Or if you want to pass multiple input at once, replace input
with batch_input
. batch_input
must be an array with at least one element.
POST /api/v1/authentication_flows/states
Content-Type: application/json
{
"state_token": "{{ state_token }}"
}
Retrieve a state by by specifying state_token
. Typically you do not need this because the state is returned after creation or after input was passed.
GET /api/v1/authentication_flows/ws?flow_id={{ FLOW_ID }}
Connection: Upgrade
Connect to the websocket by specifying flow_id
. The only message you will receive is {"kind":"refresh"}
. Upon receiving the message, you should retrieve the state again with Retrieve a state again. The step.data
should contain updated information.
When you are in this step of this flow, you will see a response like the following.
{
"result": {
"state_token": "authflowstate_5R6NM7HGGKV64538R0QEGY9RQBDM4PZD",
"type": "signup",
"name": "default",
"action": {
"type": "identify",
"data": {
"options": [
{
"identification": "email"
},
{
"identification": "phone"
},
{
"identification": "oauth",
"provider_type": "google",
"alias": "google"
},
{
"identification": "oauth",
"provider_type": "wechat",
"alias": "wechat_mobile",
"wechat_app_type": "mobile"
}
]
}
}
}
}
The presence of this means you can sign up with an email address.
{
"identification": "email"
}
The corresponding input is
{
"identification": "email",
"login_id": "johndoe@example.com"
}
The presence of this means you can sign up with a phone number.
{
"identification": "phone"
}
The corresponding input is
{
"identification": "phone",
"login_id": "+85298765432"
}
Note that the phone number MUST BE in E.164 format without any separators nor spaces.
The presence of this means you can sign up with a username.
{
"identification": "username"
}
The corresponding input is
{
"identification": "username",
"login_id": "johndoe"
}
The presence of this means you can sign up with an OAuth provider.
{
"identification": "oauth",
"provider_type": "google",
"alias": "google"
}
provider_type
: The type of the OAuth provider. Possible values aregoogle
facebook
github
linkedin
azureadv2
azureadb2c
adfs
apple
wechat
alias
: The identifier of the OAuth provider. You pass this in the input.
The corresponding input is
{
"identification": "oauth",
"alias": "google",
"redirect_uri": "<https://example.com/oauth/redirect/google>"
}
alias
: Thealias
you see in the response. You pass this to tell Authgear which OAuth provider you choose.redirect_uri
: The redirect URI after the provider has finished authenticating the end-user. This should be an URL to your website, where you must continue the authentication flow.
After passing this input, you will see a response like this
{
"result": {
"state_token": "authflowstate_PZMX4FG4N82WGSSY0Y398YH0F9BX4FPX",
"type": "signup",
"name": "default",
"action": {
"type": "identify",
"identification": "oauth",
"data": {
"alias": "google",
"oauth_provider_type": "google",
"oauth_authorization_url": "<https://google.com/oauth2>"
}
}
}
}
You must redirect the end user to oauth_authorization_url
. This is typically done by window.location.href = {{ oauth_authorization_url }}
. Before you perform redirection, you typically need to add the query parameter state
to oauth_authorization_url
, so that you can resume the authentication flow.
The OAuth provider will authenticate the end-user. There will be 2 cases:
- The OAuth provider authenticated the end-user successfully.
code
andstate
will be present in the query string. - The OAuth provider encountered an error.
error
andstate
will be present in the query string. Additionally,error_description
anderror_uri
may be present as well.
In either case, use state
to resume your authentication flow. After that pass the following input
{
"code": "{{ code }}"
}
for the successful case. Or this input
{
"error": "{{ error }}",
"error_description": "{{ error_description }}",
"error_uri": "{{ error_uri }}"
}
for the failure case. error_description
and error_uri
are optional.
When you are in this step, you MAY see a response like the following
{
"result": {
"state_token": "authflowstate_PZMX4FG4N82WGSSY0Y398YH0F9BX4FPX",
"type": "signup",
"name": "default",
"action": {
"type": "verify",
"data": {
"channels": [
"sms",
"whatsapp"
]
}
}
}
}
It is asking how to deliver the OTP. You pass the following input
{
"channel": "sms"
}
When you are in this step, you WILL see a response like the following if the otp is a code.
{
"result": {
"state_token": "authflowstate_blahblahblah",
"type": "signup",
"name": "default",
"action": {
"type": "verify",
"data": {
"channel": "email",
"otp_form": "code",
"masked_claim_value": "john******@example.com",
"code_length": 6,
"can_resend_at": "2023-09-21T00:00:00+08:00",
"can_check": false,
"failed_attempt_rate_limit_exceeded": false
}
}
}
}
If otp_form
is code
, a OTP will be sent to the end-user at masked_claim_value
.
To request a resend, pass this input
{
"resend": true
}
After the end-user has entered the code in your UI, pass this input
{
"code": "000000"
}
Or you WILL see a response like the following if the otp is a link.
{
"result": {
"state_token": "authflowstate_blahblahblah",
"type": "signup",
"name": "default",
"action": {
"type": "verify",
"data": {
"channel": "email",
"otp_form": "link",
"websocket_url": "wss://...",
"masked_claim_value": "john******@example.com",
"code_length": 32,
"can_resend_at": "2023-09-21T00:00:00+08:00",
"can_check": false,
"failed_attempt_rate_limit_exceeded": false
}
}
}
}
if otp_form
is link
, can_check
initially is false
and websocket_url
will be present in data
.
You can connect to a websocket with this URL to listen for the event of the link being approved.
The link will be sent to the end-user at masked_claim_value
. Clicking the link will open an approval page in the default UI.
When the user has approved the link, a websocket message of a JSON object {"type": "refresh"}
is sent.
Upon receiving the message, you can retrieve a state again.
The retrieved state should have can_check=true
.
Now you can pass this input to check if the link has been approved.
{
"check": true
}
Alternatively, you can have a button in the UI to send the above input per tap.
To request a resend, pass this input
{
"resend": true
}
can_resend_at
tells you the earliest time you can trigger resend without encountering rate limit error. Use this information to implement a cooldown counter in your UI.
code_length
tells you the length of the OTP. It is typically relevant when otp_form
is code
, because it gives an hint to the end-user how long the OTP is. When otp_form
is link
, the OTP is included in the link, the length is not an important information to the end-user.
When you are in this step, you will see the following response if you are setting up a primary authenticator.
{
"result": {
"state_token": "authflowstate_DVW3H3Q9YDB3BRAA15D74V1PYGX6XYJB",
"type": "signup",
"name": "default",
"action": {
"type": "create_authenticator",
"data": {
"options": [
{
"authentication": "primary_oob_otp_email",
"otp_form": "code",
"channels": [
"email"
]
},
{
"authentication": "primary_password",
"password_policy": {
"minimum_length": 8,
"alphabet_required": true,
"digit_required": true,
"history": {
"enabled": false
}
}
}
]
}
}
}
}
Or this response if you are setting up 2FA.
{
"result": {
"state_token": "authflowstate_blahblahblah",
"type": "signup",
"name": "default",
"action": {
"type": "create_authenticator",
"data": {
"options": [
{
"authentication": "secondary_totp"
},
{
"authentication": "secondary_password",
"password_policy": {
"minimum_length": 8,
"alphabet_required": true,
"digit_required": true
}
},
{
"authentication": "secondary_oob_otp_email"
},
{
"authentication": "secondary_oob_otp_sms"
}
]
}
}
}
}
The presence of this means you can create a primary password.
{
"authentication": "primary_password",
"password_policy": {
"minimum_length": 8
}
}
password_policy
tells you the requirements on the password. Here is the full version of it
{
"minimum_length": 8,
"uppercase_required": true,
"lowercase_required": true,
"alphabet_required": true,
"digit_required": true,
"symbol_required": true,
"minimum_zxcvbn_score": 4
}
Any of the properties can be absent. If a property is absent, then the requirement indicated by the property DOES NOT apply.
minimum_length
: The minimum length of the password.uppercase_required
: The password must contain at least one uppercase character.lowercase_required
: The password must contain at least one lowercase character.alphabet_required
: The password must contain at least one uppercase or lowercase character.digit_required
: The password must contain at least one digit.symbol_required
: The password must contain at least one non-alphanumeric character.minimum_zxcvbn_score
: The minimum zxcvbn score. Possible values are 0,1,2,3,4.
The corresponding input is
{
"authentication": "primary_password",
"new_password": "some.very.secure.password"
}
The presence of this means you can create a primary Out-of-band (OOB) One-time-password (OTP) authenticator using an email address.
{
"authentication": "primary_oob_otp_email"
}
The corresponding input is
{
"authentication": "primary_oob_otp_email"
}
After passing the input, you MAY enter a state where you need to verify the email address.
The presence of this means you can create a primary OOB OTP authenticator using phone number.
{
"authentication": "primary_oob_otp_sms"
}
The corresponding input is
{
"authentication": "primary_oob_otp_sms"
}
After passing the input, you MAY enter a state where you need to verify the phone number.
The presence of this means you can create a secondary password.
{
"authentication": "secondary_password",
"password_policy": {
"minimum_length": 8
}
}
Use password_policy
to implement your password strength validator in the UI. The corresponding input is
{
"authentication": "secondary_password",
"new_password": "some.very.secure.password"
}
The presence of this means you can create a secondary Out-of-band (OOB) One-time-password (OTP) authenticator using an email address.
{
"authentication": "secondary_oob_otp_email"
}
The corresponding input is
{
"authentication": "secondary_oob_otp_email",
"target": "johndoe@example.com"
}
target
can be different (and is usually different) from the email address the end-user uses to sign in.
After passing the input, you WILL enter a state where you need to verify the email address.
The presence of this means you can create a secondary OOB OTP authenticator using phone number.
{
"authentication": "secondary_oob_otp_sms"
}
The corresponding input is
{
"authentication": "secondary_oob_otp_sms",
"target": "+85298765432"
}
target
MUST BE in E.164 format without any separators nor spaces. It can be different (and is usually different) from the phone number the end-user uses to sign in.
After passing the input, you WILL enter a state where you need to verify the phone number.
The presence of this means you can create a secondary Time-based One-time-password (TOTP) authenticator.
{
"authentication": "secondary_totp"
}
The corresponding input is
{
"authentication": "secondary_totp"
}
After passing the above input, you will see a response like this
{
"result": {
"state_token": "authflowstate_blahblahblah",
"type": "signup",
"name": "default",
"action": {
"type": "authenticate",
"authentication": "secondary_totp",
"data": {
"secret": "SEURUM6364TM7TRL5SSGDVURZRHZY34O",
"otpauth_uri": "otpauth://totp/johndoe@example.com?algorithm=SHA1&digits=6&issuer=http%3A%2F%2Flocalhost%3A3100&period=30&secret=SEURUM6364TM7TRL5SSGDVURZRHZY34O"
}
}
}
}
secret
: It is the value the end-user need to enter if they want to set up TOTP manually.otpauth_uri
: The intended usage of this URI is construct a QR code image of it. Present the QR code image to the end-user and ask them to scan the code with their TOTP authenticator application, such as Google Authenticator.
After the end-user has set up the TOTP, they have to verify once to prove that the setup is fine. Collect the TOTP from the end-user and pass this input.
{
"code": "000000"
}
When you are in this step of this flow, you will see a response like the following.
{
"result": {
"state_token": "authflowstate_VN0JDCRTFJBPW230WXVX17RD0FKHC23B",
"type": "signup",
"name": "default",
"action": {
"type": "view_recovery_code",
"data": {
"recovery_codes": [
"94X5NST2VM",
"ZTC1BQJSMX",
"R6NA5BS8Z0",
"WFKDRJPHXB",
"K6V6EWJ6NZ",
"0XHS2ARPDM",
"4Q0GPJTC9H",
"7MWXG4SJFN",
"PN5DX4B9JV",
"NRW9NP8MXK",
"WPJQARRRKN",
"QDS53NPH8D",
"SC1AVJYT9Z",
"KY1D2EXZM2",
"ZVG3HMEFTC",
"0Z6YXC5W95"
]
}
}
}
}
You need to present recovery_codes
to the end-user, preferably allow them to download the recovery codes. Ask confirmation from the end-user that they have saved the recovery codes. After that pass this input
{
"confirm_recovery_code": true
}
When you are in this step of this flow, you will see a response like the following
{
"result": {
"state_token": "authflowstate_blahblahblah",
"type": "signup",
"name": "default",
"action": {
"type": "prompt_create_passkey",
"data": {
"creation_options": {
"publicKey": {
"challenge": "muG_Yk_VyupxTyF6A9v1RO3fwBLfYxZ4N1JtVZ6OtlU",
"rp": {
"id": "localhost",
"name": "redacted"
},
"user": {
"id": "ZDAzZjg2YTktMDA2MS00NDFiLTk1NjQtYTk3ZmVmMzFhM2E0",
"name": "johndoe@oursky.com",
"displayName": "johndoe@oursky.com"
},
"pubKeyCredParams": [
{
"type": "public-key",
"alg": -7
},
{
"type": "public-key",
"alg": -257
}
],
"timeout": 300000,
"authenticatorSelection": {
"residentKey": "preferred",
"userVerification": "preferred"
},
"attestation": "direct",
"extensions": {
"credProps": true,
"uvm": true
}
}
}
}
}
}
}
To skip creation, pass this input
{
"skip": true
}
To create the passkey, you need to run some javascript
function b64ToUint6(nChr) {
return nChr > 64 && nChr < 91
? nChr - 65
: nChr > 96 && nChr < 123
? nChr - 71
: nChr > 47 && nChr < 58
? nChr + 4
: nChr === 43
? 62
: nChr === 47
? 63
: 0;
}
function base64DecToArr(sBase64, nBlocksSize) {
var sB64Enc = sBase64.replace(/[^A-Za-z0-9\\+\\/]/g, ""),
nInLen = sB64Enc.length,
nOutLen = nBlocksSize
? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize
: (nInLen * 3 + 1) >> 2,
taBytes = new Uint8Array(nOutLen);
for (
var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0;
nInIdx < nInLen;
nInIdx++
) {
nMod4 = nInIdx & 3;
nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (6 * (3 - nMod4));
if (nMod4 === 3 || nInLen - nInIdx === 1) {
for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
}
nUint24 = 0;
}
}
return taBytes;
}
function uint6ToB64(nUint6) {
return nUint6 < 26
? nUint6 + 65
: nUint6 < 52
? nUint6 + 71
: nUint6 < 62
? nUint6 - 4
: nUint6 === 62
? 43
: nUint6 === 63
? 47
: 65;
}
function base64EncArr(aBytes) {
var nMod3 = 2,
sB64Enc = "";
for (var nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) {
nMod3 = nIdx % 3;
if (nIdx > 0 && ((nIdx * 4) / 3) % 76 === 0) {
sB64Enc += "\\r\\n";
}
nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24);
if (nMod3 === 2 || aBytes.length - nIdx === 1) {
sB64Enc += String.fromCodePoint(
uint6ToB64((nUint24 >>> 18) & 63),
uint6ToB64((nUint24 >>> 12) & 63),
uint6ToB64((nUint24 >>> 6) & 63),
uint6ToB64(nUint24 & 63),
);
nUint24 = 0;
}
}
return (
sB64Enc.substr(0, sB64Enc.length - 2 + nMod3) +
(nMod3 === 2 ? "" : nMod3 === 1 ? "=" : "==")
);
}
function base64URLToBase64(base64url) {
let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
if (base64.length % 4 !== 0) {
const count = 4 - (base64.length % 4);
base64 += "=".repeat(count);
}
return base64;
}
function base64ToBase64URL(base64) {
return base64.replace(/\\+/g, "-").replace(/\\//g, "_").replace(/=/g, "");
}
function trimNewline(str) {
return str.replace(/\\r/g, "").replace(/\\n/g, "");
}
function deserializeCreationOptions(creationOptions) {
const base64URLChallenge = creationOptions.publicKey.challenge;
const challenge = base64DecToArr(base64URLToBase64(base64URLChallenge));
creationOptions.publicKey.challenge = challenge;
const base64URLUserID = creationOptions.publicKey.user.id;
const userID = base64DecToArr(base64URLToBase64(base64URLUserID));
creationOptions.publicKey.user.id = userID;
if (creationOptions.publicKey.excludeCredentials != null) {
for (const c of creationOptions.publicKey.excludeCredentials) {
c.id = base64DecToArr(base64URLToBase64(c.id));
}
}
return creationOptions;
}
function serializeAttestationResponse(credential) {
const response = credential.response;
const attestationObject = trimNewline(
base64ToBase64URL(base64EncArr(new Uint8Array(response.attestationObject))),
);
const clientDataJSON = trimNewline(
base64ToBase64URL(base64EncArr(new Uint8Array(response.clientDataJSON))),
);
let transports = [];
if (typeof response.getTransports === "function") {
transports = response.getTransports();
}
const clientExtensionResults = credential.getClientExtensionResults();
return {
id: credential.id,
rawId: credential.id,
type: credential.type,
response: {
attestationObject,
clientDataJSON,
transports,
},
clientExtensionResults,
};
}
// Basically you need to deserialize the creation_options, and
// pass it to window.navigator.credentials.create(), and then
// serialize the return value and pass it back to the API.
async function main(creationOptions) {
creationOptions = deserializeCreationOptions(options);
const rawResponse = await window.navigator.credentials.create(creationOptions);
if (rawResponse instanceof PublicKeyCredential) {
const response = serializeAttestationResponse(rawResponse);
return response;
}
}
Pass creation_options
to main
and then pass this input
{
"creation_response": {{ resolved return value of main }}
}
See type: signup; step.type: identification. They are the same except that type
is login
.
When you are in this step, you will see a response like the following if you are performing primary authentication.
{
"result": {
"state_token": "authflowstate_X0BJ22Y0P4MB6A98X75AMQ8ADVQC94MK",
"type": "login",
"name": "default",
"action": {
"type": "authenticate",
"data": {
"options": [
{
"authentication": "primary_passkey",
"request_options": {
"publicKey": {
"challenge": "3PzOb9VvB54BIdrOC5b88ewjYt1wEOmKbCd0IM8FQSA",
"timeout": 300000,
"rpId": "localhost",
"userVerification": "preferred",
"allowCredentials": [],
"extensions": {
"uvm": true
}
}
}
},
{
"authentication": "primary_oob_otp_email",
"otp_form": "code",
"masked_display_name": "loui*****@oursky.com",
"channels": [
"email"
]
},
{
"authentication": "primary_password"
}
],
"device_token_enable": false
}
}
}
}
Or this response if you are performing secondary authentication.
{
"result": {
"state_token": "authflowstate_HYQ33WWMZM2AV91VPQWJE2M0HXWT02AK",
"type": "login",
"name": "default",
"action": {
"type": "authenticate",
"data": {
"options": [
{
"authentication": "secondary_totp"
},
{
"authentication": "secondary_password"
},
{
"authentication": "recovery_code"
}
],
"device_token_enable": true
}
}
}
}
The presence of this means you can sign in with primary password.
{
"authentication": "primary_password"
}
The corresponding input is
{
"authentication": "primary_password",
"password": "12345678"
}
The presence of this means you can sign in by receiving a OOB OTP via email.
{
"authentication": "primary_oob_otp_email",
"otp_form": "code",
"masked_display_name": "john****@example.com",
"channels": ["email"]
}
To reference this authentication, use its index in options
array.
otp_form
tells you what kind of OTP will be sent. masked_display_name
tells you what email address the OTP will be sent to. channels
tells you the available channels you must choose from.
The corresponding input is
{
"authentication": "primary_oob_otp_email",
"index": 1,
"channel": "email"
}
After passing the input, you WILL enter a state where you need to verify the OTP. type: signup; step.type: verify
The presence of this means you can sign in by receiving a OOB OTP via phone number.
{
"authentication": "primary_oob_otp_sms",
"otp_form": "code",
"masked_display_name": "+8529876****",
"channels": ["sms", "whatsapp"]
}
To reference this authentication, use its index in options
array.
otp_form
tells you what kind of OTP will be sent. masked_display_name
tells you what phone number the OTP will be sent to. channels
tells you the available channels you must choose from.
The corresponding input is
{
"authentication": "primary_oob_otp_sms",
"index": 2,
"channel": "sms"
}
After passing the input, you WILL enter a state where you need to verify the OTP. type: signup; step.type: verify
The presence of this means you can sign in with passkey.
{
"authentication": "primary_passkey",
"request_options": {
"publicKey": {
"challenge": "2tVbbyG9dJ0KuM1yHlXeah1fZ6grtP4YyOIORYxIzUM",
"timeout": 300000,
"rpId": "localhost",
"userVerification": "preferred",
"allowCredentials": [
{
"type": "public-key",
"id": "dFcL6B0cTujk-mONTRqsP4TXVrLWWvzWfa7oG_b36T8"
}
],
"extensions": {
"uvm": true
}
}
}
}
To use passkey, you need to run some javascript
function b64ToUint6(nChr) {
return nChr > 64 && nChr < 91
? nChr - 65
: nChr > 96 && nChr < 123
? nChr - 71
: nChr > 47 && nChr < 58
? nChr + 4
: nChr === 43
? 62
: nChr === 47
? 63
: 0;
}
function base64DecToArr(sBase64, nBlocksSize) {
var sB64Enc = sBase64.replace(/[^A-Za-z0-9\\+\\/]/g, ""),
nInLen = sB64Enc.length,
nOutLen = nBlocksSize
? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize
: (nInLen * 3 + 1) >> 2,
taBytes = new Uint8Array(nOutLen);
for (
var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0;
nInIdx < nInLen;
nInIdx++
) {
nMod4 = nInIdx & 3;
nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (6 * (3 - nMod4));
if (nMod4 === 3 || nInLen - nInIdx === 1) {
for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
}
nUint24 = 0;
}
}
return taBytes;
}
function uint6ToB64(nUint6) {
return nUint6 < 26
? nUint6 + 65
: nUint6 < 52
? nUint6 + 71
: nUint6 < 62
? nUint6 - 4
: nUint6 === 62
? 43
: nUint6 === 63
? 47
: 65;
}
function base64EncArr(aBytes) {
var nMod3 = 2,
sB64Enc = "";
for (var nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) {
nMod3 = nIdx % 3;
if (nIdx > 0 && ((nIdx * 4) / 3) % 76 === 0) {
sB64Enc += "\\r\\n";
}
nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24);
if (nMod3 === 2 || aBytes.length - nIdx === 1) {
sB64Enc += String.fromCodePoint(
uint6ToB64((nUint24 >>> 18) & 63),
uint6ToB64((nUint24 >>> 12) & 63),
uint6ToB64((nUint24 >>> 6) & 63),
uint6ToB64(nUint24 & 63),
);
nUint24 = 0;
}
}
return (
sB64Enc.substr(0, sB64Enc.length - 2 + nMod3) +
(nMod3 === 2 ? "" : nMod3 === 1 ? "=" : "==")
);
}
function base64URLToBase64(base64url) {
let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
if (base64.length % 4 !== 0) {
const count = 4 - (base64.length % 4);
base64 += "=".repeat(count);
}
return base64;
}
function base64ToBase64URL(base64) {
return base64.replace(/\\+/g, "-").replace(/\\//g, "_").replace(/=/g, "");
}
function trimNewline(str) {
return str.replace(/\\r/g, "").replace(/\\n/g, "");
}
function deserializeRequestOptions(requestOptions) {
const base64URLChallenge = requestOptions.publicKey.challenge;
const challenge = base64DecToArr(base64URLToBase64(base64URLChallenge));
requestOptions.publicKey.challenge = challenge;
if (requestOptions.publicKey.allowCredentials) {
for (const c of requestOptions.publicKey.allowCredentials) {
c.id = base64DecToArr(base64URLToBase64(c.id));
}
}
return requestOptions;
}
function serializeAssertionResponse(credential) {
const response = credential.response;
const authenticatorData = trimNewline(
base64ToBase64URL(base64EncArr(new Uint8Array(response.authenticatorData))),
);
const clientDataJSON = trimNewline(
base64ToBase64URL(base64EncArr(new Uint8Array(response.clientDataJSON))),
);
const signature = trimNewline(
base64ToBase64URL(base64EncArr(new Uint8Array(response.signature))),
);
const userHandle =
response.userHandle == null
? undefined
: trimNewline(
base64ToBase64URL(base64EncArr(new Uint8Array(response.userHandle))),
);
const clientExtensionResults = credential.getClientExtensionResults();
return {
id: credential.id,
rawId: credential.id,
type: credential.type,
response: {
authenticatorData,
clientDataJSON,
signature,
userHandle,
},
clientExtensionResults,
};
}
async function main(options) {
options = deserializeRequestOptions(options);
const rawResponse = await window.navigator.credentials.get(options);
if (rawResponse instanceof PublicKeyCredential) {
const response = serializeAssertionResponse(rawResponse);
return response
}
}
Pass request_options
to main
, and then pass this input
{
"assertion_response": {{ resolved return value of main }}
}
The presence of this means you can sign in with secondary password.
{
"authentication": "secondary_password"
}
The corresponding input is
{
"authentication": "secondary_password",
"password": "12345678"
}
The presence of this means you can sign in by receiving a OOB OTP via email.
{
"authentication": "secondary_oob_otp_email",
"otp_form": "code",
"masked_display_name": "john****@example.com",
"channels": ["email"]
}
To reference this authentication, use its index in options
array.
The corresponding input is
{
"authentication": "secondary_oob_otp_email",
"index": 1,
"channel": "email"
}
After passing the input, you WILL enter a state where you need to verify the OTP. type: signup; step.type: verify
The presence of this means you can sign in by receiving a OOB OTP via phone number.
{
"authentication": "secondary_oob_otp_sms",
"otp_form": "code",
"masked_display_name": "+8529876****",
"channels": ["sms", "whatsapp"]
}
To reference this authentication, use its index in options
array.
The corresponding input is
{
"authentication": "secondary_oob_otp_sms",
"index": 2,
"channel": "sms"
}
After passing the input, you WILL enter a state where you need to verify the OTP. type: signup; step.type: verify
The presence of this means you can sign in with TOTP.
{
"authentication": "secondary_totp"
}
The corresponding input is
{
"authentication": "secondary_totp",
"code": "000000"
}
When you are in this step, you will see a response like the following
{
"result": {
"state_token": "authflowstate_blahblahblah",
"type": "login",
"name": "default",
"action": {
"type": "change_password",
"data": {
"password_policy": {
"minimum_length": 8,
"alphabet_required": true,
"digit_required": true
}
}
}
}
}
The end-user is forced to change their password because their current password does not meet the password policy.
The corresponding input is
{
"new_password": "a.new.password.that.meet.the.password.policy"
}
See type: signup; step.type: prompt_create_passkey. They are the same except that type
is login
.
See type: signup; step.type: identification. They are the same except that type
is signup_login
.
When you are in this step of this flow, you will see a response like the following.
{
"result": {
"state_token": "authflowstate_5R6NM7HGGKV64538R0QEGY9RQBDM4PZD",
"type": "signup",
"name": "default",
"action": {
"type": "identify",
"data": {
"options": [
{
"identification": "email"
},
{
"identification": "phone"
}
]
}
}
}
}
The presence of this means you can receive an account recovery code with an email address.
{
"identification": "email"
}
The corresponding input is
{
"identification": "email",
"login_id": "johndoe@example.com"
}
The presence of this means you can receive an account recovery code with a phone number.
{
"identification": "phone"
}
The corresponding input is
{
"identification": "phone",
"login_id": "+85298765432"
}
Note that the phone number MUST BE in E.164 format without any separators nor spaces.
When you are in this step of this flow, you will see a response like the following.
{
"result": {
"state_token": "authflowstate_5R6NM7HGGKV64538R0QEGY9RQBDM4PZD",
"type": "signup",
"name": "default",
"action": {
"type": "select_destination",
"data": {
"options": [
{
"masked_display_name": "+8529876****",
"channel": "sms",
"otp_form": "code"
},
{
"masked_display_name": "john****@example.com",
"channel": "email",
"otp_form": "link"
}
]
}
}
}
}
It is asking where to deliver the account recovery code.
otp_form
can be code
or link
. code
is a 6-digit otp code, and link
is a long code which is attached to a link.
channel
is the channel to receiving the account recovery code. Current supported channels are sms
, email
.
You pass the following input to indicate your choice:
{
"index": 0
}
index
is the index of the option in options
array. For 0
, it sends an sms with a 6-digit account recovery code to +8529876****
.
When you are in this step of this flow, you will see a response like the following.
{
"result": {
"state_token": "authflowstate_5R6NM7HGGKV64538R0QEGY9RQBDM4PZD",
"type": "signup",
"name": "default",
"action": {
"type": "verify_account_recovery_code",
"data": {}
}
}
}
In previous steps, you should have selected the destination to receive the account recovery code. You should get the code in your selected destination, such as your mailbox, and pass it with the following input:
{
"account_recovery_code": "123456" // Put your account recovery code here
}
{
"account_recovery_code": "M6CGA4WV6M9XTXNWFYFHRQDWF6VFR7K4" // OR the code in the link.
}
Note that state_token
can be omitted in this step, if and only if your selected destination have otp_form=link
. Using only the account_recovery_code
is enough for proceeding to the next step in this case:
// POST /api/v1/authentication_flows/states/input
// Content-Type: application/json
{
"input": {
"account_recovery_code": "M6CGA4WV6M9XTXNWFYFHRQDWF6VFR7K4"
}
}
When you are in this step of this flow, you will see a response like the following.
{
"result": {
"state_token": "authflowstate_5R6NM7HGGKV64538R0QEGY9RQBDM4PZD",
"type": "signup",
"name": "default",
"action": {
"type": "reset_password",
"data": {
"password_policy": {
"minimum_length": 8,
"digit_required": true,
"history": {
"enabled": false
}
}
}
}
}
}
You can reset the password of the user in this step.
The corresponding input is
{
"new_password": "a.new.password.that.meet.the.password.policy"
}