@eaccess/auth 0.1.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 +877 -0
- package/dist/index.cjs +2632 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +930 -0
- package/dist/index.d.ts +930 -0
- package/dist/index.js +2555 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
# @prsm/easy-auth
|
|
2
|
+
|
|
3
|
+
An Express authentication middleware specifically designed for Postgres that provides complete authentication functionality without being tied to any specific ORM, query builder, or user table structure. Comprehensive auth without overwhelming complexity. A clean separation of concerns -- not conflating authentication with user management.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Flexible User Mapping**: Links to your existing user table structure
|
|
8
|
+
- **Zero ORM Dependencies**: Pure SQL with configurable table prefixes
|
|
9
|
+
- **Complete Auth Flow**: Registration, login, email verification, password reset
|
|
10
|
+
- **Role-based Permissions**: Built-in role system with bitmasks
|
|
11
|
+
- **Remember Me**: Persistent login tokens
|
|
12
|
+
- **Session Management**: Force logout, logout everywhere
|
|
13
|
+
- **Admin Functions**: User management and impersonation
|
|
14
|
+
- **OAuth Integration**: GitHub, Google, Azure providers with extensible architecture
|
|
15
|
+
- **TypeScript Support**: Full type safety
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @prsm/easy-auth express-session
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import express from 'express';
|
|
27
|
+
import session from 'express-session';
|
|
28
|
+
import { Pool } from 'pg';
|
|
29
|
+
import { createAuthMiddleware, createAuthTables } from '@prsm/easy-auth';
|
|
30
|
+
|
|
31
|
+
const app = express();
|
|
32
|
+
const pool = new Pool({ connectionString: 'postgresql://...' });
|
|
33
|
+
|
|
34
|
+
// Setup session middleware
|
|
35
|
+
app.use(session({
|
|
36
|
+
secret: 'your-session-secret',
|
|
37
|
+
resave: false,
|
|
38
|
+
saveUninitialized: false,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
// Configure auth middleware
|
|
42
|
+
const authConfig = {
|
|
43
|
+
db: pool,
|
|
44
|
+
tablePrefix: 'auth_', // Creates: auth_accounts, auth_confirmations, etc.
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Create auth tables (run once)
|
|
48
|
+
await createAuthTables(authConfig);
|
|
49
|
+
|
|
50
|
+
// Add auth middleware
|
|
51
|
+
app.use(createAuthMiddleware(authConfig));
|
|
52
|
+
|
|
53
|
+
// Now use auth in your routes
|
|
54
|
+
app.post('/register', async (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
// Option 1: Let the library auto-generate a UUID for the user
|
|
57
|
+
const account = await req.auth.register(
|
|
58
|
+
req.body.email,
|
|
59
|
+
req.body.password,
|
|
60
|
+
undefined, // Auto-generates UUID
|
|
61
|
+
(token) => {
|
|
62
|
+
// Send confirmation email with token
|
|
63
|
+
console.log('Confirmation token:', token);
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Option 2: Link to your existing user system
|
|
68
|
+
// const user = await db.insert(users).values({...}).returning();
|
|
69
|
+
// const account = await req.auth.register(
|
|
70
|
+
// req.body.email,
|
|
71
|
+
// req.body.password,
|
|
72
|
+
// user.id, // Link to your user
|
|
73
|
+
// (token) => {
|
|
74
|
+
// console.log('Confirmation token:', token);
|
|
75
|
+
// }
|
|
76
|
+
// );
|
|
77
|
+
res.json({ success: true, account });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
res.status(400).json({ error: error.message });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
app.post('/login', async (req, res) => {
|
|
84
|
+
try {
|
|
85
|
+
await req.auth.login(req.body.email, req.body.password, req.body.remember);
|
|
86
|
+
res.json({ success: true });
|
|
87
|
+
} catch (error) {
|
|
88
|
+
res.status(401).json({ error: error.message });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
app.get('/profile', (req, res) => {
|
|
93
|
+
if (!req.auth.isLoggedIn()) {
|
|
94
|
+
return res.status(401).json({ error: 'Not logged in' });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
res.json({
|
|
98
|
+
email: req.auth.getEmail(),
|
|
99
|
+
status: req.auth.getStatusName(),
|
|
100
|
+
roles: req.auth.getRoleNames(),
|
|
101
|
+
isAdmin: await req.auth.isAdmin(),
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## OAuth Setup
|
|
107
|
+
|
|
108
|
+
Easy-auth supports OAuth providers (GitHub, Google, Azure) with a clean, extensible API.
|
|
109
|
+
|
|
110
|
+
### OAuth Configuration
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import express from 'express';
|
|
114
|
+
import session from 'express-session';
|
|
115
|
+
import { Pool } from 'pg';
|
|
116
|
+
import { createAuthMiddleware, createAuthTables, type OAuthUserData } from '@prsm/easy-auth';
|
|
117
|
+
|
|
118
|
+
const app = express();
|
|
119
|
+
const pool = new Pool({ connectionString: 'postgresql://...' });
|
|
120
|
+
|
|
121
|
+
// Your app's user table (example)
|
|
122
|
+
const users: Array<{ id: number; name: string; email: string }> = [];
|
|
123
|
+
|
|
124
|
+
const authConfig = {
|
|
125
|
+
db: pool,
|
|
126
|
+
// Optional: OAuth createUser function to handle new user registration
|
|
127
|
+
createUser: async (userData: OAuthUserData) => {
|
|
128
|
+
// userData contains: { id, email, username?, name?, avatar? }
|
|
129
|
+
// Create user in your app's user table
|
|
130
|
+
const user = await db.insert(users).values({
|
|
131
|
+
name: userData.name || userData.username,
|
|
132
|
+
email: userData.email,
|
|
133
|
+
}).returning();
|
|
134
|
+
|
|
135
|
+
return user.id; // Return the new user's ID
|
|
136
|
+
},
|
|
137
|
+
tablePrefix: 'auth_',
|
|
138
|
+
|
|
139
|
+
// OAuth provider configuration
|
|
140
|
+
providers: {
|
|
141
|
+
github: {
|
|
142
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
143
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
144
|
+
redirectUri: 'http://localhost:3000/auth/github/callback'
|
|
145
|
+
},
|
|
146
|
+
google: {
|
|
147
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
148
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
149
|
+
redirectUri: 'http://localhost:3000/auth/google/callback'
|
|
150
|
+
},
|
|
151
|
+
azure: {
|
|
152
|
+
clientId: process.env.AZURE_CLIENT_ID!,
|
|
153
|
+
clientSecret: process.env.AZURE_CLIENT_SECRET!,
|
|
154
|
+
tenantId: process.env.AZURE_TENANT_ID!,
|
|
155
|
+
redirectUri: 'http://localhost:3000/auth/azure/callback'
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
app.use(createAuthMiddleware(authConfig));
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### OAuth Routes
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// Initiate OAuth flow
|
|
167
|
+
app.get('/auth/github', (req, res) => {
|
|
168
|
+
const authUrl = req.auth.providers.github.getAuthUrl();
|
|
169
|
+
res.redirect(authUrl);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Handle OAuth callback (this does everything!)
|
|
173
|
+
app.get('/auth/github/callback', async (req, res) => {
|
|
174
|
+
try {
|
|
175
|
+
await req.auth.providers.github.handleCallback(req);
|
|
176
|
+
res.redirect('/dashboard'); // Success!
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error.message.includes('already have an account')) {
|
|
179
|
+
res.redirect('/login?error=email_taken');
|
|
180
|
+
} else {
|
|
181
|
+
res.redirect('/login?error=oauth_failed');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Same pattern for Google and Azure
|
|
187
|
+
app.get('/auth/google', (req, res) => {
|
|
188
|
+
const authUrl = req.auth.providers.google.getAuthUrl();
|
|
189
|
+
res.redirect(authUrl);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
app.get('/auth/google/callback', async (req, res) => {
|
|
193
|
+
try {
|
|
194
|
+
await req.auth.providers.google.handleCallback(req);
|
|
195
|
+
res.redirect('/dashboard');
|
|
196
|
+
} catch (error) {
|
|
197
|
+
res.redirect('/login?error=oauth_failed');
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Frontend Integration
|
|
203
|
+
|
|
204
|
+
```html
|
|
205
|
+
<!-- Login page -->
|
|
206
|
+
<a href="/auth/github" class="oauth-btn">
|
|
207
|
+
<img src="/github-icon.svg" /> Login with GitHub
|
|
208
|
+
</a>
|
|
209
|
+
<a href="/auth/google" class="oauth-btn">
|
|
210
|
+
<img src="/google-icon.svg" /> Login with Google
|
|
211
|
+
</a>
|
|
212
|
+
<a href="/auth/azure" class="oauth-btn">
|
|
213
|
+
<img src="/azure-icon.svg" /> Login with Azure
|
|
214
|
+
</a>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### OAuth Flow Explained
|
|
218
|
+
|
|
219
|
+
1. **User clicks "Login with GitHub"** → Browser goes to `/auth/github`
|
|
220
|
+
2. **Server redirects to GitHub** → User sees GitHub's login page
|
|
221
|
+
3. **User authorizes your app** → GitHub redirects to `/auth/github/callback?code=abc123`
|
|
222
|
+
4. **Server processes callback** → `handleCallback()` does:
|
|
223
|
+
- Exchange code for access token
|
|
224
|
+
- Fetch user data from GitHub API
|
|
225
|
+
- Check if OAuth user exists (by provider + provider_id)
|
|
226
|
+
- If exists: log them in
|
|
227
|
+
- If new but email exists: throw error
|
|
228
|
+
- If completely new: call `createUser()`, create account + provider record, log them in
|
|
229
|
+
|
|
230
|
+
### OAuth Error Handling
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
app.get('/auth/github/callback', async (req, res) => {
|
|
234
|
+
try {
|
|
235
|
+
await req.auth.providers.github.handleCallback(req);
|
|
236
|
+
res.redirect('/dashboard');
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (error.message.includes('already have an account')) {
|
|
239
|
+
// Email exists with different login method
|
|
240
|
+
res.redirect('/login?error=Please use your existing email/password login');
|
|
241
|
+
} else if (error.message.includes('No authorization code')) {
|
|
242
|
+
// User cancelled or OAuth flow failed
|
|
243
|
+
res.redirect('/login?error=Authorization cancelled');
|
|
244
|
+
} else {
|
|
245
|
+
// Other OAuth errors
|
|
246
|
+
console.error('OAuth error:', error);
|
|
247
|
+
res.redirect('/login?error=Login failed, please try again');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Environment Variables
|
|
254
|
+
|
|
255
|
+
Create a `.env` file:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
# GitHub OAuth App (https://github.com/settings/developers)
|
|
259
|
+
GITHUB_CLIENT_ID=your_github_client_id
|
|
260
|
+
GITHUB_CLIENT_SECRET=your_github_client_secret
|
|
261
|
+
|
|
262
|
+
# Google OAuth App (https://console.cloud.google.com/)
|
|
263
|
+
GOOGLE_CLIENT_ID=your_google_client_id.apps.googleusercontent.com
|
|
264
|
+
GOOGLE_CLIENT_SECRET=your_google_client_secret
|
|
265
|
+
|
|
266
|
+
# Azure OAuth App (https://portal.azure.com/)
|
|
267
|
+
AZURE_CLIENT_ID=your_azure_client_id
|
|
268
|
+
AZURE_CLIENT_SECRET=your_azure_client_secret
|
|
269
|
+
AZURE_TENANT_ID=your_azure_tenant_id
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Advanced OAuth Usage
|
|
273
|
+
|
|
274
|
+
For more control over the OAuth flow:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
app.get('/auth/github/callback', async (req, res) => {
|
|
278
|
+
try {
|
|
279
|
+
// Get user data without logging in
|
|
280
|
+
const userData = await req.auth.providers.github.getUserData(req);
|
|
281
|
+
|
|
282
|
+
// Your custom logic here
|
|
283
|
+
const existingUser = await findUserByEmail(userData.email);
|
|
284
|
+
if (existingUser && !existingUser.allowOAuth) {
|
|
285
|
+
throw new Error('OAuth disabled for this account');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Then complete the OAuth flow manually
|
|
289
|
+
await req.auth.providers.github.handleCallback(req);
|
|
290
|
+
|
|
291
|
+
res.json({ success: true, user: userData });
|
|
292
|
+
} catch (error) {
|
|
293
|
+
res.status(400).json({ error: error.message });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Multi-Factor Authentication (MFA)
|
|
299
|
+
|
|
300
|
+
Easy-auth supports TOTP (authenticator apps), Email OTP, and SMS OTP for enhanced security.
|
|
301
|
+
|
|
302
|
+
### MFA Configuration
|
|
303
|
+
|
|
304
|
+
Enable MFA in your auth config:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
const authConfig = {
|
|
308
|
+
db: pool,
|
|
309
|
+
|
|
310
|
+
twoFactor: {
|
|
311
|
+
enabled: true,
|
|
312
|
+
requireForOAuth: false, // Skip MFA for OAuth users (optional)
|
|
313
|
+
issuer: 'MyApp', // TOTP issuer name
|
|
314
|
+
codeLength: 6, // OTP code length
|
|
315
|
+
tokenExpiry: '5m', // OTP expiration
|
|
316
|
+
totpWindow: 1, // TOTP time window tolerance
|
|
317
|
+
backupCodesCount: 10 // Number of backup codes
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### MFA Login Flow
|
|
323
|
+
|
|
324
|
+
When MFA is enabled, the login process becomes:
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
app.post('/login', async (req, res) => {
|
|
328
|
+
try {
|
|
329
|
+
await req.auth.login(req.body.email, req.body.password);
|
|
330
|
+
res.json({ success: true });
|
|
331
|
+
} catch (error) {
|
|
332
|
+
if (error instanceof SecondFactorRequiredError) {
|
|
333
|
+
// User needs to complete MFA
|
|
334
|
+
return res.status(202).json({
|
|
335
|
+
requiresTwoFactor: true,
|
|
336
|
+
availableMethods: error.challenge,
|
|
337
|
+
message: 'Please complete two-factor authentication'
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
res.status(401).json({ error: error.message });
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### MFA Challenge Structure
|
|
346
|
+
|
|
347
|
+
The `SecondFactorRequiredError.challenge` contains:
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
interface TwoFactorChallenge {
|
|
351
|
+
totp?: boolean; // TOTP available
|
|
352
|
+
email?: {
|
|
353
|
+
otpValue: string; // The actual OTP code that should be sent via email
|
|
354
|
+
maskedContact: string; // "j***@example.com"
|
|
355
|
+
};
|
|
356
|
+
sms?: {
|
|
357
|
+
otpValue: string; // The actual OTP code that should be sent via SMS
|
|
358
|
+
maskedContact: string; // "+1***90"
|
|
359
|
+
};
|
|
360
|
+
selectors?: {
|
|
361
|
+
email?: string; // Internal selector (stored in session & database)
|
|
362
|
+
sms?: string; // Internal selector (stored in session & database)
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Important**: The `otpValue` fields contain the actual codes that should be delivered to the user. The `selectors` are internal identifiers used by the library. In production, you should:
|
|
368
|
+
1. Send the `otpValue` codes via your email/SMS service
|
|
369
|
+
2. Remove both `otpValue` and `selectors` from client responses for security
|
|
370
|
+
3. Only return the `maskedContact` to the frontend (selectors are automatically stored in the user's session)
|
|
371
|
+
|
|
372
|
+
### Completing MFA Login
|
|
373
|
+
|
|
374
|
+
After receiving `SecondFactorRequiredError`, verify the second factor:
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
app.post('/verify-2fa', async (req, res) => {
|
|
378
|
+
try {
|
|
379
|
+
const { code, method } = req.body;
|
|
380
|
+
|
|
381
|
+
// Verify based on method
|
|
382
|
+
switch (method) {
|
|
383
|
+
case 'totp':
|
|
384
|
+
await req.auth.twoFactor.verify.totp(code);
|
|
385
|
+
break;
|
|
386
|
+
case 'email':
|
|
387
|
+
await req.auth.twoFactor.verify.email(code);
|
|
388
|
+
break;
|
|
389
|
+
case 'sms':
|
|
390
|
+
await req.auth.twoFactor.verify.sms(code);
|
|
391
|
+
break;
|
|
392
|
+
case 'backup':
|
|
393
|
+
await req.auth.twoFactor.verify.backupCode(code);
|
|
394
|
+
break;
|
|
395
|
+
case 'otp':
|
|
396
|
+
// Smart OTP - works for both email and SMS
|
|
397
|
+
await req.auth.twoFactor.verify.otp(code);
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Complete login
|
|
402
|
+
await req.auth.completeTwoFactorLogin();
|
|
403
|
+
res.json({ success: true });
|
|
404
|
+
} catch (error) {
|
|
405
|
+
res.status(400).json({ error: error.message });
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### MFA Enrollment
|
|
411
|
+
|
|
412
|
+
Users can enroll in multiple MFA methods:
|
|
413
|
+
|
|
414
|
+
#### TOTP (Authenticator App)
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
app.post('/setup-totp', async (req, res) => {
|
|
418
|
+
try {
|
|
419
|
+
const { secret, qrCode, backupCodes } = await req.auth.twoFactor.setup.totp();
|
|
420
|
+
|
|
421
|
+
// Show QR code to user for scanning with authenticator app
|
|
422
|
+
res.json({
|
|
423
|
+
secret, // Manual entry secret
|
|
424
|
+
qrCode, // QR code URL for scanning
|
|
425
|
+
backupCodes // One-time backup codes
|
|
426
|
+
});
|
|
427
|
+
} catch (error) {
|
|
428
|
+
res.status(400).json({ error: error.message });
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
#### Email OTP
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
app.post('/setup-email-2fa', async (req, res) => {
|
|
437
|
+
try {
|
|
438
|
+
await req.auth.twoFactor.setup.email();
|
|
439
|
+
res.json({ success: true });
|
|
440
|
+
} catch (error) {
|
|
441
|
+
res.status(400).json({ error: error.message });
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
#### SMS OTP
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
app.post('/setup-sms-2fa', async (req, res) => {
|
|
450
|
+
try {
|
|
451
|
+
const { phoneNumber } = req.body;
|
|
452
|
+
await req.auth.twoFactor.setup.sms(phoneNumber);
|
|
453
|
+
res.json({ success: true });
|
|
454
|
+
} catch (error) {
|
|
455
|
+
res.status(400).json({ error: error.message });
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### MFA Enrollment with Verification
|
|
461
|
+
|
|
462
|
+
For production apps, require verification during enrollment:
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
app.post('/setup-totp', async (req, res) => {
|
|
466
|
+
try {
|
|
467
|
+
// Setup but require verification
|
|
468
|
+
const { secret, qrCode } = await req.auth.twoFactor.setup.totp(true);
|
|
469
|
+
res.json({ secret, qrCode, requiresVerification: true });
|
|
470
|
+
} catch (error) {
|
|
471
|
+
res.status(400).json({ error: error.message });
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
app.post('/verify-totp-setup', async (req, res) => {
|
|
476
|
+
try {
|
|
477
|
+
const { code } = req.body;
|
|
478
|
+
const backupCodes = await req.auth.twoFactor.complete.totp(code);
|
|
479
|
+
res.json({ success: true, backupCodes });
|
|
480
|
+
} catch (error) {
|
|
481
|
+
res.status(400).json({ error: error.message });
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### MFA Management
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
// Check MFA status
|
|
490
|
+
app.get('/mfa-status', async (req, res) => {
|
|
491
|
+
const status = {
|
|
492
|
+
enabled: await req.auth.twoFactor.isEnabled(),
|
|
493
|
+
methods: {
|
|
494
|
+
totp: await req.auth.twoFactor.totpEnabled(),
|
|
495
|
+
email: await req.auth.twoFactor.emailEnabled(),
|
|
496
|
+
sms: await req.auth.twoFactor.smsEnabled()
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
res.json(status);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Disable MFA method
|
|
503
|
+
app.delete('/mfa/:method', async (req, res) => {
|
|
504
|
+
try {
|
|
505
|
+
const mechanism = req.params.method === 'totp' ? 1 :
|
|
506
|
+
req.params.method === 'email' ? 2 : 3;
|
|
507
|
+
await req.auth.twoFactor.disable(mechanism);
|
|
508
|
+
res.json({ success: true });
|
|
509
|
+
} catch (error) {
|
|
510
|
+
res.status(400).json({ error: error.message });
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// Generate new backup codes
|
|
515
|
+
app.post('/mfa/backup-codes', async (req, res) => {
|
|
516
|
+
try {
|
|
517
|
+
const backupCodes = await req.auth.twoFactor.generateNewBackupCodes();
|
|
518
|
+
res.json({ backupCodes });
|
|
519
|
+
} catch (error) {
|
|
520
|
+
res.status(400).json({ error: error.message });
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## Configuration
|
|
526
|
+
|
|
527
|
+
### User ID Mapping
|
|
528
|
+
|
|
529
|
+
The auth library maintains its own auth tables (accounts, roles, sessions) that can optionally link to your application's user records via a user ID.
|
|
530
|
+
|
|
531
|
+
**Registration now takes an optional userId parameter**:
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
app.post('/register', async (req, res) => {
|
|
535
|
+
// Option 1: Let easy-auth auto-generate a UUID (simplest)
|
|
536
|
+
const account = await req.auth.register(req.body.email, req.body.password);
|
|
537
|
+
|
|
538
|
+
// Option 2: Link to your existing user table
|
|
539
|
+
const user = await db.insert(users).values({
|
|
540
|
+
name: req.body.name,
|
|
541
|
+
email: req.body.email
|
|
542
|
+
}).returning();
|
|
543
|
+
|
|
544
|
+
const account = await req.auth.register(req.body.email, req.body.password, user.id);
|
|
545
|
+
|
|
546
|
+
res.json({ success: true, userId: user.id });
|
|
547
|
+
});
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
**For OAuth**, you can optionally provide a `createUser` function to handle new OAuth users. This is the ONLY use case for `createUser` - it's not used for regular registration or admin user creation:
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
const authConfig = {
|
|
554
|
+
db: pool,
|
|
555
|
+
// ONLY used for OAuth new user creation
|
|
556
|
+
createUser: async (userData: OAuthUserData) => {
|
|
557
|
+
// Create user in your app's user table
|
|
558
|
+
const user = await db.insert(users).values({
|
|
559
|
+
name: userData.name || userData.username,
|
|
560
|
+
email: userData.email,
|
|
561
|
+
}).returning();
|
|
562
|
+
|
|
563
|
+
return user.id; // This will be stored as user_id in auth tables
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
**If you don't provide `createUser` for OAuth**, a UUID will be auto-generated - no configuration needed!
|
|
569
|
+
|
|
570
|
+
**For login**, simply call `req.auth.login()`. You don't need to identify the user beforehand because the `login` method itself does the authentication using the provided credentials.
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
app.post('/login', async (req, res) => {
|
|
574
|
+
try {
|
|
575
|
+
await req.auth.login(req.body.email, req.body.password);
|
|
576
|
+
} catch (error) {
|
|
577
|
+
if (error instanceof UserNotFoundError || error instanceof InvalidPasswordError) {
|
|
578
|
+
return res.status(401).json({ error: 'Invalid email or password' });
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (error instanceof UserInactiveError) {
|
|
582
|
+
return res.status(403).json({ error: 'Account inactive' });
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
throw error;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
res.json({ success: true });
|
|
589
|
+
});
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Important**: If you use `req.session.userId`, it could be helpful to augment the session type if you're using TypeScript:
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
declare module "express-session" {
|
|
596
|
+
interface SessionData {
|
|
597
|
+
userId?: string;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### AuthConfig
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
interface AuthConfig {
|
|
606
|
+
// PostgreSQL connection pool
|
|
607
|
+
db: Pool;
|
|
608
|
+
|
|
609
|
+
// Optional OAuth new user creation function
|
|
610
|
+
createUser?: (userData: OAuthUserData) => string | number | Promise<string | number>; // Called when OAuth user doesn't exist in your system
|
|
611
|
+
|
|
612
|
+
// Optional settings
|
|
613
|
+
tablePrefix?: string; // default: 'user_'
|
|
614
|
+
minPasswordLength?: number; // default: 8
|
|
615
|
+
maxPasswordLength?: number; // default: 64
|
|
616
|
+
rememberDuration?: string; // default: '30d'
|
|
617
|
+
rememberCookieName?: string; // default: 'remember_token'
|
|
618
|
+
resyncInterval?: string; // default: '30s'
|
|
619
|
+
|
|
620
|
+
// OAuth provider configuration
|
|
621
|
+
providers?: {
|
|
622
|
+
github?: GitHubProviderConfig;
|
|
623
|
+
google?: GoogleProviderConfig;
|
|
624
|
+
azure?: AzureProviderConfig;
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// Multi-factor authentication
|
|
628
|
+
twoFactor?: {
|
|
629
|
+
enabled?: boolean; // default: false
|
|
630
|
+
requireForOAuth?: boolean; // default: false
|
|
631
|
+
issuer?: string; // default: 'EasyAuth'
|
|
632
|
+
codeLength?: number; // default: 6
|
|
633
|
+
tokenExpiry?: string; // default: '5m'
|
|
634
|
+
totpWindow?: number; // default: 1
|
|
635
|
+
backupCodesCount?: number; // default: 10
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
## Database Schema
|
|
641
|
+
|
|
642
|
+
The library creates its own tables that link to your existing user table:
|
|
643
|
+
|
|
644
|
+
```sql
|
|
645
|
+
-- your existing user table
|
|
646
|
+
CREATE TABLE users (
|
|
647
|
+
id SERIAL PRIMARY KEY,
|
|
648
|
+
name VARCHAR(100),
|
|
649
|
+
-- whatever else
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
-- library creates these tables
|
|
653
|
+
CREATE TABLE user_accounts (
|
|
654
|
+
id SERIAL PRIMARY KEY,
|
|
655
|
+
user_id VARCHAR(255) NOT NULL, -- links to your users.id or auto-generated UUID
|
|
656
|
+
email VARCHAR(255) NOT NULL UNIQUE,
|
|
657
|
+
password VARCHAR(255) NOT NULL,
|
|
658
|
+
verified BOOLEAN DEFAULT FALSE,
|
|
659
|
+
status INTEGER DEFAULT 0,
|
|
660
|
+
rolemask INTEGER DEFAULT 0,
|
|
661
|
+
-- ...
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
-- also: user_confirmations, user_remembers, user_resets, user_providers
|
|
665
|
+
-- MFA tables: user_2fa_methods, user_2fa_tokens
|
|
666
|
+
-- Activity: user_activity_log
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
## API Reference
|
|
670
|
+
|
|
671
|
+
### Auth Manager (`req.auth`)
|
|
672
|
+
|
|
673
|
+
#### Authentication
|
|
674
|
+
- `isLoggedIn(): boolean`
|
|
675
|
+
- `login(email, password, remember?): Promise<void>`
|
|
676
|
+
- `completeTwoFactorLogin(): Promise<void>`
|
|
677
|
+
- `logout(): Promise<void>`
|
|
678
|
+
- `register(email, password, callback?): Promise<AuthAccount>`
|
|
679
|
+
|
|
680
|
+
#### User Info
|
|
681
|
+
- `getId(): number | null`
|
|
682
|
+
- `getEmail(): string | null`
|
|
683
|
+
- `getStatus(): number | null`
|
|
684
|
+
- `getVerified(): boolean | null`
|
|
685
|
+
- `getRoleNames(rolemask?): string[]`
|
|
686
|
+
- `getStatusName(): string | null`
|
|
687
|
+
|
|
688
|
+
#### Permissions
|
|
689
|
+
- `hasRole(role): Promise<boolean>`
|
|
690
|
+
- `isAdmin(): Promise<boolean>`
|
|
691
|
+
- `isRemembered(): boolean`
|
|
692
|
+
|
|
693
|
+
#### Email Management
|
|
694
|
+
- `changeEmail(newEmail, callback): Promise<void>`
|
|
695
|
+
- `confirmEmail(token): Promise<string>`
|
|
696
|
+
- `confirmEmailAndLogin(token, remember?): Promise<void>`
|
|
697
|
+
|
|
698
|
+
#### Password Management
|
|
699
|
+
- `resetPassword(email, expiresAfter?, maxRequests?, callback?): Promise<void>`
|
|
700
|
+
- `confirmResetPassword(token, password, logout?): Promise<void>`
|
|
701
|
+
- `verifyPassword(password): Promise<boolean>`
|
|
702
|
+
|
|
703
|
+
#### Session Management
|
|
704
|
+
- `logoutEverywhere(): Promise<void>`
|
|
705
|
+
- `logoutEverywhereElse(): Promise<void>`
|
|
706
|
+
|
|
707
|
+
#### Multi-Factor Authentication (`req.auth.twoFactor`)
|
|
708
|
+
- `isEnabled(): Promise<boolean>`
|
|
709
|
+
- `totpEnabled(): Promise<boolean>`
|
|
710
|
+
- `emailEnabled(): Promise<boolean>`
|
|
711
|
+
- `smsEnabled(): Promise<boolean>`
|
|
712
|
+
- `getEnabledMethods(): Promise<TwoFactorMechanism[]>`
|
|
713
|
+
|
|
714
|
+
**Setup Methods:**
|
|
715
|
+
- `setup.totp(requireVerification?): Promise<TwoFactorSetupResult>`
|
|
716
|
+
- `setup.email(email?, requireVerification?): Promise<void>`
|
|
717
|
+
- `setup.sms(phone, requireVerification?): Promise<void>`
|
|
718
|
+
|
|
719
|
+
**Completion Methods (for verification during enrollment):**
|
|
720
|
+
- `complete.totp(code): Promise<string[]>`
|
|
721
|
+
- `complete.email(code): Promise<void>`
|
|
722
|
+
- `complete.sms(code): Promise<void>`
|
|
723
|
+
|
|
724
|
+
**Verification Methods (during login):**
|
|
725
|
+
- `verify.totp(code): Promise<void>`
|
|
726
|
+
- `verify.email(code): Promise<void>`
|
|
727
|
+
- `verify.sms(code): Promise<void>`
|
|
728
|
+
- `verify.backupCode(code): Promise<void>`
|
|
729
|
+
- `verify.otp(code): Promise<void>`
|
|
730
|
+
|
|
731
|
+
**Management Methods:**
|
|
732
|
+
- `disable(mechanism): Promise<void>`
|
|
733
|
+
- `generateNewBackupCodes(): Promise<string[]>`
|
|
734
|
+
- `getContact(mechanism): Promise<string | null>`
|
|
735
|
+
|
|
736
|
+
### Admin Manager (`req.authAdmin`)
|
|
737
|
+
|
|
738
|
+
#### User Management
|
|
739
|
+
- `createUser(credentials, callback?): Promise<AuthAccount>`
|
|
740
|
+
- `loginAsUserBy(identifier): Promise<void>`
|
|
741
|
+
- `deleteUserBy(identifier): Promise<void>`
|
|
742
|
+
|
|
743
|
+
#### Role Management
|
|
744
|
+
- `addRoleForUserBy(identifier, role): Promise<void>`
|
|
745
|
+
- `removeRoleForUserBy(identifier, role): Promise<void>`
|
|
746
|
+
- `hasRoleForUserBy(identifier, role): Promise<boolean>`
|
|
747
|
+
|
|
748
|
+
#### Account Management
|
|
749
|
+
- `changePasswordForUserBy(identifier, password): Promise<void>`
|
|
750
|
+
- `setStatusForUserBy(identifier, status): Promise<void>`
|
|
751
|
+
- `initiatePasswordResetForUserBy(identifier, expiresAfter?, callback?): Promise<void>`
|
|
752
|
+
|
|
753
|
+
### Schema Utilities
|
|
754
|
+
|
|
755
|
+
```typescript
|
|
756
|
+
import { createAuthTables, dropAuthTables, cleanupExpiredTokens, getAuthTableStats } from '@prsm/easy-auth';
|
|
757
|
+
|
|
758
|
+
// Setup tables
|
|
759
|
+
await createAuthTables(config);
|
|
760
|
+
|
|
761
|
+
// Cleanup (useful for cron jobs)
|
|
762
|
+
await cleanupExpiredTokens(config);
|
|
763
|
+
|
|
764
|
+
// Get statistics
|
|
765
|
+
const stats = await getAuthTableStats(config);
|
|
766
|
+
console.log(`${stats.accounts} accounts, ${stats.expiredRemembers} expired tokens`);
|
|
767
|
+
|
|
768
|
+
// Remove all auth tables
|
|
769
|
+
await dropAuthTables(config);
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
## Constants
|
|
773
|
+
|
|
774
|
+
```typescript
|
|
775
|
+
import { AuthStatus, AuthRole } from '@prsm/easy-auth';
|
|
776
|
+
|
|
777
|
+
// User statuses
|
|
778
|
+
AuthStatus.Normal // 0
|
|
779
|
+
AuthStatus.Archived // 1
|
|
780
|
+
AuthStatus.Banned // 2
|
|
781
|
+
AuthStatus.Locked // 3
|
|
782
|
+
AuthStatus.PendingReview // 4
|
|
783
|
+
AuthStatus.Suspended // 5
|
|
784
|
+
|
|
785
|
+
// User roles (bitmask)
|
|
786
|
+
AuthRole.Admin // 1
|
|
787
|
+
AuthRole.Author // 2
|
|
788
|
+
AuthRole.Collaborator // 4
|
|
789
|
+
// ... many more
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
## Error Handling
|
|
793
|
+
|
|
794
|
+
```typescript
|
|
795
|
+
import {
|
|
796
|
+
EmailTakenError,
|
|
797
|
+
InvalidPasswordError,
|
|
798
|
+
UserNotFoundError,
|
|
799
|
+
SecondFactorRequiredError,
|
|
800
|
+
InvalidTwoFactorCodeError
|
|
801
|
+
} from '@prsm/easy-auth';
|
|
802
|
+
|
|
803
|
+
app.post('/register', async (req, res) => {
|
|
804
|
+
try {
|
|
805
|
+
await req.auth.register(email, password);
|
|
806
|
+
} catch (error) {
|
|
807
|
+
if (error instanceof EmailTakenError) {
|
|
808
|
+
return res.status(409).json({ error: 'Email already exists' });
|
|
809
|
+
}
|
|
810
|
+
if (error instanceof InvalidPasswordError) {
|
|
811
|
+
return res.status(400).json({ error: 'Password too weak' });
|
|
812
|
+
}
|
|
813
|
+
throw error;
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
app.post('/login', async (req, res) => {
|
|
818
|
+
try {
|
|
819
|
+
await req.auth.login(req.body.email, req.body.password);
|
|
820
|
+
res.json({ success: true });
|
|
821
|
+
} catch (error) {
|
|
822
|
+
if (error instanceof SecondFactorRequiredError) {
|
|
823
|
+
return res.status(202).json({
|
|
824
|
+
requiresTwoFactor: true,
|
|
825
|
+
availableMethods: error.challenge
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
if (error instanceof InvalidTwoFactorCodeError) {
|
|
829
|
+
return res.status(400).json({ error: 'Invalid verification code' });
|
|
830
|
+
}
|
|
831
|
+
throw error;
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
## Examples
|
|
837
|
+
|
|
838
|
+
### Database Setup
|
|
839
|
+
|
|
840
|
+
```typescript
|
|
841
|
+
import { Pool } from 'pg';
|
|
842
|
+
|
|
843
|
+
const pool = new Pool({
|
|
844
|
+
connectionString: 'postgresql://user:password@localhost:5432/dbname'
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
const config = {
|
|
848
|
+
db: pool,
|
|
849
|
+
tablePrefix: 'auth_',
|
|
850
|
+
};
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
### Role-Based Access Control
|
|
854
|
+
|
|
855
|
+
```typescript
|
|
856
|
+
app.get('/admin', async (req, res) => {
|
|
857
|
+
if (!req.auth.isLoggedIn()) {
|
|
858
|
+
return res.status(401).json({ error: 'Not logged in' });
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (!await req.auth.hasRole(AuthRole.Admin)) {
|
|
862
|
+
return res.status(403).json({ error: 'Admin access required' });
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Admin-only content
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
// Add role to user
|
|
869
|
+
await req.authAdmin.addRoleForUserBy(
|
|
870
|
+
{ email: 'user@example.com' },
|
|
871
|
+
AuthRole.Admin | AuthRole.Editor
|
|
872
|
+
);
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
## License
|
|
876
|
+
|
|
877
|
+
MIT
|