From 23bc2c4eee282a2c79c95f0d5aea49fa7ea5fd4f Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 4 Mar 2026 09:28:21 +0000 Subject: [PATCH] feat: branch on MIN_VERSION to write plaintext token or null Check MIN_VERSION_SUPPORTS_TOKEN_HASH at runtime: write plaintext to token column while old workers exist, switch to NULL once all workers are >= 1.649.0. Co-Authored-By: Claude Opus 4.6 --- backend/windmill-api-auth/src/lib.rs | 13 +++++++++++-- backend/windmill-api-users/src/users.rs | 18 ++++++++++++++++-- backend/windmill-api/src/mcp/oauth_server.rs | 15 +++++++++++++-- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/backend/windmill-api-auth/src/lib.rs b/backend/windmill-api-auth/src/lib.rs index 77e1dd737b..f3f42b6a17 100644 --- a/backend/windmill-api-auth/src/lib.rs +++ b/backend/windmill-api-auth/src/lib.rs @@ -514,12 +514,21 @@ pub async fn create_token_internal( ) -> Result { use tracing::Instrument; use windmill_audit::{audit_oss::audit_log, ActionKind}; - use windmill_common::{utils::rd_string, worker::CLOUD_HOSTED}; + use windmill_common::{ + min_version::MIN_VERSION_SUPPORTS_TOKEN_HASH, utils::rd_string, worker::CLOUD_HOSTED, + }; let token = rd_string(32); let t_hash = hash_token(&token); let t_prefix = &token[..TOKEN_PREFIX_LEN]; + // Write plaintext token column until all workers support hash-based lookup + let plaintext: Option<&str> = if MIN_VERSION_SUPPORTS_TOKEN_HASH.met().await { + None + } else { + Some(&token) + }; + let is_super_admin = sqlx::query_scalar!( "SELECT super_admin FROM password WHERE email = $1", authed.email @@ -548,7 +557,7 @@ pub async fn create_token_internal( )", t_hash, t_prefix, - &token, + plaintext as Option<&str>, authed.email, token_config.label, token_config.expiration, diff --git a/backend/windmill-api-users/src/users.rs b/backend/windmill-api-users/src/users.rs index 94f1d92954..675ce45e7a 100644 --- a/backend/windmill-api-users/src/users.rs +++ b/backend/windmill-api-users/src/users.rs @@ -1746,9 +1746,16 @@ pub async fn create_session_token<'c>( tx: &mut sqlx::Transaction<'c, sqlx::Postgres>, cookies: Cookies, ) -> Result { + use windmill_common::min_version::MIN_VERSION_SUPPORTS_TOKEN_HASH; + let token = rd_string(32); let t_hash = windmill_common::auth::hash_token(&token); let t_prefix = &token[..TOKEN_PREFIX_LEN]; + let plaintext: Option<&str> = if MIN_VERSION_SUPPORTS_TOKEN_HASH.met().await { + None + } else { + Some(&token) + }; if *INVALIDATE_OLD_SESSIONS { sqlx::query!( @@ -1782,7 +1789,7 @@ pub async fn create_session_token<'c>( VALUES ($1, $2, $3, $4, $5, now() + ($6 || ' seconds')::interval, $7)", t_hash, t_prefix, - &token, + plaintext as Option<&str>, email, "session", &MAX_SESSION_VALIDITY_SECONDS.to_string(), @@ -1827,9 +1834,16 @@ async fn impersonate( authed: ApiAuthed, Json(new_token): Json, ) -> Result<(StatusCode, String)> { + use windmill_common::min_version::MIN_VERSION_SUPPORTS_TOKEN_HASH; + let token = rd_string(32); let t_hash = windmill_common::auth::hash_token(&token); let t_prefix = &token[..TOKEN_PREFIX_LEN]; + let plaintext: Option<&str> = if MIN_VERSION_SUPPORTS_TOKEN_HASH.met().await { + None + } else { + Some(&token) + }; require_super_admin(&db, &authed.email).await?; if new_token.impersonate_email.is_none() { @@ -1855,7 +1869,7 @@ async fn impersonate( VALUES ($1, $2, $3, $4, $5, $6, $7)", t_hash, t_prefix, - &token, + plaintext as Option<&str>, impersonated, new_token.label, new_token.expiration, diff --git a/backend/windmill-api/src/mcp/oauth_server.rs b/backend/windmill-api/src/mcp/oauth_server.rs index d5452fd723..b9edbc34f9 100644 --- a/backend/windmill-api/src/mcp/oauth_server.rs +++ b/backend/windmill-api/src/mcp/oauth_server.rs @@ -12,6 +12,7 @@ use sqlx::FromRow; use windmill_common::{ auth::{hash_token, TOKEN_PREFIX_LEN}, error::{Error, Result}, + min_version::MIN_VERSION_SUPPORTS_TOKEN_HASH, utils::rd_string, BASE_URL, DB, }; @@ -386,6 +387,11 @@ async fn handle_authorization_code_grant( let access_token = rd_string(32); let access_token_hash = hash_token(&access_token); let access_token_prefix = &access_token[..TOKEN_PREFIX_LEN]; + let plaintext: Option<&str> = if MIN_VERSION_SUPPORTS_TOKEN_HASH.met().await { + None + } else { + Some(&access_token) + }; let refresh_token = rd_string(32); let token_family = sqlx::types::Uuid::new_v4(); let scopes = auth_code.scopes; @@ -397,7 +403,7 @@ async fn handle_authorization_code_grant( WHERE NOT EXISTS(SELECT 1 FROM workspace WHERE id = $8 AND deleted = true)", access_token_hash, access_token_prefix, - &access_token, + plaintext as Option<&str>, auth_code.user_email, format!("mcp-oauth-{}", auth_code.client_id), MCP_OAUTH_TOKEN_EXPIRATION_SECS.to_string(), @@ -525,6 +531,11 @@ async fn handle_refresh_token_grant( let new_access_token = rd_string(32); let new_access_token_hash = hash_token(&new_access_token); let new_access_token_prefix = &new_access_token[..TOKEN_PREFIX_LEN]; + let new_plaintext: Option<&str> = if MIN_VERSION_SUPPORTS_TOKEN_HASH.met().await { + None + } else { + Some(&new_access_token) + }; let new_refresh_token = rd_string(32); let scopes = token_row.scopes; @@ -535,7 +546,7 @@ async fn handle_refresh_token_grant( WHERE NOT EXISTS(SELECT 1 FROM workspace WHERE id = $8 AND deleted = true)", new_access_token_hash, new_access_token_prefix, - &new_access_token, + new_plaintext as Option<&str>, token_row.user_email, format!("mcp-oauth-{}", token_row.client_id), MCP_OAUTH_TOKEN_EXPIRATION_SECS.to_string(),