Authentication and authorization are the security foundation of a backend application.
Authentication (AuthN) answers:
Who are you?
Authorization (AuthZ) answers:
What are you allowed to do?
A production backend usually combines password authentication, sessions or tokens, OAuth 2.0 / OpenID Connect, role-based authorization, secure cookies, MFA, and audit logging.
A secure design should also treat authentication, session management, and authorization as separate responsibilities.
1. Authentication vs Authorization
Consider an API endpoint:
DELETE /api/users/42
The backend must answer two different questions:
Authentication
↓
Is this request associated with a valid authenticated user?
↓
Authorization
↓
Does that authenticated user have permission to delete user 42?
A valid login does not automatically grant permission to perform every operation.
For example:
User: alice@example.com
Authenticated: YES
Role: USER
GET /api/profile
→ ALLOWED
DELETE /api/users/42
→ DENIED
An administrator might have:
User: admin@example.com
Authenticated: YES
Role: ADMIN
GET /api/profile
→ ALLOWED
DELETE /api/users/42
→ ALLOWED
This separation prevents a common security mistake: treating identity as permission.
2. The Complete Authentication Architecture
A typical backend authentication system looks like this:
┌──────────────────────┐
│ Browser / App │
└──────────┬───────────┘
│
HTTPS Request
│
▼
┌──────────────────────┐
│ API Gateway / │
│ Load Balancer │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Authentication Layer │
│ │
│ Session / JWT / OAuth│
└──────────┬───────────┘
│
Authenticated Identity
│
▼
┌──────────────────────┐
│ Authorization Layer │
│ │
│ RBAC / ABAC / Scopes │
└──────────┬───────────┘
│
Authorized Request
│
▼
┌──────────────────────┐
│ Application Services │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ PostgreSQL / Redis │
└──────────────────────┘
The client should never be trusted to determine whether a request is authorized. The backend must enforce authorization on protected operations.
3. Password Authentication
A traditional authentication flow is:
Registration
↓
User submits email + password
↓
Validate input
↓
Hash password
↓
Store password hash
↓
Login
↓
Find user
↓
Verify password against hash
↓
Create session/token
↓
Return authenticated response
Never store:
password = "MyPassword123"
Store a strong password hash instead.
Modern password hashing should use a dedicated password-hashing algorithm such as Argon2id, bcrypt, or another appropriately configured password hashing mechanism. OWASP recommends secure password storage rather than reversible encryption or plaintext storage.
Node.js Example
npm install express argon2 jsonwebtoken cookie-parser
import express from "express";
import argon2 from "argon2";
import jwt from "jsonwebtoken";
import cookieParser from "cookie-parser";
const app = express();
app.use(express.json());
app.use(cookieParser());
const users = new Map();
app.post("/register", async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({
error: "Email and password are required"
});
}
if (users.has(email)) {
return res.status(409).json({
error: "User already exists"
});
}
const passwordHash = await argon2.hash(password);
users.set(email, {
email,
passwordHash,
role: "USER"
});
return res.status(201).json({
message: "User created"
});
});
app.listen(3000, () => {
console.log("Server running on port 3000");
});
The important distinction is:
Plain password
↓
Argon2id
↓
Password hash
↓
Database
During login, the server does not decrypt a password. It verifies whether the submitted password matches the stored hash.
4. JWT Authentication
JSON Web Token (JWT) is a compact token format commonly used to represent claims between systems.
A JWT normally contains:
HEADER.PAYLOAD.SIGNATURE
Example structure:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjMiLCJyb2xlIjoiVVNFUiJ9
.
signature
The payload can contain claims such as:
{
"sub": "123",
"role": "USER",
"iss": "api.example.com",
"aud": "web-app",
"exp": 1786780000
}
Important JWT claims include:
- sub — subject or user identifier
- iss — token issuer
- aud — intended audience
- exp — expiration time
- iat — issued-at time
- scope — permitted OAuth scopes
A JWT is signed, not automatically encrypted. Therefore, sensitive secrets should not be placed inside its payload.
JWT Authentication Flow
Client
│
│ POST /login
│ email + password
▼
Backend
│
├── Find user
├── Verify password
└── Issue access token
│
▼
Client
│
│ Authorization: Bearer <token>
▼
Authentication Middleware
│
├── Verify signature
├── Validate expiration
├── Validate issuer
└── Validate audience
│
▼
Authorization Middleware
│
├── Check role
└── Check permissions
│
▼
Protected Controller
Complete JWT Middleware
import jwt from "jsonwebtoken";
const JWT_SECRET = process.env.JWT_SECRET;
export function authenticate(req, res, next) {
const authorization = req.headers.authorization;
if (!authorization) {
return res.status(401).json({
error: "Authentication required"
});
}
const [scheme, token] = authorization.split(" ");
if (scheme !== "Bearer" || !token) {
return res.status(401).json({
error: "Invalid authorization header"
});
}
try {
const payload = jwt.verify(token, JWT_SECRET, {
issuer: "api.example.com",
audience: "web-app"
});
req.user = {
id: payload.sub,
role: payload.role
};
next();
} catch {
return res.status(401).json({
error: "Invalid or expired token"
});
}
}
The middleware converts:
HTTP Authorization Header
↓
Token Verification
↓
Validated Identity
↓
req.user
A Bearer token must be protected carefully because possession of the token is generally sufficient to use it.
5. JWT Access Tokens and Refresh Tokens
A common architecture separates short-lived access tokens from long-lived refresh tokens.
Login
│
├──────────────► Access Token
│ Short lifetime
│ Used for APIs
│
└──────────────► Refresh Token
Longer lifetime
Used to obtain new access tokens
Example:
Access Token
↓
15 minutes
Refresh Token
↓
Days / weeks depending on security requirements
When the access token expires:
Client
│
│ refresh token
▼
Backend
│
├── Validate refresh token
├── Check revocation/session state
└── Rotate refresh token
│
▼
New access token
For browser applications, authentication credentials should generally not be stored in localStorage or sessionStorage, because JavaScript running in the origin can access them. OWASP recommends appropriately protected cookies or architectures such as a Backend-for-Frontend (BFF).
6. Secure Cookie Configuration
A session or refresh token stored in a cookie should use appropriate security attributes:
res.cookie("refresh_token", refreshToken, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/auth",
maxAge: 7 * 24 * 60 * 60 * 1000
});
Important attributes:
HttpOnly
↓
JavaScript cannot directly read the cookie
Secure
↓
Cookie is sent only over HTTPS
SameSite
↓
Controls cross-site cookie transmission
Path
↓
Limits where the cookie is sent
Max-Age
↓
Controls cookie lifetime
Cookie-based authentication must also consider CSRF protection, particularly when cookies are automatically attached to cross-site requests.
7. Server-Side Sessions
A session-based authentication system stores authentication state on the server.
The browser receives only an opaque session identifier:
Browser
│
│ session_id=abc123
▼
Backend
│
▼
Session Store
│
└── abc123
├── userId: 42
├── role: USER
└── expiresAt: ...
The session identifier should be random, unpredictable, and contain no sensitive user information. OWASP recommends strong server-generated session identifiers and careful lifecycle management.
Redis Session Example
import crypto from "crypto";
function createSession(userId) {
const sessionId = crypto.randomBytes(32).toString("hex");
const session = {
userId,
createdAt: Date.now(),
expiresAt: Date.now() + 1000 * 60 * 60 * 24
};
return {
sessionId,
session
};
}
The architecture becomes:
Client
│
│ Cookie: session_id
▼
API
│
▼
Redis
│
└── session_id → user identity
This makes logout, session revocation, and server-side session invalidation straightforward.
8. OAuth 2.0
OAuth 2.0 is primarily an authorization framework, not a password-login protocol.
It allows a client application to obtain limited access to protected resources without receiving the resource owner's password.
The important roles are:
Resource Owner
│
▼
Authorization Server
│
▼
Client Application
│
▼
Resource Server
For example:
User
│
│ "Allow application to access profile"
▼
Google Authorization Server
│
│ authorization code
▼
Application
│
│ exchange code
▼
Access Token
│
▼
Protected API
For modern applications, the Authorization Code flow with PKCE is the standard pattern to understand, especially for public clients where a client secret cannot safely be kept confidential.
9. OAuth 2.0 Authorization Code + PKCE
The architecture:
┌────────────┐
│ Client │
└─────┬──────┘
│
│ 1. Generate code_verifier
│
│ 2. Create code_challenge
▼
┌─────────────────────┐
│ Authorization │
│ Server │
└──────────┬──────────┘
│
│ 3. User authenticates
│
│ 4. User grants access
▼
Authorization Code
│
▼
┌──────────┴──────────┐
│ Client │
│ │
│ code + verifier │
└──────────┬──────────┘
│
│ 5. Token request
▼
┌─────────────────────┐
│ Authorization │
│ Server │
└──────────┬──────────┘
│
│ 6. Access Token
▼
┌─────────────────────┐
│ Resource Server │
└─────────────────────┘
PKCE binds the authorization-code exchange to the client instance that initiated the flow.
Conceptually:
code_verifier
│
▼
SHA-256
│
▼
code_challenge
The client sends the code_challenge during authorization and later sends the original code_verifier during token exchange.
The authorization server verifies that they match.
10. OAuth 2.0 Scopes
OAuth permissions should be expressed using narrow scopes.
Instead of:
access = everything
use:
profile:read
orders:read
orders:write
Example:
Authorization: Bearer ACCESS_TOKEN
The backend validates:
Token valid?
↓
Scope present?
↓
orders:write
↓
Allow operation
A token with:
scope = "orders:read"
should not be accepted for:
DELETE /orders/123
because deletion requires a stronger permission such as:
orders:delete
11. OpenID Connect
OAuth 2.0 provides authorization.
OpenID Connect (OIDC) adds an identity layer on top of OAuth 2.0.
The conceptual difference is:
OAuth 2.0
↓
"What can this application access?"
OpenID Connect
↓
"Who authenticated this user?"
OIDC commonly introduces an ID Token, which contains identity-related claims intended for the client.
A backend integrating with an identity provider should validate tokens according to the provider's documented issuer, audience, signature, expiration, and other required claims.
12. Authorization with RBAC
Role-Based Access Control (RBAC) assigns permissions through roles.
Example:
USER
├── profile:read
└── order:create
EDITOR
├── profile:read
├── article:create
└── article:update
ADMIN
├── user:read
├── user:update
├── user:delete
└── system:manage
Authorization Middleware
function authorize(...allowedRoles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
error: "Authentication required"
});
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({
error: "Forbidden"
});
}
next();
};
}
Usage:
app.delete(
"/api/users/:id",
authenticate,
authorize("ADMIN"),
deleteUser
);
The pipeline becomes:
Request
↓
authenticate()
↓
Identity established
↓
authorize("ADMIN")
↓
Permission verified
↓
deleteUser()
401 Unauthorized generally means the request lacks valid authentication.
403 Forbidden means the server understands the identity but refuses the requested operation.
13. Resource-Level Authorization
Checking only the user's role is not always sufficient.
Suppose:
GET /api/orders/500
The user may have:
role = USER
but the backend must additionally verify:
Does order 500 belong to this user?
Correct:
const order = await db.order.findUnique({
where: {
id: req.params.id
}
});
if (!order) {
return res.status(404).json({
error: "Order not found"
});
}
if (order.userId !== req.user.id && req.user.role !== "ADMIN") {
return res.status(403).json({
error: "Forbidden"
});
}
This protects against broken object-level authorization, where an authenticated user attempts to access another user's resource by changing an identifier.
14. RBAC vs ABAC
RBAC evaluates roles:
role = ADMIN
Attribute-Based Access Control (ABAC) evaluates multiple attributes:
user.department
resource.owner
resource.classification
request.time
request.location
operation
Example policy:
ALLOW
IF
user.role = MANAGER
AND
user.department = resource.department
AND
operation = "READ"
This becomes valuable in large systems where simple roles are insufficient.
15. Authentication Middleware Structure
A production backend can separate security responsibilities into layers:
HTTP Request
│
▼
Rate Limiting
│
▼
Authentication
│
▼
Identity Context
│
▼
Authorization
│
▼
Validation
│
▼
Business Logic
│
▼
Database
A clean project structure might look like:
src/
├── auth/
│ ├── auth.controller.js
│ ├── auth.service.js
│ ├── auth.middleware.js
│ ├── token.service.js
│ ├── password.service.js
│ └── authorization.js
│
├── users/
│ ├── user.controller.js
│ ├── user.service.js
│ └── user.repository.js
│
├── middleware/
│ ├── rateLimit.js
│ ├── validation.js
│ └── errorHandler.js
│
├── routes/
│ ├── auth.routes.js
│ └── user.routes.js
│
├── database/
│ └── client.js
│
└── server.js
This prevents authentication logic from becoming mixed into every controller.
16. Complete Protected API Example
import express from "express";
import jwt from "jsonwebtoken";
const app = express();
app.use(express.json());
const JWT_SECRET = process.env.JWT_SECRET;
function authenticate(req, res, next) {
const header = req.headers.authorization;
if (!header) {
return res.status(401).json({
error: "Missing Authorization header"
});
}
const [scheme, token] = header.split(" ");
if (scheme !== "Bearer" || !token) {
return res.status(401).json({
error: "Invalid Authorization header"
});
}
try {
const payload = jwt.verify(token, JWT_SECRET, {
issuer: "api.example.com",
audience: "web-app"
});
req.user = {
id: payload.sub,
role: payload.role
};
next();
} catch {
return res.status(401).json({
error: "Invalid or expired access token"
});
}
}
function authorize(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: "Insufficient permissions"
});
}
next();
};
}
app.get("/api/profile", authenticate, (req, res) => {
res.json({
userId: req.user.id,
role: req.user.role
});
});
app.delete(
"/api/users/:id",
authenticate,
authorize("ADMIN"),
async (req, res) => {
// Delete user from database here.
res.json({
message: "User deleted"
});
}
);
app.listen(3000, () => {
console.log("API running on port 3000");
});
The important security boundary is:
authenticate()
↓
WHO IS THE USER?
↓
authorize()
↓
WHAT MAY THE USER DO?
↓
controller
↓
business operation
17. Multi-Factor Authentication
Multi-Factor Authentication (MFA) combines independent authentication factors.
Common factors include:
Something you know
→ Password
Something you have
→ Security key / authenticator device
Something you are
→ Biometric characteristic
A stronger login might therefore become:
Email + Password
↓
Password Verification
↓
MFA Challenge
↓
MFA Verification
↓
Authenticated Session
MFA is especially important for administrative accounts, financial operations, account recovery, and other high-impact actions. OWASP recommends stronger authentication and re-authentication for sensitive operations and risk events.
18. Session Lifecycle
Authentication is not complete when a user logs in.
A secure session has a lifecycle:
CREATE
↓
AUTHENTICATED
↓
ACTIVE
↓
RENEWED
↓
EXPIRED / REVOKED
↓
DESTROYED
Important events include:
login
logout
session_created
session_renewed
session_expired
session_revoked
password_changed
MFA_enabled
MFA_failed
authorization_denied
Session identifiers should be regenerated when appropriate, particularly across authentication state transitions, to reduce session fixation risks.
19. Logout and Token Revocation
With server-side sessions:
POST /logout
↓
Destroy session
↓
Clear cookie
With refresh-token systems:
POST /logout
↓
Revoke refresh token
↓
Clear authentication cookie
A simple cookie cleanup:
res.clearCookie("refresh_token", {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/auth"
});
res.status(204).send();
For high-security systems, maintain server-side refresh-token/session state so stolen credentials can be revoked before their natural expiration.
20. Authentication Security Controls
A production authentication system should include:
HTTPS / TLS
+
Secure password hashing
+
MFA
+
Rate limiting
+
Credential stuffing protection
+
Account recovery protection
+
Secure session management
+
Short-lived access tokens
+
Refresh-token rotation
+
Token/session revocation
+
Authorization checks
+
Audit logging
+
Re-authentication for sensitive operations
Authentication endpoints should also avoid revealing whether an account exists.
Instead of:
{
"error": "Email does not exist"
}
prefer a generic response such as:
{
"error": "Invalid email or password"
}
This reduces user enumeration.
Authentication systems should also log security-relevant events while avoiding passwords, access tokens, refresh tokens, and other secrets in logs. OWASP specifically recommends monitoring the lifecycle of sessions and authentication-related events.
21. Authentication Decision Structure
REQUEST
│
▼
Is HTTPS enabled?
│ │
NO YES
│ │
REJECT ▼
Authenticate
│
┌────────┴────────┐
│ │
INVALID VALID
│ │
401 ▼
Authorize
│
┌────────┴────────┐
│ │
DENIED ALLOWED
│ │
403 ▼
API
│
▼
Database
The core security model is therefore:
IDENTITY
↓
AUTHENTICATION
↓
SESSION / TOKEN
↓
AUTHORIZATION
↓
PERMISSION
↓
RESOURCE

Top comments (0)