@emdash-cms/auth 0.0.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/dist/adapters/kysely.d.mts +62 -0
- package/dist/adapters/kysely.d.mts.map +1 -0
- package/dist/adapters/kysely.mjs +379 -0
- package/dist/adapters/kysely.mjs.map +1 -0
- package/dist/authenticate-D5UgaoTH.d.mts +124 -0
- package/dist/authenticate-D5UgaoTH.d.mts.map +1 -0
- package/dist/authenticate-j5GayLXB.mjs +373 -0
- package/dist/authenticate-j5GayLXB.mjs.map +1 -0
- package/dist/index.d.mts +444 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +728 -0
- package/dist/index.mjs.map +1 -0
- package/dist/oauth/providers/github.d.mts +12 -0
- package/dist/oauth/providers/github.d.mts.map +1 -0
- package/dist/oauth/providers/github.mjs +55 -0
- package/dist/oauth/providers/github.mjs.map +1 -0
- package/dist/oauth/providers/google.d.mts +7 -0
- package/dist/oauth/providers/google.d.mts.map +1 -0
- package/dist/oauth/providers/google.mjs +38 -0
- package/dist/oauth/providers/google.mjs.map +1 -0
- package/dist/passkey/index.d.mts +2 -0
- package/dist/passkey/index.mjs +3 -0
- package/dist/types-Bu4irX9A.d.mts +35 -0
- package/dist/types-Bu4irX9A.d.mts.map +1 -0
- package/dist/types-CiSNpRI9.mjs +60 -0
- package/dist/types-CiSNpRI9.mjs.map +1 -0
- package/dist/types-HtRc90Wi.d.mts +208 -0
- package/dist/types-HtRc90Wi.d.mts.map +1 -0
- package/package.json +72 -0
- package/src/adapters/kysely.ts +715 -0
- package/src/config.ts +214 -0
- package/src/index.ts +135 -0
- package/src/invite.ts +205 -0
- package/src/magic-link/index.ts +150 -0
- package/src/oauth/consumer.ts +324 -0
- package/src/oauth/providers/github.ts +68 -0
- package/src/oauth/providers/google.ts +34 -0
- package/src/oauth/types.ts +36 -0
- package/src/passkey/authenticate.ts +183 -0
- package/src/passkey/index.ts +27 -0
- package/src/passkey/register.ts +232 -0
- package/src/passkey/types.ts +120 -0
- package/src/rbac.test.ts +141 -0
- package/src/rbac.ts +205 -0
- package/src/signup.ts +210 -0
- package/src/tokens.test.ts +141 -0
- package/src/tokens.ts +238 -0
- package/src/types.ts +352 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { sha256 } from "@oslojs/crypto/sha2";
|
|
2
|
+
import { decodeBase64urlIgnorePadding, encodeBase64urlNoPadding } from "@oslojs/encoding";
|
|
3
|
+
import { ECDSAPublicKey, decodePKIXECDSASignature, decodeSEC1PublicKey, p256, verifyECDSASignature } from "@oslojs/crypto/ecdsa";
|
|
4
|
+
import { AttestationStatementFormat, COSEKeyType, ClientDataType, coseAlgorithmES256, coseAlgorithmRS256, coseEllipticCurveP256, createAssertionSignatureMessage, parseAttestationObject, parseAuthenticatorData, parseClientDataJSON } from "@oslojs/webauthn";
|
|
5
|
+
|
|
6
|
+
//#region src/tokens.ts
|
|
7
|
+
/**
|
|
8
|
+
* Secure token utilities
|
|
9
|
+
*
|
|
10
|
+
* Crypto via Oslo.js (@oslojs/crypto). Base64url via @oslojs/encoding.
|
|
11
|
+
*
|
|
12
|
+
* Tokens are opaque random values. We store only the SHA-256 hash in the database.
|
|
13
|
+
*/
|
|
14
|
+
const TOKEN_BYTES = 32;
|
|
15
|
+
/** Valid API token prefixes */
|
|
16
|
+
const TOKEN_PREFIXES = {
|
|
17
|
+
PAT: "ec_pat_",
|
|
18
|
+
OAUTH_ACCESS: "ec_oat_",
|
|
19
|
+
OAUTH_REFRESH: "ec_ort_"
|
|
20
|
+
};
|
|
21
|
+
/** All valid API token scopes */
|
|
22
|
+
const VALID_SCOPES = [
|
|
23
|
+
"content:read",
|
|
24
|
+
"content:write",
|
|
25
|
+
"media:read",
|
|
26
|
+
"media:write",
|
|
27
|
+
"schema:read",
|
|
28
|
+
"schema:write",
|
|
29
|
+
"admin"
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Validate that scopes are all valid.
|
|
33
|
+
* Returns the invalid scopes, or empty array if all valid.
|
|
34
|
+
*/
|
|
35
|
+
function validateScopes(scopes) {
|
|
36
|
+
const validSet = new Set(VALID_SCOPES);
|
|
37
|
+
return scopes.filter((s) => !validSet.has(s));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check if a set of scopes includes a required scope.
|
|
41
|
+
* The `admin` scope grants access to everything.
|
|
42
|
+
*/
|
|
43
|
+
function hasScope(scopes, required) {
|
|
44
|
+
if (scopes.includes("admin")) return true;
|
|
45
|
+
return scopes.includes(required);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Generate a cryptographically secure random token
|
|
49
|
+
* Returns base64url-encoded string (URL-safe)
|
|
50
|
+
*/
|
|
51
|
+
function generateToken() {
|
|
52
|
+
const bytes = new Uint8Array(TOKEN_BYTES);
|
|
53
|
+
crypto.getRandomValues(bytes);
|
|
54
|
+
return encodeBase64urlNoPadding(bytes);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Hash a token for storage
|
|
58
|
+
* We never store raw tokens - only their SHA-256 hash
|
|
59
|
+
*/
|
|
60
|
+
function hashToken(token) {
|
|
61
|
+
return encodeBase64urlNoPadding(sha256(decodeBase64urlIgnorePadding(token)));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Generate a token and its hash together
|
|
65
|
+
*/
|
|
66
|
+
function generateTokenWithHash() {
|
|
67
|
+
const token = generateToken();
|
|
68
|
+
return {
|
|
69
|
+
token,
|
|
70
|
+
hash: hashToken(token)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Generate a session ID (shorter, for cookie storage)
|
|
75
|
+
*/
|
|
76
|
+
function generateSessionId() {
|
|
77
|
+
const bytes = new Uint8Array(20);
|
|
78
|
+
crypto.getRandomValues(bytes);
|
|
79
|
+
return encodeBase64urlNoPadding(bytes);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Generate an auth secret for configuration
|
|
83
|
+
*/
|
|
84
|
+
function generateAuthSecret() {
|
|
85
|
+
const bytes = new Uint8Array(32);
|
|
86
|
+
crypto.getRandomValues(bytes);
|
|
87
|
+
return encodeBase64urlNoPadding(bytes);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Generate a prefixed API token and its hash.
|
|
91
|
+
* Returns the raw token (shown once to the user), the hash (stored server-side),
|
|
92
|
+
* and a display prefix (for identification in UIs/logs).
|
|
93
|
+
*
|
|
94
|
+
* Uses oslo/crypto for SHA-256 hashing.
|
|
95
|
+
*/
|
|
96
|
+
function generatePrefixedToken(prefix) {
|
|
97
|
+
const bytes = new Uint8Array(TOKEN_BYTES);
|
|
98
|
+
crypto.getRandomValues(bytes);
|
|
99
|
+
const raw = `${prefix}${encodeBase64urlNoPadding(bytes)}`;
|
|
100
|
+
return {
|
|
101
|
+
raw,
|
|
102
|
+
hash: hashPrefixedToken(raw),
|
|
103
|
+
prefix: raw.slice(0, prefix.length + 4)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Hash a prefixed API token for storage/lookup.
|
|
108
|
+
* Hashes the full prefixed token string via SHA-256, returns base64url (no padding).
|
|
109
|
+
*/
|
|
110
|
+
function hashPrefixedToken(token) {
|
|
111
|
+
return encodeBase64urlNoPadding(sha256(new TextEncoder().encode(token)));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Compute an S256 PKCE code challenge from a code verifier.
|
|
115
|
+
* Used server-side to verify that code_verifier matches the stored code_challenge.
|
|
116
|
+
*
|
|
117
|
+
* Equivalent to: BASE64URL(SHA256(ASCII(code_verifier)))
|
|
118
|
+
*/
|
|
119
|
+
function computeS256Challenge(codeVerifier) {
|
|
120
|
+
return encodeBase64urlNoPadding(sha256(new TextEncoder().encode(codeVerifier)));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Constant-time comparison to prevent timing attacks
|
|
124
|
+
*/
|
|
125
|
+
function secureCompare(a, b) {
|
|
126
|
+
if (a.length !== b.length) return false;
|
|
127
|
+
const aBytes = new TextEncoder().encode(a);
|
|
128
|
+
const bBytes = new TextEncoder().encode(b);
|
|
129
|
+
let result = 0;
|
|
130
|
+
for (let i = 0; i < aBytes.length; i++) result |= aBytes[i] ^ bBytes[i];
|
|
131
|
+
return result === 0;
|
|
132
|
+
}
|
|
133
|
+
const ALGORITHM = "AES-GCM";
|
|
134
|
+
const IV_BYTES = 12;
|
|
135
|
+
/**
|
|
136
|
+
* Derive an encryption key from the auth secret
|
|
137
|
+
*/
|
|
138
|
+
async function deriveKey(secret) {
|
|
139
|
+
const decoded = decodeBase64urlIgnorePadding(secret);
|
|
140
|
+
const buffer = new Uint8Array(decoded).buffer;
|
|
141
|
+
const keyMaterial = await crypto.subtle.importKey("raw", buffer, "PBKDF2", false, ["deriveKey"]);
|
|
142
|
+
return crypto.subtle.deriveKey({
|
|
143
|
+
name: "PBKDF2",
|
|
144
|
+
salt: new TextEncoder().encode("emdash-auth-v1"),
|
|
145
|
+
iterations: 1e5,
|
|
146
|
+
hash: "SHA-256"
|
|
147
|
+
}, keyMaterial, {
|
|
148
|
+
name: ALGORITHM,
|
|
149
|
+
length: 256
|
|
150
|
+
}, false, ["encrypt", "decrypt"]);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Encrypt a value using AES-GCM
|
|
154
|
+
*/
|
|
155
|
+
async function encrypt(plaintext, secret) {
|
|
156
|
+
const key = await deriveKey(secret);
|
|
157
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
158
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
159
|
+
const ciphertext = await crypto.subtle.encrypt({
|
|
160
|
+
name: ALGORITHM,
|
|
161
|
+
iv
|
|
162
|
+
}, key, encoded);
|
|
163
|
+
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
|
|
164
|
+
combined.set(iv);
|
|
165
|
+
combined.set(new Uint8Array(ciphertext), iv.length);
|
|
166
|
+
return encodeBase64urlNoPadding(combined);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Decrypt a value encrypted with encrypt()
|
|
170
|
+
*/
|
|
171
|
+
async function decrypt(encrypted, secret) {
|
|
172
|
+
const key = await deriveKey(secret);
|
|
173
|
+
const combined = decodeBase64urlIgnorePadding(encrypted);
|
|
174
|
+
const iv = combined.slice(0, IV_BYTES);
|
|
175
|
+
const ciphertext = combined.slice(IV_BYTES);
|
|
176
|
+
const decrypted = await crypto.subtle.decrypt({
|
|
177
|
+
name: ALGORITHM,
|
|
178
|
+
iv
|
|
179
|
+
}, key, ciphertext);
|
|
180
|
+
return new TextDecoder().decode(decrypted);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/passkey/register.ts
|
|
185
|
+
/**
|
|
186
|
+
* Passkey registration (credential creation)
|
|
187
|
+
*
|
|
188
|
+
* Based on oslo webauthn documentation:
|
|
189
|
+
* https://webauthn.oslojs.dev/examples/registration
|
|
190
|
+
*/
|
|
191
|
+
const CHALLENGE_TTL$1 = 300 * 1e3;
|
|
192
|
+
/**
|
|
193
|
+
* Generate registration options for creating a new passkey
|
|
194
|
+
*/
|
|
195
|
+
async function generateRegistrationOptions(config, user, existingCredentials, challengeStore) {
|
|
196
|
+
const challenge = generateToken();
|
|
197
|
+
await challengeStore.set(challenge, {
|
|
198
|
+
type: "registration",
|
|
199
|
+
userId: user.id,
|
|
200
|
+
expiresAt: Date.now() + CHALLENGE_TTL$1
|
|
201
|
+
});
|
|
202
|
+
const userIdEncoded = encodeBase64urlNoPadding(new TextEncoder().encode(user.id));
|
|
203
|
+
return {
|
|
204
|
+
challenge,
|
|
205
|
+
rp: {
|
|
206
|
+
name: config.rpName,
|
|
207
|
+
id: config.rpId
|
|
208
|
+
},
|
|
209
|
+
user: {
|
|
210
|
+
id: userIdEncoded,
|
|
211
|
+
name: user.email,
|
|
212
|
+
displayName: user.name || user.email
|
|
213
|
+
},
|
|
214
|
+
pubKeyCredParams: [{
|
|
215
|
+
type: "public-key",
|
|
216
|
+
alg: coseAlgorithmES256
|
|
217
|
+
}, {
|
|
218
|
+
type: "public-key",
|
|
219
|
+
alg: coseAlgorithmRS256
|
|
220
|
+
}],
|
|
221
|
+
timeout: 6e4,
|
|
222
|
+
attestation: "none",
|
|
223
|
+
authenticatorSelection: {
|
|
224
|
+
residentKey: "preferred",
|
|
225
|
+
userVerification: "preferred"
|
|
226
|
+
},
|
|
227
|
+
excludeCredentials: existingCredentials.map((cred) => ({
|
|
228
|
+
type: "public-key",
|
|
229
|
+
id: cred.id,
|
|
230
|
+
transports: cred.transports
|
|
231
|
+
}))
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Verify a registration response and extract credential data
|
|
236
|
+
*/
|
|
237
|
+
async function verifyRegistrationResponse(config, response, challengeStore) {
|
|
238
|
+
const clientDataJSON = decodeBase64urlIgnorePadding(response.response.clientDataJSON);
|
|
239
|
+
const attestationObject = decodeBase64urlIgnorePadding(response.response.attestationObject);
|
|
240
|
+
const clientData = parseClientDataJSON(clientDataJSON);
|
|
241
|
+
if (clientData.type !== ClientDataType.Create) throw new Error("Invalid client data type");
|
|
242
|
+
const challengeString = encodeBase64urlNoPadding(clientData.challenge);
|
|
243
|
+
const challengeData = await challengeStore.get(challengeString);
|
|
244
|
+
if (!challengeData) throw new Error("Challenge not found or expired");
|
|
245
|
+
if (challengeData.type !== "registration") throw new Error("Invalid challenge type");
|
|
246
|
+
if (challengeData.expiresAt < Date.now()) {
|
|
247
|
+
await challengeStore.delete(challengeString);
|
|
248
|
+
throw new Error("Challenge expired");
|
|
249
|
+
}
|
|
250
|
+
await challengeStore.delete(challengeString);
|
|
251
|
+
if (clientData.origin !== config.origin) throw new Error(`Invalid origin: expected ${config.origin}, got ${clientData.origin}`);
|
|
252
|
+
const attestation = parseAttestationObject(attestationObject);
|
|
253
|
+
if (attestation.attestationStatement.format !== AttestationStatementFormat.None) {}
|
|
254
|
+
const { authenticatorData } = attestation;
|
|
255
|
+
if (!authenticatorData.verifyRelyingPartyIdHash(config.rpId)) throw new Error("Invalid RP ID hash");
|
|
256
|
+
if (!authenticatorData.userPresent) throw new Error("User presence not verified");
|
|
257
|
+
if (!authenticatorData.credential) throw new Error("No credential data in attestation");
|
|
258
|
+
const { credential } = authenticatorData;
|
|
259
|
+
const algorithm = credential.publicKey.algorithm();
|
|
260
|
+
let encodedPublicKey;
|
|
261
|
+
if (algorithm === coseAlgorithmES256) {
|
|
262
|
+
if (credential.publicKey.type() !== COSEKeyType.EC2) throw new Error("Expected EC2 key type for ES256");
|
|
263
|
+
const cosePublicKey = credential.publicKey.ec2();
|
|
264
|
+
if (cosePublicKey.curve !== coseEllipticCurveP256) throw new Error("Expected P-256 curve for ES256");
|
|
265
|
+
encodedPublicKey = new ECDSAPublicKey(p256, cosePublicKey.x, cosePublicKey.y).encodeSEC1Uncompressed();
|
|
266
|
+
} else if (algorithm === coseAlgorithmRS256) throw new Error("RS256 not yet supported - please use ES256");
|
|
267
|
+
else throw new Error(`Unsupported algorithm: ${algorithm}`);
|
|
268
|
+
return {
|
|
269
|
+
credentialId: response.id,
|
|
270
|
+
publicKey: encodedPublicKey,
|
|
271
|
+
counter: authenticatorData.signatureCounter,
|
|
272
|
+
deviceType: "singleDevice",
|
|
273
|
+
backedUp: false,
|
|
274
|
+
transports: response.response.transports ?? []
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Register a new passkey for a user
|
|
279
|
+
*/
|
|
280
|
+
async function registerPasskey(adapter, userId, verified, name) {
|
|
281
|
+
if (await adapter.countCredentialsByUserId(userId) >= 10) throw new Error("Maximum number of passkeys reached (10)");
|
|
282
|
+
if (await adapter.getCredentialById(verified.credentialId)) throw new Error("Credential already registered");
|
|
283
|
+
const newCredential = {
|
|
284
|
+
id: verified.credentialId,
|
|
285
|
+
userId,
|
|
286
|
+
publicKey: verified.publicKey,
|
|
287
|
+
counter: verified.counter,
|
|
288
|
+
deviceType: verified.deviceType,
|
|
289
|
+
backedUp: verified.backedUp,
|
|
290
|
+
transports: verified.transports,
|
|
291
|
+
name
|
|
292
|
+
};
|
|
293
|
+
return adapter.createCredential(newCredential);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/passkey/authenticate.ts
|
|
298
|
+
/**
|
|
299
|
+
* Passkey authentication (credential assertion)
|
|
300
|
+
*
|
|
301
|
+
* Based on oslo webauthn documentation:
|
|
302
|
+
* https://webauthn.oslojs.dev/examples/authentication
|
|
303
|
+
*/
|
|
304
|
+
const CHALLENGE_TTL = 300 * 1e3;
|
|
305
|
+
/**
|
|
306
|
+
* Generate authentication options for signing in with a passkey
|
|
307
|
+
*/
|
|
308
|
+
async function generateAuthenticationOptions(config, credentials, challengeStore) {
|
|
309
|
+
const challenge = generateToken();
|
|
310
|
+
await challengeStore.set(challenge, {
|
|
311
|
+
type: "authentication",
|
|
312
|
+
expiresAt: Date.now() + CHALLENGE_TTL
|
|
313
|
+
});
|
|
314
|
+
return {
|
|
315
|
+
challenge,
|
|
316
|
+
rpId: config.rpId,
|
|
317
|
+
timeout: 6e4,
|
|
318
|
+
userVerification: "preferred",
|
|
319
|
+
allowCredentials: credentials.length > 0 ? credentials.map((cred) => ({
|
|
320
|
+
type: "public-key",
|
|
321
|
+
id: cred.id,
|
|
322
|
+
transports: cred.transports
|
|
323
|
+
})) : void 0
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Verify an authentication response
|
|
328
|
+
*/
|
|
329
|
+
async function verifyAuthenticationResponse(config, response, credential, challengeStore) {
|
|
330
|
+
const clientDataJSON = decodeBase64urlIgnorePadding(response.response.clientDataJSON);
|
|
331
|
+
const authenticatorData = decodeBase64urlIgnorePadding(response.response.authenticatorData);
|
|
332
|
+
const signature = decodeBase64urlIgnorePadding(response.response.signature);
|
|
333
|
+
const clientData = parseClientDataJSON(clientDataJSON);
|
|
334
|
+
if (clientData.type !== ClientDataType.Get) throw new Error("Invalid client data type");
|
|
335
|
+
const challengeString = encodeBase64urlNoPadding(clientData.challenge);
|
|
336
|
+
const challengeData = await challengeStore.get(challengeString);
|
|
337
|
+
if (!challengeData) throw new Error("Challenge not found or expired");
|
|
338
|
+
if (challengeData.type !== "authentication") throw new Error("Invalid challenge type");
|
|
339
|
+
if (challengeData.expiresAt < Date.now()) {
|
|
340
|
+
await challengeStore.delete(challengeString);
|
|
341
|
+
throw new Error("Challenge expired");
|
|
342
|
+
}
|
|
343
|
+
await challengeStore.delete(challengeString);
|
|
344
|
+
if (clientData.origin !== config.origin) throw new Error(`Invalid origin: expected ${config.origin}, got ${clientData.origin}`);
|
|
345
|
+
const authData = parseAuthenticatorData(authenticatorData);
|
|
346
|
+
if (!authData.verifyRelyingPartyIdHash(config.rpId)) throw new Error("Invalid RP ID hash");
|
|
347
|
+
if (!authData.userPresent) throw new Error("User presence not verified");
|
|
348
|
+
if (authData.signatureCounter !== 0 && authData.signatureCounter <= credential.counter) throw new Error("Invalid signature counter - possible cloned authenticator");
|
|
349
|
+
const signatureMessage = createAssertionSignatureMessage(authenticatorData, clientDataJSON);
|
|
350
|
+
const ecdsaPublicKey = decodeSEC1PublicKey(p256, credential.publicKey instanceof Uint8Array ? credential.publicKey : new Uint8Array(credential.publicKey));
|
|
351
|
+
const ecdsaSignature = decodePKIXECDSASignature(signature);
|
|
352
|
+
if (!verifyECDSASignature(ecdsaPublicKey, sha256(signatureMessage), ecdsaSignature)) throw new Error("Invalid signature");
|
|
353
|
+
return {
|
|
354
|
+
credentialId: response.id,
|
|
355
|
+
newCounter: authData.signatureCounter
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Authenticate a user with a passkey
|
|
360
|
+
*/
|
|
361
|
+
async function authenticateWithPasskey(config, adapter, response, challengeStore) {
|
|
362
|
+
const credential = await adapter.getCredentialById(response.id);
|
|
363
|
+
if (!credential) throw new Error("Credential not found");
|
|
364
|
+
const verified = await verifyAuthenticationResponse(config, response, credential, challengeStore);
|
|
365
|
+
await adapter.updateCredentialCounter(verified.credentialId, verified.newCounter);
|
|
366
|
+
const user = await adapter.getUserById(credential.userId);
|
|
367
|
+
if (!user) throw new Error("User not found");
|
|
368
|
+
return user;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
//#endregion
|
|
372
|
+
export { hasScope as _, registerPasskey as a, secureCompare as b, VALID_SCOPES as c, encrypt as d, generateAuthSecret as f, generateTokenWithHash as g, generateToken as h, generateRegistrationOptions as i, computeS256Challenge as l, generateSessionId as m, generateAuthenticationOptions as n, verifyRegistrationResponse as o, generatePrefixedToken as p, verifyAuthenticationResponse as r, TOKEN_PREFIXES as s, authenticateWithPasskey as t, decrypt as u, hashPrefixedToken as v, validateScopes as x, hashToken as y };
|
|
373
|
+
//# sourceMappingURL=authenticate-j5GayLXB.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authenticate-j5GayLXB.mjs","names":["CHALLENGE_TTL"],"sources":["../src/tokens.ts","../src/passkey/register.ts","../src/passkey/authenticate.ts"],"sourcesContent":["/**\n * Secure token utilities\n *\n * Crypto via Oslo.js (@oslojs/crypto). Base64url via @oslojs/encoding.\n *\n * Tokens are opaque random values. We store only the SHA-256 hash in the database.\n */\n\nimport { sha256 } from \"@oslojs/crypto/sha2\";\nimport { encodeBase64urlNoPadding, decodeBase64urlIgnorePadding } from \"@oslojs/encoding\";\n\nconst TOKEN_BYTES = 32; // 256 bits of entropy\n\n// ---------------------------------------------------------------------------\n// API Token Prefixes\n// ---------------------------------------------------------------------------\n\n/** Valid API token prefixes */\nexport const TOKEN_PREFIXES = {\n\tPAT: \"ec_pat_\",\n\tOAUTH_ACCESS: \"ec_oat_\",\n\tOAUTH_REFRESH: \"ec_ort_\",\n} as const;\n\n// ---------------------------------------------------------------------------\n// Scopes\n// ---------------------------------------------------------------------------\n\n/** All valid API token scopes */\nexport const VALID_SCOPES = [\n\t\"content:read\",\n\t\"content:write\",\n\t\"media:read\",\n\t\"media:write\",\n\t\"schema:read\",\n\t\"schema:write\",\n\t\"admin\",\n] as const;\n\nexport type ApiTokenScope = (typeof VALID_SCOPES)[number];\n\n/**\n * Validate that scopes are all valid.\n * Returns the invalid scopes, or empty array if all valid.\n */\nexport function validateScopes(scopes: string[]): string[] {\n\tconst validSet = new Set<string>(VALID_SCOPES);\n\treturn scopes.filter((s) => !validSet.has(s));\n}\n\n/**\n * Check if a set of scopes includes a required scope.\n * The `admin` scope grants access to everything.\n */\nexport function hasScope(scopes: string[], required: string): boolean {\n\tif (scopes.includes(\"admin\")) return true;\n\treturn scopes.includes(required);\n}\n\n/**\n * Generate a cryptographically secure random token\n * Returns base64url-encoded string (URL-safe)\n */\nexport function generateToken(): string {\n\tconst bytes = new Uint8Array(TOKEN_BYTES);\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64urlNoPadding(bytes);\n}\n\n/**\n * Hash a token for storage\n * We never store raw tokens - only their SHA-256 hash\n */\nexport function hashToken(token: string): string {\n\tconst bytes = decodeBase64urlIgnorePadding(token);\n\tconst hash = sha256(bytes);\n\treturn encodeBase64urlNoPadding(hash);\n}\n\n/**\n * Generate a token and its hash together\n */\nexport function generateTokenWithHash(): { token: string; hash: string } {\n\tconst token = generateToken();\n\tconst hash = hashToken(token);\n\treturn { token, hash };\n}\n\n/**\n * Generate a session ID (shorter, for cookie storage)\n */\nexport function generateSessionId(): string {\n\tconst bytes = new Uint8Array(20); // 160 bits\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64urlNoPadding(bytes);\n}\n\n/**\n * Generate an auth secret for configuration\n */\nexport function generateAuthSecret(): string {\n\tconst bytes = new Uint8Array(32);\n\tcrypto.getRandomValues(bytes);\n\treturn encodeBase64urlNoPadding(bytes);\n}\n\n// ---------------------------------------------------------------------------\n// Prefixed API tokens (ec_pat_, ec_oat_, ec_ort_)\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a prefixed API token and its hash.\n * Returns the raw token (shown once to the user), the hash (stored server-side),\n * and a display prefix (for identification in UIs/logs).\n *\n * Uses oslo/crypto for SHA-256 hashing.\n */\nexport function generatePrefixedToken(prefix: string): {\n\traw: string;\n\thash: string;\n\tprefix: string;\n} {\n\tconst bytes = new Uint8Array(TOKEN_BYTES);\n\tcrypto.getRandomValues(bytes);\n\n\tconst encoded = encodeBase64urlNoPadding(bytes);\n\tconst raw = `${prefix}${encoded}`;\n\tconst hash = hashPrefixedToken(raw);\n\n\t// First few chars for identification in UIs\n\tconst displayPrefix = raw.slice(0, prefix.length + 4);\n\n\treturn { raw, hash, prefix: displayPrefix };\n}\n\n/**\n * Hash a prefixed API token for storage/lookup.\n * Hashes the full prefixed token string via SHA-256, returns base64url (no padding).\n */\nexport function hashPrefixedToken(token: string): string {\n\tconst bytes = new TextEncoder().encode(token);\n\tconst hash = sha256(bytes);\n\treturn encodeBase64urlNoPadding(hash);\n}\n\n// ---------------------------------------------------------------------------\n// PKCE (RFC 7636) — server-side verification\n// ---------------------------------------------------------------------------\n\n/**\n * Compute an S256 PKCE code challenge from a code verifier.\n * Used server-side to verify that code_verifier matches the stored code_challenge.\n *\n * Equivalent to: BASE64URL(SHA256(ASCII(code_verifier)))\n */\nexport function computeS256Challenge(codeVerifier: string): string {\n\tconst hash = sha256(new TextEncoder().encode(codeVerifier));\n\treturn encodeBase64urlNoPadding(hash);\n}\n\n/**\n * Constant-time comparison to prevent timing attacks\n */\nexport function secureCompare(a: string, b: string): boolean {\n\tif (a.length !== b.length) return false;\n\n\tconst aBytes = new TextEncoder().encode(a);\n\tconst bBytes = new TextEncoder().encode(b);\n\n\tlet result = 0;\n\tfor (let i = 0; i < aBytes.length; i++) {\n\t\tresult |= aBytes[i]! ^ bBytes[i]!;\n\t}\n\treturn result === 0;\n}\n\n// ============================================================================\n// Encryption utilities (for storing OAuth secrets)\n// ============================================================================\n\nconst ALGORITHM = \"AES-GCM\";\nconst IV_BYTES = 12;\n\n/**\n * Derive an encryption key from the auth secret\n */\nasync function deriveKey(secret: string): Promise<CryptoKey> {\n\tconst decoded = decodeBase64urlIgnorePadding(secret);\n\t// Create a new ArrayBuffer to ensure compatibility with crypto.subtle\n\tconst buffer = new Uint8Array(decoded).buffer;\n\tconst keyMaterial = await crypto.subtle.importKey(\"raw\", buffer, \"PBKDF2\", false, [\"deriveKey\"]);\n\n\treturn crypto.subtle.deriveKey(\n\t\t{\n\t\t\tname: \"PBKDF2\",\n\t\t\tsalt: new TextEncoder().encode(\"emdash-auth-v1\"),\n\t\t\titerations: 100000,\n\t\t\thash: \"SHA-256\",\n\t\t},\n\t\tkeyMaterial,\n\t\t{ name: ALGORITHM, length: 256 },\n\t\tfalse,\n\t\t[\"encrypt\", \"decrypt\"],\n\t);\n}\n\n/**\n * Encrypt a value using AES-GCM\n */\nexport async function encrypt(plaintext: string, secret: string): Promise<string> {\n\tconst key = await deriveKey(secret);\n\tconst iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));\n\tconst encoded = new TextEncoder().encode(plaintext);\n\n\tconst ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv }, key, encoded);\n\n\t// Prepend IV to ciphertext\n\tconst combined = new Uint8Array(iv.length + ciphertext.byteLength);\n\tcombined.set(iv);\n\tcombined.set(new Uint8Array(ciphertext), iv.length);\n\n\treturn encodeBase64urlNoPadding(combined);\n}\n\n/**\n * Decrypt a value encrypted with encrypt()\n */\nexport async function decrypt(encrypted: string, secret: string): Promise<string> {\n\tconst key = await deriveKey(secret);\n\tconst combined = decodeBase64urlIgnorePadding(encrypted);\n\n\tconst iv = combined.slice(0, IV_BYTES);\n\tconst ciphertext = combined.slice(IV_BYTES);\n\n\tconst decrypted = await crypto.subtle.decrypt({ name: ALGORITHM, iv }, key, ciphertext);\n\n\treturn new TextDecoder().decode(decrypted);\n}\n","/**\n * Passkey registration (credential creation)\n *\n * Based on oslo webauthn documentation:\n * https://webauthn.oslojs.dev/examples/registration\n */\n\nimport { ECDSAPublicKey, p256 } from \"@oslojs/crypto/ecdsa\";\nimport { encodeBase64urlNoPadding, decodeBase64urlIgnorePadding } from \"@oslojs/encoding\";\nimport {\n\tparseAttestationObject,\n\tparseClientDataJSON,\n\tcoseAlgorithmES256,\n\tcoseAlgorithmRS256,\n\tcoseEllipticCurveP256,\n\tClientDataType,\n\tAttestationStatementFormat,\n\tCOSEKeyType,\n} from \"@oslojs/webauthn\";\n\nimport { generateToken } from \"../tokens.js\";\nimport type { Credential, NewCredential, AuthAdapter, User, DeviceType } from \"../types.js\";\nimport type {\n\tRegistrationOptions,\n\tRegistrationResponse,\n\tVerifiedRegistration,\n\tChallengeStore,\n\tPasskeyConfig,\n} from \"./types.js\";\n\nconst CHALLENGE_TTL = 5 * 60 * 1000; // 5 minutes\n\nexport type { PasskeyConfig };\n\n/**\n * Generate registration options for creating a new passkey\n */\nexport async function generateRegistrationOptions(\n\tconfig: PasskeyConfig,\n\tuser: Pick<User, \"id\" | \"email\" | \"name\">,\n\texistingCredentials: Credential[],\n\tchallengeStore: ChallengeStore,\n): Promise<RegistrationOptions> {\n\tconst challenge = generateToken();\n\n\t// Store challenge for verification\n\tawait challengeStore.set(challenge, {\n\t\ttype: \"registration\",\n\t\tuserId: user.id,\n\t\texpiresAt: Date.now() + CHALLENGE_TTL,\n\t});\n\n\t// Encode user ID as base64url\n\tconst userIdBytes = new TextEncoder().encode(user.id);\n\tconst userIdEncoded = encodeBase64urlNoPadding(userIdBytes);\n\n\treturn {\n\t\tchallenge,\n\t\trp: {\n\t\t\tname: config.rpName,\n\t\t\tid: config.rpId,\n\t\t},\n\t\tuser: {\n\t\t\tid: userIdEncoded,\n\t\t\tname: user.email,\n\t\t\tdisplayName: user.name || user.email,\n\t\t},\n\t\tpubKeyCredParams: [\n\t\t\t{ type: \"public-key\", alg: coseAlgorithmES256 }, // ES256 (-7)\n\t\t\t{ type: \"public-key\", alg: coseAlgorithmRS256 }, // RS256 (-257)\n\t\t],\n\t\ttimeout: 60000,\n\t\tattestation: \"none\", // We don't need attestation for our use case\n\t\tauthenticatorSelection: {\n\t\t\tresidentKey: \"preferred\", // Allow discoverable credentials\n\t\t\tuserVerification: \"preferred\",\n\t\t},\n\t\texcludeCredentials: existingCredentials.map((cred) => ({\n\t\t\ttype: \"public-key\" as const,\n\t\t\tid: cred.id,\n\t\t\ttransports: cred.transports,\n\t\t})),\n\t};\n}\n\n/**\n * Verify a registration response and extract credential data\n */\nexport async function verifyRegistrationResponse(\n\tconfig: PasskeyConfig,\n\tresponse: RegistrationResponse,\n\tchallengeStore: ChallengeStore,\n): Promise<VerifiedRegistration> {\n\t// Decode the response\n\tconst clientDataJSON = decodeBase64urlIgnorePadding(response.response.clientDataJSON);\n\tconst attestationObject = decodeBase64urlIgnorePadding(response.response.attestationObject);\n\n\t// Parse client data\n\tconst clientData = parseClientDataJSON(clientDataJSON);\n\n\t// Verify client data\n\tif (clientData.type !== ClientDataType.Create) {\n\t\tthrow new Error(\"Invalid client data type\");\n\t}\n\n\t// Verify challenge - convert Uint8Array back to base64url string (no padding, matching stored format)\n\tconst challengeString = encodeBase64urlNoPadding(clientData.challenge);\n\tconst challengeData = await challengeStore.get(challengeString);\n\tif (!challengeData) {\n\t\tthrow new Error(\"Challenge not found or expired\");\n\t}\n\tif (challengeData.type !== \"registration\") {\n\t\tthrow new Error(\"Invalid challenge type\");\n\t}\n\tif (challengeData.expiresAt < Date.now()) {\n\t\tawait challengeStore.delete(challengeString);\n\t\tthrow new Error(\"Challenge expired\");\n\t}\n\n\t// Delete challenge (single-use)\n\tawait challengeStore.delete(challengeString);\n\n\t// Verify origin\n\tif (clientData.origin !== config.origin) {\n\t\tthrow new Error(`Invalid origin: expected ${config.origin}, got ${clientData.origin}`);\n\t}\n\n\t// Parse attestation object\n\tconst attestation = parseAttestationObject(attestationObject);\n\n\t// We only support 'none' attestation for simplicity\n\tif (attestation.attestationStatement.format !== AttestationStatementFormat.None) {\n\t\t// For other formats, we'd need to verify the attestation statement\n\t\t// For now, we just ignore it and trust the credential\n\t}\n\n\tconst { authenticatorData } = attestation;\n\n\t// Verify RP ID hash\n\tif (!authenticatorData.verifyRelyingPartyIdHash(config.rpId)) {\n\t\tthrow new Error(\"Invalid RP ID hash\");\n\t}\n\n\t// Verify flags\n\tif (!authenticatorData.userPresent) {\n\t\tthrow new Error(\"User presence not verified\");\n\t}\n\n\t// Extract credential data\n\tif (!authenticatorData.credential) {\n\t\tthrow new Error(\"No credential data in attestation\");\n\t}\n\n\tconst { credential } = authenticatorData;\n\n\t// Verify algorithm is supported and encode public key\n\t// Currently only supporting ES256 (ECDSA with P-256)\n\tconst algorithm = credential.publicKey.algorithm();\n\tlet encodedPublicKey: Uint8Array;\n\n\tif (algorithm === coseAlgorithmES256) {\n\t\t// Verify it's EC2 key type\n\t\tif (credential.publicKey.type() !== COSEKeyType.EC2) {\n\t\t\tthrow new Error(\"Expected EC2 key type for ES256\");\n\t\t}\n\t\tconst cosePublicKey = credential.publicKey.ec2();\n\t\tif (cosePublicKey.curve !== coseEllipticCurveP256) {\n\t\t\tthrow new Error(\"Expected P-256 curve for ES256\");\n\t\t}\n\t\t// Encode as SEC1 uncompressed format for storage\n\t\tencodedPublicKey = new ECDSAPublicKey(\n\t\t\tp256,\n\t\t\tcosePublicKey.x,\n\t\t\tcosePublicKey.y,\n\t\t).encodeSEC1Uncompressed();\n\t} else if (algorithm === coseAlgorithmRS256) {\n\t\t// RSA is less common for passkeys, skip for now\n\t\tthrow new Error(\"RS256 not yet supported - please use ES256\");\n\t} else {\n\t\tthrow new Error(`Unsupported algorithm: ${algorithm}`);\n\t}\n\n\t// Determine device type and backup status\n\t// Note: oslo webauthn doesn't expose backup flags, so we default to singleDevice\n\t// In practice, most modern passkeys are multi-device (e.g., iCloud Keychain, Google Password Manager)\n\tconst deviceType: DeviceType = \"singleDevice\";\n\tconst backedUp = false;\n\n\treturn {\n\t\tcredentialId: response.id,\n\t\tpublicKey: encodedPublicKey,\n\t\tcounter: authenticatorData.signatureCounter,\n\t\tdeviceType,\n\t\tbackedUp,\n\t\ttransports: response.response.transports ?? [],\n\t};\n}\n\n/**\n * Register a new passkey for a user\n */\nexport async function registerPasskey(\n\tadapter: AuthAdapter,\n\tuserId: string,\n\tverified: VerifiedRegistration,\n\tname?: string,\n): Promise<Credential> {\n\t// Check credential limit\n\tconst count = await adapter.countCredentialsByUserId(userId);\n\tif (count >= 10) {\n\t\tthrow new Error(\"Maximum number of passkeys reached (10)\");\n\t}\n\n\t// Check if credential already exists\n\tconst existing = await adapter.getCredentialById(verified.credentialId);\n\tif (existing) {\n\t\tthrow new Error(\"Credential already registered\");\n\t}\n\n\tconst newCredential: NewCredential = {\n\t\tid: verified.credentialId,\n\t\tuserId,\n\t\tpublicKey: verified.publicKey,\n\t\tcounter: verified.counter,\n\t\tdeviceType: verified.deviceType,\n\t\tbackedUp: verified.backedUp,\n\t\ttransports: verified.transports,\n\t\tname,\n\t};\n\n\treturn adapter.createCredential(newCredential);\n}\n","/**\n * Passkey authentication (credential assertion)\n *\n * Based on oslo webauthn documentation:\n * https://webauthn.oslojs.dev/examples/authentication\n */\n\nimport {\n\tverifyECDSASignature,\n\tp256,\n\tdecodeSEC1PublicKey,\n\tdecodePKIXECDSASignature,\n} from \"@oslojs/crypto/ecdsa\";\nimport { sha256 } from \"@oslojs/crypto/sha2\";\nimport { encodeBase64urlNoPadding, decodeBase64urlIgnorePadding } from \"@oslojs/encoding\";\nimport {\n\tparseAuthenticatorData,\n\tparseClientDataJSON,\n\tClientDataType,\n\tcreateAssertionSignatureMessage,\n} from \"@oslojs/webauthn\";\n\nimport { generateToken } from \"../tokens.js\";\nimport type { Credential, AuthAdapter, User } from \"../types.js\";\nimport type {\n\tAuthenticationOptions,\n\tAuthenticationResponse,\n\tVerifiedAuthentication,\n\tChallengeStore,\n\tPasskeyConfig,\n} from \"./types.js\";\n\nconst CHALLENGE_TTL = 5 * 60 * 1000; // 5 minutes\n\n/**\n * Generate authentication options for signing in with a passkey\n */\nexport async function generateAuthenticationOptions(\n\tconfig: PasskeyConfig,\n\tcredentials: Credential[],\n\tchallengeStore: ChallengeStore,\n): Promise<AuthenticationOptions> {\n\tconst challenge = generateToken();\n\n\t// Store challenge for verification\n\tawait challengeStore.set(challenge, {\n\t\ttype: \"authentication\",\n\t\texpiresAt: Date.now() + CHALLENGE_TTL,\n\t});\n\n\treturn {\n\t\tchallenge,\n\t\trpId: config.rpId,\n\t\ttimeout: 60000,\n\t\tuserVerification: \"preferred\",\n\t\tallowCredentials:\n\t\t\tcredentials.length > 0\n\t\t\t\t? credentials.map((cred) => ({\n\t\t\t\t\t\ttype: \"public-key\" as const,\n\t\t\t\t\t\tid: cred.id,\n\t\t\t\t\t\ttransports: cred.transports,\n\t\t\t\t\t}))\n\t\t\t\t: undefined, // Empty = allow any discoverable credential\n\t};\n}\n\n/**\n * Verify an authentication response\n */\nexport async function verifyAuthenticationResponse(\n\tconfig: PasskeyConfig,\n\tresponse: AuthenticationResponse,\n\tcredential: Credential,\n\tchallengeStore: ChallengeStore,\n): Promise<VerifiedAuthentication> {\n\t// Decode the response\n\tconst clientDataJSON = decodeBase64urlIgnorePadding(response.response.clientDataJSON);\n\tconst authenticatorData = decodeBase64urlIgnorePadding(response.response.authenticatorData);\n\tconst signature = decodeBase64urlIgnorePadding(response.response.signature);\n\n\t// Parse client data\n\tconst clientData = parseClientDataJSON(clientDataJSON);\n\n\t// Verify client data type\n\tif (clientData.type !== ClientDataType.Get) {\n\t\tthrow new Error(\"Invalid client data type\");\n\t}\n\n\t// Verify challenge - convert Uint8Array back to base64url string (no padding, matching stored format)\n\tconst challengeString = encodeBase64urlNoPadding(clientData.challenge);\n\tconst challengeData = await challengeStore.get(challengeString);\n\tif (!challengeData) {\n\t\tthrow new Error(\"Challenge not found or expired\");\n\t}\n\tif (challengeData.type !== \"authentication\") {\n\t\tthrow new Error(\"Invalid challenge type\");\n\t}\n\tif (challengeData.expiresAt < Date.now()) {\n\t\tawait challengeStore.delete(challengeString);\n\t\tthrow new Error(\"Challenge expired\");\n\t}\n\n\t// Delete challenge (single-use)\n\tawait challengeStore.delete(challengeString);\n\n\t// Verify origin\n\tif (clientData.origin !== config.origin) {\n\t\tthrow new Error(`Invalid origin: expected ${config.origin}, got ${clientData.origin}`);\n\t}\n\n\t// Parse authenticator data\n\tconst authData = parseAuthenticatorData(authenticatorData);\n\n\t// Verify RP ID hash\n\tif (!authData.verifyRelyingPartyIdHash(config.rpId)) {\n\t\tthrow new Error(\"Invalid RP ID hash\");\n\t}\n\n\t// Verify flags\n\tif (!authData.userPresent) {\n\t\tthrow new Error(\"User presence not verified\");\n\t}\n\n\t// Verify counter (prevent replay attacks)\n\tif (authData.signatureCounter !== 0 && authData.signatureCounter <= credential.counter) {\n\t\tthrow new Error(\"Invalid signature counter - possible cloned authenticator\");\n\t}\n\n\t// Create the message that was signed\n\tconst signatureMessage = createAssertionSignatureMessage(authenticatorData, clientDataJSON);\n\n\t// Ensure public key is a Uint8Array (may come as Buffer from some DB drivers)\n\tconst publicKeyBytes =\n\t\tcredential.publicKey instanceof Uint8Array\n\t\t\t? credential.publicKey\n\t\t\t: new Uint8Array(credential.publicKey);\n\n\t// Decode the stored SEC1-encoded public key and verify signature\n\t// The signature from WebAuthn is DER-encoded (PKIX format)\n\tconst ecdsaPublicKey = decodeSEC1PublicKey(p256, publicKeyBytes);\n\tconst ecdsaSignature = decodePKIXECDSASignature(signature);\n\tconst hash = sha256(signatureMessage);\n\tconst signatureValid = verifyECDSASignature(ecdsaPublicKey, hash, ecdsaSignature);\n\n\tif (!signatureValid) {\n\t\tthrow new Error(\"Invalid signature\");\n\t}\n\n\treturn {\n\t\tcredentialId: response.id,\n\t\tnewCounter: authData.signatureCounter,\n\t};\n}\n\n/**\n * Authenticate a user with a passkey\n */\nexport async function authenticateWithPasskey(\n\tconfig: PasskeyConfig,\n\tadapter: AuthAdapter,\n\tresponse: AuthenticationResponse,\n\tchallengeStore: ChallengeStore,\n): Promise<User> {\n\t// Find the credential\n\tconst credential = await adapter.getCredentialById(response.id);\n\tif (!credential) {\n\t\tthrow new Error(\"Credential not found\");\n\t}\n\n\t// Verify the response\n\tconst verified = await verifyAuthenticationResponse(config, response, credential, challengeStore);\n\n\t// Update counter\n\tawait adapter.updateCredentialCounter(verified.credentialId, verified.newCounter);\n\n\t// Get the user\n\tconst user = await adapter.getUserById(credential.userId);\n\tif (!user) {\n\t\tthrow new Error(\"User not found\");\n\t}\n\n\treturn user;\n}\n"],"mappings":"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"}
|