feat(oidc): add oidc method in OXICLOUD_AUTH_METHODS

permit an admin to specify `oidc` only as the only method to login/register
note that if OIDC is enabled, the engine always append oidc in OXICLOUD_AUTH_METHODS
we could move to an explicit declaration in a major release
This commit is contained in:
Edouard Vanbelle
2026-08-03 00:11:49 +02:00
parent 02db85c040
commit b91f2fab2b
6 changed files with 144 additions and 60 deletions
+6 -4
View File
@@ -46,18 +46,20 @@ The same dispatch applies to `POST /api/auth/magic-link/send` — its `email` fi
## Deployment auth policy
Two env vars control the self-service auth surface, orthogonal to OIDC:
Two env vars control the auth surface. OIDC is a first-class allowlist token, no longer orthogonal:
- `OXICLOUD_AUTH_METHODS` — allowlist of enabled methods (`password`, `magic_link`, or both). Default: both. Removing one produces distinct error_type codes so the SPA can render specific UX:
- `OXICLOUD_AUTH_METHODS` — allowlist of enabled methods (`password`, `magic_link`, `oidc`, or any comma-separated combination). Default (unset): `password,magic_link`. Removing a token produces distinct error_type codes so the SPA can render specific UX:
- Removing `password` → `POST /api/auth/login` → 403 `PasswordLoginDisabled`; password-based `register` → 403 `PasswordRegistrationDisabled`.
- Removing `magic_link` → `magic-link/send` → 403 `MagicLinkLoginDisabled`; login-purpose token redemption refuses.
- **Startup gate:** magic-link-only + no SMTP wired → server refuses to start (main.rs panics).
- Setting to just `oidc` → SSO-only posture, local login surface disabled.
- **Fail-fast on boot:** unknown token, empty allowlist, or `oidc` listed without `OXICLOUD_OIDC_ENABLED=true` all panic startup.
- **Startup gate:** `magic_link` as the only working method (no `password`, no `oidc`) with no SMTP wired → server refuses to start.
- `OXICLOUD_AUTH_POLICIES` — additive policy switches. Today: `permit_magic_link_for_password_users`. Future variants (`Require...`, `Deny...`) reuse the same vector-shaped env var — no per-policy env-var proliferation.
- `OXICLOUD_REQUIRE_VERIFIED_EMAIL` — when true, `POST /api/auth/login` returns 403 `EmailNotVerified` for accounts with `email_verified_at IS NULL`. Checked AFTER password validation (anti-enum — an attacker without the password can't probe verification state). **Admin accounts are exempt** from this gate to prevent a config flip from locking pre-existing admins out of their own instance.
**Verification piggyback.** When the `EmailNotVerified` branch fires (password OK + email unverified), the login handler auto-sends a verification magic-link to the account via a distinct service method that bypasses the `has_password` eligibility gate — the password itself just proved identity, so mailbox-only trust isn't being extended beyond what the password already established. Response is 403 `EmailNotVerified` with "check your inbox"; re-submitting the same login re-triggers the send. This is why there is no unauthenticated "resend verification" endpoint — one would leak `has_password` state to unauthenticated callers.
**OIDC-master rule.** When `OXICLOUD_OIDC_ENABLED=true`, magic-link login is hard-off regardless of `OXICLOUD_AUTH_METHODS`. Magic-link would bypass any 2FA / step-up the IdP enforces.
**OIDC-master rule.** When OIDC is enabled (either explicitly in `OXICLOUD_AUTH_METHODS` or via `OXICLOUD_OIDC_ENABLED=true`), magic-link login is hard-off regardless of what the allowlist says. Magic-link would bypass any 2FA / step-up the IdP enforces.
## Login paths
+16 -5
View File
@@ -38,21 +38,32 @@ OxiCloud ships with JWT-based authentication and Argon2id password hashing for l
## Configuring which methods are offered
Two environment variables control the self-service surface (OIDC is orthogonal — see `OXICLOUD_OIDC_ENABLED`).
Two environment variables control the auth surface. OIDC is a first-class allowlist token alongside `password` and `magic_link`.
### `OXICLOUD_AUTH_METHODS`
Comma-separated allowlist of `password` and/or `magic_link`. Default `password,magic_link`.
Comma-separated allowlist of `password`, `magic_link`, and/or `oidc`. Default (when unset): `password,magic_link`.
| Configuration | Effect |
| --- | --- |
| Unset or `password,magic_link` | Both methods allowed (default) |
| Unset | Password + magic-link (OIDC gated separately by `OXICLOUD_OIDC_ENABLED`) |
| `password,magic_link` | Same as unset — both self-service methods |
| `password` | Password login OK. Magic-link send / redeem → 403 `MagicLinkLoginDisabled` |
| `magic_link` | Password login → 403 `PasswordLoginDisabled`. Password-based `register` → 403 `PasswordRegistrationDisabled`. Email-only signup still works |
| `oidc` | **SSO-only** posture. Requires `OXICLOUD_OIDC_ENABLED=true` + a full OIDC config bucket; local password + magic-link both disabled |
| `password,oidc` | Hybrid: local password + SSO, no magic-link |
| `password,magic_link,oidc` | Everything on |
**Startup gate.** If `magic_link` is the only method allowed AND no SMTP transport is configured (`OXICLOUD_SMTP_HOST` empty), the server refuses to start with a fatal message. A magic-link-only policy without a working mailer silently locks every user out.
**Fail-fast.** Misconfiguration panics at boot instead of degrading silently:
- Unknown token (e.g. `password,sso2`) → boot panic with `expected: password, magic_link, oidc`
- Empty allowlist (e.g. `OXICLOUD_AUTH_METHODS=`) → boot panic (would lock everyone out otherwise)
- `oidc` listed but `OXICLOUD_OIDC_ENABLED != true` → boot panic (advertising a method the server can't serve)
**OIDC master rule.** When `OXICLOUD_OIDC_ENABLED=true`, magic-link login is **hard-disabled** regardless of this list. The IdP is the identity boundary; magic-link would bypass any 2FA / step-up policy the IdP enforces. The startup gate above does **not** trigger in this case — OIDC provides the login path.
**Loose semantic (documented).** The symmetric case is NOT fatal yet: when `OXICLOUD_AUTH_METHODS` is explicitly set WITHOUT `oidc` but `OXICLOUD_OIDC_ENABLED=true`, OIDC is served in addition to the listed methods — the enabled flag wins. A warning is logged at boot to make the mismatch visible. **Planned for the next major release**: this will escalate to a fail-fast panic so `AUTH_METHODS` becomes the authoritative allowlist for OIDC too. Align configs now (either add `oidc` to the list or set `OXICLOUD_OIDC_ENABLED=false`) to avoid the breaking change.
**Startup gate.** If `magic_link` is the only working method (no `password`, no `oidc`) AND no SMTP transport is configured (`OXICLOUD_SMTP_HOST` empty), the server refuses to start with a fatal message. A magic-link-only policy without a working mailer silently locks every user out.
**OIDC master rule.** When OIDC is enabled, magic-link login is **hard-disabled** regardless of this list. The IdP is the identity boundary; magic-link would bypass any 2FA / step-up policy the IdP enforces. The startup gate above does **not** trigger in this case — OIDC provides the login path.
Legacy alias: `OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN=true` still removes `password` from the effective allowlist.
+1 -1
View File
@@ -46,7 +46,7 @@ Most runtime variables use the `OXICLOUD_` prefix. A few build-time or allocator
| `OXICLOUD_HASH_PARALLELISM` | `2` | Argon2id parallelism lanes |
| `OXICLOUD_DISABLE_REGISTRATION` | false | Disable registration of new user accounts |
| `OXICLOUD_REGISTRATION_ALLOWED_EMAIL_DOMAINS` | — | Comma-separated allowlist of email domains accepted on `POST /api/auth/register` (case-insensitive, exact match on the post-`@` part). Empty = any domain is allowed. **Distinct from `OXICLOUD_EXTERNAL_EMAIL_DOMAINS`**: this one gates SELF-registration (public sign-up), the external list gates INVITATIONS (grants + magic-link to third parties). An operator can lock sign-up to their company domain while leaving invitations open. Subdomains must be listed explicitly. Rejected registrations return 403 `RegistrationDomainNotAllowed` and emit an `audit` line. Example: `mycompany.com,mycompany-eu.com`. |
| `OXICLOUD_AUTH_METHODS` | `password,magic_link` | Comma-separated allowlist of self-service auth methods (`password`, `magic_link`). OIDC is orthogonal (see `OXICLOUD_OIDC_ENABLED`). Removing `password` disables `POST /api/auth/login` (returns 403 `PasswordLoginDisabled`) and password-based `register` (returns 403 `PasswordRegistrationDisabled`). Removing `magic_link` disables `POST /api/auth/magic-link/send` (returns 403 `MagicLinkLoginDisabled`) and the redemption path for login-purpose tokens. **Startup gate**: if `magic_link` is the only method allowed AND no SMTP transport is configured (`OXICLOUD_SMTP_HOST` empty), the server refuses to start. **OIDC master rule**: when `OXICLOUD_OIDC_ENABLED=true`, magic-link login is hard-disabled regardless of this list (would otherwise bypass IdP-enforced MFA / step-up). Legacy alias: `OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN=true` still removes `password` from the list. |
| `OXICLOUD_AUTH_METHODS` | `password,magic_link` | Comma-separated allowlist of auth methods (`password`, `magic_link`, `oidc`). **Fail-fast**: unknown token → boot panic; empty allowlist → boot panic; `oidc` in list without `OXICLOUD_OIDC_ENABLED=true` → boot panic. Removing `password` disables `POST /api/auth/login` (returns 403 `PasswordLoginDisabled`) and password-based `register` (returns 403 `PasswordRegistrationDisabled`). Removing `magic_link` disables `POST /api/auth/magic-link/send` (returns 403 `MagicLinkLoginDisabled`) and the redemption path for login-purpose tokens. Setting `OXICLOUD_AUTH_METHODS=oidc` is the cleanest "SSO-only" posture. **Loose semantic (deprecation warning)**: if this list is explicitly set WITHOUT `oidc` but `OXICLOUD_OIDC_ENABLED=true`, OIDC is served regardless — a boot warning is emitted and this will become a fail-fast panic in the next major release. **Startup gate**: if `magic_link` is the only working method (no `password`, no `oidc`) AND no SMTP transport is configured (`OXICLOUD_SMTP_HOST` empty), the server refuses to start. **OIDC master rule**: when OIDC is enabled, magic-link login is hard-disabled regardless of this list (would otherwise bypass IdP-enforced MFA / step-up). Legacy alias: `OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN=true` still removes `password` from the list. |
| `OXICLOUD_AUTH_POLICIES` | — | Comma-separated additive policy switches. Each token grants an exception or restriction to the default auth behaviour; empty (unset) = pure defaults. Recognised tokens: `permit_magic_link_for_password_users` (allow magic-link login for accounts that also have a password — off by default because magic-link would weaken the password to mailbox-strength; OIDC-linked users are still refused regardless). |
| `OXICLOUD_REQUIRE_VERIFIED_EMAIL` | `false` | When `true`, `POST /api/auth/login` returns 403 `EmailNotVerified` for any account whose `email_verified_at` is NULL. Users can prove control by requesting a magic-link (whose redemption stamps `email_verified_at`), so this composes with `magic_link` in `OXICLOUD_AUTH_METHODS` to give users a self-service verification path. Admin-created (`POST /api/admin/users`) and setup-admin (`POST /api/setup`) users are auto-verified. OIDC-JIT users are also stamped verified at creation. |
+41 -24
View File
@@ -700,40 +700,57 @@ OXICLOUD_WOPI_ENABLED=false
#OXICLOUD_REGISTRATION_ALLOWED_EMAIL_DOMAINS=mycompany.com,mycompany-eu.com
# ---------------------------------------------------------------------------
# OXICLOUD_AUTH_METHODS — self-service authentication method allowlist.
# OXICLOUD_AUTH_METHODS — authentication method allowlist.
# ---------------------------------------------------------------------------
# Comma-separated list of `password` and/or `magic_link`. Controls which
# self-service authentication methods this deployment offers on the login
# page and accepts at the corresponding endpoints. OIDC is orthogonal —
# use `OXICLOUD_OIDC_ENABLED` for that.
# Comma-separated list of `password`, `magic_link`, and/or `oidc`. Controls
# which authentication methods this deployment offers on the login page
# and accepts at the corresponding endpoints.
#
# FAIL-FAST semantics — misconfiguration crashes the server at boot with
# a specific error, never silently degrades:
# * Unknown token (e.g. `password,sso2`) → panic on startup
# * Empty allowlist (e.g. `OXICLOUD_AUTH_METHODS=`) → panic
# * `oidc` in the list but `OXICLOUD_OIDC_ENABLED != true` → panic
# ("advertising a login method the server can't serve")
#
# LOOSE SEMANTIC (documented) — the reverse of the last bullet is NOT
# fatal today: when this list is explicitly set WITHOUT `oidc` but
# `OXICLOUD_OIDC_ENABLED=true`, OIDC is served in addition to the
# listed methods. The enabled flag wins. A warning is logged at boot
# telling the admin to reconcile. This will escalate to a fail-fast
# panic in the next major release — align configs now to avoid the
# breaking change.
#
# Semantics per configuration:
# * Empty (unset) or `password,magic_link` — both methods allowed
# (default). Matches pre-flag behaviour.
# * `password` — `POST /api/auth/login` OK,
# magic-link send / redeem
# return 403 `MagicLinkLoginDisabled`.
# * `magic_link` — `POST /api/auth/login`
# returns 403 `PasswordLoginDisabled`;
# password-based `register`
# returns 403 `PasswordRegistrationDisabled`.
# * Unset — permissive default
# (password + magic_link;
# OIDC gated by its own flag).
# * `password` — password login only.
# * `magic_link` — magic-link login only
# (requires SMTP; see gate below).
# * `oidc` — OIDC only, no local login.
# Cleanest "SSO-only" posture.
# * `password,oidc` — hybrid: local + SSO,
# no magic-link.
# * `password,magic_link,oidc` — everything on.
#
# SECURITY — startup gate. When `magic_link` is the ONLY method allowed
# but no SMTP transport is configured, the server refuses to start with a
# fatal message. A magic-link-only policy without a mail sender silently
# locks every user out of the deployment.
# SECURITY — startup gate. When `magic_link` is the ONLY working method
# (no `password`, no `oidc`) but no SMTP transport is configured, the
# server refuses to start. Prevents silently locking every user out.
#
# SECURITY — OIDC master rule. When `OXICLOUD_OIDC_ENABLED=true`, magic-
# link login is HARD-disabled regardless of what this list says. OIDC is
# the master identity provider; magic-link would sidestep any 2FA / step-
# up the IdP enforces. The startup gate above does NOT trigger in this
# case (OIDC provides a login path).
# SECURITY — OIDC master rule. When OIDC is enabled (either explicitly
# in this list or via `OXICLOUD_OIDC_ENABLED=true`), magic-link login is
# HARD-disabled regardless of what this list says. OIDC is the master
# identity provider; magic-link would sidestep any 2FA / step-up the
# IdP enforces.
#
# Legacy alias: `OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN=true` still removes
# `password` from this list. New deployments should prefer this env var.
#
# Default: password,magic_link
# Default (when unset): password + magic_link.
#OXICLOUD_AUTH_METHODS=password,magic_link
#OXICLOUD_AUTH_METHODS=oidc # OIDC-only (needs OIDC_ENABLED=true)
#OXICLOUD_AUTH_METHODS=password,oidc # hybrid local + SSO
# ---------------------------------------------------------------------------
# OXICLOUD_REQUIRE_VERIFIED_EMAIL — gate login on email verification.
+73 -24
View File
@@ -1452,21 +1452,26 @@ pub struct AuthConfig {
/// Self-service auth method. Exposed as `AuthConfig::allowed_auth_methods`
/// and parsed from `OXICLOUD_AUTH_METHODS` (comma-separated). OIDC is
/// deliberately excluded — it lives in `OidcConfig` with its own gate.
/// a first-class allowlist token: `OXICLOUD_AUTH_METHODS=oidc` = OIDC
/// only (needs `OXICLOUD_OIDC_ENABLED=true` + a full OIDC config bucket
/// or the boot rejects — cross-validation lives in `AppConfig::from_env`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthMethod {
Password,
MagicLink,
Oidc,
}
impl AuthMethod {
/// Case-insensitive parse: accepts `password`, `magic_link`, and the
/// dash form `magic-link` (some operators habitually use dashes).
/// Unknown token returns `None` so the caller can log-and-skip.
/// dash form `magic-link` (some operators habitually use dashes),
/// plus `oidc`. Unknown token returns `None`; the caller (env
/// parser) treats that as a fatal boot error rather than a warning.
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"password" => Some(Self::Password),
"magic_link" | "magic-link" | "magiclink" => Some(Self::MagicLink),
"oidc" | "sso" => Some(Self::Oidc),
_ => None,
}
}
@@ -2653,28 +2658,72 @@ impl AppConfig {
// operator wrote `OXICLOUD_AUTH_METHODS=nope`), we restore the
// default — a zero-method allowlist would refuse every login.
if let Ok(v) = env::var("OXICLOUD_AUTH_METHODS") {
let methods: Vec<AuthMethod> = v
.split(',')
.filter_map(|s| {
let parsed = AuthMethod::parse(s);
if parsed.is_none() && !s.trim().is_empty() {
eprintln!(
"⚠️ OXICLOUD_AUTH_METHODS: ignoring unknown token '{}' \
(expected: password, magic_link)",
s.trim()
);
}
parsed
})
.collect();
if methods.is_empty() {
eprintln!(
"⚠️ OXICLOUD_AUTH_METHODS parsed to an empty allowlist; \
falling back to default (password, magic_link)"
);
} else {
config.auth.allowed_auth_methods = methods;
// Fail-fast on operator error: an unknown token, an empty
// allowlist, or a listed method whose infrastructure isn't
// wired all indicate a misconfiguration that would silently
// change auth surface behaviour (per memory
// `feedback_fail_fast_config`: boot panic > silent skip
// for anything a mistyped env var could break).
let mut methods: Vec<AuthMethod> = Vec::new();
for raw in v.split(',') {
let token = raw.trim();
if token.is_empty() {
continue;
}
match AuthMethod::parse(token) {
Some(m) => methods.push(m),
None => panic!(
"OXICLOUD_AUTH_METHODS: unknown token '{}' — expected any of: \
password, magic_link, oidc",
token
),
}
}
if methods.is_empty() {
panic!(
"OXICLOUD_AUTH_METHODS is set to '{}' but produced an empty allowlist. \
Either unset the variable (default = password, magic_link) or list at \
least one method (password, magic_link, oidc).",
v
);
}
// Cross-validation A: `oidc` in the allowlist requires OIDC
// to be enabled. Otherwise the login page would advertise a
// method the server can't actually serve.
let oidc_env_enabled = env::var("OXICLOUD_OIDC_ENABLED")
.ok()
.and_then(|s| s.parse::<bool>().ok())
.unwrap_or(false);
if methods.contains(&AuthMethod::Oidc) && !oidc_env_enabled {
panic!(
"OXICLOUD_AUTH_METHODS includes 'oidc' but OXICLOUD_OIDC_ENABLED \
is not 'true'. Either set OXICLOUD_OIDC_ENABLED=true (plus \
OXICLOUD_OIDC_ISSUER_URL / OXICLOUD_OIDC_CLIENT_ID / \
OXICLOUD_OIDC_CLIENT_SECRET), or configure OIDC via the admin \
panel and drop 'oidc' from OXICLOUD_AUTH_METHODS until it's ready."
);
}
// Cross-validation B: the reverse — OIDC enabled but the
// admin's explicit AUTH_METHODS list doesn't include `oidc`.
// Today: warn loudly (soft mismatch). PLANNED for the next
// major release: escalate to a fail-fast panic to match the
// symmetric cross-validation A above. The current loose
// behaviour silently serves OIDC in addition to what
// AUTH_METHODS lists — the enabled flag wins — which
// contradicts the "AUTH_METHODS is the authoritative
// allowlist" mental model.
if !methods.contains(&AuthMethod::Oidc) && oidc_env_enabled {
eprintln!(
"⚠️ OXICLOUD_AUTH_METHODS excludes 'oidc' but \
OXICLOUD_OIDC_ENABLED=true — OIDC will be served \
regardless. Add 'oidc' to OXICLOUD_AUTH_METHODS to \
make the allowlist authoritative, or set \
OXICLOUD_OIDC_ENABLED=false to exclude OIDC. \
A future release will escalate this to a fatal boot error."
);
}
config.auth.allowed_auth_methods = methods;
}
// Legacy alias: OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN=true still
+7 -2
View File
@@ -532,13 +532,18 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
.auth
.allowed_auth_methods
.contains(&common::config::AuthMethod::Password)
&& !config
.auth
.allowed_auth_methods
.contains(&common::config::AuthMethod::Oidc)
&& !config.smtp.is_enabled()
{
panic!(
"FATAL: OXICLOUD_AUTH_METHODS enables `magic_link` as the ONLY \
self-service auth method, but no SMTP transport is configured. \
Set OXICLOUD_SMTP_HOST (and matching OXICLOUD_SMTP_* settings) \
or add `password` to OXICLOUD_AUTH_METHODS. Refusing to start."
Set OXICLOUD_SMTP_HOST (and matching OXICLOUD_SMTP_* settings), \
add `password` or `oidc` to OXICLOUD_AUTH_METHODS, or drop \
`magic_link` from the list. Refusing to start."
);
}