"austin" ++ "writes" ++ "code"

Authentication with Auth0 in a Reason React App

March 11, 2018

This post is mostly an adaptation of the Auth0 React Quick Start using ReasonML. If you have not used Auth0 before, their documentation will be a good place to start before adapting to using it in Reason. This also assumes that you are familiar with using Reason to write react apps. If you need to get started with that, checkout the ReasonReact Documentation.

The first place to start here is getting the Auth0 library set up for use in your ReasonML project. This was one of the first bindings I wrote to a JavaScript library and it was a good place to start since it’s a pretty simple one to setup. The first thing we want to do is look at the usage from the Auth0 docs to see what we need to make available in Reason.

Getting Auth0 in Our Reason App

First we need to install auth0 with npm, same as their documentation:

# installation with npm
npm install --save auth0-js

# installation with yarn
yarn add auth0-js

Now we need to include their javascript into our bundle which can be accomplished in reason like so:

[%bs.raw {|require('../node_modules/auth0-js/build/auth0.js')|}];

Binding auth0-js to ReasonML

Now we can start on binding. This is what we have in their documentation for setting up an instance of Auth0 in your JavaScript app:

import auth0 from 'auth0-js';

export default class Auth {
  auth0 = new auth0.WebAuth({
    domain: 'YOUR_AUTH0_DOMAIN',
    clientID: 'YOUR_CLIENT_ID',
    redirectUri: 'http://localhost:3000/callback',
    audience: 'https://YOUR_AUTH0_DOMAIN/userinfo',
    responseType: 'token id_token',
    scope: 'openid'
  });

  login() {
    this.auth0.authorize();
  }
}

To accomplish this in reason we need to import the auth0-js dependency with Reason and make a reason function to invoke the auth0.WebAuth constructor.

In Reason in will look like this:

// auth0.re
type generatedAuth0Client = {.
  "authorize": [@bs.meth] (unit => unit)
};

type clientOptions = {
  .
  "domain": string,
  "clientID": string,
  "redirectUri": string,
  "responseType": string,
  "scope": string
};

[@bs.module "auth0-js"] [@bs.new]  external createClient : (clientOptions => generatedAuth0Client) = "WebAuth";

Here we have defined two types. The first is the result of our constructor and is a Js.t with an authorize method. The [@bs.meth] annotation tells the bucklescript compiler that this proprty is a method on a javascript object. Our clientOptions type is just a simple Js.t with the properties we need to pass to our constructor. The external is where we are actually importing our auth0-js library. @bs.module tells bucklescript where to find it in node_modules and @bs.new tells bucklescript that it needs to use the JavaScript new keyword to use the function we are importing. The "WebAuth" at the end of this statement tells bucklescript that we want the WebAuth property from the thing we are importing.

That is all we need to do to get reason to interop with the JavaScript auth0-js library. No we can get started using it within our Reason app.

Logging in from ReasonML

Now somewhere in your reason app you can create an instance of the auth0 client like so:

// app.re
let authOptions = {
  "domain": "yourdomain.com",
  "clientID": "yourClientID",
  "redirectUri": "http://localhost:3000/callback",
  "responseType": "token id_token",
  "scope": "openid"
};

let authClient = Auth0.createClient(authOptions);

And now we can create a function to open our login from a reason component.

// app.re
  render: (self) => {
    let onLogin = (_event => authClient##authorize());
    <NavBar
        openLogin=(onLogin)
    />
  }

I am using the default Auth0 redirect for login here, so if you wanted to send the login request from your own component, you will need to import some different things from auth0-js. When using the redirect, after login you will be sent back to your app at the location specified in your Auth0 settings. I am using http://localhost:3000/callback for my testing. After login Auth0 will send you back to our app at the callback location specified and will append the access_token, id_token, and expires_at as query params that we will need to parse to save our new tokens.

Saving our Login Tokens

The first thing we need to do is handle routing for our callback location so we can get our tokens. I am using ReasonReact’s new built in router so my example will be using that. If you are not using the new router and would like some more info about it, check out this post by Kyle Henderson.

// app.re
let mapUrlToRoute = (url: ReasonReact.Router.url) =>
  switch url.path {
  | [] => Routes.HomeRoute
  | ["callback"] => {
    Auth0.handleAuth(url)
    Routes.HomeRoute
  }
  | _ => Routes.NotFound
  };

This is the function I am using to select the correct route and when my route matches my callback it passes my params to a function that will put them in localstorage. After saving the tokens, I am returning my home route, but this is something you will want to change so you can get your user back to where they were before logging in.

Here is what my handleAuth function looks like:

let matchAccessToken = [%re "/access_token=([^\$&]+)/g"];
let matchExpiresIn = [%re "/expires_in=([^\$&]+)/g"];
let matchIdToken = [%re "/id_token=([^\$&]+)/g"];

let resolveOption = (opt) => switch opt {
| None => ""
| Some(s) => s
};

let resolveRegex = (exp, str) => {
  let res = exp |> Js.Re.exec(str);
  switch res {
  | None => ""
  | Some(result) => {
      let captures = result |> Js.Re.captures;
      switch captures {
      | [|_, token|] => token |> Js.Nullable.to_opt |> resolveOption 
      | _ => ""
      };
    }
  };
};

let handleAuth = (url: ReasonReact.Router.url) => {
  let accessToken = url.hash |> resolveRegex(matchAccessToken);
  let idToken = url.hash |> resolveRegex(matchIdToken);
  let expiresIn = url.hash |> resolveRegex(matchExpiresIn);
  localStorage |> setItem("access_token", accessToken);
  localStorage |> setItem("id_token", idToken);
  localStorage |> setItem("expires_at", expiresIn);
  Routes.HomeRoute;
};

let getIdToken = () => localStorage |> getItem("id_token") |> resolveOption;

Here I created three regular expressions for finding each token from my url path. These are all using the [%re] bucklescript annotation. These create native JavaScript regular expressions that are executed with Js.Re.exec which returns an optional that has to be pattern matched for None or Some. The last thing I have here is a function that gets the id_token from localstorage when I need to use it. The application where I am using this is using GraphQL through the reason-apollo bindings, so I just have it attached to my graphql context on every request.

Closing

That is pretty much all there is to getting Auth0 up and running in a ReasonReact application. If you have any questions or feeback let me know.


Austin Willis

Written by Austin Willis who lives in Saint Louis and builds things on the internet. You can follow Austin on Twitter