oauth-2.0azure-ad-b2c

Pass custom claim from access token endpoint in Azure B2C custom policy


I am creating a custom policy in azureb2c with atlassian as identity provider (oauth2.0). I followed this article. In my endpoint (in the article it's an azure function) where I exchange the authorization code for an access token with atlassian, I also use this token to query user groups from JIRA. If the user is in a certain group, I return that from my endpoint in the top level. So it's just a boolean "user_is_in_group": true.

I want to have this claim in the final access token I retrieve from azureb2c, so when exchanging the auth code I get back.

What I have done is:

Added a custom claim type in the TRUSTFRAMEWORKEXTENSIONS.xml:

<ClaimType Id="user_is_in_group">
   <DisplayName>Is user in group</DisplayName>
   <DataType>boolean</DataType>
   <DefaultPartnerClaimTypes>
      <Protocol Name="OAuth2" PartnerClaimType="user_is_in_group" />
   </DefaultPartnerClaimTypes>
   <UserHelpText>Indicates if user is in my group</UserHelpText>
</ClaimType>

In the TechnicalProfile for the atlassian oauth2 flow added the following item in metadata:

<Item Key="ExtraParamsInAccessTokenEndpointResponse">user_is_in_group</Item>

and make it available in the claims bag by adding this to output claims:

<OutputClaim ClaimTypeReferenceId="user_is_in_group" />

In the relying party, I added it as output claim as well in the technical profile for "PolicyProfile":

<OutputClaim ClaimTypeReferenceId="user_is_in_group" />

But it is not attached to the access token.

Full claims provider for atlassian:

<ClaimsProvider>
      <Domain>atlassian.com</Domain>
      <DisplayName>Atlassian</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="Atlassian-OAuth2">
          <DisplayName>Atlassian</DisplayName>
          <Protocol Name="OAuth2" />
          <Metadata>
            <Item Key="authorization_endpoint">https://auth.atlassian.com/authorize?audience=api.atlassian.com&amp;scope=read:me%20read:jira-user%20read:group:jira</Item>
            <Item Key="client_id">myClientId</Item>
            <Item Key="HttpBinding">POST</Item>
            <Item Key="AccessTokenEndpoint">myEndpoint</Item>
            <Item Key="ClaimsEndpoint">https://api.atlassian.com/me</Item>
            <Item Key="BearerTokenTransmissionMethod">AuthorizationHeader</Item>
            <Item Key="UsePolicyInRedirectUri">false</Item>
            <Item Key="ExtraParamsInAccessTokenEndpointResponse">user_is_in_group</Item>
          </Metadata>
          <CryptographicKeys>
            <Key Id="client_secret" StorageReferenceId="B2C_1A_AtlassianClientSecret" />
          </CryptographicKeys>
          <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="socialIdpAuthentication" AlwaysUseDefaultValue="true" />
        <OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="atlassian.com" AlwaysUseDefaultValue="true" />
        <OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="account_id" />
        <OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
        <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="email" />
        <OutputClaim ClaimTypeReferenceId="user_is_in_group" />
          </OutputClaims>
          <OutputClaimsTransformations>
        <OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName" />
        <OutputClaimsTransformation ReferenceId="CreateUserPrincipalName" />
        <OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId" />
          </OutputClaimsTransformations>
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-SocialLogin" />
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>

Full user journey:

<UserJourney Id="AtlassianSignUpOrSignIn">
      <OrchestrationSteps>

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

        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="AtlassianExchange" TechnicalProfileReferenceId="Atlassian-OAuth2" />
          </ClaimsExchanges>
        </OrchestrationStep>

        <!-- For social IDP authentication, attempt to find the user account in the directory. -->
        <OrchestrationStep Order="3" Type="ClaimsExchange">
          <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).  -->
        <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>

        <!-- 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="5" 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="6" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />

      </OrchestrationSteps>
      <ClientDefinition ReferenceId="DefaultWeb" />
    </UserJourney>

Full relying party:

<RelyingParty>
    <DefaultUserJourney ReferenceId="AtlassianSignUpOrSignIn" />
    <TechnicalProfile Id="PolicyProfile">
      <DisplayName>PolicyProfile</DisplayName>
      <Protocol Name="OpenIdConnect" />
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="displayName" />
        <OutputClaim ClaimTypeReferenceId="givenName" />
        <OutputClaim ClaimTypeReferenceId="surname" />
        <OutputClaim ClaimTypeReferenceId="email" />
        <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
        <OutputClaim ClaimTypeReferenceId="identityProvider" />
        <OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
        <OutputClaim ClaimTypeReferenceId="user_is_in_group" />
      </OutputClaims>
      <SubjectNamingInfo ClaimType="sub" />
    </TechnicalProfile>
  </RelyingParty>

Solution

  • After adding Application Insights I saw that the user_is_in_group claim was nested, the claims bag had the following entry:

    "Atlassian-OAuth2": {
              "ContentType": "Json",
              "Created": "2025-06-02T11:54:45.5364908Z",
              "Key": "Atlassian-OAuth2",
              "Persistent": true,
              "Value": "{\"account_id\":\"accountId\",\"email\":\"email\",\"name\":\"name\",\"picture\":\"picture\",\"account_status\":\"active\",\"characteristics\":{\"not_mentionable\":false},\"last_updated\":\"2025-04-02T12:45:55.13Z\",\"nickname\":\"nickname\",\"zoneinfo\":\"zoneInfo\",\"locale\":\"de\",\"extended_profile\":{\"job_title\":\"Account-Manager\",\"phone_numbers\":[],\"team_type\":\"team type\"},\"account_type\":\"atlassian\",\"email_verified\":true,\"{token_exchange:access_token}\":\"access_token\",\"{token_exchange:user_is_in_group }\":true,\"{oauth2:access_token}\":\"accessToken\"};2;Atlassian-OAuth2;False"
            },
    

    I replaced most properties with dummy values but as you can see, the user_is_in_group was prefixed with token_exchange. So I simply exchanged the output claim of the technical profile to

    <OutputClaim ClaimTypeReferenceId="is_active_support_worker" PartnerClaimType="{token_exchange:user_is_in_group}" />
    

    and it worked.