Manipulate signInName in azure ad b2c via REST API

109 Views Asked by At

So, we currently try to switch from our old legacy system to a new IAM Service. And we have decided for Azure AD B2C. But our old legacy system supports all kinds of usernames, something like: "aAADWWADA223" "Test" "[email protected]" "AWDA@..@23" And its kinda hard for us to actually import them properly. And we have actually decided for a JIT migration with the help of the custom policies (the XML policies). But none of us is actually an "identity pro" as Microsoft calls it. So we are basically going for trial and error and we have made some alright progress so far.

So our idea was to actually hash the "signInName" and actually save the hash + the tentant id as the userPrincipalName and the hash as the userName. So the users can still login via their "old" usernames, but in the background we just use the hashes.

We have implemented a small rest-api that actually hashes the names and migrates the accounts just in time. So we have the signInName in our "InputClaims" going to the rest-api. And we wanted to manipulate it there and send it back as an "OutputClaim".

But as soon as we declare the "signInName" as an OutputClaim, we are not allowed to click "Sign In" anymore. Does anybody have an idea, why?

Here is a code-snippet out of "JIT_Migration_TrustFrameworkExtensions":

<!--Demo: Checks if user exists in the migration table. If yes, validate the credentials and migrate the account -->
<TechnicalProfile Id="REST-UserMigration-LocalAccount-SignIn">
  <DisplayName>Migrate user sign-in 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://api.azurecontainerapps.io/api/identity/hash</Item>
    <Item Key="AuthenticationType">None</Item>
    <Item Key="SendClaimsIn">Body</Item>
    <Item Key="AllowInsecureAuthInProduction">True</Item>
  </Metadata>
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="signInName" Required="true"/>
    <InputClaim ClaimTypeReferenceId="password" Required="true"/>
  </InputClaims>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="userPrincipalName"/>
    <OutputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="userName"/>
  </OutputClaims>
  <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop"/>
</TechnicalProfile>

Basically we have tried what I've written in the details of the problem already.

1

There are 1 best solutions below

4
On

If you want to support identities that contain reserved characters then I'd recommend just encoding the characters, then encoding the username entered by the user within B2C itself.

I can't find anything that details which characters can't be used (the docs page seems to be wrong), so I've only used : as we know that's reserved from what you've tried so far.

You'd first still make a Graph call to create the user, but with the : character encoded - I used URL encoding (%3A):

POST https://graph.microsoft.com/v1.0/users

{   
  "accountEnabled": true,
  "displayName": "Username Test",
  "givenName": "Username",
  "surname": "Test",
  "identities": [
  {
    "signInType": "username",
    "issuer": "{tenant_name}.onmicrosoft.com",
    "issuerAssignedId": "testvalue%3A123"
  }],
  "passwordProfile": {
    "password": "{password}",
    "forceChangePasswordNextSignIn": false
  },
  "passwordPolicies": "DisablePasswordExpiration"
}

Then in your sign-in page custom policy, call a claims transformation technical profile as a validation step before your non-interactive sign-in.

<TechnicalProfile Id="Page-SignIn">
  <DisplayName>User Login</DisplayName>
  <Description>Log in to your account</Description>
  <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  <Metadata>
    <Item Key="ContentDefinitionReferenceId">content.generic</Item>
    <Item Key="language.heading">Sign in</Item>
    <Item Key="language.button_continue">Sign in</Item>
    <Item Key="setting.showCancelButton">true</Item>
  </Metadata>
  <IncludeInSso>false</IncludeInSso>
  <OutputClaims>
    <!-- captured the username entered by the user -->
    <OutputClaim ClaimTypeReferenceId="signInUsername" />
    <OutputClaim ClaimTypeReferenceId="signInPassword" Required="true" />

    <OutputClaim ClaimTypeReferenceId="objectId" />
    <OutputClaim ClaimTypeReferenceId="givenName"/>
    <OutputClaim ClaimTypeReferenceId="surname"/>
    <OutputClaim ClaimTypeReferenceId="displayName" />
  </OutputClaims>
  <ValidationTechnicalProfiles>
    <!-- encodes the reserved characters in the username entered by the user -->
    <ValidationTechnicalProfile ReferenceId="Transform-NormaliseUsername" />
    <!-- signs the user in using the encoded username -->
    <ValidationTechnicalProfile ReferenceId="AAD-SignIn-NonInteractive-Email" />
  </ValidationTechnicalProfiles>
  <UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD"/>
</TechnicalProfile>

This claims transformation technical profile should copy the username your user entered (mine is in the claim signInUsername) into an, encodedUsername claim and then call claims transformations to replace the reserved characters with encoded versions.

Claims transformation technical profile

<TechnicalProfile Id="Transform-NormaliseUsername">
  <DisplayName>Normalise username</DisplayName>
  <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="encodedUsername" />
  </OutputClaims>
  <OutputClaimsTransformations>
    <OutputClaimsTransformation ReferenceId="CopyUsername" />
    <OutputClaimsTransformation ReferenceId="NormaliseUsernameColon" />
  </OutputClaimsTransformations>
  <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>

Claims transformations

<ClaimsTransformation Id="CopyUsername" TransformationMethod="CopyClaim">
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="signInUsername" TransformationClaimType="inputClaim"/>
  </InputClaims>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="encodedUsername" TransformationClaimType="outputClaim"/>
  </OutputClaims>
</ClaimsTransformation>
<ClaimsTransformation Id="NormaliseUsernameColon" TransformationMethod="StringReplace">
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="encodedUsername" TransformationClaimType="inputClaim" />
  </InputClaims>
  <InputParameters>
    <InputParameter Id="oldValue" DataType="string" Value=":" />
    <InputParameter Id="newValue" DataType="string" Value="%3A" />
  </InputParameters>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="encodedUsername" TransformationClaimType="outputClaim" />
  </OutputClaims>
</ClaimsTransformation>

You should then update your non-interactive sign-in technical profile to use the username claim rather than the email your user entered.

<TechnicalProfile Id="AAD-SignIn-NonInteractive-Email">
  <DisplayName>AAD ROPC</DisplayName>
  <Protocol Name="OpenIdConnect" />
  <Metadata>
    <Item Key="ProviderName">https://sts.windows.net/</Item>
    <Item Key="METADATA">{B2C AAD metadata endpoint}</Item>
    <Item Key="authorization_endpoint">{B2C AAD token endpoint}</Item>
    <Item Key="client_id">{PolicyEngineProxy client_id}</Item>
    <Item Key="IdTokenAudience">{PolicyEngine client_id}</Item>
    <Item Key="response_types">id_token</Item>
    <Item Key="response_mode">query</Item>
    <Item Key="scope">email openid</Item>

    <Item Key="UsePolicyInRedirectUri">false</Item>
    <Item Key="HttpBinding">POST</Item>
  </Metadata>
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="client_id" DefaultValue="{PolicyEngineProxy client_id}" />
    <InputClaim ClaimTypeReferenceId="resource_id" PartnerClaimType="resource" DefaultValue="{PolicyEngine client_id}" />
    <!-- username is your encoded username, not the email/username the user entered on the page -->
    <InputClaim ClaimTypeReferenceId="encodedUsername" PartnerClaimType="username" Required="true" />
    <InputClaim ClaimTypeReferenceId="signInPassword" PartnerClaimType="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" AlwaysUseDefaultValue="true" />
  </InputClaims>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="oid" />
    <OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="given_name" />
    <OutputClaim ClaimTypeReferenceId="surname" PartnerClaimType="family_name" />
    <OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
  </OutputClaims>
</TechnicalProfile>

You'd need to find out all the characters that aren't supported in an identity and add one claims transformation per character, e.g. if # also weren't supported (even though it is):

<ClaimsTransformation Id="CopyUsername" TransformationMethod="CopyClaim">
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="signInUsername" TransformationClaimType="inputClaim"/>
  </InputClaims>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="encodedUsername" TransformationClaimType="outputClaim"/>
  </OutputClaims>
</ClaimsTransformation>
<ClaimsTransformation Id="NormaliseUsernameColon" TransformationMethod="StringReplace">
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="encodedUsername" TransformationClaimType="inputClaim" />
  </InputClaims>
  <InputParameters>
    <InputParameter Id="oldValue" DataType="string" Value=":" />
    <InputParameter Id="newValue" DataType="string" Value="%3A" />
  </InputParameters>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="encodedUsername" TransformationClaimType="outputClaim" />
  </OutputClaims>
</ClaimsTransformation>
<ClaimsTransformation Id="NormaliseUsernameHash" TransformationMethod="StringReplace">
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="encodedUsername" TransformationClaimType="inputClaim" />
  </InputClaims>
  <InputParameters>
    <InputParameter Id="oldValue" DataType="string" Value="#" />
    <InputParameter Id="newValue" DataType="string" Value="%23" />
  </InputParameters>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="encodedUsername" TransformationClaimType="outputClaim" />
  </OutputClaims>
</ClaimsTransformation>

<!-- other bits of custom policy -->

<TechnicalProfile Id="Transform-NormaliseUsername">
  <DisplayName>Normalise username</DisplayName>
  <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="encodedUsername" />
  </OutputClaims>
  <OutputClaimsTransformations>
    <OutputClaimsTransformation ReferenceId="CopyUsername" />
    <OutputClaimsTransformation ReferenceId="NormaliseUsernameColon" />
    <OutputClaimsTransformation ReferenceId="NormaliseUsernameHash" />
  </OutputClaimsTransformations>
  <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>

Your REST API technical profile should also work equally well using this approach. Instead of calling Transform-NormaliseUsername you'd call your REST-UserMigration-LocalAccount-SignIn but you need the (only) output claim to be encodedUsername to make it work using the technical profiles above.