fix(encryptid): persist verified email and fix OIDC re-prompt bug

Normalize emails to lowercase at all setUserEmail() call sites so
case mismatches no longer break the OIDC allowedEmails check. Split
the authorize error into email_required (shows verification form) vs
access_denied (shows error message) so users with a verified email
are never re-prompted unnecessarily.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-12 18:28:07 -07:00
parent 154f1230dc
commit 79448a230a
1 changed files with 14 additions and 9 deletions

View File

@ -581,7 +581,7 @@ app.post('/api/register/complete', async (c) => {
// Set recovery email if provided during registration
if (email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
await setUserEmail(userId, email);
await setUserEmail(userId, email.trim().toLowerCase());
}
// Resolve the RP ID from the caller's origin
@ -1203,7 +1203,7 @@ app.post('/api/account/email/verify', async (c) => {
}
await markRecoveryTokenUsed(tokenKey);
await setUserEmail(claims.sub as string, email);
await setUserEmail(claims.sub as string, email.trim().toLowerCase());
return c.json({ success: true, email });
});
@ -1433,8 +1433,9 @@ app.post('/api/recovery/email/set', async (c) => {
return c.json({ error: 'Valid email required' }, 400);
}
await setUserEmail(payload.sub as string, email);
return c.json({ success: true, email });
const normalizedEmail = email.trim().toLowerCase();
await setUserEmail(payload.sub as string, normalizedEmail);
return c.json({ success: true, email: normalizedEmail });
} catch {
return c.json({ error: 'Unauthorized' }, 401);
}
@ -5441,10 +5442,14 @@ app.post('/oidc/authorize', async (c) => {
if (!user) {
return c.json({ error: 'User not found' }, 404);
}
const userEmail = user.email || user.profile_email;
const userEmail = (user.email || user.profile_email || '').toLowerCase();
if (client.allowedEmails.length > 0) {
if (!userEmail || !client.allowedEmails.includes(userEmail)) {
return c.json({ error: 'access_denied', message: 'You do not have access to this application.' }, 403);
const allowedLower = client.allowedEmails.map((e: string) => e.toLowerCase());
if (!userEmail) {
return c.json({ error: 'email_required', message: 'Email verification is required for this application.' }, 403);
}
if (!allowedLower.includes(userEmail)) {
return c.json({ error: 'access_denied', message: 'Your email is not authorized for this application.' }, 403);
}
}
@ -5906,8 +5911,8 @@ function oidcAuthorizePage(appName: string, clientId: string, redirectUri: strin
const authorizeResult = await authorizeRes.json();
if (authorizeResult.error) {
if (authorizeResult.error === 'access_denied') {
// Show email verification flow instead of dead-end error
if (authorizeResult.error === 'email_required') {
// User has no verified email — show email verification form
loginBtn.style.display = 'none';
statusEl.style.display = 'none';
verifySection.style.display = 'block';