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