Exploring SubJourneys in Azure AD B2C

Rory Braybrook
The new control plane
4 min readMar 1, 2021

The official docs are here.

I based this on the “Sign-up and sign-in with embedded password reset” policy simply because this already had a sub journey within the custom policy.

(BTW. This policy demonstrates how to embed the password reset flow in a part of the sign-up or sign-in policy without the AADB2C90118 error message). Very neat!

“A sub journey is a grouping of orchestration steps that can be invoked at any point within a user journey. You can use sub journeys to create reusable step sequences or implement branching to better represent the business logic”.

i.e. they are a sort of subroutine.

Most SUSI flows have a similar structure e.g.

<UserJourneys>
<UserJourney Id="SignUpOrSignInWithEmbeddedPasswordReset">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
<ClaimsProviderSelection TargetClaimsExchangeId="FacebookExchange" />
<!--Sample: password reset button-->
<ClaimsProviderSelection TargetClaimsExchangeId="PasswordResetExchange" />
<ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
</ClaimsProviderSelections>
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
</ClaimsExchanges>
</OrchestrationStep>

<!-- Check if the user has selected to sign in using one of the social providers -->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="FacebookExchange" TechnicalProfileReferenceId="Facebook-OAUTH" />
<ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail" />
<!--Sample: Start the password reset flow-->
<ClaimsExchange Id="PasswordResetExchange" TechnicalProfileReferenceId="StartPasswordResetFlow" />
</ClaimsExchanges>
</OrchestrationStep>

We have “SelfAsserted-LocalAccountSignin-Email” followed by “StartPasswordResetFlow”.

So we could replace this with:

<!-- Phase1 -->
<OrchestrationStep Order="1" Type="InvokeSubJourney">
<JourneyList>
<Candidate SubJourneyReferenceId="Phase1"/>
</JourneyList>
</OrchestrationStep>

where “Phase1” is:

<SubJourney Id="Phase1" Type="Call">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
<!-- <ClaimsProviderSelection TargetClaimsExchangeId="FacebookExchange"/> -->
<!-- Password reset button -->
<ClaimsProviderSelection TargetClaimsExchangeId="PasswordResetExchange"/>
<ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange"/>
</ClaimsProviderSelections>
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email"/>
</ClaimsExchanges>
</OrchestrationStep>
<!-- Check if the user has selected to sign in using one of the social providers -->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<!-- <ClaimsExchange Id="FacebookExchange" TechnicalProfileReferenceId="Facebook-OAUTH"/> -->
<ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail"/>
<!-- Start the password reset flow -->
<ClaimsExchange Id="PasswordResetExchange" TechnicalProfileReferenceId="StartPasswordResetFlow"/>
</ClaimsExchanges>
</OrchestrationStep>
</OrchestrationSteps>
</SubJourney>

Note: I commented out the Facebook / social stuff for clarity. The basic points still stand.

Similarly, we could have:

<SubJourney Id="Phase2" Type="Call">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SelfAsserted-Social" TechnicalProfileReferenceId="SelfAsserted-Social"/>
</ClaimsExchanges>
</OrchestrationStep>
<!-- This step reads any user attributes that we may not have received when authenticating using ESTS so they can be sent
in the token. -->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>authenticationSource</Value>
<Value>socialIdpAuthentication</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId"/>
</ClaimsExchanges>
</OrchestrationStep>
</OrchestrationSteps>
</SubJourney>

as another sub journey.

Which make the whole user journey look like:

<!-- Phase1 -->
<OrchestrationStep Order="1" Type="InvokeSubJourney">
<JourneyList>
<Candidate SubJourneyReferenceId="Phase1"/>
</JourneyList>
</OrchestrationStep>
<!-- Start the resets the password flow -->
<OrchestrationStep Order="2" Type="InvokeSubJourney">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>isPasswordResetFlow</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<JourneyList>
<Candidate SubJourneyReferenceId="PasswordReset"/>
</JourneyList>
</OrchestrationStep>

<!-- Phase2 -->
<OrchestrationStep Order="3" Type="InvokeSubJourney">
<JourneyList>
<Candidate SubJourneyReferenceId="Phase2"/>
</JourneyList>
</OrchestrationStep>

Finishing off with:

<OrchestrationStep Order="4" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer"/>
</OrchestrationSteps>

Effectively, we’ve halved the number of steps and we can use these sub journeys across a number of user journeys.

You can also put all the sub journeys into an extension file. This could then essentially be a “library”.

So you would have:

RP -> sub journey extension -> extension -> base

The only problem here is that if the sub journeys include things like claims or claims providers that are defined in the RP file then you have to move them up into the sub journey extension file so I’m not sure how practical this idea is?

“There are two types of sub journeys:

  • Call — Returns control to the caller. The sub journey executes, and then control is returned to the orchestration step that is currently executing within the user journey.
  • Transfer — Transfers control to the sub journey (irreversible branching). The sub journey must have a SendClaims step to return the claims back to the relying party application”.

The sub journeys above use “call”.

<SubJourney Id="Phase1" Type="Call">

There are three sub journeys above followed by a “SendClaims”.

<OrchestrationStep Order="3" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer"/>

We can move “SendClaims” into the “Phase2” sub journey and then change the type to “Transfer”.

When you run this you get:

AADB2C90040: User journey ‘SignUpOrSignInWithEmbeddedPasswordReset’ does not contain a send claims step.

I was using the library approach so perhaps this doesn’t work?

If I add the “SendClaims” back into the user journey as well, I get:

User journey ‘SignUpOrSignInWithEmbeddedPasswordReset’ in policy ‘B2C_1A_SUSI_Embedded_Pwd_Reset_UserJ’ of tenant ‘azureug.onmicrosoft.com’ has 2 sendClaims steps.

The “library” approach does have some downsides 😢

As an aside, notice how “LocalAccountSigninEmailExchange” and “SelfAsserted-LocalAccountSignin-Email” are combined in one step when often they use two.

<OrchestrationSteps>
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
<ClaimsProviderSelection TargetClaimsExchangeId="FacebookExchange" />
<!--Sample: password reset button-->
<ClaimsProviderSelection TargetClaimsExchangeId="PasswordResetExchange" />
<ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
</ClaimsProviderSelections>
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
</ClaimsExchanges>
</OrchestrationStep>

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