fix(auth): make JIT provisioning idempotent under concurrent first-login
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled

On a user's first login the authed shell fires several API calls at once;
each ran the JIT-provision path and raced to INSERT the same new user row,
tripping the partial unique index on auth_subject. The losing requests
500'd and the post-login SSR errored out. Use INSERT ... ON CONFLICT
(auth_subject) DO UPDATE ... RETURNING so concurrent callers converge on
the row the winner created. Regenerated the .sqlx offline query cache.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-15 03:25:31 -07:00
co-authored by Claude Opus 4.8
parent 6823147334
commit 6071e1fd61
3 changed files with 46 additions and 24 deletions
@@ -0,0 +1,33 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO users (id, workspace_id, email, role, display_name, auth_subject)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (auth_subject) WHERE auth_subject IS NOT NULL\n DO UPDATE SET role = EXCLUDED.role\n RETURNING id, workspace_id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Text",
"Text",
"Text",
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "3b52f6b5c493c72e79a88d223acafa4f02c582a9293ad2f72782e60136e6c287"
}
@@ -1,19 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO users (id, workspace_id, email, role, display_name, auth_subject)\n VALUES ($1, $2, $3, $4, $5, $6)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Text",
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "ff63ee4d5947897ef004654a80c4729c4824d2965b23dfe03ab8c59bce243a2e"
}
+13 -5
View File
@@ -124,9 +124,17 @@ impl AuthService {
.unwrap_or_else(|| format!("{}@sso.local", claims.sub)); .unwrap_or_else(|| format!("{}@sso.local", claims.sub));
let display_name = email.split('@').next().unwrap_or("teammate").to_owned(); let display_name = email.split('@').next().unwrap_or("teammate").to_owned();
let user_id = UserId::new(); let user_id = UserId::new();
sqlx::query!( // Idempotent under concurrent first-login: the authed shell fires
// several API calls at once, and each would otherwise race to INSERT
// this row and trip the partial unique index on auth_subject (the
// losers 500'd). ON CONFLICT resolves to the row the winning request
// created, so every concurrent caller returns the same identity.
let row = sqlx::query!(
"INSERT INTO users (id, workspace_id, email, role, display_name, auth_subject) "INSERT INTO users (id, workspace_id, email, role, display_name, auth_subject)
VALUES ($1, $2, $3, $4, $5, $6)", VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (auth_subject) WHERE auth_subject IS NOT NULL
DO UPDATE SET role = EXCLUDED.role
RETURNING id, workspace_id",
user_id.as_uuid(), user_id.as_uuid(),
workspace, workspace,
email, email,
@@ -134,11 +142,11 @@ impl AuthService {
display_name, display_name,
claims.sub, claims.sub,
) )
.execute(&self.pool) .fetch_one(&self.pool)
.await?; .await?;
Ok(AuthedUser { Ok(AuthedUser {
user_id, user_id: UserId::from(row.id),
workspace_id: WorkspaceId::from(workspace), workspace_id: WorkspaceId::from(row.workspace_id),
role, role,
}) })
} }