[GH-ISSUE #1500] Manually verifying magic link token throws unexpected invalid_type #26115

Closed
opened 2026-04-17 16:33:28 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @SanderPeeters on GitHub (Feb 19, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/1500

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create a backend with the expo() plugin
  2. Create an authClient with the magicLink and expoClient plugin in an Expo app
  3. Create a magic login link with a valid token
  4. Try to verify the token in the expo client app

Current vs. Expected behavior

When I try to verify my token using the method described here, I receive an unexpected type error.

My client setup

import * as SecureStore from 'expo-secure-store'
import { createAuthClient } from 'better-auth/react'
import { expoClient } from '@better-auth/expo/client'
import { magicLinkClient } from 'better-auth/client/plugins'

export const auth = createAuthClient({
    baseURL: process.env.EXPO_PUBLIC_API_URl,
    plugins: [magicLinkClient(), expoClient({ storage: SecureStore })],
})

Calling the verify method on root layout:

    useEffect(() => {
        const prepare = async () => {
            try {
                const response = await auth.magicLink.verify({
                    query: { token: 'super-secret' },
                })
                console.log(response)
            } catch (error) {
                console.log(error)
            }
        }

        if (token) {
            prepare()
        }
    }, [token])

Throws following error:

{"data": null, "error": {"code": "______CODE_INVALID_TYPE____EXPECTED_STRING____RECEIVED_UNDEFINED____PATH_______TOKEN________MESSAGE_REQUIRED__", "details": [[Object]], "message": "[
  {
    \"code\": \"invalid_type\",
    \"expected\": \"string\",
    \"received\": \"undefined\",
    \"path\": [
      \"token\"
    ],
    \"message\": \"Required\"
  }
]", "status": 400, "statusText": ""}}

The expected behaviour should return successfully the data.

What version of Better Auth are you using?

1.1.18

Provide environment information

- OS: Mac OSX 14.6.1
- iOS simulator: iPhone 16 Pro (iOS 18.2)
- Expo SDK: 52

Which area(s) are affected? (Select all that apply)

Types

Auth config (if applicable)

export const auth = betterAuth({
    database: drizzleAdapter(db, {
        provider: 'pg',
        schema: {
            user: schema.user,
            session: schema.session,
            account: schema.account,
            verification: schema.verification,
        },
    }),
    user: {
        additionalFields: {
            activeCompanyId: {
                required: false,
                type: 'string',
            },
            type: {
                type: 'string',
            },
            language: {
                type: 'string',
            },
            dateFormat: {
                type: 'string',
            },
            timeFormat: {
                type: 'string',
            },
        },
    },
    plugins: [
        expo(),
        magicLink({
            disableSignUp: true,
            sendMagicLink,
        }),
    ],
    trustedOrigins: ['*'], // ['exp://'],
})

Additional context

No response

Originally created by @SanderPeeters on GitHub (Feb 19, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/1500 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Create a backend with the expo() plugin 2. Create an authClient with the magicLink and expoClient plugin in an Expo app 3. Create a magic login link with a valid token 4. Try to verify the token in the expo client app ### Current vs. Expected behavior When I try to verify my token using the method described [here](https://www.better-auth.com/docs/plugins/magic-link#verify-magic-link), I receive an unexpected type error. My client setup ``` import * as SecureStore from 'expo-secure-store' import { createAuthClient } from 'better-auth/react' import { expoClient } from '@better-auth/expo/client' import { magicLinkClient } from 'better-auth/client/plugins' export const auth = createAuthClient({ baseURL: process.env.EXPO_PUBLIC_API_URl, plugins: [magicLinkClient(), expoClient({ storage: SecureStore })], }) ``` Calling the verify method on root layout: ``` useEffect(() => { const prepare = async () => { try { const response = await auth.magicLink.verify({ query: { token: 'super-secret' }, }) console.log(response) } catch (error) { console.log(error) } } if (token) { prepare() } }, [token]) ``` Throws following error: ``` {"data": null, "error": {"code": "______CODE_INVALID_TYPE____EXPECTED_STRING____RECEIVED_UNDEFINED____PATH_______TOKEN________MESSAGE_REQUIRED__", "details": [[Object]], "message": "[ { \"code\": \"invalid_type\", \"expected\": \"string\", \"received\": \"undefined\", \"path\": [ \"token\" ], \"message\": \"Required\" } ]", "status": 400, "statusText": ""}} ``` The expected behaviour should return successfully the data. ### What version of Better Auth are you using? 1.1.18 ### Provide environment information ```bash - OS: Mac OSX 14.6.1 - iOS simulator: iPhone 16 Pro (iOS 18.2) - Expo SDK: 52 ``` ### Which area(s) are affected? (Select all that apply) Types ### Auth config (if applicable) ```typescript export const auth = betterAuth({ database: drizzleAdapter(db, { provider: 'pg', schema: { user: schema.user, session: schema.session, account: schema.account, verification: schema.verification, }, }), user: { additionalFields: { activeCompanyId: { required: false, type: 'string', }, type: { type: 'string', }, language: { type: 'string', }, dateFormat: { type: 'string', }, timeFormat: { type: 'string', }, }, }, plugins: [ expo(), magicLink({ disableSignUp: true, sendMagicLink, }), ], trustedOrigins: ['*'], // ['exp://'], }) ``` ### Additional context _No response_
GiteaMirror added the lockedbug labels 2026-04-17 16:33:28 -05:00
Author
Owner

@Bekacru commented on GitHub (Feb 20, 2025):

token is required. Make sure it's defined.

<!-- gh-comment-id:2670605145 --> @Bekacru commented on GitHub (Feb 20, 2025): token is required. Make sure it's defined.
Author
Owner

@SanderPeeters commented on GitHub (Feb 20, 2025):

Yeah I know, but that's the problem: it is defined

<!-- gh-comment-id:2670676146 --> @SanderPeeters commented on GitHub (Feb 20, 2025): Yeah I know, but that's the problem: it is defined
Author
Owner

@SanderPeeters commented on GitHub (Feb 26, 2025):

For other people bumping in this issue, the current status in latest 1.2.0 beta is that the fetch wrapper and react native are acting weird and omitting all query params resulting in a VALIDATION_ERROR:
{"data": null, "error": {"code": "VALIDATION_ERROR", "message": "Invalid query parameters", "status": 400, "statusText": ""}}
@Bekacru is looking at it

<!-- gh-comment-id:2684430342 --> @SanderPeeters commented on GitHub (Feb 26, 2025): For other people bumping in this issue, the current status in latest 1.2.0 beta is that the fetch wrapper and react native are acting weird and omitting all query params resulting in a VALIDATION_ERROR: ```{"data": null, "error": {"code": "VALIDATION_ERROR", "message": "Invalid query parameters", "status": 400, "statusText": ""}}``` @Bekacru is looking at it
Author
Owner

@martis900 commented on GitHub (Mar 22, 2025):

Hey, I just ran into this exact same issue! I'm also using React Native/Expo and had the same problem with query parameters not being sent properly with the magic link verification endpoint.

After investigating, I found that the query parameters are getting lost somewhere in the request pipeline. They're correctly set in options.query but never make it to the final URL in the network request.

Here's a fix that worked for me - a custom fetch plugin that ensures the query parameters make it to the URL:

const magicLinkFixPlugin = {
  id: 'magic-link-query-fix',
  name: 'Magic Link Query Fix',
  hooks: {
    onRequest(context) {
      // Just look for magic-link/verify endpoints
      if (context.url.includes("/magic-link/verify") && 
          context.query && 
          Object.keys(context.query).length > 0) {
        try {
          // Manually add the query params to the URL
          const url = new URL(context.url);
          Object.entries(context.query).forEach(([key, value]) => {
            if (value !== undefined && value !== null) {
              url.searchParams.append(key, String(value));
            }
          });
          
          // Update the URL and wipe the query object
          context.url = url.toString();
          context.query = {};
        } catch (err) {
          console.error('Error fixing magic link URL:', err);
        }
      }
      return context;
    }
  }
};

Then just add it to your auth client setup:

import * as SecureStore from 'expo-secure-store';
import { createAuthClient } from 'better-auth/react';
import { expoClient } from '@better-auth/expo/client';
import { magicLinkClient } from 'better-auth/client/plugins';

export const authClient = createAuthClient({
  baseURL: process.env.EXPO_PUBLIC_API_URL,
  fetchOptions: {
    plugins: [magicLinkFixPlugin], // Add our fix here
  },
  plugins: [
    magicLinkClient(),
    expoClient({ storage: SecureStore })
  ],
});

The key insight is that we need to intercept the request at the last possible moment before it's sent to the network. This plugin grabs the query parameters that are stored in context.query and manually adds them to the URL.

This workaround got me unstuck, so hopefully it helps you too while waiting for an official fix!

<!-- gh-comment-id:2745911439 --> @martis900 commented on GitHub (Mar 22, 2025): Hey, I just ran into this exact same issue! I'm also using React Native/Expo and had the same problem with query parameters not being sent properly with the magic link verification endpoint. After investigating, I found that the query parameters are getting lost somewhere in the request pipeline. They're correctly set in `options.query` but never make it to the final URL in the network request. Here's a fix that worked for me - a custom fetch plugin that ensures the query parameters make it to the URL: ```javascript const magicLinkFixPlugin = { id: 'magic-link-query-fix', name: 'Magic Link Query Fix', hooks: { onRequest(context) { // Just look for magic-link/verify endpoints if (context.url.includes("/magic-link/verify") && context.query && Object.keys(context.query).length > 0) { try { // Manually add the query params to the URL const url = new URL(context.url); Object.entries(context.query).forEach(([key, value]) => { if (value !== undefined && value !== null) { url.searchParams.append(key, String(value)); } }); // Update the URL and wipe the query object context.url = url.toString(); context.query = {}; } catch (err) { console.error('Error fixing magic link URL:', err); } } return context; } } }; ``` Then just add it to your auth client setup: ```javascript import * as SecureStore from 'expo-secure-store'; import { createAuthClient } from 'better-auth/react'; import { expoClient } from '@better-auth/expo/client'; import { magicLinkClient } from 'better-auth/client/plugins'; export const authClient = createAuthClient({ baseURL: process.env.EXPO_PUBLIC_API_URL, fetchOptions: { plugins: [magicLinkFixPlugin], // Add our fix here }, plugins: [ magicLinkClient(), expoClient({ storage: SecureStore }) ], }); ``` The key insight is that we need to intercept the request at the last possible moment before it's sent to the network. This plugin grabs the query parameters that are stored in `context.query` and manually adds them to the URL. This workaround got me unstuck, so hopefully it helps you too while waiting for an official fix!
Author
Owner

@Bekacru commented on GitHub (Apr 12, 2025):

this should be fixed starting from 1.2.5 and onward

<!-- gh-comment-id:2799047328 --> @Bekacru commented on GitHub (Apr 12, 2025): this should be fixed starting from `1.2.5` and onward
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#26115