# JWT Validation Integration Guide ## Overview This guide describes how to integrate the complete JWT validation system into the syntaxis-api server. ## Components ### 1. AuthConfig (in shared-api-lib) Located in `shared/rust-api/shared-api-lib/src/config/mod.rs` ```toml # configs/features/auth.toml api_keys = ["key1", "key2"] jwt_secret = "your-secret-key-at-least-32-chars" jwt_enabled = true jwt_expiration = 3600 # 1 hour ``` ### 2. AppState Updates Added to `syntaxis/crates/syntaxis-api/src/state.rs`: ```rust pub struct AppState { pub db: Arc, pub jwt_provider: Option>, // NEW // ... other fields } ``` ### 3. Middleware Integration File: `syntaxis/crates/syntaxis-api/src/middleware/auth.rs` ```rust pub async fn auth_middleware( State(state): State, headers: HeaderMap, mut req: Request, next: Next, ) -> Result ``` ### 4. Auth Handlers Updated in `syntaxis/crates/syntaxis-api/src/handlers/auth.rs`: - **register()**: Creates JWT token for new user - **login()**: Generates JWT token on successful auth - **logout()**: Adds token to revoked_tokens table - **current_user()**: Extracts user from JWT claims ## Integration Steps ### Step 1: Initialize JwtProvider in main.rs ```rust // In main.rs, during app startup: let jwt_provider = if cfg.features.auth.jwt_enabled && cfg.auth.jwt_secret.is_some() { let secret = cfg.auth.jwt_secret.as_ref().unwrap(); Some(Arc::new(JwtProvider::new(secret, cfg.auth.jwt_expiration)?)) } else { None }; let app_state = AppState::new( db, metrics, rate_limiter, auth, jwt_provider, // NEW events, ); ``` ### Step 2: Apply Middleware to Protected Routes ```rust // In main.rs, when building routes: let protected_routes = Router::new() .route("/user/current", get(handlers::auth::current_user)) .route("/user/logout", post(handlers::auth::logout)) .route("/projects", get(handlers::projects::list)) // Protected .route("/projects/:id", get(handlers::projects::get)) // Protected .layer(axum::middleware::from_fn_with_state( app_state.clone(), middleware::auth::auth_middleware, )); let public_routes = Router::new() .route("/auth/register", post(handlers::auth::register)) .route("/auth/login", post(handlers::auth::login)) .route("/health", get(handlers::health)); let app = Router::new() .merge(public_routes) .merge(protected_routes) .with_state(app_state); ``` ### Step 3: Extract Claims in Handlers ```rust // In any protected handler: pub async fn my_handler( State(state): State, axum::Extension(claims): axum::Extension, // Claims from middleware ) -> ApiResult> { // Use claims.sub for user ID // Use claims.roles for authorization // Use claims.tenant_id for multi-tenancy Ok(Json(response)) } ``` ### Step 4: Implement RBAC Guards ```rust // Helper function for role-based access: fn require_role(claims: &Claims, required_role: &str) -> ApiResult<()> { if claims.roles.contains(&required_role.to_string()) { Ok(()) } else { Err(ApiError::Unauthorized( format!("Required role: {}", required_role) )) } } // In handler: pub async fn admin_handler( axum::Extension(claims): axum::Extension, ) -> ApiResult> { require_role(&claims, "admin")?; // ... handler code } ``` ## Configuration ### Development ```toml # syntaxis-api-config.toml [server] host = "127.0.0.1" port = 3001 database_path = "data/workspace.db" [server.features] auth = { enabled = true } ``` ```toml # configs/features/auth.toml api_keys = ["dev-key-123"] jwt_secret = "development-secret-key-minimum-32-characters-long" jwt_enabled = true jwt_expiration = 3600 # 1 hour ``` ### Production ```toml # Ensure jwt_secret has at least 32 characters # Use environment variables for sensitive values: jwt_secret = "${JWT_SECRET}" # Will be replaced at runtime # Or set at runtime: # export JWT_SECRET="your-production-secret-key-min-32-chars" ``` ## Token Lifecycle ### Token Creation (Login/Register) 1. User credentials validated 2. JwtProvider creates signed JWT with claims: - `sub` (user ID) - `roles` (user roles) - `tenant_id` (optional, for multi-tenancy) - `exp` (expiration timestamp) - `iat` (issued at) - `aud` (audience: "syntaxis-api") 3. Token returned to client ### Token Usage (Protected Routes) 1. Client sends request with: `Authorization: Bearer ` 2. Middleware extracts Bearer token 3. JwtProvider.verify_token() validates: - Token signature - Expiration time - Audience claim 4. Claims inserted into request extensions 5. Handlers access via `Extension(claims)` ### Token Revocation (Logout) 1. User calls logout endpoint with Bearer token 2. Token added to `revoked_tokens` table with: - `token_jti`: Unique identifier - `user_id`: User who owned token - `reason`: "logout" - `revoked_at`: Timestamp - `expires_at`: When to cleanup DB 3. Future requests with revoked token return 401 ## Security Considerations ### Secret Management - Store `jwt_secret` in environment variables, never in code - Minimum 32 characters (requirement for HS256) - Rotate secrets periodically - Use different secrets for different environments ### Token Expiration - Default: 1 hour - Configurable via `jwt_expiration` in config - Expired tokens automatically rejected by middleware - Cleanup of revoked tokens older than expiration ### HTTPS Only - Always use HTTPS in production - Tokens sent in Authorization header (secure) - Never send tokens in URLs ### Validation - Signature validated every request - Claims validated (exp, aud, etc.) - Revocation checked (TODO: implement in middleware) ## Testing ### Unit Tests ```rust #[tokio::test] async fn test_jwt_token_creation() { let provider = JwtProvider::new("secret-key-min-32-chars-long", 3600).unwrap(); let token = provider.create_token( "user123", vec!["user".to_string()], None ).unwrap(); let claims = provider.verify_token(&token).unwrap(); assert_eq!(claims.sub, "user123"); } ``` ### Integration Tests ```rust #[tokio::test] async fn test_login_returns_jwt() { // Setup app with real auth enabled // POST /auth/login with credentials // Verify response contains valid JWT token // Extract user ID from token } #[tokio::test] async fn test_protected_route_requires_jwt() { // Setup app // GET /user/current without Authorization header // Verify returns 401 Unauthorized // GET /user/current with invalid token // Verify returns 401 // GET /user/current with valid token // Verify returns 200 with user data } #[tokio::test] async fn test_logout_revokes_token() { // Login to get token // POST /user/logout with token // Verify returns 204 No Content // POST /user/logout with same token again // Verify returns 401 Unauthorized (token revoked) } ``` ## Troubleshooting ### "JWT provider not configured" - Ensure `jwt_enabled = true` in `configs/features/auth.toml` - Ensure `jwt_secret` is set and has at least 32 characters - Check that JwtProvider is initialized in AppState ### "Invalid Authorization header format" - Ensure header is: `Authorization: Bearer ` - Check for typos (case-sensitive "Bearer") - Verify no extra spaces ### "JWT validation failed" - Check token signature (may be expired) - Verify token uses correct secret - Check if token has been revoked - Verify audience matches "syntaxis-api" ### Token expiration issues - Increase `jwt_expiration` if needed - Client should refresh tokens before expiration - Implement token refresh endpoint (optional) ## Future Enhancements 1. **Token Refresh**: Implement /auth/refresh endpoint 2. **Revocation Check**: Complete TODO in middleware 3. **Custom Claims**: Add additional claims as needed 4. **Rate Limiting**: Combine with rate limiter for auth endpoints 5. **Audit Logging**: Log all auth events 6. **Multi-Factor Auth**: Add 2FA support