Repository navigation
JSON Web Tokens
This document provides a comprehensive guide to the JSON Web Token (JWT) authentication and authorization system implemented in the NR Waste Plus backend. The system leverages Spring Security's OAuth2 resource server capabilities with custom extensions to support role-based access control (RBAC), identity provider validation, and fine-grained authorization rules.
The implementation goes beyond Spring's default JWT handling to support:
-
Custom JWT Claims: Processing of
cognito:groupsclaims instead of standard authorities -
Abstract Role Mapping: Dynamic role composition based on client identifiers (e.g.,
PLANNER_12345) - Identity Provider Validation: Enforcement of access rules based on user identity providers (BCeID, IDIR, etc.)
- Request-Level Authorization: Custom authorization managers for granular endpoint-level security
The JWT authentication system is built on several foundational Spring Security components:
┌─────────────────────────────────────────────────────────────────┐
│ Spring Security Filter Chain │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ BearerTokenAuthenticationFilter (OAuth2) │
│ ◄─ Extracts JWT from Authorization: Bearer <token> header │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Oauth2SecurityCustomizer Configuration │
│ ◄─ Configures JwtAuthenticationConverter & JWK Set URI │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ JwtAuthenticationConverter & Authority Processing │
│ ◄─ Extracts cognito:groups from JWT claims │
│ ◄─ Converts groups to GrantedAuthority instances │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ JwtRoleChecker & JwtRoleAuthorizationManagerFactory │
│ ◄─ Performs runtime authorization checks │
│ ◄─ Evaluates abstract roles, concrete roles, and IDP │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ApiAuthorizationCustomizer │
│ ◄─ Applies endpoint-level authorization rules │
└─────────────────────────────────────────────────────────────────┘
Location: ca.bc.gov.nrs.hrs.configuration.SecurityConfiguration
Purpose: The primary configuration class that assembles the complete Spring Security filter chain.
Key Responsibilities:
- Registers the primary
SecurityFilterChainbean - Composes multiple security customizers in a logical order
- Enables method-level security via
@EnableMethodSecurity
Why Custom Configuration: Spring's default configuration doesn't natively support:
- Custom JWT claim extraction from
cognito:groups - Abstract role composition (role + client ID)
- Identity provider-based access control
- Cross-service JWT forwarding
By creating a modular configuration with composable customizers, we gain:
- Flexibility: Each security concern is isolated in its own customizer
- Testability: Individual components can be tested independently
- Maintainability: Changes to one aspect don't affect others
- Reusability: Customizers can be applied to different filter chains
Reference: Spring Security HTTP Security Builders
Location: ca.bc.gov.nrs.hrs.security.Oauth2SecurityCustomizer
Purpose: Configures OAuth2 resource server support and custom JWT processing.
Implementation Details:
@Component
public class Oauth2SecurityCustomizer implements
Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>> {
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
String jwkSetUri;
@Override
public void customize(OAuth2ResourceServerConfigurer<HttpSecurity> customize) {
customize.jwt(jwt -> jwt.jwtAuthenticationConverter(converter()).jwkSetUri(jwkSetUri));
}
private Converter<Jwt, AbstractAuthenticationToken> converter() {
JwtGrantedAuthoritiesConverter authConverter = new JwtGrantedAuthoritiesConverter();
authConverter.setAuthoritiesClaimName("cognito:groups");
authConverter.setAuthorityPrefix("");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authConverter);
return converter;
}
}Why It's Customized:
Spring's default JwtAuthenticationConverter looks for authorities in the scope or scp claim. However, AWS Cognito (our identity provider) places group information in the cognito:groups claim. This customizer:
-
Redirects Authority Extraction: Tells Spring to extract authorities from
cognito:groupsinstead of the default claim -
Removes Authority Prefix: By default, Spring prefixes authorities with "SCOPE_". We disable this since our groups are already properly formatted (e.g.,
PLANNER,VIEWER) - Configures JWK Set URI: Points to the JWKS endpoint for JWT signature validation
Reference:
Location: ca.bc.gov.nrs.hrs.security.JwtRoleChecker
Purpose: Runtime role checking utility used in @PreAuthorize annotations and custom authorization logic.
Key Methods:
Checks for both concrete and abstract role variants:
public boolean hasRole(String role) {
return hasRoleMatching(
currentRole -> currentRole.equalsIgnoreCase(role)
|| currentRole.startsWith((role + "_").toUpperCase(Locale.ROOT))
);
}Example:
- User has role:
PLANNER_12345 - Query:
hasRole("PLANNER") - Result:
true(matches both the prefix and abstract variant)
Use Cases:
- General role checking where both generic and client-specific variants are acceptable
- Backwards compatibility with code expecting concrete roles
Checks for exact role match only:
public boolean hasConcreteRole(String role) {
return hasRoleMatching(currentRole -> currentRole.equalsIgnoreCase(role));
}Example:
- User has role:
VIEWER - Query:
hasConcreteRole("VIEWER") - Result:
true
Use Cases:
- Global administrator roles (e.g.,
ADMIN) - System-wide permissions that don't require client-specific variants
- Strict access control scenarios
Checks for a dynamically composed abstract role:
public boolean hasAbstractRole(String rolePrefix, String clientId) {
return hasRoleMatching(currentRole ->
currentRole.equalsIgnoreCase(rolePrefix + "_" + clientId)
);
}Example:
- User has role:
PLANNER_12345 - Query:
hasAbstractRole("PLANNER", "12345") - Result:
true
Use Cases:
- Multi-tenant scenarios where users have roles scoped to specific clients
- Resource-based access control (e.g., "Is this user a planner for this specific forest client?")
- Dynamic role composition based on request parameters
Generic role checking using arbitrary predicates:
public boolean hasRoleMatching(Predicate<String> matcher) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return false;
}
return authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.map(String::toUpperCase)
.anyMatch(matcher);
}Use Cases:
- Pattern-based role checking (starts with, contains, regex matching)
- Complex authorization logic
- Future-proof checking when role names follow conventions
Checks if the authenticated user's identity provider matches:
public boolean hasIdpProvider(IdentityProvider identityProvider) {
return JwtPrincipalUtil
.getIdentityProvider(getJwt())
.equals(identityProvider);
}Example:
- User authenticated via BCeID
- Query:
hasIdpProvider(IdentityProvider.BUSINESS_BCEID) - Result:
true
Use Cases:
- Restricting endpoints to specific identity providers (e.g., only IDIR users can access admin features)
- External partner access control
- Compliance with organizational policies
Reference: Spring Security Method Security
Location: ca.bc.gov.nrs.hrs.security.JwtRoleAuthorizationManagerFactory
Purpose: Factory for creating AuthorizationManager instances that bridge JwtRoleChecker with Spring Security's request-level authorization framework.
Why It's Needed:
Spring Security distinguishes between:
-
Method-level authorization:
@PreAuthorize,@PostAuthorize(handled via SpEL) -
Request-level authorization: Endpoint-level rules in
SecurityFilterChain
The factory translates JwtRoleChecker logic into AuthorizationManager instances suitable for request-level authorization:
.authorizeHttpRequests(authorize ->
authorize
.requestMatchers("/api/planner/**")
.access(roleFactory.gotRole("PLANNER")) // ◄─ Factory creates this
)Key Factory Methods:
// Arbitrary predicate-based checking
AuthorizationManager<RequestAuthorizationContext> gotRoleMatching(Predicate<String> matcher)
// Concrete role checking
AuthorizationManager<RequestAuthorizationContext> gotRole(String role)
// Abstract role checking with dynamic client ID extraction
AuthorizationManager<RequestAuthorizationContext> gotAbstractRole(
String rolePrefix,
Function<HttpServletRequest, String> clientIdExtractor)
// Identity provider checking (both string and enum variants)
AuthorizationManager<RequestAuthorizationContext> gotIdp(String provider)
AuthorizationManager<RequestAuthorizationContext> gotIdp(IdentityProvider provider)Reference: Spring Security Authorization Architecture
Location: ca.bc.gov.nrs.hrs.security.ApiAuthorizationCustomizer
Purpose: Applies route-level authorization rules to HTTP endpoints.
Current Authorization Rules:
authorize
// Actuator endpoints - no auth required
.requestMatchers(HttpMethod.GET, "/metrics", "/health")
.permitAll()
// OPTIONS requests require authentication (CORS preflight)
.requestMatchers(HttpMethod.OPTIONS, "/**")
.authenticated()
// User preferences - authenticated users only
.requestMatchers("/api/users/preferences")
.authenticated()
// Codes endpoint - authenticated users only
.requestMatchers("/api/codes/**")
.authenticated()
// Forest clients - BCeID users only
.requestMatchers("/api/forest-clients/clients")
.access(roleCheck.gotIdp(IdentityProvider.BUSINESS_BCEID))
// Forest clients - authenticated users only
.requestMatchers("/api/forest-clients/**")
.authenticated()
// Search endpoints - authenticated users only
.requestMatchers("/api/search/**")
.authenticated()Why Custom Configuration:
Default Spring Security requires all endpoints to be explicitly configured. By creating a customizer, we:
- Keep authorization rules centralized and maintainable
- Support both simple (
authenticated()) and complex (access()with custom managers) rules - Enable easy auditing of endpoint security
Reference: Spring Security Authorize HTTP Requests
Method-level security is enforced at the method invocation level and is perfect for protecting business logic methods. The @PreAuthorize annotation uses Spring Expression Language (SpEL) to evaluate authorization rules.
Available SpEL Beans:
-
@auth→JwtRoleCheckerinstance -
@roles→ Enum of role constants
Use when you need to verify an exact role match:
@PreAuthorize("@auth.hasConcreteRole(@roles.VIEWER)")
public ResponseEntity<?> getDataList() {
return ResponseEntity.ok("Data for VIEWER role users");
}When to use:
- Global/system-wide permissions
- Roles that don't have client-specific variants
- Strict access control for sensitive operations
Spring Documentation: SpEL for Authorization
Use when you need to verify a role that is scoped to a specific client. The client ID is typically extracted from a path variable:
@PreAuthorize("@auth.hasAbstractRole(@roles.PLANNER, #clientNumber)")
public ResponseEntity<?> getDataByClient(@PathVariable String clientNumber) {
return ResponseEntity.ok("Access granted for client " + clientNumber);
}Flow:
- User makes request to
/api/forest-clients/12345/planning - Spring extracts
clientNumber="12345"from path variable -
JwtRoleCheckerchecks if user has rolePLANNER_12345 - If user's JWT contains
PLANNER_12345incognito:groups, access is granted
When to use:
- Multi-tenant applications
- Client-specific permissions
- Resource-based access control
- Most common pattern in forest client management
Example Scenario:
@GetMapping("/api/forest-clients/{clientNumber}/planning-data")
@PreAuthorize("@auth.hasAbstractRole(@roles.PLANNER, #clientNumber)")
public ResponseEntity<?> getPlanningData(@PathVariable String clientNumber) {
// Only users with PLANNER_<clientNumber> role can access
return ResponseEntity.ok(planningService.getData(clientNumber));
}Use when you need complex role-matching logic, such as pattern-based checks:
@PreAuthorize("@auth.hasRoleMatching(role -> role.startsWith('APPROVER_'))")
public ResponseEntity<?> getApprovalQueue() {
return ResponseEntity.ok("Approval queue data");
}Advanced Examples:
// Check for any client-scoped planner role
@PreAuthorize("@auth.hasRoleMatching(role -> role.matches('^PLANNER_\\d+$'))")
public ResponseEntity<?> getPlannerDashboard() {
return ResponseEntity.ok("All clients where user is a planner");
}
// Check for multiple role prefixes
@PreAuthorize("@auth.hasRoleMatching(role -> " +
"role.startsWith('ADMIN_') || role.startsWith('SUPER_'))")
public ResponseEntity<?> getAdminPanel() {
return ResponseEntity.ok("Admin functionality");
}
// Check for roles containing specific pattern
@PreAuthorize("@auth.hasRoleMatching(role -> role.contains('REVIEWER'))")
public ResponseEntity<?> getReviewTasks() {
return ResponseEntity.ok("Tasks requiring review");
}When to use:
- Complex authorization logic
- Bulk role checking across multiple role families
- Future-proof checking when role naming conventions are stable
Benefits:
- Eliminates need for multiple
@PreAuthorizeannotations - Reduces code duplication
- Makes business logic clearer
Use when you need to restrict access based on the user's identity provider:
@PreAuthorize("@auth.hasIdpProvider(@providers.BUSINESS_BCEID)")
public ResponseEntity<?> registerNewBusiness() {
return ResponseEntity.ok("Business registration endpoint");
}Use Cases:
- Restricting external business operations to BCeID users
- Limiting internal administrative tasks to IDIR users
- Compliance-based access control
Example Scenario:
@PostMapping("/api/forest-clients")
@PreAuthorize("@auth.hasIdpProvider(@providers.BUSINESS_BCEID)")
public ResponseEntity<?> createForestClient(@RequestBody ClientRequest request) {
// Only BCeID-authenticated users can create clients
return ResponseEntity.ok(clientService.create(request));
}Request-level authorization is evaluated early in the filter chain and is best for endpoint-level rules. This approach integrates with the security configuration and provides consistent enforcement across all access paths.
@Component
public class ApiAuthorizationCustomizer implements
Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>
.AuthorizationManagerRequestMatcherRegistry> {
private final JwtRoleAuthorizationManagerFactory roleCheck;
@Override
public void customize(
AuthorizeHttpRequestsConfigurer<HttpSecurity>
.AuthorizationManagerRequestMatcherRegistry authorize) {
authorize
// Forest clients accessible by BCeID users only
.requestMatchers("/api/forest-clients/clients")
.access(roleCheck.gotIdp(IdentityProvider.BUSINESS_BCEID))
// Planning endpoints accessible by users with PLANNER role
.requestMatchers("/api/planning/**")
.access(roleCheck.gotRole("PLANNER"))
// Generic admin endpoints
.requestMatchers("/api/admin/**")
.access(roleCheck.gotRole("ADMIN"))
// Everything else requires authentication
.anyRequest()
.authenticated();
}
}Advantages:
- Early filtering (before business logic execution)
- Centralized security policy
- Consistent rule application
- Better performance for denied requests
Reference: Spring Security Request Authorization
@RestController
@RequestMapping("/api/forest-clients")
@RequiredArgsConstructor
public class ForestClientController {
private final ForestClientService clientService;
/**
* Retrieve forest client details.
* Accessible to authenticated users (any role, any IDP).
*/
@GetMapping("/{clientNumber}")
@PreAuthorize("authenticated")
public ResponseEntity<ClientDTO> getClient(@PathVariable String clientNumber) {
ClientDTO client = clientService.findByNumber(clientNumber);
return ResponseEntity.ok(client);
}
/**
* Get planning data for a specific client.
* Only PLANNER_<clientNumber> role holders can access this.
*/
@GetMapping("/{clientNumber}/planning")
@PreAuthorize("@auth.hasAbstractRole(@roles.PLANNER, #clientNumber)")
public ResponseEntity<PlanningDataDTO> getPlanningData(
@PathVariable String clientNumber) {
PlanningDataDTO data = clientService.getPlanningData(clientNumber);
return ResponseEntity.ok(data);
}
/**
* Update planning data for a client.
* Requires both PLANNER role for the client AND BCeID identity.
*/
@PutMapping("/{clientNumber}/planning")
@PreAuthorize("@auth.hasAbstractRole(@roles.PLANNER, #clientNumber) AND " +
"@auth.hasIdpProvider(@providers.BUSINESS_BCEID)")
public ResponseEntity<PlanningDataDTO> updatePlanningData(
@PathVariable String clientNumber,
@RequestBody UpdatePlanningRequest request) {
PlanningDataDTO updated = clientService.updatePlanningData(clientNumber, request);
return ResponseEntity.ok(updated);
}
/**
* Approve a plan for a client.
* Only users with APPROVER_<clientNumber> role can approve.
*/
@PostMapping("/{clientNumber}/planning/approve")
@PreAuthorize("@auth.hasAbstractRole(@roles.APPROVER, #clientNumber)")
public ResponseEntity<ApprovalResultDTO> approvePlan(
@PathVariable String clientNumber,
@RequestBody ApprovalRequest request) {
ApprovalResultDTO result = clientService.approvePlan(clientNumber, request);
return ResponseEntity.ok(result);
}
/**
* Create a new forest client.
* Only BCeID business users can create new clients.
*/
@PostMapping
@PreAuthorize("@auth.hasIdpProvider(@providers.BUSINESS_BCEID)")
public ResponseEntity<ClientDTO> createClient(@RequestBody CreateClientRequest request) {
ClientDTO created = clientService.create(request);
return ResponseEntity.created(URI.create("/api/forest-clients/" + created.getNumber()))
.body(created);
}
/**
* List all clients where user has any role.
* Uses role matching to find all PLANNER_* or APPROVER_* roles.
*/
@GetMapping("/my-clients")
@PreAuthorize("@auth.hasRoleMatching(role -> " +
"role.startsWith('PLANNER_') || role.startsWith('APPROVER_'))")
public ResponseEntity<List<ClientSummaryDTO>> getMyClients() {
List<ClientSummaryDTO> clients = clientService.getUserClients();
return ResponseEntity.ok(clients);
}
}@RestController
@RequestMapping("/api/admin")
@RequiredArgsConstructor
public class AdminController {
private final AdminService adminService;
/**
* System administration dashboard.
* Only users with exact ADMIN role can access.
*/
@GetMapping("/dashboard")
@PreAuthorize("@auth.hasConcreteRole(@roles.ADMIN)")
public ResponseEntity<AdminDashboardDTO> getDashboard() {
AdminDashboardDTO dashboard = adminService.buildDashboard();
return ResponseEntity.ok(dashboard);
}
/**
* Grant roles to users.
* System-wide admin operation.
*/
@PostMapping("/users/{userId}/roles")
@PreAuthorize("@auth.hasConcreteRole(@roles.ADMIN)")
public ResponseEntity<UserDTO> grantRole(
@PathVariable String userId,
@RequestBody GrantRoleRequest request) {
UserDTO user = adminService.grantRole(userId, request.getRole());
return ResponseEntity.ok(user);
}
/**
* Revoke roles from users.
*/
@DeleteMapping("/users/{userId}/roles/{roleId}")
@PreAuthorize("@auth.hasConcreteRole(@roles.ADMIN)")
public ResponseEntity<Void> revokeRole(
@PathVariable String userId,
@PathVariable String roleId) {
adminService.revokeRole(userId, roleId);
return ResponseEntity.noContent().build();
}
/**
* System configuration endpoint.
*/
@PutMapping("/config")
@PreAuthorize("@auth.hasConcreteRole(@roles.ADMIN)")
public ResponseEntity<SystemConfigDTO> updateConfig(
@RequestBody UpdateConfigRequest request) {
SystemConfigDTO config = adminService.updateConfig(request);
return ResponseEntity.ok(config);
}
}@RestController
@RequestMapping("/api/compliance")
@RequiredArgsConstructor
public class ComplianceController {
private final ComplianceService complianceService;
/**
* Compliance audit trail endpoint.
* Accessible to any auditor or reviewer role.
*/
@GetMapping("/audit-trail")
@PreAuthorize("@auth.hasRoleMatching(role -> " +
"role.startsWith('AUDITOR') || role.startsWith('REVIEWER'))")
public ResponseEntity<List<AuditLogDTO>> getAuditTrail() {
List<AuditLogDTO> logs = complianceService.getAuditTrail();
return ResponseEntity.ok(logs);
}
/**
* Generate compliance report.
* Accessible to power users (multiple role variants).
*/
@PostMapping("/reports")
@PreAuthorize("@auth.hasRoleMatching(role -> " +
"role.equals('ADMIN') || role.startsWith('COMPLIANCE_OFFICER'))")
public ResponseEntity<ComplianceReportDTO> generateReport(
@RequestBody ReportRequest request) {
ComplianceReportDTO report = complianceService.generateReport(request);
return ResponseEntity.ok(report);
}
/**
* View compliance status for a specific client.
* Combines abstract role check with custom matcher.
*/
@GetMapping("/{clientNumber}/status")
@PreAuthorize("@auth.hasAbstractRole(@roles.PLANNER, #clientNumber) OR " +
"@auth.hasRoleMatching(role -> role.startsWith('COMPLIANCE_'))")
public ResponseEntity<ComplianceStatusDTO> getComplianceStatus(
@PathVariable String clientNumber) {
ComplianceStatusDTO status = complianceService.getStatus(clientNumber);
return ResponseEntity.ok(status);
}
}@RestController
@RequestMapping("/api/public")
@RequiredArgsConstructor
public class PublicIntegrationController {
private final IntegrationService integrationService;
/**
* Business partner integration endpoint.
* Restricted to BCeID business users.
* Allows external businesses to interact with the system.
*/
@PostMapping("/partner/submit-data")
@PreAuthorize("@auth.hasIdpProvider(@providers.BUSINESS_BCEID)")
public ResponseEntity<SubmissionDTO> submitPartnerData(
@RequestBody PartnerDataRequest request) {
SubmissionDTO submission = integrationService.processPartnerSubmission(request);
return ResponseEntity.ok(submission);
}
/**
* Internal administration endpoint.
* Restricted to IDIR users only (government employees).
*/
@GetMapping("/internal/statistics")
@PreAuthorize("@auth.hasIdpProvider(@providers.IDIR)")
public ResponseEntity<SystemStatisticsDTO> getInternalStatistics() {
SystemStatisticsDTO stats = integrationService.getStatistics();
return ResponseEntity.ok(stats);
}
/**
* Service account operations.
* Restricted to service account identity provider.
*/
@PostMapping("/service/process-batch")
@PreAuthorize("@auth.hasIdpProvider(@providers.SERVICE_ACCOUNT)")
public ResponseEntity<BatchProcessingResultDTO> processBatch(
@RequestBody BatchProcessRequest request) {
BatchProcessingResultDTO result = integrationService.processBatch(request);
return ResponseEntity.ok(result);
}
/**
* Combine role and IDP checks.
* Only IDIR auditors can generate internal compliance reports.
*/
@PostMapping("/internal/audit-report")
@PreAuthorize("@auth.hasIdpProvider(@providers.IDIR) AND " +
"@auth.hasConcreteRole(@roles.AUDITOR)")
public ResponseEntity<AuditReportDTO> generateAuditReport(
@RequestBody AuditReportRequest request) {
AuditReportDTO report = integrationService.generateAuditReport(request);
return ResponseEntity.ok(report);
}
}The JWT tokens issued by the identity provider contain the following claims used by the system:
{
"sub": "user-id",
"aud": "application-client-id",
"iss": "https://cognito-idp.region.amazonaws.com/user-pool-id",
"cognito:groups": [
"PLANNER_12345",
"VIEWER",
"APPROVER_67890"
],
"custom:idp_name": "BUSINESS_BCEID",
"custom:idp_username": "username@business.ca",
"custom:idp_business_id": "BC0123456789",
"custom:idp_business_name": "Business Name Ltd.",
"email": "user@business.ca",
"iat": 1609459200,
"exp": 1609545600
}Key Claims:
-
cognito:groups: Array of roles/groups the user belongs to. This is the source of authorization data. -
custom:idp_name: Identity provider name (extracted and normalized byJwtPrincipalUtil) -
custom:idp_username: User's username in the identity provider -
custom:idp_business_id: Business identifier (for BCeID users) -
custom:idp_business_name: Business name (for BCeID users)
sequenceDiagram
participant Client
participant Frontend
participant SecurityFilter
participant OAuth2Filter
participant JwtConverter
participant RoleChecker
participant Controller
Client->>Frontend: Login request
Frontend->>Client: Issue JWT (with cognito:groups)
Client->>SecurityFilter: HTTP request with Authorization: Bearer <JWT>
SecurityFilter->>OAuth2Filter: Extract bearer token
OAuth2Filter->>JwtConverter: Validate JWT signature & decode
JwtConverter->>JwtConverter: Extract cognito:groups claim
JwtConverter->>JwtConverter: Convert groups to GrantedAuthority
OAuth2Filter->>SecurityFilter: Set JwtAuthenticationToken
SecurityFilter->>SecurityFilter: Store in SecurityContext
SecurityFilter->>Controller: Forward request
Controller->>RoleChecker: Evaluate @PreAuthorize
RoleChecker->>RoleChecker: Check roles from SecurityContext
RoleChecker->>Controller: Authorization decision
Controller-->>Client: Response
- Forwarding JWT Token to Legacy Services
- Spring Security OAuth2 Resource Server
- Spring Security Method-Level Security
- Spring Expression Language (SpEL)
- JWT.io - Introduction to JWT
- Request-level: For coarse-grained endpoint protection
- Method-level: For fine-grained business logic protection
- Combine both: For defense-in-depth security
-
hasConcreteRole(): For system-wide permissions -
hasAbstractRole(): For client-scoped multi-tenant access -
hasRoleMatching(): For complex pattern matching -
hasIdpProvider(): For external/internal boundary enforcement
@SpringBootTest
@WithMockJwt(authorities = {"PLANNER_12345"})
void testPlannerAccess() {
// Test will include role in mock JWT
}- Add JavaDoc comments explaining authorization rules
- Include examples in API documentation
- Document role names and their purposes
- Log authorization failures for debugging
- Track role assignments and changes
- Regularly review who has which roles
Q: Why do we need both hasRole() and hasConcreteRole()?
A: hasRole() provides flexibility for both generic and client-specific roles, while hasConcreteRole() ensures exact matches for system-wide permissions.
Q: Can I combine multiple @PreAuthorize conditions?
A: Yes, use SpEL operators: @auth.hasRole('PLANNER') AND @auth.hasIdpProvider('IDIR') or use || for OR logic.
Q: How are role changes reflected in existing sessions? A: JWT tokens are immutable. Role changes require token re-issuance. Implement token refresh to apply changes.
Q: What happens if JWT validation fails? A: Spring Security rejects the request with a 401 Unauthorized or 403 Forbidden response.
Q: Can I extract custom claims from JWT in my code?
A: Yes, inject the JWT authentication and call getToken().getClaim("claim-name").
This wiki documents NR Waste Plus for users and developers. Verify security and deployment guidance against the current application configuration before applying it elsewhere.
- Report an issue or ask a question in the GitHub repository.
- Follow the Contributing Guidelines to propose an update.
- Read the Code of Conduct.
Collection indexes support human browsing and machine retrieval. Keep both current when adding or moving wiki pages.
NR Waste Plus Wiki | GitHub Repository