[GH-ISSUE #2911] Sign in with Passkey doesn't work #9393

Closed
opened 2026-04-13 04:50:09 -05:00 by GiteaMirror · 10 comments
Owner

Originally created by @kunalsinghdadhwal on GitHub (Jun 5, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/2911

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Just use the passkey plugin with the clietn

Current vs. Expected behavior

Current
when authClient.signIn.passkey() is invoked on the client side it only hits the generateAuthenticateOptions which returns a 200 status code and then nothing happens

Expected
Browser should ask for a key password on the device

What version of Better Auth are you using?

1.2.8

Provide environment information

- OS: [ wsl2 ]
- Browser: [firefox, chrome]

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

Package

Auth config (if applicable)

export const auth = betterAuth({
    appName: "KAuth",
    database: drizzleAdapter(db, {
        provider: "pg",
        schema: {
            ...schema
        }
    }),
    emailVerification: {
        sendOnSignUp: true,
        autoSignInAfterVerification: true,
        expiresIn: 60 * 60,
        // sendVerificationEmail:
    },
    emailAndPassword: {
        enabled: true,
        requireEmailVerification: true,
        // sendResetPassword:
        resetPasswordTokenExpiresIn: 60 * 60,
    },
    session: {
        expiresIn: 60 * 60 * 24 * 7,
        updateAge: 60 * 60 * 24,
        cookieCache: {
            enabled: true,
            maxAge: 5 * 60,
        },
    },
    socialProviders: {
        google: {
            prompt: "select_account",
            clientId: process.env.GOOGLE_CLIENT_ID as string,
            clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
        },
        github: {
            clientId: process.env.GITHUB_CLIENT_ID as string,
            clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
        }
    },
    user: {
        modelName: "users",
        deleteUser: {
            enabled: true,
            // sendDeleteAccountVerification
        }
    },
    account: {
        accountLinking: {
            enabled: true,
            trustedProviders: ["google", "github", "email-password"],
            allowDifferentEmails: true,
        }
    },
    verification: {
        disableCleanup: false
    },
    advanced: {
        ipAddress: {
            disableIpTracking: false
        },
        useSecureCookies: true,
        disableCSRFCheck: false,
        defaultCookieAttributes: {
            httpOnly: true,
            secure: true,
            sameSite: "lax",
        },
        cookiePrefix: "KAUTH_",
    },
    plugins: [
        openAPI(),
        nextCookies(),
        twoFactor(),
        haveIBeenPwned({
            customPasswordCompromisedMessage: "Password found in data breach, Use a more secure password"
        }),
        magicLink({
            sendMagicLink: async ({ email, token }) => { }
        }),
        captcha({
            provider: "cloudflare-turnstile",
            secretKey: process.env.TURNSTILE_SECRET_KEY as string,
        }),
        passkey({
            rpID: "localhost",
            rpName: "KAuth",
            origin: "http://localhost:3000",
            authenticatorSelection: {
                residentKey: "required",
                userVerification: "required"
            }
        })
    ]
} satisfies BetterAuthOptions);

export type Session = typeof auth.$Infer.Session;

Additional context

No response

Originally created by @kunalsinghdadhwal on GitHub (Jun 5, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/2911 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce Just use the passkey plugin with the clietn ### Current vs. Expected behavior **Current** when `authClient.signIn.passkey()` is invoked on the client side it only hits the generateAuthenticateOptions which returns a 200 status code and then nothing happens **Expected** Browser should ask for a key password on the device ### What version of Better Auth are you using? 1.2.8 ### Provide environment information ```bash - OS: [ wsl2 ] - Browser: [firefox, chrome] ``` ### Which area(s) are affected? (Select all that apply) Package ### Auth config (if applicable) ```typescript export const auth = betterAuth({ appName: "KAuth", database: drizzleAdapter(db, { provider: "pg", schema: { ...schema } }), emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, expiresIn: 60 * 60, // sendVerificationEmail: }, emailAndPassword: { enabled: true, requireEmailVerification: true, // sendResetPassword: resetPasswordTokenExpiresIn: 60 * 60, }, session: { expiresIn: 60 * 60 * 24 * 7, updateAge: 60 * 60 * 24, cookieCache: { enabled: true, maxAge: 5 * 60, }, }, socialProviders: { google: { prompt: "select_account", clientId: process.env.GOOGLE_CLIENT_ID as string, clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, }, github: { clientId: process.env.GITHUB_CLIENT_ID as string, clientSecret: process.env.GITHUB_CLIENT_SECRET as string, } }, user: { modelName: "users", deleteUser: { enabled: true, // sendDeleteAccountVerification } }, account: { accountLinking: { enabled: true, trustedProviders: ["google", "github", "email-password"], allowDifferentEmails: true, } }, verification: { disableCleanup: false }, advanced: { ipAddress: { disableIpTracking: false }, useSecureCookies: true, disableCSRFCheck: false, defaultCookieAttributes: { httpOnly: true, secure: true, sameSite: "lax", }, cookiePrefix: "KAUTH_", }, plugins: [ openAPI(), nextCookies(), twoFactor(), haveIBeenPwned({ customPasswordCompromisedMessage: "Password found in data breach, Use a more secure password" }), magicLink({ sendMagicLink: async ({ email, token }) => { } }), captcha({ provider: "cloudflare-turnstile", secretKey: process.env.TURNSTILE_SECRET_KEY as string, }), passkey({ rpID: "localhost", rpName: "KAuth", origin: "http://localhost:3000", authenticatorSelection: { residentKey: "required", userVerification: "required" } }) ] } satisfies BetterAuthOptions); export type Session = typeof auth.$Infer.Session; ``` ### Additional context _No response_
GiteaMirror added the locked label 2026-04-13 04:50:09 -05:00
Author
Owner

@Kinfe123 commented on GitHub (Jun 6, 2025):

how are you calling it like the frontend code ?

<!-- gh-comment-id:2948310372 --> @Kinfe123 commented on GitHub (Jun 6, 2025): how are you calling it like the frontend code ?
Author
Owner

@kunalsinghdadhwal commented on GitHub (Jun 6, 2025):

The method on client side

  const handlePasskey = async () => {
    await authClient.signIn.passkey();
  };

<Button
    type="button"
    variant="outline"
    className="w-full border-zinc-800 bg-zinc-900/50 text-white hover:bg-zinc-800 h-10"
    onClick={handlePasskey}
>
   <KeyRound className="mr-2 h-4 w-4" />
        Sign in with passkey
</Button>
<!-- gh-comment-id:2949135499 --> @kunalsinghdadhwal commented on GitHub (Jun 6, 2025): The method on client side ```typescript const handlePasskey = async () => { await authClient.signIn.passkey(); }; <Button type="button" variant="outline" className="w-full border-zinc-800 bg-zinc-900/50 text-white hover:bg-zinc-800 h-10" onClick={handlePasskey} > <KeyRound className="mr-2 h-4 w-4" /> Sign in with passkey </Button> ```
Author
Owner

@Kinfe123 commented on GitHub (Jun 6, 2025):

Are you on localhost or production? Also, can you please make sure that the navigator.credentials.get browser API is being called? Be sure to check the console for any related logs or errors as well

<!-- gh-comment-id:2950110240 --> @Kinfe123 commented on GitHub (Jun 6, 2025): Are you on localhost or production? Also, can you please make sure that the navigator.credentials.get browser API is being called? Be sure to check the console for any related logs or errors as well
Author
Owner

@Kinfe123 commented on GitHub (Jun 11, 2025):

Im closing this now. if you still this still an issue. feel free to tag me here.

<!-- gh-comment-id:2964005833 --> @Kinfe123 commented on GitHub (Jun 11, 2025): Im closing this now. if you still this still an issue. feel free to tag me here.
Author
Owner

@DarthGigi commented on GitHub (Aug 8, 2025):

@Kinfe123 I'm going to have to revive this issue as I'm having the same problem. After calling authClient.signIn.passkey() and a session gets generated in the database, nothing happens on the client-side. I have to manually add a hook in the fetchOptions and redirect the user like that, but I'm not even sure if this is safe?

minimal login-form.svelte:

<script lang="ts">
  import { goto } from "$app/navigation";
  import { authClient } from "$lib/auth-client";
  import { Button } from "$ui/button";
  import Key from "@lucide/svelte/icons/key";
  import { untrack } from "svelte";
  import { toast } from "svelte-sonner";

  let toastLoading = $state<number | string>();

  async function signInWithPasskey(autoFill = false) {
    await authClient.signIn.passkey({
      autoFill,
      fetchOptions: {
        onSuccess: (context) => {
          console.log("Successfully logged in with passkey", context);
          goto("/dashboard");
        },
        onError: (error) => {
          console.error("Failed to login with passkey", error);
        }
      }
    });
  }

  // Taken from https://www.better-auth.com/docs/plugins/passkey#preload-the-passkeys
  $effect(() => {
    if (!PublicKeyCredential.isConditionalMediationAvailable || !PublicKeyCredential.isConditionalMediationAvailable()) {
      return;
    }

    untrack(async () => {
      void (await signInWithPasskey(true));
    });
  });
</script>

<Button variant="outline" data-sveltekit-preload-data="tap" onclick={async () => await signInWithPasskey()}>
  <Key class="pointer-events-none h-6 w-auto transition-opacity duration-300 select-none group-hover:opacity-70" />
  Login with Passkey
</Button>
<!-- gh-comment-id:3169201835 --> @DarthGigi commented on GitHub (Aug 8, 2025): @Kinfe123 I'm going to have to revive this issue as I'm having the same problem. After calling `authClient.signIn.passkey()` and a session gets generated in the database, nothing happens on the client-side. I have to manually add a hook in the `fetchOptions` and redirect the user like that, but I'm not even sure if this is safe? minimal `login-form.svelte`: ```html <script lang="ts"> import { goto } from "$app/navigation"; import { authClient } from "$lib/auth-client"; import { Button } from "$ui/button"; import Key from "@lucide/svelte/icons/key"; import { untrack } from "svelte"; import { toast } from "svelte-sonner"; let toastLoading = $state<number | string>(); async function signInWithPasskey(autoFill = false) { await authClient.signIn.passkey({ autoFill, fetchOptions: { onSuccess: (context) => { console.log("Successfully logged in with passkey", context); goto("/dashboard"); }, onError: (error) => { console.error("Failed to login with passkey", error); } } }); } // Taken from https://www.better-auth.com/docs/plugins/passkey#preload-the-passkeys $effect(() => { if (!PublicKeyCredential.isConditionalMediationAvailable || !PublicKeyCredential.isConditionalMediationAvailable()) { return; } untrack(async () => { void (await signInWithPasskey(true)); }); }); </script> <Button variant="outline" data-sveltekit-preload-data="tap" onclick={async () => await signInWithPasskey()}> <Key class="pointer-events-none h-6 w-auto transition-opacity duration-300 select-none group-hover:opacity-70" /> Login with Passkey </Button>
Author
Owner

@kunalsinghdadhwal commented on GitHub (Aug 16, 2025):

@DarthGigi Did this work for you?

<!-- gh-comment-id:3193387007 --> @kunalsinghdadhwal commented on GitHub (Aug 16, 2025): @DarthGigi Did this work for you?
Author
Owner

@DarthGigi commented on GitHub (Aug 16, 2025):

@kunalsinghdadhwal Are you asking if my code snippet worked for me? If so, then yes it did, but I'm not sure if this is the intended way of doing this. Shouldn't there be a callbackUrl prop for the passkey signin?

<!-- gh-comment-id:3193674139 --> @DarthGigi commented on GitHub (Aug 16, 2025): @kunalsinghdadhwal Are you asking if my code snippet worked for me? If so, then yes it did, but I'm not sure if this is the intended way of doing this. Shouldn't there be a `callbackUrl` prop for the passkey signin?
Author
Owner

@kunalsinghdadhwal commented on GitHub (Aug 16, 2025):

Shouldn't there be a callbackUrl prop for the passkey signin?

Yes, you can pass a callbackUrl to the signin. Reference

<!-- gh-comment-id:3193675763 --> @kunalsinghdadhwal commented on GitHub (Aug 16, 2025): > Shouldn't there be a `callbackUrl` prop for the passkey signin? Yes, you can pass a callbackUrl to the signin. [Reference](https://www.better-auth.com/docs/plugins/passkey#sign-in-with-a-passkey)
Author
Owner

@DarthGigi commented on GitHub (Aug 16, 2025):

The docs are wrong, callbackUrl doesn't exist on the passkey method:

declare const passkeyClient: () => {
    id: "passkey";
    $InferServerPlugin: ReturnType<typeof passkey>;
    getActions: ($fetch: BetterFetch) => {
        signIn: {
            /**
             * Sign in with a registered passkey
             */
            passkey: (opts?: {
                autoFill?: boolean;
                email?: string;
                fetchOptions?: BetterFetchOption;
            },
          // ...
};
<!-- gh-comment-id:3193778308 --> @DarthGigi commented on GitHub (Aug 16, 2025): The docs are wrong, `callbackUrl` doesn't exist on the passkey method: ```ts declare const passkeyClient: () => { id: "passkey"; $InferServerPlugin: ReturnType<typeof passkey>; getActions: ($fetch: BetterFetch) => { signIn: { /** * Sign in with a registered passkey */ passkey: (opts?: { autoFill?: boolean; email?: string; fetchOptions?: BetterFetchOption; }, // ... }; ```
Author
Owner

@ElasticBottle commented on GitHub (Aug 24, 2025):

second what @DarthGigi is experiencing, the passkey props seems to be missing a few other props that is normally there, including the new user, and error callback URLs as well

<!-- gh-comment-id:3218027342 --> @ElasticBottle commented on GitHub (Aug 24, 2025): second what @DarthGigi is experiencing, the passkey props seems to be missing a few other props that is normally there, including the new user, and error callback URLs as well
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#9393