Browse Source

Merge aa25c221e9 into a6c3bd6d18

pull/7127/merge
Nathan Moreton 2 days ago
committed by GitHub
parent
commit
0412e0bdca
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 26
      .env.template
  2. 200
      docs/sso-cookie-vendor.md
  3. 29
      src/api/core/mod.rs
  4. 299
      src/api/core/sso_cookie_vendor.rs
  5. 28
      src/config.rs

26
.env.template

@ -560,6 +560,32 @@
## Log all the tokens, LOG_LEVEL=debug is required
# SSO_DEBUG_TOKENS=false
##########################################
### SSO Cookie Vendor settings ###
##########################################
## Serve the SSO cookie vendor endpoint. Set this when Vaultwarden sits behind a
## reverse proxy that authenticates every request, such as Cloudflare Access,
## Authentik, Authelia, or oauth2-proxy. Without it the Bitwarden mobile and
## desktop apps cannot finish logging in, because the proxy answers their API
## calls with a browser redirect they cannot follow. The endpoint reads the
## cookie the proxy set and redirects the browser to a bitwarden:// deep link
## carrying that cookie, which the app then sends with every later request.
## The three settings below are required when this is true.
## See docs/sso-cookie-vendor.md for the full setup guide.
# SSO_COOKIE_VENDOR_ENABLED=false
## URL the app opens in a browser to authenticate. For Cloudflare Access this is
## the Access Login URL shown on the application's details page.
# SSO_COOKIE_VENDOR_IDP_LOGIN_URL=https://example.cloudflareaccess.com/cdn-cgi/access/login/vault.example.com
## Name of the cookie the proxy sets on authenticated requests. Cloudflare Access
## always uses CF_Authorization.
# SSO_COOKIE_VENDOR_COOKIE_NAME=CF_Authorization
## Domain scope of the proxy auth cookie, which is the domain the proxy protects.
# SSO_COOKIE_VENDOR_COOKIE_DOMAIN=vault.example.com
########################
### MFA/2FA settings ###
########################

200
docs/sso-cookie-vendor.md

@ -0,0 +1,200 @@
# SSO cookie vendor
Lets the Bitwarden mobile and desktop apps log in when Vaultwarden sits behind a
reverse proxy that authenticates every request, such as Cloudflare Access,
Authentik, Authelia, or oauth2-proxy.
## Background
Putting Vaultwarden behind an authenticating proxy means only users who pass
your identity provider (IdP) reach the vault at all. Bots cannot crawl the
endpoint, credential stuffing never reaches the login form, and the exposed
surface shrinks to the proxy.
The cost is that the Bitwarden mobile and desktop apps can no longer finish
logging in. The proxy expects a browser with a cookie jar and OAuth 2.0 redirect
support, and the apps' HTTP clients have neither. After the browser step, the
apps receive the proxy's HTML login page where they expect JSON from
Vaultwarden, and login stalls.
Bitwarden solved this in their own server in February 2026 with a flow called
SSO cookie vending:
1. The server states in `/api/config` that it sits behind an authenticating
proxy, and names the IdP login URL and the cookie to look for.
2. The app opens a system browser at that IdP login URL.
3. After the user authenticates, the proxy sets its cookie and the browser
reaches `/api/sso-cookie-vendor`.
4. The server reads the cookie and redirects the browser to a `bitwarden://`
deep link carrying it.
5. The app attaches that cookie to every later API request, and the proxy lets
those requests through.
Vaultwarden 2026.2.0 shipped the web-vault connector page from
[bitwarden/clients#18476][pr-18476], but not the two server-side pieces the flow
needs: the `/api/sso-cookie-vendor` endpoint and the `communication.bootstrap`
block in `/api/config`. Without them the apps detect the connector page, open a
browser, complete the proxy's authentication, and then get a 404 when they try
to collect the cookie. This change adds both pieces.
## What this adds
A configuration section, `sso_cookie_vendor`, holding four settings:
| Setting | Purpose |
|---|---|
| `SSO_COOKIE_VENDOR_ENABLED` | Turns the feature on. Defaults to `false`. |
| `SSO_COOKIE_VENDOR_IDP_LOGIN_URL` | URL the app opens in a browser to authenticate. |
| `SSO_COOKIE_VENDOR_COOKIE_NAME` | Name of the cookie the proxy sets on authenticated requests. |
| `SSO_COOKIE_VENDOR_COOKIE_DOMAIN` | Domain scope of that cookie. |
On top of those settings, this change:
- Publishes the three string settings in `/api/config` as a
`communication.bootstrap` object, in the shape Bitwarden's clients already
read from [bitwarden/server#6892][pr-6892].
- Serves `GET /api/sso-cookie-vendor`, which reads the proxy's cookie from the
request and returns a 302 to
`bitwarden://sso-cookie-vendor?COOKIE_NAME=COOKIE_VALUE&d=1`. Replace
`COOKIE_NAME` and `COOKIE_VALUE` with your configured cookie name and its
percent-encoded value; `d=1` is the sentinel the clients look for.
- Refuses to start when `SSO_COOKIE_VENDOR_ENABLED` is `true` and any of the
three string settings is empty, and reports which ones to set.
The route is registered only when the feature is on. With
`SSO_COOKIE_VENDOR_ENABLED=false`, Vaultwarden serves exactly the routes it
served before.
### Sharded cookies
Cloudflare Access splits its auth JWT across numbered cookies, such as
`CF_Authorization-0` and `CF_Authorization-1`, when the token outgrows the
per-cookie size limit. The endpoint looks for up to 20 shards, `-0` through
`-19`, and forwards every shard it finds in one deep link, in ascending order,
so the app can reassemble the token. An unsuffixed cookie takes precedence over
any shards, matching the Bitwarden server.
### Why this belongs in the server
The existing workaround for Cloudflare Access users is a Cloudflare Worker that
intercepts `/api/config` and `/api/sso-cookie-vendor` and supplies the same
behavior. That works, with three drawbacks:
- Every user behind Cloudflare Access has to deploy and maintain a Worker.
- A Worker helps only Cloudflare Access users. Authentik, Authelia,
oauth2-proxy, and any other authenticating proxy that sets a cookie can use
the same flow, but each one needs its own shim.
- `communication.bootstrap` is part of Bitwarden's `/api/config` contract, so it
belongs in the server rather than in a proxy layer.
Implementing it in Vaultwarden makes any authenticating proxy work with the
mobile and desktop apps once you set four environment variables.
## Configure the endpoint
Set the four settings in `.env`, in `config.json`, or through the admin panel:
```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
- `SSO_COOKIE_VENDOR_IDP_LOGIN_URL` is the Access Login URL on the
application's details page. It takes the form
`https://TEAM.cloudflareaccess.com/cdn-cgi/access/login/YOUR_DOMAIN`, where
`TEAM` is your Cloudflare Zero Trust team name and `YOUR_DOMAIN` is the
hostname the Access application protects.
- `SSO_COOKIE_VENDOR_COOKIE_NAME` is always `CF_Authorization`.
- `SSO_COOKIE_VENDOR_COOKIE_DOMAIN` is the domain the Access application
protects.
### Other authenticating proxies
Any proxy works that redirects unauthenticated requests to a browser-based IdP
flow and sets a cookie on the authenticated response. Set
`SSO_COOKIE_VENDOR_IDP_LOGIN_URL` to the proxy's login URL, and set
`SSO_COOKIE_VENDOR_COOKIE_NAME` and `SSO_COOKIE_VENDOR_COOKIE_DOMAIN` to the
cookie the proxy sets on authenticated sessions.
Note: Cloudflare Access is the only proxy this has run against in production.
The others meet the requirements above, but no one has reported a tested
configuration for them yet.
## How the login flow runs
From the user's side, with the feature configured:
1. The user opens the Bitwarden app and points it at their Vaultwarden server.
2. The app reads `/api/config`, finds
`communication.bootstrap.type` set to `ssoCookieVendor`, and switches to the
cookie vending flow.
3. The app prompts the user to sign in through their browser and opens the
system browser at `SSO_COOKIE_VENDOR_IDP_LOGIN_URL`.
4. The browser follows the proxy to the IdP. The user authenticates.
5. The proxy sets its auth cookie and sends the browser to
`/api/sso-cookie-vendor`.
6. Vaultwarden reads the cookie off the request and redirects the browser to
`bitwarden://sso-cookie-vendor?CF_Authorization=COOKIE_VALUE&d=1`.
7. The operating system hands the deep link to the Bitwarden app.
8. The app stores the cookie and sends it with every later API request. The
proxy recognizes the cookie, lets the request through, and the app continues
to the usual master password unlock.
The apps need no changes. This uses the cookie vending support Bitwarden's
clients already ship.
## Security notes
- The endpoint exists only when `SSO_COOKIE_VENDOR_ENABLED` is `true`. An
install that leaves the feature off serves the same routes it served before.
- The endpoint reads a cookie from a request the proxy has already
authenticated. The proxy validates the IdP session before the request reaches
Vaultwarden, so this adds no authentication boundary of its own.
- The redirect moves the cookie between two parties that both already hold it:
the browser that received it and the app on the same device. The proxy
validates the same cookie in either case.
- Vaultwarden's own authentication still applies. The user unlocks the vault
with their master password after the proxy gate, so this does not weaken the
vault.
- The deep link is capped at 8192 bytes, matching the Bitwarden server. A
request that would exceed the cap gets a 400 and an HTML error page.
- A request carrying neither the cookie nor any shard gets a 404 and the same
error page, which tells the user to return to the app.
## Test the change
Unit tests live in `src/api/core/sso_cookie_vendor.rs` under `#[cfg(test)] mod
tests`. Run them with:
```bash
cargo test --features sqlite -- sso_cookie_vendor
```
They cover:
- A single cookie, the common case.
- Sharded cookies, forwarded in suffix order regardless of map iteration order.
- An unsuffixed cookie taking precedence when shards are also present.
- A missing cookie producing a 404.
- Percent-encoding of values holding spaces and reserved characters.
- An oversize cookie producing a link past the 8192-byte cap.
- The error page matching the Bitwarden server's HTML.
## References
- [bitwarden/server#6880][pr-6880]: configuration infrastructure.
- [bitwarden/server#6892][pr-6892]: exposing the configuration in `/api/config`.
- [bitwarden/server#6903][pr-6903]: the endpoint implementation.
- [bitwarden/clients#18476][pr-18476]: the web-vault connector page, shipped in
Vaultwarden 2026.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

29
src/api/core/mod.rs

@ -8,6 +8,7 @@ mod folders;
mod organizations;
mod public;
mod sends;
mod sso_cookie_vendor;
pub use accounts::purge_auth_requests;
pub use ciphers::{CipherData, CipherSyncData, CipherSyncType, purge_trashed_ciphers};
@ -51,6 +52,12 @@ pub fn routes() -> Vec<Route> {
routes.append(&mut hibp_routes);
routes.append(&mut meta_routes);
// Mounted only when the feature is on, so that installs without an authenticating proxy in
// front of them keep answering /api/sso-cookie-vendor with the standard 404.
if CONFIG.sso_cookie_vendor_enabled() {
routes.append(&mut sso_cookie_vendor::routes());
}
routes
}
@ -221,6 +228,23 @@ fn config() -> Json<Value> {
);
feature_states.insert("pm-19148-innovation-archive".to_owned(), true);
// Tells clients whether reaching this server takes extra work beyond a plain HTTPS request.
// A populated bootstrap block sends the Bitwarden apps through the SSO cookie vending flow in
// api::core::sso_cookie_vendor; null means the server is reachable directly.
// See: https://github.com/bitwarden/server/pull/6892
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
@ -248,16 +272,13 @@ fn config() -> Json<Value> {
"sso": "",
"cloudRegion": null,
},
"communication": communication,
// Bitwarden uses this for the self-hosted servers to indicate the default push technology
"push": {
"pushTechnology": 0,
"vapidPublicKey": null
},
"featureStates": feature_states,
// Not supported right now
// Used for by clients to learn if the server requires extra work to establish a connection.
// See: https://github.com/bitwarden/server/pull/6892 | https://github.com/bitwarden/server/commit/52955d1860b4dfb905f67bbe39d9b10bbd61ded0
"communication": null,
"object": "config",
}))
}

299
src/api/core/sso_cookie_vendor.rs

@ -0,0 +1,299 @@
//! SSO cookie vending for the Bitwarden mobile and desktop apps behind an authenticating proxy.
//!
//! When Vaultwarden runs behind a reverse proxy that gates every request on an identity provider
//! (Cloudflare Access, Authentik, Authelia, oauth2-proxy), the Bitwarden mobile and desktop apps
//! cannot finish logging in. The proxy answers their API calls with a browser redirect to the
//! identity provider, and their HTTP clients have no cookie jar or browser to follow it with.
//!
//! Bitwarden's answer is cookie vending. The server advertises the flow through the
//! `communication.bootstrap` object in `/api/config`, the app opens a system browser at the
//! identity provider, and the browser lands on the route in this module once the proxy has set its
//! auth cookie. The route hands that cookie back to the app as a `bitwarden://` deep link, and the
//! app attaches it to every later API request so the proxy lets those requests through.
//!
//! Vaultwarden authenticates nobody here. The proxy is the gate, and the vault's own master
//! password unlock still runs afterwards. The route is registered only when
//! `SSO_COOKIE_VENDOR_ENABLED` is true, so installs that have not opted in are unaffected.
//!
//! This is the server half of bitwarden/server#6880, #6892, and #6903. For operator-facing setup,
//! see `docs/sso-cookie-vendor.md`.
use std::collections::HashMap;
use rocket::{
Route,
http::{CookieJar, Status},
response::{Redirect, content::RawHtml as Html},
};
use crate::CONFIG;
/// Maximum length of the `bitwarden://` deep link, in bytes.
///
/// Matches the limit the Bitwarden server enforces, so an oversize token fails the same way on
/// both servers instead of producing a link the app or the operating system truncates silently.
const MAX_REDIRECT_URI_LENGTH: usize = 8192;
/// Number of sharded cookie suffixes to look for, `-0` through `-19`.
///
/// Cloudflare Access splits its auth JWT across numbered cookies when the token outgrows the
/// per-cookie size limit, so reading only the unsuffixed name would miss the token entirely.
const MAX_SHARD_COUNT: usize = 20;
/// Returns the routes this module serves.
///
/// The caller in `api::core::routes` invokes this only when `SSO_COOKIE_VENDOR_ENABLED` is true,
/// so the endpoint does not exist on installs that leave the feature off.
pub fn routes() -> Vec<Route> {
routes![sso_cookie_vendor]
}
/// Returns the HTML error page for `status_code`, in the format the Bitwarden server uses.
///
/// A browser renders this response, not an API client, so the page tells the user to return to the
/// app rather than describing why the lookup failed.
fn error_html(status_code: u16) -> Html<String> {
Html(format!(
"<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"><title>Error</title></head>\
<body><p>Error code {status_code}. Please return to the Bitwarden app and try again.</p></body></html>"
))
}
/// Vends the reverse proxy's auth cookie to the calling app as a `bitwarden://` deep link.
///
/// The browser arrives at `GET /api/sso-cookie-vendor` after the proxy has authenticated the user,
/// so the request already carries the proxy's cookie. The response is a 302 to
/// `bitwarden://sso-cookie-vendor?<cookie-name>=<value>&d=1`, which the operating system hands to
/// the Bitwarden app. The `d=1` parameter is the sentinel Bitwarden's clients look for.
///
/// # Errors
///
/// Every failure renders an HTML page rather than a JSON body, because a browser displays it:
///
/// - 500 when `SSO_COOKIE_VENDOR_COOKIE_NAME` is empty. Config validation rejects that combination
/// at startup and on admin-panel updates, so reaching it means the config was bypassed.
/// - 404 when the request carries neither the cookie nor any of its shards. This is the response
/// an unconfigured Bitwarden server gives, and the clients already handle it.
/// - 400 when the deep link would exceed `MAX_REDIRECT_URI_LENGTH`.
#[get("/sso-cookie-vendor")]
fn sso_cookie_vendor(cookies: &CookieJar<'_>) -> Result<Redirect, (Status, Html<String>)> {
let cookie_name = CONFIG.sso_cookie_vendor_cookie_name();
if cookie_name.is_empty() {
return Err((Status::InternalServerError, error_html(500)));
}
// Copy the relevant cookies out of the jar so that link building stays a plain function over a
// map, which the tests below can drive without standing up a request.
let mut cookie_map = HashMap::new();
if let Some(cookie) = cookies.get(&cookie_name) {
cookie_map.insert(cookie_name.clone(), cookie.value().to_owned());
}
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_owned());
}
}
let redirect_uri = build_redirect_uri(&cookie_name, &cookie_map)?;
// Measured on the finished link rather than on the raw cookie, because percent-encoding and
// the shard names both count against what the app has to receive.
if redirect_uri.len() > MAX_REDIRECT_URI_LENGTH {
return Err((Status::BadRequest, error_html(400)));
}
Ok(Redirect::found(redirect_uri))
}
/// Builds the `bitwarden://` deep link from the cookies found on the request.
///
/// An unsuffixed cookie wins over any shards, matching the Bitwarden server. When only shards are
/// present, every shard found is forwarded in ascending suffix order so the app can reassemble the
/// token. The counting loop, not the map's iteration order, is what makes that order deterministic.
///
/// # Errors
///
/// Returns 404 and an HTML error page when neither the unsuffixed cookie nor any shard is present.
fn build_redirect_uri(cookie_name: &str, cookies: &HashMap<String, String>) -> Result<String, (Status, Html<String>)> {
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"));
}
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<String> = shards.into_iter().map(|(name, value)| format!("{name}={value}")).collect();
Ok(format!("bitwarden://sso-cookie-vendor?{}&d=1", params.join("&")))
}
/// Percent-encodes a cookie value for the deep link's query string.
///
/// Uses `application/x-www-form-urlencoded` serialization so a value containing `&` or `=` cannot
/// be read by the receiving app as an extra query parameter.
fn url_encode(value: &str) -> String {
url::form_urlencoded::byte_serialize(value.as_bytes()).collect()
}
#[cfg(test)]
mod tests {
//! The tests drive `build_redirect_uri` directly rather than the route, because the route needs a
//! live `CookieJar` and the configured cookie name. The status codes the route maps these
//! results to are documented on `sso_cookie_vendor`.
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("<!DOCTYPE html>"));
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();
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();
// The unsuffixed cookie wins, and no shard reaches the link.
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_oversize_cookie_exceeds_uri_limit() {
let mut cookies = HashMap::new();
// A cookie long enough to push the finished link past the cap.
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();
// Building succeeds. The 400 comes from `sso_cookie_vendor`, which applies the cap.
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();
// Inserted out of order: the link must still come back in suffix order.
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 appear as 0, 1, 2 whatever order the map yields them in.
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"));
}
}

28
src/config.rs

@ -849,6 +849,22 @@ make_config! {
sso_debug_tokens: bool, true, def, false;
},
/// SSO Cookie Vendor settings
sso_cookie_vendor {
/// Enabled |> Serve `/api/sso-cookie-vendor` and advertise the flow in `/api/config`
/// Set this when Vaultwarden sits behind a reverse proxy that authenticates every request, such as
/// Cloudflare Access, Authentik, Authelia, or oauth2-proxy. Without it the Bitwarden mobile and desktop
/// apps cannot finish logging in, because the proxy answers their API calls with a browser redirect they
/// cannot follow. The three settings below are required while this is enabled.
sso_cookie_vendor_enabled: bool, true, def, false;
/// IdP Login URL |> URL the app opens in a browser to authenticate, for example, the Cloudflare Access login URL for this vault
sso_cookie_vendor_idp_login_url: String, true, def, String::new();
/// Cookie Name |> Name of the cookie the proxy sets on authenticated requests, for example, `CF_Authorization`
sso_cookie_vendor_cookie_name: String, true, def, String::new();
/// Cookie Domain |> Domain scope of the proxy auth cookie, for example, `vault.example.com`
sso_cookie_vendor_cookie_domain: String, true, def, String::new();
},
/// Yubikey settings
yubico: _enable_yubico {
/// Enabled
@ -1116,6 +1132,18 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> {
validate_sso_master_password_policy(cfg.sso_master_password_policy.as_ref())?;
}
// Refuse a half-configured cookie vendor: with any of these blank the endpoint would hand
// clients a bootstrap block they cannot act on, or answer with a 500.
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_ENABLED` is true"
)
}
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")

Loading…
Cancel
Save