vanara 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/bin/vanara.js +66 -2
- package/catalog/agents/api-designer/AGENT.md +140 -0
- package/catalog/agents/api-designer/examples/openapi-snippet.yaml +130 -0
- package/catalog/agents/api-designer/examples/review-notes.md +61 -0
- package/catalog/agents/api-designer/references/contract-and-openapi.md +82 -0
- package/catalog/agents/api-designer/references/design-checklist.md +68 -0
- package/catalog/agents/api-designer/references/versioning-and-evolution.md +63 -0
- package/catalog/agents/api-designer/scripts/lint-openapi.mjs +180 -0
- package/catalog/agents/code-reviewer/AGENT.md +168 -0
- package/catalog/agents/code-reviewer/examples/pr-comment-template.md +48 -0
- package/catalog/agents/code-reviewer/examples/sample-review-output.md +38 -0
- package/catalog/agents/code-reviewer/references/review-checklist.md +46 -0
- package/catalog/agents/code-reviewer/references/security-review.md +62 -0
- package/catalog/agents/code-reviewer/references/severity-rubric.md +63 -0
- package/catalog/agents/code-reviewer/scripts/review-guard.mjs +102 -0
- package/catalog/agents/debugger/AGENT.md +112 -0
- package/catalog/agents/debugger/examples/postmortem-template.md +59 -0
- package/catalog/agents/debugger/examples/repro-template.md +50 -0
- package/catalog/agents/debugger/references/bisection.md +63 -0
- package/catalog/agents/debugger/references/debugging-method.md +57 -0
- package/catalog/agents/debugger/references/observability.md +61 -0
- package/catalog/agents/debugger/scripts/parse-stacktrace.mjs +109 -0
- package/catalog/agents/pr-summarizer/AGENT.md +156 -0
- package/catalog/agents/pr-summarizer/examples/sample-pr-summary.md +46 -0
- package/catalog/agents/pr-summarizer/examples/weak-vs-strong-summary.md +68 -0
- package/catalog/agents/pr-summarizer/references/identifying-risk.md +68 -0
- package/catalog/agents/pr-summarizer/references/reading-the-diff.md +68 -0
- package/catalog/agents/pr-summarizer/references/test-plan-and-missing-tests.md +64 -0
- package/catalog/agents/pr-summarizer/references/writing-the-summary.md +63 -0
- package/catalog/agents/pr-summarizer/scripts/diff-risk.mjs +111 -0
- package/catalog/agents/refactoring-specialist/AGENT.md +185 -0
- package/catalog/agents/refactoring-specialist/examples/before-after.md +91 -0
- package/catalog/agents/refactoring-specialist/examples/refactor-plan.md +97 -0
- package/catalog/agents/refactoring-specialist/references/large-scale-refactor.md +50 -0
- package/catalog/agents/refactoring-specialist/references/safe-workflow.md +75 -0
- package/catalog/agents/refactoring-specialist/references/smell-catalog.md +76 -0
- package/catalog/agents/refactoring-specialist/scripts/detect-smells.mjs +150 -0
- package/catalog/agents/security-auditor/AGENT.md +168 -0
- package/catalog/agents/security-auditor/examples/audit-report.md +68 -0
- package/catalog/agents/security-auditor/examples/finding-template.md +31 -0
- package/catalog/agents/security-auditor/references/audit-workflow.md +116 -0
- package/catalog/agents/security-auditor/references/severity-and-reporting.md +56 -0
- package/catalog/agents/security-auditor/references/vuln-classes.md +129 -0
- package/catalog/agents/security-auditor/scripts/scan-secrets.mjs +86 -0
- package/catalog/agents/technical-writer/AGENT.md +163 -0
- package/catalog/agents/technical-writer/examples/how-to-example.md +76 -0
- package/catalog/agents/technical-writer/examples/reference-example.md +75 -0
- package/catalog/agents/technical-writer/references/clarity-and-style.md +52 -0
- package/catalog/agents/technical-writer/references/diataxis-and-doc-types.md +61 -0
- package/catalog/agents/technical-writer/references/structure-and-maintenance.md +59 -0
- package/catalog/agents/technical-writer/scripts/readability.mjs +103 -0
- package/catalog/agents/test-author/AGENT.md +177 -0
- package/catalog/agents/test-author/examples/test-plan.md +54 -0
- package/catalog/agents/test-author/examples/unit-test-example.ts +55 -0
- package/catalog/agents/test-author/references/aaa-and-naming.md +75 -0
- package/catalog/agents/test-author/references/mocking-and-fixtures.md +94 -0
- package/catalog/agents/test-author/references/test-types-and-pyramid.md +60 -0
- package/catalog/agents/test-author/scripts/check-coverage.mjs +145 -0
- package/catalog/agents/threat-modeler/AGENT.md +163 -0
- package/catalog/agents/threat-modeler/examples/dfd-example.md +55 -0
- package/catalog/agents/threat-modeler/examples/threat-model.md +81 -0
- package/catalog/agents/threat-modeler/references/dfd-and-trust-boundaries.md +76 -0
- package/catalog/agents/threat-modeler/references/mitigation-catalog.md +77 -0
- package/catalog/agents/threat-modeler/references/stride-method.md +68 -0
- package/catalog/agents/threat-modeler/scripts/stride-checklist.mjs +183 -0
- package/catalog/agents/vuln-scanner/AGENT.md +146 -0
- package/catalog/agents/vuln-scanner/examples/finding-template.md +57 -0
- package/catalog/agents/vuln-scanner/examples/scan-report.md +49 -0
- package/catalog/agents/vuln-scanner/references/remediation-and-severity.md +68 -0
- package/catalog/agents/vuln-scanner/references/scan-types-and-tools.md +83 -0
- package/catalog/agents/vuln-scanner/references/triage-and-false-positives.md +73 -0
- package/catalog/agents/vuln-scanner/scripts/parse-scan-results.mjs +133 -0
- package/catalog/packs/code-review-pack/PACK.md +100 -0
- package/catalog/packs/code-review-pack/examples/pr-flow-example.md +50 -0
- package/catalog/packs/code-review-pack/references/rollout-guide.md +40 -0
- package/catalog/packs/security-pack/PACK.md +128 -0
- package/catalog/packs/security-pack/examples/owasp-vuln-triage-and-fix.md +66 -0
- package/catalog/packs/security-pack/examples/threat-model-then-audit.md +77 -0
- package/catalog/packs/security-pack/references/choosing-the-right-tool.md +37 -0
- package/catalog/packs/security-pack/references/threat-model-to-remediation.md +69 -0
- package/catalog/packs/security-pack/references/workflow.md +56 -0
- package/catalog/skills/api-pagination/SKILL.md +83 -0
- package/catalog/skills/api-pagination/examples/keyset-queries.sql +38 -0
- package/catalog/skills/api-pagination/examples/response-shapes.md +51 -0
- package/catalog/skills/api-pagination/references/consuming-pages.md +39 -0
- package/catalog/skills/api-pagination/references/counting-and-totals.md +41 -0
- package/catalog/skills/api-pagination/references/cursor-encoding.md +34 -0
- package/catalog/skills/api-pagination/scripts/check-cursor.mjs +51 -0
- package/catalog/skills/caching-strategies/SKILL.md +180 -0
- package/catalog/skills/caching-strategies/examples/http-cache-headers.md +82 -0
- package/catalog/skills/caching-strategies/examples/redis-cache-aside.js +110 -0
- package/catalog/skills/caching-strategies/references/cache-patterns.md +93 -0
- package/catalog/skills/caching-strategies/references/eviction-and-ttl.md +67 -0
- package/catalog/skills/caching-strategies/references/invalidation-and-stampede.md +99 -0
- package/catalog/skills/caching-strategies/scripts/ttl-jitter.mjs +99 -0
- package/catalog/skills/conventional-commits/SKILL.md +135 -0
- package/catalog/skills/conventional-commits/examples/commit-examples.md +60 -0
- package/catalog/skills/conventional-commits/examples/commitlint.config.js +31 -0
- package/catalog/skills/conventional-commits/references/breaking-changes-semver.md +50 -0
- package/catalog/skills/conventional-commits/references/scoping-commits.md +42 -0
- package/catalog/skills/conventional-commits/references/spec.md +78 -0
- package/catalog/skills/conventional-commits/scripts/lint-commit.mjs +97 -0
- package/catalog/skills/database-migrations/SKILL.md +137 -0
- package/catalog/skills/database-migrations/examples/add-column-safe.sql +34 -0
- package/catalog/skills/database-migrations/examples/migration-config.example.json +27 -0
- package/catalog/skills/database-migrations/references/expand-contract.md +74 -0
- package/catalog/skills/database-migrations/references/rollback-and-safety.md +71 -0
- package/catalog/skills/database-migrations/references/zero-downtime-changes.md +84 -0
- package/catalog/skills/database-migrations/scripts/check-migration-reversible.mjs +139 -0
- package/catalog/skills/error-handling-patterns/SKILL.md +136 -0
- package/catalog/skills/error-handling-patterns/examples/express-error-middleware.js +93 -0
- package/catalog/skills/error-handling-patterns/examples/result-pattern.ts +81 -0
- package/catalog/skills/error-handling-patterns/references/error-taxonomy.md +91 -0
- package/catalog/skills/error-handling-patterns/references/language-patterns.md +102 -0
- package/catalog/skills/error-handling-patterns/references/retry-and-backoff.md +76 -0
- package/catalog/skills/error-handling-patterns/scripts/lint-empty-catch.mjs +113 -0
- package/catalog/skills/git-collaboration-workflows/SKILL.md +104 -0
- package/catalog/skills/git-collaboration-workflows/examples/branch-protection.md +48 -0
- package/catalog/skills/git-collaboration-workflows/examples/pr-review-playbook.md +48 -0
- package/catalog/skills/git-collaboration-workflows/references/branching-models.md +52 -0
- package/catalog/skills/git-collaboration-workflows/references/history-hygiene.md +44 -0
- package/catalog/skills/git-collaboration-workflows/scripts/check-branch-hygiene.mjs +62 -0
- package/catalog/skills/owasp-top10/SKILL.md +112 -0
- package/catalog/skills/owasp-top10/examples/sql-injection-fix.md +33 -0
- package/catalog/skills/owasp-top10/examples/xss-fix.md +27 -0
- package/catalog/skills/owasp-top10/references/access-control.md +36 -0
- package/catalog/skills/owasp-top10/references/crypto-failures.md +29 -0
- package/catalog/skills/owasp-top10/references/injection.md +39 -0
- package/catalog/skills/owasp-top10/references/ssrf-and-design.md +35 -0
- package/catalog/skills/owasp-top10/scripts/scan-injection.mjs +73 -0
- package/catalog/skills/prompt-engineering/SKILL.md +100 -0
- package/catalog/skills/prompt-engineering/examples/classification-prompt.md +29 -0
- package/catalog/skills/prompt-engineering/examples/extraction-prompt.md +25 -0
- package/catalog/skills/prompt-engineering/references/prompt-injection.md +34 -0
- package/catalog/skills/prompt-engineering/references/structured-output.md +34 -0
- package/catalog/skills/prompt-engineering/references/techniques.md +33 -0
- package/catalog/skills/prompt-engineering/scripts/validate-output.mjs +71 -0
- package/catalog/skills/readme-writing/SKILL.md +176 -0
- package/catalog/skills/readme-writing/examples/README.template.md +73 -0
- package/catalog/skills/readme-writing/examples/good-readme-example.md +95 -0
- package/catalog/skills/readme-writing/references/anatomy.md +107 -0
- package/catalog/skills/readme-writing/references/maintenance.md +68 -0
- package/catalog/skills/readme-writing/references/writing-style.md +79 -0
- package/catalog/skills/readme-writing/scripts/lint-readme.mjs +117 -0
- package/catalog/skills/refactoring-patterns/SKILL.md +178 -0
- package/catalog/skills/refactoring-patterns/examples/extract-function.before-after.js +57 -0
- package/catalog/skills/refactoring-patterns/examples/replace-conditional-with-polymorphism.before-after.ts +63 -0
- package/catalog/skills/refactoring-patterns/references/code-smells.md +92 -0
- package/catalog/skills/refactoring-patterns/references/refactoring-catalog.md +78 -0
- package/catalog/skills/refactoring-patterns/references/safe-workflow.md +74 -0
- package/catalog/skills/refactoring-patterns/scripts/detect-smells.mjs +137 -0
- package/catalog/skills/rest-api-design/SKILL.md +147 -0
- package/catalog/skills/rest-api-design/examples/error-envelope.json +26 -0
- package/catalog/skills/rest-api-design/examples/orders-api.openapi.yaml +106 -0
- package/catalog/skills/rest-api-design/references/error-handling.md +43 -0
- package/catalog/skills/rest-api-design/references/pagination.md +41 -0
- package/catalog/skills/rest-api-design/references/status-codes.md +32 -0
- package/catalog/skills/rest-api-design/references/versioning.md +31 -0
- package/catalog/skills/rest-api-design/scripts/check-envelope.mjs +62 -0
- package/catalog/skills/secrets-management/SKILL.md +134 -0
- package/catalog/skills/secrets-management/examples/env-example.md +41 -0
- package/catalog/skills/secrets-management/examples/rotation-runbook.md +50 -0
- package/catalog/skills/secrets-management/references/leak-response.md +47 -0
- package/catalog/skills/secrets-management/references/rotation.md +46 -0
- package/catalog/skills/secrets-management/references/secret-managers.md +52 -0
- package/catalog/skills/secrets-management/scripts/detect-hardcoded.mjs +96 -0
- package/catalog/skills/secure-auth/SKILL.md +135 -0
- package/catalog/skills/secure-auth/examples/auth-flow.md +60 -0
- package/catalog/skills/secure-auth/examples/jwt-verify.ts +89 -0
- package/catalog/skills/secure-auth/references/auth-attacks.md +76 -0
- package/catalog/skills/secure-auth/references/oauth2-oidc.md +65 -0
- package/catalog/skills/secure-auth/references/password-hashing.md +70 -0
- package/catalog/skills/secure-auth/references/sessions-vs-jwt.md +69 -0
- package/catalog/skills/secure-auth/scripts/check-password-policy.mjs +114 -0
- package/catalog/skills/sql-index-tuning/SKILL.md +171 -0
- package/catalog/skills/sql-index-tuning/examples/explain-walkthrough.sql +91 -0
- package/catalog/skills/sql-index-tuning/examples/index-ddl.sql +67 -0
- package/catalog/skills/sql-index-tuning/references/btree-internals.md +58 -0
- package/catalog/skills/sql-index-tuning/references/composite-and-covering.md +60 -0
- package/catalog/skills/sql-index-tuning/references/reading-explain.md +54 -0
- package/catalog/skills/sql-index-tuning/references/selectivity-and-cardinality.md +62 -0
- package/catalog/skills/sql-index-tuning/scripts/suggest-index.mjs +213 -0
- package/catalog/skills/test-plan-design/SKILL.md +154 -0
- package/catalog/skills/test-plan-design/examples/test-case-template.md +45 -0
- package/catalog/skills/test-plan-design/examples/test-plan.md +57 -0
- package/catalog/skills/test-plan-design/references/case-design.md +79 -0
- package/catalog/skills/test-plan-design/references/risk-prioritization.md +58 -0
- package/catalog/skills/test-plan-design/references/test-pyramid.md +58 -0
- package/catalog/skills/test-plan-design/scripts/coverage-gaps.mjs +109 -0
- package/free-tier.json +15 -0
- package/package.json +15 -4
- package/premium-index.json +1 -0
- package/src/config.js +31 -1
- package/src/license.js +143 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Annotated Auth Flows
|
|
2
|
+
|
|
3
|
+
Three end-to-end flows with the security-relevant step called out at each line. These are reference
|
|
4
|
+
sequences, not framework-specific code.
|
|
5
|
+
|
|
6
|
+
## 1. Password login → session
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
POST /login { email, password }
|
|
10
|
+
1. Look up user by email.
|
|
11
|
+
2. ALWAYS run argon2.verify even if the user doesn't exist (compare against a dummy hash)
|
|
12
|
+
-> keeps response time uniform, defeats account enumeration by timing.
|
|
13
|
+
3. On success:
|
|
14
|
+
a. Regenerate the session ID <- defeats session fixation
|
|
15
|
+
b. Persist session server-side (user, absolute + idle expiry, CSRF token)
|
|
16
|
+
c. Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax
|
|
17
|
+
4. On failure: generic 401 "invalid email or password" (never say which was wrong).
|
|
18
|
+
5. Increment a per-account + per-IP failure counter -> backoff/lockout on abuse.
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The dummy-hash compare in step 2 matters: skipping the hash when the user is missing makes "no such
|
|
22
|
+
user" measurably faster than "wrong password," which is an enumeration oracle.
|
|
23
|
+
|
|
24
|
+
## 2. Access token refresh (JWT model)
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
Login issues:
|
|
28
|
+
access_token (JWT, exp ~10 min, carried in Authorization: Bearer)
|
|
29
|
+
refresh_token (opaque, stored server-side, exp ~14 days, in HttpOnly cookie)
|
|
30
|
+
|
|
31
|
+
POST /token/refresh (presents refresh_token)
|
|
32
|
+
1. Look up the refresh token record server-side.
|
|
33
|
+
2. If it's already been used (rotated out) -> REUSE DETECTED:
|
|
34
|
+
revoke the entire token family + force re-login. <- catches stolen refresh tokens
|
|
35
|
+
3. Otherwise: issue a NEW access_token AND a NEW refresh_token,
|
|
36
|
+
invalidate the old refresh token (rotation).
|
|
37
|
+
4. Logout: delete the refresh record -> access dies within ~10 min.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 3. OAuth2 / OIDC login (Authorization Code + PKCE)
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
1. App generates code_verifier (random) + code_challenge = SHA256(verifier),
|
|
44
|
+
and a random `state`. Store both in the user's pending session.
|
|
45
|
+
2. Redirect to provider /authorize?...&code_challenge=...&state=...&scope=openid email
|
|
46
|
+
3. User authenticates at the provider; redirected back with ?code=...&state=...
|
|
47
|
+
4. App verifies returned state == stored state. <- CSRF defense on callback
|
|
48
|
+
5. App POSTs code + code_verifier to /token. <- PKCE binds code to this client
|
|
49
|
+
6. Provider returns access_token + id_token (+ refresh_token).
|
|
50
|
+
7. App verifies id_token: signature (JWKS), iss, aud, exp, nonce. (see jwt-verify.ts)
|
|
51
|
+
8. App maps id_token.sub -> local user (create on first login). Use `sub`, not `email`.
|
|
52
|
+
9. Establish a local session/token exactly as in flows 1 or 2.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Logout that actually logs out
|
|
56
|
+
|
|
57
|
+
- Server-side sessions: delete the session record (not just the cookie). Optionally support
|
|
58
|
+
"log out all devices" by deleting every session for the user.
|
|
59
|
+
- JWT model: delete the refresh token; rely on short access-token expiry for the residual window. If you
|
|
60
|
+
need *instant* access-token revocation, keep a short-TTL denylist of token IDs (`jti`).
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// jwt-verify.ts
|
|
2
|
+
//
|
|
3
|
+
// A dependency-light illustration of how to verify a JWT *correctly*. In production
|
|
4
|
+
// you would use a maintained library (e.g. `jose`) plus JWKS fetching/caching — this
|
|
5
|
+
// file shows the checks that library performs so you know what "good" looks like and
|
|
6
|
+
// can audit a hand-rolled verifier. It uses only the Node built-in `crypto` module.
|
|
7
|
+
//
|
|
8
|
+
// SECURITY NOTES (the parts people get wrong):
|
|
9
|
+
// 1. PIN the algorithm. Never trust the token's own `alg` header — an attacker can
|
|
10
|
+
// set `alg: none` or switch RS256 -> HS256 and sign with the public key.
|
|
11
|
+
// 2. ALWAYS verify the signature BEFORE reading any claim as trusted.
|
|
12
|
+
// 3. Check `iss`, `aud`, and `exp` (and `nbf`/`iat`) — a valid signature on a token
|
|
13
|
+
// meant for a different audience is still not for you.
|
|
14
|
+
|
|
15
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
16
|
+
|
|
17
|
+
interface VerifyOptions {
|
|
18
|
+
secret: string; // HMAC secret (HS256). For RS256 you'd verify with a public key.
|
|
19
|
+
issuer: string; // expected `iss`
|
|
20
|
+
audience: string; // expected `aud`
|
|
21
|
+
now?: number; // injectable clock (seconds) for testing
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface JwtClaims {
|
|
25
|
+
iss?: string;
|
|
26
|
+
aud?: string | string[];
|
|
27
|
+
exp?: number;
|
|
28
|
+
nbf?: number;
|
|
29
|
+
sub?: string;
|
|
30
|
+
[k: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function b64urlDecode(part: string): Buffer {
|
|
34
|
+
return Buffer.from(part.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function b64urlEncode(buf: Buffer): string {
|
|
38
|
+
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function verifyJwtHS256(token: string, opts: VerifyOptions): JwtClaims {
|
|
42
|
+
const parts = token.split('.');
|
|
43
|
+
if (parts.length !== 3) throw new Error('malformed token: expected 3 segments');
|
|
44
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
45
|
+
|
|
46
|
+
// 1. Pin the algorithm — reject whatever the header claims if it isn't ours.
|
|
47
|
+
const header = JSON.parse(b64urlDecode(headerB64).toString('utf8'));
|
|
48
|
+
if (header.alg !== 'HS256') throw new Error(`unexpected alg: ${header.alg}`);
|
|
49
|
+
|
|
50
|
+
// 2. Recompute and compare the signature in constant time.
|
|
51
|
+
const expected = createHmac('sha256', opts.secret)
|
|
52
|
+
.update(`${headerB64}.${payloadB64}`)
|
|
53
|
+
.digest();
|
|
54
|
+
const provided = b64urlDecode(signatureB64);
|
|
55
|
+
if (expected.length !== provided.length || !timingSafeEqual(expected, provided)) {
|
|
56
|
+
throw new Error('invalid signature');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3. Only now is it safe to read claims as trusted.
|
|
60
|
+
const claims: JwtClaims = JSON.parse(b64urlDecode(payloadB64).toString('utf8'));
|
|
61
|
+
const now = opts.now ?? Math.floor(Date.now() / 1000);
|
|
62
|
+
|
|
63
|
+
if (claims.iss !== opts.issuer) throw new Error('issuer mismatch');
|
|
64
|
+
const aud = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
|
|
65
|
+
if (!aud.includes(opts.audience)) throw new Error('audience mismatch');
|
|
66
|
+
if (typeof claims.exp === 'number' && now >= claims.exp) throw new Error('token expired');
|
|
67
|
+
if (typeof claims.nbf === 'number' && now < claims.nbf) throw new Error('token not yet valid');
|
|
68
|
+
|
|
69
|
+
return claims;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Tiny demo helper to mint a token for local experimentation (NOT for production use).
|
|
73
|
+
export function signJwtHS256(claims: JwtClaims, secret: string): string {
|
|
74
|
+
const header = b64urlEncode(Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })));
|
|
75
|
+
const payload = b64urlEncode(Buffer.from(JSON.stringify(claims)));
|
|
76
|
+
const sig = b64urlEncode(createHmac('sha256', secret).update(`${header}.${payload}`).digest());
|
|
77
|
+
return `${header}.${payload}.${sig}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Example (fake secret — never hardcode real secrets; load from a secrets manager):
|
|
81
|
+
// const SECRET = process.env.JWT_SECRET!;
|
|
82
|
+
// const token = signJwtHS256(
|
|
83
|
+
// { iss: 'https://auth.example.com', aud: 'my-api', sub: 'user-123',
|
|
84
|
+
// exp: Math.floor(Date.now() / 1000) + 600 },
|
|
85
|
+
// SECRET,
|
|
86
|
+
// );
|
|
87
|
+
// const claims = verifyJwtHS256(token, {
|
|
88
|
+
// secret: SECRET, issuer: 'https://auth.example.com', audience: 'my-api',
|
|
89
|
+
// });
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Common Auth Attacks and Defenses
|
|
2
|
+
|
|
3
|
+
Most authentication breaches exploit operational gaps, not broken crypto. Know the named attacks and the
|
|
4
|
+
specific control that stops each.
|
|
5
|
+
|
|
6
|
+
## Credential stuffing
|
|
7
|
+
|
|
8
|
+
Attackers replay username/password pairs leaked from *other* breaches, betting on password reuse. It is
|
|
9
|
+
high-volume and automated.
|
|
10
|
+
|
|
11
|
+
**Defenses:**
|
|
12
|
+
- Rate-limit and progressively delay/lock by IP **and** by account.
|
|
13
|
+
- Screen new/changed passwords against a breached-password corpus (HaveIBeenPwned range API or a local
|
|
14
|
+
Pwned Passwords list) — see `scripts/check-password-policy.mjs`.
|
|
15
|
+
- Offer/require MFA; it neutralizes a correct-but-stolen password.
|
|
16
|
+
- Add bot defenses (device fingerprinting, CAPTCHA on risk) for anomalous traffic.
|
|
17
|
+
|
|
18
|
+
## Brute force
|
|
19
|
+
|
|
20
|
+
Guessing one account's password through sheer volume.
|
|
21
|
+
|
|
22
|
+
**Defenses:** account lockout / exponential backoff, strong minimum length, slow password hashing
|
|
23
|
+
(argon2id), and MFA. Avoid hard permanent lockouts — they become a denial-of-service against legitimate
|
|
24
|
+
users; prefer temporary backoff plus alerting.
|
|
25
|
+
|
|
26
|
+
## Session fixation
|
|
27
|
+
|
|
28
|
+
The attacker obtains or sets a session ID *before* the victim logs in, then rides the now-authenticated
|
|
29
|
+
session.
|
|
30
|
+
|
|
31
|
+
**Defense:** **regenerate the session identifier at every privilege change** (especially on login). Never
|
|
32
|
+
accept a session ID supplied via URL/query string. Set `HttpOnly; Secure; SameSite` cookies.
|
|
33
|
+
|
|
34
|
+
## CSRF (Cross-Site Request Forgery)
|
|
35
|
+
|
|
36
|
+
A malicious site causes the victim's browser to send an authenticated, state-changing request to your app
|
|
37
|
+
using the victim's cookies.
|
|
38
|
+
|
|
39
|
+
**Defenses:**
|
|
40
|
+
- `SameSite=Lax` (or `Strict`) cookies — blocks most cross-site sends.
|
|
41
|
+
- Anti-CSRF tokens (synchronizer or double-submit) on state-changing requests.
|
|
42
|
+
- Require a custom header / re-auth for sensitive actions.
|
|
43
|
+
- Note: token-in-`Authorization`-header APIs (not cookie-auth) are largely immune, since the browser
|
|
44
|
+
doesn't auto-attach the header cross-site.
|
|
45
|
+
|
|
46
|
+
## Account enumeration
|
|
47
|
+
|
|
48
|
+
The app reveals whether an account exists via different responses, status codes, or **timing** on login,
|
|
49
|
+
signup, or password reset.
|
|
50
|
+
|
|
51
|
+
**Defense:** return identical responses and keep timing uniform regardless of account existence ("If that
|
|
52
|
+
email is registered, we've sent a reset link.").
|
|
53
|
+
|
|
54
|
+
## XSS-driven token theft
|
|
55
|
+
|
|
56
|
+
Cross-site scripting that steals tokens from `localStorage` or readable cookies.
|
|
57
|
+
|
|
58
|
+
**Defenses:** `HttpOnly` cookies (JS can't read them), a strict Content-Security-Policy, output encoding,
|
|
59
|
+
and never storing long-lived credentials in web storage.
|
|
60
|
+
|
|
61
|
+
## Phishing / MITM of credentials
|
|
62
|
+
|
|
63
|
+
**Defenses:** phishing-resistant MFA (WebAuthn/passkeys bind to origin), HSTS, and never accepting
|
|
64
|
+
credentials over plain HTTP.
|
|
65
|
+
|
|
66
|
+
## Quick map
|
|
67
|
+
|
|
68
|
+
| Attack | Primary control |
|
|
69
|
+
|---|---|
|
|
70
|
+
| Credential stuffing | Rate limit + breached-password screen + MFA |
|
|
71
|
+
| Brute force | Backoff/lockout + slow hash + MFA |
|
|
72
|
+
| Session fixation | Regenerate session ID on login |
|
|
73
|
+
| CSRF | SameSite cookies + anti-CSRF token |
|
|
74
|
+
| Enumeration | Uniform responses + timing |
|
|
75
|
+
| XSS token theft | HttpOnly cookies + CSP |
|
|
76
|
+
| Phishing | WebAuthn/passkeys + HSTS |
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# OAuth2 / OIDC — Flows and Validation
|
|
2
|
+
|
|
3
|
+
OAuth2 is a **delegated authorization** framework: it lets your app obtain an access token to call an API
|
|
4
|
+
on a user's behalf. OpenID Connect (OIDC) is a thin identity layer on top that adds an `id_token` so you
|
|
5
|
+
also learn **who the user is**. For "log in with Google/Apple/etc.", you want OIDC.
|
|
6
|
+
|
|
7
|
+
## Use Authorization Code + PKCE
|
|
8
|
+
|
|
9
|
+
For both server-side web apps and public clients (SPAs, mobile), use the **Authorization Code flow with
|
|
10
|
+
PKCE** (Proof Key for Code Exchange).
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
1. Client generates code_verifier (random) + code_challenge = SHA256(code_verifier).
|
|
14
|
+
2. Redirect user to /authorize?response_type=code&client_id=...&redirect_uri=...
|
|
15
|
+
&scope=openid profile email&state=<csrf>&code_challenge=...&code_challenge_method=S256
|
|
16
|
+
3. User authenticates at the provider, is redirected back with ?code=...&state=...
|
|
17
|
+
4. Client verifies returned state == sent state, then POSTs to /token with the code + code_verifier.
|
|
18
|
+
5. Provider returns access_token (+ id_token for OIDC, + refresh_token).
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
PKCE binds the authorization code to the client that started the flow, so a stolen code is useless to an
|
|
22
|
+
attacker who lacks the original `code_verifier`.
|
|
23
|
+
|
|
24
|
+
## Flows to avoid
|
|
25
|
+
|
|
26
|
+
| Flow | Verdict |
|
|
27
|
+
|---|---|
|
|
28
|
+
| Authorization Code + PKCE | **Use this** for web, SPA, mobile |
|
|
29
|
+
| Client Credentials | Machine-to-machine only (no user) |
|
|
30
|
+
| Implicit | **Deprecated** — tokens leak via URL/history; use Code+PKCE |
|
|
31
|
+
| Resource Owner Password | **Avoid** — app handles the user's IdP password; defeats the point of delegation |
|
|
32
|
+
|
|
33
|
+
## The `state` parameter (CSRF defense)
|
|
34
|
+
|
|
35
|
+
Always send a random, unguessable `state` on the authorize request and reject the callback if the
|
|
36
|
+
returned `state` doesn't match what you stored. Without it, an attacker can splice their own
|
|
37
|
+
authorization code into the victim's session (login CSRF).
|
|
38
|
+
|
|
39
|
+
## Validating the `id_token` (OIDC)
|
|
40
|
+
|
|
41
|
+
The `id_token` is a JWT. Do not trust it until you verify:
|
|
42
|
+
|
|
43
|
+
- **Signature** — against the provider's published JWKS (`jwks_uri`), matched by the `kid` header.
|
|
44
|
+
- **`iss`** — exactly equals the provider's issuer.
|
|
45
|
+
- **`aud`** — contains your client_id.
|
|
46
|
+
- **`exp` / `iat`** — not expired; issued recently.
|
|
47
|
+
- **`nonce`** — matches the nonce you sent (replay defense), when used.
|
|
48
|
+
|
|
49
|
+
Never accept an unsigned token (`alg: none`) and never let the token dictate the algorithm — pin the
|
|
50
|
+
expected algorithm server-side. See `examples/jwt-verify.ts` for the structural checks.
|
|
51
|
+
|
|
52
|
+
## Scopes vs claims
|
|
53
|
+
|
|
54
|
+
- **Scopes** (`openid profile email`) request *access* to categories of data.
|
|
55
|
+
- **Claims** (`sub`, `email`, `name`) are the actual values inside the `id_token` / userinfo response.
|
|
56
|
+
- Treat `sub` (subject) as the stable user identifier — `email` can change or be reassigned.
|
|
57
|
+
|
|
58
|
+
## Checklist
|
|
59
|
+
|
|
60
|
+
- [ ] Authorization Code + PKCE (not implicit, not password grant)
|
|
61
|
+
- [ ] `state` generated, stored, and verified on callback
|
|
62
|
+
- [ ] `id_token` signature verified against JWKS; `iss`/`aud`/`exp` checked
|
|
63
|
+
- [ ] Algorithm pinned server-side; `alg: none` rejected
|
|
64
|
+
- [ ] `sub` used as the durable user key, not `email`
|
|
65
|
+
- [ ] `redirect_uri` allowlisted exactly (no open redirects)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Password Hashing — argon2id, bcrypt, and tuning
|
|
2
|
+
|
|
3
|
+
The job of a password hash is to make offline cracking of a stolen database *slow and expensive*. That
|
|
4
|
+
is the opposite of a normal hash function's goal (speed). Never use `MD5`, `SHA-1`, `SHA-256`, or any
|
|
5
|
+
fast hash for passwords — a single GPU computes billions of SHA-256 hashes per second, so an unsalted
|
|
6
|
+
fast-hash dump is effectively plaintext within hours.
|
|
7
|
+
|
|
8
|
+
## Algorithm choice
|
|
9
|
+
|
|
10
|
+
| Algorithm | Status | Notes |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| **argon2id** | **Preferred** | Memory-hard; resists GPU/ASIC cracking. Tunable memory + time + parallelism. |
|
|
13
|
+
| **bcrypt** | Acceptable | Mature, ubiquitous. 72-byte input cap; pre-hash long inputs with SHA-256. |
|
|
14
|
+
| **scrypt** | Acceptable | Memory-hard; fine if argon2 unavailable. |
|
|
15
|
+
| **PBKDF2** | Last resort | Only when FIPS compliance forces it; use a high iteration count. |
|
|
16
|
+
| MD5 / SHA-* | **Never** | Fast = crackable. Not password hashes. |
|
|
17
|
+
|
|
18
|
+
## Salting and peppering
|
|
19
|
+
|
|
20
|
+
- **Salt** — a unique random value per password that defeats rainbow tables and stops identical
|
|
21
|
+
passwords producing identical hashes. argon2 and bcrypt generate and embed the salt for you; do not
|
|
22
|
+
add your own or store it separately.
|
|
23
|
+
- **Pepper** — an optional secret key mixed in (or used to encrypt the hash) and stored *outside* the
|
|
24
|
+
database (e.g. in a secrets manager / HSM). It means a DB-only leak isn't enough to start cracking.
|
|
25
|
+
Manage it like any other secret (see the `secrets-management` skill).
|
|
26
|
+
|
|
27
|
+
## Tuning cost parameters
|
|
28
|
+
|
|
29
|
+
Pick parameters so a single verification takes roughly **250–500 ms** on your production hardware — slow
|
|
30
|
+
enough to hurt attackers, fast enough not to DoS your own login endpoint.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import argon2 from 'argon2';
|
|
34
|
+
|
|
35
|
+
const hash = await argon2.hash(password, {
|
|
36
|
+
type: argon2.argon2id,
|
|
37
|
+
memoryCost: 19456, // ~19 MiB; raise as hardware allows (OWASP baseline)
|
|
38
|
+
timeCost: 2, // iterations
|
|
39
|
+
parallelism: 1,
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Cap input length (e.g. 128 chars) before hashing so an attacker can't submit a multi-megabyte password
|
|
44
|
+
to exhaust CPU/memory — a cheap denial-of-service otherwise.
|
|
45
|
+
|
|
46
|
+
## Rehash on login (parameter migration)
|
|
47
|
+
|
|
48
|
+
Cost targets rise as hardware improves. On each successful login, check whether the stored hash used
|
|
49
|
+
weaker parameters than your current policy and transparently rehash:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
if (await argon2.verify(stored, attempt)) {
|
|
53
|
+
if (argon2.needsRehash(stored, currentOpts)) {
|
|
54
|
+
const fresh = await argon2.hash(attempt, currentOpts);
|
|
55
|
+
await users.update(id, { passwordHash: fresh });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This lets you upgrade an entire user base's hash strength gradually without a forced reset.
|
|
61
|
+
|
|
62
|
+
## Checklist
|
|
63
|
+
|
|
64
|
+
- [ ] argon2id (or bcrypt) — never a fast/general-purpose hash
|
|
65
|
+
- [ ] Per-password salt (library-managed), not reused or hand-rolled
|
|
66
|
+
- [ ] Verification tuned to ~250–500 ms on prod hardware
|
|
67
|
+
- [ ] Input length capped before hashing (DoS guard)
|
|
68
|
+
- [ ] Breached-password screening at registration (`scripts/check-password-policy.mjs`)
|
|
69
|
+
- [ ] Rehash-on-login to migrate parameters over time
|
|
70
|
+
- [ ] Optional pepper stored outside the database
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Sessions vs JWT — Storage, Expiry, Refresh, Revocation
|
|
2
|
+
|
|
3
|
+
Both answer "how do we remember a logged-in user across stateless HTTP requests?" The decisive
|
|
4
|
+
difference is **revocation**: server-side sessions are trivial to revoke; signed JWTs are not.
|
|
5
|
+
|
|
6
|
+
## Server-side sessions (stateful)
|
|
7
|
+
|
|
8
|
+
The server issues a random, high-entropy **opaque** session ID and stores the session state
|
|
9
|
+
(user, expiry, CSRF token) in a database or Redis. The ID travels in a cookie.
|
|
10
|
+
|
|
11
|
+
```http
|
|
12
|
+
Set-Cookie: session=Gd8...n2; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- **Revocation:** delete the server-side record — instant logout-everywhere.
|
|
16
|
+
- **Expiry:** enforce both an idle timeout and an absolute lifetime.
|
|
17
|
+
- **Trade-off:** requires shared session storage; a tiny lookup per request.
|
|
18
|
+
- **Best for:** classic server-rendered web apps; anything needing reliable forced logout.
|
|
19
|
+
|
|
20
|
+
## Stateless JWT
|
|
21
|
+
|
|
22
|
+
A signed token (header.payload.signature) the client carries, usually in the `Authorization: Bearer`
|
|
23
|
+
header. The server validates the signature and claims with no lookup.
|
|
24
|
+
|
|
25
|
+
- **Cannot be un-issued.** Once signed, a JWT is valid until `exp`. There is no "delete the row."
|
|
26
|
+
- **Expiry is your main control** — keep access tokens **short (5–15 min)**.
|
|
27
|
+
- **Best for:** APIs and microservices that must validate auth without shared session state.
|
|
28
|
+
|
|
29
|
+
### Access + refresh token pattern
|
|
30
|
+
|
|
31
|
+
Pair a short-lived access token with a longer-lived **refresh token** that *is* tracked server-side so
|
|
32
|
+
it can be revoked:
|
|
33
|
+
|
|
34
|
+
1. Login issues a short access token (~10 min) + a refresh token (~days/weeks), refresh stored server-side.
|
|
35
|
+
2. Client uses the access token until it expires.
|
|
36
|
+
3. Client exchanges the refresh token for a new access token (and a **new** refresh token — rotation).
|
|
37
|
+
4. Logout / compromise: delete the refresh token record; access dies within minutes.
|
|
38
|
+
|
|
39
|
+
### Refresh token rotation + reuse detection
|
|
40
|
+
|
|
41
|
+
On each refresh, issue a new refresh token and invalidate the old one. If an *already-used* (rotated-out)
|
|
42
|
+
refresh token is presented again, treat it as theft: revoke the entire token family and force re-login.
|
|
43
|
+
This catches a stolen refresh token because either the attacker or the victim will replay a consumed one.
|
|
44
|
+
|
|
45
|
+
## Storage: where the token/cookie lives
|
|
46
|
+
|
|
47
|
+
| Location | XSS-readable? | CSRF-exposed? | Verdict |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| `HttpOnly` cookie | No | Yes (mitigate w/ SameSite + token) | **Preferred** for browser auth |
|
|
50
|
+
| `localStorage` / `sessionStorage` | **Yes** | No | Avoid — any XSS exfiltrates it |
|
|
51
|
+
| In-memory (JS variable) | Only during session | No | OK for short access token in SPA |
|
|
52
|
+
|
|
53
|
+
The headline rule: **never put a long-lived credential in `localStorage`.** `HttpOnly` cookies keep the
|
|
54
|
+
token out of reach of injected JavaScript.
|
|
55
|
+
|
|
56
|
+
## Quick decision guide
|
|
57
|
+
|
|
58
|
+
- Need easy forced logout / session revocation, server-rendered app → **server-side sessions**.
|
|
59
|
+
- Stateless API across many services, comfortable with short tokens + refresh rotation → **JWT**.
|
|
60
|
+
- Hybrid is common and good: opaque session cookie for the web app, short JWT for downstream service calls.
|
|
61
|
+
|
|
62
|
+
## Checklist
|
|
63
|
+
|
|
64
|
+
- [ ] Access tokens short-lived (5–15 min)
|
|
65
|
+
- [ ] Refresh tokens server-side, rotated on use, with reuse detection
|
|
66
|
+
- [ ] Cookies set `HttpOnly; Secure; SameSite`
|
|
67
|
+
- [ ] No long-lived credential in `localStorage`
|
|
68
|
+
- [ ] Session ID regenerated on login (anti session-fixation)
|
|
69
|
+
- [ ] Logout actually revokes server-side state, not just clears the client
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runnable check: validates a candidate password against a configurable policy
|
|
3
|
+
// (minimum length, character-class diversity, and a small breached/common-password
|
|
4
|
+
// denylist). Zero dependencies — Node built-ins only.
|
|
5
|
+
//
|
|
6
|
+
// This is a *policy linter*, not a strength meter. It mirrors the kind of
|
|
7
|
+
// server-side validation you should run at registration / password-change time,
|
|
8
|
+
// BEFORE hashing with argon2id or bcrypt (see references/password-hashing.md).
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// node check-password-policy.mjs '<password>' # check one password
|
|
12
|
+
// echo '<password>' | node check-password-policy.mjs # check from stdin
|
|
13
|
+
// node check-password-policy.mjs --selftest # run built-in test cases
|
|
14
|
+
//
|
|
15
|
+
// Exit code: 0 = password satisfies the policy, 1 = it does not (or selftest failed).
|
|
16
|
+
|
|
17
|
+
// --- Policy configuration ---------------------------------------------------
|
|
18
|
+
// NIST SP 800-63B favors length over composition rules and screening against
|
|
19
|
+
// known-breached passwords. We keep a modest class requirement for demos but the
|
|
20
|
+
// breached-list screen is the most valuable check in practice.
|
|
21
|
+
const POLICY = Object.freeze({
|
|
22
|
+
minLength: 12,
|
|
23
|
+
maxLength: 128, // cap to avoid DoS on the hashing function
|
|
24
|
+
minClasses: 3, // of: lowercase, uppercase, digit, symbol
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// A tiny stand-in for a real breached-password corpus (e.g. HaveIBeenPwned's
|
|
28
|
+
// k-anonymity range API or a local Pwned Passwords bloom filter). These are
|
|
29
|
+
// obviously fake/illustrative entries used only to exercise the check.
|
|
30
|
+
const BREACHED_COMMON = new Set([
|
|
31
|
+
'password', 'password1', 'password123', '123456', '12345678', '123456789',
|
|
32
|
+
'qwerty', 'qwerty123', 'letmein', 'admin', 'admin123', 'welcome', 'iloveyou',
|
|
33
|
+
'monkey', 'dragon', 'football', 'changeme', 'passw0rd', 'p@ssw0rd', 'test1234',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
// --- Validation logic -------------------------------------------------------
|
|
37
|
+
export function countClasses(pw) {
|
|
38
|
+
let n = 0;
|
|
39
|
+
if (/[a-z]/.test(pw)) n++;
|
|
40
|
+
if (/[A-Z]/.test(pw)) n++;
|
|
41
|
+
if (/[0-9]/.test(pw)) n++;
|
|
42
|
+
if (/[^A-Za-z0-9]/.test(pw)) n++;
|
|
43
|
+
return n;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validatePassword(pw, policy = POLICY) {
|
|
47
|
+
const failures = [];
|
|
48
|
+
if (typeof pw !== 'string') {
|
|
49
|
+
return { ok: false, failures: ['password must be a string'] };
|
|
50
|
+
}
|
|
51
|
+
if (pw.length < policy.minLength) {
|
|
52
|
+
failures.push(`too short: ${pw.length} < ${policy.minLength} chars`);
|
|
53
|
+
}
|
|
54
|
+
if (pw.length > policy.maxLength) {
|
|
55
|
+
failures.push(`too long: ${pw.length} > ${policy.maxLength} chars`);
|
|
56
|
+
}
|
|
57
|
+
const classes = countClasses(pw);
|
|
58
|
+
if (classes < policy.minClasses) {
|
|
59
|
+
failures.push(`not enough character classes: ${classes} < ${policy.minClasses}`);
|
|
60
|
+
}
|
|
61
|
+
// Case-insensitive membership check against the breached/common denylist.
|
|
62
|
+
if (BREACHED_COMMON.has(pw.toLowerCase())) {
|
|
63
|
+
failures.push('matches a known breached/common password');
|
|
64
|
+
}
|
|
65
|
+
return { ok: failures.length === 0, failures };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- CLI plumbing -----------------------------------------------------------
|
|
69
|
+
function check(label, pw) {
|
|
70
|
+
const { ok, failures } = validatePassword(pw);
|
|
71
|
+
if (ok) {
|
|
72
|
+
console.log(`OK ${label}: password satisfies policy`);
|
|
73
|
+
} else {
|
|
74
|
+
console.error(`FAIL ${label}:`);
|
|
75
|
+
failures.forEach((f) => console.error(` - ${f}`));
|
|
76
|
+
}
|
|
77
|
+
return ok;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function selftest() {
|
|
81
|
+
// Each case asserts the *expected* verdict so the selftest catches regressions
|
|
82
|
+
// in either direction (a strong password wrongly rejected, or a weak one passed).
|
|
83
|
+
const cases = [
|
|
84
|
+
// name, password (obviously fake), shouldPass
|
|
85
|
+
['strong_passphrase', 'Tr0ubadour&Horse-Battery!', true],
|
|
86
|
+
['strong_mixed', 'X9k!mQ2$wLp7vZ', true],
|
|
87
|
+
['weak_short', 'Ab1!', false],
|
|
88
|
+
['weak_common', 'password123', false],
|
|
89
|
+
['weak_one_class', 'aaaaaaaaaaaaaaaa', false],
|
|
90
|
+
['weak_two_classes', 'lowercaseonly123', false],
|
|
91
|
+
];
|
|
92
|
+
let allExpected = true;
|
|
93
|
+
for (const [name, pw, shouldPass] of cases) {
|
|
94
|
+
const { ok } = validatePassword(pw);
|
|
95
|
+
const correct = ok === shouldPass;
|
|
96
|
+
allExpected = allExpected && correct;
|
|
97
|
+
const mark = correct ? 'OK' : 'XX';
|
|
98
|
+
console.log(
|
|
99
|
+
`${mark} selftest ${name}: ${ok ? 'accepted' : 'rejected'} (expected ${shouldPass ? 'accepted' : 'rejected'})`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
process.exit(allExpected ? 0 : 1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const arg = process.argv[2];
|
|
106
|
+
if (arg === '--selftest') {
|
|
107
|
+
selftest();
|
|
108
|
+
} else if (arg !== undefined) {
|
|
109
|
+
process.exit(check('password', arg) ? 0 : 1);
|
|
110
|
+
} else {
|
|
111
|
+
let buf = '';
|
|
112
|
+
process.stdin.on('data', (c) => (buf += c));
|
|
113
|
+
process.stdin.on('end', () => process.exit(check('stdin', buf.replace(/\r?\n$/, '')) ? 0 : 1));
|
|
114
|
+
}
|