Gift list dev diary: authentication

All development for this project is shared on github at https://github.com/codonnell/mygiftlist-blog. I will endeavor to focus on the most relevant snippets of code rather than go over all of them on this blog. For interested parties, the github repo is available to survey all of the minor details.

In this post, we’ll create a skeleton project and implement authentication in the client using Auth0. Users will be able to register, log in, and log out. Because we’ll be using Auth0 hosted login, they’ll also be able to reset their password and verify their email address.

Before diving in, I’d like to thank Tony Kay and Wilker LĂșcio da Silva for all of the incredible work they’ve done on not only the core fulcro and pathom projects, but also on building the documentation and developer tooling that makes these projects so pleasant to use. I won’t try to rehash the information that is freely available as documentation on fulcro and pathom; I will assume the reader has some familiarity. My focus will be on how to build an application with these libraries rather than the mechanics of how the libraries work.

My first order of business when starting a new app is getting a “Hello World” project up and running. My personal preference these days when starting greenfield web apps is to use shadow-cljs to manage javascript dependencies and clojurescript compilation and deps.edn to manage clojure(script) dependencies and classpath building.

In order to simplify the initial development step, I like to use shadow-cljs’s development HTTP server rather than implement my own server in Clojure right away. The boilerplate code necessary to implement “Hello World” is available in this commit. The vast majority of this code is pulled from the fulcro template project and is relatively standard; I won’t go over it.

Authenticaton

Authentication is a subtle and high stakes component of any application. There is a constantly-evolving set of attacks, and the average developer cannot expect to stay abreast of all of them. I prefer to rely on libraries and infrastructure written by experts and keep the authentication-related code I write as thin as possible. Because the Auth0 free tier will suffice for this application and Auth0 offers an easy-to-use javascript client library, I will use Auth0 for authentication. My application’s authentication layer is thin and the client library follows the OAuth2 standard; it should not be too difficult to adapt this approach to another provider.

In order to use Auth0, we need to sign up for Auth0, create a new tenant, create an SPA application in that tenant, and create an API for that application. This is documented in their javascript getting started guide (with creating an API documented in part 2). Be sure to add http://localhost:8080 as a callback URL, logout URL, and allowable web origin in your application’s configuration.

The Auth0 client library uses the authorization code grant flow. Briefly, this means our application will redirect a user to Auth0 to log in. After logging in, Auth0 redirects the user back to our application with an authorization code in the query params. Our app then makes a POST request to Auth0 with the authorization code and gets an acccess token back. These steps are implemented by the client library, and we orchestrate them using methods on an Auth0Client javascript object.

The API

We’ll need to install the auth0 client library with npm install --save-dev @auth0/auth0-spa-js. In addition, we’ll use the wsscode-async library to ease wrapping Auth0’s promise-based API. This should be added to the dev alias in deps.edn as com.wsscode/async {:mvn/version "1.0.2"}. To see a full changeset for the rest of this post, see this commit.

With dependecies installed, we wrap the Auth0Client methods in an authentication namespace:

(ns rocks.mygiftlist.authentication
  (:require ["@auth0/auth0-spa-js" :as create-auth0-client]
            [com.wsscode.async.async-cljs :refer [go-promise <!p]]
            [rocks.mygiftlist.config :as config]))

(defonce auth0-client (atom nil))

(defn create-auth0-client! []
  (go-promise
    (reset! auth0-client
      (<!p (create-auth0-client
             #js {:domain config/AUTH0_DOMAIN
                  :client_id config/AUTH0_CLIENT_ID
                  :audience config/AUTH0_AUDIENCE
                  :connection config/AUTH0_CONNECTION})))))

(defn is-authenticated? []
  (go-promise (<!p (.isAuthenticated @auth0-client))))

(defn login []
  (.loginWithRedirect @auth0-client
    #js {:redirect_uri (.. js/window -location -origin)}))

(defn handle-redirect-callback []
  (go-promise (<!p (.handleRedirectCallback @auth0-client))))

(defn logout []
  (.logout @auth0-client
    #js {:returnTo (.. js/window -location -origin)}))

(defn get-access-token []
  (go-promise (<!p (.getTokenSilently @auth0-client))))

(defn get-user-info []
  (go-promise (<!p (.getUser @auth0-client))))

The configuration comes from a simple config namespace

(ns rocks.mygiftlist.config)

(goog-define AUTH0_DOMAIN "")
(goog-define AUTH0_CLIENT_ID "")
(goog-define AUTH0_AUDIENCE "")
(goog-define AUTH0_CONNECTION "")

and the corresponding closure-defines in the shadow-cljs config

:closure-defines 
{rocks.mygiftlist.config/AUTH0_CLIENT_ID "heIlMgUZmvjI3muqPO3Ua5F5VpLgTpM3"
 rocks.mygiftlist.config/AUTH0_DOMAIN "mygiftlistrocks-blog.auth0.com"
 rocks.mygiftlist.config/AUTH0_AUDIENCE "https://blog.mygiftlist.rocks"
 rocks.mygiftlist.config/AUTH0_CONNECTION "Username-Password-Authentication"}

This gives us an authentication API that is completely decoupled from our application state. Every authentication call that doesn’t redirect us away from the page is wrapped using <!p and go-promise to return a core.async promise-chan. I prefer to provide a clean API returning only channels, since that is conducive to writing idiomatic clojurescript.

I like to use closure defines to configure clojurescript applications, commiting them to source control. It is simple, allows parameterizing by build, and does not require any external dependencies.

With this API written, we now need to use it to allow a user to log in and log out.

Login/Logout

We’ll handling logging in and logging out by initializing our authentication state on page load. We will store the current user data if a user is logged in and use the presence or absence of that user data to determine whether to render a login or logout button.

I’m not great at styling, so we’ll bring in semantic UI at this point to help create a page that looks at least tolerable. We can install it with npm install --save-dev semantic-ui-react. We’ll use a light wrapper library to avoid some annoying interop, adding com.fulcrologic/semantic-ui-wrapper {:mvn/version "1.0.0"} to deps.edn. Lastly, we need to add <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.css" /> to the head of our index.html page, and we’re ready to go.

We’ll create a CurrentUser component that we can use to log in and log out. Let’s put it on the main page for now; we can integrate it in a proper navbar later. The component displays a loading button while we wait to figure out if the user is authenticated or not. If the user is authenticated, it displays a logout button. If the user isn’t authenticated, it displays a login button.

(defsc CurrentUser [this {:user/keys [id email]
                          :ui/keys [loading] :as user}]
  {:query [:user/id :user/email :ui/loading]
   :ident (fn [] [:component/id :current-user])
   :initial-state {:ui/loading true}}
  (cond
    loading (dom/button :.ui.loading.primary.button)
    (and id email) (dom/button :.ui.primary.button
                     {:onClick #(auth/logout)}
                     "Logout")
    :else (dom/button :.ui.primary.button
            {:onClick #(auth/login)}
            "Login/Signup")))

I’ll omit the code necessary to render this from the Root component–it can be found on github. With this in place, we need to write some code to initialize our auth0 client. We’ll use core.async for this, so we should add it to deps.edn with org.clojure/core.async {:mvn/version "1.0.567"}. We update our initialization code as follows:

(defn ^:export init []
  (log/info "Application starting...")
  (app/mount! SPA Root "app")
  (go
    (<! (auth/create-auth0-client!))
    (when (str/includes? (.. js/window -location -search) "code=")
      (<! (auth/handle-redirect-callback))
      (.replaceState js/window.history #js {} js/document.title js/window.location.pathname))
    (if-let [authenticated (<! (auth/is-authenticated?))]
      (let [{:strs [sub email]} (js->clj (<! (auth/get-user-info)))]
        (comp/transact! SPA [(set-current-user {:user/id sub :user/email email})]))
      (comp/transact! SPA [(set-current-user {})]))))

We create the auth0 client, waiting for it to finish. We detect that this page load is a redirect after a login by looking for code= in the query parameters. In that case, we use auth/handle-redirect-callback to fetch an access token. We also remove the query params from the URL so this code path isn’t activated again unnecessarily when the user refreshes the page. If that succeeds, then the user is authenticated. We get that user’s info and transact it into application state with a set-current-user mutation. If the user is not authenticated, we transact an empty map into application state with set-current-user.

The job of set-current-user is to communicate to the CurrentUser component that it is no longer loading and to pass it the correct user info. Since it doesn’t do much, it is a concise mutation:

(defmutation set-current-user [user]
  (action [{:keys [state]}]
    (swap! state assoc-in [:component/id :current-user] (assoc user :ui/loading false))))

And we’re done! Users can sign up, log in, and log out to our client-side application. We’ll handle authenticating to an API server when we implement one.

Prev: Gift list dev diary: introduction Next: Gift list dev diary: routing