diff --git a/docs/app/global.css b/docs/app/global.css
index e01da85633..60ddb4263f 100644
--- a/docs/app/global.css
+++ b/docs/app/global.css
@@ -242,3 +242,7 @@
@apply bg-background text-foreground;
}
}
+
+html {
+ scroll-behavior: smooth;
+}
diff --git a/docs/content/docs/plugins/oidc-provider.mdx b/docs/content/docs/plugins/oidc-provider.mdx
index 17dec99f85..d974f51815 100644
--- a/docs/content/docs/plugins/oidc-provider.mdx
+++ b/docs/content/docs/plugins/oidc-provider.mdx
@@ -85,14 +85,42 @@ Once installed, you can utilize the OIDC Provider to manage authentication flows
To register a new OIDC client, use the `oauth2.register` method.
+
+
```ts title="client.ts"
const application = await client.oauth2.register({
name: "My Client",
- redirectURLs: ["https://client.example.com/callback"],
+ redirect_uris: ["https://client.example.com/callback"],
});
```
-Once the application is created, you will receive a `clientId` and `clientSecret` that you can display to the user.
+Once the application is created, you will receive a `client_id` and `client_secret` that you can display to the user.
+
+This Endpoint support [RFC7591](https://datatracker.ietf.org/doc/html/rfc7591) compliant client registration.
+
+### UserInfo Endpoint
+
+The OIDC Provider includes a UserInfo endpoint that allows clients to retrieve information about the authenticated user. This endpoint is available at `/oauth2/userinfo` and requires a valid access token.
+
+
+
+```ts title="client-app.ts"
+// Example of how a client would use the UserInfo endpoint
+const response = await fetch('https://your-domain.com/api/auth/oauth2/userinfo', {
+ headers: {
+ 'Authorization': 'Bearer ACCESS_TOKEN'
+ }
+});
+
+const userInfo = await response.json();
+// userInfo contains user details based on the scopes granted
+```
+
+The UserInfo endpoint returns different claims based on the scopes that were granted during authorization:
+
+- With `openid` scope: Returns the user's ID (`sub` claim)
+- With `profile` scope: Returns name, picture, given_name, family_name
+- With `email` scope: Returns email and email_verified
### Consent Screen
@@ -110,6 +138,8 @@ export const auth = betterAuth({
The plugin will redirect the user to the specified path with a `client_id` and `scope` query parameter. You can use this information to display a custom consent screen. Once the user consents, you can call `oauth2.consent` to complete the authorization.
+
+
```ts title="server.ts"
const res = await client.oauth2.consent({
accept: true, // or false to deny
diff --git a/packages/better-auth/src/plugins/oidc-provider/index.ts b/packages/better-auth/src/plugins/oidc-provider/index.ts
index f341ebd585..e9fe78ebc3 100644
--- a/packages/better-auth/src/plugins/oidc-provider/index.ts
+++ b/packages/better-auth/src/plugins/oidc-provider/index.ts
@@ -695,48 +695,156 @@ export const oidcProvider = (options: OIDCOptions) => {
{
method: "POST",
body: z.object({
- name: z.string(),
- icon: z.string().optional(),
+ redirect_uris: z.array(z.string()),
+ token_endpoint_auth_method: z
+ .enum(["none", "client_secret_basic", "client_secret_post"])
+ .default("client_secret_basic")
+ .optional(),
+ grant_types: z
+ .array(
+ z.enum([
+ "authorization_code",
+ "implicit",
+ "password",
+ "client_credentials",
+ "refresh_token",
+ "urn:ietf:params:oauth:grant-type:jwt-bearer",
+ "urn:ietf:params:oauth:grant-type:saml2-bearer",
+ ]),
+ )
+ .default(["authorization_code"])
+ .optional(),
+ response_types: z
+ .array(z.enum(["code", "token"]))
+ .default(["code"])
+ .optional(),
+ client_name: z.string().optional(),
+ client_uri: z.string().optional(),
+ logo_uri: z.string().optional(),
+ scope: z.string().optional(),
+ contacts: z.array(z.string()).optional(),
+ tos_uri: z.string().optional(),
+ policy_uri: z.string().optional(),
+ jwks_uri: z.string().optional(),
+ jwks: z.record(z.any()).optional(),
metadata: z.record(z.any()).optional(),
- redirectURLs: z.array(z.string()),
+ software_id: z.string().optional(),
+ software_version: z.string().optional(),
+ software_statement: z.string().optional(),
}),
},
async (ctx) => {
const body = ctx.body;
const session = await getSessionFromCtx(ctx);
+
+ // Check authorization
if (!session && !options.allowDynamicClientRegistration) {
throw new APIError("UNAUTHORIZED", {
- message: "Unauthorized",
+ error: "invalid_token",
+ error_description:
+ "Authentication required for client registration",
});
}
+
+ // Validate redirect URIs for redirect-based flows
+ if (
+ (!body.grant_types ||
+ body.grant_types.includes("authorization_code") ||
+ body.grant_types.includes("implicit")) &&
+ (!body.redirect_uris || body.redirect_uris.length === 0)
+ ) {
+ throw new APIError("BAD_REQUEST", {
+ error: "invalid_redirect_uri",
+ error_description:
+ "Redirect URIs are required for authorization_code and implicit grant types",
+ });
+ }
+
+ // Validate correlation between grant_types and response_types
+ if (body.grant_types && body.response_types) {
+ if (
+ body.grant_types.includes("authorization_code") &&
+ !body.response_types.includes("code")
+ ) {
+ throw new APIError("BAD_REQUEST", {
+ error: "invalid_client_metadata",
+ error_description:
+ "When 'authorization_code' grant type is used, 'code' response type must be included",
+ });
+ }
+ if (
+ body.grant_types.includes("implicit") &&
+ !body.response_types.includes("token")
+ ) {
+ throw new APIError("BAD_REQUEST", {
+ error: "invalid_client_metadata",
+ error_description:
+ "When 'implicit' grant type is used, 'token' response type must be included",
+ });
+ }
+ }
+
const clientId =
options.generateClientId?.() ||
generateRandomString(32, "a-z", "A-Z");
const clientSecret =
options.generateClientSecret?.() ||
generateRandomString(32, "a-z", "A-Z");
- const client = await ctx.context.adapter.create>({
+
+ // Create the client with the existing schema
+ const client: Client = await ctx.context.adapter.create({
model: modelName.oauthClient,
data: {
- name: body.name,
- icon: body.icon,
+ name: body.client_name,
+ icon: body.logo_uri,
metadata: body.metadata ? JSON.stringify(body.metadata) : null,
clientId: clientId,
clientSecret: clientSecret,
- redirectURLs: body.redirectURLs.join(","),
+ redirectURLs: body.redirect_uris.join(","),
type: "web",
- authenticationScheme: "client_secret",
+ authenticationScheme:
+ body.token_endpoint_auth_method || "client_secret_basic",
disabled: false,
userId: session?.session.userId,
createdAt: new Date(),
updatedAt: new Date(),
},
});
- return ctx.json({
- ...client,
- redirectURLs: client.redirectURLs.split(","),
- metadata: client.metadata ? JSON.parse(client.metadata) : null,
- } as Client);
+
+ // Format the response according to RFC7591
+ return ctx.json(
+ {
+ client_id: clientId,
+ client_secret: clientSecret,
+ client_id_issued_at: Math.floor(Date.now() / 1000),
+ client_secret_expires_at: 0, // 0 means it doesn't expire
+ redirect_uris: body.redirect_uris,
+ token_endpoint_auth_method:
+ body.token_endpoint_auth_method || "client_secret_basic",
+ grant_types: body.grant_types || ["authorization_code"],
+ response_types: body.response_types || ["code"],
+ client_name: body.client_name,
+ client_uri: body.client_uri,
+ logo_uri: body.logo_uri,
+ scope: body.scope,
+ contacts: body.contacts,
+ tos_uri: body.tos_uri,
+ policy_uri: body.policy_uri,
+ jwks_uri: body.jwks_uri,
+ jwks: body.jwks,
+ software_id: body.software_id,
+ software_version: body.software_version,
+ software_statement: body.software_statement,
+ metadata: body.metadata,
+ },
+ {
+ status: 201,
+ headers: {
+ "Cache-Control": "no-store",
+ Pragma: "no-cache",
+ },
+ },
+ );
},
),
getOAuthClient: createAuthEndpoint(
diff --git a/packages/better-auth/src/plugins/oidc-provider/oidc.test.ts b/packages/better-auth/src/plugins/oidc-provider/oidc.test.ts
index 7dad0a7600..36336c8bb0 100644
--- a/packages/better-auth/src/plugins/oidc-provider/oidc.test.ts
+++ b/packages/better-auth/src/plugins/oidc-provider/oidc.test.ts
@@ -62,28 +62,33 @@ describe("oidc", async () => {
it("should create oidc client", async ({ expect }) => {
const createdClient = await serverClient.oauth2.register({
- name: application.name,
- redirectURLs: application.redirectURLs,
- icon: application.icon,
- metadata: {
- custom: "data",
- },
+ client_name: application.name,
+ redirect_uris: application.redirectURLs,
+ logo_uri: application.icon,
});
expect(createdClient.data).toMatchObject({
- id: expect.any(String),
- name: "test",
- icon: "",
- metadata: {
- custom: "data",
- },
- clientId: expect.any(String),
- clientSecret: expect.any(String),
- redirectURLs: ["http://localhost:3000/api/auth/oauth2/callback/test"],
- type: "web",
- disabled: false,
+ client_id: expect.any(String),
+ client_secret: expect.any(String),
+ client_name: "test",
+ logo_uri: "",
+ redirect_uris: ["http://localhost:3000/api/auth/oauth2/callback/test"],
+ grant_types: ["authorization_code"],
+ response_types: ["code"],
+ token_endpoint_auth_method: "client_secret_basic",
+ client_id_issued_at: expect.any(Number),
+ client_secret_expires_at: 0,
});
if (createdClient.data) {
- application = createdClient.data;
+ application = {
+ clientId: createdClient.data.client_id,
+ clientSecret: createdClient.data.client_secret,
+ redirectURLs: createdClient.data.redirect_uris,
+ metadata: {},
+ icon: createdClient.data.logo_uri || "",
+ type: "web",
+ disabled: false,
+ name: createdClient.data.client_name || "",
+ };
}
});