Phase 2.3: JWT Authentication implementation

- Implemented JWT-based authentication system with access and refresh tokens
- Added password hashing service using PBKDF2
- Created authentication handlers: register, login, refresh, logout
- Added protected routes with JWT middleware
- Created user profile handlers
- Fixed all compilation errors
- Added integration tests for authentication endpoints
- Added reqwest dependency for testing
- Created test script and environment example documentation

All changes:
- backend/src/auth/: Complete auth module (JWT, password, claims)
- backend/src/handlers/: Auth, users, and health handlers
- backend/src/middleware/: JWT authentication middleware
- backend/src/config/: Added AppState with Clone derive
- backend/src/main.rs: Fixed imports and added auth routes
- backend/src/db/mod.rs: Changed error handling to anyhow::Result
- backend/Cargo.toml: Added reqwest for testing
- backend/tests/auth_tests.rs: Integration tests
- thoughts/: Documentation updates (STATUS.md, env.example, test_auth.sh)
This commit is contained in:
goose 2026-02-14 20:03:11 -03:00
parent 154c3d1152
commit 8b2c13501f
19 changed files with 935 additions and 98 deletions

110
backend/src/auth/jwt.rs Normal file
View file

@ -0,0 +1,110 @@
use crate::config::JwtConfig;
use crate::auth::claims::{AccessClaims, RefreshClaims};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use uuid::Uuid;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{Result, anyhow};
#[derive(Clone)]
pub struct JwtService {
config: JwtConfig,
encoding_key: EncodingKey,
decoding_key: DecodingKey,
}
impl JwtService {
pub fn new(config: JwtConfig) -> Self {
let encoding_key = EncodingKey::from_base64_secret(&config.secret)
.unwrap_or_else(|_| EncodingKey::from_secret(config.secret.as_bytes()));
let decoding_key = DecodingKey::from_base64_secret(&config.secret)
.unwrap_or_else(|_| DecodingKey::from_secret(config.secret.as_bytes()));
Self {
config,
encoding_key,
decoding_key,
}
}
fn now_secs() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64
}
pub fn generate_access_token(
&self,
user_id: &str,
email: &str,
family_id: Option<&str>,
permissions: Vec<String>,
) -> Result<String> {
let now = Self::now_secs();
let expiry_secs = self.config.access_token_expiry_duration().as_secs() as i64;
let expiry = now + expiry_secs;
let jti = Uuid::new_v4().to_string();
let claims = AccessClaims {
sub: user_id.to_string(),
email: email.to_string(),
family_id: family_id.map(|s| s.to_string()),
permissions,
token_type: "access".to_string(),
iat: now,
exp: expiry,
jti,
};
let token = encode(&Header::default(), &claims, &self.encoding_key)
.map_err(|e| anyhow!("Failed to encode access token: {}", e))?;
Ok(token)
}
pub fn generate_refresh_token(&self, user_id: &str) -> Result<String> {
let now = Self::now_secs();
let expiry_secs = self.config.refresh_token_expiry_duration().as_secs() as i64;
let expiry = now + expiry_secs;
let jti = Uuid::new_v4().to_string();
let claims = RefreshClaims {
sub: user_id.to_string(),
token_type: "refresh".to_string(),
iat: now,
exp: expiry,
jti,
};
let token = encode(&Header::default(), &claims, &self.encoding_key)
.map_err(|e| anyhow!("Failed to encode refresh token: {}", e))?;
Ok(token)
}
pub fn verify_access_token(&self, token: &str) -> Result<AccessClaims> {
let token_data = decode::<AccessClaims>(
token,
&self.decoding_key,
&Validation::default()
).map_err(|e| anyhow!("Invalid access token: {}", e))?;
if token_data.claims.token_type != "access" {
return Err(anyhow!("Invalid token type"));
}
Ok(token_data.claims)
}
pub fn verify_refresh_token(&self, token: &str) -> Result<RefreshClaims> {
let token_data = decode::<RefreshClaims>(
token,
&self.decoding_key,
&Validation::default()
).map_err(|e| anyhow!("Invalid refresh token: {}", e))?;
if token_data.claims.token_type != "refresh" {
return Err(anyhow!("Invalid token type"));
}
Ok(token_data.claims)
}
}