Updated files, will be switching to sessions
This commit is contained in:
@@ -71,6 +71,4 @@ docker run --env-file .env -it --rm --name siren siren:latest
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
The Siren service uses a JWT/session based authentication system, in that JWT tokens are issued and used, but a state is also kept server-side. This is to allow for the ability to revoke and expire tokens, as well as to allow for the ability to have multiple tokens per user.
|
The Siren service uses a stateful JWT authentication system, which allows for the ability to revoke and expire tokens, as well as to allow for the ability to have multiple tokens per user. A public/private key is needed for the JWT. The keys can be generated with `./generate_keys.sh`. These keys should be located within a `/keys` directory in the root of the project. The `KEYS_DIR_PATH` within the service's .env file should be updated to reflect the location of the keys.
|
||||||
|
|
||||||
Public/Private keys can be generated with `./generate_keys.sh`. These keys should be located within a `/keys` directory in the root of the project. The service's .env file should be updated to reflect the location of the keys.
|
|
||||||
@@ -7,17 +7,15 @@ if [ "$#" -eq 1 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Create the keys directory (if it doesn't exist)
|
# Create the keys directory (if it doesn't exist)
|
||||||
echo "Generating keys in: $DIR"
|
echo "Generating public/private keys in: $DIR"
|
||||||
mkdir -p "$DIR"
|
mkdir -p "$DIR"
|
||||||
|
|
||||||
# Generate Access Keys
|
# Remove any existing keys
|
||||||
openssl genrsa -out $DIR/access_private_key.pem 4096
|
rm -f $DIR/private_key.pem
|
||||||
openssl rsa -in $DIR/access_private_key.pem -pubout -outform PEM -out $DIR/access_public_key.pem
|
rm -f $DIR/public_key.pem
|
||||||
chmod 600 $DIR/access_private_key.pem
|
|
||||||
chmod 644 $DIR/access_public_key.pem
|
|
||||||
|
|
||||||
# Generate Refresh Keys
|
# Generate Keys
|
||||||
openssl genrsa -out $DIR/refresh_private_key.pem 4096
|
openssl genrsa -out $DIR/private_key.pem 4096
|
||||||
openssl rsa -in $DIR/refresh_private_key.pem -pubout -outform PEM -out $DIR/refresh_public_key.pem
|
openssl rsa -in $DIR/private_key.pem -pubout -outform PEM -out $DIR/public_key.pem
|
||||||
chmod 600 $DIR/refresh_private_key.pem
|
chmod 600 $DIR/private_key.pem
|
||||||
chmod 644 $DIR/refresh_public_key.pem
|
chmod 644 $DIR/public_key.pem
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ 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"
|
sha2 = "0.10.8"
|
||||||
|
rand_chacha = "0.3.1"
|
||||||
|
|
||||||
[dependencies.tokio]
|
[dependencies.tokio]
|
||||||
version = "1.32.0"
|
version = "1.32.0"
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ FROM debian:bookworm-slim as keys
|
|||||||
WORKDIR /keys
|
WORKDIR /keys
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y openssl libpq-dev
|
RUN apt-get update && apt-get install -y openssl libpq-dev
|
||||||
RUN openssl genrsa -out access.pem 4096
|
RUN openssl genrsa -out private_key.pem 4096
|
||||||
RUN openssl rsa -in access.pem -pubout -outform PEM -out access.pem.pub
|
RUN openssl rsa -in private_key.pem -pubout -outform PEM -out public_key.pem
|
||||||
RUN openssl genrsa -out refresh.pem 4096
|
RUN chmod 600 private_key.pem
|
||||||
RUN openssl rsa -in refresh.pem -pubout -outform PEM -out refresh.pem.pub
|
RUN chmod 644 public_key.pem
|
||||||
|
|
||||||
# ==========
|
# ==========
|
||||||
# Packages
|
# Packages
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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;
|
||||||
@@ -9,7 +8,7 @@ use siren::ServiceError;
|
|||||||
|
|
||||||
use crate::storage::{schema::users, connection};
|
use crate::storage::{schema::users, connection};
|
||||||
|
|
||||||
use super::AccessToken;
|
use super::{hash, AccessToken};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct RegisterUser {
|
pub struct RegisterUser {
|
||||||
@@ -21,10 +20,9 @@ pub struct RegisterUser {
|
|||||||
|
|
||||||
impl RegisterUser {
|
impl RegisterUser {
|
||||||
pub fn convert_to_insert(self) -> Result<InsertUser, ServiceError> {
|
pub fn convert_to_insert(self) -> Result<InsertUser, ServiceError> {
|
||||||
let hash = hash_password(self.password.as_bytes())?;
|
|
||||||
Ok(InsertUser {
|
Ok(InsertUser {
|
||||||
email: self.email.to_lowercase(),
|
email: self.email.to_lowercase(),
|
||||||
hash,
|
hash: hash(&self.password)?,
|
||||||
role: "user".to_string(),
|
role: "user".to_string(),
|
||||||
first_name: self.first_name,
|
first_name: self.first_name,
|
||||||
last_name: self.last_name,
|
last_name: self.last_name,
|
||||||
@@ -36,16 +34,6 @@ 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,
|
||||||
@@ -134,7 +122,7 @@ impl From<QueryUser> for ResponseUser {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct JwtAuth {
|
pub struct JwtAuth {
|
||||||
pub token: uuid::Uuid,
|
pub id: String,
|
||||||
pub user: ResponseUser
|
pub user: ResponseUser
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +145,7 @@ 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!("{}/public_key.pem", keys_dir)).expect("Failed to read access public key");
|
||||||
|
|
||||||
let access_token = match AccessToken::decode(&access_token_string, &public_key) {
|
let access_token = match AccessToken::decode(&access_token_string, &public_key) {
|
||||||
Ok(token_details) => token_details,
|
Ok(token_details) => token_details,
|
||||||
@@ -169,8 +157,6 @@ impl FromRequest for JwtAuth {
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
||||||
@@ -182,7 +168,7 @@ 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.id.clone().to_string()) {
|
||||||
Ok(result) => serde_json::from_str::<AccessToken>(&result).unwrap().email,
|
Ok(result) => serde_json::from_str::<AccessToken>(&result).unwrap().email,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return ready(Err(ActixError::from(ServiceError {
|
return ready(Err(ActixError::from(ServiceError {
|
||||||
@@ -194,7 +180,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 { token: access_token_uuid, user: user.into() }))
|
ready(Ok(JwtAuth { id: access_token.id, user: user.into() }))
|
||||||
}
|
}
|
||||||
Err(_) => return ready(Err(ActixError::from(ServiceError {
|
Err(_) => return ready(Err(ActixError::from(ServiceError {
|
||||||
status: 401,
|
status: 401,
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ use redis::AsyncCommands;
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use siren::ServiceError;
|
use siren::ServiceError;
|
||||||
|
|
||||||
use crate::{auth::{verify_password, AccessToken, InsertUser, JwtAuth, LoginRequest, QueryUser, RefreshToken, RegisterUser}, storage};
|
use crate::{auth::{InsertUser, JwtAuth, LoginRequest, QueryUser, RefreshToken, RegisterUser}, storage};
|
||||||
|
|
||||||
|
use super::verify_hash;
|
||||||
|
|
||||||
#[post("/register")]
|
#[post("/register")]
|
||||||
async fn register(user: web::Json<RegisterUser>) -> HttpResponse {
|
async fn register(user: web::Json<RegisterUser>) -> HttpResponse {
|
||||||
@@ -38,87 +40,79 @@ async fn login(request: HttpRequest, login_request: web::Json<LoginRequest>) ->
|
|||||||
|
|
||||||
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(_) => return ResponseError::error_response(&ServiceError {
|
||||||
};
|
|
||||||
let hash = &query_user.hash;
|
|
||||||
let password = login_request.password.as_bytes();
|
|
||||||
match verify_password(hash, password) {
|
|
||||||
Ok(_) => {
|
|
||||||
let access_token = match AccessToken::new(&email) {
|
|
||||||
Ok(token_details) => token_details,
|
|
||||||
Err(err) => {
|
|
||||||
error!("Failed to generate access 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,
|
|
||||||
Err(err) => {
|
|
||||||
error!("Failed to get redis connection: {}", 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 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.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 {
|
|
||||||
status: 500,
|
|
||||||
message: format!("Failed to set access token in redis: {}", err)
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
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 {
|
|
||||||
status: 500,
|
|
||||||
message: format!("Failed to set refresh token in redis: {}", err)
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
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.hash.clone())
|
|
||||||
.path("/")
|
|
||||||
.max_age(Duration::new(refresh_token_max_age * 60, 0))
|
|
||||||
.http_only(true)
|
|
||||||
.secure(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();
|
|
||||||
|
|
||||||
let access_token_uuid = uuid::Uuid::parse_str(&access_token.token_uuid.to_string()).unwrap();
|
|
||||||
|
|
||||||
HttpResponse::Ok()
|
|
||||||
.cookie(access_cookie)
|
|
||||||
.cookie(refresh_cookie)
|
|
||||||
.cookie(logged_in_cookie)
|
|
||||||
.json(JwtAuth { token: access_token_uuid, user: query_user.into() })
|
|
||||||
},
|
|
||||||
Err(err) => ResponseError::error_response(&ServiceError {
|
|
||||||
status: 401,
|
status: 401,
|
||||||
message: err.to_string()
|
message: "The email or password was incorrect.".to_string()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
// Verify password
|
||||||
|
if verify_hash(&login_request.password, &query_user.hash) {
|
||||||
|
let mut refresh_token = RefreshToken::new(&email, &ip_address);
|
||||||
|
let access_token = match refresh_token.create_access_token() {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to generate access token: {}", 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 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.id.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::from(err))
|
||||||
|
};
|
||||||
|
|
||||||
|
let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token.id.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::from(err))
|
||||||
|
};
|
||||||
|
|
||||||
|
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.id.clone())
|
||||||
|
.path("/")
|
||||||
|
.max_age(Duration::new(refresh_token_max_age * 60, 0))
|
||||||
|
.http_only(true)
|
||||||
|
.secure(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(refresh_cookie)
|
||||||
|
.cookie(logged_in_cookie)
|
||||||
|
.json(JwtAuth { id: access_token.id, user: query_user.into() })
|
||||||
|
} else {
|
||||||
|
return ResponseError::error_response(&ServiceError {
|
||||||
|
status: 401,
|
||||||
|
message: "The email or password was incorrect.".to_string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,7 +133,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let refresh_token_hash = match req.cookie("refresh_token") {
|
let refresh_token_string = 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,
|
||||||
@@ -155,23 +149,26 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let refresh_token: RefreshToken = match conn.get::<_, String>(refresh_token_hash.clone()).await {
|
let mut refresh_token: RefreshToken = match conn.get::<_, String>(refresh_token_string.clone()).await {
|
||||||
Ok(result) => match serde_json::from_str(&result) {
|
Ok(result) => match serde_json::from_str::<RefreshToken>(&result) {
|
||||||
Ok(result) => result,
|
Ok(result) => {
|
||||||
|
if verify_hash(&ip_address, &result.ip_address) {
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
return ResponseError::error_response(&ServiceError {
|
||||||
|
status: 401,
|
||||||
|
message: "Refresh token is invalid".to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Failed to deserialize refresh token: {}", err);
|
error!("Failed to deserialize refresh token: {}", err);
|
||||||
return ResponseError::error_response(&ServiceError {
|
return ResponseError::error_response(&ServiceError::from(err))
|
||||||
status: 500,
|
|
||||||
message: format!("Failed to deserialize refresh token: {}", err)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Failed to get refresh token from redis: {}", err);
|
error!("Failed to get refresh token from redis: {}", err);
|
||||||
return ResponseError::error_response(&ServiceError {
|
return ResponseError::error_response(&ServiceError::from(err))
|
||||||
status: 500,
|
|
||||||
message: format!("Failed to get refresh token from redis: {}", err)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -179,46 +176,33 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
|
|
||||||
match QueryUser::get_by_email(&email) {
|
match QueryUser::get_by_email(&email) {
|
||||||
Ok(query_user) => {
|
Ok(query_user) => {
|
||||||
let access_token_details = match AccessToken::new(&email) {
|
// Revoke all old access tokens
|
||||||
Ok(token_details) => token_details,
|
for id in refresh_token.tokens {
|
||||||
|
let _: redis::RedisResult<()> = conn.del(id).await;
|
||||||
|
}
|
||||||
|
refresh_token.tokens = vec![];
|
||||||
|
|
||||||
|
// Create new access token
|
||||||
|
let access_token = match refresh_token.create_access_token() {
|
||||||
|
Ok(token) => token,
|
||||||
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)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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 AccessToken::decode(&access_token, &public_key) {
|
|
||||||
Ok(token_details) => {
|
|
||||||
let _: redis::RedisResult<()> = conn.del(token_details.token_uuid.to_string()).await;
|
|
||||||
},
|
|
||||||
Err(_) => {}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
None => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE")
|
let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE")
|
||||||
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
||||||
.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(), &serde_json::to_string(&access_token_details).unwrap(), (access_token_max_age * 60) as usize).await;
|
let access_result: redis::RedisResult<()> = conn.set_ex(access_token.id.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::from(err))
|
||||||
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())
|
let access_cookie = Cookie::build("access_token", access_token.id.clone())
|
||||||
.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)
|
||||||
@@ -229,8 +213,6 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
.max_age(Duration::new(access_token_max_age * 60, 0))
|
.max_age(Duration::new(access_token_max_age * 60, 0))
|
||||||
.http_only(false)
|
.http_only(false)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
let access_token_uuid = uuid::Uuid::parse_str(&access_token_details.token_uuid.to_string()).unwrap();
|
|
||||||
|
|
||||||
// Refresh the refresh token if requested
|
// Refresh the refresh token if requested
|
||||||
let refresh_token_rotation = match params.refresh_token_rotation {
|
let refresh_token_rotation = match params.refresh_token_rotation {
|
||||||
@@ -238,8 +220,8 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
None => false
|
None => false
|
||||||
};
|
};
|
||||||
if refresh_token_rotation {
|
if refresh_token_rotation {
|
||||||
// Delete the old refresh token
|
// Delete the old refresh token from redis
|
||||||
let _: redis::RedisResult<()> = conn.del(refresh_token.hash.to_string()).await;
|
let _: redis::RedisResult<()> = conn.del(refresh_token.id.to_string()).await;
|
||||||
|
|
||||||
let refresh_token = RefreshToken::new(&refresh_token.email, &ip_address);
|
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")
|
||||||
@@ -247,7 +229,8 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
.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.hash.to_string(), &serde_json::to_string(&refresh_token).unwrap(), (refresh_token_max_age * 60) as usize).await;
|
// Add the new refresh token to redis
|
||||||
|
let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token.id.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 {
|
||||||
@@ -256,7 +239,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let refresh_cookie = Cookie::build("refresh_token", refresh_token.hash.clone())
|
let refresh_cookie = Cookie::build("refresh_token", refresh_token.id.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)
|
||||||
@@ -267,12 +250,12 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
|
|||||||
.cookie(refresh_cookie)
|
.cookie(refresh_cookie)
|
||||||
.cookie(access_cookie)
|
.cookie(access_cookie)
|
||||||
.cookie(logged_in_cookie)
|
.cookie(logged_in_cookie)
|
||||||
.json(JwtAuth { token: access_token_uuid, user: query_user.into() })
|
.json(JwtAuth { id: access_token.id, user: query_user.into() })
|
||||||
} else {
|
} else {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.cookie(access_cookie)
|
.cookie(access_cookie)
|
||||||
.cookie(logged_in_cookie)
|
.cookie(logged_in_cookie)
|
||||||
.json(JwtAuth { token: access_token_uuid, user: query_user.into() })
|
.json(JwtAuth { id: access_token.id, user: query_user.into() })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(err) => return ResponseError::error_response(&err)
|
Err(err) => return ResponseError::error_response(&err)
|
||||||
@@ -299,7 +282,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.to_string(),
|
refresh_token.to_string(),
|
||||||
auth.token.to_string()
|
auth.id.to_string()
|
||||||
]).await;
|
]).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);
|
||||||
@@ -312,11 +295,13 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse {
|
|||||||
let access_cookie = Cookie::build("access_token", "")
|
let access_cookie = Cookie::build("access_token", "")
|
||||||
.path("/")
|
.path("/")
|
||||||
.max_age(Duration::new(-1, 0))
|
.max_age(Duration::new(-1, 0))
|
||||||
|
.secure(true)
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.finish();
|
.finish();
|
||||||
let refresh_cookie = Cookie::build("refresh_token", "")
|
let refresh_cookie = Cookie::build("refresh_token", "")
|
||||||
.path("/")
|
.path("/")
|
||||||
.max_age(Duration::new(-1, 0))
|
.max_age(Duration::new(-1, 0))
|
||||||
|
.secure(true)
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.finish();
|
.finish();
|
||||||
let logged_in_cookie = Cookie::build("logged_in", "")
|
let logged_in_cookie = Cookie::build("logged_in", "")
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use argon2::password_hash::{rand_core::OsRng, SaltString};
|
use argon2::{password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm};
|
use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm};
|
||||||
|
use rand::prelude::*;
|
||||||
|
use rand_chacha::ChaCha20Rng;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{digest::{generic_array::GenericArray, typenum::U32}, Digest, Sha256};
|
|
||||||
use siren::ServiceError;
|
use siren::ServiceError;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct TokenClaims {
|
struct TokenClaims {
|
||||||
sub: String, // Subject (User)
|
sub: String, // Subject (User)
|
||||||
token_uuid: String, // Access Token UUID
|
id: String, // Access Token ID
|
||||||
iss: String, // Issuer (Service)
|
iss: String, // Issuer (Service)
|
||||||
exp: i64, // Expiration time
|
exp: i64, // Expiration time
|
||||||
iat: i64, // Issued At
|
iat: i64, // Issued At
|
||||||
@@ -18,32 +19,32 @@ struct TokenClaims {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct AccessToken {
|
pub struct AccessToken {
|
||||||
pub token: Option<String>,
|
pub token: Option<String>, // Access Token
|
||||||
pub token_uuid: uuid::Uuid,
|
pub id: String, // Access Token ID
|
||||||
pub email: String,
|
pub email: String, // Subject (User)
|
||||||
pub expires_in: Option<i64>
|
pub expiration: Option<i64> // Expiration time
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AccessToken {
|
impl AccessToken {
|
||||||
pub fn new(email: &str) -> Result<Self, ServiceError> {
|
fn new(email: &str) -> Result<Self, ServiceError> {
|
||||||
let ttl = env::var("ACCESS_TOKEN_MAXAGE")
|
let ttl = env::var("ACCESS_TOKEN_MAXAGE")
|
||||||
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
.expect("ACCESS_TOKEN_MAXAGE must be set")
|
||||||
.parse::<i64>()
|
.parse::<i64>()
|
||||||
.expect("ACCESS_TOKEN_MAXAGE must be an integer");
|
.expect("ACCESS_TOKEN_MAXAGE must be an integer");
|
||||||
let keys_dir = env::var("KEYS_DIR_PATH")?;
|
let keys_dir = env::var("KEYS_DIR_PATH")?;
|
||||||
let private_key = std::fs::read_to_string(format!("{}/access_private_key.pem", keys_dir))?;
|
let private_key = std::fs::read_to_string(format!("{}/private_key.pem", keys_dir))?;
|
||||||
let now = chrono::Utc::now();
|
let now = chrono::Utc::now();
|
||||||
let mut token_details = Self {
|
let mut token_details = Self {
|
||||||
token: None,
|
token: None,
|
||||||
token_uuid: uuid::Uuid::new_v4(),
|
id: csprng_128bit(),
|
||||||
email: email.to_string(),
|
email: email.to_string(),
|
||||||
expires_in: Some((now + chrono::Duration::minutes(ttl)).timestamp())
|
expiration: Some((now + chrono::Duration::minutes(ttl)).timestamp())
|
||||||
};
|
};
|
||||||
let claims = TokenClaims {
|
let claims = TokenClaims {
|
||||||
sub: token_details.email.clone(),
|
sub: token_details.email.clone(),
|
||||||
iss: "siren".to_string(),
|
iss: "siren".to_string(),
|
||||||
token_uuid: token_details.token_uuid.to_string(),
|
id: token_details.id.to_string(),
|
||||||
exp: token_details.expires_in.unwrap(),
|
exp: token_details.expiration.unwrap(),
|
||||||
iat: now.timestamp(),
|
iat: now.timestamp(),
|
||||||
nbf: now.timestamp()
|
nbf: now.timestamp()
|
||||||
};
|
};
|
||||||
@@ -59,29 +60,64 @@ impl AccessToken {
|
|||||||
let validation = Validation::new(Algorithm::RS256);
|
let validation = Validation::new(Algorithm::RS256);
|
||||||
let decoded = decode::<TokenClaims>(token, &key, &validation)?;
|
let decoded = decode::<TokenClaims>(token, &key, &validation)?;
|
||||||
let email = decoded.claims.sub;
|
let email = decoded.claims.sub;
|
||||||
let token_uuid = uuid::Uuid::parse_str(decoded.claims.token_uuid.as_str()).unwrap();
|
let id = decoded.claims.id;
|
||||||
Ok(Self { token: None, token_uuid, email, expires_in: None })
|
Ok(Self { token: None, id, email, expiration: None })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct RefreshToken {
|
pub struct RefreshToken {
|
||||||
pub hash: String,
|
pub id: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub ip_address: String,
|
pub ip_address: String,
|
||||||
pub timestamp: i64,
|
pub tokens: Vec<String>,
|
||||||
|
pub expiration: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RefreshToken {
|
impl RefreshToken {
|
||||||
pub fn new(email: &str, ip_address: &str) -> Self {
|
pub fn new(email: &str, ip_address: &str) -> Self {
|
||||||
let now = chrono::Utc::now().timestamp();
|
let ttl = env::var("REFRESH_TOKEN_MAXAGE")
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
.expect("REFRESH_TOKEN_MAXAGE must be set")
|
||||||
let hash: GenericArray<u8, U32> = Sha256::digest(format!("{}:{}:{}:{}", email, ip_address, now, salt).as_bytes());
|
.parse::<i64>()
|
||||||
|
.expect("REFRESH_TOKEN_MAXAGE must be an integer");
|
||||||
|
let now = chrono::Utc::now();
|
||||||
Self {
|
Self {
|
||||||
hash: format!("{:x}", hash),
|
id: csprng_128bit(),
|
||||||
email: email.to_string(),
|
email: email.to_string(),
|
||||||
ip_address: ip_address.to_string(),
|
ip_address: hash(&ip_address).unwrap(),
|
||||||
timestamp: now
|
tokens: vec![],
|
||||||
|
expiration: (now + chrono::Duration::minutes(ttl)).timestamp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_access_token(&mut self) -> Result<AccessToken, ServiceError> {
|
||||||
|
let access_token = AccessToken::new(&self.email)?;
|
||||||
|
self.tokens.push(access_token.id.clone());
|
||||||
|
Ok(access_token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn csprng_128bit() -> String {
|
||||||
|
// Generate a CSPRNG 128-bit (16 byte) ID using alphanumeric characters (a-z, A-Z, 0-9)
|
||||||
|
let rng = ChaCha20Rng::from_entropy();
|
||||||
|
rng.sample_iter(rand::distributions::Alphanumeric).take(16).map(char::from).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash(str: &str) -> Result<String, ServiceError> {
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
let bytes = str.as_bytes();
|
||||||
|
let hash = Argon2::default().hash_password(bytes, &salt)?.to_string();
|
||||||
|
Ok(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_hash(str: &str, hash: &str) -> bool {
|
||||||
|
let bytes = str.as_bytes();
|
||||||
|
let parsed_hash = match PasswordHash::new(hash) {
|
||||||
|
Ok(h) => h,
|
||||||
|
Err(_) => return false
|
||||||
|
};
|
||||||
|
match Argon2::default().verify_password(bytes, &parsed_hash) {
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(_) => false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user