Use SMS OTP for MFA login

Stytch's Multi-Factor Authentication is a multi-step login process that enforces the use of a secondary factor like SMS OTP. A second form of authentication can create a more secure and protected policy for accounts.

In this guide, you'll learn how to use SMS OTP as a secondary factor when logging in to an Organization as a Member. By the end, you'll have:

  • Updated an Organization's MFA policy settings.
  • Authenticated with EML as a primary factor.
  • Authenticated with SMS OTP as a secondary factor.
  • Created a fully minted Session and completed the MFA flow.

Before you start

In order to complete this guide, you'll need the following:

  • A Stytch B2B project. If you don't have one already, in the Dashboard, click on your existing project name in the top left corner of the Dashboard, click Create a new project, and then select B2B Authentication.
  • The project Test environment's project_id and secret from the API keys section. You'll need to pass these values into the Authorization request header for every Stytch API call.
  • An Organization with Members that already use Email Magic Links (EML) as an auth method. You can visit this EML guide to get fully set up.

Step 1: Update your Organization's settings

Copy the organization_id of your existing Organization. Then call the Update Organization endpoint with the following parameters:

curl --request PUT \
	--url https://test.stytch.com/v1/b2b/organizations/{ORGANIZATION_ID} \
	-u '{PROJECT_ID}:{SECRET}' \
	-H 'Content-Type: application/json' \
	-d '{
        "mfa_policy": "REQUIRED_FOR_ALL"
	}'

After a successful API call, all Members will now be required to complete the MFA flow in order to log in.

Step 2: Authenticate with EML as the primary factor

Just like in steps 3 through 6 in the EML guide, you'll authenticate a Member using the same exact flow.

First, call the Send Login Or Signup Email endpoint.

curl --request POST \
	--url https://test.stytch.com/v1/b2b/magic_links/email/login_or_signup \
	-u '{PROJECT_ID}:{SECRET}' \
	-H 'Content-Type: application/json' \
	-d '{
		"organization_id": "{ORGANIZATION_ID}",
		"email_address": "{MEMBER_EMAIL_ADDRESS}"
	}'

Then copy the token value from the redirect URL's query parameters.

{REDIRECT_URL}?slug=example-org&stytch_token_type=magic_links_token&token={MAGIC_LINKS_TOKEN}

And finally, call the Authenticate Magic Link endpoint with the copied token to finish authenticating with the primary factor.

curl --request POST \
	--url https://test.stytch.com/v1/b2b/magic_links/authenticate \
	-u 'PROJECT_ID:SECRET' \
	-H 'Content-Type: application/json' \
	-d '{
		"magic_links_token": "{MAGIC_LINKS_TOKEN}"
	}'

Step 3: Breaking down the primary factor response

The response from the Authenticate Magic Link endpoint will look like this:

{
	"intermediate_session_token": "oNJB3foIA79dn_uNVMNghG_MGkKSLHnR65NsKXv0gZzY",
    "mfa_required": {
		"member_options": null,
		"secondary_auth_initiated": null
	},
	"member_authenticated": false,
    "member": {...},
    "organization": {...},
	"member_id": "member-test-20023bbf-8700-4312-969f-5245e4df5713",
	"member_session": null,
	"method_id": "member-email-test-67e95fda-8489-4433-bc32-0323c69244c3",
	"organization_id": "organization-test-16a682b6-4a1c-443f-9fd9-48fda5d910fd",
	"request_id": "request-id-test-4c1ebbfd-fe49-49ca-80dd-f495357cd0fd",
	"reset_sessions": false,
	"session_jwt": "",
	"session_token": "",
	"status_code": 200
}

Breaking down the response, the following data fields have the most important pieces of information to analyze:

  • member_authenticated is false. At this point, only the primary factor has been completed. The Member is not fully authenticated yet.
  • The mfa_required object will contain additional information (like a phone number) if the Member has completed the MFA flow before.
  • session_jwt and session_token are empty. These fields will only be populated when a Session is created, which requires the Member to complete the MFA flow.
  • intermediate_session_tokenis populated with a token.

Copy the intermediate_session_token, member_id, and organization_id. You'll need them for the next steps.

Step 4: Call the Send OTP SMS endpoint

Call the Send OTP SMS endpoint with the following parameters:

curl --request POST \
	--url https://test.stytch.com/v1/b2b/otps/sms/send \
	-u 'PROJECT_ID:SECRET' \
	-H 'Content-Type: application/json' \
	-d '{
		"organization_id": {ORGANIZATION_ID},
		"member_id": {MEMBER_ID},
		"mfa_phone_number": {MEMBER_PHONE_NUMBER}
	}'

Make sure the mfa_phone_number is in the E.164 format, e.g. “+14155551234”.

After a successful API call, the provided phone number will recieve an SMS with an OTP code.

Step 5: Authenticate the OTP as a secondary factor

Call the Authenticate OTP SMS endpoint with the following parameters to finish authenticating with the secondary factor:

curl --request POST \
	--url https://test.stytch.com/v1/b2b/otps/sms/authenticate \
	-u 'PROJECT_ID:SECRET' \
	-H 'Content-Type: application/json' \
	-d '{
		"organization_id": {ORGANIZATION_ID},
		"member_id": {MEMBER_ID},
		"intermediate_session_token": {INTERMEDIATE_SESSION_TOKEN},
		"code": {SMS_OTP_CODE}
	}'

Step 6: Breaking down the secondary factor response

The response from the Authenticate OTP SMS endpoint will look like this:

{
	"member": {
		"email_address": "{MEMBER_EMAIL_ADDRESS}",
		"email_address_verified": true,
		"member_id": "member-test-dd178c82-0c72-4f3e-8623-708aa599ce7f",
		"mfa_enrolled": true,
		"mfa_phone_number": "{MEMBER_PHONE_NUMBER}",
		"mfa_phone_number_verified": true,
        ...
	},
	"member_session": {
		"authentication_factors": [
			{
				"created_at": "2023-08-15T19:29:37Z",
				"delivery_method": "email",
				"email_factor": {
					"email_address": "{MEMBER_EMAIL_ADDRESS}",
					"email_id": "member-email-test-5d761b80-b178-4a13-aa12-7d91cc8903d6"
				},
				"last_authenticated_at": "2023-08-15T19:29:37Z",
				"sequence_order": "PRIMARY",
				"type": "magic_link",
				"updated_at": "2023-08-15T19:29:37Z"
			},
			{
				"created_at": "2023-08-15T19:31:02Z",
				"delivery_method": "sms",
				"last_authenticated_at": "2023-08-15T19:31:02Z",
				"phone_number_factor": {
					"phone_id": "member-phone-number-test-62280af5-c2ac-42e8-8518-c3c0abe7ace2",
					"phone_number": "{MEMBER_PHONE_NUMBER}"
				},
				"sequence_order": "SECONDARY",
				"type": "otp",
				"updated_at": "2023-08-15T19:31:02Z"
			}
		],
		"custom_claims": {},
		"expires_at": "2023-08-15T20:31:02Z",
		"last_accessed_at": "2023-08-15T19:31:02Z",
		"member_id": "member-test-dd178c82-0c72-4f3e-8623-708aa599ce7f",
		"member_session_id": "member-session-test-703b5e0a-4ac4-43f4-8bc6-67f9351cdb84",
		"organization_id": "organization-test-c4e1dab4-c4d3-48d7-beb6-a45bced46332",
		"started_at": "2023-08-15T19:31:02Z"
	},
	"organization": {...},
	"request_id": "request-id-test-56afb9e7-e26b-418f-b82e-0c0d525ba875",
	"session_jwt": "eyJhbGciOiJSUzI1...",
	"session_token": "oFRmDIMpP-4xVHcJq1Rue4IJYn1ygGZfTjHtHyfzlyPZ",
	"status_code": 200
}

After a successful API call, the data fields in the response will indicate:

  • The Member is fully authenticated and done with the MFA flow.
  • A Session object, Member object, Organization object, session_token, and session_jwt are all populated.
  • The Member is updated with a populated mfa_phone_number.
  • The Member has mfa_phone_number_verified and mfa_enrolled set to true.
  • The Session object has metadata on the primary and secondary factor used in the MFA flow in the authentication_factors array.

What's next

Build a user interface that allows end users to log in with MFA using EML as a primary factor and SMS OTP as a secondary factor.

Clone our B2B Next.js example app for helpful templates that can get you started quickly. Also check out our interactive B2B demo app to see the app in action.