axum_jwt_sessions/handlers/
refresh.rs

1use 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    // Verify refresh token
32    let refresh_claims = state
33        .token_generator
34        .verify_refresh_token(&payload.refresh_token)?;
35
36    // Extract user_id from refresh token claims
37    let user_id = refresh_claims
38        .user_id
39        .as_ref()
40        .ok_or(AuthError::InvalidRefreshToken)?
41        .clone();
42
43    // Check if this specific session exists for the user
44    if !state
45        .storage
46        .user_session_exists(&user_id, &refresh_claims.sub)
47        .await?
48    {
49        return Err(AuthError::SessionNotFound);
50    }
51
52    // Get session data for this specific session
53    let _session_data = state
54        .storage
55        .get_session_data(&user_id, &refresh_claims.sub)
56        .await?
57        .ok_or(AuthError::SessionNotFound)?;
58
59    // Refresh session data using the user_id
60    let fresh_session_data = state.refresher.refresh_session_data(&user_id).await?;
61
62    // Generate new token pair with fresh session data
63    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        // Revoke the old session
74        state
75            .storage
76            .revoke_user_session(&user_id, &refresh_claims.sub)
77            .await?;
78
79        // Create new session for the user
80        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/// Macro to create a typed refresh handler for use with OpenAPI and routes! macro
106///
107/// # Example
108/// ```rust
109/// use axum_jwt_sessions::typed_refresh_handler;
110///
111/// // Create a typed refresh handler for your storage types
112/// typed_refresh_handler!(my_refresh_handler, MyStorage, MyRefresher);
113///
114/// // Use it with utoipa-axum routes! macro
115/// let (router, api) = OpenApiRouter::new()
116///     .routes(routes!(my_refresh_handler))
117///     .with_state(auth_state)
118///     .split_for_parts();
119/// ```
120#[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}