Putting together a user journey in Azure AD B2C

A common question over on stackoverflow is how to put a user journey together i.e. hook together a series of screens that ask the user for input, display status etc.

A screen that asks a user for input in B2C is called a self-asserted technical profile (TP).

So we need a series of self-asserted TP. We will use the normal sign-up flow but after the user has entered their details, we will display two additional screens; collecting two extension attributes on the first screen and one on the second.

In addition, we want to display the email address entered during the normal sign-up flow on the first screen and make it read-only. To do this we need to copy one claim to another. The reason for these is that these are all common questions on stackoverflow so I’m killing a few birds with one stone!

As usual, the gist is here. I always put the custom code in the extension file.

For the first screen:

<TechnicalProfile Id="SelfAsserted-Screen1"><DisplayName>Screen1</DisplayName><Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=, Culture=neutral, PublicKeyToken=null" />
<Item Key="ContentDefinitionReferenceId">api.selfasserted</Item></Metadata>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
<InputClaimsTransformation ReferenceId="CopyEmail" />
<InputClaim ClaimTypeReferenceId="emailRO" />
<OutputClaim ClaimTypeReferenceId="emailRO" />
<OutputClaim ClaimTypeReferenceId="extension_Field1" Required="true" />
<OutputClaim ClaimTypeReferenceId="extension_Field2" Required="true" />
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />

The output claims define the fields on the form and we are making the two extension fields mandatory.

The read-only email claim is:

<ClaimType Id="emailRO">
<DisplayName>Email Address</DisplayName>
<UserHelpText>Email address that can be used to contact you.</UserHelpText>

To copy the email claim to the read-only one, we use a InputClaimsTransformations called “CopyEmail”.

<ClaimsTransformation Id="CopyEmail" TransformationMethod="FormatStringClaim"><InputClaims>
<InputClaim ClaimTypeReferenceId="email" TransformationClaimType="inputClaim" />
<InputParameter Id="stringFormat" DataType="string" Value="{0}" />
<OutputClaim ClaimTypeReferenceId="emailRO" TransformationClaimType="outputClaim" />

We call this in the TP for the first screen. Note that in order to pre-define a claim in an output field, you use an input field.

To make testing easier, I turned off email validation:

<Item Key="EnforceEmailVerification">False</Item>

With that background, the user journey is then:

<UserJourney Id="SignUpOrSignInScreens">
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin"><ClaimsProviderSelections>
<ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
<ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmailScreens" />
<!-- This step reads any user attributes that we may not have received when in the token. --><OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
<OrchestrationStep Order="4" Type="ClaimsExchange">
<ClaimsExchange Id="Screen1" TechnicalProfileReferenceId="SelfAsserted-Screen1" />
<OrchestrationStep Order="5" Type="ClaimsExchange">
<ClaimsExchange Id="Screen2" TechnicalProfileReferenceId="SelfAsserted-Screen2" />
<OrchestrationStep Order="6" Type="ClaimsExchange">
<ClaimsExchange Id="WriteScreen" TechnicalProfileReferenceId="AAD-UserWriteProfileUsingObjectIdScreen" />
<OrchestrationStep Order="7" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
<ClientDefinition ReferenceId="DefaultWeb" /></UserJourney>

We have the normal sign-up flow.

Then in step 4 we display the first screen, in step 5 we display the second screen and in step 6 we write the new extension attributes to Azure AD.

The Relying Party is:

<DefaultUserJourney ReferenceId="SignUpOrSignInScreens" />
<JourneyInsights TelemetryEngine="ApplicationInsights" InstrumentationKey="1959ac09-0bbd-4ffe-ba84-c794d8a6425b" DeveloperMode="true" ClientEnabled="false" ServerEnabled="true" TelemetryVersion="1.0.0" />
<TechnicalProfile Id="PolicyProfile">
<Protocol Name="OpenIdConnect" />
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="extension_Field3" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
<SubjectNamingInfo ClaimType="sub" />

and here we call the “SignUpOrSignInScreens” user journey that we defined above.

Note that we are returning “extension_Field3” in the JWT.

When we run this:

We select “Sign up now”.

We enter the normal fields for sign-up and click “Create”

Now we see the first screen. Notice that the email address has been copied over and is read-only.

When we enter the extensions attributes and then click “Continue”, we see the second screen.

When we enter the extensions attribute and then click “Continue”, we see the JWT returned.

Notice that extension_Field3 was returned.

The only button displayed on the screens is “Continue”. If I wanted to add the “Cancel” button as well, this can be done by using:


When I run the utility to read Azure AD, I see:

"signInNames": [
"type": "emailAddress",
"value": "gordon@company.com"
"sipProxyAddress": null,
"state": null,
"streetAddress": null,
"surname": "Smith",
"telephoneNumber": null,
"userState": null,
"userStateChangedOn": null,
"userType": "Member",
"extension_51f...e4e_Field3": "extension3_ccc",
"extension_51f...e4e_Field2": "extension2_bbb",
"extension_51f...e4e_Field1": "extension1_aaa"

We see the extension values entered on the screens during the user journey proving that they were saved.

All good!




“Identity is the new control plane”. Articles around Microsoft Identity, Auth0 and identityserver. Click the “Archive” link at the bottom for more posts.

Recommended from Medium

5 CSS tools that will facilitate your web developer work

Debug CSS Chrome Extension being used in caniuse

Article Spinning with Python

Overview of the Aleo!

Oh, there is a difference - Project, Program, and Portfolio Management?

W3F approved CESS Lab’s grant proposal for providing a CESS storage pallet for Substrate

getting Firefox/Chrome to trust your internal websites (internal Certificate Authority)

How to Connect Your Ruby on Rails App to the Shopify API

Doing Shots with my Daughter

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Rory Braybrook

Rory Braybrook

NZ Microsoft Identity dude and MVP. Azure AD/B2C/ADFS/Auth0/identityserver. StackOverflow: https://bit.ly/2XU4yvJ Presentations: http://bit.ly/334ZPt5

More from Medium

Fixing one of the Azure AD B2C samples that blocks a disabled federated user.

Image showing “No entry”.

Hotwired ASP.NET Core Web Application — Part 4

How to use Replace Tokens in Azure Pipelines

Using Azure Web Apps (for Free) with Oracle Autonomous Database (without Wallets)