@javagt/express-easy-auth 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/readme.md ADDED
@@ -0,0 +1,165 @@
1
+ # Express Auth Service (Library)
2
+
3
+ **Express Auth Service** is a modular, professional-grade authentication and session management library for Node.js/Express. It provides a complete identity solution with a focus on modern security (Passkeys) and developer experience.
4
+
5
+ - **🚀 Passkeys (WebAuthn)**: Passwordless biometric authentication (TouchID, FaceID).
6
+ - **🛡️ TOTP 2FA**: Google Authenticator, Authy, and hardware token support.
7
+ - **🔑 API Key Management**: Per-user API keys with granular permissions for programmatic access.
8
+ - **💾 Session Store**: Secure, ACID-compliant SQLite-backed session management.
9
+ - **🔒 Fresh Auth**: Middleware to require recent re-authentication for sensitive actions.
10
+ - **📜 System Logs**: Built-in diagnostics and activity logging.
11
+
12
+ ---
13
+
14
+ ## 🚀 5-Minute Quickstart
15
+
16
+ ### 1. Install
17
+ ```bash
18
+ npm install auth-server
19
+ ```
20
+
21
+ ### 2. Initialize
22
+ ```javascript
23
+ import express from 'express';
24
+ import { setupAuth, authRouter } from 'auth-server';
25
+
26
+ const app = express();
27
+ app.use(express.json());
28
+
29
+ setupAuth(app, {
30
+ dataDir: './data',
31
+ config: {
32
+ domain: 'localhost',
33
+ rpName: 'My Auth App',
34
+ rpID: 'localhost',
35
+ origin: 'http://localhost:3000'
36
+ }
37
+ });
38
+
39
+ // Standard mounting pattern
40
+ app.use('/api/v1/auth', authRouter);
41
+ app.listen(3000);
42
+ ```
43
+
44
+ ### 3. Protect
45
+ ```javascript
46
+ import { requireAuth } from 'auth-server';
47
+
48
+ app.get('/dashboard', requireAuth, (req, res) => {
49
+ res.json({ message: `Hello User ${req.userId}` });
50
+ });
51
+ ```
52
+
53
+ ---
54
+
55
+ ## 🌐 Frontend SDK
56
+
57
+ The library hosts its own lightweight SDK at `/auth-sdk.js` (can be customized). No installation required.
58
+
59
+ ```html
60
+ <script type="module">
61
+ import { AuthClient } from '/auth-sdk.js';
62
+ const auth = new AuthClient();
63
+
64
+ // High-level ceremonies
65
+ await auth.loginWithPasskey();
66
+ await auth.registerPasskey('My Mac');
67
+
68
+ // Standard methods
69
+ const status = await auth.getStatus();
70
+ </script>
71
+ ```
72
+
73
+ ---
74
+
75
+ ## 📖 Features & Documentation
76
+
77
+ ### 🛡️ Multi-Factor Auth (TOTP)
78
+ Users can enable TOTP 2FA for an extra layer of security.
79
+ - **Setup**: `POST /api/v1/auth/2fa/setup`
80
+ - **Verify**: `POST /api/v1/auth/2fa/verify-setup`
81
+ - [View Example: TOTP Setup](examples/04-totp-setup.js)
82
+
83
+ ### 🏎️ Passkeys (WebAuthn)
84
+ Full WebAuthn support including discovery and residency.
85
+ - **Integration**: Use `AuthClient.loginWithPasskey()` and `AuthClient.registerPasskey()`.
86
+ - [View Example: Passkey Ceremony](examples/02-passkeys.js)
87
+
88
+ ### 🔑 Programmatic Access (API Keys)
89
+ Allow users to create and manage their own API keys for your service.
90
+ - **Middleware**: Use `requireApiKey` to protect machine-to-machine routes.
91
+ - **Permissions**: Supports `action:read` and `action:write`.
92
+ - [View Example: API Key Integration](examples/03-api-keys.js)
93
+
94
+ ### 🔗 Linking to your Application Database
95
+ **Auth-server** is designed to be a standalone identity provider. It does not store application-specific data (like bios or preferences). Instead, you should link your application database to the `userId` provided by the library.
96
+
97
+ **Pattern:**
98
+ 1. Use `requireAuth` to get `req.userId`.
99
+ 2. Query your own database using that ID.
100
+
101
+ [View full example of linking databases](examples/08-external-db-linking.js)
102
+
103
+ ### 📜 Logging & Debugging
104
+ The library provides a flexible logging system and explicit control over error exposure.
105
+ - **`exposeErrors`**: Boolean flag to toggle detailed error messages in the API responses. Recommended to set to `process.env.NODE_ENV !== 'production'`.
106
+ - **Custom Logger**: Plug in your own logger (e.g., Winston, Pino) by passing it to `setupAuth`.
107
+ - [View Example: Custom Logger](examples/05-custom-logger.js)
108
+
109
+ ---
110
+
111
+ ## 📜 API Reference
112
+
113
+ ### Backend API (V1)
114
+ All identity endpoints are nested under the router (recommended path: `/api/v1/auth`).
115
+
116
+ | Category | Method | Path | Auth | Description |
117
+ | :--- | :--- | :--- | :--- | :--- |
118
+ | **Auth** | POST | `/login` | — | Login with password |
119
+ | | POST | `/register` | — | Create account |
120
+ | | POST | `/logout` | ✓ | Destroy session |
121
+ | | GET | `/status` | — | Check session status |
122
+ | **2FA** | POST | `/2fa/setup` | ✓ | Generate TOTP secret |
123
+ | | POST | `/2fa/verify-setup` | ✓ | Enable 2FA after verification |
124
+ | | POST | `/2fa/disable` | ✓ (Fresh) | Disable TOTP |
125
+ | **Passkeys** | POST | `/passkeys/register/options` | ✓ | Get registration options |
126
+ | | POST | `/passkeys/register/verify` | ✓ | Verify and save passkey |
127
+ | | POST | `/passkeys/authenticate/options` | — | Get login options |
128
+ | | POST | `/passkeys/authenticate/verify` | — | Verify passkey login |
129
+ | | GET | `/passkeys` | ✓ | List registered passkeys |
130
+ | | DELETE | `/passkeys/:id` | ✓ | Delete a passkey |
131
+ | **Password Reset** | POST | `/password-reset/request` | — | Generate reset token |
132
+ | | POST | `/password-reset/reset` | — | Complete reset |
133
+ | **API Keys** | GET | `/api-keys` | ✓ | List user API keys |
134
+ | | POST | `/api-keys` | ✓ | Create new API key |
135
+ | | DELETE | `/api-keys/:id` | ✓ | Revoke API key |
136
+
137
+ ### Frontend SDK (AuthClient)
138
+ The SDK is available at `/auth-sdk.js`.
139
+
140
+ #### Methods
141
+ - `client.login(username, password)`: Manual login.
142
+ - `client.register(username, email, password)`: Manual registration.
143
+ - `client.logout()`: Logout.
144
+ - `client.getStatus()`: Get authentication and security status.
145
+ - `client.loginWithPasskey(username?)`: Biometric login.
146
+ - `client.registerPasskey(name)`: Register a biometric key.
147
+ - `client.createApiKey(name, permissions)`: Generate a new API key.
148
+ - `client.listApiKeys()`: List keys.
149
+ - `client.reportError(err, context)`: Report client-side errors to server.
150
+
151
+ ## Examples
152
+ The `examples/` directory contains standalone, documented reference implementations.
153
+
154
+ 1. `01-basic-setup.js`: Minimal Express integration.
155
+ 2. `02-passkeys.js`: WebAuthn registration and login.
156
+ 3. `03-api-keys.js`: Service-to-service authentication.
157
+ 4. `04-totp-setup.js`: TOTP 2FA lifecycle.
158
+ 5. `05-custom-logger.js`: Injecting a custom logger.
159
+ 6. `06-password-reset.js`: Full password recovery flow.
160
+ 7. `08-external-db-linking.js`: Linking to your own application database.
161
+
162
+ ---
163
+
164
+ ## License
165
+ MIT
@@ -0,0 +1,29 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ const db = new DatabaseSync(':memory:');
3
+ db.exec('CREATE TABLE test (a TEXT, b TEXT)');
4
+ const p1 = 'val1';
5
+ const p2 = undefined;
6
+
7
+ try {
8
+ // This represents my "fix" that failed
9
+ db.prepare('SELECT * FROM test WHERE a=? AND b=?').get([p1 || null, p2 || null]);
10
+ console.log('Array format worked');
11
+ } catch (e) {
12
+ console.error('Array format failed:', e.message);
13
+ }
14
+
15
+ try {
16
+ // This represents the original problematic call
17
+ db.prepare('SELECT * FROM test WHERE a=? AND b=?').get(p1, p2);
18
+ console.log('Positional with undefined worked');
19
+ } catch (e) {
20
+ console.error('Positional with undefined failed:', e.message);
21
+ }
22
+
23
+ try {
24
+ // This represents the REAL fix
25
+ db.prepare('SELECT * FROM test WHERE a=? AND b=?').get(p1 || null, p2 || null);
26
+ console.log('Positional with null fallback worked');
27
+ } catch (e) {
28
+ console.error('Positional with null fallback failed:', e.message);
29
+ }
@@ -0,0 +1,7 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ const db = new DatabaseSync(':memory:');
3
+ db.exec('CREATE TABLE test (a TEXT, b TEXT)');
4
+ db.prepare('INSERT INTO test (a, b) VALUES (?, ?)').run('val1', 'val2');
5
+ console.log('Single run with multiple args finished');
6
+ const row = db.prepare('SELECT * FROM test WHERE a=? AND b=?').get('val1', 'val2');
7
+ console.log('Result:', row);
@@ -0,0 +1,17 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ const db = new DatabaseSync(':memory:');
3
+ db.exec('CREATE TABLE test (a TEXT, b TEXT)');
4
+ // This SHOULD fail if it only takes one argument because ? ? needs two values
5
+ try {
6
+ db.prepare('INSERT INTO test (a, b) VALUES (?, ?)').run('a', 'b');
7
+ console.log('Run with 2 args succeeded (Wait, how?)');
8
+ } catch (e) {
9
+ console.error('Run with 2 args failed:', e.message);
10
+ }
11
+
12
+ try {
13
+ const row = db.prepare('SELECT * FROM test WHERE a=? AND b=?').get('a', 'b');
14
+ console.log('Get with 2 args returned:', row);
15
+ } catch (e) {
16
+ console.error('Get with 2 args failed:', e.message);
17
+ }
@@ -0,0 +1,9 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ const db = new DatabaseSync(':memory:');
3
+ db.exec('CREATE TABLE test (a TEXT, b TEXT)');
4
+ try {
5
+ db.prepare('SELECT * FROM test WHERE a=? AND b=?').get('val1', undefined);
6
+ console.log('Finished with undefined');
7
+ } catch (e) {
8
+ console.error('Error with undefined:', e);
9
+ }
@@ -0,0 +1,14 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ const db = new DatabaseSync(':memory:');
3
+ db.exec('CREATE TABLE users (id TEXT, username TEXT, email TEXT)');
4
+ const username = 'testuser';
5
+ const email = undefined;
6
+
7
+ // This mimics the problematic call
8
+ try {
9
+ const user = db.prepare('SELECT * FROM users WHERE username=? OR email=?').get([username || null, email || null]);
10
+ console.log('Final verification: Array format with null fallbacks succeeded!');
11
+ console.log('Result:', user);
12
+ } catch (e) {
13
+ console.error('Final verification failed:', e.message);
14
+ }
package/src/client.js ADDED
@@ -0,0 +1,295 @@
1
+ /**
2
+ * AuthClient - Frontend SDK for Auth Server
3
+ *
4
+ * This library handles authentication ceremonies, including WebAuthn (Passkeys),
5
+ * TOTP 2FA, and session management.
6
+ */
7
+
8
+ export class AuthClient {
9
+ /**
10
+ * @param {Object} options
11
+ * @param {string} [options.baseUrl] - The base URL for the API (default: '/api')
12
+ * @param {string} [options.apiVersion] - The API version (default: 'v1')
13
+ */
14
+ constructor(options = {}) {
15
+ this.baseUrl = options.baseUrl || '/api';
16
+ this.apiVersion = options.apiVersion || 'v1';
17
+ this.apiPrefix = `${this.baseUrl}/${this.apiVersion}/auth`;
18
+ }
19
+
20
+ // ─── PRIVATE HELPERS ────────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * Standard fetch wrapper with error handling
24
+ * @param {string} path - The API path (relative to /auth or /v1)
25
+ * @param {Object} [options] - Fetch options
26
+ */
27
+ /**
28
+ * Standard fetch wrapper with error handling
29
+ * @param {string} path - The API path (relative to /auth or /v1)
30
+ * @param {Object} [options] - Fetch options
31
+ */
32
+ async request(path, options = {}) {
33
+ // Standardize path: remove redundant prefixes if they exist
34
+ let cleanPath = path
35
+ .replace(/^\/?api\/v1\/auth/, '')
36
+ .replace(/^\/?api\/v1/, '')
37
+ .replace(/^\/?auth/, '')
38
+ .replace(/^\//, '');
39
+
40
+ const url = `${this.apiPrefix}/${cleanPath}`;
41
+
42
+ const res = await fetch(url, {
43
+ headers: {
44
+ 'Content-Type': 'application/json',
45
+ ...(options.headers || {}),
46
+ },
47
+ credentials: 'same-origin',
48
+ ...options,
49
+ body: options.body ? JSON.stringify(options.body) : undefined,
50
+ });
51
+
52
+ const data = await res.json().catch(() => ({}));
53
+ if (!res.ok) {
54
+ throw Object.assign(new Error(data.error || 'Request failed'), {
55
+ code: data.code,
56
+ status: res.status,
57
+ data,
58
+ });
59
+ }
60
+ return data;
61
+ }
62
+
63
+ _base64urlToBuffer(base64url) {
64
+ const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
65
+ const pad = base64.length % 4;
66
+ const padded = pad ? base64 + '='.repeat(4 - pad) : base64;
67
+ const binary = window.atob(padded);
68
+ const bytes = new Uint8Array(binary.length);
69
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
70
+ return bytes.buffer;
71
+ }
72
+
73
+ _bufferToBase64url(buffer) {
74
+ const bytes = new Uint8Array(buffer);
75
+ let binary = '';
76
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
77
+ const base64 = window.btoa(binary);
78
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
79
+ }
80
+
81
+ // ─── AUTH METHODS ──────────────────────────────────────────────────────────
82
+
83
+ async getStatus() {
84
+ return this.request('/status');
85
+ }
86
+
87
+ async login(username, password, totp) {
88
+ return this.request('/login', {
89
+ method: 'POST',
90
+ body: { username, password, totp: totp || undefined },
91
+ });
92
+ }
93
+
94
+ async register(username, email, password) {
95
+ return this.request('/register', {
96
+ method: 'POST',
97
+ body: { username, email, password },
98
+ });
99
+ }
100
+
101
+ async logout() {
102
+ return this.request('/logout', { method: 'POST' });
103
+ }
104
+
105
+ async changePassword(newPassword, token) {
106
+ return this.request('/change-password', {
107
+ method: 'POST',
108
+ body: { newPassword, token }
109
+ });
110
+ }
111
+
112
+ // ─── PASSKEY METHODS ────────────────────────────────────────────────────────
113
+
114
+ async registerPasskey(name = 'My Device') {
115
+ if (!window.PublicKeyCredential) throw new Error('WebAuthn not supported');
116
+ const opts = await this.request('/passkeys/register/options', { method: 'POST' });
117
+ const creationOptions = {
118
+ ...opts,
119
+ challenge: this._base64urlToBuffer(opts.challenge),
120
+ user: { ...opts.user, id: this._base64urlToBuffer(opts.user.id) },
121
+ excludeCredentials: (opts.excludeCredentials || []).map(c => ({
122
+ ...c, id: this._base64urlToBuffer(c.id)
123
+ })),
124
+ };
125
+ const cred = await navigator.credentials.create({ publicKey: creationOptions });
126
+ const response = {
127
+ id: cred.id,
128
+ rawId: this._bufferToBase64url(cred.rawId),
129
+ type: cred.type,
130
+ response: {
131
+ clientDataJSON: this._bufferToBase64url(cred.response.clientDataJSON),
132
+ attestationObject: this._bufferToBase64url(cred.response.attestationObject),
133
+ transports: cred.response.getTransports ? cred.response.getTransports() : [],
134
+ },
135
+ };
136
+ return this.request('/passkeys/register/verify', {
137
+ method: 'POST',
138
+ body: { response, name },
139
+ });
140
+ }
141
+
142
+ async loginWithPasskey(username) {
143
+ if (!window.PublicKeyCredential) throw new Error('WebAuthn not supported');
144
+ const opts = await this.request('/passkeys/authenticate/options', {
145
+ method: 'POST',
146
+ body: { username },
147
+ });
148
+ const requestOptions = {
149
+ ...opts,
150
+ challenge: this._base64urlToBuffer(opts.challenge),
151
+ allowCredentials: (opts.allowCredentials || []).map(c => ({
152
+ ...c, id: this._base64urlToBuffer(c.id)
153
+ })),
154
+ };
155
+ const cred = await navigator.credentials.get({ publicKey: requestOptions });
156
+ const response = {
157
+ id: cred.id,
158
+ rawId: this._bufferToBase64url(cred.rawId),
159
+ type: cred.type,
160
+ response: {
161
+ clientDataJSON: this._bufferToBase64url(cred.response.clientDataJSON),
162
+ authenticatorData: this._bufferToBase64url(cred.response.authenticatorData),
163
+ signature: this._bufferToBase64url(cred.response.signature),
164
+ userHandle: cred.response.userHandle ? this._bufferToBase64url(cred.response.userHandle) : null,
165
+ },
166
+ };
167
+ return this.request('/passkeys/authenticate/verify', {
168
+ method: 'POST',
169
+ body: { response },
170
+ });
171
+ }
172
+
173
+ async listPasskeys() {
174
+ return this.request('/passkeys');
175
+ }
176
+
177
+ async deletePasskey(id) {
178
+ return this.request(`/passkeys/${id}`, { method: 'DELETE' });
179
+ }
180
+
181
+ // ─── SESSION METHODS ────────────────────────────────────────────────────────
182
+
183
+ async listSessions() {
184
+ return this.request('/sessions');
185
+ }
186
+
187
+ async revokeSession(id) {
188
+ return this.request(`/sessions/${id}`, { method: 'DELETE' });
189
+ }
190
+
191
+ // ─── API KEY METHODS ────────────────────────────────────────────────────────
192
+
193
+ async listApiKeys() {
194
+ return this.request('/api-keys');
195
+ }
196
+
197
+ async createApiKey(name, permissions) {
198
+ return this.request('/api-keys', {
199
+ method: 'POST',
200
+ body: { name, permissions }
201
+ });
202
+ }
203
+
204
+ async deleteApiKey(id) {
205
+ return this.request(`/api-keys/${id}`, { method: 'DELETE' });
206
+ }
207
+
208
+ // ─── 2FA METHODS ────────────────────────────────────────────────────────────
209
+
210
+ /**
211
+ * Start 2FA (TOTP) setup
212
+ */
213
+ async setup2FA() {
214
+ return this.request('/2fa/setup', { method: 'POST' });
215
+ }
216
+
217
+ /**
218
+ * Verify and enable 2FA
219
+ */
220
+ async verify2FASetup(token) {
221
+ return this.request('/2fa/verify-setup', {
222
+ method: 'POST',
223
+ body: { token },
224
+ });
225
+ }
226
+
227
+ /**
228
+ * Disable 2FA
229
+ * @param {string} password - Required for security
230
+ * @param {string} [token] - Optional current code
231
+ */
232
+ async disable2FA(password, token) {
233
+ return this.request('/2fa/disable', {
234
+ method: 'POST',
235
+ body: { password, token },
236
+ });
237
+ }
238
+
239
+ // ─── UTIL METHODS ───────────────────────────────────────────────────────────
240
+
241
+ /**
242
+ * Report an error to the server's system logs
243
+ */
244
+ async reportError(error, context = {}) {
245
+ const isString = typeof error === 'string';
246
+ const message = isString ? error : (error.message || String(error));
247
+ const stack = isString ? null : (error.stack || null);
248
+
249
+ return this.request('/report-error', {
250
+ method: 'POST',
251
+ body: { level: 'error', message, stack, context },
252
+ }).catch(e => console.warn('[Auth SDK] Failed to report error:', e));
253
+ }
254
+
255
+ // ─── PASSWORD RESET METHODS ──────────────────────────────────────────────────
256
+
257
+ /**
258
+ * Request a password reset token
259
+ * @param {string} identity - Username or email
260
+ */
261
+ async forgotPassword(identity) {
262
+ return this.request('/password-reset/request', {
263
+ method: 'POST',
264
+ body: { username: identity, email: identity }
265
+ });
266
+ }
267
+
268
+ /**
269
+ * Reset password using a token
270
+ * @param {string} token - The reset token
271
+ * @param {string} newPassword - The new password
272
+ */
273
+ async resetPassword(token, newPassword) {
274
+ return this.request('/password-reset/reset', {
275
+ method: 'POST',
276
+ body: { token, newPassword }
277
+ });
278
+ }
279
+
280
+ /**
281
+ * Sync passkeys with device (conditional UI / Signal API)
282
+ */
283
+ async syncPasskeys(credentialIds) {
284
+ if (!window.PublicKeyCredential || !PublicKeyCredential.signalAllAcceptedCredentials) {
285
+ return { supported: false };
286
+ }
287
+ try {
288
+ const ids = credentialIds.map(id => this._base64urlToBuffer(id));
289
+ await PublicKeyCredential.signalAllAcceptedCredentials({ credentialIds: ids });
290
+ return { supported: true, success: true };
291
+ } catch (err) {
292
+ return { supported: true, success: false, error: err };
293
+ }
294
+ }
295
+ }