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:
parent
154c3d1152
commit
8b2c13501f
19 changed files with 935 additions and 98 deletions
110
backend/src/auth/jwt.rs
Normal file
110
backend/src/auth/jwt.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue