Skip to content

feat(OTP): add OTP#212

Closed
maxirozay wants to merge 8 commits intotrailbaseio:mainfrom
maxirozay:OTP
Closed

feat(OTP): add OTP#212
maxirozay wants to merge 8 commits intotrailbaseio:mainfrom
maxirozay:OTP

Conversation

@maxirozay
Copy link
Copy Markdown

This PR add OTP #208 by sending an email with a 8 characters code. It is implemented in the typescript client and in the admin login.

@maxirozay maxirozay marked this pull request as draft February 16, 2026 14:07
@ignatz
Copy link
Copy Markdown
Contributor

ignatz commented Feb 16, 2026

Thanks so much for this contribution 🙏

The one thing that's surprising to me is that "request OTP" seems to be its own independent flow. I'm still paging in, but naively I would have expected that for users with OTP enabled, the auth flow doesn't return an access token but rather an "upgradable token" that together with the OTP can be "upgraded" into a proper auth token. In other words, I'm surprised that this change doesn't touch the login flow.

@maxirozay
Copy link
Copy Markdown
Author

You mean to have multifactor authentification, sign in with password then ask for OTP ? It could also work like that. I made it the way I use it with firebase (which is more a magic link) and supabase where I receive an email with a code (a link for firebase) and use it to sign in without password.

@ignatz
Copy link
Copy Markdown
Contributor

ignatz commented Feb 16, 2026

I see now - thanks. This is an alternative, not an expanded authentication flow 👍

I'll admit, that's not a flow i've ever used. Naively, I would expect this to be less secure and less convenient than a pw manager. Curious to hear what makes you favor this approach?

To be transparent, what I originally had in mind was to add a second-factor for the added security.

@maxirozay
Copy link
Copy Markdown
Author

So I added passwordless TOTP signin. Now we can add settings to enable/disable those auth methods and maybe choose if we want 2 authentication factors. In my case I need passwordless signin but for sure we should be able to ask for 2 auth methods if they are setup. The login UI probably will change and I added the TOTP generation in the AuthButton, maybe it should be somewhere else and it needs also a verification step.

In my company we switched from password auth to passwordless to have less support and firebase magic link works very well. I think it's good to have the option and for sensitive accounts use multifactor auth. Password manager are good but not everybody uses them and clients are happy to just receive a link or code rather than have to remember a password that they will forget and then do the "forget password" flow to sign in every time ^^.

@maxirozay
Copy link
Copy Markdown
Author

In Pocketbase if there's 2 factors enabled the first auth throws an error 400 and the second auth works (the order doesn't matter). We could enable 2FA and when someone sign in the first signin would respond that it worked but needs another signin method then if it succeed the user would be signed in. I'll check that this week.

@maxirozay
Copy link
Copy Markdown
Author

maxirozay commented Feb 17, 2026

@ignatz what about:

  • The OTP with 12 characters that last 5 minutes can be a sign in option alone. It's basically a 12 characters random password that will be reset in 5 minutes, so way more robust than someone's password that has been leaked like 'password123' (supabase use only 6 digits for their OTP which I agree is not very robust).
  • If a user sign in with password or OTP and has TOTP registered we do one of those 2 options.
    • We ask for the TOTP plus the password or the OTP.
    • We return an auth code like in login_with_authorization_code_flow_and_pkce and ask for the auth code plus the TOTP to sign in.

Do you have preferences or suggestions?

@maxirozay maxirozay marked this pull request as ready for review February 17, 2026 14:10
@maxirozay
Copy link
Copy Markdown
Author

Sumary of what I did:

  • Sign in with OTP
  • Sign in with password plus TOTP
  • Sign in with OTP plus TOTP

New TS client fonctions:

  • requestOTP to receive the OTP by email
  • verifyOTP to sign in with an OTP
  • generateTOTP to generate a secret for TOTP
  • confirmTOTP confirm that you register the TOTP in your authenticator app and save the secret in the DB
  • disableTOTP check that you have the TOTP and remove the secret from the DB
  • verifyTOTP sign in with TOTP plus password or OTP

To generate/disable TOTP sign in in the admin then go in the auth button (bottom left).

@ignatz
Copy link
Copy Markdown
Contributor

ignatz commented Feb 20, 2026

Sorry, for the radio silence - apologies! I fell down a cross-platform-build rabbit hole and must have banged my head.

I'm back now and will look at this ASAP - thanks already 🙏

In my company we switched from password auth to passwordless to have less support and firebase magic link works very well. I think it's good to have the option and for sensitive accounts use multifactor auth. Password manager are good but not everybody uses them and clients are happy to just receive a link or code rather than have to remember a password that they will forget and then do the "forget password" flow to sign in every time ^^.

Thanks for the motivation. I guess I could see this work well for internal tooling 👍 . I'll also take a closer look at what firebase offers (and supabase apparently).

The OTP with 12 characters that last 5 minutes can be a sign in option alone. It's basically a 12 characters random password that will be reset in 5 minutes....Do you have preferences or suggestions?

W/o yet having looked at it, I would assume we could do many digits. Isn't it just a link folks click on?

We return an auth code like in login_with_authorization_code_flow_and_pkce and ask for the auth code plus the TOTP to sign in.

That sounds pretty much what I would have naively expected 👍

Sumary of what I did:

  • Sign in with OTP
  • Sign in with password plus TOTP
  • Sign in with OTP plus TOTP

That's amazing 🎉 🙏

@maxirozay
Copy link
Copy Markdown
Author

I've been researching more about the topic so there's things I'd do differently. The main points I found out are:

  • The OTP/magic link are the best ways to register a user since it verify the email at the same time.
  • OTP
    • should be 6 digits (or alphanumeric) because it's easy to memorise and to type
    • should be reset after a couple of failed attempt to avoid brut force (so we need to add a counter)
  • magic link
    • token should be a long (32+ characters) string
    • the link should contain the email so the user doesn't have to retype it if they open the link on another browser.
  • The OTP and magic link token hash should be stored in a KV storage so we don't use the DB for a temporary thing and they can't be stolen.

Since OTP/magic link are both short lived, random, reset after a couple of attempts, are not stored anywhere by the user I think they're way more secure than a password created and managed by a user (like storing password123 in a note app or using the same leaked password for everything ^^).

I made a nuxt project to have a project with the basics stuff, if you want to look at the auth part https://github.com/maxirozay/nuxt-base/tree/master/server/api/auth.

@ignatz
Copy link
Copy Markdown
Contributor

ignatz commented Feb 24, 2026

should be reset after a couple of failed attempt to avoid brut force (so we need to add a counter)

That sounds sensible.

  • magic link
    • token should be a long (32+ characters) string
    • the link should contain the email so the user doesn't have to retype it if they open the link on another browser.
  • The OTP and magic link token hash should be stored in a KV storage so we don't use the DB for a temporary thing and they can't be stolen.

You can round-trip the email or just pick-it up server side, given a secure token. Both seems fine. We already use the DB for reset codes, etc. I don't understand why a KV store would be more secure, in memory or persistent. Especially since you argue they should have a short lifetime. Persistence is a good thing, you want TB to be able to restart (e.g. update), while a user auths. Is there something I'm missing?

Since OTP/magic link are both short lived, random, reset after a couple of attempts, are not stored anywhere by the user I think they're way more secure than a password created and managed by a user (like storing password123 in a note app or using the same leaked password for everything ^^).

Disagree. You're just delegating the security to he user's email account, i.e. you cannot make any strong statements either way. It may be more secure, it may be less secure 🤷‍♀️

Coming back to the PR, I'll start taking a closer look. As you've already noticed, there's a lot of details, considerations and trade-offs. I'm not sure what the best way is to go about it. Breaking this PR up and tuning all those details, will probably require a lot of back-and-forth. My naive approach would be to pull out and modify bits and pieces, make sure that changes are properly attributed.

Was this PR mostly prompted? No hate, this is not a college exam, I care about the destination, not the journey. It just seems silly to go back and forth between me and your LLM with you as a person-in-the-middle.

Let me know what works for you, I'm flexible 🙏

@ignatz
Copy link
Copy Markdown
Contributor

ignatz commented Feb 24, 2026

Just a quick update (also for myself to organize my thoughts and remember) after looking at the PR. There's some good stuff, things that may require some work (just to be clear, I'm not trying to dump extra work on your lap, always happy to get my hands dirty):

  • Support the non-admin auth UI. This has implications, since the auth UI is a pure HTML form w/o JS, e.g. QR codes would have to be generated server-side, flows would need to be modeled as a series of redirects, ...
  • Currently OTP is enabled by default, which may be ok for internal tools but otherwise should probably not be the case. Further, I'm wondering if this shouldn't be a global + per user setting. For example, it may be allowed by the admin, but I wouldn't want it for my account.
  • The OTP email should probably contain a link.
  • There are some state management issues in the UI with the TOTP onboarding. Haven't looked into the details yet but it's also not really a concern especially if the TOTP flows were to change to allow for the auth-ui to work

@maxirozay
Copy link
Copy Markdown
Author

I don't understand why a KV store would be more secure, in memory or persistent.

For the KV store I'm not saying it's more secure just that it doesn't have to bother the DB with all the code requests. You can have the persistance with an external KV like redis that doesn't stop while you restart TB.

All my points are general thought not necessarily things that should be done.

Was this PR mostly prompted?

I don't know rust so the rust code yes, for some trivial code I use the autocomplete but everything was read, edited and cleaned, it's not just a dumb prompt dump.

@ignatz
Copy link
Copy Markdown
Contributor

ignatz commented Feb 24, 2026

All my points are general thought not necessarily things that should be done.

Appreciated. Just want to make sure, I'm understanding things alright. Oftentimes, I'm just confused

I don't understand why a KV store would be more secure, in memory or persistent.

For the KV store I'm not saying it's more secure just that it doesn't have to bother the DB with all the code requests. You can have the persistance with an external KV like redis that doesn't stop while you restart TB.

After reading your initial point again, you may have meant that pulling the tokens out, they're safe from any hypothetical SQL injection (in TB or users' WASM code). That makes a lot of sense. I like the idea. I'm not sure redis would be a good fit, TBs general approach but e.g. pulling them out into a separate db connection would have some nice properties. That's true for all tokens: otp, password reset, email verification, ... I will add this to my list of things to do 👍 🙏

Was this PR mostly prompted?

I don't know rust so the rust code yes, for some trivial code I use the autocomplete but everything was read, edited and cleaned, it's not just a dumb prompt dump.

👍

I've been thinking a bit more about how to best skin the cat. I think that some level of initial auth-ui support would be healthy to inform the flows. Email OTP and TOTP could be staggered. I'm leaning prioritizing TOTP, mostly because it's more complicated. We'd have to figure out the flows. PocketBase's approach for returning 401 on first-factor auth won't work for the auth-ui. Nice puzzle :)

@maxirozay
Copy link
Copy Markdown
Author

I don't know what can be done with the auth UI but if that can help, on the project i previously linked, I made a route to get all the sign in options. I only ask for the email then I get the options and show them to the user. So if a user has TOTP after validating their email I'll show the TOTP field.

The route doesn't say if a user exist or not it just return the options so you can technically see if a user exist but you can't see if it doesn't exist. That's a point I'm still not sure if I should keep it this way.

@ignatz
Copy link
Copy Markdown
Contributor

ignatz commented Feb 27, 2026

I don't know what can be done with the auth UI but if that can help, on the project i previously linked, I made a route to get all the sign in options. I only ask for the email then I get the options and show them to the user. So if a user has TOTP after validating their email I'll show the TOTP field.

Appreciated. This works well for client-side JS. Ideally all auth UIs would work w/o JS purely relying on HTML forms and SSR. At the same time the auth APIs and UIs need to be decoupled.

I ended up passing an mfa_redirect target to the login, so the API can hand back to the UI, when TOTP or other MFA is required, which can then pass an mfa_token plus second secret to a mfa_login endpoint. Not sure this is most elegant but it works :/.

This is all turning out to be pretty big :hide:

@maxirozay
Copy link
Copy Markdown
Author

If it works it works ^^ it still an optional component anyway

@ignatz ignatz force-pushed the OTP branch 2 times, most recently from de57875 to 25ffbd0 Compare March 5, 2026 08:02
@ignatz
Copy link
Copy Markdown
Contributor

ignatz commented Mar 6, 2026

Quick update. I have most parts in place including TOTP and OTP and a bunch of nice improvements. Besides some more testing and updating more clients, I'm primarily missing a good way to pass configuration/state to the admin SPA, specifically if OTP is enabled or not for the instance. I would either:

  • Have to change how we render/deploy the admin UI
  • Risk showing a disfunctional button (which may be ok)
  • Not allow OTP login through the admin login page (FWIW, it would still work to log in through the auth component and then go the admin UI :/)

@maxirozay
Copy link
Copy Markdown
Author

You can't check if OTP is enabled then render the button if it is, even with an env or an endpoint to get the config?

@ignatz
Copy link
Copy Markdown
Contributor

ignatz commented Mar 9, 2026

You can't check if OTP is enabled then render the button if it is, even with an env or an endpoint to get the config?

I take this as a rhetorical question :). A build-time env variable wouldn't work, since enablement is a runtime property and the admin UI is currently fully client-rendered. We don't inject any state at least right now.

With an endpoint we could. There just isn't a public settings endpoint yet. There are admin config endpoints but they require you to be logged in as an admin. I was considering to add a public auth settings endpoint, which is still an option, for now I just made the OTP UI less prominent and it will just yield an error if you try 🤷‍♀️

ignatz added a commit that referenced this pull request Mar 16, 2026
…gins UIs to admin dash and the auth-ui crate.

Loosely based on maxirozay@'s #212.
@ignatz
Copy link
Copy Markdown
Contributor

ignatz commented Mar 16, 2026

v0.25.0 adds second-factor TOTP and OTP email support in the server, the two UIs and all 8 clients. It also changes how we handle session like state and OTP codes are moved to a separate session.db. Thanks @maxirozay 🙏

@ignatz ignatz closed this Mar 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants