JWT authentication for re-frame using Auth0
As a new clojurescript user looking to get started with a personal project, authentication was a problem I encountered quickly. Finding out how to use the Auth0 service with my application proved surprisingly time consuming, so here is a simple example that will enable you to hit the ground running. It is tailored for the re-frame framework, but you can still salvage some code if you want to use something else.
Jump straight to the code
Requirements
Before we start, I’ll assume that you are somewhat familiar with:
- ClojureScript or Clojure
- Leiningen
- re-frame
- The basics of JWT authentication
Setting up the clojurescript project
Let’s create a new re-frame application, we’ll use the vanilla template, but you can add other profiles if you want.
lein new re-frame reframe-auth0
reframe-auth0/
├── project.clj
├── README.md
├── resources
│ └── public
│ └── index.html
└── src
├── clj
│ └── reframe_auth0
│ └── core.clj
└── cljs
└── reframe_auth0
├── config.cljs
├── core.cljs
├── db.cljs
├── events.cljs
├── subs.cljs
└── views.cljs
Open the project.clj
file, and add the latest cljsjs jar of the auth0-lock library :
Configuring the auth0 account
If you don’t already have an account on Auth0, create one.
You’ll then need to add a new client for the application we’re creating.
Take note of the domain
and client id
properties, we’ll need them later.
In Allowed Callback URLs
and Allowed Origins
, enter http://localhost:3449/
, and save the changes.
Setting up the login screen
Go to the config.cljs
file, and add a definition for the auth0 credentials.
Add the auth0 client id and domain
(def auth0
{:client-id "abcd1234"
:domain "xyz.auth0.com"})
In the directory with all the .cljs
files, we’ll create an auth0.cljs
file.
Make sure it starts with the following namespace declaration.
(ns reframe-auth0.auth0
(:require [re-frame.core :as re-frame]
[reframe-auth0.config :as config]
[cljsjs.auth0-lock]))
Declare an Auth0 lock configured with the relevant properties.
(def lock
"The auth0 lock instance used to login and make requests to Auth0"
(let [client-id (:client-id config/auth0)
domain (:domain config/auth0)
options (clj->js {})]
(js/Auth0Lock. client-id domain options)))
Here, we’ll use the default options (empty map). The configuration options are described here.
Let’s add a simple authentication callback for now
(defn on-authenticated
"Function called by auth0 lock on authentication"
[auth-result-js]
(js/alert (str "Auth0 authentication result: "
(js->clj auth-result-js))))
(.on lock "authenticated" on-authenticated)
Login button
Let’s update the views.cljs
file to add a simple login button.
(ns reframe-auth0.views
(:require [re-frame.core :as re-frame]
[reframe-auth0.auth0 :as auth0]))
(defn button [text on-click]
[:button
{:type "button"
:on-click on-click}
text])
(def login-button
(button "Log in" #(.show auth0/lock)))
(defn main-panel []
(let [name (re-frame/subscribe [:name])]
(fn []
[:div
[:div "Hello from " @name]
login-button]
)))
First test
Run your application with lein figwheel
, and go to http://localhost:3449/.
When you login, the result returned by auth0 should appear in a browser alert box.
Retrieving the user profile details
When authenticating, the auth0 lock gives you an authResult
containing the properties:
accessToken
, idToken
, idTokenPayload
, state
, refreshToken
. (see doc)
The idToken
is sufficient to secure your API calls, but by default it does not contain
informations like the user name or email.
You could create a token with additional informations in it, but in general you want to keep it
small since it will be sent with every API request.
However, you can retrieve the user profile from Auth0 using the accessToken
.
Let’s edit the code. We’ll convert the authResult
to a clojure map and extract the accessToken
,
the we’ll make a call to auth0 using the getUserInfo
function provided by the lock.
(defn handle-profile-response [error profile] *
"Handle the response for Auth0 profile request"
(js/alert (str "Auth0 user profile: "
(js->clj profile))))
(defn on-authenticated
"Function called by auth0 lock on authentication"
[auth-result-js]
(js/alert (str "Auth0 authentication result: "
(js->clj auth-result-js)))
(let [auth-result-clj (js->clj auth-result-js :keywordize-keys true)
access-token (:accessToken auth-result-clj)]
(.getUserInfo lock access-token handle-profile-response)))
If you try it now, you will get one alert box with the authResult
, and then another one
with the content of the user profile.
Storing the access token and user details.
In typical re-frame fashion, we’ll store the information in the central storage atom. Let’s store everything we got this far in the following data structure :
{
:user {
:auth-result xxxx
:profile xxxx
}
}
Let’s add the required events and subscriptions.
It is best practice to put events and subscription in dedicated files, but for something this simple
I’m tempted to put them in the auth0.cljs
file, to have everything available at a glance.
This will require us to make sure that the events are registered before loading other parts of the application, so let’s
require the auth0 namespace in core.cljs
first:
(ns reframe-auth0.core
(:require [reagent.core :as reagent]
[re-frame.core :as re-frame]
[reframe-auth0.events]
[reframe-auth0.subs]
[reframe-auth0.auth0]
[reframe-auth0.views :as views]
[reframe-auth0.config :as config]))
Then add to the auth0.cljs
file:
;;; events
(re-frame/reg-event-db
::set-auth-result
(fn [db [_ auth-result]]
(assoc-in db [:user :auth-result] auth-result)))
(re-frame/reg-event-db
::set-user-profile
(fn [db [_ profile]]
(assoc-in db [:user :profile] profile)))
We can then edit the handler:
(defn handle-profile-response [error profile] *
"Handle the response for Auth0 profile request"
(let [profile-clj (js->clj profile :keywordize-keys true)]
(re-frame/dispatch [::set-user-profile profile-clj])))
(defn on-authenticated
"Function called by auth0 lock on authentication"
[auth-result-js]
(let [auth-result-clj (js->clj auth-result-js :keywordize-keys true)
access-token (:accessToken auth-result-clj)]
(re-frame/dispatch [::set-auth-result auth-result-clj])
(.getUserInfo lock access-token handle-profile-response)))
Personalizing the view
Let’s personalize the default re-frame page by changing it to
Hello 'username' from re-frame
We’ll also add a logout button that removes all the user data we stored.
We’ll first create a subscription to get the user name in auth0.cljs
;;; subscriptions
(re-frame/reg-sub
::user-name
(fn [db]
(get-in db [:user :profile :name])))
Also we’ll register a logout event
(re-frame/reg-event-db
::logout
(fn [db [_ profile]]
(dissoc db :user)))
Then we’ll update the views.cljs
.
(def logout-button
(button "Log out" #(re-frame/dispatch [::auth0/logout])))
(defn main-panel []
(let [name (re-frame/subscribe [:name])
user-name (re-frame/subscribe [::auth0/user-name])]
(fn []
(if @user-name
[:div
[:div "Hello " @user-name " from " @name]
logout-button]
[:div
[:div "Hello from " @name]
login-button]))))
You can now login, logout, and see the user name on the main page.
Full project on github
What’s Next
- Create a simple backend and make a secure API call using the JWT.
- Persist the information to local storage.
- Tokens expire. Implement validity checks and a renewal mechanism.
- Let’s see if we can create a reframe template