fix(sso): default tokenEndpointAuthentication to client_secret_post (#3627)

* 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 <mini.jeong@navercorp.com>

* 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 <mini.jeong@navercorp.com>

---------

Signed-off-by: Mini Jeong <mini.jeong@navercorp.com>
This commit is contained in:
mini
2026-04-08 00:46:18 +09:00
committed by GitHub
parent ed19fed0ca
commit f46886e6cf

View File

@@ -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<boolean> {
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: