@technomoron/api-server-base 2.0.0-beta.4 → 2.0.0-beta.6
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.
|
@@ -448,6 +448,12 @@ class ApiServer {
|
|
|
448
448
|
}
|
|
449
449
|
return this.passkeyServiceAdapter;
|
|
450
450
|
}
|
|
451
|
+
async listUserCredentials(userId) {
|
|
452
|
+
return this.ensurePasskeyService().listUserCredentials(userId);
|
|
453
|
+
}
|
|
454
|
+
async deletePasskeyCredential(credentialId) {
|
|
455
|
+
return this.ensurePasskeyService().deleteCredential(credentialId);
|
|
456
|
+
}
|
|
451
457
|
ensureOAuthStore() {
|
|
452
458
|
if (!this.oauthStoreAdapter) {
|
|
453
459
|
throw new Error('OAuth store is not configured');
|
|
@@ -13,7 +13,7 @@ import type { AuthAdapter, AuthIdentifier } from './auth-api/types.js';
|
|
|
13
13
|
import type { OAuthStore } from './oauth/base.js';
|
|
14
14
|
import type { AuthCodeData, AuthCodeRequest, OAuthClient } from './oauth/types.js';
|
|
15
15
|
import type { PasskeyService } from './passkey/service.js';
|
|
16
|
-
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyVerificationParams, PasskeyVerificationResult } from './passkey/types.js';
|
|
16
|
+
import type { PasskeyChallenge, PasskeyChallengeParams, StoredPasskeyCredential, PasskeyVerificationParams, PasskeyVerificationResult } from './passkey/types.js';
|
|
17
17
|
import type { Token } from './token/types.js';
|
|
18
18
|
import type { UserStore } from './user/base.js';
|
|
19
19
|
import type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
@@ -139,6 +139,8 @@ export declare class ApiServer {
|
|
|
139
139
|
private ensureUserStore;
|
|
140
140
|
private ensureTokenStore;
|
|
141
141
|
private ensurePasskeyService;
|
|
142
|
+
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
143
|
+
deletePasskeyCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
142
144
|
private ensureOAuthStore;
|
|
143
145
|
getUser(identifier: AuthIdentifier): Promise<any | null>;
|
|
144
146
|
getUserPasswordHash(user: any): string;
|
|
@@ -34,6 +34,26 @@ function toBuffer(value) {
|
|
|
34
34
|
const view = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
35
35
|
return Buffer.from(view);
|
|
36
36
|
}
|
|
37
|
+
function toBufferOrNull(value) {
|
|
38
|
+
if (!value) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
if (Buffer.isBuffer(value)) {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
|
|
45
|
+
return toBuffer(value);
|
|
46
|
+
}
|
|
47
|
+
if (typeof value === 'string') {
|
|
48
|
+
try {
|
|
49
|
+
return fromBase64Url(value);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
37
57
|
class PasskeyService {
|
|
38
58
|
constructor(config, adapter, logger = console) {
|
|
39
59
|
this.config = config;
|
|
@@ -113,7 +133,8 @@ class PasskeyService {
|
|
|
113
133
|
return {
|
|
114
134
|
challenge: options.challenge,
|
|
115
135
|
expiresAt: expiresAt.toISOString(),
|
|
116
|
-
userId: user.id
|
|
136
|
+
userId: user.id,
|
|
137
|
+
publicKey: options
|
|
117
138
|
};
|
|
118
139
|
}
|
|
119
140
|
async createAuthenticationChallenge(params, metadata) {
|
|
@@ -142,7 +163,8 @@ class PasskeyService {
|
|
|
142
163
|
return {
|
|
143
164
|
challenge: options.challenge,
|
|
144
165
|
expiresAt: expiresAt.toISOString(),
|
|
145
|
-
userId: user.id
|
|
166
|
+
userId: user.id,
|
|
167
|
+
publicKey: options
|
|
146
168
|
};
|
|
147
169
|
}
|
|
148
170
|
async verifyRegistration(params, record) {
|
|
@@ -161,14 +183,31 @@ class PasskeyService {
|
|
|
161
183
|
requireUserVerification: true
|
|
162
184
|
});
|
|
163
185
|
if (!result.verified || !result.registrationInfo) {
|
|
186
|
+
if (!result.verified) {
|
|
187
|
+
const err = result.error ?? result;
|
|
188
|
+
this.logger.error?.('Passkey registration verification failed', err);
|
|
189
|
+
}
|
|
164
190
|
return { verified: false };
|
|
165
191
|
}
|
|
166
192
|
const registrationInfo = result.registrationInfo;
|
|
193
|
+
const attestationResponse = params.response?.response;
|
|
194
|
+
const credentialIdPrimary = toBufferOrNull(registrationInfo.credentialID);
|
|
195
|
+
const credentialIdFallback = toBufferOrNull(params.response?.id);
|
|
196
|
+
const credentialId = credentialIdPrimary && credentialIdPrimary.length > 0 ? credentialIdPrimary : credentialIdFallback;
|
|
197
|
+
const publicKeyPrimary = toBufferOrNull(registrationInfo.credentialPublicKey);
|
|
198
|
+
const publicKeyFallback = toBufferOrNull(attestationResponse?.publicKey);
|
|
199
|
+
const publicKey = publicKeyPrimary && publicKeyPrimary.length > 0 ? publicKeyPrimary : publicKeyFallback;
|
|
200
|
+
if (!credentialId || credentialId.length === 0) {
|
|
201
|
+
return { verified: false, message: 'Missing credential id in registration response' };
|
|
202
|
+
}
|
|
203
|
+
if (!publicKey || publicKey.length === 0) {
|
|
204
|
+
return { verified: false, message: 'Missing public key in registration response' };
|
|
205
|
+
}
|
|
167
206
|
await this.adapter.saveCredential({
|
|
168
207
|
userId: user.id,
|
|
169
|
-
credentialId
|
|
170
|
-
publicKey
|
|
171
|
-
counter: registrationInfo.counter,
|
|
208
|
+
credentialId,
|
|
209
|
+
publicKey,
|
|
210
|
+
counter: registrationInfo.counter ?? 0,
|
|
172
211
|
transports: sanitizeTransports(params.response.transports),
|
|
173
212
|
backedUp: registrationInfo.credentialDeviceType === 'multiDevice',
|
|
174
213
|
deviceType: registrationInfo.credentialDeviceType
|
|
@@ -204,6 +243,8 @@ class PasskeyService {
|
|
|
204
243
|
requireUserVerification: true
|
|
205
244
|
});
|
|
206
245
|
if (!result.verified) {
|
|
246
|
+
const err = result.error ?? result;
|
|
247
|
+
this.logger.error?.('Passkey authentication verification failed', err);
|
|
207
248
|
return { verified: false };
|
|
208
249
|
}
|
|
209
250
|
await this.adapter.updateCredentialCounter(credential.credentialId, result.authenticationInfo.newCounter);
|
|
@@ -13,7 +13,7 @@ import type { AuthAdapter, AuthIdentifier } from './auth-api/types.js';
|
|
|
13
13
|
import type { OAuthStore } from './oauth/base.js';
|
|
14
14
|
import type { AuthCodeData, AuthCodeRequest, OAuthClient } from './oauth/types.js';
|
|
15
15
|
import type { PasskeyService } from './passkey/service.js';
|
|
16
|
-
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyVerificationParams, PasskeyVerificationResult } from './passkey/types.js';
|
|
16
|
+
import type { PasskeyChallenge, PasskeyChallengeParams, StoredPasskeyCredential, PasskeyVerificationParams, PasskeyVerificationResult } from './passkey/types.js';
|
|
17
17
|
import type { Token } from './token/types.js';
|
|
18
18
|
import type { UserStore } from './user/base.js';
|
|
19
19
|
import type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
|
@@ -139,6 +139,8 @@ export declare class ApiServer {
|
|
|
139
139
|
private ensureUserStore;
|
|
140
140
|
private ensureTokenStore;
|
|
141
141
|
private ensurePasskeyService;
|
|
142
|
+
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
143
|
+
deletePasskeyCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
142
144
|
private ensureOAuthStore;
|
|
143
145
|
getUser(identifier: AuthIdentifier): Promise<any | null>;
|
|
144
146
|
getUserPasswordHash(user: any): string;
|
|
@@ -440,6 +440,12 @@ export class ApiServer {
|
|
|
440
440
|
}
|
|
441
441
|
return this.passkeyServiceAdapter;
|
|
442
442
|
}
|
|
443
|
+
async listUserCredentials(userId) {
|
|
444
|
+
return this.ensurePasskeyService().listUserCredentials(userId);
|
|
445
|
+
}
|
|
446
|
+
async deletePasskeyCredential(credentialId) {
|
|
447
|
+
return this.ensurePasskeyService().deleteCredential(credentialId);
|
|
448
|
+
}
|
|
443
449
|
ensureOAuthStore() {
|
|
444
450
|
if (!this.oauthStoreAdapter) {
|
|
445
451
|
throw new Error('OAuth store is not configured');
|
|
@@ -31,6 +31,26 @@ function toBuffer(value) {
|
|
|
31
31
|
const view = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
32
32
|
return Buffer.from(view);
|
|
33
33
|
}
|
|
34
|
+
function toBufferOrNull(value) {
|
|
35
|
+
if (!value) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (Buffer.isBuffer(value)) {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
|
|
42
|
+
return toBuffer(value);
|
|
43
|
+
}
|
|
44
|
+
if (typeof value === 'string') {
|
|
45
|
+
try {
|
|
46
|
+
return fromBase64Url(value);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
34
54
|
export class PasskeyService {
|
|
35
55
|
constructor(config, adapter, logger = console) {
|
|
36
56
|
this.config = config;
|
|
@@ -110,7 +130,8 @@ export class PasskeyService {
|
|
|
110
130
|
return {
|
|
111
131
|
challenge: options.challenge,
|
|
112
132
|
expiresAt: expiresAt.toISOString(),
|
|
113
|
-
userId: user.id
|
|
133
|
+
userId: user.id,
|
|
134
|
+
publicKey: options
|
|
114
135
|
};
|
|
115
136
|
}
|
|
116
137
|
async createAuthenticationChallenge(params, metadata) {
|
|
@@ -139,7 +160,8 @@ export class PasskeyService {
|
|
|
139
160
|
return {
|
|
140
161
|
challenge: options.challenge,
|
|
141
162
|
expiresAt: expiresAt.toISOString(),
|
|
142
|
-
userId: user.id
|
|
163
|
+
userId: user.id,
|
|
164
|
+
publicKey: options
|
|
143
165
|
};
|
|
144
166
|
}
|
|
145
167
|
async verifyRegistration(params, record) {
|
|
@@ -158,14 +180,31 @@ export class PasskeyService {
|
|
|
158
180
|
requireUserVerification: true
|
|
159
181
|
});
|
|
160
182
|
if (!result.verified || !result.registrationInfo) {
|
|
183
|
+
if (!result.verified) {
|
|
184
|
+
const err = result.error ?? result;
|
|
185
|
+
this.logger.error?.('Passkey registration verification failed', err);
|
|
186
|
+
}
|
|
161
187
|
return { verified: false };
|
|
162
188
|
}
|
|
163
189
|
const registrationInfo = result.registrationInfo;
|
|
190
|
+
const attestationResponse = params.response?.response;
|
|
191
|
+
const credentialIdPrimary = toBufferOrNull(registrationInfo.credentialID);
|
|
192
|
+
const credentialIdFallback = toBufferOrNull(params.response?.id);
|
|
193
|
+
const credentialId = credentialIdPrimary && credentialIdPrimary.length > 0 ? credentialIdPrimary : credentialIdFallback;
|
|
194
|
+
const publicKeyPrimary = toBufferOrNull(registrationInfo.credentialPublicKey);
|
|
195
|
+
const publicKeyFallback = toBufferOrNull(attestationResponse?.publicKey);
|
|
196
|
+
const publicKey = publicKeyPrimary && publicKeyPrimary.length > 0 ? publicKeyPrimary : publicKeyFallback;
|
|
197
|
+
if (!credentialId || credentialId.length === 0) {
|
|
198
|
+
return { verified: false, message: 'Missing credential id in registration response' };
|
|
199
|
+
}
|
|
200
|
+
if (!publicKey || publicKey.length === 0) {
|
|
201
|
+
return { verified: false, message: 'Missing public key in registration response' };
|
|
202
|
+
}
|
|
164
203
|
await this.adapter.saveCredential({
|
|
165
204
|
userId: user.id,
|
|
166
|
-
credentialId
|
|
167
|
-
publicKey
|
|
168
|
-
counter: registrationInfo.counter,
|
|
205
|
+
credentialId,
|
|
206
|
+
publicKey,
|
|
207
|
+
counter: registrationInfo.counter ?? 0,
|
|
169
208
|
transports: sanitizeTransports(params.response.transports),
|
|
170
209
|
backedUp: registrationInfo.credentialDeviceType === 'multiDevice',
|
|
171
210
|
deviceType: registrationInfo.credentialDeviceType
|
|
@@ -201,6 +240,8 @@ export class PasskeyService {
|
|
|
201
240
|
requireUserVerification: true
|
|
202
241
|
});
|
|
203
242
|
if (!result.verified) {
|
|
243
|
+
const err = result.error ?? result;
|
|
244
|
+
this.logger.error?.('Passkey authentication verification failed', err);
|
|
204
245
|
return { verified: false };
|
|
205
246
|
}
|
|
206
247
|
await this.adapter.updateCredentialCounter(credential.credentialId, result.authenticationInfo.newCounter);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@technomoron/api-server-base",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.6",
|
|
4
4
|
"description": "Api Server Skeleton / Base Class",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.cjs",
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
"build:esm": "tsc --project tsconfig/tsconfig.esm.json",
|
|
31
31
|
"build": "node scripts/run-builds.cjs",
|
|
32
32
|
"test": "vitest run",
|
|
33
|
+
"test:unit": "vitest run tests/unit",
|
|
34
|
+
"test:functional": "vitest run tests/functional",
|
|
33
35
|
"test:watch": "vitest --watch",
|
|
34
36
|
"prepublishOnly": "node scripts/run-builds.cjs",
|
|
35
37
|
"lint": "eslint --ext .js,.ts,.vue ./",
|