Search code examples
azureoauth-2.0azure-active-directoryazure-ad-b2cazure-ad-b2c-custom-policy

Azure B2C Policy for Azure AD IdP and Local Logon Problem


I have what should seem like a very common scenario yet no solution in the policy starter pack or in any of the public repo's and custom policy examples.

I have an application which is used by both internal staff and external customers. I am using B2C for this and our own Azure AD as a 'social' IdP, and local logon for external users.

The built-in functionality through the Azure Portal does not meet the requirements for numerous reasons. The external user accounts are created manually in the B2C directory and signup is prohibited. Thus, SignUpSignSignIn is unviable. The experience I am trying to achieve is:

If LocalLogon Then
    Authenticate with Azure B2C Directory
    Redirect to Application
Else
    If AADSocialIdP Selected
        Authenticate with Azure AD
        If User Exists in B2C Then
            Redirect to Application
        Else
            Create User in B2C using claims received (do not prompt for email verification)
            Redirect to Application

I have resorted to using custom policies, using SocialAndLocalAccounts from the starter pack as a baseline, and have significantly modified the UserJourney so that single sign-on with AAD is achieved, the user is not prompted for their name, surname, email address, and then to verify their email address (as is the case with the built-in functionality). And, the user is properly redirected to the application. However, by creating this AAD TechnicalProfile and integrating it with the SignUpSignIn journey - though I disabled Signup through various changes in the policy pack.

However, once this is integrated, the local logon is broken. I have used the vanilla LocalAccounts policy pack and confirmed that it works and redirects to the application with the claims as expected, but once I add my AAD TechnicalProfile and ClaimsExchange in then when using local login all I get is Username or password is incorrect.

I believe this is an issue with the UserJourney I've written but at the moment I'm lost as to invoke a different journey for a local logon to a social one. I believe that my TechnicalProfile is overwriting claims during the journey which is causing this error.

My AAD TechnicalProfile is:

        <TechnicalProfile Id="AAD-GB-OpenIdConnect">
          <DisplayName>XXXXXXXXXXXXX</DisplayName>
          <Description>XXXXXXXXXXXXX</Description>
          <Protocol Name="OpenIdConnect"/>
          <OutputTokenFormat>JWT</OutputTokenFormat>
          <Metadata>
            <Item Key="METADATA">https://login.microsoftonline.com/XXXXXXXXXXXXX/v2.0/.well-known/openid-configuration</Item>
            <Item Key="client_id">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</Item>
            <Item Key="response_types">code</Item>
            <Item Key="scope">openid profile</Item>
            <Item Key="response_mode">form_post</Item>
            <Item Key="HttpBinding">POST</Item>
            <Item Key="UsePolicyInRedirectUri">false</Item>
            <Item Key="Prompt">true</Item>
          </Metadata>
          <CryptographicKeys>
            <Key Id="client_secret" StorageReferenceId="B2C_1A_PortalAADSecret"/>
          </CryptographicKeys>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="oid"/>
            <OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="given_name" />
            <OutputClaim ClaimTypeReferenceId="surName" PartnerClaimType="family_name" />
            <OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="socialIdpAuthentication" AlwaysUseDefaultValue="true" />
            <OutputClaim ClaimTypeReferenceId="identityProvider" PartnerClaimType="iss" />
            <OutputClaim ClaimTypeReferenceId="identityProviderAccessToken" PartnerClaimType="{oauth2:access_token}" />
            <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="email" />
          </OutputClaims>
          <OutputClaimsTransformations>
            <OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName"/>
            <OutputClaimsTransformation ReferenceId="CreateUserPrincipalName"/>
            <OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId"/>
            <OutputClaimsTransformation ReferenceId="CreateSubjectClaimFromAlternativeSecurityId"/>
            <OutputClaimsTransformation ReferenceId="CreateOtherMailsFromEmail"/>
          </OutputClaimsTransformations>
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-SocialLogin"/>
        </TechnicalProfile>

It is worth mentioning here that I have created a CreateOtherMailsFromEmail OutputClaimsTransformation which basically creates an emails output claim, since the application is designed to take the first element of an array of emails, as opposed to a single email address.

My UserJourney is as follows:

    <UserJourney Id="CustomSignIn">
      <OrchestrationSteps>
        
          <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
            <ClaimsProviderSelections>
              <ClaimsProviderSelection TargetClaimsExchangeId="AzureADXXXXXXXXExchange" />
              <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="AzureADXXXXXXXXExchange" TechnicalProfileReferenceId="AAD-GB-OpenIdConnect" />
              <ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail" />
            </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>

          <!-- 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="4" 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="5" 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>
          <!-- 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="6" 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="7" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
  
        </OrchestrationSteps>
      <ClientDefinition ReferenceId="DefaultWeb" />
    </UserJourney>

I have enabled debugging with App Insights and am examining the logs through VSCode but didn't find anything helpful.

How can this journey be adapted to support both login methods?


Solution

  • This issue is the result of specifying output claims with the "required" attribute set in the unified SignInSignUp relying party section. All were available in the bag through the Social IdP flow but not through the local account sign-in option.

    When specifying "required" output claims you must ensure that each possible user journey will follow a technical profile chain which retrieves or adds these claims. In my specific case, the specification of claims required by the application developer included some which were not made available through the baseline technical profiles in the starter pack.

    To resolve, I had to modify several technical profiles, create a unique claims transformation and apply it as an output claims transformation to the AAD-UserReadUsingObjectId baseline technical profile.

    aka.ms/iefsetup

    This is a very useful tool which will help to create a Social IdP and Local Login custom B2C configuration (credit @Jas Suri - MSFT). It provides a fully automated means to customise and deploy all the necessary configuration to configure a B2C tenant to use the Identity Experience Framework.

    In my case this was not the entire solution for me but it helped me take a fresh look at exactly how the framework operates and eventually achieve the desired solution.