Back to Blog
Lesson 33 of the System Design: System Design Fundamentals course
ArchitectureAugust 19, 20264 min read

Authentication and Authorization: Secure System Identity Patterns

Master the essentials of authentication and authorization. Learn to implement JWT-based auth, enforce role-based access control, and secure service communication.

authenticationauthorizationsecurityjwtsystem-design
Close-up view of a mouse cursor over digital security text on display.

Previously in this course, we discussed securing communication with HTTPS/TLS to ensure data in transit is encrypted. While TLS protects the pipe, it doesn't verify who is on the other end. That is the job of authentication and authorization.

Authentication (AuthN) proves a user is who they claim to be, while Authorization (AuthZ) defines what that user is allowed to do. In modern distributed systems, we often handle these concerns using stateless tokens to maintain scalability.

Implementing JWT-Based Authentication

JSON Web Tokens (JWT) are the standard for stateless authentication. Instead of storing session data in a database and checking it on every request, we encode user identity and claims into a cryptographically signed token.

A JWT consists of three parts: a Header (algorithm), a Payload (user info), and a Signature (the verification). Because the server signs the token with a secret key, we can verify its authenticity without a database lookup.

Here is a simplified implementation of a JWT creation flow in Node.js:

JAVASCRIPT
const jwt = require(CE9178">'jsonwebtoken');

// Never hardcode this in production; use a secure vault!
const SECRET_KEY = process.env.JWT_SECRET;

function generateToken(user) {
  const payload = {
    sub: user.id, // Subject (user ID)
    role: user.role, // Claim for RBAC
    iat: Date.now()
  };
  return jwt.sign(payload, SECRET_KEY, { expiresIn: CE9178">'1h' });
}

When the client sends this token in the Authorization: Bearer <token> header, your API middleware validates it:

JAVASCRIPT
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(CE9178">' ')[1];
  if (!token) return res.status(401).send(CE9178">'Access Denied');

  try {
    const verified = jwt.verify(token, SECRET_KEY);
    req.user = verified;
    next();
  } catch (err) {
    res.status(400).send(CE9178">'Invalid Token');
  }
}

Defining Role-Based Access Control (RBAC)

Once you have identified the user, you must verify their permissions. Role-based access control (RBAC) simplifies this by assigning permissions to roles (e.g., admin, editor, viewer) rather than individual users.

In our JWT payload above, we included a role field. We can now write a simple wrapper to gate access to specific endpoints:

JAVASCRIPT
const authorize = (requiredRole) => {
  return (req, res, next) => {
    if (req.user.role !== requiredRole) {
      return res.status(403).send(CE9178">'Forbidden: Insufficient Permissions');
    }
    next();
  };
};

// Usage
app.delete(CE9178">'/api/posts/:id', authenticate, authorize(CE9178">'admin'), (req, res) => {
  // Only admins reach this
});

Securing Service-to-Service Calls

In a microservices architecture, you often have internal services calling each other. You shouldn't pass the end-user's JWT directly if the internal service needs to perform actions on its own behalf.

Instead, use Service Tokens. Each service acts as a client with its own identity. You can secure these calls using:

  1. Shared Secrets: Using a mutual TLS (mTLS) handshake (see our previous lesson) to verify the identity of the calling service.
  2. Internal Auth Tokens: Issuing short-lived tokens specifically for machine-to-machine communication, signed by an internal Certificate Authority (CA).

Hands-on Exercise

Modify your design document for our project. Add an "Identity & Access" section:

  1. Define the claims you will store in your JWT.
  2. List the roles your system requires (e.g., Guest, RegisteredUser, Admin).
  3. Sketch a sequence diagram showing how a service validates a request from the frontend versus an internal request from another microservice.

Common Pitfalls

  • Storing Sensitive Data in JWTs: JWTs are Base64 encoded, not encrypted. Anyone can decode them. Never put passwords or PII in the payload.
  • Infinite Token Lifetimes: Always set an exp (expiration) claim. If a token is stolen, a short expiration limits the damage.
  • Lack of Revocation: Because JWTs are stateless, you cannot easily "log out" a user before the token expires. If this is a requirement, you will need a "blacklist" or a caching layer like Redis to check if a token ID (jti) has been revoked.
  • Over-complicating Auth: For beginners, start with standard JWTs and simple RBAC before jumping into complex OAuth2 flows unless your specific use case requires hardened flows.

FAQ

Q: Should I use cookies or local storage for JWTs? A: This is a classic debate. Cookies (specifically HttpOnly and Secure) are generally safer against XSS attacks, but require careful CSRF protection. See this guide on secure token storage for a deeper breakdown.

Q: Can I use JWTs for service-to-service auth? A: Yes, but ensure the "issuer" and "audience" claims are strictly enforced so a user-generated token cannot be used to impersonate a service.

Recap

Authentication and authorization are the bedrock of system security. We’ve established how to use JWTs for stateless identity, implemented RBAC for granular access control, and outlined the need for distinct service-to-service authentication. Keeping these layers separate and using standard protocols ensures your architecture remains scalable and secure.

Up next: Data Sanitization and Validation

Similar Posts