I am using custom policies to do some User Journeys and using SocialAndLocalAccountsWithMfa. In one of the step I am asking the user for their email address
I am using "LocalAccountDiscoveryUsingEmailAddress" to get their email address on the first screen. And then depending on if they are registered for MFA they are sent to Mobile OTP screen or sent to the mail address OTP screen.
Now what happens is that after they put their email address and press ok (and they are sent to the emial OTP screen) they are presented again with another screen to put their email address again to verify. I am looking for two possibilites here
1) (Preferred) They are immediately sent an email OTP - so they don't have to type their email address and then click on "verify emial" to send OTP
Or
2) Their email address is populated in the screen already so they don't have to type it again and thus all they have to do is click on "Verify Email" button.
My Userjourney for this looks something like
<UserJourney Id="PasswordReset">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="PasswordResetUsingEmailAddress" TechnicalProfileReferenceId="LocalAccountDiscoveryUsingEmailAddress" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>strongAuthenticationPhoneNumber</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="PasswordResetUsingEmailAddressExchange" TechnicalProfileReferenceId="LocalAccountDiscoveryUsingEmailAddressOTP" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>strongAuthenticationPhoneNumber</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="PhoneFactor-Verify" TechnicalProfileReferenceId="PhoneFactor-InputOrVerify" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="4" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="NewCredentials" TechnicalProfileReferenceId="LocalAccountWritePasswordUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="5" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
Firstly, for #2, you can implement a technical profile that accepts the email address as an input claim so that it is pre-filled in the self-asserted page, such as:
<TechnicalProfile Id="SelfAsserted-LocalAccount-EmailVerification">
<DisplayName>Local Account Email Address Verification</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.localaccount.emailverification</Item>
<Item Key="EnforceEmailVerification">true</Item>
</Metadata>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CreateReadonlyEmailClaim" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="readonlyEmail" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="readonlyEmail" PartnerClaimType="verified.email" Required="true" />
</OutputClaims>
</TechnicalProfile>
This technical profile is referring to a read-only email address so that the end user can't change the email address for the OTP verification.
The CreateReadonlyEmailClaim claims transformation is defined as:
<ClaimsTransformation Id="CreateReadonlyEmailClaim" TransformationMethod="FormatStringClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" TransformationClaimType="inputClaim" />
</InputClaims>
<InputParameters>
<InputParameter Id="stringFormat" DataType="string" Value="{0}" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="readonlyEmail" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
The readonlyEmail claim type is declared as:
<ClaimType Id="readonlyEmail">
<DisplayName>E-mail Address</DisplayName>
<DataType>string</DataType>
<UserInputType>Readonly</UserInputType>
</ClaimType>
For #1, you can implement the above changes as well as implement a JavaScript function in a custom page UI to "click" the Verify Email button to initiate the OTP verification.
Related
I have a current policy that I need to send a confirmation email. I have a send grid account that is currently delivering a verification email and its working fine.
In my password reset flow, this is what I have.
<SubJourneys>
<SubJourney Id="PasswordReset" Type="Call">
<OrchestrationSteps>
<!-- Validate user's email address. -->
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="PasswordResetUsingEmailAddressExchange" TechnicalProfileReferenceId="LocalAccountDiscoveryUsingEmailAddress" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- Show TOU-->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="SelfAssertedConsentExchange" TechnicalProfileReferenceId="SelfAsserted-PasswordResetConsent" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- Collect and persist a new password. -->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="NewCredentials" TechnicalProfileReferenceId="LocalAccountWritePasswordUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="4" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="GetAccessTokenPwdChangeNotification" TechnicalProfileReferenceId="SendPasswordChangeEmail" />
</ClaimsExchanges>
</OrchestrationStep>
</OrchestrationSteps>
</SubJourney>
In step one of this flow there is a verification email that takes place that uses send grid and it works fine.
step 4 is where the process seems to fail.
There is the technical profile for step 4
<ClaimsProvider>
<DisplayName>RestfulProvider</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="SendPasswordChangeEmail">
<DisplayName>Use SendGrid's email API to send the code the the user</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ServiceUrl">https://api.sendgrid.com/v3/mail/send</Item>
<Item Key="AuthenticationType">Bearer</Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="ClaimUsedForRequestPayload">emailChangedBody</Item>
</Metadata>
<CryptographicKeys>
<Key Id="BearerAuthenticationToken" StorageReferenceId="B2C_1A_SendGridSecret" />
</CryptographicKeys>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="GenerateEmailChangedBody" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="emailChangedBody" />
</InputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
That references this claims transformation.
<ClaimsTransformation Id="GenerateEmailChangedBody" TransformationMethod="GenerateJson">
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" TransformationClaimType="personalizations.0.to.0.email" />
</InputClaims>
<InputParameters>
<!-- Update the template_id value with the ID of your SendGrid template. -->
<InputParameter Id="template_id" DataType="string" Value="my template"/>
<InputParameter Id="from.email" DataType="string" Value="my email"/>
<!-- Update with a subject line appropriate for your organization. -->
<InputParameter Id="personalizations.0.dynamic_template_data.subject" DataType="string" Value="Account Password Changed"/>
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="emailChangedBody" TransformationClaimType="outputClaim"/>
</OutputClaims>
</ClaimsTransformation>
I am getting a bad request when this goes out.
My ONLY guess is that the email is not getting passed into that claims transformation so it does not know where to send the email.
Any help would be appreicated.
I want the below flow for authentication with the Azure B2C custom policy
user should see field, where user enters his email id
based on the email id (domain name), we decide the claims exchange to authenticate the user with.
available claims provider - local account, sign in with Azure B2B AD Tenant.
For point 2, we can parse the domain using Parse Domain Claims Transformation.
For point 3, I have already setup the necessary Claims Provider and verified it works, using the default Signing with Local and Social starter pack.
You can create a user journey such as this.
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="SelfAsserted-HRD" TechnicalProfileReferenceId="SelfAsserted-HRD" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>domainName</Value>
<Value>contoso.com</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AAD-OIDC" TechnicalProfileReferenceId="AAD-OIDC" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>domainName</Value>
<Value>contoso.com</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsProviderSelections>
<ClaimsProviderSelection ValidationClaimsExchangeId="SelfAsserted-LocalAccountSignin-Email" />
</ClaimsProviderSelections>
<ClaimsExchanges>
<ClaimsExchange Id="SelfAsserted-LocalAccountSignin-Email" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
</ClaimsExchanges>
</OrchestrationStep>
...
</OrchestrationSteps>
The first orchestration steps executes the SelfAsserted-HRD technical profile, at which an e-mail address can be entered, and then invokes the SetDomainName claims transformation that parses the e-mail domain.
<ClaimsProvider>
<DisplayName>Self-Asserted</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="SelfAsserted-HRD">
<DisplayName>Self-Asserted HRD</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>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" Required="true" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="SetDomainName" />
</OutputClaimsTransformations>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
If the e-mail domain is equal to the federated domain, then the second orchestration step executes the AAD-OIDC technical profile that redirects the external identity provider.
If the e-mail domain is not equal to the federated domain, then the third orchestration step executes the SelfAsserted-LocalAccountSignin-Email technical profile that shows the local account sign-up or sign-in page.
I am trying to create an Azure AD B2C custom policy that has the following user journey -
Sign-in / Sign-up with Local Account and Social Accounts wherein the sign-up flow must split the email verification and the actual sign-up page.
To do this, I started with the sample policy - https://github.com/azure-ad-b2c/samples/tree/master/policies/sign-up-with-social-and-local-account
and added the EmailVerification and LocalAccountSignUpWithReadOnlyEmail technical profiles from the sample policy - https://github.com/azure-ad-b2c/samples/tree/master/policies/split-email-verification-and-signup
In order to trigger the split email verification and signup flow, I have set the SignUpTarget to EmailVerification.
I am able to see the sign-in/sign-up page and clicking on the sign up link triggers the email verification flow. However, I am not sure how to get the LocalAccountSignUpWithReadOnlyEmail technical profile triggered after the email verification. Adding this as part of a ClaimsExchange orchestration step causes validation errors while uploading my custom policy.
Here is how my user journey configuration looks like -
<UserJourneys>
<UserJourney Id="SignUpOrSignIn">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
<ClaimsProviderSelection TargetClaimsExchangeId="FacebookExchange" />
<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="EmailVerification" TechnicalProfileReferenceId="EmailVerification" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSignUpWithReadOnlyEmail" TechnicalProfileReferenceId="LocalAccountSignUpWithReadOnlyEmail" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- For social IDP authentication, attempt to find the user account in the directory. -->
<OrchestrationStep Order="4" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>authenticationSource</Value>
<Value>localAccountAuthentication</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadUsingAlternativeSecurityId" TechnicalProfileReferenceId="AAD-UserReadUsingAlternativeSecurityId-NoError" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- Show self-asserted page only if the directory does not have the user account already (i.e. we do not have an objectId).
This can only happen when authentication happened using a social IDP. If local account was created or authentication done
using ESTS in step 2, then an user account must exist in the directory by this time. -->
<OrchestrationStep Order="5" 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="6" 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>
<!-- The previous step (SelfAsserted-Social) could have been skipped if there were no attributes to collect
from the user. So, in that case, create the user in the directory if one does not already exist
(verified using objectId which would be set from the last step if account was created in the directory. -->
<OrchestrationStep Order="7" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADUserWrite" TechnicalProfileReferenceId="AAD-UserWriteUsingAlternativeSecurityId" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="8" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
</UserJourneys>
Here is what the technical profiles look like -
<ClaimsProviders>
<ClaimsProvider>
<DisplayName>Email Verification</DisplayName>
<TechnicalProfiles>
<!--Sample: Email verification only-->
<TechnicalProfile Id="EmailVerification">
<DisplayName>Initiate Email Address Verification For Local Account</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.localaccountsignup</Item>
<Item Key="language.button_continue">Continue</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
</OutputClaims>
</TechnicalProfile>
<!-- This technical profile uses a validation technical profile to authenticate the user. -->
<TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Email">
<DisplayName>Local Account Signin</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="SignUpTarget">EmailVerification</Item>
<Item Key="setting.operatingMode">Email</Item>
<Item Key="ContentDefinitionReferenceId">api.selfasserted</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="signInName" Required="true" />
<OutputClaim ClaimTypeReferenceId="password" Required="true" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="login-NonInteractive" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>Local Account</DisplayName>
<TechnicalProfiles>
<!--Sample: Sign-up self-asserted technical profile without Email verification-->
<TechnicalProfile Id="LocalAccountSignUpWithReadOnlyEmail">
<DisplayName>Email signup</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="IpAddressClaimReferenceId">IpAddress</Item>
<Item Key="ContentDefinitionReferenceId">api.localaccountsignup</Item>
<Item Key="language.button_continue">Create</Item>
<!-- Sample: Remove sign-up email verification -->
<Item Key="EnforceEmailVerification">False</Item>
</Metadata>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CreateReadonlyEmailClaim" />
</InputClaimsTransformations>
<InputClaims>
<!--Sample: Set input the ReadOnlyEmail claim type to prefilled the email address-->
<InputClaim ClaimTypeReferenceId="readOnlyEmail" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" />
<!-- Sample: Display the ReadOnlyEmail claim type (instead of email claim type)-->
<OutputClaim ClaimTypeReferenceId="readOnlyEmail" Required="true" />
<OutputClaim ClaimTypeReferenceId="newPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
<OutputClaim ClaimTypeReferenceId="newUser" />
<!-- Optional claims, to be collected from the user -->
<!--OutputClaim ClaimTypeReferenceId="displayName" /-->
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surName" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
</ValidationTechnicalProfiles>
<!-- Sample: Disable session management for sign-up page -->
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
</ClaimsProviders>
Here is the error that I get when I tried to upload the policy -
Validation failed: 4 validation error(s) found in policy "B2C_1A_CUSTOM_SIGNUP_SIGNIN" of tenant "testtenant.onmicrosoft.com".User journey "SignUpOrSignIn" in policy "B2C_1A_custom_signup_signin" of tenant "testtenant.onmicrosoft.com" has step 3 with 2 claims exchanges. It must be preceded by a claims provider selection in order to determine which claims exchange can be used.User journey "SignUpOrSignIn" in policy "B2C_1A_custom_signup_signin" of tenant "testtenant.onmicrosoft.com" has step 4 with 2 claims exchanges. It must be preceded by a claims provider selection in order to determine which claims exchange can be used.User journey "SignUpOrSignIn" in policy "B2C_1A_custom_signup_signin" of tenant "testtenant.onmicrosoft.com" has step 5 with 2 claims exchanges. It must be preceded by a claims provider selection in order to determine which claims exchange can be used.User journey "SignUpOrSignIn" in policy "B2C_1A_custom_signup_signin" of tenant "testtenant.onmicrosoft.com" has step 6 with 2 claims exchanges. It must be preceded by a claims provider selection in order to determine which claims exchange can be used.
Looking for some advice here...
The reason why you are getting this error is because you might have written User Journey ID SignUpOrSignIn in 2 files: Base/Extension and Replying Party Policy.
If the count of steps and ClaimsExchange ID is unique, then it will accept or else it will treat as 2 different ClaimsExchange and error will occur while uploading the RP Policy. Please make sure to not duplicate the User Journey, keep only one copy of the User Journey Steps or if you want to extend the Journey steps, then add the steps. For Example: In the Base Policy you have total 5 Steps, then in the Extension or in RP you can start adding new ClaimsExchange from 5th Step and the last step will be JwtIssuer/SamlIssuer.
I've followed the detailed advice here to add a custom policy to my Azure B2C service which is designed to populate a groups claim via an API during the authentication flow.
I've built this on top of a fresh B2C instance and apart from my modifications, the custom policies are those available in the Azure sample here. I'm just using the local accounts sample and my modifications target the SignUpOrSignIn custom policy. For now, all my changes are in the TrustFrameworkBase.xml file.
When I test the policy via the portal with a redirect URI set to https://jwt.ms/ my resultant token does not include a groups claim at all. However, via Application Insights I can see my REST api being called with the correct parameter and according to its logs is successfully loading the user groups and returning the expected result.
Is there anything obvious I'm doing wrong here? This is the user journey I've changed:
<UserJourney Id="SignUpOrSignIn">
<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="LocalAccountSignUpWithLogonEmail" />
</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="GetUserGroups" TechnicalProfileReferenceId="GetUserGroups" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="5" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
My only change here was to change the order of the SendClaims step to 5 and add a new step 4. This references a GetUserGroups technical profile which I added at the end of the technical profiles under the "Azure Active Directory" claims provider (I wasn't sure if this was correct). It looks like this:
<TechnicalProfile Id="GetUserGroups">
<DisplayName>Retrieves security groups assigned to the user</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ServiceUrl">https://<redacted>.azurewebsites.net/api/UserGroups?code=<auth-code></Item>
<Item Key="AuthenticationType">None</Item>
<Item Key="SendClaimsIn">QueryString</Item>
<Item Key="AllowInsecureAuthInProduction">true</Item>
</Metadata>
<InputClaims>
<InputClaim Required="true" ClaimTypeReferenceId="objectId" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="groups" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
The service URL is an Azure function which accepts a user object Id as objectId query parameters and returns the follow JSON:
{"groups":["b1cc6d36-ac97-420a-8a9a-58a7be4aff36","71fa71e1-7edd-48a7-a147-16705c856cb0"]}
If anyone can point me in the right direction I'd be grateful.
Turns out there was one crucial thing I was missing that wasn't referenced in the guide I was following. I found the answer here. Having retrieved the claim value from my REST API I needed to configure the custom policy to include the claim:
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpOrSignIn" />
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
<OutputClaim ClaimTypeReferenceId="groups" DefaultValue="" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
We have created a custom IEF policy in Azure AD B2C to allow for sign-up and sign-in with username identified local accounts, following the instructions found in Custom B2C Policy for Username based Local Accounts answer. When trying to run the SignUpOrSignIn policy from IEF, it displays the following exception on the logon UI.
AADB2C: An exception has occurred.
After enabling Application Insights logging, we captured the following fatal exception:
Orchestration step '1' of in policy 'B2C_1A_signup_signin of tenant 'xxxxxxxxxx.onmicrosoft.com' specifies more than one enabled validation claims exchange
Orchestration Steps configured as follows:
<UserJourney Id="SignUpOrSignIn">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
<ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninUsernameExchange" />
</ClaimsProviderSelections>
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSigninUsernameExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Username" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SignUpWithLogonUsernameExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonName" />
</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="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
Referenced Technical Profile is as follows:
<TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Username">
<DisplayName>Local Account Signin</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="SignUpTarget">SignUpWithLogonUsernameExchange</Item>
<Item Key="setting.operatingMode">Username</Item>
<Item Key="ContentDefinitionReferenceId">api.selfasserted</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="signInName" Required="true" />
<OutputClaim ClaimTypeReferenceId="password" Required="true" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="login-NonInteractive" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
We are unable to find any references to this error on google or SO, and can't figure out where there could be multiple enabled validation claims exchanges? The best we can figure, there is some part of the Base configuration that is not being adequately overridden by the extension configuration, and so it sees duplicates?
You are right the base configuration is not being overridden. We did the following to fix.
In TrustFrameworkExtensions.xml
change the id to something else
<UserJouney id="SignUpOrSignInUsername">
...OrchestrationSteps
</UserJourney>
then navigate in SignUpOrSignIn.xml
change referenceid to the what you put in the extenstions.xml
<DefaultUserJourney ReferenceId="SignUpOrSignInUsername" />