I have successfully setup Azure B2C using the local account start packs using B2C as the IDP, we've integrated with a partner and can access their application. How do we go about Passing the local IDP accounts 'email' attribute to the partner as a claim. They're getting first, last name and display name without issue but not receiving the email claim.
Similar issue to this: Azure B2C SAML Custom Policy Assert Email
I've inserted this into the BaseFramework
<ClaimType Id="email">
<DisplayName>Email Address</DisplayName>
<DataType>string</DataType>
<DefaultPartnerClaimTypes>
<Protocol Name="OAuth2" PartnerClaimType="email" />
<Protocol Name="OpenIdConnect" PartnerClaimType="email" />
<Protocol Name="SAML2" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email" />
</DefaultPartnerClaimTypes>
...
</ClaimType>
And this Technical Profile
<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" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
and then this in the SAML flow:
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpOrSignInWithCA" />
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="SAML2" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="email" DefaultValue="" />
<OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="objectId" />
</OutputClaims>
<SubjectNamingInfo ClaimType="objectId" ExcludeAsClaim="true" />
</TechnicalProfile>
</RelyingParty>
But it's still not working
As above modified as per documentation
You didn’t need to make any of these changes, except in the third snippet you needed to put this
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" PartnerClaimType="email" />
This takes the signInNames.emailAddress and issues it as “email” into the token.
Currently you defined a claim called email, read a claim called signInNames.emailAddress, then try to issue a claim called email into the token, which will be null.
Related
I am using B2C custom policies which allows signup/signin with the username instead of the traditional email.
As part of the signup process, I am saving the Email in the otherMails attribute.
when choosing MFA as Email, I don't see the email field prepopulated with the email that I have on user record.
Can otherMails attribute be used for MFA email?
<ClaimsTransformation Id="CreateEmailsFromOtherMailsAndSignInNamesInfo" TransformationMethod="AddItemToStringCollection">
<InputClaims>
<InputClaim ClaimTypeReferenceId="otherMails" TransformationClaimType="collection" />
</InputClaims>
<TechnicalProfile Id="AAD-UserWriteUsingLogonName">
<PersistedClaims>
<PersistedClaim ClaimTypeReferenceId="otherMails" />
</PersistedClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="otherMails" />
</OutputClaims>
<TechnicalProfile Id="LocalAccountSignUpWithLogonName">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="otherMails" />
</OutputClaims>
<TechnicalProfile Id="LocalAccountDiscoveryUsingUserNameAndValidateStrongAuthenticationEmailAddress">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="otherMails" />
</OutputClaims>
<TechnicalProfile Id="AAD-ReadCommon">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="otherMails" />
</OutputClaims>
<RelyingParty>
<OutputClaim ClaimTypeReferenceId="otherMails" PartnerClaimType="emails" />
</RelyingParty>
The field used for MFA is "strongAuthenticationEmailAddress".
That's used by the back end so I doubt it can be changed.
I have a scenario where i have to pass a query parameter in the URL to my custom sign-up policy and so far all my attempts did not work. there seem to be something that i am missing following the guidelines i found in github. I am trying to pass LoyaltyNumber and i have this attribute defined in my policy as extension_LoyaltyNumber. Below is the snippet
my custom signup policy
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUp" />
<UserJourneyBehaviors>
<ContentDefinitionParameters>
<Parameter Name="LoyaltyNumber">{OAUTH-KV:LoyaltyNumber}</Parameter>
</ContentDefinitionParameters>
</UserJourneyBehaviors>
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
<InputClaim ClaimTypeReferenceId="extension_LoyaltyNumber" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" PartnerClaimType="email" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
<OutputClaim ClaimTypeReferenceId="extension_ValidPassword" />
<OutputClaim ClaimTypeReferenceId="extension_LoyaltyNumber" AlwaysUseDefaultValue="true"/>
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
in my TrustFrameworkExtension.xml, i have defined it in the Local Account as follows
<ClaimsProvider>
<DisplayName>Local Account</DisplayName>
<TechnicalProfiles>
<!--Local account sign-up page-->
<TechnicalProfile Id="LocalAccountSignUpWithLogonEmail">
<Metadata>
<Item Key="IncludeClaimResolvingInClaimsHandling">true</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="extension_LoyaltyNumber" AlwaysUseDefaultValue="true" DefaultValue="{OAUTH-K:LoyaltyNumber}" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
<OutputClaim ClaimTypeReferenceId="newPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surName" />
</OutputClaims>
</TechnicalProfile>
<TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Email">
<Metadata>
<Item Key="setting.showSignupLink">false</Item>
</Metadata>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
i also have it in the building bolocks section as...
<BuildingBlocks>
<ClaimsSchema>
<ClaimType Id="extension_LoyaltyNumber">
<DisplayName>Loyality-Number</DisplayName>
<DataType>string</DataType>
<UserHelpText>Your loyality from your membership card</UserHelpText>
</ClaimType>
</ClaimsSchema>
</BuildingBlocks>
i have it also to write to Azure Active Directory claims provider section as follows
<ClaimsProvider>
<DisplayName>Azure Active Directory</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="AAD-Common">
<Metadata>
<!--Insert b2c-extensions-app application ID here, for example: 11111111-1111-1111-1111-111111111111-->
<Item Key="ClientId">11111111-1111-1111-1111-111111111111</Item>
<!--Insert b2c-extensions-app application ObjectId here, for example: 22222222-2222-2222-2222-222222222222-->
<Item Key="ApplicationObjectId">22222222-2222-2222-2222-222222222222</Item>
</Metadata>
</TechnicalProfile>
<TechnicalProfile Id="AAD-UserWriteUsingLogonEmail">
<PersistedClaims>
<PersistedClaim ClaimTypeReferenceId="extension_LoyaltyNumber" />
</PersistedClaims>
</TechnicalProfile>
<TechnicalProfile Id="AAD-UserReadUsingObjectId">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="extension_LoyaltyNumber" />
</OutputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
And the redirect happens from my web application using the following method
public void SignUpNewUser()
{
if (!Request.IsAuthenticated)
{
var authenticationProperties = new AuthenticationProperties();
authenticationProperties.Dictionary.Add("LoyaltyNumber", "556677");
authenticationProperties.RedirectUri = "/";
HttpContext.GetOwinContext().Authentication.Challenge(authenticationProperties, Startup.SignUpPolicyId);
}
}
The result i am getting after the user sign-up for the custom attribute extension_LoyaltyNumber is {OAUTH-K:LoyaltyNumber}
Somehow the value 556677 i pass as a query param is not getting to this custom attribute and get stored in Azure user attribute
Can you help?
Thanks
Add
<OutputClaim ClaimTypeReferenceId="extension_LoyaltyNumber" />
to LocalAccountSignUpWithLogonEmail. Output claims allow the claim to be used in subsequent orchestration steps.
In the <RelyingParty> section, change your output claim to the following, its already resolved so no need to resolve it again here:
<OutputClaim ClaimTypeReferenceId="extension_LoyaltyNumber" />
What I do when I want to have query string parameters handled is this:
Create a TechnicalProfile:
<TechnicalProfile Id="GetMyParameter">
<DisplayName>GetMyParameter</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
<Metadata>
<Item Key="IncludeClaimResolvingInClaimsHandling">true</Item>
</Metadata>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="myParameter" AlwaysUseDefaultValue="true" DefaultValue="{OAUTH-KV:myparameter}" />
</OutputClaims>
</TechnicalProfile>
Then run that profile in an OrchestrationStep before the claim value is required (it can be the first one or somewhere later along the way, doesn't matter much as far as I can tell).
You can have as many parameters in the technical profile as you wish. Works every time, doesn't crash if you don't provide value.
I Faced with the same problem, and found answer in official documentation
Don't forget to create claimType in your claims schema
<ClaimType Id="q_param">
<DisplayName>q_param</DisplayName>
<DataType>string</DataType>
<UserHelpText>Special parameter passed for authentication context</UserHelpText>
</ClaimType>
Additionally you need to set output claim in technical profile like in example
from TrustFrameworkExtensions.xml
<ClaimsProvider>
<DisplayName>Local Account SignIn</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="login-NonInteractive">
<Metadata>
<Item Key="client_id">ProxyIdentityExperienceFrameworkAppId</Item>
<Item Key="IdTokenAudience">IdentityExperienceFrameworkAppId</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="client_id" DefaultValue="ProxyIdentityExperienceFrameworkAppId" />
<InputClaim ClaimTypeReferenceId="resource_id" PartnerClaimType="resource" DefaultValue="IdentityExperienceFrameworkAppId" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="q_param" DefaultValue="{OAUTH-KV:q_param}" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
I'm following this article to pass email and displayName as id_token_hint to my custom policy. Following is the technical profile I'm using to extract the data:
<TechnicalProfiles>
<TechnicalProfile Id="IdTokenHint_ExtractClaims">
<DisplayName> My ID Token Hint TechnicalProfile</DisplayName>
<Protocol Name="None" />
<CryptographicKeys>
<Key Id="client_secret" StorageReferenceId="B2C_1A_ClientAssertionSigningKey" />
</CryptographicKeys>
<OutputClaims>
<!--Sample: Read the email cliam from the id_token_hint-->
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="newUserEmail"/>
<OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="displayName"/>
</OutputClaims>
</TechnicalProfile>
Problem is, I'm only able to extract the email value. The displayName claim type not getting extracted. I checked the id_token_hint value in jwt.ms, value for both email and displayName is present there. How can i solve this?
You must also add the incoming claims as the input claims for the RelyingParty technical profile.
Example
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpOrSignIn" />
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<!-- The following claims are read from the ID token -->
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="newUserEmail" />
<InputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="displayName" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub" />
...
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
I am using AZure Ad B2C tenant and i have custom Consent field on signup. This is working fine and whenever we update consent its prompting to user.
I have to keep history of user's consent. Lets say on signup user1 accepted Consent (v1) and some time later user1 accepted Consent (v2), etc.
I need the history of all consents accepted/declined by user. Is that possible to retrieve?
I have tried the following.
Added two extension claim along with consent field with string type (since stringcollection is not supported for extension claims)
<ClaimType Id="extension_TermsOfUseConsented">
<DisplayName>Terms of Use Consented</DisplayName>
<DataType>string</DataType>
<UserInputType>CheckboxMultiSelect</UserInputType>
<Restriction>
<Enumeration Text="I am agreeing to the terms of use" Value="2018-10-29" SelectByDefault="false" />
</Restriction>
</ClaimType>
<ClaimType Id="extension_TempConsent">
<DisplayName>temp Consent</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="extension_ConsentHistory">
<DisplayName>consent history</DisplayName>
<DataType>string</DataType>
</ClaimType>
Added the above field in signup relyingparty section.
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpOrSignIn-withConsent" />
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<InputClaims>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" PartnerClaimType="email" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
<OutputClaim ClaimTypeReferenceId="extension_TermsOfUseConsented" />
<OutputClaim ClaimTypeReferenceId="extension_ConsentHistory" />
<OutputClaim ClaimTypeReferenceId="extension_TempConsent" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
In user journey step 3, reading the above claim in "AAD-UserReadUsingObjectId" technical profile.
<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" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="extension_TermsOfUseConsented" />
<OutputClaim ClaimTypeReferenceId="extension_TempConsent" />
<OutputClaim ClaimTypeReferenceId="extension_ConsentHistory" />
<OutputClaim ClaimTypeReferenceId="extension_ReadStringFromVP" />
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
In step 4, reading consent value and if terms is new then prompting user to accept. In claims exchange calling below technical profile.
<TechnicalProfile Id="SelfAsserted-Consent">
<DisplayName>User Consent</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.consent</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaims />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="extension_TermsOfUseConsented" Required="true" />
<OutputClaim ClaimTypeReferenceId="extension_TempConsent" DefaultValue="," />
<OutputClaim ClaimTypeReferenceId="extension_ConsentHistory" DefaultValue="," />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="AppendConsent" />
<OutputClaimsTransformation ReferenceId="TakeConsentBackup" />
</OutputClaimsTransformations>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-WriteUserConsentByObjectId-ThrowIfNotExists" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
In ClaimsTransformation doing some string manupulation
<ClaimsTransformation Id="TakeConsentBackup" TransformationMethod="FormatStringMultipleClaims">
<InputClaims>
<InputClaim ClaimTypeReferenceId="extension_ConsentHistory" TransformationClaimType="inputClaim1" />
<InputClaim ClaimTypeReferenceId="extension_TempConsent" TransformationClaimType="inputClaim2" />
</InputClaims>
<InputParameters>
<InputParameter Id="stringFormat" DataType="string" Value="{1},{0}" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="extension_ConsentHistory" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
<ClaimsTransformation Id="AppendConsent" TransformationMethod="FormatStringMultipleClaims">
<InputClaims>
<InputClaim ClaimTypeReferenceId="extension_TermsOfUseConsented" TransformationClaimType="inputClaim1" />
<InputClaim ClaimTypeReferenceId="extension_TempConsent" TransformationClaimType="inputClaim2" />
</InputClaims>
<InputParameters>
<InputParameter Id="stringFormat" DataType="string" Value="{1}{0}" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="extension_TempConsent" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
In AAD-WriteUserConsentByObjectId-ThrowIfNotExists writing the extension claim as below.
<TechnicalProfile Id="AAD-WriteUserConsentByObjectId-ThrowIfNotExists">
<Metadata>
<Item Key="Operation">Write</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="objectId" Required="true" />
</InputClaims>
<PersistedClaims>
<PersistedClaim ClaimTypeReferenceId="objectId" />
<PersistedClaim ClaimTypeReferenceId="extension_TermsOfUseConsented" />
<PersistedClaim ClaimTypeReferenceId="extension_ConsentHistory" />
On claim return, its showing string manipulated values (on time on consent)
but on next login (without consent) its not returning the extension field. getting only extension_TermsOfUseConsented field value.
enter image description here
The expected claim extension manipulated in policy only not from user input. Is that a problem?
what i am missing here?
thanks in advance.
I want to programatically set user attributes for the sign up policy. I saw a previous question (Pass parameters to Sign-up policy) asked over a year ago and it was not possible at the time. Any update on this?
Is this possible with the AuthenticationProperties.Dictionary property? Something like this?
HttpContext.GetOwinContext().Set("Policy", Startup.SignUpPolicyId);
var authenticationProperties = new AuthenticationProperties();
authenticationProperties.Dictionary.Add("myattribute", "myvalue");
HttpContext.GetOwinContext().Authentication.Challenge(authenticationProperties);
This can be implemented using a custom policy.
A working sample of passing an input claim from a relying party application to a custom policy (e.g. an invitation flow as a sign-up policy) is here.
In the WingTipGamesWebApplication project, the InvitationController controller class has two action methods, Create and Redeem.
The Create action method sends a signed redemption link to the email address for the invited user. This redemption link contains this email address.
The Redeem action method handles the redemption link. It passes the email address, as the verified_email claim in a JWT that is signed with the client secret of the Wingtip Games application (see the CreateSelfIssuedToken method in the Startup class in the WingTipGamesWebApplication project), from the redemption link to the Invitation policy.
The Invitation policy can be found at here.
The Invitation policy declares the verified_email claim as an input claim:
<RelyingParty>
<DefaultUserJourney ReferenceId="Invitation" />
<TechnicalProfile Id="Invitation">
<InputTokenFormat>JWT</InputTokenFormat>
<CryptographicKeys>
<Key Id="client_secret" StorageReferenceId="WingTipGamesClientSecret" />
</CryptographicKeys>
<InputClaims>
<InputClaim ClaimTypeReferenceId="extension_VerifiedEmail" />
</InputClaims>
</TechnicalProfile>
</RelyingParty>
The extension_verifiedEmail claim type, which is declared as a read-only field (so that it can't be modified by the end user), is mapped to the verified_email input claim:
<BuildingBlocks>
<ClaimsSchema>
<ClaimType Id="extension_VerifiedEmail">
<DisplayName>Verified Email</DisplayName>
<DataType>string</DataType>
<DefaultPartnerClaimTypes>
<Protocol Name="OAuth2" PartnerClaimType="verified_email" />
<Protocol Name="OpenIdConnect" PartnerClaimType="verified_email" />
<Protocol Name="SAML2" PartnerClaimType="http://schemas.wingtipb2c.net/identity/claims/verifiedemail" />
</DefaultPartnerClaimTypes>
<UserInputType>Readonly</UserInputType>
</ClaimType>
</ClaimsSchema>
</BuildingBlocks>
The Invitation user journey can be found in here.
The second orchestration step of the Invitation user journey executes the LocalAccount-Registration-VerifiedEmail technical profile:
<UserJourney Id="Invitation">
<OrchestrationSteps>
...
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
...
<ClaimsExchange Id="LocalAccountRegistrationExchange" TechnicalProfileReferenceId="LocalAccount-Registration-VerifiedEmail" />
</ClaimsExchanges>
</OrchestrationStep>
</OrchestrationSteps>
</UserJourney>
The LocalAccount-Registration-VerifiedEmail technical profile copies from the extension_verifiedEmail claim to the email claim and then displays the sign-up form with the verified email address (the extension_verifiedEmail claim):
<TechnicalProfile Id="LocalAccount-Registration-VerifiedEmail">
<DisplayName>WingTip 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.localaccount.registration</Item>
<Item Key="IpAddressClaimReferenceId">IpAddress</Item>
<Item Key="language.button_continue">Create</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CreateEmailFromVerifiedEmail" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="extension_VerifiedEmail" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="extension_VerifiedEmail" Required="true" />
<OutputClaim ClaimTypeReferenceId="newPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="displayName" Required="true" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
<OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="newUser" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="sub" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AzureActiveDirectoryStore-WriteUserByEmail-ThrowIfExists" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SSOSession-AzureActiveDirectory" />
</TechnicalProfile>
This LocalAccount-Registration-VerifiedEmail technical profile references the AzureActiveDirectoryStore-WriteUserByEmail-ThrowIfExists validation technical profile that saves the local account with the verified email address (the email claim):
<TechnicalProfile Id="AzureActiveDirectoryStore-WriteUserByEmail-ThrowIfExists">
<Metadata>
<Item Key="Operation">Write</Item>
<Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">true</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" Required="true" />
</InputClaims>
<PersistedClaims>
<PersistedClaim ClaimTypeReferenceId="displayName" />
<PersistedClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" />
<PersistedClaim ClaimTypeReferenceId="givenName" />
<PersistedClaim ClaimTypeReferenceId="newPassword" PartnerClaimType="password" />
<PersistedClaim ClaimTypeReferenceId="passwordPolicies" DefaultValue="DisablePasswordExpiration" />
<PersistedClaim ClaimTypeReferenceId="surname" />
<PersistedClaim ClaimTypeReferenceId="verified.strongAuthenticationPhoneNumber" PartnerClaimType="strongAuthenticationPhoneNumber" />
</PersistedClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="newUser" PartnerClaimType="newClaimsPrincipalCreated" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CreateSubject" />
</OutputClaimsTransformations>
<IncludeTechnicalProfile ReferenceId="AzureActiveDirectoryStore-Common" />
<UseTechnicalProfileForSessionManagement ReferenceId="SSOSession-AzureActiveDirectory" />
</TechnicalProfile>