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.