azure-ad-b2cazure-ad-b2c-custom-policy

Email address empty when trying to read it out at the back end


I'm trying to log in using a custom HTML template via sendgrid which will contain the OTP code. But when I verify the code I see that "Username" property is empty in the back end. And it must be going wrong somewhere in my custom policy where the Username should be passed on:

<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="XXXXXXXXXXXXXXXXX.onmicrosoft.com" 
  PolicyId="B2C_1A_DisplayControl_sendgrid_Extensions" 
  PublicPolicyUri="http://XXXXXXXXXXXXXXXXX.onmicrosoft.com/B2C_1A_DisplayControl_sendgrid_Extensions">
  <BasePolicy>
    <TenantId>XXXXXXXXXXXXXXXXX.onmicrosoft.com</TenantId>
    <PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>
  </BasePolicy>
  <BuildingBlocks>
    <ClaimsSchema>
      <ClaimType Id="otp">
        <DisplayName>Secondary One-time password</DisplayName>
        <DataType>string</DataType>
      </ClaimType>
      <ClaimType Id="emailRequestBody">
        <DisplayName>SendGrid request body</DisplayName>
        <DataType>string</DataType>
      </ClaimType>
      <ClaimType Id="verificationCode">
        <DisplayName>Secondary Verification Code</DisplayName>
        <DataType>string</DataType>
        <UserHelpText>Enter your email verification code</UserHelpText>
        <UserInputType>TextBox</UserInputType>
      </ClaimType>
      <ClaimType Id="subject">
        <DisplayName>Email subject</DisplayName>
        <DataType>string</DataType>
      </ClaimType>
      <ClaimType Id="message">
        <DisplayName>Email message</DisplayName>
        <DataType>string</DataType>
      </ClaimType>
      <ClaimType Id="codeIntro">
        <DisplayName>Email code introduction</DisplayName>
        <DataType>string</DataType>
      </ClaimType>
      <ClaimType Id="signature">
        <DisplayName>Email signature</DisplayName>
        <DataType>string</DataType>
      </ClaimType>
    </ClaimsSchema>
    <ClaimsTransformations>
      <ClaimsTransformation Id="GetLocalizedStringsForEmail" TransformationMethod="GetLocalizedStringsTransformation">
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="subject" TransformationClaimType="email_subject" />
          <OutputClaim ClaimTypeReferenceId="message" TransformationClaimType="email_message" />
          <OutputClaim ClaimTypeReferenceId="codeIntro" TransformationClaimType="email_code" />
          <OutputClaim ClaimTypeReferenceId="signature" TransformationClaimType="email_signature" />
        </OutputClaims>
      </ClaimsTransformation>
      <ClaimsTransformation Id="GenerateEmailRequestBody" TransformationMethod="GenerateJson">
        <InputClaims>
          <!-- needed claims depend on the email template used -->
          <InputClaim ClaimTypeReferenceId="email" TransformationClaimType="personalizations.0.to.0.email" />
          <InputClaim ClaimTypeReferenceId="otp" TransformationClaimType="personalizations.0.dynamic_template_data.otp" />

        </InputClaims>
        <InputParameters>
          <InputParameter Id="template_id" DataType="string" Value="d-XXXXXXXXXXXXXXXXXXXXXX" />
          <InputParameter Id="from.email" DataType="string" Value="info@XXXXXXXXX.com" />
        </InputParameters>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="emailRequestBody" TransformationClaimType="outputClaim" />
        </OutputClaims>
      </ClaimsTransformation>
      <ClaimsTransformation Id="CopySignInToEmailClaim" TransformationMethod="CopyClaim">
        <InputClaims>
          <InputClaim ClaimTypeReferenceId="signInName" TransformationClaimType="inputClaim"/>
        </InputClaims>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="email" TransformationClaimType="outputClaim"/>
        </OutputClaims>
      </ClaimsTransformation>
    </ClaimsTransformations>
    <ContentDefinitions>
      <ContentDefinition Id="api.localaccountsignup">
        <DataUri>urn:com:microsoft:aad:b2c:elements:contract:selfasserted:2.1.0</DataUri>
        <LocalizedResourcesReferences MergeBehavior="Prepend">
          <LocalizedResourcesReference Language="en" LocalizedResourcesReferenceId="api.custom-email.en" />
          <LocalizedResourcesReference Language="es" LocalizedResourcesReferenceId="api.custom-email.es" />
        </LocalizedResourcesReferences>
      </ContentDefinition>
      <ContentDefinition Id="api.localaccountpasswordreset">
        <DataUri>urn:com:microsoft:aad:b2c:elements:contract:selfasserted:2.1.0</DataUri>
        <LocalizedResourcesReferences MergeBehavior="Prepend">
          <LocalizedResourcesReference Language="en" LocalizedResourcesReferenceId="api.custom-email.en" />
          <LocalizedResourcesReference Language="es" LocalizedResourcesReferenceId="api.custom-email.es" />
        </LocalizedResourcesReferences>
      </ContentDefinition>
    </ContentDefinitions>
    
    <DisplayControls>
      <DisplayControl Id="emailVerificationControl" UserInterfaceControlType="VerificationControl">
        <!-- input claim to prepopulated the email claim in MFA section -->
        <InputClaims>
          <InputClaim ClaimTypeReferenceId="email"/>
        </InputClaims>
        <DisplayClaims>
          <DisplayClaim ClaimTypeReferenceId="email" Required="true" />
          <DisplayClaim ClaimTypeReferenceId="verificationCode" ControlClaimType="VerificationCode" Required="true" />
        </DisplayClaims>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="email" />
        </OutputClaims>
        <Actions>
          <Action Id="SendCode">
            <ValidationClaimsExchange>
              <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="GenerateOtp" />
              <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="SendOtp" />
            </ValidationClaimsExchange>
          </Action>
          <Action Id="VerifyCode">
            <ValidationClaimsExchange>
              <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="VerifyOtp" />
            </ValidationClaimsExchange>
          </Action>
        </Actions>
      </DisplayControl>
      <DisplayControl Id="emailVerificationSSPRControl" UserInterfaceControlType="VerificationControl">
        <InputClaims>
          <InputClaim ClaimTypeReferenceId="email" DefaultValue="{OIDC:LoginHint}" AlwaysUseDefaultValue="true" />
        </InputClaims>
        <DisplayClaims>
          <DisplayClaim ClaimTypeReferenceId="email" Required="true" />
          <DisplayClaim ClaimTypeReferenceId="verificationCode" ControlClaimType="VerificationCode" Required="true" />
        </DisplayClaims>
        <OutputClaims></OutputClaims>
        <Actions>
          <Action Id="SendCode">
            <ValidationClaimsExchange>
              <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="AAD-CheckEmailAddressExists" />
              <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="GenerateOtp" />
              <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="SendOtp">
                <Preconditions>
                  <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
                    <Value>objectId</Value>
                    <Action>SkipThisValidationTechnicalProfile</Action>
                  </Precondition>
                </Preconditions>
              </ValidationClaimsExchangeTechnicalProfile>
            </ValidationClaimsExchange>
          </Action>
          <Action Id="VerifyCode">
            <ValidationClaimsExchange>
              <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="VerifyOtp" />
            </ValidationClaimsExchange>
          </Action>
        </Actions>
      </DisplayControl>
    </DisplayControls>
  </BuildingBlocks>
  <ClaimsProviders>
    <ClaimsProvider>
      <DisplayName>One time password technical profiles</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="GenerateOtp">
          <DisplayName>Generate one time password</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="CodeExpirationInSeconds">60</Item>
            <Item Key="CodeLength">6</Item>
            <Item Key="CharacterSet">0-9</Item>
            <Item Key="ReuseSameCode">true</Item>
            <Item Key="NumRetryAttempts">2</Item>
          </Metadata>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="email" PartnerClaimType="identifier" />
          </InputClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="otp" PartnerClaimType="otpGenerated" />
          </OutputClaims>
        </TechnicalProfile>
        <TechnicalProfile Id="VerifyOtp">
          <DisplayName>Verify one time password</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.OneTimePasswordProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          <Metadata>
            <Item Key="Operation">VerifyCode</Item>
          </Metadata>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="email" PartnerClaimType="identifier" />
            <InputClaim ClaimTypeReferenceId="verificationCode" PartnerClaimType="otpToVerify" />
          </InputClaims>
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>
    <ClaimsProvider>
      <DisplayName>RestfulProvider</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="SendOtp">
          <DisplayName>Use SendGrid's email API to send the code the the user</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          <Metadata>
            <Item Key="ServiceUrl">https://api.sendgrid.com/v3/mail/send</Item>
            <Item Key="AuthenticationType">Bearer</Item>
            <Item Key="SendClaimsIn">Body</Item>
            <Item Key="ClaimUsedForRequestPayload">emailRequestBody</Item>
          </Metadata>
          <CryptographicKeys>
            <Key Id="BearerAuthenticationToken" StorageReferenceId="B2C_1A_SendGridSecret" />
          </CryptographicKeys>
          <InputClaimsTransformations>
            <InputClaimsTransformation ReferenceId="GenerateEmailRequestBody" />
          </InputClaimsTransformations>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="emailRequestBody" />
          </InputClaims>
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>
    <ClaimsProvider>
      <DisplayName>Local Account</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="LocalAccountSignUpWithLogonEmail">
          <InputClaimsTransformations>
            <InputClaimsTransformation ReferenceId="GetLocalizedStringsForEmail" />
          </InputClaimsTransformations>
          <DisplayClaims>
            <DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
            <DisplayClaim ClaimTypeReferenceId="displayName" Required="true" />
            <DisplayClaim ClaimTypeReferenceId="givenName" Required="true" />
            <DisplayClaim ClaimTypeReferenceId="surName" Required="true" />
            <DisplayClaim ClaimTypeReferenceId="newPassword" Required="true" />
            <DisplayClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
          </DisplayClaims>
        </TechnicalProfile>
        <TechnicalProfile Id="AAD-CheckEmailAddressExists">
          <Metadata>
            <Item Key="Operation">Read</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item>
          </Metadata>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" Required="true"/>
          </InputClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="objectId"/>
          </OutputClaims>
          <IncludeTechnicalProfile ReferenceId="AAD-Common"/>
        </TechnicalProfile>
        <TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Email">
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="signInName"/>
          </InputClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="signInName"/>
          </OutputClaims>
          <OutputClaimsTransformations>
            <OutputClaimsTransformation ReferenceId="CopySignInToEmailClaim"/>
          </OutputClaimsTransformations>
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>
    <ClaimsProvider>
      <DisplayName>Reset Password</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="LocalAccountDiscoveryUsingEmailAddress">
          <InputClaimsTransformations>
            <InputClaimsTransformation ReferenceId="GetLocalizedStringsForEmail" />
          </InputClaimsTransformations>
          <DisplayClaims>
            <DisplayClaim DisplayControlReferenceId="emailVerificationSSPRControl"/>
          </DisplayClaims>
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>
    <ClaimsProvider>
    <DisplayName>MFA</DisplayName>
    <TechnicalProfiles>
      <TechnicalProfile Id="Authenticate2Factor">
        <DisplayName>Verify Email</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.localaccountpasswordreset</Item>
        </Metadata>
        <IncludeInSso>false</IncludeInSso>
        <DisplayClaims>
          <DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
        </DisplayClaims>
      </TechnicalProfile>
    </TechnicalProfiles>
    </ClaimsProvider>
  </ClaimsProviders>
  <UserJourneys>
    <UserJourney Id="SignUpOrSignInV2">
      <OrchestrationSteps>
        <!-- Existing CombinedSignInAndSignUp Step -->
        <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
          <ClaimsProviderSelections>
            <ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
          </ClaimsProviderSelections>
          <ClaimsExchanges>
            <ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- Step 2: Registration Step if user is not registered -->
        <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>
        <OrchestrationStep Order="3" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
              <Value>email</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="MFA-Exchange" TechnicalProfileReferenceId="Authenticate2Factor" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="4" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- Step 7: Issue Token -->
        <OrchestrationStep Order="5" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
      </OrchestrationSteps>
      <ClientDefinition ReferenceId="DefaultWeb" />
    </UserJourney>
  </UserJourneys>
</TrustFrameworkPolicy>

What am I doing wrong?


Solution

  • Add this as part of your RP:

    <OutputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="Username" />

    No more claims declaration is needed for this as it's part of JWT. It's been tested and here is a cropped JWT claims where we could see the Username claim as a copy of signInName as follows.

    {"auth_time":1733398231,"email":"abc@xyz.com","iat":1733398231,"name":"abc@xyz.com","nonce":"defaultNonce","Username":"abc@xyz.com"}

    Chaining conversation is added here for our reference