25.3 Application-Level Authorization Frameworks
Modern Java applications use framework-based authorization with Spring Security, Jakarta Security, and custom implementations.
Spring Security Authorization
// Spring Security Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableMethodSecurity // Enable method-level security
public class SecurityConfig {
// URL-based authorization
public void configureHttpSecurity(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// Public endpoints
.requestMatchers("/public/**", "/login").permitAll()
// Role-based access
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
// Permission-based access
.requestMatchers("/api/documents/**")
.hasAuthority("DOCUMENT_READ")
// Pattern matching
.requestMatchers("/api/users/{id}/**")
.access("@authzService.canAccessUser(authentication, #id)")
// Everything else requires authentication
.anyRequest().authenticated()
)
.csrf(csrf -> csrf.disable()) // Configure CSRF as needed
.formLogin(form -> form.defaultSuccessUrl("/dashboard"));
}
}
// Method-level security with annotations
import org.springframework.security.access.prepost.*;
import org.springframework.stereotype.Service;
@Service
public class DocumentService {
// Role-based
@PreAuthorize("hasRole('ADMIN')")
public void deleteDocument(Long id) {
// Only admins can delete
}
// Permission-based
@PreAuthorize("hasAuthority('DOCUMENT_WRITE')")
public Document updateDocument(Long id, Document doc) {
// Requires DOCUMENT_WRITE permission
return doc;
}
// Multiple roles
@PreAuthorize("hasAnyRole('USER', 'MANAGER', 'ADMIN')")
public List<Document> listDocuments() {
return List.of();
}
// Expression-based
@PreAuthorize("@authzService.canModifyDocument(authentication, #id)")
public void modifyDocument(Long id, String content) {
// Custom authorization logic
}
// Owner check
@PreAuthorize("@authzService.isOwner(authentication, #doc)")
public void updateOwnDocument(Document doc) {
// Only owner can update
}
// Post-authorization (filter results)
@PostAuthorize("returnObject.owner == authentication.name")
public Document getDocument(Long id) {
// Returns document only if user is owner
return new Document();
}
// Filter collections
@PostFilter("filterObject.isPublic || filterObject.owner == authentication.name")
public List<Document> getAllDocuments() {
// Returns only public documents or user's own documents
return List.of();
}
}
// Custom authorization service
@Service
public class AuthorizationService {
public boolean canAccessUser(Authentication auth, String userId) {
String currentUser = auth.getName();
// User can access own profile
if (currentUser.equals(userId)) {
return true;
}
// Admin can access any profile
return auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
}
public boolean canModifyDocument(Authentication auth, Long documentId) {
String username = auth.getName();
// Check if user is document owner or has admin role
Document doc = documentRepository.findById(documentId).orElse(null);
if (doc == null) return false;
return doc.getOwner().equals(username) ||
hasRole(auth, "ADMIN");
}
public boolean isOwner(Authentication auth, Document doc) {
return doc.getOwner().equals(auth.getName());
}
private boolean hasRole(Authentication auth, String role) {
return auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_" + role));
}
}
Custom UserDetails with Authorities
// Custom UserDetails implementation
import org.springframework.security.core.*;
import org.springframework.security.core.userdetails.*;
public class CustomUserDetails implements UserDetails {
private final String username;
private final String password;
private final Set<String> roles;
private final Set<String> permissions;
public CustomUserDetails(String username, String password,
Set<String> roles, Set<String> permissions) {
this.username = username;
this.password = password;
this.roles = roles;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<GrantedAuthority> authorities = new HashSet<>();
// Add roles (prefixed with ROLE_)
roles.forEach(role ->
authorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
// Add permissions
permissions.forEach(permission ->
authorities.add(new SimpleGrantedAuthority(permission)));
return authorities;
}
@Override
public String getPassword() { return password; }
@Override
public String getUsername() { return username; }
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() { return true; }
}
// UserDetailsService implementation
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() ->
new UsernameNotFoundException("User not found: " + username));
return new CustomUserDetails(
user.getUsername(),
user.getPassword(),
user.getRoles(),
user.getPermissions()
);
}
}
Jakarta Security (formerly Java EE Security)
// Jakarta Security configuration
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.security.enterprise.authentication.mechanism.http.HttpAuthenticationMechanism;
import jakarta.security.enterprise.identitystore.DatabaseIdentityStoreDefinition;
@DatabaseIdentityStoreDefinition(
dataSourceLookup = "java:app/jdbc/securityDS",
callerQuery = "SELECT password FROM users WHERE username = ?",
groupsQuery = "SELECT role FROM user_roles WHERE username = ?",
hashAlgorithm = Pbkdf2PasswordHash.class
)
@ApplicationScoped
public class SecurityConfiguration {
// Configuration via annotation
}
// Custom identity store
import jakarta.security.enterprise.identitystore.*;
import jakarta.security.enterprise.credential.*;
@ApplicationScoped
public class CustomIdentityStore implements IdentityStore {
@Inject
private UserService userService;
@Override
public CredentialValidationResult validate(Credential credential) {
if (credential instanceof UsernamePasswordCredential) {
UsernamePasswordCredential cred =
(UsernamePasswordCredential) credential;
User user = userService.authenticate(
cred.getCaller(),
cred.getPasswordAsString()
);
if (user != null) {
return new CredentialValidationResult(
user.getUsername(),
new HashSet<>(user.getRoles())
);
}
}
return CredentialValidationResult.INVALID_RESULT;
}
}
// Authorization with annotations
import jakarta.annotation.security.*;
import jakarta.ejb.Stateless;
@Stateless
public class DocumentBean {
@RolesAllowed("ADMIN")
public void deleteDocument(Long id) {
// Only admins
}
@RolesAllowed({"USER", "ADMIN"})
public Document getDocument(Long id) {
// Users and admins
return null;
}
@PermitAll
public List<Document> listPublicDocuments() {
// Everyone can access
return List.of();
}
@DenyAll
public void internalMethod() {
// No one can call directly
}
}
// Programmatic security
import jakarta.ejb.EJBContext;
import jakarta.annotation.Resource;
@Stateless
public class SecurityBean {
@Resource
private EJBContext context;
public void processDocument(Document doc) {
String caller = context.getCallerPrincipal().getName();
if (context.isCallerInRole("ADMIN")) {
// Admin can do anything
} else if (context.isCallerInRole("USER")) {
// Check if user is owner
if (!doc.getOwner().equals(caller)) {
throw new SecurityException("Not authorized");
}
} else {
throw new SecurityException("No role assigned");
}
}
}
OAuth 2.0 and JWT Authorization
// JWT-based authorization with Spring Security
import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.oauth2.server.resource.authentication.*;
@Configuration
public class JwtSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/api/**").authenticated()
.anyRequest().denyAll()
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
// Configure JWT decoder (RS256, etc.)
return NimbusJwtDecoder.withJwkSetUri(
"https://auth.example.com/.well-known/jwks.json"
).build();
}
private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(
new CustomJwtGrantedAuthoritiesConverter()
);
return converter;
}
}
// Extract authorities from JWT claims
public class CustomJwtGrantedAuthoritiesConverter
implements Converter<Jwt, Collection<GrantedAuthority>> {
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
Set<GrantedAuthority> authorities = new HashSet<>();
// Extract roles from 'roles' claim
List<String> roles = jwt.getClaimAsStringList("roles");
if (roles != null) {
roles.forEach(role ->
authorities.add(new SimpleGrantedAuthority("ROLE_" + role))
);
}
// Extract scopes from 'scope' claim
String scope = jwt.getClaimAsString("scope");
if (scope != null) {
Arrays.stream(scope.split(" "))
.forEach(s ->
authorities.add(new SimpleGrantedAuthority("SCOPE_" + s))
);
}
// Extract permissions from custom claim
List<String> permissions = jwt.getClaimAsStringList("permissions");
if (permissions != null) {
permissions.forEach(perm ->
authorities.add(new SimpleGrantedAuthority(perm))
);
}
return authorities;
}
}
// Scope-based authorization
@RestController
@RequestMapping("/api")
public class ApiController {
// Require specific OAuth scope
@PreAuthorize("hasAuthority('SCOPE_read:documents')")
@GetMapping("/documents")
public List<Document> getDocuments() {
return List.of();
}
@PreAuthorize("hasAuthority('SCOPE_write:documents')")
@PostMapping("/documents")
public Document createDocument(@RequestBody Document doc) {
return doc;
}
// Multiple scopes required
@PreAuthorize("hasAuthority('SCOPE_read:users') and hasAuthority('SCOPE_admin')")
@GetMapping("/users")
public List<User> getUsers() {
return List.of();
}
}
Policy-Based Authorization with OPA
// Open Policy Agent integration
import com.bisconti.opa.client.*;
@Service
public class OPAAuthorizationService {
private final OpaClient opaClient;
public OPAAuthorizationService() {
this.opaClient = OpaClient.builder()
.opaConfiguration("http://opa:8181")
.build();
}
public boolean isAuthorized(String user, String resource, String action) {
// Build input for OPA
Map<String, Object> input = Map.of(
"user", user,
"resource", resource,
"action", action
);
try {
// Query OPA policy
OpaQueryResult result = opaClient.queryForDocument(
input,
"authz/allow" // Policy path
);
return result.getResult() != null &&
(Boolean) result.getResult();
} catch (Exception e) {
// Fail closed: deny on error
return false;
}
}
public Map<String, Object> getPermissions(String user) {
Map<String, Object> input = Map.of("user", user);
try {
OpaQueryResult result = opaClient.queryForDocument(
input,
"authz/user_permissions"
);
return (Map<String, Object>) result.getResult();
} catch (Exception e) {
return Map.of();
}
}
}
// OPA Policy (Rego language)
// File: authz.rego
/*
package authz
import future.keywords.if
# Allow admins to do everything
allow if {
input.user.role == "admin"
}
# Allow users to read their own documents
allow if {
input.action == "read"
input.resource.type == "document"
input.resource.owner == input.user.id
}
# Allow managers to read documents in their department
allow if {
input.action == "read"
input.resource.type == "document"
input.user.role == "manager"
input.resource.department == input.user.department
}
# Get all permissions for a user
user_permissions := permissions if {
permissions := {
"canReadDocuments": can_read_documents,
"canWriteDocuments": can_write_documents,
"isAdmin": is_admin
}
}
can_read_documents if {
input.user.role in ["user", "manager", "admin"]
}
can_write_documents if {
input.user.role in ["manager", "admin"]
}
is_admin if {
input.user.role == "admin"
}
*/
Audit Logging for Authorization
// Authorization audit logging
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AuthorizationAuditAspect {
private static final Logger logger =
LoggerFactory.getLogger(AuthorizationAuditAspect.class);
// Log all @PreAuthorize checks
@Before("@annotation(org.springframework.security.access.prepost.PreAuthorize)")
public void logAuthorizationCheck(JoinPoint joinPoint) {
Authentication auth = SecurityContextHolder.getContext()
.getAuthentication();
String username = auth != null ? auth.getName() : "anonymous";
String method = joinPoint.getSignature().toShortString();
Object[] args = joinPoint.getArgs();
logger.info("Authorization check: user={}, method={}, args={}",
username, method, Arrays.toString(args));
}
// Log authorization failures
@AfterThrowing(
pointcut = "@annotation(org.springframework.security.access.prepost.PreAuthorize)",
throwing = "ex"
)
public void logAuthorizationFailure(JoinPoint joinPoint, Throwable ex) {
if (ex instanceof AccessDeniedException) {
Authentication auth = SecurityContextHolder.getContext()
.getAuthentication();
String username = auth != null ? auth.getName() : "anonymous";
String method = joinPoint.getSignature().toShortString();
logger.warn("Authorization DENIED: user={}, method={}, reason={}",
username, method, ex.getMessage());
}
}
}
// Custom authorization event listener
@Component
public class AuthorizationEventListener {
private static final Logger logger =
LoggerFactory.getLogger(AuthorizationEventListener.class);
@EventListener
public void onAuthorizationSuccess(AuthorizationSuccessEvent event) {
Authentication auth = event.getAuthentication();
Object resource = event.getSource();
logger.debug("Authorization SUCCESS: user={}, resource={}",
auth.getName(), resource);
}
@EventListener
public void onAuthorizationFailure(AuthorizationFailureEvent event) {
Authentication auth = event.getAuthentication();
AccessDeniedException exception = event.getAccessDeniedException();
logger.warn("Authorization FAILURE: user={}, reason={}",
auth.getName(), exception.getMessage());
// Could send alert, increment metrics, etc.
}
}
Testing Authorization
// Testing Spring Security authorization
import org.springframework.security.test.context.support.*;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest
@AutoConfigureMockMvc
public class AuthorizationTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(roles = "ADMIN")
public void adminCanDeleteDocument() throws Exception {
mockMvc.perform(delete("/api/documents/1"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
public void userCannotDeleteDocument() throws Exception {
mockMvc.perform(delete("/api/documents/1"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(username = "alice", authorities = {"DOCUMENT_READ"})
public void userWithPermissionCanRead() throws Exception {
mockMvc.perform(get("/api/documents/1"))
.andExpect(status().isOk());
}
@Test
@WithAnonymousUser
public void anonymousCannotAccessProtectedResource() throws Exception {
mockMvc.perform(get("/api/documents"))
.andExpect(status().isUnauthorized());
}
}
// Testing service-level authorization
@SpringBootTest
public class DocumentServiceTest {
@Autowired
private DocumentService documentService;
@Test
@WithMockUser(roles = "ADMIN")
public void adminCanDelete() {
assertDoesNotThrow(() ->
documentService.deleteDocument(1L)
);
}
@Test
@WithMockUser(roles = "USER")
public void userCannotDelete() {
assertThrows(AccessDeniedException.class, () ->
documentService.deleteDocument(1L)
);
}
}
Best Practices
- Framework choice: Use established frameworks (Spring Security, Jakarta Security).
- Consistent model: Choose one authorization model (RBAC, ABAC) and stick to it.
- Fail secure: Deny access by default, permit explicitly.
- Layer authorization: Enforce at multiple layers (URL, method, data).
- Audit all decisions: Log authorization checks and failures.
- Test thoroughly: Test all permission combinations.
- Centralize policies: Keep authorization logic in one place.
- Performance: Cache authorization decisions when appropriate.
- Token claims: Use JWT claims for stateless authorization.
- Regular reviews: Audit roles and permissions periodically.