Need Help Single Sign-On (SSO) with OIDC Provider Plugin
Hey everyone! I'm integrating Single Sign-On (SSO) with Better Auth using the OIDC Provider Plugin across two Next.js apps:
Main app (Backend + Auth) ā http://localhost:3000
Frontend (SSO Login UI) ā http://localhost:3001
š¹ Current Setup Main App - Auth Configuration (
Main app (Backend + Auth) ā http://localhost:3000
Frontend (SSO Login UI) ā http://localhost:3001
š¹ Current Setup Main App - Auth Configuration (
auth.ts
)
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/db";
import {
oidcProvider,
phoneNumber,
openAPI,
admin,
organization,
jwt,
} from "better-auth/plugins";
import { sso } from "better-auth/plugins/sso";
import { authSchema } from "@/db/schema";
import { nextCookies } from "better-auth/next-js";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg", // or "mysql", "sqlite"
schema: {
...authSchema,
user: authSchema.user,
},
}),
trustedOrigins: ["http://localhost:3001"],
account: {
accountLinking: {
enabled: true,
trustedProviders: ["google", "test-app"],
},
},
user: {
additionalFields: {
jobTitle: {
type: "string",
required: false,
},
},
},
emailAndPassword: {
enabled: true,
},
socialProviders: {
google: {
clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
},
plugins: [
phoneNumber(),
openAPI(),
organization(),
admin(),
nextCookies(),
oidcProvider({
loginPage: "/sign-in",
consentPage: "/oauth2/authorize",
requirePKCE: true,
}),
jwt(),
sso(),
]
});
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/db";
import {
oidcProvider,
phoneNumber,
openAPI,
admin,
organization,
jwt,
} from "better-auth/plugins";
import { sso } from "better-auth/plugins/sso";
import { authSchema } from "@/db/schema";
import { nextCookies } from "better-auth/next-js";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg", // or "mysql", "sqlite"
schema: {
...authSchema,
user: authSchema.user,
},
}),
trustedOrigins: ["http://localhost:3001"],
account: {
accountLinking: {
enabled: true,
trustedProviders: ["google", "test-app"],
},
},
user: {
additionalFields: {
jobTitle: {
type: "string",
required: false,
},
},
},
emailAndPassword: {
enabled: true,
},
socialProviders: {
google: {
clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
},
plugins: [
phoneNumber(),
openAPI(),
organization(),
admin(),
nextCookies(),
oidcProvider({
loginPage: "/sign-in",
consentPage: "/oauth2/authorize",
requirePKCE: true,
}),
jwt(),
sso(),
]
});


3 Replies
Main App - Client Configuration (
Frontend - SSO Login (
ā Issue 1: /oauth2/token Returns 401
Logs:
Reason:
- Missing
ā Fix:
- Manually added
- Now
ā Issue 2: "Key not found" & "token_not_verified" Logs:
Reason:
- The issued token cannot be verified with the JWKS keys
JWKS Response (
If anyone has faced similar issues or knows a better fix, please share your insights! Thanks in advance! š
auth-client.ts
)
import { createAuthClient } from "better-auth/react";
import {
inferAdditionalFields,
phoneNumberClient,
organizationClient,
adminClient,
oidcClient,
jwtClient,
ssoClient,
} from "better-auth/client/plugins";
import { toast } from "sonner";
export const client = createAuthClient({
baseURL: process.env.BETTER_AUTH_URL, // the base url of your auth server
plugins: [
organizationClient(),
ssoClient(),
adminClient(),
jwtClient(),
oidcClient(),
phoneNumberClient(),
inferAdditionalFields({
user: {
jobTitle: {
type: "string",
required: false,
},
},
}),
],
fetchOptions: {
onError(e) {
if (e.error.status === 429) {
toast.error("Too many requests. Please try again later.");
}
},
},
});
export const {
signUp,
signIn,
signOut,
useSession,
deleteUser,
admin
} = client;
import { createAuthClient } from "better-auth/react";
import {
inferAdditionalFields,
phoneNumberClient,
organizationClient,
adminClient,
oidcClient,
jwtClient,
ssoClient,
} from "better-auth/client/plugins";
import { toast } from "sonner";
export const client = createAuthClient({
baseURL: process.env.BETTER_AUTH_URL, // the base url of your auth server
plugins: [
organizationClient(),
ssoClient(),
adminClient(),
jwtClient(),
oidcClient(),
phoneNumberClient(),
inferAdditionalFields({
user: {
jobTitle: {
type: "string",
required: false,
},
},
}),
],
fetchOptions: {
onError(e) {
if (e.error.status === 429) {
toast.error("Too many requests. Please try again later.");
}
},
},
});
export const {
signUp,
signIn,
signOut,
useSession,
deleteUser,
admin
} = client;
page.tsx
) - 3001
"use client";
import { Button } from "@/components/ui/button";
import { createAuthClient } from "better-auth/react"
import { ssoClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
baseURL: "http://localhost:3000",
plugins: [ ssoClient()],
})
export default function SsoLogin() {
const handleClick = async () => {
const res = await authClient.signIn.sso(
{
providerId: "test-app",
callbackURL: "/dashboard",
}
);
console.log(res);
};
const handleRegister = async () => {
const res = await authClient.sso.register(
{
issuer: "http://localhost:3000/api/auth",
domain: "localhost.com",
providerId: "test-app",
clientId: "ksrbewcpwkbasyxjtfivxbacwmlgyjjz",
clientSecret: "icsyzwjtcdzgfhbjupdmifehklglmbko",
authorizationEndpoint:
"http://localhost:3000/api/auth/oauth2/authorize",
tokenEndpoint: "http://localhost:3000/api/auth/oauth2/token",
jwksEndpoint: "http://localhost:3000/api/auth/jwks",
pkce: true,
}
);
console.log(res);
};
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Button onClick={handleClick}>Sso Login</Button>
<Button onClick={handleRegister}>Register</Button>
</main>
</div>
);
}
"use client";
import { Button } from "@/components/ui/button";
import { createAuthClient } from "better-auth/react"
import { ssoClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
baseURL: "http://localhost:3000",
plugins: [ ssoClient()],
})
export default function SsoLogin() {
const handleClick = async () => {
const res = await authClient.signIn.sso(
{
providerId: "test-app",
callbackURL: "/dashboard",
}
);
console.log(res);
};
const handleRegister = async () => {
const res = await authClient.sso.register(
{
issuer: "http://localhost:3000/api/auth",
domain: "localhost.com",
providerId: "test-app",
clientId: "ksrbewcpwkbasyxjtfivxbacwmlgyjjz",
clientSecret: "icsyzwjtcdzgfhbjupdmifehklglmbko",
authorizationEndpoint:
"http://localhost:3000/api/auth/oauth2/authorize",
tokenEndpoint: "http://localhost:3000/api/auth/oauth2/token",
jwksEndpoint: "http://localhost:3000/api/auth/jwks",
pkce: true,
}
);
console.log(res);
};
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Button onClick={handleClick}>Sso Login</Button>
<Button onClick={handleRegister}>Register</Button>
</main>
</div>
);
}
POST /api/auth/oauth2/token 401
GET /api/auth/error/error?error=invalid_provider&error_description=token_response_not_found
POST /api/auth/oauth2/token 401
GET /api/auth/error/error?error=invalid_provider&error_description=token_response_not_found
Reason:
- Missing
client_id
and client_secret
in /oauth2/token
requestā Fix:
- Manually added
client_id
and client_secret
via hooks
hooks: {
before: createAuthMiddleware(async (ctx) => {
if (ctx.path === "/oauth2/token") {
return {
context: {
...ctx,
body: {
...ctx.body,
client_id: "ksrbewcpwkbasyxjtfivxbacwmlgyjjz",
client_secret: "icsyzwjtcdzgfhbjupdmifehklglmbko",
},
},
};
}
}),
},
hooks: {
before: createAuthMiddleware(async (ctx) => {
if (ctx.path === "/oauth2/token") {
return {
context: {
...ctx,
body: {
...ctx.body,
client_id: "ksrbewcpwkbasyxjtfivxbacwmlgyjjz",
client_secret: "icsyzwjtcdzgfhbjupdmifehklglmbko",
},
},
};
}
}),
},
/oauth2/token
returns 200 OK ā
ā Issue 2: "Key not found" & "token_not_verified" Logs:
POST /api/auth/oauth2/token 200
GET /api/auth/jwks 200
[Better Auth]: Error: Key not found
GET /api/auth/error/error?error=invalid_provider&error_description=token_not_verified
POST /api/auth/oauth2/token 200
GET /api/auth/jwks 200
[Better Auth]: Error: Key not found
GET /api/auth/error/error?error=invalid_provider&error_description=token_not_verified
Reason:
- The issued token cannot be verified with the JWKS keys
JWKS Response (
/api/auth/jwks
):{
"keys": [
{
"crv": "Ed25519",
"x": "d_aGBZ-mriny68ulckvvsaCHLi1Go64nNzjKCmR0vpY",
"kty": "OKP",
"kid": "eqfft7yeL6QdVeWitEFY834lMYXDyWpr"
}
]
}
{
"keys": [
{
"crv": "Ed25519",
"x": "d_aGBZ-mriny68ulckvvsaCHLi1Go64nNzjKCmR0vpY",
"kty": "OKP",
"kid": "eqfft7yeL6QdVeWitEFY834lMYXDyWpr"
}
]
}
If anyone has faced similar issues or knows a better fix, please share your insights! Thanks in advance! š
are you doing PKCE?
Yes, but failid to verifcation.
