From f46886e6cf0636d83e827f9585e6b8392e8830ed Mon Sep 17 00:00:00 2001 From: mini Date: Wed, 8 Apr 2026 00:46:18 +0900 Subject: [PATCH] fix(sso): default tokenEndpointAuthentication to client_secret_post (#3627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(sso): default tokenEndpointAuthentication to client_secret_post better-auth's SSO plugin does not URL-encode credentials before Base64 encoding in client_secret_basic mode (RFC 6749 §2.3.1). When the client secret contains special characters (+, =, /), OIDC providers decode them incorrectly, causing invalid_client errors. Default to client_secret_post when tokenEndpointAuthentication is not explicitly set to avoid this upstream encoding issue. Fixes #3626 * fix(sso): use nullish coalescing and add env var for tokenEndpointAuthentication - Use ?? instead of || for semantic correctness - Add SSO_OIDC_TOKEN_ENDPOINT_AUTH env var so users can explicitly set client_secret_basic when their provider requires it * docs(sso): add SSO_OIDC_TOKEN_ENDPOINT_AUTH to script usage comment Signed-off-by: Mini Jeong * fix(sso): validate SSO_OIDC_TOKEN_ENDPOINT_AUTH env var value Replace unsafe `as` type cast with runtime validation to ensure only 'client_secret_post' or 'client_secret_basic' are accepted. Invalid values (typos, empty strings) now fall back to undefined, letting the downstream ?? fallback apply correctly. Signed-off-by: Mini Jeong --------- Signed-off-by: Mini Jeong --- packages/db/scripts/register-sso-provider.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/db/scripts/register-sso-provider.ts b/packages/db/scripts/register-sso-provider.ts index 07d0ff7684..ed00894efc 100644 --- a/packages/db/scripts/register-sso-provider.ts +++ b/packages/db/scripts/register-sso-provider.ts @@ -21,6 +21,7 @@ * SSO_OIDC_CLIENT_ID=your_client_id * SSO_OIDC_CLIENT_SECRET=your_client_secret * SSO_OIDC_SCOPES=openid,profile,email (optional) + * SSO_OIDC_TOKEN_ENDPOINT_AUTH=client_secret_post|client_secret_basic (optional, defaults to client_secret_post) * * SAML Providers: * SSO_SAML_ENTRY_POINT=https://your-idp/sso @@ -215,6 +216,11 @@ function buildSSOConfigFromEnv(): SSOProviderConfig | null { pkce: process.env.SSO_OIDC_PKCE !== 'false', authorizationEndpoint: process.env.SSO_OIDC_AUTHORIZATION_ENDPOINT, tokenEndpoint: process.env.SSO_OIDC_TOKEN_ENDPOINT, + tokenEndpointAuthentication: + process.env.SSO_OIDC_TOKEN_ENDPOINT_AUTH === 'client_secret_post' || + process.env.SSO_OIDC_TOKEN_ENDPOINT_AUTH === 'client_secret_basic' + ? process.env.SSO_OIDC_TOKEN_ENDPOINT_AUTH + : undefined, userInfoEndpoint: process.env.SSO_OIDC_USERINFO_ENDPOINT, jwksEndpoint: process.env.SSO_OIDC_JWKS_ENDPOINT, discoveryEndpoint: @@ -507,7 +513,11 @@ async function registerSSOProvider(): Promise { clientSecret: ssoConfig.oidcConfig.clientSecret, authorizationEndpoint: ssoConfig.oidcConfig.authorizationEndpoint, tokenEndpoint: ssoConfig.oidcConfig.tokenEndpoint, - tokenEndpointAuthentication: ssoConfig.oidcConfig.tokenEndpointAuthentication, + // Default to client_secret_post: better-auth sends client_secret_basic + // credentials without URL-encoding per RFC 6749 §2.3.1, so '+' in secrets + // is decoded as space by OIDC providers, causing invalid_client errors. + tokenEndpointAuthentication: + ssoConfig.oidcConfig.tokenEndpointAuthentication ?? 'client_secret_post', jwksEndpoint: ssoConfig.oidcConfig.jwksEndpoint, pkce: ssoConfig.oidcConfig.pkce, discoveryEndpoint: