330 lines
8.0 KiB
Markdown
330 lines
8.0 KiB
Markdown
|
|
# 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
|