@payez/next-mvp 4.1.21 → 4.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/POST-INSTALL.md +408 -0
- package/README.md +7 -0
- package/dist/api-handlers/account/recovery/initiate.js +35 -2
- package/dist/api-handlers/account/recovery/send-code.d.ts +0 -33
- package/dist/api-handlers/account/recovery/send-code.js +35 -2
- package/dist/api-handlers/account/recovery/verify-code.d.ts +0 -33
- package/dist/api-handlers/account/recovery/verify-code.js +35 -2
- package/dist/api-handlers/account/reset-password.js +35 -2
- package/dist/api-handlers/account/validate-password.js +21 -3
- package/dist/api-handlers/admin/analytics.js +2 -1
- package/dist/api-handlers/admin/audit.js +2 -1
- package/dist/api-handlers/admin/sessions.js +2 -1
- package/dist/api-handlers/admin/stats.js +2 -1
- package/dist/api-handlers/admin/users.js +2 -1
- package/dist/api-handlers/admin/vibe-data.js +2 -1
- package/dist/api-handlers/auth/jwks.js +10 -0
- package/dist/api-handlers/auth/login.js +5 -2
- package/dist/api-handlers/auth/passkey-signin.js +16 -1
- package/dist/api-handlers/auth/refresh.d.ts +23 -3
- package/dist/api-handlers/auth/refresh.js +107 -16
- package/dist/api-handlers/auth/signout.js +3 -2
- package/dist/api-handlers/auth/verify-code.js +5 -2
- package/dist/api-handlers/session/viability.js +27 -1
- package/dist/api-handlers/test/force-expire.d.ts +29 -15
- package/dist/api-handlers/test/force-expire.js +109 -14
- package/dist/auth/better-auth.d.ts +263 -6
- package/dist/auth/better-auth.js +437 -59
- package/dist/auth/utils/idp-client.js +1 -1
- package/dist/client/AuthContext.js +49 -17
- package/dist/client/better-auth-client.d.ts +62 -62
- package/dist/components/account/UserAvatarMenu.js +12 -0
- package/dist/components/auth/ModeAwareLoginPage.js +10 -8
- package/dist/components/auth/ModeAwareSignupPage.js +5 -2
- package/dist/components/auth/TraditionalAuthSection.js +5 -1
- package/dist/hooks/useAuthSettings.js +14 -2
- package/dist/hooks/usePublicAuthSettings.d.ts +2 -2
- package/dist/hooks/usePublicAuthSettings.js +28 -14
- package/dist/lib/client-key.d.ts +61 -0
- package/dist/lib/client-key.js +72 -0
- package/dist/lib/demo-mode.js +2 -1
- package/dist/lib/ensure-fresh-access-token.js +5 -1
- package/dist/lib/idp-client-config.d.ts +25 -2
- package/dist/lib/idp-client-config.js +381 -36
- package/dist/lib/idp-fetch.d.ts +2 -0
- package/dist/lib/idp-fetch.js +31 -1
- package/dist/lib/idp-url-guard.d.ts +29 -0
- package/dist/lib/idp-url-guard.js +38 -0
- package/dist/lib/redis.js +77 -5
- package/dist/lib/session-cookie-shape.d.ts +69 -0
- package/dist/lib/session-cookie-shape.js +126 -0
- package/dist/lib/session-store.d.ts +35 -2
- package/dist/lib/session-store.js +135 -4
- package/dist/lib/single-flight.d.ts +35 -0
- package/dist/lib/single-flight.js +30 -0
- package/dist/lib/token-lifecycle.js +12 -0
- package/dist/middleware/create-middleware.js +12 -0
- package/dist/models/SessionModel.d.ts +41 -0
- package/dist/models/SessionModel.js +9 -0
- package/dist/pages/admin-login/page.d.ts +1 -1
- package/dist/pages/admin-login/page.js +5 -41
- package/dist/pages/security/EnhancedSecurityPage.d.ts +35 -1
- package/dist/pages/security/EnhancedSecurityPage.js +42 -2
- package/dist/pages/security/index.d.ts +1 -0
- package/dist/pages/security/index.js +3 -0
- package/dist/pages/test-env/EmergencyLogoutPage.d.ts +17 -5
- package/dist/pages/test-env/EmergencyLogoutPage.js +17 -5
- package/dist/pages/test-env/JwtInspectPage.d.ts +30 -5
- package/dist/pages/test-env/JwtInspectPage.js +166 -7
- package/dist/pages/test-env/RefreshTokenPage.d.ts +22 -6
- package/dist/pages/test-env/RefreshTokenPage.js +22 -6
- package/dist/pages/test-env/TestEnvPage.d.ts +16 -4
- package/dist/pages/test-env/TestEnvPage.js +16 -4
- package/dist/pages/test-env/guarded.d.ts +4 -0
- package/dist/pages/test-env/guarded.js +86 -0
- package/dist/pages/test-env/index.d.ts +50 -7
- package/dist/pages/test-env/index.js +58 -12
- package/dist/routes/auth/logout.js +3 -2
- package/dist/routes/auth/refresh.js +27 -1
- package/dist/routes/auth/session.d.ts +12 -3
- package/dist/routes/auth/session.js +92 -9
- package/dist/routes/auth/settings.d.ts +34 -4
- package/dist/routes/auth/settings.js +87 -33
- package/dist/routes/auth/viability.d.ts +1 -1
- package/dist/routes/auth/viability.js +122 -11
- package/dist/server/auth-guard.js +22 -15
- package/dist/server/auth.d.ts +177 -20
- package/dist/server/auth.js +34 -75
- package/dist/server/decode-session.js +84 -10
- package/dist/types/auth.d.ts +18 -0
- package/package.json +969 -958
- package/scripts/check-internal-url-usage.sh +73 -73
- package/scripts/check-publish-health.mjs +162 -0
- package/scripts/dev-broker.ps1 +71 -6
- package/scripts/print-client-config.mjs +162 -0
- package/scripts/server-only-stub.cjs +4 -0
- package/src/api-handlers/account/recovery/initiate.ts +35 -2
- package/src/api-handlers/account/recovery/send-code.ts +36 -2
- package/src/api-handlers/account/recovery/verify-code.ts +36 -2
- package/src/api-handlers/account/reset-password.ts +35 -2
- package/src/api-handlers/account/validate-password.ts +22 -3
- package/src/api-handlers/admin/analytics.ts +2 -1
- package/src/api-handlers/admin/audit.ts +2 -1
- package/src/api-handlers/admin/sessions.ts +2 -1
- package/src/api-handlers/admin/stats.ts +2 -1
- package/src/api-handlers/admin/users.ts +2 -1
- package/src/api-handlers/admin/vibe-data.ts +2 -1
- package/src/api-handlers/auth/jwks.ts +10 -0
- package/src/api-handlers/auth/login.ts +5 -2
- package/src/api-handlers/auth/passkey-signin.ts +112 -98
- package/src/api-handlers/auth/refresh.ts +112 -15
- package/src/api-handlers/auth/signout.ts +4 -3
- package/src/api-handlers/auth/verify-code.ts +5 -2
- package/src/api-handlers/mfa/backup-code-verify.ts +175 -175
- package/src/api-handlers/mfa/backup-codes.ts +41 -41
- package/src/api-handlers/mfa/methods.ts +46 -46
- package/src/api-handlers/mfa/totp-confirm.ts +42 -42
- package/src/api-handlers/mfa/totp-enroll.ts +42 -42
- package/src/api-handlers/passkeys/list.ts +31 -31
- package/src/api-handlers/passkeys/manage.ts +54 -54
- package/src/api-handlers/passkeys/register-options.ts +42 -42
- package/src/api-handlers/passkeys/register-verify.ts +41 -41
- package/src/api-handlers/session/viability.ts +29 -1
- package/src/api-handlers/test/force-expire.ts +118 -13
- package/src/auth/better-auth.ts +486 -51
- package/src/auth/utils/idp-client.ts +1 -1
- package/src/client/AuthContext.tsx +55 -19
- package/src/components/account/UserAvatarMenu.tsx +11 -0
- package/src/components/auth/ModeAwareLoginPage.tsx +40 -49
- package/src/components/auth/ModeAwareSignupPage.tsx +5 -2
- package/src/components/auth/TraditionalAuthSection.tsx +5 -1
- package/src/hooks/useAuthSettings.ts +14 -2
- package/src/hooks/usePublicAuthSettings.ts +38 -16
- package/src/lib/client-key.ts +88 -0
- package/src/lib/demo-mode.ts +2 -1
- package/src/lib/ensure-fresh-access-token.ts +5 -1
- package/src/lib/idp-client-config.ts +399 -36
- package/src/lib/idp-fetch.ts +36 -1
- package/src/lib/idp-url-guard.ts +37 -0
- package/src/lib/redis.ts +84 -6
- package/src/lib/session-cookie-shape.ts +184 -0
- package/src/lib/session-store.ts +152 -6
- package/src/lib/single-flight.ts +64 -0
- package/src/lib/token-lifecycle.ts +12 -0
- package/src/middleware/create-middleware.ts +11 -0
- package/src/models/SessionModel.ts +49 -0
- package/src/pages/admin-login/page.tsx +17 -127
- package/src/pages/security/EnhancedSecurityPage.tsx +95 -9
- package/src/pages/security/index.ts +3 -0
- package/src/pages/test-env/EmergencyLogoutPage.tsx +17 -5
- package/src/pages/test-env/JwtInspectPage.tsx +235 -20
- package/src/pages/test-env/RefreshTokenPage.tsx +22 -6
- package/src/pages/test-env/TestEnvPage.tsx +16 -4
- package/src/pages/test-env/guarded.tsx +81 -0
- package/src/pages/test-env/index.ts +59 -7
- package/src/routes/auth/logout.ts +4 -3
- package/src/routes/auth/passkey-signin.ts +13 -13
- package/src/routes/auth/refresh.ts +30 -1
- package/src/routes/auth/session.ts +98 -9
- package/src/routes/auth/settings.ts +144 -42
- package/src/routes/auth/viability.ts +129 -11
- package/src/routes/mfa/backup-code-verify.ts +1 -1
- package/src/routes/mfa/backup-codes.ts +10 -10
- package/src/routes/mfa/methods.ts +22 -22
- package/src/routes/mfa/totp-confirm.ts +10 -10
- package/src/routes/mfa/totp-enroll.ts +10 -10
- package/src/routes/mfa/totp-verify.ts +14 -14
- package/src/routes/passkeys/list.ts +10 -10
- package/src/routes/passkeys/manage.ts +23 -23
- package/src/routes/passkeys/register-options.ts +10 -10
- package/src/routes/passkeys/register-verify.ts +10 -10
- package/src/server/auth-guard.ts +24 -17
- package/src/server/auth.ts +37 -73
- package/src/server/decode-session.ts +95 -11
- package/src/templates/instrumentation.ts +21 -2
- package/src/types/auth.ts +20 -0
- package/dist/lib/auth-secret.d.ts +0 -17
- package/dist/lib/auth-secret.js +0 -116
- package/src/lib/auth-secret.ts +0 -137
|
@@ -38,7 +38,7 @@ export function getIdpUrl(): string {
|
|
|
38
38
|
* Get client ID for this application.
|
|
39
39
|
*/
|
|
40
40
|
export function getClientId(): string {
|
|
41
|
-
const clientId = process.env.CLIENT_ID
|
|
41
|
+
const clientId = process.env.CLIENT_ID;
|
|
42
42
|
if (!clientId) {
|
|
43
43
|
throw new Error('[IDP_CLIENT] FATAL: CLIENT_ID environment variable is REQUIRED');
|
|
44
44
|
}
|
|
@@ -42,6 +42,12 @@ export function AuthProvider({ children, config, useDynamicProviders = true }: A
|
|
|
42
42
|
const [dynamicProviders, setDynamicProviders] = useState<FederatedProvider[]>([]);
|
|
43
43
|
const [providersLoaded, setProvidersLoaded] = useState(!useDynamicProviders);
|
|
44
44
|
|
|
45
|
+
// What the IDP says this client offers. null until /api/auth/settings answers.
|
|
46
|
+
const [serverSettings, setServerSettings] = useState<{
|
|
47
|
+
allowFederatedLogin: boolean;
|
|
48
|
+
allowTraditionalLogin: boolean;
|
|
49
|
+
} | null>(null);
|
|
50
|
+
|
|
45
51
|
// Create QueryClient instance for React Query - used internally by MVP hooks
|
|
46
52
|
const [queryClient] = useState(() => new QueryClient({
|
|
47
53
|
defaultOptions: {
|
|
@@ -61,32 +67,53 @@ export function AuthProvider({ children, config, useDynamicProviders = true }: A
|
|
|
61
67
|
|
|
62
68
|
async function fetchDynamicProviders() {
|
|
63
69
|
try {
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
// READ THE IDP. This used to be a hardcoded literal:
|
|
71
|
+
// const result = { google: { id: 'google', name: 'Google' } };
|
|
72
|
+
// sitting under a comment saying providers were loaded dynamically. It returned
|
|
73
|
+
// Google for every client regardless of what the tenant actually had enabled -
|
|
74
|
+
// a config-shaped constant, which is the one thing configuration must never be.
|
|
75
|
+
//
|
|
76
|
+
// /api/auth/settings already serves the real answer (enabledProviders,
|
|
77
|
+
// allowFederatedLogin, enablePasswordReset) and nothing consumed it.
|
|
78
|
+
const response = await fetch('/api/auth/settings');
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
// No silent substitution. Absent settings means we do not know what this
|
|
81
|
+
// client offers, and inventing a provider list is how a tenant ends up with a
|
|
82
|
+
// sign-in button their IDP will refuse.
|
|
83
|
+
console.error('[AuthProvider] /api/auth/settings returned ' + response.status +
|
|
84
|
+
' - provider list unknown. Mount ./routes/auth/settings.');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
69
87
|
|
|
88
|
+
const body = await response.json();
|
|
89
|
+
const settings = body?.data ?? body;
|
|
70
90
|
if (!mounted) return;
|
|
71
91
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
const ids: string[] = Array.isArray(settings?.enabledProviders)
|
|
93
|
+
? settings.enabledProviders
|
|
94
|
+
: [];
|
|
95
|
+
|
|
96
|
+
const oauthProviders = ids
|
|
97
|
+
.filter(id => OAUTH_PROVIDER_IDS.includes(id))
|
|
98
|
+
.map(id => PROVIDER_MAP[id])
|
|
99
|
+
.filter((p): p is FederatedProvider => p !== undefined);
|
|
100
|
+
|
|
101
|
+
setDynamicProviders(oauthProviders);
|
|
102
|
+
setServerSettings({
|
|
103
|
+
allowFederatedLogin: settings?.allowFederatedLogin !== false,
|
|
104
|
+
// TRADITIONAL DEFAULTS ON. The IDP does not carry an explicit flag for it yet;
|
|
105
|
+
// until it does, username/password is available unless a site opts out. An
|
|
106
|
+
// OAuth-only client is a deliberate choice, not the default.
|
|
107
|
+
allowTraditionalLogin: settings?.allowTraditionalLogin !== false,
|
|
108
|
+
});
|
|
81
109
|
} catch (err) {
|
|
82
|
-
|
|
110
|
+
console.error('[AuthProvider] could not read /api/auth/settings:', err);
|
|
83
111
|
} finally {
|
|
84
112
|
if (mounted) {
|
|
85
113
|
setProvidersLoaded(true);
|
|
86
114
|
}
|
|
87
115
|
}
|
|
88
116
|
}
|
|
89
|
-
|
|
90
117
|
fetchDynamicProviders();
|
|
91
118
|
|
|
92
119
|
return () => {
|
|
@@ -102,7 +129,12 @@ export function AuthProvider({ children, config, useDynamicProviders = true }: A
|
|
|
102
129
|
const authConfig: AuthConfig = {
|
|
103
130
|
...defaultConfig,
|
|
104
131
|
...config,
|
|
105
|
-
providers, //
|
|
132
|
+
providers, // from the IDP when useDynamicProviders is on
|
|
133
|
+
// THE SERVER DECIDES, THE SITE MAY OVERRIDE. A site that passes an explicit value
|
|
134
|
+
// still wins; absent that, what the tenant's IDP reports is what renders. A client
|
|
135
|
+
// hardcoding its own auth mode is how a sign-in screen and its IDP disagree.
|
|
136
|
+
allowFederatedLogin: config?.allowFederatedLogin ?? serverSettings?.allowFederatedLogin ?? true,
|
|
137
|
+
allowTraditionalLogin: config?.allowTraditionalLogin ?? serverSettings?.allowTraditionalLogin ?? true,
|
|
106
138
|
};
|
|
107
139
|
|
|
108
140
|
return (
|
|
@@ -134,10 +166,14 @@ export function useFederatedProviders(): FederatedProvider[] {
|
|
|
134
166
|
|
|
135
167
|
export function useFederatedAuthEnabled(): boolean {
|
|
136
168
|
const config = useAuthConfig();
|
|
137
|
-
|
|
169
|
+
// Was `config.mode === 'federated'`, which made the two mutually exclusive and let a
|
|
170
|
+
// site's hardcoded mode silently hide what the IDP offers.
|
|
171
|
+
return config.allowFederatedLogin !== false && config.providers.length > 0;
|
|
138
172
|
}
|
|
139
173
|
|
|
140
174
|
export function useTraditionalAuthEnabled(): boolean {
|
|
141
175
|
const config = useAuthConfig();
|
|
142
|
-
|
|
176
|
+
// Was `config.mode === 'traditional'`. Both can be on at once - that is the normal
|
|
177
|
+
// case, and OAuth-only is the exception a client opts into.
|
|
178
|
+
return config.allowTraditionalLogin !== false;
|
|
143
179
|
}
|
|
@@ -104,6 +104,17 @@ export function UserAvatarMenu({
|
|
|
104
104
|
} else {
|
|
105
105
|
// Use NEXT_PUBLIC env var or default to root
|
|
106
106
|
const logoutUrl = process.env.NEXT_PUBLIC_LOGOUT_REDIRECT_URL || '/';
|
|
107
|
+
// 272068: revoke server-side BEFORE the better-auth client sign-out clears
|
|
108
|
+
// the cookie jar. authClient.signOut() ALONE leaves the server-side sess
|
|
109
|
+
// record alive - a copied pre-signout cookie still authenticates. The
|
|
110
|
+
// logout route (routes/auth/logout) runs deleteSessionEverywhere and
|
|
111
|
+
// clears the __Secure- twin; it must still see the cookie, so it goes
|
|
112
|
+
// first. Best-effort: a failed revoke must not trap the user on the page.
|
|
113
|
+
try {
|
|
114
|
+
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
|
|
115
|
+
} catch {
|
|
116
|
+
// sign-out continues regardless
|
|
117
|
+
}
|
|
107
118
|
await authClient.signOut();
|
|
108
119
|
window.location.href = logoutUrl;
|
|
109
120
|
}
|
|
@@ -42,8 +42,12 @@ export function ModeAwareLoginPage({
|
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// READ THE FLAGS, NOT `mode`. A single mode made these mutually exclusive, so a site
|
|
46
|
+
// could only ever show ONE of OAuth or username/password. Both are normal together;
|
|
47
|
+
// OAuth-only is the exception a client opts into. Source is the IDP
|
|
48
|
+
// (/api/auth/settings) unless the site deliberately overrides.
|
|
49
|
+
const isFederated = config.allowFederatedLogin !== false && config.providers.length > 0;
|
|
50
|
+
const isTraditional = config.allowTraditionalLogin !== false;
|
|
47
51
|
const totalLoading = isLoading || localLoading;
|
|
48
52
|
|
|
49
53
|
return (
|
|
@@ -60,7 +64,7 @@ export function ModeAwareLoginPage({
|
|
|
60
64
|
</div>
|
|
61
65
|
)}
|
|
62
66
|
|
|
63
|
-
{/* Federated
|
|
67
|
+
{/* Federated. Renders alongside the form below when the client offers both. */}
|
|
64
68
|
{isFederated && (
|
|
65
69
|
<FederatedAuthSection
|
|
66
70
|
providers={config.providers}
|
|
@@ -69,7 +73,20 @@ export function ModeAwareLoginPage({
|
|
|
69
73
|
/>
|
|
70
74
|
)}
|
|
71
75
|
|
|
72
|
-
{/*
|
|
76
|
+
{/* Separator, only when BOTH are offered. Previously the two sections rendered
|
|
77
|
+
flush against each other because a single `mode` meant only one could ever
|
|
78
|
+
appear — nothing needed to sit between them. */}
|
|
79
|
+
{isFederated && isTraditional && (
|
|
80
|
+
<div className="flex items-center gap-3 my-6" aria-hidden="true">
|
|
81
|
+
<span className="h-px flex-1" style={{ background: 'var(--border-default, #e5e7eb)' }} />
|
|
82
|
+
<span className="text-xs uppercase tracking-wide" style={{ color: 'var(--text-muted, #6b7280)' }}>
|
|
83
|
+
or
|
|
84
|
+
</span>
|
|
85
|
+
<span className="h-px flex-1" style={{ background: 'var(--border-default, #e5e7eb)' }} />
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{/* Username / password. */}
|
|
73
90
|
{isTraditional && (
|
|
74
91
|
<TraditionalAuthSection
|
|
75
92
|
email={email}
|
|
@@ -84,51 +101,25 @@ export function ModeAwareLoginPage({
|
|
|
84
101
|
/>
|
|
85
102
|
)}
|
|
86
103
|
|
|
87
|
-
{/* Hybrid Mode - Show both
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<div className="relative flex justify-center text-sm">
|
|
107
|
-
<span
|
|
108
|
-
className="px-2"
|
|
109
|
-
style={{
|
|
110
|
-
background: 'var(--bg-card)',
|
|
111
|
-
color: 'var(--text-secondary)',
|
|
112
|
-
}}
|
|
113
|
-
>
|
|
114
|
-
Or sign in with email
|
|
115
|
-
</span>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
)}
|
|
119
|
-
|
|
120
|
-
<TraditionalAuthSection
|
|
121
|
-
email={email}
|
|
122
|
-
password={password}
|
|
123
|
-
onEmailChange={setEmail}
|
|
124
|
-
onPasswordChange={setPassword}
|
|
125
|
-
onSubmit={handleTraditionalSubmit}
|
|
126
|
-
isLoading={totalLoading}
|
|
127
|
-
buttonText="Sign In"
|
|
128
|
-
showForgotPassword={config.enableRecovery}
|
|
129
|
-
onForgotPassword={onForgotPassword}
|
|
130
|
-
/>
|
|
131
|
-
</>
|
|
104
|
+
{/* NEITHER is offered. This branch used to be labelled "Hybrid Mode - Show both"
|
|
105
|
+
and rendered BOTH sections on the same condition - so a client configured with
|
|
106
|
+
no sign-in method at all got the full form. Say so instead: a blank card leaves
|
|
107
|
+
the user pressing nothing, and the operator with no idea what is wrong. */}
|
|
108
|
+
{!isFederated && !isTraditional && (
|
|
109
|
+
<div
|
|
110
|
+
role="alert"
|
|
111
|
+
className="px-4 py-3 rounded-lg text-sm text-left"
|
|
112
|
+
style={{
|
|
113
|
+
border: '1px solid var(--border-default, #e5e7eb)',
|
|
114
|
+
color: 'var(--text-primary, #111827)',
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
<p className="font-semibold">Sign-in is not available for this application.</p>
|
|
118
|
+
<p className="mt-1">
|
|
119
|
+
No sign-in method is enabled for this client. An administrator needs to enable
|
|
120
|
+
a federated provider or username and password sign-in.
|
|
121
|
+
</p>
|
|
122
|
+
</div>
|
|
132
123
|
)}
|
|
133
124
|
</>
|
|
134
125
|
);
|
|
@@ -46,8 +46,11 @@ export function ModeAwareSignupPage({
|
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
// Same change as ModeAwareLoginPage: read the independent flags, not the single mode.
|
|
50
|
+
// Leaving signup on `mode` would have made sign-in and sign-up disagree about what the
|
|
51
|
+
// client offers - the worse half of a half-migration.
|
|
52
|
+
const isFederated = config.allowFederatedLogin !== false && config.providers.length > 0;
|
|
53
|
+
const isTraditional = config.allowTraditionalLogin !== false;
|
|
51
54
|
const totalLoading = isLoading || localLoading;
|
|
52
55
|
|
|
53
56
|
return (
|
|
@@ -25,8 +25,12 @@ export function TraditionalAuthSection({
|
|
|
25
25
|
showForgotPassword = true,
|
|
26
26
|
onForgotPassword,
|
|
27
27
|
}: Props) {
|
|
28
|
+
// text-left is DELIBERATE and load-bearing: a host card with text-align:center made the
|
|
29
|
+
// Email label render CENTRED while Password stayed left, because Password sits in a flex
|
|
30
|
+
// row (for the forgot link) that ignores inherited alignment and Email did not. A shared
|
|
31
|
+
// component must not inherit its alignment from whatever wraps it.
|
|
28
32
|
return (
|
|
29
|
-
<form onSubmit={onSubmit} className="space-y-4">
|
|
33
|
+
<form onSubmit={onSubmit} className="space-y-4 text-left">
|
|
30
34
|
{/* Email */}
|
|
31
35
|
<div>
|
|
32
36
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
@@ -57,7 +57,14 @@ export function useAuthSettings(): AuthSettings | null {
|
|
|
57
57
|
*/
|
|
58
58
|
export function useRequires2FA(): boolean {
|
|
59
59
|
const authSettings = useAuthSettings();
|
|
60
|
-
|
|
60
|
+
// 257255: DELIBERATELY LEFT FAIL-CLOSED, and flagged rather than silently kept.
|
|
61
|
+
// QAPert proved any hardcoded fallback is wrong in one environment (same client
|
|
62
|
+
// is require_2fa TRUE on AKS, FALSE on dev-93), so `?? true` is not correct - it
|
|
63
|
+
// is merely the SAFE direction: it demands more auth rather than less, and
|
|
64
|
+
// flipping it to false would be fail-OPEN. The genuinely correct answer is to
|
|
65
|
+
// propagate "unknown" to the caller, which is a breaking signature change
|
|
66
|
+
// (boolean -> boolean | null) and needs its own decision. Tracked on 257255.
|
|
67
|
+
return authSettings?.require2FA ?? true;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
/**
|
|
@@ -67,7 +74,12 @@ export function useRequires2FA(): boolean {
|
|
|
67
74
|
*/
|
|
68
75
|
export function useAllowed2FAMethods(): string[] {
|
|
69
76
|
const authSettings = useAuthSettings();
|
|
70
|
-
|
|
77
|
+
// 257255 / v32(a) OFFER NOTHING YOU CANNOT CONFIRM.
|
|
78
|
+
// Was `?? ['email','sms']` - a fabricated method list rendered whenever the
|
|
79
|
+
// policy could not be read. On 2026-08-24 that told users to use email+sms
|
|
80
|
+
// while the database said emailcode+totp. An empty list is the honest answer;
|
|
81
|
+
// the caller must show "we could not load sign-in options" rather than guess.
|
|
82
|
+
return authSettings?.allowed2FAMethods ?? [];
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
/**
|
|
@@ -12,11 +12,19 @@ import type { FederatedProvider } from '../types/auth';
|
|
|
12
12
|
|
|
13
13
|
export interface PublicAuthSettings {
|
|
14
14
|
enabledProviders: FederatedProvider[];
|
|
15
|
-
allowPublicRegistration: boolean;
|
|
16
15
|
allowFederatedLogin: boolean;
|
|
17
|
-
enablePasswordReset: boolean;
|
|
18
16
|
require2FA: boolean;
|
|
19
17
|
allowed2FAMethods: string[];
|
|
18
|
+
// OPTIONAL: present only when the IDP states them, never invented.
|
|
19
|
+
// 257255 removed these on the basis that "the config document does not
|
|
20
|
+
// carry them"; the 240447 loader fix has since made the document carry
|
|
21
|
+
// them, so routes/auth/settings.ts reads and returns them again. They stay
|
|
22
|
+
// OPTIONAL because an older IDP build may omit them, and `?? true` would
|
|
23
|
+
// re-create the exact permission claim this card exists to kill.
|
|
24
|
+
// (The @example below already assumed this field - it referenced
|
|
25
|
+
// settings?.allowPublicRegistration while the type had removed it.)
|
|
26
|
+
allowPublicRegistration?: boolean;
|
|
27
|
+
enablePasswordReset?: boolean;
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
export interface UsePublicAuthSettingsResult {
|
|
@@ -35,14 +43,19 @@ const PROVIDER_MAP: Record<string, FederatedProvider> = {
|
|
|
35
43
|
'azure-ad': 'microsoft',
|
|
36
44
|
};
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
// 257255 / BAPert ruling v32(a): DEFAULT_SETTINGS DELETED.
|
|
47
|
+
//
|
|
48
|
+
// It was substituted on BOTH failure branches below, so a page kept rendering
|
|
49
|
+
// require2FA=true with methods ['email','sms'] whenever the fetch failed - a
|
|
50
|
+
// FABRICATED AUTHORIZATION POLICY, client-side. Hard-failing the endpoint alone
|
|
51
|
+
// does not fix that: it just moves the lie from the server to the browser.
|
|
52
|
+
//
|
|
53
|
+
// QAPert's measurement makes this a proof rather than a preference: the SAME
|
|
54
|
+
// client is require_2fa=TRUE on AKS and FALSE on dev-93, so any hardcoded
|
|
55
|
+
// fallback is wrong in at least one environment BY CONSTRUCTION.
|
|
56
|
+
//
|
|
57
|
+
// On failure `settings` stays NULL and `error` is set. Callers must render an
|
|
58
|
+
// honest "we could not load sign-in options" state and OFFER NOTHING.
|
|
46
59
|
|
|
47
60
|
/**
|
|
48
61
|
* Hook to fetch public auth settings for login/signup pages.
|
|
@@ -98,7 +111,12 @@ export function usePublicAuthSettings(): UsePublicAuthSettingsResult {
|
|
|
98
111
|
enabledProviders: providers,
|
|
99
112
|
});
|
|
100
113
|
} else {
|
|
101
|
-
|
|
114
|
+
// The endpoint told us it could not read a policy (503 +
|
|
115
|
+
// success:false + code). Do NOT invent one - surface it.
|
|
116
|
+
const code = data?.error?.code ?? 'POLICY_UNAVAILABLE';
|
|
117
|
+
setSettings(null);
|
|
118
|
+
setError(new Error(`[auth-settings] ${code}: sign-in options unavailable`));
|
|
119
|
+
return;
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
setError(null);
|
|
@@ -106,7 +124,7 @@ export function usePublicAuthSettings(): UsePublicAuthSettingsResult {
|
|
|
106
124
|
if (!mounted) return;
|
|
107
125
|
console.warn('[usePublicAuthSettings] Failed to fetch settings:', err);
|
|
108
126
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
109
|
-
setSettings(
|
|
127
|
+
setSettings(null); // 257255: never fabricate a policy
|
|
110
128
|
} finally {
|
|
111
129
|
if (mounted) {
|
|
112
130
|
setIsLoading(false);
|
|
@@ -136,14 +154,18 @@ export function useFederatedLoginEnabled(): boolean {
|
|
|
136
154
|
* Hook to check if public registration is enabled.
|
|
137
155
|
*/
|
|
138
156
|
export function usePublicRegistrationEnabled(): boolean {
|
|
139
|
-
|
|
140
|
-
|
|
157
|
+
// 257255: was `?? true`, which OFFERED a sign-up lane the IDP may forbid -
|
|
158
|
+
// card 240447 measured exactly that on portal. The field is no longer served
|
|
159
|
+
// (it was never read from config), so this is now always fail-closed until
|
|
160
|
+
// the config document carries it.
|
|
161
|
+
return false;
|
|
141
162
|
}
|
|
142
163
|
|
|
143
164
|
/**
|
|
144
165
|
* Hook to check if password reset is enabled.
|
|
145
166
|
*/
|
|
146
167
|
export function usePasswordResetEnabled(): boolean {
|
|
147
|
-
|
|
148
|
-
|
|
168
|
+
// 257255: was `?? true` and was never read from config. Fail-closed until
|
|
169
|
+
// the config document carries it.
|
|
170
|
+
return false;
|
|
149
171
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE resolver for "which client is this request for?" — card 268410.
|
|
3
|
+
*
|
|
4
|
+
* THE ASYMMETRY THIS EXISTS TO SURVIVE (measured at source in the IDP,
|
|
5
|
+
* RequirePayEzUnAuthorizedAttribute.cs):
|
|
6
|
+
*
|
|
7
|
+
* :31-33 int.TryParse succeeds -> Items[PayEzClientId] = numericId; return; NO existence check
|
|
8
|
+
* :41-47 slug -> GetClientIdByNameAsync; if null -> NotFoundResult() VALIDATED
|
|
9
|
+
* :55 no client id at all -> NotFoundResult()
|
|
10
|
+
*
|
|
11
|
+
* So a WRONG SLUG 404s loudly. A WRONG NUMERIC is silently accepted and the
|
|
12
|
+
* request is answered for whatever client that number happens to be. On a
|
|
13
|
+
* multi-tenant identity service that is a WRONG-TENANT ANSWER, not merely a bug.
|
|
14
|
+
*
|
|
15
|
+
* And a stale numeric is the DOCUMENTED NORMAL CASE, not a hypothetical: numeric
|
|
16
|
+
* ids are environment-specific (card 250596 — 79 was dev-93's id for the SAME
|
|
17
|
+
* slug, and the merge collapsed 76 into 12), while the slug is stable across
|
|
18
|
+
* environments.
|
|
19
|
+
*
|
|
20
|
+
* WHY THE FALLBACK WAS SILENT: idp-client-config.ts:462 and :669 resolve
|
|
21
|
+
* `clientSlug: ... ?? ''`. Empty string is FALSY, so the idiom every call site
|
|
22
|
+
* used —
|
|
23
|
+
*
|
|
24
|
+
* config.clientSlug || String(config.clientId)
|
|
25
|
+
*
|
|
26
|
+
* — falls through to the numeric arm whenever the IDP config carries no slug,
|
|
27
|
+
* with nothing emitted to say it happened. Absence collapsed into a
|
|
28
|
+
* usable-looking default: the same class the graft's passkey plugin already
|
|
29
|
+
* called out when `|| ''` made an unset env var look like a valid client_id.
|
|
30
|
+
*
|
|
31
|
+
* WHAT THIS DOES **NOT** DO: it does not flip precedence. Slug still wins, the
|
|
32
|
+
* numeric fallback is still taken, and every deployment that works today keeps
|
|
33
|
+
* working byte-for-byte. Blanket-flipping these to numeric-first is the
|
|
34
|
+
* dangerous-looking-safe change the 268410 ruling explicitly holds.
|
|
35
|
+
*
|
|
36
|
+
* WHAT IT DOES: it makes the dangerous arm ATTRIBUTABLE. Taking the numeric
|
|
37
|
+
* fallback now emits a warning naming the call site, so a wrong-tenant answer
|
|
38
|
+
* is diagnosable in seconds instead of being indistinguishable from a correct
|
|
39
|
+
* one. That ambiguity is what cost hours on S12.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
export interface ClientKeySource {
|
|
43
|
+
clientSlug?: string | null;
|
|
44
|
+
clientId?: string | number | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ResolvedClientKey {
|
|
48
|
+
/** The routing key to send as X-Client-Id / client_id, or null if none resolved. */
|
|
49
|
+
key: string | null;
|
|
50
|
+
/** True when the environment-specific numeric arm was taken. */
|
|
51
|
+
usedNumericFallback: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the IDP routing key from a client config.
|
|
56
|
+
*
|
|
57
|
+
* `site` names the caller (e.g. 'admin/analytics') and appears verbatim in the
|
|
58
|
+
* fallback warning — that string is the whole diagnostic value, so pass a real one.
|
|
59
|
+
*
|
|
60
|
+
* Pure and non-throwing: callers decide whether a null key is fatal (refuse) or
|
|
61
|
+
* merely means "omit the header". Both shapes exist in this package and both
|
|
62
|
+
* are correct for their path.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveClientKey(
|
|
65
|
+
config: ClientKeySource | null | undefined,
|
|
66
|
+
site: string
|
|
67
|
+
): ResolvedClientKey {
|
|
68
|
+
const slug = typeof config?.clientSlug === 'string' ? config.clientSlug.trim() : '';
|
|
69
|
+
if (slug) return { key: slug, usedNumericFallback: false };
|
|
70
|
+
|
|
71
|
+
const rawNumeric = config?.clientId;
|
|
72
|
+
// Guard String(undefined) === 'undefined' — a 13-char string that is not a
|
|
73
|
+
// slug and not a number, so the IDP would 404 it with no hint as to why.
|
|
74
|
+
if (rawNumeric === null || rawNumeric === undefined) {
|
|
75
|
+
return { key: null, usedNumericFallback: false };
|
|
76
|
+
}
|
|
77
|
+
const numeric = String(rawNumeric).trim();
|
|
78
|
+
if (!numeric) return { key: null, usedNumericFallback: false };
|
|
79
|
+
|
|
80
|
+
console.warn(
|
|
81
|
+
`[client-key] ${site}: no clientSlug on the resolved IDP config — falling back to the ` +
|
|
82
|
+
`NUMERIC client id ${numeric}. Numeric ids are ENVIRONMENT-SPECIFIC and the IDP accepts ` +
|
|
83
|
+
`them with NO existence check, so if this number is stale for this environment the ` +
|
|
84
|
+
`request is being answered for the WRONG CLIENT, silently. Card 268410.`
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return { key: numeric, usedNumericFallback: true };
|
|
88
|
+
}
|
package/src/lib/demo-mode.ts
CHANGED
|
@@ -12,6 +12,7 @@ export function isAuthConfigured(): boolean {
|
|
|
12
12
|
return !!(
|
|
13
13
|
process.env.BETTER_AUTH_SECRET ||
|
|
14
14
|
process.env.NEXTAUTH_SECRET ||
|
|
15
|
-
|
|
15
|
+
// was NEXT_CLIENT_ID - a third name for CLIENT_ID, read only here. Collapsed 2026-08-26.
|
|
16
|
+
(process.env.CLIENT_ID && process.env.NEXT_CLIENT_PRIVATE_KEY_PEM)
|
|
16
17
|
);
|
|
17
18
|
}
|
|
@@ -361,7 +361,11 @@ export async function ensureFreshAccessToken(
|
|
|
361
361
|
bearerKeyId: newKid || session.bearerKeyId,
|
|
362
362
|
authenticationMethods: amrClaims,
|
|
363
363
|
authenticationLevel: acrLevel,
|
|
364
|
-
|
|
364
|
+
// 170007 rule 2 (BAPert ruling on 121350): re-derive from the token
|
|
365
|
+
// every exchange, never carry forward. See the identical fix + full
|
|
366
|
+
// rationale in api-handlers/auth/refresh.ts's sessionUpdate — this is
|
|
367
|
+
// the same defect duplicated at a second call site.
|
|
368
|
+
mfaVerified: amrClaims.includes('mfa'),
|
|
365
369
|
mfaCompletedAt: decoded?.mfa_time ? parseInt(decoded.mfa_time) * 1000 : session.mfaCompletedAt,
|
|
366
370
|
mfaExpiresAt: decoded?.mfa_expires ? parseInt(decoded.mfa_expires) * 1000 : session.mfaExpiresAt,
|
|
367
371
|
mfaValidityHours: decoded?.mfa_validity_hours ? parseInt(decoded.mfa_validity_hours) : session.mfaValidityHours,
|