@robelest/convex-auth 0.0.4-preview.27 → 0.0.4-preview.28
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/README.md +3 -5
- package/dist/bin.js +6488 -1571
- package/dist/browser/index.js +10 -7
- package/dist/browser/locks.js +3 -5
- package/dist/browser/navigation.js +7 -10
- package/dist/browser/runtime.js +35 -33
- package/dist/client/core/types.js +17 -0
- package/dist/client/factors/device.js +26 -19
- package/dist/client/index.js +151 -163
- package/dist/client/runtime/proxy.js +6 -6
- package/dist/client/services/adapters.js +3 -7
- package/dist/client/services/http.js +2 -5
- package/dist/client/services/resolve.js +5 -11
- package/dist/client/services/runtime.js +2 -5
- package/dist/component/_generated/component.d.ts +46 -0
- package/dist/component/index.d.ts +3 -3
- package/dist/component/model.d.ts +25 -25
- package/dist/component/public/identity/sessions.js +38 -1
- package/dist/component/public/identity/tokens.js +81 -3
- package/dist/component/public/identity/verifiers.js +9 -3
- package/dist/component/public.js +3 -3
- package/dist/component/schema.d.ts +320 -320
- package/dist/core/index.d.ts +380 -0
- package/dist/core/index.js +83 -0
- package/dist/otel.d.ts +13 -17
- package/dist/otel.js +39 -49
- package/dist/providers/email.d.ts +2 -2
- package/dist/providers/password.js +8 -16
- package/dist/providers/phone.js +2 -9
- package/dist/server/auth-context.d.ts +204 -0
- package/dist/server/auth-context.js +76 -0
- package/dist/server/auth.d.ts +25 -187
- package/dist/server/auth.js +5 -96
- package/dist/server/componentContext.d.ts +12 -0
- package/dist/server/componentContext.js +1 -0
- package/dist/server/config.js +1 -12
- package/dist/server/constants.js +6 -0
- package/dist/server/contract.d.ts +1 -1
- package/dist/server/core.js +5 -14
- package/dist/server/crypto.js +26 -18
- package/dist/server/db.js +6 -1
- package/dist/server/device.js +88 -78
- package/dist/server/http.d.ts +4 -3
- package/dist/server/http.js +74 -86
- package/dist/server/index.d.ts +2 -1
- package/dist/server/limits.js +22 -15
- package/dist/server/mounts.d.ts +103 -103
- package/dist/server/mutations/account.js +6 -4
- package/dist/server/mutations/invalidate.js +3 -6
- package/dist/server/mutations/oauth.js +86 -88
- package/dist/server/mutations/refresh.js +45 -87
- package/dist/server/mutations/register.js +19 -19
- package/dist/server/mutations/retrieve.js +17 -15
- package/dist/server/mutations/signature.js +9 -13
- package/dist/server/mutations/signin.js +7 -3
- package/dist/server/mutations/signout.js +10 -15
- package/dist/server/mutations/store.js +22 -12
- package/dist/server/mutations/verifier.js +11 -6
- package/dist/server/mutations/verify.js +55 -46
- package/dist/server/oauth/runtime.js +27 -25
- package/dist/server/passkey.js +299 -250
- package/dist/server/prefetch.js +283 -281
- package/dist/server/refresh.js +7 -60
- package/dist/server/runtime.d.ts +82 -206
- package/dist/server/runtime.js +63 -56
- package/dist/server/services/config.js +5 -3
- package/dist/server/services/logger.js +2 -4
- package/dist/server/services/providers.js +2 -4
- package/dist/server/services/refresh.js +2 -4
- package/dist/server/services/resolve.js +15 -14
- package/dist/server/services/signin.js +2 -4
- package/dist/server/sessions.js +32 -33
- package/dist/server/signin.js +177 -142
- package/dist/server/sso/domain.d.ts +20 -68
- package/dist/server/sso/domain.js +444 -413
- package/dist/server/sso/http.js +53 -59
- package/dist/server/sso/oidc.js +94 -80
- package/dist/server/tokens.js +13 -3
- package/dist/server/totp.js +153 -116
- package/dist/server/types.d.ts +2 -2
- package/dist/server/users.js +18 -23
- package/dist/server/utils/cache.js +51 -0
- package/dist/server/utils/dispatch.js +36 -0
- package/dist/server/utils/retry.js +24 -0
- package/dist/server/utils/span.js +32 -0
- package/dist/shared/errors.js +9 -3
- package/dist/shared/log.js +20 -22
- package/package.json +41 -33
package/dist/server/passkey.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { userIdFromIdentitySubject } from "./identity.js";
|
|
2
|
-
import { siteUrlsFromEnv } from "./url.js";
|
|
3
|
-
import { authFlowError } from "../shared/errors.js";
|
|
4
|
-
import { toConvexError } from "./errors.js";
|
|
5
2
|
import { authDb } from "./db.js";
|
|
6
|
-
import { callVerifierSignature } from "./mutations/signature.js";
|
|
7
3
|
import { callSignIn } from "./mutations/signin.js";
|
|
8
4
|
import { callVerifier } from "./mutations/verifier.js";
|
|
5
|
+
import { siteUrlsFromEnv } from "./url.js";
|
|
6
|
+
import { authFlowError } from "../shared/errors.js";
|
|
7
|
+
import { toConvexError } from "./errors.js";
|
|
9
8
|
import { mutatePasskeyInsert, mutatePasskeyUpdateCounter, mutateVerifierDelete, queryPasskeyByCredentialId, queryPasskeysByUserId, queryUserById, queryUserByVerifiedEmail, queryVerifierById } from "./types.js";
|
|
10
9
|
import { ConvexError } from "convex/values";
|
|
11
|
-
import { Effect, Match } from "effect";
|
|
12
10
|
import { sha256 } from "@oslojs/crypto/sha2";
|
|
13
11
|
import { decodeBase64urlIgnorePadding, encodeBase64urlNoPadding } from "@oslojs/encoding";
|
|
14
12
|
import { decodePKIXECDSASignature, decodeSEC1PublicKey, p256, verifyECDSASignature } from "@oslojs/crypto/ecdsa";
|
|
@@ -48,167 +46,204 @@ const requireStringParam = (value, name) => {
|
|
|
48
46
|
};
|
|
49
47
|
const convexError = (code, message) => toConvexError(authFlowError(code, message));
|
|
50
48
|
const asConvexError = (error, code, message) => error instanceof ConvexError ? error : error instanceof Error ? toConvexError(authFlowError(code, error.message || message)) : convexError(code, message);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
49
|
+
function resolveRpOptions(provider) {
|
|
50
|
+
try {
|
|
51
|
+
const configuredSiteUrls = process.env.SITE_URL === void 0 ? null : siteUrlsFromEnv();
|
|
52
|
+
const siteUrl = configuredSiteUrls?.primaryUrl;
|
|
53
|
+
const hasSiteUrl = siteUrl !== void 0 && siteUrl !== "";
|
|
54
|
+
const hasRpId = provider.options.rpId !== void 0;
|
|
55
|
+
if (!hasSiteUrl && !hasRpId) throw convexError("PASSKEY_MISSING_CONFIG", "Passkey provider requires SITE_URL env var (your frontend URL) or explicit rpId / origin in the provider config. CONVEX_SITE_URL cannot be used because WebAuthn RP ID must match the frontend domain.");
|
|
56
|
+
const siteHostname = siteUrl ? new URL(siteUrl).hostname : void 0;
|
|
57
|
+
const defaultOrigin = configuredSiteUrls?.allowedUrls ?? siteUrl;
|
|
58
|
+
return {
|
|
59
|
+
rpName: provider.options.rpName ?? siteHostname ?? "localhost",
|
|
60
|
+
rpId: provider.options.rpId ?? siteHostname ?? "localhost",
|
|
61
|
+
origin: provider.options.origin ?? defaultOrigin ?? "http://localhost",
|
|
62
|
+
attestation: provider.options.attestation ?? "none",
|
|
63
|
+
userVerification: provider.options.userVerification ?? "required",
|
|
64
|
+
residentKey: provider.options.residentKey ?? "preferred",
|
|
65
|
+
authenticatorAttachment: provider.options.authenticatorAttachment,
|
|
66
|
+
algorithms: provider.options.algorithms ?? [coseAlgorithmES256, coseAlgorithmRS256],
|
|
67
|
+
challengeExpirationMs: provider.options.challengeExpirationMs ?? 3e5
|
|
68
|
+
};
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw asConvexError(error, "PASSKEY_MISSING_CONFIG", "Passkey relying party configuration is invalid.");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function verifyClientDataType(clientData, expectedType, label) {
|
|
74
|
+
if (clientData.type !== expectedType) throw convexError("PASSKEY_INVALID_CLIENT_DATA", `Invalid client data type: expected ${label}`);
|
|
75
|
+
return clientData;
|
|
76
|
+
}
|
|
77
|
+
function verifyOrigin(clientData, rp) {
|
|
73
78
|
const allowed = Array.isArray(rp.origin) ? rp.origin : [rp.origin];
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
if (!allowed.includes(clientData.origin)) throw convexError("PASSKEY_INVALID_ORIGIN", `Invalid origin: ${clientData.origin}, expected one of: ${allowed.join(", ")}`);
|
|
80
|
+
return clientData;
|
|
81
|
+
}
|
|
82
|
+
async function verifyAndConsumeChallenge(clientData, ctx, verifierValue) {
|
|
77
83
|
const challengeHash = encodeBase64urlNoPadding(new Uint8Array(sha256(clientData.challenge)));
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
84
|
+
let doc;
|
|
85
|
+
try {
|
|
86
|
+
doc = await queryVerifierById(ctx, verifierValue);
|
|
87
|
+
} catch {
|
|
88
|
+
throw convexError("PASSKEY_INVALID_CHALLENGE", "Invalid or expired passkey challenge.");
|
|
89
|
+
}
|
|
90
|
+
if (!doc || doc.signature !== challengeHash) throw convexError("PASSKEY_INVALID_CHALLENGE", "Invalid or expired passkey challenge.");
|
|
91
|
+
try {
|
|
92
|
+
await mutateVerifierDelete(ctx, verifierValue);
|
|
93
|
+
} catch {
|
|
94
|
+
throw convexError("PASSKEY_INVALID_CHALLENGE", "Invalid or expired passkey challenge.");
|
|
95
|
+
}
|
|
96
|
+
return clientData;
|
|
97
|
+
}
|
|
98
|
+
function verifyRpId(authData, rpId) {
|
|
99
|
+
if (!authData.verifyRelyingPartyIdHash(rpId)) throw convexError("PASSKEY_RP_MISMATCH", "Relying party ID mismatch.");
|
|
100
|
+
return authData;
|
|
101
|
+
}
|
|
102
|
+
function verifyUserFlags(authData, rp) {
|
|
103
|
+
if (!authData.userPresent) throw convexError("PASSKEY_USER_PRESENCE", "User presence flag not set.");
|
|
104
|
+
if (rp.userVerification === "required" && !authData.userVerified) throw convexError("PASSKEY_USER_VERIFICATION", "User verification required but not performed.");
|
|
105
|
+
return authData;
|
|
106
|
+
}
|
|
107
|
+
function resolvePasskeyDispatch(params) {
|
|
94
108
|
const flow = params.flow;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
vx >>= 8n;
|
|
109
|
+
if (typeof flow === "string" && PASSKEY_FLOWS.includes(flow)) return { flow };
|
|
110
|
+
throw convexError("PASSKEY_MISSING_FLOW", "Missing `flow` parameter. Expected one of: registerOptions, registerVerify, authOptions, authVerify");
|
|
111
|
+
}
|
|
112
|
+
function requirePasskeyVerifier(verifier) {
|
|
113
|
+
if (verifier != null) return verifier;
|
|
114
|
+
throw convexError("PASSKEY_MISSING_VERIFIER", "Missing verifier for passkey operation.");
|
|
115
|
+
}
|
|
116
|
+
async function requireAuthenticatedUserId(ctx) {
|
|
117
|
+
let identity;
|
|
118
|
+
try {
|
|
119
|
+
identity = await ctx.auth.getUserIdentity();
|
|
120
|
+
} catch {
|
|
121
|
+
throw convexError("PASSKEY_AUTH_REQUIRED", "Sign in first, then add a passkey to your account.");
|
|
109
122
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
123
|
+
if (identity === null) throw convexError("PASSKEY_AUTH_REQUIRED", "Sign in first, then add a passkey to your account.");
|
|
124
|
+
return userIdFromIdentitySubject(identity.subject);
|
|
125
|
+
}
|
|
126
|
+
function resolveRegistrationPublicKeyBytes(publicKey, algorithm) {
|
|
127
|
+
if (algorithm === coseAlgorithmES256) {
|
|
128
|
+
const ec2 = publicKey.ec2();
|
|
129
|
+
const xBytes = new Uint8Array(32);
|
|
130
|
+
let vx = ec2.x;
|
|
131
|
+
for (let i = 31; i >= 0; i--) {
|
|
132
|
+
xBytes[i] = Number(vx & 255n);
|
|
133
|
+
vx >>= 8n;
|
|
134
|
+
}
|
|
135
|
+
const yBytes = new Uint8Array(32);
|
|
136
|
+
let vy = ec2.y;
|
|
137
|
+
for (let i = 31; i >= 0; i--) {
|
|
138
|
+
yBytes[i] = Number(vy & 255n);
|
|
139
|
+
vy >>= 8n;
|
|
140
|
+
}
|
|
141
|
+
const bytes = new Uint8Array(65);
|
|
142
|
+
bytes[0] = 4;
|
|
143
|
+
bytes.set(xBytes, 1);
|
|
144
|
+
bytes.set(yBytes, 33);
|
|
145
|
+
return bytes;
|
|
115
146
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
147
|
+
if (algorithm === coseAlgorithmRS256) {
|
|
148
|
+
const rsa = publicKey.rsa();
|
|
149
|
+
return new RSAPublicKey(rsa.n, rsa.e).encodePKCS1();
|
|
150
|
+
}
|
|
151
|
+
throw convexError("PASSKEY_UNSUPPORTED_ALGORITHM", `Unsupported algorithm: ${algorithm}`);
|
|
152
|
+
}
|
|
153
|
+
function verifyAssertionSignature(passkey, signature, messageHash) {
|
|
154
|
+
if (passkey.algorithm === coseAlgorithmES256) {
|
|
155
|
+
if (!verifyECDSASignature(decodeSEC1PublicKey(p256, new Uint8Array(passkey.publicKey)), messageHash, decodePKIXECDSASignature(signature))) throw convexError("PASSKEY_INVALID_SIGNATURE", "Invalid passkey signature.");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (passkey.algorithm === coseAlgorithmRS256) {
|
|
159
|
+
if (!verifyRSASSAPKCS1v15Signature(decodePKCS1RSAPublicKey(new Uint8Array(passkey.publicKey)), sha256ObjectIdentifier, messageHash, signature)) throw convexError("PASSKEY_INVALID_SIGNATURE", "Invalid passkey signature.");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
throw convexError("PASSKEY_UNSUPPORTED_ALGORITHM", `Unsupported algorithm: ${passkey.algorithm}`);
|
|
163
|
+
}
|
|
164
|
+
async function handlePasskeyFx(ctx, provider, args) {
|
|
132
165
|
const params = args.params ?? {};
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
catch
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
alg
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
166
|
+
const dispatch = resolvePasskeyDispatch(params);
|
|
167
|
+
const handler = {
|
|
168
|
+
registerOptions: async () => {
|
|
169
|
+
const userId = await requireAuthenticatedUserId(ctx);
|
|
170
|
+
const rp = resolveRpOptions(provider);
|
|
171
|
+
const challenge = new Uint8Array(32);
|
|
172
|
+
crypto.getRandomValues(challenge);
|
|
173
|
+
const challengeHash = encodeBase64urlNoPadding(new Uint8Array(sha256(challenge)));
|
|
174
|
+
let verifier;
|
|
175
|
+
try {
|
|
176
|
+
verifier = await callVerifier(ctx, challengeHash);
|
|
177
|
+
} catch {
|
|
178
|
+
throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
|
|
179
|
+
}
|
|
180
|
+
let user;
|
|
181
|
+
try {
|
|
182
|
+
user = await queryUserById(ctx, userId);
|
|
183
|
+
} catch {
|
|
184
|
+
throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
|
|
185
|
+
}
|
|
186
|
+
const userName = params.userName ?? user?.email ?? "user";
|
|
187
|
+
const userDisplayName = params.userDisplayName ?? user?.name ?? userName;
|
|
188
|
+
let existing;
|
|
189
|
+
try {
|
|
190
|
+
existing = await queryPasskeysByUserId(ctx, userId);
|
|
191
|
+
} catch {
|
|
192
|
+
throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
|
|
193
|
+
}
|
|
194
|
+
const excludeCredentials = existing.map((pk) => ({
|
|
195
|
+
id: pk.credentialId,
|
|
196
|
+
transports: pk.transports
|
|
197
|
+
}));
|
|
198
|
+
const userHandle = encodeBase64urlNoPadding(new TextEncoder().encode(userId));
|
|
199
|
+
return {
|
|
200
|
+
kind: "passkeyOptions",
|
|
201
|
+
options: {
|
|
202
|
+
rp: {
|
|
203
|
+
name: rp.rpName,
|
|
204
|
+
id: rp.rpId
|
|
205
|
+
},
|
|
206
|
+
user: {
|
|
207
|
+
id: userHandle,
|
|
208
|
+
name: userName,
|
|
209
|
+
displayName: userDisplayName
|
|
210
|
+
},
|
|
211
|
+
challenge: encodeBase64urlNoPadding(challenge),
|
|
212
|
+
pubKeyCredParams: rp.algorithms.map((alg) => ({
|
|
213
|
+
type: "public-key",
|
|
214
|
+
alg
|
|
215
|
+
})),
|
|
216
|
+
timeout: rp.challengeExpirationMs,
|
|
217
|
+
attestation: rp.attestation,
|
|
218
|
+
authenticatorSelection: {
|
|
219
|
+
residentKey: rp.residentKey,
|
|
220
|
+
requireResidentKey: rp.residentKey === "required",
|
|
221
|
+
userVerification: rp.userVerification,
|
|
222
|
+
...rp.authenticatorAttachment ? { authenticatorAttachment: rp.authenticatorAttachment } : {}
|
|
223
|
+
},
|
|
224
|
+
excludeCredentials
|
|
188
225
|
},
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
yield* Effect.tryPromise({
|
|
211
|
-
try: async () => {
|
|
226
|
+
verifier
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
registerVerify: async () => {
|
|
230
|
+
const userId = await requireAuthenticatedUserId(ctx);
|
|
231
|
+
const rp = resolveRpOptions(provider);
|
|
232
|
+
const verifier = requirePasskeyVerifier(args.verifier);
|
|
233
|
+
const clientData = parseClientDataJSON(decodeBase64urlIgnorePadding(requireStringParam(params.clientDataJSON, "clientDataJSON")));
|
|
234
|
+
verifyClientDataType(clientData, ClientDataType.Create, "webauthn.create");
|
|
235
|
+
verifyOrigin(clientData, rp);
|
|
236
|
+
await verifyAndConsumeChallenge(clientData, ctx, verifier);
|
|
237
|
+
const authData = parseAttestationObject(decodeBase64urlIgnorePadding(requireStringParam(params.attestationObject, "attestationObject"))).authenticatorData;
|
|
238
|
+
verifyRpId(authData, rp.rpId);
|
|
239
|
+
verifyUserFlags(authData, rp);
|
|
240
|
+
if (authData.credential == null) throw convexError("PASSKEY_NO_CREDENTIAL", "No credential in attestation.");
|
|
241
|
+
const credential = authData.credential;
|
|
242
|
+
const credentialId = encodeBase64urlNoPadding(credential.id);
|
|
243
|
+
const publicKey = credential.publicKey;
|
|
244
|
+
const algorithm = publicKey.isAlgorithmDefined() ? publicKey.algorithm() : publicKey.type() === COSEKeyType.EC2 ? coseAlgorithmES256 : publicKey.type() === COSEKeyType.RSA ? coseAlgorithmRS256 : coseAlgorithmES256;
|
|
245
|
+
const publicKeyBytes = resolveRegistrationPublicKeyBytes(publicKey, algorithm);
|
|
246
|
+
try {
|
|
212
247
|
const deviceType = params.deviceType ?? "single-device";
|
|
213
248
|
const backedUp = params.backedUp ?? false;
|
|
214
249
|
await authDb(ctx, ctx.auth.config).accounts.create({
|
|
@@ -228,103 +263,117 @@ function handlePasskeyFx(ctx, provider, args) {
|
|
|
228
263
|
name: params.passkeyName,
|
|
229
264
|
createdAt: Date.now()
|
|
230
265
|
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
try: () => callSignIn(ctx, {
|
|
266
|
+
} catch {
|
|
267
|
+
throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
|
|
268
|
+
}
|
|
269
|
+
let signInResult;
|
|
270
|
+
try {
|
|
271
|
+
signInResult = await callSignIn(ctx, {
|
|
238
272
|
userId,
|
|
239
273
|
generateTokens: true
|
|
240
|
-
}),
|
|
241
|
-
catch: () => convexError("INTERNAL_ERROR", "An unexpected error occurred.")
|
|
242
|
-
})
|
|
243
|
-
};
|
|
244
|
-
})), Match.when({ flow: "authOptions" }, () => Effect.gen(function* () {
|
|
245
|
-
const rp = yield* resolveRpOptionsFx(provider);
|
|
246
|
-
const challenge = new Uint8Array(32);
|
|
247
|
-
crypto.getRandomValues(challenge);
|
|
248
|
-
const challengeHash = encodeBase64urlNoPadding(new Uint8Array(sha256(challenge)));
|
|
249
|
-
const verifier = yield* Effect.tryPromise({
|
|
250
|
-
try: async () => {
|
|
251
|
-
const verifier$1 = await callVerifier(ctx);
|
|
252
|
-
await callVerifierSignature(ctx, {
|
|
253
|
-
verifier: verifier$1,
|
|
254
|
-
signature: challengeHash
|
|
255
|
-
});
|
|
256
|
-
return verifier$1;
|
|
257
|
-
},
|
|
258
|
-
catch: () => convexError("INTERNAL_ERROR", "An unexpected error occurred.")
|
|
259
|
-
});
|
|
260
|
-
let allowCredentials;
|
|
261
|
-
if (params.email) {
|
|
262
|
-
const email = requireStringParam(params.email, "email");
|
|
263
|
-
const user = yield* Effect.tryPromise({
|
|
264
|
-
try: () => queryUserByVerifiedEmail(ctx, email),
|
|
265
|
-
catch: () => convexError("INTERNAL_ERROR", "An unexpected error occurred.")
|
|
266
|
-
});
|
|
267
|
-
if (user) {
|
|
268
|
-
const passkeys = yield* Effect.tryPromise({
|
|
269
|
-
try: () => queryPasskeysByUserId(ctx, user._id),
|
|
270
|
-
catch: () => convexError("INTERNAL_ERROR", "An unexpected error occurred.")
|
|
271
274
|
});
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
id: pk.credentialId,
|
|
275
|
-
transports: pk.transports
|
|
276
|
-
}));
|
|
275
|
+
} catch {
|
|
276
|
+
throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
|
|
277
277
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
278
|
+
return {
|
|
279
|
+
kind: "signedIn",
|
|
280
|
+
signedIn: signInResult
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
authOptions: async () => {
|
|
284
|
+
const rp = resolveRpOptions(provider);
|
|
285
|
+
const challenge = new Uint8Array(32);
|
|
286
|
+
crypto.getRandomValues(challenge);
|
|
287
|
+
const challengeHash = encodeBase64urlNoPadding(new Uint8Array(sha256(challenge)));
|
|
288
|
+
let verifier;
|
|
289
|
+
try {
|
|
290
|
+
verifier = await callVerifier(ctx, challengeHash);
|
|
291
|
+
} catch {
|
|
292
|
+
throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
|
|
293
|
+
}
|
|
294
|
+
let allowCredentials;
|
|
295
|
+
if (params.email) {
|
|
296
|
+
const email = requireStringParam(params.email, "email");
|
|
297
|
+
let user;
|
|
298
|
+
try {
|
|
299
|
+
user = await queryUserByVerifiedEmail(ctx, email);
|
|
300
|
+
} catch {
|
|
301
|
+
throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
|
|
302
|
+
}
|
|
303
|
+
if (user) {
|
|
304
|
+
let passkeys;
|
|
305
|
+
try {
|
|
306
|
+
passkeys = await queryPasskeysByUserId(ctx, user._id);
|
|
307
|
+
} catch {
|
|
308
|
+
throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
|
|
309
|
+
}
|
|
310
|
+
if (passkeys.length > 0) allowCredentials = passkeys.map((pk) => ({
|
|
311
|
+
type: "public-key",
|
|
312
|
+
id: pk.credentialId,
|
|
313
|
+
transports: pk.transports
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const options = {
|
|
318
|
+
challenge: encodeBase64urlNoPadding(challenge),
|
|
319
|
+
timeout: rp.challengeExpirationMs,
|
|
320
|
+
rpId: rp.rpId,
|
|
321
|
+
userVerification: rp.userVerification
|
|
322
|
+
};
|
|
323
|
+
if (allowCredentials) options.allowCredentials = allowCredentials;
|
|
324
|
+
return {
|
|
325
|
+
kind: "passkeyOptions",
|
|
326
|
+
options,
|
|
327
|
+
verifier
|
|
328
|
+
};
|
|
329
|
+
},
|
|
330
|
+
authVerify: async () => {
|
|
331
|
+
const rp = resolveRpOptions(provider);
|
|
332
|
+
const verifier = requirePasskeyVerifier(args.verifier);
|
|
333
|
+
const clientDataJSON = decodeBase64urlIgnorePadding(requireStringParam(params.clientDataJSON, "clientDataJSON"));
|
|
334
|
+
const clientData = parseClientDataJSON(clientDataJSON);
|
|
335
|
+
verifyClientDataType(clientData, ClientDataType.Get, "webauthn.get");
|
|
336
|
+
verifyOrigin(clientData, rp);
|
|
337
|
+
await verifyAndConsumeChallenge(clientData, ctx, verifier);
|
|
338
|
+
const credentialId = params.credentialId;
|
|
339
|
+
if (credentialId == null) throw convexError("PASSKEY_UNKNOWN_CREDENTIAL", "Missing credential ID");
|
|
340
|
+
let passkey;
|
|
341
|
+
try {
|
|
342
|
+
passkey = await queryPasskeyByCredentialId(ctx, credentialId);
|
|
343
|
+
} catch {
|
|
344
|
+
throw convexError("PASSKEY_UNKNOWN_CREDENTIAL", "Unknown passkey credential.");
|
|
345
|
+
}
|
|
346
|
+
if (passkey === null) throw convexError("PASSKEY_UNKNOWN_CREDENTIAL", "Unknown credential");
|
|
347
|
+
const authenticatorDataBytes = decodeBase64urlIgnorePadding(requireStringParam(params.authenticatorData, "authenticatorData"));
|
|
348
|
+
const authenticatorData = parseAuthenticatorData(authenticatorDataBytes);
|
|
349
|
+
const signatureBytes = decodeBase64urlIgnorePadding(requireStringParam(params.signature, "signature"));
|
|
350
|
+
const messageHash = sha256(createAssertionSignatureMessage(authenticatorDataBytes, clientDataJSON));
|
|
351
|
+
verifyRpId(authenticatorData, rp.rpId);
|
|
352
|
+
verifyUserFlags(authenticatorData, rp);
|
|
353
|
+
verifyAssertionSignature(passkey, signatureBytes, messageHash);
|
|
354
|
+
if (passkey.counter !== 0 && authenticatorData.signatureCounter !== 0 && authenticatorData.signatureCounter <= passkey.counter) throw convexError("PASSKEY_COUNTER_ERROR", "Authenticator counter did not increase — possible credential cloning detected.");
|
|
355
|
+
try {
|
|
356
|
+
await mutatePasskeyUpdateCounter(ctx, passkey._id, authenticatorData.signatureCounter, Date.now());
|
|
357
|
+
} catch {
|
|
358
|
+
throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
|
|
359
|
+
}
|
|
360
|
+
let signInResult;
|
|
361
|
+
try {
|
|
362
|
+
signInResult = await callSignIn(ctx, {
|
|
321
363
|
userId: passkey.userId,
|
|
322
364
|
generateTokens: true
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
365
|
+
});
|
|
366
|
+
} catch {
|
|
367
|
+
throw convexError("INTERNAL_ERROR", "An unexpected error occurred.");
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
kind: "signedIn",
|
|
371
|
+
signedIn: signInResult
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}[dispatch.flow];
|
|
375
|
+
if (!handler) throw convexError("PASSKEY_MISSING_FLOW", `Unknown passkey flow: ${dispatch.flow}`);
|
|
376
|
+
return handler();
|
|
328
377
|
}
|
|
329
378
|
|
|
330
379
|
//#endregion
|