@periodic/tungsten 1.0.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/CHANGELOG.md +44 -0
- package/LICENSE +21 -0
- package/README.md +814 -0
- package/dist/index.d.mts +357 -0
- package/dist/index.d.ts +357 -0
- package/dist/index.js +557 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +529 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +79 -0
package/README.md
ADDED
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
# ๐ฉ Periodic Tungsten
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@periodic/tungsten)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
**Production-grade, security-auditable authentication primitives for Node.js with TypeScript support**
|
|
8
|
+
|
|
9
|
+
Part of the **Periodic** series of Node.js packages by Uday Thakur.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## ๐ก Why Tungsten?
|
|
14
|
+
|
|
15
|
+
**Tungsten** gets its name from the chemical element renowned for having the highest melting point of all metals โ it holds its structure under conditions that destroy everything else. In engineering, tungsten is the material you reach for when the environment is too extreme for anything less. Just like tungsten performs where other materials fail, this library **handles authentication under conditions where a mistake means a breach**.
|
|
16
|
+
|
|
17
|
+
In chemistry, tungsten forms exceptionally strong carbide compounds used in cutting tools and armour-piercing projectiles โ its strength comes not from softness or flexibility, but from density and resistance to deformation. Similarly, **@periodic/tungsten** is uncompromising: constant-time comparisons, memory-hard hashing, cryptographically secure generation, and no shortcuts.
|
|
18
|
+
|
|
19
|
+
The name represents:
|
|
20
|
+
- **Hardness**: Cryptographic primitives that don't bend under attack
|
|
21
|
+
- **Precision**: Every operation is explicit โ no magic, no hidden configuration
|
|
22
|
+
- **Durability**: Secure defaults that remain correct over time and key rotation
|
|
23
|
+
- **Purity**: No framework coupling, no database assumptions, no transport opinions
|
|
24
|
+
|
|
25
|
+
Just as tungsten is the material engineers trust in the most demanding environments, **@periodic/tungsten** is the authentication layer you trust when the cost of failure is highest.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## ๐ฏ Why Choose Tungsten?
|
|
30
|
+
|
|
31
|
+
Authentication is one of the most common sources of critical security vulnerabilities โ and most implementations get the subtle parts wrong:
|
|
32
|
+
|
|
33
|
+
- **DIY JWT libraries** miss `issuer` and `audience` validation, opening the door to token confusion attacks
|
|
34
|
+
- **bcrypt** is showing its age โ Argon2id is the current OWASP recommendation and tungsten uses it by default
|
|
35
|
+
- **API key generation** with `Math.random()` or weak entropy is endemic in backend codebases
|
|
36
|
+
- **No refresh token rotation** means stolen tokens are valid forever
|
|
37
|
+
- **Timing-unsafe comparisons** in API key and HMAC verification leak information to attackers
|
|
38
|
+
- **No key rotation support** means rotating credentials requires a deployment instead of a config change
|
|
39
|
+
|
|
40
|
+
**Periodic Tungsten** provides the perfect solution:
|
|
41
|
+
|
|
42
|
+
โ
**Zero framework dependencies** โ works with Express, Fastify, Koa, or no framework at all
|
|
43
|
+
โ
**JWT Access & Refresh Tokens** โ HS256 and RS256 with key rotation built in
|
|
44
|
+
โ
**Argon2id Password Hashing** โ OWASP-recommended defaults, constant-time verification
|
|
45
|
+
โ
**API Key Generation** โ cryptographically secure with prefix support
|
|
46
|
+
โ
**Opaque Tokens** โ session identifier generation
|
|
47
|
+
โ
**TOTP (RFC 6238)** โ time-based one-time passwords for 2FA
|
|
48
|
+
โ
**HMAC Request Signing** โ webhook verification with replay protection
|
|
49
|
+
โ
**Cookie Utilities** โ secure configuration helpers
|
|
50
|
+
โ
**Multi-Tenant Key Abstraction** โ enterprise key management
|
|
51
|
+
โ
**Key Rotation** โ add, retire, and switch signing keys without downtime
|
|
52
|
+
โ
**Type-safe** โ strict TypeScript throughout, zero `any`
|
|
53
|
+
โ
**Tree-shakeable** โ ESM + CJS, import only what you use
|
|
54
|
+
โ
**No global state** โ no side effects on import
|
|
55
|
+
โ
**Production-ready** โ timing-safe, entropy-safe, no secret leakage
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## ๐ฆ Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install @periodic/tungsten
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or with yarn:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
yarn add @periodic/tungsten
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## ๐ Quick Start
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import {
|
|
77
|
+
signAccessToken,
|
|
78
|
+
verifyAccessToken,
|
|
79
|
+
hashPassword,
|
|
80
|
+
verifyPassword,
|
|
81
|
+
generateApiKey,
|
|
82
|
+
SimpleKeyProvider,
|
|
83
|
+
} from '@periodic/tungsten';
|
|
84
|
+
|
|
85
|
+
// JWT
|
|
86
|
+
const keyProvider = new SimpleKeyProvider('your-secret-key-min-32-chars', 'HS256');
|
|
87
|
+
const token = await signAccessToken({ sub: 'user_123', role: 'admin' }, {
|
|
88
|
+
expiresIn: '15m',
|
|
89
|
+
issuer: 'api.example.com',
|
|
90
|
+
audience: 'dashboard',
|
|
91
|
+
keyProvider,
|
|
92
|
+
});
|
|
93
|
+
const payload = await verifyAccessToken(token, { keyProvider, issuer: 'api.example.com', audience: 'dashboard' });
|
|
94
|
+
|
|
95
|
+
// Password
|
|
96
|
+
const hash = await hashPassword('MySecurePassword123!');
|
|
97
|
+
const isValid = await verifyPassword('MySecurePassword123!', hash);
|
|
98
|
+
|
|
99
|
+
// API Key
|
|
100
|
+
const apiKey = generateApiKey({ prefix: 'sk_live_' }); // sk_live_xYz123...
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Example token payload:**
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"sub": "user_123",
|
|
108
|
+
"role": "admin",
|
|
109
|
+
"iss": "api.example.com",
|
|
110
|
+
"aud": "dashboard",
|
|
111
|
+
"iat": 1708000000,
|
|
112
|
+
"exp": 1708000900,
|
|
113
|
+
"jti": "01HQ4K2N..."
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## ๐ง Core Concepts
|
|
120
|
+
|
|
121
|
+
### Key Providers
|
|
122
|
+
|
|
123
|
+
- **Key providers are the central abstraction** โ they decouple signing keys from the functions that use them
|
|
124
|
+
- `SimpleKeyProvider` for single-tenant, single-key setups
|
|
125
|
+
- `RotatingKeyProvider` for production systems that need to retire old keys without downtime
|
|
126
|
+
- **Pass the provider at call time** โ no global state, safe for multi-tenant apps
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// Single key
|
|
130
|
+
const provider = new SimpleKeyProvider('your-secret-key', 'HS256');
|
|
131
|
+
|
|
132
|
+
// Key rotation โ sign with new key, verify with old and new
|
|
133
|
+
const provider = new RotatingKeyProvider({
|
|
134
|
+
kid: process.env.CURRENT_KEY_ID,
|
|
135
|
+
secret: process.env.CURRENT_KEY_SECRET,
|
|
136
|
+
algorithm: 'HS256',
|
|
137
|
+
});
|
|
138
|
+
provider.addKey(process.env.OLD_KEY_ID, process.env.OLD_KEY_SECRET, 'HS256');
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Security Model
|
|
142
|
+
|
|
143
|
+
**Design principle:**
|
|
144
|
+
> Every function is explicit. Nothing reads from `process.env`, nothing has global configuration, nothing silently falls back to insecure defaults. If a parameter is required for security, it is required by the type system.
|
|
145
|
+
|
|
146
|
+
- All comparisons are timing-safe โ no early exits that leak information
|
|
147
|
+
- All generation uses `crypto.randomBytes` โ no `Math.random()`
|
|
148
|
+
- All hashing uses Argon2id with OWASP-recommended parameters
|
|
149
|
+
- All JWT verification validates `issuer` and `audience` when provided
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## โจ Features
|
|
154
|
+
|
|
155
|
+
### ๐ JWT Access & Refresh Tokens
|
|
156
|
+
|
|
157
|
+
Sign and verify tokens with HS256 or RS256, with key rotation and refresh token replay detection:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { signAccessToken, verifyAccessToken, rotateRefreshToken } from '@periodic/tungsten';
|
|
161
|
+
|
|
162
|
+
// Sign
|
|
163
|
+
const token = await signAccessToken({ sub: 'user_123' }, {
|
|
164
|
+
expiresIn: '15m',
|
|
165
|
+
issuer: 'api.example.com',
|
|
166
|
+
audience: 'dashboard',
|
|
167
|
+
keyProvider,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Verify
|
|
171
|
+
const payload = await verifyAccessToken(token, {
|
|
172
|
+
keyProvider,
|
|
173
|
+
issuer: 'api.example.com',
|
|
174
|
+
audience: 'dashboard',
|
|
175
|
+
clockTolerance: 60, // seconds
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Rotate refresh token with replay detection
|
|
179
|
+
const result = await rotateRefreshToken(oldToken, {
|
|
180
|
+
keyProvider,
|
|
181
|
+
onTokenReused: async (jti) => {
|
|
182
|
+
logger.warn('Token reuse detected โ revoking all sessions', { jti });
|
|
183
|
+
await revokeAllUserSessions(jti);
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### ๐ Password Hashing
|
|
189
|
+
|
|
190
|
+
Argon2id with OWASP-recommended defaults and constant-time verification:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { hashPassword, verifyPassword } from '@periodic/tungsten';
|
|
194
|
+
|
|
195
|
+
const hash = await hashPassword('MySecurePassword123!');
|
|
196
|
+
// Defaults: 64MB memory, 3 iterations, parallelism 4
|
|
197
|
+
|
|
198
|
+
const isValid = await verifyPassword('MySecurePassword123!', hash);
|
|
199
|
+
// Constant-time โ safe against timing attacks
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### ๐๏ธ API Key Generation
|
|
203
|
+
|
|
204
|
+
Cryptographically secure generation with timing-safe verification:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { generateApiKey, hashApiKey, verifyApiKey } from '@periodic/tungsten';
|
|
208
|
+
|
|
209
|
+
const apiKey = generateApiKey({ prefix: 'sk_live_', length: 32 });
|
|
210
|
+
// sk_live_xYz123... (crypto.randomBytes, min 16-byte entropy enforced)
|
|
211
|
+
|
|
212
|
+
const hash = hashApiKey(apiKey); // SHA-256, store this
|
|
213
|
+
const isValid = verifyApiKey(apiKey, hash); // timing-safe comparison
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### ๐ TOTP (Two-Factor Authentication)
|
|
217
|
+
|
|
218
|
+
RFC 6238 compliant, compatible with Google Authenticator and Authy:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { generateTOTPSecret, generateTOTP, verifyTOTP } from '@periodic/tungsten';
|
|
222
|
+
|
|
223
|
+
const secret = generateTOTPSecret(); // show QR code to user
|
|
224
|
+
const code = generateTOTP(secret, { period: 30, digits: 6 });
|
|
225
|
+
|
|
226
|
+
const result = verifyTOTP(userProvidedCode, secret, { window: 1 });
|
|
227
|
+
if (result.valid) {
|
|
228
|
+
console.log('2FA verified');
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### โ๏ธ HMAC Request Signing
|
|
233
|
+
|
|
234
|
+
Webhook payload signing with replay protection:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import { signPayload, verifySignature } from '@periodic/tungsten';
|
|
238
|
+
|
|
239
|
+
const signature = signPayload({ event: 'user.created', userId: '123' }, 'webhook-secret');
|
|
240
|
+
const isValid = verifySignature(payload, signature, 'webhook-secret');
|
|
241
|
+
// Validates timestamp โ rejects requests older than 5 minutes by default
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### ๐ช Cookie Utilities
|
|
245
|
+
|
|
246
|
+
Secure cookie configuration helpers:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import { getSecureCookieOptions } from '@periodic/tungsten';
|
|
250
|
+
|
|
251
|
+
const options = getSecureCookieOptions({
|
|
252
|
+
maxAge: 15 * 60, // 15 minutes
|
|
253
|
+
sameSite: 'strict',
|
|
254
|
+
});
|
|
255
|
+
// httpOnly: true, secure: true, sameSite: 'strict' โ safe defaults
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### ๐ข Key Rotation
|
|
259
|
+
|
|
260
|
+
Add, retire, and switch signing keys without downtime:
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
import { RotatingKeyProvider } from '@periodic/tungsten';
|
|
264
|
+
|
|
265
|
+
const provider = new RotatingKeyProvider({
|
|
266
|
+
kid: 'key-2025-01',
|
|
267
|
+
secret: process.env.KEY_2025_01,
|
|
268
|
+
algorithm: 'HS256',
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Add new key โ start signing with it
|
|
272
|
+
provider.addKey('key-2025-02', process.env.KEY_2025_02, 'HS256');
|
|
273
|
+
provider.setCurrentKey('key-2025-02');
|
|
274
|
+
|
|
275
|
+
// Old key stays registered for verifying tokens still in circulation
|
|
276
|
+
// Remove it once all old tokens have expired
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## ๐ Common Patterns
|
|
282
|
+
|
|
283
|
+
### 1. Authentication Middleware
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
import { verifyAccessToken, SimpleKeyProvider } from '@periodic/tungsten';
|
|
287
|
+
|
|
288
|
+
const keyProvider = new SimpleKeyProvider(process.env.JWT_SECRET, 'HS256');
|
|
289
|
+
|
|
290
|
+
export async function authMiddleware(req, res, next) {
|
|
291
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
292
|
+
if (!token) return res.status(401).json({ error: 'Missing token' });
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const payload = await verifyAccessToken(token, {
|
|
296
|
+
keyProvider,
|
|
297
|
+
issuer: process.env.JWT_ISSUER,
|
|
298
|
+
audience: process.env.JWT_AUDIENCE,
|
|
299
|
+
});
|
|
300
|
+
req.user = payload;
|
|
301
|
+
next();
|
|
302
|
+
} catch {
|
|
303
|
+
res.status(401).json({ error: 'Invalid token' });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### 2. Registration and Login
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { hashPassword, verifyPassword, signAccessToken } from '@periodic/tungsten';
|
|
312
|
+
|
|
313
|
+
async function register(email: string, password: string) {
|
|
314
|
+
const hash = await hashPassword(password);
|
|
315
|
+
await db.users.create({ email, passwordHash: hash });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function login(email: string, password: string) {
|
|
319
|
+
const user = await db.users.findOne({ email });
|
|
320
|
+
const isValid = await verifyPassword(password, user.passwordHash);
|
|
321
|
+
if (!isValid) throw new Error('Invalid credentials');
|
|
322
|
+
|
|
323
|
+
return signAccessToken({ sub: user.id, role: user.role }, {
|
|
324
|
+
expiresIn: '15m',
|
|
325
|
+
keyProvider,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### 3. Refresh Token Rotation
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import { rotateRefreshToken } from '@periodic/tungsten';
|
|
334
|
+
|
|
335
|
+
app.post('/auth/refresh', async (req, res) => {
|
|
336
|
+
try {
|
|
337
|
+
const result = await rotateRefreshToken(req.cookies.refresh_token, {
|
|
338
|
+
keyProvider,
|
|
339
|
+
onTokenReused: async (jti) => {
|
|
340
|
+
logger.warn('Refresh token reuse โ possible theft', { jti });
|
|
341
|
+
await db.sessions.revokeAll({ jti });
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
res.cookie('refresh_token', result.newToken, getSecureCookieOptions({ maxAge: 7 * 24 * 60 * 60 }));
|
|
346
|
+
res.json({ accessToken: result.accessToken });
|
|
347
|
+
} catch {
|
|
348
|
+
res.status(401).json({ error: 'Invalid refresh token' });
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### 4. API Key Issuance and Verification
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
import { generateApiKey, hashApiKey, verifyApiKey } from '@periodic/tungsten';
|
|
357
|
+
|
|
358
|
+
// Issuance โ show the plain key once, store only the hash
|
|
359
|
+
async function issueApiKey(userId: string) {
|
|
360
|
+
const apiKey = generateApiKey({ prefix: 'sk_live_' });
|
|
361
|
+
const hash = hashApiKey(apiKey);
|
|
362
|
+
await db.apiKeys.create({ userId, hash, createdAt: new Date() });
|
|
363
|
+
return apiKey; // return to user once โ never stored
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Verification
|
|
367
|
+
async function verifyApiKeyRequest(providedKey: string) {
|
|
368
|
+
const keys = await db.apiKeys.findAll();
|
|
369
|
+
return keys.find(k => verifyApiKey(providedKey, k.hash));
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### 5. TOTP Enrollment and Verification
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
import { generateTOTPSecret, verifyTOTP } from '@periodic/tungsten';
|
|
377
|
+
import QRCode from 'qrcode';
|
|
378
|
+
|
|
379
|
+
async function enrollTOTP(userId: string) {
|
|
380
|
+
const secret = generateTOTPSecret();
|
|
381
|
+
await db.users.update({ userId }, { totpSecret: secret, totpEnabled: false });
|
|
382
|
+
|
|
383
|
+
const otpAuthUrl = `otpauth://totp/MyApp:${userId}?secret=${secret}&issuer=MyApp`;
|
|
384
|
+
const qrCode = await QRCode.toDataURL(otpAuthUrl);
|
|
385
|
+
return { secret, qrCode };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function verifyAndActivateTOTP(userId: string, code: string) {
|
|
389
|
+
const user = await db.users.findOne({ userId });
|
|
390
|
+
const result = verifyTOTP(code, user.totpSecret);
|
|
391
|
+
if (!result.valid) throw new Error('Invalid TOTP code');
|
|
392
|
+
await db.users.update({ userId }, { totpEnabled: true });
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### 6. Webhook Signing and Verification
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
import { signPayload, verifySignature } from '@periodic/tungsten';
|
|
400
|
+
|
|
401
|
+
// Signing outbound webhooks
|
|
402
|
+
async function sendWebhook(url: string, event: object) {
|
|
403
|
+
const signature = signPayload(event, process.env.WEBHOOK_SECRET);
|
|
404
|
+
await fetch(url, {
|
|
405
|
+
method: 'POST',
|
|
406
|
+
headers: { 'X-Signature': signature, 'Content-Type': 'application/json' },
|
|
407
|
+
body: JSON.stringify(event),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Verifying inbound webhooks
|
|
412
|
+
app.post('/webhooks/stripe', (req, res) => {
|
|
413
|
+
const isValid = verifySignature(req.body, req.headers['x-signature'], process.env.WEBHOOK_SECRET);
|
|
414
|
+
if (!isValid) return res.status(401).json({ error: 'Invalid signature' });
|
|
415
|
+
// process event
|
|
416
|
+
res.sendStatus(200);
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### 7. Structured Logging Integration
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
|
|
424
|
+
import { rotateRefreshToken } from '@periodic/tungsten';
|
|
425
|
+
|
|
426
|
+
const logger = createLogger({
|
|
427
|
+
transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
await rotateRefreshToken(oldToken, {
|
|
431
|
+
keyProvider,
|
|
432
|
+
onTokenReused: async (jti) => {
|
|
433
|
+
logger.warn('tungsten.token_reuse', { jti, severity: 'high' });
|
|
434
|
+
await revokeAllUserSessions(jti);
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### 8. Production Configuration
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
import {
|
|
443
|
+
RotatingKeyProvider,
|
|
444
|
+
signAccessToken,
|
|
445
|
+
verifyAccessToken,
|
|
446
|
+
hashPassword,
|
|
447
|
+
verifyPassword,
|
|
448
|
+
} from '@periodic/tungsten';
|
|
449
|
+
|
|
450
|
+
const keyProvider = new RotatingKeyProvider({
|
|
451
|
+
kid: process.env.JWT_KEY_ID,
|
|
452
|
+
secret: process.env.JWT_KEY_SECRET,
|
|
453
|
+
algorithm: 'HS256',
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Register previous key for tokens still in circulation
|
|
457
|
+
if (process.env.JWT_PREV_KEY_ID) {
|
|
458
|
+
keyProvider.addKey(process.env.JWT_PREV_KEY_ID, process.env.JWT_PREV_KEY_SECRET, 'HS256');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export const auth = {
|
|
462
|
+
sign: (payload: object) =>
|
|
463
|
+
signAccessToken(payload, {
|
|
464
|
+
expiresIn: '15m',
|
|
465
|
+
issuer: process.env.JWT_ISSUER,
|
|
466
|
+
audience: process.env.JWT_AUDIENCE,
|
|
467
|
+
keyProvider,
|
|
468
|
+
}),
|
|
469
|
+
|
|
470
|
+
verify: (token: string) =>
|
|
471
|
+
verifyAccessToken(token, {
|
|
472
|
+
keyProvider,
|
|
473
|
+
issuer: process.env.JWT_ISSUER,
|
|
474
|
+
audience: process.env.JWT_AUDIENCE,
|
|
475
|
+
clockTolerance: 60,
|
|
476
|
+
}),
|
|
477
|
+
|
|
478
|
+
hashPassword,
|
|
479
|
+
verifyPassword,
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
export default auth;
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## ๐๏ธ Configuration Options
|
|
488
|
+
|
|
489
|
+
### `signAccessToken` Options
|
|
490
|
+
|
|
491
|
+
| Option | Type | Default | Description |
|
|
492
|
+
|--------|------|---------|-------------|
|
|
493
|
+
| `keyProvider` | `KeyProvider` | required | Key provider instance |
|
|
494
|
+
| `expiresIn` | `string \| number` | required | Expiration (e.g. `'15m'`, `'1h'`) |
|
|
495
|
+
| `issuer` | `string` | โ | Token issuer (`iss` claim) |
|
|
496
|
+
| `audience` | `string \| string[]` | โ | Token audience (`aud` claim) |
|
|
497
|
+
| `kid` | `string` | โ | Key ID override for rotation |
|
|
498
|
+
|
|
499
|
+
### `verifyAccessToken` Options
|
|
500
|
+
|
|
501
|
+
| Option | Type | Default | Description |
|
|
502
|
+
|--------|------|---------|-------------|
|
|
503
|
+
| `keyProvider` | `KeyProvider` | required | Key provider instance |
|
|
504
|
+
| `issuer` | `string` | โ | Expected issuer (validated if provided) |
|
|
505
|
+
| `audience` | `string \| string[]` | โ | Expected audience (validated if provided) |
|
|
506
|
+
| `clockTolerance` | `number` | `60` | Clock skew tolerance in seconds |
|
|
507
|
+
|
|
508
|
+
### `generateApiKey` Options
|
|
509
|
+
|
|
510
|
+
| Option | Type | Default | Description |
|
|
511
|
+
|--------|------|---------|-------------|
|
|
512
|
+
| `prefix` | `string` | โ | Key prefix (e.g. `'sk_live_'`) |
|
|
513
|
+
| `length` | `number` | `32` | Key entropy in bytes (min: 16) |
|
|
514
|
+
|
|
515
|
+
### `generateTOTP` / `verifyTOTP` Options
|
|
516
|
+
|
|
517
|
+
| Option | Type | Default | Description |
|
|
518
|
+
|--------|------|---------|-------------|
|
|
519
|
+
| `period` | `number` | `30` | Time step in seconds |
|
|
520
|
+
| `digits` | `number` | `6` | Code length |
|
|
521
|
+
| `algorithm` | `'SHA1' \| 'SHA256' \| 'SHA512'` | `'SHA1'` | Hash algorithm |
|
|
522
|
+
| `window` | `number` | `1` | Verification tolerance window |
|
|
523
|
+
|
|
524
|
+
### `RotatingKeyProvider`
|
|
525
|
+
|
|
526
|
+
| Method | Description |
|
|
527
|
+
|--------|-------------|
|
|
528
|
+
| `addKey(kid, secret, algorithm)` | Register an additional key for verification |
|
|
529
|
+
| `setCurrentKey(kid)` | Switch signing to a different registered key |
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## ๐ API Reference
|
|
534
|
+
|
|
535
|
+
### JWT
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
signAccessToken(payload: JWTPayload, options: SignAccessTokenOptions): Promise<string>
|
|
539
|
+
verifyAccessToken(token: string, options: VerifyAccessTokenOptions): Promise<JWTPayload>
|
|
540
|
+
rotateRefreshToken(oldToken: string, options: RotateRefreshTokenOptions): Promise<RefreshTokenRotationResult>
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Password
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
hashPassword(password: string): Promise<string>
|
|
547
|
+
verifyPassword(password: string, hash: string): Promise<boolean>
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### API Keys
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
generateApiKey(options?: GenerateApiKeyOptions): string
|
|
554
|
+
hashApiKey(apiKey: string): string
|
|
555
|
+
verifyApiKey(apiKey: string, hash: string): boolean
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### TOTP
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
generateTOTPSecret(): string
|
|
562
|
+
generateTOTP(secret: string, options?: TOTPOptions): string
|
|
563
|
+
verifyTOTP(code: string, secret: string, options?: TOTPOptions): TOTPVerificationResult
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### HMAC
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
signPayload(payload: unknown, secret: string): string
|
|
570
|
+
verifySignature(payload: unknown, signature: string, secret: string): boolean
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### Key Providers
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
new SimpleKeyProvider(secret: string, algorithm: Algorithm, kid?: string): KeyProvider
|
|
577
|
+
new RotatingKeyProvider(primaryKey: KeyConfig): KeyProvider
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### Types
|
|
581
|
+
|
|
582
|
+
```typescript
|
|
583
|
+
import type {
|
|
584
|
+
JWTPayload,
|
|
585
|
+
KeyProvider,
|
|
586
|
+
SignAccessTokenOptions,
|
|
587
|
+
VerifyAccessTokenOptions,
|
|
588
|
+
GenerateApiKeyOptions,
|
|
589
|
+
TOTPOptions,
|
|
590
|
+
TOTPVerificationResult,
|
|
591
|
+
RefreshTokenRotationResult,
|
|
592
|
+
} from '@periodic/tungsten';
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## ๐งฉ Architecture
|
|
598
|
+
|
|
599
|
+
```
|
|
600
|
+
@periodic/tungsten/
|
|
601
|
+
โโโ src/
|
|
602
|
+
โ โโโ jwt/
|
|
603
|
+
โ โ โโโ sign.ts # signAccessToken()
|
|
604
|
+
โ โ โโโ verify.ts # verifyAccessToken()
|
|
605
|
+
โ โ โโโ refresh.ts # rotateRefreshToken() + replay detection
|
|
606
|
+
โ โโโ password/
|
|
607
|
+
โ โ โโโ index.ts # hashPassword(), verifyPassword() โ Argon2id
|
|
608
|
+
โ โโโ apikey/
|
|
609
|
+
โ โ โโโ index.ts # generateApiKey(), hashApiKey(), verifyApiKey()
|
|
610
|
+
โ โโโ totp/
|
|
611
|
+
โ โ โโโ index.ts # generateTOTPSecret(), generateTOTP(), verifyTOTP()
|
|
612
|
+
โ โโโ hmac/
|
|
613
|
+
โ โ โโโ index.ts # signPayload(), verifySignature()
|
|
614
|
+
โ โโโ cookies/
|
|
615
|
+
โ โ โโโ index.ts # getSecureCookieOptions()
|
|
616
|
+
โ โโโ keys/
|
|
617
|
+
โ โ โโโ simple.ts # SimpleKeyProvider
|
|
618
|
+
โ โ โโโ rotating.ts # RotatingKeyProvider
|
|
619
|
+
โ โโโ types.ts # All shared TypeScript interfaces
|
|
620
|
+
โ โโโ index.ts # Public API
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
**Design Philosophy:**
|
|
624
|
+
- **Primitives only** โ no framework coupling, no database assumptions, no transport opinions
|
|
625
|
+
- **Explicit over implicit** โ every security parameter is required or has a safe default
|
|
626
|
+
- **No global state** โ all configuration is passed at call time
|
|
627
|
+
- **Timing-safe throughout** โ no early exits in comparisons that could leak information
|
|
628
|
+
- **Key providers** decouple signing keys from the functions that use them โ swap without changing call sites
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
## ๐ Performance
|
|
633
|
+
|
|
634
|
+
- **Argon2id** is intentionally slow for password hashing โ that's the security property
|
|
635
|
+
- **JWT signing and verification** are async and non-blocking
|
|
636
|
+
- **API key generation** uses `crypto.randomBytes` โ synchronous but fast
|
|
637
|
+
- **TOTP** verification is synchronous and sub-millisecond
|
|
638
|
+
- **No global state** โ multiple instances in the same process are fully isolated
|
|
639
|
+
- **Tree-shakeable** โ only the primitives you use end up in your bundle
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
|
|
643
|
+
## ๐ซ Explicit Non-Goals
|
|
644
|
+
|
|
645
|
+
This package **intentionally does not** include:
|
|
646
|
+
|
|
647
|
+
โ Session management or storage (bring your own database)
|
|
648
|
+
โ OAuth / OpenID Connect flows (use a dedicated OAuth library)
|
|
649
|
+
โ Framework middleware (adapt the primitives yourself)
|
|
650
|
+
โ User model or database schema (no opinions on your data layer)
|
|
651
|
+
โ Rate limiting (use `@periodic/titanium` for that)
|
|
652
|
+
โ HTTP transport (no `req`/`res` coupling)
|
|
653
|
+
โ Magic or implicit behavior on import
|
|
654
|
+
โ Configuration files (configure in code)
|
|
655
|
+
|
|
656
|
+
Focus on doing one thing well: **cryptographically sound, framework-agnostic authentication primitives**.
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
## ๐จ TypeScript Support
|
|
661
|
+
|
|
662
|
+
Full TypeScript support with complete type safety:
|
|
663
|
+
|
|
664
|
+
```typescript
|
|
665
|
+
import type {
|
|
666
|
+
JWTPayload,
|
|
667
|
+
KeyProvider,
|
|
668
|
+
TOTPVerificationResult,
|
|
669
|
+
RefreshTokenRotationResult,
|
|
670
|
+
} from '@periodic/tungsten';
|
|
671
|
+
|
|
672
|
+
// Generic payload โ type flows through automatically
|
|
673
|
+
const payload = await verifyAccessToken<{ sub: string; role: 'admin' | 'user' }>(token, options);
|
|
674
|
+
payload.role; // typed as 'admin' | 'user'
|
|
675
|
+
|
|
676
|
+
// TOTPVerificationResult is discriminated
|
|
677
|
+
const result = verifyTOTP(code, secret);
|
|
678
|
+
if (result.valid) {
|
|
679
|
+
result.delta; // number โ present only when valid
|
|
680
|
+
}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
---
|
|
684
|
+
|
|
685
|
+
## ๐งช Testing
|
|
686
|
+
|
|
687
|
+
```bash
|
|
688
|
+
# Run tests
|
|
689
|
+
npm test
|
|
690
|
+
|
|
691
|
+
# Run tests with coverage
|
|
692
|
+
npm run test:coverage
|
|
693
|
+
|
|
694
|
+
# Run tests in watch mode
|
|
695
|
+
npm run test:watch
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**Note:** All tests achieve >80% code coverage.
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
## ๐ค Related Packages
|
|
703
|
+
|
|
704
|
+
Part of the **Periodic** series by Uday Thakur:
|
|
705
|
+
|
|
706
|
+
- [**@periodic/iridium**](https://www.npmjs.com/package/@periodic/iridium) - Structured logging
|
|
707
|
+
- [**@periodic/arsenic**](https://www.npmjs.com/package/@periodic/arsenic) - Semantic runtime monitoring
|
|
708
|
+
- [**@periodic/zirconium**](https://www.npmjs.com/package/@periodic/zirconium) - Environment configuration
|
|
709
|
+
- [**@periodic/vanadium**](https://www.npmjs.com/package/@periodic/vanadium) - Idempotency and distributed locks
|
|
710
|
+
- [**@periodic/strontium**](https://www.npmjs.com/package/@periodic/strontium) - Resilient HTTP client
|
|
711
|
+
- [**@periodic/obsidian**](https://www.npmjs.com/package/@periodic/obsidian) - HTTP error handling
|
|
712
|
+
- [**@periodic/titanium**](https://www.npmjs.com/package/@periodic/titanium) - Rate limiting
|
|
713
|
+
- [**@periodic/osmium**](https://www.npmjs.com/package/@periodic/osmium) - Redis caching
|
|
714
|
+
|
|
715
|
+
Build complete, production-ready APIs with the Periodic series!
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
## ๐ Documentation
|
|
720
|
+
|
|
721
|
+
- [Quick Start Guide](QUICKSTART.md)
|
|
722
|
+
- [Contributing Guide](CONTRIBUTING.md)
|
|
723
|
+
- [Changelog](CHANGELOG.md)
|
|
724
|
+
|
|
725
|
+
---
|
|
726
|
+
|
|
727
|
+
## ๐ ๏ธ Production Recommendations
|
|
728
|
+
|
|
729
|
+
### Key Management
|
|
730
|
+
|
|
731
|
+
Store signing keys in environment variables or a secrets manager โ never hardcode them:
|
|
732
|
+
|
|
733
|
+
```bash
|
|
734
|
+
JWT_KEY_ID=key-2025-01
|
|
735
|
+
JWT_KEY_SECRET=your-256-bit-secret-here
|
|
736
|
+
JWT_PREV_KEY_ID=key-2024-12 # keep until old tokens expire
|
|
737
|
+
JWT_PREV_KEY_SECRET=previous-secret
|
|
738
|
+
JWT_ISSUER=api.example.com
|
|
739
|
+
JWT_AUDIENCE=dashboard
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
### Log Aggregation
|
|
743
|
+
|
|
744
|
+
Pair with `@periodic/iridium` for structured JSON output:
|
|
745
|
+
|
|
746
|
+
```typescript
|
|
747
|
+
import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
|
|
748
|
+
|
|
749
|
+
const logger = createLogger({
|
|
750
|
+
transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
await rotateRefreshToken(oldToken, {
|
|
754
|
+
keyProvider,
|
|
755
|
+
onTokenReused: async (jti) => {
|
|
756
|
+
logger.warn('tungsten.token_reuse', { jti, severity: 'critical' });
|
|
757
|
+
await revokeAllUserSessions(jti);
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
// Pipe to Elasticsearch, Datadog, CloudWatch, etc.
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
### Security Monitoring
|
|
765
|
+
|
|
766
|
+
Capture authentication anomalies in your error tracker:
|
|
767
|
+
|
|
768
|
+
```typescript
|
|
769
|
+
app.use(async (req, res, next) => {
|
|
770
|
+
try {
|
|
771
|
+
req.user = await verifyAccessToken(token, { keyProvider, issuer, audience });
|
|
772
|
+
next();
|
|
773
|
+
} catch (err) {
|
|
774
|
+
Sentry.captureException(err, { extra: { url: req.url } });
|
|
775
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
---
|
|
781
|
+
|
|
782
|
+
## ๐ License
|
|
783
|
+
|
|
784
|
+
MIT ยฉ [Uday Thakur](LICENSE)
|
|
785
|
+
|
|
786
|
+
---
|
|
787
|
+
|
|
788
|
+
## ๐ Contributing
|
|
789
|
+
|
|
790
|
+
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on:
|
|
791
|
+
|
|
792
|
+
- Code of conduct
|
|
793
|
+
- Development setup
|
|
794
|
+
- Pull request process
|
|
795
|
+
- Coding standards
|
|
796
|
+
- Architecture principles
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
800
|
+
## ๐ Support
|
|
801
|
+
|
|
802
|
+
- ๐ง **Email:** udaythakurwork@gmail.com
|
|
803
|
+
- ๐ **Issues:** [GitHub Issues](https://github.com/udaythakur7469/periodic-tungsten/issues)
|
|
804
|
+
- ๐ฌ **Discussions:** [GitHub Discussions](https://github.com/udaythakur7469/periodic-tungsten/discussions)
|
|
805
|
+
|
|
806
|
+
---
|
|
807
|
+
|
|
808
|
+
## ๐ Show Your Support
|
|
809
|
+
|
|
810
|
+
Give a โญ๏ธ if this project helped you build better applications!
|
|
811
|
+
|
|
812
|
+
---
|
|
813
|
+
|
|
814
|
+
**Built with โค๏ธ by Uday Thakur for production-grade Node.js applications**
|