New authentication struct draft
This commit is contained in:
parent
c787ff0fa1
commit
2d8f9f8697
1 changed files with 101 additions and 2 deletions
|
@ -7,15 +7,68 @@ use serde::{Deserialize, Serialize};
|
||||||
const DAYS_VALID: i64 = 7;
|
const DAYS_VALID: i64 = 7;
|
||||||
const JWT_SECRET: &[u8] = "secret".as_bytes();
|
const JWT_SECRET: &[u8] = "secret".as_bytes();
|
||||||
|
|
||||||
|
/// Claims holds the data that will be encoded into the JWT token.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Claims {
|
pub struct Claims {
|
||||||
|
#[serde(rename = "sub")]
|
||||||
pub sub: String,
|
pub sub: String,
|
||||||
|
#[serde(rename = "iss")]
|
||||||
pub iss: String,
|
pub iss: String,
|
||||||
pub aud: String,
|
#[serde(rename = "iat")]
|
||||||
pub iat: usize,
|
pub iat: usize,
|
||||||
|
#[serde(rename = "exp")]
|
||||||
pub exp: usize,
|
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<String> {
|
||||||
|
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<String> {
|
||||||
|
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<Claims> {
|
||||||
|
decode::<Claims>(token, &self.decoding_key, &self.validation).map(|data| data.claims)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// JwtResult is just a predefined error from the jsonwebtoken crate
|
// JwtResult is just a predefined error from the jsonwebtoken crate
|
||||||
pub fn token_factory(user: &str) -> JwtResult<String> {
|
pub fn token_factory(user: &str) -> JwtResult<String> {
|
||||||
info!("Issuing JWT token for {}", user);
|
info!("Issuing JWT token for {}", user);
|
||||||
|
@ -25,7 +78,6 @@ pub fn token_factory(user: &str) -> JwtResult<String> {
|
||||||
&Claims {
|
&Claims {
|
||||||
sub: user.to_string(),
|
sub: user.to_string(),
|
||||||
iss: "frostbyte".to_string(),
|
iss: "frostbyte".to_string(),
|
||||||
aud: "frostbyte".to_string(),
|
|
||||||
iat: chrono::Utc::now().timestamp() as usize,
|
iat: chrono::Utc::now().timestamp() as usize,
|
||||||
exp: (chrono::Utc::now() + chrono::Duration::days(DAYS_VALID)).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<Claims> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue