Use the Discovery flow with MFA login

Stytch's Discovery and Multi-Factor Authentication flows can be implemented together to create a seamless login experience. Log in once, discover all your memberships, and select an Organization to authenticate into by also completing the MFA flow.

In this guide, you'll learn how to use MFA with the Discovery flow. By the end, you'll have:

  • Sent a Discovery EML to an end user's email inbox.
  • Authenticated with Discovery EML as a primary factor.
  • Returned a list of Discovered Organizations.
  • Authenticated with SMS OTP as a secondary factor.
  • Logged the end user into the desired Organization.

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 requires MFA and already uses Email Magic Links (EML) as an auth method.

Step 1: Authenticate with Discovery EML as the primary factor

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

First, call the Send Discovery Email endpoint.

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

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

{REDIRECT_URL}?stytch_token_type=discovery&token={DISCOVERY_MAGIC_LINKS_TOKEN}

And finally, call the Authenticate Discovery 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/discovery/authenticate \
	-u 'PROJECT_ID:SECRET' \
	-H 'Content-Type: application/json' \
	-d '{
		"discovery_magic_links_token": "{DISCOVERY_MAGIC_LINKS_TOKEN}"
	}'

Step 2: Select an Organization from the response

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

{
	"discovered_organizations": [
		{
			"member_authenticated": true,
			"membership": {...},
			"mfa_required": {...},
			"organization": {...},
			"primary_required": null
		},
        {
			"member_authenticated": false,
			"membership": {...},
			"mfa_required": {...},
			"organization": {...},
			"primary_required": null
		},
        ...
	],
	"email_address": "{MEMBER_EMAIL_ADDRESS}",
	"intermediate_session_token": "{INTERMEDIATE_SESSION_TOKEN}",
	"request_id": "request-id-test-4775415e-de2d-4529-a816-b5a0b2b29dd0",
	"status_code": 200
}

From the discovered_organizations array, select an Organization that requires MFA -- where member_authenticated is false and organization.mfa_policy is REQUIRED_FOR_ALL.

Copy the following values:

  • The related member_id.
  • The related organization_id.
  • The intermediate_session_token.

Step 3: 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 receive an SMS with an OTP code.

Step 4: 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_SESION_TOKEN},
		"code": {SMS_OTP_CODE}
	}'

Step 5: 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 and Discovery 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 and the Discovery flow.

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.