Files
windmill/backend/windmill-store/src/oauth_refresh_oss.rs
T
Ruben FiszelandClaude Opus 4.8 76c0d970a1 fix(oauth): persist refreshed token through configured secret backend (#9471)
The lazy on-fetch OAuth token refresh persisted the new access token with a raw
`UPDATE variable SET value = <db-encrypted>`, bypassing the secret-backend
abstraction. With an external secret backend (AWS Secrets Manager / Azure Key
Vault / Vault), secret reads resolve through the backend and ignore
`variable.value` entirely, so refresh advanced `account.expires_at` and updated
Postgres but never wrote the new token to the external store. Every read that
did not itself trigger a mint kept serving the frozen connect-time token, which
expired ~1h after connect (RefreshError on Google clients).

`windmill-oauth` can't depend on `windmill-store` (circular), so variable
persistence moves out of `refresh_token{,_for_account}` (which now only exchange
the token + update the `account` row and return the new token) into the
`windmill-store` callers, via a new `store_oauth_token_value` helper that writes
through the configured backend and stores the returned value (encrypted blob for
the DB backend, `$...:` marker for external backends) in `variable.value`.

If persisting the refreshed token fails (more likely now that it can be a
network write to an external backend) after the account was committed fresh,
`store_oauth_token_value` resets `expires_at` to the past and records
`refresh_error` — looking the account up via `variable.account` — so the next
fetch retries instead of serving the stale token for the whole token lifetime.

Also add `windmill-store/tests/oauth_refresh_secret_backend.rs`, an opt-in e2e
regression suite (RUN_SECRET_BACKEND_E2E / RUN_AWS_SM_TESTS) covering database
and external (AWS SM via LocalStack) backends plus the self-healing reset.
Verified against Postgres + LocalStack: 3 passed.

EE companion (oauth_refresh_ee.rs: 3 refresh paths) merged via #607; this OSS
half completes the fix (ee-repo-ref already at EE main 481ea7f).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:52:23 +00:00

45 lines
1.2 KiB
Rust

/*
* Author: Windmill Labs, Inc
* Copyright: Windmill Labs, Inc 2024
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
#[cfg(feature = "private")]
pub use crate::oauth_refresh_ee::_refresh_token;
#[cfg(not(feature = "private"))]
use sqlx::{Postgres, Transaction};
#[cfg(not(feature = "private"))]
use windmill_common::db::DB;
#[cfg(not(feature = "private"))]
use windmill_common::error;
#[cfg(not(feature = "private"))]
pub async fn _refresh_token<'c>(
tx: Transaction<'c, Postgres>,
path: &str,
w_id: &str,
id: i32,
db: &DB,
) -> error::Result<String> {
let token = windmill_oauth::refresh_token(
tx,
w_id,
id,
db,
&*windmill_oauth::OAUTH_CLIENTS.load(),
&windmill_oauth::OAUTH_HTTP_CLIENT,
include_str!("../../oauth_connect.json"),
)
.await?;
// Persist the refreshed token through the configured secret backend so an
// external backend (Vault / Azure KV / AWS Secrets Manager) is updated too,
// not just the in-DB variable mirror.
crate::secret_backend_ext::store_oauth_token_value(db, w_id, path, &token).await?;
Ok(token)
}