Adding Authentication

One area Biff seems curiously un-opinionated is authentication. I would like to layer on something like buddy-auth so that I can secure both a web front end and an api. A tutorial for adding this to Biff would be helpful, as most tutorials out there focus on a PostgreSQL back end for session management. But I’m all-in on XTDB and HTMX, albeit both are somewhat new to work with. Absent that, any advice or pointers in the right direction would be appreciated.

Sincerely,

Bob

Noted–a “how to set up a public API with Biff” guide would be a good addition. Biff does have the :api-routes plugin key for that purpose, but I don’t actually use it in any of my own apps (apart from a single endpoint for stripe webhooks perhaps…), and there might be rough edges that need to be fixed.

Anyway, although I probably won’t get to that for a while, you can at least take a look at the authentication plugin source to get a feel for how that works. See also the middleware that’s used for authorization.

If you’re happy with using that for the web frontend authentication, you could leave that as-is and then add a separate thing for API auth. Buddy might be helpful, or it may be easy enough to do it from scratch. As a very simple setup, you could have a page in the web frontend where users can generate an API token, then have them include it in the Authorization: Bearer <token> header in API requests, then add middleware to your API routes that check that header and query XTDB to see if it’s a valid token. Since XTDB has local indexes it hopefully should be fine to run that query on every request.

A fancier option would be to implement OAuth, though I’m afraid I can’t help much here as I haven’t done that myself (not on the server side at least).

If you’d like something different for the web frontend auth, you may want to look at using a 3rd-party provider like Firebase Authentication or Auth0. You’d need to configure htmx to include the Firebase/Auth0 token on each request, then add middleware that decrypts the token and merges it into the incoming ring request, which you can then use for authorization. (Maybe Auth0 could also be used to provision OAuth API tokens? I haven’t looked into that myself.)

Can you please explain exactly how out of the box authentication works? All I need to do initially is add a password and some other components to my user entity.

Sure. Biff’s authentication plugin includes two authentication flows:

Email links (“magic links”):

  1. The user enters their email into a form.
  2. On submission, the form is posted to the /auth/send-link endpoint. That endpoint generates an encrypted JWT containing the user’s email. It also contains a random token that’s stored on the client’s device via a cookie (it’s the CSRF token, specifically). The JWT is embedded in a link, and the link is emailed to the user.
  3. The user opens the email and clicks the link, which takes them to the /auth/verify-link endpoint. If the JWT is valid and the random token in the JWT matches the CSRF token stored on the user’s device (i.e. the user is clicking the link on the same device + browser that they requested the link from), then they’re authenticated (see 4). If the random token doesn’t match, the user is prompted to enter their email address again; if the address they enter matches the one in the JWT, then they’re authenticated. (This prevents attackers from tricking people into signing into the attacker’s account.)
  4. When the user is authenticated, a user document in XTDB is created for their email address if one doesn’t already exist. Then the document ID (a random UUID) is stored in the user’s session. The rest of the application can check authentication by looking at the ID stored in the session and querying for the associated user document.

Sign-in codes:

  1. The user enters their email into a form and submits it to the /auth/send-code endpoint.
  2. That endpoint generates a random 6-digit code and stores it in XTDB along with the user’s email address. The code is emailed to the user. The endpoint redirects to a page with another form where the user can enter the code. That form includes a hidden field that contains the user’s email address.
  3. The user gets the code from their email and enters it into the form. The form is posted to the /auth/verify-code endpoint.
  4. That endpoint queries XTDB for the code associated with the user’s email address. If that code matches the code that the user entered AND the code hasn’t expired AND the code hasn’t had too many incorrect sign-in attempts, then the user is authenticated.
  5. Authentication: same as step 4 in the sign-in link flow.

Both flows can be used for either creating a new account or for signing in; the example app uses the link flow for creating an account and the code flow for signing in.

If you do want to do password authentication instead of email authentication, there are a few options:

  • use a 3rd-party service like Firebase Auth or Auth0 as I mentioned previously.
  • use momerath42/biff-plugins-auth, an alternative authentication plugin that someone else wrote up which includes password auth (I haven’t read its code, so can’t provide any detailed information about it).
  • Write up your own authentication flow has the user enter an email/username + password and then use that to create the user document, and then have an additional sign-in flow that checks the password. Buddy has some password hashing+salting functions you can use. Authentication can work the same way: stick the user ID in the session.

If you’re fine with using the email link/code auth flows and just want to add more stuff to the user document when it’s created, you can look at the :biff.auth/new-user-tx option in the default authentication plugin:

(defn new-user-tx [ctx email]
  ;; modify this:
  [{:db/doc-type :user
    :db.op/upsert {:user/email email}
    :user/joined-at :db/now}])

(def plugins
  [app/plugin
   (biff/authentication-plugin {:biff.auth/new-user-tx new-user-tx})
   home/plugin
   schema/plugin
   worker/plugin])

The ctx parameter there includes the Ring request, so you can potentially use that to access additional fields that you may want to have in your sign up form. The only difficulty is that the new-user-tx function isn’t called until after the user clicks the email link/enters the code, so any fields in the original email form won’t necessarily be there. To record additional fields, there are at least two options:

  • Do it after authentication. In your app, see if the user has already submitted the additional fields you want to capture. If not, show them a form; if they have, skip the form.
  • Copy the authentication plugin source into your project, then modify the flow(s) to capture more fields. For the link flow, you can insert more fields into the JWT; for the code flow, you can create more hidden fields in the code verification form.

My authentication needs initially are super simple: sign up allows a user to establish their username, password, and some other fields. Then they can log in.

This is because the app will be running standalone on-prem initially. We are piloting with small offices that have their own LAN and run applications locally. Our app will be one that they run locally, that accesses external internet sites via email, SFTP, and HTTP endpoints.

I expect the next authentication requirement will be integrating with local LDAP/AD, or maybe and external provider like Auth0.

So I actually want to remove any dependency at this time on captcha and external mail programs. Even wrt emails, they’ll configure an email account they already use for outbound emails to known recipients. I don’t think I need an externa mail provider yet.

Though for a normal internet app I love the Biff defaults it would be helpful to have tutorial to strip signup/signing and auth down to bare bones. I know security wise these are … perhaps too minimalist, but we are piloting the software next month and will have a better understanding after that what next steps we need to take for security purposes.

So any help just getting basic sign up/sign in and auth working with whatever is out of the box already would be great. I see, for example, com.biffweb.misc has methods for jwt encoding/decoding - that would be useful for any api-routes, and I’m not yet really sure how to hook it all up in a way that makes sense with biff internals.

Makes sense. I’d be happy to code up a quick example for a sign up/sign in form that uses passwords once I get a few minutes, maybe in the next few days. Presumably you don’t need a password recovery flow or email verification, at least not at first, which makes simpler. The rough steps will be to remove the authentication plugin altogether, then edit home.clj to remove the existing sign in/sign up forms, then replace them with a couple password-based forms.

As far as integrating with Biff, there’s not much that needs to be done. Biff handles session management, so you can return something like {:status 200, :session (assoc (:session ctx) :uid #uuid "...", ...} from a Ring handler and the session will be stored in an encrypted cookie. Other than that, authorization happens entirely in your application code; Biff’s internal code doesn’t have any expectations about authorization. Most likely you’ll want to create some middleware that takes the user ID from the session / takes the API token from the request headers and then shows an error message if the user doesn’t have access to the route in question. (I.e. what the wrap-signed-in middleware in the example app does).

Password recovery is necessary but not email verification. There is a chicken-egg problem here in that on first use the system has no way to send an email that can be redirected to the locally running instance because at this point no SMTP/IMAP accounts have been configured. Or at least, I haven’t found a good way to do that yet.

They will have to set up an SMTP or IMAP account the first time they run the application, accept license terms, and set up the initial user account and company details. So password recovery would use the local IP/DNS address for the user to click, and available only after initial setup.

I’m trying to figure out how to do this somewhat urgently but look forward to any help you can provide as you are able.

OK… if I’m understanding correctly, I wonder if it would work to just use the default email-based authentication then, after initial setup has been completed and SMTP/IMAP credentials are in place? Then there’d be no need to code up any additional authentication flows.

E.g. when the app is loaded, if an admin account hasn’t been created yet, it’ll show a form prompting for email-sending credentials. it’ll also ask for an email address for the admin account. when the form is submitted, it saves the email credentials and attempts to send an email containing a sign-in link. when that link is clicked, the admin account is created.

Then other accounts can use the default email-based authentication flow, but with recaptcha disabled (an easy charge to make).

Also, I might be available for a limited amount of consulting if that would be helpful–E.g. a few hours of pair programming per week in the evenings or weekends. I’d need to see how my wife feels about spending more time working :wink:. But you can email me if you want to discuss that: hello@tfos.co