Replaced refresh tokens with hashed string, refactored logic
This commit is contained in:
@@ -32,6 +32,7 @@ rust-s3 = "0.33.0"
|
||||
actix-multipart = "0.6.1"
|
||||
openssl = "0.10.60" # Resolve `openssl` `X509StoreRef::objects` is unsound #10
|
||||
rand = "0.8.5"
|
||||
sha2 = "0.10.8"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.32.0"
|
||||
|
||||
@@ -1,148 +1,7 @@
|
||||
use std::env;
|
||||
|
||||
use argon2::{password_hash::{rand_core::OsRng, PasswordHasher, PasswordVerifier, SaltString, Error as HashError}, Argon2, PasswordHash};
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod model;
|
||||
mod routes;
|
||||
mod tokens;
|
||||
|
||||
pub use model::*;
|
||||
pub use tokens::*;
|
||||
pub use routes::init_routes;
|
||||
use siren::ServiceError;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct TokenClaims {
|
||||
sub: String, // Subject (User)
|
||||
token_uuid: String, // Token UUID
|
||||
iss: String, // Issuer (Service)
|
||||
exp: i64, // Expiration time
|
||||
iat: i64, // Issued At
|
||||
nbf: i64 // Not Before
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum TokenType {
|
||||
#[serde(rename = "access")]
|
||||
Access,
|
||||
#[serde(rename = "refresh")]
|
||||
Refresh,
|
||||
#[serde(rename = "none")]
|
||||
None
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TokenType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TokenType::Access => write!(f, "access"),
|
||||
TokenType::Refresh => write!(f, "refresh"),
|
||||
TokenType::None => write!(f, "none")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for TokenType {
|
||||
type Err = ServiceError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"access" => Ok(TokenType::Access),
|
||||
"refresh" => Ok(TokenType::Refresh),
|
||||
"none" => Ok(TokenType::None),
|
||||
_ => Err(ServiceError::new(400, "Invalid token type".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TokenDetails {
|
||||
pub token: Option<String>,
|
||||
pub token_uuid: uuid::Uuid,
|
||||
pub email: String,
|
||||
pub token_type: TokenType,
|
||||
pub expires_in: Option<i64>
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TokenDetails {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}:{}:{}", self.token_type.to_string(), self.email, self.token_uuid.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for TokenDetails {
|
||||
type Err = ServiceError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let parts: Vec<&str> = s.split(":").collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(ServiceError::new(400, "Invalid token".to_string()));
|
||||
}
|
||||
let token_type = parts[0].parse::<TokenType>()?;
|
||||
let email = parts[1].to_string();
|
||||
let uuid = uuid::Uuid::parse_str(parts[2])?;
|
||||
Ok(TokenDetails { token: None, token_uuid: uuid, email, token_type, expires_in: None })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_token(token: &str, public_key: &str) -> Result<TokenDetails, ServiceError> {
|
||||
let key = DecodingKey::from_rsa_pem(public_key.as_bytes())?;
|
||||
let validation = Validation::new(Algorithm::RS256);
|
||||
let decoded = decode::<TokenClaims>(token, &key, &validation)?;
|
||||
let email = decoded.claims.sub;
|
||||
let token_uuid = uuid::Uuid::parse_str(decoded.claims.token_uuid.as_str()).unwrap();
|
||||
Ok(TokenDetails { token: None, token_uuid, email, token_type: TokenType::None, 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 keys_dir = env::var("KEYS_DIR_PATH")?;
|
||||
let access_private_key = std::fs::read_to_string(format!("{}/access_private_key.pem", keys_dir))?;
|
||||
generate_token(&email, TokenType::Refresh, 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 keys_dir = env::var("KEYS_DIR_PATH")?;
|
||||
let refresh_private_key = std::fs::read_to_string(format!("{}/refresh_private_key.pem", keys_dir))?;
|
||||
generate_token(&email, TokenType::Access, refresh_token_max_age, &refresh_private_key)
|
||||
}
|
||||
|
||||
fn generate_token(email: &str, token_type: TokenType, ttl: i64, private_key: &str) -> Result<TokenDetails, ServiceError> {
|
||||
let now = chrono::Utc::now();
|
||||
let mut token_details = TokenDetails {
|
||||
token: None,
|
||||
token_uuid: uuid::Uuid::new_v4(),
|
||||
email: email.to_string(),
|
||||
token_type,
|
||||
expires_in: Some((now + chrono::Duration::minutes(ttl)).timestamp())
|
||||
};
|
||||
let claims = TokenClaims {
|
||||
sub: token_details.email.clone(),
|
||||
iss: "siren".to_string(),
|
||||
token_uuid: token_details.token_uuid.to_string(),
|
||||
exp: token_details.expires_in.unwrap(),
|
||||
iat: now.timestamp(),
|
||||
nbf: now.timestamp()
|
||||
};
|
||||
let header = Header::new(Algorithm::RS256);
|
||||
let key = EncodingKey::from_rsa_pem(private_key.as_bytes())?;
|
||||
let token = encode(&header, &claims, &key)?;
|
||||
token_details.token = Some(token);
|
||||
Ok(token_details)
|
||||
}
|
||||
|
||||
pub fn hash_password(password: &[u8]) -> Result<String, HashError> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
Ok(Argon2::default().hash_password(password, &salt)?.to_string())
|
||||
}
|
||||
|
||||
pub fn verify_password(hash: &str, password: &[u8]) -> Result<(), HashError> {
|
||||
let parsed_hash = PasswordHash::new(hash)?;
|
||||
Ok(Argon2::default().verify_password(password, &parsed_hash)?)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::{future::{ready, Ready}, env};
|
||||
use actix_web::{FromRequest, Error as ActixError, HttpRequest, dev::Payload, http};
|
||||
use argon2::{password_hash::{rand_core::OsRng, PasswordHasher, PasswordVerifier, SaltString, Error as HashError}, Argon2, PasswordHash};
|
||||
use diesel::prelude::*;
|
||||
use log::error;
|
||||
use redis::Commands;
|
||||
@@ -8,7 +9,7 @@ use siren::ServiceError;
|
||||
|
||||
use crate::storage::{schema::users, connection};
|
||||
|
||||
use super::{hash_password, verify_token};
|
||||
use super::AccessToken;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RegisterUser {
|
||||
@@ -35,6 +36,16 @@ impl RegisterUser {
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_password(password: &[u8]) -> Result<String, HashError> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
Ok(Argon2::default().hash_password(password, &salt)?.to_string())
|
||||
}
|
||||
|
||||
pub fn verify_password(hash: &str, password: &[u8]) -> Result<(), HashError> {
|
||||
let parsed_hash = PasswordHash::new(hash)?;
|
||||
Ok(Argon2::default().verify_password(password, &parsed_hash)?)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LoginRequest {
|
||||
pub email: String,
|
||||
@@ -131,7 +142,7 @@ impl FromRequest for JwtAuth {
|
||||
type Error = ActixError;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
let access_token = match req
|
||||
let access_token_string = match req
|
||||
.cookie("access_token")
|
||||
.map(|c| c.value().to_string())
|
||||
.or_else(|| {
|
||||
@@ -148,18 +159,18 @@ impl FromRequest for JwtAuth {
|
||||
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("Failed to read access public key");
|
||||
|
||||
let access_token_details = match verify_token(&access_token, &public_key) {
|
||||
let access_token = match AccessToken::decode(&access_token_string, &public_key) {
|
||||
Ok(token_details) => token_details,
|
||||
Err(err) => {
|
||||
error!("Failed to verify access token: {}", err);
|
||||
return ready(Err(ActixError::from(ServiceError {
|
||||
status: 401,
|
||||
message: format!("Failed to verify access token: {}", err)
|
||||
message: format!("Access token is invaid: {}", err)
|
||||
})))
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
let mut conn = match crate::storage::redis_connection() {
|
||||
Ok(conn) => conn,
|
||||
@@ -172,11 +183,11 @@ impl FromRequest for JwtAuth {
|
||||
}
|
||||
};
|
||||
let user_email = match conn.get::<_, String>(access_token_uuid.clone().to_string()) {
|
||||
Ok(result) => result,
|
||||
Ok(result) => serde_json::from_str::<AccessToken>(&result).unwrap().email,
|
||||
Err(_) => {
|
||||
return ready(Err(ActixError::from(ServiceError {
|
||||
status: 401,
|
||||
message: format!("Access token was not found")
|
||||
message: format!("Access token is invalid")
|
||||
})))
|
||||
}
|
||||
};
|
||||
@@ -187,7 +198,7 @@ impl FromRequest for JwtAuth {
|
||||
}
|
||||
Err(_) => return ready(Err(ActixError::from(ServiceError {
|
||||
status: 401,
|
||||
message: format!("User was not found")
|
||||
message: format!("User does not exist")
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
87
service/src/auth/tokens.rs
Normal file
87
service/src/auth/tokens.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use std::env;
|
||||
|
||||
use argon2::password_hash::{rand_core::OsRng, SaltString};
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{digest::{generic_array::GenericArray, typenum::U32}, Digest, Sha256};
|
||||
use siren::ServiceError;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct TokenClaims {
|
||||
sub: String, // Subject (User)
|
||||
token_uuid: String, // Access Token UUID
|
||||
iss: String, // Issuer (Service)
|
||||
exp: i64, // Expiration time
|
||||
iat: i64, // Issued At
|
||||
nbf: i64 // Not Before
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AccessToken {
|
||||
pub token: Option<String>,
|
||||
pub token_uuid: uuid::Uuid,
|
||||
pub email: String,
|
||||
pub expires_in: Option<i64>
|
||||
}
|
||||
|
||||
impl AccessToken {
|
||||
pub fn new(email: &str) -> Result<Self, ServiceError> {
|
||||
let ttl = env::var("ACCESS_TOKEN_MAXAGE")
|
||||
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
||||
.parse::<i64>()
|
||||
.expect("ACCESS_TOKEN_MAXAGE must be an integer");
|
||||
let keys_dir = env::var("KEYS_DIR_PATH")?;
|
||||
let private_key = std::fs::read_to_string(format!("{}/access_private_key.pem", keys_dir))?;
|
||||
let now = chrono::Utc::now();
|
||||
let mut token_details = Self {
|
||||
token: None,
|
||||
token_uuid: uuid::Uuid::new_v4(),
|
||||
email: email.to_string(),
|
||||
expires_in: Some((now + chrono::Duration::minutes(ttl)).timestamp())
|
||||
};
|
||||
let claims = TokenClaims {
|
||||
sub: token_details.email.clone(),
|
||||
iss: "siren".to_string(),
|
||||
token_uuid: token_details.token_uuid.to_string(),
|
||||
exp: token_details.expires_in.unwrap(),
|
||||
iat: now.timestamp(),
|
||||
nbf: now.timestamp()
|
||||
};
|
||||
let header = Header::new(Algorithm::RS256);
|
||||
let key = EncodingKey::from_rsa_pem(private_key.as_bytes())?;
|
||||
let token = encode(&header, &claims, &key)?;
|
||||
token_details.token = Some(token);
|
||||
Ok(token_details)
|
||||
}
|
||||
|
||||
pub fn decode(token: &str, public_key: &str) -> Result<Self, ServiceError> {
|
||||
let key = DecodingKey::from_rsa_pem(public_key.as_bytes())?;
|
||||
let validation = Validation::new(Algorithm::RS256);
|
||||
let decoded = decode::<TokenClaims>(token, &key, &validation)?;
|
||||
let email = decoded.claims.sub;
|
||||
let token_uuid = uuid::Uuid::parse_str(decoded.claims.token_uuid.as_str()).unwrap();
|
||||
Ok(Self { token: None, token_uuid, email, expires_in: None })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RefreshToken {
|
||||
pub hash: String,
|
||||
pub email: String,
|
||||
pub ip_address: String,
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
impl RefreshToken {
|
||||
pub fn new(email: &str, ip_address: &str) -> Self {
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let hash: GenericArray<u8, U32> = Sha256::digest(format!("{}:{}:{}:{}", email, ip_address, now, salt).as_bytes());
|
||||
Self {
|
||||
hash: format!("{:x}", hash),
|
||||
email: email.to_string(),
|
||||
ip_address: ip_address.to_string(),
|
||||
timestamp: now
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export async function me(): Promise<ResponseAuth | undefined> {
|
||||
}
|
||||
|
||||
export async function hasSession(): Promise<boolean> {
|
||||
const response = await getRequest('auth/check-session');
|
||||
const response = await getRequest('auth/session');
|
||||
if (response?.status === 200) {
|
||||
return response?.json();
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user