feat: add refresh token rotation flow

This commit is contained in:
2026-04-21 15:27:04 +08:00
parent 70dbefda2b
commit 584a77e572
16 changed files with 1048 additions and 85 deletions

View File

@@ -0,0 +1,84 @@
use axum::{
extract::{Extension, State},
http::HeaderMap,
response::IntoResponse,
};
use module_auth::{RefreshSessionError, RotateRefreshSessionInput};
use platform_auth::hash_refresh_session_token;
use serde::Serialize;
use time::OffsetDateTime;
use crate::{
api_response::json_success_body,
auth::RefreshSessionToken,
auth_session::{
attach_set_cookie_header, build_clear_refresh_session_cookie_header,
build_refresh_session_cookie_header, map_refresh_session_error, sign_access_token_for_user,
},
http_error::AppError,
request_context::RequestContext,
state::AppState,
};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RefreshSessionResponse {
pub token: String,
}
pub async fn refresh_session(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
maybe_refresh_token: Option<Extension<RefreshSessionToken>>,
) -> Result<impl IntoResponse, AppError> {
let raw_refresh_token = maybe_refresh_token
.map(|token| token.0.token().to_string())
.unwrap_or_default();
if raw_refresh_token.trim().is_empty() {
return Err(map_refresh_error_with_clear_cookie(
&state,
RefreshSessionError::MissingToken,
));
}
let refresh_token_hash = hash_refresh_session_token(&raw_refresh_token);
let next_refresh_token = platform_auth::create_refresh_session_token();
let next_refresh_token_hash = hash_refresh_session_token(&next_refresh_token);
let rotated = state
.refresh_session_service()
.rotate_session(
RotateRefreshSessionInput {
refresh_token_hash,
next_refresh_token_hash,
},
OffsetDateTime::now_utc(),
)
.map_err(|error| map_refresh_error_with_clear_cookie(&state, error))?;
let access_token =
sign_access_token_for_user(&state, &rotated.user, &rotated.session.session_id)?;
let mut headers = HeaderMap::new();
attach_set_cookie_header(
&mut headers,
build_refresh_session_cookie_header(&state, &next_refresh_token)?,
);
Ok((
headers,
json_success_body(
Some(&request_context),
RefreshSessionResponse {
token: access_token,
},
),
))
}
fn map_refresh_error_with_clear_cookie(state: &AppState, error: RefreshSessionError) -> AppError {
let response_error = map_refresh_session_error(error);
if let Ok(set_cookie) = build_clear_refresh_session_cookie_header(state) {
return response_error.with_header("set-cookie", set_cookie);
}
response_error
}