@technomoron/apicore-server 1.0.0-beta.1
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/LICENSE +21 -0
- package/dist/cjs/api-module.cjs +34 -0
- package/dist/cjs/api-module.d.ts +45 -0
- package/dist/cjs/apicore-server.cjs +1561 -0
- package/dist/cjs/apicore-server.d.ts +288 -0
- package/dist/cjs/auth-api/auth-module.cjs +1248 -0
- package/dist/cjs/auth-api/auth-module.d.ts +116 -0
- package/dist/cjs/auth-api/compat-auth-storage.cjs +128 -0
- package/dist/cjs/auth-api/compat-auth-storage.d.ts +57 -0
- package/dist/cjs/auth-api/mem-auth-store.cjs +121 -0
- package/dist/cjs/auth-api/mem-auth-store.d.ts +68 -0
- package/dist/cjs/auth-api/module.cjs +25 -0
- package/dist/cjs/auth-api/module.d.ts +20 -0
- package/dist/cjs/auth-api/schemas.cjs +171 -0
- package/dist/cjs/auth-api/schemas.d.ts +21 -0
- package/dist/cjs/auth-api/sql-auth-store.cjs +179 -0
- package/dist/cjs/auth-api/sql-auth-store.d.ts +87 -0
- package/dist/cjs/auth-api/storage.cjs +102 -0
- package/dist/cjs/auth-api/storage.d.ts +38 -0
- package/dist/cjs/auth-api/types.cjs +2 -0
- package/dist/cjs/auth-api/types.d.ts +34 -0
- package/dist/cjs/auth-api/user-id.cjs +47 -0
- package/dist/cjs/auth-api/user-id.d.ts +5 -0
- package/dist/cjs/auth-cookie-options.cjs +66 -0
- package/dist/cjs/auth-cookie-options.d.ts +13 -0
- package/dist/cjs/base/client-info.cjs +285 -0
- package/dist/cjs/base/client-info.d.ts +27 -0
- package/dist/cjs/base/error-utils.cjs +50 -0
- package/dist/cjs/base/error-utils.d.ts +16 -0
- package/dist/cjs/base/request-utils.cjs +27 -0
- package/dist/cjs/base/request-utils.d.ts +8 -0
- package/dist/cjs/index.cjs +51 -0
- package/dist/cjs/index.d.ts +34 -0
- package/dist/cjs/limiter/auth-rate-limiter.cjs +35 -0
- package/dist/cjs/limiter/auth-rate-limiter.d.ts +12 -0
- package/dist/cjs/limiter/fixed-window.cjs +41 -0
- package/dist/cjs/limiter/fixed-window.d.ts +11 -0
- package/dist/cjs/oauth/base.cjs +7 -0
- package/dist/cjs/oauth/base.d.ts +17 -0
- package/dist/cjs/oauth/memory.cjs +135 -0
- package/dist/cjs/oauth/memory.d.ts +22 -0
- package/dist/cjs/oauth/models.cjs +47 -0
- package/dist/cjs/oauth/models.d.ts +50 -0
- package/dist/cjs/oauth/sequelize.cjs +159 -0
- package/dist/cjs/oauth/sequelize.d.ts +30 -0
- package/dist/cjs/oauth/types.cjs +3 -0
- package/dist/cjs/oauth/types.d.ts +51 -0
- package/dist/cjs/passkey/base.cjs +7 -0
- package/dist/cjs/passkey/base.d.ts +28 -0
- package/dist/cjs/passkey/config.cjs +26 -0
- package/dist/cjs/passkey/config.d.ts +2 -0
- package/dist/cjs/passkey/memory.cjs +123 -0
- package/dist/cjs/passkey/memory.d.ts +34 -0
- package/dist/cjs/passkey/models.cjs +142 -0
- package/dist/cjs/passkey/models.d.ts +34 -0
- package/dist/cjs/passkey/sequelize.cjs +126 -0
- package/dist/cjs/passkey/sequelize.d.ts +42 -0
- package/dist/cjs/passkey/service.cjs +413 -0
- package/dist/cjs/passkey/service.d.ts +21 -0
- package/dist/cjs/passkey/types.cjs +2 -0
- package/dist/cjs/passkey/types.d.ts +84 -0
- package/dist/cjs/sequelize-utils.cjs +56 -0
- package/dist/cjs/sequelize-utils.d.ts +8 -0
- package/dist/cjs/token/base.cjs +120 -0
- package/dist/cjs/token/base.d.ts +46 -0
- package/dist/cjs/token/memory.cjs +234 -0
- package/dist/cjs/token/memory.d.ts +29 -0
- package/dist/cjs/token/sequelize.cjs +400 -0
- package/dist/cjs/token/sequelize.d.ts +58 -0
- package/dist/cjs/token/types.cjs +2 -0
- package/dist/cjs/token/types.d.ts +34 -0
- package/dist/cjs/upload/memory.cjs +92 -0
- package/dist/cjs/upload/memory.d.ts +17 -0
- package/dist/cjs/upload/tus-module.cjs +270 -0
- package/dist/cjs/upload/tus-module.d.ts +38 -0
- package/dist/cjs/upload/types.cjs +2 -0
- package/dist/cjs/upload/types.d.ts +28 -0
- package/dist/cjs/user/base.cjs +53 -0
- package/dist/cjs/user/base.d.ts +36 -0
- package/dist/cjs/user/memory.cjs +194 -0
- package/dist/cjs/user/memory.d.ts +37 -0
- package/dist/cjs/user/sequelize.cjs +194 -0
- package/dist/cjs/user/sequelize.d.ts +46 -0
- package/dist/cjs/user/types.cjs +2 -0
- package/dist/cjs/user/types.d.ts +11 -0
- package/dist/esm/api-module.d.ts +45 -0
- package/dist/esm/api-module.js +30 -0
- package/dist/esm/apicore-server.d.ts +288 -0
- package/dist/esm/apicore-server.js +1552 -0
- package/dist/esm/auth-api/auth-module.d.ts +116 -0
- package/dist/esm/auth-api/auth-module.js +1246 -0
- package/dist/esm/auth-api/compat-auth-storage.d.ts +57 -0
- package/dist/esm/auth-api/compat-auth-storage.js +124 -0
- package/dist/esm/auth-api/mem-auth-store.d.ts +68 -0
- package/dist/esm/auth-api/mem-auth-store.js +117 -0
- package/dist/esm/auth-api/module.d.ts +20 -0
- package/dist/esm/auth-api/module.js +21 -0
- package/dist/esm/auth-api/schemas.d.ts +21 -0
- package/dist/esm/auth-api/schemas.js +168 -0
- package/dist/esm/auth-api/sql-auth-store.d.ts +87 -0
- package/dist/esm/auth-api/sql-auth-store.js +175 -0
- package/dist/esm/auth-api/storage.d.ts +38 -0
- package/dist/esm/auth-api/storage.js +98 -0
- package/dist/esm/auth-api/types.d.ts +34 -0
- package/dist/esm/auth-api/types.js +1 -0
- package/dist/esm/auth-api/user-id.d.ts +5 -0
- package/dist/esm/auth-api/user-id.js +41 -0
- package/dist/esm/auth-cookie-options.d.ts +13 -0
- package/dist/esm/auth-cookie-options.js +63 -0
- package/dist/esm/base/client-info.d.ts +27 -0
- package/dist/esm/base/client-info.js +282 -0
- package/dist/esm/base/error-utils.d.ts +16 -0
- package/dist/esm/base/error-utils.js +44 -0
- package/dist/esm/base/request-utils.d.ts +8 -0
- package/dist/esm/base/request-utils.js +23 -0
- package/dist/esm/index.d.ts +34 -0
- package/dist/esm/index.js +21 -0
- package/dist/esm/limiter/auth-rate-limiter.d.ts +12 -0
- package/dist/esm/limiter/auth-rate-limiter.js +32 -0
- package/dist/esm/limiter/fixed-window.d.ts +11 -0
- package/dist/esm/limiter/fixed-window.js +37 -0
- package/dist/esm/oauth/base.d.ts +17 -0
- package/dist/esm/oauth/base.js +3 -0
- package/dist/esm/oauth/memory.d.ts +22 -0
- package/dist/esm/oauth/memory.js +128 -0
- package/dist/esm/oauth/models.d.ts +50 -0
- package/dist/esm/oauth/models.js +38 -0
- package/dist/esm/oauth/sequelize.d.ts +30 -0
- package/dist/esm/oauth/sequelize.js +148 -0
- package/dist/esm/oauth/types.d.ts +51 -0
- package/dist/esm/oauth/types.js +2 -0
- package/dist/esm/passkey/base.d.ts +28 -0
- package/dist/esm/passkey/base.js +3 -0
- package/dist/esm/passkey/config.d.ts +2 -0
- package/dist/esm/passkey/config.js +23 -0
- package/dist/esm/passkey/memory.d.ts +34 -0
- package/dist/esm/passkey/memory.js +119 -0
- package/dist/esm/passkey/models.d.ts +34 -0
- package/dist/esm/passkey/models.js +135 -0
- package/dist/esm/passkey/sequelize.d.ts +42 -0
- package/dist/esm/passkey/sequelize.js +122 -0
- package/dist/esm/passkey/service.d.ts +21 -0
- package/dist/esm/passkey/service.js +376 -0
- package/dist/esm/passkey/types.d.ts +84 -0
- package/dist/esm/passkey/types.js +1 -0
- package/dist/esm/sequelize-utils.d.ts +8 -0
- package/dist/esm/sequelize-utils.js +47 -0
- package/dist/esm/token/base.d.ts +46 -0
- package/dist/esm/token/base.js +113 -0
- package/dist/esm/token/memory.d.ts +29 -0
- package/dist/esm/token/memory.js +230 -0
- package/dist/esm/token/sequelize.d.ts +58 -0
- package/dist/esm/token/sequelize.js +396 -0
- package/dist/esm/token/types.d.ts +34 -0
- package/dist/esm/token/types.js +1 -0
- package/dist/esm/upload/memory.d.ts +17 -0
- package/dist/esm/upload/memory.js +86 -0
- package/dist/esm/upload/tus-module.d.ts +38 -0
- package/dist/esm/upload/tus-module.js +266 -0
- package/dist/esm/upload/types.d.ts +28 -0
- package/dist/esm/upload/types.js +1 -0
- package/dist/esm/user/base.d.ts +36 -0
- package/dist/esm/user/base.js +46 -0
- package/dist/esm/user/memory.d.ts +37 -0
- package/dist/esm/user/memory.js +190 -0
- package/dist/esm/user/sequelize.d.ts +46 -0
- package/dist/esm/user/sequelize.js +188 -0
- package/dist/esm/user/types.d.ts +11 -0
- package/dist/esm/user/types.js +1 -0
- package/docs/swagger/openapi.json +2162 -0
- package/package.json +131 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from '@simplewebauthn/server';
|
|
2
|
+
import { isoBase64URL, isoCBOR, decodeAttestationObject, parseAuthenticatorData } from '@simplewebauthn/server/helpers';
|
|
3
|
+
const ALLOWED_TRANSPORTS = [
|
|
4
|
+
'ble',
|
|
5
|
+
'cable',
|
|
6
|
+
'hybrid',
|
|
7
|
+
'internal',
|
|
8
|
+
'nfc',
|
|
9
|
+
'smart-card',
|
|
10
|
+
'usb'
|
|
11
|
+
];
|
|
12
|
+
function sanitizeTransports(input) {
|
|
13
|
+
if (!Array.isArray(input)) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const filtered = input
|
|
17
|
+
.map((value) => String(value))
|
|
18
|
+
.filter((value) => ALLOWED_TRANSPORTS.includes(value));
|
|
19
|
+
return filtered.length > 0 ? filtered : undefined;
|
|
20
|
+
}
|
|
21
|
+
function toOptionalString(value) {
|
|
22
|
+
if (typeof value !== 'string') {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const trimmed = value.trim();
|
|
26
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
27
|
+
}
|
|
28
|
+
function toBase64Url(buffer) {
|
|
29
|
+
return isoBase64URL.fromBuffer(new Uint8Array(buffer));
|
|
30
|
+
}
|
|
31
|
+
function fromBase64Url(value) {
|
|
32
|
+
return Buffer.from(isoBase64URL.toBuffer(value));
|
|
33
|
+
}
|
|
34
|
+
function toBuffer(value) {
|
|
35
|
+
if (Buffer.isBuffer(value)) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
const view = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
39
|
+
return Buffer.from(view);
|
|
40
|
+
}
|
|
41
|
+
function toBufferOrNull(value) {
|
|
42
|
+
if (!value) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (Buffer.isBuffer(value)) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
|
|
49
|
+
return toBuffer(value);
|
|
50
|
+
}
|
|
51
|
+
if (typeof value === 'string') {
|
|
52
|
+
try {
|
|
53
|
+
return fromBase64Url(value);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
async function spkiToCosePublicKey(spki) {
|
|
62
|
+
try {
|
|
63
|
+
const subtle = (globalThis.crypto?.subtle ??
|
|
64
|
+
(await import('crypto')).webcrypto?.subtle);
|
|
65
|
+
if (!subtle) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const spkiView = new Uint8Array(spki);
|
|
69
|
+
const ecCurves = [
|
|
70
|
+
{ namedCurve: 'P-256', crv: 1, alg: -7, rawLength: 65 },
|
|
71
|
+
{ namedCurve: 'P-384', crv: 2, alg: -35, rawLength: 97 },
|
|
72
|
+
{ namedCurve: 'P-521', crv: 3, alg: -36, rawLength: 133 }
|
|
73
|
+
];
|
|
74
|
+
for (const curve of ecCurves) {
|
|
75
|
+
try {
|
|
76
|
+
const key = await subtle.importKey('spki', spkiView, { name: 'ECDSA', namedCurve: curve.namedCurve }, true, ['verify']);
|
|
77
|
+
const raw = Buffer.from(await subtle.exportKey('raw', key));
|
|
78
|
+
if (raw.length !== curve.rawLength || raw[0] !== 0x04) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const coordLength = (raw.length - 1) / 2;
|
|
82
|
+
const x = raw.slice(1, 1 + coordLength);
|
|
83
|
+
const y = raw.slice(1 + coordLength);
|
|
84
|
+
const coseMap = new Map([
|
|
85
|
+
[1, 2], // kty: EC2
|
|
86
|
+
[3, curve.alg],
|
|
87
|
+
[-1, curve.crv],
|
|
88
|
+
[-2, x],
|
|
89
|
+
[-3, y]
|
|
90
|
+
]);
|
|
91
|
+
return Buffer.from(isoCBOR.encode(coseMap));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// try next algorithm
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const edKey = await subtle.importKey('spki', spkiView, { name: 'Ed25519' }, true, ['verify']);
|
|
99
|
+
const raw = Buffer.from(await subtle.exportKey('raw', edKey));
|
|
100
|
+
if (raw.length !== 32) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const coseMap = new Map([
|
|
104
|
+
[1, 1], // kty: OKP
|
|
105
|
+
[3, -8], // alg: EdDSA
|
|
106
|
+
[-1, 6], // crv: Ed25519
|
|
107
|
+
[-2, raw]
|
|
108
|
+
]);
|
|
109
|
+
return Buffer.from(isoCBOR.encode(coseMap));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export class PasskeyService {
|
|
120
|
+
constructor(config, adapter, logger = console) {
|
|
121
|
+
this.config = config;
|
|
122
|
+
this.adapter = adapter;
|
|
123
|
+
this.logger = logger;
|
|
124
|
+
}
|
|
125
|
+
async listUserCredentials(userId) {
|
|
126
|
+
return this.adapter.listUserCredentials(userId);
|
|
127
|
+
}
|
|
128
|
+
async deleteCredential(credentialId, userId) {
|
|
129
|
+
if (userId !== undefined) {
|
|
130
|
+
const credentials = await this.adapter.listUserCredentials(userId);
|
|
131
|
+
const target = Buffer.isBuffer(credentialId) ? credentialId : Buffer.from(String(credentialId), 'base64');
|
|
132
|
+
const owns = credentials.some((c) => {
|
|
133
|
+
const stored = Buffer.isBuffer(c.credentialId)
|
|
134
|
+
? c.credentialId
|
|
135
|
+
: Buffer.from(String(c.credentialId), 'base64');
|
|
136
|
+
return stored.equals(target);
|
|
137
|
+
});
|
|
138
|
+
if (!owns)
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return this.adapter.deleteCredential(credentialId);
|
|
142
|
+
}
|
|
143
|
+
async createChallenge(params) {
|
|
144
|
+
await this.adapter.cleanupChallenges?.(new Date());
|
|
145
|
+
if (params.action === 'register') {
|
|
146
|
+
return this.createRegistrationChallenge(params);
|
|
147
|
+
}
|
|
148
|
+
if (params.action === 'authenticate') {
|
|
149
|
+
return this.createAuthenticationChallenge(params);
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`Unsupported passkey action: ${String(params.action)}`);
|
|
152
|
+
}
|
|
153
|
+
async verifyResponse(params) {
|
|
154
|
+
await this.adapter.cleanupChallenges?.(new Date());
|
|
155
|
+
const existing = this.adapter.getChallenge ? await this.adapter.getChallenge(params.expectedChallenge) : null;
|
|
156
|
+
if (existing && existing.expiresAt.getTime() <= Date.now()) {
|
|
157
|
+
return { verified: false };
|
|
158
|
+
}
|
|
159
|
+
const record = await this.adapter.consumeChallenge(params.expectedChallenge);
|
|
160
|
+
if (!record) {
|
|
161
|
+
return { verified: false };
|
|
162
|
+
}
|
|
163
|
+
if (record.expiresAt.getTime() <= Date.now()) {
|
|
164
|
+
return { verified: false };
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
if (record.action === 'register') {
|
|
168
|
+
return this.verifyRegistration(params, record);
|
|
169
|
+
}
|
|
170
|
+
if (record.action === 'authenticate') {
|
|
171
|
+
return this.verifyAuthentication(params, record);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
this.logger.error?.('Passkey verification failed', error);
|
|
176
|
+
}
|
|
177
|
+
return { verified: false };
|
|
178
|
+
}
|
|
179
|
+
async createRegistrationChallenge(params) {
|
|
180
|
+
const user = await this.requireUser({ userId: params.userId, login: params.login });
|
|
181
|
+
const existing = await this.adapter.listUserCredentials(user.id);
|
|
182
|
+
const excludeCredentials = existing.map((credential) => {
|
|
183
|
+
const transports = credential.transports;
|
|
184
|
+
return transports && transports.length > 0
|
|
185
|
+
? { id: toBase64Url(credential.credentialId), transports }
|
|
186
|
+
: { id: toBase64Url(credential.credentialId) };
|
|
187
|
+
});
|
|
188
|
+
const options = await generateRegistrationOptions({
|
|
189
|
+
rpName: this.config.rpName,
|
|
190
|
+
rpID: this.config.rpId,
|
|
191
|
+
userID: Buffer.from(String(user.id)),
|
|
192
|
+
userName: user.login,
|
|
193
|
+
userDisplayName: user.displayName,
|
|
194
|
+
excludeCredentials
|
|
195
|
+
});
|
|
196
|
+
const expiresAt = this.createExpiry();
|
|
197
|
+
await this.adapter.saveChallenge({
|
|
198
|
+
challenge: options.challenge,
|
|
199
|
+
action: 'register',
|
|
200
|
+
userId: user.id,
|
|
201
|
+
login: user.login,
|
|
202
|
+
expiresAt
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
challenge: options.challenge,
|
|
206
|
+
expiresAt: expiresAt.toISOString(),
|
|
207
|
+
userId: user.id,
|
|
208
|
+
publicKey: options
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async createAuthenticationChallenge(params) {
|
|
212
|
+
const user = await this.requireUser({ userId: params.userId, login: params.login });
|
|
213
|
+
const credentials = await this.adapter.listUserCredentials(user.id);
|
|
214
|
+
const allowCredentials = credentials.map((credential) => {
|
|
215
|
+
const transports = sanitizeTransports(credential.transports);
|
|
216
|
+
return transports && transports.length > 0
|
|
217
|
+
? { type: 'public-key', id: toBase64Url(credential.credentialId), transports }
|
|
218
|
+
: { type: 'public-key', id: toBase64Url(credential.credentialId) };
|
|
219
|
+
});
|
|
220
|
+
const options = await generateAuthenticationOptions({
|
|
221
|
+
allowCredentials,
|
|
222
|
+
userVerification: this.config.userVerification,
|
|
223
|
+
rpID: this.config.rpId
|
|
224
|
+
});
|
|
225
|
+
const expiresAt = this.createExpiry();
|
|
226
|
+
await this.adapter.saveChallenge({
|
|
227
|
+
challenge: options.challenge,
|
|
228
|
+
action: 'authenticate',
|
|
229
|
+
userId: user.id,
|
|
230
|
+
login: user.login,
|
|
231
|
+
expiresAt
|
|
232
|
+
});
|
|
233
|
+
return {
|
|
234
|
+
challenge: options.challenge,
|
|
235
|
+
expiresAt: expiresAt.toISOString(),
|
|
236
|
+
userId: user.id,
|
|
237
|
+
publicKey: options
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
async verifyRegistration(params, record) {
|
|
241
|
+
const parsed = typeof params.response === 'object' && params.response ? { ...params.response } : {};
|
|
242
|
+
const response = {
|
|
243
|
+
...parsed,
|
|
244
|
+
id: String(parsed.id ?? ''),
|
|
245
|
+
rawId: String(parsed.rawId ?? '')
|
|
246
|
+
};
|
|
247
|
+
const user = await this.requireUser({ userId: record.userId, login: record.login });
|
|
248
|
+
const result = await verifyRegistrationResponse({
|
|
249
|
+
response,
|
|
250
|
+
expectedChallenge: record.challenge,
|
|
251
|
+
expectedOrigin: this.config.origins,
|
|
252
|
+
expectedRPID: this.config.rpId,
|
|
253
|
+
requireUserVerification: this.requireUserVerification()
|
|
254
|
+
});
|
|
255
|
+
if (!result.verified || !result.registrationInfo) {
|
|
256
|
+
if (!result.verified) {
|
|
257
|
+
const err = result.error ?? result;
|
|
258
|
+
this.logger.error?.('Passkey registration verification failed', err);
|
|
259
|
+
}
|
|
260
|
+
return { verified: false };
|
|
261
|
+
}
|
|
262
|
+
const registrationInfo = result.registrationInfo;
|
|
263
|
+
const attestationResponse = params.response.response;
|
|
264
|
+
const credentialIdPrimary = toBufferOrNull(registrationInfo.credentialID);
|
|
265
|
+
const credentialIdFallback = toBufferOrNull(params.response.id);
|
|
266
|
+
const credentialId = credentialIdPrimary && credentialIdPrimary.length > 0
|
|
267
|
+
? credentialIdPrimary
|
|
268
|
+
: (credentialIdFallback ?? Buffer.alloc(0));
|
|
269
|
+
const publicKeyPrimary = toBufferOrNull(registrationInfo.credentialPublicKey);
|
|
270
|
+
let publicKeyFallback = toBufferOrNull(attestationResponse?.publicKey);
|
|
271
|
+
if ((!publicKeyPrimary || publicKeyPrimary.length === 0) && attestationResponse?.attestationObject) {
|
|
272
|
+
try {
|
|
273
|
+
const attestationObject = String(attestationResponse.attestationObject);
|
|
274
|
+
const attObj = decodeAttestationObject(isoBase64URL.toBuffer(attestationObject));
|
|
275
|
+
const parsedAuth = parseAuthenticatorData(attObj.get('authData'));
|
|
276
|
+
publicKeyFallback = toBufferOrNull(parsedAuth.credentialPublicKey) ?? publicKeyFallback;
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
this.logger.warn?.('Passkey registration: failed to parse attestationObject', error);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const publicKey = publicKeyPrimary && publicKeyPrimary.length > 0 ? publicKeyPrimary : publicKeyFallback;
|
|
283
|
+
if (this.config.debug && this.logger?.warn) {
|
|
284
|
+
const pkPrimaryHex = publicKeyPrimary ? publicKeyPrimary.slice(0, 4).toString('hex') : 'null';
|
|
285
|
+
const pkFallbackHex = publicKeyFallback ? publicKeyFallback.slice(0, 4).toString('hex') : 'null';
|
|
286
|
+
this.logger.warn(`Passkey registration: pkPrimary len=${publicKeyPrimary?.length ?? 0} head=${pkPrimaryHex}, pkFallback len=${publicKeyFallback?.length ?? 0} head=${pkFallbackHex}`);
|
|
287
|
+
}
|
|
288
|
+
if (!credentialId || credentialId.length === 0) {
|
|
289
|
+
return { verified: false, message: 'Missing credential id in registration response' };
|
|
290
|
+
}
|
|
291
|
+
if (!publicKey || publicKey.length === 0) {
|
|
292
|
+
return { verified: false, message: 'Missing public key in registration response' };
|
|
293
|
+
}
|
|
294
|
+
let storedPublicKey = Buffer.isBuffer(publicKey) ? publicKey : toBuffer(publicKey);
|
|
295
|
+
// If fallback is an SPKI key (DER, starts with 0x30) convert to COSE so verification succeeds.
|
|
296
|
+
if (storedPublicKey[0] === 0x30) {
|
|
297
|
+
const converted = await spkiToCosePublicKey(storedPublicKey);
|
|
298
|
+
if (converted) {
|
|
299
|
+
storedPublicKey = converted;
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
this.logger.warn?.('Passkey registration: failed to convert SPKI public key to COSE');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
await this.adapter.saveCredential({
|
|
306
|
+
userId: user.id,
|
|
307
|
+
credentialId,
|
|
308
|
+
publicKey: storedPublicKey,
|
|
309
|
+
counter: registrationInfo.counter ?? 0,
|
|
310
|
+
transports: sanitizeTransports(params.response.transports),
|
|
311
|
+
backedUp: registrationInfo.credentialBackedUp ?? registrationInfo.credentialDeviceType === 'multiDevice',
|
|
312
|
+
deviceType: registrationInfo.credentialDeviceType,
|
|
313
|
+
label: toOptionalString(params.label),
|
|
314
|
+
createdDomain: toOptionalString(params.domain),
|
|
315
|
+
createdUserAgent: toOptionalString(params.userAgent),
|
|
316
|
+
createdBrowser: toOptionalString(params.browser),
|
|
317
|
+
createdOs: toOptionalString(params.os),
|
|
318
|
+
createdDevice: toOptionalString(params.device),
|
|
319
|
+
createdIp: toOptionalString(params.ip)
|
|
320
|
+
});
|
|
321
|
+
return { verified: true, userId: user.id, login: user.login };
|
|
322
|
+
}
|
|
323
|
+
async verifyAuthentication(params, record) {
|
|
324
|
+
const parsed = typeof params.response === 'object' && params.response ? { ...params.response } : {};
|
|
325
|
+
const response = {
|
|
326
|
+
...parsed,
|
|
327
|
+
id: String(parsed.id ?? ''),
|
|
328
|
+
rawId: String(parsed.rawId ?? '')
|
|
329
|
+
};
|
|
330
|
+
const credential = await this.adapter.findCredentialById(fromBase64Url(response.id));
|
|
331
|
+
if (!credential) {
|
|
332
|
+
return { verified: false };
|
|
333
|
+
}
|
|
334
|
+
const user = await this.requireUser({ userId: credential.userId, login: record.login });
|
|
335
|
+
const storedAuthData = {
|
|
336
|
+
id: toBase64Url(credential.credentialId),
|
|
337
|
+
publicKey: new Uint8Array(toBuffer(credential.publicKey)),
|
|
338
|
+
counter: credential.counter,
|
|
339
|
+
transports: credential.transports ?? undefined
|
|
340
|
+
// simplewebauthn accepts either Uint8Array or Buffer; ensure Buffer
|
|
341
|
+
// see https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/authentication/verifyAuthenticationResponse.ts
|
|
342
|
+
};
|
|
343
|
+
const result = await verifyAuthenticationResponse({
|
|
344
|
+
response,
|
|
345
|
+
expectedChallenge: record.challenge,
|
|
346
|
+
expectedOrigin: this.config.origins,
|
|
347
|
+
expectedRPID: this.config.rpId,
|
|
348
|
+
credential: storedAuthData,
|
|
349
|
+
requireUserVerification: this.requireUserVerification()
|
|
350
|
+
});
|
|
351
|
+
if (!result.verified) {
|
|
352
|
+
const err = result.error ?? result;
|
|
353
|
+
this.logger.error?.('Passkey authentication verification failed', err);
|
|
354
|
+
return { verified: false };
|
|
355
|
+
}
|
|
356
|
+
await this.adapter.updateCredentialCounter(credential.credentialId, result.authenticationInfo.newCounter);
|
|
357
|
+
return {
|
|
358
|
+
verified: true,
|
|
359
|
+
userId: user.id,
|
|
360
|
+
login: user.login
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
async requireUser(params) {
|
|
364
|
+
const user = await this.adapter.resolveUser(params);
|
|
365
|
+
if (!user) {
|
|
366
|
+
throw new Error('User not found');
|
|
367
|
+
}
|
|
368
|
+
return user;
|
|
369
|
+
}
|
|
370
|
+
createExpiry() {
|
|
371
|
+
return new Date(Date.now() + this.config.timeoutMs);
|
|
372
|
+
}
|
|
373
|
+
requireUserVerification() {
|
|
374
|
+
return this.config.userVerification !== 'discouraged';
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
2
|
+
import type { Token, TokenPair } from '../token/types.js';
|
|
3
|
+
import type { AuthenticatorTransportFuture } from '@simplewebauthn/server';
|
|
4
|
+
export type CredentialDeviceType = 'singleDevice' | 'multiDevice';
|
|
5
|
+
export interface PasskeyServiceConfig {
|
|
6
|
+
rpId: string;
|
|
7
|
+
rpName: string;
|
|
8
|
+
origins: string[];
|
|
9
|
+
timeoutMs: number;
|
|
10
|
+
userVerification?: 'preferred' | 'required' | 'discouraged';
|
|
11
|
+
/**
|
|
12
|
+
* When enabled, PasskeyService emits additional diagnostic logs during registration/authentication.
|
|
13
|
+
* Defaults to false.
|
|
14
|
+
*/
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface PasskeyChallengeRecord {
|
|
18
|
+
challenge: string;
|
|
19
|
+
action: 'register' | 'authenticate';
|
|
20
|
+
userId?: AuthIdentifier;
|
|
21
|
+
login?: string;
|
|
22
|
+
expiresAt: Date;
|
|
23
|
+
}
|
|
24
|
+
export interface PasskeyUserDescriptor {
|
|
25
|
+
id: AuthIdentifier;
|
|
26
|
+
login: string;
|
|
27
|
+
displayName: string;
|
|
28
|
+
}
|
|
29
|
+
export interface StoredPasskeyCredential {
|
|
30
|
+
userId: AuthIdentifier;
|
|
31
|
+
credentialId: Buffer;
|
|
32
|
+
publicKey: Buffer;
|
|
33
|
+
counter: number;
|
|
34
|
+
transports?: AuthenticatorTransportFuture[];
|
|
35
|
+
backedUp: boolean;
|
|
36
|
+
deviceType: CredentialDeviceType;
|
|
37
|
+
label?: string;
|
|
38
|
+
createdDomain?: string;
|
|
39
|
+
createdUserAgent?: string;
|
|
40
|
+
createdBrowser?: string;
|
|
41
|
+
createdOs?: string;
|
|
42
|
+
createdDevice?: string;
|
|
43
|
+
createdIp?: string;
|
|
44
|
+
createdAt?: Date;
|
|
45
|
+
updatedAt?: Date;
|
|
46
|
+
}
|
|
47
|
+
export interface PasskeyStorageAdapter {
|
|
48
|
+
resolveUser(params: {
|
|
49
|
+
userId?: AuthIdentifier;
|
|
50
|
+
login?: string;
|
|
51
|
+
}): Promise<PasskeyUserDescriptor | null>;
|
|
52
|
+
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
53
|
+
deleteCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
54
|
+
findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
|
|
55
|
+
saveCredential(record: StoredPasskeyCredential): Promise<void>;
|
|
56
|
+
updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
|
|
57
|
+
saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
|
|
58
|
+
getChallenge?(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
59
|
+
consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
60
|
+
cleanupChallenges?(now: Date): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
export interface PasskeyChallengeParams {
|
|
63
|
+
action: 'register' | 'authenticate';
|
|
64
|
+
login?: string;
|
|
65
|
+
userId?: AuthIdentifier;
|
|
66
|
+
}
|
|
67
|
+
export interface PasskeyChallenge extends Record<string, unknown> {
|
|
68
|
+
challenge: string;
|
|
69
|
+
expiresAt?: string | number | Date;
|
|
70
|
+
userId?: AuthIdentifier;
|
|
71
|
+
}
|
|
72
|
+
export interface PasskeyVerificationParams extends Partial<Omit<Token, 'userId'>> {
|
|
73
|
+
expectedChallenge: string;
|
|
74
|
+
login?: string;
|
|
75
|
+
response: Record<string, unknown>;
|
|
76
|
+
userId?: AuthIdentifier;
|
|
77
|
+
userAgent?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface PasskeyVerificationResult extends Record<string, unknown> {
|
|
80
|
+
login?: string;
|
|
81
|
+
tokens?: TokenPair;
|
|
82
|
+
userId?: AuthIdentifier;
|
|
83
|
+
verified: boolean;
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { DataTypes, type InitOptions, type Model, type Sequelize } from 'sequelize';
|
|
2
|
+
export declare const DIALECTS_SUPPORTING_UNSIGNED: Set<string>;
|
|
3
|
+
export declare function integerIdType(sequelize: Sequelize): DataTypes.IntegerDataTypeConstructor;
|
|
4
|
+
export declare function tableOptions<ModelType extends Model>(sequelize: Sequelize, tableName: string, tablePrefix?: string, extra?: Partial<InitOptions<ModelType>>): InitOptions<ModelType>;
|
|
5
|
+
export declare function normalizeTablePrefix(prefix?: string): string | undefined;
|
|
6
|
+
export declare function applyTablePrefix(prefix: string | undefined, tableName: string): string;
|
|
7
|
+
export declare function encodeStringArray(values: string[] | undefined): string;
|
|
8
|
+
export declare function decodeStringArray(raw: string | null | undefined): string[];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
export const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
|
|
3
|
+
export function integerIdType(sequelize) {
|
|
4
|
+
return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
|
|
5
|
+
}
|
|
6
|
+
export function tableOptions(sequelize, tableName, tablePrefix, extra) {
|
|
7
|
+
const opts = { sequelize, tableName: applyTablePrefix(tablePrefix, tableName) };
|
|
8
|
+
if (extra) {
|
|
9
|
+
Object.assign(opts, extra);
|
|
10
|
+
}
|
|
11
|
+
if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
|
|
12
|
+
opts.charset = 'utf8mb4';
|
|
13
|
+
opts.collate = 'utf8mb4_unicode_ci';
|
|
14
|
+
}
|
|
15
|
+
return opts;
|
|
16
|
+
}
|
|
17
|
+
export function normalizeTablePrefix(prefix) {
|
|
18
|
+
if (!prefix) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const trimmed = prefix.trim();
|
|
22
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
23
|
+
}
|
|
24
|
+
export function applyTablePrefix(prefix, tableName) {
|
|
25
|
+
const normalized = normalizeTablePrefix(prefix);
|
|
26
|
+
return normalized ? `${normalized}${tableName}` : tableName;
|
|
27
|
+
}
|
|
28
|
+
export function encodeStringArray(values) {
|
|
29
|
+
return JSON.stringify(values ?? []);
|
|
30
|
+
}
|
|
31
|
+
export function decodeStringArray(raw) {
|
|
32
|
+
if (!raw) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
if (Array.isArray(parsed)) {
|
|
38
|
+
return parsed.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Malformed JSON — return empty rather than guessing via whitespace split.
|
|
43
|
+
// A whitespace-split fallback could silently grant unintended permissions
|
|
44
|
+
// if a scope/role column contains a corrupted value like "admin user".
|
|
45
|
+
}
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Token } from './types.js';
|
|
2
|
+
import type { DecodeOptions, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
3
|
+
export interface JwtSignResult {
|
|
4
|
+
success: boolean;
|
|
5
|
+
token?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
export type JwtSignPayload = string | Buffer | Record<string, unknown>;
|
|
9
|
+
export interface JwtVerifyResult<T> {
|
|
10
|
+
success: boolean;
|
|
11
|
+
data?: T;
|
|
12
|
+
expired?: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface JwtDecodeResult<T> {
|
|
16
|
+
success: boolean;
|
|
17
|
+
data?: T;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Base contract for token/session persistence backends plus shared JWT helpers. */
|
|
21
|
+
export declare abstract class TokenStore {
|
|
22
|
+
/** Create/persist a token record. */
|
|
23
|
+
abstract save(record: Token): Promise<void>;
|
|
24
|
+
/** Read a token record by partial query. */
|
|
25
|
+
abstract get(query: Partial<Token>, opts?: {
|
|
26
|
+
includeExpired?: boolean;
|
|
27
|
+
}): Promise<Token | null>;
|
|
28
|
+
/** Delete token records matching a partial query. */
|
|
29
|
+
abstract delete(query: Partial<Token>): Promise<number>;
|
|
30
|
+
/** Update a token identified by refresh token. */
|
|
31
|
+
abstract update(update: Partial<Token> & {
|
|
32
|
+
refreshToken: string;
|
|
33
|
+
}): Promise<boolean>;
|
|
34
|
+
/** List tokens for a specific user. */
|
|
35
|
+
abstract list(userId: string | number, opts?: {
|
|
36
|
+
limit?: number;
|
|
37
|
+
offset?: number;
|
|
38
|
+
includeExpired?: boolean;
|
|
39
|
+
}): Promise<Token[]>;
|
|
40
|
+
/** Close underlying resources. */
|
|
41
|
+
abstract close(): Promise<void>;
|
|
42
|
+
normalizeToken(token: Partial<Token>): Token;
|
|
43
|
+
jwtSign(payload: JwtSignPayload, secret: string, expiresInSeconds: number, options?: SignOptions): JwtSignResult;
|
|
44
|
+
jwtVerify<T>(token: string, secret: string, options?: VerifyOptions): JwtVerifyResult<T>;
|
|
45
|
+
jwtDecode<T>(token: string, options?: DecodeOptions): JwtDecodeResult<T>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
function normalizeScope(scope) {
|
|
3
|
+
if (!scope) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
if (Array.isArray(scope)) {
|
|
7
|
+
return scope.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
8
|
+
}
|
|
9
|
+
return scope
|
|
10
|
+
.split(/\s+/)
|
|
11
|
+
.map((entry) => entry.trim())
|
|
12
|
+
.filter((entry) => entry.length > 0);
|
|
13
|
+
}
|
|
14
|
+
function normalizeRefreshTtlSeconds(value) {
|
|
15
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
|
16
|
+
return Math.floor(value);
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
const parsed = Number(value);
|
|
20
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
21
|
+
return Math.floor(parsed);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
function normalizeTokenInternal(input) {
|
|
27
|
+
if (!input.refreshToken) {
|
|
28
|
+
throw new Error('refreshToken is required');
|
|
29
|
+
}
|
|
30
|
+
if (!input.accessToken) {
|
|
31
|
+
throw new Error('accessToken is required');
|
|
32
|
+
}
|
|
33
|
+
if (input.userId === undefined || input.userId === null) {
|
|
34
|
+
throw new Error('userId is required');
|
|
35
|
+
}
|
|
36
|
+
const userId = String(input.userId);
|
|
37
|
+
const ruid = input.ruid === undefined || input.ruid === null ? undefined : String(input.ruid);
|
|
38
|
+
const expires = input.expires ? new Date(input.expires) : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
|
39
|
+
if (Number.isNaN(expires.getTime())) {
|
|
40
|
+
throw new Error(`Invalid token expiry value: ${String(input.expires)}`);
|
|
41
|
+
}
|
|
42
|
+
const issuedAt = input.issuedAt ? new Date(input.issuedAt) : new Date();
|
|
43
|
+
const lastSeenAt = input.lastSeenAt ? new Date(input.lastSeenAt) : issuedAt;
|
|
44
|
+
const scope = normalizeScope(input.scope);
|
|
45
|
+
const refreshTtlSeconds = normalizeRefreshTtlSeconds(input.refreshTtlSeconds);
|
|
46
|
+
const status = input.status === 'revoked' ? 'revoked' : expires.getTime() < Date.now() ? 'expired' : 'active';
|
|
47
|
+
const sessionCookie = typeof input.sessionCookie === 'boolean' ? input.sessionCookie : false;
|
|
48
|
+
return {
|
|
49
|
+
accessToken: input.accessToken,
|
|
50
|
+
refreshToken: input.refreshToken,
|
|
51
|
+
userId,
|
|
52
|
+
clientId: typeof input.clientId === 'string' && input.clientId.length > 0 ? input.clientId : undefined,
|
|
53
|
+
domain: typeof input.domain === 'string' ? input.domain : '',
|
|
54
|
+
fingerprint: typeof input.fingerprint === 'string' ? input.fingerprint : '',
|
|
55
|
+
label: typeof input.label === 'string' ? input.label : '',
|
|
56
|
+
browser: typeof input.browser === 'string' ? input.browser : '',
|
|
57
|
+
device: typeof input.device === 'string' ? input.device : '',
|
|
58
|
+
ip: typeof input.ip === 'string' ? input.ip : '',
|
|
59
|
+
os: typeof input.os === 'string' ? input.os : '',
|
|
60
|
+
loginType: typeof input.loginType === 'string' && input.loginType.length > 0 ? input.loginType : undefined,
|
|
61
|
+
scope,
|
|
62
|
+
refreshTtlSeconds,
|
|
63
|
+
expires,
|
|
64
|
+
issuedAt,
|
|
65
|
+
lastSeenAt,
|
|
66
|
+
status,
|
|
67
|
+
ruid,
|
|
68
|
+
sessionCookie
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Base contract for token/session persistence backends plus shared JWT helpers. */
|
|
72
|
+
export class TokenStore {
|
|
73
|
+
// Instance helpers
|
|
74
|
+
normalizeToken(token) {
|
|
75
|
+
return normalizeTokenInternal(token);
|
|
76
|
+
}
|
|
77
|
+
// JWT helpers live on TokenStore so every adapter automatically exposes
|
|
78
|
+
// signing/verification without additional wiring.
|
|
79
|
+
jwtSign(payload, secret, expiresInSeconds, options) {
|
|
80
|
+
const opts = { ...(options ?? {}), expiresIn: expiresInSeconds };
|
|
81
|
+
try {
|
|
82
|
+
const token = jwt.sign(payload, secret, opts);
|
|
83
|
+
return { success: true, token };
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
jwtVerify(token, secret, options) {
|
|
90
|
+
try {
|
|
91
|
+
const data = jwt.verify(token, secret, options ?? {});
|
|
92
|
+
return { success: true, data };
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof jwt.TokenExpiredError) {
|
|
96
|
+
return { success: false, expired: true, error: 'Token expired' };
|
|
97
|
+
}
|
|
98
|
+
return { success: false, expired: false, error: error instanceof Error ? error.message : String(error) };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
jwtDecode(token, options) {
|
|
102
|
+
try {
|
|
103
|
+
const data = jwt.decode(token, options ?? {});
|
|
104
|
+
if (data === null) {
|
|
105
|
+
return { success: false, error: 'Invalid token format' };
|
|
106
|
+
}
|
|
107
|
+
return { success: true, data };
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|