This is page 38 of 67. Use http://codebase.md/better-auth/better-auth?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitattributes ├── .github │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── renovate.json5 │ └── workflows │ ├── ci.yml │ ├── e2e.yml │ ├── preview.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .vscode │ └── settings.json ├── banner-dark.png ├── banner.png ├── biome.json ├── bump.config.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── demo │ ├── expo-example │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── app.config.ts │ │ ├── assets │ │ │ ├── bg-image.jpeg │ │ │ ├── fonts │ │ │ │ └── SpaceMono-Regular.ttf │ │ │ ├── icon.png │ │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── logo.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── [email protected] │ │ │ ├── [email protected] │ │ │ └── splash.png │ │ ├── babel.config.js │ │ ├── components.json │ │ ├── expo-env.d.ts │ │ ├── index.ts │ │ ├── metro.config.js │ │ ├── nativewind-env.d.ts │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── app │ │ │ │ ├── _layout.tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...route]+api.ts │ │ │ │ ├── dashboard.tsx │ │ │ │ ├── forget-password.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── sign-up.tsx │ │ │ ├── components │ │ │ │ ├── icons │ │ │ │ │ └── google.tsx │ │ │ │ └── ui │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── separator.tsx │ │ │ │ └── text.tsx │ │ │ ├── global.css │ │ │ └── lib │ │ │ ├── auth-client.ts │ │ │ ├── auth.ts │ │ │ ├── icons │ │ │ │ ├── iconWithClassName.ts │ │ │ │ └── X.tsx │ │ │ └── utils.ts │ │ ├── tailwind.config.js │ │ └── tsconfig.json │ └── nextjs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── (auth) │ │ │ ├── forget-password │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ ├── sign-in │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── two-factor │ │ │ ├── otp │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── accept-invitation │ │ │ └── [id] │ │ │ ├── invitation-error.tsx │ │ │ └── page.tsx │ │ ├── admin │ │ │ └── page.tsx │ │ ├── api │ │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ │ ├── apps │ │ │ └── register │ │ │ └── page.tsx │ │ ├── client-test │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── change-plan.tsx │ │ │ ├── client.tsx │ │ │ ├── organization-card.tsx │ │ │ ├── page.tsx │ │ │ ├── upgrade-button.tsx │ │ │ └── user-card.tsx │ │ ├── device │ │ │ ├── approve │ │ │ │ └── page.tsx │ │ │ ├── denied │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── success │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── features.tsx │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── oauth │ │ │ └── authorize │ │ │ ├── concet-buttons.tsx │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── pricing │ │ └── page.tsx │ ├── components │ │ ├── account-switch.tsx │ │ ├── blocks │ │ │ └── pricing.tsx │ │ ├── logo.tsx │ │ ├── one-tap.tsx │ │ ├── sign-in-btn.tsx │ │ ├── sign-in.tsx │ │ ├── sign-up.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggle.tsx │ │ ├── tier-labels.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── chart.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── menubar.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── pagination.tsx │ │ │ ├── password-input.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tabs2.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ └── wrapper.tsx │ ├── components.json │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── auth-client.ts │ │ ├── auth-types.ts │ │ ├── auth.ts │ │ ├── email │ │ │ ├── invitation.tsx │ │ │ ├── resend.ts │ │ │ └── reset-password.tsx │ │ ├── metadata.ts │ │ ├── shared.ts │ │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── proxy.ts │ ├── public │ │ ├── __og.png │ │ ├── _og.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── logo.svg │ │ └── og.png │ ├── README.md │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── turbo.json ├── docker-compose.yml ├── docs │ ├── .env.example │ ├── .gitignore │ ├── app │ │ ├── api │ │ │ ├── ai-chat │ │ │ │ └── route.ts │ │ │ ├── analytics │ │ │ │ ├── conversation │ │ │ │ │ └── route.ts │ │ │ │ ├── event │ │ │ │ │ └── route.ts │ │ │ │ └── feedback │ │ │ │ └── route.ts │ │ │ ├── chat │ │ │ │ └── route.ts │ │ │ ├── og │ │ │ │ └── route.tsx │ │ │ ├── og-release │ │ │ │ └── route.tsx │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ └── support │ │ │ └── route.ts │ │ ├── blog │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── blog-list.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── icons.tsx │ │ │ │ ├── stat-field.tsx │ │ │ │ └── support.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── changelogs │ │ │ ├── _components │ │ │ │ ├── _layout.tsx │ │ │ │ ├── changelog-layout.tsx │ │ │ │ ├── default-changelog.tsx │ │ │ │ ├── fmt-dates.tsx │ │ │ │ ├── grid-pattern.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── stat-field.tsx │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── community │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── stats.tsx │ │ │ └── page.tsx │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ ├── page.client.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── lib │ │ │ └── get-llm-text.ts │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── llms.txt │ │ │ ├── [...slug] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ ├── reference │ │ │ └── route.ts │ │ ├── sitemap.xml │ │ ├── static.json │ │ │ └── route.ts │ │ └── v1 │ │ ├── _components │ │ │ └── v1-text.tsx │ │ ├── bg-line.tsx │ │ └── page.tsx │ ├── assets │ │ ├── Geist.ttf │ │ └── GeistMono.ttf │ ├── components │ │ ├── ai-chat-modal.tsx │ │ ├── anchor-scroll-fix.tsx │ │ ├── api-method-tabs.tsx │ │ ├── api-method.tsx │ │ ├── banner.tsx │ │ ├── blocks │ │ │ └── features.tsx │ │ ├── builder │ │ │ ├── beam.tsx │ │ │ ├── code-tabs │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── code-tabs.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-bar.tsx │ │ │ │ └── theme.ts │ │ │ ├── index.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── sign-up.tsx │ │ │ ├── social-provider.tsx │ │ │ ├── store.ts │ │ │ └── tabs.tsx │ │ ├── display-techstack.tsx │ │ ├── divider-text.tsx │ │ ├── docs │ │ │ ├── docs.client.tsx │ │ │ ├── docs.tsx │ │ │ ├── layout │ │ │ │ ├── nav.tsx │ │ │ │ ├── theme-toggle.tsx │ │ │ │ ├── toc-thumb.tsx │ │ │ │ └── toc.tsx │ │ │ ├── page.client.tsx │ │ │ ├── page.tsx │ │ │ ├── shared.tsx │ │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── popover.tsx │ │ │ └── scroll-area.tsx │ │ ├── endpoint.tsx │ │ ├── features.tsx │ │ ├── floating-ai-search.tsx │ │ ├── fork-button.tsx │ │ ├── generate-apple-jwt.tsx │ │ ├── generate-secret.tsx │ │ ├── github-stat.tsx │ │ ├── icons.tsx │ │ ├── landing │ │ │ ├── gradient-bg.tsx │ │ │ ├── grid-pattern.tsx │ │ │ ├── hero.tsx │ │ │ ├── section-svg.tsx │ │ │ ├── section.tsx │ │ │ ├── spotlight.tsx │ │ │ └── testimonials.tsx │ │ ├── logo-context-menu.tsx │ │ ├── logo.tsx │ │ ├── markdown-renderer.tsx │ │ ├── markdown.tsx │ │ ├── mdx │ │ │ ├── add-to-cursor.tsx │ │ │ └── database-tables.tsx │ │ ├── message-feedback.tsx │ │ ├── mobile-search-icon.tsx │ │ ├── nav-bar.tsx │ │ ├── nav-link.tsx │ │ ├── nav-mobile.tsx │ │ ├── promo-card.tsx │ │ ├── resource-card.tsx │ │ ├── resource-grid.tsx │ │ ├── resource-section.tsx │ │ ├── ripple.tsx │ │ ├── search-dialog.tsx │ │ ├── side-bar.tsx │ │ ├── sidebar-content.tsx │ │ ├── techstack-icons.tsx │ │ ├── theme-provider.tsx │ │ ├── theme-toggler.tsx │ │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aside-link.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── background-beams.tsx │ │ ├── background-boxes.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── callout.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── code-block.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── dynamic-code-block.tsx │ │ ├── fade-in.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── sparkles.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip-docs.tsx │ │ ├── tooltip.tsx │ │ └── use-copy-button.tsx │ ├── components.json │ ├── content │ │ ├── blogs │ │ │ ├── 0-supabase-auth-to-planetscale-migration.mdx │ │ │ ├── 1-3.mdx │ │ │ ├── authjs-joins-better-auth.mdx │ │ │ └── seed-round.mdx │ │ ├── changelogs │ │ │ ├── 1-2.mdx │ │ │ └── 1.0.mdx │ │ └── docs │ │ ├── adapters │ │ │ ├── community-adapters.mdx │ │ │ ├── drizzle.mdx │ │ │ ├── mongo.mdx │ │ │ ├── mssql.mdx │ │ │ ├── mysql.mdx │ │ │ ├── other-relational-databases.mdx │ │ │ ├── postgresql.mdx │ │ │ ├── prisma.mdx │ │ │ └── sqlite.mdx │ │ ├── authentication │ │ │ ├── apple.mdx │ │ │ ├── atlassian.mdx │ │ │ ├── cognito.mdx │ │ │ ├── discord.mdx │ │ │ ├── dropbox.mdx │ │ │ ├── email-password.mdx │ │ │ ├── facebook.mdx │ │ │ ├── figma.mdx │ │ │ ├── github.mdx │ │ │ ├── gitlab.mdx │ │ │ ├── google.mdx │ │ │ ├── huggingface.mdx │ │ │ ├── kakao.mdx │ │ │ ├── kick.mdx │ │ │ ├── line.mdx │ │ │ ├── linear.mdx │ │ │ ├── linkedin.mdx │ │ │ ├── microsoft.mdx │ │ │ ├── naver.mdx │ │ │ ├── notion.mdx │ │ │ ├── other-social-providers.mdx │ │ │ ├── paypal.mdx │ │ │ ├── reddit.mdx │ │ │ ├── roblox.mdx │ │ │ ├── salesforce.mdx │ │ │ ├── slack.mdx │ │ │ ├── spotify.mdx │ │ │ ├── tiktok.mdx │ │ │ ├── twitch.mdx │ │ │ ├── twitter.mdx │ │ │ ├── vk.mdx │ │ │ └── zoom.mdx │ │ ├── basic-usage.mdx │ │ ├── comparison.mdx │ │ ├── concepts │ │ │ ├── api.mdx │ │ │ ├── cli.mdx │ │ │ ├── client.mdx │ │ │ ├── cookies.mdx │ │ │ ├── database.mdx │ │ │ ├── email.mdx │ │ │ ├── hooks.mdx │ │ │ ├── oauth.mdx │ │ │ ├── plugins.mdx │ │ │ ├── rate-limit.mdx │ │ │ ├── session-management.mdx │ │ │ ├── typescript.mdx │ │ │ └── users-accounts.mdx │ │ ├── examples │ │ │ ├── astro.mdx │ │ │ ├── next-js.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ └── svelte-kit.mdx │ │ ├── guides │ │ │ ├── auth0-migration-guide.mdx │ │ │ ├── browser-extension-guide.mdx │ │ │ ├── clerk-migration-guide.mdx │ │ │ ├── create-a-db-adapter.mdx │ │ │ ├── next-auth-migration-guide.mdx │ │ │ ├── optimizing-for-performance.mdx │ │ │ ├── saml-sso-with-okta.mdx │ │ │ ├── supabase-migration-guide.mdx │ │ │ └── your-first-plugin.mdx │ │ ├── installation.mdx │ │ ├── integrations │ │ │ ├── astro.mdx │ │ │ ├── convex.mdx │ │ │ ├── elysia.mdx │ │ │ ├── expo.mdx │ │ │ ├── express.mdx │ │ │ ├── fastify.mdx │ │ │ ├── hono.mdx │ │ │ ├── lynx.mdx │ │ │ ├── nestjs.mdx │ │ │ ├── next.mdx │ │ │ ├── nitro.mdx │ │ │ ├── nuxt.mdx │ │ │ ├── remix.mdx │ │ │ ├── solid-start.mdx │ │ │ ├── svelte-kit.mdx │ │ │ ├── tanstack.mdx │ │ │ └── waku.mdx │ │ ├── introduction.mdx │ │ ├── meta.json │ │ ├── plugins │ │ │ ├── 2fa.mdx │ │ │ ├── admin.mdx │ │ │ ├── anonymous.mdx │ │ │ ├── api-key.mdx │ │ │ ├── autumn.mdx │ │ │ ├── bearer.mdx │ │ │ ├── captcha.mdx │ │ │ ├── community-plugins.mdx │ │ │ ├── device-authorization.mdx │ │ │ ├── dodopayments.mdx │ │ │ ├── dub.mdx │ │ │ ├── email-otp.mdx │ │ │ ├── generic-oauth.mdx │ │ │ ├── have-i-been-pwned.mdx │ │ │ ├── jwt.mdx │ │ │ ├── last-login-method.mdx │ │ │ ├── magic-link.mdx │ │ │ ├── mcp.mdx │ │ │ ├── multi-session.mdx │ │ │ ├── oauth-proxy.mdx │ │ │ ├── oidc-provider.mdx │ │ │ ├── one-tap.mdx │ │ │ ├── one-time-token.mdx │ │ │ ├── open-api.mdx │ │ │ ├── organization.mdx │ │ │ ├── passkey.mdx │ │ │ ├── phone-number.mdx │ │ │ ├── polar.mdx │ │ │ ├── siwe.mdx │ │ │ ├── sso.mdx │ │ │ ├── stripe.mdx │ │ │ └── username.mdx │ │ └── reference │ │ ├── contributing.mdx │ │ ├── faq.mdx │ │ ├── options.mdx │ │ ├── resources.mdx │ │ ├── security.mdx │ │ └── telemetry.mdx │ ├── hooks │ │ └── use-mobile.ts │ ├── ignore-build.sh │ ├── lib │ │ ├── blog.ts │ │ ├── chat │ │ │ └── inkeep-qa-schema.ts │ │ ├── constants.ts │ │ ├── export-search-indexes.ts │ │ ├── inkeep-analytics.ts │ │ ├── is-active.ts │ │ ├── metadata.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── proxy.ts │ ├── public │ │ ├── avatars │ │ │ └── beka.jpg │ │ ├── blogs │ │ │ ├── authjs-joins.png │ │ │ ├── seed-round.png │ │ │ └── supabase-ps.png │ │ ├── branding │ │ │ ├── better-auth-brand-assets.zip │ │ │ ├── better-auth-logo-dark.png │ │ │ ├── better-auth-logo-dark.svg │ │ │ ├── better-auth-logo-light.png │ │ │ ├── better-auth-logo-light.svg │ │ │ ├── better-auth-logo-wordmark-dark.png │ │ │ ├── better-auth-logo-wordmark-dark.svg │ │ │ ├── better-auth-logo-wordmark-light.png │ │ │ └── better-auth-logo-wordmark-light.svg │ │ ├── extension-id.png │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── light │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ └── site.webmanifest │ │ │ └── site.webmanifest │ │ ├── images │ │ │ └── blogs │ │ │ └── better auth (1).png │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── LogoDark.webp │ │ ├── LogoLight.webp │ │ ├── og.png │ │ ├── open-api-reference.png │ │ ├── people-say │ │ │ ├── code-with-antonio.jpg │ │ │ ├── dagmawi-babi.png │ │ │ ├── dax.png │ │ │ ├── dev-ed.png │ │ │ ├── egoist.png │ │ │ ├── guillermo-rauch.png │ │ │ ├── jonathan-wilke.png │ │ │ ├── josh-tried-coding.jpg │ │ │ ├── kitze.jpg │ │ │ ├── lazar-nikolov.png │ │ │ ├── nizzy.png │ │ │ ├── omar-mcadam.png │ │ │ ├── ryan-vogel.jpg │ │ │ ├── saltyatom.jpg │ │ │ ├── sebastien-chopin.png │ │ │ ├── shreyas-mididoddi.png │ │ │ ├── tech-nerd.png │ │ │ ├── theo.png │ │ │ ├── vybhav-bhargav.png │ │ │ └── xavier-pladevall.jpg │ │ ├── plus.svg │ │ ├── release-og │ │ │ ├── 1-2.png │ │ │ ├── 1-3.png │ │ │ └── changelog-og.png │ │ └── v1-og.png │ ├── README.md │ ├── scripts │ │ ├── endpoint-to-doc │ │ │ ├── index.ts │ │ │ ├── input.ts │ │ │ ├── output.mdx │ │ │ └── readme.md │ │ └── sync-orama.ts │ ├── source.config.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── turbo.json ├── e2e │ ├── integration │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── solid-vinxi │ │ │ ├── .gitignore │ │ │ ├── app.config.ts │ │ │ ├── e2e │ │ │ │ ├── test.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── favicon.ico │ │ │ ├── src │ │ │ │ ├── app.tsx │ │ │ │ ├── entry-client.tsx │ │ │ │ ├── entry-server.tsx │ │ │ │ ├── global.d.ts │ │ │ │ ├── lib │ │ │ │ │ ├── auth-client.ts │ │ │ │ │ └── auth.ts │ │ │ │ └── routes │ │ │ │ ├── [...404].tsx │ │ │ │ ├── api │ │ │ │ │ └── auth │ │ │ │ │ └── [...all].ts │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── test-utils │ │ │ ├── package.json │ │ │ └── src │ │ │ └── playwright.ts │ │ └── vanilla-node │ │ ├── e2e │ │ │ ├── app.ts │ │ │ ├── domain.spec.ts │ │ │ ├── postgres-js.spec.ts │ │ │ ├── test.spec.ts │ │ │ └── utils.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ │ ├── main.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── smoke │ ├── package.json │ ├── test │ │ ├── bun.spec.ts │ │ ├── cloudflare.spec.ts │ │ ├── deno.spec.ts │ │ ├── fixtures │ │ │ ├── bun-simple.ts │ │ │ ├── cloudflare │ │ │ │ ├── .gitignore │ │ │ │ ├── drizzle │ │ │ │ │ ├── 0000_clean_vector.sql │ │ │ │ │ └── meta │ │ │ │ │ ├── _journal.json │ │ │ │ │ └── 0000_snapshot.json │ │ │ │ ├── drizzle.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── auth-schema.ts │ │ │ │ │ ├── db.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── test │ │ │ │ │ ├── apply-migrations.ts │ │ │ │ │ ├── env.d.ts │ │ │ │ │ └── index.test.ts │ │ │ │ ├── tsconfig.json │ │ │ │ ├── vitest.config.ts │ │ │ │ ├── worker-configuration.d.ts │ │ │ │ └── wrangler.json │ │ │ ├── deno-simple.ts │ │ │ ├── tsconfig-decelration │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-exact-optional-property-types │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── user-additional-fields.ts │ │ │ │ │ └── username.ts │ │ │ │ └── tsconfig.json │ │ │ ├── tsconfig-verbatim-module-syntax-node10 │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ └── index.ts │ │ │ │ └── tsconfig.json │ │ │ └── vite │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── client.ts │ │ │ │ └── server.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── ssr.ts │ │ ├── typecheck.spec.ts │ │ └── vite.spec.ts │ └── tsconfig.json ├── LICENSE.md ├── package.json ├── packages │ ├── better-auth │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── init.test.ts.snap │ │ │ ├── adapters │ │ │ │ ├── adapter-factory │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── adapter-factory.test.ts.snap │ │ │ │ │ │ └── adapter-factory.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── create-test-suite.ts │ │ │ │ ├── drizzle-adapter │ │ │ │ │ ├── drizzle-adapter.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── adapter.drizzle.mysql.test.ts │ │ │ │ │ ├── adapter.drizzle.pg.test.ts │ │ │ │ │ ├── adapter.drizzle.sqlite.test.ts │ │ │ │ │ └── generate-schema.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely-adapter │ │ │ │ │ ├── bun-sqlite-dialect.ts │ │ │ │ │ ├── dialect.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kysely-adapter.ts │ │ │ │ │ ├── node-sqlite-dialect.ts │ │ │ │ │ ├── test │ │ │ │ │ │ ├── adapter.kysely.mssql.test.ts │ │ │ │ │ │ ├── adapter.kysely.mysql.test.ts │ │ │ │ │ │ ├── adapter.kysely.pg.test.ts │ │ │ │ │ │ ├── adapter.kysely.sqlite.test.ts │ │ │ │ │ │ └── node-sqlite-dialect.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── memory-adapter │ │ │ │ │ ├── adapter.memory.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── memory-adapter.ts │ │ │ │ ├── mongodb-adapter │ │ │ │ │ ├── adapter.mongo-db.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mongodb-adapter.ts │ │ │ │ ├── prisma-adapter │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── prisma-adapter.ts │ │ │ │ │ └── test │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── base.prisma │ │ │ │ │ ├── generate-auth-config.ts │ │ │ │ │ ├── generate-prisma-schema.ts │ │ │ │ │ ├── get-prisma-client.ts │ │ │ │ │ ├── prisma.mysql.test.ts │ │ │ │ │ ├── prisma.pg.test.ts │ │ │ │ │ ├── prisma.sqlite.test.ts │ │ │ │ │ └── push-prisma-schema.ts │ │ │ │ ├── test-adapter.ts │ │ │ │ ├── test.ts │ │ │ │ ├── tests │ │ │ │ │ ├── auth-flow.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── normal.ts │ │ │ │ │ ├── number-id.ts │ │ │ │ │ ├── performance.ts │ │ │ │ │ └── transactions.ts │ │ │ │ └── utils.ts │ │ │ ├── api │ │ │ │ ├── check-endpoint-conflicts.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── middlewares │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── origin-check.test.ts │ │ │ │ │ └── origin-check.ts │ │ │ │ ├── rate-limiter │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rate-limiter.test.ts │ │ │ │ ├── routes │ │ │ │ │ ├── account.test.ts │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── callback.ts │ │ │ │ │ ├── email-verification.test.ts │ │ │ │ │ ├── email-verification.ts │ │ │ │ │ ├── error.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ok.ts │ │ │ │ │ ├── reset-password.test.ts │ │ │ │ │ ├── reset-password.ts │ │ │ │ │ ├── session-api.test.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── sign-in.test.ts │ │ │ │ │ ├── sign-in.ts │ │ │ │ │ ├── sign-out.test.ts │ │ │ │ │ ├── sign-out.ts │ │ │ │ │ ├── sign-up.test.ts │ │ │ │ │ ├── sign-up.ts │ │ │ │ │ ├── update-user.test.ts │ │ │ │ │ └── update-user.ts │ │ │ │ ├── to-auth-endpoints.test.ts │ │ │ │ └── to-auth-endpoints.ts │ │ │ ├── auth.test.ts │ │ │ ├── auth.ts │ │ │ ├── call.test.ts │ │ │ ├── client │ │ │ │ ├── client-ssr.test.ts │ │ │ │ ├── client.test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── fetch-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ ├── lynx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── lynx-store.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── path-to-object.ts │ │ │ │ ├── plugins │ │ │ │ │ ├── index.ts │ │ │ │ │ └── infer-plugin.ts │ │ │ │ ├── proxy.ts │ │ │ │ ├── query.ts │ │ │ │ ├── react │ │ │ │ │ ├── index.ts │ │ │ │ │ └── react-store.ts │ │ │ │ ├── session-atom.ts │ │ │ │ ├── solid │ │ │ │ │ ├── index.ts │ │ │ │ │ └── solid-store.ts │ │ │ │ ├── svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── test-plugin.ts │ │ │ │ ├── types.ts │ │ │ │ ├── url.test.ts │ │ │ │ ├── vanilla.ts │ │ │ │ └── vue │ │ │ │ ├── index.ts │ │ │ │ └── vue-store.ts │ │ │ ├── cookies │ │ │ │ ├── check-cookies.ts │ │ │ │ ├── cookie-utils.ts │ │ │ │ ├── cookies.test.ts │ │ │ │ └── index.ts │ │ │ ├── crypto │ │ │ │ ├── buffer.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt.ts │ │ │ │ ├── password.test.ts │ │ │ │ ├── password.ts │ │ │ │ └── random.ts │ │ │ ├── db │ │ │ │ ├── db.test.ts │ │ │ │ ├── field.ts │ │ │ │ ├── get-migration.ts │ │ │ │ ├── get-schema.ts │ │ │ │ ├── get-tables.test.ts │ │ │ │ ├── get-tables.ts │ │ │ │ ├── index.ts │ │ │ │ ├── internal-adapter.test.ts │ │ │ │ ├── internal-adapter.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── secondary-storage.test.ts │ │ │ │ ├── to-zod.ts │ │ │ │ ├── utils.ts │ │ │ │ └── with-hooks.ts │ │ │ ├── index.ts │ │ │ ├── init.test.ts │ │ │ ├── init.ts │ │ │ ├── integrations │ │ │ │ ├── next-js.ts │ │ │ │ ├── node.ts │ │ │ │ ├── react-start.ts │ │ │ │ ├── solid-start.ts │ │ │ │ └── svelte-kit.ts │ │ │ ├── oauth2 │ │ │ │ ├── index.ts │ │ │ │ ├── link-account.test.ts │ │ │ │ ├── link-account.ts │ │ │ │ ├── state.ts │ │ │ │ └── utils.ts │ │ │ ├── plugins │ │ │ │ ├── access │ │ │ │ │ ├── access.test.ts │ │ │ │ │ ├── access.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── additional-fields │ │ │ │ │ ├── additional-fields.test.ts │ │ │ │ │ └── client.ts │ │ │ │ ├── admin │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── admin.test.ts │ │ │ │ │ ├── admin.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── anonymous │ │ │ │ │ ├── anon.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── api-key │ │ │ │ │ ├── api-key.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── create-api-key.ts │ │ │ │ │ │ ├── delete-all-expired-api-keys.ts │ │ │ │ │ │ ├── delete-api-key.ts │ │ │ │ │ │ ├── get-api-key.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── list-api-keys.ts │ │ │ │ │ │ ├── update-api-key.ts │ │ │ │ │ │ └── verify-api-key.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── bearer │ │ │ │ │ ├── bearer.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── captcha │ │ │ │ │ ├── captcha.test.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-handlers │ │ │ │ │ ├── captchafox.ts │ │ │ │ │ ├── cloudflare-turnstile.ts │ │ │ │ │ ├── google-recaptcha.ts │ │ │ │ │ ├── h-captcha.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-session.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── device-authorization │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── device-authorization.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schema.ts │ │ │ │ ├── email-otp │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── email-otp.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── generic-oauth │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── generic-oauth.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── haveibeenpwned │ │ │ │ │ ├── haveibeenpwned.test.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── jwt │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── sign.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── last-login-method │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── custom-prefix.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── last-login-method.test.ts │ │ │ │ ├── magic-link │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── magic-link.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── mcp │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── mcp.test.ts │ │ │ │ ├── multi-session │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── multi-session.test.ts │ │ │ │ ├── oauth-proxy │ │ │ │ │ ├── index.ts │ │ │ │ │ └── oauth-proxy.test.ts │ │ │ │ ├── oidc-provider │ │ │ │ │ ├── authorize.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── oidc.test.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── ui.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── one-tap │ │ │ │ │ ├── client.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── one-time-token │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── one-time-token.test.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── open-api │ │ │ │ │ ├── generator.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── logo.ts │ │ │ │ │ └── open-api.test.ts │ │ │ │ ├── organization │ │ │ │ │ ├── access │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── statement.ts │ │ │ │ │ ├── adapter.ts │ │ │ │ │ ├── call.ts │ │ │ │ │ ├── client.test.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── error-codes.ts │ │ │ │ │ ├── has-permission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── organization-hook.test.ts │ │ │ │ │ ├── organization.test.ts │ │ │ │ │ ├── organization.ts │ │ │ │ │ ├── permission.ts │ │ │ │ │ ├── routes │ │ │ │ │ │ ├── crud-access-control.test.ts │ │ │ │ │ │ ├── crud-access-control.ts │ │ │ │ │ │ ├── crud-invites.ts │ │ │ │ │ │ ├── crud-members.test.ts │ │ │ │ │ │ ├── crud-members.ts │ │ │ │ │ │ ├── crud-org.test.ts │ │ │ │ │ │ ├── crud-org.ts │ │ │ │ │ │ └── crud-team.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── team.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── passkey │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── passkey.test.ts │ │ │ │ ├── phone-number │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── phone-number-error.ts │ │ │ │ │ └── phone-number.test.ts │ │ │ │ ├── siwe │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── siwe.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── two-factor │ │ │ │ │ ├── backup-codes │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── client.ts │ │ │ │ │ ├── constant.ts │ │ │ │ │ ├── error-code.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── otp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── totp │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── two-factor.test.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── verify-two-factor.ts │ │ │ │ └── username │ │ │ │ ├── client.ts │ │ │ │ ├── error-codes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── schema.ts │ │ │ │ └── username.test.ts │ │ │ ├── social-providers │ │ │ │ └── index.ts │ │ │ ├── social.test.ts │ │ │ ├── test-utils │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ └── test-instance.ts │ │ │ ├── types │ │ │ │ ├── adapter.ts │ │ │ │ ├── api.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── models.ts │ │ │ │ ├── plugins.ts │ │ │ │ └── types.test.ts │ │ │ └── utils │ │ │ ├── await-object.ts │ │ │ ├── boolean.ts │ │ │ ├── clone.ts │ │ │ ├── constants.ts │ │ │ ├── date.ts │ │ │ ├── ensure-utc.ts │ │ │ ├── get-request-ip.ts │ │ │ ├── hashing.ts │ │ │ ├── hide-metadata.ts │ │ │ ├── id.ts │ │ │ ├── import-util.ts │ │ │ ├── index.ts │ │ │ ├── is-atom.ts │ │ │ ├── is-promise.ts │ │ │ ├── json.ts │ │ │ ├── merger.ts │ │ │ ├── middleware-response.ts │ │ │ ├── misc.ts │ │ │ ├── password.ts │ │ │ ├── plugin-helper.ts │ │ │ ├── shim.ts │ │ │ ├── time.ts │ │ │ ├── url.ts │ │ │ └── wildcard.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── generate.ts │ │ │ │ ├── info.ts │ │ │ │ ├── init.ts │ │ │ │ ├── login.ts │ │ │ │ ├── mcp.ts │ │ │ │ ├── migrate.ts │ │ │ │ └── secret.ts │ │ │ ├── generators │ │ │ │ ├── auth-config.ts │ │ │ │ ├── drizzle.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kysely.ts │ │ │ │ ├── prisma.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ ├── add-svelte-kit-env-modules.ts │ │ │ ├── check-package-managers.ts │ │ │ ├── format-ms.ts │ │ │ ├── get-config.ts │ │ │ ├── get-package-info.ts │ │ │ ├── get-tsconfig-info.ts │ │ │ └── install-dependencies.ts │ │ ├── test │ │ │ ├── __snapshots__ │ │ │ │ ├── auth-schema-mysql-enum.txt │ │ │ │ ├── auth-schema-mysql-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey-number-id.txt │ │ │ │ ├── auth-schema-mysql-passkey.txt │ │ │ │ ├── auth-schema-mysql.txt │ │ │ │ ├── auth-schema-number-id.txt │ │ │ │ ├── auth-schema-pg-enum.txt │ │ │ │ ├── auth-schema-pg-passkey.txt │ │ │ │ ├── auth-schema-sqlite-enum.txt │ │ │ │ ├── auth-schema-sqlite-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey-number-id.txt │ │ │ │ ├── auth-schema-sqlite-passkey.txt │ │ │ │ ├── auth-schema-sqlite.txt │ │ │ │ ├── auth-schema.txt │ │ │ │ ├── migrations.sql │ │ │ │ ├── schema-mongodb.prisma │ │ │ │ ├── schema-mysql-custom.prisma │ │ │ │ ├── schema-mysql.prisma │ │ │ │ ├── schema-numberid.prisma │ │ │ │ └── schema.prisma │ │ │ ├── generate-all-db.test.ts │ │ │ ├── generate.test.ts │ │ │ ├── get-config.test.ts │ │ │ ├── info.test.ts │ │ │ └── migrate.test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.test.json │ │ └── tsdown.config.ts │ ├── core │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ └── index.ts │ │ │ ├── async_hooks │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── endpoint-context.ts │ │ │ │ ├── index.ts │ │ │ │ └── transaction.ts │ │ │ ├── db │ │ │ │ ├── adapter │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── schema │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── rate-limit.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shared.ts │ │ │ │ │ ├── user.ts │ │ │ │ │ └── verification.ts │ │ │ │ └── type.ts │ │ │ ├── env │ │ │ │ ├── color-depth.ts │ │ │ │ ├── env-impl.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.test.ts │ │ │ │ └── logger.ts │ │ │ ├── error │ │ │ │ ├── codes.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── oauth2 │ │ │ │ ├── client-credentials-token.ts │ │ │ │ ├── create-authorization-url.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-provider.ts │ │ │ │ ├── refresh-access-token.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate-authorization-code.ts │ │ │ ├── social-providers │ │ │ │ ├── apple.ts │ │ │ │ ├── atlassian.ts │ │ │ │ ├── cognito.ts │ │ │ │ ├── discord.ts │ │ │ │ ├── dropbox.ts │ │ │ │ ├── facebook.ts │ │ │ │ ├── figma.ts │ │ │ │ ├── github.ts │ │ │ │ ├── gitlab.ts │ │ │ │ ├── google.ts │ │ │ │ ├── huggingface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── kakao.ts │ │ │ │ ├── kick.ts │ │ │ │ ├── line.ts │ │ │ │ ├── linear.ts │ │ │ │ ├── linkedin.ts │ │ │ │ ├── microsoft-entra-id.ts │ │ │ │ ├── naver.ts │ │ │ │ ├── notion.ts │ │ │ │ ├── paypal.ts │ │ │ │ ├── reddit.ts │ │ │ │ ├── roblox.ts │ │ │ │ ├── salesforce.ts │ │ │ │ ├── slack.ts │ │ │ │ ├── spotify.ts │ │ │ │ ├── tiktok.ts │ │ │ │ ├── twitch.ts │ │ │ │ ├── twitter.ts │ │ │ │ ├── vk.ts │ │ │ │ └── zoom.ts │ │ │ ├── types │ │ │ │ ├── context.ts │ │ │ │ ├── cookie.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── index.ts │ │ │ │ ├── init-options.ts │ │ │ │ ├── plugin-client.ts │ │ │ │ └── plugin.ts │ │ │ └── utils │ │ │ ├── error-codes.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── expo │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── expo.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── sso │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── oidc.test.ts │ │ │ └── saml.test.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── stripe │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── client.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── stripe.test.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── telemetry │ ├── package.json │ ├── src │ │ ├── detectors │ │ │ ├── detect-auth-config.ts │ │ │ ├── detect-database.ts │ │ │ ├── detect-framework.ts │ │ │ ├── detect-project-info.ts │ │ │ ├── detect-runtime.ts │ │ │ └── detect-system-info.ts │ │ ├── index.ts │ │ ├── project-id.ts │ │ ├── telemetry.test.ts │ │ ├── types.ts │ │ └── utils │ │ ├── hash.ts │ │ ├── id.ts │ │ ├── import-util.ts │ │ └── package-json.ts │ ├── tsconfig.json │ └── tsdown.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── README.md ├── SECURITY.md ├── tsconfig.json └── turbo.json ``` # Files -------------------------------------------------------------------------------- /docs/content/docs/installation.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Installation 3 | description: Learn how to configure Better Auth in your project. 4 | --- 5 | 6 | <Steps> 7 | 8 | <Step> 9 | ### Install the Package 10 | 11 | Let's start by adding Better Auth to your project: 12 | 13 | ```package-install 14 | better-auth 15 | ``` 16 | 17 | <Callout type="info"> 18 | If you're using a separate client and server setup, make sure to install Better Auth in both parts of your project. 19 | </Callout> 20 | </Step> 21 | 22 | <Step> 23 | ### Set Environment Variables 24 | 25 | Create a `.env` file in the root of your project and add the following environment variables: 26 | 27 | 1. **Secret Key** 28 | 29 | Random value used by the library for encryption and generating hashes. **You can generate one using the button below** or you can use something like openssl. 30 | 31 | ```txt title=".env" 32 | BETTER_AUTH_SECRET= 33 | ``` 34 | 35 | <GenerateSecret /> 36 | 37 | 2. **Set Base URL** 38 | 39 | ```txt title=".env" 40 | BETTER_AUTH_URL=http://localhost:3000 # Base URL of your app 41 | ``` 42 | 43 | </Step> 44 | 45 | <Step> 46 | ### Create A Better Auth Instance 47 | 48 | Create a file named `auth.ts` in one of these locations: 49 | 50 | - Project root 51 | - `lib/` folder 52 | - `utils/` folder 53 | 54 | You can also nest any of these folders under `src/`, `app/` or `server/` folder. (e.g. `src/lib/auth.ts`, `app/lib/auth.ts`). 55 | 56 | And in this file, import Better Auth and create your auth instance. Make sure to export the auth instance with the variable name `auth` or as a `default` export. 57 | 58 | ```ts title="auth.ts" 59 | import { betterAuth } from "better-auth"; 60 | 61 | export const auth = betterAuth({ 62 | //... 63 | }); 64 | ``` 65 | 66 | </Step> 67 | 68 | <Step> 69 | ### Configure Database 70 | 71 | Better Auth requires a database to store user data. 72 | You can easily configure Better Auth to use SQLite, PostgreSQL, or MySQL, and more! 73 | 74 | <Tabs items={["sqlite", "postgres", "mysql"]}> 75 | <Tab value="sqlite"> 76 | ```ts title="auth.ts" 77 | import { betterAuth } from "better-auth"; 78 | import Database from "better-sqlite3"; 79 | 80 | export const auth = betterAuth({ 81 | database: new Database("./sqlite.db"), 82 | }) 83 | ``` 84 | </Tab> 85 | <Tab value="postgres"> 86 | ```ts title="auth.ts" 87 | import { betterAuth } from "better-auth"; 88 | import { Pool } from "pg"; 89 | 90 | export const auth = betterAuth({ 91 | database: new Pool({ 92 | // connection options 93 | }), 94 | }) 95 | ``` 96 | </Tab> 97 | <Tab value="mysql"> 98 | ```ts title="auth.ts" 99 | import { betterAuth } from "better-auth"; 100 | import { createPool } from "mysql2/promise"; 101 | 102 | export const auth = betterAuth({ 103 | database: createPool({ 104 | // connection options 105 | }), 106 | }) 107 | ``` 108 | </Tab> 109 | 110 | </Tabs> 111 | 112 | Alternatively, if you prefer to use an ORM, you can use one of the built-in adapters. 113 | 114 | <Tabs items={["drizzle", "prisma", "mongodb"]}> 115 | 116 | <Tab value="drizzle"> 117 | ```ts title="auth.ts" 118 | import { betterAuth } from "better-auth"; 119 | import { drizzleAdapter } from "better-auth/adapters/drizzle"; 120 | import { db } from "@/db"; // your drizzle instance 121 | 122 | export const auth = betterAuth({ 123 | database: drizzleAdapter(db, { 124 | provider: "pg", // or "mysql", "sqlite" 125 | }), 126 | }); 127 | ``` 128 | </Tab> 129 | <Tab value="prisma"> 130 | ```ts title="auth.ts" 131 | import { betterAuth } from "better-auth"; 132 | import { prismaAdapter } from "better-auth/adapters/prisma"; 133 | // If your Prisma file is located elsewhere, you can change the path 134 | import { PrismaClient } from "@/generated/prisma"; 135 | 136 | const prisma = new PrismaClient(); 137 | export const auth = betterAuth({ 138 | database: prismaAdapter(prisma, { 139 | provider: "sqlite", // or "mysql", "postgresql", ...etc 140 | }), 141 | }); 142 | ``` 143 | </Tab> 144 | 145 | <Tab value="mongodb"> 146 | ```ts title="auth.ts" 147 | import { betterAuth } from "better-auth"; 148 | import { mongodbAdapter } from "better-auth/adapters/mongodb"; 149 | import { client } from "@/db"; // your mongodb client 150 | 151 | export const auth = betterAuth({ 152 | database: mongodbAdapter(client), 153 | }); 154 | ``` 155 | </Tab> 156 | 157 | </Tabs> 158 | 159 | <Callout> 160 | If your database is not listed above, check out our other supported 161 | [databases](/docs/adapters/other-relational-databases) for more information, 162 | or use one of the supported ORMs. 163 | </Callout> 164 | 165 | </Step> 166 | 167 | <Step> 168 | ### Create Database Tables 169 | Better Auth includes a CLI tool to help manage the schema required by the library. 170 | 171 | - **Generate**: This command generates an ORM schema or SQL migration file. 172 | 173 | <Callout> 174 | If you're using Kysely, you can apply the migration directly with `migrate` command below. Use `generate` only if you plan to apply the migration manually. 175 | </Callout> 176 | 177 | ```bash title="Terminal" 178 | npx @better-auth/cli generate 179 | ``` 180 | 181 | - **Migrate**: This command creates the required tables directly in the database. (Available only for the built-in Kysely adapter) 182 | 183 | ```bash title="Terminal" 184 | npx @better-auth/cli migrate 185 | ``` 186 | 187 | see the [CLI documentation](/docs/concepts/cli) for more information. 188 | 189 | <Callout> 190 | If you instead want to create the schema manually, you can find the core schema required in the [database section](/docs/concepts/database#core-schema). 191 | </Callout> 192 | 193 | </Step> 194 | 195 | <Step> 196 | 197 | ### Authentication Methods 198 | 199 | Configure the authentication methods you want to use. Better Auth comes with built-in support for email/password, and social sign-on providers. 200 | 201 | ```ts title="auth.ts" 202 | import { betterAuth } from "better-auth"; 203 | 204 | export const auth = betterAuth({ 205 | //...other options // [!code highlight] 206 | emailAndPassword: { // [!code highlight] 207 | enabled: true, // [!code highlight] 208 | }, // [!code highlight] 209 | socialProviders: { // [!code highlight] 210 | github: { // [!code highlight] 211 | clientId: process.env.GITHUB_CLIENT_ID as string, // [!code highlight] 212 | clientSecret: process.env.GITHUB_CLIENT_SECRET as string, // [!code highlight] 213 | }, // [!code highlight] 214 | }, // [!code highlight] 215 | }); 216 | ``` 217 | 218 | <Callout type="info"> 219 | You can use even more authentication methods like [passkey](/docs/plugins/passkey), [username](/docs/plugins/username), [magic link](/docs/plugins/magic-link) and more through plugins. 220 | </Callout> 221 | </Step> 222 | 223 | <Step> 224 | ### Mount Handler 225 | To handle API requests, you need to set up a route handler on your server. 226 | 227 | Create a new file or route in your framework's designated catch-all route handler. This route should handle requests for the path `/api/auth/*` (unless you've configured a different base path). 228 | 229 | <Callout> 230 | Better Auth supports any backend framework with standard Request and Response 231 | objects and offers helper functions for popular frameworks. 232 | </Callout> 233 | 234 | <Tabs items={["next-js", "nuxt", "svelte-kit", "remix", "solid-start", "hono", "cloudflare-workers", "express", "elysia", "tanstack-start", "expo"]} defaultValue="next-js"> 235 | <Tab value="next-js"> 236 | ```ts title="/app/api/auth/[...all]/route.ts" 237 | import { auth } from "@/lib/auth"; // path to your auth file 238 | import { toNextJsHandler } from "better-auth/next-js"; 239 | 240 | export const { POST, GET } = toNextJsHandler(auth); 241 | ``` 242 | </Tab> 243 | <Tab value="nuxt"> 244 | ```ts title="/server/api/auth/[...all].ts" 245 | import { auth } from "~/utils/auth"; // path to your auth file 246 | 247 | export default defineEventHandler((event) => { 248 | return auth.handler(toWebRequest(event)); 249 | }); 250 | ``` 251 | </Tab> 252 | <Tab value="svelte-kit"> 253 | ```ts title="hooks.server.ts" 254 | import { auth } from "$lib/auth"; // path to your auth file 255 | import { svelteKitHandler } from "better-auth/svelte-kit"; 256 | import { building } from '$app/environment' 257 | 258 | export async function handle({ event, resolve }) { 259 | return svelteKitHandler({ event, resolve, auth, building }); 260 | } 261 | ``` 262 | </Tab> 263 | <Tab value="remix"> 264 | ```ts title="/app/routes/api.auth.$.ts" 265 | import { auth } from '~/lib/auth.server' // Adjust the path as necessary 266 | import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node" 267 | 268 | export async function loader({ request }: LoaderFunctionArgs) { 269 | return auth.handler(request) 270 | } 271 | 272 | export async function action({ request }: ActionFunctionArgs) { 273 | return auth.handler(request) 274 | } 275 | ``` 276 | </Tab> 277 | <Tab value="solid-start"> 278 | ```ts title="/routes/api/auth/*all.ts" 279 | import { auth } from "~/lib/auth"; // path to your auth file 280 | import { toSolidStartHandler } from "better-auth/solid-start"; 281 | 282 | export const { GET, POST } = toSolidStartHandler(auth); 283 | ``` 284 | </Tab> 285 | <Tab value="hono"> 286 | ```ts title="src/index.ts" 287 | import { Hono } from "hono"; 288 | import { auth } from "./auth"; // path to your auth file 289 | import { serve } from "@hono/node-server"; 290 | import { cors } from "hono/cors"; 291 | 292 | const app = new Hono(); 293 | 294 | app.on(["POST", "GET"], "/api/auth/*", (c) => auth.handler(c.req.raw)); 295 | 296 | serve(app); 297 | ``` 298 | </Tab> 299 | <Tab value="cloudflare-workers"> 300 | ```ts title="src/index.ts" 301 | import { auth } from "./auth"; // path to your auth file 302 | 303 | export default { 304 | async fetch(request: Request) { 305 | const url = new URL(request.url); 306 | 307 | // Handle auth routes 308 | if (url.pathname.startsWith("/api/auth")) { 309 | return auth.handler(request); 310 | } 311 | 312 | // Handle other routes 313 | return new Response("Not found", { status: 404 }); 314 | }, 315 | }; 316 | ``` 317 | 318 | <Callout type="info"> 319 | **Node.js AsyncLocalStorage Support**: Better Auth uses AsyncLocalStorage for async context tracking. To enable this in Cloudflare Workers, add the `nodejs_compat` flag to your `wrangler.toml`: 320 | 321 | ```toml title="wrangler.toml" 322 | compatibility_flags = ["nodejs_compat"] 323 | compatibility_date = "2024-09-23" 324 | ``` 325 | 326 | Alternatively, if you only need AsyncLocalStorage support: 327 | ```toml title="wrangler.toml" 328 | compatibility_flags = ["nodejs_als"] 329 | ``` 330 | 331 | In the next major release, we will assume AsyncLocalStorage support by default, so this configuration will be necessary. 332 | </Callout> 333 | </Tab> 334 | 335 | <Tab value="express"> 336 | <Callout type="warn"> 337 | ExpressJS v5 introduced breaking changes to route path matching by switching to `path-to-regexp@6`. Wildcard routes like `*` should now be written using the new named syntax, e.g. `/{*any}`, to properly capture catch-all patterns. This ensures compatibility and predictable behavior across routing scenarios. 338 | See the [Express v5 migration guide](https://expressjs.com/en/guide/migrating-5.html) for details. 339 | 340 | As a result, the implementation in ExpressJS v5 should look like this: 341 | 342 | ```ts 343 | app.all('/api/auth/{*any}', toNodeHandler(auth)); 344 | ``` 345 | *The name any is arbitrary and can be replaced with any identifier you prefer.* 346 | </Callout> 347 | 348 | ```ts title="server.ts" 349 | import express from "express"; 350 | import { toNodeHandler } from "better-auth/node"; 351 | import { auth } from "./auth"; 352 | 353 | const app = express(); 354 | const port = 8000; 355 | 356 | app.all("/api/auth/*", toNodeHandler(auth)); 357 | 358 | // Mount express json middleware after Better Auth handler 359 | // or only apply it to routes that don't interact with Better Auth 360 | app.use(express.json()); 361 | 362 | app.listen(port, () => { 363 | console.log(`Better Auth app listening on port ${port}`); 364 | }); 365 | ``` 366 | This will also work for any other node server framework like express, fastify, hapi, etc., but may require some modifications. See [fastify guide](/docs/integrations/fastify). Note that CommonJS (cjs) isn't supported. 367 | </Tab> 368 | <Tab value="astro"> 369 | ```ts title="/pages/api/auth/[...all].ts" 370 | import type { APIRoute } from "astro"; 371 | import { auth } from "@/auth"; // path to your auth file 372 | 373 | export const GET: APIRoute = async (ctx) => { 374 | return auth.handler(ctx.request); 375 | }; 376 | 377 | export const POST: APIRoute = async (ctx) => { 378 | return auth.handler(ctx.request); 379 | }; 380 | ``` 381 | </Tab> 382 | <Tab value="elysia"> 383 | ```ts 384 | import { Elysia, Context } from "elysia"; 385 | import { auth } from "./auth"; 386 | 387 | const betterAuthView = (context: Context) => { 388 | const BETTER_AUTH_ACCEPT_METHODS = ["POST", "GET"] 389 | // validate request method 390 | if(BETTER_AUTH_ACCEPT_METHODS.includes(context.request.method)) { 391 | return auth.handler(context.request); 392 | } else { 393 | context.error(405) 394 | } 395 | } 396 | 397 | const app = new Elysia().all("/api/auth/*", betterAuthView).listen(3000); 398 | 399 | console.log( 400 | `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` 401 | ); 402 | ``` 403 | </Tab> 404 | <Tab value="tanstack-start"> 405 | ```ts title="src/routes/api/auth/$.ts" 406 | import { auth } from '~/lib/server/auth' 407 | import { createServerFileRoute } from '@tanstack/react-start/server' 408 | 409 | export const ServerRoute = createServerFileRoute('/api/auth/$').methods({ 410 | GET: ({ request }) => { 411 | return auth.handler(request) 412 | }, 413 | POST: ({ request }) => { 414 | return auth.handler(request) 415 | }, 416 | }); 417 | ``` 418 | </Tab> 419 | <Tab value="expo"> 420 | ```ts title="app/api/auth/[...all]+api.ts" 421 | import { auth } from '@/lib/server/auth'; // path to your auth file 422 | 423 | const handler = auth.handler; 424 | export { handler as GET, handler as POST }; 425 | ``` 426 | </Tab> 427 | 428 | </Tabs> 429 | </Step> 430 | 431 | <Step> 432 | ### Create Client Instance 433 | 434 | The client-side library helps you interact with the auth server. Better Auth comes with a client for all the popular web frameworks, including vanilla JavaScript. 435 | 436 | 1. Import `createAuthClient` from the package for your framework (e.g., "better-auth/react" for React). 437 | 2. Call the function to create your client. 438 | 3. Pass the base URL of your auth server. (If the auth server is running on the same domain as your client, you can skip this step.) 439 | 440 | <Callout type="info"> 441 | If you're using a different base path other than `/api/auth` make sure to pass 442 | the whole URL including the path. (e.g. 443 | `http://localhost:3000/custom-path/auth`) 444 | </Callout> 445 | 446 | <Tabs items={["react", "vue", "svelte", "solid", 447 | "vanilla"]} defaultValue="react"> 448 | <Tab value="vanilla"> 449 | ```ts title="lib/auth-client.ts" 450 | import { createAuthClient } from "better-auth/client" 451 | export const authClient = createAuthClient({ 452 | /** The base URL of the server (optional if you're using the same domain) */ // [!code highlight] 453 | baseURL: "http://localhost:3000" // [!code highlight] 454 | }) 455 | ``` 456 | </Tab> 457 | <Tab value="react" title="lib/auth-client.ts"> 458 | ```ts title="lib/auth-client.ts" 459 | import { createAuthClient } from "better-auth/react" 460 | export const authClient = createAuthClient({ 461 | /** The base URL of the server (optional if you're using the same domain) */ // [!code highlight] 462 | baseURL: "http://localhost:3000" // [!code highlight] 463 | }) 464 | ``` 465 | </Tab> 466 | <Tab value="vue" title="lib/auth-client.ts"> 467 | ```ts title="lib/auth-client.ts" 468 | import { createAuthClient } from "better-auth/vue" 469 | export const authClient = createAuthClient({ 470 | /** The base URL of the server (optional if you're using the same domain) */ // [!code highlight] 471 | baseURL: "http://localhost:3000" // [!code highlight] 472 | }) 473 | ``` 474 | </Tab> 475 | <Tab value="svelte" title="lib/auth-client.ts"> 476 | ```ts title="lib/auth-client.ts" 477 | import { createAuthClient } from "better-auth/svelte" 478 | export const authClient = createAuthClient({ 479 | /** The base URL of the server (optional if you're using the same domain) */ // [!code highlight] 480 | baseURL: "http://localhost:3000" // [!code highlight] 481 | }) 482 | ``` 483 | </Tab> 484 | <Tab value="solid" title="lib/auth-client.ts"> 485 | ```ts title="lib/auth-client.ts" 486 | import { createAuthClient } from "better-auth/solid" 487 | export const authClient = createAuthClient({ 488 | /** The base URL of the server (optional if you're using the same domain) */ // [!code highlight] 489 | baseURL: "http://localhost:3000" // [!code highlight] 490 | }) 491 | ``` 492 | </Tab> 493 | </Tabs> 494 | 495 | <Callout type="info"> 496 | Tip: You can also export specific methods if you prefer: 497 | </Callout> 498 | ```ts 499 | export const { signIn, signUp, useSession } = createAuthClient() 500 | ``` 501 | </Step> 502 | 503 | <Step> 504 | ### 🎉 That's it! 505 | That's it! You're now ready to use better-auth in your application. Continue to [basic usage](/docs/basic-usage) to learn how to use the auth instance to sign in users. 506 | </Step> 507 | </Steps> 508 | ``` -------------------------------------------------------------------------------- /docs/content/docs/plugins/2fa.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Two-Factor Authentication (2FA) 3 | description: Enhance your app's security with two-factor authentication. 4 | --- 5 | 6 | `OTP` `TOTP` `Backup Codes` `Trusted Devices` 7 | 8 | Two-Factor Authentication (2FA) adds an extra security step when users log in. Instead of just using a password, they'll need to provide a second form of verification. This makes it much harder for unauthorized people to access accounts, even if they've somehow gotten the password. 9 | 10 | This plugin offers two main methods to do a second factor verification: 11 | 12 | 1. **OTP (One-Time Password)**: A temporary code sent to the user's email or phone. 13 | 2. **TOTP (Time-based One-Time Password)**: A code generated by an app on the user's device. 14 | 15 | **Additional features include:** 16 | - Generating backup codes for account recovery 17 | - Enabling/disabling 2FA 18 | - Managing trusted devices 19 | 20 | ## Installation 21 | 22 | <Steps> 23 | <Step> 24 | ### Add the plugin to your auth config 25 | 26 | Add the two-factor plugin to your auth configuration and specify your app name as the issuer. 27 | 28 | ```ts title="auth.ts" 29 | import { betterAuth } from "better-auth" 30 | import { twoFactor } from "better-auth/plugins" // [!code highlight] 31 | 32 | export const auth = betterAuth({ 33 | // ... other config options 34 | appName: "My App", // provide your app name. It'll be used as an issuer. // [!code highlight] 35 | plugins: [ 36 | twoFactor() // [!code highlight] 37 | ] 38 | }) 39 | ``` 40 | </Step> 41 | <Step> 42 | ### Migrate the database 43 | 44 | Run the migration or generate the schema to add the necessary fields and tables to the database. 45 | 46 | <Tabs items={["migrate", "generate"]}> 47 | <Tab value="migrate"> 48 | ```bash 49 | npx @better-auth/cli migrate 50 | ``` 51 | </Tab> 52 | <Tab value="generate"> 53 | ```bash 54 | npx @better-auth/cli generate 55 | ``` 56 | </Tab> 57 | </Tabs> 58 | See the [Schema](#schema) section to add the fields manually. 59 | </Step> 60 | 61 | <Step> 62 | ### Add the client plugin 63 | 64 | Add the client plugin and Specify where the user should be redirected if they need to verify 2nd factor 65 | 66 | ```ts title="auth-client.ts" 67 | import { createAuthClient } from "better-auth/client" 68 | import { twoFactorClient } from "better-auth/client/plugins" 69 | 70 | export const authClient = createAuthClient({ 71 | plugins: [ 72 | twoFactorClient() 73 | ] 74 | }) 75 | ``` 76 | </Step> 77 | </Steps> 78 | 79 | ## Usage 80 | 81 | ### Enabling 2FA 82 | 83 | To enable two-factor authentication, call `twoFactor.enable` with the user's password and issuer (optional): 84 | 85 | <APIMethod 86 | path="/two-factor/enable" 87 | method="POST" 88 | requireSession 89 | > 90 | ```ts 91 | type enableTwoFactor = { 92 | /** 93 | * The user's password 94 | */ 95 | password: string = "secure-password" 96 | /** 97 | * An optional custom issuer for the TOTP URI. Defaults to app-name defined in your auth config. 98 | */ 99 | issuer?: string = "my-app-name" 100 | } 101 | ``` 102 | </APIMethod> 103 | 104 | When 2FA is enabled: 105 | - An encrypted `secret` and `backupCodes` are generated. 106 | - `enable` returns `totpURI` and `backupCodes`. 107 | 108 | Note: `twoFactorEnabled` won’t be set to `true` until the user verifies their TOTP code. Learn more about veryifying TOTP [here](#totp). You can skip verification by setting `skipVerificationOnEnable` to true in your plugin config. 109 | 110 | <Callout type="warn"> 111 | Two Factor can only be enabled for credential accounts at the moment. For social accounts, it's assumed the provider already handles 2FA. 112 | </Callout> 113 | 114 | ### Sign In with 2FA 115 | 116 | When a user with 2FA enabled tries to sign in via email, the response object will contain `twoFactorRedirect` set to `true`. This indicates that the user needs to verify their 2FA code. 117 | 118 | You can handle this in the `onSuccess` callback or by providing a `onTwoFactorRedirect` callback in the plugin config. 119 | 120 | ```ts title="sign-in.tsx" 121 | await authClient.signIn.email({ 122 | email: "[email protected]", 123 | password: "password123", 124 | }, 125 | { 126 | async onSuccess(context) { 127 | if (context.data.twoFactorRedirect) { 128 | // Handle the 2FA verification in place 129 | } 130 | }, 131 | } 132 | ) 133 | ``` 134 | 135 | Using the `onTwoFactorRedirect` config: 136 | 137 | ```ts title="sign-in.ts" 138 | import { createAuthClient } from "better-auth/client"; 139 | import { twoFactorClient } from "better-auth/client/plugins"; 140 | 141 | const authClient = createAuthClient({ 142 | plugins: [ 143 | twoFactorClient({ 144 | onTwoFactorRedirect(){ 145 | // Handle the 2FA verification globally 146 | }, 147 | }), 148 | ], 149 | }); 150 | ``` 151 | 152 | 153 | 154 | 155 | <Callout type="warn"> 156 | **With `auth.api`** 157 | 158 | When you call `auth.api.signInEmail` on the server, and the user has 2FA enabled, it will return an object where `twoFactorRedirect` is set to `true`. This behavior isn’t inferred in TypeScript, which can be misleading. You can check using `in` instead to check if `twoFactorRedirect` is set to `true`. 159 | 160 | ```ts 161 | const response = await auth.api.signInEmail({ 162 | body: { 163 | email: "[email protected]", 164 | password: "test", 165 | }, 166 | }); 167 | 168 | if ("twoFactorRedirect" in response) { 169 | // Handle the 2FA verification in place 170 | } 171 | ``` 172 | </Callout> 173 | 174 | ### Disabling 2FA 175 | 176 | To disable two-factor authentication, call `twoFactor.disable` with the user's password: 177 | 178 | <APIMethod 179 | path="/two-factor/disable" 180 | method="POST" 181 | requireSession 182 | > 183 | ```ts 184 | type disableTwoFactor = { 185 | /** 186 | * The user's password 187 | */ 188 | password: string 189 | } 190 | ``` 191 | </APIMethod> 192 | 193 | ### TOTP 194 | 195 | TOTP (Time-Based One-Time Password) is an algorithm that generates a unique password for each login attempt using time as a counter. Every fixed interval (Better Auth defaults to 30 seconds), a new password is generated. This addresses several issues with traditional passwords: they can be forgotten, stolen, or guessed. OTPs solve some of these problems, but their delivery via SMS or email can be unreliable (or even risky, considering it opens new attack vectors). 196 | 197 | TOTP, however, generates codes offline, making it both secure and convenient. You just need an authenticator app on your phone. 198 | 199 | #### Getting TOTP URI 200 | 201 | After enabling 2FA, you can get the TOTP URI to display to the user. This URI is generated by the server using the `secret` and `issuer` and can be used to generate a QR code for the user to scan with their authenticator app. 202 | 203 | <APIMethod 204 | path="/two-factor/get-totp-uri" 205 | method="POST" 206 | requireSession 207 | > 208 | ```ts 209 | type getTOTPURI = { 210 | /** 211 | * The user's password 212 | */ 213 | password: string 214 | } 215 | ``` 216 | </APIMethod> 217 | 218 | 219 | **Example: Using React** 220 | 221 | Once you have the TOTP URI, you can use it to generate a QR code for the user to scan with their authenticator app. 222 | 223 | ```tsx title="user-card.tsx" 224 | import QRCode from "react-qr-code"; 225 | 226 | export default function UserCard({ password }: { password: string }){ 227 | const { data: session } = client.useSession(); 228 | const { data: qr } = useQuery({ 229 | queryKey: ["two-factor-qr"], 230 | queryFn: async () => { 231 | const res = await authClient.twoFactor.getTotpUri({ password }); 232 | return res.data; 233 | }, 234 | enabled: !!session?.user.twoFactorEnabled, 235 | }); 236 | return ( 237 | <QRCode value={qr?.totpURI || ""} /> 238 | ) 239 | } 240 | ``` 241 | 242 | <Callout> 243 | By default the issuer for TOTP is set to the app name provided in the auth config or if not provided it will be set to `Better Auth`. You can override this by passing `issuer` to the plugin config. 244 | </Callout> 245 | 246 | #### Verifying TOTP 247 | 248 | After the user has entered their 2FA code, you can verify it using `twoFactor.verifyTotp` method. `Better Auth` follows standard practice by accepting TOTP codes from one period before and one after the current code, ensuring users can authenticate even with minor time delays on their end. 249 | 250 | <APIMethod path="/two-factor/verify-totp" method="POST"> 251 | ```ts 252 | type verifyTOTP = { 253 | /** 254 | * The otp code to verify. 255 | */ 256 | code: string = "012345" 257 | /** 258 | * If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. 259 | */ 260 | trustDevice?: boolean = true 261 | } 262 | ``` 263 | </APIMethod> 264 | 265 | ### OTP 266 | 267 | OTP (One-Time Password) is similar to TOTP but a random code is generated and sent to the user's email or phone. 268 | 269 | Before using OTP to verify the second factor, you need to configure `sendOTP` in your Better Auth instance. This function is responsible for sending the OTP to the user's email, phone, or any other method supported by your application. 270 | 271 | ```ts title="auth.ts" 272 | import { betterAuth } from "better-auth" 273 | import { twoFactor } from "better-auth/plugins" 274 | 275 | export const auth = betterAuth({ 276 | plugins: [ 277 | twoFactor({ 278 | otpOptions: { 279 | async sendOTP({ user, otp }, request) { 280 | // send otp to user 281 | }, 282 | }, 283 | }) 284 | ] 285 | }) 286 | ``` 287 | 288 | #### Sending OTP 289 | 290 | Sending an OTP is done by calling the `twoFactor.sendOtp` function. This function will trigger your sendOTP implementation that you provided in the Better Auth configuration. 291 | 292 | <APIMethod path="/two-factor/send-otp" method="POST"> 293 | ```ts 294 | type send2FaOTP = { 295 | /** 296 | * If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. 297 | */ 298 | trustDevice?: boolean = true 299 | } 300 | 301 | if (data) { 302 | // redirect or show the user to enter the code 303 | } 304 | ``` 305 | </APIMethod> 306 | 307 | #### Verifying OTP 308 | 309 | After the user has entered their OTP code, you can verify it 310 | 311 | <APIMethod path="/two-factor/verify-otp" method="POST"> 312 | ```ts 313 | type verifyOTP = { 314 | /** 315 | * The otp code to verify. 316 | */ 317 | code: string = "012345" 318 | /** 319 | * If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. 320 | */ 321 | trustDevice?: boolean = true 322 | } 323 | ``` 324 | </APIMethod> 325 | 326 | ### Backup Codes 327 | 328 | Backup codes are generated and stored in the database. This can be used to recover access to the account if the user loses access to their phone or email. 329 | 330 | #### Generating Backup Codes 331 | Generate backup codes for account recovery: 332 | 333 | <APIMethod 334 | path="/two-factor/generate-backup-codes" 335 | method="POST" 336 | requireSession 337 | > 338 | ```ts 339 | type generateBackupCodes = { 340 | /** 341 | * The users password. 342 | */ 343 | password: string 344 | } 345 | 346 | if (data) { 347 | // Show the backup codes to the user 348 | } 349 | ``` 350 | </APIMethod> 351 | 352 | 353 | <Callout type="warn"> 354 | When you generate backup codes, the old backup codes will be deleted and new ones will be generated. 355 | </Callout> 356 | 357 | #### Using Backup Codes 358 | 359 | You can now allow users to provider backup code as account recover method. 360 | 361 | 362 | <APIMethod path="/two-factor/verify-backup-code" method="POST"> 363 | ```ts 364 | type verifyBackupCode = { 365 | /** 366 | * A backup code to verify. 367 | */ 368 | code: string = "123456" 369 | /** 370 | * If true, the session cookie will not be set. 371 | */ 372 | disableSession?: boolean = false 373 | /** 374 | * If true, the device will be trusted for 30 days. It'll be refreshed on every sign in request within this time. 375 | */ 376 | trustDevice?: boolean = true 377 | } 378 | ``` 379 | </APIMethod> 380 | 381 | <Callout> 382 | Once a backup code is used, it will be removed from the database and can't be used again. 383 | </Callout> 384 | 385 | #### Viewing Backup Codes 386 | 387 | To display the backup codes to the user, you can call `viewBackupCodes` on the server. This will return the backup codes in the response. You should only this if the user has a fresh session - a session that was just created. 388 | 389 | <APIMethod 390 | path="/two-factor/view-backup-codes" 391 | method="GET" 392 | isServerOnly 393 | forceAsBody 394 | > 395 | ```ts 396 | type viewBackupCodes = { 397 | /** 398 | * The user ID to view all backup codes. 399 | */ 400 | userId?: string | null = "user-id" 401 | } 402 | ``` 403 | </APIMethod> 404 | 405 | ### Trusted Devices 406 | 407 | You can mark a device as trusted by passing `trustDevice` to `verifyTotp` or `verifyOtp`. 408 | 409 | ```ts 410 | const verify2FA = async (code: string) => { 411 | const { data, error } = await authClient.twoFactor.verifyTotp({ 412 | code, 413 | callbackURL: "/dashboard", 414 | trustDevice: true // Mark this device as trusted 415 | }) 416 | if (data) { 417 | // 2FA verified and device trusted 418 | } 419 | } 420 | ``` 421 | 422 | When `trustDevice` is set to `true`, the current device will be remembered for 60 days. During this period, the user won't be prompted for 2FA on subsequent sign-ins from this device. The trust period is refreshed each time the user signs in successfully. 423 | 424 | ### Issuer 425 | 426 | By adding an `issuer` you can set your application name for the 2fa application. 427 | 428 | For example, if your user uses Google Auth, the default appName will show up as `Better Auth`. However, by using the following code, it will show up as `my-app-name`. 429 | 430 | ```ts 431 | twoFactor({ 432 | issuer: "my-app-name" // [!code highlight] 433 | }) 434 | ``` 435 | --- 436 | 437 | ## Schema 438 | 439 | The plugin requires 1 additional fields in the `user` table and 1 additional table to store the two factor authentication data. 440 | 441 | Table: `user` 442 | 443 | <DatabaseTable 444 | fields={[ 445 | { name: "twoFactorEnabled", type: "boolean", description: "Whether two factor authentication is enabled for the user.", isOptional: true }, 446 | ]} 447 | /> 448 | 449 | Table: `twoFactor` 450 | 451 | <DatabaseTable 452 | fields={[ 453 | { name: "id", type: "string", description: "The ID of the two factor authentication.", isPrimaryKey: true }, 454 | { name: "userId", type: "string", description: "The ID of the user", isForeignKey: true }, 455 | { name: "secret", type: "string", description: "The secret used to generate the TOTP code.", isOptional: true }, 456 | { name: "backupCodes", type: "string", description: "The backup codes used to recover access to the account if the user loses access to their phone or email.", isOptional: true }, 457 | ]} 458 | /> 459 | 460 | ## Options 461 | 462 | ### Server 463 | 464 | **twoFactorTable**: The name of the table that stores the two factor authentication data. Default: `twoFactor`. 465 | 466 | **skipVerificationOnEnable**: Skip the verification process before enabling two factor for a user. 467 | 468 | **Issuer**: The issuer is the name of your application. It's used to generate TOTP codes. It'll be displayed in the authenticator apps. 469 | 470 | **TOTP options** 471 | 472 | these are options for TOTP. 473 | 474 | <TypeTable 475 | type={{ 476 | digits:{ 477 | description: "The number of digits the otp to be", 478 | type: "number", 479 | default: 6, 480 | }, 481 | period: { 482 | description: "The period for totp in seconds.", 483 | type: "number", 484 | default: 30, 485 | }, 486 | }} 487 | /> 488 | 489 | **OTP options** 490 | 491 | these are options for OTP. 492 | 493 | <TypeTable 494 | type={{ 495 | sendOTP: { 496 | description: "a function that sends the otp to the user's email or phone number. It takes two parameters: user and otp", 497 | type: "function", 498 | }, 499 | period: { 500 | description: "The period for otp in minutes.", 501 | type: "number", 502 | default: 3, 503 | }, 504 | storeOTP: { 505 | description: "How to store the otp in the database. Whether to store it as plain text, encrypted or hashed. You can also provide a custom encryptor or hasher.", 506 | type: "string", 507 | default: "plain", 508 | }, 509 | }} 510 | /> 511 | 512 | **Backup Code Options** 513 | 514 | backup codes are generated and stored in the database when the user enabled two factor authentication. This can be used to recover access to the account if the user loses access to their phone or email. 515 | 516 | <TypeTable 517 | type={{ 518 | amount: { 519 | description: "The amount of backup codes to generate", 520 | type: "number", 521 | default: 10, 522 | }, 523 | length: { 524 | description: "The length of the backup codes", 525 | type: "number", 526 | default: 10, 527 | }, 528 | customBackupCodesGenerate: { 529 | description: "A function that generates custom backup codes. It takes no parameters and returns an array of strings.", 530 | type: "function", 531 | }, 532 | storeBackupCodes: { 533 | description: "How to store the backup codes in the database. Whether to store it as plain text or encrypted. You can also provide a custom encryptor.", 534 | type: "string", 535 | default: "plain", 536 | }, 537 | }} 538 | /> 539 | 540 | ### Client 541 | 542 | To use the two factor plugin in the client, you need to add it on your plugins list. 543 | 544 | ```ts title="auth-client.ts" 545 | import { createAuthClient } from "better-auth/client" 546 | import { twoFactorClient } from "better-auth/client/plugins" 547 | 548 | const authClient = createAuthClient({ 549 | plugins: [ 550 | twoFactorClient({ // [!code highlight] 551 | onTwoFactorRedirect(){ // [!code highlight] 552 | window.location.href = "/2fa" // Handle the 2FA verification redirect // [!code highlight] 553 | } // [!code highlight] 554 | }) // [!code highlight] 555 | ] 556 | }) 557 | ``` 558 | 559 | 560 | **Options** 561 | 562 | `onTwoFactorRedirect`: A callback that will be called when the user needs to verify their 2FA code. This can be used to redirect the user to the 2FA page. 563 | ``` -------------------------------------------------------------------------------- /docs/content/docs/guides/clerk-migration-guide.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Migrating from Clerk to Better Auth 3 | description: A step-by-step guide to transitioning from Clerk to Better Auth. 4 | --- 5 | 6 | In this guide, we'll walk through the steps to migrate a project from Clerk to Better Auth — including email/password with proper hashing, social/external accounts, phone number, two-factor data, and more. 7 | 8 | <Callout type="warn"> 9 | This migration will invalidate all active sessions. This guide doesn't currently show you how to migrate Organization but it should be possible with additional steps and the [Organization](/docs/plugins/organization) Plugin. 10 | </Callout> 11 | 12 | ## Before You Begin 13 | 14 | Before starting the migration process, set up Better Auth in your project. Follow the [installation guide](/docs/installation) to get started. And go to 15 | 16 | <Steps> 17 | <Step> 18 | ### Connect to your database 19 | 20 | You'll need to connect to your database to migrate the users and accounts. You can use any database you want, but for this example, we'll use PostgreSQL. 21 | 22 | ```package-install 23 | npm install pg 24 | ``` 25 | 26 | And then you can use the following code to connect to your database. 27 | 28 | ```ts title="auth.ts" 29 | import { Pool } from "pg"; 30 | 31 | export const auth = betterAuth({ 32 | database: new Pool({ 33 | connectionString: process.env.DATABASE_URL 34 | }), 35 | }) 36 | ``` 37 | </Step> 38 | <Step> 39 | ### Enable Email and Password (Optional) 40 | 41 | Enable the email and password in your auth config and implement your own logic for sending verification emails, reset password emails, etc. 42 | 43 | ```ts title="auth.ts" 44 | import { betterAuth } from "better-auth"; 45 | 46 | export const auth = betterAuth({ 47 | database: new Pool({ 48 | connectionString: process.env.DATABASE_URL 49 | }), 50 | emailAndPassword: { // [!code highlight] 51 | enabled: true, // [!code highlight] 52 | }, // [!code highlight] 53 | emailVerification: { 54 | sendVerificationEmail: async({ user, url })=>{ 55 | // implement your logic here to send email verification 56 | } 57 | }, 58 | }) 59 | ``` 60 | 61 | See [Email and Password](/docs/authentication/email-password) for more configuration options. 62 | </Step> 63 | <Step> 64 | ### Setup Social Providers (Optional) 65 | 66 | Add social providers you have enabled in your Clerk project in your auth config. 67 | 68 | ```ts title="auth.ts" 69 | import { betterAuth } from "better-auth"; 70 | 71 | export const auth = betterAuth({ 72 | database: new Pool({ 73 | connectionString: process.env.DATABASE_URL 74 | }), 75 | emailAndPassword: { 76 | enabled: true, 77 | }, 78 | socialProviders: { // [!code highlight] 79 | github: { // [!code highlight] 80 | clientId: process.env.GITHUB_CLIENT_ID, // [!code highlight] 81 | clientSecret: process.env.GITHUB_CLIENT_SECRET, // [!code highlight] 82 | } // [!code highlight] 83 | } // [!code highlight] 84 | }) 85 | ``` 86 | </Step> 87 | <Step> 88 | ### Add Plugins (Optional) 89 | 90 | You can add the following plugins to your auth config based on your needs. 91 | 92 | [Admin](/docs/plugins/admin) Plugin will allow you to manage users, user impersonations and app level roles and permissions. 93 | 94 | [Two Factor](/docs/plugins/2fa) Plugin will allow you to add two-factor authentication to your application. 95 | 96 | [Phone Number](/docs/plugins/phone-number) Plugin will allow you to add phone number authentication to your application. 97 | 98 | [Username](/docs/plugins/username) Plugin will allow you to add username authentication to your application. 99 | 100 | ```ts title="auth.ts" 101 | import { Pool } from "pg"; 102 | import { betterAuth } from "better-auth"; 103 | import { admin, twoFactor, phoneNumber, username } from "better-auth/plugins"; 104 | 105 | export const auth = betterAuth({ 106 | database: new Pool({ 107 | connectionString: process.env.DATABASE_URL 108 | }), 109 | emailAndPassword: { 110 | enabled: true, 111 | }, 112 | socialProviders: { 113 | github: { 114 | clientId: process.env.GITHUB_CLIENT_ID!, 115 | clientSecret: process.env.GITHUB_CLIENT_SECRET!, 116 | } 117 | }, 118 | plugins: [admin(), twoFactor(), phoneNumber(), username()], // [!code highlight] 119 | }) 120 | ``` 121 | </Step> 122 | <Step> 123 | ### Generate Schema 124 | 125 | If you're using a custom database adapter, generate the schema: 126 | 127 | ```sh 128 | npx @better-auth/cli generate 129 | ``` 130 | 131 | or if you're using the default adapter, you can use the following command: 132 | 133 | ```sh 134 | npx @better-auth/cli migrate 135 | ``` 136 | </Step> 137 | <Step> 138 | ### Export Clerk Users 139 | Go to the Clerk dashboard and export the users. Check how to do it [here](https://clerk.com/docs/deployments/exporting-users#export-your-users-data-from-the-clerk-dashboard). It will download a CSV file with the users data. You need to save it as `exported_users.csv` and put it in the root of your project. 140 | </Step> 141 | <Step> 142 | ### Create the migration script 143 | 144 | Create a new file called `migrate-clerk.ts` in the `scripts` folder and add the following code: 145 | 146 | ```ts title="scripts/migrate-clerk.ts" 147 | import { generateRandomString, symmetricEncrypt } from "better-auth/crypto"; 148 | 149 | import { auth } from "@/lib/auth"; // import your auth instance 150 | 151 | function getCSVData(csv: string) { 152 | const lines = csv.split('\n').filter(line => line.trim()); 153 | const headers = lines[0]?.split(',').map(header => header.trim()) || []; 154 | const jsonData = lines.slice(1).map(line => { 155 | const values = line.split(',').map(value => value.trim()); 156 | return headers.reduce((obj, header, index) => { 157 | obj[header] = values[index] || ''; 158 | return obj; 159 | }, {} as Record<string, string>); 160 | }); 161 | 162 | return jsonData as Array<{ 163 | id: string; 164 | first_name: string; 165 | last_name: string; 166 | username: string; 167 | primary_email_address: string; 168 | primary_phone_number: string; 169 | verified_email_addresses: string; 170 | unverified_email_addresses: string; 171 | verified_phone_numbers: string; 172 | unverified_phone_numbers: string; 173 | totp_secret: string; 174 | password_digest: string; 175 | password_hasher: string; 176 | }>; 177 | } 178 | 179 | const exportedUserCSV = await Bun.file("exported_users.csv").text(); // this is the file you downloaded from Clerk 180 | 181 | async function getClerkUsers(totalUsers: number) { 182 | const clerkUsers: { 183 | id: string; 184 | first_name: string; 185 | last_name: string; 186 | username: string; 187 | image_url: string; 188 | password_enabled: boolean; 189 | two_factor_enabled: boolean; 190 | totp_enabled: boolean; 191 | backup_code_enabled: boolean; 192 | banned: boolean; 193 | locked: boolean; 194 | lockout_expires_in_seconds: number; 195 | created_at: number; 196 | updated_at: number; 197 | external_accounts: { 198 | id: string; 199 | provider: string; 200 | identification_id: string; 201 | provider_user_id: string; 202 | approved_scopes: string; 203 | email_address: string; 204 | first_name: string; 205 | last_name: string; 206 | image_url: string; 207 | created_at: number; 208 | updated_at: number; 209 | }[] 210 | }[] = []; 211 | for (let i = 0; i < totalUsers; i += 500) { 212 | const response = await fetch(`https://api.clerk.com/v1/users?offset=${i}&limit=${500}`, { 213 | headers: { 214 | 'Authorization': `Bearer ${process.env.CLERK_SECRET_KEY}` 215 | } 216 | }); 217 | if (!response.ok) { 218 | throw new Error(`Failed to fetch users: ${response.statusText}`); 219 | } 220 | const clerkUsersData = await response.json(); 221 | // biome-ignore lint/suspicious/noExplicitAny: <explanation> 222 | clerkUsers.push(...clerkUsersData as any); 223 | } 224 | return clerkUsers; 225 | } 226 | 227 | 228 | export async function generateBackupCodes( 229 | secret: string, 230 | ) { 231 | const key = secret; 232 | const backupCodes = Array.from({ length: 10 }) 233 | .fill(null) 234 | .map(() => generateRandomString(10, "a-z", "0-9", "A-Z")) 235 | .map((code) => `${code.slice(0, 5)}-${code.slice(5)}`); 236 | const encCodes = await symmetricEncrypt({ 237 | data: JSON.stringify(backupCodes), 238 | key: key, 239 | }); 240 | return encCodes 241 | } 242 | 243 | // Helper function to safely convert timestamp to Date 244 | function safeDateConversion(timestamp?: number): Date { 245 | if (!timestamp) return new Date(); 246 | 247 | // Convert seconds to milliseconds 248 | const date = new Date(timestamp * 1000); 249 | 250 | // Check if the date is valid 251 | if (isNaN(date.getTime())) { 252 | console.warn(`Invalid timestamp: ${timestamp}, falling back to current date`); 253 | return new Date(); 254 | } 255 | 256 | // Check for unreasonable dates (before 2000 or after 2100) 257 | const year = date.getFullYear(); 258 | if (year < 2000 || year > 2100) { 259 | console.warn(`Suspicious date year: ${year}, falling back to current date`); 260 | return new Date(); 261 | } 262 | 263 | return date; 264 | } 265 | 266 | async function migrateFromClerk() { 267 | const jsonData = getCSVData(exportedUserCSV); 268 | const clerkUsers = await getClerkUsers(jsonData.length); 269 | const ctx = await auth.$context 270 | const isAdminEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "admin"); 271 | const isTwoFactorEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "two-factor"); 272 | const isUsernameEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "username"); 273 | const isPhoneNumberEnabled = ctx.options?.plugins?.find(plugin => plugin.id === "phone-number"); 274 | for (const user of jsonData) { 275 | const { id, first_name, last_name, username, primary_email_address, primary_phone_number, verified_email_addresses, unverified_email_addresses, verified_phone_numbers, unverified_phone_numbers, totp_secret, password_digest, password_hasher } = user; 276 | const clerkUser = clerkUsers.find(clerkUser => clerkUser?.id === id); 277 | 278 | // create user 279 | const createdUser = await ctx.adapter.create<{ 280 | id: string; 281 | }>({ 282 | model: "user", 283 | data: { 284 | id, 285 | email: primary_email_address, 286 | emailVerified: verified_email_addresses.length > 0, 287 | name: `${first_name} ${last_name}`, 288 | image: clerkUser?.image_url, 289 | createdAt: safeDateConversion(clerkUser?.created_at), 290 | updatedAt: safeDateConversion(clerkUser?.updated_at), 291 | // # Two Factor (if you enabled two factor plugin) 292 | ...(isTwoFactorEnabled ? { 293 | twoFactorEnabled: clerkUser?.two_factor_enabled 294 | } : {}), 295 | // # Admin (if you enabled admin plugin) 296 | ...(isAdminEnabled ? { 297 | banned: clerkUser?.banned, 298 | banExpiresAt: clerkUser?.lockout_expires_in_seconds, 299 | role: "user" 300 | } : {}), 301 | // # Username (if you enabled username plugin) 302 | ...(isUsernameEnabled ? { 303 | username: username, 304 | } : {}), 305 | // # Phone Number (if you enabled phone number plugin) 306 | ...(isPhoneNumberEnabled ? { 307 | phoneNumber: primary_phone_number, 308 | phoneNumberVerified: verified_phone_numbers.length > 0, 309 | } : {}), 310 | }, 311 | forceAllowId: true 312 | }).catch(async e => { 313 | return await ctx.adapter.findOne<{ 314 | id: string; 315 | }>({ 316 | model: "user", 317 | where: [{ 318 | field: "id", 319 | value: id 320 | }] 321 | }) 322 | }) 323 | // create external account 324 | const externalAccounts = clerkUser?.external_accounts; 325 | if (externalAccounts) { 326 | for (const externalAccount of externalAccounts) { 327 | const { id, provider, identification_id, provider_user_id, approved_scopes, email_address, first_name, last_name, image_url, created_at, updated_at } = externalAccount; 328 | if (externalAccount.provider === "credential") { 329 | await ctx.adapter.create({ 330 | model: "account", 331 | data: { 332 | id, 333 | providerId: provider, 334 | accountId: externalAccount.provider_user_id, 335 | scope: approved_scopes, 336 | userId: createdUser?.id, 337 | createdAt: safeDateConversion(created_at), 338 | updatedAt: safeDateConversion(updated_at), 339 | password: password_digest, 340 | } 341 | }) 342 | } else { 343 | await ctx.adapter.create({ 344 | model: "account", 345 | data: { 346 | id, 347 | providerId: provider.replace("oauth_", ""), 348 | accountId: externalAccount.provider_user_id, 349 | scope: approved_scopes, 350 | userId: createdUser?.id, 351 | createdAt: safeDateConversion(created_at), 352 | updatedAt: safeDateConversion(updated_at), 353 | }, 354 | forceAllowId: true 355 | }) 356 | } 357 | } 358 | } 359 | 360 | //two factor 361 | if (isTwoFactorEnabled) { 362 | await ctx.adapter.create({ 363 | model: "twoFactor", 364 | data: { 365 | userId: createdUser?.id, 366 | secret: totp_secret, 367 | backupCodes: await generateBackupCodes(totp_secret) 368 | } 369 | }) 370 | } 371 | } 372 | } 373 | 374 | migrateFromClerk() 375 | .then(() => { 376 | console.log('Migration completed'); 377 | process.exit(0); 378 | }) 379 | .catch((error) => { 380 | console.error('Migration failed:', error); 381 | process.exit(1); 382 | }); 383 | ``` 384 | Make sure to replace the `process.env.CLERK_SECRET_KEY` with your own Clerk secret key. Feel free to customize the script to your needs. 385 | </Step> 386 | 387 | <Step> 388 | ### Run the migration 389 | 390 | Run the migration: 391 | 392 | ```sh 393 | bun run script/migrate-clerk.ts # you can use any thing you like to run the script 394 | ``` 395 | 396 | <Callout type="warning"> 397 | Make sure to: 398 | 1. Test the migration in a development environment first 399 | 2. Monitor the migration process for any errors 400 | 3. Verify the migrated data in Better Auth before proceeding 401 | 4. Keep Clerk installed and configured until the migration is complete 402 | </Callout> 403 | 404 | </Step> 405 | <Step> 406 | ### Verify the migration 407 | 408 | After running the migration, verify that all users have been properly migrated by checking the database. 409 | </Step> 410 | <Step> 411 | ### Update your components 412 | 413 | Now that the data is migrated, you can start updating your components to use Better Auth. Here's an example for the sign-in component: 414 | 415 | ```tsx title="components/auth/sign-in.tsx" 416 | import { authClient } from "better-auth/client"; 417 | 418 | export const SignIn = () => { 419 | const handleSignIn = async () => { 420 | const { data, error } = await authClient.signIn.email({ 421 | email: "[email protected]", 422 | password: "password", 423 | }); 424 | 425 | if (error) { 426 | console.error(error); 427 | return; 428 | } 429 | // Handle successful sign in 430 | }; 431 | 432 | return ( 433 | <form onSubmit={handleSignIn}> 434 | <button type="submit">Sign in</button> 435 | </form> 436 | ); 437 | }; 438 | ``` 439 | </Step> 440 | <Step> 441 | ### Update the middleware 442 | 443 | Replace your Clerk middleware with Better Auth's middleware: 444 | 445 | ```ts title="middleware.ts" 446 | 447 | import { NextRequest, NextResponse } from "next/server"; 448 | import { getSessionCookie } from "better-auth/cookies"; 449 | export async function middleware(request: NextRequest) { 450 | const sessionCookie = getSessionCookie(request); 451 | const { pathname } = request.nextUrl; 452 | if (sessionCookie && ["/login", "/signup"].includes(pathname)) { 453 | return NextResponse.redirect(new URL("/dashboard", request.url)); 454 | } 455 | if (!sessionCookie && pathname.startsWith("/dashboard")) { 456 | return NextResponse.redirect(new URL("/login", request.url)); 457 | } 458 | return NextResponse.next(); 459 | } 460 | 461 | export const config = { 462 | matcher: ["/dashboard", "/login", "/signup"], 463 | }; 464 | ``` 465 | </Step> 466 | <Step> 467 | ### Remove Clerk Dependencies 468 | 469 | Once you've verified that everything is working correctly with Better Auth, you can remove Clerk: 470 | 471 | ```bash title="Remove Clerk" 472 | pnpm remove @clerk/nextjs @clerk/themes @clerk/types 473 | ``` 474 | </Step> 475 | </Steps> 476 | 477 | ## Additional Resources 478 | 479 | [Goodbye Clerk, Hello Better Auth – Full Migration Guide!](https://www.youtube.com/watch?v=Za_QihbDSuk) 480 | 481 | ## Wrapping Up 482 | 483 | Congratulations! You've successfully migrated from Clerk to Better Auth. 484 | 485 | Better Auth offers greater flexibility and more features—be sure to explore the [documentation](/docs) to unlock its full potential. ``` -------------------------------------------------------------------------------- /packages/better-auth/src/plugins/mcp/mcp.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { afterAll, describe, it } from "vitest"; 2 | import { getTestInstance } from "../../test-utils/test-instance"; 3 | import { mcp, withMcpAuth } from "."; 4 | import { genericOAuth } from "../generic-oauth"; 5 | import type { Client } from "../oidc-provider/types"; 6 | import { createAuthClient } from "../../client"; 7 | import { genericOAuthClient } from "../generic-oauth/client"; 8 | import { listen } from "listhen"; 9 | import { toNodeHandler } from "../../integrations/node"; 10 | import { jwt } from "../jwt"; 11 | 12 | describe("mcp", async () => { 13 | // Start server on ephemeral port first to get available port 14 | const tempServer = await listen( 15 | toNodeHandler(async () => new Response("temp")), 16 | { 17 | port: 0, 18 | }, 19 | ); 20 | const port = tempServer.address?.port || 3001; 21 | const baseURL = `http://localhost:${port}`; 22 | await tempServer.close(); 23 | 24 | const { auth, signInWithTestUser, customFetchImpl, testUser, cookieSetter } = 25 | await getTestInstance({ 26 | baseURL, 27 | plugins: [ 28 | mcp({ 29 | loginPage: "/login", 30 | oidcConfig: { 31 | loginPage: "/login", 32 | requirePKCE: true, 33 | 34 | getAdditionalUserInfoClaim(user, scopes, client) { 35 | return { 36 | custom: "custom value", 37 | userId: user.id, 38 | }; 39 | }, 40 | }, 41 | }), 42 | jwt(), 43 | ], 44 | }); 45 | 46 | const signInResult = await signInWithTestUser(); 47 | const headers = signInResult.headers; 48 | 49 | const serverClient = createAuthClient({ 50 | baseURL, 51 | fetchOptions: { 52 | customFetchImpl, 53 | headers, 54 | }, 55 | }); 56 | 57 | const server = await listen(toNodeHandler(auth.handler), { 58 | port, 59 | }); 60 | afterAll(async () => { 61 | await server.close(); 62 | }); 63 | 64 | let publicClient: Client; 65 | let confidentialClient: Client; 66 | 67 | it("should register public client with token_endpoint_auth_method: none", async ({ 68 | expect, 69 | }) => { 70 | const createdClient = await serverClient.$fetch("/mcp/register", { 71 | method: "POST", 72 | body: { 73 | client_name: "test-public-client", 74 | redirect_uris: [ 75 | "http://localhost:3000/api/auth/oauth2/callback/test-public", 76 | ], 77 | logo_uri: "", 78 | token_endpoint_auth_method: "none", 79 | }, 80 | onResponse(context) { 81 | expect(context.response.status).toBe(201); 82 | expect(context.response.headers.get("Content-Type")).toBe( 83 | "application/json", 84 | ); 85 | }, 86 | }); 87 | 88 | expect(createdClient.data).toMatchObject({ 89 | client_id: expect.any(String), 90 | client_name: "test-public-client", 91 | logo_uri: "", 92 | redirect_uris: [ 93 | "http://localhost:3000/api/auth/oauth2/callback/test-public", 94 | ], 95 | grant_types: ["authorization_code"], 96 | response_types: ["code"], 97 | token_endpoint_auth_method: "none", 98 | client_id_issued_at: expect.any(Number), 99 | }); 100 | 101 | // Public clients should NOT receive client_secret or client_secret_expires_at 102 | expect(createdClient.data).not.toHaveProperty("client_secret"); 103 | expect(createdClient.data).not.toHaveProperty("client_secret_expires_at"); 104 | 105 | publicClient = { 106 | clientId: (createdClient.data as any).client_id, 107 | clientSecret: "", // Public clients don't have secrets, but our type expects a string 108 | redirectURLs: (createdClient.data as any).redirect_uris, 109 | metadata: {}, 110 | icon: (createdClient.data as any).logo_uri || "", 111 | type: "public", 112 | disabled: false, 113 | name: (createdClient.data as any).client_name || "", 114 | }; 115 | }); 116 | 117 | it("should register confidential client with client_secret_basic", async ({ 118 | expect, 119 | }) => { 120 | const createdClient = await serverClient.$fetch("/mcp/register", { 121 | method: "POST", 122 | body: { 123 | client_name: "test-confidential-client", 124 | redirect_uris: [ 125 | "http://localhost:3000/api/auth/oauth2/callback/test-confidential", 126 | ], 127 | logo_uri: "", 128 | token_endpoint_auth_method: "client_secret_basic", 129 | }, 130 | }); 131 | 132 | expect(createdClient.data).toMatchObject({ 133 | client_id: expect.any(String), 134 | client_secret: expect.any(String), 135 | client_name: "test-confidential-client", 136 | logo_uri: "", 137 | redirect_uris: [ 138 | "http://localhost:3000/api/auth/oauth2/callback/test-confidential", 139 | ], 140 | grant_types: ["authorization_code"], 141 | response_types: ["code"], 142 | token_endpoint_auth_method: "client_secret_basic", 143 | client_id_issued_at: expect.any(Number), 144 | client_secret_expires_at: 0, 145 | }); 146 | 147 | // Confidential clients should receive client_secret and client_secret_expires_at 148 | expect(createdClient.data).toHaveProperty("client_secret"); 149 | expect(createdClient.data).toHaveProperty("client_secret_expires_at"); 150 | 151 | confidentialClient = { 152 | clientId: (createdClient.data as any).client_id, 153 | clientSecret: (createdClient.data as any).client_secret, 154 | redirectURLs: (createdClient.data as any).redirect_uris, 155 | metadata: {}, 156 | icon: (createdClient.data as any).logo_uri || "", 157 | type: "web", 158 | disabled: false, 159 | name: (createdClient.data as any).client_name || "", 160 | }; 161 | }); 162 | 163 | it("should authenticate public client with PKCE only", async ({ expect }) => { 164 | const { customFetchImpl: customFetchImplRP, cookieSetter } = 165 | await getTestInstance({ 166 | account: { 167 | accountLinking: { 168 | trustedProviders: ["test-public"], 169 | }, 170 | }, 171 | plugins: [ 172 | genericOAuth({ 173 | config: [ 174 | { 175 | providerId: "test-public", 176 | clientId: publicClient.clientId, 177 | clientSecret: "", // Public client has no secret 178 | authorizationUrl: `${baseURL}/api/auth/mcp/authorize`, 179 | tokenUrl: `${baseURL}/api/auth/mcp/token`, 180 | scopes: ["openid", "profile", "email"], 181 | pkce: true, 182 | }, 183 | ], 184 | }), 185 | ], 186 | }); 187 | 188 | const client = createAuthClient({ 189 | plugins: [genericOAuthClient()], 190 | baseURL: "http://localhost:5001", 191 | fetchOptions: { 192 | customFetchImpl: customFetchImplRP, 193 | }, 194 | }); 195 | const oAuthHeaders = new Headers(); 196 | const data = await client.signIn.oauth2( 197 | { 198 | providerId: "test-public", 199 | callbackURL: "/dashboard", 200 | }, 201 | { 202 | throw: true, 203 | onSuccess: cookieSetter(oAuthHeaders), 204 | }, 205 | ); 206 | 207 | expect(data.url).toContain(`${baseURL}/api/auth/mcp/authorize`); 208 | expect(data.url).toContain(`client_id=${publicClient.clientId}`); 209 | expect(data.url).toContain("code_challenge="); 210 | expect(data.url).toContain("code_challenge_method=S256"); 211 | 212 | let redirectURI = ""; 213 | await serverClient.$fetch(data.url, { 214 | method: "GET", 215 | onError(context: any) { 216 | redirectURI = context.response.headers.get("Location") || ""; 217 | }, 218 | }); 219 | expect(redirectURI).toContain( 220 | "http://localhost:3000/api/auth/oauth2/callback/test-public?code=", 221 | ); 222 | 223 | let callbackURL = ""; 224 | await client.$fetch(redirectURI, { 225 | headers: oAuthHeaders, 226 | onError(context: any) { 227 | callbackURL = context.response.headers.get("Location") || ""; 228 | }, 229 | }); 230 | expect(callbackURL).toContain("/dashboard"); 231 | }); 232 | 233 | it("should reject public client without code_verifier", async ({ 234 | expect, 235 | }) => { 236 | // Create a mock token request without code_verifier 237 | const authCode = "test-auth-code"; 238 | 239 | const result = await serverClient.$fetch("/mcp/token", { 240 | method: "POST", 241 | body: { 242 | grant_type: "authorization_code", 243 | client_id: publicClient.clientId, 244 | code: authCode, 245 | redirect_uri: publicClient.redirectURLs[0], 246 | // Missing code_verifier for public client 247 | }, 248 | }); 249 | 250 | expect(result.error).toBeTruthy(); 251 | expect((result.error as any).error).toBe("invalid_request"); 252 | expect((result.error as any).error_description).toContain( 253 | "code verifier is missing", 254 | ); 255 | }); 256 | 257 | it("should still support confidential clients in MCP context", async ({ 258 | expect, 259 | }) => { 260 | const { customFetchImpl: customFetchImplRP, cookieSetter } = 261 | await getTestInstance({ 262 | account: { 263 | accountLinking: { 264 | trustedProviders: ["test-confidential"], 265 | }, 266 | }, 267 | plugins: [ 268 | genericOAuth({ 269 | config: [ 270 | { 271 | providerId: "test-confidential", 272 | clientId: confidentialClient.clientId, 273 | clientSecret: confidentialClient.clientSecret || "", 274 | authorizationUrl: `${baseURL}/api/auth/mcp/authorize`, 275 | tokenUrl: `${baseURL}/api/auth/mcp/token`, 276 | scopes: ["openid", "profile", "email"], 277 | pkce: true, 278 | }, 279 | ], 280 | }), 281 | ], 282 | }); 283 | const oAuthHeaders = new Headers(); 284 | const client = createAuthClient({ 285 | plugins: [genericOAuthClient()], 286 | baseURL: "http://localhost:5001", 287 | fetchOptions: { 288 | customFetchImpl: customFetchImplRP, 289 | }, 290 | }); 291 | 292 | const data = await client.signIn.oauth2( 293 | { 294 | providerId: "test-confidential", 295 | callbackURL: "/dashboard", 296 | }, 297 | { 298 | throw: true, 299 | onSuccess: cookieSetter(oAuthHeaders), 300 | }, 301 | ); 302 | 303 | expect(data.url).toContain(`${baseURL}/api/auth/mcp/authorize`); 304 | expect(data.url).toContain(`client_id=${confidentialClient.clientId}`); 305 | 306 | let redirectURI = ""; 307 | await serverClient.$fetch(data.url, { 308 | method: "GET", 309 | onError(context: any) { 310 | redirectURI = context.response.headers.get("Location") || ""; 311 | }, 312 | }); 313 | expect(redirectURI).toContain( 314 | "http://localhost:3000/api/auth/oauth2/callback/test-confidential?code=", 315 | ); 316 | 317 | let callbackURL = ""; 318 | await client.$fetch(redirectURI, { 319 | headers: oAuthHeaders, 320 | onError(context: any) { 321 | callbackURL = context.response.headers.get("Location") || ""; 322 | }, 323 | }); 324 | expect(callbackURL).toContain("/dashboard"); 325 | }); 326 | 327 | it("should expose OAuth discovery metadata", async ({ expect }) => { 328 | const metadata = await serverClient.$fetch( 329 | "/.well-known/oauth-authorization-server", 330 | ); 331 | 332 | expect(metadata.data).toMatchObject({ 333 | issuer: baseURL, 334 | authorization_endpoint: `${baseURL}/api/auth/mcp/authorize`, 335 | token_endpoint: `${baseURL}/api/auth/mcp/token`, 336 | userinfo_endpoint: `${baseURL}/api/auth/mcp/userinfo`, 337 | jwks_uri: `${baseURL}/api/auth/mcp/jwks`, 338 | registration_endpoint: `${baseURL}/api/auth/mcp/register`, 339 | scopes_supported: ["openid", "profile", "email", "offline_access"], 340 | response_types_supported: ["code"], 341 | response_modes_supported: ["query"], 342 | grant_types_supported: ["authorization_code", "refresh_token"], 343 | subject_types_supported: ["public"], 344 | id_token_signing_alg_values_supported: ["RS256", "none"], 345 | token_endpoint_auth_methods_supported: [ 346 | "client_secret_basic", 347 | "client_secret_post", 348 | "none", 349 | ], 350 | code_challenge_methods_supported: ["S256"], 351 | claims_supported: [ 352 | "sub", 353 | "iss", 354 | "aud", 355 | "exp", 356 | "nbf", 357 | "iat", 358 | "jti", 359 | "email", 360 | "email_verified", 361 | "name", 362 | ], 363 | }); 364 | }); 365 | 366 | it("should expose OAuth protected resource metadata", async ({ expect }) => { 367 | const metadata = await serverClient.$fetch( 368 | "/.well-known/oauth-protected-resource", 369 | ); 370 | 371 | expect(metadata.data).toMatchObject({ 372 | resource: baseURL, 373 | authorization_servers: [`${baseURL}/api/auth`], 374 | jwks_uri: `${baseURL}/api/auth/mcp/jwks`, 375 | scopes_supported: ["openid", "profile", "email", "offline_access"], 376 | bearer_methods_supported: ["header"], 377 | resource_signing_alg_values_supported: ["RS256", "none"], 378 | }); 379 | }); 380 | 381 | it("should handle token refresh flow", async ({ expect }) => { 382 | // Create a confidential client for easier testing (avoids PKCE complexity) 383 | const createdClient = await serverClient.$fetch("/mcp/register", { 384 | method: "POST", 385 | body: { 386 | client_name: "test-refresh-client", 387 | redirect_uris: [ 388 | "http://localhost:3000/api/auth/oauth2/callback/test-refresh", 389 | ], 390 | logo_uri: "", 391 | token_endpoint_auth_method: "client_secret_basic", 392 | }, 393 | }); 394 | 395 | // Create a mock access token in the database to test refresh functionality 396 | // We'll simulate an existing token with refresh capabilities 397 | const clientId = (createdClient.data as any).client_id; 398 | const clientSecret = (createdClient.data as any).client_secret; 399 | 400 | // Test the refresh token flow by creating a refresh token request 401 | // For this test, we'll verify the endpoint handles refresh_token grant_type 402 | const refreshTokenRequest = await serverClient.$fetch("/mcp/token", { 403 | method: "POST", 404 | body: { 405 | grant_type: "refresh_token", 406 | client_id: clientId, 407 | client_secret: clientSecret, 408 | refresh_token: "invalid-refresh-token", // This should fail but test the flow 409 | }, 410 | }); 411 | 412 | // Should fail with invalid_grant error for invalid refresh token 413 | expect(refreshTokenRequest.error).toBeTruthy(); 414 | expect((refreshTokenRequest.error as any).error).toBe("invalid_grant"); 415 | expect((refreshTokenRequest.error as any).error_description).toContain( 416 | "invalid refresh token", 417 | ); 418 | }); 419 | 420 | it("should return user info from userinfo endpoint", async ({ expect }) => { 421 | // First get an access token through the OAuth flow 422 | const createdClient = await serverClient.$fetch("/mcp/register", { 423 | method: "POST", 424 | body: { 425 | client_name: "test-userinfo-client", 426 | redirect_uris: [ 427 | "http://localhost:3000/api/auth/oauth2/callback/test-userinfo", 428 | ], 429 | logo_uri: "", 430 | token_endpoint_auth_method: "none", 431 | }, 432 | }); 433 | 434 | const userinfoClient = { 435 | clientId: (createdClient.data as any).client_id, 436 | clientSecret: (createdClient.data as any).client_secret, 437 | redirectURLs: (createdClient.data as any).redirect_uris, 438 | }; 439 | 440 | // Set up OAuth flow 441 | const { customFetchImpl: customFetchImplRP } = await getTestInstance({ 442 | account: { 443 | accountLinking: { 444 | trustedProviders: ["test-userinfo"], 445 | }, 446 | }, 447 | plugins: [ 448 | genericOAuth({ 449 | config: [ 450 | { 451 | providerId: "test-userinfo", 452 | clientId: userinfoClient.clientId, 453 | clientSecret: "", 454 | authorizationUrl: `${baseURL}/api/auth/mcp/authorize`, 455 | tokenUrl: `${baseURL}/api/auth/mcp/token`, 456 | scopes: ["openid", "profile", "email"], 457 | pkce: true, 458 | }, 459 | ], 460 | }), 461 | ], 462 | }); 463 | 464 | const client = createAuthClient({ 465 | plugins: [genericOAuthClient()], 466 | baseURL: "http://localhost:5003", 467 | fetchOptions: { 468 | customFetchImpl: customFetchImplRP, 469 | }, 470 | }); 471 | 472 | // Perform OAuth flow 473 | const data = await client.signIn.oauth2( 474 | { 475 | providerId: "test-userinfo", 476 | callbackURL: "/dashboard", 477 | }, 478 | { 479 | throw: true, 480 | }, 481 | ); 482 | 483 | // Follow OAuth flow to get access token (simplified version) 484 | // In a real test, we'd complete the full flow, but for this test we'll 485 | // use the getMcpSession endpoint which validates bearer tokens 486 | 487 | // For now, let's test the userinfo endpoint structure by calling it directly 488 | // This will fail auth but we can check the endpoint exists and returns proper errors 489 | const userinfoResponse = await serverClient.$fetch("/mcp/userinfo", { 490 | method: "GET", 491 | headers: { 492 | Authorization: "Bearer invalid-token", 493 | }, 494 | }); 495 | 496 | // Should return null for invalid token 497 | expect(userinfoResponse.data).toBeNull(); 498 | }); 499 | 500 | it("should handle ID token requests", async ({ expect }) => { 501 | // Create a confidential client to test ID token flow 502 | const createdClient = await serverClient.$fetch("/mcp/register", { 503 | method: "POST", 504 | body: { 505 | client_name: "test-idtoken-client", 506 | redirect_uris: [ 507 | "http://localhost:3000/api/auth/oauth2/callback/test-idtoken", 508 | ], 509 | logo_uri: "", 510 | token_endpoint_auth_method: "client_secret_basic", 511 | }, 512 | }); 513 | 514 | const clientId = (createdClient.data as any).client_id; 515 | const clientSecret = (createdClient.data as any).client_secret; 516 | 517 | // Test that token endpoint handles openid scope properly 518 | // We'll test with invalid code but valid structure to verify ID token logic 519 | const tokenRequest = await serverClient.$fetch("/mcp/token", { 520 | method: "POST", 521 | body: { 522 | grant_type: "authorization_code", 523 | client_id: clientId, 524 | client_secret: clientSecret, 525 | code: "invalid-auth-code", 526 | redirect_uri: (createdClient.data as any).redirect_uris[0], 527 | // Missing code_verifier but that's OK for confidential clients 528 | }, 529 | }); 530 | 531 | // Should fail due to missing code verifier, but this tests the ID token flow exists 532 | expect(tokenRequest.error).toBeTruthy(); 533 | expect((tokenRequest.error as any).error).toBe("invalid_request"); 534 | expect((tokenRequest.error as any).error_description).toContain( 535 | "code verifier is missing", 536 | ); 537 | }); 538 | 539 | describe("withMCPAuth", () => { 540 | it("should return 401 if the request is not authenticated returning the right WWW-Authenticate header", async ({ 541 | expect, 542 | }) => { 543 | // Test the handler using a newly instantiated Request instead of the server, since this route isn't handled by the server 544 | const response = await withMcpAuth(auth, async () => { 545 | // it will never be reached since the request is not authenticated 546 | return new Response("unnecessary"); 547 | })(new Request(`${baseURL}/mcp`)); 548 | 549 | expect(response.status).toBe(401); 550 | expect(response.headers.get("WWW-Authenticate")).toBe( 551 | `Bearer resource_metadata="${baseURL}/api/auth/.well-known/oauth-protected-resource"`, 552 | ); 553 | expect(response.headers.get("Access-Control-Expose-Headers")).toBe( 554 | "WWW-Authenticate", 555 | ); 556 | }); 557 | }); 558 | }); 559 | ``` -------------------------------------------------------------------------------- /docs/content/docs/integrations/expo.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: Expo Integration 3 | description: Integrate Better Auth with Expo. 4 | --- 5 | 6 | Expo is a popular framework for building cross-platform apps with React Native. Better Auth supports both Expo native and web apps. 7 | 8 | ## Installation 9 | 10 | <Steps> 11 | <Step> 12 | ## Configure A Better Auth Backend 13 | Before using Better Auth with Expo, make sure you have a Better Auth backend set up. You can either use a separate server or leverage Expo's new [API Routes](https://docs.expo.dev/router/reference/api-routes) feature to host your Better Auth instance. 14 | 15 | To get started, check out our [installation](/docs/installation) guide for setting up Better Auth on your server. If you prefer to check out the full example, you can find it [here](https://github.com/better-auth/examples/tree/main/expo-example). 16 | 17 | To use the new API routes feature in Expo to host your Better Auth instance you can create a new API route in your Expo app and mount the Better Auth handler. 18 | 19 | ```ts title="app/api/auth/[...auth]+api.ts" 20 | import { auth } from "@/lib/auth"; // import Better Auth handler 21 | 22 | const handler = auth.handler; 23 | export { handler as GET, handler as POST }; // export handler for both GET and POST requests 24 | ``` 25 | </Step> 26 | <Step> 27 | ## Install Server Dependencies 28 | 29 | Install both the Better Auth package and Expo plugin into your server application. 30 | 31 | ```package-install 32 | better-auth @better-auth/expo 33 | ``` 34 | 35 | </Step> 36 | 37 | <Step> 38 | ## Install Client Dependencies 39 | 40 | You also need to install both the Better Auth package and Expo plugin into your Expo application. 41 | 42 | ```package-install 43 | better-auth @better-auth/expo 44 | ``` 45 | 46 | If you plan on using our social integrations (Google, Apple etc.) then there are a few more dependencies that are required in your Expo app. In the default Expo template these are already installed so you may be able to skip this step if you have these dependencies already. 47 | 48 | ```package-install 49 | expo-linking expo-web-browser expo-constants 50 | 51 | ``` 52 | </Step> 53 | 54 | <Step> 55 | ## Add the Expo Plugin on Your Server 56 | 57 | Add the Expo plugin to your Better Auth server. 58 | 59 | ```ts title="lib/auth.ts" 60 | import { betterAuth } from "better-auth"; 61 | import { expo } from "@better-auth/expo"; 62 | 63 | export const auth = betterAuth({ 64 | plugins: [expo()], 65 | emailAndPassword: { 66 | enabled: true, // Enable authentication using email and password. 67 | }, 68 | }); 69 | ``` 70 | </Step> 71 | 72 | <Step> 73 | ## Initialize Better Auth Client 74 | 75 | To initialize Better Auth in your Expo app, you need to call `createAuthClient` with the base URL of your Better Auth backend. Make sure to import the client from `/react`. 76 | 77 | Make sure you install the `expo-secure-store` package into your Expo app. This is used to store the session data and cookies securely. 78 | 79 | ```package-install 80 | expo-secure-store 81 | ``` 82 | 83 | You need to also import client plugin from `@better-auth/expo/client` and pass it to the `plugins` array when initializing the auth client. 84 | 85 | This is important because: 86 | 87 | - **Social Authentication Support:** enables social auth flows by handling authorization URLs and callbacks within the Expo web browser. 88 | - **Secure Cookie Management:** stores cookies securely and automatically adds them to the headers of your auth requests. 89 | 90 | ```ts title="lib/auth-client.ts" 91 | import { createAuthClient } from "better-auth/react"; 92 | import { expoClient } from "@better-auth/expo/client"; 93 | import * as SecureStore from "expo-secure-store"; 94 | 95 | export const authClient = createAuthClient({ 96 | baseURL: "http://localhost:8081", // Base URL of your Better Auth backend. 97 | plugins: [ 98 | expoClient({ 99 | scheme: "myapp", 100 | storagePrefix: "myapp", 101 | storage: SecureStore, 102 | }) 103 | ] 104 | }); 105 | ``` 106 | <Callout> 107 | Be sure to include the full URL, including the path, if you've changed the default path from `/api/auth`. 108 | </Callout> 109 | </Step> 110 | 111 | <Step> 112 | ## Scheme and Trusted Origins 113 | 114 | Better Auth uses deep links to redirect users back to your app after authentication. To enable this, you need to add your app's scheme to the `trustedOrigins` list in your Better Auth config. 115 | 116 | First, make sure you have a scheme defined in your `app.json` file. 117 | 118 | ```json title="app.json" 119 | { 120 | "expo": { 121 | "scheme": "myapp" 122 | } 123 | } 124 | ``` 125 | 126 | Then, update your Better Auth config to include the scheme in the `trustedOrigins` list. 127 | 128 | ```ts title="auth.ts" 129 | export const auth = betterAuth({ 130 | trustedOrigins: ["myapp://"] 131 | }) 132 | ``` 133 | 134 | If you have multiple schemes or need to support deep linking with various paths, you can use specific patterns or wildcards: 135 | 136 | ```ts title="auth.ts" 137 | export const auth = betterAuth({ 138 | trustedOrigins: [ 139 | // Basic scheme 140 | "myapp://", 141 | 142 | // Production & staging schemes 143 | "myapp-prod://", 144 | "myapp-staging://", 145 | 146 | // Wildcard support for all paths following the scheme 147 | "myapp://*" 148 | ] 149 | }) 150 | ``` 151 | 152 | ### Development Mode 153 | 154 | During development, Expo uses the `exp://` scheme with your device's local IP address. To support this, you can use wildcards to match common local IP ranges: 155 | 156 | ```ts title="auth.ts" 157 | export const auth = betterAuth({ 158 | trustedOrigins: [ 159 | "myapp://", 160 | 161 | // Development mode - Expo's exp:// scheme with local IP ranges 162 | ...(process.env.NODE_ENV === "development" ? [ 163 | "exp://*/*", // Trust all Expo development URLs 164 | "exp://10.0.0.*:*/*", // Trust 10.0.0.x IP range 165 | "exp://192.168.*.*:*/*", // Trust 192.168.x.x IP range 166 | "exp://172.*.*.*:*/*", // Trust 172.x.x.x IP range 167 | "exp://localhost:*/*" // Trust localhost 168 | ] : []) 169 | ] 170 | }) 171 | ``` 172 | 173 | <Callout type="warn"> 174 | The wildcard patterns for `exp://` should only be used in development. In production, use your app's specific scheme (e.g., `myapp://`). 175 | </Callout> 176 | </Step> 177 | 178 | <Step> 179 | ## Configure Metro Bundler 180 | 181 | To resolve Better Auth exports you'll need to enable `unstable_enablePackageExports` in your metro config. 182 | 183 | ```js title="metro.config.js" 184 | const { getDefaultConfig } = require("expo/metro-config"); 185 | 186 | const config = getDefaultConfig(__dirname) 187 | 188 | config.resolver.unstable_enablePackageExports = true; // [!code highlight] 189 | 190 | module.exports = config; 191 | ``` 192 | 193 | <Callout>In case you don't have a `metro.config.js` file in your project run `npx expo customize metro.config.js`.</Callout> 194 | 195 | If you can't enable `unstable_enablePackageExports` option, you can use [babel-plugin-module-resolver](https://github.com/tleunen/babel-plugin-module-resolver) to manually resolve the paths. 196 | 197 | ```ts title="babel.config.js" 198 | module.exports = function (api) { 199 | api.cache(true); 200 | return { 201 | presets: ["babel-preset-expo"], 202 | plugins: [ 203 | [ 204 | "module-resolver", 205 | { 206 | alias: { 207 | "better-auth/react": "./node_modules/better-auth/dist/client/react/index.cjs", 208 | "better-auth/client/plugins": "./node_modules/better-auth/dist/client/plugins/index.cjs", 209 | "@better-auth/expo/client": "./node_modules/@better-auth/expo/dist/client.cjs", 210 | }, 211 | }, 212 | ], 213 | ], 214 | } 215 | } 216 | ``` 217 | 218 | <Callout>In case you don't have a `babel.config.js` file in your project run `npx expo customize babel.config.js`.</Callout> 219 | 220 | Don't forget to clear the cache after making changes. 221 | 222 | ```bash 223 | npx expo start --clear 224 | ``` 225 | 226 | </Step> 227 | </Steps> 228 | 229 | 230 | ## Usage 231 | 232 | ### Authenticating Users 233 | 234 | With Better Auth initialized, you can now use the `authClient` to authenticate users in your Expo app. 235 | 236 | <Tabs items={["sign-in", "sign-up"]}> 237 | <Tab value="sign-in"> 238 | ```tsx title="app/sign-in.tsx" 239 | import { useState } from "react"; 240 | import { View, TextInput, Button } from "react-native"; 241 | import { authClient } from "@/lib/auth-client"; 242 | 243 | export default function SignIn() { 244 | const [email, setEmail] = useState(""); 245 | const [password, setPassword] = useState(""); 246 | 247 | const handleLogin = async () => { 248 | await authClient.signIn.email({ 249 | email, 250 | password, 251 | }) 252 | }; 253 | 254 | return ( 255 | <View> 256 | <TextInput 257 | placeholder="Email" 258 | value={email} 259 | onChangeText={setEmail} 260 | /> 261 | <TextInput 262 | placeholder="Password" 263 | value={password} 264 | onChangeText={setPassword} 265 | /> 266 | <Button title="Login" onPress={handleLogin} /> 267 | </View> 268 | ); 269 | } 270 | ``` 271 | </Tab> 272 | <Tab value="sign-up"> 273 | ```tsx title="app/sign-up.tsx" 274 | import { useState } from "react"; 275 | import { View, TextInput, Button } from "react-native"; 276 | import { authClient } from "@/lib/auth-client"; 277 | 278 | export default function SignUp() { 279 | const [email, setEmail] = useState(""); 280 | const [name, setName] = useState(""); 281 | const [password, setPassword] = useState(""); 282 | 283 | const handleLogin = async () => { 284 | await authClient.signUp.email({ 285 | email, 286 | password, 287 | name 288 | }) 289 | }; 290 | 291 | return ( 292 | <View> 293 | <TextInput 294 | placeholder="Name" 295 | value={name} 296 | onChangeText={setName} 297 | /> 298 | <TextInput 299 | placeholder="Email" 300 | value={email} 301 | onChangeText={setEmail} 302 | /> 303 | <TextInput 304 | placeholder="Password" 305 | value={password} 306 | onChangeText={setPassword} 307 | /> 308 | <Button title="Login" onPress={handleLogin} /> 309 | </View> 310 | ); 311 | } 312 | ``` 313 | </Tab> 314 | </Tabs> 315 | 316 | #### Social Sign-In 317 | 318 | For social sign-in, you can use the `authClient.signIn.social` method with the provider name and a callback URL. 319 | 320 | ```tsx title="app/social-sign-in.tsx" 321 | import { Button } from "react-native"; 322 | 323 | export default function SocialSignIn() { 324 | const handleLogin = async () => { 325 | await authClient.signIn.social({ 326 | provider: "google", 327 | callbackURL: "/dashboard" // this will be converted to a deep link (eg. `myapp://dashboard`) on native 328 | }) 329 | }; 330 | return <Button title="Login with Google" onPress={handleLogin} />; 331 | } 332 | ``` 333 | 334 | #### IdToken Sign-In 335 | 336 | If you want to make provider request on the mobile device and then verify the ID token on the server, you can use the `authClient.signIn.social` method with the `idToken` option. 337 | 338 | ```tsx title="app/social-sign-in.tsx" 339 | import { Button } from "react-native"; 340 | 341 | export default function SocialSignIn() { 342 | const handleLogin = async () => { 343 | await authClient.signIn.social({ 344 | provider: "google", // only google, apple and facebook are supported for idToken signIn 345 | idToken: { 346 | token: "...", // ID token from provider 347 | nonce: "...", // nonce from provider (optional) 348 | } 349 | callbackURL: "/dashboard" // this will be converted to a deep link (eg. `myapp://dashboard`) on native 350 | }) 351 | }; 352 | return <Button title="Login with Google" onPress={handleLogin} />; 353 | } 354 | ``` 355 | 356 | ### Session 357 | 358 | Better Auth provides a `useSession` hook to access the current user's session in your app. 359 | 360 | ```tsx title="app/index.tsx" 361 | import { Text } from "react-native"; 362 | import { authClient } from "@/lib/auth-client"; 363 | 364 | export default function Index() { 365 | const { data: session } = authClient.useSession(); 366 | 367 | return <Text>Welcome, {session?.user.name}</Text>; 368 | } 369 | ``` 370 | 371 | On native, the session data will be cached in SecureStore. This will allow you to remove the need for a loading spinner when the app is reloaded. You can disable this behavior by passing the `disableCache` option to the client. 372 | 373 | 374 | ### Making Authenticated Requests to Your Server 375 | 376 | To make authenticated requests to your server that require the user's session, you have to retrieve the session cookie from `SecureStore` and manually add it to your request headers. 377 | 378 | ```tsx 379 | import { authClient } from "@/lib/auth-client"; 380 | 381 | const makeAuthenticatedRequest = async () => { 382 | const cookies = authClient.getCookie(); // [!code highlight] 383 | const headers = { 384 | "Cookie": cookies, // [!code highlight] 385 | }; 386 | const response = await fetch("http://localhost:8081/api/secure-endpoint", { 387 | headers, 388 | // 'include' can interfere with the cookies we just set manually in the headers 389 | credentials: "omit" // [!code highlight] 390 | }); 391 | const data = await response.json(); 392 | return data; 393 | }; 394 | ``` 395 | 396 | **Example: Usage With TRPC** 397 | 398 | ```tsx title="lib/trpc-provider.tsx" 399 | //...other imports 400 | import { authClient } from "@/lib/auth-client"; // [!code highlight] 401 | 402 | export const api = createTRPCReact<AppRouter>(); 403 | 404 | export function TRPCProvider(props: { children: React.ReactNode }) { 405 | const [queryClient] = useState(() => new QueryClient()); 406 | const [trpcClient] = useState(() => 407 | api.createClient({ 408 | links: [ 409 | httpBatchLink({ 410 | //...your other options 411 | headers() { 412 | const headers = new Map<string, string>(); // [!code highlight] 413 | const cookies = authClient.getCookie(); // [!code highlight] 414 | if (cookies) { // [!code highlight] 415 | headers.set("Cookie", cookies); // [!code highlight] 416 | } // [!code highlight] 417 | return Object.fromEntries(headers); // [!code highlight] 418 | }, 419 | }), 420 | ], 421 | }), 422 | ); 423 | 424 | return ( 425 | <api.Provider client={trpcClient} queryClient={queryClient}> 426 | <QueryClientProvider client={queryClient}> 427 | {props.children} 428 | </QueryClientProvider> 429 | </api.Provider> 430 | ); 431 | } 432 | ``` 433 | 434 | 435 | ## Options 436 | 437 | ### Expo Client 438 | 439 | **storage**: the storage mechanism used to cache the session data and cookies. 440 | 441 | ```ts title="lib/auth-client.ts" 442 | import { createAuthClient } from "better-auth/react"; 443 | import SecureStorage from "expo-secure-store"; 444 | 445 | const authClient = createAuthClient({ 446 | baseURL: "http://localhost:8081", 447 | storage: SecureStorage 448 | }); 449 | ``` 450 | 451 | **scheme**: scheme is used to deep link back to your app after a user has authenticated using oAuth providers. By default, Better Auth tries to read the scheme from the `app.json` file. If you need to override this, you can pass the scheme option to the client. 452 | 453 | ```ts title="lib/auth-client.ts" 454 | import { createAuthClient } from "better-auth/react"; 455 | 456 | const authClient = createAuthClient({ 457 | baseURL: "http://localhost:8081", 458 | scheme: "myapp" 459 | }); 460 | ``` 461 | 462 | **disableCache**: By default, the client will cache the session data in SecureStore. You can disable this behavior by passing the `disableCache` option to the client. 463 | 464 | ```ts title="lib/auth-client.ts" 465 | import { createAuthClient } from "better-auth/react"; 466 | 467 | const authClient = createAuthClient({ 468 | baseURL: "http://localhost:8081", 469 | disableCache: true 470 | }); 471 | ``` 472 | 473 | 474 | ### Expo Servers 475 | 476 | Server plugin options: 477 | 478 | **overrideOrigin**: Override the origin for Expo API routes (default: false). Enable this if you're facing cors origin issues with Expo API routes. 479 | ``` -------------------------------------------------------------------------------- /packages/better-auth/src/api/routes/session-api.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { beforeEach, describe, expect, expectTypeOf, it, vi } from "vitest"; 2 | import { getTestInstance } from "../../test-utils/test-instance"; 3 | import { parseSetCookieHeader } from "../../cookies"; 4 | import { getDate } from "../../utils/date"; 5 | import { memoryAdapter, type MemoryDB } from "../../adapters/memory-adapter"; 6 | import { runWithEndpointContext } from "@better-auth/core/context"; 7 | import type { GenericEndpointContext } from "@better-auth/core"; 8 | 9 | describe("session", async () => { 10 | const { client, testUser, sessionSetter, cookieSetter, auth } = 11 | await getTestInstance(); 12 | 13 | it("should set cookies correctly on sign in", async () => { 14 | const headers = new Headers(); 15 | await client.signIn.email( 16 | { 17 | email: testUser.email, 18 | password: testUser.password, 19 | }, 20 | { 21 | onSuccess(context) { 22 | const header = context.response.headers.get("set-cookie"); 23 | const cookies = parseSetCookieHeader(header || ""); 24 | cookieSetter(headers)(context); 25 | const cookie = cookies.get("better-auth.session_token"); 26 | expect(cookie).toMatchObject({ 27 | value: expect.any(String), 28 | "max-age": 60 * 60 * 24 * 7, 29 | path: "/", 30 | samesite: "lax", 31 | httponly: true, 32 | }); 33 | }, 34 | }, 35 | ); 36 | const { data } = await client.getSession({ 37 | fetchOptions: { 38 | headers, 39 | }, 40 | }); 41 | const expiresAt = new Date(data?.session.expiresAt || ""); 42 | const now = new Date(); 43 | 44 | expect(expiresAt.getTime()).toBeGreaterThan( 45 | now.getTime() + 6 * 24 * 60 * 60 * 1000, 46 | ); 47 | }); 48 | 49 | it("should return null when not authenticated", async () => { 50 | const response = await client.getSession(); 51 | expect(response.data).toBeNull(); 52 | }); 53 | 54 | it("should update session when update age is reached", async () => { 55 | const { client, testUser } = await getTestInstance({ 56 | session: { 57 | updateAge: 60, 58 | expiresIn: 60 * 2, 59 | }, 60 | }); 61 | let headers = new Headers(); 62 | 63 | await client.signIn.email( 64 | { 65 | email: testUser.email, 66 | password: testUser.password, 67 | }, 68 | { 69 | onSuccess(context) { 70 | const header = context.response.headers.get("set-cookie"); 71 | const cookies = parseSetCookieHeader(header || ""); 72 | const signedCookie = cookies.get("better-auth.session_token")?.value; 73 | headers.set("cookie", `better-auth.session_token=${signedCookie}`); 74 | }, 75 | }, 76 | ); 77 | 78 | const data = await client.getSession({ 79 | fetchOptions: { 80 | headers, 81 | throw: true, 82 | }, 83 | }); 84 | 85 | if (!data) { 86 | throw new Error("No session found"); 87 | } 88 | expect(new Date(data?.session.expiresAt).getTime()).toBeGreaterThan( 89 | new Date(Date.now() + 1000 * 2 * 59).getTime(), 90 | ); 91 | 92 | expect(new Date(data?.session.expiresAt).getTime()).toBeLessThan( 93 | new Date(Date.now() + 1000 * 2 * 60).getTime(), 94 | ); 95 | for (const t of [60, 80, 100, 121]) { 96 | const span = new Date(); 97 | span.setSeconds(span.getSeconds() + t); 98 | vi.setSystemTime(span); 99 | const response = await client.getSession({ 100 | fetchOptions: { 101 | headers, 102 | onSuccess(context) { 103 | const parsed = parseSetCookieHeader( 104 | context.response.headers.get("set-cookie") || "", 105 | ); 106 | const maxAge = parsed.get("better-auth.session_token")?.["max-age"]; 107 | expect(maxAge).toBe(t === 121 ? 0 : 60 * 2); 108 | }, 109 | }, 110 | }); 111 | if (t === 121) { 112 | //expired 113 | expect(response.data).toBeNull(); 114 | } else { 115 | expect( 116 | new Date(response.data?.session.expiresAt!).getTime(), 117 | ).toBeGreaterThan(new Date(Date.now() + 1000 * 2 * 59).getTime()); 118 | } 119 | } 120 | vi.useRealTimers(); 121 | }); 122 | 123 | it("should update the session every time when set to 0", async () => { 124 | const { client, signInWithTestUser } = await getTestInstance({ 125 | session: { 126 | updateAge: 0, 127 | }, 128 | }); 129 | const { runWithUser } = await signInWithTestUser(); 130 | 131 | await runWithUser(async () => { 132 | const session = await client.getSession(); 133 | 134 | vi.useFakeTimers(); 135 | await vi.advanceTimersByTimeAsync(1000 * 60 * 5); 136 | const session2 = await client.getSession(); 137 | expect(session2.data?.session.expiresAt).not.toBe( 138 | session.data?.session.expiresAt, 139 | ); 140 | expect( 141 | new Date(session2.data!.session.expiresAt).getTime(), 142 | ).toBeGreaterThan(new Date(session.data!.session.expiresAt).getTime()); 143 | }); 144 | }); 145 | 146 | it("should handle 'don't remember me' option", async () => { 147 | let headers = new Headers(); 148 | const res = await client.signIn.email( 149 | { 150 | email: testUser.email, 151 | password: testUser.password, 152 | rememberMe: false, 153 | }, 154 | { 155 | onSuccess(context) { 156 | const header = context.response.headers.get("set-cookie"); 157 | const cookies = parseSetCookieHeader(header || ""); 158 | const signedCookie = cookies.get("better-auth.session_token")?.value; 159 | const dontRememberMe = cookies.get( 160 | "better-auth.dont_remember", 161 | )?.value; 162 | headers.set( 163 | "cookie", 164 | `better-auth.session_token=${signedCookie};better-auth.dont_remember=${dontRememberMe}`, 165 | ); 166 | }, 167 | }, 168 | ); 169 | const data = await client.getSession({ 170 | fetchOptions: { 171 | headers, 172 | throw: true, 173 | }, 174 | }); 175 | if (!data) { 176 | throw new Error("No session found"); 177 | } 178 | const expiresAt = data.session.expiresAt; 179 | expect(new Date(expiresAt).valueOf()).toBeLessThanOrEqual( 180 | getDate(1000 * 60 * 60 * 24).valueOf(), 181 | ); 182 | const response = await client.getSession({ 183 | fetchOptions: { 184 | headers, 185 | }, 186 | }); 187 | 188 | if (!response.data?.session) { 189 | throw new Error("No session found"); 190 | } 191 | // Check that the session wasn't update 192 | expect( 193 | new Date(response.data.session.expiresAt).valueOf(), 194 | ).toBeLessThanOrEqual(getDate(1000 * 60 * 60 * 24).valueOf()); 195 | }); 196 | 197 | it("should set cookies correctly on sign in after changing config", async () => { 198 | const headers = new Headers(); 199 | await client.signIn.email( 200 | { 201 | email: testUser.email, 202 | password: testUser.password, 203 | }, 204 | { 205 | onSuccess(context) { 206 | const header = context.response.headers.get("set-cookie"); 207 | const cookies = parseSetCookieHeader(header || ""); 208 | expect(cookies.get("better-auth.session_token")).toMatchObject({ 209 | value: expect.any(String), 210 | "max-age": 60 * 60 * 24 * 7, 211 | path: "/", 212 | httponly: true, 213 | samesite: "lax", 214 | }); 215 | headers.set( 216 | "cookie", 217 | `better-auth.session_token=${ 218 | cookies.get("better-auth.session_token")?.value 219 | }`, 220 | ); 221 | }, 222 | }, 223 | ); 224 | const data = await client.getSession({ 225 | fetchOptions: { 226 | headers, 227 | throw: true, 228 | }, 229 | }); 230 | if (!data) { 231 | throw new Error("No session found"); 232 | } 233 | const expiresAt = new Date(data?.session?.expiresAt || ""); 234 | const now = new Date(); 235 | 236 | expect(expiresAt.getTime()).toBeGreaterThan( 237 | now.getTime() + 6 * 24 * 60 * 60 * 1000, 238 | ); 239 | }); 240 | 241 | it("should clear session on sign out", async () => { 242 | let headers = new Headers(); 243 | const res = await client.signIn.email( 244 | { 245 | email: testUser.email, 246 | password: testUser.password, 247 | }, 248 | { 249 | onSuccess(context) { 250 | const header = context.response.headers.get("set-cookie"); 251 | const cookies = parseSetCookieHeader(header || ""); 252 | const signedCookie = cookies.get("better-auth.session_token")?.value; 253 | headers.set("cookie", `better-auth.session_token=${signedCookie}`); 254 | }, 255 | }, 256 | ); 257 | const data = await client.getSession({ 258 | fetchOptions: { 259 | headers, 260 | throw: true, 261 | }, 262 | }); 263 | 264 | expect(data).not.toBeNull(); 265 | await client.signOut({ 266 | fetchOptions: { 267 | headers, 268 | }, 269 | }); 270 | const response = await client.getSession({ 271 | fetchOptions: { 272 | headers, 273 | }, 274 | }); 275 | expect(response.data); 276 | }); 277 | 278 | it("should list sessions", async () => { 279 | const headers = new Headers(); 280 | await client.signIn.email( 281 | { 282 | email: testUser.email, 283 | password: testUser.password, 284 | }, 285 | { 286 | onSuccess: sessionSetter(headers), 287 | }, 288 | ); 289 | 290 | const response = await client.listSessions({ 291 | fetchOptions: { 292 | headers, 293 | }, 294 | }); 295 | 296 | expect(response.data?.length).toBeGreaterThan(1); 297 | }); 298 | 299 | it("should revoke session", async () => { 300 | const headers = new Headers(); 301 | const headers2 = new Headers(); 302 | const res = await client.signIn.email({ 303 | email: testUser.email, 304 | password: testUser.password, 305 | fetchOptions: { 306 | onSuccess: sessionSetter(headers), 307 | }, 308 | }); 309 | await client.signIn.email({ 310 | email: testUser.email, 311 | password: testUser.password, 312 | fetchOptions: { 313 | onSuccess: sessionSetter(headers2), 314 | }, 315 | }); 316 | const session = await client.getSession({ 317 | fetchOptions: { 318 | headers, 319 | throw: true, 320 | }, 321 | }); 322 | await client.revokeSession({ 323 | fetchOptions: { 324 | headers, 325 | }, 326 | token: session?.session?.token || "", 327 | }); 328 | const newSession = await client.getSession({ 329 | fetchOptions: { 330 | headers, 331 | }, 332 | }); 333 | expect(newSession.data).toBeNull(); 334 | const revokeRes = await client.revokeSessions({ 335 | fetchOptions: { 336 | headers: headers2, 337 | }, 338 | }); 339 | expect(revokeRes.data?.status).toBe(true); 340 | }); 341 | 342 | it("should return session headers", async () => { 343 | const context = await auth.$context; 344 | await runWithEndpointContext( 345 | { 346 | context, 347 | } as unknown as GenericEndpointContext, 348 | async () => { 349 | const signInRes = await auth.api.signInEmail({ 350 | body: { 351 | email: testUser.email, 352 | password: testUser.password, 353 | }, 354 | returnHeaders: true, 355 | }); 356 | 357 | const signInHeaders = new Headers(); 358 | signInHeaders.set("cookie", signInRes.headers.getSetCookie()[0]!); 359 | 360 | const sessionResWithoutHeaders = await auth.api.getSession({ 361 | headers: signInHeaders, 362 | }); 363 | 364 | const sessionResWithHeaders = await auth.api.getSession({ 365 | headers: signInHeaders, 366 | returnHeaders: true, 367 | }); 368 | 369 | expect(sessionResWithHeaders.headers).toBeDefined(); 370 | expect(sessionResWithHeaders.response?.user).toBeDefined(); 371 | expect(sessionResWithHeaders.response?.session).toBeDefined(); 372 | expectTypeOf({ 373 | headers: sessionResWithHeaders.headers, 374 | }).toMatchObjectType<{ 375 | headers: Headers; 376 | }>(); 377 | 378 | // @ts-expect-error: headers should not exist on sessionResWithoutHeaders 379 | expect(sessionResWithoutHeaders.headers).toBeUndefined(); 380 | 381 | const sessionResWithHeadersAndAsResponse = await auth.api.getSession({ 382 | headers: signInHeaders, 383 | returnHeaders: true, 384 | asResponse: true, 385 | }); 386 | 387 | expectTypeOf({ 388 | res: sessionResWithHeadersAndAsResponse, 389 | }).toMatchObjectType<{ res: Response }>(); 390 | 391 | expect(sessionResWithHeadersAndAsResponse.ok).toBe(true); 392 | expect(sessionResWithHeadersAndAsResponse.status).toBe(200); 393 | }, 394 | ); 395 | }); 396 | }); 397 | 398 | describe("session storage", async () => { 399 | let store = new Map<string, string>(); 400 | const { client, signInWithTestUser, db } = await getTestInstance({ 401 | secondaryStorage: { 402 | set(key, value, ttl) { 403 | store.set(key, value); 404 | }, 405 | get(key) { 406 | return store.get(key) || null; 407 | }, 408 | delete(key) { 409 | store.delete(key); 410 | }, 411 | }, 412 | rateLimit: { 413 | enabled: false, 414 | }, 415 | }); 416 | 417 | beforeEach(() => { 418 | store.clear(); 419 | }); 420 | 421 | it("should store session in secondary storage", async () => { 422 | //since the instance creates a session on init, we expect the store to have 2 item (1 for session and 1 for active sessions record for the user) 423 | expect(store.size).toBe(0); 424 | const { runWithUser } = await signInWithTestUser(); 425 | expect(store.size).toBe(2); 426 | await runWithUser(async () => { 427 | const session = await client.getSession(); 428 | expect(session.data).toMatchObject({ 429 | session: { 430 | userId: expect.any(String), 431 | token: expect.any(String), 432 | expiresAt: expect.any(Date), 433 | ipAddress: expect.any(String), 434 | userAgent: expect.any(String), 435 | }, 436 | user: { 437 | id: expect.any(String), 438 | name: "test user", 439 | email: "[email protected]", 440 | emailVerified: false, 441 | image: null, 442 | createdAt: expect.any(Date), 443 | updatedAt: expect.any(Date), 444 | }, 445 | }); 446 | }); 447 | }); 448 | 449 | it("should list sessions", async () => { 450 | const { runWithUser } = await signInWithTestUser(); 451 | await runWithUser(async () => { 452 | const response = await client.listSessions(); 453 | expect(response.data?.length).toBe(1); 454 | }); 455 | }); 456 | 457 | it("revoke session and list sessions", async () => { 458 | const { runWithUser } = await signInWithTestUser(); 459 | await runWithUser(async () => { 460 | const session = await client.getSession(); 461 | expect(session.data).not.toBeNull(); 462 | expect(session.data?.session?.token).toBeDefined(); 463 | const userId = session.data!.session.userId; 464 | const sessions = JSON.parse(store.get(`active-sessions-${userId}`)!); 465 | expect(sessions.length).toBe(1); 466 | const res = await client.revokeSession({ 467 | token: session.data?.session?.token!, 468 | }); 469 | expect(res.data?.status).toBe(true); 470 | const response = await client.listSessions(); 471 | expect(response.data).toBe(null); 472 | expect(store.size).toBe(0); 473 | }); 474 | }); 475 | 476 | it("should revoke session", async () => { 477 | const { runWithUser } = await signInWithTestUser(); 478 | await runWithUser(async () => { 479 | const session = await client.getSession(); 480 | expect(session.data).not.toBeNull(); 481 | const res = await client.revokeSession({ 482 | token: session.data?.session?.token || "", 483 | }); 484 | const revokedSession = await client.getSession(); 485 | expect(revokedSession.data).toBeNull(); 486 | }); 487 | }); 488 | }); 489 | 490 | describe("cookie cache", async () => { 491 | const database: MemoryDB = { 492 | user: [], 493 | account: [], 494 | session: [], 495 | verification: [], 496 | }; 497 | const adapter = memoryAdapter(database); 498 | 499 | const { client, testUser, auth, cookieSetter } = await getTestInstance({ 500 | database: adapter, 501 | session: { 502 | additionalFields: { 503 | sensitiveData: { 504 | type: "string", 505 | returned: false, 506 | defaultValue: "sensitive-data", 507 | }, 508 | }, 509 | cookieCache: { 510 | enabled: true, 511 | }, 512 | }, 513 | }); 514 | const ctx = await auth.$context; 515 | 516 | it("should cache cookies", async () => {}); 517 | const fn = vi.spyOn(ctx.adapter, "findOne"); 518 | 519 | const headers = new Headers(); 520 | it("should cache cookies", async () => { 521 | await client.signIn.email( 522 | { 523 | email: testUser.email, 524 | password: testUser.password, 525 | }, 526 | { 527 | onSuccess(context) { 528 | const header = context.response.headers.get("set-cookie"); 529 | const cookies = parseSetCookieHeader(header || ""); 530 | headers.set( 531 | "cookie", 532 | `better-auth.session_token=${ 533 | cookies.get("better-auth.session_token")?.value 534 | };better-auth.session_data=${ 535 | cookies.get("better-auth.session_data")?.value 536 | }`, 537 | ); 538 | }, 539 | }, 540 | ); 541 | expect(fn).toHaveBeenCalledTimes(1); 542 | const session = await client.getSession({ 543 | fetchOptions: { 544 | headers, 545 | }, 546 | }); 547 | expect(session.data?.session).not.toHaveProperty("sensitiveData"); 548 | expect(session.data).not.toBeNull(); 549 | expect(fn).toHaveBeenCalledTimes(1); 550 | }); 551 | 552 | it("should disable cookie cache", async () => { 553 | const ctx = await auth.$context; 554 | 555 | const s = await client.getSession({ 556 | fetchOptions: { 557 | headers, 558 | }, 559 | }); 560 | expect(s.data?.user.emailVerified).toBe(false); 561 | await runWithEndpointContext( 562 | { 563 | context: ctx, 564 | } as unknown as GenericEndpointContext, 565 | async () => { 566 | await ctx.internalAdapter.updateUser(s.data?.user.id || "", { 567 | emailVerified: true, 568 | }); 569 | }, 570 | ); 571 | expect(fn).toHaveBeenCalledTimes(1); 572 | 573 | const session = await client.getSession({ 574 | query: { 575 | disableCookieCache: true, 576 | }, 577 | fetchOptions: { 578 | headers, 579 | }, 580 | }); 581 | expect(session.data?.user.emailVerified).toBe(true); 582 | expect(session.data).not.toBeNull(); 583 | expect(fn).toHaveBeenCalledTimes(3); 584 | }); 585 | 586 | it("should reset cache when expires", async () => { 587 | expect(fn).toHaveBeenCalledTimes(3); 588 | await client.getSession({ 589 | fetchOptions: { 590 | headers, 591 | }, 592 | }); 593 | vi.useFakeTimers(); 594 | await vi.advanceTimersByTimeAsync(1000 * 60 * 10); // 10 minutes 595 | await client.getSession({ 596 | fetchOptions: { 597 | headers, 598 | onSuccess(context) { 599 | cookieSetter(headers)(context); 600 | }, 601 | }, 602 | }); 603 | expect(fn).toHaveBeenCalledTimes(5); 604 | await client.getSession({ 605 | fetchOptions: { 606 | headers, 607 | onSuccess(context) { 608 | cookieSetter(headers)(context); 609 | }, 610 | }, 611 | }); 612 | expect(fn).toHaveBeenCalledTimes(5); 613 | }); 614 | }); 615 | 616 | describe("getSession type tests", async () => { 617 | const { auth } = await getTestInstance(); 618 | 619 | it("has parameters", () => { 620 | type Params = Parameters<typeof auth.api.getSession>[0]["headers"]; 621 | 622 | expectTypeOf<Params>().toEqualTypeOf< 623 | [string, string][] | Record<string, string> | Headers 624 | >(); 625 | }); 626 | 627 | it("can return a response", () => { 628 | type Returns = Awaited<ReturnType<typeof auth.api.getSession<true, false>>>; 629 | 630 | expectTypeOf<{ returns: Returns }>().toMatchObjectType<{ 631 | returns: Response; 632 | }>(); 633 | }); 634 | 635 | it("can return headers", () => { 636 | type Returns = Awaited<ReturnType<typeof auth.api.getSession<false, true>>>; 637 | 638 | expectTypeOf<{ returns: Returns["headers"] }>().toMatchObjectType<{ 639 | returns: Headers; 640 | }>(); 641 | }); 642 | 643 | it("asResponse takes prescedence", () => { 644 | type Returns = Awaited<ReturnType<typeof auth.api.getSession<true, true>>>; 645 | 646 | expectTypeOf<{ returns: Returns }>().toMatchObjectType<{ 647 | returns: Response; 648 | }>(); 649 | }); 650 | }); 651 | ```