2
votes

I'm wondering if it is possible to propagate an oid claim or basically any other claim issued by Identity Server 3 to the AAD B2C and make it be a part of the token issued by Azure AD B2C? We need to have an original ID on the client side and all we can get from sub and oid claims is an ID of an AAD B2C user account. I would also like to do the same with roles claim. Any help would be much appreciated.

[EDITED]

Here is how TechnicalProfile of my Identity Server configuration look like in Custom Policy (TrustFrameworkExtensions.xml):

<TechnicalProfile Id="IdentityServerProfile">
    <DisplayName>IdentityServer</DisplayName>
    <Description>Login with your IdentityServer account</Description>
    <Protocol Name="OpenIdConnect"/>
    <OutputTokenFormat>JWT</OutputTokenFormat>
    <Metadata>
        <Item Key="METADATA">https://{identity_server_hostname}/identity/.well-known/openid-configuration</Item>
        <Item Key="ProviderName">https://{identity_server_hostname}/identity</Item>
        <Item Key="client_id">00000000-0000-0000-0000-000000000000</Item>
        <Item Key="IdTokenAudience">00000000-0000-0000-0000-000000000000</Item>
        <Item Key="response_types">code</Item>
        <Item Key="scope">openid profile customScope</Item>
        <Item Key="UsePolicyInRedirectUri">false</Item>
        <Item Key="AccessTokenResponseFormat">json</Item>
        <Item Key="HttpBinding">POST</Item>
    </Metadata>
    <CryptographicKeys>
        <Key Id="client_secret" StorageReferenceId="B2C_1A_IdentityServerAppSecret"/>
    </CryptographicKeys>
    <OutputClaims>      
        <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="IdentityServer" />
        <OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
        <OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="tid" />
        <OutputClaim ClaimTypeReferenceId="socialIdpUserId" PartnerClaimType="sub" />
    </OutputClaims>
    <OutputClaimsTransformations>
        <OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName"/>
        <OutputClaimsTransformation ReferenceId="CreateUserPrincipalName"/>
        <OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId"/>
    </OutputClaimsTransformations>
    <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop"/>
</TechnicalProfile>

And here is the original log from Application Instights from the log with given correlationId:

{  

"TenantId":"{tenant}.onmicrosoft.com", "PolicyId":"B2C_1A_signup_signin_capcom", "RedirectUri":"http://localhost:3000/", "AdditionalParameters":{
"client_info":"1", "x-client-SKU":"MSAL.JS", "x-client-Ver":"0.1.5", "client-request-id":"a0ef9d95-ba21-4796-ad65-bc9e2b8c5e89" }, "Nonce":"bf198ee6-e084-433d-885c-2bd8f0292b3d", "State":"7f732141-498e-4445-9efe-9b0d54178ef6", "ClientId":"012c0f85-4568-483f-9bca-054d6201ac00", "ResponseType":"id_token", "ResponseMode":"fragment", "ResponseRedirector":{
"URI":"http://localhost:3000/", "D":false, "WF":true, "R":false, "S":false }, "AppModelVersion":1, "ScopedProviders":[

] }

Something I've noticed and which I found strange is ResponseType value. In our TechnicalProfile is set to code and here its value is id_token.

[EDITED 2] Claim for socialIdpUserId finally passes through. There is also an answer below what the problem was. Now, I still have a problem with having firstName, lastName and displayName. I've followed the same pattern as I did for socialIdpUserId but the only thing I can see are the default values. Here are my configurations for them:

1) Claim types definition:

<ClaimType Id="displayName">
    <DisplayName>Display Name</DisplayName>
    <DataType>string</DataType>
    <DefaultPartnerClaimTypes>
      <Protocol Name="OAuth2" PartnerClaimType="unique_name" />
      <Protocol Name="OpenIdConnect" PartnerClaimType="displayName" />
      <Protocol Name="SAML2" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" />
    </DefaultPartnerClaimTypes>
    <UserHelpText>Your display name.</UserHelpText>
    <UserInputType>TextBox</UserInputType>
 </ClaimType>
...
<ClaimType Id="givenName">
    <DisplayName>Given Name</DisplayName>
    <DataType>string</DataType>
    <DefaultPartnerClaimTypes>
      <Protocol Name="OAuth2" PartnerClaimType="given_name" />
      <Protocol Name="OpenIdConnect" PartnerClaimType="given_name" />
      <Protocol Name="SAML2" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" />
    </DefaultPartnerClaimTypes>
    <UserHelpText>Your given name (also known as first name).</UserHelpText>
    <UserInputType>TextBox</UserInputType>
 </ClaimType>
<ClaimType Id="surname">
    <DisplayName>Surname</DisplayName>
    <DataType>string</DataType>
    <DefaultPartnerClaimTypes>
      <Protocol Name="OAuth2" PartnerClaimType="family_name" />
      <Protocol Name="OpenIdConnect" PartnerClaimType="family_name" />
      <Protocol Name="SAML2" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" />
    </DefaultPartnerClaimTypes>
    <UserHelpText>Your surname (also known as family name or last name).</UserHelpText>
    <UserInputType>TextBox</UserInputType>
 </ClaimType>

And output claims in the technical profile for my IdP:

<TechnicalProfiles>
    <TechnicalProfile Id="CapcomProfile">
...
   <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="displayName" />
    <OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="firstName" DefaultValue="No" />
    <OutputClaim ClaimTypeReferenceId="surname" PartnerClaimType="lastName" DefaultValue="Name" />
...

And finally, the RP output definition:

<OutputClaims>
  <OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="displayName" />
  <OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="givenName"  />
  <OutputClaim ClaimTypeReferenceId="surname" PartnerClaimType="surname" DefaultValue="Not found in IdP" />
...

Any suggestions?

2

2 Answers

1
votes

For this, you'll have to move from a built-in policy to a custom policy, which passes the incoming claims from the identity provider through the outgoing claims to the application.

For example, if you create a custom policy from the starter pack , then you can pass through the user identifier from Facebook to the application as follows:

1) Ensure the socialIdpUserId claim is declared:

<ClaimType Id="socialIdpUserId">
  <DisplayName>Username</DisplayName>
  <DataType>string</DataType>
</ClaimType>

2) Ensure the socialIdpUserId claim is added to the <OutputClaims /> collection for the Facebook technical profile:

<ClaimsProvider>
  <Domain>facebook.com</Domain>
  ...
  <TechnicalProfiles>
    <TechnicalProfile Id="Facebook-OAUTH">
      <DisplayName>Facebook</DisplayName>
      ...
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="socialIdpUserId" PartnerClaimType="id" />
        ...
      </OutputClaims>
      ...
    </TechnicalProfile>
  </TechnicalProfiles>
</ClaimsProvider>

3) Ensure the socialIdpUserId claim is added to the <OutputClaims /> collection for the relying party technical profile:

<RelyingParty>
  <DefaultUserJourney ReferenceId="SignUpOrSignIn" />
  <TechnicalProfile Id="PolicyProfile">
    <DisplayName>PolicyProfile</DisplayName>
    ...
    <OutputClaims>
      ...
      <OutputClaim ClaimTypeReferenceId="socialIdpUserId" PartnerClaimType="ext_id" />
    </OutputClaims>
    ...
  </TechnicalProfile>
</RelyingParty>

You can follow the same pattern for other external claims.

0
votes

In the end, I made this work and now most of my claims are passing through. Most likely, the problem was caused by lack of appropriate Client Secret from Azure AD application in my Key Policies, B2C_1A_CapcomIdentityServerAppSecret to be more specific. So, double check it if you have copied over the appropriate one from AAD Application keys to you Policy Keys.

Unfortunately, that was just part of the solution as it stll didn't work completely and I didn't recieve token back on the client. But the good thing was, after I had this keys configured well, I started receiving very useful exceptions in Application Insights (you can see here how to connect you Custom Policies with Application Insights: https://docs.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-troubleshoot-custom). I highly recommend anyone who has any problem with custom policies to set it up. Also, if you use IdentityServer3 like I do, try with temporary turning on http logging on its side. Here is the example:

LoggingOptions = new LoggingOptions
                {
                    EnableHttpLogging = true
                }

While I was analyzing traces in Application Insights, most of the exceptions happened because I missed some of the attributes, for example socialIdpUserId, which is later used in OutputClaimsTransformations. It would be better if this was checked immediately when uploading custom policies to the Azure together with receiving some detailed message about an error.

After I have this fixed, I finally started receiving userId of my IdP authenticated user on the client side. The only thing now that has left is data from profile scope such as displayName, givenName and surname but I hope I'll find the solution very soon for that as well. You can find problem description under [EDITED 2] as a part of the original question.

[UPDATE - SOLUTION]

Yesterday, after quiet a long time spent on trying so many different things, I finally realized why we were not getting all claims back on the client. They actually didn't exist in identity token but only in access token. AAD B2C uses the first one, the identity token, while doing mappings defined in custom policies and that was the whole point. In the end I had to make some small changes on IdentityServer3 side (take a look at the code below).

This is how the class which is responsible for issuing claims and generating both identity and access tokens now looks like:

 public class CustomClaimsProvider : DefaultClaimsProvider
{
    private readonly IIndex<string, IClaimsDefinition> claimDefinitions;

    public CustomClaimsProvider(
        IUserService users,
        IIndex<string, IClaimsDefinition> claimDefinitions)
        : base(users)
    {
        this.claimDefinitions = claimDefinitions;
    }

    public override async Task<IEnumerable<Claim>> GetIdentityTokenClaimsAsync(
       ClaimsPrincipal subject,
       Client client,
       IEnumerable<Scope> scopes,
       bool includeAllIdentityClaims,
       ValidatedRequest request)
    {
        var claims = await base.GetIdentityTokenClaimsAsync(subject, client, scopes, includeAllIdentityClaims, request).ConfigureAwait(false);
        return GetAdditionalClaims(scopes, claims);
    }

    public override async Task<IEnumerable<Claim>> GetAccessTokenClaimsAsync(
        ClaimsPrincipal subject,
        Client client,
        IEnumerable<Scope> scopes,
        ValidatedRequest request)
    {
        var claims = await base.GetAccessTokenClaimsAsync(subject, client, scopes, request).ConfigureAwait(false);
        return GetAdditionalClaims(scopes,  claims);
    }

    private IEnumerable<Claim> GetAdditionalClaims(IEnumerable<Scope> scopes, IEnumerable<Claim> claims)
    {
        var scopesList = scopes.ToList();
        var claimsList = claims.ToList();

        foreach (var scope in scopesList.Select(x => x.Name))
        {
            if (claimDefinitions.TryGetValue(scope, out IClaimsDefinition claimDef))
            {
                claimsList.AddRange(claimDef.GetClaims(claims));
            }
        }

        return claimsList;
    }
}

So, the main point is, you should also override GetIdentityTokenClaimsAsync method in the class derived from DefaultClaimsProvider if you want to have some additional claims as a part of your identity token.