Commitfc0ca99unknown_key
fix(sso): stop dumping raw IdP 404 HTML into the /login error box
fix(sso): stop dumping raw IdP 404 HTML into the /login error box User screenshot from prod: after a Google SSO misconfig, /login rendered the raw `<!DOCTYPE html> <html lang=en>` body of Google's 404 page inside a red error div. Junior-tier UX surfacing. Three fixes: 1) `friendlySsoError(err)` in src/routes/sso.tsx maps every raw OIDC failure shape we ever see (`token_endpoint 4xx/5xx`, `userinfo_endpoint …`, expired state cookie, email-domain reject) to a one-sentence message safe to drop into HTML, with a concrete "go check /admin/sso" pointer. The raw `err.message` still goes to console.error for server-side diagnosis — operators don't lose the detail, end users don't see it. 2) `/admin/sso` now has four preset buttons (Google Workspace, Okta, Auth0, Microsoft Entra). Clicking one fills in issuer / authorize / token / userinfo / scopes / button-label from a vetted table. Removes the typing-URLs-by-hand step that produced the 404 in the first place. 3) The "Sign in with X" label on /login no longer falls back to a generic "SSO" when the admin left the `provider_name` field blank. `inferSsoProviderName(cfg)` inspects the configured issuer + authorize URLs and returns "Google" / "Okta" / "Microsoft" / "Auth0" / "Authentik". Only falls back to "SSO" when no URL match. Suite 1491 / 0 fail / 2 skip.
2 files changed+143−8fc0ca99c69b65b95d6f7b614375ac64655c26f52
2 changed files+143−8
Modifiedsrc/routes/auth.tsx+23−1View fileUnifiedSplit
@@ -186,7 +186,8 @@ auth.get("/login", async (c) => {
186186 !!ssoCfg.userinfoEndpoint &&
187187 !!ssoCfg.clientId &&
188188 !!ssoCfg.clientSecret;
189 const ssoLabel = ssoCfg?.providerName || "SSO";
189 const ssoLabel =
190 ssoCfg?.providerName || inferSsoProviderName(ssoCfg) || "SSO";
190191 // Block L6 — "Sign in with GitHub" (separate row keyed id='github').
191192 const githubCfg = await getGithubOauthConfig();
192193 const githubEnabled =
@@ -632,4 +633,25 @@ auth.post("/api/auth/login", async (c) => {
632633 });
633634});
634635
636/**
637 * Pick a friendly provider name for the "Sign in with X" button when the
638 * admin hasn't set one explicitly. Looks at the configured IdP URLs.
639 * Falls back to undefined so the caller can default to a literal "SSO".
640 */
641function inferSsoProviderName(
642 cfg: { issuer?: string | null; authorizationEndpoint?: string | null } | null | undefined
643): string | undefined {
644 const urls = [cfg?.issuer, cfg?.authorizationEndpoint]
645 .filter((s): s is string => !!s)
646 .join(" ")
647 .toLowerCase();
648 if (!urls) return undefined;
649 if (urls.includes("google")) return "Google";
650 if (urls.includes("okta")) return "Okta";
651 if (urls.includes("microsoftonline") || urls.includes("azure")) return "Microsoft";
652 if (urls.includes("auth0.com")) return "Auth0";
653 if (urls.includes("authentik")) return "Authentik";
654 return undefined;
655}
656
635657export default auth;
Modifiedsrc/routes/sso.tsx+120−7View fileUnifiedSplit
@@ -106,6 +106,91 @@ sso.get("/admin/sso", requireAuth, async (c) => {
106106 )}
107107 {error && <div class="auth-error">{decodeURIComponent(error)}</div>}
108108
109 <div
110 class="panel"
111 style="padding:14px 16px;margin-bottom:14px;display:flex;gap:10px;flex-wrap:wrap;align-items:center"
112 >
113 <span style="font-size:13px;color:var(--text-muted)">
114 Quick fill from a preset:
115 </span>
116 <button
117 type="button"
118 class="btn btn-sm"
119 onclick="window.gluecronSsoPreset('google')"
120 >
121 Google Workspace
122 </button>
123 <button
124 type="button"
125 class="btn btn-sm"
126 onclick="window.gluecronSsoPreset('okta')"
127 >
128 Okta
129 </button>
130 <button
131 type="button"
132 class="btn btn-sm"
133 onclick="window.gluecronSsoPreset('auth0')"
134 >
135 Auth0
136 </button>
137 <button
138 type="button"
139 class="btn btn-sm"
140 onclick="window.gluecronSsoPreset('azure')"
141 >
142 Microsoft Entra
143 </button>
144 </div>
145 <script
146 dangerouslySetInnerHTML={{
147 __html: /* js */ `
148 window.gluecronSsoPreset = function (provider) {
149 var P = {
150 google: {
151 provider_name: 'Google',
152 issuer: 'https://accounts.google.com',
153 authorization_endpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
154 token_endpoint: 'https://oauth2.googleapis.com/token',
155 userinfo_endpoint: 'https://openidconnect.googleapis.com/v1/userinfo',
156 scopes: 'openid email profile',
157 },
158 okta: {
159 provider_name: 'Okta',
160 issuer: 'https://YOUR-TENANT.okta.com',
161 authorization_endpoint: 'https://YOUR-TENANT.okta.com/oauth2/v1/authorize',
162 token_endpoint: 'https://YOUR-TENANT.okta.com/oauth2/v1/token',
163 userinfo_endpoint: 'https://YOUR-TENANT.okta.com/oauth2/v1/userinfo',
164 scopes: 'openid email profile',
165 },
166 auth0: {
167 provider_name: 'Auth0',
168 issuer: 'https://YOUR-TENANT.auth0.com',
169 authorization_endpoint: 'https://YOUR-TENANT.auth0.com/authorize',
170 token_endpoint: 'https://YOUR-TENANT.auth0.com/oauth/token',
171 userinfo_endpoint: 'https://YOUR-TENANT.auth0.com/userinfo',
172 scopes: 'openid email profile',
173 },
174 azure: {
175 provider_name: 'Microsoft',
176 issuer: 'https://login.microsoftonline.com/YOUR-TENANT-ID/v2.0',
177 authorization_endpoint: 'https://login.microsoftonline.com/YOUR-TENANT-ID/oauth2/v2.0/authorize',
178 token_endpoint: 'https://login.microsoftonline.com/YOUR-TENANT-ID/oauth2/v2.0/token',
179 userinfo_endpoint: 'https://graph.microsoft.com/oidc/userinfo',
180 scopes: 'openid email profile',
181 },
182 };
183 var p = P[provider];
184 if (!p) return;
185 for (var k in p) {
186 var el = document.getElementById(k);
187 if (el) el.value = p[k];
188 }
189 };
190 `,
191 }}
192 />
193
109194 <form
110195 method="post"
111196 action="/admin/sso"
@@ -391,16 +476,44 @@ sso.get("/login/sso/callback", async (c) => {
391476 return c.redirect("/");
392477 } catch (err) {
393478 console.error("[sso] callback error:", err);
394 return c.redirect(
395 `/login?error=${encodeURIComponent(
396 err instanceof Error
397 ? `SSO sign-in failed: ${err.message}`
398 : "SSO sign-in failed"
399 )}`
400 );
479 const friendly = friendlySsoError(err);
480 return c.redirect(`/login?error=${encodeURIComponent(friendly)}`);
401481 }
402482});
403483
484/**
485 * Map the raw OIDC failure shape to a one-sentence message safe to render
486 * inside an HTML <div>. We never surface the IdP's response body — those
487 * have been Google 404 HTML pages, Azure JSON blobs full of object IDs,
488 * etc. The raw `err.message` is logged via console.error above so admins
489 * can still diagnose from server logs.
490 */
491function friendlySsoError(err: unknown): string {
492 const raw = err instanceof Error ? err.message : String(err ?? "");
493 if (raw.includes("token_endpoint")) {
494 if (/\b40[01]\b/.test(raw)) {
495 return "SSO sign-in failed: the identity provider rejected our token request (HTTP 4xx). Check the Token endpoint URL and Client Secret at /admin/sso.";
496 }
497 if (/\b404\b/.test(raw)) {
498 return "SSO sign-in failed: the Token endpoint URL returned 404. Verify the URL at /admin/sso — for Google it's https://oauth2.googleapis.com/token.";
499 }
500 if (/\b5\d\d\b/.test(raw)) {
501 return "SSO sign-in failed: the identity provider returned a server error. Try again, or check the IdP's status page.";
502 }
503 return "SSO sign-in failed at the token exchange step. Check /admin/sso configuration.";
504 }
505 if (raw.includes("userinfo_endpoint")) {
506 return "SSO sign-in failed while fetching profile info. Verify the Userinfo endpoint URL at /admin/sso.";
507 }
508 if (raw.includes("state cookie") || raw.includes("nonce")) {
509 return "SSO sign-in expired before you returned to the site. Please try again.";
510 }
511 if (raw.includes("email") && raw.includes("not allowed")) {
512 return "SSO sign-in failed: your email domain is not on the allowlist for this site.";
513 }
514 return "SSO sign-in failed. Check /admin/sso configuration or try again.";
515}
516
404517// ----------------------------------------------------------------------------
405518// User: unlink SSO
406519// ----------------------------------------------------------------------------
407520