I'm trying to setup a custom Azure B2C policy. The policy sends the request to the endpoint but doesn't populate the objectId in the body
<ClaimsProvider>
<DisplayName>REST API SignUp</DisplayName>
<TechnicalProfiles>
<!-- Custom Restful service -->
<TechnicalProfile Id="REST-API-SignUp">
<DisplayName>Validate user's input data and Save details to Web API</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://xxxxxxxxxxx.azurewebsites.net/api/Users/RegisterNewUser</Item>
<Item Key="AllowInsecureAuthInProduction">true</Item>
<Item Key="AuthenticationType">None</Item>
<Item Key="SendClaimsIn">Body</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="objectId" />
<InputClaim ClaimTypeReferenceId="email" />
<InputClaim ClaimTypeReferenceId="displayName"/>
<InputClaim ClaimTypeReferenceId="surname" />
<InputClaim ClaimTypeReferenceId="phoneNumber" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="roles" PartnerClaimType="roles" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
<!-- Add validation technical profile to LocalAccountSignUpWithLogonEmail technical profile -->
<TechnicalProfile Id="LocalAccountSignUpWithLogonEmail">
<Metadata>
<Item Key="ContentDefinitionReferenceId">api.localaccountsignup</Item>
</Metadata>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="roles" PartnerClaimType="roles" DefaultValue="" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="REST-API-SignUp" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
</ClaimsProviders>
All the other InputClaims gets populated in the request body except objectId
{"email":"sakkiexxx#xxxxch","displayName":"Sakkie","surname":"NA","phoneNumber":"9876543215"}
I have the following orchestration steps
<UserJourney Id="SignUp">
<OrchestrationSteps>
<!-- Track that we have received a sign in request -->
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="TrackSignUp-RequestReceived" TechnicalProfileReferenceId="AppInsights-SignUpRequest" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail" />
</ClaimsExchanges>
</OrchestrationStep>
...
and then the LocalAccountSignUpWithLogonEmail technical profile outputs the object Id
<ClaimsProvider>
<DisplayName>Local Account</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="LocalAccountSignUpWithLogonEmail">
<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">Sign Up</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" Required="true" />
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" 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" />
<OutputClaim ClaimTypeReferenceId="phoneNumber" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
............
I update my user journey to include the API call as a separate step, this sorted out the objectId
<UserJourney Id="SignUp">
<OrchestrationSteps>
<!-- Track that we have received a sign in request -->
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="TrackSignUp-RequestReceived" TechnicalProfileReferenceId="AppInsights-SignUpRequest" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="Web-API-Update" TechnicalProfileReferenceId="REST-API-SignUp" />
</ClaimsExchanges>
</OrchestrationStep>
Adding an extra step to your User Journey is one of the options.
You could also use a ValidationProfile as an alternative, in that case you would change the following XML
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
</ValidationTechnicalProfiles>
To this
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
<ValidationTechnicalProfile ReferenceId="REST-API-SignUp" />
</ValidationTechnicalProfiles>
There is one 'but' and that is that both solutions have a downside. The ObjectId is generated in technical profile AAD-UserWriteUsingLogonEmail. Which means that your account is created in Azure AD. If your next step or validation profile failed to call the API you might end up in a state where the account is registered but the object id is not stored in your API. This is also the other way around, if your API is providing additional claims to the user, these are not saved on the users' profile.
More info about the technical profile AAD-UserWriteUsingLogonEmail can be found on the official docs:
https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-technical-profile#azure-ad-technical-provider-operations
Same for Validation Technical Profiles:
https://learn.microsoft.com/en-us/azure/active-directory-b2c/validation-technical-profile
This is exactly what happens for me too. I tried adding the
AAD-UserWriteUsingLogonEmail as a first node in the validation technical profiles in the TrustFrameworkExtensions.xml file (note that in the TrustFrameworkBase.xml, the AAD-UserWriteUsingLogonEmail already exists. I don't want to change the base as its not recommended.
In TrustFrameworkExtensions.xml (for LocalAccountsSignUpWithLogonEmail)
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
<ValidationTechnicalProfile ReferenceId="REST-API-SignUp" />
</ValidationTechnicalProfiles>
In TrustFrameworkBase.xml (for LocalAccountsSignUpWithLogonEmail)
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail"/>
</ValidationTechnicalProfiles>
but what happens is that I think the the AAD-UserWriteUsingLogonEmail gets called twice. once in the extension and then once in the base.
Does anyone actually know how this inheritance or overriding work? If you define something in the extension xml, does it overwrite or get merged? if so in what order? it seems that the base node gets called first, so that's why oid isn't populated. but then when you put it in the extension as a first node (you want it to overwrite the base), the base gets called again. is there anyway to change the order or say "I want this to replace (override) what is in the base? Is it merged? There is no documentation on how base policies work.
I will try changing the journey as recommended, but would still like to know how having a base policy works, if anyone can answer
Related
Our current reset password flow works by first showing you a field for email address. Then sends you an email with OTP code to verify your email with and goes on with a password reset. We use the starter pack and SendGrid for the emails, implemented with Microsoft's example documentation for it.
I cannot get the custom policy to verify/validate that the email actually exists as a registered user (i.e. has objectId) in our AAD prior to sending the email.
What I want is: the policy checks if this email address is already registered on AAD. If yes, then it proceeds and sends a OTP code to verify the email, helps the user to reset the password. If not, interrupt the flow, do not send any email and do not return any error message.
Microsoft has a working demo of this, but for sign in.
This is the UserJourney for Password Reset that I use:
<UserJourney Id="PasswordReset">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="PasswordResetUsingEmailAddressExchange" TechnicalProfileReferenceId="LocalAccountDiscoveryUsingEmailAddress" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="NewCredentials" TechnicalProfileReferenceId="LocalAccountWritePasswordUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- Send security alert email -->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="Sendalertemail" TechnicalProfileReferenceId="Alertemail" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="4" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
The contents of the technical profile in step 1:
<TechnicalProfile Id="LocalAccountDiscoveryUsingEmailAddress">
<DisplayName>Reset password using email address</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.localaccountpasswordreset</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<DisplayClaims>
<DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
</DisplayClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress-emailAddress" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
And the validation technical profile from it:
<TechnicalProfile Id="AAD-UserReadUsingEmailAddress-emailAddress">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" Required="true" />
</InputClaims>
<OutputClaims>
<!-- Required claims -->
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
<!-- Optional claims -->
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="otherMails" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
I tried this solution, but it didn't change the behavior of my page at all.
I managed to solve this by adding this to the SendCode action, before GenerateOtp:
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="AAD-UserReadUsingEmailAddress-emailAddress" ContinueOnError="false" />
AAD-UserReadUsingEmailAddress-emailAddress is responsible for checking for objectId against AAD.
Then, a precondition, within SendOtp:
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>objectId</Value>
<Action>SkipThisValidationTechnicalProfile</Action>
</Precondition>
I have a business requirement but I've failed to figure out how to implement it.
The very first time a user signs-in, they are challenged with email OTP followed by a forced password reset.
All subsequence sign-in attempts will prompt the user only for email address and password (OTP is a one-time user flow).
My problem is that Orchestration step 1 collects the email address, then I query AAD by the email address provided and read an extension attribute called extension_EmailValidated. If the attribute is not TRUE, B2C will force the user to do email OTP verification, however, Orchestration step 2 won't let me pre-populate the previously entered email address input box AND show the OTP buttons. It will only let me do one or the other (hope that makes sense).
My UserJourney looks like this
<UserJourneys>
<UserJourney Id="B2CSignIn">
<OrchestrationSteps>
<!--User enters their email address-->
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="AADEmailDiscovery" TechnicalProfileReferenceId="AAD-EmailDiscovery" />
</ClaimsExchanges>
</OrchestrationStep>
<!--User enters their OTP-->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>extension_EmailVerified</Value>
<Value>True</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADEmailVerification" TechnicalProfileReferenceId="AAD-EmailVerification" />
</ClaimsExchanges>
</OrchestrationStep>
<!--Set extension_EmailVerified to TRUE-->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>extension_EmailVerified</Value>
<Value>True</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADUserWriteEmailVerifiedUsingEmail" TechnicalProfileReferenceId="AAD-UserWriteEmailVerifiedUsingEmail" />
</ClaimsExchanges>
</OrchestrationStep>
Here are the technical profiles for Step 1 and Step 2.
<TechnicalProfile Id="AAD-EmailDiscovery">
<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>
<Item Key="setting.showCancelButton">False</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="extension_EmailVerified" DefaultValue="false" Required="true" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
<TechnicalProfile Id="AAD-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>
<Item Key="setting.showCancelButton">False</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>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
Another very confusing issue is that the only way I can get the OTP buttons to appear on Step 2 is to include "ParterClaimType=Verified.Email" for both the InputClaim and OutputClaims -- and that makes no sense.
If I omit "ParterClaimType=Verified.Email" from InputClaims and OutputClaims, I can get the email address to pre-populate from Step 1.
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
</OutputClaims>
Advice and guidance is very much appreciated.
Thanks!
The solution here is to make the input text box readOnly.
Send the email collected at the sign in screen through a claim transform to copy it to a new claim, called readOnlyEmail. The claim should be defined as read only.
<ClaimType Id="readOnlyEmail">
<DisplayName>Email Address</DisplayName>
<DataType>string</DataType>
<UserHelpText/>
<UserInputType>Readonly</UserInputType>
</ClaimType>
Copy the email claim into the readOnly claim
<ClaimsTransformation Id="CopySignInNameToReadOnly" TransformationMethod="FormatStringClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" TransformationClaimType="inputClaim" />
</InputClaims>
<InputParameters>
<InputParameter Id="stringFormat" DataType="string" Value="{0}" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="readOnlyEmail" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
Then make a call to the claims transform from your sign up technical profile using an OutputClaimsTransformations node.
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CopySignInNameToReadOnly" />
</OutputClaimsTransformations>
Finally on the email verification technical profile (selfAsserted technical profile), pass in the readOnly email to be validated:
<InputClaims>
<InputClaim ClaimTypeReferenceId="readOnlyEmail"/>
</InputClaims>
<OutputClaims>
<!-- Required claims -->
<OutputClaim ClaimTypeReferenceId="readOnlyEmail" PartnerClaimType="Verified.Email"/>
</OutputClaims>
The concept is demonstrated here:
https://github.com/azure-ad-b2c/samples/tree/master/policies/signin-email-verification
You just need to add your conditional logic on top of that sample.
I have taken the change sign in example (https://github.com/azure-ad-b2c/samples/tree/master/policies/change-sign-in-name) and added this into my custom policy files. If I run through this process and change the user's email address, then the value in B2C's AD is updated to the new email address, however the value in the returned token is still the user's old email address before they updated it.
However I want to return the new email address that the user entered back to the application. Is this possible?
My code:
Relying Party:
<RelyingParty>
<DefaultUserJourney ReferenceId="ChangeSignInName" />
<UserJourneyBehaviors>
<ScriptExecution>Allow</ScriptExecution>
</UserJourneyBehaviors>
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="objectId"/>
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
User Journey:
<UserJourney Id="ChangeSignInName">
<OrchestrationSteps>
<!-- To change the sign-in names, user need to sign-in with local account only -->
<OrchestrationStep Order="1" Type="ClaimsProviderSelection" ContentDefinitionReferenceId="api.selfasserted.changesignin">
<ClaimsProviderSelections>
<ClaimsProviderSelection TargetClaimsExchangeId="LocalAccountSigninEmailExchange" />
</ClaimsProviderSelections>
</OrchestrationStep>
<!-- Sign-in user with local account-->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="LocalAccountDiscoveryUsingEmailAddress2" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- Read the user data from the Azure Active Directory -->
<OrchestrationStep Order="3" 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>
<!-- This self asserted technical profile validates and update the sign-in name -->
<OrchestrationStep Order="4" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="B2CUserSignInUpdateExchange" TechnicalProfileReferenceId="SelfAsserted-EmailUpdate" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="5" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
Technical Profiles:
<TechnicalProfile Id="LocalAccountDiscoveryUsingEmailAddress2">
<DisplayName>Reset password using email address</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.selfasserted.changesignin</Item>
<Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">Your account has been locked. Contact your support person to unlock it, then try again.</Item>
<!--OTP validation error messages-->
<Item Key="UserMessageIfSessionDoesNotExist">You have exceed the maximum time allowed.</Item>
<Item Key="UserMessageIfMaxRetryAttempted">You have exceed the number of retries allowed.</Item>
<Item Key="UserMessageIfInvalidCode">You have entered the wrong code.</Item>
<Item Key="UserMessageIfSessionConflict">Cannot verify the code, please try again later.</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<IncludeInSso>false</IncludeInSso>
<DisplayClaims>
<DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
</DisplayClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
<TechnicalProfile Id="AAD-UserReadUsingObjectId">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="objectId" Required="true" />
</InputClaims>
<OutputClaims>
<!-- Optional claims -->
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="otherMails" />
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
<TechnicalProfile Id="SelfAsserted-EmailUpdate">
<DisplayName>Email update</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.changesignin</Item>
<!--OTP validation error messages-->
<Item Key="UserMessageIfSessionDoesNotExist">You have exceed the maximum time allowed.</Item>
<Item Key="UserMessageIfMaxRetryAttempted">You have exceed the number of retries allowed.</Item>
<Item Key="UserMessageIfInvalidCode">You have entered the wrong code.</Item>
<Item Key="UserMessageIfSessionConflict">Cannot verify the code, please try again later.</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="alternativeSecurityId" />
<InputClaim ClaimTypeReferenceId="userPrincipalName" />
</InputClaims>
<DisplayClaims>
<DisplayClaim DisplayControlReferenceId="emailVerificationControlUpdateEmail" />
</DisplayClaims>
<OutputClaims>
<!-- Collect and validate the email address -->
<OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
</OutputClaims>
<ValidationTechnicalProfiles>
<!-- Call validation technical profile to persist the data-->
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteEmailUsingObjectId" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
If Azure AD User's object is updated correctly then most likely updated claim is not added to the claim's bag. To make things easier, do this test first and report the findings:
Add a new step right before the last step, making it step 5 and the last step will be step 6 as shown below:
<OrchestrationStep Order="5" Type="ClaimsExchange">
<ClaimsExchange Id="AADUserReadWithObjectId_AfterUpdate" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="5" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
Run the policy, and confirm whether you do see an updated email or an old one? if you see the updated email then it means one of the technical profiles not emitting the output claim correctly.
I have implemented an invite policy (invite users to the site by sending them an email link) via this example https://github.com/azure-ad-b2c/samples/tree/master/policies/invite
I have this user journey
<UserJourney Id="SignUpInvitation">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="GetClaims" CpimIssuerTechnicalProfileReferenceId="IdTokenHint_ExtractClaims" />
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>email</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SelfAsserted-Unsolicited" TechnicalProfileReferenceId="SelfAsserted-Unsolicited"/>
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSignUpWithReadOnlyEmail" TechnicalProfileReferenceId="LocalAccountSignUpWithReadOnlyEmail"/>
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="4" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="5" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer"/>
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb"/>
</UserJourney>
In the first step, I extract the claims from the JWT. An email and a custom claim,
<TechnicalProfiles>
<TechnicalProfile Id="IdTokenHint_ExtractClaims">
<DisplayName> My ID Token Hint TechnicalProfile</DisplayName>
<Protocol Name="None" />
<Metadata>
<Item Key="METADATA">https://mywebsite.com/internal/v1/invitation/.well-known/openid-configuration</Item>
</Metadata>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="extension_DBId" DefaultValue="1" />
</OutputClaims>
</TechnicalProfile>
</TechnicalProfiles>
(Note that I set a default value of 1 to my custom claim)
Then, I have the technical profile that writes the user to the directory
<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="CopyEmailAddress" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="ReadOnlyEmail" />
<InputClaim ClaimTypeReferenceId="extension_DBId" DefaultValue="2" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" />
<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" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail-Custom" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
(Note that here I set a default value of 2 to my custom claim)
AAD-UserWriteUsingLogonEmail-Custom is a modification of the built-in (inside TrustFrameworkBase.xml) AAD-UserWriteUsingLogonEmail profile, I've modified it to include my custom claim extension_DBId as a persisted claim:
<PersistedClaim ClaimTypeReferenceId="extension_DBId" DefaultValue="3" />
Here is the problem,
I generate a JWT with some arbitrary extension_DBId (Not 1,2 or 3)
But when I run the policy with it, in the output claims, I get extension_DBId=2
Which indicates that the third orchestration step (LocalAccountSignUpWithReadOnlyEmail) didn't receive the extension_DBId claim from the first orchestration step (IdTokenHint_ExtractClaims).
Why doesn't the IdTokenHint_ExtractClaims profile transfer the extension_DBId claim to the LocalAccountSignUpWithReadOnlyEmail profile?
I tried to remove the second step (SelfAsserted-Unsolicited), Still didn't work.
What could it be?
For a user journey to receive an incoming claim from the input JWT, you must add an <InputClaim /> to the relying party <TechnicalProfile />:
<RelyingParty>
<TechnicalProfile Id="PolicyProfile">
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
<InputClaim ClaimTypeReferenceId="extension_DBId" />
</InputClaims>
</TechnicalProfile>
</RelyingParty>
If the claim type of the internal claim (e.g. extension_DBId) is different to that of the input JWT (e.g. DBId), then you can add the PartnerClaimType attribute to the InputClaim element:
<InputClaim ClaimTypeReferenceId="extension_DBId" PartnerClaimType="DBId" />
#ChrisPadgett pointed out that I have to add my custom claim(s) (extension_DBId) to the InputClaims in relying party policy
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpInvitation" />
<UserJourneyBehaviors>
<ContentDefinitionParameters>
<Parameter Name="ui_locales">{Culture:RFC5646}</Parameter>
</ContentDefinitionParameters>
<ScriptExecution>Allow</ScriptExecution>
</UserJourneyBehaviors>
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="email" />
<InputClaim ClaimTypeReferenceId="DBId" /> <!-- This was added -->
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="emails" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
<OutputClaim ClaimTypeReferenceId="extension_DBId" />
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
(I then have a claim transformation that converts DBId to extension_DBId)
I am creating a custom B2C policy and I am trying to replicate the password reset journey for local accounts created with a username.
I can read the username from AD but I am unsure how to validate the verified email address against the account.
Currently if the username is correct any email address can be used to verify.
Technical profile:
<TechnicalProfile Id="SA-LocalAccountDiscoveryUsingLogonName">
<DisplayName>Reset password using logon name</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.localaccountpasswordreset</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="signInName" Required="true" />
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingLogonName" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
Validation Technical Profile:
<TechnicalProfile Id="AAD-UserReadUsingLogonName">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">true</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="signInNames.userName" Required="true" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
User Journey:
<UserJourney Id="PasswordReset">
<OrchestrationSteps>
<!--Get user by username-->
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="PasswordResetUsingEmailAddressExchange" TechnicalProfileReferenceId="SA-LocalAccountDiscoveryUsingLogonName" />
</ClaimsExchanges>
</OrchestrationStep>
<!--Reset password-->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="NewCredentials" TechnicalProfileReferenceId="SA-LocalAccountPasswordReset" />
</ClaimsExchanges>
</OrchestrationStep>
<!--Read remaining attributes of user-->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="ReadUser" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<!--Create token-->
<OrchestrationStep Order="4" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
If you write the email address to both the "otherMails" and "strongAuthenticationEmailAddress" properties during the sign-up policy, then you can verify that the email address is associated with the user name during the password reset policy using a REST API.
This REST API must be declared as a claims provider:
<ClaimsProvider>
<DisplayName>REST APIs</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="RestApi-CheckUser">
<DisplayName>Check User REST API</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ServiceUrl">Insert the REST API endpoint URL</Item>
<Item Key="AuthenticationType">None</Item>
<Item Key="SendClaimsIn">Body</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" />
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
The REST API can query the user object by the "signInNames" and "otherMails" properties using the Azure AD Graph API (you can't read the "strongAuthenticationEmailAddress" property using this Graph API) and, as described in the REST API walkthrough, then return 200 OK if the email address is associated with the user name or 409 Conflict if not so.
The REST API technical profile can then be invoked as a validation technical profile from the "SA-LocalAccountDiscoveryUsingLogonName" technical profile:
<TechnicalProfile Id="SA-LocalAccountDiscoveryUsingLogonName">
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="RestApi-CheckUser" />
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingLogonName" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
If the "RestApi-CheckUser" technical profile returns 200 OK, then the "AAD-UserReadUsingLogonName" technical profile is invoked and the end user can continue with the password reset. If the "RestApi-CheckUser" technical profile returns 409 Conflict, then the "AAD-UserReadUsingLogonName" technical profile isn't invoked and the end user can't continue.