Skip to content

Commit 02f6b0d

Browse files
committed
adds refresh path
1 parent 1b9c479 commit 02f6b0d

File tree

8 files changed

+118
-44
lines changed

8 files changed

+118
-44
lines changed

server/src/handlers/auth.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
use axum::{Extension, extract::State};
22

3-
use crate::{error::AppResult, models::User, schemas::token::LoginResponse, state::AppState};
3+
use crate::{
4+
error::AppResult,
5+
models::User,
6+
schemas::token::{LoginResponse, RefreshResponse},
7+
state::AppState,
8+
};
49

510
#[axum::debug_handler]
611
#[utoipa::path(post, path = "/auth/login")]
@@ -10,10 +15,15 @@ pub async fn login(
1015
) -> AppResult<LoginResponse> {
1116
let access_token = state.auth_service.generate_access_token(&user)?;
1217
let refresh_token = state.auth_service.generate_refresh_token(&user).await?;
13-
Ok(LoginResponse {
14-
access_token,
15-
refresh_token,
16-
token_type: String::from("Bearer"),
17-
expires_in: 60 * 15, // Todo get from auth service
18-
})
18+
Ok(LoginResponse::new(access_token, refresh_token))
19+
}
20+
21+
#[axum::debug_handler]
22+
#[utoipa::path(post, path = "/auth/refresh")]
23+
pub async fn refresh(
24+
State(state): State<AppState>,
25+
Extension(user): Extension<User>,
26+
) -> AppResult<RefreshResponse> {
27+
let access_token = state.auth_service.generate_access_token(&user)?;
28+
Ok(RefreshResponse::new(access_token))
1929
}

server/src/main.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ mod routes;
77
mod schemas;
88
mod services;
99
mod state;
10-
mod util;
1110

1211
use crate::state::AppState;
1312

server/src/models/refresh_token.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use uuid::Uuid;
66
pub struct RefreshToken {
77
pub id: Uuid,
88
pub user_id: Uuid,
9-
pub token_hash: String,
9+
pub refresh_token_hash: String,
1010
pub created_at: DateTime<Local>,
1111
pub updated_at: DateTime<Local>,
1212
}

server/src/repositories/mocks/refresh_token.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::sync::{Arc, Mutex};
22

33
use async_trait::async_trait;
4-
use chrono::DateTime;
4+
use chrono::{DateTime, Local};
55
use uuid::Uuid;
66

77
use crate::{
@@ -16,7 +16,7 @@ impl RefreshToken {
1616
id: Uuid::default(),
1717
user_id: Uuid::default(),
1818
// password123
19-
token_hash: String::from(
19+
refresh_token_hash: String::from(
2020
"$argon2id$v=19$m=16,t=2,p=1$NjFWcEMwUEQ0dmZXcDMwSg$TfJtuSrudRp6hhV2mFSt3g",
2121
),
2222
created_at: DateTime::default(),
@@ -51,10 +51,14 @@ impl RefreshTokenRepository for MockRefreshTokenRepository {
5151
.collect::<Vec<RefreshToken>>())
5252
}
5353

54-
async fn create(&self, user_id: Uuid, token_hash: String) -> RepositoryResult<RefreshToken> {
54+
async fn create(
55+
&self,
56+
user_id: Uuid,
57+
refresh_token_hash: String,
58+
) -> RepositoryResult<RefreshToken> {
5559
let mut refresh_token = RefreshToken::mock();
5660
refresh_token.user_id = user_id;
57-
refresh_token.token_hash = token_hash;
61+
refresh_token.refresh_token_hash = refresh_token_hash;
5862
let mut data = self.data.lock().unwrap();
5963
data.push(refresh_token.clone());
6064
Ok(refresh_token)

server/src/repositories/refresh_token.rs

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@ impl PostgresRefreshTokenRepository {
2929
#[async_trait]
3030
impl RefreshTokenRepository for PostgresRefreshTokenRepository {
3131
async fn find_by_user_id(&self, id: Uuid) -> RepositoryResult<Vec<RefreshToken>> {
32-
let refresh_token =
33-
sqlx::query_as::<_, RefreshToken>("SELECT * FROM refresh_tokens WHERE id = $1;")
34-
.bind(id)
35-
.fetch_all(&self.pool)
36-
.await?;
32+
let query = "
33+
SELECT * FROM refresh_tokens
34+
WHERE id = $1;
35+
";
36+
let refresh_token = sqlx::query_as::<_, RefreshToken>(query)
37+
.bind(id)
38+
.fetch_all(&self.pool)
39+
.await?;
3740
Ok(refresh_token)
3841
}
3942

@@ -42,23 +45,29 @@ impl RefreshTokenRepository for PostgresRefreshTokenRepository {
4245
user_id: Uuid,
4346
refresh_token_hash: String,
4447
) -> RepositoryResult<RefreshToken> {
45-
let user = sqlx::query_as::<_, RefreshToken>(
46-
"INSERT INTO refresh_tokens (user_id, token_hash) VALUES ($1, $2) RETURNING *;",
47-
)
48-
.bind(user_id)
49-
.bind(&refresh_token_hash)
50-
.fetch_one(&self.pool)
51-
.await?;
48+
let query = "
49+
INSERT INTO refresh_tokens (user_id, refresh_token_hash)
50+
VALUES ($1, $2)
51+
RETURNING *;
52+
";
53+
let user = sqlx::query_as::<_, RefreshToken>(query)
54+
.bind(user_id)
55+
.bind(&refresh_token_hash)
56+
.fetch_one(&self.pool)
57+
.await?;
5258
Ok(user)
5359
}
5460

5561
async fn delete(&self, id: Uuid) -> RepositoryResult<RefreshToken> {
56-
let refresh_token = sqlx::query_as::<_, RefreshToken>(
57-
"DELETE FROM refresh_tokens WHERE id = $1 RETURNING *;",
58-
)
59-
.bind(id)
60-
.fetch_one(&self.pool)
61-
.await?;
62+
let query = "
63+
DELETE FROM refresh_tokens
64+
WHERE id = $1
65+
RETURNING *;
66+
";
67+
let refresh_token = sqlx::query_as::<_, RefreshToken>(query)
68+
.bind(id)
69+
.fetch_one(&self.pool)
70+
.await?;
6271
Ok(refresh_token)
6372
}
6473
}

server/src/routes/auth.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
use axum::{Router, routing::post};
22

3-
use crate::{handlers::auth::login, state::AppState};
3+
use crate::{
4+
handlers::auth::{login, refresh},
5+
state::AppState,
6+
};
47

58
pub fn router() -> Router<AppState> {
6-
Router::new().route("/login", post(login))
9+
Router::new()
10+
.route("/login", post(login))
11+
.route("/refresh", post(refresh))
712
}

server/src/schemas/token.rs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,24 @@ use axum::{
33
http::{StatusCode, header},
44
response::{IntoResponse, Response},
55
};
6-
use serde::Serialize;
6+
use serde::{Deserialize, Serialize};
77

88
#[derive(Serialize)]
99
pub struct LoginResponse {
1010
pub access_token: String,
1111
pub refresh_token: String,
1212
pub token_type: String,
13-
pub expires_in: u64,
13+
}
14+
15+
impl LoginResponse {
16+
pub fn new(access_token: String, refresh_token: String) -> Self {
17+
let token_type = String::from("Bearer");
18+
Self {
19+
access_token,
20+
refresh_token,
21+
token_type,
22+
}
23+
}
1424
}
1525

1626
impl IntoResponse for LoginResponse {
@@ -20,16 +30,55 @@ impl IntoResponse for LoginResponse {
2030
[
2131
(header::CACHE_CONTROL, "no-store"),
2232
(header::PRAGMA, "no-cache"),
33+
(
34+
header::SET_COOKIE,
35+
&refresh_token_cookie(&self.refresh_token),
36+
),
2337
],
2438
Json(self),
2539
)
2640
.into_response()
2741
}
2842
}
2943

44+
#[derive(Deserialize)]
45+
pub struct RefreshRequest {
46+
pub access_token: String,
47+
}
48+
3049
#[derive(Serialize)]
3150
pub struct RefreshResponse {
3251
pub access_token: String,
3352
pub token_type: String,
34-
pub expires_in: String,
53+
}
54+
55+
impl RefreshResponse {
56+
pub fn new(access_token: String) -> Self {
57+
let token_type = String::from("Bearer");
58+
Self {
59+
access_token,
60+
token_type,
61+
}
62+
}
63+
}
64+
65+
impl IntoResponse for RefreshResponse {
66+
fn into_response(self) -> Response {
67+
(
68+
StatusCode::OK,
69+
[
70+
(header::CACHE_CONTROL, "no-store"),
71+
(header::PRAGMA, "no-cache"),
72+
],
73+
Json(self),
74+
)
75+
.into_response()
76+
}
77+
}
78+
79+
fn refresh_token_cookie(refresh_token: &str) -> String {
80+
format!(
81+
"refresh_token={}; HttpOnly; Secure; SameSite=Strict",
82+
refresh_token
83+
)
3584
}

server/src/services/auth.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use argon2::{
33
password_hash::{SaltString, rand_core::OsRng},
44
};
55
use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD};
6-
use chrono::Utc;
6+
use chrono::Local;
77
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
88
use serde::{Deserialize, Serialize};
99
use std::{
@@ -18,14 +18,12 @@ use crate::{
1818
repositories::{refresh_token::RefreshTokenRepository, user::UserRepository},
1919
};
2020

21-
const CONFIG_FILE_PATH: &str = "Server.toml";
2221
const ISSUER: &str = "open-erase";
23-
const ACCESS_TOKEN_VALIDITY_SECS: Duration = Duration::from_secs(60 * 15);
22+
const ACCESS_TOKEN_VALIDITY_DURATION: Duration = Duration::from_secs(60 * 15); // 15 minutes
2423
const KEY_LENGTH: usize = 32;
2524

26-
static ARGON2: LazyLock<Argon2<'static>> = LazyLock::new(|| Argon2::default());
27-
static ENCRYPTION_KEY: LazyLock<[u8; KEY_LENGTH]> =
28-
LazyLock::new(|| generate_byte_key::<KEY_LENGTH>());
25+
static ARGON2: LazyLock<Argon2<'static>> = LazyLock::new(Argon2::default);
26+
static ENCRYPTION_KEY: LazyLock<[u8; KEY_LENGTH]> = LazyLock::new(generate_byte_key::<KEY_LENGTH>);
2927

3028
#[derive(Clone, Serialize, Deserialize)]
3129
pub struct Claims {
@@ -37,8 +35,8 @@ pub struct Claims {
3735

3836
impl Claims {
3937
pub fn new(user_id: Uuid) -> Self {
40-
let now = Utc::now();
41-
let access_token_expires_at = now + ACCESS_TOKEN_VALIDITY_SECS;
38+
let now = Local::now();
39+
let access_token_expires_at = now + ACCESS_TOKEN_VALIDITY_DURATION;
4240
let sub = user_id.to_string();
4341
let exp = access_token_expires_at.timestamp() as usize;
4442
let iat = now.timestamp() as usize;

0 commit comments

Comments
 (0)