Skip to content

Instantly share code, notes, and snippets.

@jacobobryant
Created August 31, 2024 18:23
Show Gist options
  • Save jacobobryant/4f143f678f5e52ba7f98d4af5402997c to your computer and use it in GitHub Desktop.
Save jacobobryant/4f143f678f5e52ba7f98d4af5402997c to your computer and use it in GitHub Desktop.
How to add extra fields to your signup form with Biff's authentication module

Adding fields to the signup form is more complex than just setting a custom :biff.auth/new-user-tx function because both authentication flows (email link and six-digit code) include multiple requests (e.g. in the email link flow, first you submit a form with your email address, then you click a link), and new-user-tx doesn't get called until the last request. So we have to modify the auth flows to take the extra form params from the first request and propagate them so that new-user-tx has access to them.

Below is a diff of the starter app showing exactly how to do that. First, you'll need to copy over the authentication module code into your project, as demonstrated in step 4 of the Postgres howto. Then apply the changes in the diff below to your auth.clj file. Remove the com.biffweb.impl.* namespaces and use a single [com.biffweb :as biff] require instead.

With those changes applied, the authentication-module function will now accept a :biff.auth/extra-params key in which you can specify whatever extra form fields you want to include. This example sets it to [:display-name]. Update your main namespace to pass in the form fields you want, then update schema.clj and home.clj to match as demonstrated in the diff.

At some point I'll probably update the authentication module to have the :biff.auth/extra-params key built-in.

Also, NOTE: if you want to add a lot of extra form fields, it might make the sign-in link too long (supposedly 2048 chars is safe for browsers, but when it comes to email clients, who knows).

diff --git a/src/com/biffweb/impl/auth.clj b/src/com/biffweb/impl/auth.clj
index fe991cc..26397b6 100644
--- a/src/com/biffweb/impl/auth.clj
+++ b/src/com/biffweb/impl/auth.clj
@@ -3,6 +3,7 @@
[com.biffweb.impl.rum :as brum]
[com.biffweb.impl.time :as btime]
[com.biffweb.impl.util :as butil]
+ [com.biffweb.impl.util.ns :as biff.util.ns]
[com.biffweb.impl.xtdb :as bxt]
;;; NOTE: if you copy this file into your own project, remove the
;;; above lines and replace them with the com.biffweb namespace:
@@ -29,16 +30,20 @@
(not (re-find #"\s" email))))
(defn new-link [{:keys [biff.auth/check-state
+ biff.auth/extra-params
biff/base-url
biff/secret
- anti-forgery-token]}
+ anti-forgery-token
+ params]}
email]
(str base-url "/auth/verify-link/"
(bmisc/jwt-encrypt
- (cond-> {:intent "signin"
- :email email
- :exp-in (* 60 60)}
- check-state (assoc :state (butil/sha256 anti-forgery-token)))
+ (merge (select-keys params extra-params)
+ {:intent "signin"
+ :email email
+ :exp-in (* 60 60)}
+ (when check-state
+ {:state (butil/sha256 anti-forgery-token)}))
(secret :biff/jwt-secret))))
(defn new-code [length]
@@ -82,9 +87,10 @@
path-params
params
anti-forgery-token]}]
- (let [{:keys [intent email state]} (-> (merge params path-params)
- :token
- (bmisc/jwt-decrypt (secret :biff/jwt-secret)))
+ (let [{:keys [intent email state]
+ :as token-params} (-> (merge params path-params)
+ :token
+ (bmisc/jwt-decrypt (secret :biff/jwt-secret)))
valid-state (= state (butil/sha256 anti-forgery-token))
valid-email (= email (:email params))]
(cond
@@ -92,7 +98,7 @@
{:success false :error "invalid-link"}
(or (not check-state) valid-state valid-email)
- {:success true :email email}
+ (assoc token-params :success true)
(some? (:email params))
{:success false :error "invalid-email"}
@@ -126,6 +132,21 @@
:else
{:success true :email email :code code :user-id @user-id})))
+(defn query-encode [s]
+ (some-> s
+ (java.net.URLEncoder/encode "UTF-8")
+ (str/replace "+" "%20")))
+
+(defn append-extra-params [{:keys [biff.auth/extra-params params]} url]
+ ;; This assumes that url already has at least one query param in it, since the
+ ;; appended part starts with a "&"
+ (apply str
+ url
+ (for [k extra-params
+ :let [v (get params k)]
+ :when v]
+ (str "&" (name k) "=" (query-encode v)))))
+
;;; HANDLERS -------------------------------------------------------------------
(defn send-link-handler [{:keys [biff.auth/single-opt-in
@@ -135,7 +156,7 @@
:as ctx}]
(let [{:keys [success error email user-id]} (send-link! ctx)]
(when (and success single-opt-in (not user-id))
- (bxt/submit-tx (assoc ctx :biff.xtdb/retry false) (new-user-tx ctx email)))
+ (bxt/submit-tx (assoc ctx :biff.xtdb/retry false) (new-user-tx ctx (assoc params :email email))))
{:status 303
:headers {"location" (if success
(str "/link-sent?email=" (:email params))
@@ -150,11 +171,11 @@
params
path-params]
:as ctx}]
- (let [{:keys [success error email]} (verify-link ctx)
+ (let [{:keys [success error email] :as token-params} (verify-link ctx)
existing-user-id (when success (get-user-id (xt/db node) email))
token (:token (merge params path-params))]
(when (and success (not existing-user-id))
- (bxt/submit-tx ctx (new-user-tx ctx email)))
+ (bxt/submit-tx ctx (new-user-tx ctx token-params)))
{:status 303
:headers {"location" (cond
success
@@ -174,6 +195,7 @@
(defn send-code-handler [{:keys [biff.auth/single-opt-in
biff.auth/new-user-tx
+ biff.auth/extra-params
biff/db
params]
:as ctx}]
@@ -186,10 +208,10 @@
:biff.auth.code/created-at :db/now
:biff.auth.code/failed-attempts 0}]
(when (and single-opt-in (not user-id))
- (new-user-tx ctx email)))))
+ (new-user-tx ctx (assoc params :email email))))))
{:status 303
:headers {"location" (if success
- (str "/verify-code?email=" (:email params))
+ (append-extra-params ctx (str "/verify-code?email=" (:email params)))
(str (:on-error params "/") "?error=" error))}}))
(defn verify-code-handler [{:keys [biff.auth/app-path
@@ -212,7 +234,7 @@
success
(concat [[::xt/delete (:xt/id code)]]
(when-not existing-user-id
- (new-user-tx ctx email)))
+ (new-user-tx ctx (assoc params :email email))))
(and (not success)
(some? code)
@@ -228,7 +250,7 @@
:session (assoc session :uid (or existing-user-id
(get-user-id (xt/db node) email)))}
{:status 303
- :headers {"location" (str "/verify-code?error=invalid-code&email=" email)}})))
+ :headers {"location" (append-extra-params ctx (str "/verify-code?error=invalid-code&email=" email))}})))
(defn signout [{:keys [session]}]
{:status 303
@@ -237,10 +259,15 @@
;;; ----------------------------------------------------------------------------
-(defn new-user-tx [ctx email]
- [{:db/doc-type :user
- :db.op/upsert {:user/email email}
- :user/joined-at :db/now}])
+(defn new-user-tx [{:biff.auth/keys [extra-params]} {:keys [email] :as params}]
+ [(-> params
+ (select-keys extra-params)
+ ;; Adds a `:user/` namespace to the keys in params, e.g. changes
+ ;; :display-name to :user/display-name
+ (biff.util.ns/select-ns-as nil 'user)
+ (merge {:db/doc-type :user
+ :db.op/upsert {:user/email email}
+ :user/joined-at :db/now}))])
(defn get-user-id [db email]
(bxt/lookup-id db :user/email email))
@@ -249,6 +276,7 @@
#:biff.auth{:app-path "/app"
:invalid-link-path "/signin?error=invalid-link"
:check-state true
+ :extra-params []
:new-user-tx new-user-tx
:get-user-id get-user-id
:single-opt-in false
diff --git a/starter/src/com/example.clj b/starter/src/com/example.clj
index 63fafb1..7225fff 100644
--- a/starter/src/com/example.clj
+++ b/starter/src/com/example.clj
@@ -17,7 +17,7 @@
(def modules
[app/module
- (biff/authentication-module {})
+ (biff/authentication-module {:biff.auth/extra-params [:display-name]})
home/module
schema/module
worker/module])
diff --git a/starter/src/com/example/app.clj b/starter/src/com/example/app.clj
index 265dab1..668f5f3 100644
--- a/starter/src/com/example/app.clj
+++ b/starter/src/com/example/app.clj
@@ -92,10 +92,10 @@
(map message (sort-by :msg/sent-at #(compare %2 %1) messages))]]))
(defn app [{:keys [session biff/db] :as ctx}]
- (let [{:user/keys [email foo bar]} (xt/entity db (:uid session))]
+ (let [{:user/keys [email foo bar display-name]} (xt/entity db (:uid session))]
(ui/page
{}
- [:div "Signed in as " email ". "
+ [:div "Signed in as " display-name " (" email "). "
(biff/form
{:action "/auth/signout"
:class "inline"}
diff --git a/starter/src/com/example/home.clj b/starter/src/com/example/home.clj
index c19235d..19ab383 100644
--- a/starter/src/com/example/home.clj
+++ b/starter/src/com/example/home.clj
@@ -22,18 +22,21 @@
(biff/recaptcha-callback "submitSignup" "signup")
[:h2.text-2xl.font-bold (str "Sign up for " settings/app-name)]
[:.h-3]
- [:.flex
+ [:.flex.flex-col.gap-2
[:input#email {:name "email"
:type "email"
:autocomplete "email"
:placeholder "Enter your email address"}]
- [:.w-3]
- [:button.btn.g-recaptcha
- (merge (when site-key
- {:data-sitekey site-key
- :data-callback "submitSignup"})
- {:type "submit"})
- "Sign up"]]
+ [:input {:name "display-name"
+ :type "text"
+ :placeholder "Enter your name"}]
+ [:div
+ [:button.btn.g-recaptcha
+ (merge (when site-key
+ {:data-sitekey site-key
+ :data-callback "submitSignup"})
+ {:type "submit"})
+ "Sign up"]]]
(when-some [error (:error params)]
[:<>
[:.h-1]
@@ -92,18 +95,22 @@
(biff/recaptcha-callback "submitSignin" "signin")
[:h2.text-2xl.font-bold "Sign in to " settings/app-name]
[:.h-3]
- [:.flex
+ [:.flex.flex-col.gap-2
[:input#email {:name "email"
:type "email"
:autocomplete "email"
:placeholder "Enter your email address"}]
- [:.w-3]
- [:button.btn.g-recaptcha
- (merge (when site-key
- {:data-sitekey site-key
- :data-callback "submitSignin"})
- {:type "submit"})
- "Sign in"]]
+ ;; Uncomment this if you want to use the 6-digit-code flow for signup
+ #_[:input {:name "display-name"
+ :type "text"
+ :placeholder "Enter your name"}]
+ [:div
+ [:button.btn.g-recaptcha
+ (merge (when site-key
+ {:data-sitekey site-key
+ :data-callback "submitSignin"})
+ {:type "submit"})
+ "Sign in"]]]
(when-some [error (:error params)]
[:<>
[:.h-1]
@@ -129,7 +136,7 @@
(biff/form
{:action "/auth/verify-code"
:id "code-form"
- :hidden {:email (:email params)}}
+ :hidden (select-keys params [:email :display-name])}
(biff/recaptcha-callback "submitCode" "code-form")
[:div [:label {:for "code"} "Enter the 6-digit code that we sent to "
[:span.font-bold (:email params)]]]
diff --git a/starter/src/com/example/schema.clj b/starter/src/com/example/schema.clj
index e8e979a..33d3a38 100644
--- a/starter/src/com/example/schema.clj
+++ b/starter/src/com/example/schema.clj
@@ -5,6 +5,7 @@
:user [:map {:closed true}
[:xt/id :user/id]
[:user/email :string]
+ [:user/display-name :string]
[:user/joined-at inst?]
[:user/foo {:optional true} :string]
[:user/bar {:optional true} :string]]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment