Authsignal provides a simple integration with Azure AD B2C via our Open ID Connect (OIDC) endpoints.

With this integration you can use Azure AD B2C’s custom policies and technical profiles to orchestrate passwordless login and adaptive MFA through Authsignal’s pre-built UI.

The guide below outlines how to use Authsignal as a MFA provider for Azure AD B2C. Authsignal provides other utility endpoints to achieve further customization, see Authsignal’s custom policy code snippets.

Authsignal integrates with Azure AD B2C by acting as an OIDC provider - this integration model can also be used for other platforms which support OIDC.

Prerequisites

This guide assumes that you have already set up an Azure AD B2C tenant and are using custom policies. If not, it is recommended to familiarize yourself with Azure AD B2C custom policies and to follow Microsoft’s official getting started with custom policies guide before continuing.

We also assume that you have an Authsignal tenant created, with at least one Authenticator enabled. Enable an Authenticator on your Authsignal tenant here.

Sequence

The following sequence diagram demonstrates the necessary orchestration steps and corresponding requests when using Authsignal as an MFA provider for Azure AD B2C.

Code example

You can find a full code example referenced in this guide on Github.

This guide builds on top of the SignUpOrSignIn policy from the Azure AD B2C starter pack.

Step by step guide

Step 1: Add orchestration steps to your user journey

Add the following five steps to your user journey. These steps should be performed after the user has been identified, but before the user is authenticated and issued a token.

<!-- NOTE: Change the Order attribute accordingly, adjusting other orchestration steps in your user journey as necessary -->

<!-- A. Call Authsignal's OIDC init auth endpoint to generate a short lived token representing the user.
This is a prerequisite before federating the flows to Authsignal via the OIDC Authorize technical profile in the step below  -->
<OrchestrationStep Order="4" Type="ClaimsExchange">
    <ClaimsExchanges>
        <ClaimsExchange Id="AuthsignalOidcInitAuth" TechnicalProfileReferenceId="authsignalOidcInitAuth"/>
    </ClaimsExchanges>
</OrchestrationStep>

<!-- B. Feed the token returned by the init auth endpoint to Authsignal's OIDC authorize endpoint -->
<OrchestrationStep Order="5" Type="ClaimsExchange">
    <ClaimsExchanges>
        <ClaimsExchange Id="AuthsignalOidcAuth" TechnicalProfileReferenceId="authsignalOidcAuth"/>
    </ClaimsExchanges>
</OrchestrationStep>

<!-- C. Defensively check that the id token sub (aka the Authsignal userId) matches the objectId we are currently processing -->
<OrchestrationStep Order="6" Type="ClaimsExchange">
  <Preconditions>
    <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
      <Value>isMatchingUserId</Value>
      <Value>True</Value>
      <Action>SkipThisOrchestrationStep</Action>
    </Precondition>
  </Preconditions>
  <ClaimsExchanges>
    <ClaimsExchange Id="ShowUserIdMismatchErrorPage" TechnicalProfileReferenceId="SelfAsserted-AuthenticationError" />
  </ClaimsExchanges>
</OrchestrationStep>

<!-- D. Check that the authentication challenge has been successful -->
<OrchestrationStep Order="7" Type="ClaimsExchange">
  <Preconditions>
    <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
      <Value>isChallengeSuccessful</Value>
      <Value>true</Value>
      <Action>SkipThisOrchestrationStep</Action>
    </Precondition>
  </Preconditions>
  <ClaimsExchanges>
    <ClaimsExchange Id="ShowChallengeFailedPage" TechnicalProfileReferenceId="SelfAsserted-AuthenticationError" />
  </ClaimsExchanges>
</OrchestrationStep>

<!-- E. Check that the user has enrolled an authenticator -->
<OrchestrationStep Order="8" Type="ClaimsExchange">
  <Preconditions>
    <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
      <Value>authsignalEnrolled</Value>
      <Value>true</Value>
      <Action>SkipThisOrchestrationStep</Action>
    </Precondition>
  </Preconditions>
  <ClaimsExchanges>
    <ClaimsExchange Id="ShowUserAuthenticatorNotEnrolledErrorPage" TechnicalProfileReferenceId="SelfAsserted-NotEnrolledError" />
  </ClaimsExchanges>
</OrchestrationStep>

See the orchestration steps in context of a custom policy file

Step 2: Add the base technical profile

This is the base technical profile that is used to connect to the Authsignal Connect (OIDC) API by setting the authorization header, request body and other necessary configuration for the Azure AD B2C’s RestfulProvider. It is referenced by other technical profiles.

<TechnicalProfile Id="AuthsignalConnectApiBase">
    <DisplayName>Authsignal Connect API Base</DisplayName>
    <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
    <Metadata>
        <Item Key="SendClaimsIn">Body</Item>
        <Item Key="AuthenticationType">Basic</Item>
        <Item Key="AllowInsecureAuthInProduction">false</Item>
        <Item Key="ResolveJsonPathsInJsonTokens">true</Item>
        <Item Key="ClaimUsedForRequestPayload">requestBody</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>
    <InputClaims>
        <InputClaim ClaimTypeReferenceId="requestBody" />
    </InputClaims>
</TechnicalProfile>

See the base technical profile in context of a custom policy file

Step 3: Add the init auth technical profile and corresponding input claims transformation

Replace the AUTHSIGNAL_CONNECT_HOSTNAME and INSERT_AUTHSIGNAL_ACTION placeholders with the appropriate values.

A list of available hostnames can be found in the OIDC documentation. You should use the hostname where your Authsignal tenant is located.

Technical profile:

<TechnicalProfile Id="AuthsignalOidcInitAuth">
    <DisplayName>Authsignal OIDC Init Auth API</DisplayName>
    <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
    <!-- NOTE: Change the host name below -->
    <Metadata>
        <Item Key="ServiceUrl">https://AUTHSIGNAL_CONNECT_HOSTNAME/init-auth</Item>
    </Metadata>
    <InputClaimsTransformations>
        <InputClaimsTransformation ReferenceId="AuthsignalCreateInitAuthRequestBody"/>
    </InputClaimsTransformations>
    <InputClaims>
        <InputClaim ClaimTypeReferenceId="requestBody"/>
    </InputClaims>
    <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="bearerToken" PartnerClaimType="token"/>
    </OutputClaims>
    <IncludeTechnicalProfile ReferenceId="AuthsignalConnectApiBase"/>
</TechnicalProfile>

See the init auth technical profile in context of a custom policy file

Input Claims Transformation:

Replace the INSERT_AUTHSIGNAL_ACTION placeholder with the desired Authsignal action.

<ClaimsTransformation Id="AuthsignalCreateInitAuthRequestBody" TransformationMethod="GenerateJson">
    <InputClaims>
        <InputClaim ClaimTypeReferenceId="objectId" TransformationClaimType="userId" />
        <InputClaim ClaimTypeReferenceId="signInNames.emailAddress" TransformationClaimType="email" />
    </InputClaims>
    <InputParameters>
        <!-- NOTE: Change the action value here to the corresponding Authsignal action your user is performing. This may be signIn, signUp, manageSettings etc. -->
        <InputParameter Id="action" DataType="string" Value="INSERT_AUTHSIGNAL_ACTION" />
        <InputParameter Id="redirectToSettings" DataType="boolean" Value="false" />
    </InputParameters>
    <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="requestBody" TransformationClaimType="outputClaim" />
    </OutputClaims>
</ClaimsTransformation>

See the init auth input claims transformation in context of a custom policy file

Step 4: Add the OIDC authorize technical profile and corresponding output claims transformations

Replace the AUTHSIGNAL_CONNECT_HOSTNAME and INSERT_AUTHSIGNAL_TENANT_ID placeholders with the appropriate values.

Technical profile:

<TechnicalProfile Id="AuthsignalOidcAuth">
    <DisplayName>Authsignal OIDC Authorize API</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="bearerToken" PartnerClaimType="token" />
    </InputClaims>
    <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="actionCode" PartnerClaimType="action_code" />
        <OutputClaim ClaimTypeReferenceId="actionState" PartnerClaimType="action_state" />
        <OutputClaim ClaimTypeReferenceId="authsignalUserId" PartnerClaimType="sub" />
        <OutputClaim ClaimTypeReferenceId="authsignalEnrolled" PartnerClaimType="is_enrolled" />
    </OutputClaims>
    <OutputClaimsTransformations>
        <OutputClaimsTransformation ReferenceId="AuthsignalCheckIsChallengeSuccessful" />
        <OutputClaimsTransformation ReferenceId="AuthsignalCheckMatchingUserId" />
    </OutputClaimsTransformations>
</TechnicalProfile>

See the OIDC authorize technical profile in context of a custom policy file

Output Claims Transformations:

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

<ClaimsTransformation Id="AuthsignalCheckIsChallengeSuccessful" TransformationMethod="LookupValue">
    <InputClaims>
        <InputClaim ClaimTypeReferenceId="actionState" TransformationClaimType="inputParameterId" />
    </InputClaims>
    <InputParameters>
        <InputParameter Id="ALLOW" DataType="string" Value="true" />
        <InputParameter Id="CHALLENGE_SUCCEEDED" DataType="string" Value="true" />
        <InputParameter Id="CHALLENGE_FAILED" DataType="string" Value="false" />
        <InputParameter Id="BLOCK" DataType="string" Value="false" />
        <InputParameter Id="errorOnFailedLookup" DataType="boolean" Value="false" />
    </InputParameters>
    <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="isChallengeSuccessful" TransformationClaimType="outputClaim" />
    </OutputClaims>
</ClaimsTransformation>

See the OIDC authorize output claims transformation in context of a custom policy file

Step 5: Add technical profiles for the error pages

These are basic error pages shown when the authentication challenge is failed. It is using the default api.selfasserted content definition. You may want to customize this page to suit your branding.

<TechnicalProfile Id="SelfAsserted-NotEnrolledError">
  <DisplayName>Not enrolled error</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="errorMessage" DefaultValue="You must register an authentication method to continue."/>
  </InputClaims>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="errorMessage" Required="true"/>
  </OutputClaims>
</TechnicalProfile>

<TechnicalProfile Id="SelfAsserted-AuthenticationError">
  <DisplayName>Authentication 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="errorMessage" DefaultValue="Authentication challenge failed. Please try again."/>
  </InputClaims>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="errorMessage" Required="true"/>
  </OutputClaims>
  </TechnicalProfile>
</TechnicalProfiles>

See the error technical profile in context of a custom policy file

Step 6: Declare the claims used by the technical profiles

<ClaimType Id="requestBody">
    <DisplayName>requestBody</DisplayName>
    <DataType>string</DataType>
</ClaimType>

<ClaimType Id="bearerToken">
    <DisplayName>bearerToken</DisplayName>
    <DataType>string</DataType>
</ClaimType>

<ClaimType Id="actionCode">
    <DisplayName>actionCode</DisplayName>
    <DataType>string</DataType>
</ClaimType>

<ClaimType Id="actionState">
    <DisplayName>actionState</DisplayName>
    <DataType>string</DataType>
</ClaimType>

<ClaimType Id="isMatchingUserId">
    <DataType>boolean</DataType>
</ClaimType>

<ClaimType Id="isChallengeSuccessful">
    <DataType>string</DataType>
</ClaimType>

<ClaimType Id="authsignalUserId">
    <DataType>string</DataType>
</ClaimType>

<ClaimType Id="authsignalEnrolled">
    <DataType>boolean</DataType>
</ClaimType>

<ClaimType Id="errorMessage">
    <DisplayName></DisplayName>
    <DataType>string</DataType>
    <UserInputType>Paragraph</UserInputType>
</ClaimType>

See the claims in context of a custom policy file

Step 7: Store your Authsignal Tenant Secret on Azure AD B2C

Your Authsignal tenant secret is stored as a policy key on Azure AD B2C’s Identity Experience Framework and referenced by our technical profile with the Id B2C_1A_AuthsignalSecret.

You can find the secret key for your tenant in the Authsignal Admin Portal and add it to your Azure AD B2C tenant as a policy key via the Azure Portal.

Step 8: Upload your custom policy and test your integration

You can now test your integration by navigating to your custom policy in the Azure AD B2C portal and clicking “Run now”. You should see the Authsignal UI for MFA after enrolling your user and for subsequent login attempts.

Congratulations!

You have successfully integrated Authsignal with Azure AD B2C for MFA.

Next steps