DEV Community

CodeWithDhanian
CodeWithDhanian

Posted on

Authentication & Authorization (JWT, OAuth2, Sessions)

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
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

An administrator might have:

User: admin@example.com
Authenticated: YES
Role: ADMIN

GET /api/profile
→ ALLOWED

DELETE /api/users/42
→ ALLOWED
Enter fullscreen mode Exit fullscreen mode

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   │
                    └──────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Never store:

password = "MyPassword123"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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");
});
Enter fullscreen mode Exit fullscreen mode

The important distinction is:

Plain password
     ↓
Argon2id
     ↓
Password hash
     ↓
Database
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Example structure:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjMiLCJyb2xlIjoiVVNFUiJ9
.
signature
Enter fullscreen mode Exit fullscreen mode

The payload can contain claims such as:

{
  "sub": "123",
  "role": "USER",
  "iss": "api.example.com",
  "aud": "web-app",
  "exp": 1786780000
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

The middleware converts:

HTTP Authorization Header
            ↓
Token Verification
            ↓
Validated Identity
            ↓
req.user
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Example:

Access Token
    ↓
15 minutes

Refresh Token
    ↓
Days / weeks depending on security requirements
Enter fullscreen mode Exit fullscreen mode

When the access token expires:

Client
  │
  │ refresh token
  ▼
Backend
  │
  ├── Validate refresh token
  ├── Check revocation/session state
  └── Rotate refresh token
  │
  ▼
New access token
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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: ...
Enter fullscreen mode Exit fullscreen mode

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
  };
}
Enter fullscreen mode Exit fullscreen mode

The architecture becomes:

Client
  │
  │ Cookie: session_id
  ▼
API
  │
  ▼
Redis
  │
  └── session_id → user identity
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For example:

User
 │
 │ "Allow application to access profile"
 ▼
Google Authorization Server
 │
 │ authorization code
 ▼
Application
 │
 │ exchange code
 ▼
Access Token
 │
 ▼
Protected API
Enter fullscreen mode Exit fullscreen mode

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     │
└─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

PKCE binds the authorization-code exchange to the client instance that initiated the flow.

Conceptually:

code_verifier
      │
      ▼
SHA-256
      │
      ▼
code_challenge
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

use:

profile:read
orders:read
orders:write
Enter fullscreen mode Exit fullscreen mode

Example:

Authorization: Bearer ACCESS_TOKEN
Enter fullscreen mode Exit fullscreen mode

The backend validates:

Token valid?
      ↓
Scope present?
      ↓
orders:write
      ↓
Allow operation
Enter fullscreen mode Exit fullscreen mode

A token with:

scope = "orders:read"
Enter fullscreen mode Exit fullscreen mode

should not be accepted for:

DELETE /orders/123
Enter fullscreen mode Exit fullscreen mode

because deletion requires a stronger permission such as:

orders:delete
Enter fullscreen mode Exit fullscreen mode

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?"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
  };
}
Enter fullscreen mode Exit fullscreen mode

Usage:

app.delete(
  "/api/users/:id",
  authenticate,
  authorize("ADMIN"),
  deleteUser
);
Enter fullscreen mode Exit fullscreen mode

The pipeline becomes:

Request
   ↓
authenticate()
   ↓
Identity established
   ↓
authorize("ADMIN")
   ↓
Permission verified
   ↓
deleteUser()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The user may have:

role = USER
Enter fullscreen mode Exit fullscreen mode

but the backend must additionally verify:

Does order 500 belong to this user?
Enter fullscreen mode Exit fullscreen mode

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"
  });
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Attribute-Based Access Control (ABAC) evaluates multiple attributes:

user.department
resource.owner
resource.classification
request.time
request.location
operation
Enter fullscreen mode Exit fullscreen mode

Example policy:

ALLOW

IF
    user.role = MANAGER
AND
    user.department = resource.department
AND
    operation = "READ"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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");
});
Enter fullscreen mode Exit fullscreen mode

The important security boundary is:

authenticate()
      ↓
WHO IS THE USER?
      ↓
authorize()
      ↓
WHAT MAY THE USER DO?
      ↓
controller
      ↓
business operation
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A stronger login might therefore become:

Email + Password
       ↓
Password Verification
       ↓
MFA Challenge
       ↓
MFA Verification
       ↓
Authenticated Session
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Important events include:

login
logout
session_created
session_renewed
session_expired
session_revoked
password_changed
MFA_enabled
MFA_failed
authorization_denied
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

With refresh-token systems:

POST /logout
     ↓
Revoke refresh token
     ↓
Clear authentication cookie
Enter fullscreen mode Exit fullscreen mode

A simple cookie cleanup:

res.clearCookie("refresh_token", {
  httpOnly: true,
  secure: true,
  sameSite: "lax",
  path: "/auth"
});

res.status(204).send();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Authentication endpoints should also avoid revealing whether an account exists.

Instead of:

{
  "error": "Email does not exist"
}
Enter fullscreen mode Exit fullscreen mode

prefer a generic response such as:

{
  "error": "Invalid email or password"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The core security model is therefore:

IDENTITY
   ↓
AUTHENTICATION
   ↓
SESSION / TOKEN
   ↓
AUTHORIZATION
   ↓
PERMISSION
   ↓
RESOURCE
Enter fullscreen mode Exit fullscreen mode

Grab the Backend Engineering ebook

Authentication & Authorization (JWT, OAuth2, Sessions)

Top comments (0)