Putting together a user journey in Azure AD B2C

Rory Braybrook
The new control plane
4 min readOct 28, 2019

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=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ContentDefinitionReferenceId">api.selfasserted</Item></Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CopyEmail" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="emailRO" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="emailRO" />
<OutputClaim ClaimTypeReferenceId="extension_Field1" Required="true" />
<OutputClaim ClaimTypeReferenceId="extension_Field2" Required="true" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>

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>
<DataType>string</DataType>
<UserHelpText>Email address that can be used to contact you.</UserHelpText>
<UserInputType>Readonly</UserInputType>
</ClaimType>

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" />
</InputClaims>
<InputParameters>
<InputParameter Id="stringFormat" DataType="string" Value="{0}" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="emailRO" TransformationClaimType="outputClaim" />
</OutputClaims></ClaimsTransformation>

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">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin"><ClaimsProviderSelections>
<ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
</ClaimsProviderSelections>
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmailScreens" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- This step reads any user attributes that we may not have received when in the token. --><OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="4" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="Screen1" TechnicalProfileReferenceId="SelfAsserted-Screen1" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="5" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="Screen2" TechnicalProfileReferenceId="SelfAsserted-Screen2" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="6" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="WriteScreen" TechnicalProfileReferenceId="AAD-UserWriteProfileUsingObjectIdScreen" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="7" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<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:

<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpOrSignInScreens" />
<UserJourneyBehaviors>
<JourneyInsights TelemetryEngine="ApplicationInsights" InstrumentationKey="1959ac09-0bbd-4ffe-ba84-c794d8a6425b" DeveloperMode="true" ClientEnabled="false" ServerEnabled="true" TelemetryVersion="1.0.0" />
</UserJourneyBehaviors>
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<OutputClaims>
<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}" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>

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:

setting.showCancelButton

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!

--

--

Rory Braybrook
The new control plane

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