tech#entra-id#external-id#ciam#azure#authentication

Building demo authentication with Microsoft Entra External ID and email OTP

Published 👁
Building demo authentication with Microsoft Entra External ID and email OTP

While planning a demo that lets users submit data and view it later, I needed to ensure that only the person who submitted the data could see it.

That required authentication, but I did not want administrators to register every demo user manually. I wanted users to register themselves without creating another password, so I chose a Microsoft Entra External ID external tenant with email one-time passcode (OTP) authentication.

This article covers the settings I used to create an external tenant and connect a React SPA to an Azure Functions API. I replaced all environment-specific information, including tenant IDs, application (client) IDs, Azure subscriptions, and host names, with placeholders.

Table of Contents

Key takeaways

This architecture keeps demo user accounts in an External ID external tenant instead of mixing them into the Microsoft Entra ID tenant used for employees. Demo users can register themselves with an OTP sent to their email address, without creating or remembering a new password.

The frontend and API are registered as separate applications in Microsoft Entra ID. SPA stands for single-page application; here, it refers to the React browser application.

App registrationRole
SPAOpens the authentication page and defines the redirect URI used after sign-in. Obtains an access token to call the Web API on behalf of the user
Web APIDefines the API that receives the access token and permissions such as access_as_user. Validates that each token is intended for the API and includes the required permission

In other words, the SPA initiates authentication through the External ID page. The Web API registration does not open that page; it tells Microsoft Entra ID which API the SPA wants to call and which permission it requests.

The implementation required seven tasks:

  1. Confirm the source directory and Azure subscription, and prepare the Tenant Creator role
  2. Create an external tenant and switch to its directory
  3. Register the Web API and SPA separately
  4. Expose a delegated permission scope on the API and add it to the SPA
  5. Create a sign-up and sign-in user flow that uses email OTP
  6. Associate the SPA with the user flow
  7. Obtain an access token in the SPA, then validate the token and data owner in the API

The critical point is that adding authentication alone does not restrict users to their own data. The API stores an immutable ID from the validated access token as the data owner. It must also filter every read and write operation by that ID before it can reliably return only the current user’s data.

For an overview of External ID use cases and how it differs from employee tenants and B2B collaboration, see Getting Started with Microsoft Entra External ID (2026 Edition).

Requirements and architecture

I built an authentication foundation that allows demo users to register themselves and sign in. This is not a B2B collaboration scenario in which business partners are invited to an employee tenant as guests.

ItemConfiguration
UsersGeneral users who register themselves for the demo
Authentication platformMicrosoft Entra External ID external tenant
Authentication pageMicrosoft-hosted sign-in page
Sign-in methodEmail OTP
ClientReact SPA, Authorization Code Flow + PKCE
APIAzure Functions Web API
PermissionDelegated access_as_user scope
Owner keyoid claim from the access token
Information collected during registrationEmail address only
Client secretNone for either the SPA or API

I registered the SPA and API separately. The SPA calls the API on behalf of the signed-in user, and the API accepts only access tokens issued for itself.

Decisions to make before you start

Before using the portal, I decided on the following values and operational responsibilities.

ItemDecision
Source directoryThe directory that owns the subscription and permits tenant creation
Billing scopeThe subscription and resource group to associate with the external tenant
Tenant nameA name that makes the tenant’s purpose clear in the admin interface
Domain nameThe subdomain for <TENANT_SUBDOMAIN>.onmicrosoft.com
Country/RegionJapan; this cannot be changed after creation
Go-LocalNot used for this implementation
App namesSeparate names that clearly identify the SPA and API
Redirect URIsThe URIs that the SPA actually uses in local and production environments
Operations ownersPeople responsible for the apps, user flow, demo user accounts, and emergency recovery

As of August 14, 2026, the official procedure requires the built-in Microsoft Entra directory role Tenant Creator to create an external tenant. This is not an Azure RBAC role. However, the official procedure assigns Tenant Creator at the scope of an Azure subscription or resource group.

By contrast, Owner is an Azure RBAC role for managing resources such as Azure subscriptions. Because these roles belong to different role systems, an Azure subscription Owner still cannot create an external tenant without Tenant Creator.

The country/region cannot be changed later. Selecting Japan also displays the paid Go-Local data residency option. Before a production rollout, review your organization’s requirements and the official data residency information.

Create an external tenant

I created the external tenant in the Microsoft Entra admin center.

  1. Sign in to the Microsoft Entra admin center with an administrator account from the source directory
  2. Open Entra ID, Overview, and Manage tenants
  3. Select Create, then choose External as the tenant type
  4. Select Use Azure subscription
  5. Enter the tenant name, domain name, and country/region
  6. Select the subscription and resource group
  7. Review the settings under Review + create, then create the tenant
The Microsoft Entra admin center tenant creation page with External selected
Select External as the tenant type

The official documentation notes that creation can take up to 30 minutes. After the tenant is created, switch to the new external tenant from Settings, Directories + subscriptions.

In Tenant overview, record the values you will use later to configure the applications.

Tenant name    : <EXTERNAL_TENANT_NAME>
Primary domain : <TENANT_SUBDOMAIN>.onmicrosoft.com
Tenant ID      : <EXTERNAL_TENANT_ID>
External tenant overview in the Microsoft Entra admin center
Confirm the tenant ID and primary domain in the external tenant overview

Tenant IDs and client IDs are not secrets, but they can identify an environment. Do not use real values in public materials. Also, do not record client secrets, access tokens, OTPs, or user email addresses.

Register the Web API

I registered the Web API that the SPA will call first.

  1. While switched to the external tenant, open Entra ID, App registrations, and New registration
  2. Enter a name that clearly identifies the API’s purpose
  3. Register the app for accounts in this external tenant only
  4. Under Expose an API, set the Application ID URI to api://<API_CLIENT_ID>
  5. Under Add a scope, create a delegated scope that allows the API to be called on behalf of a user
Application registration page in the Microsoft Entra admin center
Register the application for accounts in this external tenant only

I configured the scope as follows.

ItemValue
Scope nameaccess_as_user
Who can consentAdmins and users
Admin consent display nameAccess the demo API
User consent display nameAccess the demo API
StateEnabled

The SPA requests the scope by its full name.

api://<API_CLIENT_ID>/access_as_user

Because the API only validates access tokens received from the SPA in this implementation, I did not create a client secret for the API app registration.

A client secret is a credential that a server-side application uses to prove to Microsoft Entra ID that a token request comes from the application itself. It is required, for example, when a background job or service-to-service integration uses the client credentials flow without a signed-in user, when a server-side web application handles authentication, or when an API uses the On-Behalf-Of flow to call another API for a user.

For applications running on Azure, prefer less exposed options such as managed identities or certificates over client secrets, which must be stored and rotated regularly.

Register the SPA

Next, I registered the SPA that calls the API.

  1. Open App registrations and New registration
  2. Enter a name that clearly identifies the SPA’s purpose
  3. Register the app for accounts in this external tenant only
  4. Select Authentication, Add a platform, and Single-page application
  5. Register the redirect URIs for the local and production environments
http://localhost:<LOCAL_PORT>
https://<APP_HOST>/auth/callback

The redirect URI must match the redirectUri sent by the SPA, including the scheme, host, port, path, and trailing slash. Because a SPA is delivered to the browser, it cannot keep a secret securely. Users can inspect a secret even if it is embedded in the code, so do not create a client secret for the SPA.

Next, open API permissions, Add a permission, and APIs my organization uses. Select the API registered earlier and add the delegated access_as_user permission. If the application does not use Microsoft Graph, also confirm whether it needs the default User.Read permission.

Create a sign-up and sign-in user flow

To use email OTP, first open External Identities, All identity providers, and confirm that Email One-time Passcode is enabled for the tenant.

The External Identities page in the Microsoft Entra admin center showing where to select User flows
Configure identity providers and user flows from the External Identities menu
  1. Open External Identities, User flows, and New user flow
  2. Enter a name for the user flow
  3. Under identity providers, select Email Accounts and Email one-time passcode
  4. Select only Email Address as the attribute to collect from users, then create the flow
  5. Open Use, Applications in the user flow you created
  6. Select the SPA from Add application

One user flow can be associated with multiple applications. However, each application can be associated with only one user flow.

The b2c-extensions-app in the application list is created automatically to store custom attributes for the external tenant. Do not delete it, even if this implementation does not use custom attributes.

Configure authentication in the app

Do not guess the OIDC endpoints. Retrieve issuer and jwks_uri from the external tenant’s OpenID configuration.

https://<TENANT_SUBDOMAIN>.ciamlogin.com/<EXTERNAL_TENANT_ID>/v2.0/.well-known/openid-configuration

Configure the application with the following values.

authority : https://<TENANT_SUBDOMAIN>.ciamlogin.com/<EXTERNAL_TENANT_ID>
clientId  : <SPA_CLIENT_ID>
scope     : api://<API_CLIENT_ID>/access_as_user
audience  : <API_CLIENT_ID>
issuer    : <OPENID_CONFIGURATION_ISSUER>
jwksUri   : <OPENID_CONFIGURATION_JWKS_URI>

The SPA obtains an access token through Authorization Code Flow + PKCE. Using the signing keys and issuer information from the OpenID configuration, the API validates at least the signature and the iss, tid, aud, exp, nbf, and scp claims.

Send the API an access token issued for that API. The ID token allows the SPA to confirm the user’s sign-in state; it must not be used for API access control.

Return only the current user’s data

A successful sign-in alone does not guarantee that user A cannot read user B’s data. The API must also determine who owns each item.

For this implementation, I stored the oid claim from the validated access token as the owner ID.

ownerId = verifiedAccessToken.oid

When saving data, do not trust an owner ID supplied in the request body. Store the oid from the validated token with the data. For read, update, and delete operations, do not query by the requested data ID alone; always include ownerId in the query criteria.

resourceId = <REQUEST_RESOURCE_ID>
ownerId    = verifiedAccessToken.oid

Email addresses can change, so do not use them as owner keys. An oid value is unique within a tenant. If the API accepts tokens from multiple tenants, combine it with the tid claim that identifies the tenant. This implementation allows tokens issued by only one external tenant.

Test the configuration

I verified the following successful flow.

  1. Start sign-up with an unregistered test email address
  2. Complete registration with the received OTP and confirm that the browser returns to the SPA
  3. Sign out, then confirm that the same account can sign in again
  4. Confirm that the SPA can obtain an access token with the access_as_user scope
  5. Confirm that the API validates the token and returns only the current user’s data
  6. Sign out, then confirm that the authentication cache is cleared and protected pages are no longer accessible

I also tested the following rejection cases.

Troubleshooting

The external tenant creation button is disabled

In my case, Azure RBAC Owner was not enough to create an external tenant. The built-in Microsoft Entra directory role Tenant Creator is also required. Check these four items:

  1. The directory currently selected in the portal
  2. Whether tenant creation is allowed in the source directory
  3. Whether the Microsoft Entra Tenant Creator role is assigned
  4. The scope of Tenant Creator and access to the target subscription or resource group

For an account that belongs to multiple directories, check not only its permissions but also the directory selected in the upper-right corner of the portal.

Redirect URI mismatch

Do not mix the SPA origin with /auth/callback. Compare the authentication library’s redirectUri with the app registration value character by character. Check the localhost port, HTTP versus HTTPS, and the trailing slash as well.

The API returns 401 after receiving an access token

Check the following in order.

Do not paste token contents or personal information into public articles, Issues, or work logs.

Post-implementation checklist

Even after validating the PoC, additional work remains before operating this as an authentication platform.

I did not grant the demo API Microsoft Graph permissions to delete directory users. Instead, administrators delete demo user accounts through a separate procedure.

Summary

To ensure that only the person who submitted demo data can view it, I configured a Microsoft Entra External ID external tenant, an email OTP user flow, and separate app registrations for the SPA and API.

External ID separates user self-registration and sign-in from the application. However, a successful sign-in alone does not ensure that the API returns only the current user’s data. The API must validate the token and restrict every data operation by using oid as the owner key.

During setup, check the currently selected directory, Tenant Creator, API scope, SPA permissions, and user flow association in that order. This makes missing configuration easier to identify.

Official sources