# Phase 10: Authentication & Token Validation in Rust (CORRECTED)

**Version**: 2.0 (QA Corrections Applied)
**Date**: December 21, 2024
**Status**: ✅ Ready for Implementation

**Objective**: Move JWT token validation, user context extraction, and authentication logic from Python to Rust for 5-10x performance improvement and reduced Python overhead.

**Current State**: Authentication happens in Python (Auth0Provider, JWT validation) before GraphQL execution

**Target State**: Rust handles all token validation, user extraction, and auth errors with zero Python overhead

---

## QA Status

**QA Review**: ✅ Complete
**Issues Found**: 14 (5 critical, 5 medium, 4 minor)
**Issues Fixed**: 14/14 (100%)
**Status**: Ready for implementation

**Changes from v1.0**:
- ✅ Fixed JWK to PEM conversion (use built-in)
- ✅ Added all missing imports
- ✅ Fixed PyO3 async return types
- ✅ Added Clone derive to UserContext
- ✅ Added exp field to UserContext
- ✅ Added JWKS fetch timeout
- ✅ Switched to LRU cache for JWKS
- ✅ Added HTTPS validation
- ✅ Improved error messages
- ✅ Updated dependencies

---

## Context

**Why This Phase Matters:**
- Token validation is on the critical path (every request)
- JWT libraries in Rust (jsonwebtoken) are 5-10x faster than Python PyJWT
- Eliminates Python auth provider overhead
- Enables auth caching in Rust for sub-millisecond validation

**Dependencies:**
- Phase 9 (Unified Pipeline) ✅ Complete
- Rust GraphQL execution pipeline
- UserContext struct already exists in unified.rs (will be updated)

**Performance Target:**
- JWT validation: <1ms (currently ~5-10ms in Python)
- Cached user context: <0.1ms
- Auth0 JWKS fetch: <50ms (cached for 1 hour)

---

## Files to Modify/Create

### Rust Files (fraiseql_rs/src/auth/)
- **mod.rs** (NEW): Auth module exports
- **jwt.rs** (NEW): JWT token validation with jsonwebtoken crate
- **provider.rs** (NEW): Auth provider trait (Auth0, JWT, custom)
- **cache.rs** (NEW): User context caching with LRU
- **errors.rs** (NEW): Auth error types (TokenExpired, InvalidToken, etc.)

### Integration Files
- **fraiseql_rs/src/lib.rs**: Add auth module, PyAuth class
- **fraiseql_rs/src/pipeline/unified.rs**: Update UserContext, integrate auth validation
- **fraiseql_rs/Cargo.toml**: Add dependencies

### Python Migration Files
- **src/fraiseql/auth/rust_provider.py** (NEW): Python wrapper for Rust auth
- **src/fraiseql/auth/base.py**: Keep interface, deprecate Python implementations

### Test Files
- **tests/test_rust_auth.py** (NEW): Integration tests for Rust auth
- **fraiseql_rs/tests/auth_tests.rs** (NEW): Rust unit tests

---

## Implementation Steps

### Step 1: Auth Module (fraiseql_rs/src/auth/mod.rs)

```rust
//! Authentication module for FraiseQL.

pub mod jwt;
pub mod provider;
pub mod cache;
pub mod errors;

pub use errors::AuthError;
pub use provider::{AuthProvider, Auth0Provider, CustomJWTProvider};
pub use cache::{UserContextCache, hash_token};
pub use jwt::{JWTValidator, Claims};
```

---

### Step 2: Rust JWT Validation Core (jwt.rs) - ✅ CORRECTED

```rust
//! JWT token validation with Auth0/custom JWKS support.

use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
use jsonwebtoken::jwk::{JwkSet, Jwk};  // ✅ Use built-in JWK support
use serde::{Deserialize, Serialize};
use anyhow::{Result, anyhow};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};  // ✅ Fixed: Added imports
use std::time::{SystemTime, Duration};  // ✅ Fixed: Added imports
use lru::LruCache;  // ✅ Fixed: Use LRU instead of HashMap
use std::num::NonZeroUsize;

/// JWT claims structure (Auth0 compatible)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
    pub sub: String,  // User ID
    pub email: Option<String>,
    pub name: Option<String>,
    pub exp: usize,  // Expiration timestamp
    pub iat: usize,  // Issued at
    pub iss: String,  // Issuer
    pub aud: Vec<String>,  // Audience

    // Auth0 custom claims
    #[serde(flatten)]
    pub custom: HashMap<String, serde_json::Value>,
}

/// JWT validator with JWKS support
pub struct JWTValidator {
    issuer: String,
    audience: Vec<String>,
    jwks_url: String,
    jwks_cache: JWKSCache,
    algorithms: Vec<Algorithm>,
    http_client: reqwest::Client,  // ✅ Fixed: Reuse HTTP client with timeout
}

impl JWTValidator {
    pub fn new(issuer: String, audience: Vec<String>, jwks_url: String) -> Result<Self> {
        // ✅ Fixed: Validate HTTPS
        if !jwks_url.starts_with("https://") {
            return Err(anyhow!("JWKS URL must use HTTPS: {}", jwks_url));
        }

        // ✅ Fixed: Create HTTP client with timeout
        let http_client = reqwest::Client::builder()
            .timeout(Duration::from_secs(5))
            .build()?;

        Ok(Self {
            issuer,
            audience,
            jwks_url,
            jwks_cache: JWKSCache::new(),
            algorithms: vec![Algorithm::RS256],
            http_client,
        })
    }

    /// Validate JWT token and return claims
    pub async fn validate(&self, token: &str) -> Result<Claims> {
        // 1. Decode header to get key ID (kid)
        let header = decode_header(token)?;
        let kid = header.kid.ok_or_else(|| anyhow!("Missing kid in token header"))?;

        // 2. Get JWK from cache (fetches if not cached)
        let jwk = self.jwks_cache.get_jwk(&kid, &self.jwks_url, &self.http_client).await?;

        // ✅ Fixed: Use jsonwebtoken's built-in JWK support (no manual PEM conversion)
        let decoding_key = DecodingKey::from_jwk(&jwk)
            .map_err(|e| anyhow!("Failed to create decoding key from JWK: {}", e))?;

        // 3. Validate token
        let mut validation = Validation::new(Algorithm::RS256);
        validation.set_issuer(&[&self.issuer]);
        validation.set_audience(&self.audience);

        // ✅ Fixed: Better error messages
        let token_data = decode::<Claims>(token, &decoding_key, &validation)
            .map_err(|e| match e.kind() {
                jsonwebtoken::errors::ErrorKind::InvalidAudience => {
                    anyhow!("Invalid audience. Expected: {:?}", self.audience)
                }
                jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
                    anyhow!("Token expired")
                }
                jsonwebtoken::errors::ErrorKind::InvalidIssuer => {
                    anyhow!("Invalid issuer. Expected: {}", self.issuer)
                }
                _ => anyhow!("JWT validation failed: {}", e)
            })?;

        Ok(token_data.claims)
    }
}

/// JWKS cache with LRU eviction and 1-hour TTL
struct JWKSCache {
    cache: Arc<Mutex<LruCache<String, (Jwk, SystemTime)>>>,  // ✅ Fixed: LRU instead of HashMap
}

impl JWKSCache {
    pub fn new() -> Self {
        Self {
            cache: Arc::new(Mutex::new(
                LruCache::new(NonZeroUsize::new(100).unwrap())  // ✅ Fixed: Max 100 keys
            )),
        }
    }

    /// Get JWK by kid (fetches from JWKS if not cached)
    pub async fn get_jwk(
        &self,
        kid: &str,
        jwks_url: &str,
        client: &reqwest::Client,
    ) -> Result<Jwk> {
        // Check cache first
        {
            let mut cache = self.cache.lock().unwrap();
            if let Some((jwk, cached_at)) = cache.get(kid) {
                // Check if cache is still valid (1 hour TTL)
                let elapsed = SystemTime::now().duration_since(*cached_at)?;
                if elapsed.as_secs() < 3600 {
                    return Ok(jwk.clone());
                }
                // Expired - remove from cache
                cache.pop(kid);
            }
        }

        // Cache miss - fetch JWKS from URL
        let jwks = self.fetch_jwks(jwks_url, client).await?;

        // Find key by kid
        let jwk = jwks.keys.iter()
            .find(|k| k.common.key_id.as_ref() == Some(&kid.to_string()))
            .ok_or_else(|| anyhow!("Key not found in JWKS: {}", kid))?
            .clone();

        // Cache the key
        {
            let mut cache = self.cache.lock().unwrap();
            cache.put(kid.to_string(), (jwk.clone(), SystemTime::now()));
        }

        Ok(jwk)
    }

    async fn fetch_jwks(&self, url: &str, client: &reqwest::Client) -> Result<JwkSet> {
        // ✅ Fixed: Use client with timeout (5 seconds)
        let response = client.get(url).send().await
            .map_err(|e| anyhow!("Failed to fetch JWKS from {}: {}", url, e))?;

        let jwks: JwkSet = response.json().await
            .map_err(|e| anyhow!("Failed to parse JWKS response: {}", e))?;

        Ok(jwks)
    }
}
```

---

### Step 3: Auth Provider Trait (provider.rs) - ✅ CORRECTED

```rust
//! Authentication provider trait and implementations.

use async_trait::async_trait;
use anyhow::Result;
use crate::pipeline::unified::UserContext;
use super::jwt::JWTValidator;

/// Auth provider trait (supports Auth0, JWT, custom)
#[async_trait]
pub trait AuthProvider: Send + Sync {
    /// Validate token and extract user context
    async fn validate_token(&self, token: &str) -> Result<UserContext>;

    /// Optional: Refresh token
    async fn refresh_token(&self, refresh_token: &str) -> Result<(String, String)> {
        Err(anyhow::anyhow!("Token refresh not supported"))
    }

    /// Optional: Revoke token
    async fn revoke_token(&self, token: &str) -> Result<()> {
        Err(anyhow::anyhow!("Token revocation not supported"))
    }
}

/// Auth0 provider implementation
///
/// Expected Auth0 custom claims:
/// - `https://fraiseql.com/roles`: Array of role names
/// - `https://fraiseql.com/permissions`: Array of permission strings
///
/// Example JWT payload:
/// ```json
/// {
///   "sub": "auth0|123456",
///   "https://fraiseql.com/roles": ["admin", "user"],
///   "https://fraiseql.com/permissions": ["posts:write", "users:read"]
/// }
/// ```
pub struct Auth0Provider {
    validator: JWTValidator,
}

impl Auth0Provider {
    pub fn new(domain: &str, audience: Vec<String>) -> Result<Self> {
        let issuer = format!("https://{}/", domain);
        let jwks_url = format!("https://{}/.well-known/jwks.json", domain);

        Ok(Self {
            validator: JWTValidator::new(issuer, audience, jwks_url)?,
        })
    }
}

#[async_trait]
impl AuthProvider for Auth0Provider {
    async fn validate_token(&self, token: &str) -> Result<UserContext> {
        let claims = self.validator.validate(token).await?;

        // Extract roles and permissions from Auth0 custom claims
        let roles = claims.custom.get("https://fraiseql.com/roles")
            .and_then(|v| v.as_array())
            .map(|arr| arr.iter()
                .filter_map(|v| v.as_str().map(String::from))
                .collect())
            .unwrap_or_default();

        let permissions = claims.custom.get("https://fraiseql.com/permissions")
            .and_then(|v| v.as_array())
            .map(|arr| arr.iter()
                .filter_map(|v| v.as_str().map(String::from))
                .collect())
            .unwrap_or_default();

        // ✅ Fixed: Include exp in UserContext
        Ok(UserContext {
            user_id: Some(claims.sub),
            permissions,
            roles,
            exp: claims.exp as u64,
        })
    }
}

/// Custom JWT provider (for self-hosted auth)
///
/// Expected custom claims:
/// - `roles`: Array of role names
/// - `permissions`: Array of permission strings
pub struct CustomJWTProvider {
    validator: JWTValidator,
}

impl CustomJWTProvider {
    pub fn new(issuer: String, audience: Vec<String>, jwks_url: String) -> Result<Self> {
        Ok(Self {
            validator: JWTValidator::new(issuer, audience, jwks_url)?,
        })
    }
}

#[async_trait]
impl AuthProvider for CustomJWTProvider {
    async fn validate_token(&self, token: &str) -> Result<UserContext> {
        let claims = self.validator.validate(token).await?;

        // Extract roles/permissions from custom claims
        let roles = claims.custom.get("roles")
            .and_then(|v| v.as_array())
            .map(|arr| arr.iter()
                .filter_map(|v| v.as_str().map(String::from))
                .collect())
            .unwrap_or_default();

        let permissions = claims.custom.get("permissions")
            .and_then(|v| v.as_array())
            .map(|arr| arr.iter()
                .filter_map(|v| v.as_str().map(String::from))
                .collect())
            .unwrap_or_default();

        // ✅ Fixed: Include exp in UserContext
        Ok(UserContext {
            user_id: Some(claims.sub),
            permissions,
            roles,
            exp: claims.exp as u64,
        })
    }
}
```

---

### Step 4: User Context Cache (cache.rs) - ✅ CORRECTED

```rust
//! User context caching with LRU eviction.

use lru::LruCache;
use std::sync::Mutex;
use std::num::NonZeroUsize;
use crate::pipeline::unified::UserContext;

/// User context cache (token hash -> UserContext)
pub struct UserContextCache {
    cache: Mutex<LruCache<String, UserContext>>,  // ✅ Fixed: No need to store exp separately
}

impl UserContextCache {
    pub fn new(capacity: usize) -> Self {
        Self {
            cache: Mutex::new(LruCache::new(NonZeroUsize::new(capacity).unwrap())),
        }
    }

    /// Get cached user context if valid
    pub fn get(&self, token_hash: &str) -> Option<UserContext> {
        let mut cache = self.cache.lock().unwrap();

        if let Some(context) = cache.get(token_hash) {
            // ✅ Fixed: Check exp from UserContext itself
            let now = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_secs();

            if now < context.exp {
                return Some(context.clone());  // ✅ UserContext now derives Clone
            }

            // Expired - remove from cache
            cache.pop(token_hash);
        }

        None
    }

    /// Cache user context (exp is in UserContext)
    pub fn set(&self, token_hash: String, context: UserContext) {
        let mut cache = self.cache.lock().unwrap();
        cache.put(token_hash, context);
    }

    /// Clear entire cache
    pub fn clear(&self) {
        let mut cache = self.cache.lock().unwrap();
        cache.clear();
    }

    /// Get cache statistics
    pub fn stats(&self) -> CacheStats {
        let cache = self.cache.lock().unwrap();
        CacheStats {
            capacity: cache.cap().get(),
            size: cache.len(),
        }
    }
}

/// Cache statistics
#[derive(Debug, Clone)]
pub struct CacheStats {
    pub capacity: usize,
    pub size: usize,
}

/// Hash token for cache key (SHA256)
///
/// ✅ Security: Never store raw JWT tokens in cache.
/// Always hash them first to prevent token leakage.
pub fn hash_token(token: &str) -> String {
    use sha2::{Sha256, Digest};
    let mut hasher = Sha256::new();
    hasher.update(token.as_bytes());
    format!("{:x}", hasher.finalize())
}
```

---

### Step 5: Auth Errors (errors.rs) - No changes needed

```rust
//! Authentication error types.

use thiserror::Error;

#[derive(Error, Debug)]
pub enum AuthError {
    #[error("Token expired")]
    TokenExpired,

    #[error("Invalid token: {0}")]
    InvalidToken(String),

    #[error("Missing authorization header")]
    MissingAuthHeader,

    #[error("Invalid authorization header format")]
    InvalidAuthHeader,

    #[error("JWKS fetch failed: {0}")]
    JWKSFetchError(String),

    #[error("Key not found: {0}")]
    KeyNotFound(String),

    #[error("Insufficient permissions")]
    InsufficientPermissions,

    #[error("Authentication failed: {0}")]
    AuthenticationFailed(String),
}
```

---

### Step 6: Updated UserContext (unified.rs) - ✅ CORRECTED

```rust
// Update in fraiseql_rs/src/pipeline/unified.rs

/// User context for authorization and personalization.
#[derive(Debug, Clone)]  // ✅ Fixed: Added Clone derive
pub struct UserContext {
    pub user_id: Option<String>,
    pub permissions: Vec<String>,
    pub roles: Vec<String>,
    pub exp: u64,  // ✅ Fixed: Added expiration timestamp
}
```

---

### Step 7: Python Wrapper (src/fraiseql/auth/rust_provider.py) - No changes needed

```python
"""Rust-based authentication provider (Python wrapper)."""

from typing import Any

from fraiseql._fraiseql_rs import PyAuthProvider, PyUserContext
from fraiseql.auth.base import AuthProvider, UserContext, AuthenticationError


class RustAuth0Provider(AuthProvider):
    """Auth0 provider using Rust implementation.

    This is 5-10x faster than the Python implementation.
    """

    def __init__(self, domain: str, audience: list[str]):
        self._rust_provider = PyAuthProvider.auth0(domain, audience)

    async def validate_token(self, token: str) -> dict[str, Any]:
        """Validate token using Rust."""
        try:
            py_context = await self._rust_provider.validate_token(token)
            return {
                "sub": py_context.user_id,
                "roles": py_context.roles,
                "permissions": py_context.permissions,
            }
        except Exception as e:
            raise AuthenticationError(str(e))

    async def get_user_from_token(self, token: str) -> UserContext:
        """Get user context from token using Rust."""
        try:
            py_context = await self._rust_provider.validate_token(token)
            return UserContext(
                user_id=py_context.user_id,
                roles=py_context.roles,
                permissions=py_context.permissions,
            )
        except Exception as e:
            raise AuthenticationError(str(e))


class RustJWTProvider(AuthProvider):
    """Custom JWT provider using Rust implementation."""

    def __init__(self, issuer: str, audience: list[str], jwks_url: str):
        self._rust_provider = PyAuthProvider.jwt(issuer, audience, jwks_url)

    async def validate_token(self, token: str) -> dict[str, Any]:
        """Validate token using Rust."""
        try:
            py_context = await self._rust_provider.validate_token(token)
            return {
                "sub": py_context.user_id,
                "roles": py_context.roles,
                "permissions": py_context.permissions,
            }
        except Exception as e:
            raise AuthenticationError(str(e))

    async def get_user_from_token(self, token: str) -> UserContext:
        """Get user context from token using Rust."""
        try:
            py_context = await self._rust_provider.validate_token(token)
            return UserContext(
                user_id=py_context.user_id,
                roles=py_context.roles,
                permissions=py_context.permissions,
            )
        except Exception as e:
            raise AuthenticationError(str(e))
```

---

### Step 8: Integration with Unified Pipeline (unified.rs) - No changes needed

```rust
// Add auth validation to execute_sync()

pub fn execute_sync(
    &self,
    query_string: &str,
    variables: HashMap<String, JsonValue>,
    user_context: UserContext,  // Already validated by auth middleware
    auth_required: bool,
) -> Result<Vec<u8>> {
    // Check authentication if required
    if auth_required && user_context.user_id.is_none() {
        return Err(anyhow!("Authentication required"));
    }

    // Phase 6: Parse GraphQL query
    let parsed_query = crate::graphql::parser::parse_query(query_string)?;

    // ... rest of pipeline ...
}
```

---

### Step 9: PyO3 Bindings (lib.rs) - ✅ CORRECTED

```rust
// Add to lib.rs

use crate::auth::provider::{AuthProvider, Auth0Provider, CustomJWTProvider};
use std::sync::Arc;

#[pyclass]
pub struct PyAuthProvider {
    provider: Arc<dyn AuthProvider>,
}

#[pymethods]
impl PyAuthProvider {
    /// Create Auth0 provider
    #[staticmethod]
    pub fn auth0(domain: String, audience: Vec<String>) -> PyResult<Self> {
        let provider = Auth0Provider::new(&domain, audience)
            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;

        Ok(Self {
            provider: Arc::new(provider),
        })
    }

    /// Create custom JWT provider
    #[staticmethod]
    pub fn jwt(issuer: String, audience: Vec<String>, jwks_url: String) -> PyResult<Self> {
        let provider = CustomJWTProvider::new(issuer, audience, jwks_url)
            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;

        Ok(Self {
            provider: Arc::new(provider),
        })
    }

    /// Validate token and return user context
    pub fn validate_token(&self, py: Python, token: String) -> PyResult<PyObject> {
        let provider = self.provider.clone();

        // ✅ Fixed: Proper async handling with type conversion
        pyo3_asyncio::tokio::future_into_py(py, async move {
            let context = provider.validate_token(&token)
                .await
                .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;

            // ✅ Fixed: Convert UserContext to PyUserContext
            Python::with_gil(|py| {
                let py_context = PyUserContext {
                    user_id: context.user_id,
                    roles: context.roles,
                    permissions: context.permissions,
                };
                Ok(Py::new(py, py_context)?.into_py(py))
            })
        })
    }
}

#[pyclass]
#[derive(Clone)]
pub struct PyUserContext {
    #[pyo3(get)]
    pub user_id: Option<String>,
    #[pyo3(get)]
    pub roles: Vec<String>,
    #[pyo3(get)]
    pub permissions: Vec<String>,
}

// Add to module registration
fn fraiseql_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
    // ... existing exports ...

    m.add_class::<PyAuthProvider>()?;
    m.add_class::<PyUserContext>()?;

    Ok(())
}
```

---

## Verification Commands

### Build Rust Extension
```bash
cd fraiseql_rs
cargo build --release
cd ..
maturin develop --release
```

### Run Auth Tests
```bash
# Unit tests (Rust)
cargo test --lib auth

# Integration tests (Python)
pytest tests/test_rust_auth.py -xvs

# Auth enforcement tests
pytest tests/integration/auth/test_auth_enforcement.py -xvs

# Performance benchmark
pytest tests/performance/test_auth_performance.py -xvs
```

### Expected Test Output
```
tests/test_rust_auth.py::test_auth0_validation ✓ (2ms)
tests/test_rust_auth.py::test_jwt_validation ✓ (1ms)
tests/test_rust_auth.py::test_cached_validation ✓ (<1ms)
tests/test_rust_auth.py::test_expired_token ✓
tests/test_rust_auth.py::test_invalid_token ✓
tests/test_rust_auth.py::test_https_validation ✓
tests/test_rust_auth.py::test_timeout ✓

Performance:
- First token validation: ~5ms (JWKS fetch)
- Cached validation: <1ms (10x faster than Python)
- Cache hit rate: >95% for repeated tokens
```

---

## Acceptance Criteria

**Functionality:**
- ✅ JWT token validation with JWKS support (built-in)
- ✅ Auth0 provider implementation
- ✅ Custom JWT provider implementation
- ✅ User context caching with LRU eviction
- ✅ Proper error handling (TokenExpired, InvalidToken, etc.)
- ✅ Python wrapper maintains backward compatibility
- ✅ HTTPS validation for JWKS URLs
- ✅ Timeout protection for JWKS fetching

**Performance:**
- ✅ JWT validation: <1ms (cached), <10ms (uncached)
- ✅ 5-10x faster than Python implementation
- ✅ Cache hit rate >95% for production workloads
- ✅ JWKS cache reduces external API calls

**Testing:**
- ✅ All existing auth tests pass
- ✅ New Rust unit tests for JWT validation
- ✅ Integration tests for Auth0 and custom JWT
- ✅ Performance benchmarks show improvement
- ✅ Error handling tests (expired, invalid, missing tokens)
- ✅ Security tests (HTTPS validation, timeout)

**Quality:**
- ✅ No compilation warnings
- ✅ No clippy warnings
- ✅ Proper error propagation
- ✅ Thread-safe caching
- ✅ Documentation for all public APIs
- ✅ All QA issues fixed

---

## DO NOT

❌ **DO NOT** change the Python auth interface (maintain backward compatibility)
❌ **DO NOT** implement rate limiting here (Phase 12)
❌ **DO NOT** implement RBAC permission resolution here (Phase 11)
❌ **DO NOT** add complex auth flows (OAuth2 flows, SAML, etc.) - focus on JWT validation
❌ **DO NOT** implement token refresh/revoke in Phase 10 (nice-to-have for later)
❌ **DO NOT** add database lookups for user data (use JWT claims only)

---

## Dependencies (Cargo.toml) - ✅ CORRECTED

```toml
[dependencies]
# Existing dependencies...
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-native-tls"] }
tokio = { version = "1.35", features = ["full"] }  # ✅ Ensure full features

# Auth dependencies (Phase 10) - ✅ CORRECTED
jsonwebtoken = "9.2"  # Has built-in JWK support
reqwest = { version = "0.11", features = ["json"] }
sha2 = "0.10"
lru = "0.12"
async-trait = "0.1"
thiserror = "1.0"

# Python bindings
pyo3 = { version = "0.25", features = ["extension-module"] }
pyo3-asyncio = { version = "0.21", features = ["tokio-runtime"] }  # ✅ Added
```

---

## Migration Strategy

**Phase 1: Add Rust Auth (Week 1)**
- Implement Rust JWT validation (with all fixes applied)
- Add Auth0Provider and CustomJWTProvider
- Add caching layer with LRU
- Unit tests for all components

**Phase 2: Python Wrapper (Week 1)**
- Create RustAuth0Provider wrapper
- Maintain backward compatibility
- Add integration tests
- Performance benchmarks

**Phase 3: Gradual Migration (Week 2)**
- Update FastAPI to use Rust auth by default
- Keep Python auth as fallback
- Monitor performance improvements
- Feature flag: `use_rust_auth`

**Phase 4: Production Rollout (Week 2-3)**
- Canary deployment (1% → 10% → 50% → 100%)
- Monitor error rates and latency
- Collect performance metrics
- Full Rust auth in production

**Phase 5: Deprecation (Week 3+)**
- Deprecate Python auth implementations
- Remove after 2 releases
- Update documentation

---

## Performance Expectations

**Before (Python):**
- JWT validation: ~5-10ms
- No caching (every request validates)
- Python PyJWT overhead
- No JWKS caching

**After (Rust):**
- First validation: ~5ms (JWKS fetch)
- Cached validation: <1ms
- JWKS cached for 1 hour
- 5-10x improvement overall
- Reduced memory usage
- LRU cache prevents unbounded growth

**Real-World Impact:**
- 1000 req/s → Auth overhead: 1s/s → 0.1s/s
- P99 latency: -4-9ms
- CPU usage: -10-20%
- Memory: Bounded by LRU cache size

---

## Security Enhancements

**✅ All Security Issues Fixed:**
1. Token hashing (SHA256) - never store raw tokens
2. HTTPS-only JWKS URLs - reject HTTP
3. Algorithm restriction (RS256 only)
4. Timeout protection (5 seconds)
5. Cache expiration (1 hour for JWKS, JWT exp for user context)
6. Bounded cache size (LRU prevents memory leaks)

---

## QA Corrections Applied

**Critical Fixes (5/5):**
- ✅ JWK to PEM conversion (use built-in `DecodingKey::from_jwk`)
- ✅ Missing imports (SystemTime, Arc, Mutex)
- ✅ PyO3 async return type (UserContext → PyUserContext)
- ✅ UserContext Clone derive
- ✅ pyo3-asyncio dependency

**Runtime Fixes (5/5):**
- ✅ JWKS fetch timeout (5 seconds)
- ✅ LRU cache for JWKS (100 keys max)
- ✅ HTTPS validation for JWKS URLs
- ✅ Better error messages
- ✅ Exp field in UserContext

**Improvements (4/4):**
- ✅ Documented Auth0 custom claims format
- ✅ Improved error messages for validation failures
- ✅ Exp in UserContext (no duplicate extraction)
- ✅ Reusable HTTP client with timeout

**Total**: 14/14 issues fixed (100%)

---

## Next Phase Preview

**Phase 11** will add:
- RBAC permission resolution in Rust
- Role hierarchy computation
- PostgreSQL-backed permission caching
- Field-level authorization enforcement

---

## Summary of Changes from v1.0

**Code Changes:**
1. jwt.rs: Use built-in JWK support, add timeout, HTTPS validation, LRU cache
2. provider.rs: Add exp to UserContext, document custom claims
3. cache.rs: Simplify (exp now in UserContext), add stats
4. unified.rs: Add Clone derive and exp field to UserContext
5. lib.rs: Fix PyO3 async return type conversion

**Dependency Changes:**
- Added: pyo3-asyncio with tokio features
- Ensured: tokio has "full" features
- All others unchanged (already correct)

**Status**: ✅ Ready for implementation
**Confidence**: High - all issues addressed
**Risk**: Low - straightforward fixes

---

*Last Updated: December 21, 2024*
*Version: 2.0 (QA Corrected)*
*Status: ✅ Ready for Implementation*
