Authentication and Authorization in Backend Systems: Understanding the Flow

The Confusion

Authentication and authorization are both security concepts. They both protect your application. They both appear in the same code. They're often implemented together. So most developers treat them as the same thing.

They're not.

This confusion is not just semantic. It has real consequences. I've seen systems where:

  • Login endpoints check user roles before verifying passwords
  • Protected routes trust client-side permission checks
  • Error messages leak whether a username exists or the password was wrong
  • Tokens contain sensitive data because "it's encrypted" (it's not)
  • Authorization logic is scattered across authentication code, making both fragile

The root cause is always the same: authentication and authorization were never clearly separated in the developer's mental model.

This article exists to fix that. I will show you what each concept actually does, how they differ, how they work together, and how to implement them without mixing concerns. By the end, you'll understand the complete flow from login to resource access, and you'll recognize the security pitfalls before they become vulnerabilities.

Terminology and Conventions

I use these terms consistently throughout:

  • Authentication (AuthN): The process of verifying identity. Who are you?
  • Authorization (AuthZ): The process of verifying permissions. What can you do?
  • Credential: Information used to prove identity (username/password, API key, token)
  • Token: A signed data structure containing user identity and metadata
  • JWT (JSON Web Token): A specific token format containing header, payload, and signature
  • Guard: A middleware component that protects routes
  • Principal: The authenticated entity (usually a user)
  • Role: A named collection of permissions (e.g., "admin", "user")
  • Permission: A specific action on a resource (e.g., "read:posts", "delete:users")

Visual Convention:

flowchart LR
    A[Client] -->|Request| B[Backend]
    B -->|Response| A
    
    style A fill:#2d2d2d,stroke:#4a9eff
    style B fill:#2d2d2d,stroke:#4a9eff

1. Authentication vs Authorization: The Core Distinction

The confusion starts here. Authentication and authorization sound similar. They both protect resources. They often happen together. But they solve different problems.

Authentication: Who Are You?

Authentication answers one question: Are you who you claim to be?

Example 1: Airport Security

  • You show your passport
  • Security verifies it's a real passport
  • They confirm the photo matches your face
  • Result: Your identity is authenticated

Example 2: Backend Login

  • User provides username and password
  • Backend checks if password hash matches stored hash
  • Result: User identity is authenticated

Authentication does not care what you can do. It only cares that you are who you claim to be.

Authorization: What Can You Do?

Authorization answers a different question: Are you allowed to do this specific action?

Example 1: Airport Security (Continued)

  • Your identity is authenticated ✓
  • You try to enter the first-class lounge
  • They check your ticket → economy class
  • Result: You are not authorized to enter

Example 2: Backend API

  • User is authenticated ✓
  • User tries to delete another user's post
  • Backend checks permissions → user doesn't own that post
  • Result: Action not authorized

The Sequential Relationship

These concepts have a strict order:

flowchart TD
    A[Incoming Request] --> B{Authenticated?}
    B -->|No| C[Return 401 Unauthorized]
    B -->|Yes| D{Authorized?}
    D -->|No| E[Return 403 Forbidden]
    D -->|Yes| F[Process Request]
    
    style C fill:#ff4444
    style E fill:#ff8844
    style F fill:#44ff88

You cannot authorize without authentication. You must know who someone is before you can determine what they can do.

HTTP Status Codes Signal the Difference

  • 401 Unauthorized (misleading name): Authentication failed. "I don't know who you are."
  • 403 Forbidden: Authorization failed. "I know who you are, but you can't do this."

2. The Complete Authentication Flow

Let me show you what happens when a user logs in, step by step.

Step 1: User Submits Credentials

POST /auth/login
Content-Type: application/json

{
  "username": "john@example.com",
  "password": "secret123"
}

Step 2: Backend Validates Credentials

// Simplified authentication logic
async login(username: string, password: string) {
  // 1. Find user in database
  const user = await db.users.findOne({ username });
  if (!user) {
    throw new UnauthorizedException('Invalid credentials');
  }
  
  // 2. Verify password
  const isValid = await bcrypt.compare(password, user.passwordHash);
  if (!isValid) {
    throw new UnauthorizedException('Invalid credentials');
  }
  
  // 3. User is authenticated
  return user;
}

Security Note: Never reveal which part failed. Don't say "username not found" or "wrong password". Always return generic "invalid credentials".

Step 3: Generate Access Token

async generateToken(user: User) {
  const payload = {
    sub: user.id,        // Subject (user ID)
    username: user.username,
    roles: user.roles,
    iat: Date.now(),     // Issued at
  };
  
  // Sign the token with secret key
  return jwt.sign(payload, SECRET_KEY, { expiresIn: '1h' });
}

Step 4: Return Token to Client

HTTP/1.1 200 OK
Content-Type: application/json

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600
}

Step 5: Client Stores Token

The client (browser, mobile app) stores this token. Common storage locations:

  • Memory (most secure, lost on refresh)
  • LocalStorage (persistent, vulnerable to XSS)
  • HttpOnly Cookie (good balance)

Step 6: Client Includes Token in Subsequent Requests

GET /api/posts
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Step 7: Backend Verifies Token

async verifyToken(token: string) {
  try {
    // Verify signature and expiration
    const payload = jwt.verify(token, SECRET_KEY);
    
    // Extract user info
    const user = {
      id: payload.sub,
      username: payload.username,
      roles: payload.roles,
    };
    
    return user;
  } catch (error) {
    throw new UnauthorizedException('Invalid token');
  }
}

Visual Flow Diagram

sequenceDiagram
    participant C as Client
    participant B as Backend
    participant DB as Database
    
    C->>B: POST /auth/login<br/>{username, password}
    B->>DB: Find user by username
    DB->>B: User record
    B->>B: Verify password hash
    B->>B: Generate JWT token
    B->>C: {access_token: "..."}
    
    Note over C: Client stores token
    
    C->>B: GET /api/posts<br/>Authorization: Bearer token
    B->>B: Verify JWT signature
    B->>B: Check expiration
    B->>C: Protected resource data

3. The Complete Authorization Flow

Now that the user is authenticated, authorization determines what they can access.

Role-Based Access Control (RBAC)

The simplest authorization model uses roles.

Example Roles:

  • user - Regular user
  • moderator - Can moderate content
  • admin - Full access

Step 1: Attach Roles to User

When generating the token, include roles:

const payload = {
  sub: user.id,
  username: user.username,
  roles: ['user', 'moderator'],  // User has multiple roles
};

Step 2: Protect Routes with Role Guards

@Controller('admin')
export class AdminController {
  
  @Get('users')
  @Roles('admin')  // Only admins can access
  async getAllUsers() {
    return this.userService.findAll();
  }
  
  @Delete('posts/:id')
  @Roles('admin', 'moderator')  // Admins or moderators
  async deletePost(@Param('id') id: string) {
    return this.postService.delete(id);
  }
}

Step 3: Guard Checks Authorization

// Simplified guard implementation
@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    // 1. Get required roles from route metadata
    const requiredRoles = this.reflector.get('roles', context.getHandler());
    if (!requiredRoles) {
      return true;  // No roles required, allow access
    }
    
    // 2. Get user from request (set by authentication middleware)
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    
    // 3. Check if user has any required role
    return requiredRoles.some(role => user.roles?.includes(role));
  }
}

Permission-Based Authorization (More Fine-Grained)

For complex systems, permissions provide more control than roles.

Example Permissions:

  • posts:read - Can read posts
  • posts:create - Can create posts
  • posts:delete - Can delete own posts
  • posts:delete:any - Can delete any post
@Delete('posts/:id')
@RequirePermissions('posts:delete:any')
async deleteAnyPost(@Param('id') id: string) {
  return this.postService.delete(id);
}

Resource-Based Authorization

Sometimes authorization depends on the specific resource being accessed.

Example: User can only delete their own posts

@Delete('posts/:id')
async deletePost(
  @Param('id') id: string,
  @CurrentUser() user: User
) {
  const post = await this.postService.findById(id);
  
  // Check ownership
  if (post.authorId !== user.id && !user.roles.includes('admin')) {
    throw new ForbiddenException('You can only delete your own posts');
  }
  
  return this.postService.delete(id);
}

Authorization Flow Diagram

flowchart TD
    A[Request with Token] --> B[Extract Token]
    B --> C[Verify Signature]
    C --> D{Valid?}
    D -->|No| E[401 Unauthorized]
    D -->|Yes| F[Extract User + Roles]
    F --> G{Has Required Role?}
    G -->|No| H[403 Forbidden]
    G -->|Yes| I{Resource Check Needed?}
    I -->|Yes| J{Owns Resource?}
    I -->|No| K[Process Request]
    J -->|No| H
    J -->|Yes| K
    
    style E fill:#ff4444
    style H fill:#ff8844
    style K fill:#44ff88

4. Conclusion: The Two-Question Security Model

Every secure backend system answers two questions in order:

  1. Who are you? (Authentication)
  2. What can you do? (Authorization)

Get these wrong, and your system is vulnerable. Get them right, and you have a foundation for building secure applications.

The flow is always:

  1. User proves identity → receives token
  2. Token included in requests → backend verifies identity
  3. Backend checks permissions → grants or denies access

This pattern scales from simple blogs to complex enterprise systems. The core principles remain the same. What changes is the complexity of the authorization rules and the sophistication of the token management.

Remember: authentication is about identity. Authorization is about permission. They're different problems, solved at different layers, with different failure modes. Keep them separate in your code, and your security will be easier to reason about, audit, and maintain.

References

  1. NestJS Documentation - Authentication
    https://docs.nestjs.com/security/authentication
    Official NestJS guide covering Passport integration, JWT strategies, and authentication patterns.

  2. NestJS Documentation - Authorization
    https://docs.nestjs.com/security/authorization
    Official guide on implementing role-based access control (RBAC) and guards in NestJS.

  3. JWT.io - JSON Web Tokens
    https://jwt.io
    Interactive JWT debugger and comprehensive documentation on JWT structure and best practices.