@ -1,91 +1,98 @@
# SSO Cookie Vendor — Native App Support Behind Authenticating Reverse Proxies
# 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
## Background
Users of Vaultwarden frequently put it behind an authenticating reverse proxy —
Putting Vaultwarden behind an authenticating proxy means only users who pass
most commonly **Cloudflare Access** or similar Zero Trust gateways — so that
your identity provider (IdP) reach the vault at all. Bots cannot crawl the
only authenticated users can reach the vault at all. This is a strong defensive
endpoint, credential stuffing never reaches the login form, and the exposed
layer: bots can't crawl the endpoint, credential-stuffing never reaches the
surface shrinks to the proxy.
login form, and the attack surface drops to "whoever passes my IdP."
The cost is that the Bitwarden mobile and desktop apps can no longer finish
The problem is that when the proxy sits in front of the API, the **native
logging in. The proxy expects a browser with a cookie jar and OAuth 2.0 redirect
Bitwarden clients (mobile, desktop)** can no longer complete their login flow.
support, and the apps' HTTP clients have neither. After the browser step, the
The proxy expects a browser with a cookie jar and OAuth redirect support; the
apps receive the proxy's HTML login page where they expect JSON from
native apps' HTTP clients have neither. After the browser-assisted IdP step,
Vaultwarden, and login stalls.
the client is stuck — requests to the API come back as HTML login pages from
the proxy instead of JSON from Vaultwarden.
Bitwarden solved this in their own server in February 2026 with a flow called
SSO cookie vending:
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
1. The server states in `/api/config` that it sits behind an authenticating
behind an authenticating proxy, and exposes an endpoint (`/api/sso-cookie-vendor`)
proxy, and names the IdP login URL and the cookie to look for.
that reads the proxy's auth cookie after the user authenticates in a browser
2. The app opens a system browser at that IdP login URL.
and hands it back to the native app via a `bitwarden://` deep link. The app
3. After the user authenticates, the proxy sets its cookie and the browser
then attaches that cookie to every subsequent API request, and the proxy lets
reaches `/api/sso-cookie-vendor` .
those requests through.
4. The server reads the cookie and redirects the browser to a `bitwarden://`
deep link carrying it.
See the upstream PRs: [bitwarden/server#6880][pr-6880],
5. The app attaches that cookie to every later API request, and the proxy lets
[bitwarden/server#6892][pr-6892], [bitwarden/server#6903][pr-6903],
those requests through.
[bitwarden/clients#18476][pr-18476], [bitwarden/clients#19392][pr-19392].
Vaultwarden 2026.2.0 shipped the web-vault connector page from
Vaultwarden shipped the web-vault connector page (from
[bitwarden/clients#18476][pr-18476], but not the two server-side pieces the flow
`bitwarden/clients#18476` ) as part of v2026.2.0, but the server-side pieces
needs: the `/api/sso-cookie-vendor` endpoint and the `communication.bootstrap`
(`/api/sso-cookie-vendor` and the `communication.bootstrap` advertisement in
block in `/api/config` . Without them the apps detect the connector page, open a
`/api/config` ) were missing. Native apps would detect the web-vault connector,
browser, complete the proxy's authentication, and then get a 404 when they try
open a browser, complete the Access auth, and then 404 when they tried to
to collect the cookie. This change adds both pieces.
hand the cookie off. This change adds the two missing server pieces.
## What this adds
## What this change does
A configuration section, `sso_cookie_vendor` , holding four settings:
Four things:
| Setting | Purpose |
1. **Adds a new config section** `sso_cookie_vendor` with four fields:
|---|---|
- `SSO_COOKIE_VENDOR_ENABLED` — master switch (default `false` )
| `SSO_COOKIE_VENDOR_ENABLED` | Turns the feature on. Defaults to `false` . |
- `SSO_COOKIE_VENDOR_IDP_LOGIN_URL` — the URL the app should navigate to
| `SSO_COOKIE_VENDOR_IDP_LOGIN_URL` | URL the app opens in a browser to authenticate. |
in a browser for IdP authentication (e.g. the Cloudflare Access login
| `SSO_COOKIE_VENDOR_COOKIE_NAME` | Name of the cookie the proxy sets on authenticated requests. |
URL for your Vaultwarden application)
| `SSO_COOKIE_VENDOR_COOKIE_DOMAIN` | Domain scope of that cookie. |
- `SSO_COOKIE_VENDOR_COOKIE_NAME` — the name of the cookie the proxy sets
on authenticated requests (e.g. `CF_Authorization` for Cloudflare Access)
On top of those settings, this change:
- `SSO_COOKIE_VENDOR_COOKIE_DOMAIN` — the cookie's domain scope
2. **Advertises the configuration** in the `/api/config` response as a
- Publishes the three string settings in `/api/config` as a
`communication.bootstrap` object, matching the shape Bitwarden's clients
`communication.bootstrap` object, in the shape Bitwarden's clients already
already expect from `bitwarden/server#6892` .
read from [bitwarden/server#6892][pr-6892].
3. **Adds the `/api/sso-cookie-vendor` endpoint** that reads the proxy cookie
- Serves `GET /api/sso-cookie-vendor` , which reads the proxy's cookie from the
from the incoming request and 302-redirects to
request and returns a 302 to
`bitwarden://sso-cookie-vendor?<cookie-name>=<url-encoded-value>&d=1` .
`bitwarden://sso-cookie-vendor?COOKIE_NAME=COOKIE_VALUE&d=1` . Replace
4. **Validates config at startup** : if `SSO_COOKIE_VENDOR_ENABLED=true` but
`COOKIE_NAME` and `COOKIE_VALUE` with your configured cookie name and its
any of the three string fields is empty, Vaultwarden refuses to start with
percent-encoded value; `d=1` is the sentinel the clients look for.
a clear error message.
- 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 endpoint is only registered when the feature is enabled, so disabled
installs behave exactly as before — no new attack surface.
The route is registered only when the feature is on. With
`SSO_COOKIE_VENDOR_ENABLED=false` , Vaultwarden serves exactly the routes it
### Sharded cookie support
served before.
Cloudflare Access can split its auth JWT across multiple cookies when the JWT
### Sharded cookies
grows past browser size limits (`CF_Authorization-0`, `CF_Authorization-1` ,
…). The endpoint checks for up to 20 shards (`{name}-0` through `{name}-19` )
Cloudflare Access splits its auth JWT across numbered cookies, such as
and forwards all present shards in a single deep link. A non-sharded cookie,
`CF_Authorization-0` and `CF_Authorization-1` , when the token outgrows the
if present, takes precedence (matching upstream Bitwarden's semantics).
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,
### Why this belongs in the server and not in a reverse-proxy shim
so the app can reassemble the token. An unsuffixed cookie takes precedence over
any shards, matching the Bitwarden server.
The original workaround for Cloudflare Access users was a small Cloudflare
Worker that intercepted `/api/config` and `/api/sso-cookie-vendor` and
### Why this belongs in the server
injected the same behavior. That works, but:
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.
- Every user behind Cloudflare Access has to deploy and maintain a Worker.
- A Worker only helps Cloudflare Access users — Authentik, Authelia,
- A Worker helps only Cloudflare Access users. Authentik, Authelia,
oauth2-proxy, and any other authenticating proxy that drops a cookie can
oauth2-proxy, and any other authenticating proxy that sets a cookie can use
use the exact same flow, but each would need its own shim.
the same flow, but each one needs its own shim.
- The `communication.bootstrap` block is a fi rs t-class feature of Bitwarden's
- `communication.bootstrap` is p art of Bitwarden's `/api/config` contract, so it
`/api/config` contract — it should come from the server, not a proxy layer.
belongs in the server rather than in a proxy layer.
Putting the logic in Vaultwarden makes any authenticating proxy work with
Implementing it in Vaultwarden makes any authenticating proxy work with the
native clients just by flipping four env var s.
mobile and desktop apps once you set four environment variable s.
## How to enable i t
## Configure the endpoin t
In your `.env` (or `config.json` , or the admin UI) :
Set the four settings in `.env` , in `config.json` , or through the admin panel :
```bash
```bash
SSO_COOKIE_VENDOR_ENABLED=true
SSO_COOKIE_VENDOR_ENABLED=true
@ -94,84 +101,97 @@ SSO_COOKIE_VENDOR_COOKIE_NAME=CF_Authorization
SSO_COOKIE_VENDOR_COOKIE_DOMAIN=vault.example.com
SSO_COOKIE_VENDOR_COOKIE_DOMAIN=vault.example.com
```
```
### Cloudflare Access specifics
### Cloudflare Access
`SSO_COOKIE_VENDOR_IDP_LOGIN_URL` is the "Access Login URL" shown on the
- `SSO_COOKIE_VENDOR_IDP_LOGIN_URL` is the Access Login URL on the
application's details page (format:
application's details page. It takes the form
`https://<team>.cloudflareaccess.com/cdn-cgi/access/login/<your-domain>` ).
`https://TEAM.cloudflareaccess.com/cdn-cgi/access/login/YOUR_DOMAIN` , where
`SSO_COOKIE_VENDOR_COOKIE_NAME` is always `CF_Authorization` for Cloudflare
`TEAM` is your Cloudflare Zero Trust team name and `YOUR_DOMAIN` is the
Access. `SSO_COOKIE_VENDOR_COOKIE_DOMAIN` is the domain your Access
hostname the Access application protects.
application protects.
- `SSO_COOKIE_VENDOR_COOKIE_NAME` is always `CF_Authorization` .
- `SSO_COOKIE_VENDOR_COOKIE_DOMAIN` is the domain the Access application
### Other proxies (Authentik, Authelia, oauth2-proxy, …)
protects.
Any reverse proxy that (a) redirects unauthenticated requests to a
### Other authenticating proxies
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
Any proxy works that redirects unauthenticated requests to a browser-based IdP
and `SSO_COOKIE_VENDOR_COOKIE_NAME` / `SSO_COOKIE_VENDOR_COOKIE_DOMAIN` to
flow and sets a cookie on the authenticated response. Set
the cookie your proxy sets on authenticated sessions.
`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
## End-to-end flow (what the user sees)
cookie the proxy sets on authenticated sessions.
1. User opens the Bitwarden app and points it at their Vaultwarden server.
Note: Cloudflare Access is the only proxy this has run against in production.
2. App fetches `/api/config` , sees `communication.bootstrap.type == "ssoCookieVendor"` ,
The others meet the requirements above, but no one has reported a tested
and knows to use the cookie-vending flow.
configuration for them yet.
3. App shows a "sync your browser" prompt and opens the system browser at
`idpLoginUrl` .
## How the login flow runs
4. Browser is redirected through the IdP (Google, GitHub, Okta, …). User
authenticates.
From the user's side, with the feature configured:
5. Proxy sets its auth cookie on the response and redirects the browser to
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` .
`/api/sso-cookie-vendor` .
6. Vaultwarden receives the request, pulls the cookie out of the jar, and
6. Vaultwarden reads the cookie off the request and redirects the browser to
302-redirects the browser to
`bitwarden://sso-cookie-vendor?CF_Authorization=COOKIE_VALUE&d=1` .
`bitwarden://sso-cookie-vendor?CF_Authorization=<value>&d=1` .
7. The operating system hands the deep link to the Bitwarden app.
7. The OS hands the deep link back to the Bitwarden app.
8. The app stores the cookie and sends it with every later API request. The
8. App stores the cookie value and attaches it to every subsequent API
proxy recognizes the cookie, lets the request through, and the app continues
request. The proxy sees the cookie, lets the request through, and the app
to the usual master password unlock.
continues with the normal Bitwarden master-password unlock.
The apps need no changes. This uses the cookie vending support Bitwarden's
No app-side modifications are required — this uses the cookie-vending support
clients already ship.
Bitwarden's clients already ship.
## Security notes
## Security considerations
- The endpoint exists only when `SSO_COOKIE_VENDOR_ENABLED` is `true` . An
- The endpoint is only registered when `SSO_COOKIE_VENDOR_ENABLED=true` .
install that leaves the feature off serves the same routes it served before.
Default-off installs are byte-identical to current behavior.
- The endpoint reads a cookie from a request the proxy has already
- The endpoint **reads the cookie from an already-authenticated request** —
authenticated. The proxy validates the IdP session before the request reaches
the proxy has already validated the IdP session before the request ever
Vaultwarden, so this adds no authentication boundary of its own.
reaches Vaultwarden. No new authentication boundary is introduced.
- The redirect moves the cookie between two parties that both already hold it:
- The deep-link response never crosses a trust boundary the browser wasn't
the browser that received it and the app on the same device. The proxy
already on: the browser holds the same cookie, the app holds the same
validates the same cookie in either case.
cookie, the proxy validates the same cookie.
- Vaultwarden's own authentication still applies. The user unlocks the vault
- Vaultwarden's own authentication (master password) is still required after
with their master password after the proxy gate, so this does not weaken the
the proxy gate — this feature does not weaken the vault.
vault.
- Deep-link length is capped at 8192 bytes to match the upstream Bitwarden
- The deep link is capped at 8192 bytes, matching the Bitwarden server. A
limit; oversize requests return HTTP 400 with the standard error page.
request that would exceed the cap gets a 400 and an HTML error page.
- Missing/empty cookie returns HTTP 404 with the upstream-compatible error
- A request carrying neither the cookie nor any shard gets a 404 and the same
page telling the user to return to the app.
error page, which tells the user to return to the app.
## Testing
## Test the change
Unit tests live inline in `src/api/core/sso_cookie_vendor.rs` under the usual
Unit tests live in `src/api/core/sso_cookie_vendor.rs` under `#[cfg(test)] mod
`#[cfg(test)] mod tests` pattern. They cover:
tests`. Run them with:
- Single-cookie happy path
```bash
- Sharded cookies (ordered 0..19)
cargo test --features sqlite -- sso_cookie_vendor
- Single cookie takes precedence over shards when both are present
```
- Missing cookie → 404
- URL-encoding of cookie values with spaces and special characters
They cover:
- Oversize URI handling
- Error-page HTML matches the upstream Bitwarden format
- A single cookie, the common case.
- Sharded cookies, forwarded in suffix order regardless of map iteration order.
Run with `cargo test --features sqlite -- sso_cookie_vendor` .
- 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
## References
- [bitwarden/server#6880][pr-6880] — Config infrastructure
- [bitwarden/server#6880][pr-6880]: configuration infrastructure.
- [bitwarden/server#6892][pr-6892] — Expose config in `/api/config`
- [bitwarden/server#6892][pr-6892]: exposing the configuration in `/api/config` .
- [bitwarden/server#6903][pr-6903] — Endpoint implementation
- [bitwarden/server#6903][pr-6903]: the endpoint implementation.
- [bitwarden/clients#18476][pr-18476] — Web-vault connector page (already in Vaultwarden v2026.2.0)
- [bitwarden/clients#18476][pr-18476]: the web-vault connector page, shipped in
- [bitwarden/clients#19392][pr-19392] — Client-side cookie acquisition
Vaultwarden 2026.2.0.
- [bitwarden/clients#19392][pr-19392]: client-side cookie acquisition.
[pr-6880]: https://github.com/bitwarden/server/pull/6880
[pr-6880]: https://github.com/bitwarden/server/pull/6880
[pr-6892]: https://github.com/bitwarden/server/pull/6892
[pr-6892]: https://github.com/bitwarden/server/pull/6892