January 9, 2021

Auth0 SPA login flow with Shadow CLJS

Table of Contents

  1. Import dependencies
  2. Create Auth0 Client
  3. Login With Redirect
  4. Handle Redirect or Initialize token silently
  5. Everything Put Together

Initial setup

Create a new Clojurescript application to start with.

npx create-cljs-app cljs-auth0-spa

Install the dependencies.

  • reagent
  • auth0-spa-js

Auth0-spa-js from npm. Clojurescript packages are downloaded automatically on the startup if not yet downloaded, when they are listed in the dependencies of =shadow-cljs.edn.

npm i @auth0/auth0-spa-js

{:dependencies [[reagent "0.8.1"]]}

The the development server is started by running =npm start. This will run the shadow-cljs cli that takes care of installing the clojurescript dependencies.

Import dependencies

Reagent as clojuresciprt deps packages. NPM package auth0/auth0-spa-js can be imported by the package name. Read more about the npm package imports from Shadow CLJS documentation.

(ns app.home
 (:require [reagent.core :as r]
           [app.config :as c]
           [clojure.string :as str]
           ["@auth0/auth0-spa-js" :as auth0]))

Create Auth0 Client

Get the credentials from the Auth0 dashboard.

(defonce auth0-client
  (auth0/Auth0Client.
   (clj->js {:client_id ""
             :domain ""
             :redirect_uri""
             })))

Login With Redirect

(defn login [] (.loginWithRedirect auth0-client))

(defn login-button []
    [:button
      {:on-click login} "Login"])

Handle Redirect or Initialize token silently

After successfull redirect the the id token call auth0client.handleRedirectCallback.

(defn handle-auth-redirect []
  (->
   (.handleRedirectCallback auth0client)
   (.then load-profile)))

(defn load-silently []
  (pr "load silently")
  (-> (.getTokenSilently auth0client)
      (.then load-profile)
      (.catch #(pr %))))


     (defn handle-redirect?
       "
       Handle auth0 redirect?

       Should handle redirect if state and code query params.
       "
       []
       (let [params (url-search-params)]
         (and
          (.has params "state")
          (.has params "code"))))

     (defn init-session []
       (if (should-handle-redirect?)
           (handle-redirect)
           (init-session)))

     (defn on-load-window [](init-session)
       (.addEventListener js/window "load" on-load-window)

Everything Put Together

(ns app.core
  "This namespace contains your application and is the entrypoint for 'yarn start'."
  (:require [reagent.core :as r]
            [app.home :as home]
            [app.config :as c]
            [clojure.string :as str]
            ["@auth0/auth0-spa-js" :as auth0]))

(enable-console-print!)

(defonce auth0client
  (auth0/Auth0Client.
  (clj->js c/auth0)))

(defn url-search-params
  "Parse URLSearchParams from window location."
  []
  (-> js/window.location.href
      (str/split "?")
      (get 1)
      (js/URLSearchParams.)))

(defn handle-redirect?
  "
  Handle auth0 redirect?

  Should handle redirect if state and code query params.
  "
  []
  (let [params (url-search-params)]
    (and
    (.has params "state")
    (.has params "code"))))

(def raw_token (atom nil))

(defn auth-action-to-take
  "Decide whether to handle a redirect or try to initialize session silently."
  []
  (pr "What auth action to take?")
  (if (handle-redirect?)
    :handle-redirect
    :load-silently))

(def profile (r/atom {}))
(def errors  (r/atom {}))

(defn set-error [e] (reset! errors e))

(defn id-token-claims-to-user [claims]
  (let [claims-map (js->clj claims :keywordize-keys true)]
    ;; save the claims to
    (reset! profile claims-map)))

(defn load-profile []
  (->
    (.getIdTokenClaims auth0client)
    (.then id-token-claims-to-user)
    (.catch set-error)
    (.finally (pr "load-profile: finally"))))

(defn handle-auth-redirect []
  (->
    (.handleRedirectCallback auth0client)
    (.then load-profile)))

(defn load-silently []
  (pr "load silently")
  (-> (.getTokenSilently auth0client)
      (.then load-profile)
      (.catch #(pr %))))

(defn on-window-load []
  (pr "on window load")
  (case (auth-action-to-take)
    :handle-redirect  (handle-auth-redirect)
    :load-silently  (load-silently)
    (pr "no action")))

(.addEventListener js/window "load" on-window-load)

(defn on-login [] (.loginWithRedirect auth0client))
(defn on-logout [] (.logout auth0client))

(defn login-button []
  [:button
  {:on-click on-login} "Login"])

(defn logout-button []
  [:button
  {:on-click on-logout} "Logout"])

(defn view []
  [:<>
  [:h1 "Auth0 SPA CLJS"]
  (when (not @profile) [login-button])
  (when @profile [logout-button])
  [:pre (.stringify js/JSON (clj->js c/auth0) nil 2)]
  [:pre (.stringify js/JSON (clj->js @errors) nil 2)]
  [:pre (.stringify js/JSON (clj->js @profile) nil 2)]])


(defn ^:dev/after-load render
  "Render the toplevel component for this app."
  []
  (r/render [home/view] (.getElementById js/document "app")))


(defn ^:export main
  "Run application startup logic."
  []
  (render))
Tags: Auth0 Clojure ClojureScript Shadow-CLJS JS Interop