committed by
GitHub
5 changed files with 578 additions and 4 deletions
@ -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 |
|||
@ -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")); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue