Implemented refresh token
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
use argon2::{password_hash::{rand_core::OsRng, PasswordHasher, PasswordVerifier, SaltString, Error as HashError}, Argon2, PasswordHash};
|
use argon2::{password_hash::{rand_core::OsRng, PasswordHasher, PasswordVerifier, SaltString, Error as HashError}, Argon2, PasswordHash};
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm};
|
use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm};
|
||||||
@@ -41,6 +43,26 @@ pub fn verify_token(token: &str, public_key: &str) -> Result<TokenDetails, Servi
|
|||||||
Ok(TokenDetails { token: None, token_uuid, email, expires_in: None })
|
Ok(TokenDetails { token: None, token_uuid, email, expires_in: None })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generate_access_token(email: &str) -> Result<TokenDetails, ServiceError> {
|
||||||
|
let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE")
|
||||||
|
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
||||||
|
.parse::<i64>()
|
||||||
|
.expect("ACCESS_TOKEN_MAXAGE must be an integer");
|
||||||
|
let access_private_key = env::var("ACCESS_TOKEN_PRIVATE_KEY")
|
||||||
|
.expect("ACCESS_TOKEN_PRIVATE_KEY must be set");
|
||||||
|
generate_token(&email, access_token_max_age, &access_private_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_refresh_token(email: &str) -> Result<TokenDetails, ServiceError> {
|
||||||
|
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_private_key = env::var("REFRESH_TOKEN_PRIVATE_KEY")
|
||||||
|
.expect("REFRESH_TOKEN_PRIVATE_KEY must be set");
|
||||||
|
generate_token(&email, refresh_token_max_age, &refresh_private_key)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn generate_token(email: &str, ttl: i64, private_key: &str) -> Result<TokenDetails, ServiceError> {
|
pub fn generate_token(email: &str, ttl: i64, private_key: &str) -> Result<TokenDetails, ServiceError> {
|
||||||
let now = chrono::Utc::now();
|
let now = chrono::Utc::now();
|
||||||
let mut token_details = TokenDetails {
|
let mut token_details = TokenDetails {
|
||||||
|
|||||||
@@ -100,9 +100,8 @@ impl From<QueryUser> for ResponseUser {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct JwtAuth {
|
pub struct JwtAuth {
|
||||||
pub access_token_uuid: uuid::Uuid,
|
pub token: uuid::Uuid,
|
||||||
pub email: String,
|
pub user: ResponseUser
|
||||||
pub role: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromRequest for JwtAuth {
|
impl FromRequest for JwtAuth {
|
||||||
@@ -162,7 +161,7 @@ impl FromRequest for JwtAuth {
|
|||||||
|
|
||||||
match QueryUser::get_by_email(&user_email) {
|
match QueryUser::get_by_email(&user_email) {
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
ready(Ok(JwtAuth { access_token_uuid, email: user.email, role: user.role }))
|
ready(Ok(JwtAuth { token: access_token_uuid, user: user.into() }))
|
||||||
}
|
}
|
||||||
Err(err) => return ready(Err(ActixError::from(ServiceError {
|
Err(err) => return ready(Err(ActixError::from(ServiceError {
|
||||||
status: 500,
|
status: 500,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use log::error;
|
|||||||
use redis::AsyncCommands;
|
use redis::AsyncCommands;
|
||||||
use siren::ServiceError;
|
use siren::ServiceError;
|
||||||
|
|
||||||
use crate::{auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, generate_token, JwtAuth, ResponseUser}, db};
|
use crate::{auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, JwtAuth, verify_token, generate_access_token, generate_refresh_token}, db};
|
||||||
|
|
||||||
#[post("/register")]
|
#[post("/register")]
|
||||||
async fn register(user: web::Json<RegisterUser>) -> HttpResponse {
|
async fn register(user: web::Json<RegisterUser>) -> HttpResponse {
|
||||||
@@ -37,30 +37,19 @@ async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
|
|||||||
Ok(query_user) => query_user,
|
Ok(query_user) => query_user,
|
||||||
Err(err) => return ResponseError::error_response(&err)
|
Err(err) => return ResponseError::error_response(&err)
|
||||||
};
|
};
|
||||||
let hash = query_user.hash;
|
let hash = &query_user.hash;
|
||||||
let password = request.password.as_bytes();
|
let password = request.password.as_bytes();
|
||||||
match verify_password(&hash, password) {
|
match verify_password(hash, password) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE")
|
let access_token_details = match generate_access_token(&email) {
|
||||||
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
|
||||||
.parse::<i64>()
|
|
||||||
.expect("ACCESS_TOKEN_MAXAGE must be an integer");
|
|
||||||
let access_private_key = env::var("ACCESS_TOKEN_PRIVATE_KEY")
|
|
||||||
.expect("ACCESS_TOKEN_PRIVATE_KEY must be set");
|
|
||||||
let access_token_details = match generate_token(&email, access_token_max_age, &access_private_key) {
|
|
||||||
Ok(token_details) => token_details,
|
Ok(token_details) => token_details,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Failed to generate access token: {}", err);
|
error!("Failed to generate access token: {}", err);
|
||||||
return ResponseError::error_response(&err)
|
return ResponseError::error_response(&err)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let refresh_token_max_age = env::var("REFRESH_TOKEN_MAXAGE")
|
|
||||||
.expect("REFRESH_TOKEN_MAXAGE must be set")
|
let refresh_token_details = match generate_refresh_token(&email) {
|
||||||
.parse::<i64>()
|
|
||||||
.expect("REFRESH_TOKEN_MAXAGE must be an integer");
|
|
||||||
let refresh_private_key = env::var("REFRESH_TOKEN_PRIVATE_KEY")
|
|
||||||
.expect("REFRESH_TOKEN_PRIVATE_KEY must be set");
|
|
||||||
let refresh_token_details = match generate_token(&email, refresh_token_max_age, &refresh_private_key) {
|
|
||||||
Ok(token_details) => token_details,
|
Ok(token_details) => token_details,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Failed to generate refresh token: {}", err);
|
error!("Failed to generate refresh token: {}", err);
|
||||||
@@ -76,6 +65,16 @@ async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE")
|
||||||
|
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
||||||
|
.parse::<i64>()
|
||||||
|
.expect("ACCESS_TOKEN_MAXAGE must be an integer");
|
||||||
|
|
||||||
|
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 access_result: redis::RedisResult<()> = conn.set_ex(access_token_details.token_uuid.to_string(), &email, (access_token_max_age * 60) as usize).await;
|
let access_result: redis::RedisResult<()> = conn.set_ex(access_token_details.token_uuid.to_string(), &email, (access_token_max_age * 60) as usize).await;
|
||||||
if let Err(err) = access_result {
|
if let Err(err) = access_result {
|
||||||
error!("Failed to set access token in redis: {}", err);
|
error!("Failed to set access token in redis: {}", err);
|
||||||
@@ -110,11 +109,13 @@ async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
|
|||||||
.http_only(false)
|
.http_only(false)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
|
let access_token_uuid = uuid::Uuid::parse_str(&access_token_details.token_uuid.to_string()).unwrap();
|
||||||
|
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.cookie(access_cookie)
|
.cookie(access_cookie)
|
||||||
.cookie(refresh_cookie)
|
.cookie(refresh_cookie)
|
||||||
.cookie(logged_in_cookie)
|
.cookie(logged_in_cookie)
|
||||||
.json(access_token_details.token.unwrap())
|
.json(JwtAuth { token: access_token_uuid, user: query_user.into() })
|
||||||
},
|
},
|
||||||
Err(err) => ResponseError::error_response(&ServiceError {
|
Err(err) => ResponseError::error_response(&ServiceError {
|
||||||
status: 401,
|
status: 401,
|
||||||
@@ -125,7 +126,79 @@ async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
|
|||||||
|
|
||||||
#[get("/refresh")]
|
#[get("/refresh")]
|
||||||
async fn refresh(req: HttpRequest) -> HttpResponse {
|
async fn refresh(req: HttpRequest) -> HttpResponse {
|
||||||
HttpResponse::Ok().finish()
|
let refresh_token = match req.cookie("refresh_token") {
|
||||||
|
Some(cookie) => cookie.value().to_string(),
|
||||||
|
None => return ResponseError::error_response(&ServiceError {
|
||||||
|
status: 401,
|
||||||
|
message: "Refresh token not found".to_string()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let public_key = env::var("REFRESH_TOKEN_PUBLIC_KEY")
|
||||||
|
.expect("REFRESH_TOKEN_PUBLIC_KEY must be set");
|
||||||
|
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 db::redis_async_connection().await {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to get redis connection: {}", err);
|
||||||
|
return ResponseError::error_response(&err)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let redis_result: redis::RedisResult<String> = conn.get(refresh_token_details.token_uuid.to_string()).await;
|
||||||
|
let email = match redis_result {
|
||||||
|
Ok(email) => email,
|
||||||
|
Err(err) => return ResponseError::error_response(&ServiceError {
|
||||||
|
status: 500,
|
||||||
|
message: format!("Failed to get refresh token from redis: {}", err)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
match QueryUser::get_by_email(&email) {
|
||||||
|
Ok(_) => {
|
||||||
|
let access_token_details = match generate_access_token(&email) {
|
||||||
|
Ok(token_details) => token_details,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to generate access token: {}", err);
|
||||||
|
return ResponseError::error_response(&err)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE")
|
||||||
|
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
||||||
|
.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(), &email, (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 {
|
||||||
|
status: 500,
|
||||||
|
message: format!("Failed to set access token in redis: {}", err)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let access_cookie = Cookie::build("access_token", access_token_details.token.clone().unwrap())
|
||||||
|
.path("/")
|
||||||
|
.max_age(Duration::new(access_token_max_age * 60, 0))
|
||||||
|
.http_only(true)
|
||||||
|
.finish();
|
||||||
|
let logged_in_cookie = Cookie::build("logged_in", "true")
|
||||||
|
.path("/")
|
||||||
|
.max_age(Duration::new(access_token_max_age * 60, 0))
|
||||||
|
.http_only(false)
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.cookie(access_cookie)
|
||||||
|
.cookie(logged_in_cookie)
|
||||||
|
.json(access_token_details.token.unwrap())
|
||||||
|
},
|
||||||
|
Err(err) => return ResponseError::error_response(&err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/logout")]
|
#[post("/logout")]
|
||||||
@@ -135,12 +208,7 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse {
|
|||||||
|
|
||||||
#[get("/me")]
|
#[get("/me")]
|
||||||
async fn me(auth: JwtAuth) -> HttpResponse {
|
async fn me(auth: JwtAuth) -> HttpResponse {
|
||||||
let query_user = match QueryUser::get_by_email(&auth.email) {
|
HttpResponse::Ok().json(auth)
|
||||||
Ok(user) => user,
|
|
||||||
Err(err) => return ResponseError::error_response(&err)
|
|
||||||
};
|
|
||||||
let user: ResponseUser = query_user.into();
|
|
||||||
HttpResponse::Ok().json(user)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
pub fn init_routes(config: &mut web::ServiceConfig) {
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
export interface ResponseUser {
|
||||||
|
token: string;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export default function Topbar() {
|
|||||||
if (Cookies.get('logged_in')) {
|
if (Cookies.get('logged_in')) {
|
||||||
me().then((response) => {
|
me().then((response) => {
|
||||||
if (response?.status == 200) {
|
if (response?.status == 200) {
|
||||||
setUser(response.data);
|
setUser(response.data.user);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user