[GH-ISSUE #4266] Return response body on magic link verification #18508

Closed
opened 2026-04-15 17:00:27 -05:00 by GiteaMirror · 2 comments
Owner

Originally created by @j23schoen on GitHub (Aug 27, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/4266

Is this suited for github?

  • Yes, this is suited for github

Context

Our team started using better auth recently in our NextJs app. We initially used email/pass with success and are going to leverage magic links now for another feature. We're migrating off of auth0 and we also use express-session for session management.

Issue

We've got a cypress test suite where we want to programmatically log users in using only the api. This worked really easily with email/pass because the sign in api accepts an asResponse boolean prop. When true, the api returns everything you need to manage the session yourself, including the headers with the cookie.

The magic link verification api does not support this. Omitting the callbackUrl will return a response from the signInMagicLink api, but it's only got the session token in it. Setting the cookie to that token isn't enough for better auth to find the associated session. I found a hack, which I documented in the alternatives section below, but it's quite messy and relies on the api throwing an error.

Describe the solution you'd like

Ideally there is an asResponse prop that makes it clear you want a response body, this would include the headers. This approach would match the email/pass sign in api and makes it clear you want the full response so you can handle manual sign in yourself.

Or, return the full headers object when the callbackUrl is omitted. This approach would require updated docs to make it clear what happens and the response you get back.

Describe alternatives you've considered

Here's the hack I used. I found out that calling magicLinkVerify with a callbackUrl on the server will throw an error. The thrown error includes the headers. I catch the error and use the headers to set the cookie value.

const client = getClient()
    await client.api.signInMagicLink({
      body: {
        email: user.email, // required
        name: user.name,
        callbackURL: '/dashboard'
      },
      // This endpoint requires session cookies.
      headers: req.headers
    })

    try {
      await client.api.magicLinkVerify({
        query: {
          token: 'test-token',
          callbackURL: '/'
        },
        // This endpoint requires session cookies.
        headers: req.headers
      })
    } catch (error) {
      // Set the session cookie
      const cookies = error.headers.get('set-cookie')
      if (cookies) {
        res.setHeader('Set-Cookie', cookies)
      }
      res.status(200).json({ success: true })
    }

This is the response from the magicLinkVerify api when I passed a callbackUrl prop:

{
  "error": {
    "status": "FOUND",
    "body": null,
    "headers": {
      "set-cookie": "better-auth.session_token=DQhMGSIGBHnOcu5ej1EeZ1a4q7xRaGE0.%2F7qJ9EmQGZHge5jbX%2FVVIK8eLOBglhRxY6GHTddlhZ4%3D; Max-Age=604800; Path=/; HttpOnly; SameSite=Lax",
      "location": "http://localhost:3001/"
    },
    "statusCode": 302
  }
}

This is the response from the magicLinkVerify api when I omit the callbackUrl prop:

  {
    "response": {
      "token": "1t6BZd2aifnqLKcb8YrsCb3hEzpxtFBL",
      "user": {
        "id": "1",
        "email": "Andy.Langworth48@hotmail.com",
        "emailVerified": true,
        "name": "TP",
        "image": null,
        "createdAt": "2025-08-27T13:40:54.341Z",
        "updatedAt": "2025-08-27T13:40:54.342Z"
      }
    }
  }

Additional context

No response

Originally created by @j23schoen on GitHub (Aug 27, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/4266 ### Is this suited for github? - [x] Yes, this is suited for github ### Is your feature request related to a problem? Please describe. ### Context Our team started using better auth recently in our NextJs app. We initially used email/pass with success and are going to leverage magic links now for another feature. We're migrating off of auth0 and we also use express-session for session management. ## Issue We've got a cypress test suite where we want to programmatically log users in using only the api. This worked really easily with email/pass because the sign in api accepts an `asResponse` boolean prop. When true, the api returns everything you need to manage the session yourself, including the headers with the cookie. The magic link verification api does not support this. Omitting the `callbackUrl` will return a response from the `signInMagicLink` api, but it's only got the session token in it. Setting the cookie to that token isn't enough for better auth to find the associated session. I found a hack, which I documented in the alternatives section below, but it's quite messy and relies on the api throwing an error. ### Describe the solution you'd like Ideally there is an `asResponse` prop that makes it clear you want a response body, this would include the headers. This approach would match the email/pass sign in api and makes it clear you want the full response so you can handle manual sign in yourself. Or, return the full headers object when the `callbackUrl` is omitted. This approach would require updated docs to make it clear what happens and the response you get back. ### Describe alternatives you've considered Here's the hack I used. I found out that calling `magicLinkVerify` with a `callbackUrl` on the server will throw an error. The thrown error includes the headers. I catch the error and use the headers to set the cookie value. ```javascript const client = getClient() await client.api.signInMagicLink({ body: { email: user.email, // required name: user.name, callbackURL: '/dashboard' }, // This endpoint requires session cookies. headers: req.headers }) try { await client.api.magicLinkVerify({ query: { token: 'test-token', callbackURL: '/' }, // This endpoint requires session cookies. headers: req.headers }) } catch (error) { // Set the session cookie const cookies = error.headers.get('set-cookie') if (cookies) { res.setHeader('Set-Cookie', cookies) } res.status(200).json({ success: true }) } ``` This is the response from the `magicLinkVerify` api when I passed a `callbackUrl` prop: ``` { "error": { "status": "FOUND", "body": null, "headers": { "set-cookie": "better-auth.session_token=DQhMGSIGBHnOcu5ej1EeZ1a4q7xRaGE0.%2F7qJ9EmQGZHge5jbX%2FVVIK8eLOBglhRxY6GHTddlhZ4%3D; Max-Age=604800; Path=/; HttpOnly; SameSite=Lax", "location": "http://localhost:3001/" }, "statusCode": 302 } } ``` This is the response from the `magicLinkVerify` api when I omit the `callbackUrl` prop: ```json { "response": { "token": "1t6BZd2aifnqLKcb8YrsCb3hEzpxtFBL", "user": { "id": "1", "email": "Andy.Langworth48@hotmail.com", "emailVerified": true, "name": "TP", "image": null, "createdAt": "2025-08-27T13:40:54.341Z", "updatedAt": "2025-08-27T13:40:54.342Z" } } } ``` ### Additional context _No response_
GiteaMirror added the lockedenhancement labels 2026-04-15 17:00:27 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Aug 27, 2025):

Hi @j23schoen! I'm Dosu and I’m helping the better-auth team.

The magic link verification API in Better Auth does not currently support an asResponse or returnHeaders option to return the full response body including headers, unlike the email/pass sign-in API which does support this for manual session management and testing flows. The magic link verification endpoint either performs a redirect (302) with cookies set in the headers when a callbackUrl is provided, or returns a session token and user info in the JSON response when omitted—without headers or cookies for manual handling. This means your workaround of catching the thrown error to extract headers is the only way to programmatically obtain the session cookie right now [docs].

If you want a consistent experience, adding an asResponse option to the magic link verification handler would be a targeted change and could mirror the logic used in the email/pass sign-in API [api concepts]. There is also a pending feature to generate signed session cookies directly via a returnCookies: true option, but it is not yet available [issue].

For now, your documented workaround is the best approach until the API supports returning the full response or headers directly.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

<!-- gh-comment-id:3228361832 --> @dosubot[bot] commented on GitHub (Aug 27, 2025): <!-- Greeting --> Hi @j23schoen! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The magic link verification API in Better Auth does not currently support an `asResponse` or `returnHeaders` option to return the full response body including headers, unlike the email/pass sign-in API which does support this for manual session management and testing flows. The magic link verification endpoint either performs a redirect (302) with cookies set in the headers when a `callbackUrl` is provided, or returns a session token and user info in the JSON response when omitted—without headers or cookies for manual handling. This means your workaround of catching the thrown error to extract headers is the only way to programmatically obtain the session cookie right now [[docs](https://better-auth.com/docs/plugins/magic-link)]. If you want a consistent experience, adding an `asResponse` option to the magic link verification handler would be a targeted change and could mirror the logic used in the email/pass sign-in API [[api concepts](https://better-auth.com/docs/concepts/api)]. There is also a pending feature to generate signed session cookies directly via a `returnCookies: true` option, but it is not yet available [[issue](https://github.com/better-auth/better-auth/issues/3851)]. For now, your documented workaround is the best approach until the API supports returning the full response or headers directly. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/d19f5771-95d6-4661-9a00-85cc8fd5cb70?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/d19f5771-95d6-4661-9a00-85cc8fd5cb70?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/d19f5771-95d6-4661-9a00-85cc8fd5cb70?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/d19f5771-95d6-4661-9a00-85cc8fd5cb70?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/d19f5771-95d6-4661-9a00-85cc8fd5cb70?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/d19f5771-95d6-4661-9a00-85cc8fd5cb70?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/d19f5771-95d6-4661-9a00-85cc8fd5cb70?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4266)
Author
Owner

@j23schoen commented on GitHub (Aug 27, 2025):

closing this issue. I misunderstood the api exposed by better auth. asResponse is in fact part of the magic link apis.

Maybe worth another issue, though, is that the magic link plugin's types are not working at all. you don't get any type hints and typescript thinks the apis don't exist at all. That led to my confusion about the missing asResponse prop as it's also not mentioned explicitly on the magic link plugin page in the docs.

<!-- gh-comment-id:3228533227 --> @j23schoen commented on GitHub (Aug 27, 2025): closing this issue. I misunderstood the api exposed by better auth. `asResponse` is in fact part of the magic link apis. Maybe worth another issue, though, is that the magic link plugin's types are not working at all. you don't get any type hints and typescript thinks the apis don't exist at all. That led to my confusion about the missing `asResponse` prop as it's also not mentioned explicitly on the magic link plugin page in the docs.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#18508