How to include custom claim in access token with Azure AD B2C custom policy and MSAL? - azure-ad-b2c

I have Azure B2C configured with custom policies to allow signups and sign ins of local accounts and multi-tenant Azure AD. The UI is Angular using MSAL with a .NET Core Web API backend. The custom policy defines a custom claim named clientIds that is populated through a REST call to an internally developed Azure Function.
This custom claim is successfully included in the id_token when the user signs in. However, it is NOT included in the access_token sent to the API. How do I include it in the access token?
Other details:
I found one post that said the solution was to add it as a PersistedClaim to the SM-AAD technical profile, but it didn't have an effect.
If I configure the custom claim in a user flow and an API Connector, everything works great - it's included in both tokens. I've studied the XML of the user flow to look for clues but didn't get anywhere. I cannot use user flows because I must support multi-tenant Azure AD.
I've included the claim in SignUpOrSignin.xml:
<RelyingParty>
<DefaultUserJourney ReferenceId="AegisSignUpOrSignIn" />
<Endpoints>
<Endpoint Id="Token" UserJourneyReferenceId="RedeemRefreshToken" />
</Endpoints>
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
<OutputClaim ClaimTypeReferenceId="identityProvider" />
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
<OutputClaim ClaimTypeReferenceId="otherMails" PartnerClaimType="emails" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" PartnerClaimType="email" />
<OutputClaim ClaimTypeReferenceId="clientIds" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
I think the solution is editing something in TrustFrameworkExtensions.xml but I've been trying all sorts of things for a couple days with no success. Here it is:
<?xml version="1.0" encoding="utf-8" ?>
<TrustFrameworkPolicy
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06"
PolicySchemaVersion="0.3.0.0"
TenantId="aegispremierdev.onmicrosoft.com"
PolicyId="B2C_1A_TrustFrameworkExtensions"
PublicPolicyUri="http://aegispremierdev.onmicrosoft.com/B2C_1A_TrustFrameworkExtensions">
<BasePolicy>
<TenantId>aegispremierdev.onmicrosoft.com</TenantId>
<PolicyId>B2C_1A_TrustFrameworkLocalization</PolicyId>
</BasePolicy>
<BuildingBlocks>
<ClaimsSchema>
<ClaimType Id="prompt">
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="isForgotPassword">
<DisplayName>isForgotPassword</DisplayName>
<DataType>boolean</DataType>
<AdminHelpText>Whether the user has selected Forgot your Password</AdminHelpText>
</ClaimType>
<ClaimType Id="clientIds">
<DisplayName>Comma-separated list of authorized CRM clients</DisplayName>
<DataType>string</DataType>
</ClaimType>
</ClaimsSchema>
</BuildingBlocks>
<ClaimsProviders>
<ClaimsProvider>
<Domain>commonaad</Domain>
<DisplayName>Sign in with Microsoft</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="AADCommon-OpenIdConnect">
<DisplayName>Sign in with Microsoft</DisplayName>
<Description>Login with your company account</Description>
<Protocol Name="OpenIdConnect"/>
<Metadata>
<Item Key="METADATA">https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration</Item>
<Item Key="client_id">64e56953-cb54-48ee-ae81-3bd1325050d6</Item>
<Item Key="response_types">code</Item>
<Item Key="scope">openid profile email</Item>
<Item Key="response_mode">form_post</Item>
<Item Key="HttpBinding">POST</Item>
<Item Key="UsePolicyInRedirectUri">false</Item>
<Item Key="DiscoverMetadataByTokenIssuer">true</Item>
<Item Key="ValidTokenIssuerPrefixes">https://login.microsoftonline.com/</Item>
</Metadata>
<CryptographicKeys>
<Key Id="client_secret" StorageReferenceId="B2C_1A_AzureAdAppSecret"/>
</CryptographicKeys>
<InputClaims>
<InputClaim ClaimTypeReferenceId="prompt" PartnerClaimType="prompt" DefaultValue= "select_account"/>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="oid"/>
<OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="azureAdIdpAuthentication" AlwaysUseDefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="identityProvider" PartnerClaimType="iss" />
<OutputClaim ClaimTypeReferenceId="othermails" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" PartnerClaimType="email"/>
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="email" />
<!-- <OutputClaim ClaimTypeReferenceId="clientIds" /> Adding it here doesn't help -->
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName"/>
<OutputClaimsTransformation ReferenceId="CreateUserPrincipalName"/>
<OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId"/>
<OutputClaimsTransformation ReferenceId="CreateSubjectClaimFromAlternativeSecurityId"/>
</OutputClaimsTransformations>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-SocialLogin"/>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>Local Account SignIn</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="login-NonInteractive">
<Metadata>
<Item Key="client_id">6a178567-c2fb-4f88-93e8-ed5775869485</Item>
<Item Key="IdTokenAudience">3290010d-2d76-4fc0-95fc-6c6ad6c637bb</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="client_id" DefaultValue="6a178567-c2fb-4f88-93e8-ed5775869485" />
<InputClaim ClaimTypeReferenceId="resource_id" PartnerClaimType="resource" DefaultValue="3290010d-2d76-4fc0-95fc-6c6ad6c637bb" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="email" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" AlwaysUseDefaultValue="true" />
<!-- <OutputClaim ClaimTypeReferenceId="clientIds" /> Adding it here doesn't help -->
</OutputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>Local Account</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="LocalAccountSignUpWithLogonEmail">
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="REST-OnAegisAzureB2CSignUp" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<!-- Adding it as a PersistedClaim as suggested in the referenced SO post doesn't help -->
<!-- <ClaimsProvider>
<DisplayName>Session Management</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="SM-AAD">
<DisplayName>Session Mananagement Provider</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.SSO.DefaultSSOSessionProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<PersistedClaims>
<PersistedClaim ClaimTypeReferenceId="objectId" />
<PersistedClaim ClaimTypeReferenceId="signInName" />
<PersistedClaim ClaimTypeReferenceId="authenticationSource" />
<PersistedClaim ClaimTypeReferenceId="identityProvider" />
<PersistedClaim ClaimTypeReferenceId="newUser" />
<PersistedClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" />
<PersistedClaim ClaimTypeReferenceId="clientIds" />
</PersistedClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectIdFromSession" DefaultValue="true" />
</OutputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>-->
<ClaimsProvider>
<DisplayName>Self Asserted</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="SelfAsserted-Social">
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
<InputClaim ClaimTypeReferenceId="authenticationSource" />
</InputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="REST-OnAegisAzureB2CSignUp"/>
</ValidationTechnicalProfiles>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>REST APIs - OnAegisAzureB2CSignUp</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="REST-OnAegisAzureB2CSignUp">
<DisplayName>Verify user exists in DonorOne Users table</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<!-- Set the ServiceUrl with your own REST API endpoint -->
<Item Key="ServiceUrl">https://apt-fa-azure-b2c-dev-wus2.azurewebsites.net/api/onAegisAzureB2CSignUp</Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="AuthenticationType">Basic</Item>
<Item Key="AllowInsecureAuthInProduction">false</Item>
</Metadata>
<CryptographicKeys>
<Key Id="BasicAuthenticationUsername" StorageReferenceId="B2C_1A_RestApiUsername" />
<Key Id="BasicAuthenticationPassword" StorageReferenceId="B2C_1A_RestApiPassword" />
</CryptographicKeys>
<InputClaims>
<!-- Claims sent to your REST API -->
<InputClaim ClaimTypeReferenceId="email" />
<InputClaim ClaimTypeReferenceId="authenticationSource" />
</InputClaims>
<OutputClaims>
<!-- Claims parsed from your REST API -->
<OutputClaim ClaimTypeReferenceId="clientIds" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>REST APIs - OnAegisAzureB2CSignIn</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="REST-OnAegisAzureB2CSignIn">
<DisplayName>Get user extended profile Azure Function web hook</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://apt-fa-azure-b2c-dev-wus2.azurewebsites.net/api/onAegisAzureB2CSignIn</Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="AuthenticationType">Basic</Item>
<Item Key="AllowInsecureAuthInProduction">false</Item>
</Metadata>
<CryptographicKeys>
<Key Id="BasicAuthenticationUsername" StorageReferenceId="B2C_1A_RestApiUsername" />
<Key Id="BasicAuthenticationPassword" StorageReferenceId="B2C_1A_RestApiPassword" />
</CryptographicKeys>
<InputClaims>
<!-- Claims sent to your REST API -->
<InputClaim ClaimTypeReferenceId="email" />
<InputClaim ClaimTypeReferenceId="signInName" />
<InputClaim ClaimTypeReferenceId="authenticationSource" />
</InputClaims>
<OutputClaims>
<!-- Claims parsed from your REST API -->
<OutputClaim ClaimTypeReferenceId="clientIds" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>Local Account</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="ForgotPassword">
<DisplayName>Forgot your password?</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="isForgotPassword" DefaultValue="true" AlwaysUseDefaultValue="true"/>
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
<TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Email">
<Metadata>
<Item Key="setting.forgotPasswordLinkOverride">ForgotPasswordExchange</Item>
</Metadata>
</TechnicalProfile>
<TechnicalProfile Id="LocalAccountWritePasswordUsingObjectId">
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
</ClaimsProviders>
<UserJourneys>
<UserJourney Id="AegisSignUpOrSignIn">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
<ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
<ClaimsProviderSelection TargetClaimsExchangeId="AzureADCommonExchange" />
<ClaimsProviderSelection TargetClaimsExchangeId="ForgotPasswordExchange" />
</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="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail" />
<ClaimsExchange Id="AzureADCommonExchange" TechnicalProfileReferenceId="AADCommon-OpenIdConnect" />
<ClaimsExchange Id="ForgotPasswordExchange" TechnicalProfileReferenceId="ForgotPassword" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="InvokeSubJourney">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>isForgotPassword</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<JourneyList>
<Candidate SubJourneyReferenceId="PasswordReset" />
</JourneyList>
</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>azureAdIdpAuthentication</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="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="RESTOnAegisAzureB2CSignIn" TechnicalProfileReferenceId="REST-OnAegisAzureB2CSignIn" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="9" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
</UserJourneys>
<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>
<!-- Collect and persist a new password. -->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="NewCredentials" TechnicalProfileReferenceId="LocalAccountWritePasswordUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
</OrchestrationSteps>
</SubJourney>
</SubJourneys>
</TrustFrameworkPolicy>

#JasSuri-MSFT that was the trick - thx! This was the UserJourney I added to TrustFrameworkExtensions.xml:
<UserJourney Id="AegisRedeemRefreshToken">
<PreserveOriginalAssertion>false</PreserveOriginalAssertion>
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="RefreshTokenSetupExchange" TechnicalProfileReferenceId="RefreshTokenReadAndSetup" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="CheckRefreshTokenDateFromAadExchange" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId-CheckRefreshTokenDate" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- I inserted step 3, which calls the REST API to return the clientIds claim -->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="RESTOnAegisAzureB2CSignIn" TechnicalProfileReferenceId="REST-OnAegisAzureB2CSignIn" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="4" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
</UserJourney>
Then I referenced this journey from SignUpOrSignin.xml:
<Endpoint Id="Token" UserJourneyReferenceId="AegisRedeemRefreshToken" />
My REST API expects two claims to be sent in: emailAddress and authenticationSource. They were not included after the above changes, so I attempted to include them by overriding a couple technical profiles. That is, I added this to the ClaimsProviders section in TrustFrameworkExtensions.xml:
<ClaimsProvider>
<DisplayName>Refresh token journey</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="RefreshTokenReadAndSetup">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
</OutputClaims>
</TechnicalProfile>
<TechnicalProfile Id="AAD-UserReadUsingObjectId-CheckRefreshTokenDate">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
</OutputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
That worked to include the email address, but for reasons I don't yet understand the authenticationSource is still not passed in. But that's out of scope for this question, so I'll end it here.

Related

B2C Custom Impersonation Policy Giving Error

I am trying to create a custom policy that allows a user to impersonate another user. It is based off the impersonation sample (https://github.com/azure-ad-b2c/samples/tree/master/policies/impersonation) and uses the starter pack.
However I didn't want social accounts so I used the LocalAccounts starter pack and modified the policy. It brings up the login screen but then does not bring up the second screen and instead returns a generic error (ServerError: AADB2C90037: An error occurred while processing the request. Please contact administrator of the site you are trying to access.)
Can anyone help me get this policy working or point out the problem in it?
<TrustFrameworkPolicy xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06" PolicySchemaVersion="0.3.0.0" TenantId="mytenantdev.onmicrosoft.com" PolicyId="B2C_1A_Impersonation" PublicPolicyUri="http://mytenantdev.onmicrosoft.com/B2C_1A_Impersonation" TenantObjectId="b21d8c41-8fdb-4f82-835d-2c1765fcc855">
<BasePolicy>
<TenantId>mytenantdev.onmicrosoft.com</TenantId>
<PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>
</BasePolicy>
<BuildingBlocks>
<ClaimsSchema>
<!-- Sample: targetEmail is the email of the user originally requested impersonatedUser is the
email value returned from SignInNames.Email from Azure AD after we requested (this is directory data) -->
<ClaimType Id="targetEmail">
<DisplayName>Sign-in on behalf of</DisplayName>
<DataType>string</DataType>
<UserHelpText>Email address of the impersonated user</UserHelpText>
<UserInputType>EmailBox</UserInputType>
<Restriction>
<Pattern RegularExpression="^[a-zA-Z0-9.!#$%&'^_`{}~-]+#[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" HelpText="Please enter a valid email address." />
</Restriction>
</ClaimType>
<ClaimType Id="impersonatedUser">
<DisplayName>Impersonated account</DisplayName>
<DataType>string</DataType>
<UserHelpText />
</ClaimType>
<!--Sample: Indicates whether a user can impersonate (if the value is 1)-->
<ClaimType Id="extension_can_impersonate">
<DisplayName>Can impersonate</DisplayName>
<DataType>string</DataType>
<UserHelpText>Add help text here</UserHelpText>
</ClaimType>
<!--Sample: Stores the error message if user can't impersonate-->
<ClaimType Id="errorMessage">
<DisplayName>Error</DisplayName>
<DataType>string</DataType>
<UserHelpText>Add help text here</UserHelpText>
<UserInputType>Paragraph</UserInputType>
</ClaimType>
</ClaimsSchema>
<ClaimsTransformations>
<!--Sample: Initiates the errorMessage claims type with the error message-->
<ClaimsTransformation Id="CreateUnsolicitedErrorMessage" TransformationMethod="CreateStringClaim">
<InputParameters>
<InputParameter Id="value" DataType="string" Value="You are not allow to impersonate. Please content your administrator" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="errorMessage" TransformationClaimType="createdClaim" />
</OutputClaims>
</ClaimsTransformation>
</ClaimsTransformations>
</BuildingBlocks>
<ClaimsProviders>
<!--Sample: this technical profile provides input for targetEmail and runs validation technical
profile for to read the impersonated user profile -->
<ClaimsProvider>
<DisplayName>Self Asserted Targeted Email Exchange</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="SelfAsserted-TargetEmailExchange">
<DisplayName>Target Email Page</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>
<!--Sample: indicating that claim resolving should be performed. So, we can read the value of {OAUTH-KV:targetEmail} claim resolver -->
<Item Key="IncludeClaimResolvingInClaimsHandling">true</Item>
</Metadata>
<InputClaims>
<!--Sample: read the targetEmail query string parameter e.g. &targetemail=bob#contoso.com -->
<InputClaim ClaimTypeReferenceId="targetEmail" DefaultValue="{OAUTH-KV:targetEmail}" AlwaysUseDefaultValue="true" />
</InputClaims>
<OutputClaims>
<!-- Required claims -->
<OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="impersonatedUser" Required="true" />
<OutputClaim ClaimTypeReferenceId="targetEmail" Required="true" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-ImpersonatedUserRead" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
<!-- Sample: Show error message if user is not allowed to impersonate-->
<TechnicalProfile Id="SelfAsserted-ErrorMessage">
<DisplayName>Error message</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>
<!-- Sample: Remove the continue button-->
<Item Key="setting.showContinueButton">false</Item>
</Metadata>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CreateUnsolicitedErrorMessage" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="errorMessage" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="errorMessage" />
</OutputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>Azure Active Directory</DisplayName>
<TechnicalProfiles>
<!--Sample: read the extension_can_impersonate attribute-->
<TechnicalProfile Id="AAD-UserReadUsingObjectId">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="extension_can_impersonate" />
</OutputClaims>
</TechnicalProfile>
<!--Sample: Read target user and return error it doesn't exist.
If exists, pipes value into impersonatedUser claim type -->
<TechnicalProfile Id="AAD-ImpersonatedUserRead">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
<Item Key="UserMessageIfClaimsPrincipalDoesNotExist">An account could not be found for the provided user ID.</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<!--Sample: Look up in the signInNames to see if the value in targetEmail contains - look it up-->
<InputClaim ClaimTypeReferenceId="targetEmail" PartnerClaimType="signInNames" Required="true" />
</InputClaims>
<OutputClaims>
<!-- Sample: Returns the value in targetEmail and check in signInNames collection, then
returns value in SignInName and pipe into impersonatedUser-->
<OutputClaim ClaimTypeReferenceId="impersonatedUser" PartnerClaimType="signInNames.emailAddress" />
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
</ClaimsProviders>
<UserJourneys>
<UserJourney Id="Impersonation">
<OrchestrationSteps>
<!--Sample: Sign-in with your own credentials or select to sign-in with social account -->
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
<ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
</ClaimsProviderSelections>
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
</ClaimsExchanges>
</OrchestrationStep>
<!--Sample: Sign-in with social account, or create new local account -->
<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>
<!--Sample: this step reads your own account properties-->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- Sample: Check whether user can impersonate -->
<OrchestrationStep Order="4" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>extension_can_impersonate</Value>
<Value>1</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SelfAsserted-ErrorMessage" TechnicalProfileReferenceId="SelfAsserted-ErrorMessage" />
</ClaimsExchanges>
</OrchestrationStep>
<!--Sample: Collect the email address of the end user of act of behalf of-->
<OrchestrationStep Order="5" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="TargetEmailExchange" TechnicalProfileReferenceId="SelfAsserted-TargetEmailExchange" />
</ClaimsExchanges>
</OrchestrationStep>
<!--Sample: Issu an access token-->
<OrchestrationStep Order="6" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
</UserJourneys>
<RelyingParty>
<DefaultUserJourney ReferenceId="Impersonation" />
<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="objectId" PartnerClaimType="oid" />
<OutputClaim ClaimTypeReferenceId="impersonatedUser" />
</OutputClaims>
<!-- <SubjectNamingInfo ClaimType="sub" /> -->
<SubjectNamingInfo ClaimType="oid" />
</TechnicalProfile>
</RelyingParty>
</TrustFrameworkPolicy>

Validate Email through DisplayControl

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>

Azure B2C - Exception: "Claim with id 'email' was not found in the collection."

I can't get past this weird exception. Please, need your help.
Even though Application Insights (in vscode) shows that the email claim is in fact retrieved, I keep getting this error and don't make it to the Saml2Issuer orchestration step, hence, no SAML token. See the screenshot form the ApplicationInsights extension in vscode. Sensitive information screened. The claimsTransformation shown copies user email from an extension attribute into the email claim, which is to b eused in the SAML token.
RP File (irrelevant parts removed)
<TrustFrameworkPolicy xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06" PolicySchemaVersion="0.3.0.0" TenantId="{Settings:Tenant}" PolicyId="B2C_1A_signup_signin_saml" PublicPolicyUri="http://{Settings:Tenant}/B2C_1A_signup_signin_saml" DeploymentMode="Development" UserJourneyRecorderEndpoint="urn:journeyrecorder:applicationinsights">
<BasePolicy>
<TenantId>{Settings:Tenant}</TenantId>
<PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>
</BasePolicy>
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpOrSignIn" />
<UserJourneyBehaviors>
<SingleSignOn Scope="Tenant" />
<SessionExpiryType>Absolute</SessionExpiryType>
<SessionExpiryInSeconds>86400</SessionExpiryInSeconds>
<JourneyInsights TelemetryEngine="ApplicationInsights" InstrumentationKey="{Settings:ApplicationInsightskey}" DeveloperMode="true" ClientEnabled="false" ServerEnabled="true" TelemetryVersion="1.0.0" />
<!-- Enable JavaScript-->
<ScriptExecution>Allow</ScriptExecution>
</UserJourneyBehaviors>
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="SAML2"/>
<Metadata>
<Item Key="XmlSignatureAlgorithm">Sha256</Item>
</Metadata>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="email"/>
<OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="objectId"/>
</OutputClaims>
<SubjectNamingInfo ClaimType="email" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" ExcludeAsClaim="true"/>
</TechnicalProfile>
</RelyingParty>
</TrustFrameworkPolicy>
TFE File (irrelevant parts removed)
<TrustFrameworkPolicy xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06" PolicySchemaVersion="0.3.0.0" TenantId="{Settings:Tenant}" PolicyId="B2C_1A_TrustFrameworkExtensions" PublicPolicyUri="http://{Settings:Tenant}/B2C_1A_TrustFrameworkExtensions">
<BasePolicy>
<TenantId>{Settings:Tenant}</TenantId>
<PolicyId>B2C_1A_TrustFrameworkBase</PolicyId>
</BasePolicy>
<BuildingBlocks>
<ClaimsSchema>
...
...
<!--Overriding this here to add a partner claim type for SAML2-->
<ClaimType Id="email">
<DisplayName>Email Address</DisplayName>
<DataType>string</DataType>
<DefaultPartnerClaimTypes>
<Protocol Name="OpenIdConnect" PartnerClaimType="email" />
<Protocol Name="SAML2" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" />
</DefaultPartnerClaimTypes>
</ClaimType>
<ClaimType Id="extension_XXXX">
<DisplayName>Email Address</DisplayName>
<DataType>string</DataType>
<DefaultPartnerClaimTypes>
<Protocol Name="OpenIdConnect" PartnerClaimType="extension_XXXX" />
<Protocol Name="SAML2" PartnerClaimType="extension_XXXX" />
</DefaultPartnerClaimTypes>
<UserInputType>TextBox</UserInputType>
</ClaimType>
...
</ClaimsSchema>
<ClaimsTransformations>
<!--We use this claims transformation to copy the value in extension_ExternalUserEmail claim to the
email claim after extension_XXXX is read from AAD using the AAD-UserReadWithObjectId Technical Profile.
-->
<ClaimsTransformation Id="CopyXXXXEmailAddress" TransformationMethod="CopyClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="extension_XXXX" TransformationClaimType="inputClaim"/>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" TransformationClaimType="outputClaim"/>
</OutputClaims>
</ClaimsTransformation>
...
</ClaimsTransformations>
<ContentDefinitions>
...
</ContentDefinitions>
</BuildingBlocks>
<ClaimsProviders>
<ClaimsProvider>
<DisplayName>Application Insights</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="AppInsights-Common">
<DisplayName>Application Insights</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.Insights.AzureApplicationInsightsProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<!-- The ApplicationInsights instrumentation key, which you use for logging the events -->
<Item Key="InstrumentationKey">{Settings:ApplicationInsightskey}</Item>
<Item Key="DeveloperMode">true</Item>
<Item Key="DisableTelemetry ">false</Item>
</Metadata>
<InputClaims>
<!-- Properties of an event are added through the syntax {property:NAME}, where NAME is the property being added to the event. DefaultValue can be either a static value or a value that's resolved by one of the supported DefaultClaimResolvers. -->
<InputClaim ClaimTypeReferenceId="EventTimestamp" PartnerClaimType="{property:EventTimestamp}" DefaultValue="{Context:DateTimeInUtc}" />
<InputClaim ClaimTypeReferenceId="tenantId" PartnerClaimType="{property:TenantId}" DefaultValue="{Policy:TrustFrameworkTenantId}" />
<InputClaim ClaimTypeReferenceId="PolicyId" PartnerClaimType="{property:Policy}" DefaultValue="{Policy:PolicyId}" />
<InputClaim ClaimTypeReferenceId="CorrelationId" PartnerClaimType="{property:CorrelationId}" DefaultValue="{Context:CorrelationId}" />
<InputClaim ClaimTypeReferenceId="Culture" PartnerClaimType="{property:Culture}" DefaultValue="{Culture:RFC5646}" />
</InputClaims>
</TechnicalProfile>
<TechnicalProfile Id="AppInsights-SignInRequest">
<InputClaims>
<!-- An input claim with a PartnerClaimType="eventName" is required. This is used by the AzureApplicationInsightsProvider to create an event with the specified value. -->
<InputClaim ClaimTypeReferenceId="EventType" PartnerClaimType="eventName" DefaultValue="SignInRequest" />
</InputClaims>
<IncludeTechnicalProfile ReferenceId="AppInsights-Common" />
</TechnicalProfile>
<TechnicalProfile Id="AppInsights-UserSignUp">
<InputClaims>
<InputClaim ClaimTypeReferenceId="EventType" PartnerClaimType="eventName" DefaultValue="UserSignUp" />
</InputClaims>
<IncludeTechnicalProfile ReferenceId="AppInsights-Common" />
</TechnicalProfile>
<TechnicalProfile Id="AppInsights-SignInComplete">
<InputClaims>
<InputClaim ClaimTypeReferenceId="EventType" PartnerClaimType="eventName" DefaultValue="SignInComplete" />
<InputClaim ClaimTypeReferenceId="federatedUser" PartnerClaimType="{property:FederatedUser}" DefaultValue="false" />
<InputClaim ClaimTypeReferenceId="parsedDomain" PartnerClaimType="{property:FederationPartner}" DefaultValue="Not Applicable" />
<InputClaim ClaimTypeReferenceId="identityProvider" PartnerClaimType="{property:IDP}" DefaultValue="Local" />
</InputClaims>
<IncludeTechnicalProfile ReferenceId="AppInsights-Common" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>Local Account</DisplayName>
<TechnicalProfiles>
...
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>Azure Active Directory</DisplayName>
<TechnicalProfiles>
...
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>Local Account SignIn</DisplayName>
<TechnicalProfiles>
<!-- This technical profile is used to read a local B2C user from AAD using an objectId.
I am overriding it here so as to call the CopyXXXXEmailAddress Claims Transformation, which
will copy the value in the extension_XXXX claim to the email claim. That way, when I add the email
claim to output claims, there is something in it when it is added to the claims bag. So,
when the email claim is used in the technical profile (PolicyProfile) in the relying party file
to populate the NameID attribute in the Subject Naming Info element, it is not null. If it is,
this error is thown "The relying party technical profile of policy 'B2C_1A_signup_signin_saml'
in tenant '<tenant>.onmicrosoft.com' specifies the claim type 'email' as the subject naming info
claim, but the claim is not present or is null" -->
<TechnicalProfile Id="AAD-UserReadUsingObjectId">
<OutputClaims>
<!--Read user's email from extension attribute-->
<OutputClaim ClaimTypeReferenceId="extension_XXXX"/>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CopyXXXXEmailAddress" />
</OutputClaimsTransformations>
</TechnicalProfile>
...
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>Self Asserted</DisplayName>
<TechnicalProfiles>
...
</TechnicalProfiles>
</ClaimsProvider>
</ClaimsProviders>
<UserJourneys>
<UserJourney Id="SignUpOrSignIn">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
<ClaimsProviderSelection TargetClaimsExchangeId="MyOrgExchange" />
<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="MyOrgExchange" TechnicalProfileReferenceId="OIDC-MyOrg" />
<ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail" />
<ClaimsExchange Id="TrackSignInRequest" TechnicalProfileReferenceId="AppInsights-SignInRequest" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- For social IDP authentication, attempt to find the user account in the directory. -->
<OrchestrationStep Order="3" 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>
<!-- 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="4" 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>
...
<OrchestrationStep Order="10" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="Saml2AssertionIssuer" />
<!-- Track that we have successfully sent a token -->
<OrchestrationStep Order="11" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="TrackSignInComplete" TechnicalProfileReferenceId="AppInsights-SignInComplete" />
</ClaimsExchanges>
</OrchestrationStep>
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
</UserJourneys>
</TrustFrameworkPolicy>

Azure B2C - change sign in - token returns old email address

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.

Azure Active Directory B2C Custom Invite Policy - Passing Custom Claims Between Steps

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)

Resources