Understanding the Difference
Authentication (AuthN) and Authorization (AuthZ) are often confused but serve completely different purposes. Understanding this distinction is fundamental to building secure applications.
Simple Analogy: Authentication is showing your ID at the airport (proving who you are). Authorization is your boarding pass determining which flight you can board (what you're allowed to do).
The Core Distinction
// Authentication: "WHO are you?"
// - Verifies identity
// - Happens FIRST
// - Results in: User identity (principal)
// Authorization: "WHAT can you do?"
// - Verifies permissions
// - Happens AFTER authentication
// - Results in: Access granted or denied
// Visual flow:
┌─────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ Request │ ───▶ │ Authentication │ ───▶ │ Authorization │
│ arrives │ │ "Who is this?" │ │ "Can they do X?" │
└─────────────┘ └─────────────────┘ └──────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ 401 Unauthorized│ │ 403 Forbidden │
│ (Not logged in) │ │ (No permission) │
└─────────────────┘ └──────────────────┘
HTTP Status Codes
| Aspect | Authentication | Authorization |
|---|---|---|
| Question | Who are you? | What can you do? |
| Verifies | Identity | Permissions |
| Failure Code | 401 Unauthorized | 403 Forbidden |
| Examples | Login, Password, Biometrics | Roles, Permissions, ACLs |
| Occurs | First | After authentication |
Authentication Methods
1. Knowledge-Based (Something You Know)
// Password-based authentication
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authManager;
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(
@RequestBody LoginRequest request) {
// Create authentication token from credentials
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
);
// Attempt authentication
Authentication auth = authManager.authenticate(authToken);
// If we get here, authentication succeeded
String token = jwtService.generateToken(auth);
return ResponseEntity.ok(
new AuthResponse(token)
);
}
}
// Custom UserDetailsService implementation
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(
"User not found: " + username
));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(), // Hashed password
user.isEnabled(),
true, true, true, // Account status flags
getAuthorities(user.getRoles())
);
}
}
2. Possession-Based (Something You Have)
// Two-Factor Authentication (2FA) with TOTP
@Service
public class TwoFactorAuthService {
private final GoogleAuthenticator gAuth = new GoogleAuthenticator();
// Generate secret for user (shown as QR code)
public String generateSecret() {
GoogleAuthenticatorKey key = gAuth.createCredentials();
return key.getKey(); // Store this securely per user
}
// Generate QR code URL for authenticator apps
public String getQRCodeUrl(String username, String secret) {
return GoogleAuthenticatorQRGenerator.getOtpAuthURL(
"MyApp",
username,
new GoogleAuthenticatorKey.Builder(secret).build()
);
}
// Verify the 6-digit code from user's authenticator app
public boolean verifyCode(String secret, int code) {
return gAuth.authorize(secret, code);
}
}
// Enhanced login with 2FA
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
// Step 1: Verify password
Authentication auth = authManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
User user = userService.findByUsername(request.getUsername());
// Step 2: Check if 2FA is enabled
if (user.isTwoFactorEnabled()) {
if (request.getTotpCode() == null) {
return ResponseEntity.ok(
new TwoFactorRequiredResponse()
);
}
// Verify TOTP code
if (!twoFactorService.verifyCode(
user.getTotpSecret(),
request.getTotpCode())) {
throw new BadCredentialsException("Invalid 2FA code");
}
}
// Generate JWT token
String token = jwtService.generateToken(auth);
return ResponseEntity.ok(new AuthResponse(token));
}
3. Inherence-Based (Something You Are)
// Biometric authentication concepts
// Typically handled on client side (mobile/desktop)
// Server receives a signed assertion from the device
@PostMapping("/biometric-auth")
public ResponseEntity<AuthResponse> biometricAuth(
@RequestBody BiometricAuthRequest request) {
// The device verified biometrics locally
// Server verifies the cryptographic signature
boolean valid = webAuthnService.verifyAssertion(
request.getUserId(),
request.getAuthenticatorData(),
request.getSignature(),
request.getClientDataJson()
);
if (!valid) {
throw new AuthenticationException("Biometric verification failed");
}
User user = userService.findById(request.getUserId());
String token = jwtService.generateToken(user);
return ResponseEntity.ok(new AuthResponse(token));
}
Authentication Factors Comparison
| Factor Type | Examples | Pros | Cons |
|---|---|---|---|
| Knowledge | Passwords, PINs, Security questions | Easy to implement, No hardware needed | Can be stolen, forgotten, guessed |
| Possession | Phone (SMS/TOTP), Hardware tokens, Smart cards | Hard to steal remotely | Can be lost, SIM swapping attacks |
| Inherence | Fingerprint, Face ID, Voice, Retina | Can't be forgotten or lost | Privacy concerns, can't be changed if compromised |
Authorization Models
1. Role-Based Access Control (RBAC)
// Simple RBAC - Users have roles, roles have permissions
@Entity
public class User {
@Id
private Long id;
private String username;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
}
@Entity
public class Role {
@Id
private Long id;
private String name; // ROLE_ADMIN, ROLE_USER, ROLE_MANAGER
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id")
)
private Set<Permission> permissions = new HashSet<>();
}
@Entity
public class Permission {
@Id
private Long id;
private String name; // READ_USERS, WRITE_USERS, DELETE_USERS
}
// Spring Security role-based authorization
@RestController
@RequestMapping("/api/users")
public class UserController {
// Only admins can access
@PreAuthorize("hasRole('ADMIN')")
@GetMapping
public List<User> getAllUsers() {
return userService.findAll();
}
// Admin or Manager can access
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
// Only Admin can delete
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
}
2. Permission-Based Access Control
// Fine-grained permission checking
@RestController
@RequestMapping("/api/documents")
public class DocumentController {
// Check specific permission
@PreAuthorize("hasAuthority('DOCUMENT_READ')")
@GetMapping
public List<Document> getDocuments() {
return documentService.findAll();
}
// Multiple permissions (OR logic)
@PreAuthorize("hasAnyAuthority('DOCUMENT_WRITE', 'DOCUMENT_ADMIN')")
@PostMapping
public Document createDocument(@RequestBody Document doc) {
return documentService.create(doc);
}
// Complex permission logic with SpEL
@PreAuthorize("hasAuthority('DOCUMENT_DELETE') and #doc.owner == authentication.name")
@DeleteMapping("/{id}")
public void deleteDocument(@PathVariable Long id) {
documentService.delete(id);
}
}
// Custom permission evaluator for complex rules
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(
Authentication auth,
Object targetDomainObject,
Object permission) {
if (targetDomainObject instanceof Document doc) {
String perm = (String) permission;
String username = auth.getName();
return switch (perm) {
case "READ" -> doc.isPublic() ||
doc.getOwner().equals(username) ||
doc.getSharedWith().contains(username);
case "WRITE" -> doc.getOwner().equals(username);
case "DELETE" -> doc.getOwner().equals(username);
default -> false;
};
}
return false;
}
@Override
public boolean hasPermission(
Authentication auth,
Serializable targetId,
String targetType,
Object permission) {
// Load object by ID and check permission
return false;
}
}
// Using the custom evaluator
@PreAuthorize("hasPermission(#document, 'WRITE')")
public void updateDocument(Document document) {
// Only called if permission check passes
}
3. Attribute-Based Access Control (ABAC)
// ABAC considers multiple attributes for authorization
// - Subject attributes (user department, clearance level)
// - Resource attributes (document classification, owner)
// - Environment attributes (time, location, device)
// - Action attributes (read, write, delete)
@Service
public class AbacService {
public boolean canAccess(
User user,
Document document,
String action,
AccessContext context) {
// Policy: Users can read documents from their department
// during business hours from trusted locations
// Check subject attributes
boolean sameDepartment = user.getDepartment()
.equals(document.getDepartment());
// Check clearance level
boolean hasClearance = user.getClearanceLevel() >=
document.getClassificationLevel();
// Check environment attributes
boolean isBusinessHours = isWithinBusinessHours(context.getTime());
boolean isTrustedLocation = trustedIps.contains(context.getIpAddress());
// Combine policies
if ("READ".equals(action)) {
return sameDepartment && hasClearance;
}
if ("WRITE".equals(action)) {
return sameDepartment && hasClearance &&
isBusinessHours && isTrustedLocation;
}
if ("DELETE".equals(action)) {
return document.getOwner().equals(user.getId()) &&
user.getClearanceLevel() >= 4;
}
return false;
}
}
Authorization Models Comparison
| Model | Best For | Complexity | Flexibility |
|---|---|---|---|
| RBAC | Most applications, clear role hierarchies | Low | Medium |
| Permission-Based | Fine-grained control needed | Medium | High |
| ABAC | Complex policies, context-aware decisions | High | Very High |
| ACL | Per-object permissions (files, documents) | Medium | High |
Spring Security Configuration
Complete Security Configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // Enables @PreAuthorize, @PostAuthorize
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtFilter;
@Autowired
private CustomUserDetailsService userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// Disable CSRF for stateless API
.csrf(csrf -> csrf.disable())
// Configure session management
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// Configure authorization rules
.authorizeHttpRequests(auth -> auth
// Public endpoints
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
// Role-based rules
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/manager/**").hasAnyRole("ADMIN", "MANAGER")
// Permission-based rules
.requestMatchers(HttpMethod.DELETE, "/api/**")
.hasAuthority("DELETE_PRIVILEGE")
// All other requests require authentication
.anyRequest().authenticated()
)
// Add JWT filter before UsernamePasswordAuthenticationFilter
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
// Exception handling
.exceptionHandling(ex -> ex
.authenticationEntryPoint((req, res, e) -> {
res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
res.getWriter().write("Unauthorized: " + e.getMessage());
})
.accessDeniedHandler((req, res, e) -> {
res.setStatus(HttpServletResponse.SC_FORBIDDEN);
res.getWriter().write("Access Denied: " + e.getMessage());
})
)
.build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Method-Level Security
@Service
public class OrderService {
// Only authenticated users can access
@PreAuthorize("isAuthenticated()")
public List<Order> getMyOrders() {
String username = SecurityContextHolder.getContext()
.getAuthentication().getName();
return orderRepository.findByUsername(username);
}
// Check parameter against authenticated user
@PreAuthorize("#username == authentication.name or hasRole('ADMIN')")
public List<Order> getOrdersByUsername(String username) {
return orderRepository.findByUsername(username);
}
// Post-authorize: filter return value
@PostAuthorize("returnObject.owner == authentication.name")
public Order getOrder(Long id) {
return orderRepository.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id));
}
// Filter collections
@PostFilter("filterObject.owner == authentication.name or hasRole('ADMIN')")
public List<Order> getAllOrders() {
return orderRepository.findAll();
}
// Combine with custom permission evaluator
@PreAuthorize("hasPermission(#order, 'CANCEL')")
public void cancelOrder(Order order) {
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}
}
Common Security Patterns
Principal Hierarchy Pattern
// Role hierarchy: ADMIN > MANAGER > USER
@Bean
public RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.withDefaultRolePrefix()
.role("ADMIN").implies("MANAGER")
.role("MANAGER").implies("USER")
.build();
}
// Now ADMIN automatically has MANAGER and USER permissions
Resource Owner Pattern
// Users can only access their own resources
@GetMapping("/api/users/{userId}/orders")
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
public List<Order> getUserOrders(@PathVariable Long userId) {
return orderService.findByUserId(userId);
}
// Alternative: Verify in service layer
@Service
public class OrderService {
public Order getOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new NotFoundException());
String currentUser = SecurityContextHolder.getContext()
.getAuthentication().getName();
if (!order.getOwner().equals(currentUser)) {
throw new AccessDeniedException("Not your order");
}
return order;
}
}
Best Practices
Authentication Best Practices
- Use MFA: Always offer multi-factor authentication for sensitive operations
- Secure password storage: Use bcrypt, Argon2, or PBKDF2 with high work factors
- Implement rate limiting: Prevent brute force attacks on login endpoints
- Account lockout: Temporarily lock accounts after failed attempts
- Secure session tokens: Use cryptographically strong random tokens
- Audit authentication events: Log successful and failed login attempts
Authorization Best Practices
- Principle of Least Privilege: Grant minimum permissions needed
- Default deny: Require explicit permission grants
- Defense in depth: Check authorization at multiple layers
- Avoid direct object references: Use indirect references or verify ownership
- Log authorization failures: Detect potential attacks
- Regular permission audits: Review and clean up unused permissions
Common Mistakes to Avoid
- Client-side authorization only: Always verify on the server
- Hardcoding credentials: Use environment variables or secrets management
- Security through obscurity: Don't rely on hidden URLs for protection
- Missing authorization checks: Every endpoint needs verification
- Verbose error messages: Don't reveal whether username or password was wrong
Summary
- Authentication verifies identity (WHO you are)
- Authorization verifies permissions (WHAT you can do)
- 401 Unauthorized = authentication failure
- 403 Forbidden = authorization failure
- RBAC is suitable for most applications
- ABAC provides maximum flexibility for complex policies
- Always implement both at the server layer
- Follow the principle of least privilege