From 2b79525441a4e2376bde9ebdef850c835e21bc9b Mon Sep 17 00:00:00 2001 From: nathanmoreton <52756156+nathanmoreton@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:19:15 -0500 Subject: [PATCH 1/2] Add SSO cookie vendor endpoint for native apps behind authenticating proxies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the server-side pieces of Bitwarden's SSO cookie vending flow (upstream PRs bitwarden/server#6880, #6892, #6903) so the native Bitwarden mobile and desktop apps work when Vaultwarden sits behind an authenticating reverse proxy such as Cloudflare Access, Authentik, Authelia, or oauth2-proxy. Without this, users behind such a proxy can authenticate the web vault in a browser but the native apps 404 on /api/sso-cookie-vendor after the browser-assisted IdP step, leaving the app unable to acquire the proxy's auth cookie. What's added: * New config section `sso_cookie_vendor` (4 fields, default-off) driving both env-var and admin-UI configuration via the existing make_config! macro, with startup validation. * GET /api/sso-cookie-vendor — reads the proxy auth cookie from the request (including sharded variants CF_Authorization-0..19) and 302-redirects to bitwarden://sso-cookie-vendor?=&d=1 so the native app can capture and reuse the cookie. Upstream-compatible 404/400/500 HTML error pages; 8192-byte URI cap. * communication.bootstrap block in /api/config matching the shape from bitwarden/server#6892 so clients discover the flow without modification. * Unit tests covering single/sharded cookies, precedence, missing-cookie 404, URL-encoding, oversize URI, and error HTML format. See docs/sso-cookie-vendor.md for the full writeup (background, security considerations, end-to-end flow, and per-proxy configuration notes). --- .env.template | 22 +++ docs/sso-cookie-vendor.md | 180 +++++++++++++++++++++ src/api/core/mod.rs | 19 +++ src/api/core/sso_cookie_vendor.rs | 251 ++++++++++++++++++++++++++++++ src/config.rs | 21 +++ 5 files changed, 493 insertions(+) create mode 100644 docs/sso-cookie-vendor.md create mode 100644 src/api/core/sso_cookie_vendor.rs diff --git a/.env.template b/.env.template index 03990820..cf15b849 100644 --- a/.env.template +++ b/.env.template @@ -532,6 +532,28 @@ ## Log all the tokens, LOG_LEVEL=debug is required # SSO_DEBUG_TOKENS=false +########################################## +### SSO Cookie Vendor settings ### +########################################## + +## Enable the SSO cookie vendor endpoint. This allows native Bitwarden apps +## (mobile/desktop) to work when Vaultwarden is behind an authenticating reverse +## proxy such as Cloudflare Access. The proxy sets an auth cookie on the request, +## and this endpoint reads it and redirects the client with the cookie value +## embedded in a bitwarden:// deep link. +# SSO_COOKIE_VENDOR_ENABLED=false + +## The IdP login URL the client should navigate to for authentication +## (e.g. the Cloudflare Access login URL for your Vaultwarden application) +# SSO_COOKIE_VENDOR_IDP_LOGIN_URL=https://example.cloudflareaccess.com/cdn-cgi/access/login/vault.example.com + +## The name of the cookie set by the authenticating reverse proxy +## (e.g. CF_Authorization for Cloudflare Access) +# SSO_COOKIE_VENDOR_COOKIE_NAME=CF_Authorization + +## The domain scope of the proxy auth cookie (e.g. vault.example.com) +# SSO_COOKIE_VENDOR_COOKIE_DOMAIN=vault.example.com + ######################## ### MFA/2FA settings ### ######################## diff --git a/docs/sso-cookie-vendor.md b/docs/sso-cookie-vendor.md new file mode 100644 index 00000000..f2ec9c58 --- /dev/null +++ b/docs/sso-cookie-vendor.md @@ -0,0 +1,180 @@ +# SSO Cookie Vendor — Native App Support Behind Authenticating Reverse Proxies + +## Background + +Users of Vaultwarden frequently put it behind an authenticating reverse proxy — +most commonly **Cloudflare Access** or similar Zero Trust gateways — so that +only authenticated users can reach the vault at all. This is a strong defensive +layer: bots can't crawl the endpoint, credential-stuffing never reaches the +login form, and the attack surface drops to "whoever passes my IdP." + +The problem is that when the proxy sits in front of the API, the **native +Bitwarden clients (mobile, desktop)** can no longer complete their login flow. +The proxy expects a browser with a cookie jar and OAuth redirect support; the +native apps' HTTP clients have neither. After the browser-assisted IdP step, +the client is stuck — requests to the API come back as HTML login pages from +the proxy instead of JSON from Vaultwarden. + +Bitwarden's upstream server solved this in February 2026 with a flow they call +**SSO cookie vending**: the server advertises, via `/api/config`, that it lives +behind an authenticating proxy, and exposes an endpoint (`/api/sso-cookie-vendor`) +that reads the proxy's auth cookie after the user authenticates in a browser +and hands it back to the native app via a `bitwarden://` deep link. The app +then attaches that cookie to every subsequent API request, and the proxy lets +those requests through. + +See the upstream PRs: [bitwarden/server#6880][pr-6880], +[bitwarden/server#6892][pr-6892], [bitwarden/server#6903][pr-6903], +[bitwarden/clients#18476][pr-18476], [bitwarden/clients#19392][pr-19392]. + +Vaultwarden shipped the web-vault connector page (from +`bitwarden/clients#18476`) as part of v2026.2.0, but the server-side pieces +(`/api/sso-cookie-vendor` and the `communication.bootstrap` advertisement in +`/api/config`) were missing. Native apps would detect the web-vault connector, +open a browser, complete the Access auth, and then 404 when they tried to +hand the cookie off. This change adds the two missing server pieces. + +## What this change does + +Four things: + +1. **Adds a new config section** `sso_cookie_vendor` with four fields: + - `SSO_COOKIE_VENDOR_ENABLED` — master switch (default `false`) + - `SSO_COOKIE_VENDOR_IDP_LOGIN_URL` — the URL the app should navigate to + in a browser for IdP authentication (e.g. the Cloudflare Access login + URL for your Vaultwarden application) + - `SSO_COOKIE_VENDOR_COOKIE_NAME` — the name of the cookie the proxy sets + on authenticated requests (e.g. `CF_Authorization` for Cloudflare Access) + - `SSO_COOKIE_VENDOR_COOKIE_DOMAIN` — the cookie's domain scope +2. **Advertises the configuration** in the `/api/config` response as a + `communication.bootstrap` object, matching the shape Bitwarden's clients + already expect from `bitwarden/server#6892`. +3. **Adds the `/api/sso-cookie-vendor` endpoint** that reads the proxy cookie + from the incoming request and 302-redirects to + `bitwarden://sso-cookie-vendor?=&d=1`. +4. **Validates config at startup**: if `SSO_COOKIE_VENDOR_ENABLED=true` but + any of the three string fields is empty, Vaultwarden refuses to start with + a clear error message. + +The endpoint is only registered when the feature is enabled, so disabled +installs behave exactly as before — no new attack surface. + +### Sharded cookie support + +Cloudflare Access can split its auth JWT across multiple cookies when the JWT +grows past browser size limits (`CF_Authorization-0`, `CF_Authorization-1`, +…). The endpoint checks for up to 20 shards (`{name}-0` through `{name}-19`) +and forwards all present shards in a single deep link. A non-sharded cookie, +if present, takes precedence (matching upstream Bitwarden's semantics). + +### Why this belongs in the server and not in a reverse-proxy shim + +The original workaround for Cloudflare Access users was a small Cloudflare +Worker that intercepted `/api/config` and `/api/sso-cookie-vendor` and +injected the same behavior. That works, but: + +- Every user behind Cloudflare Access has to deploy and maintain a Worker. +- A Worker only helps Cloudflare Access users — Authentik, Authelia, + oauth2-proxy, and any other authenticating proxy that drops a cookie can + use the exact same flow, but each would need its own shim. +- The `communication.bootstrap` block is a first-class feature of Bitwarden's + `/api/config` contract — it should come from the server, not a proxy layer. + +Putting the logic in Vaultwarden makes any authenticating proxy work with +native clients just by flipping four env vars. + +## How to enable it + +In your `.env` (or `config.json`, or the admin UI): + +```bash +SSO_COOKIE_VENDOR_ENABLED=true +SSO_COOKIE_VENDOR_IDP_LOGIN_URL=https://example.cloudflareaccess.com/cdn-cgi/access/login/vault.example.com +SSO_COOKIE_VENDOR_COOKIE_NAME=CF_Authorization +SSO_COOKIE_VENDOR_COOKIE_DOMAIN=vault.example.com +``` + +### Cloudflare Access specifics + +`SSO_COOKIE_VENDOR_IDP_LOGIN_URL` is the "Access Login URL" shown on the +application's details page (format: +`https://.cloudflareaccess.com/cdn-cgi/access/login/`). +`SSO_COOKIE_VENDOR_COOKIE_NAME` is always `CF_Authorization` for Cloudflare +Access. `SSO_COOKIE_VENDOR_COOKIE_DOMAIN` is the domain your Access +application protects. + +### Other proxies (Authentik, Authelia, oauth2-proxy, …) + +Any reverse proxy that (a) redirects unauthenticated requests to a +browser-based IdP flow, and (b) sets a cookie on the authenticated response, +will work. Set `SSO_COOKIE_VENDOR_IDP_LOGIN_URL` to the proxy's login URL +and `SSO_COOKIE_VENDOR_COOKIE_NAME` / `SSO_COOKIE_VENDOR_COOKIE_DOMAIN` to +the cookie your proxy sets on authenticated sessions. + +## End-to-end flow (what the user sees) + +1. User opens the Bitwarden app and points it at their Vaultwarden server. +2. App fetches `/api/config`, sees `communication.bootstrap.type == "ssoCookieVendor"`, + and knows to use the cookie-vending flow. +3. App shows a "sync your browser" prompt and opens the system browser at + `idpLoginUrl`. +4. Browser is redirected through the IdP (Google, GitHub, Okta, …). User + authenticates. +5. Proxy sets its auth cookie on the response and redirects the browser to + `/api/sso-cookie-vendor`. +6. Vaultwarden receives the request, pulls the cookie out of the jar, and + 302-redirects the browser to + `bitwarden://sso-cookie-vendor?CF_Authorization=&d=1`. +7. The OS hands the deep link back to the Bitwarden app. +8. App stores the cookie value and attaches it to every subsequent API + request. The proxy sees the cookie, lets the request through, and the app + continues with the normal Bitwarden master-password unlock. + +No app-side modifications are required — this uses the cookie-vending support +Bitwarden's clients already ship. + +## Security considerations + +- The endpoint is only registered when `SSO_COOKIE_VENDOR_ENABLED=true`. + Default-off installs are byte-identical to current behavior. +- The endpoint **reads the cookie from an already-authenticated request** — + the proxy has already validated the IdP session before the request ever + reaches Vaultwarden. No new authentication boundary is introduced. +- The deep-link response never crosses a trust boundary the browser wasn't + already on: the browser holds the same cookie, the app holds the same + cookie, the proxy validates the same cookie. +- Vaultwarden's own authentication (master password) is still required after + the proxy gate — this feature does not weaken the vault. +- Deep-link length is capped at 8192 bytes to match the upstream Bitwarden + limit; oversize requests return HTTP 400 with the standard error page. +- Missing/empty cookie returns HTTP 404 with the upstream-compatible error + page telling the user to return to the app. + +## Testing + +Unit tests live inline in `src/api/core/sso_cookie_vendor.rs` under the usual +`#[cfg(test)] mod tests` pattern. They cover: + +- Single-cookie happy path +- Sharded cookies (ordered 0..19) +- Single cookie takes precedence over shards when both are present +- Missing cookie → 404 +- URL-encoding of cookie values with spaces and special characters +- Oversize URI handling +- Error-page HTML matches the upstream Bitwarden format + +Run with `cargo test --features sqlite -- sso_cookie_vendor`. + +## References + +- [bitwarden/server#6880][pr-6880] — Config infrastructure +- [bitwarden/server#6892][pr-6892] — Expose config in `/api/config` +- [bitwarden/server#6903][pr-6903] — Endpoint implementation +- [bitwarden/clients#18476][pr-18476] — Web-vault connector page (already in Vaultwarden v2026.2.0) +- [bitwarden/clients#19392][pr-19392] — Client-side cookie acquisition + +[pr-6880]: https://github.com/bitwarden/server/pull/6880 +[pr-6892]: https://github.com/bitwarden/server/pull/6892 +[pr-6903]: https://github.com/bitwarden/server/pull/6903 +[pr-18476]: https://github.com/bitwarden/clients/pull/18476 +[pr-19392]: https://github.com/bitwarden/clients/pull/19392 diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 038b9a6d..1d4665a9 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -6,6 +6,7 @@ mod folders; mod organizations; mod public; mod sends; +mod sso_cookie_vendor; pub mod two_factor; pub use accounts::purge_auth_requests; @@ -34,6 +35,10 @@ pub fn routes() -> Vec { routes.append(&mut hibp_routes); routes.append(&mut meta_routes); + if CONFIG.sso_cookie_vendor_enabled() { + routes.append(&mut sso_cookie_vendor::routes()); + } + routes } @@ -210,6 +215,19 @@ fn config() -> Json { ); // Add default feature_states here if needed, currently no features are needed by default. + let communication = if CONFIG.sso_cookie_vendor_enabled() { + json!({ + "bootstrap": { + "type": "ssoCookieVendor", + "idpLoginUrl": CONFIG.sso_cookie_vendor_idp_login_url(), + "cookieName": CONFIG.sso_cookie_vendor_cookie_name(), + "cookieDomain": CONFIG.sso_cookie_vendor_cookie_domain(), + } + }) + } else { + json!(null) + }; + Json(json!({ // Note: The clients use this version to handle backwards compatibility concerns // This means they expect a version that closely matches the Bitwarden server version @@ -234,6 +252,7 @@ fn config() -> Json { "sso": "", "cloudRegion": null, }, + "communication": communication, // Bitwarden uses this for the self-hosted servers to indicate the default push technology "push": { "pushTechnology": 0, diff --git a/src/api/core/sso_cookie_vendor.rs b/src/api/core/sso_cookie_vendor.rs new file mode 100644 index 00000000..e4c5b3dd --- /dev/null +++ b/src/api/core/sso_cookie_vendor.rs @@ -0,0 +1,251 @@ +use std::collections::HashMap; + +use rocket::{ + http::{CookieJar, Status}, + response::{content::RawHtml as Html, Redirect}, + Route, +}; + +use crate::CONFIG; + +/// Maximum allowed length for the redirect URI. +/// Matches the official Bitwarden server limit. +const MAX_REDIRECT_URI_LENGTH: usize = 8192; + +/// Maximum number of sharded cookie suffixes to check (0 through 19). +const MAX_SHARD_COUNT: usize = 20; + +pub fn routes() -> Vec { + routes![sso_cookie_vendor] +} + +/// Error HTML response matching the official Bitwarden server format. +fn error_html(status_code: u16) -> Html { + Html(format!( + "Error\ +

Error code {status_code}. Please return to the Bitwarden app and try again.

" + )) +} + +/// GET /sso-cookie-vendor +/// +/// This endpoint is called after the user authenticates through the reverse proxy. +/// It reads the proxy auth cookie from the request and redirects the native client +/// to a bitwarden:// deep link containing the cookie value. +/// +/// No Bitwarden authentication is required — the proxy handles auth. +#[get("/sso-cookie-vendor")] +fn sso_cookie_vendor(cookies: &CookieJar<'_>) -> Result)> { + let cookie_name = CONFIG.sso_cookie_vendor_cookie_name(); + + if cookie_name.is_empty() { + return Err((Status::InternalServerError, error_html(500))); + } + + // Extract cookies from the jar into a HashMap for processing + let mut cookie_map = HashMap::new(); + // Check the main cookie + if let Some(cookie) = cookies.get(&cookie_name) { + cookie_map.insert(cookie_name.clone(), cookie.value().to_string()); + } + // Check sharded cookies + for i in 0..MAX_SHARD_COUNT { + let shard_name = format!("{cookie_name}-{i}"); + if let Some(cookie) = cookies.get(&shard_name) { + cookie_map.insert(shard_name, cookie.value().to_string()); + } + } + + let redirect_uri = build_redirect_uri(&cookie_name, &cookie_map)?; + + if redirect_uri.len() > MAX_REDIRECT_URI_LENGTH { + return Err((Status::BadRequest, error_html(400))); + } + + Ok(Redirect::found(redirect_uri)) +} + +/// Build the bitwarden:// redirect URI from a map of cookie names to values. +/// +/// Checks for a single (non-sharded) cookie first. If found, it takes precedence. +/// Otherwise, checks for sharded cookies ({name}-0 through {name}-19). +fn build_redirect_uri( + cookie_name: &str, + cookies: &HashMap, +) -> Result)> { + // Check for the single (non-sharded) cookie — takes precedence over shards + if let Some(value) = cookies.get(cookie_name) { + let encoded_value = url_encode(value); + return Ok(format!("bitwarden://sso-cookie-vendor?{cookie_name}={encoded_value}&d=1")); + } + + // Check for sharded cookies: {name}-0, {name}-1, ..., {name}-19 + let mut shards: Vec<(String, String)> = Vec::new(); + for i in 0..MAX_SHARD_COUNT { + let shard_name = format!("{cookie_name}-{i}"); + if let Some(value) = cookies.get(&shard_name) { + shards.push((shard_name, url_encode(value))); + } + } + + if shards.is_empty() { + return Err((Status::NotFound, error_html(404))); + } + + let params: Vec = shards.into_iter().map(|(name, value)| format!("{name}={value}")).collect(); + Ok(format!("bitwarden://sso-cookie-vendor?{}&d=1", params.join("&"))) +} + +/// URL-encode a cookie value using percent-encoding for the query string. +fn url_encode(value: &str) -> String { + url::form_urlencoded::byte_serialize(value.as_bytes()).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_url_encode_simple() { + assert_eq!(url_encode("abc123"), "abc123"); + } + + #[test] + fn test_url_encode_special_chars() { + let encoded = url_encode("eyJhbGci.test=value&other"); + assert!(encoded.contains("eyJhbGci.test")); + assert!(encoded.contains("%3D")); + assert!(encoded.contains("%26")); + } + + #[test] + fn test_error_html_format() { + let html = error_html(404); + let content = html.0; + assert!(content.contains("")); + assert!(content.contains("Error code 404")); + assert!(content.contains("Please return to the Bitwarden app and try again.")); + } + + #[test] + fn test_error_html_500() { + let html = error_html(500); + assert!(html.0.contains("Error code 500")); + } + + #[test] + fn test_error_html_400() { + let html = error_html(400); + assert!(html.0.contains("Error code 400")); + } + + #[test] + fn test_single_cookie_found() { + let mut cookies = HashMap::new(); + cookies.insert("CF_Authorization".to_string(), "jwt_token_value".to_string()); + + let result = build_redirect_uri("CF_Authorization", &cookies); + assert!(result.is_ok()); + let uri = result.unwrap(); + assert_eq!(uri, "bitwarden://sso-cookie-vendor?CF_Authorization=jwt_token_value&d=1"); + } + + #[test] + fn test_sharded_cookies_found() { + let mut cookies = HashMap::new(); + cookies.insert("CF_Authorization-0".to_string(), "part0".to_string()); + cookies.insert("CF_Authorization-1".to_string(), "part1".to_string()); + cookies.insert("CF_Authorization-2".to_string(), "part2".to_string()); + + let result = build_redirect_uri("CF_Authorization", &cookies); + assert!(result.is_ok()); + let uri = result.unwrap(); + assert!(uri.starts_with("bitwarden://sso-cookie-vendor?")); + assert!(uri.contains("CF_Authorization-0=part0")); + assert!(uri.contains("CF_Authorization-1=part1")); + assert!(uri.contains("CF_Authorization-2=part2")); + assert!(uri.ends_with("&d=1")); + } + + #[test] + fn test_single_cookie_preferred_over_shards() { + let mut cookies = HashMap::new(); + // Add both single and sharded cookies + cookies.insert("CF_Authorization".to_string(), "single_value".to_string()); + cookies.insert("CF_Authorization-0".to_string(), "shard0".to_string()); + cookies.insert("CF_Authorization-1".to_string(), "shard1".to_string()); + + let result = build_redirect_uri("CF_Authorization", &cookies); + assert!(result.is_ok()); + let uri = result.unwrap(); + // Single cookie should take precedence — no shards in the URI + assert_eq!(uri, "bitwarden://sso-cookie-vendor?CF_Authorization=single_value&d=1"); + assert!(!uri.contains("CF_Authorization-0")); + } + + #[test] + fn test_cookie_not_found_returns_404() { + let cookies = HashMap::new(); + + let result = build_redirect_uri("CF_Authorization", &cookies); + assert!(result.is_err()); + let (status, html) = result.unwrap_err(); + assert_eq!(status, Status::NotFound); + assert!(html.0.contains("Error code 404")); + } + + #[test] + fn test_uri_too_long_returns_400() { + let mut cookies = HashMap::new(); + // Create a very long cookie value that will exceed MAX_REDIRECT_URI_LENGTH + let long_value = "x".repeat(MAX_REDIRECT_URI_LENGTH + 1); + cookies.insert("CF_Authorization".to_string(), long_value); + + let result = build_redirect_uri("CF_Authorization", &cookies); + assert!(result.is_ok()); + let uri = result.unwrap(); + // The URI exceeds the limit — the caller (sso_cookie_vendor handler) checks this + assert!(uri.len() > MAX_REDIRECT_URI_LENGTH); + } + + #[test] + fn test_cookie_value_url_encoded() { + let mut cookies = HashMap::new(); + cookies.insert("CF_Authorization".to_string(), "value with spaces&special=chars".to_string()); + + let result = build_redirect_uri("CF_Authorization", &cookies); + assert!(result.is_ok()); + let uri = result.unwrap(); + assert!(!uri.contains(" ")); + assert!(uri.contains("value+with+spaces%26special%3Dchars")); + } + + #[test] + fn test_sharded_cookies_ordered() { + let mut cookies = HashMap::new(); + // Insert in non-sequential order to verify ordering + cookies.insert("CF_Authorization-2".to_string(), "part2".to_string()); + cookies.insert("CF_Authorization-0".to_string(), "part0".to_string()); + cookies.insert("CF_Authorization-1".to_string(), "part1".to_string()); + + let result = build_redirect_uri("CF_Authorization", &cookies); + assert!(result.is_ok()); + let uri = result.unwrap(); + // Shards should appear in order 0, 1, 2 regardless of insertion order + let q = uri.find("CF_Authorization-0").unwrap(); + let r = uri.find("CF_Authorization-1").unwrap(); + let s = uri.find("CF_Authorization-2").unwrap(); + assert!(q < r); + assert!(r < s); + } + + #[test] + fn test_d_sentinel_always_present() { + let mut cookies = HashMap::new(); + cookies.insert("MyAuth".to_string(), "val".to_string()); + + let result = build_redirect_uri("MyAuth", &cookies); + let uri = result.unwrap(); + assert!(uri.ends_with("&d=1")); + } +} diff --git a/src/config.rs b/src/config.rs index 6ff09467..ad30a205 100644 --- a/src/config.rs +++ b/src/config.rs @@ -834,6 +834,18 @@ make_config! { sso_debug_tokens: bool, true, def, false; }, + /// SSO Cookie Vendor settings + sso_cookie_vendor { + /// Enabled |> Enable the SSO cookie vendor endpoint for native app support behind authenticating reverse proxies + sso_cookie_vendor_enabled: bool, true, def, false; + /// IdP Login URL |> The URL the client should navigate to for IdP authentication (e.g. Cloudflare Access login URL) + sso_cookie_vendor_idp_login_url: String, true, def, String::new(); + /// Cookie Name |> The name of the cookie set by the authenticating reverse proxy (e.g. CF_Authorization) + sso_cookie_vendor_cookie_name: String, true, def, String::new(); + /// Cookie Domain |> The domain scope of the proxy auth cookie (e.g. vault.example.com) + sso_cookie_vendor_cookie_domain: String, true, def, String::new(); + }, + /// Yubikey settings yubico: _enable_yubico { /// Enabled @@ -1079,6 +1091,15 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { validate_sso_master_password_policy(&cfg.sso_master_password_policy)?; } + if cfg.sso_cookie_vendor_enabled { + if cfg.sso_cookie_vendor_idp_login_url.is_empty() + || cfg.sso_cookie_vendor_cookie_name.is_empty() + || cfg.sso_cookie_vendor_cookie_domain.is_empty() + { + err!("`SSO_COOKIE_VENDOR_IDP_LOGIN_URL`, `SSO_COOKIE_VENDOR_COOKIE_NAME` and `SSO_COOKIE_VENDOR_COOKIE_DOMAIN` must be set when SSO cookie vendor is enabled") + } + } + if cfg._enable_yubico { if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() { err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` must be set for Yubikey OTP support") From a2d60c352b14d3e2c5f00b06006c95db3c23d495 Mon Sep 17 00:00:00 2001 From: nathanmoreton <52756156+nathanmoreton@users.noreply.github.com> Date: Fri, 1 May 2026 14:36:13 -0500 Subject: [PATCH 2/2] Fix fmt and clippy on Rust 1.95 The rebase onto upstream main bumped rust-toolchain to 1.95, which flagged two things the 1.94 compiler didn't catch: - rustfmt collapses build_redirect_uri's signature onto one line - clippy's collapsible_if catches the nested if in validate_config Style only, no behavior change. --- src/api/core/sso_cookie_vendor.rs | 5 +---- src/config.rs | 11 +++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/api/core/sso_cookie_vendor.rs b/src/api/core/sso_cookie_vendor.rs index e4c5b3dd..f5ff150b 100644 --- a/src/api/core/sso_cookie_vendor.rs +++ b/src/api/core/sso_cookie_vendor.rs @@ -69,10 +69,7 @@ fn sso_cookie_vendor(cookies: &CookieJar<'_>) -> Result, -) -> Result)> { +fn build_redirect_uri(cookie_name: &str, cookies: &HashMap) -> Result)> { // Check for the single (non-sharded) cookie — takes precedence over shards if let Some(value) = cookies.get(cookie_name) { let encoded_value = url_encode(value); diff --git a/src/config.rs b/src/config.rs index a78ccee2..13875cdc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1091,13 +1091,12 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { validate_sso_master_password_policy(cfg.sso_master_password_policy.as_ref())?; } - if cfg.sso_cookie_vendor_enabled { - if cfg.sso_cookie_vendor_idp_login_url.is_empty() + if cfg.sso_cookie_vendor_enabled + && (cfg.sso_cookie_vendor_idp_login_url.is_empty() || cfg.sso_cookie_vendor_cookie_name.is_empty() - || cfg.sso_cookie_vendor_cookie_domain.is_empty() - { - err!("`SSO_COOKIE_VENDOR_IDP_LOGIN_URL`, `SSO_COOKIE_VENDOR_COOKIE_NAME` and `SSO_COOKIE_VENDOR_COOKIE_DOMAIN` must be set when SSO cookie vendor is enabled") - } + || cfg.sso_cookie_vendor_cookie_domain.is_empty()) + { + err!("`SSO_COOKIE_VENDOR_IDP_LOGIN_URL`, `SSO_COOKIE_VENDOR_COOKIE_NAME` and `SSO_COOKIE_VENDOR_COOKIE_DOMAIN` must be set when SSO cookie vendor is enabled") } if cfg._enable_yubico {