axum_jwt_sessions/handlers/
refresh.rs1use axum::{extract::State, http::HeaderMap, Json};
2use serde::{Deserialize, Serialize};
3
4use crate::{
5 error::{AuthError, Result},
6 middleware::AuthState,
7 refresher::SessionDataRefresher,
8 storage::SessionStorage,
9};
10
11#[derive(Debug, Deserialize)]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13pub struct RefreshRequest {
14 pub refresh_token: String,
15}
16
17#[derive(Debug, Serialize)]
18#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
19pub struct RefreshResponse {
20 pub access_token: String,
21 pub refresh_token: Option<String>,
22 pub access_expires_at: i64,
23 pub refresh_expires_at: Option<i64>,
24}
25
26pub async fn refresh_handler<S: SessionStorage, R: SessionDataRefresher>(
27 State(state): State<AuthState<S, R>>,
28 _headers: HeaderMap,
29 Json(payload): Json<RefreshRequest>,
30) -> Result<Json<RefreshResponse>> {
31 let refresh_claims = state
33 .token_generator
34 .verify_refresh_token(&payload.refresh_token)?;
35
36 let user_id = refresh_claims
38 .user_id
39 .as_ref()
40 .ok_or(AuthError::InvalidRefreshToken)?
41 .clone();
42
43 if !state
45 .storage
46 .user_session_exists(&user_id, &refresh_claims.sub)
47 .await?
48 {
49 return Err(AuthError::SessionNotFound);
50 }
51
52 let _session_data = state
54 .storage
55 .get_session_data(&user_id, &refresh_claims.sub)
56 .await?
57 .ok_or(AuthError::SessionNotFound)?;
58
59 let fresh_session_data = state.refresher.refresh_session_data(&user_id).await?;
61
62 let token_pair = state.token_generator.generate_token_pair(
64 refresh_claims.sub,
65 user_id.clone(),
66 fresh_session_data,
67 )?;
68
69 let (new_refresh_token, new_refresh_expires_at) = if state
70 .token_generator
71 .should_renew_refresh_token(&refresh_claims)
72 {
73 state
75 .storage
76 .revoke_user_session(&user_id, &refresh_claims.sub)
77 .await?;
78
79 state
81 .storage
82 .create_user_session(
83 user_id.clone(),
84 refresh_claims.sub,
85 token_pair.refresh_expires_at,
86 )
87 .await?;
88
89 (
90 Some(token_pair.refresh_token.clone()),
91 Some(token_pair.refresh_expires_at.unix_timestamp()),
92 )
93 } else {
94 (None, None)
95 };
96
97 Ok(Json(RefreshResponse {
98 access_token: token_pair.access_token,
99 refresh_token: new_refresh_token,
100 access_expires_at: token_pair.access_expires_at.unix_timestamp(),
101 refresh_expires_at: new_refresh_expires_at,
102 }))
103}
104
105#[cfg(feature = "openapi")]
121#[macro_export]
122macro_rules! typed_refresh_handler {
123 ($name:ident, $storage:ty, $refresher:ty) => {
124 #[utoipa::path(
125 post,
126 path = "/refresh",
127 request_body = $crate::handlers::RefreshRequest,
128 responses(
129 (status = 200, description = "Tokens refreshed successfully", body = $crate::handlers::RefreshResponse),
130 (status = 401, description = "Invalid refresh token", body = $crate::error::ErrorResponse),
131 (status = 401, description = "Session not found", body = $crate::error::ErrorResponse),
132 ),
133 tag = "auth"
134 )]
135 pub async fn $name(
136 state: ::axum::extract::State<$crate::middleware::AuthState<$storage, $refresher>>,
137 headers: ::axum::http::HeaderMap,
138 payload: ::axum::Json<$crate::handlers::RefreshRequest>,
139 ) -> $crate::error::Result<::axum::Json<$crate::handlers::RefreshResponse>> {
140 $crate::handlers::refresh_handler(state, headers, payload).await
141 }
142 };
143}