feat: SSO plugin with OIDC and SAML support (#3185)

* fix(email-verification): improve email verification logic to check session and user email consistency (#3042)

* docs(passkey): Fixed signIn passkey props (#3014)

callbackURL doesn't exist.

* fix(email-otp): auto-verify on email otp reset (#3022)

* fix: delete user should respect freshAge config (#3075)

* fix: delete user needs to enforced through fresh age

* cleanup

* cleanup

* chore(org): add comments explaining what shimContext does (#3098)

* feat: Allow passing `id` in DB hook `create` (#3048)

* feat(database-hooks): Allow passing `id` in DB hook `create`

It's the same to using a custom `idGenerator`, except configurable by the database hook which would in theory provide more data.

A use-case is to generate the id based on user info in the user before DB hook.

Solves https://discord.com/channels/1288403910284935179/1379190465588367540/1384217435535835216

* chore: lint

* fix: tests failing

* docs: basic errs with svg props (#3102)

* docs: corrected github user email scope name (#3099)

* docs: corrected github user email scope name

* docs: cubic dev suggestion

* fix: use correct refresh token endpoint for github (#3095)

* chore: fix typo in authorize comment (#3106)

* docs: fix session parameter spelling (#3108)

* docs: input field usage on additional fields (#2991)

* fix: onLinkAccount trigger on phone number verification (#3007)

* fix: expose headers override in jwt plugin (#3019)

* expose headers override in jwt plugin

* clean up

* lint

* fix(expo): remove duplicated trusted origins

* feat: link account with idToken  (#1830)

* add idToken to link account

* add docs

* Implemented linking accounts based on idToken

* fix: tests

* docs: prevent diff

* docs: prevent diff

---------

Co-authored-by: kzlar <120426485+kzlar@users.noreply.github.com>

* feat: add Hugging Face provider (#3089)

* feat: add huggingface provider

* Add hugging face to doc

* chore: update hugging face logo

* chore: release v1.2.10

* docs: fix builder failing to open

* docs(NextJS): Improve middleware example to be more secure (#3135)

* docs(NextJS): Improve middleware example to be more secure

Users can skim code without reading the text, and LLMs can read code and miss-understand context correctly.  Our current middleware example only checks for existence of a cookie, and doesn't validate it.

While we do warn users this isn't secure, some users has raised concern in a Github issue saying it's not obvious enough for users who skim.

Also we don't provide examples on how to authenticate users on each route, we only show middleware optimistic check examples.

* Update docs/content/docs/integrations/next.mdx

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fix(username): log the correct username (#3127)

* docs: fix typo in plugin (#3122)

* typo

* typo

* typo

* typo

* typo

* docs: fix typos on mcp guide (#3146)

* docs: update TanStack Start integration guide (#3142)

* fix(sveltekit): only dynamic import $app/environment once (#3152)

Co-authored-by: Work <work@Jasons-MacBook-Pro.local>

* docs: fix typo in oauth proxy documentation (#3151)

* blog: seed round announcement  (#3168)

* init

* cleanup

* fix seed round announcemnt

* fix seed round announcemnt

* seed round blog

* add nav mobile

* fix typo

* Update docs/content/blogs/seed-round.mdx

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Update docs/app/blog/[[...slug]]/page.tsx

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Update docs/app/blog/[[...slug]]/page.tsx

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* update og

* cleanup

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* docs: fix email address

* refactor(mongo-adapter): migrate to createAdapter (#3170)

In the past we didn't have mongoDb adapter move over to createAdapter since we've seen users running into issues.

However some time ago I've merged a PR which I believe fixed the issue, and after testing the org plugin with the mongo adapter that uses `createAdapter` I don't see any issues.

* fix(api-key): update should only use by ID

* docs: fix blog page layout (#3176)

* fix/blog-page-layouts

* clean up

* docs: update contact email in seed round blog

* init

* cleanup

* feat(better-auth): add test utilities and update dependencies

- Introduced a new test utility module in `src/test-utils/index.ts` for better testing support.
- Updated `package.json` to include new test utilities in the build configuration.
- Added `oauth2-mock-server` dependency to `pnpm-lock.yaml` and `sso/package.json` for OAuth2 testing.
- Enhanced the SSO provider registration process with improved error handling.

* docs update

---------

Co-authored-by: Maxwell <145994855+ping-maxwell@users.noreply.github.com>
Co-authored-by: KinfeMichael Tariku <65047246+Kinfe123@users.noreply.github.com>
Co-authored-by: Undefined Ninja <74867549+0xCodeMaieutics@users.noreply.github.com>
Co-authored-by: artemoire <18062266+artemoire@users.noreply.github.com>
Co-authored-by: reslear <12596485+reslear@users.noreply.github.com>
Co-authored-by: kzlar <120426485+kzlar@users.noreply.github.com>
Co-authored-by: Eliott C. <coyotte508@protonmail.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Alessandro Bortolin <bortolin.alessandro@outlook.it>
Co-authored-by: Lakshya Thakur <lapstjup@gmail.com>
Co-authored-by: Usman S. (Max Programming) <51731966+max-programming@users.noreply.github.com>
Co-authored-by: Jason Venable <jason.venable@gmail.com>
Co-authored-by: Work <work@Jasons-MacBook-Pro.local>
Co-authored-by: Dan McGrath <daniel.mcgrath9@gmail.com>
This commit is contained in:
Bereket Engida
2025-06-27 20:19:19 -07:00
committed by GitHub
parent 4e38645b44
commit a6a66d9c7e
60 changed files with 4982 additions and 662 deletions

View File

@@ -0,0 +1,35 @@
---
title: "Announcing our $5M seed round"
description: "We raised $5M seed led by Peak XV Partners"
date: 2025-06-24
author:
name: "Bereket Engida"
avatar: "/blogs/bereket.png"
twitter: "iambereket"
image: "/blogs/seed-round.png"
tags: ["seed round", "authentication", "funding"]
---
## Announcing our $5M seed round
Were excited to share that Better Auth has raised a $5 million seed round led by Peak XV Partners (formerly Sequoia Capital India & SEA), with participation from Y Combinator, Chapter One, P1 Ventures, and a group of incredible investors and angels.
This funding fuels the next phase of **Better Auth**.
From the start we are obsessed with making it possible for developers to **own their auth**. To **democratize high quality authentication** and make rolling your own auth not just doable, but the obvious choice.
It started with building the framework. Since then, weve seen incredible growth and support from the community. Thank you everyone for being part of this journey. Its still early days, and theres so much more to build. This funding will allow us to have more people invloved and to push the boundaries of what's possible.
On top of the framework, were also building the infrastructure to cover the gaps we couldn't cover in the framework:
* A unified dashboard to manage users and user analytics
* Enterprise-grade security: bot, abuse, and fraud protection
* Authentication Email and SMS service
* Fast, globally distributed session storage
* and more.
[Join the waitlist](https://better-auth.build) to get early access to the infrastructure.
And if you're excited about making auth accessible - we're hiring!
Reach out to [bereket@better-auth.com](mailto:bereket@better-auth.com).

View File

@@ -10,7 +10,7 @@ description: GitHub provider setup and usage.
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/github` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
Important: You MUST include the user.email scope in your Github app. See details below.
Important: You MUST include the user:email scope in your GitHub app. See details below.
</Step>
<Step>

View File

@@ -0,0 +1,47 @@
---
title: Hugging Face
description: Hugging Face provider setup and usage.
---
<Steps>
<Step>
### Get your Hugging Face credentials
To use Hugging Face sign in, you need a client ID and client secret. [Hugging Face OAuth documentation](https://huggingface.co/docs/hub/oauth). Make sure the created oauth app on Hugging Face has the "email" scope.
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/huggingface` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
</Step>
<Step>
### Configure the provider
To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance.
```ts title="auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
socialProviders: {
huggingface: { // [!code highlight]
clientId: process.env.HUGGINGFACE_CLIENT_ID as string, // [!code highlight]
clientSecret: process.env.HUGGINGFACE_CLIENT_SECRET as string, // [!code highlight]
}, // [!code highlight]
},
})
```
</Step>
<Step>
### Sign In with Hugging Face
To sign in with Hugging Face, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
- `provider`: The provider to use. It should be set to `huggingface`.
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()
const signIn = async () => {
const data = await authClient.signIn.social({
provider: "huggingface"
})
}
```
</Step>
</Steps>

View File

@@ -44,7 +44,7 @@ To sign in with Microsoft, you can use the `signIn.social` function provided by
- `provider`: The provider to use. It should be set to `microsoft`.
```ts title="auth-client.ts" /
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
const authClient = createAuthClient();

View File

@@ -34,7 +34,7 @@ description: Spotify provider setup and usage.
To sign in with Spotify, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
- `provider`: The provider to use. It should be set to `spotify`.
```ts title="auth-client.ts" /
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
const authClient = createAuthClient()

View File

@@ -5,7 +5,7 @@ description: Learn how to use plugins with Better Auth.
Plugins are a key part of Better Auth, they let you extend the base functionalities. You can use them to add new authentication methods, features, or customize behaviors.
Better Auth offers comes with many built-in plugins ready to use. Check the plugins section for details. You can also create your own plugins.
Better Auth comes with many built-in plugins ready to use. Check the plugins section for details. You can also create your own plugins.
## Using a Plugin
@@ -510,7 +510,7 @@ See built-in plugins for examples of how to use atoms properly.
### Path methods
by default, inferred paths use `GET` method if they don't require a body and `POST` if they do. You can override this by passing a `pathMethods` object. The key should be the path and the value should be the method ("POST" | "GET").
By default, inferred paths use `GET` method if they don't require a body and `POST` if they do. You can override this by passing a `pathMethods` object. The key should be the path and the value should be the method ("POST" | "GET").
```ts title="client-plugin.ts"
import type { BetterAuthClientPlugin } from "better-auth/client";

View File

@@ -71,7 +71,8 @@ export const auth = betterAuth({
user: {
additionalFields: {
role: {
type: "string"
type: "string",
input: false
}
}
}
@@ -83,6 +84,26 @@ type Session = typeof auth.$Infer.Session
In the example above, we added a `role` field to the user object. This field is now available on the `Session` type.
### The `input` property
The `input` property in an additional field configuration determines whether the field should be included in the user input. This property defaults to `true`, meaning the field will be part of the user input during operations like registration.
To prevent a field from being part of the user input, you must explicitly set `input: false`:
```ts
additionalFields: {
role: {
type: "string",
input: false
}
}
```
When `input` is set to `false`, the field will be excluded from user input, preventing users from passing a value for it.
By default, additional fields are included in the user input, which can lead to security vulnerabilities if not handled carefully. For fields that should not be set by the user, like a `role`, it is crucial to set `input: false` in the configuration.
### Inferring Additional Fields on Client
To make sure proper type inference for additional fields on the client side, you need to inform the client about these fields. There are two approaches to achieve this, depending on your project structure:

View File

@@ -357,6 +357,27 @@ Users already signed in can manually link their account to additional social pro
});
```
You can also link accounts using ID tokens directly, without redirecting to the provider's OAuth flow:
```ts
await authClient.linkSocial({
provider: "google",
idToken: {
token: "id_token_from_provider",
nonce: "nonce_used_for_token", // Optional
accessToken: "access_token", // Optional, may be required by some providers
refreshToken: "refresh_token" // Optional
}
});
```
This is useful when you already have valid tokens from the provider, for example:
- After signing in with a native SDK
- When using a mobile app that handles authentication
- When implementing custom OAuth flows
The ID token must be valid and the provider must support ID token verification.
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.
```ts title="auth.ts"
export const auth = betterAuth({
@@ -368,6 +389,18 @@ Users already signed in can manually link their account to additional social pro
});
```
If you want the newly linked accounts to update the user information, you need to enable this in the account linking settings.
```ts title="auth.ts"
export const auth = betterAuth({
account: {
accountLinking: {
updateUserInfoOnLink: true
}
},
});
```
- **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.
```ts

View File

@@ -142,6 +142,9 @@ import { getSessionCookie } from "better-auth/cookies";
export async function middleware(request: NextRequest) {
const sessionCookie = getSessionCookie(request);
// THIS IS NOT SECURE!
// This is the recommended approach to optimistically redirect users
// We recommend handling auth checks in each page/route
if (!sessionCookie) {
return NextResponse.redirect(new URL("/", request.url));
}
@@ -178,6 +181,33 @@ export async function middleware(request: NextRequest) {
}
```
### How to handle auth checks in each page/route
In this example, we are using the `auth.api.getSession` function within a server component to get the session object,
then we are checking if the session is valid. If it's not, we are redirecting the user to the sign-in page.
```tsx title="app/dashboard/page.tsx"
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth.api.getSession({
headers: await headers()
})
if(!session) {
redirect("/sign-in")
}
return (
<div>
<h1>Welcome {session.user.name}</h1>
</div>
)
}
```
### For Next.js release `15.1.7` and below
If you need the full session object, you'll have to fetch it from the `/get-session` API route. Since Next.js middleware doesn't support running Node.js APIs directly, you must make an HTTP request.

View File

@@ -9,14 +9,14 @@ Before you start, make sure you have a Better Auth instance configured. If you h
### Mount the handler
We need to mount the handler to a TanStack API endpoint.
Create a new file: `/app/routes/api/auth/$.ts`
We need to mount the handler to a TanStack API endpoint/Server Route.
Create a new file: `/src/routes/api/auth/$.ts`
```ts title="routes/api/auth/$.ts"
```ts title="src/routes/api/auth/$.ts"
import { auth } from '@/lib/auth' // import your auth instance
import { createAPIFileRoute } from '@tanstack/react-start/api'
import { createServerFileRoute } from '@tanstack/react-start/server'
export const APIRoute = createAPIFileRoute('/api/auth/$')({
export const ServerRoute = createServerFileRoute('/api/auth/$').methods({
GET: ({ request }) => {
return auth.handler(request)
},
@@ -26,15 +26,18 @@ export const APIRoute = createAPIFileRoute('/api/auth/$')({
})
```
If you haven't defined an API Route yet, you can do so by creating a file: `/app/api.ts`
If you haven't created your server route handler yet, you can do so by creating a file: `/src/server.ts`
```ts title="app/api.ts"
```ts title="src/server.ts"
import {
createStartAPIHandler,
defaultAPIFileRouteHandler,
} from '@tanstack/react-start/api'
createStartHandler,
defaultStreamHandler,
} from '@tanstack/react-start/server'
import { createRouter } from './router'
export default createStartAPIHandler(defaultAPIFileRouteHandler)
export default createStartHandler({
createRouter,
})(defaultStreamHandler)
```
### Usage tips
@@ -42,7 +45,7 @@ export default createStartAPIHandler(defaultAPIFileRouteHandler)
- We recommend using the client SDK or `authClient` to handle authentication, rather than server actions with `auth.api`.
- When you call functions that need to set cookies (like `signInEmail` or `signUpEmail`), you'll need to handle cookie setting for TanStack Start. Better Auth provides a `reactStartCookies` plugin to automatically handle this for you.
```ts title="auth.ts"
```ts title="src/lib/auth.ts"
import { betterAuth } from "better-auth";
import { reactStartCookies } from "better-auth/react-start";

View File

@@ -66,6 +66,6 @@ To share cookies between the proxy server and your main server it uses URL query
## Options
**currentURL**: The application's current URL is automatically determined by the plugin. It first it check for the request URL if invoked by a client, then it checks the base URL from popular hosting providers, and finally falls back to the `baseURL` in your auth config. If the URL isnt inferred correctly, you can specify it manually here.
**currentURL**: The application's current URL is automatically determined by the plugin. It first checks for the request URL if invoked by a client, then it checks the base URL from popular hosting providers, and finally falls back to the `baseURL` in your auth config. If the URL isnt inferred correctly, you can specify it manually here.
**productionURL**: If this value matches the `baseURL` in your auth config, requests will not be proxied. Defaults to the `BETTER_AUTH_URL` environment variable.

View File

@@ -3,13 +3,9 @@ title: Single Sign-On (SSO)
description: Integrate Single Sign-On (SSO) with your application.
---
`OIDC` `OAuth2` `SSO`
`OIDC` `OAuth2` `SSO` `SAML`
Single Sign-On (SSO) allows users to authenticate with multiple applications using a single set of credentials. This plugin supports OpenID Connect (OIDC) and OAuth2 providers.
<Callout>
SAML support is coming soon. Upvote the feature request on our [GitHub](https://github.com/better-auth/better-auth/issues/96)
</Callout>
Single Sign-On (SSO) allows users to authenticate with multiple applications using a single set of credentials. This plugin supports OpenID Connect (OIDC), OAuth2 providers, and SAML 2.0.
## Installation
@@ -67,30 +63,30 @@ Single Sign-On (SSO) allows users to authenticate with multiple applications usi
### Register an OIDC Provider
To register an OIDC provider, use the `createOIDCProvider` endpoint and provide the necessary configuration details for the provider.
To register an OIDC provider, use the `registerSSOProvider` endpoint and provide the necessary configuration details for the provider.
A redirect URL will be automatically generated using the provider ID. For instance, if the provider ID is `hydra`, the redirect URL would be `{baseURL}/api/auth/sso/callback/hydra`. Note that `/api/auth` may vary depending on your base path configuration.
<Tabs items={["client", "server"]}>
<Tab value="client">
```ts title="register-provider.ts"
```ts title="register-oidc-provider.ts"
import { authClient } from "@/lib/auth-client";
// only with issuer if the provider supports discovery
// Register with OIDC configuration
await authClient.sso.register({
issuer: "https://idp.example.com",
providerId: "example-provider",
});
// with all fields
await authClient.sso.register({
issuer: "https://idp.example.com",
domain: "example.com",
clientId: "client-id",
clientSecret: "client-secret",
authorizationEndpoint: "https://idp.example.com/authorize",
tokenEndpoint: "https://idp.example.com/token",
jwksEndpoint: "https://idp.example.com/jwks",
oidcConfig: {
clientId: "client-id",
clientSecret: "client-secret",
authorizationEndpoint: "https://idp.example.com/authorize",
tokenEndpoint: "https://idp.example.com/token",
jwksEndpoint: "https://idp.example.com/jwks",
discoveryEndpoint: "https://idp.example.com/.well-known/openid-configuration",
scopes: ["openid", "email", "profile"],
pkce: true,
},
mapping: {
id: "sub",
email: "email",
@@ -98,23 +94,28 @@ await authClient.sso.register({
name: "name",
image: "picture",
},
providerId: "example-provider",
});
```
</Tab>
<Tab value="server">
```ts title="register-provider.ts"
```ts title="register-oidc-provider.ts"
const { headers } = await signInWithTestUser();
await auth.api.createOIDCProvider({
await auth.api.registerSSOProvider({
body: {
providerId: "example-provider",
issuer: "https://idp.example.com",
domain: "example.com",
clientId: "your-client-id",
clientSecret: "your-client-secret",
authorizationEndpoint: "https://idp.example.com/authorize",
tokenEndpoint: "https://idp.example.com/token",
jwksEndpoint: "https://idp.example.com/jwks",
oidcConfig: {
clientId: "your-client-id",
clientSecret: "your-client-secret",
authorizationEndpoint: "https://idp.example.com/authorize",
tokenEndpoint: "https://idp.example.com/token",
jwksEndpoint: "https://idp.example.com/jwks",
discoveryEndpoint: "https://idp.example.com/.well-known/openid-configuration",
scopes: ["openid", "email", "profile"],
pkce: true,
},
mapping: {
id: "sub",
email: "email",
@@ -122,7 +123,6 @@ await auth.api.createOIDCProvider({
name: "name",
image: "picture",
},
providerId: "example-provider",
},
headers,
});
@@ -130,6 +130,130 @@ await auth.api.createOIDCProvider({
</Tab>
</Tabs>
### Register a SAML Provider
To register a SAML provider, use the `registerSSOProvider` endpoint with SAML configuration details. The provider will act as a Service Provider (SP) and integrate with your Identity Provider (IdP).
<Tabs items={["client", "server"]}>
<Tab value="client">
```ts title="register-saml-provider.ts"
import { authClient } from "@/lib/auth-client";
await authClient.sso.register({
providerId: "saml-provider",
issuer: "https://idp.example.com",
domain: "example.com",
samlConfig: {
entryPoint: "https://idp.example.com/sso",
cert: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
callbackUrl: "https://yourapp.com/api/auth/sso/saml2/callback/saml-provider",
audience: "https://yourapp.com",
wantAssertionsSigned: true,
signatureAlgorithm: "sha256",
digestAlgorithm: "sha256",
identifierFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
idpMetadata: {
metadata: "<!-- IdP Metadata XML -->",
privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
privateKeyPass: "your-private-key-password",
isAssertionEncrypted: true,
encPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
encPrivateKeyPass: "your-encryption-key-password"
},
spMetadata: {
metadata: "<!-- SP Metadata XML -->",
binding: "post",
privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
privateKeyPass: "your-sp-private-key-password",
isAssertionEncrypted: true,
encPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
encPrivateKeyPass: "your-sp-encryption-key-password"
}
},
mapping: {
id: "nameID",
email: "email",
name: "displayName",
firstName: "givenName",
lastName: "surname",
extraFields: {
department: "department",
role: "role"
}
},
});
```
</Tab>
<Tab value="server">
```ts title="register-saml-provider.ts"
const { headers } = await signInWithTestUser();
await auth.api.registerSSOProvider({
body: {
providerId: "saml-provider",
issuer: "https://idp.example.com",
domain: "example.com",
samlConfig: {
entryPoint: "https://idp.example.com/sso",
cert: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
callbackUrl: "https://yourapp.com/api/auth/sso/saml2/callback/saml-provider",
audience: "https://yourapp.com",
wantAssertionsSigned: true,
signatureAlgorithm: "sha256",
digestAlgorithm: "sha256",
identifierFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
idpMetadata: {
metadata: "<!-- IdP Metadata XML -->",
privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
privateKeyPass: "your-private-key-password",
isAssertionEncrypted: true,
encPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
encPrivateKeyPass: "your-encryption-key-password"
},
spMetadata: {
metadata: "<!-- SP Metadata XML -->",
binding: "post",
privateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
privateKeyPass: "your-sp-private-key-password",
isAssertionEncrypted: true,
encPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----",
encPrivateKeyPass: "your-sp-encryption-key-password"
}
},
mapping: {
id: "nameID",
email: "email",
name: "displayName",
firstName: "givenName",
lastName: "surname",
extraFields: {
department: "department",
role: "role"
}
},
},
headers,
});
```
</Tab>
</Tabs>
### Get Service Provider Metadata
For SAML providers, you can retrieve the Service Provider metadata XML that needs to be configured in your Identity Provider:
```ts title="get-sp-metadata.ts"
const response = await auth.api.spMetadata({
query: {
providerId: "saml-provider",
format: "xml" // or "json"
}
});
const metadataXML = await response.text();
console.log(metadataXML);
```
### Sign In with SSO
To sign in with an SSO provider, you can call `signIn.sso`
@@ -183,7 +307,6 @@ const res = await auth.api.signInSSO({
When a user is authenticated, if the user does not exist, the user will be provisioned using the `provisionUser` function. If the organization provisioning is enabled and a provider is associated with an organization, the user will be added to the organization.
```ts title="auth.ts"
const auth = betterAuth({
plugins: [
@@ -203,6 +326,280 @@ const auth = betterAuth({
});
```
## Provisioning
The SSO plugin provides powerful provisioning capabilities to automatically set up users and manage their organization memberships when they sign in through SSO providers.
### User Provisioning
User provisioning allows you to run custom logic whenever a user signs in through an SSO provider. This is useful for:
- Setting up user profiles with additional data from the SSO provider
- Synchronizing user attributes with external systems
- Creating user-specific resources
- Logging SSO sign-ins
- Updating user information from the SSO provider
```ts title="auth.ts"
const auth = betterAuth({
plugins: [
sso({
provisionUser: async ({ user, userInfo, token, provider }) => {
// Update user profile with SSO data
await updateUserProfile(user.id, {
department: userInfo.attributes?.department,
jobTitle: userInfo.attributes?.jobTitle,
manager: userInfo.attributes?.manager,
lastSSOLogin: new Date(),
});
// Create user-specific resources
await createUserWorkspace(user.id);
// Sync with external systems
await syncUserWithCRM(user.id, userInfo);
// Log the SSO sign-in
await auditLog.create({
userId: user.id,
action: 'sso_signin',
provider: provider.providerId,
metadata: {
email: userInfo.email,
ssoProvider: provider.issuer,
},
});
},
}),
],
});
```
The `provisionUser` function receives:
- **user**: The user object from the database
- **userInfo**: User information from the SSO provider (includes attributes, email, name, etc.)
- **token**: OAuth2 tokens (for OIDC providers) - may be undefined for SAML
- **provider**: The SSO provider configuration
### Organization Provisioning
Organization provisioning automatically manages user memberships in organizations when SSO providers are linked to specific organizations. This is particularly useful for:
- Enterprise SSO where each company/domain maps to an organization
- Automatic role assignment based on SSO attributes
- Managing team memberships through SSO
#### Basic Organization Provisioning
```ts title="auth.ts"
const auth = betterAuth({
plugins: [
sso({
organizationProvisioning: {
disabled: false, // Enable org provisioning
defaultRole: "member", // Default role for new members
},
}),
],
});
```
#### Advanced Organization Provisioning with Custom Roles
```ts title="auth.ts"
const auth = betterAuth({
plugins: [
sso({
organizationProvisioning: {
disabled: false,
defaultRole: "member",
getRole: async ({ user, userInfo, provider }) => {
// Assign roles based on SSO attributes
const department = userInfo.attributes?.department;
const jobTitle = userInfo.attributes?.jobTitle;
// Admins based on job title
if (jobTitle?.toLowerCase().includes('manager') ||
jobTitle?.toLowerCase().includes('director') ||
jobTitle?.toLowerCase().includes('vp')) {
return "admin";
}
// Special roles for IT department
if (department?.toLowerCase() === 'it') {
return "admin";
}
// Default to member for everyone else
return "member";
},
},
}),
],
});
```
#### Linking SSO Providers to Organizations
When registering an SSO provider, you can link it to a specific organization:
```ts title="register-org-provider.ts"
await auth.api.registerSSOProvider({
body: {
providerId: "acme-corp-saml",
issuer: "https://acme-corp.okta.com",
domain: "acmecorp.com",
organizationId: "org_acme_corp_id", // Link to organization
samlConfig: {
// SAML configuration...
},
},
headers,
});
```
Now when users from `acmecorp.com` sign in through this provider, they'll automatically be added to the "Acme Corp" organization with the appropriate role.
#### Multiple Organizations Example
You can set up multiple SSO providers for different organizations:
```ts title="multi-org-setup.ts"
// Acme Corp SAML provider
await auth.api.registerSSOProvider({
body: {
providerId: "acme-corp",
issuer: "https://acme.okta.com",
domain: "acmecorp.com",
organizationId: "org_acme_id",
samlConfig: { /* ... */ },
},
headers,
});
// TechStart OIDC provider
await auth.api.registerSSOProvider({
body: {
providerId: "techstart-google",
issuer: "https://accounts.google.com",
domain: "techstart.io",
organizationId: "org_techstart_id",
oidcConfig: { /* ... */ },
},
headers,
});
```
#### Organization Provisioning Flow
1. **User signs in** through an SSO provider linked to an organization
2. **User is authenticated** and either found or created in the database
3. **Organization membership is checked** - if the user isn't already a member of the linked organization
4. **Role is determined** using either the `defaultRole` or `getRole` function
5. **User is added** to the organization with the determined role
6. **User provisioning runs** (if configured) for additional setup
### Provisioning Best Practices
#### 1. Idempotent Operations
Make sure your provisioning functions can be safely run multiple times:
```ts
provisionUser: async ({ user, userInfo }) => {
// Check if already provisioned
const existingProfile = await getUserProfile(user.id);
if (!existingProfile.ssoProvisioned) {
await createUserResources(user.id);
await markAsProvisioned(user.id);
}
// Always update attributes (they might change)
await updateUserAttributes(user.id, userInfo.attributes);
},
```
#### 2. Error Handling
Handle errors gracefully to avoid blocking user sign-in:
```ts
provisionUser: async ({ user, userInfo }) => {
try {
await syncWithExternalSystem(user, userInfo);
} catch (error) {
// Log error but don't throw - user can still sign in
console.error('Failed to sync user with external system:', error);
await logProvisioningError(user.id, error);
}
},
```
#### 3. Conditional Provisioning
Only run certain provisioning steps when needed:
```ts
organizationProvisioning: {
disabled: false,
getRole: async ({ user, userInfo, provider }) => {
// Only process role assignment for certain providers
if (provider.providerId.includes('enterprise')) {
return determineEnterpriseRole(userInfo);
}
return "member";
},
},
```
## SAML Configuration
### Service Provider Configuration
When registering a SAML provider, you need to provide Service Provider (SP) metadata configuration:
- **metadata**: XML metadata for the Service Provider
- **binding**: The binding method, typically "post" or "redirect"
- **privateKey**: Private key for signing (optional)
- **privateKeyPass**: Password for the private key (if encrypted)
- **isAssertionEncrypted**: Whether assertions should be encrypted
- **encPrivateKey**: Private key for decryption (if encryption is enabled)
- **encPrivateKeyPass**: Password for the encryption private key
### Identity Provider Configuration
You also need to provide Identity Provider (IdP) configuration:
- **metadata**: XML metadata from your Identity Provider
- **privateKey**: Private key for the IdP communication (optional)
- **privateKeyPass**: Password for the IdP private key (if encrypted)
- **isAssertionEncrypted**: Whether assertions from IdP are encrypted
- **encPrivateKey**: Private key for IdP assertion decryption
- **encPrivateKeyPass**: Password for the IdP decryption key
### SAML Attribute Mapping
Configure how SAML attributes map to user fields:
```ts
mapping: {
id: "nameID", // Default: "nameID"
email: "email", // Default: "email" or "nameID"
name: "displayName", // Default: "displayName"
firstName: "givenName", // Default: "givenName"
lastName: "surname", // Default: "surname"
extraFields: {
department: "department",
role: "jobTitle",
phone: "telephoneNumber"
}
}
```
### SAML Endpoints
The plugin automatically creates the following SAML endpoints:
- **SP Metadata**: `/api/auth/sso/saml2/sp/metadata?providerId={providerId}`
- **SAML Callback**: `/api/auth/sso/saml2/callback/{providerId}`
## Schema
The plugin requires additional fields in the `ssoProvider` table to store the provider's configuration.
@@ -214,7 +611,8 @@ The plugin requires additional fields in the `ssoProvider` table to store the pr
},
{ name: "issuer", type: "string", description: "The issuer identifier", isRequired: true },
{ name: "domain", type: "string", description: "The domain of the provider", isRequired: true },
{ name: "oidcConfig", type: "string", description: "The OIDC configuration", isRequired: false },
{ name: "oidcConfig", type: "string", description: "The OIDC configuration (JSON string)", isRequired: false },
{ name: "samlConfig", type: "string", description: "The SAML configuration (JSON string)", isRequired: false },
{ name: "userId", type: "string", description: "The user ID", isRequired: true, references: { model: "user", field: "id" } },
{ name: "providerId", type: "string", description: "The provider ID. Used to identify a provider and to generate a redirect URL.", isRequired: true, isUnique: true },
{ name: "organizationId", type: "string", description: "The organization Id. If provider is linked to an organization.", isRequired: false },
@@ -229,6 +627,10 @@ The plugin requires additional fields in the `ssoProvider` table to store the pr
**organizationProvisioning**: Options for provisioning users to an organization.
**defaultOverrideUserInfo**: Override user info with the provider info by default.
**disableImplicitSignUp**: Disable implicit sign up for new users.
<TypeTable
type={{
provisionUser: {
@@ -256,5 +658,15 @@ The plugin requires additional fields in the `ssoProvider` table to store the pr
},
},
},
defaultOverrideUserInfo: {
description: "Override user info with the provider info by default.",
type: "boolean",
default: false,
},
disableImplicitSignUp: {
description: "Disable implicit sign up for new users. When set to true, sign-in needs to be called with requestSignUp as true to create new users.",
type: "boolean",
default: false,
},
}}
/>