[GH-ISSUE #4228] authClient.signIn.oneTimeToken #27194

Closed
opened 2026-04-17 18:03:22 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @rinarakaki on GitHub (Aug 26, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/4228

Is this suited for github?

  • Yes, this is suited for github

Just like Magic link and Email OTP but with other communication channels than email, such as messengers.

Usage

  1. Native App calls API to send URL with one-time token in query string
  2. Server sends message with the custom URL
  3. User clicks the URL and opens browser
  4. Browser attempts to sign in with the given one-time token

Describe the solution you'd like

authClient.signIn.oneTimeToken or kind

Describe alternatives you've considered

Manually set session on client side using information returned from custom 'sign-in with one-time token' endpoint.

Additional context

No response

Originally created by @rinarakaki on GitHub (Aug 26, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/4228 ### Is this suited for github? - [x] Yes, this is suited for github ### Is your feature request related to a problem? Please describe. Just like Magic link and Email OTP but with other communication channels than email, such as messengers. # Usage 1. Native App calls API to send URL with one-time token in query string 2. Server sends message with the custom URL 3. User clicks the URL and opens browser 4. Browser attempts to sign in with the given one-time token ### Describe the solution you'd like authClient.signIn.oneTimeToken or kind ### Describe alternatives you've considered Manually set session on client side using information returned from custom 'sign-in with one-time token' endpoint. ### Additional context _No response_
GiteaMirror added the lockedenhancement labels 2026-04-17 18:03:22 -05:00
Author
Owner

@Kinfe123 commented on GitHub (Sep 2, 2025):

one-time token right now is for authenticated user with session already on some domain to be used across other domains usecase like how are you plnaning to call it like with email or without it or sth ?

<!-- gh-comment-id:3245464632 --> @Kinfe123 commented on GitHub (Sep 2, 2025): one-time token right now is for authenticated user with session already on some domain to be used across other domains usecase like how are you plnaning to call it like with email or without it or sth ?
Author
Owner

@rinarakaki commented on GitHub (Sep 4, 2025):

I've already found the solution using custom plugin as in https://github.com/better-auth/better-auth/issues/4250. Thanks for your help!

<!-- gh-comment-id:3253261115 --> @rinarakaki commented on GitHub (Sep 4, 2025): I've already found the solution using custom plugin as in https://github.com/better-auth/better-auth/issues/4250. Thanks for your help!
Author
Owner

@Fruup commented on GitHub (Oct 13, 2025):

I would love to have a sign in method using a one-time-token. I don't really know what to do with only the session object 🤔

As a workaround, I wrote this plugin that sets a session_token cookie on a successful verify response. Note the sameSite: 'none' that is necessary for cross-domain cookies.

function attachSessionCookieToOneTimeTokenVerifyResponse(): BetterAuthPlugin {
  return {
    id: 'attach-session-cookie-to-one-time-token-verify-response',
    async onRequest(request, ctx) {
      // @ts-expect-error
      ctx._requestUrl = new URL(request.url);
      // @ts-expect-error
      ctx._requestOriginHeader = request.headers.get('Origin');
    },
    async onResponse(response, ctx) {
      // @ts-expect-error
      const requestUrl: URL = ctx._requestUrl;

      // Only run this logic for the one-time-token verify endpoint.
      if (requestUrl.pathname !== '/api/auth/one-time-token/verify/') {
        return;
      }

      // Verify the domain is trusted.
      // @ts-expect-error
      const originHeader: string = ctx._requestOriginHeader;
      console.log({ originHeader });
      if (!originHeader) {
        return { response: toResponse(new APIError('FORBIDDEN')) };
      }

      const originToCheck = new URL(originHeader).origin;

      if (
        !ctx.trustedOrigins.some((pattern) => {
          if (!pattern.includes('*')) {
            return pattern === originToCheck;
          }

          return new RegExp(
            '^' +
              pattern
                .replace(/\./g, '\\.')
                .replace(/\*/g, '.*')
                .replace(/\//g, '\\/') +
              '$',
          ).test(originToCheck);
        })
      ) {
        return { response: toResponse(new APIError('FORBIDDEN')) };
      }

      // Try to extract the session token from the response body.
      const token = await (async () => {
        const data = await response.clone().json();

        try {
          const token = data.session.token;
          if (token && typeof token === 'string') return token;
        } catch {}

        return null;
      })();

      if (token) {
        // If the token is present, serialize it into a cookie and attach it.
        // We have to sign it just like better-auth does internally (this should be exposed imo).
        const cookie = await serializeSignedCookie(
          ctx.authCookies.sessionToken.name,
          token,
          ctx.secret,
          {
            sameSite: 'none', // Important for cross-site cookies!
            secure: true, // Necessary if SameSite=None
          },
        );

        response.headers.append(`Set-Cookie`, cookie);
      }

      return;
    },
  };
}

Edit

I found out what to do with the plain session. The bearer plugin can be used to accept authentication with the header Authorization: Bearer <token>. This method does not need a signed token - the plain one from the successful oneTimeToken-verify request suffices.

<!-- gh-comment-id:3398752634 --> @Fruup commented on GitHub (Oct 13, 2025): I would love to have a sign in method using a one-time-token. I don't really know what to do with only the session object 🤔 As a workaround, I wrote this plugin that sets a `session_token` cookie on a successful `verify` response. Note the `sameSite: 'none'` that is necessary for cross-domain cookies. ```ts function attachSessionCookieToOneTimeTokenVerifyResponse(): BetterAuthPlugin { return { id: 'attach-session-cookie-to-one-time-token-verify-response', async onRequest(request, ctx) { // @ts-expect-error ctx._requestUrl = new URL(request.url); // @ts-expect-error ctx._requestOriginHeader = request.headers.get('Origin'); }, async onResponse(response, ctx) { // @ts-expect-error const requestUrl: URL = ctx._requestUrl; // Only run this logic for the one-time-token verify endpoint. if (requestUrl.pathname !== '/api/auth/one-time-token/verify/') { return; } // Verify the domain is trusted. // @ts-expect-error const originHeader: string = ctx._requestOriginHeader; console.log({ originHeader }); if (!originHeader) { return { response: toResponse(new APIError('FORBIDDEN')) }; } const originToCheck = new URL(originHeader).origin; if ( !ctx.trustedOrigins.some((pattern) => { if (!pattern.includes('*')) { return pattern === originToCheck; } return new RegExp( '^' + pattern .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\//g, '\\/') + '$', ).test(originToCheck); }) ) { return { response: toResponse(new APIError('FORBIDDEN')) }; } // Try to extract the session token from the response body. const token = await (async () => { const data = await response.clone().json(); try { const token = data.session.token; if (token && typeof token === 'string') return token; } catch {} return null; })(); if (token) { // If the token is present, serialize it into a cookie and attach it. // We have to sign it just like better-auth does internally (this should be exposed imo). const cookie = await serializeSignedCookie( ctx.authCookies.sessionToken.name, token, ctx.secret, { sameSite: 'none', // Important for cross-site cookies! secure: true, // Necessary if SameSite=None }, ); response.headers.append(`Set-Cookie`, cookie); } return; }, }; } ``` --- ## Edit I found out what to do with the plain session. The `bearer` plugin can be used to accept authentication with the header `Authorization: Bearer <token>`. This method does not need a signed token - the plain one from the successful oneTimeToken-verify request suffices.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#27194