{"record":{"id":"8f7ad49dc70481a3","repo":"danny-avila/LibreChat","slug":"email-domain-not-allowed","errorCode":null,"errorMessage":"Email domain not allowed","messagePattern":"Email domain not allowed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"api/strategies/openidStrategy.js","lineNumber":587,"sourceCode":"  const claims = tokenset.claims ? tokenset.claims() : tokenset;\n  const userinfo = {\n    ...claims,\n  };\n\n  if (tokenset.access_token) {\n    const providerUserinfo = await getUserInfo(openidConfig, tokenset.access_token, claims.sub);\n    Object.assign(userinfo, providerUserinfo);\n  }\n\n  const email = getOpenIdEmail(userinfo);\n  const openidIssuer = getOpenIdIssuer(claims, openidConfig);\n\n  const baseConfig = await getAppConfig({ baseOnly: true });\n  if (!isEmailDomainAllowed(email, baseConfig?.registration?.allowedDomains)) {\n    logger.error(\n      `[OpenID Strategy] Authentication blocked - email domain not allowed [Identifier: ${email}]`,\n    );\n    throw new Error('Email domain not allowed');\n  }\n\n  const result = await findOpenIDUser({\n    findUser,\n    email: email,\n    openidId: claims.sub || userinfo.sub,\n    openidIssuer,\n    idOnTheSource: claims.oid || userinfo.oid,\n    strategyName: 'openidStrategy',\n  });\n  let user = result.user;\n  const error = result.error;\n\n  if (error) {\n    throw new Error(ErrorTypes.AUTH_FAILED);\n  }\n\n  const appConfig = user?.tenantId ? await resolveAppConfigForUser(getAppConfig, user) : baseConfig;","sourceCodeStart":569,"sourceCodeEnd":605,"githubUrl":"https://github.com/danny-avila/LibreChat/blob/5ff282f9006c436e561de1afd39a481bea1ef0d8/api/strategies/openidStrategy.js#L569-L605","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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`)."],"exampleFix":"// before\nif (!isEmailDomainAllowed(email, baseConfig?.registration?.allowedDomains)) {\n  logger.error(`[OpenID Strategy] Authentication blocked - email domain not allowed [Identifier: ${email}]`);\n  throw new Error('Email domain not allowed');\n}\n// after — include the domain list in the log so operators can self-diagnose\nconst allowed = baseConfig?.registration?.allowedDomains;\nif (!isEmailDomainAllowed(email, allowed)) {\n  logger.error(`[OpenID Strategy] Blocked ${email}: domain not in [${(allowed || []).join(', ') || 'allow-all'}]`);\n  throw new Error('Email domain not allowed');\n}","handlingStrategy":"validation","validationCode":"// Resolve and inspect the effective base allowlist before going through SSO\nconst baseConfig = await getAppConfig({ baseOnly: true });\nconst allowed = baseConfig?.registration?.allowedDomains;\nif (Array.isArray(allowed) && allowed.length && !allowed.includes(userEmail.split('@')[1]?.toLowerCase())) {\n  throw new Error(`Email domain blocked at base-config level; allowed: ${allowed.join(', ')}`);\n}","typeGuard":"function isAllowedDomainConfig(v: unknown): v is string[] {\n  return Array.isArray(v) && v.every((d) => typeof d === 'string' && d.length > 0);\n}","tryCatchPattern":"// In the OpenID callback authenticator, map 'Email domain not allowed' to a user-facing message\ntry {\n  await processOpenIDAuth(tokenset, existingUsersOnly);\n} catch (err) {\n  if (err.message === 'Email domain not allowed') return done(null, false, { message: err.message });\n  done(err);\n}","preventionTips":["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."],"tags":["authentication","openid","authorization","domain-allowlist","config"],"backgroundTag":null,"analyzedSha":"5ff282f9006c436e561de1afd39a481bea1ef0d8","analyzedAt":"2026-08-12T21:38:08.145Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}