Search code examples
azureazure-ad-b2cazure-ad-b2c-custom-policy

Sign in to Azure B2C with an OpenID account (Microsoft Work email) but prevent user from signing up


I am working on a Single Page App (SPA) and am trying to set up a custom user flow policy for our clients.

Currently a user can log in via a created AAD email that we make in our Azure B2C tenant. What we are now tasked with is allowing users from other organizations to log in if they are invited as an External AAD User so they will follow their own organizations security policies (MFA, password complexity, etc).

I am having trouble figuring out how to set up my .xml files (TrustFrameworkExtensions, Base, SignUpOrSignIn) in such a way that allows local accounts to login as well as OpenID accounts to login and not create a new user, but rather use the invited email.

I have followed the MS Learn tutorial pages and downloaded the custom policy starterpack.

(I could also be looking in a very wrong place and don't know it as I am very unfamiliar with Azure B2C)

First time poster so please let me know if any more context is required

Attached below are the policy files I have been editing with confidential information removed.

TrustFrameworkExtensions.xml

<?xml version="1.0" encoding="utf-8"?>
<TrustFrameworkPolicy>

  <BasePolicy>
    <TenantId><tenantid>.onmicrosoft.com</TenantId>
    <PolicyId><frameworklocalization></PolicyId>
  </BasePolicy>

  <BuildingBlocks>
  </BuildingBlocks>

  <ClaimsProviders>

    <ClaimsProvider>
      <DisplayName>Local Account SignIn</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="login-NonInteractive">
          <Metadata>
            <Item Key="client_id">000000-0000-0000-0000-000000000000</Item>
            <Item Key="IdTokenAudience">000000-0000-0000-0000-000000000000</Item>
            <Item Key="setting.showSignupLink">false</Item>
          </Metadata>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="client_id"
              DefaultValue="000000-0000-0000-0000-000000000000" />
            <InputClaim ClaimTypeReferenceId="resource_id" PartnerClaimType="resource"
              DefaultValue="000000-0000-0000-0000-000000000000" />
          </InputClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="email" />
            <OutputClaim ClaimTypeReferenceId="objectId" />
          </OutputClaims>
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>

    <ClaimsProvider>
      <Domain>externallogin</Domain>
      <DisplayName>External Login</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="AADCommon-OpenIdConnect">
          <DisplayName>Microsoft Login</DisplayName>
          <Description>Login with your Microsoft account</Description>
          <Protocol Name="OpenIdConnect" />
          <Metadata>


            <Item Key="METADATA">
              https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration</Item>
            <Item Key="client_id">000000-0000-0000-0000-000000000000</Item>
            <Item Key="response_types">id_token</Item>
            <Item Key="scope">openid profile email</Item>
            <Item Key="response_mode">form_post</Item>
            <Item Key="HttpBinding">POST</Item>
            <Item Key="UsePolicyInRedirectUri">false</Item>
            <Item Key="DiscoverMetadataByTokenIssuer">true</Item>

            <Item Key="ValidTokenIssuerPrefixes">https://login.microsoftonline.com/</Item>
          </Metadata>
          <CryptographicKeys>
            <Key Id="client_secret" StorageReferenceId="<keycontainer>" />
          </CryptographicKeys>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="oid" />
            <OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="given_name" />
            <OutputClaim ClaimTypeReferenceId="surName" PartnerClaimType="family_name" />
            <OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" PartnerClaimType="email" />
            <OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource"
              DefaultValue="socialIdpAuthentication" AlwaysUseDefaultValue="true" />
            <OutputClaim ClaimTypeReferenceId="identityProvider" PartnerClaimType="iss" />
            <OutputClaim ClaimTypeReferenceId="email" />

          </OutputClaims>
          <OutputClaimsTransformations>
            <OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName" />
            <OutputClaimsTransformation ReferenceId="CreateUserPrincipalName" />
            <OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId" />
            <OutputClaimsTransformation ReferenceId="CreateSubjectClaimFromAlternativeSecurityId" />
          </OutputClaimsTransformations>
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
          </ValidationTechnicalProfiles>
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-SocialLogin" />
          <!-- <UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" /> -->
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>

  </ClaimsProviders>

  <UserJourneys>
    <UserJourney Id="CustomSignUpOrSignIn">
      <OrchestrationSteps>

        <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp"
          ContentDefinitionReferenceId="api.signuporsignin">
          <ClaimsProviderSelections>
            <ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />

            <ClaimsProviderSelection TargetClaimsExchangeId="MicrosoftAccountCustomExchange" />
          </ClaimsProviderSelections>
          <ClaimsExchanges>
            <ClaimsExchange Id="LocalAccountSigninEmailExchange"
              TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
          </ClaimsExchanges>
        </OrchestrationStep>

        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>objectId</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="MicrosoftAccountCustomExchange"
              TechnicalProfileReferenceId="AADCommon-OpenIdConnect" />
          </ClaimsExchanges>
        </OrchestrationStep>

        <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>

        <OrchestrationStep Order="4" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>objectId</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AADCommon-OpenIdConnect"
              TechnicalProfileReferenceId="AADCommon-OpenIdConnect" />
          </ClaimsExchanges>
        </OrchestrationStep>

        <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>
       
        <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>
  </UserJourneys>

</TrustFrameworkPolicy>

SignUpOrSignin.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<TrustFrameworkPolicy>

  <BasePolicy>
    <TenantId><tenantId>.onmicrosoft.com</TenantId>
    <PolicyId><frameworkExtensions></PolicyId>
  </BasePolicy>

  <RelyingParty>
    <DefaultUserJourney ReferenceId="CustomSignUpOrSignIn" />
    <Endpoints>
      <Endpoint Id="Token" UserJourneyReferenceId="RedeemRefreshToken" />
    </Endpoints>
    <TechnicalProfile Id="PolicyProfile">
      <DisplayName>PolicyProfile</DisplayName>
      <Protocol Name="OpenIdConnect" />
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="displayName" />
        <OutputClaim ClaimTypeReferenceId="givenName" />
        <OutputClaim ClaimTypeReferenceId="surname" />
        <OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" PartnerClaimType="email"/>
        <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
        <OutputClaim ClaimTypeReferenceId="identityProvider" />
        <OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
      </OutputClaims>
      <SubjectNamingInfo ClaimType="sub" />
    </TechnicalProfile>
  </RelyingParty>
</TrustFrameworkPolicy>

I've been testing this through the Azure B2C Portal Identity Experience Framework page where I can run my policy, and I have the redirect_uri pointing to https://jwt.ms to see if I get a successful token and avoid my codebase altogether.

Even if the user is not invited, it will still create a user in the tenant with the randomized UPN <OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName" /> regardless if the user as been invited to the tenant or not which is done through the Users tab in the portal aswell.

Local accounts work as intended and I have removed the signup button for them.


EDIT, MORE INFORMATION:

We have an app that we need to allow external AAD tenants to log into. Currently we create an account for them in our Azure B2C tenant with their own password for them to login to the application.

A complaint we've received is our security protocol (MFA, password complexity) is not sufficient to their needs. We are trying to change the login flow to allow clients to add users to B2C easier. We were trying to invite clients as an external user through the Azure B2C portal so when they login it will follow their organizations security policy.

My goal is to invite the user via the Azure portal and for them to login via the OpenID button I implemented in the custom user flow. From my understanding, this is the way to do it. I followed the starter pack and the following Microsoft Learn guide on how to set this up. Set up sign-in for multitenant Microsoft Entra ID using custom policies - Azure AD B2C | Microsoft Learn

My reason for posting the question was that I could not figure out how to prevent an uninvited user from using the external login button to login and instead deny them entry. I am starting to believe that this isn't possible with my current setup. I also need each login to include and email in the token and not just on the "sign up" portion as it is utilized when the token is decoded inside our app.


Solution

  • B2C does not have a concept of a guest account, and you cannot invite them (in terms of sending an email invite). The invite in the portal is only for creating another admin.

    What you are doing is federation, where a user clicks a button on the login screen and logs in to another Entra ID tenant.

    This creates a "shadow account" in B2C i.e. an account referenced by alternativeSecurityId rather than objectId.

    I'm unsure why you call "AADCommon-OpenIdConnect" twice during the user journey. Look at the template for social and local accounts in the starter pack. Just replace all Facebook references with your equivalent Entra ID ones.

    If you click on the Identities link in the user record, you see e.g.:

    "identities": [
            {
              "signInType": "federated",
              "issuer": "https://sts.windows.net/123456/",
              "issuerAssignedId": "2ce...588"
            },
            {
              "signInType": "userPrincipalName",
              "issuer": "tenant.onmicrosoft.com",
              "issuerAssignedId": "[email protected]"
            }
          ],
    

    So you have a federated identity and a created UPN identity.

    Hope that helps.

    That said, could you please be more specific about what you are trying to achieve?

    Update

    Again, you cannot "invite" users to B2C. What you are doing is federating. You've added a button to the B2C screen. Anyone with an Entra ID login can click the button and federate.

    That's how federation works. It is by tenant, not by user.

    If you want to restrict this, create a table with valid users, containing e.g., tenant ID, first name, and last name. After federation, compare against the table via an API call. If not there, use a "Paragraph" to display an error message.

    If you want an email claim in the JWT, ensure it's an output claim in the RP.