From 632e36dca52a5aa4619f44dfb7b72a8c221818e9 Mon Sep 17 00:00:00 2001 From: Bryan Brooks Date: Wed, 18 Feb 2026 21:37:34 -0600 Subject: [PATCH] fix: OIDC login form uses fetch instead of form POST for iOS Safari MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iOS Safari doesn't reliably follow HTTP 302 redirects after HTML form POST submissions. The OIDC login flow now uses fetch() + JS redirect via window.location.href, which works on all platforms. Also includes the /auth/callback → /oauth/callback forwarding fix for OIDC clients that was deployed earlier. --- gateway/src/routes/auth.ts | 10 ++++++- gateway/src/routes/oidc/authorize.ts | 45 ++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/gateway/src/routes/auth.ts b/gateway/src/routes/auth.ts index 87bb8bb..43e9784 100644 --- a/gateway/src/routes/auth.ts +++ b/gateway/src/routes/auth.ts @@ -136,9 +136,17 @@ export function createAuthRoutes( throw httpError.badRequest('invalid_state', 'OAuth state not found or expired'); } + // Check if this state belongs to an OIDC client — if so, forward to the + // OIDC callback handler at /oauth/callback (don't delete state, OIDC handler needs it) + const appCheck = db.getApp(savedState.app_id); + if (appCheck && (appCheck as any).client_type === 'oidc') { + const qs = req.url.includes('?') ? '?' + req.url.split('?')[1] : ''; + return res.redirect(`/oauth/callback${qs}`); + } + db.deleteOAuthState(state); - const app = db.getApp(savedState.app_id); + const app = appCheck; if (!app) { throw httpError.notFound('app_not_found', 'Application configuration not found'); } diff --git a/gateway/src/routes/oidc/authorize.ts b/gateway/src/routes/oidc/authorize.ts index e8be497..9fb00f9 100644 --- a/gateway/src/routes/oidc/authorize.ts +++ b/gateway/src/routes/oidc/authorize.ts @@ -297,16 +297,43 @@ export function createAuthorizeRouter( const submitBtn = document.getElementById('submitBtn'); const errorDiv = document.getElementById('error'); - form.addEventListener('submit', function(e) { + form.addEventListener('submit', async function(e) { + e.preventDefault(); const handle = handleInput.value.trim(); if (!handle) { - e.preventDefault(); errorDiv.textContent = 'Please enter your handle'; errorDiv.style.display = 'block'; return; } submitBtn.disabled = true; submitBtn.textContent = 'Redirecting...'; + errorDiv.style.display = 'none'; + + try { + const res = await fetch('/oauth/authorize/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, + body: JSON.stringify({ + auth_code: form.querySelector('[name="auth_code"]').value, + state: form.querySelector('[name="state"]').value, + handle: handle + }) + }); + const data = await res.json(); + if (data.redirect_url) { + window.location.href = data.redirect_url; + } else { + errorDiv.textContent = data.error || 'Login failed'; + errorDiv.style.display = 'block'; + submitBtn.disabled = false; + submitBtn.textContent = 'Continue'; + } + } catch (err) { + errorDiv.textContent = 'Network error. Please try again.'; + errorDiv.style.display = 'block'; + submitBtn.disabled = false; + submitBtn.textContent = 'Continue'; + } }); @@ -364,11 +391,19 @@ export function createAuthorizeRouter( created_at: Math.floor(Date.now() / 1000), }); - // Redirect to AT Protocol OAuth - res.redirect(atprotoAuth.url); + // Return redirect URL — JSON for fetch clients, 302 for form fallback + if (req.accepts('json')) { + res.json({ redirect_url: atprotoAuth.url }); + } else { + res.redirect(atprotoAuth.url); + } } catch (error) { console.error('[OIDC Login] Error:', error); - res.status(500).send('Authentication failed: ' + (error instanceof Error ? error.message : 'Unknown error')); + if (req.accepts('json')) { + res.status(500).json({ error: 'Authentication failed: ' + (error instanceof Error ? error.message : 'Unknown error') }); + } else { + res.status(500).send('Authentication failed: ' + (error instanceof Error ? error.message : 'Unknown error')); + } } }); -- knotmirror 0.0.1