October 1, 2026
When OAuth State Became a Magic Link in Better Auth
The September 30 patch addresses two login records that could be mistaken for each other in shared storage.
By Mehmet Sinan TATAR
4 min read
A login screen might offer "Continue with Google" alongside "Sign in by email." For the user, both lead to their account. The server accepts different evidence along each path. For an email link to authorize a login, it has to belong to the email login flow in the first place.
That distinction broke down in the issue described by Better Auth's September 30, 2026 advisory. Under certain configurations, a state value created for OAuth was accepted as a Magic Link token. This made it possible to sign in as a user without accessing their mailbox. The fix shipped in version 1.7.7.
The records sharing a store
A Magic Link is straightforward: someone follows a special link in their email and signs in without entering a password. The token โ the temporary value used for that login โ refers to a server-side record. The flow relies on it having reached the user through email. Better Auth's Magic Link documentation describes this path.
OAuth state connects a request sent to an external sign-in provider with the callback. When someone returns from a provider such as Google, the application can associate that return with the login they started. That is its role in the OAuth specification. It does not, on its own, show that the person can access an email address.
It makes sense for these features to share storage: both create records that only need to live briefly. But if the Magic Link verifier cannot tell that a record was created for OAuth, it can accept that record as evidence for an email login. In this issue, finding the record was enough to accept a claim that the originating flow had never established.
The red path below shows the OAuth record being accepted on the Magic Link side. The lower strip shows how version 1.7.7 separates the records in storage.
Figure 1 โ Simplified from the advisory and official patch. The red path represents affected older configurations; other checks in a normal OAuth flow are omitted.
Which configurations are affected?
Not every deployment in the >=1.4.0-beta.18, <1.7.7 range has the same exposure. The advisory covers deployments where these settings come together:
- Magic Link and a social sign-in or Generic OAuth provider are enabled.
- OAuth
stateuses thedatabasestorage option. - Magic Link uses its default
storeToken: "plain"setting, orstoreToken: "hashed"is combined with globalverification.storeIdentifier: "hashed".
With custom hash functions, exposure depends on the function being used. Hashing here is the transformation applied before storing a value. A "hashed" option describes storage; it does not decide which login flow may use the record. The combination of Magic Link and global verification settings matters because the code writing a record and the code granting a login have to agree on what that record means.
How 1.7.7 checks the record's purpose
The new version separates Magic Link records with magic-link: and database-backed OAuth/SAML state records with auth-state:. Those prefixes go into the server-side record identifier; the token sent to the user keeps its existing form. Custom storage or hashing code also has to preserve that separation. Verification options
The verifier then looks at the contents. It checks the expected type and fields, rejecting a record with fields outside the accepted structure even if its type looks right. Records rejected at this first check are not consumed. A pending login does not lose its record just because that record reached the wrong verifier. Magic Link implementation
Once a record passes, it is consumed atomically: retrieval and removal from use happen in one operation, without a gap for another request to intervene. The returned record is validated again. The session decision uses the record actually consumed, rather than a copy from the first read.
The project's regression tests โ tests intended to catch the bug returning โ cover both purpose confusion and the check after consumption. I'm referring to the test code here; I did not run the tests for this article. The next diagram summarizes the order in the patch.
Figure 2 โ Simplified order of the purpose checks in the 1.7.7 patch. Expiration and other account-related checks are omitted.
Before moving state into a cookie
After seeing a problem with database-backed state, moving it into a browser cookie might seem like a straightforward option. A separate OAuth Proxy advisory, published the same day, adds a qualification. It covers >=1.5.0-beta.12, <1.7.7 when cookie-backed state, an omitted proxy secret or one matching the global Better Auth secret, and implicit account linking are combined.
Implicit account linking automatically associates accounts with matching emails from eligible providers. So a change to state storage also involves the proxy's secret configuration and its account-linking behavior.
Version 1.7.7 also derives separate keys for the different purposes of encrypted OAuth state and proxy data. It uses HKDF to derive keys assigned to particular uses from existing secret material. This prevents data encrypted for one purpose from being decrypted and accepted for another. Key derivation implementation
Deployments sharing verification storage need a coordinated upgrade. Pending logins have to restart, and users need new Magic Links. OAuth Proxy participants belong in the same cutover. Rejecting old links is expected during this transition; migrating the database schema or existing user accounts is not required. 1.7.7 release notes, proxy upgrade documentation
Where this comes up in our own applications
A successful Magic Link test shows that the correct link lets someone sign in. To check the separation exposed by this bug, it also matters where records sharing that store are created, how long they live, and which code uses them.
In an authorized test environment, the expected outcome has three parts: a record created for another purpose is rejected, its own pending login can continue, and a valid Magic Link still works in its intended flow. That covers side effects such as deleting the wrong record as well as rejection. This is a test suggestion drawn from the patch.
Tokens still need to be random, short-lived, and single-use. The OWASP password recovery guidance recommends those controls for the tokens it covers. What stands out to me in the Better Auth example is the decision immediately after token found: what does that code now accept as proven about the user? That is where a record belonging to an OAuth request was mistaken for evidence of email access.
Sources
- Better Auth security advisory: scope and impact.
- 1.7.7 release notes and official patch: remediation and cutover.
- Magic Link, verification options, and OAuth Proxy: behavior and configuration.
- OAuth 2.0, section 4.1.1 and OWASP password recovery guidance: protocol role and token lifecycle.