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 usermoderator- Can moderate contentadmin- 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 postsposts:create- Can create postsposts:delete- Can delete own postsposts: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:
- Who are you? (Authentication)
- 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:
- User proves identity → receives token
- Token included in requests → backend verifies identity
- 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
-
NestJS Documentation - Authentication
https://docs.nestjs.com/security/authentication
Official NestJS guide covering Passport integration, JWT strategies, and authentication patterns. -
NestJS Documentation - Authorization
https://docs.nestjs.com/security/authorization
Official guide on implementing role-based access control (RBAC) and guards in NestJS. -
JWT.io - JSON Web Tokens
https://jwt.io
Interactive JWT debugger and comprehensive documentation on JWT structure and best practices.