Intro
We’re employing 2 interns this year at Spikes. They are developing an application for internal use and are learning new technologies to accomplish that goal. One of those is Horizon .
Horizon offers multiple OAuth providers out of the box, such as Auth0, Github, Facebook, Google, … We would like to use Azure AD in this project and as the interns already have enough on their plate, we decided to implement this part for them.
Creating the provider
Having a close look at the already implemented providers, it wasn’t that hard to figure out the mechanics used for the others. Most of them had a similar implementation, except for the Twitter one.
I focused on the standard implementations which all implement 3 specific calls:
- Acquire an authorization code
- Use the acquired authorization code to request an access token
- Acquire user information based on the access token
Microsoft has a pretty good documentation for part 1 and 2, which you can find here.
Part 3 however was another story. Apparently, the userinfo endpoint on Azure requires a non standard access token. This token can be acquired by requesting an authorization code and access token without mentioning a resource. (source)
Looking more closely at the information that Horizon needs from the user, we decided to just decode the acquired access token.
The code
Acquiring the the authorization code (source):
oauth_options.make_acquire_url = (state, redirect_uri) =>url.format({protocol: 'https',host: 'login.microsoftonline.com',pathname: `/${options.tenant}/oauth2/authorize`,query: { client_id, redirect_uri, state, response_type: 'code', response_mode: 'query' },body: { response_type: 'code' }});
Acquiring the access token (source):
oauth_options.make_token_request = (code, redirect_uri) => {const body_params = querystring.stringify({grant_type: 'authorization_code', client_id, client_secret, redirect_uri, code,resource: options.resource});const path = `/${options.tenant}/oauth2/token`;const req = https.request({method: 'POST',port: 443,host: 'login.microsoftonline.com',path: path,headers: {'content-type': 'application/x-www-form-urlencoded','Content-Length': body_params.length}}); req.write(body_params);return req;};
Decoding the access token (source):
if (provider === 'azuread') {const user_info = jwt(access_token);const user_id = user_info.oid;horizon._auth.generate(provider, user_id).nodeify((err3, jwt) => {// Clear the nonce just so we aren't polluting clients' cookiesclear_nonce(res, horizon._name);do_redirect(res, err3 ? make_failure_url('invalid user') : make_success_url(jwt.token));});}
Full code can be found here.
Usage
Using the provider in a nodejs server application:
const horizon = require('@horizon/server');const http = require('http'); const server = http.createServer();server.listen(80); const options = {project_name: '<project-name>',rdb_host: '<rdb-host>.cloudapp.azure.com',rdb_port: '<rdb-port>',auto_create_collection: true,auto_create_index: true,permissions: false,access_control_allow_origin: '*',auth: {token_secret: '<TOKEN_SECRET>',allow_anonymous: false,allow_unauthenticated: false,success_redirect: '<REDIRECT-URL>'}};const horizonServer = horizon(server, options); horizonServer.add_auth_provider(horizon.auth.azuread,{id: '<azuread-appid>',secret: '<azuread-secret>',path: 'azuread',tenant: '<azuread-tenante>',resource: '<azuread-resourceid[GUID]>'});
Feel free to comment down below!