Skip to content

Commit b4502e0

Browse files
os-zhuangclaude
andcommitted
feat(auth): surface features.sso in public /auth/config
getPublicConfig() advertised every other auth capability flag (oidcProvider, twoFactor, multiOrgEnabled, …) but omitted enterprise SSO, even though the manager already computes whether @better-auth/sso is wired (OS_SSO_ENABLED / plugins.sso). The login UI therefore had nothing to gate on and rendered the "Sign in with SSO" button unconditionally; on a deployment without SSO wired, clicking it only then surfaced "No SSO provider is configured for this email domain." features.sso is now resolved with the EXACT logic that decides whether the plugin is mounted in buildPlugins(), so the advertised capability can never disagree with the actual /sign-in/sso route. Tests cover default-off, config-on, and OS_SSO_ENABLED env override. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8bb026b commit b4502e0

3 files changed

Lines changed: 70 additions & 0 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
---
4+
5+
feat(auth): surface `features.sso` in the public `/auth/config` response
6+
7+
`getPublicConfig()` reported every other auth capability flag (`oidcProvider`,
8+
`twoFactor`, `multiOrgEnabled`, …) but omitted enterprise SSO, even though the
9+
manager already computes whether the domain-routed `@better-auth/sso` plugin is
10+
wired (`OS_SSO_ENABLED` / `plugins.sso`). Without it the login UI had no signal
11+
to gate on, so it rendered a "Sign in with SSO" button unconditionally — and on
12+
a self-hosted / local deployment where SSO isn't wired, clicking it only then
13+
surfaced "No SSO provider is configured for this email domain."
14+
15+
The config now includes `features.sso`, resolved with the EXACT logic that
16+
decides whether the plugin is mounted in `buildPlugins()`, so the advertised
17+
capability can never disagree with the actual `/sign-in/sso` route. The console
18+
login form consumes this to hide the button when SSO is off (objectui side).

packages/plugins/plugin-auth/src/auth-manager.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,7 @@ describe('AuthManager', () => {
10461046
magicLink: false,
10471047
organization: true,
10481048
oidcProvider: false,
1049+
sso: false,
10491050
deviceAuthorization: false,
10501051
admin: false,
10511052
multiOrgEnabled: false,
@@ -1054,6 +1055,48 @@ describe('AuthManager', () => {
10541055
});
10551056
});
10561057

1058+
// Enterprise SSO (@better-auth/sso) is opt-in: the plugin is only wired
1059+
// when `plugins.sso` / `OS_SSO_ENABLED` is on. The public config MUST
1060+
// report the same value so the login UI can hide the "Sign in with SSO"
1061+
// button when the `/sign-in/sso` route isn't mounted (otherwise the
1062+
// button only fails at click time with "No SSO provider is configured").
1063+
it('should report features.sso=false by default', () => {
1064+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1065+
const manager = new AuthManager({
1066+
secret: 'test-secret-at-least-32-chars-long',
1067+
});
1068+
warnSpy.mockRestore();
1069+
1070+
expect(manager.getPublicConfig().features.sso).toBe(false);
1071+
});
1072+
1073+
it('should report features.sso=true when enabled via plugins config', () => {
1074+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1075+
const manager = new AuthManager({
1076+
secret: 'test-secret-at-least-32-chars-long',
1077+
plugins: { sso: true } as any,
1078+
});
1079+
warnSpy.mockRestore();
1080+
1081+
expect(manager.getPublicConfig().features.sso).toBe(true);
1082+
});
1083+
1084+
it('should let OS_SSO_ENABLED env override the config (matches buildPlugins wiring)', () => {
1085+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1086+
const prev = process.env.OS_SSO_ENABLED;
1087+
process.env.OS_SSO_ENABLED = 'true';
1088+
try {
1089+
const manager = new AuthManager({
1090+
secret: 'test-secret-at-least-32-chars-long',
1091+
});
1092+
expect(manager.getPublicConfig().features.sso).toBe(true);
1093+
} finally {
1094+
if (prev === undefined) delete process.env.OS_SSO_ENABLED;
1095+
else process.env.OS_SSO_ENABLED = prev;
1096+
warnSpy.mockRestore();
1097+
}
1098+
});
1099+
10571100
it('should filter out disabled providers', () => {
10581101
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
10591102
const manager = new AuthManager({

packages/plugins/plugin-auth/src/auth-manager.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1462,6 +1462,14 @@ export class AuthManager {
14621462
// frontend will render UI for endpoints that 404.
14631463
const oidcEnv = (globalThis as any)?.process?.env?.OS_OIDC_PROVIDER_ENABLED;
14641464
const oidcFromEnv = oidcEnv != null ? String(oidcEnv).toLowerCase() === 'true' : undefined;
1465+
// Enterprise SSO (@better-auth/sso, domain-routed). Resolve it with the
1466+
// EXACT logic that decides whether the plugin is wired in `buildPlugins()`
1467+
// (`sso: ssoFromEnv ?? pluginConfig.sso ?? false`) so the two can never
1468+
// disagree. Surfacing it lets the login UI hide the "Sign in with SSO"
1469+
// button when `/sign-in/sso` isn't mounted, instead of rendering a button
1470+
// that only reveals "no SSO provider configured" at click time.
1471+
const ssoEnv = (globalThis as any)?.process?.env?.OS_SSO_ENABLED;
1472+
const ssoFromEnv = ssoEnv != null ? String(ssoEnv).toLowerCase() === 'true' : undefined;
14651473
const twoFactorFromEnv = readBooleanEnv('OS_AUTH_TWO_FACTOR');
14661474

14671475
const features = {
@@ -1471,6 +1479,7 @@ export class AuthManager {
14711479
organization: pluginConfig.organization ?? true,
14721480
multiOrgEnabled,
14731481
oidcProvider: oidcFromEnv ?? pluginConfig.oidcProvider ?? false,
1482+
sso: ssoFromEnv ?? (pluginConfig as any).sso ?? false,
14741483
deviceAuthorization: pluginConfig.deviceAuthorization ?? false,
14751484
admin: pluginConfig.admin ?? false,
14761485
...(termsUrl ? { termsUrl } : {}),

0 commit comments

Comments
 (0)