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)
8.0 KiB
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)
-
User credentials validated
-
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")
-
Token returned to client
Token Usage (Protected Routes)
- Client sends request with:
Authorization: Bearer <token> - Middleware extracts Bearer token
- JwtProvider.verify_token() validates:
- Token signature
- Expiration time
- Audience claim
- Claims inserted into request extensions
- Handlers access via
Extension(claims)
Token Revocation (Logout)
-
User calls logout endpoint with Bearer token
-
Token added to
revoked_tokenstable with:token_jti: Unique identifieruser_id: User who owned tokenreason: "logout"revoked_at: Timestampexpires_at: When to cleanup DB
-
Future requests with revoked token return 401
Security Considerations
Secret Management
- Store
jwt_secretin 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_expirationin 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 = trueinconfigs/features/auth.toml - Ensure
jwt_secretis 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_expirationif needed - Client should refresh tokens before expiration
- Implement token refresh endpoint (optional)
Future Enhancements
- Token Refresh: Implement /auth/refresh endpoint
- Revocation Check: Complete TODO in middleware
- Custom Claims: Add additional claims as needed
- Rate Limiting: Combine with rate limiter for auth endpoints
- Audit Logging: Log all auth events
- Multi-Factor Auth: Add 2FA support