auth-hardening

v2026.09.25

Harden authentication systems against session fixation, brute-force attacks, and weak password storage. Covers bcrypt/argon2 hashing, JWT best practices, session management, and account lockout. Use when implementing login, fixing auth vulnerabilities, or reviewing authentication code.

GitHub
安装命令
npx skhub add afu-it/auth-hardening
Markdown
SKILL.md

Auth Hardening

Harden authentication to prevent unauthorized access. Weak auth is OWASP A07 and a direct path to PDPA liability -- if attackers access user data through weak login security, the developer is negligent.

The Rule

NEVER roll your own crypto. ALWAYS use proven libraries with secure defaults.

Password Hashing

The Only Acceptable Algorithms

AlgorithmWhen to useConfig
Argon2idNew projects (recommended)memory: 64MB, iterations: 3, parallelism: 4
bcryptExisting projects, wide supportcost factor: 12+
scryptAlternative to Argon2N=2^17, r=8, p=1

NEVER use: MD5, SHA-1, SHA-256 (without salt+stretch), plain text

Node.js

BAD:

import crypto from 'crypto';
const hash = crypto.createHash('sha256').update(password).digest('hex');

GOOD:

import { hash, verify } from '@node-rs/argon2';

// Hash on registration
const passwordHash = await hash(password, {
  memoryCost: 65536,  // 64MB
  timeCost: 3,
  parallelism: 4,
});

// Verify on login
const isValid = await verify(passwordHash, password);

Alternative with bcrypt:

import bcrypt from 'bcrypt';

const SALT_ROUNDS = 12;
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
const isValid = await bcrypt.compare(password, passwordHash);

Python

from argon2 import PasswordHasher

ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)

# Hash
password_hash = ph.hash(password)

# Verify
try:
    ph.verify(password_hash, password)
except argon2.exceptions.VerifyMismatchError:
    raise InvalidCredentials()

Laravel

// Laravel uses bcrypt by default -- just use the built-in
$hash = Hash::make($password);
$valid = Hash::check($password, $hash);

// Upgrade to Argon2id in config/hashing.php
'driver' => 'argon2id',
'memory' => 65536,
'threads' => 4,
'time' => 3,

JWT Best Practices

Token Configuration

import { SignJWT, jwtVerify } from 'jose';

const SECRET = new TextEncoder().encode(env.JWT_SECRET); // Min 256 bits

// Sign
const token = await new SignJWT({ sub: userId, role: user.role })
  .setProtectedHeader({ alg: 'HS256' })
  .setIssuedAt()
  .setExpirationTime('15m')  // Short-lived access tokens
  .setAudience('https://myapp.com')
  .setIssuer('https://myapp.com')
  .sign(SECRET);

// Verify (check ALL claims)
const { payload } = await jwtVerify(token, SECRET, {
  audience: 'https://myapp.com',
  issuer: 'https://myapp.com',
  algorithms: ['HS256'],  // Prevent algorithm confusion
});

JWT Rules

  • Access tokens: 15 minutes max
  • Refresh tokens: 7 days max, stored in httpOnly cookie
  • NEVER store JWTs in localStorage (XSS can steal them)
  • ALWAYS validate exp, aud, iss, and alg
  • NEVER use alg: "none" -- always specify allowed algorithms
  • Rotate signing keys periodically

Brute-Force Protection

import { RateLimiterMemory } from 'rate-limiter-flexible';

const loginLimiter = new RateLimiterMemory({
  points: 5,        // 5 attempts
  duration: 900,    // per 15 minutes
  blockDuration: 900, // block for 15 min after exceeded
});

app.post('/login', async (req, res) => {
  const key = `${req.ip}_${req.body.email}`;

  try {
    await loginLimiter.consume(key);
  } catch {
    return res.status(429).json({
      error: 'Too many login attempts. Try again in 15 minutes.',
    });
  }

  const user = await authenticate(req.body.email, req.body.password);

  if (!user) {
    // Don't reveal whether email exists
    return res.status(401).json({ error: 'Invalid email or password' });
  }

  // Reset limiter on success
  await loginLimiter.delete(key);
  return res.json({ token: generateToken(user) });
});

Session Management

// Secure cookie settings
const SESSION_OPTIONS = {
  httpOnly: true,      // JavaScript cannot access
  secure: true,        // HTTPS only
  sameSite: 'lax',     // CSRF protection
  maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
  path: '/',
  domain: '.myapp.com',
};

// Session fixation prevention: regenerate session ID on login
app.post('/login', async (req, res) => {
  const user = await authenticate(req.body);
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  // Destroy old session and create new one
  req.session.regenerate((err) => {
    req.session.userId = user.id;
    req.session.save();
    res.json({ success: true });
  });
});

// Invalidate all sessions on password change
app.post('/change-password', async (req, res) => {
  await updatePassword(req.user.id, req.body.newPassword);
  await destroyAllSessions(req.user.id); // Force re-login everywhere
  res.json({ success: true, message: 'Please log in again' });
});

Auth Checklist

  • Passwords hashed with Argon2id or bcrypt (cost 12+)
  • Login rate-limited (5 attempts per 15 min)
  • Generic error messages ("Invalid email or password")
  • Session regenerated on login (prevent fixation)
  • All sessions invalidated on password change
  • JWTs expire in 15 minutes or less
  • Refresh tokens in httpOnly secure cookies
  • No sensitive data in JWT payload
  • Algorithm explicitly specified in JWT verification
  • Account lockout after repeated failures

PDPA 2024 Consequence

Seksyen 5(1) dan 5(2) PDPA 2010 (Pindaan 2024):

  • Denda sehingga RM1,000,000
  • Penjara sehingga 3 tahun

Weak authentication that allows unauthorized access to personal data is kecuaian keselamatan (security negligence). Using MD5/SHA-256 without proper stretching, or having no brute-force protection, demonstrates failure to implement reasonable security measures.

发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.25

发布时间

2026年9月25日

分类

未分类

许可证

未指定

源路径

skills/auth-hardening

默认分支

main

最新提交

6aeeb8f

Tree SHA

14cb46d