#
tokens: 49129/50000 6/1097 files (page 37/68)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 37 of 68. 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-isolated-module-bundler
│       │   │   │   ├── package.json
│       │   │   │   ├── src
│       │   │   │   │   └── index.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

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/username/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as z from "zod";
  2 | import {
  3 | 	createAuthEndpoint,
  4 | 	createAuthMiddleware,
  5 | } from "@better-auth/core/api";
  6 | import type { BetterAuthPlugin } from "@better-auth/core";
  7 | import { APIError } from "better-call";
  8 | import type { Account, InferOptionSchema, User } from "../../types";
  9 | import { setSessionCookie } from "../../cookies";
 10 | import { BASE_ERROR_CODES } from "@better-auth/core/error";
 11 | import { getSchema, type UsernameSchema } from "./schema";
 12 | import { mergeSchema } from "../../db";
 13 | import { USERNAME_ERROR_CODES as ERROR_CODES } from "./error-codes";
 14 | import { createEmailVerificationToken } from "../../api";
 15 | 
 16 | export { USERNAME_ERROR_CODES } from "./error-codes";
 17 | 
 18 | export type UsernameOptions = {
 19 | 	schema?: InferOptionSchema<UsernameSchema>;
 20 | 	/**
 21 | 	 * The minimum length of the username
 22 | 	 *
 23 | 	 * @default 3
 24 | 	 */
 25 | 	minUsernameLength?: number;
 26 | 	/**
 27 | 	 * The maximum length of the username
 28 | 	 *
 29 | 	 * @default 30
 30 | 	 */
 31 | 	maxUsernameLength?: number;
 32 | 	/**
 33 | 	 * A function to validate the username
 34 | 	 *
 35 | 	 * By default, the username should only contain alphanumeric characters and underscores
 36 | 	 */
 37 | 	usernameValidator?: (username: string) => boolean | Promise<boolean>;
 38 | 	/**
 39 | 	 * A function to validate the display username
 40 | 	 *
 41 | 	 * By default, no validation is applied to display username
 42 | 	 */
 43 | 	displayUsernameValidator?: (
 44 | 		displayUsername: string,
 45 | 	) => boolean | Promise<boolean>;
 46 | 	/**
 47 | 	 * A function to normalize the username
 48 | 	 *
 49 | 	 * @default (username) => username.toLowerCase()
 50 | 	 */
 51 | 	usernameNormalization?: ((username: string) => string) | false;
 52 | 	/**
 53 | 	 * A function to normalize the display username
 54 | 	 *
 55 | 	 * @default false
 56 | 	 */
 57 | 	displayUsernameNormalization?: ((displayUsername: string) => string) | false;
 58 | 	/**
 59 | 	 * The order of validation
 60 | 	 *
 61 | 	 * @default { username: "pre-normalization", displayUsername: "pre-normalization" }
 62 | 	 */
 63 | 	validationOrder?: {
 64 | 		/**
 65 | 		 * The order of username validation
 66 | 		 *
 67 | 		 * @default "pre-normalization"
 68 | 		 */
 69 | 		username?: "pre-normalization" | "post-normalization";
 70 | 		/**
 71 | 		 * The order of display username validation
 72 | 		 *
 73 | 		 * @default "pre-normalization"
 74 | 		 */
 75 | 		displayUsername?: "pre-normalization" | "post-normalization";
 76 | 	};
 77 | };
 78 | 
 79 | function defaultUsernameValidator(username: string) {
 80 | 	return /^[a-zA-Z0-9_.]+$/.test(username);
 81 | }
 82 | 
 83 | export const username = (options?: UsernameOptions) => {
 84 | 	const normalizer = (username: string) => {
 85 | 		if (options?.usernameNormalization === false) {
 86 | 			return username;
 87 | 		}
 88 | 		if (options?.usernameNormalization) {
 89 | 			return options.usernameNormalization(username);
 90 | 		}
 91 | 		return username.toLowerCase();
 92 | 	};
 93 | 
 94 | 	const displayUsernameNormalizer = (displayUsername: string) => {
 95 | 		return options?.displayUsernameNormalization
 96 | 			? options.displayUsernameNormalization(displayUsername)
 97 | 			: displayUsername;
 98 | 	};
 99 | 
100 | 	return {
101 | 		id: "username",
102 | 		init(ctx) {
103 | 			return {
104 | 				options: {
105 | 					databaseHooks: {
106 | 						user: {
107 | 							create: {
108 | 								async before(user, context) {
109 | 									const username =
110 | 										"username" in user ? (user.username as string) : null;
111 | 									const displayUsername =
112 | 										"displayUsername" in user
113 | 											? (user.displayUsername as string)
114 | 											: null;
115 | 
116 | 									return {
117 | 										data: {
118 | 											...user,
119 | 											...(username ? { username: normalizer(username) } : {}),
120 | 											...(displayUsername
121 | 												? {
122 | 														displayUsername:
123 | 															displayUsernameNormalizer(displayUsername),
124 | 													}
125 | 												: {}),
126 | 										},
127 | 									};
128 | 								},
129 | 							},
130 | 							update: {
131 | 								async before(user, context) {
132 | 									const username =
133 | 										"username" in user ? (user.username as string) : null;
134 | 									const displayUsername =
135 | 										"displayUsername" in user
136 | 											? (user.displayUsername as string)
137 | 											: null;
138 | 
139 | 									return {
140 | 										data: {
141 | 											...user,
142 | 											...(username ? { username: normalizer(username) } : {}),
143 | 											...(displayUsername
144 | 												? {
145 | 														displayUsername:
146 | 															displayUsernameNormalizer(displayUsername),
147 | 													}
148 | 												: {}),
149 | 										},
150 | 									};
151 | 								},
152 | 							},
153 | 						},
154 | 					},
155 | 				},
156 | 			};
157 | 		},
158 | 		endpoints: {
159 | 			signInUsername: createAuthEndpoint(
160 | 				"/sign-in/username",
161 | 				{
162 | 					method: "POST",
163 | 					body: z.object({
164 | 						username: z
165 | 							.string()
166 | 							.meta({ description: "The username of the user" }),
167 | 						password: z
168 | 							.string()
169 | 							.meta({ description: "The password of the user" }),
170 | 						rememberMe: z
171 | 							.boolean()
172 | 							.meta({
173 | 								description: "Remember the user session",
174 | 							})
175 | 							.optional(),
176 | 						callbackURL: z
177 | 							.string()
178 | 							.meta({
179 | 								description: "The URL to redirect to after email verification",
180 | 							})
181 | 							.optional(),
182 | 					}),
183 | 					metadata: {
184 | 						openapi: {
185 | 							summary: "Sign in with username",
186 | 							description: "Sign in with username",
187 | 							responses: {
188 | 								200: {
189 | 									description: "Success",
190 | 									content: {
191 | 										"application/json": {
192 | 											schema: {
193 | 												type: "object",
194 | 												properties: {
195 | 													token: {
196 | 														type: "string",
197 | 														description:
198 | 															"Session token for the authenticated session",
199 | 													},
200 | 													user: {
201 | 														$ref: "#/components/schemas/User",
202 | 													},
203 | 												},
204 | 												required: ["token", "user"],
205 | 											},
206 | 										},
207 | 									},
208 | 								},
209 | 								422: {
210 | 									description: "Unprocessable Entity. Validation error",
211 | 									content: {
212 | 										"application/json": {
213 | 											schema: {
214 | 												type: "object",
215 | 												properties: {
216 | 													message: {
217 | 														type: "string",
218 | 													},
219 | 												},
220 | 											},
221 | 										},
222 | 									},
223 | 								},
224 | 							},
225 | 						},
226 | 					},
227 | 				},
228 | 				async (ctx) => {
229 | 					if (!ctx.body.username || !ctx.body.password) {
230 | 						ctx.context.logger.error("Username or password not found");
231 | 						throw new APIError("UNAUTHORIZED", {
232 | 							message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
233 | 						});
234 | 					}
235 | 
236 | 					const username =
237 | 						options?.validationOrder?.username === "pre-normalization"
238 | 							? normalizer(ctx.body.username)
239 | 							: ctx.body.username;
240 | 
241 | 					const minUsernameLength = options?.minUsernameLength || 3;
242 | 					const maxUsernameLength = options?.maxUsernameLength || 30;
243 | 
244 | 					if (username.length < minUsernameLength) {
245 | 						ctx.context.logger.error("Username too short", {
246 | 							username,
247 | 						});
248 | 						throw new APIError("UNPROCESSABLE_ENTITY", {
249 | 							message: ERROR_CODES.USERNAME_TOO_SHORT,
250 | 						});
251 | 					}
252 | 
253 | 					if (username.length > maxUsernameLength) {
254 | 						ctx.context.logger.error("Username too long", {
255 | 							username,
256 | 						});
257 | 						throw new APIError("UNPROCESSABLE_ENTITY", {
258 | 							message: ERROR_CODES.USERNAME_TOO_LONG,
259 | 						});
260 | 					}
261 | 
262 | 					const validator =
263 | 						options?.usernameValidator || defaultUsernameValidator;
264 | 
265 | 					if (!validator(username)) {
266 | 						throw new APIError("UNPROCESSABLE_ENTITY", {
267 | 							message: ERROR_CODES.INVALID_USERNAME,
268 | 						});
269 | 					}
270 | 
271 | 					const user = await ctx.context.adapter.findOne<
272 | 						User & { username: string; displayUsername: string }
273 | 					>({
274 | 						model: "user",
275 | 						where: [
276 | 							{
277 | 								field: "username",
278 | 								value: normalizer(username),
279 | 							},
280 | 						],
281 | 					});
282 | 					if (!user) {
283 | 						// Hash password to prevent timing attacks from revealing valid usernames
284 | 						// By hashing passwords for invalid usernames, we ensure consistent response times
285 | 						await ctx.context.password.hash(ctx.body.password);
286 | 						ctx.context.logger.error("User not found", {
287 | 							username,
288 | 						});
289 | 						throw new APIError("UNAUTHORIZED", {
290 | 							message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
291 | 						});
292 | 					}
293 | 
294 | 					const account = await ctx.context.adapter.findOne<Account>({
295 | 						model: "account",
296 | 						where: [
297 | 							{
298 | 								field: "userId",
299 | 								value: user.id,
300 | 							},
301 | 							{
302 | 								field: "providerId",
303 | 								value: "credential",
304 | 							},
305 | 						],
306 | 					});
307 | 					if (!account) {
308 | 						throw new APIError("UNAUTHORIZED", {
309 | 							message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
310 | 						});
311 | 					}
312 | 					const currentPassword = account?.password;
313 | 					if (!currentPassword) {
314 | 						ctx.context.logger.error("Password not found", {
315 | 							username,
316 | 						});
317 | 						throw new APIError("UNAUTHORIZED", {
318 | 							message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
319 | 						});
320 | 					}
321 | 					const validPassword = await ctx.context.password.verify({
322 | 						hash: currentPassword,
323 | 						password: ctx.body.password,
324 | 					});
325 | 					if (!validPassword) {
326 | 						ctx.context.logger.error("Invalid password");
327 | 						throw new APIError("UNAUTHORIZED", {
328 | 							message: ERROR_CODES.INVALID_USERNAME_OR_PASSWORD,
329 | 						});
330 | 					}
331 | 
332 | 					if (
333 | 						ctx.context.options?.emailAndPassword?.requireEmailVerification &&
334 | 						!user.emailVerified
335 | 					) {
336 | 						if (
337 | 							!ctx.context.options?.emailVerification?.sendVerificationEmail
338 | 						) {
339 | 							throw new APIError("FORBIDDEN", {
340 | 								message: ERROR_CODES.EMAIL_NOT_VERIFIED,
341 | 							});
342 | 						}
343 | 
344 | 						if (ctx.context.options?.emailVerification?.sendOnSignIn) {
345 | 							const token = await createEmailVerificationToken(
346 | 								ctx.context.secret,
347 | 								user.email,
348 | 								undefined,
349 | 								ctx.context.options.emailVerification?.expiresIn,
350 | 							);
351 | 							const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${
352 | 								ctx.body.callbackURL || "/"
353 | 							}`;
354 | 							await ctx.context.options.emailVerification.sendVerificationEmail(
355 | 								{
356 | 									user: user,
357 | 									url,
358 | 									token,
359 | 								},
360 | 								ctx.request,
361 | 							);
362 | 						}
363 | 
364 | 						throw new APIError("FORBIDDEN", {
365 | 							message: ERROR_CODES.EMAIL_NOT_VERIFIED,
366 | 						});
367 | 					}
368 | 
369 | 					const session = await ctx.context.internalAdapter.createSession(
370 | 						user.id,
371 | 						ctx.body.rememberMe === false,
372 | 					);
373 | 					if (!session) {
374 | 						return ctx.json(null, {
375 | 							status: 500,
376 | 							body: {
377 | 								message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION,
378 | 							},
379 | 						});
380 | 					}
381 | 					await setSessionCookie(
382 | 						ctx,
383 | 						{ session, user },
384 | 						ctx.body.rememberMe === false,
385 | 					);
386 | 					return ctx.json({
387 | 						token: session.token,
388 | 						user: {
389 | 							id: user.id,
390 | 							email: user.email,
391 | 							emailVerified: user.emailVerified,
392 | 							username: user.username,
393 | 							displayUsername: user.displayUsername,
394 | 							name: user.name,
395 | 							image: user.image,
396 | 							createdAt: user.createdAt,
397 | 							updatedAt: user.updatedAt,
398 | 						},
399 | 					});
400 | 				},
401 | 			),
402 | 			isUsernameAvailable: createAuthEndpoint(
403 | 				"/is-username-available",
404 | 				{
405 | 					method: "POST",
406 | 					body: z.object({
407 | 						username: z.string().meta({
408 | 							description: "The username to check",
409 | 						}),
410 | 					}),
411 | 				},
412 | 				async (ctx) => {
413 | 					const username = ctx.body.username;
414 | 					if (!username) {
415 | 						throw new APIError("UNPROCESSABLE_ENTITY", {
416 | 							message: ERROR_CODES.INVALID_USERNAME,
417 | 						});
418 | 					}
419 | 
420 | 					const minUsernameLength = options?.minUsernameLength || 3;
421 | 					const maxUsernameLength = options?.maxUsernameLength || 30;
422 | 
423 | 					if (username.length < minUsernameLength) {
424 | 						throw new APIError("UNPROCESSABLE_ENTITY", {
425 | 							message: ERROR_CODES.USERNAME_TOO_SHORT,
426 | 						});
427 | 					}
428 | 
429 | 					if (username.length > maxUsernameLength) {
430 | 						throw new APIError("UNPROCESSABLE_ENTITY", {
431 | 							message: ERROR_CODES.USERNAME_TOO_LONG,
432 | 						});
433 | 					}
434 | 
435 | 					const validator =
436 | 						options?.usernameValidator || defaultUsernameValidator;
437 | 
438 | 					if (!(await validator(username))) {
439 | 						throw new APIError("UNPROCESSABLE_ENTITY", {
440 | 							message: ERROR_CODES.INVALID_USERNAME,
441 | 						});
442 | 					}
443 | 					const user = await ctx.context.adapter.findOne<User>({
444 | 						model: "user",
445 | 						where: [
446 | 							{
447 | 								field: "username",
448 | 								value: normalizer(username),
449 | 							},
450 | 						],
451 | 					});
452 | 					if (user) {
453 | 						return ctx.json({
454 | 							available: false,
455 | 						});
456 | 					}
457 | 					return ctx.json({
458 | 						available: true,
459 | 					});
460 | 				},
461 | 			),
462 | 		},
463 | 		schema: mergeSchema(
464 | 			getSchema({
465 | 				username: normalizer,
466 | 				displayUsername: displayUsernameNormalizer,
467 | 			}),
468 | 			options?.schema,
469 | 		),
470 | 		hooks: {
471 | 			before: [
472 | 				{
473 | 					matcher(context) {
474 | 						return (
475 | 							context.path === "/sign-up/email" ||
476 | 							context.path === "/update-user"
477 | 						);
478 | 					},
479 | 					handler: createAuthMiddleware(async (ctx) => {
480 | 						const username =
481 | 							typeof ctx.body.username === "string" &&
482 | 							options?.validationOrder?.username === "post-normalization"
483 | 								? normalizer(ctx.body.username)
484 | 								: ctx.body.username;
485 | 
486 | 						if (username !== undefined && typeof username === "string") {
487 | 							const minUsernameLength = options?.minUsernameLength || 3;
488 | 							const maxUsernameLength = options?.maxUsernameLength || 30;
489 | 							if (username.length < minUsernameLength) {
490 | 								throw new APIError("BAD_REQUEST", {
491 | 									message: ERROR_CODES.USERNAME_TOO_SHORT,
492 | 								});
493 | 							}
494 | 
495 | 							if (username.length > maxUsernameLength) {
496 | 								throw new APIError("BAD_REQUEST", {
497 | 									message: ERROR_CODES.USERNAME_TOO_LONG,
498 | 								});
499 | 							}
500 | 
501 | 							const validator =
502 | 								options?.usernameValidator || defaultUsernameValidator;
503 | 
504 | 							const valid = await validator(username);
505 | 							if (!valid) {
506 | 								throw new APIError("BAD_REQUEST", {
507 | 									message: ERROR_CODES.INVALID_USERNAME,
508 | 								});
509 | 							}
510 | 							const user = await ctx.context.adapter.findOne<User>({
511 | 								model: "user",
512 | 								where: [
513 | 									{
514 | 										field: "username",
515 | 										value: username,
516 | 									},
517 | 								],
518 | 							});
519 | 
520 | 							const blockChangeSignUp = ctx.path === "/sign-up/email" && user;
521 | 							const blockChangeUpdateUser =
522 | 								ctx.path === "/update-user" &&
523 | 								user &&
524 | 								ctx.context.session &&
525 | 								user.id !== ctx.context.session.session.userId;
526 | 							if (blockChangeSignUp || blockChangeUpdateUser) {
527 | 								throw new APIError("BAD_REQUEST", {
528 | 									message: ERROR_CODES.USERNAME_IS_ALREADY_TAKEN,
529 | 								});
530 | 							}
531 | 						}
532 | 
533 | 						const displayUsername =
534 | 							typeof ctx.body.displayUsername === "string" &&
535 | 							options?.validationOrder?.displayUsername === "post-normalization"
536 | 								? displayUsernameNormalizer(ctx.body.displayUsername)
537 | 								: ctx.body.displayUsername;
538 | 
539 | 						if (
540 | 							displayUsername !== undefined &&
541 | 							typeof displayUsername === "string"
542 | 						) {
543 | 							if (options?.displayUsernameValidator) {
544 | 								const valid =
545 | 									await options.displayUsernameValidator(displayUsername);
546 | 								if (!valid) {
547 | 									throw new APIError("BAD_REQUEST", {
548 | 										message: ERROR_CODES.INVALID_DISPLAY_USERNAME,
549 | 									});
550 | 								}
551 | 							}
552 | 						}
553 | 					}),
554 | 				},
555 | 				{
556 | 					matcher(context) {
557 | 						return (
558 | 							context.path === "/sign-up/email" ||
559 | 							context.path === "/update-user"
560 | 						);
561 | 					},
562 | 					handler: createAuthMiddleware(async (ctx) => {
563 | 						if (ctx.body.username && !ctx.body.displayUsername) {
564 | 							ctx.body.displayUsername = ctx.body.username;
565 | 						}
566 | 						if (ctx.body.displayUsername && !ctx.body.username) {
567 | 							ctx.body.username = ctx.body.displayUsername;
568 | 						}
569 | 					}),
570 | 				},
571 | 			],
572 | 		},
573 | 		$ERROR_CODES: ERROR_CODES,
574 | 	} satisfies BetterAuthPlugin;
575 | };
576 | 
```

--------------------------------------------------------------------------------
/docs/content/docs/concepts/users-accounts.mdx:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: User & Accounts
  3 | description: User and account management.
  4 | ---
  5 | 
  6 | Beyond authenticating users, Better Auth also provides a set of methods to manage users. This includes, updating user information, changing passwords, and more.
  7 | 
  8 | The user table stores the authentication data of the user [Click here to view the schema](/docs/concepts/database#user).
  9 | 
 10 | The user table can be extended using [additional fields](/docs/concepts/database#extending-core-schema) or by plugins to store additional data.
 11 | 
 12 | ## Update User
 13 | 
 14 | ### Update User Information
 15 | 
 16 | To update user information, you can use the `updateUser` function provided by the client. The `updateUser` function takes an object with the following properties:
 17 | 
 18 | ```ts
 19 | await authClient.updateUser({
 20 |     image: "https://example.com/image.jpg",
 21 |     name: "John Doe",
 22 | })
 23 | ```
 24 | ### Change Email
 25 | 
 26 | To allow users to change their email, first enable the `changeEmail` feature, which is disabled by default. Set `changeEmail.enabled` to `true`:
 27 | 
 28 | ```ts
 29 | export const auth = betterAuth({
 30 |     user: {
 31 |         changeEmail: {
 32 |             enabled: true,
 33 |         }
 34 |     }
 35 | })
 36 | ```
 37 | 
 38 | For users with a verified email, provide the `sendChangeEmailVerification` function. This function triggers when a user changes their email, sending a verification email with a URL and token. If the current email isn't verified, the change happens immediately without verification.
 39 | 
 40 | ```ts
 41 | export const auth = betterAuth({
 42 |     user: {
 43 |         changeEmail: {
 44 |             enabled: true,
 45 |             sendChangeEmailVerification: async ({ user, newEmail, url, token }, request) => {
 46 |                 await sendEmail({
 47 |                     to: user.email, // verification email must be sent to the current user email to approve the change
 48 |                     subject: 'Approve email change',
 49 |                     text: `Click the link to approve the change: ${url}`
 50 |                 })
 51 |             }
 52 |         }
 53 |     }
 54 | })
 55 | ```
 56 | 
 57 | Once enabled, use the `changeEmail` function on the client to update a user’s email. The user must verify their current email before changing it.
 58 | 
 59 | ```ts
 60 | await authClient.changeEmail({
 61 |     newEmail: "[email protected]",
 62 |     callbackURL: "/dashboard", //to redirect after verification
 63 | });
 64 | ```
 65 | 
 66 | After verification, the new email is updated in the user table, and a confirmation is sent to the new address.
 67 | 
 68 | <Callout type="warn">
 69 |     If the current email is unverified, the new email is updated without the verification step.
 70 | </Callout>
 71 |  
 72 | ### Change Password
 73 | A user's password isn't stored in the user table. Instead, it's stored in the account table. To change the password of a user, you can use one of the following approaches:
 74 | 
 75 | 
 76 | <APIMethod path="/change-password" method="POST" requireSession>
 77 | ```ts
 78 | type changePassword = {
 79 |     /**
 80 |      * The new password to set 
 81 |      */
 82 |     newPassword: string = "newpassword1234"
 83 |     /**
 84 |      * The current user password 
 85 |      */
 86 |     currentPassword: string = "oldpassword1234"
 87 |     /**
 88 |      * When set to true, all other active sessions for this user will be invalidated
 89 |      */
 90 |     revokeOtherSessions?: boolean = true
 91 | }
 92 | ```
 93 | </APIMethod>
 94 | 
 95 | ### Set Password
 96 | 
 97 | If a user was registered using OAuth or other providers, they won't have a password or a credential account. In this case, you can use the `setPassword` action to set a password for the user. For security reasons, this function can only be called from the server. We recommend having users go through a 'forgot password' flow to set a password for their account.
 98 | 
 99 | ```ts
100 | await auth.api.setPassword({
101 |     body: { newPassword: "password" },
102 |     headers: // headers containing the user's session token
103 | });
104 | ```
105 | 
106 | ## Delete User
107 | 
108 | Better Auth provides a utility to hard delete a user from your database. It's disabled by default, but you can enable it easily by passing `enabled:true`
109 | 
110 | ```ts
111 | export const auth = betterAuth({
112 |     //...other config
113 |     user: {
114 |         deleteUser: { // [!code highlight]
115 |             enabled: true // [!code highlight]
116 |         } // [!code highlight]
117 |     }
118 | })
119 | ```
120 | 
121 | Once enabled, you can call `authClient.deleteUser` to permanently delete user data from your database.
122 | 
123 | ### Adding Verification Before Deletion
124 | 
125 | For added security, you’ll likely want to confirm the user’s intent before deleting their account. A common approach is to send a verification email. Better Auth provides a `sendDeleteAccountVerification` utility for this purpose. 
126 | This is especially needed if you have OAuth setup and want them to be able to delete their account without forcing them to login again for a fresh session.
127 | 
128 | Here’s how you can set it up:
129 | 
130 | ```ts
131 | export const auth = betterAuth({
132 |     user: {
133 |         deleteUser: {
134 |             enabled: true,
135 |             sendDeleteAccountVerification: async (
136 |                 {
137 |                     user,   // The user object
138 |                     url, // The auto-generated URL for deletion
139 |                     token  // The verification token  (can be used to generate custom URL)
140 |                 },
141 |                 request  // The original request object (optional)
142 |             ) => {
143 |                 // Your email sending logic here
144 |                 // Example: sendEmail(data.user.email, "Verify Deletion", data.url);
145 |             },
146 |         },
147 |     },
148 | });
149 | ```
150 | 
151 | **How callback verification works:**
152 | 
153 | - **Callback URL**: The URL provided in `sendDeleteAccountVerification` is a pre-generated link that deletes the user data when accessed.
154 | 
155 | ```ts title="delete-user.ts"
156 | await authClient.deleteUser({
157 |     callbackURL: "/goodbye" // you can provide a callback URL to redirect after deletion
158 | });
159 | ```
160 | 
161 | - **Authentication Check**: The user must be signed in to the account they’re attempting to delete.
162 | If they aren’t signed in, the deletion process will fail. 
163 | 
164 | If you have sent a custom URL, you can use the `deleteUser` method with the token to delete the user.
165 | 
166 | ```ts title="delete-user.ts"
167 | await authClient.deleteUser({
168 |     token
169 | });
170 | ```
171 | 
172 | ### Authentication Requirements
173 | 
174 | To delete a user, the user must meet one of the following requirements:
175 | 
176 | 1. A valid password
177 | 
178 | if the user has a password, they can delete their account by providing the password.
179 | 
180 | ```ts title="delete-user.ts"
181 | await authClient.deleteUser({
182 |     password: "password"
183 | });
184 | ```
185 | 
186 | 2. Fresh session
187 | 
188 | The user must have a `fresh` session token, meaning the user must have signed in recently. This is checked if the password is not provided.
189 | 
190 | <Callout type="warn">
191 | By default `session.freshAge` is set to `60 * 60 * 24` (1 day). You can change this value by passing the `session` object to the `auth` configuration. If it is set to `0`, the freshness check is disabled. It is recommended not to disable this check if you are not using email verification for deleting the account.
192 | </Callout>
193 | 
194 | ```ts title="delete-user.ts"
195 | await authClient.deleteUser();
196 | ```
197 | 
198 | 3. Enabled email verification (needed for OAuth users)
199 | 
200 | As OAuth users don't have a password, we need to send a verification email to confirm the user's intent to delete their account. If you have already added the `sendDeleteAccountVerification` callback, you can just call the `deleteUser` method without providing any other information.
201 | 
202 | ```ts title="delete-user.ts"
203 | await authClient.deleteUser();
204 | ```
205 | 
206 | 4. If you have a custom delete account page and sent that url via the `sendDeleteAccountVerification` callback.
207 | Then you need to call the `deleteUser` method with the token to complete the deletion.
208 | 
209 | ```ts title="delete-user.ts"
210 | await authClient.deleteUser({
211 |     token
212 | });
213 | ```
214 | 
215 | ### Callbacks
216 | 
217 | **beforeDelete**: This callback is called before the user is deleted. You can use this callback to perform any cleanup or additional checks before deleting the user.
218 | 
219 | ```ts title="auth.ts"
220 | export const auth = betterAuth({
221 |     user: {
222 |         deleteUser: {
223 |             enabled: true,
224 |             beforeDelete: async (user) => {
225 |                 // Perform any cleanup or additional checks here
226 |             },
227 |         },
228 |     },
229 | });
230 | ```
231 | you can also throw `APIError` to interrupt the deletion process.
232 | 
233 | ```ts title="auth.ts"
234 | import { betterAuth } from "better-auth";
235 | import { APIError } from "better-auth/api";
236 | 
237 | export const auth = betterAuth({
238 |     user: {
239 |         deleteUser: {
240 |             enabled: true,
241 |             beforeDelete: async (user, request) => {
242 |                 if (user.email.includes("admin")) {
243 |                     throw new APIError("BAD_REQUEST", {
244 |                         message: "Admin accounts can't be deleted",
245 |                     });
246 |                 }
247 |             },
248 |         },
249 |     },
250 | });
251 | ```
252 | 
253 | **afterDelete**: This callback is called after the user is deleted. You can use this callback to perform any cleanup or additional actions after the user is deleted.
254 | 
255 | ```ts title="auth.ts"
256 | export const auth = betterAuth({
257 |     user: {
258 |         deleteUser: {
259 |             enabled: true,
260 |             afterDelete: async (user, request) => {
261 |                 // Perform any cleanup or additional actions here
262 |             },
263 |         },
264 |     },
265 | });
266 | ```
267 | 
268 | ## Accounts
269 | 
270 | Better Auth supports multiple authentication methods. Each authentication method is called a provider. For example, email and password authentication is a provider, Google authentication is a provider, etc.
271 | 
272 | When a user signs in using a provider, an account is created for the user. The account stores the authentication data returned by the provider. This data includes the access token, refresh token, and other information returned by the provider.
273 | 
274 | The account table stores the authentication data of the user [Click here to view the schema](/docs/concepts/database#account)
275 | 
276 | 
277 | ### List User Accounts
278 | 
279 | To list user accounts you can use `client.user.listAccounts` method. Which will return all accounts associated with a user.
280 | 
281 | ```ts
282 | const accounts = await authClient.listAccounts();
283 | ```
284 | 
285 | ### Token Encryption
286 | 
287 | Better Auth doesn’t encrypt tokens by default and that’s intentional. We want you to have full control over how encryption and decryption are handled, rather than baking in behavior that could be confusing or limiting. If you need to store encrypted tokens (like accessToken or refreshToken), you can use databaseHooks to encrypt them before they’re saved to your database.
288 | 
289 | ```ts
290 | export const auth = betterAuth({
291 |     databaseHooks: {
292 |         account: {
293 |             create: {
294 |                 before(account, context) {
295 |                     const withEncryptedTokens = { ...account };
296 |                     if (account.accessToken) {
297 |                         const encryptedAccessToken = encrypt(account.accessToken)  // [!code highlight]
298 |                         withEncryptedTokens.accessToken = encryptedAccessToken;
299 |                     }
300 |                     if (account.refreshToken) {
301 |                         const encryptedRefreshToken = encrypt(account.refreshToken); // [!code highlight]
302 |                         withEncryptedTokens.refreshToken = encryptedRefreshToken;
303 |                     }
304 |                     return {
305 |                         data: withEncryptedTokens
306 |                     }
307 |                 },
308 |             }
309 |         }
310 |     }
311 | })
312 | ```
313 | 
314 | Then whenever you retrieve back the account make sure to decrypt the tokens before using them.
315 | 
316 | ### Account Linking
317 | 
318 | Account linking enables users to associate multiple authentication methods with a single account. With Better Auth, users can connect additional social sign-ons or OAuth providers to their existing accounts if the provider confirms the user's email as verified.
319 | 
320 | If account linking is disabled, no accounts can be linked, regardless of the provider or email verification status.
321 | 
322 | ```ts title="auth.ts"
323 | export const auth = betterAuth({
324 |     account: {
325 |         accountLinking: {
326 |             enabled: true, 
327 |         }
328 |     },
329 | });
330 | ```
331 | 
332 | #### Forced Linking
333 | 
334 | You can specify a list of "trusted providers." When a user logs in using a trusted provider, their account will be automatically linked even if the provider doesn’t confirm the email verification status. Use this with caution as it may increase the risk of account takeover.
335 | 
336 | ```ts title="auth.ts"
337 | export const auth = betterAuth({
338 |     account: {
339 |         accountLinking: {
340 |             enabled: true,
341 |             trustedProviders: ["google", "github"]
342 |         }
343 |     },
344 | });
345 | ```
346 | 
347 | #### Manually Linking Accounts
348 | 
349 | Users already signed in can manually link their account to additional social providers or credential-based accounts.
350 | 
351 | - **Linking Social Accounts:** Use the `linkSocial` method on the client to link a social provider to the user's account.
352 | 
353 |   ```ts
354 |   await authClient.linkSocial({
355 |       provider: "google", // Provider to link
356 |       callbackURL: "/callback" // Callback URL after linking completes
357 |   });
358 |   ```
359 | 
360 |   You can also request specific scopes when linking a social account, which can be different from the scopes used during the initial authentication:
361 | 
362 |   ```ts
363 |   await authClient.linkSocial({
364 |       provider: "google",
365 |       callbackURL: "/callback",
366 |       scopes: ["https://www.googleapis.com/auth/drive.readonly"] // Request additional scopes
367 |   });
368 |   ```
369 | 
370 |   You can also link accounts using ID tokens directly, without redirecting to the provider's OAuth flow:
371 | 
372 |   ```ts
373 |   await authClient.linkSocial({
374 |       provider: "google",
375 |       idToken: {
376 |           token: "id_token_from_provider",
377 |           nonce: "nonce_used_for_token", // Optional
378 |           accessToken: "access_token", // Optional, may be required by some providers
379 |           refreshToken: "refresh_token" // Optional
380 |       }
381 |   });
382 |   ```
383 | 
384 |   This is useful when you already have valid tokens from the provider, for example:
385 |   - After signing in with a native SDK
386 |   - When using a mobile app that handles authentication
387 |   - When implementing custom OAuth flows
388 | 
389 |   The ID token must be valid and the provider must support ID token verification.
390 | 
391 |   If you want your users to be able to link a social account with a different email address than the user, or if you want to use a provider that does not return email addresses, you will need to enable this in the account linking settings. 
392 |   ```ts title="auth.ts"
393 |   export const auth = betterAuth({
394 |       account: {
395 |           accountLinking: {
396 |               allowDifferentEmails: true
397 |           }
398 |       },
399 |   });
400 |   ```
401 | 
402 |   If you want the newly linked accounts to update the user information, you need to enable this in the account linking settings. 
403 | 
404 |   ```ts title="auth.ts"
405 |   export const auth = betterAuth({
406 |       account: {
407 |           accountLinking: {
408 |               updateUserInfoOnLink: true
409 |           }
410 |       },
411 |   });
412 |   ```
413 | 
414 | - **Linking Credential-Based Accounts:** To link a credential-based account (e.g., email and password), users can initiate a "forgot password" flow, or you can call the `setPassword` method on the server. 
415 | 
416 |   ```ts
417 |   await auth.api.setPassword({
418 |       headers: /* headers containing the user's session token */,
419 |       password: /* new password */
420 |   });
421 |   ```
422 | 
423 | <Callout>
424 | `setPassword` can't be called from the client for security reasons.
425 | </Callout>
426 | 
427 | ### Account Unlinking
428 | 
429 | You can unlink a user account by providing a `providerId`.
430 | 
431 | ```ts
432 | await authClient.unlinkAccount({
433 |     providerId: "google"
434 | });
435 | 
436 | // Unlink a specific account
437 | await authClient.unlinkAccount({
438 |     providerId: "google",
439 |     accountId: "123"
440 | });
441 | ```
442 | 
443 | If the account doesn't exist, it will throw an error. Additionally, if the user only has one account, unlinking will be prevented to stop account lockout (unless `allowUnlinkingAll` is set to `true`).
444 | 
445 | ```ts title="auth.ts"
446 | export const auth = betterAuth({
447 |     account: {
448 |         accountLinking: {
449 |             allowUnlinkingAll: true
450 |         }
451 |     },
452 | });
453 | ```
454 | 
455 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/two-factor/two-factor.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, it, vi } from "vitest";
  2 | import { getTestInstance } from "../../test-utils/test-instance";
  3 | import { TWO_FACTOR_ERROR_CODES, twoFactor, twoFactorClient } from ".";
  4 | import { createAuthClient } from "../../client";
  5 | import { parseSetCookieHeader } from "../../cookies";
  6 | import type { TwoFactorTable, UserWithTwoFactor } from "./types";
  7 | import { DEFAULT_SECRET } from "../../utils/constants";
  8 | import { symmetricDecrypt } from "../../crypto";
  9 | import { convertSetCookieToCookie } from "../../test-utils/headers";
 10 | import { createOTP } from "@better-auth/utils/otp";
 11 | 
 12 | describe("two factor", async () => {
 13 | 	let OTP = "";
 14 | 	const { testUser, customFetchImpl, sessionSetter, db, auth } =
 15 | 		await getTestInstance({
 16 | 			secret: DEFAULT_SECRET,
 17 | 			plugins: [
 18 | 				twoFactor({
 19 | 					otpOptions: {
 20 | 						sendOTP({ otp }) {
 21 | 							OTP = otp;
 22 | 						},
 23 | 					},
 24 | 				}),
 25 | 			],
 26 | 		});
 27 | 
 28 | 	const headers = new Headers();
 29 | 
 30 | 	const client = createAuthClient({
 31 | 		plugins: [twoFactorClient()],
 32 | 		fetchOptions: {
 33 | 			customFetchImpl,
 34 | 			baseURL: "http://localhost:3000/api/auth",
 35 | 		},
 36 | 	});
 37 | 	const session = await client.signIn.email({
 38 | 		email: testUser.email,
 39 | 		password: testUser.password,
 40 | 		fetchOptions: {
 41 | 			onSuccess: sessionSetter(headers),
 42 | 		},
 43 | 	});
 44 | 	if (!session) {
 45 | 		throw new Error("No session");
 46 | 	}
 47 | 
 48 | 	it("should return uri and backup codes and shouldn't enable twoFactor yet", async () => {
 49 | 		const res = await client.twoFactor.enable({
 50 | 			password: testUser.password,
 51 | 			fetchOptions: {
 52 | 				headers,
 53 | 			},
 54 | 		});
 55 | 		expect(res.data?.backupCodes.length).toEqual(10);
 56 | 		expect(res.data?.totpURI).toBeDefined();
 57 | 		const dbUser = await db.findOne<UserWithTwoFactor>({
 58 | 			model: "user",
 59 | 			where: [
 60 | 				{
 61 | 					field: "id",
 62 | 					value: session.data?.user.id as string,
 63 | 				},
 64 | 			],
 65 | 		});
 66 | 		const twoFactor = await db.findOne<TwoFactorTable>({
 67 | 			model: "twoFactor",
 68 | 			where: [
 69 | 				{
 70 | 					field: "userId",
 71 | 					value: session.data?.user.id as string,
 72 | 				},
 73 | 			],
 74 | 		});
 75 | 		expect(dbUser?.twoFactorEnabled).toBe(false);
 76 | 		expect(twoFactor?.secret).toBeDefined();
 77 | 		expect(twoFactor?.backupCodes).toBeDefined();
 78 | 	});
 79 | 
 80 | 	it("should use custom issuer from request parameter", async () => {
 81 | 		const CUSTOM_ISSUER = "Custom App Name";
 82 | 		const res = await client.twoFactor.enable({
 83 | 			password: testUser.password,
 84 | 			issuer: CUSTOM_ISSUER,
 85 | 			fetchOptions: {
 86 | 				headers,
 87 | 			},
 88 | 		});
 89 | 
 90 | 		const totpURI = res.data?.totpURI;
 91 | 		expect(totpURI).toMatch(
 92 | 			new RegExp(`^otpauth://totp/${encodeURIComponent(CUSTOM_ISSUER)}:`),
 93 | 		);
 94 | 		expect(totpURI).toContain(`&issuer=Custom+App+Name&`);
 95 | 	});
 96 | 
 97 | 	it("should fallback to appName when no issuer provided", async () => {
 98 | 		const res = await client.twoFactor.enable({
 99 | 			password: testUser.password,
100 | 			fetchOptions: {
101 | 				headers,
102 | 			},
103 | 		});
104 | 
105 | 		const totpURI = res.data?.totpURI;
106 | 		expect(totpURI).toMatch(/^otpauth:\/\/totp\/Better%20Auth:/);
107 | 		expect(totpURI).toContain("&issuer=Better+Auth&");
108 | 	});
109 | 
110 | 	it("should enable twoFactor", async () => {
111 | 		const twoFactor = await db.findOne<TwoFactorTable>({
112 | 			model: "twoFactor",
113 | 			where: [
114 | 				{
115 | 					field: "userId",
116 | 					value: session.data?.user.id as string,
117 | 				},
118 | 			],
119 | 		});
120 | 		if (!twoFactor) {
121 | 			throw new Error("No two factor");
122 | 		}
123 | 
124 | 		const decrypted = await symmetricDecrypt({
125 | 			key: DEFAULT_SECRET,
126 | 			data: twoFactor.secret,
127 | 		});
128 | 		const code = await createOTP(decrypted).totp();
129 | 
130 | 		const res = await client.twoFactor.verifyTotp({
131 | 			code,
132 | 			fetchOptions: {
133 | 				headers,
134 | 				onSuccess: sessionSetter(headers),
135 | 			},
136 | 		});
137 | 		expect(res.data?.token).toBeDefined();
138 | 	});
139 | 
140 | 	it("should require two factor", async () => {
141 | 		const headers = new Headers();
142 | 		const res = await client.signIn.email({
143 | 			email: testUser.email,
144 | 			password: testUser.password,
145 | 			rememberMe: false,
146 | 			fetchOptions: {
147 | 				onResponse(context) {
148 | 					const parsed = parseSetCookieHeader(
149 | 						context.response.headers.get("Set-Cookie") || "",
150 | 					);
151 | 					expect(parsed.get("better-auth.session_token")?.value).toBe("");
152 | 					expect(parsed.get("better-auth.two_factor")?.value).toBeDefined();
153 | 					expect(parsed.get("better-auth.dont_remember")?.value).toBeDefined();
154 | 					headers.append(
155 | 						"cookie",
156 | 						`better-auth.two_factor=${
157 | 							parsed.get("better-auth.two_factor")?.value
158 | 						}`,
159 | 					);
160 | 					headers.append(
161 | 						"cookie",
162 | 						`better-auth.dont_remember=${
163 | 							parsed.get("better-auth.dont_remember")?.value
164 | 						}`,
165 | 					);
166 | 				},
167 | 			},
168 | 		});
169 | 		expect((res.data as any)?.twoFactorRedirect).toBe(true);
170 | 		await client.twoFactor.sendOtp({
171 | 			fetchOptions: {
172 | 				headers,
173 | 			},
174 | 		});
175 | 
176 | 		const verifyRes = await client.twoFactor.verifyOtp({
177 | 			code: OTP,
178 | 			fetchOptions: {
179 | 				headers,
180 | 				onResponse(context) {
181 | 					const parsed = parseSetCookieHeader(
182 | 						context.response.headers.get("Set-Cookie") || "",
183 | 					);
184 | 					expect(parsed.get("better-auth.session_token")?.value).toBeDefined();
185 | 					// max age should be undefined because we are not using remember me
186 | 					expect(
187 | 						parsed.get("better-auth.session_token")?.["max-age"],
188 | 					).not.toBeDefined();
189 | 				},
190 | 			},
191 | 		});
192 | 		expect(verifyRes.data?.token).toBeDefined();
193 | 	});
194 | 
195 | 	it("should fail if two factor cookie is missing", async () => {
196 | 		const res = await client.twoFactor.verifyTotp({
197 | 			code: "123456",
198 | 			fetchOptions: {
199 | 				headers,
200 | 			},
201 | 		});
202 | 		expect(res.error?.message).toBe(
203 | 			TWO_FACTOR_ERROR_CODES.INVALID_TWO_FACTOR_COOKIE,
204 | 		);
205 | 	});
206 | 
207 | 	let backupCodes: string[] = [];
208 | 	it("should generate backup codes", async () => {
209 | 		await client.twoFactor.enable({
210 | 			password: testUser.password,
211 | 			fetchOptions: {
212 | 				headers,
213 | 			},
214 | 		});
215 | 		const backupCodesRes = await client.twoFactor.generateBackupCodes({
216 | 			fetchOptions: {
217 | 				headers,
218 | 			},
219 | 			password: testUser.password,
220 | 		});
221 | 		expect(backupCodesRes.data?.backupCodes).toBeDefined();
222 | 		backupCodes = backupCodesRes.data?.backupCodes || [];
223 | 	});
224 | 
225 | 	it("should allow sign in with backup code", async () => {
226 | 		const headers = new Headers();
227 | 		await client.signIn.email({
228 | 			email: testUser.email,
229 | 			password: testUser.password,
230 | 			fetchOptions: {
231 | 				onSuccess(context) {
232 | 					const parsed = parseSetCookieHeader(
233 | 						context.response.headers.get("Set-Cookie") || "",
234 | 					);
235 | 					const token = parsed.get("better-auth.session_token")?.value;
236 | 					expect(token).toBe("");
237 | 					headers.append(
238 | 						"cookie",
239 | 						`better-auth.two_factor=${
240 | 							parsed.get("better-auth.two_factor")?.value
241 | 						}`,
242 | 					);
243 | 				},
244 | 			},
245 | 		});
246 | 		const backupCode = backupCodes[0]!;
247 | 
248 | 		let parsedCookies = new Map();
249 | 		await client.twoFactor.verifyBackupCode({
250 | 			code: backupCode,
251 | 			fetchOptions: {
252 | 				headers,
253 | 				onSuccess(context) {
254 | 					parsedCookies = parseSetCookieHeader(
255 | 						context.response.headers.get("Set-Cookie") || "",
256 | 					);
257 | 				},
258 | 			},
259 | 		});
260 | 		const token = parsedCookies.get("better-auth.session_token")?.value;
261 | 		expect(token?.length).toBeGreaterThan(0);
262 | 		const currentBackupCodes = await auth.api.viewBackupCodes({
263 | 			body: {
264 | 				userId: session.data?.user.id!,
265 | 			},
266 | 		});
267 | 		expect(currentBackupCodes.backupCodes).toBeDefined();
268 | 		expect(currentBackupCodes.backupCodes).not.toContain(backupCode);
269 | 
270 | 		const res = await client.twoFactor.verifyBackupCode({
271 | 			code: "invalid-code",
272 | 			fetchOptions: {
273 | 				headers,
274 | 				onSuccess(context) {
275 | 					const parsed = parseSetCookieHeader(
276 | 						context.response.headers.get("Set-Cookie") || "",
277 | 					);
278 | 					const token = parsed.get("better-auth.session_token")?.value;
279 | 					expect(token?.length).toBeGreaterThan(0);
280 | 				},
281 | 			},
282 | 		});
283 | 		expect(res.error?.message).toBe("Invalid backup code");
284 | 	});
285 | 
286 | 	it("should trust device", async () => {
287 | 		const headers = new Headers();
288 | 		const res = await client.signIn.email({
289 | 			email: testUser.email,
290 | 			password: testUser.password,
291 | 			fetchOptions: {
292 | 				onSuccess(context) {
293 | 					const parsed = parseSetCookieHeader(
294 | 						context.response.headers.get("Set-Cookie") || "",
295 | 					);
296 | 					headers.append(
297 | 						"cookie",
298 | 						`better-auth.two_factor=${
299 | 							parsed.get("better-auth.two_factor")?.value
300 | 						}`,
301 | 					);
302 | 				},
303 | 			},
304 | 		});
305 | 		expect((res.data as any)?.twoFactorRedirect).toBe(true);
306 | 		const otpRes = await client.twoFactor.sendOtp({
307 | 			fetchOptions: {
308 | 				headers,
309 | 				onSuccess(context) {
310 | 					const parsed = parseSetCookieHeader(
311 | 						context.response.headers.get("Set-Cookie") || "",
312 | 					);
313 | 					headers.append(
314 | 						"cookie",
315 | 						`better-auth.otp.counter=${
316 | 							parsed.get("better-auth.otp_counter")?.value
317 | 						}`,
318 | 					);
319 | 				},
320 | 			},
321 | 		});
322 | 		const newHeaders = new Headers();
323 | 		await client.twoFactor.verifyOtp({
324 | 			trustDevice: true,
325 | 			code: OTP,
326 | 			fetchOptions: {
327 | 				headers,
328 | 				onSuccess(context) {
329 | 					const parsed = parseSetCookieHeader(
330 | 						context.response.headers.get("Set-Cookie") || "",
331 | 					);
332 | 					newHeaders.set(
333 | 						"cookie",
334 | 						`better-auth.trust_device=${
335 | 							parsed.get("better-auth.trust_device")?.value
336 | 						}`,
337 | 					);
338 | 				},
339 | 			},
340 | 		});
341 | 
342 | 		const signInRes = await client.signIn.email({
343 | 			email: testUser.email,
344 | 			password: testUser.password,
345 | 			fetchOptions: {
346 | 				headers: newHeaders,
347 | 			},
348 | 		});
349 | 		expect(signInRes.data?.user).toBeDefined();
350 | 	});
351 | 
352 | 	it("should limit OTP verification attempts", async () => {
353 | 		const headers = new Headers();
354 | 		// Sign in to trigger 2FA
355 | 		await client.signIn.email({
356 | 			email: testUser.email,
357 | 			password: testUser.password,
358 | 			fetchOptions: {
359 | 				onSuccess(context) {
360 | 					const parsed = parseSetCookieHeader(
361 | 						context.response.headers.get("Set-Cookie") || "",
362 | 					);
363 | 					headers.append(
364 | 						"cookie",
365 | 						`better-auth.two_factor=${
366 | 							parsed.get("better-auth.two_factor")?.value
367 | 						}`,
368 | 					);
369 | 				},
370 | 			},
371 | 		});
372 | 		await client.twoFactor.sendOtp({
373 | 			fetchOptions: {
374 | 				headers,
375 | 			},
376 | 		});
377 | 		for (let i = 0; i < 5; i++) {
378 | 			const res = await client.twoFactor.verifyOtp({
379 | 				code: "000000", // Invalid code
380 | 				fetchOptions: {
381 | 					headers,
382 | 				},
383 | 			});
384 | 			expect(res.error?.message).toBe("Invalid code");
385 | 		}
386 | 
387 | 		// Next attempt should be blocked
388 | 		const res = await client.twoFactor.verifyOtp({
389 | 			code: OTP, // Even with correct code
390 | 			fetchOptions: {
391 | 				headers,
392 | 			},
393 | 		});
394 | 		expect(res.error?.message).toBe(
395 | 			"Too many attempts. Please request a new code.",
396 | 		);
397 | 	});
398 | 
399 | 	it("should disable two factor", async () => {
400 | 		const res = await client.twoFactor.disable({
401 | 			password: testUser.password,
402 | 			fetchOptions: {
403 | 				headers,
404 | 			},
405 | 		});
406 | 
407 | 		expect(res.data?.status).toBe(true);
408 | 		const dbUser = await db.findOne<UserWithTwoFactor>({
409 | 			model: "user",
410 | 			where: [
411 | 				{
412 | 					field: "id",
413 | 					value: session.data?.user.id as string,
414 | 				},
415 | 			],
416 | 		});
417 | 		expect(dbUser?.twoFactorEnabled).toBe(false);
418 | 
419 | 		const signInRes = await client.signIn.email({
420 | 			email: testUser.email,
421 | 			password: testUser.password,
422 | 		});
423 | 		expect(signInRes.data?.user).toBeDefined();
424 | 	});
425 | });
426 | 
427 | describe("two factor auth API", async () => {
428 | 	let OTP = "";
429 | 	const sendOTP = vi.fn();
430 | 	const { auth, signInWithTestUser, testUser } = await getTestInstance({
431 | 		secret: DEFAULT_SECRET,
432 | 		plugins: [
433 | 			twoFactor({
434 | 				otpOptions: {
435 | 					sendOTP({ otp }) {
436 | 						OTP = otp;
437 | 						sendOTP(otp);
438 | 					},
439 | 				},
440 | 				skipVerificationOnEnable: true,
441 | 			}),
442 | 		],
443 | 	});
444 | 	let { headers } = await signInWithTestUser();
445 | 
446 | 	it("enable two factor", async () => {
447 | 		const res = await auth.api.enableTwoFactor({
448 | 			body: {
449 | 				password: testUser.password,
450 | 			},
451 | 			headers,
452 | 			asResponse: true,
453 | 		});
454 | 		headers = convertSetCookieToCookie(res.headers);
455 | 
456 | 		const json = (await res.json()) as {
457 | 			status: boolean;
458 | 			backupCodes: string[];
459 | 			totpURI: string;
460 | 		};
461 | 		expect(json.backupCodes.length).toBe(10);
462 | 		expect(json.totpURI).toBeDefined();
463 | 		const session = await auth.api.getSession({
464 | 			headers,
465 | 		});
466 | 		expect(session?.user.twoFactorEnabled).toBe(true);
467 | 	});
468 | 
469 | 	it("should get totp uri", async () => {
470 | 		const res = await auth.api.getTOTPURI({
471 | 			headers,
472 | 			body: {
473 | 				password: testUser.password,
474 | 			},
475 | 		});
476 | 		expect(res.totpURI).toBeDefined();
477 | 	});
478 | 
479 | 	it("should request second factor", async () => {
480 | 		const signInRes = await auth.api.signInEmail({
481 | 			body: {
482 | 				email: testUser.email,
483 | 				password: testUser.password,
484 | 			},
485 | 			asResponse: true,
486 | 		});
487 | 
488 | 		headers = convertSetCookieToCookie(signInRes.headers);
489 | 
490 | 		expect(signInRes).toBeInstanceOf(Response);
491 | 		expect(signInRes.status).toBe(200);
492 | 		const parsed = parseSetCookieHeader(
493 | 			signInRes.headers.get("Set-Cookie") || "",
494 | 		);
495 | 		const twoFactorCookie = parsed.get("better-auth.two_factor");
496 | 		expect(twoFactorCookie).toBeDefined();
497 | 		const sessionToken = parsed.get("better-auth.session_token");
498 | 		expect(sessionToken?.value).toBeFalsy();
499 | 	});
500 | 
501 | 	it("should send otp", async () => {
502 | 		await auth.api.sendTwoFactorOTP({
503 | 			headers,
504 | 			body: {
505 | 				trustDevice: false,
506 | 			},
507 | 		});
508 | 		expect(OTP.length).toBe(6);
509 | 		expect(sendOTP).toHaveBeenCalledWith(OTP);
510 | 	});
511 | 
512 | 	it("should verify otp", async () => {
513 | 		const res = await auth.api.verifyTwoFactorOTP({
514 | 			headers,
515 | 			body: {
516 | 				code: OTP,
517 | 			},
518 | 			asResponse: true,
519 | 		});
520 | 		expect(res.status).toBe(200);
521 | 		expect(res.headers.get("Set-Cookie")).toBeDefined();
522 | 		headers = convertSetCookieToCookie(res.headers);
523 | 	});
524 | 
525 | 	it("should disable two factor", async () => {
526 | 		const res = await auth.api.disableTwoFactor({
527 | 			headers,
528 | 			body: {
529 | 				password: testUser.password,
530 | 			},
531 | 			asResponse: true,
532 | 		});
533 | 		headers = convertSetCookieToCookie(res.headers);
534 | 		expect(res.status).toBe(200);
535 | 		const session = await auth.api.getSession({
536 | 			headers,
537 | 		});
538 | 		expect(session?.user.twoFactorEnabled).toBe(false);
539 | 	});
540 | });
541 | 
542 | describe("view backup codes", async () => {
543 | 	const sendOTP = vi.fn();
544 | 	const { auth, signInWithTestUser, testUser, db } = await getTestInstance({
545 | 		secret: DEFAULT_SECRET,
546 | 		plugins: [
547 | 			twoFactor({
548 | 				otpOptions: {
549 | 					sendOTP({ otp }) {
550 | 						sendOTP(otp);
551 | 					},
552 | 				},
553 | 				skipVerificationOnEnable: true,
554 | 			}),
555 | 		],
556 | 	});
557 | 	let { headers } = await signInWithTestUser();
558 | 
559 | 	let session = await auth.api.getSession({ headers });
560 | 	const userId = session?.user.id!;
561 | 
562 | 	it("should return parsed array of backup codes, not JSON string", async () => {
563 | 		const enableRes = await auth.api.enableTwoFactor({
564 | 			body: { password: testUser.password },
565 | 			headers,
566 | 			asResponse: true,
567 | 		});
568 | 
569 | 		expect(enableRes.status).toBe(200);
570 | 		headers = convertSetCookieToCookie(enableRes.headers);
571 | 
572 | 		const enableJson = (await enableRes.json()) as {
573 | 			backupCodes: string[];
574 | 		};
575 | 
576 | 		const viewResult = await auth.api.viewBackupCodes({
577 | 			body: { userId },
578 | 		});
579 | 
580 | 		expect(typeof viewResult.backupCodes).not.toBe("string");
581 | 		expect(Array.isArray(viewResult.backupCodes)).toBe(true);
582 | 		expect(viewResult.backupCodes.length).toBe(10);
583 | 		viewResult.backupCodes.forEach((code) => {
584 | 			expect(typeof code).toBe("string");
585 | 			expect(code.length).toBeGreaterThan(0);
586 | 		});
587 | 		expect(viewResult.backupCodes).toEqual(enableJson.backupCodes);
588 | 		expect(viewResult.status).toBe(true);
589 | 	});
590 | 
591 | 	it("should return array after generating new backup codes", async () => {
592 | 		const generateResult = await auth.api.generateBackupCodes({
593 | 			body: { password: testUser.password },
594 | 			headers,
595 | 		});
596 | 
597 | 		expect(generateResult.backupCodes).toBeDefined();
598 | 		expect(generateResult.backupCodes.length).toBe(10);
599 | 
600 | 		const viewResult = await auth.api.viewBackupCodes({
601 | 			body: { userId },
602 | 		});
603 | 
604 | 		expect(viewResult.status).toBe(true);
605 | 		expect(typeof viewResult.backupCodes).not.toBe("string");
606 | 		expect(Array.isArray(viewResult.backupCodes)).toBe(true);
607 | 		expect(viewResult.backupCodes.length).toBe(10);
608 | 		viewResult.backupCodes.forEach((code) => {
609 | 			expect(typeof code).toBe("string");
610 | 			expect(code.length).toBeGreaterThan(0);
611 | 		});
612 | 		expect(viewResult.backupCodes).toEqual(generateResult.backupCodes);
613 | 	});
614 | });
615 | 
```

--------------------------------------------------------------------------------
/packages/sso/src/oidc.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { afterAll, beforeAll, describe, expect, it } from "vitest";
  2 | import { getTestInstanceMemory as getTestInstance } from "better-auth/test";
  3 | import { sso } from ".";
  4 | import { OAuth2Server } from "oauth2-mock-server";
  5 | import { betterFetch } from "@better-fetch/fetch";
  6 | import { organization } from "better-auth/plugins";
  7 | import { createAuthClient } from "better-auth/client";
  8 | import { ssoClient } from "./client";
  9 | 
 10 | let server = new OAuth2Server();
 11 | 
 12 | describe("SSO", async () => {
 13 | 	const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
 14 | 		await getTestInstance({
 15 | 			plugins: [sso(), organization()],
 16 | 		});
 17 | 
 18 | 	const authClient = createAuthClient({
 19 | 		plugins: [ssoClient()],
 20 | 		baseURL: "http://localhost:3000",
 21 | 		fetchOptions: {
 22 | 			customFetchImpl,
 23 | 		},
 24 | 	});
 25 | 
 26 | 	beforeAll(async () => {
 27 | 		await server.issuer.keys.generate("RS256");
 28 | 		server.issuer.on;
 29 | 		await server.start(8080, "localhost");
 30 | 		console.log("Issuer URL:", server.issuer.url); // -> http://localhost:8080
 31 | 	});
 32 | 
 33 | 	afterAll(async () => {
 34 | 		await server.stop().catch(() => {});
 35 | 	});
 36 | 
 37 | 	server.service.on("beforeUserinfo", (userInfoResponse, req) => {
 38 | 		userInfoResponse.body = {
 39 | 			email: "[email protected]",
 40 | 			name: "OAuth2 Test",
 41 | 			sub: "oauth2",
 42 | 			picture: "https://test.com/picture.png",
 43 | 			email_verified: true,
 44 | 		};
 45 | 		userInfoResponse.statusCode = 200;
 46 | 	});
 47 | 
 48 | 	server.service.on("beforeTokenSigning", (token, req) => {
 49 | 		token.payload.email = "sso-user@localhost:8000.com";
 50 | 		token.payload.email_verified = true;
 51 | 		token.payload.name = "Test User";
 52 | 		token.payload.picture = "https://test.com/picture.png";
 53 | 	});
 54 | 
 55 | 	async function simulateOAuthFlow(
 56 | 		authUrl: string,
 57 | 		headers: Headers,
 58 | 		fetchImpl?: (...args: any) => any,
 59 | 	) {
 60 | 		let location: string | null = null;
 61 | 		await betterFetch(authUrl, {
 62 | 			method: "GET",
 63 | 			redirect: "manual",
 64 | 			onError(context) {
 65 | 				location = context.response.headers.get("location");
 66 | 			},
 67 | 		});
 68 | 
 69 | 		if (!location) throw new Error("No redirect location found");
 70 | 		const newHeaders = new Headers();
 71 | 		let callbackURL = "";
 72 | 		await betterFetch(location, {
 73 | 			method: "GET",
 74 | 			customFetchImpl: fetchImpl || customFetchImpl,
 75 | 			headers,
 76 | 			onError(context) {
 77 | 				callbackURL = context.response.headers.get("location") || "";
 78 | 				cookieSetter(newHeaders)(context);
 79 | 			},
 80 | 		});
 81 | 
 82 | 		return { callbackURL, headers: newHeaders };
 83 | 	}
 84 | 
 85 | 	it("should register a new SSO provider", async () => {
 86 | 		const { headers } = await signInWithTestUser();
 87 | 		const provider = await auth.api.registerSSOProvider({
 88 | 			body: {
 89 | 				issuer: server.issuer.url!,
 90 | 				domain: "localhost.com",
 91 | 				oidcConfig: {
 92 | 					clientId: "test",
 93 | 					clientSecret: "test",
 94 | 					authorizationEndpoint: `${server.issuer.url}/authorize`,
 95 | 					tokenEndpoint: `${server.issuer.url}/token`,
 96 | 					jwksEndpoint: `${server.issuer.url}/jwks`,
 97 | 					discoveryEndpoint: `${server.issuer.url}/.well-known/openid-configuration`,
 98 | 					mapping: {
 99 | 						id: "sub",
100 | 						email: "email",
101 | 						emailVerified: "email_verified",
102 | 						name: "name",
103 | 						image: "picture",
104 | 					},
105 | 				},
106 | 				providerId: "test",
107 | 			},
108 | 			headers,
109 | 		});
110 | 		expect(provider).toMatchObject({
111 | 			id: expect.any(String),
112 | 			issuer: "http://localhost:8080",
113 | 			oidcConfig: {
114 | 				issuer: "http://localhost:8080",
115 | 				clientId: "test",
116 | 				clientSecret: "test",
117 | 				authorizationEndpoint: "http://localhost:8080/authorize",
118 | 				tokenEndpoint: "http://localhost:8080/token",
119 | 				jwksEndpoint: "http://localhost:8080/jwks",
120 | 				discoveryEndpoint:
121 | 					"http://localhost:8080/.well-known/openid-configuration",
122 | 				mapping: {
123 | 					id: "sub",
124 | 					email: "email",
125 | 					emailVerified: "email_verified",
126 | 					name: "name",
127 | 					image: "picture",
128 | 				},
129 | 			},
130 | 			userId: expect.any(String),
131 | 		});
132 | 	});
133 | 
134 | 	it("should fail to register a new SSO provider with invalid issuer", async () => {
135 | 		const { headers } = await signInWithTestUser();
136 | 
137 | 		try {
138 | 			await auth.api.registerSSOProvider({
139 | 				body: {
140 | 					issuer: "invalid",
141 | 					domain: "localhost",
142 | 					providerId: "test",
143 | 					oidcConfig: {
144 | 						clientId: "test",
145 | 						clientSecret: "test",
146 | 					},
147 | 				},
148 | 				headers,
149 | 			});
150 | 		} catch (e) {
151 | 			expect(e).toMatchObject({
152 | 				status: "BAD_REQUEST",
153 | 				body: {
154 | 					message: "Invalid issuer. Must be a valid URL",
155 | 				},
156 | 			});
157 | 		}
158 | 	});
159 | 
160 | 	it("should not allow creating a provider with duplicate providerId", async () => {
161 | 		const { headers } = await signInWithTestUser();
162 | 
163 | 		await auth.api.registerSSOProvider({
164 | 			body: {
165 | 				issuer: server.issuer.url!,
166 | 				domain: "duplicate.com",
167 | 				providerId: "duplicate-oidc-provider",
168 | 				oidcConfig: {
169 | 					clientId: "test",
170 | 					clientSecret: "test",
171 | 				},
172 | 			},
173 | 			headers,
174 | 		});
175 | 
176 | 		await expect(
177 | 			auth.api.registerSSOProvider({
178 | 				body: {
179 | 					issuer: server.issuer.url!,
180 | 					domain: "another-duplicate.com",
181 | 					providerId: "duplicate-oidc-provider",
182 | 					oidcConfig: {
183 | 						clientId: "test2",
184 | 						clientSecret: "test2",
185 | 					},
186 | 				},
187 | 				headers,
188 | 			}),
189 | 		).rejects.toMatchObject({
190 | 			status: "UNPROCESSABLE_ENTITY",
191 | 			body: {
192 | 				message: "SSO provider with this providerId already exists",
193 | 			},
194 | 		});
195 | 	});
196 | 
197 | 	it("should sign in with SSO provider with email matching", async () => {
198 | 		const headers = new Headers();
199 | 		const res = await authClient.signIn.sso({
200 | 			email: "[email protected]",
201 | 			callbackURL: "/dashboard",
202 | 			fetchOptions: {
203 | 				throw: true,
204 | 				onSuccess: cookieSetter(headers),
205 | 			},
206 | 		});
207 | 		expect(res.url).toContain("http://localhost:8080/authorize");
208 | 		expect(res.url).toContain(
209 | 			"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
210 | 		);
211 | 		expect(res.url).toContain("login_hint=my-email%40localhost.com");
212 | 		const { callbackURL } = await simulateOAuthFlow(res.url, headers);
213 | 		expect(callbackURL).toContain("/dashboard");
214 | 	});
215 | 
216 | 	it("should sign in with SSO provider with domain", async () => {
217 | 		const headers = new Headers();
218 | 		const res = await authClient.signIn.sso({
219 | 			email: "[email protected]",
220 | 			domain: "localhost.com",
221 | 			callbackURL: "/dashboard",
222 | 			fetchOptions: {
223 | 				throw: true,
224 | 				onSuccess: cookieSetter(headers),
225 | 			},
226 | 		});
227 | 		expect(res.url).toContain("http://localhost:8080/authorize");
228 | 		expect(res.url).toContain(
229 | 			"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
230 | 		);
231 | 		const { callbackURL } = await simulateOAuthFlow(res.url, headers);
232 | 		expect(callbackURL).toContain("/dashboard");
233 | 	});
234 | 
235 | 	it("should sign in with SSO provider with providerId", async () => {
236 | 		const headers = new Headers();
237 | 		const res = await authClient.signIn.sso({
238 | 			providerId: "test",
239 | 			loginHint: "[email protected]",
240 | 			callbackURL: "/dashboard",
241 | 			fetchOptions: {
242 | 				throw: true,
243 | 				onSuccess: cookieSetter(headers),
244 | 			},
245 | 		});
246 | 		expect(res.url).toContain("http://localhost:8080/authorize");
247 | 		expect(res.url).toContain(
248 | 			"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
249 | 		);
250 | 		expect(res.url).toContain("login_hint=user%40example.com");
251 | 
252 | 		const { callbackURL } = await simulateOAuthFlow(res.url, headers);
253 | 		expect(callbackURL).toContain("/dashboard");
254 | 	});
255 | });
256 | 
257 | describe("SSO disable implicit sign in", async () => {
258 | 	const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
259 | 		await getTestInstance({
260 | 			plugins: [sso({ disableImplicitSignUp: true }), organization()],
261 | 		});
262 | 
263 | 	const authClient = createAuthClient({
264 | 		plugins: [ssoClient()],
265 | 		baseURL: "http://localhost:3000",
266 | 		fetchOptions: {
267 | 			customFetchImpl,
268 | 		},
269 | 	});
270 | 
271 | 	beforeAll(async () => {
272 | 		await server.issuer.keys.generate("RS256");
273 | 		server.issuer.on;
274 | 		await server.start(8080, "localhost");
275 | 		console.log("Issuer URL:", server.issuer.url); // -> http://localhost:8080
276 | 	});
277 | 
278 | 	afterAll(async () => {
279 | 		await server.stop();
280 | 	});
281 | 
282 | 	server.service.on("beforeUserinfo", (userInfoResponse, req) => {
283 | 		userInfoResponse.body = {
284 | 			email: "[email protected]",
285 | 			name: "OAuth2 Test",
286 | 			sub: "oauth2",
287 | 			picture: "https://test.com/picture.png",
288 | 			email_verified: true,
289 | 		};
290 | 		userInfoResponse.statusCode = 200;
291 | 	});
292 | 
293 | 	server.service.on("beforeTokenSigning", (token, req) => {
294 | 		token.payload.email = "sso-user@localhost:8000.com";
295 | 		token.payload.email_verified = true;
296 | 		token.payload.name = "Test User";
297 | 		token.payload.picture = "https://test.com/picture.png";
298 | 	});
299 | 
300 | 	async function simulateOAuthFlow(
301 | 		authUrl: string,
302 | 		headers: Headers,
303 | 		fetchImpl?: (...args: any) => any,
304 | 	) {
305 | 		let location: string | null = null;
306 | 		await betterFetch(authUrl, {
307 | 			method: "GET",
308 | 			redirect: "manual",
309 | 			onError(context) {
310 | 				location = context.response.headers.get("location");
311 | 			},
312 | 		});
313 | 
314 | 		if (!location) throw new Error("No redirect location found");
315 | 		const newHeaders = new Headers(headers);
316 | 		let callbackURL = "";
317 | 		await betterFetch(location, {
318 | 			method: "GET",
319 | 			customFetchImpl: fetchImpl || customFetchImpl,
320 | 			headers,
321 | 			onError(context) {
322 | 				callbackURL = context.response.headers.get("location") || "";
323 | 				cookieSetter(newHeaders)(context);
324 | 			},
325 | 		});
326 | 
327 | 		return { callbackURL, headers: newHeaders };
328 | 	}
329 | 
330 | 	it("should register a new SSO provider", async () => {
331 | 		const { headers } = await signInWithTestUser();
332 | 		const provider = await auth.api.registerSSOProvider({
333 | 			body: {
334 | 				issuer: server.issuer.url!,
335 | 				domain: "localhost.com",
336 | 				oidcConfig: {
337 | 					clientId: "test",
338 | 					clientSecret: "test",
339 | 					authorizationEndpoint: `${server.issuer.url}/authorize`,
340 | 					tokenEndpoint: `${server.issuer.url}/token`,
341 | 					jwksEndpoint: `${server.issuer.url}/jwks`,
342 | 					discoveryEndpoint: `${server.issuer.url}/.well-known/openid-configuration`,
343 | 					mapping: {
344 | 						id: "sub",
345 | 						email: "email",
346 | 						emailVerified: "email_verified",
347 | 						name: "name",
348 | 						image: "picture",
349 | 					},
350 | 				},
351 | 				providerId: "test",
352 | 			},
353 | 			headers,
354 | 		});
355 | 		expect(provider).toMatchObject({
356 | 			id: expect.any(String),
357 | 			issuer: "http://localhost:8080",
358 | 			oidcConfig: {
359 | 				issuer: "http://localhost:8080",
360 | 				clientId: "test",
361 | 				clientSecret: "test",
362 | 				authorizationEndpoint: "http://localhost:8080/authorize",
363 | 				tokenEndpoint: "http://localhost:8080/token",
364 | 				jwksEndpoint: "http://localhost:8080/jwks",
365 | 				discoveryEndpoint:
366 | 					"http://localhost:8080/.well-known/openid-configuration",
367 | 				mapping: {
368 | 					id: "sub",
369 | 					email: "email",
370 | 					emailVerified: "email_verified",
371 | 					name: "name",
372 | 					image: "picture",
373 | 				},
374 | 			},
375 | 			userId: expect.any(String),
376 | 		});
377 | 	});
378 | 
379 | 	it("should not create user with SSO provider when sign ups are disabled", async () => {
380 | 		const headers = new Headers();
381 | 		const res = await authClient.signIn.sso({
382 | 			email: "[email protected]",
383 | 			callbackURL: "/dashboard",
384 | 			fetchOptions: {
385 | 				throw: true,
386 | 				onSuccess: cookieSetter(headers),
387 | 			},
388 | 		});
389 | 		expect(res.url).toContain("http://localhost:8080/authorize");
390 | 		expect(res.url).toContain(
391 | 			"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
392 | 		);
393 | 		const { callbackURL } = await simulateOAuthFlow(res.url, headers);
394 | 		expect(callbackURL).toContain(
395 | 			"/api/auth/error/error?error=signup disabled",
396 | 		);
397 | 	});
398 | 
399 | 	it("should create user with SSO provider when sign ups are disabled but sign up is requested", async () => {
400 | 		const headers = new Headers();
401 | 		const res = await authClient.signIn.sso({
402 | 			email: "[email protected]",
403 | 			callbackURL: "/dashboard",
404 | 			requestSignUp: true,
405 | 			fetchOptions: {
406 | 				throw: true,
407 | 				onSuccess: cookieSetter(headers),
408 | 			},
409 | 		});
410 | 		expect(res.url).toContain("http://localhost:8080/authorize");
411 | 		expect(res.url).toContain(
412 | 			"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
413 | 		);
414 | 		const { callbackURL } = await simulateOAuthFlow(res.url, headers);
415 | 		expect(callbackURL).toContain("/dashboard");
416 | 	});
417 | });
418 | 
419 | describe("provisioning", async (ctx) => {
420 | 	const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
421 | 		await getTestInstance({
422 | 			plugins: [sso(), organization()],
423 | 		});
424 | 
425 | 	const authClient = createAuthClient({
426 | 		plugins: [ssoClient()],
427 | 		baseURL: "http://localhost:3000",
428 | 		fetchOptions: {
429 | 			customFetchImpl,
430 | 		},
431 | 	});
432 | 
433 | 	beforeAll(async () => {
434 | 		await server.issuer.keys.generate("RS256");
435 | 		server.issuer.on;
436 | 		await server.start(8080, "localhost");
437 | 		console.log("Issuer URL:", server.issuer.url); // -> http://localhost:8080
438 | 	});
439 | 
440 | 	afterAll(async () => {
441 | 		await server.stop();
442 | 	});
443 | 	async function simulateOAuthFlow(
444 | 		authUrl: string,
445 | 		headers: Headers,
446 | 		fetchImpl?: (...args: any) => any,
447 | 	) {
448 | 		let location: string | null = null;
449 | 		await betterFetch(authUrl, {
450 | 			method: "GET",
451 | 			redirect: "manual",
452 | 			onError(context) {
453 | 				location = context.response.headers.get("location");
454 | 			},
455 | 		});
456 | 
457 | 		if (!location) throw new Error("No redirect location found");
458 | 
459 | 		let callbackURL = "";
460 | 		const newHeaders = new Headers();
461 | 		await betterFetch(location, {
462 | 			method: "GET",
463 | 			customFetchImpl: fetchImpl || customFetchImpl,
464 | 			headers,
465 | 			onError(context) {
466 | 				callbackURL = context.response.headers.get("location") || "";
467 | 				cookieSetter(newHeaders)(context);
468 | 			},
469 | 		});
470 | 
471 | 		return callbackURL;
472 | 	}
473 | 
474 | 	server.service.on("beforeUserinfo", (userInfoResponse, req) => {
475 | 		userInfoResponse.body = {
476 | 			email: "[email protected]",
477 | 			name: "OAuth2 Test",
478 | 			sub: "oauth2",
479 | 			picture: "https://test.com/picture.png",
480 | 			email_verified: true,
481 | 		};
482 | 		userInfoResponse.statusCode = 200;
483 | 	});
484 | 
485 | 	server.service.on("beforeTokenSigning", (token, req) => {
486 | 		token.payload.email = "sso-user@localhost:8000.com";
487 | 		token.payload.email_verified = true;
488 | 		token.payload.name = "Test User";
489 | 		token.payload.picture = "https://test.com/picture.png";
490 | 	});
491 | 	it("should provision user", async () => {
492 | 		const { headers } = await signInWithTestUser();
493 | 		const organization = await auth.api.createOrganization({
494 | 			body: {
495 | 				name: "Localhost",
496 | 				slug: "localhost",
497 | 			},
498 | 			headers,
499 | 		});
500 | 		const provider = await auth.api.registerSSOProvider({
501 | 			body: {
502 | 				issuer: server.issuer.url!,
503 | 				domain: "localhost.com",
504 | 				oidcConfig: {
505 | 					clientId: "test",
506 | 					clientSecret: "test",
507 | 					authorizationEndpoint: `${server.issuer.url}/authorize`,
508 | 					tokenEndpoint: `${server.issuer.url}/token`,
509 | 					jwksEndpoint: `${server.issuer.url}/jwks`,
510 | 					discoveryEndpoint: `${server.issuer.url}/.well-known/openid-configuration`,
511 | 					mapping: {
512 | 						id: "sub",
513 | 						email: "email",
514 | 						emailVerified: "email_verified",
515 | 						name: "name",
516 | 						image: "picture",
517 | 					},
518 | 				},
519 | 				providerId: "test2",
520 | 				organizationId: organization?.id,
521 | 			},
522 | 			headers,
523 | 		});
524 | 		expect(provider).toMatchObject({
525 | 			organizationId: organization?.id,
526 | 		});
527 | 		const newHeaders = new Headers();
528 | 		const res = await authClient.signIn.sso({
529 | 			email: "[email protected]",
530 | 			callbackURL: "/dashboard",
531 | 			fetchOptions: {
532 | 				onSuccess: cookieSetter(newHeaders),
533 | 				throw: true,
534 | 			},
535 | 		});
536 | 		expect(res.url).toContain("http://localhost:8080/authorize");
537 | 		expect(res.url).toContain(
538 | 			"redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fsso%2Fcallback%2Ftest",
539 | 		);
540 | 
541 | 		const callbackURL = await simulateOAuthFlow(res.url, newHeaders);
542 | 		expect(callbackURL).toContain("/dashboard");
543 | 		const org = await auth.api.getFullOrganization({
544 | 			query: {
545 | 				organizationId: organization?.id || "",
546 | 			},
547 | 			headers,
548 | 		});
549 | 		const member = org?.members.find(
550 | 			(m: any) => m.user.email === "sso-user@localhost:8000.com",
551 | 		);
552 | 		expect(member).toMatchObject({
553 | 			role: "member",
554 | 			user: {
555 | 				id: expect.any(String),
556 | 				name: "Test User",
557 | 				email: "sso-user@localhost:8000.com",
558 | 				image: "https://test.com/picture.png",
559 | 			},
560 | 		});
561 | 	});
562 | 
563 | 	it("should sign in with SSO provide with org slug", async () => {
564 | 		const res = await auth.api.signInSSO({
565 | 			body: {
566 | 				organizationSlug: "localhost",
567 | 				callbackURL: "/dashboard",
568 | 			},
569 | 		});
570 | 
571 | 		expect(res.url).toContain("http://localhost:8080/authorize");
572 | 	});
573 | });
574 | 
```

--------------------------------------------------------------------------------
/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 | 
```

--------------------------------------------------------------------------------
/packages/better-auth/src/plugins/oidc-provider/types.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { InferOptionSchema, User } from "../../types";
  2 | import type { schema } from "./schema";
  3 | 
  4 | export interface OIDCOptions {
  5 | 	/**
  6 | 	 * The amount of time in seconds that the access token is valid for.
  7 | 	 *
  8 | 	 * @default 3600 (1 hour) - Recommended by the OIDC spec
  9 | 	 */
 10 | 	accessTokenExpiresIn?: number;
 11 | 	/**
 12 | 	 * Allow dynamic client registration.
 13 | 	 */
 14 | 	allowDynamicClientRegistration?: boolean;
 15 | 	/**
 16 | 	 * The metadata for the OpenID Connect provider.
 17 | 	 */
 18 | 	metadata?: Partial<OIDCMetadata>;
 19 | 	/**
 20 | 	 * The amount of time in seconds that the refresh token is valid for.
 21 | 	 *
 22 | 	 * @default 604800 (7 days) - Recommended by the OIDC spec
 23 | 	 */
 24 | 	refreshTokenExpiresIn?: number;
 25 | 	/**
 26 | 	 * The amount of time in seconds that the authorization code is valid for.
 27 | 	 *
 28 | 	 * @default 600 (10 minutes) - Recommended by the OIDC spec
 29 | 	 */
 30 | 	codeExpiresIn?: number;
 31 | 	/**
 32 | 	 * The scopes that the client is allowed to request.
 33 | 	 *
 34 | 	 * @see https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
 35 | 	 * @default
 36 | 	 * ```ts
 37 | 	 * ["openid", "profile", "email", "offline_access"]
 38 | 	 * ```
 39 | 	 */
 40 | 	scopes?: string[];
 41 | 	/**
 42 | 	 * The default scope to use if the client does not provide one.
 43 | 	 *
 44 | 	 * @default "openid"
 45 | 	 */
 46 | 	defaultScope?: string;
 47 | 	/**
 48 | 	 * A URL to the consent page where the user will be redirected if the client
 49 | 	 * requests consent.
 50 | 	 *
 51 | 	 * After the user consents, they should be redirected by the client to the
 52 | 	 * `redirect_uri` with the authorization code.
 53 | 	 *
 54 | 	 * When the server redirects the user to the consent page, it will include the
 55 | 	 * following query parameters:
 56 | 	 * - `consent_code` - The consent code to identify the authorization request.
 57 | 	 * - `client_id` - The ID of the client.
 58 | 	 * - `scope` - The requested scopes.
 59 | 	 *
 60 | 	 * Once the user consents, you need to call the `/oauth2/consent` endpoint
 61 | 	 * with `accept: true` and optionally the `consent_code` (if using URL parameter flow)
 62 | 	 * to complete the authorization. This will return the client to the `redirect_uri`
 63 | 	 * with the authorization code.
 64 | 	 *
 65 | 	 * @example
 66 | 	 * ```ts
 67 | 	 * consentPage: "/oauth/authorize"
 68 | 	 * ```
 69 | 	 */
 70 | 	consentPage?: string;
 71 | 	/**
 72 | 	 * The HTML for the consent page. This is used if `consentPage` is not
 73 | 	 * provided. This should be a function that returns an HTML string.
 74 | 	 * The function will be called with the following props:
 75 | 	 */
 76 | 	getConsentHTML?: (props: {
 77 | 		clientId: string;
 78 | 		clientName: string;
 79 | 		clientIcon?: string;
 80 | 		clientMetadata: Record<string, any> | null;
 81 | 		code: string;
 82 | 		scopes: string[];
 83 | 	}) => string;
 84 | 	/**
 85 | 	 * The URL to the login page. This is used if the client requests the `login`
 86 | 	 * prompt.
 87 | 	 */
 88 | 	loginPage: string;
 89 | 	/**
 90 | 	 * Whether to require PKCE (proof key code exchange) or not
 91 | 	 *
 92 | 	 * According to OAuth2.1 spec this should be required. But in any
 93 | 	 * case if you want to disable this you can use this options.
 94 | 	 *
 95 | 	 * @default true
 96 | 	 */
 97 | 	requirePKCE?: boolean;
 98 | 	/**
 99 | 	 * Allow plain to be used as a code challenge method.
100 | 	 *
101 | 	 * @default true
102 | 	 */
103 | 	allowPlainCodeChallengeMethod?: boolean;
104 | 	/**
105 | 	 * Custom function to generate a client ID.
106 | 	 */
107 | 	generateClientId?: () => string;
108 | 	/**
109 | 	 * Custom function to generate a client secret.
110 | 	 */
111 | 	generateClientSecret?: () => string;
112 | 	/**
113 | 	 * Get the additional user info claims
114 | 	 *
115 | 	 * This applies to the `userinfo` endpoint and the `id_token`.
116 | 	 *
117 | 	 * @param user - The user object.
118 | 	 * @param scopes - The scopes that the client requested.
119 | 	 * @param client - The client object.
120 | 	 * @returns The user info claim.
121 | 	 */
122 | 	getAdditionalUserInfoClaim?: (
123 | 		user: User & Record<string, any>,
124 | 		scopes: string[],
125 | 		client: Client,
126 | 	) => Record<string, any> | Promise<Record<string, any>>;
127 | 	/**
128 | 	 * Trusted clients that are configured directly in the provider options.
129 | 	 * These clients bypass database lookups and can optionally skip consent screens.
130 | 	 */
131 | 	trustedClients?: Client[];
132 | 	/**
133 | 	 * Store the client secret in your database in a secure way
134 | 	 * Note: This will not affect the client secret sent to the user, it will only affect the client secret stored in your database
135 | 	 *
136 | 	 * - "hashed" - The client secret is hashed using the `hash` function.
137 | 	 * - "plain" - The client secret is stored in the database in plain text.
138 | 	 * - "encrypted" - The client secret is encrypted using the `encrypt` function.
139 | 	 * - { hash: (clientSecret: string) => Promise<string> } - A function that hashes the client secret.
140 | 	 * - { encrypt: (clientSecret: string) => Promise<string>, decrypt: (clientSecret: string) => Promise<string> } - A function that encrypts and decrypts the client secret.
141 | 	 *
142 | 	 * @default "plain"
143 | 	 */
144 | 	storeClientSecret?:
145 | 		| "hashed"
146 | 		| "plain"
147 | 		| "encrypted"
148 | 		| { hash: (clientSecret: string) => Promise<string> }
149 | 		| {
150 | 				encrypt: (clientSecret: string) => Promise<string>;
151 | 				decrypt: (clientSecret: string) => Promise<string>;
152 | 		  };
153 | 	/**
154 | 	 * Whether to use the JWT plugin to sign the ID token.
155 | 	 *
156 | 	 * @default false
157 | 	 */
158 | 	useJWTPlugin?: boolean;
159 | 	/**
160 | 	 * Custom schema for the OIDC plugin
161 | 	 */
162 | 	schema?: InferOptionSchema<typeof schema>;
163 | }
164 | 
165 | export interface AuthorizationQuery {
166 | 	/**
167 | 	 * The response type. Must be 'code' or 'token'. Code is for authorization code flow, token is
168 | 	 * for implicit flow.
169 | 	 */
170 | 	response_type: "code" | "token";
171 | 	/**
172 | 	 * The redirect URI for the client. Must be one of the registered redirect URLs for the client.
173 | 	 */
174 | 	redirect_uri?: string;
175 | 	/**
176 | 	 * The scope of the request. Must be a space-separated list of case sensitive strings.
177 | 	 *
178 | 	 * - "openid" is required for all requests
179 | 	 * - "profile" is required for requests that require user profile information.
180 | 	 * - "email" is required for requests that require user email information.
181 | 	 * - "offline_access" is required for requests that require a refresh token.
182 | 	 */
183 | 	scope?: string;
184 | 	/**
185 | 	 * Opaque value used to maintain state between the request and the callback. Typically,
186 | 	 * Cross-Site Request Forgery (CSRF, XSRF) mitigation is done by cryptographically binding the
187 | 	 * value of this parameter with a browser cookie.
188 | 	 *
189 | 	 * Note: Better Auth stores the state in a database instead of a cookie. - This is to minimize
190 | 	 * the complication with native apps and other clients that may not have access to cookies.
191 | 	 */
192 | 	state: string;
193 | 	/**
194 | 	 * The client ID. Must be the ID of a registered client.
195 | 	 */
196 | 	client_id: string;
197 | 	/**
198 | 	 * The prompt parameter is used to specify the type of user interaction that is required.
199 | 	 */
200 | 	prompt?: "none" | "consent" | "login" | "select_account";
201 | 	/**
202 | 	 * The display parameter is used to specify how the authorization server displays the
203 | 	 * authentication and consent user interface pages to the end user.
204 | 	 */
205 | 	display?: "page" | "popup" | "touch" | "wap";
206 | 	/**
207 | 	 * End-User's preferred languages and scripts for the user interface, represented as a
208 | 	 * space-separated list of BCP47 [RFC5646] language tag values, ordered by preference. For
209 | 	 * instance, the value "fr-CA fr en" represents a preference for French as spoken in Canada,
210 | 	 * then French (without a region designation), followed by English (without a region
211 | 	 * designation).
212 | 	 *
213 | 	 * Better Auth does not support this parameter yet. It'll not throw an error if it's provided,
214 | 	 *
215 | 	 * 🏗️ currently not implemented
216 | 	 */
217 | 	ui_locales?: string;
218 | 	/**
219 | 	 * The maximum authentication age.
220 | 	 *
221 | 	 * Specifies the allowable elapsed time in seconds since the last time the End-User was
222 | 	 * actively authenticated by the provider. If the elapsed time is greater than this value, the
223 | 	 * provider MUST attempt to actively re-authenticate the End-User.
224 | 	 *
225 | 	 * Note that max_age=0 is equivalent to prompt=login.
226 | 	 */
227 | 	max_age?: number;
228 | 	/**
229 | 	 * Requested Authentication Context Class Reference values.
230 | 	 *
231 | 	 * Space-separated string that
232 | 	 * specifies the acr values that the Authorization Server is being requested to use for
233 | 	 * processing this Authentication Request, with the values appearing in order of preference.
234 | 	 * The Authentication Context Class satisfied by the authentication performed is returned as
235 | 	 * the acr Claim Value, as specified in Section 2. The acr Claim is requested as a Voluntary
236 | 	 * Claim by this parameter.
237 | 	 */
238 | 	acr_values?: string;
239 | 	/**
240 | 	 * Hint to the Authorization Server about the login identifier the End-User might use to log in
241 | 	 * (if necessary). This hint can be used by an RP if it first asks the End-User for their
242 | 	 * e-mail address (or other identifier) and then wants to pass that value as a hint to the
243 | 	 * discovered authorization service. It is RECOMMENDED that the hint value match the value used
244 | 	 * for discovery. This value MAY also be a phone number in the format specified for the
245 | 	 * phone_number Claim. The use of this parameter is left to the OP's discretion.
246 | 	 */
247 | 	login_hint?: string;
248 | 	/**
249 | 	 * ID Token previously issued by the Authorization Server being passed as a hint about the
250 | 	 * End-User's current or past authenticated session with the Client.
251 | 	 *
252 | 	 * 🏗️ currently not implemented
253 | 	 */
254 | 	id_token_hint?: string;
255 | 	/**
256 | 	 * Code challenge
257 | 	 */
258 | 	code_challenge?: string;
259 | 	/**
260 | 	 * Code challenge method used
261 | 	 */
262 | 	code_challenge_method?: "plain" | "s256";
263 | 	/**
264 | 	 * String value used to associate a Client session with an ID Token, and to mitigate replay
265 | 	 * attacks. The value is passed through unmodified from the Authentication Request to the ID Token.
266 | 	 * If present in the ID Token, Clients MUST verify that the nonce Claim Value is equal to the
267 | 	 * value of the nonce parameter sent in the Authentication Request. If present in the
268 | 	 * Authentication Request, Authorization Servers MUST include a nonce Claim in the ID Token
269 | 	 * with the Claim Value being the nonce value sent in the Authentication Request.
270 | 	 */
271 | 	nonce?: string;
272 | }
273 | 
274 | export interface Client {
275 | 	/**
276 | 	 * Client ID
277 | 	 *
278 | 	 * size 32
279 | 	 *
280 | 	 * as described on https://www.rfc-editor.org/rfc/rfc6749.html#section-2.2
281 | 	 */
282 | 	clientId: string;
283 | 	/**
284 | 	 * Client Secret
285 | 	 *
286 | 	 * A secret for the client, if required by the authorization server.
287 | 	 * Optional for public clients using PKCE.
288 | 	 *
289 | 	 * size 32
290 | 	 */
291 | 	clientSecret?: string;
292 | 	/**
293 | 	 * The client type
294 | 	 *
295 | 	 * as described on https://www.rfc-editor.org/rfc/rfc6749.html#section-2.1
296 | 	 *
297 | 	 * - web - A web application
298 | 	 * - native - A mobile application
299 | 	 * - user-agent-based - A user-agent-based application
300 | 	 * - public - A public client (PKCE-enabled, no client_secret)
301 | 	 */
302 | 	type: "web" | "native" | "user-agent-based" | "public";
303 | 	/**
304 | 	 * List of registered redirect URLs. Must include the whole URL, including the protocol, port,
305 | 	 * and path.
306 | 	 *
307 | 	 * For example, `https://example.com/auth/callback`
308 | 	 */
309 | 	redirectURLs: string[];
310 | 	/**
311 | 	 * The name of the client.
312 | 	 */
313 | 	name: string;
314 | 	/**
315 | 	 * The icon of the client.
316 | 	 */
317 | 	icon?: string;
318 | 	/**
319 | 	 * Additional metadata about the client.
320 | 	 */
321 | 	metadata: {
322 | 		[key: string]: any;
323 | 	} | null;
324 | 	/**
325 | 	 * Whether the client is disabled or not.
326 | 	 */
327 | 	disabled: boolean;
328 | 	/**
329 | 	 * Whether to skip the consent screen for this client.
330 | 	 * Only applies to trusted clients.
331 | 	 */
332 | 	skipConsent?: boolean;
333 | }
334 | 
335 | export interface TokenBody {
336 | 	/**
337 | 	 * The grant type. Must be 'authorization_code' or 'refresh_token'.
338 | 	 */
339 | 	grant_type: "authorization_code" | "refresh_token";
340 | 	/**
341 | 	 * The authorization code received from the authorization server.
342 | 	 */
343 | 	code?: string;
344 | 	/**
345 | 	 * The redirect URI of the client.
346 | 	 */
347 | 	redirect_uri?: string;
348 | 	/**
349 | 	 * The client ID.
350 | 	 */
351 | 	client_id?: string;
352 | 	/**
353 | 	 * The client secret.
354 | 	 */
355 | 	client_secret?: string;
356 | 	/**
357 | 	 * The refresh token received from the authorization server.
358 | 	 */
359 | 	refresh_token?: string;
360 | }
361 | 
362 | export interface CodeVerificationValue {
363 | 	/**
364 | 	 * The client ID
365 | 	 */
366 | 	clientId: string;
367 | 	/**
368 | 	 * The redirect URI for the client
369 | 	 */
370 | 	redirectURI: string;
371 | 	/**
372 | 	 * The scopes that the client requested
373 | 	 */
374 | 	scope: string[];
375 | 	/**
376 | 	 * The user ID
377 | 	 */
378 | 	userId: string;
379 | 	/**
380 | 	 * The time that the user authenticated
381 | 	 */
382 | 	authTime: number;
383 | 	/**
384 | 	 * Whether the user needs to consent to the scopes
385 | 	 * before the code can be exchanged for an access token.
386 | 	 *
387 | 	 * If this is true, then the code is treated as a consent
388 | 	 * request. Once the user consents, the code will be updated
389 | 	 * with the actual code.
390 | 	 */
391 | 	requireConsent: boolean;
392 | 	/**
393 | 	 * The state parameter from the request
394 | 	 *
395 | 	 * If the prompt is set to `consent`, then the state
396 | 	 * parameter is saved here. This is to prevent the client
397 | 	 * from using the code before the user consents.
398 | 	 */
399 | 	state: string | null;
400 | 	/**
401 | 	 * Code challenge
402 | 	 */
403 | 	codeChallenge?: string;
404 | 	/**
405 | 	 * Code Challenge Method
406 | 	 */
407 | 	codeChallengeMethod?: "sha256" | "plain";
408 | 	/**
409 | 	 * Nonce
410 | 	 */
411 | 	nonce?: string;
412 | }
413 | 
414 | export interface OAuthAccessToken {
415 | 	/**
416 | 	 * The access token
417 | 	 */
418 | 	accessToken: string;
419 | 	/**
420 | 	 * The refresh token
421 | 	 */
422 | 	refreshToken: string;
423 | 	/**
424 | 	 * The time that the access token expires
425 | 	 */
426 | 	accessTokenExpiresAt: Date;
427 | 	/**
428 | 	 * The time that the refresh token expires
429 | 	 */
430 | 	refreshTokenExpiresAt: Date;
431 | 	/**
432 | 	 * The client ID
433 | 	 */
434 | 	clientId: string;
435 | 	/**
436 | 	 * The user ID
437 | 	 */
438 | 	userId: string;
439 | 	/**
440 | 	 * The scopes that the access token has access to
441 | 	 */
442 | 	scopes: string;
443 | }
444 | 
445 | export interface OIDCMetadata {
446 | 	/**
447 | 	 * The issuer identifier, this is the URL of the provider and can be used to verify
448 | 	 * the `iss` claim in the ID token.
449 | 	 *
450 | 	 * default: the base URL of the server (e.g. `https://example.com`)
451 | 	 */
452 | 	issuer: string;
453 | 	/**
454 | 	 * The URL of the authorization endpoint.
455 | 	 *
456 | 	 * @default `/oauth2/authorize`
457 | 	 */
458 | 	authorization_endpoint: string;
459 | 	/**
460 | 	 * The URL of the token endpoint.
461 | 	 *
462 | 	 * @default `/oauth2/token`
463 | 	 */
464 | 	token_endpoint: string;
465 | 	/**
466 | 	 * The URL of the userinfo endpoint.
467 | 	 *
468 | 	 * @default `/oauth2/userinfo`
469 | 	 */
470 | 	userinfo_endpoint: string;
471 | 	/**
472 | 	 * The URL of the jwks_uri endpoint.
473 | 	 *
474 | 	 * For JWKS to work, you must install the `jwt` plugin.
475 | 	 *
476 | 	 * This value is automatically set to `/jwks` if the `jwt` plugin is installed.
477 | 	 *
478 | 	 * @default `/jwks`
479 | 	 */
480 | 	jwks_uri: string;
481 | 	/**
482 | 	 * The URL of the dynamic client registration endpoint.
483 | 	 *
484 | 	 * @default `/oauth2/register`
485 | 	 */
486 | 	registration_endpoint: string;
487 | 	/**
488 | 	 * Supported scopes.
489 | 	 */
490 | 	scopes_supported: string[];
491 | 	/**
492 | 	 * Supported response types.
493 | 	 *
494 | 	 * only `code` is supported.
495 | 	 */
496 | 	response_types_supported: ["code"];
497 | 	/**
498 | 	 * Supported response modes.
499 | 	 *
500 | 	 * `query`: the authorization code is returned in the query string
501 | 	 *
502 | 	 * only `query` is supported.
503 | 	 */
504 | 	response_modes_supported: ["query"];
505 | 	/**
506 | 	 * Supported grant types.
507 | 	 *
508 | 	 * The first element MUST be "authorization_code"; additional grant types like
509 | 	 * "refresh_token" can follow. Guarantees a non-empty array at the type level.
510 | 	 */
511 | 	grant_types_supported: [
512 | 		"authorization_code",
513 | 		...("authorization_code" | "refresh_token")[],
514 | 	];
515 | 	/**
516 | 	 * acr_values supported.
517 | 	 *
518 | 	 * - `urn:mace:incommon:iap:silver`: Silver level of assurance
519 | 	 * - `urn:mace:incommon:iap:bronze`: Bronze level of assurance
520 | 	 *
521 | 	 * only `urn:mace:incommon:iap:silver` and `urn:mace:incommon:iap:bronze` are supported.
522 | 	 *
523 | 	 *
524 | 	 * @default
525 | 	 * ["urn:mace:incommon:iap:silver", "urn:mace:incommon:iap:bronze"]
526 | 	 * @see https://incommon.org/federation/attributes.html
527 | 	 */
528 | 	acr_values_supported: string[];
529 | 	/**
530 | 	 * Supported subject types.
531 | 	 *
532 | 	 * pairwise: the subject identifier is unique to the client
533 | 	 * public: the subject identifier is unique to the server
534 | 	 *
535 | 	 * only `public` is supported.
536 | 	 */
537 | 	subject_types_supported: ["public"];
538 | 	/**
539 | 	 * Supported ID token signing algorithms.
540 | 	 */
541 | 	id_token_signing_alg_values_supported: string[];
542 | 	/**
543 | 	 * Supported token endpoint authentication methods.
544 | 	 *
545 | 	 * only `client_secret_basic` and `client_secret_post` are supported.
546 | 	 *
547 | 	 * @default
548 | 	 * ["client_secret_basic", "client_secret_post"]
549 | 	 */
550 | 	token_endpoint_auth_methods_supported: [
551 | 		"client_secret_basic",
552 | 		"client_secret_post",
553 | 		"none",
554 | 	];
555 | 	/**
556 | 	 * Supported claims.
557 | 	 *
558 | 	 * @default
559 | 	 * ["sub", "iss", "aud", "exp", "nbf", "iat", "jti", "email", "email_verified", "name"]
560 | 	 */
561 | 	claims_supported: string[];
562 | 	/**
563 | 	 * Supported code challenge methods.
564 | 	 *
565 | 	 * only `S256` is supported.
566 | 	 *
567 | 	 * @default ["S256"]
568 | 	 */
569 | 	code_challenge_methods_supported: ["S256"];
570 | }
571 | 
```
Page 37/68FirstPrevNextLast