Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • About
  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

👥10,050visitors

Built for engineers who want to go deep.


← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
Chaturmind
← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
HomeLearnSystem DesignLow-Level Design & Design PatternsMachine Coding Exercises
✓ FreeAdvanced· 8 min read

Design an Authentication/Authorization Module

AuthProvider/OAuthProvider/LocalAuthProvider/TokenService via Strategy so multiple auth providers coexist, and session vs token design choices mapped directly onto Spring Security's own internals.

Published September 23, 2026


Design an Authentication/Authorization Module

Core classes

interface AuthProvider {
    AuthResult authenticate(Credentials credentials);
}

class LocalAuthProvider implements AuthProvider {
    public AuthResult authenticate(Credentials credentials) {
        User user = userRepository.findByEmail(credentials.getEmail());
        if (user == null || !passwordEncoder.matches(credentials.getPassword(), user.getPasswordHash())) {
            return AuthResult.failure("Invalid credentials");
        }
        return AuthResult.success(user);
    }
}

class OAuthProvider implements AuthProvider {
    public AuthResult authenticate(Credentials credentials) {
        // exchanges an OAuth authorization code / token with the external provider, then resolves/creates a local User
        OAuthTokenResponse tokens = oauthClient.exchangeCode(credentials.getAuthCode());
        User user = findOrCreateUserFromOAuthProfile(tokens);
        return AuthResult.success(user);
    }
}

Advertisement

Strategy: multiple auth providers coexisting

class AuthenticationService {
    private final Map<AuthMethod, AuthProvider> providers; // email/password, Google OAuth, GitHub OAuth, etc.

    AuthResult login(AuthMethod method, Credentials credentials) {
        AuthProvider provider = providers.get(method);
        return provider.authenticate(credentials);
    }
}

This is the same Strategy-per-implementation shape as Spring Security's own ProviderManager delegating to multiple AuthenticationProviders (see Authentication Mechanics) — an application supporting both local password login and "Sign in with Google" doesn't branch internally on which method was used; it looks up the right AuthProvider and delegates, exactly matching the real framework's own architecture.

TokenService: issuing and validating tokens

class TokenService {
    String issueToken(User user) {
        return jwtBuilder.subject(user.getId()).claim("roles", user.getRoles()).expiry(Duration.ofMinutes(15)).sign();
    }
    Optional<User> validateToken(String token) {
        if (!jwtVerifier.isValid(token)) return Optional.empty();
        return userRepository.findById(jwtVerifier.getSubject(token));
    }
}

Session vs token design choices, mapped to Spring Security internals

This is the direct connection point to production Spring Security, worth stating explicitly:

  • A session-based design would have AuthenticationService.login() create a server-side session (analogous to what SecurityContextHolder's default ThreadLocal-backed context does per-request once populated by the filter chain — see Authentication Mechanics), with a session ID cookie referencing it.
  • A token-based design (as TokenService above implements) matches JWT-Based Stateless Auth's custom OncePerRequestFilter directly — validateToken() here is functionally the same operation that filter performs on every incoming request to populate SecurityContextHolder.

Recognizing that this LLD exercise's AuthProvider/TokenService classes are a simplified re-derivation of what Spring Security's AuthenticationProvider/JWT filter machinery already does is exactly the kind of LLD-to-framework connection this course has built toward — the goal isn't reinventing Spring Security, it's understanding it well enough to explain why it's built the way it is.

Authorization: permissions, roles and resource ownership

Authentication answers "who are you?". Authorization answers "may you do this, to this thing?". Most real rules have two parts: a permission check ("can edit invoices"), and an ownership or scope check ("…but only invoices in your own organization"). Designing authorization as its own component keeps those rules out of controllers and services:

enum Permission { INVOICE_READ, INVOICE_EDIT, INVOICE_APPROVE, USER_ADMIN }

record Role(String name, Set<Permission> permissions) {}

record Principal(String userId, String orgId, Set<Role> roles) {
    boolean has(Permission p) { return roles.stream().anyMatch(r -> r.permissions().contains(p)); }
}

interface Policy<R> {
    boolean allows(Principal who, Permission action, R resource);
}

class InvoicePolicy implements Policy<Invoice> {
    public boolean allows(Principal who, Permission action, Invoice inv) {
        if (!who.has(action)) return false;                                  // RBAC: the role grants the action
        if (!inv.orgId().equals(who.orgId())) return false;                  // tenancy: same organization only
        if (action == Permission.INVOICE_APPROVE && inv.createdBy().equals(who.userId()))
            return false;                                                    // separation of duties
        return true;
    }
}

class AuthorizationService {
    private final Map<Class<?>, Policy<?>> policies;

    AuthorizationService(Map<Class<?>, Policy<?>> policies) { this.policies = policies; }

    @SuppressWarnings("unchecked")
    <R> void check(Principal who, Permission action, R resource) {
        Policy<R> policy = (Policy<R>) policies.get(resource.getClass());
        if (policy == null || !policy.allows(who, action, resource))         // deny by default
            throw new AccessDeniedException(who.userId() + " may not " + action);
    }
}

Design principles embedded here:

  • Grant permissions to roles, not directly to users. Roles are what administrators reason about. Code checks permissions, so renaming or reshaping roles never requires code changes.
  • Deny by default. No policy registered for a resource type means no access, not full access.
  • Ownership and tenancy checks belong in the policy, next to the permission check. Forgetting them is how "insecure direct object reference" bugs happen: any logged-in user fetches /invoices/123 from another company.
  • Business rules like "you can't approve your own invoice" (separation of duties) live in one testable place.

In Spring Security, the same structure maps to @PreAuthorize("hasAuthority('INVOICE_EDIT') and @invoicePolicy.sameOrg(principal, #id)"), or to a custom PermissionEvaluator.

Password storage and login hardening

The LocalAuthProvider verifies passwords. How they're stored and how login fails matter as much as the happy path:

class LocalAuthProvider implements AuthProvider {
    private final UserRepository users;
    private final PasswordEncoder encoder;          // BCrypt/Argon2: salted, deliberately slow
    private final LoginAttemptTracker attempts;
    private final String dummyHash;                 // precomputed hash of a random value

    public AuthResult authenticate(Credentials c) {
        if (attempts.isLocked(c.username())) return AuthResult.failed();   // same response as a wrong password
        Optional<User> user = users.findByUsername(c.username());
        String hash = user.map(User::passwordHash).orElse(dummyHash);      // always spend the hashing time
        boolean ok = encoder.matches(c.password(), hash) && user.isPresent();
        if (!ok) { attempts.recordFailure(c.username()); return AuthResult.failed(); }
        attempts.reset(c.username());
        return AuthResult.success(user.get());
    }
}
  • Never store passwords in plain text or with fast hashes (MD5, SHA-256). Use an adaptive, salted algorithm (bcrypt, scrypt, Argon2) so stolen hashes are expensive to crack.
  • One generic failure message ("invalid username or password"), and roughly constant timing (hashing a dummy value for unknown users), so attackers can't discover which usernames exist.
  • Throttle attempts per account and per IP with progressive delays or temporary locks, and let users know about suspicious activity, rather than permanently locking accounts. Permanent locks let attackers lock out real users on purpose.

Refresh tokens and revocation

Short-lived access tokens (such as 15 minutes) limit the damage from a leaked token, but users shouldn't have to log in every 15 minutes. Pair them with a refresh token: long-lived, stored server-side (or as a hash), and exchanged for a new access token.

  • Rotate refresh tokens on every use. If an old one is presented again, that indicates theft, so revoke the whole token family.
  • Logout and "sign out everywhere" delete the stored refresh tokens. Access tokens expire on their own within minutes.
  • For immediate revocation of access tokens (a fired employee, a compromised account), keep a small deny-list of token IDs (jti) until their expiry, or check a per-user "tokens valid after" timestamp.

Follow-up questions this topic invites — and their answers

Q: Why does OAuthProvider need to 'find or create' a local User, rather than just trusting the OAuth provider's identity directly? A: Most applications need their own User record (for authorization roles, application-specific data, foreign keys from other tables) regardless of how the user originally authenticated — OAuth establishes IDENTITY, but the application still needs its own representation of that identity to attach permissions and data to, which is exactly why find-or-create-on-first-login is the standard pattern.

Q: How would this design support a user who's authenticated via OAuth wanting to ALSO set a local password later? A: This argues for User supporting multiple linked auth methods (a separate AuthMethod-to-User mapping table, rather than assuming exactly one auth method per user) — a real product decision worth surfacing explicitly rather than assuming the simpler one-auth-method-per-user model always holds.

Q: Should password verification (passwordEncoder.matches()) happen inside LocalAuthProvider or a separate service? A: Keeping it inside LocalAuthProvider (as shown) is reasonable since password matching is specific to that one auth method — a shared AuthenticationService wouldn't have anywhere else meaningful to put OAuth-specific token exchange logic either, so each provider owning its own method-specific verification logic keeps the Strategy separation clean.

Q: Why 15 minutes for token expiry in this example specifically? A: An arbitrary but realistic short-lived access-token duration matching JWT-Based Stateless Auth's own 'short-lived access token, longer-lived refresh token' pattern — worth stating that the SPECIFIC number is a tunable business/security tradeoff (shorter = more secure against token theft, more refresh overhead; longer = the reverse), not a fixed correct value.

Q: What's the difference between RBAC and checking resource ownership? A: RBAC decides whether a user's roles grant an action type in general ("can edit invoices"). Ownership or tenancy checks decide whether that action is allowed on a specific resource ("this invoice belongs to your organization"). Secure systems need both, since RBAC alone lets users act on other people's data.

Q: Why should login failures return the same message whether the username or password was wrong? A: Different messages, or noticeably different response times, reveal which usernames exist, which helps attackers target accounts. A single generic error, and hashing a dummy value for unknown users, removes both signals.

Q: Why rotate refresh tokens? A: With rotation, each refresh token can be used once. If a stolen token is used after the real client has already rotated it (or the reverse), the server sees a reused token, detects the theft, and can revoke the whole session family.

Advertisement

Previous

Multi-Pattern Problem: Design a Food Ordering Class Model

Next

Design a Form/Survey Builder

AI Tutor

Lesson: Design an Authentication/Authorization Module

Quick actions

AI responses can be inaccurate. Verify critical information.