danny-avila/LibreChat · error · Error
Email domain not allowed
Error message
Email domain not allowed
What it means
The first of three `Email domain not allowed` throws in the OpenID strategy. It runs the domain allowlist check against the BASE application config (`baseConfig`, resolved with `{ baseOnly: true }`) before any user record is looked up. `isEmailDomainAllowed` returns `true` (allow) whenever `allowedDomains` is empty/null/missing; it only denies when a non-empty list is configured and the email's domain is not in it, or when there is no email at all. This throw is special-cased by `createOpenIDCallback` into `done(null, false, { message })`, so the user sees a 302 redirect to `/login?redirect=false&error=auth_failed`, not a 500.
Source
Thrown at api/strategies/openidStrategy.js:587
const claims = tokenset.claims ? tokenset.claims() : tokenset;
const userinfo = {
...claims,
};
if (tokenset.access_token) {
const providerUserinfo = await getUserInfo(openidConfig, tokenset.access_token, claims.sub);
Object.assign(userinfo, providerUserinfo);
}
const email = getOpenIdEmail(userinfo);
const openidIssuer = getOpenIdIssuer(claims, openidConfig);
const baseConfig = await getAppConfig({ baseOnly: true });
if (!isEmailDomainAllowed(email, baseConfig?.registration?.allowedDomains)) {
logger.error(
`[OpenID Strategy] Authentication blocked - email domain not allowed [Identifier: ${email}]`,
);
throw new Error('Email domain not allowed');
}
const result = await findOpenIDUser({
findUser,
email: email,
openidId: claims.sub || userinfo.sub,
openidIssuer,
idOnTheSource: claims.oid || userinfo.oid,
strategyName: 'openidStrategy',
});
let user = result.user;
const error = result.error;
if (error) {
throw new Error(ErrorTypes.AUTH_FAILED);
}
const appConfig = user?.tenantId ? await resolveAppConfigForUser(getAppConfig, user) : baseConfig;View on GitHub (pinned to 5ff282f900)
Solutions
- Check the effective `registration.allowedDomains` in the app config (filter `AppConfig` in the DB or the config file) and confirm the user's email domain is listed.
- If the IdP does not send a standard `email` claim, set `OPENID_EMAIL_CLAIM` to the claim key that carries the address (e.g. `upn`, `preferred_username`, or a custom claim).
- If the policy is intended to be per-tenant/role only, leave base `allowedDomains` empty so this pre-check passes and the tenant-aware check at line 611 enforces the real policy.
- Verify the domain string exactly — matching is case-insensitive but exact; subdomains are NOT matched (`a.company.com` will not satisfy `company.com`).
Example fix
// before
if (!isEmailDomainAllowed(email, baseConfig?.registration?.allowedDomains)) {
logger.error(`[OpenID Strategy] Authentication blocked - email domain not allowed [Identifier: ${email}]`);
throw new Error('Email domain not allowed');
}
// after — include the domain list in the log so operators can self-diagnose
const allowed = baseConfig?.registration?.allowedDomains;
if (!isEmailDomainAllowed(email, allowed)) {
logger.error(`[OpenID Strategy] Blocked ${email}: domain not in [${(allowed || []).join(', ') || 'allow-all'}]`);
throw new Error('Email domain not allowed');
} Defensive patterns
Strategy: validation
Validate before calling
// Resolve and inspect the effective base allowlist before going through SSO
const baseConfig = await getAppConfig({ baseOnly: true });
const allowed = baseConfig?.registration?.allowedDomains;
if (Array.isArray(allowed) && allowed.length && !allowed.includes(userEmail.split('@')[1]?.toLowerCase())) {
throw new Error(`Email domain blocked at base-config level; allowed: ${allowed.join(', ')}`);
} Type guard
function isAllowedDomainConfig(v: unknown): v is string[] {
return Array.isArray(v) && v.every((d) => typeof d === 'string' && d.length > 0);
} Try / catch
// In the OpenID callback authenticator, map 'Email domain not allowed' to a user-facing message
try {
await processOpenIDAuth(tokenset, existingUsersOnly);
} catch (err) {
if (err.message === 'Email domain not allowed') return done(null, false, { message: err.message });
done(err);
} Prevention
- Document that an empty/missing allowedDomains means allow-all, while a non-empty list is an exact (case-insensitive) match with no subdomain/wildcard support.
- Set OPENID_EMAIL_CLAIM to match the IdP's email-bearing claim so the domain check has a value to evaluate.
- Keep the base allowlist permissive and enforce stricter rules at the tenant/role level (line 611) to avoid locking out all SSO users.
When it happens
Trigger: An OpenID/OIDC provider authenticates a user whose email domain is not in the configured `registration.allowedDomains` list. Concretely: `OPENID_*` env is set, the IdP callback succeeds, `getOpenIdEmail` returns an address like `user@contractor.external`, and `allowedDomains` contains `['company.com']`. Also triggered if `getOpenIdEmail` returns `undefined` (no email/upn/preferred_username claim) WHILE a domain list is configured.
Common situations: Operator added `allowedDomains` to lock the instance to corporate accounts but a personal/BYOD email slipped through; the IdP sends `preferred_username` instead of `email` and the configured `OPENID_EMAIL_CLAIM` doesn't match; multi-tenant setups where the base config is narrower than a tenant's override (the tenant-aware check at line 611 is the one that matters then, but this base check still fires first).
Related errors
- You must have ${rolesList} role to log in.
- [MCP][${serverName}][${toolName}] upstream authentication fa
- User must be authenticated via OpenID to perform OBO token e
- auth_failed
- User does not exist
AI-assisted analysis of danny-avila/LibreChat@5ff282f900 (2026-08-12).
Data as JSON: /api/errors/8f7ad49dc70481a3.
Report an issue: GitHub.