Updated auth to use generated keys

This commit is contained in:
Benjamin Sherriff
2024-01-29 21:24:10 -05:00
parent f2acb585c0
commit 4609be84a8
19 changed files with 213 additions and 87 deletions

View File

@@ -1,7 +1,6 @@
use std::env;
use argon2::{password_hash::{rand_core::OsRng, PasswordHasher, PasswordVerifier, SaltString, Error as HashError}, Argon2, PasswordHash};
use base64::{engine::general_purpose, Engine as _};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm};
use serde::{Deserialize, Serialize};
@@ -14,62 +13,113 @@ use siren::ServiceError;
#[derive(Debug, Serialize, Deserialize)]
struct TokenClaims {
sub: String, // Subject
sub: String, // Subject (User)
token_uuid: String, // Token UUID
iss: String, // Issuer
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>
}
// https://codevoweb.com/rust-actix-web-jwt-access-and-refresh-tokens/
// https://github.com/wpcodevo/rust-jwt-rs256/blob/master/src/main.rs
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 bytes_public_key = general_purpose::STANDARD.decode(public_key).unwrap();
let decoded_public_key = String::from_utf8(bytes_public_key).unwrap();
let key = DecodingKey::from_rsa_pem(decoded_public_key.as_bytes())?;
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, expires_in: None })
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 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)
.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 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)
.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)
}
pub fn generate_token(email: &str, ttl: i64, private_key: &str) -> Result<TokenDetails, ServiceError> {
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 {
@@ -81,9 +131,7 @@ pub fn generate_token(email: &str, ttl: i64, private_key: &str) -> Result<TokenD
nbf: now.timestamp()
};
let header = Header::new(Algorithm::RS256);
let bytes_private_key = general_purpose::STANDARD.decode(private_key).unwrap();
let decoded_private_key = String::from_utf8(bytes_private_key).unwrap();
let key = EncodingKey::from_rsa_pem(decoded_private_key.as_bytes())?;
let key = EncodingKey::from_rsa_pem(private_key.as_bytes())?;
let token = encode(&header, &claims, &key)?;
token_details.token = Some(token);
Ok(token_details)
@@ -97,4 +145,4 @@ pub fn hash_password(password: &[u8]) -> Result<String, HashError> {
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

@@ -145,8 +145,8 @@ impl FromRequest for JwtAuth {
})))
};
let public_key = env::var("ACCESS_TOKEN_PUBLIC_KEY")
.expect("ACCESS_TOKEN_PUBLIC_KEY 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 access_token_details = match verify_token(&access_token, &public_key) {
Ok(token_details) => token_details,

View File

@@ -76,7 +76,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(), &email, (access_token_max_age * 60) as usize).await;
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;
if let Err(err) = access_result {
error!("Failed to set access token in redis: {}", err);
return ResponseError::error_response(&ServiceError {
@@ -85,7 +85,7 @@ async fn login(request: web::Json<LoginRequest>) -> HttpResponse {
})
};
let refresh_result: redis::RedisResult<()> = conn.set_ex(refresh_token_details.token_uuid.to_string(), &email, (refresh_token_max_age * 60) as usize).await;
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;
if let Err(err) = refresh_result {
error!("Failed to set refresh token in redis: {}", err);
return ResponseError::error_response(&ServiceError {
@@ -150,8 +150,9 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
})
};
let public_key = env::var("REFRESH_TOKEN_PUBLIC_KEY")
.expect("REFRESH_TOKEN_PUBLIC_KEY 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!("{}/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)
@@ -177,12 +178,12 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
}
};
// Delete old auth token if it exists
// Delete old access token if it exists
match req.cookie("access_token") {
Some(cookie) => {
let access_token = cookie.value().to_string();
let public_key = env::var("ACCESS_TOKEN_PUBLIC_KEY")
.expect("ACCESS_TOKEN_PUBLIC_KEY 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) {
Ok(token_details) => {
let _: redis::RedisResult<()> = conn.del(token_details.token_uuid.to_string()).await;
@@ -199,7 +200,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(), &email, (access_token_max_age * 60) as usize).await;
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;
if let Err(err) = access_result {
error!("Failed to set access token in redis: {}", err);
return ResponseError::error_response(&ServiceError {
@@ -244,7 +245,7 @@ async fn refresh(req: HttpRequest) -> HttpResponse {
.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.email, (refresh_token_max_age * 60) as usize).await;
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;
if let Err(err) = refresh_result {
error!("Failed to set refresh token in redis: {}", err);
return ResponseError::error_response(&ServiceError {
@@ -278,6 +279,7 @@ 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 {
@@ -285,8 +287,8 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse {
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 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)
@@ -342,12 +344,13 @@ async fn me(auth: JwtAuth) -> HttpResponse {
#[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 = env::var("ACCESS_TOKEN_PUBLIC_KEY")
.expect("ACCESS_TOKEN_PUBLIC_KEY 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) {
Ok(_) => true,
Err(_) => false
@@ -360,8 +363,8 @@ async fn check_session(req: HttpRequest) -> HttpResponse {
match req.cookie("refresh_token") {
Some(cookie) => {
let refresh_token = cookie.value().to_string();
let public_key = env::var("REFRESH_TOKEN_PUBLIC_KEY")
.expect("REFRESH_TOKEN_PUBLIC_KEY 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");
match verify_token(&refresh_token, &public_key) {
Ok(_) => return HttpResponse::Ok().json(true),
Err(_) => return HttpResponse::Ok().json(false)
@@ -380,6 +383,7 @@ async fn roles() -> HttpResponse {
}
pub fn init_routes(config: &mut web::ServiceConfig) {
// TODO: Remove this when deploying
let r = RegisterUser {
email: "admin".to_string(),
password: "admin".to_string(),