fix: use token_hash for email lookup and expiry notifications

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
HugoCasa
2026-03-12 12:55:28 +01:00
co-authored by Claude Opus 4.6
parent 5d549f931f
commit 742fa00252
12 changed files with 27 additions and 125 deletions
@@ -1,12 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM token_expiry_notification n\n USING token t\n WHERE n.token = t.token\n AND n.expiration > now()\n AND n.expiration <= now() + interval '7 days'\n RETURNING substring(t.token for 10) as token_prefix, t.label, t.email, t.workspace_id",
"query": "DELETE FROM token_expiry_notification n\n USING token t\n WHERE n.token = t.token_hash\n AND n.expiration > now()\n AND n.expiration <= now() + interval '7 days'\n RETURNING t.token_prefix, t.label, t.email, t.workspace_id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token_prefix",
"type_info": "Text"
"type_info": "Varchar"
},
{
"ordinal": 1,
@@ -28,11 +28,11 @@
"Left": []
},
"nullable": [
null,
true,
true,
true,
true
]
},
"hash": "d7e9b69fef8369117ce057d01d87288b39ea7c802007f112eb3d62230d07abb6"
"hash": "08c39f2d9e622313e670f869e23538ac8507fc8b1a3427a6b94ffb15fe43a1fa"
}
@@ -1,12 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM token WHERE expiration <= now()\n RETURNING substring(token for 10) as token_prefix, label, email, workspace_id",
"query": "DELETE FROM token WHERE expiration <= now()\n RETURNING token_prefix, label, email, workspace_id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token_prefix",
"type_info": "Text"
"type_info": "Varchar"
},
{
"ordinal": 1,
@@ -28,11 +28,11 @@
"Left": []
},
"nullable": [
null,
false,
true,
true,
true
]
},
"hash": "bb446cbb20166f274a7ee6e88abaa27e233e60e18b3d35545005eb680701241f"
"hash": "104fc7e5433abd7247323c5ef76b85f937776a6b47cd99c648bb4d819d3cfe57"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "WITH email_lookup AS (\n SELECT email FROM token WHERE token = $1\n )\n DELETE FROM token\n WHERE email = (SELECT email FROM email_lookup) AND label = 'session'\n RETURNING email",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "1bdf186d3b99bbd913cbf95150105470cd5f1d4ddbb147cb8ce46f9d1da5dfaf"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT job FROM token WHERE token = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "job",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "55cf43cb9219b43f8e9f94b23b62846cd0b1ef5f64d20b0d975d0058730f427b"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM token WHERE token = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "66e0968fe9f757755945a7010153821cf73ace9d6692750ccc4cca37701ed77a"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM token WHERE token = $1 RETURNING email",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "97e364c703bdcdfb5345810659cbe0477a28b8199ef0b297f9a22c88a43b6b5c"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email FROM token WHERE token = $1 AND (expiration > NOW() OR expiration IS NULL)",
"query": "SELECT email FROM token WHERE token_hash = $1 AND (expiration > NOW() OR expiration IS NULL)",
"describe": {
"columns": [
{
@@ -18,5 +18,5 @@
true
]
},
"hash": "19a7ebb2e7e8e57b6e7c974da8eb7c6841a5c4ff12ba7c12c73d691c49dd99ed"
"hash": "983c21be4341a7ff9eb647041aa3642a89b16701e71d624c6adacb652e231a1a"
}
@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT true FROM token WHERE token = $1 and expiration IS NOT NULL and expiration > now() + $2::int * '1 sec'::interval",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Int4"
]
},
"nullable": [
null
]
},
"hash": "bfff3d8df18db198d6ebba8a049b00147fc8bcd42f3df37ef81b9ded80974bd0"
}
+6 -4
View File
@@ -945,7 +945,7 @@ pub async fn delete_expired_items(db: &DB) -> () {
let expired_tokens_r = sqlx::query_as!(
TokenRow,
"DELETE FROM token WHERE expiration <= now()
RETURNING concat(token_prefix, '*****')",
RETURNING token_prefix, label, email, workspace_id",
)
.fetch_all(db)
.await;
@@ -1164,15 +1164,17 @@ pub async fn delete_expired_items(db: &DB) -> () {
}
pub async fn check_expiring_tokens(db: &DB) {
// Find tokens expiring within 7 days that still have a pending notification row
// Find tokens expiring within 7 days that still have a pending notification row.
// The notification table stores token_hash (not plaintext) so the join works
// even after the hash migration makes token.token nullable.
let expiring_tokens_r = sqlx::query_as!(
TokenRow,
"DELETE FROM token_expiry_notification n
USING token t
WHERE n.token = t.token
WHERE n.token = t.token_hash
AND n.expiration > now()
AND n.expiration <= now() + interval '7 days'
RETURNING substring(t.token for 10) as token_prefix, t.label, t.email, t.workspace_id",
RETURNING t.token_prefix, t.label, t.email, t.workspace_id",
)
.fetch_all(db)
.await;
+5 -4
View File
@@ -44,13 +44,14 @@ lazy_static::lazy_static! {
/// Get email from a valid token, with caching.
/// Used for WM_END_USER_EMAIL when user is authenticated but not a workspace member.
async fn get_email_from_token(db: &DB, token: &str) -> Option<String> {
if let Some(cached) = TOKEN_EMAIL_CACHE.get(token) {
let t_hash = hash_token(token);
if let Some(cached) = TOKEN_EMAIL_CACHE.get(&t_hash) {
return cached;
}
let email = sqlx::query_scalar!(
"SELECT email FROM token WHERE token = $1 AND (expiration > NOW() OR expiration IS NULL)",
token
"SELECT email FROM token WHERE token_hash = $1 AND (expiration > NOW() OR expiration IS NULL)",
t_hash
)
.fetch_optional(db)
.await
@@ -58,7 +59,7 @@ async fn get_email_from_token(db: &DB, token: &str) -> Option<String> {
.flatten()
.flatten(); // email column is nullable, so we get Option<Option<String>>
TOKEN_EMAIL_CACHE.insert(token.to_string(), email.clone());
TOKEN_EMAIL_CACHE.insert(t_hash, email.clone());
email
}
+5 -3
View File
@@ -575,7 +575,7 @@ pub async fn create_token_internal(
register_token_expiry_notification(
&mut *tx,
&token,
&t_hash,
token_config.label.as_deref(),
token_config.expiration,
)
@@ -597,12 +597,14 @@ pub async fn create_token_internal(
}
/// Insert a pending expiry notification row for user tokens that have an expiration.
/// Stores the token_hash so the join in check_expiring_tokens works even when
/// the plaintext token column is NULL (after hash migration).
/// When updating this filter, also update:
/// - `is_user_token` in src/monitor.rs
/// - `isUserToken` in frontend/src/lib/components/settings/TokensTable.svelte
pub async fn register_token_expiry_notification(
tx: &mut sqlx::PgConnection,
token: &str,
token_hash: &str,
label: Option<&str>,
expiration: Option<chrono::DateTime<chrono::Utc>>,
) {
@@ -619,7 +621,7 @@ pub async fn register_token_expiry_notification(
}
if let Err(e) = sqlx::query!(
"INSERT INTO token_expiry_notification (token, expiration) VALUES ($1, $2) ON CONFLICT DO NOTHING",
token,
token_hash,
expiration,
)
.execute(&mut *tx)
+1 -1
View File
@@ -1880,7 +1880,7 @@ async fn impersonate(
windmill_api_auth::register_token_expiry_notification(
&mut *tx,
&token,
&t_hash,
new_token.label.as_deref(),
new_token.expiration,
)