@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/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
|
+
}
|