The system uses stateless JWT-based authorization for API requests. After OIDC authentication, clients receive a JWT token that must be included in subsequent API requests as a Bearer token.
- Client includes JWT in
Authorization: Bearer <token>header AuthorizationInterceptorvalidates token signature using JWKS- System extracts user context (username, email, role)
- Request proceeds if user has required permissions
Validates JWT tokens on API requests:
- Decodes and validates JWT signature against OIDC provider's JWKS
- Validates audience claim against OIDC client ID and allowed audiences
- Extracts user context from token claims
- Enforces role-based access control
- Sets up UserContext for request processing
Utility class for accessing user context throughout the application:
- Retrieves UserContext from request-scoped attributes
- Provides convenience methods for common user properties
- Returns safe default values for public endpoints
- Thread-safe via Spring's RequestContextHolder
Immutable user session data:
- Contains user identification (userId, username, email)
- Stores role-based permissions
- Provides permission check methods
- Includes logging helper methods
Configuration properties for authentication settings:
- Defines role group mappings
- Configures JWT claim names
- Lists public endpoints
- Supports environment-based configuration
Registers the authorization interceptor:
- Applies interceptor to
/api/**endpoints - Can be disabled via configuration
- Integrates with Spring MVC
app.authentication:
allowed-audiences: ${OIDC_ALLOWED_AUDIENCES:} # Optional: comma-separated list of additional allowed audiences
groups:
write: "ei:write" # Full access to all endpoints
read: "ei:read" # Read-only access
claims:
username: preferred_username
email: email
groups: groupsThe allowed-audiences property accepts a comma-separated list of audience values that are permitted in addition to the OIDC client ID. This allows tokens issued for different audiences (e.g., partner APIs, external services) to be accepted while maintaining security.
- JWT signature verified against OIDC provider's JWKS
- Required claims validated (subject, username, email, audience)
- Audience claim must match OIDC client ID or one of the allowed audiences
- Token expiration enforced
- Malformed tokens rejected with 401
- Users assigned roles based on group membership
WRITErole: Full API accessREADrole: Read-only operationsNONErole: Access denied (403)
Certain endpoints bypass authentication:
/api/health- Health check endpoint
Include JWT token in requests:
fetch('/api/endpoint', {
headers: {
'Authorization': `Bearer ${token}`
}
})Backend components can access current user information anywhere in the application:
public void SomeFunction() {
Optional<UserContext> userContext = CurrentUser.get();
if (userContext.isPresent()) {
UserContext user = userContext.get();
String email = user.getEmail();
UserRole role = user.getRole();
}
// Convenience methods for common operations
String userId = CurrentUser.getUserId(); // Returns "system" if no user
String username = CurrentUser.getUsername(); // Returns "system" if no user
String logId = CurrentUser.getLogIdentifier(); // username + userId for logging
boolean canWrite = CurrentUser.hasWriteAccess();
boolean canRead = CurrentUser.hasReadAccess();
}Additionally, controller methods can be annotated with @RequiredRoles() to specify roles required to access an endpoint for broad control. This prevents us from needing to write full role validation in every method.
@RequiredRoles(roles = {UserRole.WRITE})
@RestController
public class SomeController {
@PostMapping("/api/data")
public ResponseEntity<String> createData(@RequestBody DataRequest request) {
log.info("User {} creating new data record", CurrentUser.getLogIdentifier());
String createdBy = CurrentUser.getUsername();
// Process request...
return ResponseEntity.ok("Data created");
}
}First, authenticate via the OIDC flow to obtain a JWT token:
# Start the authentication flow (will redirect to identity provider)
curl -v http://localhost:8080/oauth2/authorization/oidcAfter completing the OIDC flow in a browser, the success handler returns a JSON response:
{
"idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresAt": 1234567890,
"username": "testuser",
"email": "test@example.com"
}# Set the token as an environment variable
export TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
# Test authorized request (should return 200)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/batches
# Test with write access (if user has ei:write group)
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "test"}' \
http://localhost:8080/api/batches# Test without token (should return 401)
curl -v http://localhost:8080/api/batches
# Test with invalid token (should return 401)
curl -H "Authorization: Bearer invalid.token.here" \
http://localhost:8080/api/batches
# Test with expired token (should return 401)
curl -H "Authorization: Bearer $EXPIRED_TOKEN" \
http://localhost:8080/api/batches# Health endpoint should work without authentication
curl http://localhost:8080/api/health# User with READ role trying to write (should return 403)
# This requires implementing write-only endpoints with permission checks
curl -X POST -H "Authorization: Bearer $READ_ONLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "test"}' \
http://localhost:8080/api/batchesSuccessful Authorization (200)
{
"data": "..."
}Missing/Invalid Token (401)
{
"error": "Unauthorized",
"status": 401
}Insufficient Permissions (403)
{
"error": "Forbidden",
"status": 403
}