Azure B2C, OAuth2 and a Github App

jordanfinners

Jordan Finneran

Posted on April 5, 2021

Azure B2C, OAuth2 and a Github App

Contents

  1. Intro
  2. Setup
  3. Config
  4. Summary

Intro

Azure B2C is an authentication provider for Business-to-Consumer auth. It has a massive free plan of 50,000 monthly active users at the time of writing.
It supports a number of identity providers out of the box including Github OAuth apps, but it doesn't mention how to support Github Apps which differ to to OAuth ones.

Fortunately we can configure Azure B2C using Custom Policies and one of the options is for OAuth2 protocols!

OAuth2 is a pretty common standard across popular APIs and companies now, including Github Apps.

Setup

To understand the flow, you can read Github's explaination of the auth process for Github Apps. Which gives you a high level explanation of what will happen.

Next we'll need to create an Azure B2C tenant unless you've already got an existing one you want to use.

Then you'll need to create a Github App, setting the Callback URL to https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/oauth2/authresp.
And then check the box with Request user authorization (OAuth) during installation.

Hop back to Azure B2C and set up the signing and encryption keys in your Azure B2C tenant, you can ignore the Facebook key.

Then you can generate a secret for your Github App and follow the instructions to add a policy key.

We'll then jump into the Custom Policy Config!

Config

To configure an Azure B2C Custom Policy we need at least three files a TrustFrameworkBase, TrustFrameworkExtensions and SignUpOrSignin. If you are doing more advanced options there will be more files than this, but for this example these are all we will need.

TrustFrameworkBase

Firstly you will want to download the TrustFrameworkBase.

Remove the single <ClaimsProvider> containing <Domain>facebook.com</Domain> and leave the rest.
Then remove all the <UserJourneys> from this file as we will be creating our own.

Finally change yourtenant in the file to be the name of your Azure B2C tenant.

TrustFrameworkExtensions

Next up is the TrustFrameworkExtensions, this is largely based off the Azure B2C Github OAuth example with some modifications to support Github Apps.

Change yourtenant in the file to be the name of your Azure B2C tenant.
Also change YOUR GITHUB APP CLIENT ID to be the Client ID of Github App.

View full file
<?xml version="1.0" encoding="utf-8" ?>
<TrustFrameworkPolicy 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
  xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06" 
  PolicySchemaVersion="0.3.0.0" 
  TenantId="yourtenant.onmicrosoft.com" 
  PolicyId="B2C_1A_TrustFrameworkExtensions" 
  PublicPolicyUri="http://yourtenant.onmicrosoft.com/B2C_1A_TrustFrameworkExtensions">

  <BasePolicy>
    <TenantId>yourtenant.onmicrosoft.com</TenantId>
    <PolicyId>B2C_1A_TrustFrameworkBase</PolicyId>
  </BasePolicy>
  <BuildingBlocks>
  <ClaimsSchema>
    <ClaimType Id="numericUserId">
      <DisplayName>Numeric user Identifier</DisplayName>
      <DataType>long</DataType>
    </ClaimType>
    <ClaimType Id="identityProviderAccessToken">
      <DisplayName>Identity Provider Access Token</DisplayName>
      <DataType>string</DataType>
      <AdminHelpText>Stores the access token of the identity provider.</AdminHelpText>
    </ClaimType>
  </ClaimsSchema>
  <ClaimsTransformations>
    <ClaimsTransformation Id="CreateIssuerUserId" TransformationMethod="ConvertNumberToStringClaim">
      <InputClaims>
        <InputClaim ClaimTypeReferenceId="numericUserId" TransformationClaimType="inputClaim" />
      </InputClaims>
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="issuerUserId" TransformationClaimType="outputClaim" />
      </OutputClaims>
    </ClaimsTransformation>
  </ClaimsTransformations>
  </BuildingBlocks>

  <ClaimsProviders>

<ClaimsProvider>
  <Domain>github.com</Domain>
  <DisplayName>GitHub</DisplayName>
  <TechnicalProfiles>
    <TechnicalProfile Id="GitHub-OAUTH2">
      <DisplayName>GitHub</DisplayName>
      <Protocol Name="OAuth2" />
      <Metadata>
        <Item Key="ProviderName">github.com</Item>
        <Item Key="authorization_endpoint">https://github.com/login/oauth/authorize</Item>
        <Item Key="AccessTokenEndpoint">https://github.com/login/oauth/access_token</Item>
        <Item Key="ClaimsEndpoint">https://api.github.com/user</Item>
        <Item Key="HttpBinding">GET</Item>
        <Item Key="BearerTokenTransmissionMethod">AuthorizationHeader</Item>
        <Item Key="UsePolicyInRedirectUri">0</Item>
        <Item Key="UserAgentForClaimsExchange">CPIM-Basic/{tenant}/{policy}</Item>
        <Item Key="client_id">YOUR GITHUB APP CLIENT ID</Item>
      </Metadata>
      <CryptographicKeys>
        <Key Id="client_secret" StorageReferenceId="B2C_1A_GitHubAppSecret"/>
      </CryptographicKeys>
      <InputClaims />
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
        <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="email" />
        <OutputClaim ClaimTypeReferenceId="numericUserId" PartnerClaimType="id" />
        <OutputClaim ClaimTypeReferenceId="issuerUserId" />
        <OutputClaim ClaimTypeReferenceId="identityProviderAccessToken" PartnerClaimType="{oauth2:access_token}" />
        <OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="github.com" AlwaysUseDefaultValue="true" />
        <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="socialIdpAuthentication" AlwaysUseDefaultValue="true" />
      </OutputClaims>
      <OutputClaimsTransformations>
        <OutputClaimsTransformation ReferenceId="CreateIssuerUserId" />
        <OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName"/>
        <OutputClaimsTransformation ReferenceId="CreateUserPrincipalName"/>
        <OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId"/>
        <OutputClaimsTransformation ReferenceId="CreateSubjectClaimFromAlternativeSecurityId"/>
      </OutputClaimsTransformations>
      <UseTechnicalProfileForSessionManagement ReferenceId="SM-SocialLogin" />
    </TechnicalProfile>
  </TechnicalProfiles>
</ClaimsProvider>


  </ClaimsProviders>

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

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

        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="GithubExchange" TechnicalProfileReferenceId="GitHub-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>
    </UserJourneys>

</TrustFrameworkPolicy>
Enter fullscreen mode Exit fullscreen mode

To explain what is happening, we've set the protocol to OAuth2 so that Azure knows how to interact with it.
We've then set up the three urls matching the steps in the authorization process as described by Githubs flow.
We then set the HttpBinding which tells us how to interact with the ClaimsEndpoint which will get the users details, this will require the authorization token that Azure B2C will have acquired, this needs to be passed as a header so we set BearerTokenTransmissionMethod as such.
Then there are two default settings, followed finally by your Github Apps client ID.

Next we tell Azure B2C where to get the secret that we added earlier in.
We then set the OutputClaims these are the values we want to retrieve from Azure B2C but also the third party, I'll call out a few key ones:

  • email - this is pretty self explanatory, you can see it uses the Partner Claim to bind to the value that comes back from the Github API call on the ClaimsEndpoint
  • identityProviderAccessToken - this will give us the Access Token for the Github App, so we can also make API calls ourselves
  • numericUserId - this is the ID of the user in github

These last two claims have to be added in the BuildingBlocks at the top as they aren't standard fields for Azure B2C, this is the same boiler plate from the Azure B2C tutorial.

We then set up a User Journey, this is a standard Social User Journey for Azure B2C where we've set values in steps 1 and 2 to values matching our needs.

SignUpOrSignin

Lastly is the SignUpOrSignin, this is largely based off the standard Azure B2C Socials Signup and Signin Policy example with some modifications to support Github Apps.

Change yourtenant in the file to be the name of your Azure B2C tenant.

View full file
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<TrustFrameworkPolicy
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06"
  PolicySchemaVersion="0.3.0.0"
  TenantId="yourtenant.onmicrosoft.com"
  PolicyId="B2C_1A_signup_signin"
  PublicPolicyUri="http://yourtenant.onmicrosoft.com/B2C_1A_signup_signin">

  <BasePolicy>
    <TenantId>yourtenant.onmicrosoft.com</TenantId>
    <PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>
  </BasePolicy>

  <RelyingParty>
    <DefaultUserJourney ReferenceId="GithubSignUpOrSignIn" />
    <TechnicalProfile Id="PolicyProfile">
      <DisplayName>PolicyProfile</DisplayName>
      <Protocol Name="OpenIdConnect" />
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="displayName" />
        <OutputClaim ClaimTypeReferenceId="email" />
        <OutputClaim ClaimTypeReferenceId="identityProviderAccessToken" />
        <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
        <OutputClaim ClaimTypeReferenceId="identityProvider" />
        <OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
      </OutputClaims>
      <SubjectNamingInfo ClaimType="sub" />
    </TechnicalProfile>
  </RelyingParty>
</TrustFrameworkPolicy>
Enter fullscreen mode Exit fullscreen mode

You can see that these OutputClaims match those we set in the TrustFrameworkExtensions. These OutputClaims will then be available in the Json Web Token (JWT), that Azure B2C provides the browser.

Upload and App Registration

Now we've setup all the files, we can upload them. Go to the Identity Experience Framework option in your Azure B2C tenant.
You need to upload them in the following order:

  1. TrustFrameworkBase
  2. TrustFrameworkExtensions
  3. SignUpOrSignin

Once those have all been uploaded browser to App Registrations and create your application, which will allow you to interact with this policy.
After you've created your app, go into Expose an API and set the Application ID URI I often just set it to api.
Then add a scope, again I often set it to 'access'.

Hop into API Permissions and add the scope you just created and Grant consent.

A more detailed explanation of these steps to register an app can be found in the docs.

Bonus: MSAL and Azure Functions

If you are using the Azure MSAL library on your frontend to authenticate with Azure B2C.

Your config for it should look something like:
Where the clientId is that of your Azure App Registration.

{
    "auth": {
        "clientId": "XXX",
        "authority": "https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/B2C_1A_signup_signin",
        "knownAuthorities": ["yourtenant.b2clogin.com"]
    }
}
Enter fullscreen mode Exit fullscreen mode

If you are also integrating with Azure Functions:

  • Get an Access Token from MSAL.js and set it on the Authorization header Authorization: Bearer accessToken
  • Add Microsoft as identity provider on your Azure Functions Authorization, set the Application (Client) ID to that of our Azure B2C Application, and the issuer url to the OpenID Connect discovery endpoint of our Custom Policy which will be in the format of https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1A_signup_signin

Then in your functions you can use the following code to decode the JWT token and get access to the values we set in the OutputClaims, which includes the identityProviderAccessToken which is the token we can use to call the Github API!

const header = req.headers['x-ms-client-principal'];
const encoded = Buffer.from(header, 'base64');
const decoded = encoded.toString('ascii');
const auth = JSON.parse(decoded);
return auth.claims.reduce((acc, claim) => {
    if (!claim.typ.startsWith('http')) {
        return {
        ...acc,
        [claim.typ]: claim.val
        }
    }
    const parts = claim.typ.split('/')
    const key = parts[parts.length - 1]
    return {
        ...acc,
        [key]: claim.val
    }
})
Enter fullscreen mode Exit fullscreen mode

Summary

In summary, we can configure Azure B2C to use a Github App not just Github OAuth apps allowing for more fine grained permissions.

Not only this but we can take this a step further and this could be reused for any OAuth flows, which makes Azure B2C really extensible for other applications that offer these login flows!

With the updates in Azure Functions and MSAL.js means we can now really easily access the information in the Token and get access to the claims we've setup. This makes it really easy to securely interact with third party APIs having authorized it.

It's a bit of a long winded setup process, however the possibilities this unlocks is really powerful!

If you've like to really understand each of the things we configured in the various policies, the Azure B2C Custom Policy Reference guide explains them in real depth.

Happy Building!

💖 💪 🙅 🚩
jordanfinners
Jordan Finneran

Posted on April 5, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Azure B2C and MSAL
azure Azure B2C and MSAL

January 11, 2020