Skip to content

Security

Authentication mechanisms

  • Session-cookie — server stores session; cookie holds session id.
  • JWT (JSON Web Token) — stateless, signed token (header.payload.signature); self-contained claims.
  • OAuth2 / OIDC — delegated authorization; access + refresh tokens; OIDC adds identity (ID token).
  • API keys, mTLS (mutual TLS), HMAC request signing (shared-secret signature per request).

Authentication vs Authorization

  • AuthN — verify identity. AuthZ — verify permissions (RBAC roles, scopes, ABAC).

Hashing vs encryption

  • Hashing — one-way, irreversible (passwords: bcrypt/argon2 + salt; integrity: SHA-256).
  • Encryption — two-way with a key (confidentiality). Symmetric (AES) vs asymmetric (RSA/ECC).
  • HMAC — keyed hash for message authenticity + integrity (symmetric secret).

OWASP Top 10 (know these)

Broken access control · cryptographic failures · injection (SQL/command) · insecure design · security misconfiguration · vulnerable/outdated components · auth failures · data integrity failures · logging/monitoring failures · SSRF.

Defenses

  • Parameterized queries (prevent SQLi); input validation/output encoding (XSS); CSRF tokens.
  • Least privilege; secrets in a vault (never in code); rotate credentials.
  • TLS everywhere; mask/redact PII in logs; dependency scanning (CVE) + patching.
  • Constant-time comparison for secrets/signatures; rate limiting.

Spring Security

Core Components

SecurityFilterChain bean; method security (@PreAuthorize, @Secured); OAuth2 resource server; BCryptPasswordEncoder for passwords.

Role-Based Login & Authorization

1. User Service with Roles

@Service
public class CustomUserDetailsService implements UserDetailsService {
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        // Convert user roles to Spring Security authorities
        List<GrantedAuthority> authorities = user.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
            .collect(Collectors.toList());

        return new org.springframework.security.core.userdetails.User(
            user.getUsername(),
            user.getPassword(),  // BCrypt hashed
            authorities
        );
    }
}

2. Login Controller

@RestController
@RequestMapping("/auth")
public class LoginController {
    private AuthenticationManager authenticationManager;
    private JwtTokenProvider tokenProvider;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
        try {
            // Authenticate with username + password
            Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    loginRequest.getUsername(),
                    loginRequest.getPassword()
                )
            );

            // Get authenticated user with roles
            UserDetails userDetails = (UserDetails) authentication.getPrincipal();
            String token = tokenProvider.generateToken(userDetails.getUsername(), 
                userDetails.getAuthorities());

            return ResponseEntity.ok(new LoginResponse(
                token,
                userDetails.getUsername(),
                userDetails.getAuthorities().stream()
                    .map(GrantedAuthority::getAuthority)
                    .collect(Collectors.toList())
            ));
        } catch (BadCredentialsException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body("Invalid username or password");
        }
    }
}

3. Security Configuration (Role-Based Access)

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)  // Enable @PreAuthorize
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                // Public endpoints
                .antMatchers("/auth/**", "/public/**").permitAll()

                // Admin only
                .antMatchers("/admin/**").hasRole("ADMIN")

                // User or higher
                .antMatchers("/api/users/**").hasAnyRole("USER", "ADMIN")

                // Any authenticated user
                .anyRequest().authenticated()
                .and()
            .httpBasic()
            .and()
            .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

4. Method-Level Security (Fine-Grained Access)

@RestController
@RequestMapping("/api")
public class UserController {

    // Only ADMIN role
    @GetMapping("/admin/users")
    @PreAuthorize("hasRole('ADMIN')")
    public List<User> getAllUsers() {
        return userService.findAll();
    }

    // ADMIN or MANAGER role
    @DeleteMapping("/users/{id}")
    @PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build();
    }

    // User can only modify their own profile
    @PutMapping("/users/{id}")
    @PreAuthorize("hasRole('USER') and #id == authentication.principal.id")
    public ResponseEntity<User> updateUser(
        @PathVariable Long id,
        @RequestBody UserUpdateRequest req) {
        return ResponseEntity.ok(userService.update(id, req));
    }

    // Custom permission check (using SpEL)
    @PostMapping("/posts")
    @PreAuthorize("hasRole('USER') and isPostingEnabled(authentication.principal.username)")
    public ResponseEntity<Post> createPost(@RequestBody CreatePostRequest req) {
        return ResponseEntity.created(URI.create("/posts/new"))
            .body(postService.create(req));
    }
}

5. Custom Permission Evaluator

@Component
public class CustomPermissionEvaluator extends DefaultPermissionEvaluator {
    private UserService userService;

    @Override
    public boolean hasPermission(
        Authentication authentication,
        Object targetDomainObject,
        Object permission) {

        if (targetDomainObject == null) return false;

        String username = authentication.getName();
        String permissionName = permission.toString().toUpperCase();

        if (targetDomainObject instanceof Post) {
            Post post = (Post) targetDomainObject;
            // Allow author to edit, admins to do anything
            if ("EDIT".equals(permissionName)) {
                return isAdmin(username) || isAuthor(username, post);
            }
        }
        return false;
    }

    private boolean isAdmin(String username) {
        User user = userService.findByUsername(username);
        return user.getRoles().stream()
            .anyMatch(role -> "ADMIN".equals(role.getName()));
    }

    private boolean isAuthor(String username, Post post) {
        return post.getAuthor().getUsername().equals(username);
    }
}

// Usage in controller
@PutMapping("/posts/{id}")
@PreAuthorize("hasPermission(#post, 'EDIT')")
public ResponseEntity<Post> editPost(
    @PathVariable Long id,
    Post post) {  // @EntityGraph injected by Spring Data
    return ResponseEntity.ok(postService.update(post));
}

6. Role-Based Response Filtering

@Component
public class ResponseFilter {

    @Transactional
    public List<UserDTO> filterUsersForRole(List<User> users, Collection<GrantedAuthority> authorities) {
        boolean isAdmin = authorities.stream()
            .anyMatch(a -> "ROLE_ADMIN".equals(a.getAuthority()));

        return users.stream()
            .map(user -> UserDTO.builder()
                .id(user.getId())
                .username(user.getUsername())
                .email(user.getEmail())
                .roles(isAdmin ? user.getRoles() : emptyList())  // Hide roles from non-admins
                .createdAt(isAdmin ? user.getCreatedAt() : null)  // Hide audit fields
                .build())
            .collect(Collectors.toList());
    }
}

7. Access Denied Handling

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.setContentType("application/json");

        ErrorResponse errorResponse = ErrorResponse.builder()
            .timestamp(LocalDateTime.now())
            .status(HttpStatus.FORBIDDEN.value())
            .error("Access Denied")
            .message("You don't have permission to access this resource")
            .path(request.getRequestURI())
            .build();

        response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
    }
}

// Register in Security Config
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .exceptionHandling()
            .accessDeniedHandler(new CustomAccessDeniedHandler())
            .and()
        // ... rest of config
    return http.build();
}

8. Audit Logging

@Component
@Aspect
public class SecurityAuditLog {
    private AuditEventRepository auditRepository;

    @Around("@annotation(com.example.security.Auditable)")
    public Object auditAccess(ProceedingJoinPoint joinPoint) throws Throwable {
        String username = SecurityContextHolder.getContext()
            .getAuthentication().getName();
        String operation = joinPoint.getSignature().getName();

        auditRepository.save(AuditEvent.builder()
            .username(username)
            .operation(operation)
            .timestamp(LocalDateTime.now())
            .build());

        return joinPoint.proceed();
    }
}

// Usage
@DeleteMapping("/users/{id}")
@PreAuthorize("hasRole('ADMIN')")
@Auditable
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
    // ...
}