mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-05-08 12:34:43 +02:00
Merge a2d60c352b into f21a3adae2
This commit is contained in:
commit
8853169bb9
5 changed files with 489 additions and 0 deletions
|
|
@ -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 ###
|
||||
########################
|
||||
|
|
|
|||
180
docs/sso-cookie-vendor.md
Normal file
180
docs/sso-cookie-vendor.md
Normal file
|
|
@ -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?<cookie-name>=<url-encoded-value>&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://<team>.cloudflareaccess.com/cdn-cgi/access/login/<your-domain>`).
|
||||
`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=<value>&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
|
||||
|
|
@ -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<Route> {
|
|||
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<Value> {
|
|||
);
|
||||
feature_states.insert("pm-19148-innovation-archive".to_string(), true);
|
||||
|
||||
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<Value> {
|
|||
"sso": "",
|
||||
"cloudRegion": null,
|
||||
},
|
||||
"communication": communication,
|
||||
// Bitwarden uses this for the self-hosted servers to indicate the default push technology
|
||||
"push": {
|
||||
"pushTechnology": 0,
|
||||
|
|
|
|||
248
src/api/core/sso_cookie_vendor.rs
Normal file
248
src/api/core/sso_cookie_vendor.rs
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
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<Route> {
|
||||
routes![sso_cookie_vendor]
|
||||
}
|
||||
|
||||
/// Error HTML response matching the official Bitwarden server format.
|
||||
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>"
|
||||
))
|
||||
}
|
||||
|
||||
/// 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<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)));
|
||||
}
|
||||
|
||||
// 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<String, String>) -> Result<String, (Status, Html<String>)> {
|
||||
// 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<String> = 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("<!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();
|
||||
// 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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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,14 @@ 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
|
||||
&& (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")
|
||||
|
|
|
|||
Loading…
Reference in a new issue