reasonkit-web 0.1.7

High-performance MCP server for browser automation, web capture, and content extraction. Rust-powered CDP client for AI agents.
Documentation
//! # Authentication Module
//!
//! JWT-based authentication with OAuth 2.0 and 2FA support.
//!
//! ## Features
//!
//! - Email/password registration and login
//! - OAuth 2.0 providers (GitHub, Google, Microsoft)
//! - TOTP-based 2FA
//! - JWT access tokens with refresh token rotation
//! - Session management

use axum::{
    extract::{Json, Path},
    http::StatusCode,
    response::IntoResponse,
};
use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};

/// JWT Claims structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
    /// Subject (user ID)
    pub sub: String,
    /// Email
    pub email: String,
    /// Issued at timestamp
    pub iat: i64,
    /// Expiration timestamp
    pub exp: i64,
    /// Token type (access or refresh)
    pub token_type: TokenType,
    /// Scopes/permissions
    pub scopes: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum TokenType {
    Access,
    Refresh,
}

/// Access and refresh token pair
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenPair {
    pub access_token: String,
    pub refresh_token: String,
    pub token_type: String,
    pub expires_in: u64,
}

/// Authentication configuration
#[derive(Debug, Clone)]
pub struct AuthConfig {
    pub jwt_secret: String,
    pub access_token_expiry: Duration,
    pub refresh_token_expiry: Duration,
    pub issuer: String,
    pub audience: String,
}

impl Default for AuthConfig {
    fn default() -> Self {
        Self {
            jwt_secret: std::env::var("JWT_SECRET")
                .unwrap_or_else(|_| "change-me-in-production".to_string()),
            access_token_expiry: Duration::hours(1),
            refresh_token_expiry: Duration::days(7),
            issuer: "reasonkit.sh".to_string(),
            audience: "reasonkit-api".to_string(),
        }
    }
}

/// Authentication service
pub struct AuthService {
    config: AuthConfig,
    encoding_key: EncodingKey,
    decoding_key: DecodingKey,
}

impl AuthService {
    pub fn new(config: AuthConfig) -> Self {
        let encoding_key = EncodingKey::from_secret(config.jwt_secret.as_bytes());
        let decoding_key = DecodingKey::from_secret(config.jwt_secret.as_bytes());
        Self {
            config,
            encoding_key,
            decoding_key,
        }
    }

    /// Generate a token pair for a user
    pub fn generate_token_pair(
        &self,
        user_id: &str,
        email: &str,
        scopes: Vec<String>,
    ) -> TokenPair {
        let now = Utc::now();

        // Access token
        let access_claims = Claims {
            sub: user_id.to_string(),
            email: email.to_string(),
            iat: now.timestamp(),
            exp: (now + self.config.access_token_expiry).timestamp(),
            token_type: TokenType::Access,
            scopes: scopes.clone(),
        };

        let access_token = encode(&Header::default(), &access_claims, &self.encoding_key)
            .expect("Failed to encode access token");

        // Refresh token
        let refresh_claims = Claims {
            sub: user_id.to_string(),
            email: email.to_string(),
            iat: now.timestamp(),
            exp: (now + self.config.refresh_token_expiry).timestamp(),
            token_type: TokenType::Refresh,
            scopes,
        };

        let refresh_token = encode(&Header::default(), &refresh_claims, &self.encoding_key)
            .expect("Failed to encode refresh token");

        TokenPair {
            access_token,
            refresh_token,
            token_type: "Bearer".to_string(),
            expires_in: self.config.access_token_expiry.num_seconds() as u64,
        }
    }

    /// Validate and decode a token
    pub fn validate_token(&self, token: &str) -> Result<Claims, AuthError> {
        let validation = Validation::default();
        decode::<Claims>(token, &self.decoding_key, &validation)
            .map(|data| data.claims)
            .map_err(|e| AuthError::InvalidToken(e.to_string()))
    }

    /// Hash a password using argon2
    pub fn hash_password(password: &str) -> Result<String, AuthError> {
        use argon2::Argon2;
        use password_hash::{rand_core::OsRng, PasswordHasher, SaltString};

        let salt = SaltString::generate(&mut OsRng);
        let argon2 = Argon2::default();

        argon2
            .hash_password(password.as_bytes(), &salt)
            .map(|hash| hash.to_string())
            .map_err(|e| AuthError::PasswordHashError(e.to_string()))
    }

    /// Verify a password against a hash
    pub fn verify_password(password: &str, hash: &str) -> Result<bool, AuthError> {
        use argon2::Argon2;
        use password_hash::{PasswordHash, PasswordVerifier};

        let parsed_hash =
            PasswordHash::new(hash).map_err(|e| AuthError::PasswordHashError(e.to_string()))?;

        Ok(Argon2::default()
            .verify_password(password.as_bytes(), &parsed_hash)
            .is_ok())
    }
}

/// Authentication errors
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
    #[error("Invalid credentials")]
    InvalidCredentials,
    #[error("Invalid token: {0}")]
    InvalidToken(String),
    #[error("Token expired")]
    TokenExpired,
    #[error("User not found")]
    UserNotFound,
    #[error("Email already registered")]
    EmailAlreadyExists,
    #[error("Password hash error: {0}")]
    PasswordHashError(String),
    #[error("2FA required")]
    TwoFactorRequired,
    #[error("Invalid 2FA code")]
    Invalid2FACode,
    #[error("Database error: {0}")]
    DatabaseError(String),
}

/// HTTP handlers for authentication endpoints
pub mod handlers {
    use super::*;

    #[derive(Debug, Deserialize)]
    pub struct RegisterRequest {
        pub email: String,
        pub password: String,
        pub name: Option<String>,
    }

    #[derive(Debug, Deserialize)]
    pub struct LoginRequest {
        pub email: String,
        pub password: String,
        pub totp_code: Option<String>,
    }

    #[derive(Debug, Serialize)]
    pub struct AuthResponse {
        pub success: bool,
        pub tokens: Option<TokenPair>,
        pub user_id: Option<String>,
        pub requires_2fa: bool,
        pub message: String,
    }

    /// Register a new user
    pub async fn register(Json(_req): Json<RegisterRequest>) -> impl IntoResponse {
        // TODO: Implement with database
        let response = AuthResponse {
            success: true,
            tokens: None,
            user_id: Some("user_placeholder".to_string()),
            requires_2fa: false,
            message: "Registration successful. Please verify your email.".to_string(),
        };
        (StatusCode::CREATED, Json(response))
    }

    /// Login with email and password
    pub async fn login(Json(_req): Json<LoginRequest>) -> impl IntoResponse {
        // TODO: Implement with database
        let response = AuthResponse {
            success: true,
            tokens: Some(TokenPair {
                access_token: "placeholder".to_string(),
                refresh_token: "placeholder".to_string(),
                token_type: "Bearer".to_string(),
                expires_in: 3600,
            }),
            user_id: Some("user_placeholder".to_string()),
            requires_2fa: false,
            message: "Login successful".to_string(),
        };
        (StatusCode::OK, Json(response))
    }

    /// Logout (invalidate refresh token)
    pub async fn logout() -> impl IntoResponse {
        (StatusCode::OK, Json(serde_json::json!({"success": true})))
    }

    /// Refresh access token
    pub async fn refresh_token() -> impl IntoResponse {
        // TODO: Implement token refresh
        StatusCode::NOT_IMPLEMENTED
    }

    /// Request password reset email
    pub async fn request_password_reset() -> impl IntoResponse {
        StatusCode::NOT_IMPLEMENTED
    }

    /// Reset password with token
    pub async fn reset_password(Path(_token): Path<String>) -> impl IntoResponse {
        StatusCode::NOT_IMPLEMENTED
    }

    /// Setup 2FA (returns QR code)
    pub async fn setup_2fa() -> impl IntoResponse {
        StatusCode::NOT_IMPLEMENTED
    }

    /// Verify 2FA code
    pub async fn verify_2fa() -> impl IntoResponse {
        StatusCode::NOT_IMPLEMENTED
    }
}