-
Notifications
You must be signed in to change notification settings - Fork 20
Description
The current JwtAuthValidator uses the permissions property to check both JWT token scopes AND the user's hasPermission() method with OR logic. This conflates two distinct authorization concepts and creates semantic ambiguity. I propose introducing a separate scopes property for JWT-based checks while keeping permissions for user-level permission checks.
Current Behavior
When using JwtAuthValidator, firewall rules specify permissions:
{
"secureList": "v1:admin",
"permissions": "admin"
}
The validator checks both:
results.allow = (
tokenHasScopes( arguments.permissions, payload.scope ) // JWT scope
||
variables.cbsecurity.has( arguments.permissions ) // user.hasPermission()
);
The Problem
- Semantic Confusion
In the OAuth2/JWT ecosystem, "scopes" and "permissions" are distinct concepts:
Scopes are token-based claims embedded at creation time (fast, static). Whereas permissions are user/resource-level authorizations (dynamic, often database-backed) Using permissions for both makes it unclear which concept is being validated.
- Implicit OR Logic
The OR operation between scope and permission checks is not obvious from the configuration. Users may expect a variety of outcomes including, but not limited to:
- Only JWT scopes are checked (since we're using JWT authentication)
- Only permissions are checked (since the property is called "permissions")
- AND logic (must have both)
- The actual behavior (OR with short-circuit) is discovered only by reading source code.
- Performance Implications Unclear
Users don't realize that:
- Scope checking is fast (reading from decoded token)
- Permission checking may hit the database on every request
- The OR means performance varies based on which check succeeds first
- Can't Specify Exclusive Checks
There's no way to say:
- "Only check JWT scopes" (for maximum performance)
- "Only check permissions" (for dynamic, real-time authorization)
- "Check both with AND logic" (scope AND permission required)
Proposed Solution
Introduce a separate scopes property for JWT token validation:
// Check JWT scopes only (fast, static)
{
"secureList": "v1:admin",
"scopes": "admin"
}
// Check user permissions only (dynamic, database/entity-backed)
{
"secureList": "v1:reports",
"permissions": "view_reports"
}
// Check both with explicit AND logic
{
"secureList": "v1:sensitive",
"scopes": "admin", // Must have admin scope in JWT
"permissions": "delete_all" // AND have delete_all permission
}
Benefits
Semantic Clarity: Property names match industry-standard terminology
Explicit Intent: Configuration clearly shows what's being validated
Performance Visibility: Users can choose fast (scopes) vs dynamic (permissions)
Flexibility: Can use one, the other, or both with AND/OR logic
Better Documentation: Each property has a single, clear purpose
Easier Adoption: New users familiar with OAuth2/JWT understand immediately
Backward Compatibility
To minimize breaking changes:
- Keep current behavior as default for permissions (check both with OR)
- Introduce scopes in non-breaking release
- Make breaking change in next major version
What about other validators?
Non JWT validators should ignore the scopes property in the firewall, or even better, throw an exception if used since they are not compatible. I also think the JwtAuthValidator should throw an exception if someone attempts to use roles in the firewall rules list. Right now they are just ignored.
Either way, my proposal aligns with how other validators handle roles vs permissions:
AuthValidator has separate roles and permissions properties
BasicAuthValidator has separate roles and permissions properties
Only JwtAuthValidator conflates these concepts into a single permissions property and doesn't even use roles. They silently pass (which feels wrong IMO)
What About the "Secured" Annotation?
I'm open to suggestions about how to best handle a situation where a user uses the secured annotation to create a generic Authorization Context rule. Perhaps in this case we would check both the scope and permissions via a new isAuthorizationContext() method.