DEV Community

Cover image for Securing Apps: Password Hashing, RBAC, OAuth, and OpenID Connect
Akash Kumar
Akash Kumar

Posted on

Securing Apps: Password Hashing, RBAC, OAuth, and OpenID Connect

"Security isn't a feature you add at the end. It's the foundation you build everything else on top of."


Introduction: How Does a Website Know Who You Are?

Think about the last time you logged into a website. You typed your email and password, clicked a button, and suddenly the app knew who you were — your name appeared, your data loaded, your settings were right.

That moment involves several invisible systems working together. How did the server verify your password without storing it somewhere a hacker could read? How did it decide what you're allowed to see? How does "Login with Google" work without giving Google your password?

These questions touch on the core of application security. This guide answers all of them — from the fundamentals of password storage through modern authentication architectures used by the world's largest platforms.


1. Why Application Security Matters

Common Security Risks in Web Applications

Web applications face a consistent set of threats:

Credential theft — Attackers obtain usernames and passwords, either by breaching a database or by tricking users through phishing.

Privilege escalation — A regular user gains access to admin functionality by manipulating requests or exploiting missing authorization checks.

Session hijacking — An attacker steals an active session token and impersonates a legitimate user without ever knowing their password.

Injection attacks — Malicious input is used to manipulate database queries, expose data, or execute unauthorized commands.

Credential stuffing — Attackers use leaked username/password combinations from one breach to attempt logins on hundreds of other services, banking on users reusing passwords.

Authentication vs Authorization: Two Different Problems

These words are often used interchangeably, but they represent distinct concerns:

AUTHENTICATION                        AUTHORIZATION
"Who are you?"                        "What are you allowed to do?"

Verifying identity                    Enforcing permissions
Login with password, biometric, token Role-based access, feature flags
Happens first                         Happens after authentication

Example: Proving you're Alice         Example: Alice can edit posts
         at the company entrance                 but not delete them
Enter fullscreen mode Exit fullscreen mode

A system can authenticate perfectly and still authorize incorrectly. A user with a valid login might be able to access another user's data if authorization isn't enforced on every request.

The Cost of Insecure Systems

Security failures are expensive — measured in money, reputation, and legal liability:

LinkedIn (2012) — 117 million password hashes leaked. Passwords were hashed with unsalted MD5, a weak algorithm, allowing attackers to crack millions of them within days. LinkedIn paid $1.25 million in a class action settlement.

Adobe (2013) — 153 million user records exposed, including passwords encrypted (not hashed) with a weak, reversible algorithm. Because the same "encrypted" value was used for identical passwords, attackers could cross-reference records to crack them.

RockYou2024 (2024) — Nearly 10 billion unique plaintext passwords compiled from decades of breaches and released publicly, fuelling credential stuffing attacks across the web.

The pattern across every major breach: passwords were either stored in plain text, hashed with outdated algorithms, or hashed without salting. All three are preventable.


2. Password Hashing and Storage

Why Passwords Should Never Be Stored in Plain Text

Storing a plain-text password means a database breach immediately hands attackers every user's password, verbatim. They don't need to crack anything — they just read the column.

The same applies to reversible encryption. If your server can decrypt a password to verify it, an attacker with access to the encryption key can decrypt every password in the database.

The correct approach is one-way hashing — a transformation that can't be reversed.

What Hashing Is

A hash function converts input of any length into a fixed-length output (the hash or digest). The same input always produces the same output, but the process is irreversible — you cannot recover the original input from the hash.

"mypassword"  ──►  bcrypt  ──►  $2b$12$...rFqLbHFSEKkQo7f7qAW6u
"mypassword"  ──►  bcrypt  ──►  $2b$12$...rFqLbHFSEKkQo7f7qAW6u  ← same
"myPassword"  ──►  bcrypt  ──►  $2b$12$...t8ZUkPqNzDqFvLxm1Y3Xi  ← different
Enter fullscreen mode Exit fullscreen mode

The comparison direction matters. To verify a password at login:

User types password  ──►  hash it  ──►  compare to stored hash
                                         ↑
                         The stored hash never needs to be "undecrypted"
Enter fullscreen mode Exit fullscreen mode

The Problem with Simple Hashing: Rainbow Tables

If you hash "password" with SHA-256 on any computer in the world, you get the same result. Attackers exploit this by precomputing hashes for millions of common passwords — a rainbow table — and comparing them against leaked hashes.

Two more problems arise with naive hashing:

Identical passwords produce identical hashes. If Alice and Bob both use "hunter2", their hashes are identical. Crack one, crack both.

Fast hashing algorithms are a liability. SHA-256 and MD5 were designed to be fast. Modern hardware can compute billions of SHA-256 hashes per second — making brute force attacks tractable.

How bcrypt Solves Both Problems

bcrypt is a password hashing function designed specifically for storing passwords. It addresses every weakness of naive hashing:

1. Salting — defeats rainbow tables and identical-password leaks

Before hashing, bcrypt generates a random value called a salt and incorporates it into the hash. The salt is stored alongside the hash (it doesn't need to be secret — its job is uniqueness, not secrecy).

PASSWORD HASHING WITH SALT

User A: "hunter2" + salt_A  ──►  hash_A   ← unique
User B: "hunter2" + salt_B  ──►  hash_B   ← different despite same password

Each user's hash is unique, even with an identical password.
Rainbow tables are useless — attackers would need one per salt.
Enter fullscreen mode Exit fullscreen mode

2. Work factor — defeats brute force

bcrypt includes a configurable cost factor (also called work factor or rounds). Higher rounds mean more computation per hash — making each attempt slower. This is intentional.

COST FACTOR IMPACT (approximate on modern hardware)

cost=10  →  ~100ms per hash   ← 10 attempts/sec for attacker
cost=12  →  ~400ms per hash   ← 2.5 attempts/sec for attacker
cost=14  →  ~1500ms per hash  ← <1 attempt/sec for attacker

For users: a 100–400ms login is imperceptible
For attackers: 100ms × billions of attempts = years
Enter fullscreen mode Exit fullscreen mode

The cost factor can be increased over time as hardware improves, without invalidating existing hashes — just re-hash passwords when users log in next.

3. The full bcrypt hash contains everything needed for verification

$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
 ↑   ↑  ↑                   ↑
 |   |  salt (22 chars)      hash (31 chars)
 |   cost factor (12 rounds)
 algorithm version (2b)
Enter fullscreen mode Exit fullscreen mode

No separate lookup table needed — everything required to verify the password is in the hash string itself.

Password Verification Process

REGISTRATION                          LOGIN

User: "mysecretpassword"              User: "mysecretpassword"
         │                                      │
         ▼                                      ▼
  bcrypt.hash(password, 12)           bcrypt.compare(
         │                              inputPassword,
         ▼                              storedHash
  $2b$12$...Kq7mN8pLrXt              )
         │                                      │
         ▼                                      ▼
  Store in database                    true / false
  (never store plain text)
Enter fullscreen mode Exit fullscreen mode
const bcrypt = require("bcrypt");

// Registration — hash before storing
async function registerUser(email, plainPassword) {
  const SALT_ROUNDS = 12;
  const hashedPassword = await bcrypt.hash(plainPassword, SALT_ROUNDS);

  await db.users.create({ email, password: hashedPassword });
  // Store hashedPassword — NEVER plainPassword
}

// Login — compare without decrypting
async function loginUser(email, plainPassword) {
  const user = await db.users.findByEmail(email);
  if (!user) return null; // User not found

  const isMatch = await bcrypt.compare(plainPassword, user.password);
  if (!isMatch) return null; // Wrong password

  return user; // Authenticated
}
Enter fullscreen mode Exit fullscreen mode

Best Practices for Password Storage

DO                                    DO NOT
─────────────────────────────────────────────────────────────────
Use bcrypt, scrypt, or Argon2         Use MD5, SHA-1, SHA-256 alone
Use cost factor ≥ 12 for bcrypt       Store plain text passwords
Let bcrypt generate the salt          Use reversible encryption
Return generic error messages         Indicate whether email or
("Invalid credentials")               password was wrong specifically
Re-hash on login if cost is outdated  Log passwords anywhere
Enforce a minimum password length     Truncate long passwords
Enter fullscreen mode Exit fullscreen mode

3. Authentication vs Authorization

What Authentication Means

Authentication is the process of verifying that a user is who they claim to be. It answers the question: "Are you really Alice?"

Authentication happens at the boundary of your system — when a user presents credentials and the system decides whether those credentials are valid.

Authentication methods include:

  • Something you know: password, PIN, security question
  • Something you have: OTP from an authenticator app, SMS code, hardware key
  • Something you are: fingerprint, face ID, voice

Multi-factor authentication (MFA) requires two or more of these — so a stolen password alone isn't enough.

What Authorization Means

Authorization determines what an authenticated user is allowed to do. It answers the question: "Alice is authenticated — what can she access?"

Authorization is checked continuously — on every request, every route, every API call. It's not a one-time gate at login; it's a question asked every time a resource is touched.

Authorization is not:
  "Did Alice log in?" → That's authentication.

Authorization is:
  "Is Alice allowed to DELETE /posts/42?" → That's authorization.
  "Can Alice see the billing dashboard?" → That's authorization.
  "Is Alice's subscription tier high enough?" → That's authorization.
Enter fullscreen mode Exit fullscreen mode

Why Applications Need Both

AUTHENTICATION + AUTHORIZATION TOGETHER

Without authentication:
  Anyone can claim to be Alice → impersonation

Without authorization:
  Alice can access Bob's data, admin panels, billing records

Without both:
  Anyone can do anything → the application is open

With both:
  ✓ Identity is verified at login
  ✓ Permissions are enforced on every request
  ✓ Users see only what they're allowed to see
Enter fullscreen mode Exit fullscreen mode

Everyday Examples

Application Authentication Authorization
Gmail Password + 2FA Read your own emails; not others'
Notion Google SSO Viewers can read; editors can write
Stripe Dashboard Password + hardware key Developers see logs; Owners see financials
GitHub Username + SSH key Collaborators can push; Maintainers can merge
Hospital system Badge + PIN Nurses see patient notes; Doctors see prescriptions

4. Role-Based Access Control (RBAC)

What RBAC Is

Role-Based Access Control is an authorization model where permissions are assigned to roles, and roles are assigned to users. Instead of granting individual permissions directly to each user, you define roles that represent job functions and assign users to those roles.

WITHOUT RBAC (permission per user — doesn't scale)
  Alice: [read_posts, write_posts, delete_posts, manage_users, view_analytics]
  Bob:   [read_posts, write_posts]
  Carol: [read_posts, write_posts, delete_posts, view_analytics]

WITH RBAC (permission per role — scales cleanly)
  ROLE admin:      [read_posts, write_posts, delete_posts, manage_users, view_analytics]
  ROLE editor:     [read_posts, write_posts, delete_posts, view_analytics]
  ROLE writer:     [read_posts, write_posts]
  ROLE viewer:     [read_posts]

  Alice → admin
  Bob   → writer
  Carol → editor
Enter fullscreen mode Exit fullscreen mode

When Alice's role changes from admin to editor, you update one assignment — not a list of individual permissions. When a new permission is created, you add it to the role — all users in that role immediately have it.

Users, Roles, and Permissions

The RBAC model has three building blocks:

User — A person with an account in the system.

Role — A named collection of permissions, representing a job function or access level (admin, editor, billing_manager, support_agent).

Permission — A specific action on a specific resource (read:posts, delete:users, view:billing, publish:articles).

The relationship chain is: User → has Role → has Permissions

RBAC ENTITY RELATIONSHIP

┌─────────┐     assigned      ┌──────────┐    contains    ┌────────────────┐
│  User   │ ────────────────► │   Role   │ ─────────────► │  Permission    │
│         │     (many-to-many)│          │  (many-to-many)│                │
│ Alice   │                   │  admin   │                │ delete:posts   │
│ Bob     │                   │  editor  │                │ manage:users   │
│ Carol   │                   │  viewer  │                │ view:analytics │
└─────────┘                   └──────────┘                └────────────────┘
Enter fullscreen mode Exit fullscreen mode

Role Hierarchy Example: Blog Platform

ROLE HIERARCHY — BLOG PLATFORM

         ┌──────────────────────────────────────┐
         │                ADMIN                 │
         │  manage users · manage roles         │
         │  delete any content · view analytics │
         │  all editor permissions              │
         └──────────────────┬───────────────────┘
                            │ inherits from
         ┌──────────────────▼───────────────────┐
         │               EDITOR                 │
         │  publish articles · delete own posts │
         │  moderate comments · view analytics  │
         │  all writer permissions              │
         └──────────────────┬───────────────────┘
                            │ inherits from
         ┌──────────────────▼───────────────────┐
         │               WRITER                 │
         │  create posts · edit own posts       │
         │  upload images                       │
         │  all viewer permissions              │
         └──────────────────┬───────────────────┘
                            │ inherits from
         ┌──────────────────▼───────────────────┐
         │               VIEWER                 │
         │  read published articles             │
         │  post comments                       │
         └──────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Implementing RBAC in Express

// Middleware to check role
function requireRole(...allowedRoles) {
  return (req, res, next) => {
    const userRole = req.user?.role; // Set during authentication

    if (!userRole || !allowedRoles.includes(userRole)) {
      return res.status(403).json({
        error: "Forbidden — insufficient permissions"
      });
    }

    next();
  };
}

// Permission-level check (more granular than role check)
function requirePermission(permission) {
  return (req, res, next) => {
    const userPermissions = req.user?.permissions || [];

    if (!userPermissions.includes(permission)) {
      return res.status(403).json({
        error: "Forbidden — missing required permission"
      });
    }

    next();
  };
}

// Route protection examples
app.get("/admin/users",
  requireRole("admin"),               // only admins
  listAllUsers
);

app.delete("/posts/:id",
  requireRole("admin", "editor"),     // admins and editors
  deletePost
);

app.post("/posts",
  requireRole("admin", "editor", "writer"),
  createPost
);

app.get("/posts",                     // no role check — public
  listPosts
);
Enter fullscreen mode Exit fullscreen mode

Scaling Permissions in Large Applications

As applications grow, flat role lists become insufficient. Common patterns for scaling:

Attribute-Based Access Control (ABAC) — Permissions based on attributes of the user, the resource, and the environment. "Alice can edit this document because she's in the Engineering team and this document's department is Engineering."

Resource ownership — Users can always perform certain actions on resources they created, regardless of their global role.

Scoped permissions — Permissions limited to a specific organization, workspace, or tenant. Alice might be admin in workspace A but only viewer in workspace B.


5. OAuth 2.0 Explained

The Problem OAuth Solves

Before OAuth, the only way for one application to access your data in another was to give it your username and password. Want a printing service to access your Google Photos? Type your Google password into the printing service's form.

The problems with this approach are obvious:

PROBLEMS WITH SHARING PASSWORDS

1. You expose your entire account to the third party
   → They could read your emails, see all your files, access everything

2. You have no way to limit what they can access
   → There's no "give access to photos only"

3. You can't revoke access without changing your password
   → Which logs out every other service using that password

4. You can't tell what the third party is doing with your credentials
   → No audit trail of their actions
Enter fullscreen mode Exit fullscreen mode

OAuth 2.0 is an authorization framework that lets users grant third-party applications limited, scoped access to their account — without sharing their password.

The Four Actors in OAuth

OAUTH 2.0 ACTORS

┌──────────────────┐    owns data    ┌──────────────────┐
│  Resource Owner  │ ─────────────── │ Resource Server  │
│   (the user)     │                 │  (Google Photos, │
│                  │                 │   GitHub, etc.)  │
└────────┬─────────┘                 └──────────────────┘
         │
         │ grants permission to
         │
┌────────▼─────────┐  gets token from ┌──────────────────┐
│  Client App      │ ──────────────── │  Authorization   │
│ (the third-party │                  │     Server       │
│  application)    │                  │  (issues tokens) │
└──────────────────┘                  └──────────────────┘
Enter fullscreen mode Exit fullscreen mode

Resource Owner — The user who owns the data and decides what to share.

Client Application — The third-party app requesting access (a photo editor, a code analysis tool, a calendar integration).

Authorization Server — The server that authenticates the user and issues access tokens (Google's auth server, GitHub's auth server, your own identity provider).

Resource Server — The API server holding the actual data (Google Photos API, GitHub API). Often the same system as the authorization server.

The Authorization Code Flow

The most common OAuth flow for web applications:

OAUTH 2.0 — AUTHORIZATION CODE FLOW

  User                Client App           Auth Server        Resource Server
    │                     │                    │                    │
    │  1. Click           │                    │                    │
    │  "Login with        │                    │                    │
    │   Google"           │                    │                    │
    │ ─────────────────► │                    │                    │
    │                     │  2. Redirect to    │                    │
    │                     │  auth server with  │                    │
    │                     │  scope + client_id │                    │
    │                     │ ─────────────────► │                    │
    │                     │                    │                    │
    │  3. Google shows consent screen          │                    │
    │ ◄──────────────────────────────────────  │                    │
    │                     │                    │                    │
    │  4. User approves   │                    │                    │
    │ ─────────────────────────────────────── ►│                    │
    │                     │                    │                    │
    │                     │  5. Auth server sends authorization code│
    │                     │ ◄───────────────── │                    │
    │                     │                    │                    │
    │                     │  6. Client exchanges code for           │
    │                     │     access token (server-to-server)     │
    │                     │ ─────────────────► │                    │
    │                     │                    │                    │
    │                     │  7. Access token   │                    │
    │                     │ ◄───────────────── │                    │
    │                     │                    │                    │
    │                     │  8. Use access token to call API        │
    │                     │ ──────────────────────────────────────► │
    │                     │                    │                    │
    │                     │  9. Protected resource data             │
    │                     │ ◄────────────────────────────────────── │
Enter fullscreen mode Exit fullscreen mode

Access Tokens and Scopes

An access token is a credential that represents the user's grant of access to the client application. It's typically a signed string (often a JWT) that the resource server validates on every API call.

Scopes limit what the token can access. The client declares which scopes it needs, the user approves them, and the token is bound to those scopes:

Common OAuth scopes:

GitHub:   repo          → read/write repos
          read:user     → read profile info
          gist          → create gists
          notifications → read notifications

Google:   email              → read email address
          profile            → read basic profile info
          https://www.googleapis.com/auth/calendar → full calendar access
          https://www.googleapis.com/auth/drive.readonly → read-only Drive
Enter fullscreen mode Exit fullscreen mode

The principle: request only the scopes you need. A tool that reads your GitHub issues doesn't need repo (which would let it push code).

Real-World Example: "Login with GitHub"

What the user sees:
  "Would you like to authorize CodeAnalyzer to:
   ✓ Read your public profile information
   ✓ Access your public repositories
   Authorize App | Cancel"

What's happening behind the scenes:
  → Client requests scopes: ["read:user", "public_repo"]
  → User approves on GitHub's own server
  → Client receives access token
  → Client can now call GitHub API with that token
  → Client CANNOT read your emails, private repos, or billing info
  → You can revoke this token any time in GitHub Settings → Applications
Enter fullscreen mode Exit fullscreen mode

6. OpenID Connect (OIDC)

What OAuth 2.0 Is Missing

OAuth 2.0 solves authorization — it lets applications access resources on your behalf. But it doesn't answer an important question: who are you?

OAuth provides an access token that says "the bearer of this token is allowed to read photos." But it doesn't say whose photos, or anything about the user's identity. Applications had to hack around this by calling a separate /me or /userinfo endpoint — and every provider implemented it differently.

OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0 that standardizes the answer to "who are you?" It adds authentication to OAuth's authorization.

How OIDC Extends OAuth

OIDC adds one critical thing to the OAuth flow: an ID token.

OAUTH 2.0 (authorization only)
  → Access token: "I can read your photos"

OPENID CONNECT (authorization + authentication)
  → Access token: "I can read your photos"    ← same as OAuth
  → ID token:     "You are Alice, born 1990, email alice@example.com"  ← new
Enter fullscreen mode Exit fullscreen mode

The ID token is a JWT (JSON Web Token) — a signed, structured object that contains claims about the user's identity:

{
  "iss": "https://accounts.google.com",
  "sub": "10769150350006150715113082367",
  "aud": "your-client-id.apps.googleusercontent.com",
  "exp": 1735689600,
  "iat": 1735686000,
  "name": "Alice Smith",
  "email": "alice@example.com",
  "picture": "https://lh3.googleusercontent.com/...",
  "email_verified": true
}
Enter fullscreen mode Exit fullscreen mode

These fields are called claims. The application can trust them because the ID token is cryptographically signed by the identity provider.

The OIDC Flow

OPENID CONNECT FLOW

1. Client redirects user to identity provider with:
   scope: openid profile email   ← "openid" scope triggers OIDC

2. User authenticates with their identity provider
   (enters password, uses biometric, passes MFA)

3. Identity provider returns:
   → Authorization code (same as OAuth)

4. Client exchanges code for:
   → Access token  (for calling APIs)
   → ID token      (contains user identity)
   → Refresh token (optional, for long sessions)

5. Client validates the ID token signature using
   the identity provider's public key

6. Client extracts user identity from ID token claims:
   sub = "unique user ID" (never changes for this user)
   email = "alice@example.com"
   name = "Alice Smith"

7. Client creates a local session for Alice
Enter fullscreen mode Exit fullscreen mode

Authentication vs Authorization in OIDC

IN AN OIDC SYSTEM:

ID token       = authentication result
               → "This is Alice" (signed by Google/Okta/your IdP)

Access token   = authorization grant
               → "Alice allowed this app to read her calendar"

Refresh token  = session extension mechanism
               → "Silently get a new access token when this one expires"
Enter fullscreen mode Exit fullscreen mode

Why OIDC Matters for Modern Applications

Before OIDC, "Login with Google" wasn't a standard — every provider (Google, Facebook, Twitter) had different endpoints, different response formats, different claim names. Developers had to write separate integration code for each.

OIDC standardizes everything. An application built to OIDC spec works with any compliant identity provider — Google, Microsoft, Okta, Auth0, GitHub, your own corporate identity server — by changing configuration, not code.


7. Modern Authentication Architecture

From Traditional to Modern

TRADITIONAL LOGIN SYSTEM (2000s)

  Browser ──────────────────► Your Server
                               ├── Check username/password
                               ├── Create server-side session
                               ├── Set session cookie
                               └── Store session in memory/DB

  Every request:
  Browser sends cookie ──► Server looks up session ──► Get user data

  Problems:
  → Sessions stored on server (memory/database) — hard to scale
  → Works per-domain only — can't share login across subdomains or apps
  → Each application manages its own user database and auth code
Enter fullscreen mode Exit fullscreen mode
MODERN TOKEN-BASED SYSTEM (today)

  Browser ──────────────────► Auth Server (dedicated)
                               ├── Verify credentials
                               ├── Issue JWT access token (signed)
                               └── Issue refresh token

  Every API request:
  Browser sends JWT ──► API validates signature (stateless) ──► Serve data

  Advantages:
  → Stateless — no server-side session storage
  → Scalable — any server can validate the signature
  → Cross-domain — token works across subdomains and APIs
Enter fullscreen mode Exit fullscreen mode

Social Login

Social login (Login with Google/GitHub/Apple) uses OIDC:

SOCIAL LOGIN ARCHITECTURE

  Your App                Google OIDC               Your Database
     │                       │                           │
     │  1. Redirect to       │                           │
     │  Google with          │                           │
     │  scope=openid email   │                           │
     │ ────────────────────► │                           │
     │                       │  2. User logs in to       │
     │                       │  Google (not your app)    │
     │  3. ID token          │                           │
     │ ◄──────────────────── │                           │
     │                       │                           │
     │  4. Extract sub (Google's unique user ID)         │
     │ ─────────────────────────────────────────────────►│
     │                       │                           │
     │  5. Find or create user record by sub             │
     │ ◄─────────────────────────────────────────────────│
     │                       │                           │
     │  6. Issue your own session/JWT for the user       │
Enter fullscreen mode Exit fullscreen mode

You never see the user's Google password. You receive a verified, signed assertion from Google that this person authenticated successfully and here is their identity.

Single Sign-On (SSO)

SSO lets users authenticate once and access multiple applications without logging in again.

SINGLE SIGN-ON ARCHITECTURE

                        ┌─────────────────────┐
                        │   Identity Provider  │
                        │       (IdP)          │
                        │  (Okta, Azure AD,   │
                        │   Google Workspace) │
                        └──────────┬──────────┘
                                   │ issues tokens
                    ┌──────────────┼──────────────┐
                    │              │              │
             ┌──────▼──────┐ ┌────▼──────┐ ┌────▼──────┐
             │    CRM App  │ │ HR Portal │ │  Dev Tools│
             │  (Salesforce)│ │  (Workday)│ │  (GitHub) │
             └─────────────┘ └───────────┘ └───────────┘

User logs in once to the IdP → all three apps recognize the session
Log out from IdP → logged out from all three simultaneously
Enter fullscreen mode Exit fullscreen mode

SSO is standard in enterprise environments. Employees authenticate with corporate credentials once, and all internal tools (email, project management, HR, developer tools) share that authentication.

Authentication in Modern SaaS Products

MULTI-TENANT SAAS AUTHENTICATION

  Personal accounts:    Email/password + MFA, social login (OIDC)
  Team accounts:        SSO with company IdP (Okta, Azure AD)
  API access:           API keys or OAuth tokens with scopes
  Machine-to-machine:   OAuth client credentials flow (no user involved)
  Administrative:       Separate admin portal, hardware MFA required
Enter fullscreen mode Exit fullscreen mode

8. Security Best Practices

Strong Password Policies

MINIMUM PASSWORD REQUIREMENTS (modern guidance)

Length:       12+ characters minimum (NIST recommends 14-64)
Characters:   Allow all printable characters + spaces
Complexity:   Don't mandate special chars — encourages predictable patterns
              (P@ssw0rd! is weaker than a long passphrase)
Blocklist:    Reject passwords found in breach databases (HaveIBeenPwned)
Rotation:     Don't force periodic rotation — causes weak patterns
MFA:          Treat MFA as a stronger control than complex passwords
Enter fullscreen mode Exit fullscreen mode

Multi-Factor Authentication (MFA)

MFA requires a second factor beyond the password. Even if a password is leaked, an attacker without the second factor can't log in.

Factor Type Example Strength
SMS code 6-digit code via text Low (SIM swapping)
TOTP app Google Authenticator, Authy Medium-high
Hardware key YubiKey, FIDO2 Very high
Push notification Duo, Okta Verify High
Passkey (biometric) Face ID, Windows Hello Very high

For high-security routes (admin panels, payment operations, account changes), require re-authentication or step-up MFA even for already-authenticated sessions.

Token Expiration and Refresh Strategy

TOKEN LIFECYCLE

  Access token:   Short-lived (15 minutes – 1 hour)
                  Used for every API request
                  If stolen: attacker has access for a short window only

  Refresh token:  Long-lived (days to weeks)
                  Stored securely (HTTP-only cookie, secure storage)
                  Used only to get new access tokens
                  If stolen: attacker can get new access tokens

  Rotation:       Issue a new refresh token on every refresh
                  Invalidate the old one
                  If refresh token is reused → detect theft, revoke all sessions

  Revocation:     Maintain a token revocation list (blocklist) for
                  logout, password change, or suspected compromise
Enter fullscreen mode Exit fullscreen mode
// Access token payload — keep it small
{
  "sub": "user-123",           // user identifier
  "role": "editor",            // for authorization
  "iat": 1735686000,           // issued at
  "exp": 1735689600            // expires in 1 hour
}

// What NOT to include in tokens
// ❌ password hash
// ❌ sensitive personal data (full SSN, credit card)
// ❌ large permission lists (keep tokens small)
Enter fullscreen mode Exit fullscreen mode

Principle of Least Privilege

Every user, role, service, and API key should have the minimum permissions required to perform its function — nothing more.

PRINCIPLE OF LEAST PRIVILEGE IN PRACTICE

API keys:
  ❌  One master API key with full access for all services
  ✅  Separate key per service, scoped to only what it needs

Service accounts:
  ❌  All microservices run as admin
  ✅  Each microservice has a role with only its required permissions

User roles:
  ❌  Default new users to admin "for convenience"
  ✅  Default new users to viewer; elevate permissions explicitly

OAuth scopes:
  ❌  Request all available scopes at login
  ✅  Request only scopes needed for current features
  ✅  Request sensitive scopes only when needed (incremental auth)
Enter fullscreen mode Exit fullscreen mode

Protecting Sensitive Routes

// Layer multiple controls on sensitive endpoints
app.delete("/admin/users/:id",
  authenticate,                      // 1. Must be logged in
  requireMFA,                        // 2. Must have MFA verified recently
  requireRole("admin"),              // 3. Must be admin
  requirePermission("delete:users"), // 4. Must have specific permission
  logAuditEvent("user.deleted"),     // 5. Log every admin action
  deleteUser
);

// Protect against CSRF on state-changing requests
app.use(csrf({ cookie: true }));

// Rate-limit login endpoint to prevent brute force
app.use("/auth/login", rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 10                    // 10 attempts per window
}));

// Always use HTTPS — HTTP leaks tokens in transit
app.use((req, res, next) => {
  if (!req.secure) return res.redirect(`https://${req.host}${req.url}`);
  next();
});
Enter fullscreen mode Exit fullscreen mode

Secure Storage of Credentials

WHERE TO STORE TOKENS (CLIENT SIDE)

                localStorage    sessionStorage    HTTP-only Cookie
──────────────────────────────────────────────────────────────────
XSS risk          HIGH            HIGH              NONE
CSRF risk         NONE            NONE              YES (mitigate)
Persists          Yes             Tab only          Configurable
Server control    No              No                Yes (expire/revoke)
Recommended for   Short-lived     Temporary         Refresh tokens
                  non-sensitive   UI state          Long sessions
──────────────────────────────────────────────────────────────────

Best practice:
  Access tokens  → Short-lived, memory only (JS variable, not storage)
  Refresh tokens → HTTP-only, Secure, SameSite=Strict cookie
  API keys       → Environment variables (never in code, never in git)
  Secrets        → Secret management service (AWS Secrets Manager, Vault)
Enter fullscreen mode Exit fullscreen mode

Security Layers in a Complete Application

DEFENSE IN DEPTH — SECURITY LAYERS

   Request arrives
         │
   ┌─────▼────────────────────────────────────┐
   │  1. TRANSPORT LAYER                      │
   │     HTTPS/TLS — encrypt data in transit  │
   └─────┬────────────────────────────────────┘
         │
   ┌─────▼────────────────────────────────────┐
   │  2. NETWORK LAYER                        │
   │     Rate limiting · IP allowlisting      │
   │     DDoS protection · WAF rules          │
   └─────┬────────────────────────────────────┘
         │
   ┌─────▼────────────────────────────────────┐
   │  3. AUTHENTICATION LAYER                 │
   │     Verify identity · Validate token     │
   │     Check MFA · Detect anomalies         │
   └─────┬────────────────────────────────────┘
         │
   ┌─────▼────────────────────────────────────┐
   │  4. AUTHORIZATION LAYER                  │
   │     Check role · Verify permissions      │
   │     Enforce resource ownership           │
   └─────┬────────────────────────────────────┘
         │
   ┌─────▼────────────────────────────────────┐
   │  5. INPUT VALIDATION LAYER               │
   │     Sanitize input · Validate types      │
   │     Prevent injection · Schema validate  │
   └─────┬────────────────────────────────────┘
         │
   ┌─────▼────────────────────────────────────┐
   │  6. DATA LAYER                           │
   │     Parameterized queries · Encryption   │
   │     Hashed passwords · Minimal exposure  │
   └─────┬────────────────────────────────────┘
         │
   ┌─────▼────────────────────────────────────┐
   │  7. AUDIT LAYER                          │
   │     Log all sensitive actions            │
   │     Alert on anomalies · Retain logs     │
   └──────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

No single layer is impenetrable. Defense in depth means an attacker who bypasses one layer still faces all the others.


Quick Reference

Concept One-line definition
Authentication Verifying who a user is
Authorization Controlling what a user can do
Hashing One-way transformation of a value
Salt Random value added before hashing to prevent rainbow tables
bcrypt Password hashing function with configurable work factor
RBAC Permissions assigned to roles, roles assigned to users
OAuth 2.0 Framework for delegated authorization without sharing passwords
OIDC Identity layer on top of OAuth that standardizes user authentication
Access token Short-lived credential authorizing API access
Refresh token Long-lived credential used to obtain new access tokens
ID token JWT containing verified claims about user identity (OIDC)
SSO Authenticate once, access multiple applications
MFA Require two or more authentication factors
Scope A permission boundary on an OAuth token
JWT Signed, structured token format used for access and ID tokens

Key Takeaways

  • Authentication and authorization are separate problems — both are required, and authorization must be checked on every request, not just at login
  • Never store plain-text passwords — use bcrypt (cost ≥ 12), which handles salting automatically and is intentionally slow to resist brute force
  • OAuth 2.0 enables delegated authorization — users grant third-party apps scoped access without sharing their password
  • OpenID Connect adds authentication to OAuth — the ID token carries verified user identity claims in a standardized format
  • RBAC decouples permissions from users — assign permissions to roles, assign roles to users; changing a role changes everyone in it
  • Defense in depth — security must be enforced at transport, authentication, authorization, input validation, and data layers
  • Principle of least privilege — every user, service, and token should have the minimum access required
  • Short access tokens, secure refresh tokens — access tokens expire quickly; refresh tokens live longer but are stored in HTTP-only cookies

What's Next?

With a solid foundation in application security, these topics extend naturally:

  • JWT deep dive — the structure of JSON Web Tokens, signature verification, and common attack vectors (algorithm confusion, key confusion)
  • PKCE (Proof Key for Code Exchange) — hardening the OAuth flow for public clients like mobile apps and SPAs
  • Passkeys and WebAuthn — the passwordless future: device-based cryptographic authentication that eliminates phishing
  • Zero-trust architecture — never trust, always verify: extending least privilege across networks, services, and identities
  • OWASP Top 10 — the ten most critical web application security risks, with mitigation strategies for each

Security is not a checklist you complete once — it's a continuous practice of understanding threats, applying defenses, and monitoring for what slips through.


"The best security is the kind users never notice — they just feel safe." 🔐

Top comments (0)