Biff includes an authentication plugin that implements passwordless, email-based signin. There are two options: email links, where users click a link in an email to sign in; and email codes, where users copy and paste a six-digit code to sign in. The example project uses email links for the signup form and email codes for the signin form.

The authentication plugin provides the backend routes, which handle sending emails to your users and verifying the links and codes. UI and email templates are handled in your application code so that they can be easily customized.

The example project comes with code for sending emails with Postmark. Until you add API keys for Postmark and Recaptcha (which is used to protect your signin forms from bots), signin links and codes will be printed to the console instead of being emailed.

After a user authenticates successfully, their user ID will be stored in the session. Sessions are stored in encrypted cookies. For new users, a user document will be created as well. You can get the user's ID from the session like so:

(defn whoami [{:keys [session biff/db]}]
  (let [user (xt/entity db (:uid session))]
      [:p "Signed in: " (some? user)]
      [:p "Email: " (:user/email user)]]]))

(def plugin
  {:routes [["/whoami" {:get whoami}]]})

See the authentication-plugin API docs for full details.

If you need to modify the plugin beyond what the configuration options allow, you can copy the source code into your project or replace it altogether.

Your secrets.env file contains two secrets which are used to encrypt/sign your session cookies and JWTs, respectively. If you want to rotate the secrets, you can generate new values by running the bb generate-secrets command.


You can use middleware to restrict routes to certain users. The example project comes with a wrap-signed-in middleware which redirects unauthenticated users to the signin page:

(defn wrap-signed-in [handler]
  (fn [{:keys [session] :as ctx}]
    (if (some? (:uid session))
      (handler ctx)
      {:status 303
       :headers {"location" "/signin?error=not-signed-in"}})))

If you need additional roles, you could add a :user/roles key to your user documents:

(defn wrap-admin [handler]
  (fn [{:keys [biff/db session] :as ctx}]
    (let [user (xt/entity db (:uid session))]
      (if (contains? (:user/roles user) :admin)
        (handler ctx)
        {:status 403
         :headers {"content-type" "text/plain"}
         :body "Unauthorized."}))))


Biff uses ring-anti-forgery for CSRF protection. You can add a CSRF token to your forms like so:

  (:require [ring.middleware.anti-forgery :as csrf]))

(defn signin [ctx]
    [:form {:method "POST"
            :action "/signin"}
     [:input {:type "hidden"
              :name "__anti-forgery-token"
              :value csrf/*anti-forgery-token*}]
     [:input {:type "email" :name "email"}]

There is a biff/form function which does this for you, in addition to providing a couple other conveniences:

(defn signin [ctx]
     {:action "/signin"}
     [:input {:type "email" :name "email"}]

The example project also includes a com.example.ui/page function, which will inject the CSRF token into all htmx requests that are triggered by child elements, even if they aren't triggered inside a form element:

(defn page [ctx & body]
   [:div (when (bound? #'csrf/*anti-forgery-token*)
           {:hx-headers (cheshire/generate-string
                         {:x-csrf-token csrf/*anti-forgery-token*})})

CSRF protection only applies to routes that are included under the :routes key of your plugins. If you want to bypass CSRF protection (e.g. because you're providing a public API), you can use the :api-routes key:

(defn hello [ctx]
  {:status 200
   :headers {"content-type" "application/json"}
   :body {:foo "bar"}})

(def plugin
  {:api-routes [["/api/hello" {:post hello}]]})

Biff doesn't include any CORS middleware by default. If you need to bypass CORS protection (e.g. because you're providing a public API that needs to be called from the frontend), you can use ring-cors.

Have a question? Join the forum or the #biff channel on Clojurians Slack.

Sign up for Biff: The Newsletter
Announcements, blog posts, et cetera et cetera.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.