Integrating Azure B2C OAuth2 (Authorization Code Flow) with an Electron App

Integrating Azure B2C OAuth2 (Authorization Code Flow) with an Electron App

and using JWT tokens to access secured data from another API

Introduction

Recently after investigating a few cloud based identity providers (AWS Cognito, Okta, Auth0) as potential replacements for an aging rails applications authentication system. We eventually decided to give Azure B2C a try since we were already familiar with the Azure cloud infrastructure. After some (weeks?) learning... we were able to get a full production load of users migrated over using the seamless migration strategy provided by Azure on Github (with help of the incredible omniauth gem on the rails side).

Now with the new found power of our decentralized authentication... we decided to experiment with what integrating an isolate application completely unrelated to the rails application might look like (browser extensions? desktop executable?).

Understanding Authorization Code Flows for B2C?

Authentication and Authorization systems are complex and can be difficult to understand (for me at least). There are hundreds of great articles out there explaining the complexities of OAuth2 with excellent diagrams illustrating the communication between clients and endpoints, but these can be a bit confusing without any existing context to draw from. After researching various providers and combing through the B2C documentation, below is the most simple breakdown I can discern about an authorization code flow process using a B2C custom policy.

Customer Journey

  • A customer arrives at an app and wants to access some secured content

  • They click a login button and are redirected to a hosted B2C page where they can enter their credentials

    Before redirecting we generate a random string to be used a code challenge/verifier, and send along an encrypted hash of this string with the user in the query string with some other standard parameters.

  • If the customer successfully authenticates on the hosted B2C page they are issued an Authorization Code and returned back to the app on a special redirect uri we provide.

  • When the customer returns to the app we take that Authorization Code and send it back to B2C on a different endpoint to try and exchange it for a JWT Token

    When sending the authorization code we also send along the decrypted value of the original string we earlier provided with some other standard parameters.

  • if the token exchange is successful, we save the encrypted JWT token values which can then be used in calls against our own API to securely request information.

Important Endpoints for OAuth2

With any OAuth2 provider there are several different endpoints that are used to orchestrate the entire authentication and authorization process. Different identity providers will have their own security requirements and feature availability, but the concepts and processes are mostly the same.

OpenID Connect discovery endpoint / Metadata Document

https://<mytenant>.b2clogin.com/<mytenant.onmicrosoft.com>/v2.0/.well-known/openid-configuration?p=<mycustompolicy>

This endpoint provides all of the information that we need about the authentication service in JSON format including the other endpoints that we will need to interact with throughout the process

{
  "issuer": "...",

  "authorization_endpoint": "...",
  "token_endpoint": "...",
  "end_session_endpoint": "...",
  "jwks_uri": "...",

  "response_modes_supported": [ "..." ],
  "response_types_supported": [ "..." ],
  "scopes_supported": [ "..." ],
  "subject_types_supported": [ "..." ],
  "id_token_signing_alg_values_supported": [ "..." ],
  "token_endpoint_auth_methods_supported": [ "..." ],
  "claims_supported": [ "..." ]
}

JWKs Endpoint

https://<mytenant>.b2clogin.com/<mytenant.onmicrosoft.com>/discovery/v2.0/keys?p=<mycustompolicy>

This endpoint provides us with a JSON Web Key Set that we will need to use to decrypt our JWT tokens. We can query this endpoint and parse the response to get the latest key values whenever we want.

{
  "keys": [
    {
      "kid":"...",
      "use":"...",
      "kty":"...",
      "e":"...",
      "n":"..."
    }
  ]
}

Authorization Endpoint

https://<mytenant>.b2clogin.com/<mytenant.onmicrosoft.com>/oauth2/v2.0/authorize?p=<mycustompolicy>

This is the endpoint where we will send our users to authenticate. When they hit the endpoint our user journey process will start by redirecting them to the hosted B2C page. After successful authentication they are returned to our app on the redirect url we supplied - with a code & state parameter sent by B2C in the query string

GET https://<my.app.com>/<b2c_callback_path>?code=...&state=...

Token Endpoint

https://<mytenant>.b2clogin.com/<mytenant.onmicrosoft.com>/oauth2/v2.0/token?p=<mycustompolicy>

After we receive back our initial authorization code response from B2C. We will use the token endpoint to exchange the code for a JWT token. If all goes well the token will be returned in JSON format that we can take and store for whatever use we want after.

{
    "not_before": "...",
    "token_type": "...",
    "access_token": "...",
    "scope": "...",
    "expires_in": "...",
    "refresh_token": "...",
}

End Session Endpoint

https://<mytenant>.b2clogin.com/<mytenant.onmicrosoft.com>/oauth2/v2.0/logout?p=<mycustompolicy>

This is the endpoint we can use to terminate a users session with B2C when they are finished in our app. Sending them here will clear out any session data/cookies for B2C in their browser. However - note this does not end their session in our app so we must also ensure we terminate this session on the app side before ending the B2C session.

Enter Electron

For this experiment we decided to use Electron.js to create a desktop executable that was isolate to our rails application. This allowed us to leverage a lot of existing code from our web app and get the prototype created quickly.

We decided not to do a ground up implementation of OAuth2... and instead rely on one of the existing NPM packages out there to get us started as a base. Eventually landing on electron-oauth-helper which is a generic OAuth1 & OAuth2 library that has simple options to customize and use with almost any provider. The documentation on Github is simple enough to follow, so will not get into the entire process rather just include the specific tweaks we had to make to get it working through B2C.

Code Challenge/Verifier

The Azure B2C authorization code flow requires a couple of special parameters passed at different stages of the authentication/authorization process. When we start the initial authorization request a code challenge (encrypted random base64 string) and a scope must be supplied. Then during the token request phase we need to send a code verifier which is essentially just the decrypted code challenge value.

To handle this we created an OAuth class with 2 basic functions

oauth.coffee

import crypto from 'crypto'

export default class OAuth  
  @code_verifier: ->
    return crypto.randomBytes(32).toString('base64')
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=/g, '')

  @code_challenge: (verifier) ->
    return crypto.createHash('sha256').update(verifier).digest()

Using our helper package we now just manually insert these values at the before-authorize-request and before-access-token-request steps.

main.coffee

  import OAuth2Provider from 'electron-oauth-helper/dist/oauth2'
  import OAuth from 'services/oauth' 
  import { session } from 'electron'

  config =
    authorize_url: 'https://<mytenant>.b2clogin.com/<mytenant.onmicrosoft.com>/oauth2/v2.0/authorize?p=<mycustompolicy>'
    access_token_url: 'https://<mytenant>.b2clogin.com/<mytenant.onmicrosoft.com>/oauth2/v2.0/token?p=<mycustompolicy>'
    response_type: "code"
    client_id: "<xxxx-xxxxx-xxxx-xxx>"
    redirect_uri: "https://my.app.com/b2c_callback_path"

  provider = new OAuth2Provider config
  verifier = OAuth.code_verifier()

  provider.on "before-authorize-request", (parameter) ->
    parameter["code_challenge"] = OAuth.code_challenge(verifier)
    parameter["code_challenge_method"] = "S256"
    parameter["scope"] = "openid"

  provider.on "before-access-token-request", (parameter, headers) ->
    parameter["code_verifier"] = verifier

  provider
    .perform(win)
    .then((resp) =>
      win.close()  
      access_token = { url: 'https://my.railsapi.com', name: 'access-token', value: resp.body }
      session.defaultSession.cookies.set(access_token)
      .then -> 
        event.reply 'login_response', resp
      .catch (error) -> console.error(error)
    )
    .catch((error) -> console.error error )

Now with our JWT token details stored in the local session cookies within the main electron process, we can simply send these cookies along with any requests we make against our own rails API going forward.

api.coffee

  net = require('electron').net

  get_url = "https://my.railsapi.com/api/my_secure_endpoint"
  headers = "Content-Type": "application/x-www-form-urlencoded"
  request = net.request({ url: get_url, headers, method: "GET", credentials: 'include' })
  request.on 'response', (response) ->
    body = ''
    response.on 'data', (chunk) ->
      body += chunk
    response.on 'end', ->
      resp =
        headers: response.headers
        message: response.statusMessage
        statusCode: response.statusCode
        body: body
      console.log 'We got a response!', resp
  request.end()

When this request hits our rails API we simply need to decode/parse the token value and confirm that it is valid before sending back a response. We do this using the JSON Web Key Set values we obtained from the JWKS endpoint, and bolt in whatever custom logic we want to validate the user/token issuer etc.

example_controller.rb

  require 'jwt'

  before_action :require_jwt

  def my_endpoint
    render json: { data: 'Hello from the rails API!' }
  end

  def require_jwt
    token = JSON.parse(cookies['access-token'])['id_token']
    if !token
      head :forbidden
    end
    if !valid_token(token)
      head :forbidden
    end
  end

  def valid_token(token)
    unless token
      return false
    end
    begin
      keys = [
        {
          "alg": "<alg value from JWKS Endpoint>",
          "e": "<e value from JWKS Endpoint>",
          "kid": "<kid value from JWKS Endpoint>",
          "kty": "<kty value from JWKS Endpoint>",
          "n": "<n value from JWKS Endpoint>",
          "use": "<use value from JWKS Endpoint>"
        }
      ]
      decoded = JWT.decode(token, nil, true, { algorithms: ['RS256'], jwks: { keys: keys } })
      return true
    rescue JWT::DecodeError
      render json: { errors: ['Not Authenticated'] }, status: :unauthorized
    end
  end

Congratulations you now have a desktop app which can authenticate users from your B2C directory! (and use the acquired JWT tokens to communicate with your rails API). There are several other steps you will still need to consider/implement for this to be a full system (periodic token refresh, issuer validations). But hopefully this article is enough to help you through some of the road blocks in your B2C implementation.