syntaxis/core/crates/api/JWT_INTEGRATION.md

330 lines
8.0 KiB
Markdown
Raw Permalink Normal View History

# 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<PersistenceLayer>,
pub jwt_provider: Option<Arc<JwtProvider>>, // NEW
// ... other fields
}
```
### 3. Middleware Integration
File: `syntaxis/crates/syntaxis-api/src/middleware/auth.rs`
```rust
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
```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<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
```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<Claims>,
) -> ApiResult<Json<Response>> {
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 <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
```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 <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