Replaced refresh tokens with hashed string, refactored logic
This commit is contained in:
@@ -6,7 +6,7 @@ use redis::AsyncCommands;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use siren::ServiceError;
|
||||
|
||||
use crate::{auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, JwtAuth, verify_token, generate_access_token, generate_refresh_token}, storage};
|
||||
use crate::{auth::{verify_password, AccessToken, InsertUser, JwtAuth, LoginRequest, QueryUser, RefreshToken, RegisterUser}, storage};
|
||||
|
||||
#[post("/register")]
|
||||
async fn register(user: web::Json<RegisterUser>) -> HttpResponse {
|
||||
@@ -31,18 +31,20 @@ async fn register(user: web::Json<RegisterUser>) -> HttpResponse {
|
||||
}
|
||||
|
||||
#[post("/login")]
|
||||
async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
|
||||
let email = request.email.clone();
|
||||
async fn login(request: HttpRequest, login_request: web::Json<LoginRequest>) -> HttpResponse {
|
||||
let email = login_request.email.clone();
|
||||
// Get IP address
|
||||
let ip_address = request.peer_addr().unwrap().ip().to_string();
|
||||
|
||||
let query_user = match QueryUser::get_by_email(&email) {
|
||||
Ok(query_user) => query_user,
|
||||
Err(err) => return ResponseError::error_response(&err)
|
||||
};
|
||||
let hash = &query_user.hash;
|
||||
let password = request.password.as_bytes();
|
||||
let password = login_request.password.as_bytes();
|
||||
match verify_password(hash, password) {
|
||||
Ok(_) => {
|
||||
let access_token_details = match generate_access_token(&email) {
|
||||
let access_token = match AccessToken::new(&email) {
|
||||
Ok(token_details) => token_details,
|
||||
Err(err) => {
|
||||
error!("Failed to generate access token: {}", err);
|
||||
@@ -50,13 +52,7 @@ async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
|
||||
}
|
||||
};
|
||||
|
||||
let refresh_token_details = match generate_refresh_token(&email) {
|
||||
Ok(token_details) => token_details,
|
||||
Err(err) => {
|
||||
error!("Failed to generate refresh token: {}", err);
|
||||
return ResponseError::error_response(&err)
|
||||
}
|
||||
};
|
||||
let refresh_token = RefreshToken::new(&email, &ip_address);
|
||||
|
||||
let mut conn = match storage::redis_async_connection().await {
|
||||
Ok(conn) => conn,
|
||||
@@ -76,7 +72,7 @@ async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
|
||||
.parse::<i64>()
|
||||
.expect("REFRESH_TOKEN_MAXAGE must be an integer");
|
||||
|
||||
let access_result: redis::RedisResult<()> = conn.set_ex(access_token_details.token_uuid.to_string(), &access_token_details.to_string(), (access_token_max_age * 60) as usize).await;
|
||||
let access_result: redis::RedisResult<()> = conn.set_ex(access_token.token_uuid.to_string(), &serde_json::to_string(&access_token).unwrap(), (access_token_max_age * 60) as usize).await;
|
||||
if let Err(err) = access_result {
|
||||
error!("Failed to set access token in redis: {}", err);
|
||||
return ResponseError::error_response(&ServiceError {
|
||||
@@ -85,7 +81,7 @@ async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
|
||||
})
|
||||
};
|
||||
|
||||
let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token_details.token_uuid.to_string(), &refresh_token_details.to_string(), (refresh_token_max_age * 60) as usize).await;
|
||||
let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token.hash.to_string(), &serde_json::to_string(&refresh_token).unwrap(), (refresh_token_max_age * 60) as usize).await;
|
||||
if let Err(err) = refresh_result {
|
||||
error!("Failed to set refresh token in redis: {}", err);
|
||||
return ResponseError::error_response(&ServiceError {
|
||||
@@ -94,13 +90,13 @@ async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
|
||||
})
|
||||
};
|
||||
|
||||
let access_cookie = Cookie::build("access_token", access_token_details.token.clone().unwrap())
|
||||
let access_cookie = Cookie::build("access_token", access_token.token.clone().unwrap())
|
||||
.path("/")
|
||||
.max_age(Duration::new(access_token_max_age * 60, 0))
|
||||
.http_only(true)
|
||||
.secure(true)
|
||||
.finish();
|
||||
let refresh_cookie = Cookie::build("refresh_token", refresh_token_details.token.clone().unwrap())
|
||||
let refresh_cookie = Cookie::build("refresh_token", refresh_token.hash.clone())
|
||||
.path("/")
|
||||
.max_age(Duration::new(refresh_token_max_age * 60, 0))
|
||||
.http_only(true)
|
||||
@@ -112,7 +108,7 @@ async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
|
||||
.http_only(false)
|
||||
.finish();
|
||||
|
||||
let access_token_uuid = uuid::Uuid::parse_str(&access_token_details.token_uuid.to_string()).unwrap();
|
||||
let access_token_uuid = uuid::Uuid::parse_str(&access_token.token_uuid.to_string()).unwrap();
|
||||
|
||||
HttpResponse::Ok()
|
||||
.cookie(access_cookie)
|
||||
@@ -134,6 +130,7 @@ struct RefreshParams {
|
||||
|
||||
#[get("/refresh")]
|
||||
async fn refresh(req: HttpRequest) -> HttpResponse {
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
let params = match web::Query::<RefreshParams>::from_query(req.query_string()) {
|
||||
Ok(params) => params,
|
||||
Err(err) => return ResponseError::error_response(&ServiceError {
|
||||
@@ -142,7 +139,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
||||
})
|
||||
};
|
||||
|
||||
let refresh_token = match req.cookie("refresh_token") {
|
||||
let refresh_token_hash = match req.cookie("refresh_token") {
|
||||
Some(cookie) => cookie.value().to_string(),
|
||||
None => return ResponseError::error_response(&ServiceError {
|
||||
status: 401,
|
||||
@@ -150,19 +147,39 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
||||
})
|
||||
};
|
||||
|
||||
let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set");
|
||||
let public_key = std::fs::read_to_string(format!("{}/refresh_public_key.pem", keys_dir))
|
||||
.expect("Unable to read refresh public key");
|
||||
let refresh_token_details = match verify_token(&refresh_token, &public_key) {
|
||||
Ok(token_details) => token_details,
|
||||
Err(err) => return ResponseError::error_response(&err)
|
||||
let mut conn = match storage::redis_async_connection().await {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
error!("Failed to get redis connection: {}", err);
|
||||
return ResponseError::error_response(&err)
|
||||
}
|
||||
};
|
||||
|
||||
let email = refresh_token_details.email.clone();
|
||||
let refresh_token: RefreshToken = match conn.get::<_, String>(refresh_token_hash.clone()).await {
|
||||
Ok(result) => match serde_json::from_str(&result) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
error!("Failed to deserialize refresh token: {}", err);
|
||||
return ResponseError::error_response(&ServiceError {
|
||||
status: 500,
|
||||
message: format!("Failed to deserialize refresh token: {}", err)
|
||||
})
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Failed to get refresh token from redis: {}", err);
|
||||
return ResponseError::error_response(&ServiceError {
|
||||
status: 500,
|
||||
message: format!("Failed to get refresh token from redis: {}", err)
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let email = refresh_token.email.clone();
|
||||
|
||||
match QueryUser::get_by_email(&email) {
|
||||
Ok(query_user) => {
|
||||
let access_token_details = match generate_access_token(&email) {
|
||||
let access_token_details = match AccessToken::new(&email) {
|
||||
Ok(token_details) => token_details,
|
||||
Err(err) => {
|
||||
error!("Failed to generate access token: {}", err);
|
||||
@@ -170,24 +187,16 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
||||
}
|
||||
};
|
||||
|
||||
let mut conn = match storage::redis_async_connection().await {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
error!("Failed to get redis connection: {}", err);
|
||||
return ResponseError::error_response(&err)
|
||||
}
|
||||
};
|
||||
|
||||
// Delete old access token if it exists
|
||||
match req.cookie("access_token") {
|
||||
Some(cookie) => {
|
||||
let access_token = cookie.value().to_string();
|
||||
let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set");
|
||||
let public_key = std::fs::read_to_string(format!("{}/access_public_key.pem", keys_dir))
|
||||
.expect("Unable to read access public key");
|
||||
match verify_token(&access_token, &public_key) {
|
||||
match AccessToken::decode(&access_token, &public_key) {
|
||||
Ok(token_details) => {
|
||||
let _: redis::RedisResult<()> = conn.del(token_details.token_uuid.to_string()).await;
|
||||
|
||||
},
|
||||
Err(_) => {}
|
||||
};
|
||||
@@ -200,7 +209,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
||||
.parse::<i64>()
|
||||
.expect("ACCESS_TOKEN_MAXAGE must be an integer");
|
||||
|
||||
let access_result: redis::RedisResult<()> = conn.set_ex(access_token_details.token_uuid.to_string(), &access_token_details.to_string(), (access_token_max_age * 60) as usize).await;
|
||||
let access_result: redis::RedisResult<()> = conn.set_ex(access_token_details.token_uuid.to_string(), &serde_json::to_string(&access_token_details).unwrap(), (access_token_max_age * 60) as usize).await;
|
||||
if let Err(err) = access_result {
|
||||
error!("Failed to set access token in redis: {}", err);
|
||||
return ResponseError::error_response(&ServiceError {
|
||||
@@ -230,22 +239,15 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
||||
};
|
||||
if refresh_token_rotation {
|
||||
// Delete the old refresh token
|
||||
let _: redis::RedisResult<()> = conn.del(refresh_token_details.token_uuid.to_string()).await;
|
||||
let _: redis::RedisResult<()> = conn.del(refresh_token.hash.to_string()).await;
|
||||
|
||||
let refresh_token_details = match generate_refresh_token(&refresh_token_details.email) {
|
||||
Ok(token_details) => token_details,
|
||||
Err(err) => {
|
||||
error!("Failed to generate refresh token: {}", err);
|
||||
return ResponseError::error_response(&err)
|
||||
}
|
||||
};
|
||||
|
||||
let refresh_token = RefreshToken::new(&refresh_token.email, &ip_address);
|
||||
let refresh_token_max_age = env::var("REFRESH_TOKEN_MAXAGE")
|
||||
.expect("REFRESH_TOKEN_MAXAGE must be set")
|
||||
.parse::<i64>()
|
||||
.expect("REFRESH_TOKEN_MAXAGE must be an integer");
|
||||
|
||||
let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token_details.token_uuid.to_string(), &refresh_token_details.to_string(), (refresh_token_max_age * 60) as usize).await;
|
||||
let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token.hash.to_string(), &serde_json::to_string(&refresh_token).unwrap(), (refresh_token_max_age * 60) as usize).await;
|
||||
if let Err(err) = refresh_result {
|
||||
error!("Failed to set refresh token in redis: {}", err);
|
||||
return ResponseError::error_response(&ServiceError {
|
||||
@@ -254,7 +256,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
||||
})
|
||||
};
|
||||
|
||||
let refresh_cookie = Cookie::build("refresh_token", refresh_token_details.token.clone().unwrap())
|
||||
let refresh_cookie = Cookie::build("refresh_token", refresh_token.hash.clone())
|
||||
.path("/")
|
||||
.max_age(Duration::new(refresh_token_max_age * 60, 0))
|
||||
.http_only(true)
|
||||
@@ -279,7 +281,6 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
||||
|
||||
#[post("/logout")]
|
||||
async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse {
|
||||
let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set");
|
||||
let refresh_token = match req.cookie("refresh_token") {
|
||||
Some(cookie) => cookie.value().to_string(),
|
||||
None => return ResponseError::error_response(&ServiceError {
|
||||
@@ -287,12 +288,6 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse {
|
||||
message: "Refresh token not found".to_string()
|
||||
})
|
||||
};
|
||||
let public_key = std::fs::read_to_string(format!("{}/refresh_public_key.pem", keys_dir))
|
||||
.expect("Unable to read refresh public key");
|
||||
let refresh_token_details = match verify_token(&refresh_token, &public_key) {
|
||||
Ok(token_details) => token_details,
|
||||
Err(err) => return ResponseError::error_response(&err)
|
||||
};
|
||||
|
||||
let mut conn = match storage::redis_async_connection().await {
|
||||
Ok(conn) => conn,
|
||||
@@ -303,7 +298,7 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse {
|
||||
};
|
||||
|
||||
let access_result: redis::RedisResult<()> = conn.del(&[
|
||||
refresh_token_details.token_uuid.to_string(),
|
||||
refresh_token.to_string(),
|
||||
auth.token.to_string()
|
||||
]).await;
|
||||
if let Err(err) = access_result {
|
||||
@@ -342,41 +337,6 @@ async fn me(auth: JwtAuth) -> HttpResponse {
|
||||
HttpResponse::Ok().json(auth)
|
||||
}
|
||||
|
||||
#[get("/check-session")]
|
||||
async fn check_session(req: HttpRequest) -> HttpResponse {
|
||||
let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set");
|
||||
// If there is a access_token cookie, check if it is valid
|
||||
let has_session = match req.cookie("access_token") {
|
||||
Some(cookie) => {
|
||||
let access_token = cookie.value().to_string();
|
||||
let public_key = std::fs::read_to_string(format!("{}/access_public_key.pem", keys_dir))
|
||||
.expect("Unable to read access public key");
|
||||
match verify_token(&access_token, &public_key) {
|
||||
Ok(_) => true,
|
||||
Err(_) => false
|
||||
}
|
||||
},
|
||||
None => false
|
||||
};
|
||||
if !has_session {
|
||||
// If there is a refresh_token cookie, check if it is valid
|
||||
match req.cookie("refresh_token") {
|
||||
Some(cookie) => {
|
||||
let refresh_token = cookie.value().to_string();
|
||||
let public_key = std::fs::read_to_string(format!("{}/refresh_public_key.pem", keys_dir))
|
||||
.expect("Unable to read refresh public key");
|
||||
match verify_token(&refresh_token, &public_key) {
|
||||
Ok(_) => return HttpResponse::Ok().json(true),
|
||||
Err(_) => return HttpResponse::Ok().json(false)
|
||||
};
|
||||
},
|
||||
None => return HttpResponse::Ok().json(false)
|
||||
};
|
||||
} else {
|
||||
return HttpResponse::Ok().json(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/roles")]
|
||||
async fn roles() -> HttpResponse {
|
||||
HttpResponse::Ok().json(vec!["admin", "user"])
|
||||
@@ -401,6 +361,5 @@ pub fn init_routes(config: &mut web::ServiceConfig) {
|
||||
.service(logout)
|
||||
.service(me)
|
||||
.service(roles)
|
||||
.service(check_session)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user