syntaxis/core/crates/api/JWT_INTEGRATION.md
Jesús Pérez 9cef9b8d57 refactor: consolidate configuration directories
Merge _configs/ into config/ for single configuration directory.
Update all path references.

Changes:
- Move _configs/* to config/
- Update .gitignore for new patterns
- No code references to _configs/ found

Impact: -1 root directory (layout_conventions.md compliance)
2025-12-26 18:36:23 +00:00

8.0 KiB

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

# 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:

pub struct AppState {
    pub db: Arc<PersistenceLayer>,
    pub jwt_provider: Option<Arc<JwtProvider>>,  // NEW
    // ... other fields
}

3. Middleware Integration

File: syntaxis/crates/syntaxis-api/src/middleware/auth.rs

pub async fn auth_middleware(
    State(state): State<AppState>,
    headers: HeaderMap,
    mut req: Request,
    next: Next,
) -> Result<Response, ApiError>

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

// 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

// 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

// In any protected handler:

pub async fn my_handler(
    State(state): State<AppState>,
    axum::Extension(claims): axum::Extension<Claims>,  // Claims from middleware
) -> ApiResult<Json<Response>> {
    // 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

// 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<Claims>,
) -> ApiResult<Json<Response>> {
    require_role(&claims, "admin")?;
    // ... handler code
}

Configuration

Development

# syntaxis-api-config.toml
[server]
host = "127.0.0.1"
port = 3001
database_path = "data/workspace.db"

[server.features]
auth = { enabled = true }
# 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

# 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 <token>
  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

#[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

#[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 <token>
  • 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