Refresh-token rotation bug (found via Solaria smoke test): * RefreshClaims now carries a unique random jti (UUID v4). Without it, two refresh tokens issued in the same second (e.g. on rotation) were byte-identical — breaking rotation, colliding on the tokenHash unique index, and making a stolen old token indistinguishable from the new one. Every refresh token is now unique regardless of issue time. Added a unit test asserting consecutive tokens differ. Code cleanup (review items): * #15: removed unused deps tower_governor, slog, thiserror (zero refs in src/). * #30: deleted leftover src/main.rs.restore (a stale git-error-message backup). * #28: removed the 9 single-line-comment stub files under src/db/ (appointment, family, health_data, lab_result, medication, profile, permission, share, user) and their mod declarations; nothing referenced them, real logic lives in mongodb_impl.rs/init.rs. * #29: stripped 61 broad module-level #![allow(...)] suppressions across src/. Fixed the surfaced warnings instead: 4 unused 'claims' extractor bindings -> _claims; removed dead OpenFDAService.client/base_url fields + the never-called query_drug_events method + unused HashMap/reqwest imports; applied clippy's mechanical fixes (Ok(?) -> ?, Copy ObjectId clone, redundant closures, useless conversions); rewrote 'if let Ok(_) = x' -> 'if x.is_ok()'. Result: cargo build + clippy --all-targets are warning-free with no blanket allows. Verified: fmt clean, build clean, clippy 0 warnings, 19 unit tests pass (was 18; +1 jti-uniqueness test).
66 lines
1.9 KiB
Rust
66 lines
1.9 KiB
Rust
use crate::auth::jwt::Claims;
|
|
use crate::config::AppState;
|
|
use axum::{
|
|
extract::{Request, State},
|
|
http::StatusCode,
|
|
middleware::Next,
|
|
response::Response,
|
|
};
|
|
use mongodb::bson::oid::ObjectId;
|
|
|
|
pub async fn jwt_auth_middleware(
|
|
State(state): State<AppState>,
|
|
mut req: Request,
|
|
next: Next,
|
|
) -> Result<Response, StatusCode> {
|
|
let headers = req.headers();
|
|
|
|
// Extract Authorization header
|
|
let auth_header = headers
|
|
.get("Authorization")
|
|
.and_then(|h| h.to_str().ok())
|
|
.ok_or(StatusCode::UNAUTHORIZED)?;
|
|
|
|
// Check Bearer token format
|
|
if !auth_header.starts_with("Bearer ") {
|
|
return Err(StatusCode::UNAUTHORIZED);
|
|
}
|
|
|
|
let token = &auth_header[7..]; // Remove "Bearer " prefix
|
|
|
|
// Verify signature and expiry.
|
|
let claims = state
|
|
.jwt_service
|
|
.validate_token(token)
|
|
.map_err(|_| StatusCode::UNAUTHORIZED)?;
|
|
|
|
// Reject tokens whose version is stale (e.g. issued before a password
|
|
// change). The user's current version is cached briefly so we don't hit
|
|
// Mongo on every request; a missing user (deleted) also means 401.
|
|
let user_oid = ObjectId::parse_str(&claims.sub).map_err(|_| StatusCode::UNAUTHORIZED)?;
|
|
let current_version = state
|
|
.token_version_cache
|
|
.get_or_load(&user_oid, &state.db)
|
|
.await
|
|
.map_err(|_| StatusCode::UNAUTHORIZED)?;
|
|
match current_version {
|
|
Some(v) if v == claims.token_version => {}
|
|
_ => return Err(StatusCode::UNAUTHORIZED),
|
|
}
|
|
|
|
// Add claims to request extensions for handlers to use
|
|
req.extensions_mut().insert(claims);
|
|
|
|
Ok(next.run(req).await)
|
|
}
|
|
|
|
// Extension method to extract claims from request
|
|
pub trait RequestClaimsExt {
|
|
fn claims(&self) -> Option<&Claims>;
|
|
}
|
|
|
|
impl RequestClaimsExt for Request {
|
|
fn claims(&self) -> Option<&Claims> {
|
|
self.extensions().get::<Claims>()
|
|
}
|
|
}
|