> ## Documentation Index
> Fetch the complete documentation index at: https://docs.authsignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Let users manage MFA methods with Microsoft Azure AD B2C

> Let signed-in users manage their Authsignal authenticators from your app.

This guide adds a manage security policy to your Azure AD B2C tenant. A signed-in user starts it from a **Manage security** button in your app. The user verifies in Authsignal's pre-built UI, then adds, changes or removes authenticators. Your app gets the result in the ID token.

The examples use the policy ID `B2C_1A_MANAGE_SECURITY`. You can use any policy ID.

## Prerequisites

Complete the [MFA integration guide](https://docs.authsignal.com/integrations/azure-ad-b2c/azure-ad-b2c-mfa.md) first. This policy uses the same base policies and the same `B2C_1A_AuthsignalSecret` policy key.

> [card] [Try a live demo!](https://as-azure-ad-b2c-example.vercel.app/)

## Sequence

```mermaid theme={null}
sequenceDiagram
    participant F as Your frontend
    participant AAD as Azure AD B2C
    participant AS as Authsignal
    F->>AAD: User clicks the "Manage security" button
    Note over AAD: Existing B2C session identifies the user

    critical Init auth and authorize
    AAD->>AS: POST /init-auth with the manage security action
    AS->>AAD: token
    AAD->>AS: GET /oidc/auth with token
    Note over AS: User verifies, then manages authenticators
    AS->>AAD: Return auth code
    AAD->>AS: POST /oidc/token with auth code
    AS->>AAD: Return id_token
    end

    Note over AAD: Check the user ID and the action state
    AAD->>F: Return id_token with action_state and correlation_id
```

## Code example

The full policy is in each example repository:

* [ManageSecurity.xml](https://github.com/authsignal/azure-ad-b2c-example/blob/main/policies/ManageSecurity.xml) in the MFA example
* [ManageSecurity\_Passwordless.xml](https://github.com/authsignal/azure-ad-b2c-passwordless-example/blob/main/policies/ManageSecurity_Passwordless.xml) in the passwordless example
* [ManageSecurity\_Passkey.xml](https://github.com/authsignal/azure-ad-b2c-passkey-example/blob/main/policies/ManageSecurity_Passkey.xml) in the passkey example

The steps below show each part of the policy.

## Step by step guide

### Step 1: Create a manage security action in Authsignal

In the [Authsignal Portal](https://portal.authsignal.com/actions), create an action for managing authenticators, for example `manageSecurity`. In the action's configuration, add these steps to the flow:

1. A **Verification step**. Allow every method that your users can enroll, for example Email OTP, Passkey and SMS OTP.
2. A **Manage methods** step. Select the methods that users can manage.

> [!WARNING]
>
> A user can only verify with a method that the verification step allows. Otherwise the user
> sees "Verification unsuccessful".

### Step 2: Create the policy file

Create a policy file, for example `ManageSecurity.xml`. Set its base policy to the extensions policy from the MFA guide.

```xml theme={null}
<TrustFrameworkPolicy
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06"
  PolicySchemaVersion="0.3.0.0"
  TenantId="yourtenant.onmicrosoft.com"
  PolicyId="B2C_1A_MANAGE_SECURITY"
  PublicPolicyUri="http://yourtenant.onmicrosoft.com/B2C_1A_MANAGE_SECURITY">

  <BasePolicy>
    <TenantId>yourtenant.onmicrosoft.com</TenantId>
    <PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>
  </BasePolicy>

  <!-- Add the building blocks, claims providers, user journey and relying party from the steps below -->
</TrustFrameworkPolicy>
```

### Step 3: Declare the claims

Add these claims to `<BuildingBlocks><ClaimsSchema>`.

```xml theme={null}
<ClaimType Id="authsignalRequestBody">
  <DisplayName>Authsignal request body</DisplayName>
  <DataType>string</DataType>
</ClaimType>
<ClaimType Id="authsignalInitToken">
  <DisplayName>Authsignal init token</DisplayName>
  <DataType>string</DataType>
</ClaimType>
<ClaimType Id="authsignalCorrelationId">
  <DisplayName>Correlation ID</DisplayName>
  <DataType>string</DataType>
</ClaimType>
<ClaimType Id="authsignalActionState">
  <DisplayName>Authsignal action state</DisplayName>
  <DataType>string</DataType>
</ClaimType>
<ClaimType Id="authsignalSubject">
  <DisplayName>Authsignal user ID</DisplayName>
  <DataType>string</DataType>
</ClaimType>
<ClaimType Id="authsignalSubjectMatches">
  <DisplayName>Authsignal user ID matches</DisplayName>
  <DataType>boolean</DataType>
</ClaimType>
<ClaimType Id="authsignalFlowSucceeded">
  <DisplayName>Authsignal flow succeeded</DisplayName>
  <DataType>boolean</DataType>
</ClaimType>
<ClaimType Id="authsignalErrorMessage">
  <DisplayName></DisplayName>
  <DataType>string</DataType>
  <UserInputType>Paragraph</UserInputType>
</ClaimType>
```

### Step 4: Add the claims transformations

Add these claims transformations to `<BuildingBlocks><ClaimsTransformations>`. Replace the `INSERT_AUTHSIGNAL_ACTION` placeholder with the action code from step 1.

The first transformation builds the init auth request body. Its `custom.correlationId` field stores the B2C correlation ID on the Authsignal action. The other two check the user ID and the action state in the token from Authsignal.

```xml theme={null}
<ClaimsTransformation Id="AuthsignalCreateManageSecurityRequestBody" TransformationMethod="GenerateJson">
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="objectId" TransformationClaimType="userId" />
    <InputClaim ClaimTypeReferenceId="signInNames.emailAddress" TransformationClaimType="email" />
    <InputClaim ClaimTypeReferenceId="authsignalCorrelationId" TransformationClaimType="custom.correlationId" />
  </InputClaims>
  <InputParameters>
    <InputParameter Id="action" DataType="string" Value="INSERT_AUTHSIGNAL_ACTION" />
    <InputParameter Id="emailVerified" DataType="boolean" Value="true" />
  </InputParameters>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="authsignalRequestBody" TransformationClaimType="outputClaim" />
  </OutputClaims>
</ClaimsTransformation>

<ClaimsTransformation Id="AuthsignalCheckManageSecuritySubject" TransformationMethod="CompareClaims">
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="objectId" TransformationClaimType="inputClaim1" />
    <InputClaim ClaimTypeReferenceId="authsignalSubject" TransformationClaimType="inputClaim2" />
  </InputClaims>
  <InputParameters>
    <InputParameter Id="operator" DataType="string" Value="EQUAL" />
    <InputParameter Id="ignoreCase" DataType="string" Value="true" />
  </InputParameters>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="authsignalSubjectMatches" TransformationClaimType="outputClaim" />
  </OutputClaims>
</ClaimsTransformation>

<ClaimsTransformation Id="AuthsignalCheckManageSecurityFlowSucceeded" TransformationMethod="CompareClaimToValue">
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="authsignalActionState" TransformationClaimType="inputClaim1" />
  </InputClaims>
  <InputParameters>
    <InputParameter Id="compareTo" DataType="string" Value="CHALLENGE_SUCCEEDED" />
    <InputParameter Id="operator" DataType="string" Value="equal" />
    <InputParameter Id="ignoreCase" DataType="string" Value="false" />
  </InputParameters>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="authsignalFlowSucceeded" TransformationClaimType="outputClaim" />
  </OutputClaims>
</ClaimsTransformation>
```

### Step 5: Add the technical profiles

Add these technical profiles to a `<ClaimsProvider>` in `<ClaimsProviders>`. Replace the `AUTHSIGNAL_CONNECT_HOSTNAME` and `INSERT_AUTHSIGNAL_TENANT_ID` placeholders with the values from the MFA guide. The [OIDC documentation](https://docs.authsignal.com/integrations/oidc#regions) lists the hostnames.

With `SM-Noop` session management, Azure AD B2C runs the Authsignal steps every time.

```xml theme={null}
<TechnicalProfile Id="Authsignal-ManageSecurity-SetCorrelationId">
  <DisplayName>Set correlation ID</DisplayName>
  <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  <Metadata>
    <Item Key="IncludeClaimResolvingInClaimsHandling">true</Item>
  </Metadata>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="authsignalCorrelationId" DefaultValue="{Context:CorrelationId}" AlwaysUseDefaultValue="true" />
  </OutputClaims>
  <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>

<TechnicalProfile Id="Authsignal-ManageSecurity-InitAuth">
  <DisplayName>Authsignal init auth</DisplayName>
  <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  <Metadata>
    <!-- NOTE: Change the host name below -->
    <Item Key="ServiceUrl">https://AUTHSIGNAL_CONNECT_HOSTNAME/init-auth</Item>
    <Item Key="SendClaimsIn">Body</Item>
    <Item Key="ClaimUsedForRequestPayload">authsignalRequestBody</Item>
    <Item Key="AuthenticationType">Basic</Item>
    <Item Key="AllowInsecureAuthInProduction">false</Item>
    <Item Key="ResolveJsonPathsInJsonTokens">true</Item>
    <Item Key="DefaultUserMessageIfRequestFailed">Cannot process your request right now, please try again later.</Item>
  </Metadata>
  <CryptographicKeys>
    <Key Id="BasicAuthenticationUsername" StorageReferenceId="B2C_1A_AuthsignalSecret" />
    <Key Id="BasicAuthenticationPassword" StorageReferenceId="B2C_1A_AuthsignalSecret" />
  </CryptographicKeys>
  <InputClaimsTransformations>
    <InputClaimsTransformation ReferenceId="AuthsignalCreateManageSecurityRequestBody" />
  </InputClaimsTransformations>
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="authsignalRequestBody" />
  </InputClaims>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="authsignalInitToken" PartnerClaimType="token" />
  </OutputClaims>
  <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>

<TechnicalProfile Id="Authsignal-ManageSecurity-Oidc">
  <DisplayName>Authsignal</DisplayName>
  <Protocol Name="OpenIdConnect" />
  <Metadata>
    <!-- NOTE: Change the host name and tenant id below -->
    <Item Key="METADATA">https://AUTHSIGNAL_CONNECT_HOSTNAME/oidc/.well-known/openid-configuration</Item>
    <Item Key="client_id">INSERT_AUTHSIGNAL_TENANT_ID</Item>
    <Item Key="authorization_endpoint">https://AUTHSIGNAL_CONNECT_HOSTNAME/oidc/auth</Item>
    <Item Key="AccessTokenEndpoint">https://AUTHSIGNAL_CONNECT_HOSTNAME/oidc/token</Item>
    <Item Key="response_types">code</Item>
    <Item Key="scope">openid</Item>
    <Item Key="HttpBinding">POST</Item>
    <Item Key="response_mode">form_post</Item>
    <Item Key="token_endpoint_auth_method">client_secret_post</Item>
    <Item Key="UsePolicyInRedirectUri">false</Item>
    <Item Key="IncludeClaimResolvingInClaimsHandling">true</Item>
    <Item Key="ResolveJsonPathsInJsonTokens">true</Item>
  </Metadata>
  <CryptographicKeys>
    <Key Id="client_secret" StorageReferenceId="B2C_1A_AuthsignalSecret" />
  </CryptographicKeys>
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="authsignalInitToken" PartnerClaimType="token" />
  </InputClaims>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="authsignalSubject" PartnerClaimType="sub" />
    <OutputClaim ClaimTypeReferenceId="authsignalActionState" PartnerClaimType="action_state" />
  </OutputClaims>
  <OutputClaimsTransformations>
    <OutputClaimsTransformation ReferenceId="AuthsignalCheckManageSecuritySubject" />
    <OutputClaimsTransformation ReferenceId="AuthsignalCheckManageSecurityFlowSucceeded" />
  </OutputClaimsTransformations>
  <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>

<TechnicalProfile Id="Authsignal-ManageSecurity-Error">
  <DisplayName>Manage security failed</DisplayName>
  <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  <Metadata>
    <Item Key="ContentDefinitionReferenceId">api.selfasserted</Item>
    <Item Key="setting.showContinueButton">false</Item>
    <Item Key="setting.showCancelButton">false</Item>
  </Metadata>
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="authsignalErrorMessage" DefaultValue="We couldn't confirm it's you. Please try again." />
  </InputClaims>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="authsignalErrorMessage" Required="true" />
  </OutputClaims>
  <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
```

### Step 6: Add the user journey

Add this user journey to `<UserJourneys>`.

In step 1, use the sign-in technical profile from your sign-in journey. The passwordless example uses `SelfAsserted-LocalAccountSignin-Email_Passwordless`. Azure AD B2C skips this step when the user has a B2C session.

```xml theme={null}
<UserJourney Id="AuthsignalManageSecurity">
  <OrchestrationSteps>
    <!-- 1. Identify the user. Skipped when the user has a B2C session. -->
    <OrchestrationStep Order="1" Type="ClaimsExchange">
      <ClaimsExchanges>
        <ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
      </ClaimsExchanges>
    </OrchestrationStep>

    <!-- 2. Read the user's email address for the init auth request -->
    <OrchestrationStep Order="2" Type="ClaimsExchange">
      <ClaimsExchanges>
        <ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
      </ClaimsExchanges>
    </OrchestrationStep>

    <!-- 3. Store the B2C correlation ID in a claim -->
    <OrchestrationStep Order="3" Type="ClaimsExchange">
      <ClaimsExchanges>
        <ClaimsExchange Id="AuthsignalSetCorrelationId" TechnicalProfileReferenceId="Authsignal-ManageSecurity-SetCorrelationId" />
      </ClaimsExchanges>
    </OrchestrationStep>

    <!-- 4. Call Authsignal's init auth endpoint with the manage security action -->
    <OrchestrationStep Order="4" Type="ClaimsExchange">
      <ClaimsExchanges>
        <ClaimsExchange Id="AuthsignalInitAuth" TechnicalProfileReferenceId="Authsignal-ManageSecurity-InitAuth" />
      </ClaimsExchanges>
    </OrchestrationStep>

    <!-- 5. Send the user to Authsignal's pre-built UI -->
    <OrchestrationStep Order="5" Type="ClaimsExchange">
      <ClaimsExchanges>
        <ClaimsExchange Id="AuthsignalOidc" TechnicalProfileReferenceId="Authsignal-ManageSecurity-Oidc" />
      </ClaimsExchanges>
    </OrchestrationStep>

    <!-- 6. Show an error if the Authsignal user ID doesn't match the B2C user -->
    <OrchestrationStep Order="6" Type="ClaimsExchange">
      <Preconditions>
        <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
          <Value>authsignalSubjectMatches</Value>
          <Value>true</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
      </Preconditions>
      <ClaimsExchanges>
        <ClaimsExchange Id="AuthsignalSubjectMismatchError" TechnicalProfileReferenceId="Authsignal-ManageSecurity-Error" />
      </ClaimsExchanges>
    </OrchestrationStep>

    <!-- 7. Show an error if the flow didn't succeed -->
    <OrchestrationStep Order="7" Type="ClaimsExchange">
      <Preconditions>
        <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
          <Value>authsignalFlowSucceeded</Value>
          <Value>true</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
      </Preconditions>
      <ClaimsExchanges>
        <ClaimsExchange Id="AuthsignalFlowFailedError" TechnicalProfileReferenceId="Authsignal-ManageSecurity-Error" />
      </ClaimsExchanges>
    </OrchestrationStep>

    <OrchestrationStep Order="8" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
  </OrchestrationSteps>
  <ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
```

### Step 7: Add the relying party

Add the relying party after `<UserJourneys>`. It puts the action state and the correlation ID in the ID token.

```xml theme={null}
<RelyingParty>
  <DefaultUserJourney ReferenceId="AuthsignalManageSecurity" />
  <TechnicalProfile Id="PolicyProfile">
    <DisplayName>PolicyProfile</DisplayName>
    <Protocol Name="OpenIdConnect" />
    <OutputClaims>
      <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub" />
      <OutputClaim ClaimTypeReferenceId="authsignalActionState" PartnerClaimType="action_state" />
      <OutputClaim ClaimTypeReferenceId="authsignalCorrelationId" PartnerClaimType="correlation_id" />
      <OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
    </OutputClaims>
    <SubjectNamingInfo ClaimType="sub" />
  </TechnicalProfile>
</RelyingParty>
```

> [!NOTE]
>
> If your sign-in page uses JavaScript, for example for [passkey
> autofill](https://docs.authsignal.com/integrations/azure-ad-b2c/azure-ad-b2c-passkey.md), add
> `<UserJourneyBehaviors><ScriptExecution>Allow</ScriptExecution></UserJourneyBehaviors>` after
> `<DefaultUserJourney>`. Step 1 shows that page when the user has no B2C session.

### Step 8: Upload the policy and add the redirect URI

1. Upload the policy to your Azure AD B2C tenant.
2. In your app registration, add the redirect URI for the manage security response.

### Step 9: Add a manage security button to your app

When a signed-in user clicks the button, send an OpenID Connect authorization request for your manage security policy. Pass the user's email address in `login_hint`.

```
https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/oauth2/v2.0/authorize
  ?p=B2C_1A_MANAGE_SECURITY
  &client_id=YOUR_CLIENT_ID
  &response_type=code
  &scope=openid
  &redirect_uri=YOUR_REDIRECT_URI
  &login_hint=user@example.com
```

The [MFA example app](https://github.com/authsignal/azure-ad-b2c-example) does this with a second NextAuth.js provider in `pages/api/auth/[...nextauth].ts`.

### Step 10: Read the result

The ID token from the manage security policy contains these claims:

| Claim | Value |
| - | - |
| `sub` | The B2C user's object ID |
| `action_state` | `CHALLENGE_SUCCEEDED` when the user completed the flow |
| `correlation_id` | The B2C correlation ID, also stored on the Authsignal action as `custom.correlationId` |

If the flow fails or the user IDs don't match, the policy shows the error page and returns no token.

This ID token has no name or email address. Add the result to the session from sign-in. Don't replace that session.

## Next steps

* [Read the verification methods completed in a flow](https://docs.authsignal.com/integrations/azure-ad-b2c/azure-ad-b2c-mfa.md#optional-read-the-verification-methods-completed-in-a-flow)
* [Authsignal's custom policy code snippets](https://docs.authsignal.com/integrations/azure-ad-b2c/azure-ad-b2c-snippets.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.