Skip to content

feat(otp): switch one_time_tokens table to source of truth - #2788

Open
annabkr wants to merge 14 commits into
annabaker/auth-1553-ott-query-helpersfrom
annabaker/auth-1553-switch-one_time_tokens-table-to-source-of-truth
Open

feat(otp): switch one_time_tokens table to source of truth#2788
annabkr wants to merge 14 commits into
annabaker/auth-1553-ott-query-helpersfrom
annabaker/auth-1553-switch-one_time_tokens-table-to-source-of-truth

Conversation

@annabkr

@annabkr annabkr commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Feat

What is the current behavior?

We dual-write token data to the one_time_tokens and users table, but read token data from them inconsistently.

What is the new behavior?

When a flag GOTRUE_EXPERIMENTAL_ENABLE_OTT_AS_SOURCE_OF_TRUTH is enabled, we treat one_time_tokens as the source of truth for token data, and only lookup the user to validate eligibility and confirm identifier binding.

Additional context

I've intentionally split out the Twilio Verify flow + Test OTP handling into a separate PR, but will wait until both are merged to release.

Closes AUTH-1553.

@annabkr
annabkr changed the base branch from master to annabaker/auth-1572-start-writing-to-one_time_tokensexpiresat September 4, 2026 19:53
@annabkr
annabkr force-pushed the annabaker/auth-1553-switch-one_time_tokens-table-to-source-of-truth branch from 2ed1e5b to 0665647 Compare September 9, 2026 16:14
@annabkr
annabkr changed the base branch from annabaker/auth-1572-start-writing-to-one_time_tokensexpiresat to annabaker/auth-1553-ott-query-helpers September 9, 2026 16:14
@annabkr
annabkr force-pushed the annabaker/auth-1553-switch-one_time_tokens-table-to-source-of-truth branch from 0665647 to 8bf17f6 Compare September 9, 2026 16:20
@annabkr
annabkr force-pushed the annabaker/auth-1553-ott-query-helpers branch 2 times, most recently from 7577774 to fc7f405 Compare September 9, 2026 19:14
@annabkr
annabkr force-pushed the annabaker/auth-1553-switch-one_time_tokens-table-to-source-of-truth branch from 8bf17f6 to 0820b2b Compare September 9, 2026 19:19
Remove the phone-provider branches from the one_time_tokens verify path so
this PR covers only the row-based flow. In OTT mode a test OTP or Twilio
Verify code now misses the hash lookup and is rejected. A follow-up PR
stacked on this branch restores both.
@annabkr
annabkr force-pushed the annabaker/auth-1553-switch-one_time_tokens-table-to-source-of-truth branch from 31b4e74 to 6d15a97 Compare September 10, 2026 14:50
// NOTE: Test OTPs and Twilio Verify are not handled yet on this path; a follow-up PR will add them.
func verifyUserAndTokenFromOTT(conn *storage.Connection, params *VerifyParams, aud string) (*models.User, error) {

// TODO AUTH-1553: Add support for test OTPs and Twilio Verify on this path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separating this out to make it easier to review, but they'll be merged together.


// IsExpired treats nil ExpiresAt as expired. This is a security measure to avoid accidentally treating a token with no expiration as valid.
func (o OneTimeToken) IsExpired() bool {
return o.ExpiresAt == nil || time.Now().After(*o.ExpiresAt)

@annabkr annabkr Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have two options here:

  1. Treat a nil expiresAt as expired.
  2. Treat a nil expiresAt as non-expired.

expiresAt will be nil for all OTTs created before expiresAt was written to.

We can reduce the likelihood by staggering the release of these changes + the write-path changes so the OTTs without expiries age out, but eventually we plan to make this path on by default. So, if a self-hosted customer doesn't upgrade until we've made it the default, it would cause all of their recently issued tokens to expire.

I went this direction because having to re-request a OTT seems like a minor inconvenience, but I wanted to point that out so we can discuss if there are any concerns.

// columns. A lookup miss is rejected as an expired or invalid token.
//
// NOTE: Test OTPs and Twilio Verify are not handled yet on this path; a follow-up PR will add them.
func verifyUserAndTokenFromOTT(conn *storage.Connection, params *VerifyParams, aud string) (*models.User, error) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The summary is that we:

  1. Verify the OTT by looking it up and checking if it's expired
  2. Resolve the generic type=email request param type to the actual token type for post-verification steps
  3. Find the user by the OTT's user ID
  4. Ensure that the user associated with the OTT is eligible to verify it, and validate identifier binding

@annabkr
annabkr marked this pull request as ready for review September 10, 2026 20:39
@annabkr
annabkr requested a review from a team as a code owner September 10, 2026 20:39
Comment thread internal/api/verify_ott.go

@xlgmokha xlgmokha left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nicely done! Nothing stood out for me as blockers. I left one question but it's not a blocker.

I'm not as familiar with this part of the codebase so you might choose to wait for another review from someone else or not. I trust your discretion.

return mismatch.WithInternalMessage("user email does not match")
}
default: // Signup, Invite, Recovery, MagicLink
if params.Email == "" || !strings.EqualFold(user.GetEmail(), params.Email) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: is user.GetEmail() a safe fallback?

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