AD B2C - Missing custom claims after re-acquiring token - asp.net-core

I have an ASP.NET Core 3.1 project using Azure AD B2C to manage authentication. I added a custom claim (i.e. extension property) named extension_userType.
To do so, I added it to my B2C_1A_signup_signin custom policy as an output claim:
<DefaultUserJourney ReferenceId="SignUpOrSignIn" />
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<InputClaims>
<InputClaim ClaimTypeReferenceId="passwordPolicies" DefaultValue="DisablePasswordExpiration, DisableStrongPassword" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" PartnerClaimType="email" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub" />
<OutputClaim ClaimTypeReferenceId="identityProvider" />
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
<OutputClaim ClaimTypeReferenceId="extension_userType" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
Additionally, I also added it to the B2C_1A_TrustFrameworkExtensions policy under the "Local Account Sign In" ClaimsProvider:
<DisplayName>Local Account Sign In</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="login-NonInteractive">
<Metadata>
<Item Key="client_id">OMISSIS</Item>
<Item Key="IdTokenAudience">OMISSIS</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="client_id" DefaultValue="OMISSIS" />
<InputClaim ClaimTypeReferenceId="resource_id" PartnerClaimType="resource" DefaultValue="OMISSIS" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="extension_userType" />
</OutputClaims>
</TechnicalProfile>
<TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Email">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="extension_userType" />
</OutputClaims>
After the user login, the extension_userType claim is available:
But if I restart the web-app, and the signed user has to re-acquire the token (e.g. when calling a method with AuthorizeForScopesAttribute), the new token will not contain the extension_userType claim.
Instead, if the user performs a logout>login, the claim will re-appear.
I analyzed the quick redirect to AD B2C performed when re-acquiring the token: it correctly points to B2C_1A_signup_signin, so I don't understand why the token is incomplete.
Let me know if I can provide you further information to help understand the issue. Thanks in advance :)

Ok I found out why: I forgot to put the related output claim to the AAD-UserReadUsingEmailAddress and AAD-UserReadUsingObjectId technical profiles:
<TechnicalProfile Id="AAD-UserReadUsingEmailAddress">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="extension_userType" />
</OutputClaims>
</TechnicalProfile>
<TechnicalProfile Id="AAD-UserReadUsingObjectId">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="extension_userType" />
</OutputClaims>
</TechnicalProfile>

Related

azure-ad-b2c - How to check if email and phone number match an existing user

Before allowing the user to change their password, I'm trying to validate if their email and phone match an existing one in Azure B2C. If I remove the phone # and validate the email only it's all good, but if I try to validate the phone number with the email, I get the "specified credential could not be found" error.
I was wondering if someone could help me.
Here are my custom policies to verify it:
<TechnicalProfile Id="AAD-UserReadUsingEmailAddressAndPhoneNo">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" Required="true" />
<InputClaim ClaimTypeReferenceId="signInNames.phoneNumber" Required="true" />
</InputClaims>
<OutputClaims>
<!-- Required claims -->
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
<OutputClaim ClaimTypeReferenceId="strongAuthenticationPhoneNumber" />
<!-- Optional claims -->
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="accountEnabled" />
<OutputClaim ClaimTypeReferenceId="otherMails" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
<OutputClaim ClaimTypeReferenceId="signInNames.phoneNumber" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="AssertAccountEnabledIsTrue" />
</OutputClaimsTransformations>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
<ClaimType Id="signInNames.phoneNumber">
<DisplayName>+1XXX</DisplayName>
<DataType>phoneNumber</DataType>
<DefaultPartnerClaimTypes>
<Protocol Name="OpenIdConnect" PartnerClaimType="phone_number" />
</DefaultPartnerClaimTypes>
<UserHelpText>Phone Number that can be used to contact you.</UserHelpText>
<UserInputType>TextBox</UserInputType>
</ClaimType>
<ClaimsProvider>
<DisplayName>Local Account</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="LocalAccountDiscoveryUsingEmailAddress_Custom">
<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="ContentDefinitionReferenceId">api.selfasserted</Item>
</Metadata>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" Required="true" />
<OutputClaim ClaimTypeReferenceId="signInNames.phoneNumber" Required="true" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
<OutputClaim ClaimTypeReferenceId="strongAuthenticationPhoneNumber" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddressAndPhoneNo" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
You might want to verify that the phone number is being stored as signInNames.phoneNumber, instead of one of the other phone number types (like strongAuthenticationPhoneNumber). I know it sounds obvious, but sometimes it happens.
It might also help to break the check up into multiple parts. First check the email address and get the users phone number using UserReadUsingEmailAddress (or some variation of it), then use a second VTP to assert that the phone number you pulled from the account and the phone number the user typed in match.

Custom claim returned from API not included on sign up login but is included on second login

I have created a custom policy that sends some of the user input during sign up to my custom API for validation. If the data validates properly the API returns a custom claim value to be populated in the user.
During a local account sign up this works great and the custom claim that came from the API is returned in the token on the initial login during the sign up.
However during a signup with Google as the IDP, while the claim is populated properly (if I check via GraphAPI) it is not returned in the token on the initial sign up login, only on subsequent logins.
Below is my claim provider to call the API for validation.
<ClaimsProvider>
<DisplayName>Call Validation API</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="REST-ValidateUserData">
<DisplayName>Check Input Data</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://APIGOESHERE
<Item Key="SendClaimsIn">Body</Item>
<!-- Set AuthenticationType to Basic or ClientCertificate in production environments -->
<Item Key="AuthenticationType">Basic</Item>
<!-- REMOVE the following line in production environments -->
<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="extension_studentNumber" />
<InputClaim ClaimTypeReferenceId="extension_dateOfBirth" />
<InputClaim ClaimTypeReferenceId="extension_postalCode"/>
</InputClaims>
<OutputClaims>
<!-- Claims parsed from your REST API -->
<OutputClaim ClaimTypeReferenceId="extension_OPRID" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
Below is my Relying party in the SignUporSignIn custom policy.
<RelyingParty>
<DefaultUserJourney ReferenceId="CustomSignUpSignIn" />
<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="identityProvider" />
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
<OutputClaim ClaimTypeReferenceId="extension_dateOfBirth"/>
<OutputClaim ClaimTypeReferenceId="extension_postalCode"/>
<OutputClaim ClaimTypeReferenceId="extension_studentNumber"/>
<OutputClaim ClaimTypeReferenceId="extension_OPRID"/>
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>

how to add a custom claim to a web token? b2c custom policys

I have a custom policy on Azure B2C.
When the user signs up, I need to remove the display for "DisplayName" (text box) from the signup screen. Instead, I need to copy the email into DisplayName and add the DisplayName to the web token.
I have succesfully completed the first part, I no longer ask the user for the displayName, but I am not getting DisplayName in the token, so I can't know if I am copying it right.
I tried to copy and modify given_name, as that one works, but apparently something went wrong along the lines.
How do I achieve that? or what I am missing?
My custom policy:
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpOrSignIn" />
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName"/>
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
<OutputClaim ClaimTypeReferenceId="identityProvider" />
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
Base file:
<ClaimType Id="displayName">
<DisplayName>Display Name</DisplayName>
<DataType>string</DataType>
<UserHelpText>Your display name.</UserHelpText>
</ClaimType>
...
<ClaimsTransformation Id="CopyEmailAddress" TransformationMethod="CopyClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" TransformationClaimType="inputClaim"/>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" TransformationClaimType="outputClaim"/>
</OutputClaims>
</ClaimsTransformation>
...
<TechnicalProfiles>
<TechnicalProfile Id="login-NonInteractive">
<DisplayName>Local Account SignIn</DisplayName>
<Protocol Name="OpenIdConnect" />
<Metadata>
<Item Key="ProviderName">https://sts.windows.net/</Item>
<Item Key="METADATA">https://login.microsoftonline.com/{tenant}/.well-known/openid-configuration</Item>
<Item Key="authorization_endpoint">https://login.microsoftonline.com/{tenant}/oauth2/token</Item>
<Item Key="response_types">id_token</Item>
<Item Key="response_mode">query</Item>
<Item Key="scope">email openid</Item>
<!-- Policy Engine Clients -->
<Item Key="UsePolicyInRedirectUri">false</Item>
<Item Key="HttpBinding">POST</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="username" Required="true" />
<InputClaim ClaimTypeReferenceId="password" Required="true" />
<InputClaim ClaimTypeReferenceId="grant_type" DefaultValue="password" AlwaysUseDefaultValue="true" />
<InputClaim ClaimTypeReferenceId="scope" DefaultValue="openid" AlwaysUseDefaultValue="true" />
<InputClaim ClaimTypeReferenceId="nca" PartnerClaimType="nca" DefaultValue="1" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="oid" />
<OutputClaim ClaimTypeReferenceId="tenantId" PartnerClaimType="tid" />
<OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="given_name" />
<OutputClaim ClaimTypeReferenceId="surName" PartnerClaimType="family_name" />
<OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="email" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" PartnerClaimType="upn" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
</OutputClaims>
</TechnicalProfile>
....
Try to use in your login-Noninteractive your displayName claim like this:
ClaimTypeReferenceId="email" PartnerClaimType="displayName"
You really don't need the claims transformation "CopyEmailAddress"

Azure B2C OTP UI Elements: UserMessageIfVerificationFailedRetryAllowed not working

I have set the OTP user messages using localization. UserMessageIfInvalidCode is working but UserMessageIfVerificationFailedRetryAllowed is not working. I have the retry count set to 5, however it's only the UserMessageIfInvalidCode that shows when the message is invalid. Not sure what I am missing :
<ContentDefinitions>
<ContentDefinition Id="content.phone.verify">
<LoadUri>~/tenant/templates/AzureBlue/selfAsserted.cshtml</LoadUri>
<RecoveryUri>~/common/default_page_error.html</RecoveryUri>
<DataUri>urn:com:microsoft:aad:b2c:elements:contract:selfasserted:2.1.4</DataUri>
<LocalizedResourcesReferences MergeBehavior="Prepend">
<LocalizedResourcesReference Language="en" LocalizedResourcesReferenceId="content.phone.verify.en" />
</LocalizedResourcesReferences>
</ContentDefinition>
</ContentDefinitions>
<Localization Enabled="true">
<SupportedLanguages DefaultLanguage="en" MergeBehavior="ReplaceAll">
<SupportedLanguage>en</SupportedLanguage>
</SupportedLanguages>
<LocalizedResources Id="content.phone.verify.en">
<LocalizedStrings>
<LocalizedString ElementType="ErrorMessage" StringId="UserMessageIfSessionDoesNotExist">Your code has expired, please request a new one.</LocalizedString>
<LocalizedString ElementType="ErrorMessage" StringId="UserMessageIfMaxRetryAttempted">You have exceeded the number of retries allowed.</LocalizedString>
<LocalizedString ElementType="ErrorMessage" StringId="UserMessageIfMaxNumberOfCodeGenerated">You have exceeded the number of code generation attempts allowed.</LocalizedString>
<LocalizedString ElementType="ErrorMessage" StringId="UserMessageIfInvalidCode">TEST You have entered the wrong code.</LocalizedString>
<LocalizedString ElementType="ErrorMessage" StringId="UserMessageIfSessionConflict">Cannot verify the code, please try again later.</LocalizedString>
<LocalizedString ElementType="ErrorMessage" StringId="UserMessageIfVerificationFailedRetryAllowed">That code is incorrect, please try again.</LocalizedString>
</LocalizedStrings>
</LocalizedResources>
</Localization>
<TechnicalProfile Id="SelfAsserted-mfa">
<DisplayName>MFA</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ContentDefinitionReferenceId">content.phone.verify</Item>
<Item Key="setting.inputVerificationDelayTimeInMilliseconds">500</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CreatePhoneNumberList" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="phoneNumberString" />
</InputClaims>
<DisplayClaims>
<DisplayClaim ClaimTypeReferenceId="phoneNumberString" />
<DisplayClaim DisplayControlReferenceId="phoneVerificationControl" />
</DisplayClaims>
</TechnicalProfile>
<TechnicalProfile Id="MFA-GenerateCode">
<DisplayName>Generate Code</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.OneTimePasswordProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="Operation">GenerateCode</Item>
<Item Key="NumRetryAttempts">5</Item>
<Item Key="ReuseSameCode">true</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="userPhoneNumber" PartnerClaimType="identifier" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="verificationCode" PartnerClaimType="otpGenerated" />
</OutputClaims>
</TechnicalProfile>

Azure B2C getting values from API return object

I can't seem to get the values from the OutputClaims from my return object after making a REST API call to show up in the JWT token. I've tried this when I return a single string it works fine, but this is returning an object that looks like this from my B2CResponseModel object.
Do I have to do some kind of of mapping in the custom policies to match my return object or is there a way for me to just pull a value like I am trying to do below? Thanks!
TrustFrameworkExtensions.xml
<TechnicalProfile Id="REST-UserMigration-LocalAccount-SignUp">
<DisplayName>Migrate user sign-up flow</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://*******/LocalAccountSignUp</Item>
<Item Key="AuthenticationType">ApiKeyHeader</Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="AllowInsecureAuthInProduction">True</Item>
</Metadata>
<CryptographicKeys>
<Key Id="Ocp-Apim-Subscription-Key" StorageReferenceId="B2C_1A_RestApiKey" />
</CryptographicKeys>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInName"/>
<!--AM Added-->
<InputClaim ClaimTypeReferenceId="signInName" />
<InputClaim ClaimTypeReferenceId="password" />
<InputClaim ClaimTypeReferenceId="objectId" />
<InputClaim ClaimTypeReferenceId="newPassword" />
<InputClaim ClaimTypeReferenceId="reenterPassword" />
.......
</InputClaims>
<!--AM Added-->
<OutputClaims>
<!-- Claims parsed from your REST API -->
<OutputClaim ClaimTypeReferenceId="userMessage" />
<OutputClaim ClaimTypeReferenceId="loyaltyNumber"/>
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
TrustFrameworkBase.xml
<ClaimType Id="loyaltyNumber">
<DisplayName>Loyalty Identification</DisplayName>
<DataType>string</DataType>
<UserHelpText>Your loyalty number from your membership card</UserHelpText>
</ClaimType>
<ClaimType Id="userMessage">
<DisplayName>User Message</DisplayName>
<DataType>string</DataType>
<UserHelpText>User Message</UserHelpText>
</ClaimType>
SignInSignup.xml
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpOrSignIn" />
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<OutputClaims>
....
<OutputClaim ClaimTypeReferenceId="userMessage" DefaultValue="" />
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="loyaltyNumber" DefaultValue="" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>