perf(auth): fix N+1 queries in WebAuthn authentication
Add list_all() to PasskeyCredentialRepository so auth_start and auth_finish fetch all credentials in a single query instead of looping through every user with a per-user query.
This commit is contained in:
parent
a3cf7970d1
commit
f2fcada799
3 changed files with 46 additions and 42 deletions
|
|
@ -190,30 +190,19 @@ pub(crate) async fn auth_start(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(query): Query<AuthStartQuery>,
|
Query(query): Query<AuthStartQuery>,
|
||||||
) -> Result<Json<AuthStartResponse>, StatusCode> {
|
) -> Result<Json<AuthStartResponse>, StatusCode> {
|
||||||
// Load all passkey credentials from all users
|
// Load all passkey credentials in a single query
|
||||||
let users = state.user_repo.list_all().await.map_err(|err| {
|
let credentials = state.passkey_repo.list_all().await.map_err(|err| {
|
||||||
error!(error = %err, "failed to list users for auth start");
|
error!(error = %err, "failed to list all passkey credentials");
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut all_passkeys: Vec<Passkey> = Vec::new();
|
let mut all_passkeys: Vec<Passkey> = Vec::new();
|
||||||
for user in &users {
|
for cred in credentials {
|
||||||
let credentials = state
|
let passkey: Passkey = serde_json::from_str(&cred.credential_json).map_err(|err| {
|
||||||
.passkey_repo
|
error!(error = %err, credential_id = %cred.id, "failed to deserialize passkey credential");
|
||||||
.list_by_user(user.id)
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
.await
|
})?;
|
||||||
.map_err(|err| {
|
all_passkeys.push(passkey);
|
||||||
error!(error = %err, user_id = %user.id, "failed to list passkeys for user");
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
|
||||||
})?;
|
|
||||||
|
|
||||||
for cred in credentials {
|
|
||||||
let passkey: Passkey = serde_json::from_str(&cred.credential_json).map_err(|err| {
|
|
||||||
error!(error = %err, "failed to deserialize passkey credential");
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
|
||||||
})?;
|
|
||||||
all_passkeys.push(passkey);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if all_passkeys.is_empty() {
|
if all_passkeys.is_empty() {
|
||||||
|
|
@ -276,8 +265,8 @@ pub(crate) async fn auth_finish(
|
||||||
|
|
||||||
// Find the user who owns this credential
|
// Find the user who owns this credential
|
||||||
let credential_id = auth_result.cred_id();
|
let credential_id = auth_result.cred_id();
|
||||||
let users = state.user_repo.list_all().await.map_err(|err| {
|
let credentials = state.passkey_repo.list_all().await.map_err(|err| {
|
||||||
error!(error = %err, "failed to list users for credential lookup");
|
error!(error = %err, "failed to list passkey credentials for credential lookup");
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
@ -285,28 +274,17 @@ pub(crate) async fn auth_finish(
|
||||||
let mut found_cred_id = None;
|
let mut found_cred_id = None;
|
||||||
let mut found_passkey: Option<Passkey> = None;
|
let mut found_passkey: Option<Passkey> = None;
|
||||||
|
|
||||||
'outer: for user in &users {
|
for cred in &credentials {
|
||||||
let credentials = state
|
let passkey: Passkey = serde_json::from_str(&cred.credential_json)
|
||||||
.passkey_repo
|
|
||||||
.list_by_user(user.id)
|
|
||||||
.await
|
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
error!(error = %err, user_id = %user.id, "failed to list passkeys for user");
|
error!(error = %err, credential_id = %cred.id, "failed to deserialize passkey credential");
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
})?;
|
})?;
|
||||||
|
if passkey.cred_id() == credential_id {
|
||||||
for cred in &credentials {
|
found_user_id = Some(cred.user_id);
|
||||||
let passkey: Passkey = serde_json::from_str(&cred.credential_json)
|
found_cred_id = Some(cred.id);
|
||||||
.map_err(|err| {
|
found_passkey = Some(passkey);
|
||||||
error!(error = %err, credential_id = %cred.id, "failed to deserialize passkey credential");
|
break;
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
|
||||||
})?;
|
|
||||||
if passkey.cred_id() == credential_id {
|
|
||||||
found_user_id = Some(user.id);
|
|
||||||
found_cred_id = Some(cred.id);
|
|
||||||
found_passkey = Some(passkey);
|
|
||||||
break 'outer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -413,7 +391,13 @@ pub(crate) async fn passkey_add_start(
|
||||||
|
|
||||||
let exclude_credentials = existing
|
let exclude_credentials = existing
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|c| serde_json::from_str::<Passkey>(&c.credential_json).ok())
|
.filter_map(|c| {
|
||||||
|
serde_json::from_str::<Passkey>(&c.credential_json)
|
||||||
|
.map_err(|err| {
|
||||||
|
warn!(error = %err, credential_id = %c.id, "failed to deserialize passkey credential for exclude list");
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
.map(|p| p.cred_id().clone())
|
.map(|p| p.cred_id().clone())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,7 @@ pub trait PasskeyCredentialRepository: Send + Sync {
|
||||||
&self,
|
&self,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
) -> Result<Vec<PasskeyCredential>, RepositoryError>;
|
) -> Result<Vec<PasskeyCredential>, RepositoryError>;
|
||||||
|
async fn list_all(&self) -> Result<Vec<PasskeyCredential>, RepositoryError>;
|
||||||
async fn update_credential_json(
|
async fn update_credential_json(
|
||||||
&self,
|
&self,
|
||||||
id: PasskeyCredentialId,
|
id: PasskeyCredentialId,
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,25 @@ impl PasskeyCredentialRepository for SqlPasskeyCredentialRepository {
|
||||||
Ok(records.into_iter().map(Self::to_domain).collect())
|
Ok(records.into_iter().map(Self::to_domain).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_all(&self) -> Result<Vec<PasskeyCredential>, RepositoryError> {
|
||||||
|
let sql = r"
|
||||||
|
SELECT id, user_id, credential_json, name, created_at, last_used_at
|
||||||
|
FROM passkey_credentials
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
";
|
||||||
|
|
||||||
|
let records = query_as::<_, PasskeyCredentialRecord>(sql)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
RepositoryError::unexpected(format!(
|
||||||
|
"failed to list all passkey credentials: {err}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(records.into_iter().map(Self::to_domain).collect())
|
||||||
|
}
|
||||||
|
|
||||||
async fn update_credential_json(
|
async fn update_credential_json(
|
||||||
&self,
|
&self,
|
||||||
id: PasskeyCredentialId,
|
id: PasskeyCredentialId,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue