@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.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Example 05: Custom Logger
3
+ *
4
+ * Shows how to plug in a custom logger implementation (e.g. for Winston or Pino)
5
+ */
6
+
7
+ import express from 'express';
8
+ import { setupAuth, authRouter, DefaultLogger } from '../src/index.js';
9
+
10
+ const app = express();
11
+ app.use(express.json());
12
+
13
+ // 1. Define a custom logger
14
+ // It must implement: error, warn, info, debug
15
+ class MyCustomLogger {
16
+ error(msg, meta) {
17
+ console.log(`[MY-APP-ERROR] ${msg}`, meta.err?.stack || '');
18
+ // You could send to Sentry, Datadog, etc. here
19
+ }
20
+ warn(msg, meta) { console.log(`[MY-APP-WARN] ${msg}`); }
21
+ info(msg, meta) { console.log(`[MY-APP-INFO] ${msg}`); }
22
+ debug(msg, meta) { console.log(`[MY-APP-DEBUG] ${msg}`); }
23
+ }
24
+
25
+ // 2. Or extend the DefaultLogger to keep DB logging but change console output
26
+ class EnhancedLogger extends DefaultLogger {
27
+ info(msg, meta) {
28
+ super.info(msg, meta); // Keep default behavior (DB + Console)
29
+ console.log('--- Custom log decorator ---');
30
+ }
31
+ }
32
+
33
+ // 3. Initialize with custom logger
34
+ setupAuth(app, {
35
+ dataDir: './data-example',
36
+ exposeErrors: true, // Always show details for this demo
37
+ logger: new MyCustomLogger(), // Plug in your implementation
38
+ config: { domain: 'localhost' }
39
+ });
40
+
41
+ // Standard mount point
42
+ app.use('/api/v1/auth', authRouter);
43
+
44
+ // Test route to trigger an error
45
+ app.get('/test-error', (req, res, next) => {
46
+ next(new Error('This is a simulated system error'));
47
+ });
48
+
49
+ // Import the error logger middleware last!
50
+ import { authErrorLogger } from '../src/index.js';
51
+ app.use(authErrorLogger);
52
+
53
+ app.get('/', (req, res) => {
54
+ res.send(`
55
+ <!DOCTYPE html>
56
+ <html>
57
+ <head><title>Auth SDK Demo - Custom Logger</title></head>
58
+ <body style="font-family: sans-serif; padding: 2rem; background: #fafafa;">
59
+ <h1>Auth SDK Demo: Custom Logger</h1>
60
+ <p>This page demonstrates how the SDK interacts with a custom server-side logger.</p>
61
+
62
+ <div id="controls">
63
+ <button id="triggerErrorBtn">1. Trigger Server Error (500)</button>
64
+ <button id="reportBtn">2. Manually Report SDK Error</button>
65
+ </div>
66
+ <hr>
67
+ <div id="output" style="background: #333; color: #fff; padding: 1rem; border-radius: 4px; font-family: monospace; min-height: 100px; white-space: pre-wrap;">Console Output...</div>
68
+ <p><small>Check the <b>Node.js terminal</b> to see the [MY-APP-ERROR] logs!</small></p>
69
+
70
+ <script type="module">
71
+ import { AuthClient } from '/auth-sdk.js';
72
+ const auth = new AuthClient();
73
+
74
+ const log = (msg) => {
75
+ document.getElementById('output').innerText += '\\n> ' + msg;
76
+ };
77
+
78
+ document.getElementById('triggerErrorBtn').onclick = async () => {
79
+ try {
80
+ log('Fetching /test-error...');
81
+ const res = await fetch('/test-error');
82
+ const data = await res.json();
83
+ log('Server matched error: ' + JSON.stringify(data));
84
+ } catch (e) { log('Fetch error: ' + e.message); }
85
+ };
86
+
87
+ document.getElementById('reportBtn').onclick = async () => {
88
+ try {
89
+ log('Reporting client-side error to server...');
90
+ await auth.reportError(new Error('Something went wrong on the client!'), {
91
+ browser: navigator.userAgent
92
+ });
93
+ log('Error reported successfully!');
94
+ } catch (e) { log('Report Error: ' + e.message); }
95
+ };
96
+ </script>
97
+ </body>
98
+ </html>
99
+ `);
100
+ });
101
+
102
+ const PORT = 3000;
103
+ app.listen(PORT, () => {
104
+ console.log(`Example 05 running at http://localhost:${PORT}`);
105
+ });
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Example 06: Password Reset
3
+ *
4
+ * Demonstrates the full flow of requesting a reset token and resetting a password.
5
+ */
6
+
7
+ import express from 'express';
8
+ import session from 'express-session';
9
+ import {
10
+ setupAuth,
11
+ authRouter,
12
+ SQLiteSessionStore,
13
+ authDb
14
+ } from '../src/index.js';
15
+
16
+ const app = express();
17
+ app.use(express.json());
18
+
19
+ setupAuth(app, {
20
+ dataDir: './data-example-reset',
21
+ exposeErrors: true,
22
+ config: { domain: 'localhost' }
23
+ });
24
+
25
+ app.use(session({
26
+ secret: 'reset-secret',
27
+ store: new SQLiteSessionStore(),
28
+ resave: false,
29
+ saveUninitialized: false,
30
+ cookie: { secure: false }
31
+ }));
32
+
33
+ // Standard mount point
34
+ app.use('/api/v1/auth', authRouter);
35
+
36
+ app.get('/', (req, res) => {
37
+ res.send(`
38
+ <!DOCTYPE html>
39
+ <html>
40
+ <head><title>Auth SDK Demo - Password Reset</title></head>
41
+ <body style="font-family: sans-serif; padding: 2rem; background: #f0fdf4;">
42
+ <h1>Auth SDK Demo: Password Reset</h1>
43
+ <p>This demo shows how to request a reset token and use it to change a password.</p>
44
+
45
+ <div id="controls">
46
+ <button id="regBtn">1. Register User (user_reset)</button>
47
+ <button id="forgotBtn">2. Forgot Password</button>
48
+ </div>
49
+
50
+ <div id="resetArea" style="margin-top: 2rem; display: none; background: #fff; padding: 1.5rem; border: 1px solid #dcfce7; border-radius: 8px;">
51
+ <h3>Reset Password</h3>
52
+ <p>A token was generated in the database (simulating an email).</p>
53
+ <input type="text" id="tokenIn" placeholder="Enter Token">
54
+ <input type="password" id="passIn" placeholder="New Password">
55
+ <button id="resetBtn">3. Reset Password</button>
56
+ </div>
57
+
58
+ <hr>
59
+ <div id="output" style="background: #333; color: #fff; padding: 1rem; border-radius: 4px; font-family: monospace; min-height: 100px; white-space: pre-wrap;">Console Output...</div>
60
+
61
+ <script type="module">
62
+ import { AuthClient } from '/auth-sdk.js';
63
+ const auth = new AuthClient();
64
+
65
+ const log = (msg) => {
66
+ document.getElementById('output').innerText += '\\n> ' + msg;
67
+ };
68
+
69
+ document.getElementById('regBtn').onclick = async () => {
70
+ try {
71
+ await auth.register('user_reset', 'reset@example.com', 'oldpassword');
72
+ log('User registered with password: oldpassword');
73
+ } catch (e) { log('Error: ' + e.message); }
74
+ };
75
+
76
+ document.getElementById('forgotBtn').onclick = async () => {
77
+ try {
78
+ log('Requesting reset for user_reset...');
79
+ const res = await auth.forgotPassword('user_reset');
80
+ log('Request success! Check the server logs (or DB) for the token.');
81
+ document.getElementById('resetArea').style.display = 'block';
82
+ } catch (e) { log('Error: ' + e.message); }
83
+ };
84
+
85
+ document.getElementById('resetBtn').onclick = async () => {
86
+ const token = document.getElementById('tokenIn').value;
87
+ const pass = document.getElementById('passIn').value;
88
+ try {
89
+ await auth.resetPassword(token, pass);
90
+ log('Password successfully reset! You can now login with the new password.');
91
+ document.getElementById('resetArea').style.display = 'none';
92
+ } catch (e) { log('Error: ' + e.message); }
93
+ };
94
+ </script>
95
+ </body>
96
+ </html>
97
+ `);
98
+ });
99
+
100
+ const PORT = 3000;
101
+ app.listen(PORT, () => {
102
+ console.log(`Example 06 running at http://localhost:${PORT}`);
103
+ console.log(`NOTE: In a real app, you would email the token to the user.`);
104
+ });
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Example 08: External Database Linking
3
+ *
4
+ * Demonstrates the "contained identity" model:
5
+ * Auth server handles identity, while your app database handles profile data.
6
+ */
7
+
8
+ import express from 'express';
9
+ import session from 'express-session';
10
+ import { DatabaseSync } from 'node:sqlite';
11
+ import {
12
+ setupAuth,
13
+ authRouter,
14
+ SQLiteSessionStore,
15
+ requireAuth,
16
+ AuthClient
17
+ } from '../src/index.js';
18
+
19
+ const app = express();
20
+ app.use(express.json());
21
+
22
+ // ─── 1. SETUP AUTH SERVER ────────────────────────────────────────────────────
23
+ setupAuth(app, {
24
+ dataDir: './data-example-external',
25
+ config: { domain: 'localhost' }
26
+ });
27
+
28
+ app.use(session({
29
+ secret: 'external-db-secret',
30
+ store: new SQLiteSessionStore(),
31
+ resave: false,
32
+ saveUninitialized: false,
33
+ cookie: { secure: false }
34
+ }));
35
+
36
+ // Standard mount point
37
+ app.use('/api/v1/auth', authRouter);
38
+
39
+ // ─── 2. SETUP APPLICATION DATABASE (Managed by Developer) ────────────────────
40
+ const appDataDb = new DatabaseSync('./data-example-external/app_data.db');
41
+ appDataDb.exec(`
42
+ CREATE TABLE IF NOT EXISTS user_profiles (
43
+ user_id TEXT PRIMARY KEY,
44
+ bio TEXT,
45
+ website TEXT,
46
+ theme TEXT DEFAULT 'light'
47
+ )
48
+ `);
49
+
50
+ const getProfile = appDataDb.prepare('SELECT * FROM user_profiles WHERE user_id = ?');
51
+ const upsertProfile = appDataDb.prepare(`
52
+ INSERT INTO user_profiles (user_id, bio, website)
53
+ VALUES (?, ?, ?)
54
+ ON CONFLICT(user_id) DO UPDATE SET bio=excluded.bio, website=excluded.website
55
+ `);
56
+
57
+ // ─── 3. APP ROUTES (Combining Identity + Data) ───────────────────────────────
58
+
59
+ // Get merged profile (Auth Identity + App Data)
60
+ app.get('/api/app/profile', requireAuth, async (req, res) => {
61
+ // req.userId comes from the Auth Server middleware
62
+ const profile = getProfile.get(req.userId) || { bio: '', website: '' };
63
+
64
+ res.json({
65
+ userId: req.userId,
66
+ username: req.session.user.username, // From session
67
+ ...profile
68
+ });
69
+ });
70
+
71
+ // Update app-specific data
72
+ app.post('/api/app/profile', requireAuth, (req, res) => {
73
+ const { bio, website } = req.body;
74
+ upsertProfile.run(req.userId, bio, website);
75
+ res.json({ success: true });
76
+ });
77
+
78
+ app.get('/', (req, res) => {
79
+ res.send(`
80
+ <!DOCTYPE html>
81
+ <html>
82
+ <head><title>Auth SDK Demo - External DB</title></head>
83
+ <body style="font-family: sans-serif; padding: 2rem; background: #f8fafc;">
84
+ <h1>Auth SDK Demo: External DB Linking</h1>
85
+ <p>This demo shows how to keep Identity (Auth Server) separate from Application Data.</p>
86
+
87
+ <div id="status">Checking Auth...</div>
88
+ <hr>
89
+
90
+ <div id="profileSection" style="display:none; background:white; padding:1.5 val rem; border:1px solid #e2e8f0; border-radius:8px; max-width:400px;">
91
+ <h3>Your Profile (App Data)</h3>
92
+ <p>User ID: <span id="uidDisplay"></span></p>
93
+ <div style="margin-bottom:1rem">
94
+ <label>Bio:</label><br>
95
+ <textarea id="bioInput" style="width:100%"></textarea>
96
+ </div>
97
+ <div style="margin-bottom:1rem">
98
+ <label>Website:</label><br>
99
+ <input type="text" id="webInput" style="width:100%">
100
+ </div>
101
+ <button id="saveBtn">Save App Data</button>
102
+ </div>
103
+
104
+ <div id="authSection" style="display:none;">
105
+ <button id="loginBtn">Login / Register</button>
106
+ </div>
107
+
108
+ <script type="module">
109
+ import { AuthClient } from '/auth-sdk.js';
110
+ const auth = new AuthClient();
111
+
112
+ async function init() {
113
+ const status = await auth.getStatus();
114
+ if (status.authenticated) {
115
+ document.getElementById('profileSection').style.display = 'block';
116
+ document.getElementById('uidDisplay').innerText = status.userId;
117
+
118
+ // Fetch app data
119
+ const res = await fetch('/api/app/profile');
120
+ const profile = await res.json();
121
+ document.getElementById('bioInput').value = profile.bio;
122
+ document.getElementById('webInput').value = profile.website;
123
+ document.getElementById('status').innerText = 'Logged in as ' + status.username;
124
+ } else {
125
+ document.getElementById('authSection').style.display = 'block';
126
+ document.getElementById('status').innerText = 'Not logged in';
127
+ }
128
+ }
129
+
130
+ document.getElementById('loginBtn').onclick = async () => {
131
+ const u = 'db_user_' + Math.floor(Math.random()*1000);
132
+ await auth.register(u, u+'@example.com', 'password123').catch(()=>{});
133
+ await auth.login(u, 'password123');
134
+ location.reload();
135
+ };
136
+
137
+ document.getElementById('saveBtn').onclick = async () => {
138
+ const bio = document.getElementById('bioInput').value;
139
+ const website = document.getElementById('webInput').value;
140
+ await fetch('/api/app/profile', {
141
+ method: 'POST',
142
+ headers: {'Content-Type': 'application/json'},
143
+ body: JSON.stringify({ bio, website })
144
+ });
145
+ alert('Saved to App Database!');
146
+ };
147
+
148
+ init();
149
+ </script>
150
+ </body>
151
+ </html>
152
+ `);
153
+ });
154
+
155
+ const PORT = 3000;
156
+ app.listen(PORT, () => {
157
+ console.log(`Example 08 running at http://localhost:${PORT}`);
158
+ });
@@ -0,0 +1,32 @@
1
+ # Auth Server Library Examples
2
+
3
+ This directory contains standalone, well-documented reference implementations for each major feature of the library. These examples are designed to be read and run in isolation.
4
+
5
+ ## 🚀 How to Run
6
+ Most examples require an Express environment. To run an example:
7
+
8
+ 1. `npm install` (if you haven't already)
9
+ 2. `node examples/01-basic-setup.js`
10
+
11
+ ## 📖 Available Examples
12
+
13
+ ### 01. Basic Setup
14
+ **File**: `01-basic-setup.js`
15
+ The "Hello World" of the library. Shows how to initialize `setupAuth`, configure standard Express sessions, and protect a basic route.
16
+
17
+ ### 02. Passkeys (WebAuthn)
18
+ **File**: `02-passkeys.js`
19
+ Focuses on the passwordless experience. Shows how to mount the `passkeysRouter` and integrate the `AuthClient` ceremonies for registration and login.
20
+
21
+ ### 03. API Keys
22
+ **File**: `03-api-keys.js`
23
+ Demonstrates programmatic access. Create machine-to-machine keys with granular permissions (`action:read`, `action:write`) and verify them using the `requireApiKey` middleware.
24
+
25
+ ### 04. TOTP 2FA lifecycle
26
+ **File**: `04-totp-setup.js`
27
+ Covers the setup and verification of Google Authenticator-style 2FA. Includes an example of `requireFreshAuth` for sensitive actions.
28
+
29
+ ---
30
+
31
+ > [!TIP]
32
+ > **Prototyping**: You can copy these files directly into your project as a starting point. All examples use local SQLite databases (`./data-example`) by default.
package/openapi.yaml ADDED
@@ -0,0 +1,263 @@
1
+ openapi: 3.0.3
2
+ info:
3
+ title: Auth Server API
4
+ version: 1.0.0
5
+ description: |
6
+ API for authentication, user management, passkeys, and API key access.
7
+ All endpoints require either session or API key authentication.
8
+ servers:
9
+ - url: /api/v1
10
+ components:
11
+ securitySchemes:
12
+ bearerAuth:
13
+ type: http
14
+ scheme: bearer
15
+ bearerFormat: API_KEY
16
+ cookieAuth:
17
+ type: apiKey
18
+ in: cookie
19
+ name: connect.sid
20
+ paths:
21
+ /user/me:
22
+ get:
23
+ summary: Get user profile
24
+ security:
25
+ - cookieAuth: []
26
+ responses:
27
+ '200':
28
+ description: User profile
29
+ content:
30
+ application/json:
31
+ schema:
32
+ type: object
33
+ properties:
34
+ id:
35
+ type: string
36
+ username:
37
+ type: string
38
+ email:
39
+ type: string
40
+ profile:
41
+ type: object
42
+ '401':
43
+ description: Not authenticated
44
+ content:
45
+ application/json:
46
+ schema:
47
+ type: object
48
+ properties:
49
+ error:
50
+ type: string
51
+ /user/keys:
52
+ get:
53
+ summary: List API keys
54
+ security:
55
+ - cookieAuth: []
56
+ responses:
57
+ '200':
58
+ description: List of API keys
59
+ content:
60
+ application/json:
61
+ schema:
62
+ type: object
63
+ properties:
64
+ keys:
65
+ type: array
66
+ items:
67
+ type: object
68
+ properties:
69
+ id:
70
+ type: string
71
+ name:
72
+ type: string
73
+ permissions:
74
+ type: array
75
+ items:
76
+ type: string
77
+ created_at:
78
+ type: integer
79
+ last_used:
80
+ type: integer
81
+ '401':
82
+ description: Not authenticated
83
+ content:
84
+ application/json:
85
+ schema:
86
+ type: object
87
+ properties:
88
+ error:
89
+ type: string
90
+ post:
91
+ summary: Create API key
92
+ security:
93
+ - cookieAuth: []
94
+ requestBody:
95
+ required: true
96
+ content:
97
+ application/json:
98
+ schema:
99
+ type: object
100
+ properties:
101
+ name:
102
+ type: string
103
+ permissions:
104
+ type: array
105
+ items:
106
+ type: string
107
+ responses:
108
+ '201':
109
+ description: Created
110
+ content:
111
+ application/json:
112
+ schema:
113
+ type: object
114
+ properties:
115
+ success:
116
+ type: boolean
117
+ key:
118
+ type: string
119
+ metadata:
120
+ type: object
121
+ '400':
122
+ description: Bad Request
123
+ content:
124
+ application/json:
125
+ schema:
126
+ type: object
127
+ properties:
128
+ error:
129
+ type: string
130
+ delete:
131
+ summary: Delete API key
132
+ security:
133
+ - cookieAuth: []
134
+ parameters:
135
+ - in: path
136
+ name: id
137
+ required: true
138
+ schema:
139
+ type: string
140
+ responses:
141
+ '200':
142
+ description: Deleted
143
+ content:
144
+ application/json:
145
+ schema:
146
+ type: object
147
+ properties:
148
+ success:
149
+ type: boolean
150
+ '404':
151
+ description: Not found
152
+ content:
153
+ application/json:
154
+ schema:
155
+ type: object
156
+ properties:
157
+ error:
158
+ type: string
159
+ /auth/register:
160
+ post:
161
+ summary: Register a new user
162
+ requestBody:
163
+ required: true
164
+ content:
165
+ application/json:
166
+ schema:
167
+ type: object
168
+ properties:
169
+ username:
170
+ type: string
171
+ email:
172
+ type: string
173
+ password:
174
+ type: string
175
+ responses:
176
+ '201':
177
+ description: Registered
178
+ content:
179
+ application/json:
180
+ schema:
181
+ type: object
182
+ properties:
183
+ userId:
184
+ type: string
185
+ username:
186
+ type: string
187
+ message:
188
+ type: string
189
+ user:
190
+ type: object
191
+ '400':
192
+ description: Missing or invalid input
193
+ content:
194
+ application/json:
195
+ schema:
196
+ type: object
197
+ properties:
198
+ error:
199
+ type: string
200
+ '409':
201
+ description: Username or email taken
202
+ content:
203
+ application/json:
204
+ schema:
205
+ type: object
206
+ properties:
207
+ error:
208
+ type: string
209
+ /auth/login:
210
+ post:
211
+ summary: Login with username and password
212
+ requestBody:
213
+ required: true
214
+ content:
215
+ application/json:
216
+ schema:
217
+ type: object
218
+ properties:
219
+ username:
220
+ type: string
221
+ password:
222
+ type: string
223
+ responses:
224
+ '200':
225
+ description: Login successful
226
+ content:
227
+ application/json:
228
+ schema:
229
+ type: object
230
+ properties:
231
+ userId:
232
+ type: string
233
+ username:
234
+ type: string
235
+ user:
236
+ type: object
237
+ '400':
238
+ description: Missing credentials
239
+ content:
240
+ application/json:
241
+ schema:
242
+ type: object
243
+ properties:
244
+ error:
245
+ type: string
246
+ '401':
247
+ description: Invalid credentials
248
+ content:
249
+ application/json:
250
+ schema:
251
+ type: object
252
+ properties:
253
+ error:
254
+ type: string
255
+ '403':
256
+ description: Account locked
257
+ content:
258
+ application/json:
259
+ schema:
260
+ type: object
261
+ properties:
262
+ error:
263
+ type: string
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@javagt/express-easy-auth",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./client": "./src/client.js"
10
+ },
11
+ "scripts": {
12
+ "test": "echo \"Error: no test specified\" && exit 1",
13
+ "start": "node demo/server.js",
14
+ "dev": "node demo/server.js"
15
+ },
16
+ "keywords": [],
17
+ "author": "",
18
+ "license": "ISC",
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "dependencies": {
23
+ "@simplewebauthn/server": "^13.3.0",
24
+ "bcrypt": "^6.0.0",
25
+ "cookie-parser": "^1.4.7",
26
+ "cors": "^2.8.6",
27
+ "dotenv": "^17.4.1",
28
+ "express": "^5.2.1",
29
+ "express-session": "^1.19.0",
30
+ "helmet": "^8.1.0",
31
+ "otplib": "^13.4.0",
32
+ "qrcode": "^1.5.4",
33
+ "uuid": "^13.0.0"
34
+ }
35
+ }