Email Domain Allowlist
Restrict which email domains can own an account in your Volcano project, and choose whether the restriction covers signup only or sign-in as well.
Restrict which email domains can own an account in your project, and decide whether accounts that predate the restriction may keep signing in.
SUPERAGENT plan required.
Settings: allowed_email_domains, allowed_email_domains_mode
Types: Array of strings, string
Defaults: [] (any domain), "signup"
Use it for internal tools, customer-specific deployments, or any app where only people with a company address should be able to join.
Enable
Set both settings from the Dashboard, the API, or volcano-config.yaml. An empty
list accepts any domain.
The allowlist is a SUPERAGENT feature, to set and to enforce. A HOBBY project that asks
for one gets 403 the email domain allowlist is only available on the PRO plan,
and a project that downgrades keeps its domains but stops enforcing them — see
Plans.
Dashboard: in Auth Settings → General, under Email Domain Allowlist,
enter the domains one per line and choose where the list applies: New signups
only (signup), Signups and sign-ins (signup_and_signin), or Not
enforced (disabled). Picking Signups and sign-ins asks you to confirm
first, because saving it signs out every account outside the list.
API:
curl -X PUT https://api.volcano.dev/projects/PROJECT_ID/auth/config \
-H "Authorization: Bearer $PLATFORM_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"allowed_email_domains": ["domain1.com", "domain2.com"],
"allowed_email_domains_mode": "signup_and_signin"
}'volcano-config.yaml:
auth:
signup:
allowed_email_domains:
- domain1.com
- domain2.com
allowed_email_domains_mode: signup_and_signinModes
allowed_email_domains_mode decides how far the list reaches. An empty list
restricts nothing whatever the mode is.
| Mode | Signup | Sign-in | Existing sessions |
|---|---|---|---|
signup (default) | Blocked outside the list | Allowed | Kept |
signup_and_signin | Blocked outside the list | Blocked outside the list | Revoked for the sign-ins it blocks |
disabled | Allowed | Allowed | Kept |
Pick signup to stop new accounts while letting everyone who already signed up
carry on. Pick signup_and_signin to cut off accounts outside the list too —
this is the setting that turns the allowlist into an access control rather than
a registration filter. Pick disabled to park the list without deleting it, so
you can switch the same domains back on later.
What It Covers
Every path that would give an address an account is checked under signup and
signup_and_signin:
| Path | Behavior with a disallowed domain |
|---|---|
POST /auth/signup | 403, no user created |
| OAuth/SSO callback creating a new user | 403, no user created |
| OAuth/SSO callback claiming an existing unconfirmed account by email | 403 |
POST /auth/oauth/{provider}/link | 403 when the provider address would become a new address on the account |
POST /auth/user/convert-anonymous | 403, the user stays anonymous |
POST /auth/user/change-email | 403, the address is unchanged |
POST /auth/user/confirm-email-change | 403 if the allowlist narrowed after the change was requested |
POST /auth/user/methods/{methodId}/promote | 403, the account keeps its current primary |
| Anonymous signup (no email) | Allowed — nothing to check |
Promotion is on that list because it re-derives the account's canonical email from the promoted identity. An account can hold an identity that predates the restriction, and promoting it would move the account onto an address signup would refuse.
Unlinking the account's primary provider (DELETE /auth/oauth/{provider}/unlink)
also moves its canonical email to another identity. While the list is enforced,
an identity whose address is on the list goes first, so the account keeps an
allowed canonical email whenever it has one.
Under signup_and_signin, every path that hands out a session is checked as
well:
| Path | Behavior with a disallowed domain |
|---|---|
POST /auth/signin | 403, no session issued |
| OAuth/SSO callback for an already-linked account | 403 |
POST /auth/oauth/exchange | 403 |
POST /auth/device/token | 403 access_denied |
POST /auth/refresh | 403, the session cannot be extended |
POST /auth/platform/exchange | 403, no CLI platform token is minted |
| Anonymous sign-in | Allowed — anonymous accounts carry an internal address |
Linking a provider whose address the account already owns keeps working, so a user grandfathered in before the restriction can still add a provider.
A session needs two addresses on the list:
- the account's canonical email — the address of its primary identity, the
one
GET /auth/userreturns - the email of the identity it signs in with — the password identity for
POST /auth/signin, the provider's identity for an OAuth/SSO sign-in
Both are checked when the session is issued and every time it is refreshed. A session that does not sign in through one identity, such as a CLI device login, needs every identity on the account to be on the list. An identity with no email is outside the list. Session revocation applies the same rule, so the two never disagree.
The error message is email domain is not allowed for this project.
Matching Rules
Matching is an exact, case-insensitive comparison of the part after the last
@:
| Allowlist | Address | Result |
|---|---|---|
domain1.com | user@domain1.com | Allowed |
domain1.com | User@DOMAIN1.COM | Allowed |
domain1.com | user+tag@domain1.com | Allowed |
domain1.com | user@mail.domain1.com | Rejected |
domain1.com | user@domain1.com.evil.com | Rejected |
domain1.com | user@domain1.com. | Allowed |
xn--bcher-kva.de | user@bücher.de | Allowed |
Subdomains are not implied. List mail.domain1.com explicitly if you want to
accept it. Domains are compared after IDNA mapping, so list an
internationalized domain in its ASCII (xn--) form; it matches addresses
written in either form.
Entry Format
Entries are normalized before they are stored, so the forms people paste all converge:
| You write | Stored as |
|---|---|
@Domain1.com | domain1.com |
DOMAIN1.com | domain1.com |
domain1.com. | domain1.com |
Duplicates are dropped and the configured order is kept. An entry must be a bare
domain with a top-level domain of at least two letters, so localhost,
user@domain1.com, and domain 1.com are rejected with a 400 when you save.
The list holds at most 100 domains.
Existing Users
Under signup and disabled, adding a restriction never locks anyone out.
Users whose domain is no longer on the list keep signing in normally; the
allowlist only governs new accounts and email changes.
Under signup_and_signin, saving the policy signs out every session the list
refuses right away — those sessions are deleted, so their access tokens stop
working on the next request rather than at expiry. The accounts themselves stay,
so putting the domain back restores access; delete the users if you need them
gone for good.
Nobody is signed out while the list is parked on a HOBBY project: sign-in admits the same accounts, so ending their sessions would only interrupt users the project can no longer keep out.
Saving and signing out are one operation: if the sign-out fails, the save fails with it and the previous policy stays in force, so a project never advertises a lock it has not applied. A sign-in that was already in flight when you saved can still land a session — it read the old policy — but it cannot be refreshed or exchanged for a CLI token, so it ends when its access token expires.
One exception to "right away": a Realtime connection that is already open is not closed, because its session was checked when it connected. It stops working as soon as it reconnects. Close those connections from your app if you need the lock to reach live subscribers immediately.
Anonymous accounts are never affected: whether they may exist at all is governed
by anonymous users instead. The exemption follows the
account being anonymous, not the address it carries — anonymous.volcano.internal
is reserved for accounts the platform mints, and a signup asking for an address
inside it is refused with the same 403 whatever the mode, including disabled.
Builder debug users (*@volcano.local, created by the project owner from the
dashboard) are provisioned directly and never pass through the signup check.
Under signup_and_signin their sessions are checked like any other account's,
so a debug run acting as one is refused. To debug as one, add volcano.local
to allowed_email_domains.
volcano.local is reserved like anonymous.volcano.internal: a signup, email
change or OAuth account at that domain is refused with 403 in every project and
mode. Listing it admits every existing account at the domain, which means the
debug users plus any account that took a volcano.local address before Volcano
reserved it. Check GET /projects/{id}/auth/users for such accounts before you
list it.
Remove the Restriction
Two ways, depending on whether you want to keep the list:
# Park the list — the domains stay, nothing is enforced.
curl -X PUT https://api.volcano.dev/projects/PROJECT_ID/auth/config \
-H "Authorization: Bearer $PLATFORM_TOKEN" \
-H "Content-Type: application/json" \
-d '{"allowed_email_domains_mode": "disabled"}'
# Clear the list — the domains are gone.
curl -X PUT https://api.volcano.dev/projects/PROJECT_ID/auth/config \
-H "Authorization: Bearer $PLATFORM_TOKEN" \
-H "Content-Type: application/json" \
-d '{"allowed_email_domains": []}'In volcano-config.yaml, allowed_email_domains: [] clears the list; omitting
a key leaves the stored value untouched. From the Dashboard, Not enforced
parks the list and emptying the field clears it.
Both work on any plan — see below.
Plans
The allowlist is a SUPERAGENT feature, to configure and to enforce.
A project that drops to HOBBY keeps its domains — nothing is deleted, and
GET /auth/config still returns them — but they stop deciding anything: signups
and sign-ins from any domain go through again, and the sessions the policy would
have ended are left alone. Upgrading to SUPERAGENT puts the stored list straight back
to work, without anyone re-saving it. Plan changes reach the API within a few
seconds.
Downgrading reopens signups you closed. If the list is the only thing
keeping strangers out of your app, disable signup
(enable_signup: false) before you downgrade.
What a HOBBY project can do to the list it configured while on SUPERAGENT:
| Change | HOBBY |
|---|---|
Save the policy it already has (a volcano-config.yaml apply, for example) | Allowed — nothing changes |
Park the list (disabled), leaving the domains as they are | Allowed |
Clear the list ([]) | Allowed |
| Add or replace a domain, parked or not | 403, nothing is saved |
Switch to signup_and_signin, or turn a parked list back on | 403 |
The rule behind the table: a HOBBY project may move toward no restriction, never away from it. That keeps a downgrade from stranding a project behind a list it cannot clear, while still requiring SUPERAGENT to configure one. A parked list counts as the allowlist for this — nothing but a mode change and an upgrade stand between it and enforcement — so the only edit HOBBY can make to the domains is dropping them.
Troubleshooting
Signup returns 403 email domain is not allowed for this project
The address is outside the list. Add its domain, or clear the list.
Sign-in returns 403 email domain is not allowed for this project
The project is in signup_and_signin and either the account's canonical email
or the email of the identity you signed in with is not listed. Check both: the
canonical email is the one GET /auth/user returns, and the identities are
listed by GET /auth/user/identities. Add the domain, or switch the mode back
to signup to let existing accounts in.
Signup returns 403 for an @anonymous.volcano.internal or @volcano.local address
Those domains are reserved for the anonymous and Builder debug accounts the
platform creates. No project can accept a signup at either, whatever the
allowlist says.
Users were signed out after a config change
Expected under signup_and_signin: saving the policy revokes every session the
list refuses.
A subdomain is rejected even though the parent domain is listed Expected — add the subdomain as its own entry.
Saving returns 400 invalid allowed_email_domains entry
An entry is not a bare domain. Use domain1.com, not user@domain1.com,
https://domain1.com, or localhost.
Saving returns 400 invalid allowed_email_domains_mode
The mode must be disabled, signup, or signup_and_signin.
Saving returns 403 the email domain allowlist is only available on the PRO plan
The project is on HOBBY. Upgrade to SUPERAGENT to set or widen the list. A project that
was on SUPERAGENT keeps the list it saved, unenforced, and can still remove it.
An outside domain signed up even though the list is set Check the plan. On anything but SUPERAGENT the list is parked: it stays in the config and stops being enforced. Upgrade to put it back in force.
Related
- Provider Separation — which providers can create users
- Configuring Auth Methods — enabling and disabling signup
- CORS — which sites can call your auth endpoints