fix(encryptid): show success page instead of auto-OIDC redirect

The auto-redirect through OIDC authorize fails because the client app
(Postiz) didn't initiate the OAuth flow and has no matching state.
Instead, show a branded success page with a link to the app. The user
signs in with their passkey when they visit the app normally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-09 17:31:32 -07:00
parent d861c0ad99
commit 8723aae3f6
1 changed files with 23 additions and 56 deletions

View File

@ -4165,11 +4165,16 @@ app.get('/api/invites/identity/:token/info', async (c) => {
if (Date.now() > invite.expiresAt) { if (Date.now() > invite.expiresAt) {
return c.json({ error: 'Invite expired' }, 410); return c.json({ error: 'Invite expired' }, 410);
} }
// Look up OIDC client name if this is a client invite // Look up OIDC client info if this is a client invite
let clientName: string | null = null; let clientName: string | null = null;
let clientAppUrl: string | null = null;
if (invite.clientId) { if (invite.clientId) {
const client = await getOidcClient(invite.clientId); const client = await getOidcClient(invite.clientId);
clientName = client?.name || null; clientName = client?.name || null;
// Derive app URL from first redirect URI (strip the callback path)
if (client?.redirectUris?.[0]) {
try { clientAppUrl = new URL(client.redirectUris[0]).origin; } catch {}
}
} }
return c.json({ return c.json({
invitedBy: invite.invitedByUsername, invitedBy: invite.invitedByUsername,
@ -4178,6 +4183,7 @@ app.get('/api/invites/identity/:token/info', async (c) => {
spaceSlug: invite.spaceSlug, spaceSlug: invite.spaceSlug,
clientId: invite.clientId, clientId: invite.clientId,
clientName, clientName,
clientAppUrl,
}); });
}); });
@ -4774,24 +4780,23 @@ function oidcAcceptPage(token: string): string {
return; return;
} }
// If this is an OIDC client invite, redirect through the OIDC authorize flow // Show success with link to the app
if (inviteData.clientId) {
showStatus('Redirecting to ' + (inviteData.clientName || 'the app') + '...');
// Store the session token so the OIDC authorize page can use it
localStorage.setItem('eid_token', sessionToken);
// Start the OIDC authorize flow — the authorize page will auto-login
window.location.href = '/oidc/authorize?client_id=' + encodeURIComponent(inviteData.clientId) +
'&response_type=code&scope=openid+profile+email&redirect_uri=' + encodeURIComponent('auto') +
'&state=invite_accept';
return;
}
// Non-OIDC invite — show success
document.getElementById('authSection').style.display = 'none'; document.getElementById('authSection').style.display = 'none';
statusEl.style.display = 'none'; statusEl.style.display = 'none';
successEl.innerHTML = '<strong>Welcome!</strong><br>Your invitation has been accepted.' +
(claimData.spaceSlug ? '<br>You\\'ve been added to <strong>' + esc(claimData.spaceSlug) + '</strong>.' : '') + const appName = inviteData.clientName || 'rSpace';
'<br><br><a href="https://rspace.online" style="color: #7c3aed;">Go to rSpace \\u2192</a>'; const appUrl = inviteData.clientAppUrl || 'https://rspace.online';
if (inviteData.clientId) {
successEl.innerHTML = '<strong>You\\u2019re in!</strong><br>' +
'Your account is set up and you\\u2019ve been granted access to <strong>' + esc(appName) + '</strong>.' +
'<br><br><a href="' + esc(appUrl) + '" style="display:inline-block;padding:0.7rem 1.5rem;background:linear-gradient(90deg,#00d4ff,#7c3aed);color:#fff;text-decoration:none;border-radius:0.5rem;font-weight:600;">Go to ' + esc(appName) + ' \\u2192</a>' +
'<p style="color:#94a3b8;font-size:0.8rem;margin-top:1rem;">You\\u2019ll sign in with your passkey when you get there.</p>';
} else {
successEl.innerHTML = '<strong>Welcome!</strong><br>Your invitation has been accepted.' +
(claimData.spaceSlug ? '<br>You\\u2019ve been added to <strong>' + esc(claimData.spaceSlug) + '</strong>.' : '') +
'<br><br><a href="https://rspace.online" style="color: #7c3aed;">Go to rSpace \\u2192</a>';
}
successEl.style.display = 'block'; successEl.style.display = 'block';
} }
@ -4946,7 +4951,7 @@ app.get('/.well-known/openid-configuration', (c) => {
// Authorization endpoint // Authorization endpoint
app.get('/oidc/authorize', async (c) => { app.get('/oidc/authorize', async (c) => {
const clientId = c.req.query('client_id'); const clientId = c.req.query('client_id');
let redirectUri = c.req.query('redirect_uri'); const redirectUri = c.req.query('redirect_uri');
const responseType = c.req.query('response_type'); const responseType = c.req.query('response_type');
const scope = c.req.query('scope') || 'openid profile email'; const scope = c.req.query('scope') || 'openid profile email';
const state = c.req.query('state') || ''; const state = c.req.query('state') || '';
@ -4963,11 +4968,6 @@ app.get('/oidc/authorize', async (c) => {
return c.text('Unknown client_id', 400); return c.text('Unknown client_id', 400);
} }
// "auto" redirect_uri: use the client's first registered redirect URI (from invite accept flow)
if (redirectUri === 'auto') {
redirectUri = client.redirectUris[0];
}
if (!client.redirectUris.includes(redirectUri)) { if (!client.redirectUris.includes(redirectUri)) {
return c.text('Invalid redirect_uri', 400); return c.text('Invalid redirect_uri', 400);
} }
@ -5353,39 +5353,6 @@ function oidcAuthorizePage(appName: string, clientId: string, redirectUri: strin
} }
} }
// Auto-authorize if coming from invite accept flow with a stored session token
(async () => {
if (STATE !== 'invite_accept') return;
const storedToken = localStorage.getItem('eid_token');
if (!storedToken) return;
localStorage.removeItem('eid_token');
loginBtn.disabled = true;
showStatus('Authorizing...');
try {
const authorizeRes = await fetch('/oidc/authorize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientId: CLIENT_ID,
redirectUri: REDIRECT_URI,
scope: SCOPE,
state: STATE,
token: storedToken,
}),
});
const authorizeResult = await authorizeRes.json();
if (authorizeResult.error) {
showError(authorizeResult.message || authorizeResult.error);
loginBtn.disabled = false;
return;
}
showStatus('Redirecting...');
window.location.href = authorizeResult.redirectUrl;
} catch (err) {
showError('Auto-login failed. Please sign in manually.');
loginBtn.disabled = false;
}
})();
</script> </script>
</body> </body>
</html>`; </html>`;