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,132 @@
use axum::http::{
HeaderMap, HeaderValue, StatusCode,
header::SET_COOKIE,
};
use module_auth::{
AuthLoginMethod, AuthUser, CreateRefreshSessionInput, RefreshSessionError,
};
use platform_auth::{
AccessTokenClaims, AccessTokenClaimsInput, AuthProvider, BindingStatus,
build_refresh_session_clear_cookie, build_refresh_session_set_cookie,
create_refresh_session_token, hash_refresh_session_token, sign_access_token,
};
use time::OffsetDateTime;
use crate::{http_error::AppError, state::AppState};
#[derive(Debug, Clone)]
pub struct SignedAuthSession {
pub access_token: String,
pub refresh_token: String,
}
pub fn create_password_auth_session(
state: &AppState,
user: &AuthUser,
) -> Result<SignedAuthSession, AppError> {
let refresh_token = create_refresh_session_token();
let refresh_token_hash = hash_refresh_session_token(&refresh_token);
let session = state
.refresh_session_service()
.create_session(
CreateRefreshSessionInput {
user_id: user.id.clone(),
refresh_token_hash,
issued_by_provider: AuthLoginMethod::Password,
},
OffsetDateTime::now_utc(),
)
.map_err(map_refresh_session_error)?;
let access_token = sign_access_token_for_user(state, user, &session.session.session_id)?;
Ok(SignedAuthSession {
access_token,
refresh_token,
})
}
pub fn sign_access_token_for_user(
state: &AppState,
user: &AuthUser,
session_id: &str,
) -> Result<String, AppError> {
let access_claims = AccessTokenClaims::from_input(
AccessTokenClaimsInput {
user_id: user.id.clone(),
session_id: session_id.to_string(),
provider: map_auth_provider(&user.login_method),
roles: vec!["user".to_string()],
token_version: user.token_version,
phone_verified: user.phone_number_masked.is_some(),
binding_status: map_binding_status(&user.binding_status),
display_name: Some(user.display_name.clone()),
},
state.auth_jwt_config(),
OffsetDateTime::now_utc(),
)
.map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string())
})?;
sign_access_token(&access_claims, state.auth_jwt_config()).map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string())
})
}
pub fn build_refresh_session_cookie_header(
state: &AppState,
refresh_token: &str,
) -> Result<HeaderValue, AppError> {
let refresh_cookie =
build_refresh_session_set_cookie(refresh_token, state.refresh_cookie_config());
HeaderValue::from_str(&refresh_cookie).map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR)
.with_message(format!("refresh cookie 头构造失败:{error}"))
})
}
pub fn build_clear_refresh_session_cookie_header(
state: &AppState,
) -> Result<HeaderValue, AppError> {
let refresh_cookie = build_refresh_session_clear_cookie(state.refresh_cookie_config());
HeaderValue::from_str(&refresh_cookie).map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR)
.with_message(format!("refresh cookie 头构造失败:{error}"))
})
}
pub fn attach_set_cookie_header(
headers: &mut HeaderMap,
set_cookie: HeaderValue,
) {
headers.insert(SET_COOKIE, set_cookie);
}
pub fn map_refresh_session_error(error: RefreshSessionError) -> AppError {
match error {
RefreshSessionError::MissingToken
| RefreshSessionError::SessionNotFound
| RefreshSessionError::SessionExpired
| RefreshSessionError::UserNotFound => {
AppError::from_status(StatusCode::UNAUTHORIZED).with_message(error.to_string())
}
RefreshSessionError::Store(message) => {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(message)
}
}
}
fn map_auth_provider(login_method: &AuthLoginMethod) -> AuthProvider {
match login_method {
AuthLoginMethod::Password => AuthProvider::Password,
AuthLoginMethod::Phone => AuthProvider::Phone,
AuthLoginMethod::Wechat => AuthProvider::Wechat,
}
}
fn map_binding_status(binding_status: &module_auth::AuthBindingStatus) -> BindingStatus {
match binding_status {
module_auth::AuthBindingStatus::Active => BindingStatus::Active,
module_auth::AuthBindingStatus::PendingBindPhone => BindingStatus::PendingBindPhone,
}
}