Skip to content

feat(otp): add one_time_tokens query helpers - #2797

Open
annabkr wants to merge 4 commits into
annabaker/auth-1572-start-writing-to-one_time_tokensexpiresatfrom
annabaker/auth-1553-ott-query-helpers
Open

feat(otp): add one_time_tokens query helpers#2797
annabkr wants to merge 4 commits into
annabaker/auth-1572-start-writing-to-one_time_tokensexpiresatfrom
annabaker/auth-1553-ott-query-helpers

Conversation

@annabkr

@annabkr annabkr commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Feature

What is the current behavior?

There is no single-query lookup for a token hash and its pkce_-prefixed form, and no lookup by relates_to.

Callers such as FindUserByEmailChangeCurrentAndAudience run two FindOneTimeToken queries to cover the prefix, which is inefficient.

What is the new behavior?

Adds two parent query methods used by the upcoming OTT-as-source-of-truth work:

  • FindOneTimeTokenWithPKCEFallback: single-query lookup that accepts either an exact token hash or its pkce_-prefixed form, preferring the exact match.
  • FindOneTimeTokenByRelatesTo: looks up the newest token row for a given relates_to value (e.g. phone number) and token type.

Along with:

  • PKCEPrefix is now a constant in models that we reference vs. string literals
  • FindOneTimeToken and FindOneTimeTokenWithPKCEFallback now share findOneTimeToken, guided by a pkceFallback flag.

Adds tests for those methods. Not updating existing callers for now to reduce the radius of this change.

Additional context

Contributes to AUTH-1553 but does not close.

@annabkr
annabkr requested a review from a team as a code owner September 9, 2026 16:14
@annabkr
annabkr marked this pull request as draft September 9, 2026 16:15
@annabkr
annabkr force-pushed the annabaker/auth-1553-ott-query-helpers branch from fa21e86 to 7577774 Compare September 9, 2026 19:08
…yRelatesTo

Two lookups for the OTT-as-source-of-truth work.

FindOneTimeTokenWithPKCEFallback accepts a token hash or its pkce_-prefixed
form in a single query and prefers the exact match.

FindOneTimeTokenByRelatesTo returns the newest row for a relates_to value
and token type. relates_to is not unique for PhoneChangeToken, so the doc
comment tells callers to check the user against the request.
models cannot import api, so the constant moves down and api.PKCEPrefix
points at it. Replaces the bare "pkce_" literals in the one_time_tokens
finders with the constant.
FindOneTimeToken and FindOneTimeTokenWithPKCEFallback delegate to
findOneTimeToken, which takes a pkceFallback flag. With the flag off it
builds the statement FindOneTimeToken built before: same WHERE string,
same args in the same order, same Eager connection, no ORDER BY. The flag
only switches the token_hash clause and adds the ORDER BY.
Pin the exact-hash path: no pkce_ match, the type filter applies, and
either of two types is found. seedToken replaces the truncate, create
user, insert row boilerplate in the hash lookup tests.
@annabkr
annabkr force-pushed the annabaker/auth-1553-ott-query-helpers branch from 7577774 to fc7f405 Compare September 9, 2026 19:14
@annabkr
annabkr marked this pull request as ready for review September 9, 2026 19:15

@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.

The changes LGTM.

Note: I don't have a lot of experience with this ORM so I think it would be helpful to me to see the SQL queries that are generated and the explain plan for them.


if pkceFallback {
// true sorts before false in descending order, so this allows us to prefer an exact match
query = query.Order("token_hash = ? desc", tokenHash)

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.

praise: I didn't know that you could do this.

case 2:
query = query.Where("(token_type = ? or token_type = ?) and token_hash = ?", tokenTypes[0], tokenTypes[1], tokenHash) // #nosec G602
args := append([]interface{}{tokenTypes[0], tokenTypes[1]}, hashArgs...) // #nosec G602
query = query.Where("(token_type = ? or token_type = ?) and "+hashClause, args...)

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(non-blocking): would it be easier to ready to chain the where clauses together? I'm not sure if pop supports something like query.Where(...).And(..)?

Order("created_at desc").
First(oneTimeToken)
if errors.Cause(err) == sql.ErrNoRows {
return nil, OneTimeTokenNotFoundError{}

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.

thought(non-blocking): It looks like we have an errNotFound err in internal/models/errors.go. I'm not sure if it's better to reuse that or create a new error type.

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