Authentication vs Authorization

The two fundamental pillars of application security

← Back to Index

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