Module: Nodejs
Nodejs·128·6 MIN READ

128: Authentication and Authorization in Node — Passwords, Sessions, JWTs, OAuth/OIDC, RBAC, and ABAC

TOPICS COVERED: Authentication and Authorization in Node — Passwords, Sessions, JWTs, OAuth/OIDC, RBAC, and ABAC

Learning objectives

You will learn to:

  • distinguish authentication, session management, and authorization;
  • hash passwords safely;
  • understand server-side sessions;
  • understand JWT access tokens without treating them as magic;
  • verify token claims correctly;
  • understand refresh-token/session rotation concepts;
  • understand OAuth 2.0 and OpenID Connect at a practical level;
  • design RBAC and ABAC checks;
  • prevent IDOR/BOLA with resource-scoped authorization;
  • understand service-to-service authentication;
  • design logout/revocation behavior;
  • avoid putting trust in client claims that the server can derive.

Three separate questions

Authentication

Who is making this request?

Authorization

Is this principal allowed to perform this operation on this resource?

Session management

How do we maintain authenticated state across requests?

Do not collapse these into “JWT authentication.”

Password storage

Never store:

text
plaintext password
reversible encrypted password
plain SHA-256(password)

Use a password hashing function designed to be slow and salted.

Common modern choices:

text
Argon2id
scrypt
bcrypt

Choice depends on security guidance, platform support, parameters, and library maintenance.

Node's built-in crypto includes scrypt.

Scrypt example

js
import {
  randomBytes,
  scrypt as scryptCallback,
  timingSafeEqual,
} from 'node:crypto';

import { promisify } from 'node:util';

const scrypt = promisify(scryptCallback);

export async function hashPassword(password) {
  const salt = randomBytes(16);
  const derived = await scrypt(password, salt, 64);

  return {
    algorithm: 'scrypt',
    salt: salt.toString('base64'),
    hash: Buffer.from(derived).toString('base64'),
  };
}

export async function verifyPassword(password, record) {
  const salt = Buffer.from(record.salt, 'base64');
  const expected = Buffer.from(record.hash, 'base64');

  const actual = Buffer.from(
    await scrypt(password, salt, expected.length),
  );

  return (
    actual.length === expected.length &&
    timingSafeEqual(actual, expected)
  );
}

Production password-storage design also needs:

  • cost parameters;
  • migration when parameters/algorithm change;
  • maximum input length policy;
  • account lockout/rate limiting;
  • breach/password policy as product requires.

Do not copy parameters blindly; follow current security guidance.

Password verification timing

Use library functions designed to avoid timing leaks.

Do not compare secret hashes with naive string logic when a cryptographic comparison is required.

Login flow

text
credentials submitted
↓
lookup account
↓
verify password
↓
check account status
↓
rotate/create authenticated session
↓
return cookie/token
↓
audit login

Avoid revealing whether email exists:

text
"email not found"
"wrong password"

when enumeration is a concern.

Use a generic message:

text
Invalid credentials

while logging safe internal reason.

Server-side sessions

Model:

text
random session id in secure cookie
↓
server session store
↓
user id + metadata

Cookie:

text
Set-Cookie:
session=<random>;
HttpOnly;
Secure;
SameSite=Lax;
Path=/

Server store:

js
{
  sessionId,
  userId,
  createdAt,
  expiresAt,
  lastSeenAt,
  authLevel
}

Session ID must be unpredictable.

Session rotation

Rotate session ID:

  • after login;
  • after privilege elevation;
  • after suspicious events.

This reduces session fixation risk.

Session expiration

Use:

  • idle expiry;
  • absolute expiry;
  • revocation;
  • device/session management.

Do not rely only on cookie expiry if server session remains valid forever.

Logout

Server-side session:

text
delete/revoke session
clear cookie

JWT access token logout is different because signed tokens may remain valid until expiry unless revocation/introspection/session architecture exists.

JWT structure

JWT commonly:

text
header.payload.signature

Payload is encoded, not encrypted by default.

Anyone holding token can often decode claims.

Do not put secrets inside JWT payload.

JWT verification

Must verify:

  • signature;
  • allowed algorithm;
  • issuer (iss);
  • audience (aud);
  • expiration (exp);
  • not-before (nbf) when used;
  • token type/purpose;
  • key rotation/kid policy.

Do not simply:

js
decode(token)

and trust the result.

Decode is not verify.

Algorithm confusion

Use a library that enforces expected algorithms.

Do not accept whatever algorithm token header requests.

Key type/algorithm policy belongs to server configuration.

JWT access token lifetime

Short-lived access tokens reduce damage if stolen.

Refresh tokens/session records can maintain longer login.

But token architecture becomes significantly more complex:

  • rotation;
  • replay detection;
  • revocation;
  • storage;
  • multiple devices.

Do not use refresh tokens just because tutorials do.

For browser authentication, HttpOnly secure cookies can reduce token exposure to injected JavaScript, but introduce CSRF considerations.

localStorage tokens are accessible to page JavaScript and therefore to XSS.

There is no universal one-line answer; choose based on app architecture and threat model.

Do not put long-lived sensitive tokens in insecure storage casually.

Authorization model

RBAC

Role-Based Access Control:

text
admin
manager
cashier
viewer

Permissions:

text
task:create
task:read
task:update
task:delete

Map roles to permissions.

Avoid route code:

js
if (user.role === 'admin') ...

everywhere.

Centralize permission checks.

ABAC

Attribute-Based Access Control evaluates attributes:

text
user.role
user.branchId
resource.ownerId
resource.branchId
resource.status
request.operation
time/environment

Example:

js
function canUpdateTask(actor, task) {
  if (!actor.permissions.has('task:update')) {
    return false;
  }

  if (actor.tenantId !== task.tenantId) {
    return false;
  }

  if (
    actor.branchId !== task.branchId &&
    !actor.permissions.has('task:update:any-branch')
  ) {
    return false;
  }

  return true;
}

This is more precise than a single role string.

Resource-scoped authorization

Danger:

js
const task = await tasks.findById(req.params.id);

if (!task) return 404;

// update

If tenant/user scope missing, attacker can enumerate another tenant's ID.

Safer repository:

js
const task = await tasks.findOne({
  id: taskId,
  tenantId: auth.tenantId,
});

Then permission checks.

Authorization should influence query scope where possible.

404 versus 403

For resources an attacker should not learn exist, some APIs intentionally return 404 for unauthorized cross-tenant IDs.

Within known resource context, 403 can be appropriate.

Define policy consistently.

Authentication middleware

js
async function authenticate(req, res, next) {
  try {
    const session = await sessionService.fromRequest(req);

    if (!session) {
      return res.status(401).json({
        error: {
          code: 'UNAUTHENTICATED',
          message: 'Authentication required.',
        },
      });
    }

    res.locals.auth = {
      userId: session.userId,
      tenantId: session.tenantId,
      permissions: session.permissions,
    };

    next();
  } catch (error) {
    next(error);
  }
}

Do not store full password/token in locals.

Authorization middleware

Generic:

js
function requirePermission(permission) {
  return (req, res, next) => {
    const auth = res.locals.auth;

    if (!auth.permissions.has(permission)) {
      return res.status(403).json(...);
    }

    next();
  };
}

But resource-specific rules often belong after loading scoped resource in service/repository.

Do not reduce all authorization to route-level role middleware.

OAuth 2.0

OAuth is primarily an authorization framework for delegated access.

Roles:

text
resource owner
client
authorization server
resource server

Flows vary by client type.

For modern browser/mobile authorization, Authorization Code + PKCE is common.

Do not implement OAuth protocol from scratch.

Use established identity provider/library.

OpenID Connect

OIDC adds identity/authentication layer on OAuth 2.0.

ID token communicates authentication claims to client.

Access token is for resource server.

Do not send ID token as generic API authorization token unless provider/API specifically defines that use.

External identity provider flow

text
user redirects to IdP
↓
IdP authenticates
↓
authorization code returned
↓
server/client exchanges code securely
↓
verify issuer/audience/state/nonce/PKCE
↓
create local session/account link

State and nonce defend against classes of attacks.

Use framework/provider SDK.

Account linking

Same email from two identity providers does not automatically mean same trusted account.

Define verified-email/account-linking policy.

Account takeover bugs happen when identity linking is too permissive.

MFA

Multi-factor can add:

  • TOTP;
  • WebAuthn/passkeys;
  • recovery codes.

SMS OTP has different security properties.

Sensitive actions may require step-up authentication.

Do not treat “logged in” as equal assurance for every operation.

Passkeys/WebAuthn

Modern passwordless authentication uses public-key credentials.

Server stores public credential data, not private key.

Implementation is protocol-heavy; use audited WebAuthn libraries/identity providers.

Know conceptually because password-only auth is not the only model.

Service-to-service auth

Options:

  • mTLS;
  • signed service tokens;
  • workload identity/cloud IAM;
  • OAuth client credentials;
  • private network plus identity controls.

Do not share one hard-coded API key across every service forever.

Rotate credentials.

API keys

Good for machine/application identification when designed properly.

Store hashed API key server-side where feasible.

Show secret once.

Prefix can identify key record.

Rate-limit and scope permissions.

Do not log full API key.

Webhook authentication

Webhook receiver should verify authenticity:

text
HMAC signature
timestamp
raw request bytes
replay window
secret rotation

Important: signature often covers exact raw body.

If JSON parser modifies body representation before verification, signature verification can fail or become incorrect.

Design webhook route parsing separately.

Replay protection

For signed requests:

  • timestamp;
  • nonce/id;
  • idempotency key;
  • short acceptance window.

Signature alone does not prevent replay of identical valid request.

Authorization test matrix

For DELETE /tasks/:id:

text
no session → 401
valid user no permission → 403
permission wrong tenant → 404/403 policy
permission same tenant → 204
deleted already → idempotent policy
expired session → 401
revoked session → 401

Common mistakes

  • plaintext/fast password hashes;
  • JWT decode without verify;
  • no issuer/audience check;
  • long-lived access token with no revocation story;
  • secret tokens in logs/URLs;
  • client role trusted from request body;
  • role-only authorization;
  • missing tenant/resource scope;
  • OAuth implemented manually;
  • ID token confused with access token;
  • account linking by email without trust checks;
  • API keys without rotation/scope;
  • webhook signature verified after body transformation.

Exercises

  1. Implement scrypt password record and verification.
  2. Design session table/store and rotation.
  3. Compare server session versus JWT access token.
  4. Write JWT verification checklist.
  5. Build permission middleware plus resource-specific ABAC check.
  6. Add tenant scope to repository query.
  7. Draw OAuth Authorization Code + PKCE.
  8. Design API key record with hash/scope.
  9. Verify a webhook HMAC over raw bytes.
  10. Write authorization tests for cross-tenant IDs.

Mastery checklist

Explain:

  • authn/authz/session;
  • password hashing;
  • session rotation;
  • JWT verification;
  • token storage/revocation;
  • RBAC;
  • ABAC;
  • BOLA/IDOR;
  • OAuth/OIDC;
  • service auth;
  • API keys;
  • webhook signatures/replay.

Official references