@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/.env.example +13 -0
- package/demo/profileRouter.js +64 -0
- package/demo/public/css/style.css +1293 -0
- package/demo/public/index.html +272 -0
- package/demo/public/js/app.js +540 -0
- package/demo/server.js +195 -0
- package/examples/01-basic-setup.js +118 -0
- package/examples/02-passkeys.js +106 -0
- package/examples/03-api-keys.js +108 -0
- package/examples/04-totp-setup.js +125 -0
- package/examples/05-custom-logger.js +105 -0
- package/examples/06-password-reset.js +104 -0
- package/examples/08-external-db-linking.js +158 -0
- package/examples/README.md +32 -0
- package/openapi.yaml +263 -0
- package/package.json +35 -0
- package/readme.md +165 -0
- package/scratch/debug_bindings.js +29 -0
- package/scratch/test_sqlite.js +7 -0
- package/scratch/test_sqlite_multargs.js +17 -0
- package/scratch/test_sqlite_undefined.js +9 -0
- package/scratch/verify_sqlite_fix.js +14 -0
- package/src/client.js +295 -0
- package/src/db/init.js +203 -0
- package/src/db/sessionStore.js +67 -0
- package/src/index.js +61 -0
- package/src/middleware/auth.js +111 -0
- package/src/routes/auth.js +569 -0
- package/src/utils/authHelpers.js +48 -0
- package/src/utils/logger.js +71 -0
- package/test/auth.test.js +32 -0
- package/test/passkeys.test.js +19 -0
- package/test/user.test.js +29 -0
package/demo/server.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import session from 'express-session';
|
|
4
|
+
import cookieParser from 'cookie-parser';
|
|
5
|
+
import helmet from 'helmet';
|
|
6
|
+
import cors from 'cors';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { dirname } from 'path';
|
|
10
|
+
|
|
11
|
+
// 1. Import the library's main integration tools
|
|
12
|
+
import {
|
|
13
|
+
setupAuth,
|
|
14
|
+
authRouter,
|
|
15
|
+
SQLiteSessionStore,
|
|
16
|
+
authErrorLogger,
|
|
17
|
+
requireApiKey
|
|
18
|
+
} from '../src/index.js';
|
|
19
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
20
|
+
import profileRouter from './profileRouter.js';
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = dirname(__filename);
|
|
24
|
+
|
|
25
|
+
// ─── CONFIG ──────────────────────────────────────────────────────────────────
|
|
26
|
+
const DOMAIN = process.env.DOMAIN || 'localhost';
|
|
27
|
+
const PORT = parseInt(process.env.PORT || '3000', 10);
|
|
28
|
+
const SESSION_SECRET = process.env.SESSION_SECRET || 'change-this-in-production-please';
|
|
29
|
+
const IS_PROD = process.env.NODE_ENV === 'production';
|
|
30
|
+
|
|
31
|
+
// Robust hostname extraction
|
|
32
|
+
let HOSTNAME = DOMAIN.replace(/^https?:\/\//, '').split('/')[0].split(':')[0];
|
|
33
|
+
const IS_LOCALHOST = HOSTNAME.includes('localhost') || HOSTNAME.includes('127.0.0.1');
|
|
34
|
+
const PROTOCOL = (IS_PROD || !IS_LOCALHOST) ? 'https' : 'http';
|
|
35
|
+
const ORIGIN = `${PROTOCOL}://${HOSTNAME}${(!IS_PROD && IS_LOCALHOST && PORT !== 80 && PORT !== 443) ? `:${PORT}` : ''}`;
|
|
36
|
+
|
|
37
|
+
const config = {
|
|
38
|
+
domain: HOSTNAME,
|
|
39
|
+
port: PORT,
|
|
40
|
+
protocol: PROTOCOL,
|
|
41
|
+
origin: ORIGIN,
|
|
42
|
+
rpName: 'Auth Server Demo',
|
|
43
|
+
rpID: HOSTNAME,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const app = express();
|
|
47
|
+
|
|
48
|
+
// 2. Initialize Authentication Services
|
|
49
|
+
const dataDir = path.join(__dirname, '../data');
|
|
50
|
+
setupAuth(app, {
|
|
51
|
+
dataDir,
|
|
52
|
+
exposeErrors: !IS_PROD,
|
|
53
|
+
config
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 2b. Initialize Application Database (External to Auth Server)
|
|
57
|
+
const appDataDb = new DatabaseSync(path.join(dataDir, 'app_data.db'));
|
|
58
|
+
appDataDb.exec(`
|
|
59
|
+
CREATE TABLE IF NOT EXISTS profiles (
|
|
60
|
+
user_id TEXT PRIMARY KEY,
|
|
61
|
+
display_name TEXT,
|
|
62
|
+
bio TEXT,
|
|
63
|
+
avatar_url TEXT,
|
|
64
|
+
location TEXT,
|
|
65
|
+
website TEXT,
|
|
66
|
+
preferences TEXT, -- JSON string for app settings
|
|
67
|
+
created_at INTEGER NOT NULL,
|
|
68
|
+
updated_at INTEGER NOT NULL
|
|
69
|
+
);
|
|
70
|
+
`);
|
|
71
|
+
|
|
72
|
+
// Attach appDataDb to app for use in routers
|
|
73
|
+
app.set('appDataDb', appDataDb);
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
// ─── MIDDLEWARE ──────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
app.set('trust proxy', 1);
|
|
79
|
+
|
|
80
|
+
app.use(helmet({
|
|
81
|
+
hsts: IS_PROD,
|
|
82
|
+
contentSecurityPolicy: {
|
|
83
|
+
directives: {
|
|
84
|
+
defaultSrc: ["'self'"],
|
|
85
|
+
scriptSrc: ["'self'"],
|
|
86
|
+
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
|
|
87
|
+
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
|
|
88
|
+
imgSrc: ["'self'", 'data:'],
|
|
89
|
+
connectSrc: ["'self'"],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
app.use(cors({
|
|
95
|
+
origin: ORIGIN,
|
|
96
|
+
credentials: true,
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
app.use(express.json());
|
|
100
|
+
app.use(express.urlencoded({ extended: true }));
|
|
101
|
+
app.use(cookieParser(SESSION_SECRET));
|
|
102
|
+
|
|
103
|
+
// ─── SESSION ─────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
const sessionStore = new SQLiteSessionStore();
|
|
106
|
+
|
|
107
|
+
app.use(session({
|
|
108
|
+
secret: SESSION_SECRET,
|
|
109
|
+
store: sessionStore,
|
|
110
|
+
resave: false,
|
|
111
|
+
saveUninitialized: false,
|
|
112
|
+
name: 'auth.sid',
|
|
113
|
+
cookie: {
|
|
114
|
+
secure: PROTOCOL === 'https',
|
|
115
|
+
httpOnly: true,
|
|
116
|
+
sameSite: 'lax',
|
|
117
|
+
maxAge: 7 * 24 * 60 * 60 * 1000 // 1 week
|
|
118
|
+
},
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
// ─── ROUTES ──────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
// 3. Mount Library & Application Routes
|
|
124
|
+
app.use('/api/v1/auth', authRouter);
|
|
125
|
+
app.use('/api/v1/profile', profileRouter);
|
|
126
|
+
|
|
127
|
+
// Sample Public API protected by API Keys
|
|
128
|
+
app.get('/api/public/data', requireApiKey, (req, res) => {
|
|
129
|
+
if (!req.permissions.includes('action:read')) {
|
|
130
|
+
return res.status(403).json({ error: 'Missing permission: action:read' });
|
|
131
|
+
}
|
|
132
|
+
res.json({
|
|
133
|
+
message: 'Success! You accessed this data with an API key.',
|
|
134
|
+
user: req.userId,
|
|
135
|
+
permissions: req.permissions,
|
|
136
|
+
timestamp: new Date().toISOString()
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
app.post('/api/public/data', requireApiKey, (req, res) => {
|
|
141
|
+
if (!req.permissions.includes('action:write')) {
|
|
142
|
+
return res.status(403).json({ error: 'Missing permission: action:write' });
|
|
143
|
+
}
|
|
144
|
+
res.json({
|
|
145
|
+
message: 'Success! You published data with an API key.',
|
|
146
|
+
publishedAt: new Date().toISOString()
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Health check
|
|
151
|
+
app.get('/api/health', (req, res) => {
|
|
152
|
+
res.json({ status: 'ok', domain: DOMAIN, timestamp: new Date().toISOString() });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ─── DEMO MAILBOX (TEST ENDPOINTS) ──────────────────────────────────────────
|
|
156
|
+
const mailboxMessages = [];
|
|
157
|
+
|
|
158
|
+
app.get('/api/v1/test/mailbox', (req, res) => {
|
|
159
|
+
res.json({ messages: mailboxMessages });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
app.post('/api/v1/test/mailbox', (req, res) => {
|
|
163
|
+
const { type, subject, body } = req.body;
|
|
164
|
+
const msg = {
|
|
165
|
+
id: Math.random().toString(36).substring(2, 9),
|
|
166
|
+
type: type || 'System',
|
|
167
|
+
subject: subject || 'No Subject',
|
|
168
|
+
body: body || '',
|
|
169
|
+
timestamp: Date.now()
|
|
170
|
+
};
|
|
171
|
+
mailboxMessages.unshift(msg);
|
|
172
|
+
if (mailboxMessages.length > 50) mailboxMessages.pop();
|
|
173
|
+
res.status(201).json(msg);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
app.delete('/api/v1/test/mailbox', (req, res) => {
|
|
177
|
+
mailboxMessages.length = 0;
|
|
178
|
+
res.status(204).send();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Serve frontend from demo directory
|
|
182
|
+
app.use(express.static(path.join(__dirname, './public')));
|
|
183
|
+
|
|
184
|
+
// ─── ERROR HANDLER ───────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
app.use(authErrorLogger);
|
|
187
|
+
|
|
188
|
+
// ─── START ───────────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
app.listen(PORT, () => {
|
|
191
|
+
console.log(`\n🔐 Auth Demo Server running`);
|
|
192
|
+
console.log(` URL: ${ORIGIN}`);
|
|
193
|
+
console.log(` RPID: ${config.rpID}`);
|
|
194
|
+
console.log(` Env: ${IS_PROD ? 'production' : 'development'}\n`);
|
|
195
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example 01: Basic setup
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates how to initialize the auth library and protect a basic route.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import session from 'express-session';
|
|
9
|
+
// In your app, use: import { setupAuth, authRouter, SQLiteSessionStore, requireAuth } from 'auth-server';
|
|
10
|
+
import {
|
|
11
|
+
setupAuth,
|
|
12
|
+
authRouter,
|
|
13
|
+
SQLiteSessionStore,
|
|
14
|
+
requireAuth,
|
|
15
|
+
AuthClient
|
|
16
|
+
} from '../src/index.js';
|
|
17
|
+
|
|
18
|
+
const app = express();
|
|
19
|
+
|
|
20
|
+
// 1. JSON Middleware is required for API routes
|
|
21
|
+
app.use(express.json());
|
|
22
|
+
|
|
23
|
+
// 2. Initialize the Library
|
|
24
|
+
setupAuth(app, {
|
|
25
|
+
dataDir: './data-example', // Directory for SQLite DBs
|
|
26
|
+
exposeErrors: process.env.NODE_ENV !== 'production', // Explicit debug setting
|
|
27
|
+
config: {
|
|
28
|
+
domain: 'localhost',
|
|
29
|
+
rpName: 'Basic Example App',
|
|
30
|
+
rpID: 'localhost',
|
|
31
|
+
origin: 'http://localhost:3000'
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// 3. Configure Sessions
|
|
36
|
+
// The library provides SQLiteSessionStore which is optimized for auth-server
|
|
37
|
+
app.use(session({
|
|
38
|
+
secret: 'keyboard-cat-secret',
|
|
39
|
+
store: new SQLiteSessionStore(),
|
|
40
|
+
resave: false,
|
|
41
|
+
saveUninitialized: false,
|
|
42
|
+
cookie: { secure: false } // 'true' in production with HTTPS
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// 1. Mount Auth (Unified Router)
|
|
46
|
+
app.use('/api/v1/auth', authRouter);
|
|
47
|
+
|
|
48
|
+
// 5. Protect your routes
|
|
49
|
+
app.get('/dashboard', requireAuth, (req, res) => {
|
|
50
|
+
res.json({
|
|
51
|
+
message: 'Access granted!',
|
|
52
|
+
userId: req.userId,
|
|
53
|
+
sessionID: req.sessionID
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 6. Client-side SDK Demo
|
|
58
|
+
app.get('/', (req, res) => {
|
|
59
|
+
res.send(`
|
|
60
|
+
<!DOCTYPE html>
|
|
61
|
+
<html>
|
|
62
|
+
<head><title>Auth SDK Demo - Basic</title></head>
|
|
63
|
+
<body style="font-family: sans-serif; padding: 2rem; background: #f4f4f9;">
|
|
64
|
+
<h1>Auth SDK Demo: Basic Setup</h1>
|
|
65
|
+
<p>This page uses the <b>AuthClient</b> SDK to interact with the backend.</p>
|
|
66
|
+
<div id="status">Checking status...</div>
|
|
67
|
+
<hr>
|
|
68
|
+
<button id="regBtn">Register Demo User</button>
|
|
69
|
+
<button id="loginBtn">Login Demo User</button>
|
|
70
|
+
<button id="logoutBtn">Logout</button>
|
|
71
|
+
|
|
72
|
+
<script type="module">
|
|
73
|
+
import { AuthClient } from '/auth-sdk.js';
|
|
74
|
+
const auth = new AuthClient();
|
|
75
|
+
|
|
76
|
+
async function updateStatus() {
|
|
77
|
+
const status = await auth.getStatus();
|
|
78
|
+
document.getElementById('status').innerText = 'Authenticated: ' + status.authenticated + (status.username ? ' (' + status.username + ')' : '');
|
|
79
|
+
console.log('Current Status:', status);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
document.getElementById('regBtn').onclick = async () => {
|
|
83
|
+
const username = 'user_' + Math.floor(Math.random()*1000);
|
|
84
|
+
try {
|
|
85
|
+
const res = await auth.register(username, username + '@example.com', 'password123');
|
|
86
|
+
alert('Registered: ' + username);
|
|
87
|
+
await updateStatus();
|
|
88
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
document.getElementById('loginBtn').onclick = async () => {
|
|
92
|
+
const username = prompt('Username?');
|
|
93
|
+
try {
|
|
94
|
+
await auth.login(username, 'password123');
|
|
95
|
+
alert('Logged in!');
|
|
96
|
+
await updateStatus();
|
|
97
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
document.getElementById('logoutBtn').onclick = async () => {
|
|
101
|
+
await auth.logout();
|
|
102
|
+
alert('Logged out');
|
|
103
|
+
await updateStatus();
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
updateStatus();
|
|
107
|
+
</script>
|
|
108
|
+
</body>
|
|
109
|
+
</html>
|
|
110
|
+
`);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const PORT = 3000;
|
|
114
|
+
app.listen(PORT, () => {
|
|
115
|
+
console.log(`Example 01 running at http://localhost:${PORT}`);
|
|
116
|
+
console.log(`- Try: GET http://localhost:${PORT}/api/v1/auth/status`);
|
|
117
|
+
console.log(`- Try: GET http://localhost:${PORT}/dashboard (will return 401 until login)`);
|
|
118
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example 02: Passkeys (WebAuthn)
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates how to enable and manage biometric passkeys.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import session from 'express-session';
|
|
9
|
+
import {
|
|
10
|
+
setupAuth,
|
|
11
|
+
authRouter,
|
|
12
|
+
SQLiteSessionStore,
|
|
13
|
+
requireAuth
|
|
14
|
+
} from '../src/index.js';
|
|
15
|
+
|
|
16
|
+
const app = express();
|
|
17
|
+
app.use(express.json());
|
|
18
|
+
|
|
19
|
+
setupAuth(app, {
|
|
20
|
+
dataDir: './data-example',
|
|
21
|
+
config: {
|
|
22
|
+
domain: 'localhost',
|
|
23
|
+
rpName: 'Passkey Example',
|
|
24
|
+
rpID: 'localhost',
|
|
25
|
+
origin: 'http://localhost:3000'
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
app.use(session({
|
|
30
|
+
secret: 'passkey-secret',
|
|
31
|
+
store: new SQLiteSessionStore(),
|
|
32
|
+
resave: false,
|
|
33
|
+
saveUninitialized: false,
|
|
34
|
+
cookie: { secure: false }
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
// 1. Mount Auth (Unified Router)
|
|
38
|
+
app.use('/api/v1/auth', authRouter);
|
|
39
|
+
|
|
40
|
+
// 2. The SDK will now automatically use /api/v1/auth/...
|
|
41
|
+
|
|
42
|
+
app.get('/', (req, res) => {
|
|
43
|
+
res.send(`
|
|
44
|
+
<!DOCTYPE html>
|
|
45
|
+
<html>
|
|
46
|
+
<head><title>Auth SDK Demo - Passkeys</title></head>
|
|
47
|
+
<body style="font-family: sans-serif; padding: 2rem; background: #fdf6e3;">
|
|
48
|
+
<h1>Auth SDK Demo: Passkeys</h1>
|
|
49
|
+
<p>This page demonstrates <b>WebAuthn</b> registration and login via the SDK.</p>
|
|
50
|
+
<div id="status">Checking status...</div>
|
|
51
|
+
<hr>
|
|
52
|
+
<div id="controls">
|
|
53
|
+
<button id="regBtn">1. Register Password Account</button>
|
|
54
|
+
<button id="pkRegBtn">2. Register Passkey</button>
|
|
55
|
+
<button id="pkLoginBtn">3. Login with Passkey</button>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<script type="module">
|
|
59
|
+
import { AuthClient } from '/auth-sdk.js';
|
|
60
|
+
const auth = new AuthClient();
|
|
61
|
+
|
|
62
|
+
async function updateStatus() {
|
|
63
|
+
const status = await auth.getStatus();
|
|
64
|
+
document.getElementById('status').innerHTML = \`
|
|
65
|
+
<strong>Authenticated:</strong> \${status.authenticated}<br>
|
|
66
|
+
<strong>User ID:</strong> \${status.userId || 'None'}<br>
|
|
67
|
+
<strong>Passkeys:</strong> \${status.passkeyCount || 0}
|
|
68
|
+
\`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
document.getElementById('regBtn').onclick = async () => {
|
|
72
|
+
const u = 'pk_user_' + Math.floor(Math.random()*1000);
|
|
73
|
+
try {
|
|
74
|
+
await auth.register(u, u + '@example.com', 'password123');
|
|
75
|
+
alert('Created password account: ' + u + '. Now register a passkey!');
|
|
76
|
+
await updateStatus();
|
|
77
|
+
} catch (e) { alert(e.message); }
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
document.getElementById('pkRegBtn').onclick = async () => {
|
|
81
|
+
try {
|
|
82
|
+
await auth.registerPasskey('My Laptop');
|
|
83
|
+
alert('Passkey Registered!');
|
|
84
|
+
await updateStatus();
|
|
85
|
+
} catch (e) { alert(e.message); }
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
document.getElementById('pkLoginBtn').onclick = async () => {
|
|
89
|
+
try {
|
|
90
|
+
const res = await auth.loginWithPasskey();
|
|
91
|
+
alert('Logged in with Passkey!');
|
|
92
|
+
await updateStatus();
|
|
93
|
+
} catch (e) { alert(e.message); }
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
updateStatus();
|
|
97
|
+
</script>
|
|
98
|
+
</body>
|
|
99
|
+
</html>
|
|
100
|
+
`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const PORT = 3000;
|
|
104
|
+
app.listen(PORT, () => {
|
|
105
|
+
console.log(`Example 02 running at http://localhost:${PORT}`);
|
|
106
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example 03: API Keys
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates service-to-service authentication using API keys.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import { setupAuth, authRouter, requireApiKey } from '../src/index.js';
|
|
9
|
+
|
|
10
|
+
const app = express();
|
|
11
|
+
app.use(express.json());
|
|
12
|
+
|
|
13
|
+
setupAuth(app, {
|
|
14
|
+
dataDir: './data-example',
|
|
15
|
+
config: { domain: 'localhost' }
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// 1. Mount Auth router for identity management
|
|
19
|
+
app.use('/api/v1/auth', authRouter);
|
|
20
|
+
|
|
21
|
+
// 2. Protect routes with the requireApiKey middleware
|
|
22
|
+
// It looks for 'X-API-Key' or 'Authorization: Bearer <key>'
|
|
23
|
+
app.get('/api/v1/protected-data', requireApiKey, (req, res) => {
|
|
24
|
+
// Authentication verified
|
|
25
|
+
// req.userId is set to the owner of the key
|
|
26
|
+
// req.permissions contains the key's allowed scopes
|
|
27
|
+
|
|
28
|
+
res.json({
|
|
29
|
+
success: true,
|
|
30
|
+
message: 'Accessed via API key',
|
|
31
|
+
owner: req.userId,
|
|
32
|
+
permissions: req.permissions
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// 3. Granular permission check
|
|
37
|
+
app.post('/api/v1/write-data', requireApiKey, (req, res) => {
|
|
38
|
+
if (!req.permissions.includes('action:write')) {
|
|
39
|
+
return res.status(403).json({ error: 'Key lacks action:write permission' });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
res.json({ success: true, message: 'Write operation authorized' });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
app.get('/', (req, res) => {
|
|
46
|
+
res.send(`
|
|
47
|
+
<!DOCTYPE html>
|
|
48
|
+
<html>
|
|
49
|
+
<head><title>Auth SDK Demo - API Keys</title></head>
|
|
50
|
+
<body style="font-family: sans-serif; padding: 2rem; background: #eef2ff;">
|
|
51
|
+
<h1>Auth SDK Demo: API Keys</h1>
|
|
52
|
+
<p>This page demonstrates API key lifecycle management via the SDK.</p>
|
|
53
|
+
|
|
54
|
+
<div id="controls">
|
|
55
|
+
<button id="loginBtn">1. Login as Admin</button>
|
|
56
|
+
<button id="createKeyBtn">2. Create API Key</button>
|
|
57
|
+
<button id="testKeyBtn">3. Test Created Key</button>
|
|
58
|
+
</div>
|
|
59
|
+
<hr>
|
|
60
|
+
<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>
|
|
61
|
+
|
|
62
|
+
<script type="module">
|
|
63
|
+
import { AuthClient } from '/auth-sdk.js';
|
|
64
|
+
const auth = new AuthClient();
|
|
65
|
+
let lastKey = null;
|
|
66
|
+
|
|
67
|
+
const log = (msg) => {
|
|
68
|
+
document.getElementById('output').innerText += '\\n> ' + msg;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
document.getElementById('loginBtn').onclick = async () => {
|
|
72
|
+
try {
|
|
73
|
+
// Ensure a user exists first
|
|
74
|
+
await auth.register('api_admin', 'api@example.com', 'admin123').catch(() => {});
|
|
75
|
+
await auth.login('api_admin', 'admin123');
|
|
76
|
+
log('Logged in as api_admin');
|
|
77
|
+
} catch (e) { log('Login Error: ' + e.message); }
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
document.getElementById('createKeyBtn').onclick = async () => {
|
|
81
|
+
try {
|
|
82
|
+
const res = await auth.createApiKey('Demo Key', ['action:read']);
|
|
83
|
+
lastKey = res.key;
|
|
84
|
+
log('Key Created: ' + lastKey);
|
|
85
|
+
} catch (e) { log('Error: ' + e.message); }
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
document.getElementById('testKeyBtn').onclick = async () => {
|
|
89
|
+
if (!lastKey) return alert('Create a key first!');
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch('/api/v1/protected-data', {
|
|
92
|
+
headers: { 'X-API-Key': lastKey }
|
|
93
|
+
});
|
|
94
|
+
const data = await res.json();
|
|
95
|
+
log('Test Result: ' + JSON.stringify(data));
|
|
96
|
+
} catch (e) { log('Test Error: ' + e.message); }
|
|
97
|
+
};
|
|
98
|
+
</script>
|
|
99
|
+
</body>
|
|
100
|
+
</html>
|
|
101
|
+
`);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const PORT = 3000;
|
|
105
|
+
app.listen(PORT, () => {
|
|
106
|
+
console.log(`Example 03 running at http://localhost:${PORT}`);
|
|
107
|
+
console.log(`- Request with: curl -H "X-API-Key: YOUR_KEY" http://localhost:${PORT}/api/v1/protected-data`);
|
|
108
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example 04: TOTP 2FA
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates the full lifecycle of setting up and using TOTP 2FA.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import session from 'express-session';
|
|
9
|
+
import {
|
|
10
|
+
setupAuth,
|
|
11
|
+
authRouter,
|
|
12
|
+
SQLiteSessionStore,
|
|
13
|
+
requireAuth,
|
|
14
|
+
requireFreshAuth
|
|
15
|
+
} from '../src/index.js';
|
|
16
|
+
|
|
17
|
+
const app = express();
|
|
18
|
+
app.use(express.json());
|
|
19
|
+
|
|
20
|
+
setupAuth(app, {
|
|
21
|
+
dataDir: './data-example',
|
|
22
|
+
config: { domain: 'localhost' }
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
app.use(session({
|
|
26
|
+
secret: 'totp-secret',
|
|
27
|
+
store: new SQLiteSessionStore(),
|
|
28
|
+
resave: false,
|
|
29
|
+
saveUninitialized: false,
|
|
30
|
+
cookie: { secure: false }
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
// 1. Mount Auth (Unified Router)
|
|
34
|
+
app.use('/api/v1/auth', authRouter);
|
|
35
|
+
|
|
36
|
+
// 1. Initial login (Password only)
|
|
37
|
+
// POST /api/auth/login
|
|
38
|
+
|
|
39
|
+
// 2. Setup 2FA (Requires valid session)
|
|
40
|
+
// POST /api/auth/2fa/setup -> Returns { secret, qrCode }
|
|
41
|
+
|
|
42
|
+
// 3. Verify Setup (Enables 2FA for the account)
|
|
43
|
+
// POST /api/auth/2fa/verify-setup { token: "123456" }
|
|
44
|
+
|
|
45
|
+
// 4. Test Fresh Auth (Protecting sensitive actions)
|
|
46
|
+
// requireFreshAuth ensures the user has authed within the last few minutes
|
|
47
|
+
app.post('/api/user/delete-account', requireAuth, requireFreshAuth, (req, res) => {
|
|
48
|
+
res.json({ success: true, message: 'Sensitive action authorized' });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
app.get('/', (req, res) => {
|
|
52
|
+
res.send(`
|
|
53
|
+
<!DOCTYPE html>
|
|
54
|
+
<html>
|
|
55
|
+
<head><title>Auth SDK Demo - TOTP 2FA</title></head>
|
|
56
|
+
<body style="font-family: sans-serif; padding: 2rem; background: #fff5f5;">
|
|
57
|
+
<h1>Auth SDK Demo: TOTP 2FA</h1>
|
|
58
|
+
<p>This page demonstrates <b>TOTP</b> (Authenticator App) setup via the SDK.</p>
|
|
59
|
+
<div id="status">Checking status...</div>
|
|
60
|
+
<hr>
|
|
61
|
+
<div id="controls">
|
|
62
|
+
<button id="loginBtn">1. Login</button>
|
|
63
|
+
<button id="setupBtn">2. Setup 2FA</button>
|
|
64
|
+
</div>
|
|
65
|
+
<div id="qrArea" style="margin: 1rem 0; display: none;">
|
|
66
|
+
<p>Scan this QR code in your app (Google Authenticator, etc):</p>
|
|
67
|
+
<img id="qrImg" src="" style="border: 1px solid #ccc; padding: 10px;">
|
|
68
|
+
<p>Then enter the token below:</p>
|
|
69
|
+
<input type="text" id="tokenIn" placeholder="123456">
|
|
70
|
+
<button id="verifyBtn">Verify & Enable</button>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<script type="module">
|
|
74
|
+
import { AuthClient } from '/auth-sdk.js';
|
|
75
|
+
const auth = new AuthClient();
|
|
76
|
+
|
|
77
|
+
async function updateStatus() {
|
|
78
|
+
const status = await auth.getStatus();
|
|
79
|
+
document.getElementById('status').innerHTML = \`
|
|
80
|
+
<strong>Authenticated:</strong> \${status.authenticated}<br>
|
|
81
|
+
<strong>2FA Enabled:</strong> \${status.totpEnabled}<br>
|
|
82
|
+
<strong>Current User:</strong> \${status.username || 'None'}
|
|
83
|
+
\`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
document.getElementById('loginBtn').onclick = async () => {
|
|
87
|
+
const u = 'totp_user_' + Math.floor(Math.random()*1000);
|
|
88
|
+
try {
|
|
89
|
+
await auth.register(u, u + '@example.com', 'password123');
|
|
90
|
+
await auth.login(u, 'password123');
|
|
91
|
+
alert('Logged in! Now setup 2FA.');
|
|
92
|
+
await updateStatus();
|
|
93
|
+
} catch (e) { alert(e.message); }
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
document.getElementById('setupBtn').onclick = async () => {
|
|
97
|
+
try {
|
|
98
|
+
const res = await auth.setup2FA();
|
|
99
|
+
document.getElementById('qrImg').src = res.qrCode;
|
|
100
|
+
document.getElementById('qrArea').style.display = 'block';
|
|
101
|
+
alert('Scan the QR code!');
|
|
102
|
+
} catch (e) { alert(e.message); }
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
document.getElementById('verifyBtn').onclick = async () => {
|
|
106
|
+
const token = document.getElementById('tokenIn').value;
|
|
107
|
+
try {
|
|
108
|
+
await auth.verify2FASetup(token);
|
|
109
|
+
alert('2FA Enabled Successfully!');
|
|
110
|
+
document.getElementById('qrArea').style.display = 'none';
|
|
111
|
+
await updateStatus();
|
|
112
|
+
} catch (e) { alert(e.message); }
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
updateStatus();
|
|
116
|
+
</script>
|
|
117
|
+
</body>
|
|
118
|
+
</html>
|
|
119
|
+
`);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const PORT = 3000;
|
|
123
|
+
app.listen(PORT, () => {
|
|
124
|
+
console.log(`Example 04 running at http://localhost:${PORT}`);
|
|
125
|
+
});
|