Skip to content

Authentication Patterns — JWT, OAuth 2.0, Session-Based, SSO, MFA

DodaTech Updated 2026-06-22 13 min read

In this tutorial, you'll learn about Authentication Patterns. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Authentication verifies the identity of a user or system attempting to access an application, using patterns ranging from simple session cookies to JWT tokens, OAuth 2.0 delegation, and multi-factor authentication for defense-in-depth security.

What You'll Learn

By the end of this tutorial, you will implement JWT access and refresh token flows, OAuth 2.0 authorization code and client credentials grants, session-based authentication with secure cookies, single sign-on with SAML and Openid Connect, and multi-factor authentication with TOTP.

Why It Matters

Authentication is the most common security vulnerability in web applications. Weak authentication leads to account takeover, data breaches, and Compliance violations. Doda Browser uses OAuth 2.0 with Openid Connect for its user authentication, JWT for API access, and MFA for administrator accounts through Durga Antivirus Pro's enterprise portal.

Real-World Use

A user logs into a banking app. The app uses OAuth 2.0 to authenticate against the bank's identity provider, receives a JWT access token for API calls, and requires MFA for transfers over $1000. The session is managed with HTTP-only cookies and the JWT expires after 15 minutes with a refresh token for seamless re-authentication.

Authentication Architecture Comparison

flowchart TB
    subgraph "Session-Based"
        C1[Client] -->|"Login"| S1[Server]
        S1 -->|"Set-Cookie: session=abc"| C1
        C1 -->|"Cookie: session=abc"| S1
        S1 -->|"Lookup session in DB"| DB1[(Session Store)]
    end
    subgraph "JWT-Based"
        C2[Client] -->|"Login"| S2[Server]
        S2 -->|"JWT: header.payload.sig"| C2
        C2 -->|"Authorization: Bearer JWT"| S2
        S2 -->|"Verify signature"| JWT[JWT Verification]
    end
    subgraph "OAuth 2.0"
        C3[Client] -->|"Authorize"| IDP[Identity Provider]
        IDP -->|"Authorization Code"| C3
        C3 -->|"Code + Client Secret"| IDP
        IDP -->|"Access Token"| C3
        C3 -->|"Bearer Token"| API[API Server]
    end
    style S1 fill:#f90,color:#fff
    style S2 fill:#22c55e,color:#fff
    style IDP fill:#3498db,color:#fff

JWT Authentication with Access and Refresh Tokens

// JWT-auth.js
// Complete JWT authentication with access and refresh tokens

const Express = require('Express');
const JWT = require('jsonwebtoken');
const crypto = require('crypto');

const app = Express();
app.use(Express.JSON());

const ACCESS_SECRET = 'your-access-secret-change-in-production';
const REFRESH_SECRET = 'your-refresh-secret-change-in-production';
const ACCESS_EXPIRY = '15m';
const REFRESH_EXPIRY = '7d';

// In-memory refresh token store (use Redis in production)
const refreshTokens = new Set();

// ── Login endpoint ──
app.post('/API/auth/login', (req, res) => {
  const { email, password } = req.body;

  // Validate credentials (simulated)
  if (email !== 'alice@example.com' || password !== 'correct-password') {
    console.log(`[Auth] Failed login attempt for ${email}`);
    return res.status(401).JSON({ error: 'Invalid credentials' });
  }

  const user = { id: 1, email, role: 'user' };

  // Generate access token (short-lived)
  const accessToken = JWT.sign(
    { sub: user.id, email: user.email, role: user.role },
    ACCESS_SECRET,
    { expiresIn: ACCESS_EXPIRY }
  );

  // Generate refresh token (long-lived)
  const refreshToken = JWT.sign(
    { sub: user.id, type: 'refresh' },
    REFRESH_SECRET,
    { expiresIn: REFRESH_EXPIRY }
  );

  refreshTokens.add(refreshToken);

  console.log(`[Auth] Successful login: ${email}`);
  res.JSON({
    accessToken,
    refreshToken,
    expiresIn: 900,  // 15 minutes in seconds
  });
});

// ── Refresh token endpoint ──
app.post('/API/auth/refresh', (req, res) => {
  const { refreshToken } = req.body;

  if (!refreshToken || !refreshTokens.has(refreshToken)) {
    console.log('[Auth] Invalid refresh token');
    return res.status(401).JSON({ error: 'Invalid refresh token' });
  }

  try {
    const decoded = JWT.verify(refreshToken, REFRESH_SECRET);
    const user = { id: decoded.sub, role: 'user' };

    // Issue new access token
    const newAccessToken = JWT.sign(
      { sub: user.id, role: user.role },
      ACCESS_SECRET,
      { expiresIn: ACCESS_EXPIRY }
    );

    console.log(`[Auth] Token refreshed for user ${user.id}`);
    res.JSON({ accessToken: newAccessToken, expiresIn: 900 });
  } catch (err) {
    console.log('[Auth] Refresh token expired or invalid');
    refreshTokens.delete(refreshToken);
    res.status(401).JSON({ error: 'Refresh token expired' });
  }
});

// ── Auth middleware ──
function authenticateJWT(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];  // Bearer TOKEN

  if (!token) {
    return res.status(401).JSON({ error: 'Access token required' });
  }

  try {
    const decoded = JWT.verify(token, ACCESS_SECRET);
    req.user = decoded;
    console.log(`[Auth] Authenticated user ${req.user.sub} via JWT`);
    next();
  } catch (err) {
    console.log(`[Auth] Invalid token: ${err.message}`);
    return res.status(403).JSON({ error: 'Invalid or expired token' });
  }
}

// ── Protected route ──
app.get('/API/profile', authenticateJWT, (req, res) => {
  res.JSON({ userId: req.user.sub, email: req.user.email, role: req.user.role });
});

// ── Logout ──
app.post('/API/auth/logout', (req, res) => {
  const { refreshToken } = req.body;
  if (refreshToken) {
    refreshTokens.delete(refreshToken);
    console.log('[Auth] User logged out, refresh token revoked');
  }
  res.JSON({ message: 'Logged out' });
});

app.listen(3000, () => console.log('JWT Auth server on :3000'));

Expected behavior:

POST /API/auth/login {"email": "alice@example.com", "password": "correct-password"}
-> 200 {"accessToken": "eyJ...", "refreshToken": "eyJ...", "expiresIn": 900}

GET /API/profile (Authorization: Bearer eyJ...)
-> 200 {"userId": 1, "email": "alice@example.com", "role": "user"}

POST /API/auth/refresh {"refreshToken": "eyJ..."}
-> 200 {"accessToken": "eyJ...", "expiresIn": 900}

The access token expires after 15 minutes. The client uses the refresh token to get a new access token without re-entering credentials. Refresh tokens can be revoked server-side (unlike access tokens, which are stateless).

OAuth 2.0 Authorization Code Flow

# oauth2_flow.py
# OAuth 2.0 Authorization Code flow implementation (simplified)

import requests
import json
import secrets
import hashlib
import base64

class OAuth2Client:
    """Simplified OAuth 2.0 Authorization Code client with PKCE."""

    def __init__(self, client_id, client_secret, redirect_uri, auth_url, token_url):
        self.client_id = client_id
        self.client_secret = client_secret
        self.redirect_uri = redirect_uri
        self.auth_url = auth_url
        self.token_url = token_url

    def generate_pkce_challenge(self):
        """Generate PKCE code verifier and challenge."""
        code_verifier = secrets.token_urlsafe(64)
        code_challenge = base64.urlsafe_b64encode(
            hashlib.sha256(code_verifier.encode()).digest()
        ).rstrip('=').decode()
        return code_verifier, code_challenge

    def get_authorization_url(self, scope='openid profile email'):
        """Generate the URL to redirect users to the authorization server."""
        _, code_challenge = self.generate_pkce_challenge()
        params = {
            'response_type': 'code',
            'client_id': self.client_id,
            'redirect_uri': self.redirect_uri,
            'scope': scope,
            'code_challenge': code_challenge,
            'code_challenge_method': 'S256',
            'state': secrets.token_urlsafe(16),
        }
        url = f"{self.auth_url}?{requests.models.PreparedRequest()._encode_params(params)}".replace(' ', '')
        print(f"[OAuth2] Authorization URL generated:")
        print(f"  URL: {url[:100]}...")
        print(f"  State: {params['state']}")
        print(f"  Scope: {scope}")
        return url

    def exchange_code(self, code, code_verifier):
        """Exchange authorization code for tokens."""
        data = {
            'grant_type': 'authorization_code',
            'code': code,
            'redirect_uri': self.redirect_uri,
            'client_id': self.client_id,
            'client_secret': self.client_secret,
            'code_verifier': code_verifier,
        }

        print(f"[OAuth2] Exchanging authorization code for tokens...")
        try:
            response = requests.post(self.token_url, data=data, timeout=10)
            response.raise_for_status()
            tokens = response.json()
            print(f"[OAuth2] Tokens received:")
            print(f"  Access token: {tokens.get('access_token', '')[:20]}...")
            print(f"  Refresh token: {tokens.get('refresh_token', '')[:20]}...")
            print(f"  ID token: {tokens.get('id_token', '')[:20]}...")
            print(f"  Expires in: {tokens.get('expires_in', 'unknown')}s")
            return tokens
        except requests.RequestException as e:
            print(f"[OAuth2] Token exchange failed: {e}")
            return None

    def refresh_access_token(self, refresh_token):
        """Refresh an expired access token."""
        data = {
            'grant_type': 'refresh_token',
            'refresh_token': refresh_token,
            'client_id': self.client_id,
            'client_secret': self.client_secret,
        }

        print(f"[OAuth2] Refreshing access token...")
        response = requests.post(self.token_url, data=data, timeout=10)
        if response.status_code == 200:
            tokens = response.json()
            print(f"[OAuth2] New access token: {tokens.get('access_token', '')[:20]}...")
            return tokens
        else:
            print(f"[OAuth2] Refresh failed: {response.status_code}")
            return None

# ── Usage (requires a real OAuth 2.0 provider) ──
# client = OAuth2Client(
#     client_id='your-client-id',
#     client_secret='your-client-secret',
#     redirect_uri='https://app.example.com/callback',
#     auth_url='https://accounts.example.com/authorize',
#     token_url='https://accounts.example.com/token',
# )
#
# # Step 1: Redirect user to authorization URL
# auth_url = client.get_authorization_url()
# print(f"Redirect user to: {auth_url}")
#
# # Step 2: User authenticates and is redirected back with a code
# # (In a real app, this comes from the query parameter after redirect)
# authorization_code = 'abc123...'
# code_verifier = '...'  # Must match the one generated in step 1
#
# # Step 3: Exchange code for tokens
# tokens = client.exchange_code(authorization_code, code_verifier)

Expected behavior:

[OAuth2] Authorization URL generated:
  URL: https://accounts.example.com/authorize?response_type=code&client_id=...
  State: abc123...
  Scope: openid profile email
[OAuth2] Exchanging authorization code for tokens...
[OAuth2] Tokens received:
  Access token: eyJhbGciOiJSUzI1Ni...
  Refresh token: eyJhbGciOiJSUzI1Ni...
  ID token: eyJhbGciOiJSUzI1Ni...
  Expires in: 3600s

The OAuth 2.0 Authorization Code flow with PKCE is the most secure pattern for public clients (SPAs, mobile apps). The client never sees the user's password — authentication happens at the identity provider.

Session-Based Authentication

// session-auth.js
// Session-based authentication with secure cookies

const Express = require('Express');
const session = require('Express-session');
const RedisStore = require('connect-Redis').default;
const Redis = require('Redis');

const app = Express();
app.use(Express.JSON());

// Redis client for session storage
const redisClient = Redis.createClient({ URL: 'Redis://localhost:6379' });
redisClient.connect().catch(console.error);

// Session configuration
app.use(session({
  store: new RedisStore({ client: redisClient, prefix: 'session:' }),
  secret: 'your-session-secret-change-in-production',
  resave: false,
  saveUninitialized: false,
  name: 'app.sid',  // Custom cookie name (not default 'connect.sid')
  cookie: {
    httpOnly: true,      // Cannot be accessed by JavaScript
    secure: Process.env.NODE_ENV === 'production',  // HTTPS only in production
    sameSite: 'strict',  // Prevents CSRF
    maxAge: 24 * 60 * 60 * 1000,  // 24 hours
  },
}));

// ── Login ──
app.post('/API/auth/login', (req, res) => {
  const { email, password } = req.body;

  if (email !== 'alice@example.com' || password !== 'correct-password') {
    return res.status(401).JSON({ error: 'Invalid credentials' });
  }

  // Store user info in session
  req.session.userId = 1;
  req.session.email = email;
  req.session.role = 'user';
  req.session.createdAt = Date.now();

  console.log(`[Session] Login: ${email}, session ${req.session.id}`);
  res.JSON({ message: 'Logged in', user: { email, role: 'user' } });
});

// ── Auth middleware ──
function requireAuth(req, res, next) {
  if (!req.session.userId) {
    console.log(`[Session] Unauthenticated access to ${req.path}`);
    return res.status(401).JSON({ error: 'Authentication required' });
  }
  console.log(`[Session] Authenticated: user ${req.session.userId} (${req.session.email})`);
  next();
}

// ── Protected routes ──
app.get('/API/profile', requireAuth, (req, res) => {
  res.JSON({
    userId: req.session.userId,
    email: req.session.email,
    role: req.session.role,
  });
});

// ── Logout ──
app.post('/API/auth/logout', (req, res) => {
  const sessionId = req.session.id;
  req.session.destroy((err) => {
    if (err) {
      return res.status(500).JSON({ error: 'Logout failed' });
    }
    res.clearCookie('app.sid');
    console.log(`[Session] Logout: session ${sessionId} destroyed`);
    res.JSON({ message: 'Logged out' });
  });
});

app.listen(3001, () => console.log('Session Auth server on :3001'));

Expected behavior: The server sets a secure HTTP-only cookie (app.sid) on login. This cookie is automatically sent with every subsequent request. The session data (userId, role) is stored in Redis and retrieved by the session middleware. Logout destroys the session and clears the cookie.

Common Errors

1. Storing JWTs in localStorage

JWTs stored in localStorage are accessible to any JavaScript running on the same origin, making them vulnerable to XSS Attacks. Store access tokens in memory (for SPAs) or in HTTP-only cookies (for traditional apps). Use refresh tokens stored in HTTP-only cookies for persistence.

2. Not Implementing Token Rotation for Refresh Tokens

A stolen refresh token grants indefinite access. Implement refresh token rotation: issue a new refresh token with every access token refresh and invalidate the old one. If a stolen token is used, the next legitimate request will fail because the token was already rotated.

3. Using Weak Session Identifiers

Predictable or sequential session IDs allow attackers to guess active sessions. Use cryptographically random session IDs (Express session generator provides this by default). Set SameSite=Strict and HttpOnly on session cookies to prevent CSRF and XSS token theft.

4. Ignoring Multi-Factor Authentication for Sensitive Actions

Password-only authentication is insufficient for admin panels, payment processing, or data export features. Implement TOTP-based MFA (Google Authenticator, Authy) for high-risk operations. At minimum, require MFA for all administrator accounts.

5. Not Handling Token Expiry Gracefully

Clients that receive 401 errors without guidance simply break. Implement an interceptor that catches 401 responses, uses the refresh token to obtain a new access token, and retries the original request. If refresh fails, redirect to login.

6. Exposing User Enumeration Through Error Messages

Returning "User not found" for invalid emails and "Invalid password" for valid emails allows attackers to discover valid usernames. Return generic "Invalid credentials" for both cases. Rate-limit login attempts to prevent brute force enumeration.

Practice Questions

1. What is the difference between JWT and session-based authentication?

JWT is stateless — the token contains all user information and is verified by signature, no server-side storage needed. Session-based auth stores session data on the server and uses a cookie ID to look it up. JWTs scale better but cannot be revoked server-side without a blocklist.

2. How does OAuth 2.0 differ from Openid Connect?

OAuth 2.0 is an authorization framework that grants access to resources (access tokens). Openid Connect (OIDC) is an identity layer on top of OAuth 2.0 that adds authentication (ID tokens with user identity claims). OAuth 2.0 alone does not authenticate users.

3. Why is PKCE required for mobile apps using OAuth 2.0?

Mobile apps cannot securely store a client secret. PKCE uses a dynamically generated code verifier that proves the app requesting the token is the same app that started the authorization flow, preventing authorization code interception attacks.

4. What is a refresh token and why is it needed?

A refresh token is a long-lived credential used to obtain new access tokens without requiring the user to re-authenticate. It allows access tokens to have short lifetimes (15 minutes) while maintaining persistent sessions. Refresh tokens can be revoked server-side if suspicious activity is detected.

Challenge

Implement a complete authentication system for a multi-tenant SaaS platform: (1) users Register with email verification, (2) login uses password + TOTP MFA (optional per user), (3) API access uses JWT access tokens (15-minute expiry) with refresh token rotation, (4) session-based authentication for the web dashboard with Redis session store, (5) OAuth 2.0 with OIDC for third-party app integrations, (6) SSO via SAML for enterprise tenants, (7) Rate Limiting on login endpoints (5 attempts per 15 minutes), and (8) audit logging of all authentication events.

Mini Project: Multi-Strategy Auth Gateway

# auth_gateway.py
# Authentication gateway supporting multiple auth strategies

import JWT
import time
import JSON
from functools import wraps

class AuthGateway:
    """Authentication gateway supporting JWT, API keys, and session tokens."""

    def __init__(self, JWT_secret='secret', API_keys=None):
        self.JWT_secret = JWT_secret
        self.API_keys = API_keys or {'sk_live_abc123': {'client': 'Acme Corp', 'role': 'admin'}}

    def authenticate_request(self, request):
        """
        Authenticate a request using any supported Strategy.
        Returns (user, method) or raises AuthError.
        """
        # Strategy 1: JWT Bearer token
        auth_header = request.get('headers', {}).get('authorization', '')
        if auth_header.startswith('Bearer '):
            token = auth_header[7:]
            return self._verify_JWT(token)

        # Strategy 2: API Key
        API_key = request.get('headers', {}).get('x-API-key', '')
        if API_key:
            return self._verify_API_key(API_key)

        # Strategy 3: Session cookie
        cookie = request.get('cookies', {}).get('session_token', '')
        if cookie:
            return self._verify_session(cookie)

        raise AuthError('No authentication provided', 401)

    def _verify_JWT(self, token):
        """Verify JWT access token."""
        try:
            payload = JWT.decode(token, self.JWT_secret, algorithms=['HS256'])
            if payload.get('type') != 'access':
                raise AuthError('Invalid token type', 403)
            user = {
                'id': payload['sub'],
                'email': payload.get('email'),
                'role': payload.get('role', 'user'),
            }
            print(f"[AuthGateway] JWT auth: user {user['id']} ({user['role']})")
            return user, 'JWT'
        except JWT.ExpiredSignatureError:
            raise AuthError('Token expired', 401)
        except JWT.InvalidTokenError as e:
            raise AuthError(f'Invalid token: {e}', 403)

    def _verify_API_key(self, API_key):
        """Verify API key."""
        client = self.API_keys.get(API_key)
        if not client:
            raise AuthError('Invalid API key', 403)
        user = {
            'id': f"API:{client['client']}",
            'client': client['client'],
            'role': client['role'],
        }
        print(f"[AuthGateway] API key auth: {client['client']} ({client['role']})")
        return user, 'API_key'

    def _verify_session(self, session_token):
        """Verify session token (simulated)."""
        # In production: look up session in Redis/database
        if len(session_token) > 20:
            user = {'id': 1, 'email': 'alice@example.com', 'role': 'user'}
            print(f"[AuthGateway] Session auth: user {user['id']}")
            return user, 'session'
        raise AuthError('Invalid session', 401)

    def generate_JWT(self, user_id, email, role='user'):
        """Generate a JWT access token."""
        payload = {
            'sub': user_id,
            'email': email,
            'role': role,
            'type': 'access',
            'iat': int(time.time()),
            'exp': int(time.time()) + 900,  # 15 minutes
        }
        token = JWT.encode(payload, self.JWT_secret, algorithm='HS256')
        print(f"[AuthGateway] Generated JWT for user {user_id}")
        return token

class AuthError(Exception):
    def __init__(self, message, status_code=401):
        super().__init__(message)
        self.status_code = status_code

# ── Usage ──
gateway = AuthGateway()

# Test: JWT authentication
JWT_token = gateway.generate_JWT(1, 'alice@example.com', 'admin')
request = {'headers': {'authorization': f'Bearer {JWT_token}'}}
user, method = gateway.authenticate_request(request)
print(f"Authenticated as {user['email']} via {method}\n")

# Test: API key authentication
request = {'headers': {'x-API-key': 'sk_live_abc123'}}
user, method = gateway.authenticate_request(request)
print(f"Authenticated as {user['client']} via {method}\n")

# Test: No authentication
try:
    request = {'headers': {}}
    gateway.authenticate_request(request)
except AuthError as e:
    print(f"Auth failed: {e} (status {e.status_code})")

Expected output:

[AuthGateway] Generated JWT for user 1
[AuthGateway] JWT auth: user 1 (admin)
Authenticated as alice@example.com via jwt

[AuthGateway] API key auth: Acme Corp (admin)
Authenticated as Acme Corp via api_key

Auth failed: No authentication provided (status 401)

Congratulations on completing this authentication patterns tutorial! Next, explore backend security best practices for protecting authentication endpoints, then learn about caching strategies for session store optimization.

  • Practice daily — Implement JWT authentication in a small Express or Flask app
  • Build a project — Build a complete authentication system with JWT, OAuth 2.0 integration, and MFA support
  • Explore related topics — Check out Auth0, Firebase Authentication, Okta, and Keycloak for managed identity solutions

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro