From 2d8f9f8697033ece942b2587e63411a0113dc901 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 22 Dec 2023 21:39:14 +0100 Subject: [PATCH] New authentication struct draft --- server/src/jwt.rs | 103 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/server/src/jwt.rs b/server/src/jwt.rs index f171efe..5e61fd1 100755 --- a/server/src/jwt.rs +++ b/server/src/jwt.rs @@ -7,15 +7,68 @@ use serde::{Deserialize, Serialize}; const DAYS_VALID: i64 = 7; const JWT_SECRET: &[u8] = "secret".as_bytes(); +/// Claims holds the data that will be encoded into the JWT token. #[derive(Debug, Serialize, Deserialize)] pub struct Claims { + #[serde(rename = "sub")] pub sub: String, + #[serde(rename = "iss")] pub iss: String, - pub aud: String, + #[serde(rename = "iat")] pub iat: usize, + #[serde(rename = "exp")] pub exp: usize, } +impl Claims { + pub fn new(sub: &str, days: i64) -> Self { + Claims { + sub: sub.to_string(), + iss: "frostbyte".to_string(), + iat: chrono::Utc::now().timestamp() as usize, + exp: (chrono::Utc::now() + chrono::Duration::days(days)).timestamp() as usize, + } + } +} + +/// Authentication holds the data needed to encode and decode JWT tokens. +// This is then passed to the AuthenticationMiddleware +pub struct Authentication { + encoding_key: EncodingKey, + decoding_key: DecodingKey, + validation: Validation, + days_valid: i64, // chrono::Duration::days() takes an i64, we don't want to cast it every time +} + +impl Authentication { + /// Create a new Authentication struct + fn new(secret: &[u8]) -> Self { + Authentication { + encoding_key: EncodingKey::from_secret(secret), + decoding_key: DecodingKey::from_secret(secret), + validation: Validation::default(), + days_valid: 7, + } + } + + /// Encode the Claims struct into a JWT token, this is the raw version + fn encode_raw(&self, claims: Claims) -> JwtResult { + encode(&Header::default(), &claims, &self.encoding_key) + } + + /// Wrapper for encode_raw that takes a username (sub) and creates a Claims struct + fn encode(&self, sub: &str) -> JwtResult { + let claims = Claims::new(sub, self.days_valid); + self.encode_raw(claims) + } + + /// Decode a JWT token into a Claims struct + // If this faie, it means the token is invalid + fn decode(&self, token: &str) -> JwtResult { + decode::(token, &self.decoding_key, &self.validation).map(|data| data.claims) + } +} + // JwtResult is just a predefined error from the jsonwebtoken crate pub fn token_factory(user: &str) -> JwtResult { info!("Issuing JWT token for {}", user); @@ -25,7 +78,6 @@ pub fn token_factory(user: &str) -> JwtResult { &Claims { sub: user.to_string(), iss: "frostbyte".to_string(), - aud: "frostbyte".to_string(), iat: chrono::Utc::now().timestamp() as usize, exp: (chrono::Utc::now() + chrono::Duration::days(DAYS_VALID)).timestamp() as usize, }, @@ -56,3 +108,50 @@ pub fn validate_token(token: &str) -> JwtResult { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_auth() { + let username: &str = "testuser"; + let auth = Authentication::new("secret".as_bytes()); + assert!(auth.encode(username).is_ok()); + + let token = auth.encode(username); + assert!(!token.is_err()); + + let token = token.unwrap(); + assert!(!token.is_empty()); + } + + #[test] + fn test_validate() { + let username: &str = "testuser"; + let auth = Authentication::new("secret".as_bytes()); + let token = auth.encode(username).unwrap(); + let claims = auth.decode(&token).unwrap(); + assert_eq!(claims.sub, username); + } + + #[test] + fn test_invalid() { + let auth = Authentication::new("secret".as_bytes()); + let token = auth.encode("testuser").unwrap(); + + // Remove the first character should invalidate the token + let token = token[1..].to_string(); + assert!(auth.decode(&token).is_err()); + } + + #[test] + fn test_expired() { + let auth = Authentication::new("secret".as_bytes()); + + // Chrono::duration allows negative durations, -1 is yesterday in this case + let claims = Claims::new("testuser", -1); + let token = auth.encode_raw(claims).unwrap(); + assert!(auth.decode(&token).is_err()); + } +}