refactor(logging): log errors and security events in auth routes
- Log all map_err(|_| StatusCode) patterns in webauthn, tokens, and account handlers - Log fire-and-forget errors: registration token mark_used, passkey credential counter update, passkey update_last_used - Add security event logging: token create/revoke, passkey delete, login/logout, CLI token creation via passkey auth - Log session delete failures and auth check errors
This commit is contained in:
parent
5ee89549a3
commit
4ab00dd630
4 changed files with 163 additions and 84 deletions
|
|
@ -6,6 +6,7 @@ use axum::response::{IntoResponse, Redirect, Response};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tower_cookies::Cookies;
|
use tower_cookies::Cookies;
|
||||||
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use crate::application::auth::AuthenticatedUser;
|
use crate::application::auth::AuthenticatedUser;
|
||||||
use crate::application::routes::render_html;
|
use crate::application::routes::render_html;
|
||||||
|
|
@ -98,7 +99,10 @@ pub(crate) async fn account_page(
|
||||||
.passkey_repo
|
.passkey_repo
|
||||||
.list_by_user(auth_user.id)
|
.list_by_user(auth_user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
.map_err(|err| {
|
||||||
|
error!(error = %err, "failed to list passkeys for account page");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| PasskeyView {
|
.map(|p| PasskeyView {
|
||||||
id: i64::from(p.id),
|
id: i64::from(p.id),
|
||||||
|
|
@ -112,7 +116,10 @@ pub(crate) async fn account_page(
|
||||||
.token_repo
|
.token_repo
|
||||||
.list_by_user(auth_user.id)
|
.list_by_user(auth_user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
.map_err(|err| {
|
||||||
|
error!(error = %err, "failed to list tokens for account page");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(crate::domain::tokens::Token::is_active)
|
.filter(crate::domain::tokens::Token::is_active)
|
||||||
.map(|t| TokenView {
|
.map(|t| TokenView {
|
||||||
|
|
@ -123,17 +130,18 @@ pub(crate) async fn account_page(
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let ai_usage = state
|
let ai_usage = match state.ai_usage_repo.summary_for_user(auth_user.id).await {
|
||||||
.ai_usage_repo
|
Ok(summary) => Some(summary),
|
||||||
.summary_for_user(auth_user.id)
|
Err(err) => {
|
||||||
.await
|
warn!(error = %err, "failed to load AI usage summary");
|
||||||
.ok()
|
None
|
||||||
.filter(|s| s.total_calls > 0)
|
}
|
||||||
.map(|s| AiUsageView {
|
};
|
||||||
total_calls: s.total_calls,
|
let ai_usage = ai_usage.filter(|s| s.total_calls > 0).map(|s| AiUsageView {
|
||||||
total_tokens: format_number(s.total_tokens),
|
total_calls: s.total_calls,
|
||||||
total_cost: format_cost(s.total_cost),
|
total_tokens: format_number(s.total_tokens),
|
||||||
});
|
total_cost: format_cost(s.total_cost),
|
||||||
|
});
|
||||||
|
|
||||||
let template = AccountTemplate {
|
let template = AccountTemplate {
|
||||||
nav_active: "account",
|
nav_active: "account",
|
||||||
|
|
@ -165,7 +173,10 @@ pub(crate) async fn list_passkeys(
|
||||||
.passkey_repo
|
.passkey_repo
|
||||||
.list_by_user(auth_user.0.id)
|
.list_by_user(auth_user.0.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|err| {
|
||||||
|
error!(error = %err, "failed to list passkeys");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
let responses: Vec<PasskeyResponse> = passkeys
|
let responses: Vec<PasskeyResponse> = passkeys
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -186,11 +197,10 @@ pub(crate) async fn delete_passkey(
|
||||||
Path(passkey_id): Path<PasskeyCredentialId>,
|
Path(passkey_id): Path<PasskeyCredentialId>,
|
||||||
) -> Result<StatusCode, StatusCode> {
|
) -> Result<StatusCode, StatusCode> {
|
||||||
// Verify the passkey belongs to the user
|
// Verify the passkey belongs to the user
|
||||||
let passkey = state
|
let passkey = state.passkey_repo.get(passkey_id).await.map_err(|err| {
|
||||||
.passkey_repo
|
error!(error = %err, %passkey_id, "failed to get passkey for deletion");
|
||||||
.get(passkey_id)
|
StatusCode::NOT_FOUND
|
||||||
.await
|
})?;
|
||||||
.map_err(|_| StatusCode::NOT_FOUND)?;
|
|
||||||
|
|
||||||
if passkey.user_id != auth_user.0.id {
|
if passkey.user_id != auth_user.0.id {
|
||||||
return Err(StatusCode::FORBIDDEN);
|
return Err(StatusCode::FORBIDDEN);
|
||||||
|
|
@ -201,17 +211,21 @@ pub(crate) async fn delete_passkey(
|
||||||
.passkey_repo
|
.passkey_repo
|
||||||
.list_by_user(auth_user.0.id)
|
.list_by_user(auth_user.0.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|err| {
|
||||||
|
error!(error = %err, "failed to list passkeys for deletion check");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
if all_passkeys.len() <= 1 {
|
if all_passkeys.len() <= 1 {
|
||||||
return Err(StatusCode::CONFLICT);
|
return Err(StatusCode::CONFLICT);
|
||||||
}
|
}
|
||||||
|
|
||||||
state
|
state.passkey_repo.delete(passkey_id).await.map_err(|err| {
|
||||||
.passkey_repo
|
error!(error = %err, %passkey_id, "failed to delete passkey");
|
||||||
.delete(passkey_id)
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
.await
|
})?;
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
|
info!(%passkey_id, user_id = %auth_user.0.id, "passkey deleted");
|
||||||
|
|
||||||
Ok(StatusCode::NO_CONTENT)
|
Ok(StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
@ -226,15 +240,27 @@ async fn extract_user_from_session(
|
||||||
let session_token = cookie.value();
|
let session_token = cookie.value();
|
||||||
let session_token_hash = crate::infrastructure::auth::hash_token(session_token);
|
let session_token_hash = crate::infrastructure::auth::hash_token(session_token);
|
||||||
|
|
||||||
let session = state
|
let session = match state
|
||||||
.session_repo
|
.session_repo
|
||||||
.get_by_token_hash(&session_token_hash)
|
.get_by_token_hash(&session_token_hash)
|
||||||
.await
|
.await
|
||||||
.ok()?;
|
{
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = %err, "session lookup failed on account page");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if session.is_expired() {
|
if session.is_expired() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.user_repo.get(session.user_id).await.ok()
|
match state.user_repo.get(session.user_id).await {
|
||||||
|
Ok(user) => Some(user),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = %err, user_id = %session.user_id, "user lookup failed for valid session");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use axum::http::StatusCode;
|
||||||
use axum::response::{IntoResponse, Redirect, Response};
|
use axum::response::{IntoResponse, Redirect, Response};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tower_cookies::{Cookie, Cookies};
|
use tower_cookies::{Cookie, Cookies};
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::application::routes::render_html;
|
use crate::application::routes::render_html;
|
||||||
use crate::application::server::AppState;
|
use crate::application::server::AppState;
|
||||||
|
|
@ -55,11 +56,13 @@ pub(crate) async fn logout(State(state): State<AppState>, cookies: Cookies) -> R
|
||||||
.session_repo
|
.session_repo
|
||||||
.get_by_token_hash(&session_token_hash)
|
.get_by_token_hash(&session_token_hash)
|
||||||
.await
|
.await
|
||||||
|
&& let Err(err) = state.session_repo.delete(session.id).await
|
||||||
{
|
{
|
||||||
let _ = state.session_repo.delete(session.id).await;
|
warn!(error = %err, session_id = %session.id, "failed to delete session on logout");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info!("user logged out");
|
||||||
cookies.remove(Cookie::from(SESSION_COOKIE_NAME));
|
cookies.remove(Cookie::from(SESSION_COOKIE_NAME));
|
||||||
Redirect::to("/")
|
Redirect::to("/")
|
||||||
}
|
}
|
||||||
|
|
@ -79,6 +82,9 @@ pub async fn is_authenticated(state: &AppState, cookies: &Cookies) -> bool {
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(session) => !session.is_expired(),
|
Ok(session) => !session.is_expired(),
|
||||||
Err(_) => false,
|
Err(err) => {
|
||||||
|
warn!(error = %err, "session lookup failed during auth check");
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use axum::extract::{Path, State};
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::application::auth::AuthenticatedUser;
|
use crate::application::auth::AuthenticatedUser;
|
||||||
use crate::application::server::AppState;
|
use crate::application::server::AppState;
|
||||||
|
|
@ -51,16 +52,20 @@ pub async fn create_token(
|
||||||
auth_user: AuthenticatedUser,
|
auth_user: AuthenticatedUser,
|
||||||
Json(payload): Json<CreateTokenRequest>,
|
Json(payload): Json<CreateTokenRequest>,
|
||||||
) -> Result<Json<CreateTokenResponse>, StatusCode> {
|
) -> Result<Json<CreateTokenResponse>, StatusCode> {
|
||||||
let token_value = generate_token().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
let token_value = generate_token().map_err(|err| {
|
||||||
|
error!(error = %err, "failed to generate token");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
let token_hash_value = hash_token(&token_value);
|
let token_hash_value = hash_token(&token_value);
|
||||||
|
|
||||||
let new_token = NewToken::new(auth_user.0.id, token_hash_value, payload.name.clone());
|
let new_token = NewToken::new(auth_user.0.id, token_hash_value, payload.name.clone());
|
||||||
|
|
||||||
let stored_token = state
|
let stored_token = state.token_repo.insert(new_token).await.map_err(|err| {
|
||||||
.token_repo
|
error!(error = %err, "failed to store token");
|
||||||
.insert(new_token)
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
.await
|
})?;
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
|
info!(token_id = %stored_token.id, token_name = %stored_token.name, user_id = %auth_user.0.id, "API token created");
|
||||||
|
|
||||||
Ok(Json(CreateTokenResponse {
|
Ok(Json(CreateTokenResponse {
|
||||||
id: stored_token.id,
|
id: stored_token.id,
|
||||||
|
|
@ -78,7 +83,10 @@ pub async fn list_tokens(
|
||||||
.token_repo
|
.token_repo
|
||||||
.list_by_user(auth_user.0.id)
|
.list_by_user(auth_user.0.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|err| {
|
||||||
|
error!(error = %err, "failed to list tokens");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
let token_responses: Vec<TokenResponse> = tokens.into_iter().map(TokenResponse::from).collect();
|
let token_responses: Vec<TokenResponse> = tokens.into_iter().map(TokenResponse::from).collect();
|
||||||
|
|
||||||
|
|
@ -92,11 +100,10 @@ pub async fn revoke_token(
|
||||||
Path(token_id): Path<TokenId>,
|
Path(token_id): Path<TokenId>,
|
||||||
) -> Result<Json<TokenResponse>, StatusCode> {
|
) -> Result<Json<TokenResponse>, StatusCode> {
|
||||||
// Get the token to ensure it exists and belongs to the user
|
// Get the token to ensure it exists and belongs to the user
|
||||||
let token = state
|
let token = state.token_repo.get(token_id).await.map_err(|err| {
|
||||||
.token_repo
|
error!(error = %err, %token_id, "failed to get token for revocation");
|
||||||
.get(token_id)
|
StatusCode::NOT_FOUND
|
||||||
.await
|
})?;
|
||||||
.map_err(|_| StatusCode::NOT_FOUND)?;
|
|
||||||
|
|
||||||
// Ensure the token belongs to the authenticated user
|
// Ensure the token belongs to the authenticated user
|
||||||
if token.user_id != auth_user.0.id {
|
if token.user_id != auth_user.0.id {
|
||||||
|
|
@ -104,11 +111,12 @@ pub async fn revoke_token(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Revoke the token
|
// Revoke the token
|
||||||
let revoked_token = state
|
let revoked_token = state.token_repo.revoke(token_id).await.map_err(|err| {
|
||||||
.token_repo
|
error!(error = %err, %token_id, "failed to revoke token");
|
||||||
.revoke(token_id)
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
.await
|
})?;
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
|
info!(%token_id, user_id = %auth_user.0.id, "API token revoked");
|
||||||
|
|
||||||
Ok(Json(TokenResponse::from(revoked_token)))
|
Ok(Json(TokenResponse::from(revoked_token)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,10 @@ pub(crate) async fn register_start(
|
||||||
.registration_token_repo
|
.registration_token_repo
|
||||||
.get_by_token_hash(&token_hash)
|
.get_by_token_hash(&token_hash)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::UNAUTHORIZED)?;
|
.map_err(|err| {
|
||||||
|
warn!(error = %err, "registration token lookup failed");
|
||||||
|
StatusCode::UNAUTHORIZED
|
||||||
|
})?;
|
||||||
|
|
||||||
if !reg_token.is_valid() {
|
if !reg_token.is_valid() {
|
||||||
return Err(StatusCode::GONE);
|
return Err(StatusCode::GONE);
|
||||||
|
|
@ -153,14 +156,19 @@ pub(crate) async fn register_start(
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Mark registration token as used
|
// Mark registration token as used
|
||||||
let _ = state
|
if let Err(err) = state
|
||||||
.registration_token_repo
|
.registration_token_repo
|
||||||
.mark_used(reg_token.id, user.id)
|
.mark_used(reg_token.id, user.id)
|
||||||
.await;
|
.await
|
||||||
|
{
|
||||||
|
warn!(error = %err, token_id = %reg_token.id, "failed to mark registration token as used");
|
||||||
|
}
|
||||||
|
|
||||||
// Start passkey registration ceremony
|
// Start passkey registration ceremony
|
||||||
let webauthn_uuid =
|
let webauthn_uuid = Uuid::parse_str(&user_uuid).map_err(|err| {
|
||||||
Uuid::parse_str(&user_uuid).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
error!(error = %err, "failed to parse user UUID");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
let exclude_credentials = Vec::new();
|
let exclude_credentials = Vec::new();
|
||||||
|
|
||||||
let (ccr, reg_state) = state
|
let (ccr, reg_state) = state
|
||||||
|
|
@ -214,8 +222,10 @@ pub(crate) async fn register_finish(
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Store the credential
|
// Store the credential
|
||||||
let credential_json =
|
let credential_json = serde_json::to_string(&passkey).map_err(|err| {
|
||||||
serde_json::to_string(&passkey).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
error!(error = %err, "failed to serialize passkey credential");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
let new_credential = NewPasskeyCredential::new(user_id, credential_json, payload.passkey_name);
|
let new_credential = NewPasskeyCredential::new(user_id, credential_json, payload.passkey_name);
|
||||||
state
|
state
|
||||||
.passkey_repo
|
.passkey_repo
|
||||||
|
|
@ -242,11 +252,10 @@ pub(crate) async fn auth_start(
|
||||||
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 from all users
|
||||||
let users = state
|
let users = state.user_repo.list_all().await.map_err(|err| {
|
||||||
.user_repo
|
error!(error = %err, "failed to list users for auth start");
|
||||||
.list_all()
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
.await
|
})?;
|
||||||
.map_err(|_| 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 user in &users {
|
||||||
|
|
@ -254,7 +263,10 @@ pub(crate) async fn auth_start(
|
||||||
.passkey_repo
|
.passkey_repo
|
||||||
.list_by_user(user.id)
|
.list_by_user(user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|err| {
|
||||||
|
error!(error = %err, user_id = %user.id, "failed to list passkeys for user");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
for cred in credentials {
|
for cred in credentials {
|
||||||
let passkey: Passkey = serde_json::from_str(&cred.credential_json).map_err(|err| {
|
let passkey: Passkey = serde_json::from_str(&cred.credential_json).map_err(|err| {
|
||||||
|
|
@ -300,6 +312,7 @@ pub(crate) async fn auth_start(
|
||||||
|
|
||||||
// --- Authentication finish ---
|
// --- Authentication finish ---
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
#[tracing::instrument(skip(state, cookies, payload))]
|
#[tracing::instrument(skip(state, cookies, payload))]
|
||||||
pub(crate) async fn auth_finish(
|
pub(crate) async fn auth_finish(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
@ -324,11 +337,10 @@ 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
|
let users = state.user_repo.list_all().await.map_err(|err| {
|
||||||
.user_repo
|
error!(error = %err, "failed to list users for credential lookup");
|
||||||
.list_all()
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
.await
|
})?;
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
|
|
||||||
let mut found_user_id = None;
|
let mut found_user_id = None;
|
||||||
let mut found_cred_id = None;
|
let mut found_cred_id = None;
|
||||||
|
|
@ -339,11 +351,17 @@ pub(crate) async fn auth_finish(
|
||||||
.passkey_repo
|
.passkey_repo
|
||||||
.list_by_user(user.id)
|
.list_by_user(user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|err| {
|
||||||
|
error!(error = %err, user_id = %user.id, "failed to list passkeys for user");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
for cred in &credentials {
|
for cred in &credentials {
|
||||||
let passkey: Passkey = serde_json::from_str(&cred.credential_json)
|
let passkey: Passkey = serde_json::from_str(&cred.credential_json)
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|err| {
|
||||||
|
error!(error = %err, credential_id = %cred.id, "failed to deserialize passkey credential");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
if passkey.cred_id() == credential_id {
|
if passkey.cred_id() == credential_id {
|
||||||
found_user_id = Some(user.id);
|
found_user_id = Some(user.id);
|
||||||
found_cred_id = Some(cred.id);
|
found_cred_id = Some(cred.id);
|
||||||
|
|
@ -361,31 +379,45 @@ pub(crate) async fn auth_finish(
|
||||||
&& let Some(mut passkey) = found_passkey
|
&& let Some(mut passkey) = found_passkey
|
||||||
{
|
{
|
||||||
passkey.update_credential(&auth_result);
|
passkey.update_credential(&auth_result);
|
||||||
if let Ok(updated_json) = serde_json::to_string(&passkey) {
|
match serde_json::to_string(&passkey) {
|
||||||
let _ = state
|
Ok(updated_json) => {
|
||||||
.passkey_repo
|
if let Err(err) = state
|
||||||
.update_credential_json(cred_db_id, &updated_json)
|
.passkey_repo
|
||||||
.await;
|
.update_credential_json(cred_db_id, &updated_json)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!(error = %err, credential_id = %cred_db_id, "failed to update passkey credential counter");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = %err, "failed to serialize updated passkey credential");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update last used timestamp
|
// Update last used timestamp
|
||||||
let passkey_repo = state.passkey_repo.clone();
|
let passkey_repo = state.passkey_repo.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _ = passkey_repo.update_last_used(cred_db_id).await;
|
if let Err(err) = passkey_repo.update_last_used(cred_db_id).await {
|
||||||
|
warn!(error = %err, credential_id = %cred_db_id, "failed to update passkey last_used");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle CLI callback flow
|
// Handle CLI callback flow
|
||||||
if let Some(cli_info) = cli_callback {
|
if let Some(cli_info) = cli_callback {
|
||||||
// Generate a bearer token for the CLI
|
// Generate a bearer token for the CLI
|
||||||
let token_value = generate_token().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
let token_value = generate_token().map_err(|err| {
|
||||||
|
error!(error = %err, "failed to generate CLI bearer token");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
let token_hash_value = hash_token(&token_value);
|
let token_hash_value = hash_token(&token_value);
|
||||||
let new_token = NewToken::new(user_id, token_hash_value, cli_info.token_name);
|
let new_token = NewToken::new(user_id, token_hash_value, cli_info.token_name);
|
||||||
state
|
state.token_repo.insert(new_token).await.map_err(|err| {
|
||||||
.token_repo
|
error!(error = %err, "failed to store CLI bearer token");
|
||||||
.insert(new_token)
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
.await
|
})?;
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
|
info!(user_id = %user_id, "CLI token created via passkey auth");
|
||||||
|
|
||||||
let redirect_url = format!(
|
let redirect_url = format!(
|
||||||
"{}?token={}&state={}",
|
"{}?token={}&state={}",
|
||||||
|
|
@ -425,15 +457,20 @@ pub(crate) async fn passkey_add_start(
|
||||||
Json(payload): Json<PasskeyAddStartRequest>,
|
Json(payload): Json<PasskeyAddStartRequest>,
|
||||||
) -> Result<Json<ChallengeResponse<CreationChallengeResponse>>, StatusCode> {
|
) -> Result<Json<ChallengeResponse<CreationChallengeResponse>>, StatusCode> {
|
||||||
let user = auth_user.0;
|
let user = auth_user.0;
|
||||||
let webauthn_uuid =
|
let webauthn_uuid = Uuid::parse_str(&user.uuid).map_err(|err| {
|
||||||
Uuid::parse_str(&user.uuid).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
error!(error = %err, "failed to parse user UUID for passkey add");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
// Load existing credentials to exclude (prevents re-registering same authenticator)
|
// Load existing credentials to exclude (prevents re-registering same authenticator)
|
||||||
let existing = state
|
let existing = state
|
||||||
.passkey_repo
|
.passkey_repo
|
||||||
.list_by_user(user.id)
|
.list_by_user(user.id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|err| {
|
||||||
|
error!(error = %err, user_id = %user.id, "failed to list existing passkeys");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
let exclude_credentials = existing
|
let exclude_credentials = existing
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -485,8 +522,10 @@ pub(crate) async fn passkey_add_finish(
|
||||||
StatusCode::BAD_REQUEST
|
StatusCode::BAD_REQUEST
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let credential_json =
|
let credential_json = serde_json::to_string(&passkey).map_err(|err| {
|
||||||
serde_json::to_string(&passkey).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
error!(error = %err, "failed to serialize new passkey credential");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
let new_credential = NewPasskeyCredential::new(user_id, credential_json, payload.name);
|
let new_credential = NewPasskeyCredential::new(user_id, credential_json, payload.name);
|
||||||
state
|
state
|
||||||
.passkey_repo
|
.passkey_repo
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue