Replaced refresh tokens with hashed string, refactored logic

This commit is contained in:
Benjamin Sherriff
2024-01-30 11:10:57 -05:00
parent d74e8e181b
commit 40a45275d6
6 changed files with 161 additions and 244 deletions

View File

@@ -32,6 +32,7 @@ rust-s3 = "0.33.0"
actix-multipart = "0.6.1" actix-multipart = "0.6.1"
openssl = "0.10.60" # Resolve `openssl` `X509StoreRef::objects` is unsound #10 openssl = "0.10.60" # Resolve `openssl` `X509StoreRef::objects` is unsound #10
rand = "0.8.5" rand = "0.8.5"
sha2 = "0.10.8"
[dependencies.tokio] [dependencies.tokio]
version = "1.32.0" version = "1.32.0"

View File

@@ -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 model;
mod routes; mod routes;
mod tokens;
pub use model::*; pub use model::*;
pub use tokens::*;
pub use routes::init_routes; 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)?)
}

View File

@@ -1,5 +1,6 @@
use std::{future::{ready, Ready}, env}; use std::{future::{ready, Ready}, env};
use actix_web::{FromRequest, Error as ActixError, HttpRequest, dev::Payload, http}; 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 diesel::prelude::*;
use log::error; use log::error;
use redis::Commands; use redis::Commands;
@@ -8,7 +9,7 @@ use siren::ServiceError;
use crate::storage::{schema::users, connection}; use crate::storage::{schema::users, connection};
use super::{hash_password, verify_token}; use super::AccessToken;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct RegisterUser { 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)] #[derive(Debug, Serialize, Deserialize)]
pub struct LoginRequest { pub struct LoginRequest {
pub email: String, pub email: String,
@@ -131,7 +142,7 @@ impl FromRequest for JwtAuth {
type Error = ActixError; type Error = ActixError;
type Future = Ready<Result<Self, Self::Error>>; type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let access_token = match req let access_token_string = match req
.cookie("access_token") .cookie("access_token")
.map(|c| c.value().to_string()) .map(|c| c.value().to_string())
.or_else(|| { .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 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 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, Ok(token_details) => token_details,
Err(err) => { Err(err) => {
error!("Failed to verify access token: {}", err); error!("Failed to verify access token: {}", err);
return ready(Err(ActixError::from(ServiceError { return ready(Err(ActixError::from(ServiceError {
status: 401, 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() { let mut conn = match crate::storage::redis_connection() {
Ok(conn) => conn, Ok(conn) => conn,
@@ -172,11 +183,11 @@ impl FromRequest for JwtAuth {
} }
}; };
let user_email = match conn.get::<_, String>(access_token_uuid.clone().to_string()) { 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(_) => { Err(_) => {
return ready(Err(ActixError::from(ServiceError { return ready(Err(ActixError::from(ServiceError {
status: 401, 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 { Err(_) => return ready(Err(ActixError::from(ServiceError {
status: 401, status: 401,
message: format!("User was not found") message: format!("User does not exist")
}))) })))
} }
} }

View File

@@ -6,7 +6,7 @@ use redis::AsyncCommands;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use siren::ServiceError; 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")] #[post("/register")]
async fn register(user: web::Json<RegisterUser>) -> HttpResponse { async fn register(user: web::Json<RegisterUser>) -> HttpResponse {
@@ -31,18 +31,20 @@ async fn register(user: web::Json<RegisterUser>) -> HttpResponse {
} }
#[post("/login")] #[post("/login")]
async fn login(request: web::Json<LoginRequest>) -> HttpResponse { async fn login(request: HttpRequest, login_request: web::Json<LoginRequest>) -> HttpResponse {
let email = request.email.clone(); 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) { let query_user = match QueryUser::get_by_email(&email) {
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 = login_request.password.as_bytes();
match verify_password(hash, password) { match verify_password(hash, password) {
Ok(_) => { Ok(_) => {
let access_token_details = match generate_access_token(&email) { let access_token = match AccessToken::new(&email) {
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);
@@ -50,13 +52,7 @@ async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
} }
}; };
let refresh_token_details = match generate_refresh_token(&email) { let refresh_token = RefreshToken::new(&email, &ip_address);
Ok(token_details) => token_details,
Err(err) => {
error!("Failed to generate refresh token: {}", err);
return ResponseError::error_response(&err)
}
};
let mut conn = match storage::redis_async_connection().await { let mut conn = match storage::redis_async_connection().await {
Ok(conn) => conn, Ok(conn) => conn,
@@ -76,7 +72,7 @@ async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
.parse::<i64>() .parse::<i64>()
.expect("REFRESH_TOKEN_MAXAGE must be an integer"); .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 { if let Err(err) = access_result {
error!("Failed to set access token in redis: {}", err); error!("Failed to set access token in redis: {}", err);
return ResponseError::error_response(&ServiceError { 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 { if let Err(err) = refresh_result {
error!("Failed to set refresh token in redis: {}", err); error!("Failed to set refresh token in redis: {}", err);
return ResponseError::error_response(&ServiceError { 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("/") .path("/")
.max_age(Duration::new(access_token_max_age * 60, 0)) .max_age(Duration::new(access_token_max_age * 60, 0))
.http_only(true) .http_only(true)
.secure(true) .secure(true)
.finish(); .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("/") .path("/")
.max_age(Duration::new(refresh_token_max_age * 60, 0)) .max_age(Duration::new(refresh_token_max_age * 60, 0))
.http_only(true) .http_only(true)
@@ -112,7 +108,7 @@ 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(); let access_token_uuid = uuid::Uuid::parse_str(&access_token.token_uuid.to_string()).unwrap();
HttpResponse::Ok() HttpResponse::Ok()
.cookie(access_cookie) .cookie(access_cookie)
@@ -134,6 +130,7 @@ struct RefreshParams {
#[get("/refresh")] #[get("/refresh")]
async fn refresh(req: HttpRequest) -> HttpResponse { 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()) { let params = match web::Query::<RefreshParams>::from_query(req.query_string()) {
Ok(params) => params, Ok(params) => params,
Err(err) => return ResponseError::error_response(&ServiceError { 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(), Some(cookie) => cookie.value().to_string(),
None => return ResponseError::error_response(&ServiceError { None => return ResponseError::error_response(&ServiceError {
status: 401, status: 401,
@@ -150,26 +147,6 @@ 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 email = refresh_token_details.email.clone();
match QueryUser::get_by_email(&email) {
Ok(query_user) => {
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 mut conn = match storage::redis_async_connection().await { let mut conn = match storage::redis_async_connection().await {
Ok(conn) => conn, Ok(conn) => conn,
Err(err) => { Err(err) => {
@@ -178,16 +155,48 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
} }
}; };
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 AccessToken::new(&email) {
Ok(token_details) => token_details,
Err(err) => {
error!("Failed to generate access token: {}", err);
return ResponseError::error_response(&err)
}
};
// Delete old access token if it exists // Delete old access token if it exists
match req.cookie("access_token") { match req.cookie("access_token") {
Some(cookie) => { Some(cookie) => {
let access_token = cookie.value().to_string(); 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)) let public_key = std::fs::read_to_string(format!("{}/access_public_key.pem", keys_dir))
.expect("Unable to read access public key"); .expect("Unable to read access public key");
match verify_token(&access_token, &public_key) { match AccessToken::decode(&access_token, &public_key) {
Ok(token_details) => { Ok(token_details) => {
let _: redis::RedisResult<()> = conn.del(token_details.token_uuid.to_string()).await; let _: redis::RedisResult<()> = conn.del(token_details.token_uuid.to_string()).await;
}, },
Err(_) => {} Err(_) => {}
}; };
@@ -200,7 +209,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
.parse::<i64>() .parse::<i64>()
.expect("ACCESS_TOKEN_MAXAGE must be an integer"); .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 { if let Err(err) = access_result {
error!("Failed to set access token in redis: {}", err); error!("Failed to set access token in redis: {}", err);
return ResponseError::error_response(&ServiceError { return ResponseError::error_response(&ServiceError {
@@ -230,22 +239,15 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
}; };
if refresh_token_rotation { if refresh_token_rotation {
// Delete the old refresh token // 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") let refresh_token_max_age = env::var("REFRESH_TOKEN_MAXAGE")
.expect("REFRESH_TOKEN_MAXAGE must be set") .expect("REFRESH_TOKEN_MAXAGE must be set")
.parse::<i64>() .parse::<i64>()
.expect("REFRESH_TOKEN_MAXAGE must be an integer"); .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 { if let Err(err) = refresh_result {
error!("Failed to set refresh token in redis: {}", err); error!("Failed to set refresh token in redis: {}", err);
return ResponseError::error_response(&ServiceError { 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("/") .path("/")
.max_age(Duration::new(refresh_token_max_age * 60, 0)) .max_age(Duration::new(refresh_token_max_age * 60, 0))
.http_only(true) .http_only(true)
@@ -279,7 +281,6 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
#[post("/logout")] #[post("/logout")]
async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse { 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") { let refresh_token = match req.cookie("refresh_token") {
Some(cookie) => cookie.value().to_string(), Some(cookie) => cookie.value().to_string(),
None => return ResponseError::error_response(&ServiceError { 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() 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 { let mut conn = match storage::redis_async_connection().await {
Ok(conn) => conn, Ok(conn) => conn,
@@ -303,7 +298,7 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse {
}; };
let access_result: redis::RedisResult<()> = conn.del(&[ let access_result: redis::RedisResult<()> = conn.del(&[
refresh_token_details.token_uuid.to_string(), refresh_token.to_string(),
auth.token.to_string() auth.token.to_string()
]).await; ]).await;
if let Err(err) = access_result { if let Err(err) = access_result {
@@ -342,41 +337,6 @@ async fn me(auth: JwtAuth) -> HttpResponse {
HttpResponse::Ok().json(auth) 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")] #[get("/roles")]
async fn roles() -> HttpResponse { async fn roles() -> HttpResponse {
HttpResponse::Ok().json(vec!["admin", "user"]) HttpResponse::Ok().json(vec!["admin", "user"])
@@ -401,6 +361,5 @@ pub fn init_routes(config: &mut web::ServiceConfig) {
.service(logout) .service(logout)
.service(me) .service(me)
.service(roles) .service(roles)
.service(check_session)
); );
} }

View 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
}
}
}

View File

@@ -43,7 +43,7 @@ export async function me(): Promise<ResponseAuth | undefined> {
} }
export async function hasSession(): Promise<boolean> { export async function hasSession(): Promise<boolean> {
const response = await getRequest('auth/check-session'); const response = await getRequest('auth/session');
if (response?.status === 200) { if (response?.status === 200) {
return response?.json(); return response?.json();
} else { } else {