use std::sync::Arc; use axum::Extension; use axum::extract::State; use axum::http::HeaderMap; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use crate::errors::ApiError; use crate::oauth::{REFRESH_LOCK_TTL_SECS, append_cookie, read_cookie_header, refresh_grant}; use crate::services::AuthService; pub async fn refresh( State(service): State, Extension(verifier): Extension>, headers: HeaderMap, ) -> Result { let config = service.config(); let refresh_token = read_cookie_header(&headers, &config.oauth.refresh_cookie) .map(str::to_owned) .ok_or_else(ApiError::unauthorized)?; let lock_key = format!("oauth:refresh:{}", token_fingerprint(&refresh_token)); let mut acquired = service.state.redis.set_nx_ex(&lock_key, REFRESH_LOCK_TTL_SECS).await; if !acquired { tokio::time::sleep(std::time::Duration::from_millis(200)).await; acquired = service.state.redis.set_nx_ex(&lock_key, REFRESH_LOCK_TTL_SECS).await; } if !acquired { return Err(ApiError::conflict("another refresh is already in flight")); } let result = refresh_grant(&service, &refresh_token).await; let _ = export::redis::delete(&service.state.redis, &lock_key).await; let tokens = match result { Ok(tokens) => tokens, Err(_) => { let mut response = StatusCode::UNAUTHORIZED.into_response(); append_cookie(&mut response, &jwt::clear_session_cookie(&config.oauth)); append_cookie(&mut response, &jwt::clear_refresh_cookie(&config.oauth)); return Ok(response); } }; if verifier.verify_access(&tokens.access_token).await.is_err() { return Err(ApiError::from(crate::errors::AuthError::Unauthorized)); } let mut response = StatusCode::NO_CONTENT.into_response(); append_cookie( &mut response, &jwt::session_cookie(&config.oauth, &tokens.access_token, tokens.expires_in.max(60)), ); if let Some(refresh) = tokens.refresh_token { append_cookie(&mut response, &jwt::refresh_cookie(&config.oauth, &refresh)); } Ok(response) } fn token_fingerprint(token: &str) -> String { use base64::Engine as _; use sha2::{Digest, Sha256}; let digest = Sha256::digest(token.as_bytes()); base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest) }