This is page 59 of 69. 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 │ │ │ │ │ ├── demo.ts │ │ │ │ │ └── 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 │ │ └── vitest.setup.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/oidc-provider/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as z from "zod"; 2 | import { SignJWT } from "jose"; 3 | import { APIError, getSessionFromCtx, sessionMiddleware } from "../../api"; 4 | import { 5 | createAuthEndpoint, 6 | createAuthMiddleware, 7 | } from "@better-auth/core/api"; 8 | import type { BetterAuthPlugin } from "@better-auth/core"; 9 | import { 10 | generateRandomString, 11 | symmetricDecrypt, 12 | symmetricEncrypt, 13 | } from "../../crypto"; 14 | import { schema } from "./schema"; 15 | import type { 16 | Client, 17 | CodeVerificationValue, 18 | OAuthAccessToken, 19 | OIDCMetadata, 20 | OIDCOptions, 21 | } from "./types"; 22 | import { authorize } from "./authorize"; 23 | import { parseSetCookieHeader } from "../../cookies"; 24 | import { createHash } from "@better-auth/utils/hash"; 25 | import { base64 } from "@better-auth/utils/base64"; 26 | import { getJwtToken } from "../jwt/sign"; 27 | import type { jwt } from "../jwt"; 28 | import { defaultClientSecretHasher } from "./utils"; 29 | import { mergeSchema } from "../../db"; 30 | import type { GenericEndpointContext } from "@better-auth/core"; 31 | 32 | const getJwtPlugin = (ctx: GenericEndpointContext) => { 33 | return ctx.context.options.plugins?.find( 34 | (plugin) => plugin.id === "jwt", 35 | ) as ReturnType<typeof jwt>; 36 | }; 37 | 38 | /** 39 | * Get a client by ID, checking trusted clients first, then database 40 | */ 41 | export async function getClient( 42 | clientId: string, 43 | adapter: any, 44 | trustedClients: (Client & { skipConsent?: boolean })[] = [], 45 | ): Promise<(Client & { skipConsent?: boolean }) | null> { 46 | const trustedClient = trustedClients.find( 47 | (client) => client.clientId === clientId, 48 | ); 49 | if (trustedClient) { 50 | return trustedClient; 51 | } 52 | const dbClient = await adapter 53 | .findOne({ 54 | model: "oauthApplication", 55 | where: [{ field: "clientId", value: clientId }], 56 | }) 57 | .then((res: Record<string, any> | null) => { 58 | if (!res) { 59 | return null; 60 | } 61 | return { 62 | ...res, 63 | redirectURLs: (res.redirectURLs ?? "").split(","), 64 | metadata: res.metadata ? JSON.parse(res.metadata) : {}, 65 | } as Client; 66 | }); 67 | 68 | return dbClient; 69 | } 70 | 71 | export const getMetadata = ( 72 | ctx: GenericEndpointContext, 73 | options?: OIDCOptions, 74 | ): OIDCMetadata => { 75 | const jwtPlugin = getJwtPlugin(ctx); 76 | const issuer = 77 | jwtPlugin && jwtPlugin.options?.jwt && jwtPlugin.options.jwt.issuer 78 | ? jwtPlugin.options.jwt.issuer 79 | : (ctx.context.options.baseURL as string); 80 | const baseURL = ctx.context.baseURL; 81 | const supportedAlgs = options?.useJWTPlugin 82 | ? ["RS256", "EdDSA", "none"] 83 | : ["HS256", "none"]; 84 | return { 85 | issuer, 86 | authorization_endpoint: `${baseURL}/oauth2/authorize`, 87 | token_endpoint: `${baseURL}/oauth2/token`, 88 | userinfo_endpoint: `${baseURL}/oauth2/userinfo`, 89 | jwks_uri: `${baseURL}/jwks`, 90 | registration_endpoint: `${baseURL}/oauth2/register`, 91 | scopes_supported: ["openid", "profile", "email", "offline_access"], 92 | response_types_supported: ["code"], 93 | response_modes_supported: ["query"], 94 | grant_types_supported: ["authorization_code", "refresh_token"], 95 | acr_values_supported: [ 96 | "urn:mace:incommon:iap:silver", 97 | "urn:mace:incommon:iap:bronze", 98 | ], 99 | subject_types_supported: ["public"], 100 | id_token_signing_alg_values_supported: supportedAlgs, 101 | token_endpoint_auth_methods_supported: [ 102 | "client_secret_basic", 103 | "client_secret_post", 104 | "none", 105 | ], 106 | code_challenge_methods_supported: ["S256"], 107 | claims_supported: [ 108 | "sub", 109 | "iss", 110 | "aud", 111 | "exp", 112 | "nbf", 113 | "iat", 114 | "jti", 115 | "email", 116 | "email_verified", 117 | "name", 118 | ], 119 | ...options?.metadata, 120 | }; 121 | }; 122 | 123 | /** 124 | * OpenID Connect (OIDC) plugin for Better Auth. This plugin implements the 125 | * authorization code flow and the token exchange flow. It also implements the 126 | * userinfo endpoint. 127 | * 128 | * @param options - The options for the OIDC plugin. 129 | * @returns A Better Auth plugin. 130 | */ 131 | export const oidcProvider = (options: OIDCOptions) => { 132 | const modelName = { 133 | oauthClient: "oauthApplication", 134 | oauthAccessToken: "oauthAccessToken", 135 | oauthConsent: "oauthConsent", 136 | }; 137 | 138 | const opts = { 139 | codeExpiresIn: 600, 140 | defaultScope: "openid", 141 | accessTokenExpiresIn: 3600, 142 | refreshTokenExpiresIn: 604800, 143 | allowPlainCodeChallengeMethod: true, 144 | storeClientSecret: "plain" as const, 145 | ...options, 146 | scopes: [ 147 | "openid", 148 | "profile", 149 | "email", 150 | "offline_access", 151 | ...(options?.scopes || []), 152 | ], 153 | }; 154 | 155 | const trustedClients = options.trustedClients || []; 156 | 157 | /** 158 | * Store client secret according to the configured storage method 159 | */ 160 | async function storeClientSecret( 161 | ctx: GenericEndpointContext, 162 | clientSecret: string, 163 | ) { 164 | if (opts.storeClientSecret === "encrypted") { 165 | return await symmetricEncrypt({ 166 | key: ctx.context.secret, 167 | data: clientSecret, 168 | }); 169 | } 170 | if (opts.storeClientSecret === "hashed") { 171 | return await defaultClientSecretHasher(clientSecret); 172 | } 173 | if ( 174 | typeof opts.storeClientSecret === "object" && 175 | "hash" in opts.storeClientSecret 176 | ) { 177 | return await opts.storeClientSecret.hash(clientSecret); 178 | } 179 | if ( 180 | typeof opts.storeClientSecret === "object" && 181 | "encrypt" in opts.storeClientSecret 182 | ) { 183 | return await opts.storeClientSecret.encrypt(clientSecret); 184 | } 185 | 186 | return clientSecret; 187 | } 188 | 189 | /** 190 | * Verify stored client secret against provided client secret 191 | */ 192 | async function verifyStoredClientSecret( 193 | ctx: GenericEndpointContext, 194 | storedClientSecret: string, 195 | clientSecret: string, 196 | ): Promise<boolean> { 197 | if (opts.storeClientSecret === "encrypted") { 198 | return ( 199 | (await symmetricDecrypt({ 200 | key: ctx.context.secret, 201 | data: storedClientSecret, 202 | })) === clientSecret 203 | ); 204 | } 205 | if (opts.storeClientSecret === "hashed") { 206 | const hashedClientSecret = await defaultClientSecretHasher(clientSecret); 207 | return hashedClientSecret === storedClientSecret; 208 | } 209 | if ( 210 | typeof opts.storeClientSecret === "object" && 211 | "hash" in opts.storeClientSecret 212 | ) { 213 | const hashedClientSecret = 214 | await opts.storeClientSecret.hash(clientSecret); 215 | return hashedClientSecret === storedClientSecret; 216 | } 217 | if ( 218 | typeof opts.storeClientSecret === "object" && 219 | "decrypt" in opts.storeClientSecret 220 | ) { 221 | const decryptedClientSecret = 222 | await opts.storeClientSecret.decrypt(storedClientSecret); 223 | return decryptedClientSecret === clientSecret; 224 | } 225 | 226 | return clientSecret === storedClientSecret; 227 | } 228 | 229 | return { 230 | id: "oidc", 231 | hooks: { 232 | after: [ 233 | { 234 | matcher() { 235 | return true; 236 | }, 237 | handler: createAuthMiddleware(async (ctx) => { 238 | const cookie = await ctx.getSignedCookie( 239 | "oidc_login_prompt", 240 | ctx.context.secret, 241 | ); 242 | const cookieName = ctx.context.authCookies.sessionToken.name; 243 | const parsedSetCookieHeader = parseSetCookieHeader( 244 | ctx.context.responseHeaders?.get("set-cookie") || "", 245 | ); 246 | const hasSessionToken = parsedSetCookieHeader.has(cookieName); 247 | if (!cookie || !hasSessionToken) { 248 | return; 249 | } 250 | ctx.setCookie("oidc_login_prompt", "", { 251 | maxAge: 0, 252 | }); 253 | const sessionCookie = parsedSetCookieHeader.get(cookieName)?.value; 254 | const sessionToken = sessionCookie?.split(".")[0]!; 255 | if (!sessionToken) { 256 | return; 257 | } 258 | const session = 259 | await ctx.context.internalAdapter.findSession(sessionToken); 260 | if (!session) { 261 | return; 262 | } 263 | ctx.query = JSON.parse(cookie); 264 | // Don't force prompt to "consent" - let the authorize function 265 | // determine if consent is needed based on OIDC spec requirements 266 | ctx.context.session = session; 267 | const response = await authorize(ctx, opts); 268 | return response; 269 | }), 270 | }, 271 | ], 272 | }, 273 | endpoints: { 274 | getOpenIdConfig: createAuthEndpoint( 275 | "/.well-known/openid-configuration", 276 | { 277 | method: "GET", 278 | metadata: { 279 | isAction: false, 280 | }, 281 | }, 282 | async (ctx) => { 283 | const metadata = getMetadata(ctx, options); 284 | return ctx.json(metadata); 285 | }, 286 | ), 287 | oAuth2authorize: createAuthEndpoint( 288 | "/oauth2/authorize", 289 | { 290 | method: "GET", 291 | query: z.record(z.string(), z.any()), 292 | metadata: { 293 | openapi: { 294 | description: "Authorize an OAuth2 request", 295 | responses: { 296 | "200": { 297 | description: "Authorization response generated successfully", 298 | content: { 299 | "application/json": { 300 | schema: { 301 | type: "object", 302 | additionalProperties: true, 303 | description: 304 | "Authorization response, contents depend on the authorize function implementation", 305 | }, 306 | }, 307 | }, 308 | }, 309 | }, 310 | }, 311 | }, 312 | }, 313 | async (ctx) => { 314 | return authorize(ctx, opts); 315 | }, 316 | ), 317 | oAuthConsent: createAuthEndpoint( 318 | "/oauth2/consent", 319 | { 320 | method: "POST", 321 | body: z.object({ 322 | accept: z.boolean(), 323 | consent_code: z.string().optional().nullish(), 324 | }), 325 | use: [sessionMiddleware], 326 | metadata: { 327 | openapi: { 328 | description: 329 | "Handle OAuth2 consent. Supports both URL parameter-based flows (consent_code in body) and cookie-based flows (signed cookie).", 330 | requestBody: { 331 | required: true, 332 | content: { 333 | "application/json": { 334 | schema: { 335 | type: "object", 336 | properties: { 337 | accept: { 338 | type: "boolean", 339 | description: 340 | "Whether the user accepts or denies the consent request", 341 | }, 342 | consent_code: { 343 | type: "string", 344 | description: 345 | "The consent code from the authorization request. Optional if using cookie-based flow.", 346 | }, 347 | }, 348 | required: ["accept"], 349 | }, 350 | }, 351 | }, 352 | }, 353 | responses: { 354 | "200": { 355 | description: "Consent processed successfully", 356 | content: { 357 | "application/json": { 358 | schema: { 359 | type: "object", 360 | properties: { 361 | redirectURI: { 362 | type: "string", 363 | format: "uri", 364 | description: 365 | "The URI to redirect to, either with an authorization code or an error", 366 | }, 367 | }, 368 | required: ["redirectURI"], 369 | }, 370 | }, 371 | }, 372 | }, 373 | }, 374 | }, 375 | }, 376 | }, 377 | async (ctx) => { 378 | // Support both consent flow methods: 379 | // 1. URL parameter-based: consent_code in request body (standard OAuth2 pattern) 380 | // 2. Cookie-based: using signed cookie for stateful consent flows 381 | let consentCode: string | null = ctx.body.consent_code || null; 382 | 383 | if (!consentCode) { 384 | // Check for cookie-based consent flow 385 | consentCode = await ctx.getSignedCookie( 386 | "oidc_consent_prompt", 387 | ctx.context.secret, 388 | ); 389 | } 390 | 391 | if (!consentCode) { 392 | throw new APIError("UNAUTHORIZED", { 393 | error_description: 394 | "consent_code is required (either in body or cookie)", 395 | error: "invalid_request", 396 | }); 397 | } 398 | 399 | const verification = 400 | await ctx.context.internalAdapter.findVerificationValue( 401 | consentCode, 402 | ); 403 | if (!verification) { 404 | throw new APIError("UNAUTHORIZED", { 405 | error_description: "Invalid code", 406 | error: "invalid_request", 407 | }); 408 | } 409 | if (verification.expiresAt < new Date()) { 410 | throw new APIError("UNAUTHORIZED", { 411 | error_description: "Code expired", 412 | error: "invalid_request", 413 | }); 414 | } 415 | 416 | // Clear the cookie 417 | ctx.setCookie("oidc_consent_prompt", "", { 418 | maxAge: 0, 419 | }); 420 | 421 | const value = JSON.parse(verification.value) as CodeVerificationValue; 422 | if (!value.requireConsent) { 423 | throw new APIError("UNAUTHORIZED", { 424 | error_description: "Consent not required", 425 | error: "invalid_request", 426 | }); 427 | } 428 | 429 | if (!ctx.body.accept) { 430 | await ctx.context.internalAdapter.deleteVerificationValue( 431 | verification.id, 432 | ); 433 | return ctx.json({ 434 | redirectURI: `${value.redirectURI}?error=access_denied&error_description=User denied access`, 435 | }); 436 | } 437 | const code = generateRandomString(32, "a-z", "A-Z", "0-9"); 438 | const codeExpiresInMs = opts.codeExpiresIn * 1000; 439 | const expiresAt = new Date(Date.now() + codeExpiresInMs); 440 | await ctx.context.internalAdapter.updateVerificationValue( 441 | verification.id, 442 | { 443 | value: JSON.stringify({ 444 | ...value, 445 | requireConsent: false, 446 | }), 447 | identifier: code, 448 | expiresAt, 449 | }, 450 | ); 451 | await ctx.context.adapter.create({ 452 | model: modelName.oauthConsent, 453 | data: { 454 | clientId: value.clientId, 455 | userId: value.userId, 456 | scopes: value.scope.join(" "), 457 | consentGiven: true, 458 | createdAt: new Date(), 459 | updatedAt: new Date(), 460 | }, 461 | }); 462 | const redirectURI = new URL(value.redirectURI); 463 | redirectURI.searchParams.set("code", code); 464 | if (value.state) redirectURI.searchParams.set("state", value.state); 465 | return ctx.json({ 466 | redirectURI: redirectURI.toString(), 467 | }); 468 | }, 469 | ), 470 | oAuth2token: createAuthEndpoint( 471 | "/oauth2/token", 472 | { 473 | method: "POST", 474 | body: z.record(z.any(), z.any()), 475 | metadata: { 476 | isAction: false, 477 | }, 478 | }, 479 | async (ctx) => { 480 | let { body } = ctx; 481 | if (!body) { 482 | throw new APIError("BAD_REQUEST", { 483 | error_description: "request body not found", 484 | error: "invalid_request", 485 | }); 486 | } 487 | if (body instanceof FormData) { 488 | body = Object.fromEntries(body.entries()); 489 | } 490 | if (!(body instanceof Object)) { 491 | throw new APIError("BAD_REQUEST", { 492 | error_description: "request body is not an object", 493 | error: "invalid_request", 494 | }); 495 | } 496 | let { client_id, client_secret } = body; 497 | const authorization = 498 | ctx.request?.headers.get("authorization") || null; 499 | if ( 500 | authorization && 501 | !client_id && 502 | !client_secret && 503 | authorization.startsWith("Basic ") 504 | ) { 505 | try { 506 | const encoded = authorization.replace("Basic ", ""); 507 | const decoded = new TextDecoder().decode(base64.decode(encoded)); 508 | if (!decoded.includes(":")) { 509 | throw new APIError("UNAUTHORIZED", { 510 | error_description: "invalid authorization header format", 511 | error: "invalid_client", 512 | }); 513 | } 514 | const [id, secret] = decoded.split(":"); 515 | if (!id || !secret) { 516 | throw new APIError("UNAUTHORIZED", { 517 | error_description: "invalid authorization header format", 518 | error: "invalid_client", 519 | }); 520 | } 521 | client_id = id; 522 | client_secret = secret; 523 | } catch (error) { 524 | throw new APIError("UNAUTHORIZED", { 525 | error_description: "invalid authorization header format", 526 | error: "invalid_client", 527 | }); 528 | } 529 | } 530 | 531 | const now = Date.now(); 532 | const iat = Math.floor(now / 1000); 533 | const exp = iat + (opts.accessTokenExpiresIn ?? 3600); 534 | 535 | const accessTokenExpiresAt = new Date(exp * 1000); 536 | const refreshTokenExpiresAt = new Date( 537 | (iat + (opts.refreshTokenExpiresIn ?? 604800)) * 1000, 538 | ); 539 | 540 | const { 541 | grant_type, 542 | code, 543 | redirect_uri, 544 | refresh_token, 545 | code_verifier, 546 | } = body; 547 | if (grant_type === "refresh_token") { 548 | if (!refresh_token) { 549 | throw new APIError("BAD_REQUEST", { 550 | error_description: "refresh_token is required", 551 | error: "invalid_request", 552 | }); 553 | } 554 | const token = await ctx.context.adapter.findOne<OAuthAccessToken>({ 555 | model: modelName.oauthAccessToken, 556 | where: [ 557 | { 558 | field: "refreshToken", 559 | value: refresh_token.toString(), 560 | }, 561 | ], 562 | }); 563 | if (!token) { 564 | throw new APIError("UNAUTHORIZED", { 565 | error_description: "invalid refresh token", 566 | error: "invalid_grant", 567 | }); 568 | } 569 | if (token.clientId !== client_id?.toString()) { 570 | throw new APIError("UNAUTHORIZED", { 571 | error_description: "invalid client_id", 572 | error: "invalid_client", 573 | }); 574 | } 575 | if (token.refreshTokenExpiresAt < new Date()) { 576 | throw new APIError("UNAUTHORIZED", { 577 | error_description: "refresh token expired", 578 | error: "invalid_grant", 579 | }); 580 | } 581 | const accessToken = generateRandomString(32, "a-z", "A-Z"); 582 | const newRefreshToken = generateRandomString(32, "a-z", "A-Z"); 583 | 584 | await ctx.context.adapter.create({ 585 | model: modelName.oauthAccessToken, 586 | data: { 587 | accessToken, 588 | refreshToken: newRefreshToken, 589 | accessTokenExpiresAt, 590 | refreshTokenExpiresAt, 591 | clientId: client_id.toString(), 592 | userId: token.userId, 593 | scopes: token.scopes, 594 | createdAt: new Date(iat * 1000), 595 | updatedAt: new Date(iat * 1000), 596 | }, 597 | }); 598 | return ctx.json({ 599 | access_token: accessToken, 600 | token_type: "Bearer", 601 | expires_in: opts.accessTokenExpiresIn, 602 | refresh_token: newRefreshToken, 603 | scope: token.scopes, 604 | }); 605 | } 606 | 607 | if (!code) { 608 | throw new APIError("BAD_REQUEST", { 609 | error_description: "code is required", 610 | error: "invalid_request", 611 | }); 612 | } 613 | 614 | if (options.requirePKCE && !code_verifier) { 615 | throw new APIError("BAD_REQUEST", { 616 | error_description: "code verifier is missing", 617 | error: "invalid_request", 618 | }); 619 | } 620 | 621 | /** 622 | * We need to check if the code is valid before we can proceed 623 | * with the rest of the request. 624 | */ 625 | const verificationValue = 626 | await ctx.context.internalAdapter.findVerificationValue( 627 | code.toString(), 628 | ); 629 | if (!verificationValue) { 630 | throw new APIError("UNAUTHORIZED", { 631 | error_description: "invalid code", 632 | error: "invalid_grant", 633 | }); 634 | } 635 | if (verificationValue.expiresAt < new Date()) { 636 | throw new APIError("UNAUTHORIZED", { 637 | error_description: "code expired", 638 | error: "invalid_grant", 639 | }); 640 | } 641 | 642 | await ctx.context.internalAdapter.deleteVerificationValue( 643 | verificationValue.id, 644 | ); 645 | if (!client_id) { 646 | throw new APIError("UNAUTHORIZED", { 647 | error_description: "client_id is required", 648 | error: "invalid_client", 649 | }); 650 | } 651 | if (!grant_type) { 652 | throw new APIError("BAD_REQUEST", { 653 | error_description: "grant_type is required", 654 | error: "invalid_request", 655 | }); 656 | } 657 | if (grant_type !== "authorization_code") { 658 | throw new APIError("BAD_REQUEST", { 659 | error_description: "grant_type must be 'authorization_code'", 660 | error: "unsupported_grant_type", 661 | }); 662 | } 663 | 664 | if (!redirect_uri) { 665 | throw new APIError("BAD_REQUEST", { 666 | error_description: "redirect_uri is required", 667 | error: "invalid_request", 668 | }); 669 | } 670 | 671 | const client = await getClient( 672 | client_id.toString(), 673 | ctx.context.adapter, 674 | trustedClients, 675 | ); 676 | if (!client) { 677 | throw new APIError("UNAUTHORIZED", { 678 | error_description: "invalid client_id", 679 | error: "invalid_client", 680 | }); 681 | } 682 | if (client.disabled) { 683 | throw new APIError("UNAUTHORIZED", { 684 | error_description: "client is disabled", 685 | error: "invalid_client", 686 | }); 687 | } 688 | 689 | const value = JSON.parse( 690 | verificationValue.value, 691 | ) as CodeVerificationValue; 692 | if (value.clientId !== client_id.toString()) { 693 | throw new APIError("UNAUTHORIZED", { 694 | error_description: "invalid client_id", 695 | error: "invalid_client", 696 | }); 697 | } 698 | if (value.redirectURI !== redirect_uri.toString()) { 699 | throw new APIError("UNAUTHORIZED", { 700 | error_description: "invalid redirect_uri", 701 | error: "invalid_client", 702 | }); 703 | } 704 | if (value.codeChallenge && !code_verifier) { 705 | throw new APIError("BAD_REQUEST", { 706 | error_description: "code verifier is missing", 707 | error: "invalid_request", 708 | }); 709 | } 710 | if (client.type === "public") { 711 | // For public clients (type: 'public'), validate PKCE instead of client_secret 712 | if (!code_verifier) { 713 | throw new APIError("BAD_REQUEST", { 714 | error_description: 715 | "code verifier is required for public clients", 716 | error: "invalid_request", 717 | }); 718 | } 719 | // PKCE validation happens later in the flow, so we skip client_secret validation 720 | } else { 721 | if (!client.clientSecret || !client_secret) { 722 | throw new APIError("UNAUTHORIZED", { 723 | error_description: 724 | "client_secret is required for confidential clients", 725 | error: "invalid_client", 726 | }); 727 | } 728 | const isValidSecret = await verifyStoredClientSecret( 729 | ctx, 730 | client.clientSecret, 731 | client_secret.toString(), 732 | ); 733 | if (!isValidSecret) { 734 | throw new APIError("UNAUTHORIZED", { 735 | error_description: "invalid client_secret", 736 | error: "invalid_client", 737 | }); 738 | } 739 | } 740 | const challenge = 741 | value.codeChallengeMethod === "plain" 742 | ? code_verifier 743 | : await createHash("SHA-256", "base64urlnopad").digest( 744 | code_verifier, 745 | ); 746 | 747 | if (challenge !== value.codeChallenge) { 748 | throw new APIError("UNAUTHORIZED", { 749 | error_description: "code verification failed", 750 | error: "invalid_request", 751 | }); 752 | } 753 | 754 | const requestedScopes = value.scope; 755 | await ctx.context.internalAdapter.deleteVerificationValue( 756 | verificationValue.id, 757 | ); 758 | const accessToken = generateRandomString(32, "a-z", "A-Z"); 759 | const refreshToken = generateRandomString(32, "A-Z", "a-z"); 760 | await ctx.context.adapter.create({ 761 | model: modelName.oauthAccessToken, 762 | data: { 763 | accessToken, 764 | refreshToken, 765 | accessTokenExpiresAt, 766 | refreshTokenExpiresAt, 767 | clientId: client_id.toString(), 768 | userId: value.userId, 769 | scopes: requestedScopes.join(" "), 770 | createdAt: new Date(iat * 1000), 771 | updatedAt: new Date(iat * 1000), 772 | }, 773 | }); 774 | const user = await ctx.context.internalAdapter.findUserById( 775 | value.userId, 776 | ); 777 | if (!user) { 778 | throw new APIError("UNAUTHORIZED", { 779 | error_description: "user not found", 780 | error: "invalid_grant", 781 | }); 782 | } 783 | 784 | const profile = { 785 | given_name: user.name.split(" ")[0]!, 786 | family_name: user.name.split(" ")[1]!, 787 | name: user.name, 788 | profile: user.image, 789 | updated_at: new Date(user.updatedAt).toISOString(), 790 | }; 791 | const email = { 792 | email: user.email, 793 | email_verified: user.emailVerified, 794 | }; 795 | const userClaims = { 796 | ...(requestedScopes.includes("profile") ? profile : {}), 797 | ...(requestedScopes.includes("email") ? email : {}), 798 | }; 799 | 800 | const additionalUserClaims = options.getAdditionalUserInfoClaim 801 | ? await options.getAdditionalUserInfoClaim( 802 | user, 803 | requestedScopes, 804 | client, 805 | ) 806 | : {}; 807 | 808 | const payload = { 809 | sub: user.id, 810 | aud: client_id.toString(), 811 | iat: Date.now(), 812 | auth_time: ctx.context.session 813 | ? new Date(ctx.context.session.session.createdAt).getTime() 814 | : undefined, 815 | nonce: value.nonce, 816 | acr: "urn:mace:incommon:iap:silver", // default to silver - ⚠︎ this should be configurable and should be validated against the client's metadata 817 | ...userClaims, 818 | ...additionalUserClaims, 819 | }; 820 | const expirationTime = 821 | Math.floor(Date.now() / 1000) + opts.accessTokenExpiresIn; 822 | 823 | let idToken: string; 824 | 825 | // The JWT plugin is enabled, so we use the JWKS keys to sign 826 | if (options.useJWTPlugin) { 827 | const jwtPlugin = getJwtPlugin(ctx); 828 | if (!jwtPlugin) { 829 | ctx.context.logger.error( 830 | "OIDC: `useJWTPlugin` is enabled but the JWT plugin is not available. Make sure you have the JWT Plugin in your plugins array or set `useJWTPlugin` to false.", 831 | ); 832 | throw new APIError("INTERNAL_SERVER_ERROR", { 833 | error_description: "JWT plugin is not enabled", 834 | error: "internal_server_error", 835 | }); 836 | } 837 | idToken = await getJwtToken( 838 | { 839 | ...ctx, 840 | context: { 841 | ...ctx.context, 842 | session: { 843 | session: { 844 | id: generateRandomString(32, "a-z", "A-Z"), 845 | createdAt: new Date(iat * 1000), 846 | updatedAt: new Date(iat * 1000), 847 | userId: user.id, 848 | expiresAt: accessTokenExpiresAt, 849 | token: accessToken, 850 | ipAddress: ctx.request?.headers.get("x-forwarded-for"), 851 | }, 852 | user, 853 | }, 854 | }, 855 | }, 856 | { 857 | ...jwtPlugin.options, 858 | jwt: { 859 | ...jwtPlugin.options?.jwt, 860 | getSubject: () => user.id, 861 | audience: client_id.toString(), 862 | issuer: ctx.context.options.baseURL, 863 | expirationTime, 864 | definePayload: () => payload, 865 | }, 866 | }, 867 | ); 868 | 869 | // If the JWT token is not enabled, create a key and use it to sign 870 | } else { 871 | idToken = await new SignJWT(payload) 872 | .setProtectedHeader({ alg: "HS256" }) 873 | .setIssuedAt(iat) 874 | .setExpirationTime(accessTokenExpiresAt) 875 | .sign(new TextEncoder().encode(client.clientSecret)); 876 | } 877 | 878 | return ctx.json( 879 | { 880 | access_token: accessToken, 881 | token_type: "Bearer", 882 | expires_in: opts.accessTokenExpiresIn, 883 | refresh_token: requestedScopes.includes("offline_access") 884 | ? refreshToken 885 | : undefined, 886 | scope: requestedScopes.join(" "), 887 | id_token: requestedScopes.includes("openid") 888 | ? idToken 889 | : undefined, 890 | }, 891 | { 892 | headers: { 893 | "Cache-Control": "no-store", 894 | Pragma: "no-cache", 895 | }, 896 | }, 897 | ); 898 | }, 899 | ), 900 | oAuth2userInfo: createAuthEndpoint( 901 | "/oauth2/userinfo", 902 | { 903 | method: "GET", 904 | 905 | metadata: { 906 | isAction: false, 907 | openapi: { 908 | description: "Get OAuth2 user information", 909 | responses: { 910 | "200": { 911 | description: "User information retrieved successfully", 912 | content: { 913 | "application/json": { 914 | schema: { 915 | type: "object", 916 | properties: { 917 | sub: { 918 | type: "string", 919 | description: "Subject identifier (user ID)", 920 | }, 921 | email: { 922 | type: "string", 923 | format: "email", 924 | nullable: true, 925 | description: 926 | "User's email address, included if 'email' scope is granted", 927 | }, 928 | name: { 929 | type: "string", 930 | nullable: true, 931 | description: 932 | "User's full name, included if 'profile' scope is granted", 933 | }, 934 | picture: { 935 | type: "string", 936 | format: "uri", 937 | nullable: true, 938 | description: 939 | "User's profile picture URL, included if 'profile' scope is granted", 940 | }, 941 | given_name: { 942 | type: "string", 943 | nullable: true, 944 | description: 945 | "User's given name, included if 'profile' scope is granted", 946 | }, 947 | family_name: { 948 | type: "string", 949 | nullable: true, 950 | description: 951 | "User's family name, included if 'profile' scope is granted", 952 | }, 953 | email_verified: { 954 | type: "boolean", 955 | nullable: true, 956 | description: 957 | "Whether the email is verified, included if 'email' scope is granted", 958 | }, 959 | }, 960 | required: ["sub"], 961 | }, 962 | }, 963 | }, 964 | }, 965 | }, 966 | }, 967 | }, 968 | }, 969 | async (ctx) => { 970 | if (!ctx.request) { 971 | throw new APIError("UNAUTHORIZED", { 972 | error_description: "request not found", 973 | error: "invalid_request", 974 | }); 975 | } 976 | const authorization = ctx.request.headers.get("authorization"); 977 | if (!authorization) { 978 | throw new APIError("UNAUTHORIZED", { 979 | error_description: "authorization header not found", 980 | error: "invalid_request", 981 | }); 982 | } 983 | const token = authorization.replace("Bearer ", ""); 984 | const accessToken = 985 | await ctx.context.adapter.findOne<OAuthAccessToken>({ 986 | model: modelName.oauthAccessToken, 987 | where: [ 988 | { 989 | field: "accessToken", 990 | value: token, 991 | }, 992 | ], 993 | }); 994 | if (!accessToken) { 995 | throw new APIError("UNAUTHORIZED", { 996 | error_description: "invalid access token", 997 | error: "invalid_token", 998 | }); 999 | } 1000 | if (accessToken.accessTokenExpiresAt < new Date()) { 1001 | throw new APIError("UNAUTHORIZED", { 1002 | error_description: "The Access Token expired", 1003 | error: "invalid_token", 1004 | }); 1005 | } 1006 | 1007 | const client = await getClient( 1008 | accessToken.clientId, 1009 | ctx.context.adapter, 1010 | trustedClients, 1011 | ); 1012 | if (!client) { 1013 | throw new APIError("UNAUTHORIZED", { 1014 | error_description: "client not found", 1015 | error: "invalid_token", 1016 | }); 1017 | } 1018 | 1019 | const user = await ctx.context.internalAdapter.findUserById( 1020 | accessToken.userId, 1021 | ); 1022 | if (!user) { 1023 | throw new APIError("UNAUTHORIZED", { 1024 | error_description: "user not found", 1025 | error: "invalid_token", 1026 | }); 1027 | } 1028 | const requestedScopes = accessToken.scopes.split(" "); 1029 | const baseUserClaims = { 1030 | sub: user.id, 1031 | email: requestedScopes.includes("email") ? user.email : undefined, 1032 | name: requestedScopes.includes("profile") ? user.name : undefined, 1033 | picture: requestedScopes.includes("profile") 1034 | ? user.image 1035 | : undefined, 1036 | given_name: requestedScopes.includes("profile") 1037 | ? user.name.split(" ")[0]! 1038 | : undefined, 1039 | family_name: requestedScopes.includes("profile") 1040 | ? user.name.split(" ")[1]! 1041 | : undefined, 1042 | email_verified: requestedScopes.includes("email") 1043 | ? user.emailVerified 1044 | : undefined, 1045 | }; 1046 | const userClaims = options.getAdditionalUserInfoClaim 1047 | ? await options.getAdditionalUserInfoClaim( 1048 | user, 1049 | requestedScopes, 1050 | client, 1051 | ) 1052 | : baseUserClaims; 1053 | return ctx.json({ 1054 | ...baseUserClaims, 1055 | ...userClaims, 1056 | }); 1057 | }, 1058 | ), 1059 | /** 1060 | * ### Endpoint 1061 | * 1062 | * POST `/oauth2/register` 1063 | * 1064 | * ### API Methods 1065 | * 1066 | * **server:** 1067 | * `auth.api.registerOAuthApplication` 1068 | * 1069 | * **client:** 1070 | * `authClient.oauth2.register` 1071 | * 1072 | * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/oidc-provider#api-method-oauth2-register) 1073 | */ 1074 | registerOAuthApplication: createAuthEndpoint( 1075 | "/oauth2/register", 1076 | { 1077 | method: "POST", 1078 | body: z.object({ 1079 | redirect_uris: z.array(z.string()).meta({ 1080 | description: 1081 | 'A list of redirect URIs. Eg: ["https://client.example.com/callback"]', 1082 | }), 1083 | token_endpoint_auth_method: z 1084 | .enum(["none", "client_secret_basic", "client_secret_post"]) 1085 | .meta({ 1086 | description: 1087 | 'The authentication method for the token endpoint. Eg: "client_secret_basic"', 1088 | }) 1089 | .default("client_secret_basic") 1090 | .optional(), 1091 | grant_types: z 1092 | .array( 1093 | z.enum([ 1094 | "authorization_code", 1095 | "implicit", 1096 | "password", 1097 | "client_credentials", 1098 | "refresh_token", 1099 | "urn:ietf:params:oauth:grant-type:jwt-bearer", 1100 | "urn:ietf:params:oauth:grant-type:saml2-bearer", 1101 | ]), 1102 | ) 1103 | .meta({ 1104 | description: 1105 | 'The grant types supported by the application. Eg: ["authorization_code"]', 1106 | }) 1107 | .default(["authorization_code"]) 1108 | .optional(), 1109 | response_types: z 1110 | .array(z.enum(["code", "token"])) 1111 | .meta({ 1112 | description: 1113 | 'The response types supported by the application. Eg: ["code"]', 1114 | }) 1115 | .default(["code"]) 1116 | .optional(), 1117 | client_name: z 1118 | .string() 1119 | .meta({ 1120 | description: 'The name of the application. Eg: "My App"', 1121 | }) 1122 | .optional(), 1123 | client_uri: z 1124 | .string() 1125 | .meta({ 1126 | description: 1127 | 'The URI of the application. Eg: "https://client.example.com"', 1128 | }) 1129 | .optional(), 1130 | logo_uri: z 1131 | .string() 1132 | .meta({ 1133 | description: 1134 | 'The URI of the application logo. Eg: "https://client.example.com/logo.png"', 1135 | }) 1136 | .optional(), 1137 | scope: z 1138 | .string() 1139 | .meta({ 1140 | description: 1141 | 'The scopes supported by the application. Separated by spaces. Eg: "profile email"', 1142 | }) 1143 | .optional(), 1144 | contacts: z 1145 | .array(z.string()) 1146 | .meta({ 1147 | description: 1148 | 'The contact information for the application. Eg: ["[email protected]"]', 1149 | }) 1150 | .optional(), 1151 | tos_uri: z 1152 | .string() 1153 | .meta({ 1154 | description: 1155 | 'The URI of the application terms of service. Eg: "https://client.example.com/tos"', 1156 | }) 1157 | .optional(), 1158 | policy_uri: z 1159 | .string() 1160 | .meta({ 1161 | description: 1162 | 'The URI of the application privacy policy. Eg: "https://client.example.com/policy"', 1163 | }) 1164 | .optional(), 1165 | jwks_uri: z 1166 | .string() 1167 | .meta({ 1168 | description: 1169 | 'The URI of the application JWKS. Eg: "https://client.example.com/jwks"', 1170 | }) 1171 | .optional(), 1172 | jwks: z 1173 | .record(z.any(), z.any()) 1174 | .meta({ 1175 | description: 1176 | 'The JWKS of the application. Eg: {"keys": [{"kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "..."}]}', 1177 | }) 1178 | .optional(), 1179 | metadata: z 1180 | .record(z.any(), z.any()) 1181 | .meta({ 1182 | description: 1183 | 'The metadata of the application. Eg: {"key": "value"}', 1184 | }) 1185 | .optional(), 1186 | software_id: z 1187 | .string() 1188 | .meta({ 1189 | description: 1190 | 'The software ID of the application. Eg: "my-software"', 1191 | }) 1192 | .optional(), 1193 | software_version: z 1194 | .string() 1195 | .meta({ 1196 | description: 1197 | 'The software version of the application. Eg: "1.0.0"', 1198 | }) 1199 | .optional(), 1200 | software_statement: z 1201 | .string() 1202 | .meta({ 1203 | description: "The software statement of the application.", 1204 | }) 1205 | .optional(), 1206 | }), 1207 | metadata: { 1208 | openapi: { 1209 | description: "Register an OAuth2 application", 1210 | responses: { 1211 | "200": { 1212 | description: "OAuth2 application registered successfully", 1213 | content: { 1214 | "application/json": { 1215 | schema: { 1216 | type: "object", 1217 | properties: { 1218 | name: { 1219 | type: "string", 1220 | description: "Name of the OAuth2 application", 1221 | }, 1222 | icon: { 1223 | type: "string", 1224 | nullable: true, 1225 | description: "Icon URL for the application", 1226 | }, 1227 | metadata: { 1228 | type: "object", 1229 | additionalProperties: true, 1230 | nullable: true, 1231 | description: 1232 | "Additional metadata for the application", 1233 | }, 1234 | clientId: { 1235 | type: "string", 1236 | description: "Unique identifier for the client", 1237 | }, 1238 | clientSecret: { 1239 | type: "string", 1240 | description: "Secret key for the client", 1241 | }, 1242 | redirectURLs: { 1243 | type: "array", 1244 | items: { type: "string", format: "uri" }, 1245 | description: "List of allowed redirect URLs", 1246 | }, 1247 | type: { 1248 | type: "string", 1249 | description: "Type of the client", 1250 | enum: ["web"], 1251 | }, 1252 | authenticationScheme: { 1253 | type: "string", 1254 | description: 1255 | "Authentication scheme used by the client", 1256 | enum: ["client_secret"], 1257 | }, 1258 | disabled: { 1259 | type: "boolean", 1260 | description: "Whether the client is disabled", 1261 | enum: [false], 1262 | }, 1263 | userId: { 1264 | type: "string", 1265 | nullable: true, 1266 | description: 1267 | "ID of the user who registered the client, null if registered anonymously", 1268 | }, 1269 | createdAt: { 1270 | type: "string", 1271 | format: "date-time", 1272 | description: "Creation timestamp", 1273 | }, 1274 | updatedAt: { 1275 | type: "string", 1276 | format: "date-time", 1277 | description: "Last update timestamp", 1278 | }, 1279 | }, 1280 | required: [ 1281 | "name", 1282 | "clientId", 1283 | "clientSecret", 1284 | "redirectURLs", 1285 | "type", 1286 | "authenticationScheme", 1287 | "disabled", 1288 | "createdAt", 1289 | "updatedAt", 1290 | ], 1291 | }, 1292 | }, 1293 | }, 1294 | }, 1295 | }, 1296 | }, 1297 | }, 1298 | }, 1299 | async (ctx) => { 1300 | const body = ctx.body; 1301 | const session = await getSessionFromCtx(ctx); 1302 | 1303 | // Check authorization 1304 | if (!session && !options.allowDynamicClientRegistration) { 1305 | throw new APIError("UNAUTHORIZED", { 1306 | error: "invalid_token", 1307 | error_description: 1308 | "Authentication required for client registration", 1309 | }); 1310 | } 1311 | 1312 | // Validate redirect URIs for redirect-based flows 1313 | if ( 1314 | (!body.grant_types || 1315 | body.grant_types.includes("authorization_code") || 1316 | body.grant_types.includes("implicit")) && 1317 | (!body.redirect_uris || body.redirect_uris.length === 0) 1318 | ) { 1319 | throw new APIError("BAD_REQUEST", { 1320 | error: "invalid_redirect_uri", 1321 | error_description: 1322 | "Redirect URIs are required for authorization_code and implicit grant types", 1323 | }); 1324 | } 1325 | 1326 | // Validate correlation between grant_types and response_types 1327 | if (body.grant_types && body.response_types) { 1328 | if ( 1329 | body.grant_types.includes("authorization_code") && 1330 | !body.response_types.includes("code") 1331 | ) { 1332 | throw new APIError("BAD_REQUEST", { 1333 | error: "invalid_client_metadata", 1334 | error_description: 1335 | "When 'authorization_code' grant type is used, 'code' response type must be included", 1336 | }); 1337 | } 1338 | if ( 1339 | body.grant_types.includes("implicit") && 1340 | !body.response_types.includes("token") 1341 | ) { 1342 | throw new APIError("BAD_REQUEST", { 1343 | error: "invalid_client_metadata", 1344 | error_description: 1345 | "When 'implicit' grant type is used, 'token' response type must be included", 1346 | }); 1347 | } 1348 | } 1349 | 1350 | const clientId = 1351 | options.generateClientId?.() || 1352 | generateRandomString(32, "a-z", "A-Z"); 1353 | const clientSecret = 1354 | options.generateClientSecret?.() || 1355 | generateRandomString(32, "a-z", "A-Z"); 1356 | 1357 | const storedClientSecret = await storeClientSecret(ctx, clientSecret); 1358 | 1359 | // Create the client with the existing schema 1360 | const client: Client = await ctx.context.adapter.create({ 1361 | model: modelName.oauthClient, 1362 | data: { 1363 | name: body.client_name, 1364 | icon: body.logo_uri, 1365 | metadata: body.metadata ? JSON.stringify(body.metadata) : null, 1366 | clientId: clientId, 1367 | clientSecret: storedClientSecret, 1368 | redirectURLs: body.redirect_uris.join(","), 1369 | type: "web", 1370 | authenticationScheme: 1371 | body.token_endpoint_auth_method || "client_secret_basic", 1372 | disabled: false, 1373 | userId: session?.session.userId, 1374 | createdAt: new Date(), 1375 | updatedAt: new Date(), 1376 | }, 1377 | }); 1378 | 1379 | // Format the response according to RFC7591 1380 | return ctx.json( 1381 | { 1382 | client_id: clientId, 1383 | ...(client.type !== "public" 1384 | ? { 1385 | client_secret: clientSecret, 1386 | client_secret_expires_at: 0, // 0 means it doesn't expire 1387 | } 1388 | : {}), 1389 | client_id_issued_at: Math.floor(Date.now() / 1000), 1390 | client_secret_expires_at: 0, // 0 means it doesn't expire 1391 | redirect_uris: body.redirect_uris, 1392 | token_endpoint_auth_method: 1393 | body.token_endpoint_auth_method || "client_secret_basic", 1394 | grant_types: body.grant_types || ["authorization_code"], 1395 | response_types: body.response_types || ["code"], 1396 | client_name: body.client_name, 1397 | client_uri: body.client_uri, 1398 | logo_uri: body.logo_uri, 1399 | scope: body.scope, 1400 | contacts: body.contacts, 1401 | tos_uri: body.tos_uri, 1402 | policy_uri: body.policy_uri, 1403 | jwks_uri: body.jwks_uri, 1404 | jwks: body.jwks, 1405 | software_id: body.software_id, 1406 | software_version: body.software_version, 1407 | software_statement: body.software_statement, 1408 | metadata: body.metadata, 1409 | }, 1410 | { 1411 | status: 201, 1412 | headers: { 1413 | "Cache-Control": "no-store", 1414 | Pragma: "no-cache", 1415 | }, 1416 | }, 1417 | ); 1418 | }, 1419 | ), 1420 | getOAuthClient: createAuthEndpoint( 1421 | "/oauth2/client/:id", 1422 | { 1423 | method: "GET", 1424 | use: [sessionMiddleware], 1425 | metadata: { 1426 | openapi: { 1427 | description: "Get OAuth2 client details", 1428 | responses: { 1429 | "200": { 1430 | description: "OAuth2 client retrieved successfully", 1431 | content: { 1432 | "application/json": { 1433 | schema: { 1434 | type: "object", 1435 | properties: { 1436 | clientId: { 1437 | type: "string", 1438 | description: "Unique identifier for the client", 1439 | }, 1440 | name: { 1441 | type: "string", 1442 | description: "Name of the OAuth2 application", 1443 | }, 1444 | icon: { 1445 | type: "string", 1446 | nullable: true, 1447 | description: "Icon URL for the application", 1448 | }, 1449 | }, 1450 | required: ["clientId", "name"], 1451 | }, 1452 | }, 1453 | }, 1454 | }, 1455 | }, 1456 | }, 1457 | }, 1458 | }, 1459 | async (ctx) => { 1460 | const client = await getClient( 1461 | ctx.params.id, 1462 | ctx.context.adapter, 1463 | trustedClients, 1464 | ); 1465 | if (!client) { 1466 | throw new APIError("NOT_FOUND", { 1467 | error_description: "client not found", 1468 | error: "not_found", 1469 | }); 1470 | } 1471 | return ctx.json({ 1472 | clientId: client.clientId as string, 1473 | name: client.name as string, 1474 | icon: client.icon as string, 1475 | }); 1476 | }, 1477 | ), 1478 | }, 1479 | schema: mergeSchema(schema, options?.schema), 1480 | } satisfies BetterAuthPlugin; 1481 | }; 1482 | export type * from "./types"; 1483 | ```