Authentication

This document explains how authentication works in the application.

Overview: Refresh Token + Access Token Approach

Our authentication system uses two types of tokens:

  1. Access Token: Short-lived token (valid for 15 minutes) used to authenticate users for protected API routes. These tokens are kept only in memory and never stored in persistent storage.

  2. Refresh Token: Long-lived token (valid for 90 days) used only to obtain a new pair of access and refresh tokens when the access token expires. This token is stored in the session cookie (not accessible from the client app for security reasons)

This approach follows security best practices by minimizing the exposure of the more powerful access token while providing seamless user experience.

Authentication Flow

  1. Initial Login:

    • User logs in with credentials (email/password) or via OAuth provider
    • Server validates credentials and creates a token pair
    • Access token is kept in memory (JavaScript variable)
    • Refresh token is stored in user's session cookie.
  2. Authenticated Requests:

    • For each API request, the access token is attached in the Authorization header
    • Server validates the token and processes the request
  3. Token Refresh Flow:

    • When access token expires (or is about to expire), client uses the refresh token to get a new token pair
    • If refresh is successful, new access token is kept in memory and new refresh token replaces the old one in the session cookie.
    • This creates a persistent login experience across page refreshes
  4. Token Rotation and Security:

    • For security, we implement refresh token rotation - each time a refresh token is used, it's replaced with a new one
    • This helps prevent token replay attacks
    • If a refresh token is used more than once (potentially indicating it was leaked), the system invalidates the entire token family, thus forcing the user to log back in
    • Token Tampering Detection: If the system detects token tampering (e.g., invalid signature), it immediately invalidates the entire token family to prevent potential security breaches

For more detailed information on refresh token rotation, see the Auth0 documentation.

Key Files

Here are the files which are of importance:

Client-side (ClojureScript)

  • src/cljs/saas/auth.cljc: Contains the core authentication logic for the front-end including:
    • Token management
    • Login/logout functions
    • Token refresh logic
    • OAuth authentication

Server-side (Clojure)

  • src/clj/saas/web/controllers/auth/handlers.clj: Contains server-side authentication handlers:
    • User registration
    • Login
    • Token generation
    • Refresh token rotation logic
    • OAuth providers integration

Protecting API Routes

API routes that require authentication are protected using middleware:

;; Example from src/clj/saas/web/routes/api.clj
["" {:middleware [[mw/wrap-jwt-auth]]}
  ;; Public routes that only need JWT parsing but don't require authentication

  ["/me" {:middleware [[mw/wrap-authenticated]]}
   ;; Protected routes that require valid authentication
   ]]

The application uses two middleware functions:

  1. wrap-jwt-auth: Decodes the JWT token and adds :claims to the request object
  2. wrap-authenticated: Ensures that a user is authenticated by checking for the existence of :sub in claims

Accessing User Information in Route Handlers

When handling requests for authenticated routes, you can access the user information from the token claims:

(defn get-user-data [req]
  (let [claims (:claims req)
        user-id (:sub claims)]
    ;; Access other claims like :email, :role, etc.
    ;; Process request with user context
    ))

Common claims available in the token:

  • :sub: The subject (user ID)
  • :exp: Expiration time
  • :iat: Issued at time
  • Other custom claims depending on your application

User Role

The user's organisation membership role (:user-account/role) is returned by the :query/get-account endpoint and stored in the frontend DataScript DB at [:db/ident :saas.auth/values]. This role is used for role-based access control across the application — restricting routes, filtering sidebar items, and enforcing backend command/query permissions.

Refresh Token Persistence

Login persistence works because:

  1. On page load/refresh, the will issue a command/refresh-token
  2. The handler on the server looks for the refresh token in the cookie session
  3. If found, and the expiration date is valid, the server will issue a new refresh-access token pair
  4. This happens automatically before the user interacts with the application
  5. As long as the refresh token is valid, the user remains logged in without having to re-enter credentials

This approach balances security (access tokens are ephemeral and never stored) with user experience (seamless login persistence through refresh tokens).