@robelest/convex-auth 0.0.4-preview.30 → 0.0.4-preview.31
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/bin.js +125 -36
- package/dist/browser/index.d.ts +3 -13
- package/dist/browser/index.js +47 -12
- package/dist/browser/navigation.js +1 -1
- package/dist/browser/passkey.js +7 -7
- package/dist/browser/runtime.js +13 -15
- package/dist/client/core/types.d.ts +179 -63
- package/dist/client/core/types.js +6 -0
- package/dist/client/factors/totp.js +1 -1
- package/dist/client/index.d.ts +5 -4
- package/dist/client/index.js +115 -56
- package/dist/client/runtime/mutex.js +3 -2
- package/dist/component/_generated/component.d.ts +40 -0
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/http.js +9 -0
- package/dist/component/index.d.ts +1 -1
- package/dist/component/model.d.ts +25 -25
- package/dist/component/model.js +2 -1
- package/dist/component/modules.js +1 -0
- package/dist/component/public/factors/passkeys.js +31 -1
- package/dist/component/public/identity/codes.js +1 -1
- package/dist/component/public/identity/tokens.js +2 -1
- package/dist/component/public/identity/verifiers.js +15 -5
- package/dist/component/public.js +2 -2
- package/dist/component/schema.d.ts +292 -290
- package/dist/component/schema.js +2 -1
- package/dist/core/index.d.ts +8 -3
- package/dist/core/index.js +7 -2
- package/dist/expo/index.d.ts +21 -0
- package/dist/expo/index.js +148 -0
- package/dist/expo/passkey.js +174 -0
- package/dist/providers/apple.d.ts +1 -1
- package/dist/providers/apple.js +6 -8
- package/dist/providers/custom.d.ts +1 -1
- package/dist/providers/custom.js +4 -7
- package/dist/providers/github.d.ts +1 -1
- package/dist/providers/github.js +5 -8
- package/dist/providers/google.d.ts +1 -1
- package/dist/providers/google.js +5 -8
- package/dist/providers/microsoft.d.ts +1 -1
- package/dist/providers/microsoft.js +5 -9
- package/dist/providers/password.d.ts +18 -37
- package/dist/providers/password.js +170 -115
- package/dist/providers/redirect.d.ts +1 -0
- package/dist/providers/redirect.js +20 -0
- package/dist/server/auth.d.ts +6 -7
- package/dist/server/auth.js +3 -2
- package/dist/server/{ctxCache.js → cache/context.js} +2 -2
- package/dist/server/{componentContext.d.ts → component/context.d.ts} +2 -2
- package/dist/server/context.js +3 -10
- package/dist/server/contract.d.ts +2 -87
- package/dist/server/contract.js +1 -1
- package/dist/server/cookies.js +25 -1
- package/dist/server/core.js +1 -1
- package/dist/server/errors.js +24 -1
- package/dist/server/{auth-context.d.ts → facade.d.ts} +3 -45
- package/dist/server/{auth-context.js → facade.js} +11 -12
- package/dist/server/http.d.ts +7 -7
- package/dist/server/http.js +36 -7
- package/dist/server/{convexIdentity.d.ts → identity/convex.d.ts} +3 -3
- package/dist/server/index.d.ts +5 -3
- package/dist/server/index.js +3 -2
- package/dist/server/mounts.d.ts +175 -22
- package/dist/server/mutations/code.js +7 -1
- package/dist/server/mutations/{credentialsSignIn.js → credentials/signin.js} +10 -10
- package/dist/server/mutations/index.js +1 -1
- package/dist/server/mutations/invalidate.js +11 -1
- package/dist/server/mutations/oauth.js +25 -27
- package/dist/server/mutations/signin.js +6 -0
- package/dist/server/mutations/signout.js +5 -0
- package/dist/server/mutations/store.js +1 -1
- package/dist/server/oauth/factory.js +11 -3
- package/dist/server/passkey.js +126 -110
- package/dist/server/prefetch.js +8 -1
- package/dist/server/redirects.js +11 -3
- package/dist/server/refresh.js +6 -1
- package/dist/server/runtime.d.ts +58 -36
- package/dist/server/runtime.js +333 -36
- package/dist/server/services/group.js +4 -0
- package/dist/server/sessions.js +1 -0
- package/dist/server/signin.js +8 -6
- package/dist/server/sso/domain.d.ts +159 -16
- package/dist/server/sso/domain.js +1 -1
- package/dist/server/sso/http.js +144 -60
- package/dist/server/sso/oidc.js +28 -12
- package/dist/server/sso/policy.js +30 -14
- package/dist/server/sso/provision.js +1 -1
- package/dist/server/sso/saml.js +18 -9
- package/dist/server/sso/scim.js +12 -4
- package/dist/server/sso/shared.js +5 -5
- package/dist/server/telemetry.js +3 -0
- package/dist/server/tokens.js +10 -2
- package/dist/server/totp.js +127 -100
- package/dist/server/types.d.ts +224 -151
- package/dist/server/url.js +1 -1
- package/dist/server/users.js +93 -53
- package/dist/server/wellknown.d.ts +130 -0
- package/dist/server/wellknown.js +195 -0
- package/dist/shared/errors.js +0 -1
- package/package.json +36 -4
- package/dist/server/oauth/index.js +0 -12
- package/dist/server/utils/dispatch.js +0 -36
- package/dist/shared/authResults.d.ts +0 -16
- /package/dist/server/{componentContext.js → component/context.js} +0 -0
- /package/dist/server/{convexIdentity.js → identity/convex.js} +0 -0
package/dist/server/sso/oidc.js
CHANGED
|
@@ -44,7 +44,9 @@ function validateOidcUserInfo(data) {
|
|
|
44
44
|
email: typeof obj.email === "string" ? obj.email : void 0,
|
|
45
45
|
email_verified: typeof obj.email_verified === "boolean" ? obj.email_verified : void 0,
|
|
46
46
|
name: typeof obj.name === "string" ? obj.name : void 0,
|
|
47
|
-
picture: typeof obj.picture === "string" ? obj.picture : void 0
|
|
47
|
+
picture: typeof obj.picture === "string" ? obj.picture : void 0,
|
|
48
|
+
groups: obj.groups,
|
|
49
|
+
roles: obj.roles
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
52
|
const asError = (error) => error instanceof Error ? error : new Error(String(error));
|
|
@@ -127,7 +129,9 @@ async function userInfoProfileFx(opts) {
|
|
|
127
129
|
email: typeof userInfo.email === "string" ? userInfo.email : opts.verifiedProfile.email,
|
|
128
130
|
emailVerified: typeof userInfo.email_verified === "boolean" ? userInfo.email_verified : opts.verifiedProfile.emailVerified,
|
|
129
131
|
name: typeof userInfo.name === "string" ? userInfo.name : opts.verifiedProfile.name,
|
|
130
|
-
image: typeof userInfo.picture === "string" ? userInfo.picture : opts.verifiedProfile.image
|
|
132
|
+
image: typeof userInfo.picture === "string" ? userInfo.picture : opts.verifiedProfile.image,
|
|
133
|
+
groups: normalizeStringArray(userInfo.groups) ?? opts.verifiedProfile.groups,
|
|
134
|
+
roles: normalizeStringArray(userInfo.roles) ?? opts.verifiedProfile.roles
|
|
131
135
|
};
|
|
132
136
|
});
|
|
133
137
|
}
|
|
@@ -151,6 +155,7 @@ async function createGroupConnectionOidcProvider(config, redirectUri) {
|
|
|
151
155
|
const supportedIdTokenSigningAlgs = Array.isArray(discovery.id_token_signing_alg_values_supported) ? discovery.id_token_signing_alg_values_supported.filter((value) => typeof value === "string") : [];
|
|
152
156
|
const discoveredTokenEndpointAuthMethods = Array.isArray(discovery.token_endpoint_auth_methods_supported) ? discovery.token_endpoint_auth_methods_supported.filter((value) => typeof value === "string") : [];
|
|
153
157
|
const tokenEndpointAuthMethod = client.authMethod === "client_secret_basic" || client.authMethod === "client_secret_post" ? client.authMethod : discoveredTokenEndpointAuthMethods.includes("client_secret_basic") ? "client_secret_basic" : "client_secret_post";
|
|
158
|
+
if (typeof client.authMethod === "string" && discoveredTokenEndpointAuthMethods.length > 0 && !discoveredTokenEndpointAuthMethods.includes(tokenEndpointAuthMethod)) throw new Error(`OIDC token endpoint auth method ${tokenEndpointAuthMethod} is not advertised by discovery.`);
|
|
154
159
|
const userinfoEndpoint = discovery.userinfo_endpoint ?? void 0;
|
|
155
160
|
const claimMapping = typeof profile.mapping === "object" && profile.mapping !== null ? profile.mapping : void 0;
|
|
156
161
|
const oidcFetch = createGroupConnectionOidcFetch(config, discovery.issuer);
|
|
@@ -163,13 +168,8 @@ async function createGroupConnectionOidcProvider(config, redirectUri) {
|
|
|
163
168
|
];
|
|
164
169
|
const expectedAudience = Array.isArray(discoveryConfig.audience) ? discoveryConfig.audience.filter((value) => typeof value === "string") : typeof discoveryConfig.audience === "string" ? discoveryConfig.audience : String(client.id);
|
|
165
170
|
const clockToleranceSeconds = typeof security.clockToleranceSeconds === "number" ? security.clockToleranceSeconds : 10;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (issuer.startsWith("https://")) candidates.push(`http://${issuer.slice(8)}`);
|
|
169
|
-
else if (issuer.startsWith("http://")) candidates.push(`https://${issuer.slice(7)}`);
|
|
170
|
-
return candidates;
|
|
171
|
-
};
|
|
172
|
-
const expectedIssuers = strictIssuer ? [expectedIssuer] : Array.from(new Set([...getIssuerCandidates(expectedIssuer), ...getIssuerCandidates(discoveredIssuer)]));
|
|
171
|
+
if (clockToleranceSeconds < 0 || clockToleranceSeconds > 300) throw new Error("OIDC clockToleranceSeconds must be between 0 and 300.");
|
|
172
|
+
const expectedIssuers = strictIssuer ? [expectedIssuer] : Array.from(new Set([expectedIssuer, discoveredIssuer]));
|
|
173
173
|
const jwks = getOidcJwks(jwksUri, runtimeOrigin, externalHost, oidcFetch);
|
|
174
174
|
let verifiedClaims = null;
|
|
175
175
|
let verifiedProfile = null;
|
|
@@ -190,7 +190,21 @@ async function createGroupConnectionOidcProvider(config, redirectUri) {
|
|
|
190
190
|
if (nonce !== void 0) url.searchParams.set("nonce", nonce);
|
|
191
191
|
if (typeof loginHint === "string") url.searchParams.set("login_hint", loginHint);
|
|
192
192
|
const authorizationParams = typeof request.authorizationParams === "object" && request.authorizationParams !== null ? request.authorizationParams : {};
|
|
193
|
-
|
|
193
|
+
const reservedAuthorizationParams = new Set([
|
|
194
|
+
"response_type",
|
|
195
|
+
"client_id",
|
|
196
|
+
"redirect_uri",
|
|
197
|
+
"scope",
|
|
198
|
+
"state",
|
|
199
|
+
"code_challenge",
|
|
200
|
+
"code_challenge_method",
|
|
201
|
+
"nonce",
|
|
202
|
+
"login_hint"
|
|
203
|
+
]);
|
|
204
|
+
for (const [key, value] of Object.entries(authorizationParams)) {
|
|
205
|
+
if (reservedAuthorizationParams.has(key)) throw new Error(`OIDC authorizationParams cannot override reserved parameter: ${key}`);
|
|
206
|
+
if (typeof value === "string") url.searchParams.set(key, value);
|
|
207
|
+
}
|
|
194
208
|
return url;
|
|
195
209
|
},
|
|
196
210
|
async validateAuthorizationCode({ code, codeVerifier }) {
|
|
@@ -201,7 +215,9 @@ async function createGroupConnectionOidcProvider(config, redirectUri) {
|
|
|
201
215
|
});
|
|
202
216
|
const headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded" });
|
|
203
217
|
if (typeof client.secret === "string" && tokenEndpointAuthMethod === "client_secret_basic") {
|
|
204
|
-
const
|
|
218
|
+
const encodeCredential = (value) => encodeURIComponent(value).replace(/%20/g, "+");
|
|
219
|
+
const credentials = `${encodeCredential(String(client.id))}:${encodeCredential(client.secret)}`;
|
|
220
|
+
const basicAuth = typeof btoa === "function" ? btoa(credentials) : Buffer.from(credentials).toString("base64");
|
|
205
221
|
headers.set("Authorization", `Basic ${basicAuth}`);
|
|
206
222
|
} else {
|
|
207
223
|
body.set("client_id", String(client.id));
|
|
@@ -263,7 +279,7 @@ async function createGroupConnectionOidcProvider(config, redirectUri) {
|
|
|
263
279
|
const tokenIssuer = typeof tokenIssuerRaw === "string" ? tokenIssuerRaw.replace(/\/$/, "") : void 0;
|
|
264
280
|
if (!tokenIssuer || !expectedIssuers.includes(tokenIssuer)) throw new Error(`OIDC token issuer mismatch. Received: ${tokenIssuer ?? "<missing>"}. Expected one of: ${expectedIssuers.join(", ")}`);
|
|
265
281
|
if (payload.nonce !== ctx.nonce) throw new Error("OIDC nonce mismatch.");
|
|
266
|
-
if (
|
|
282
|
+
if (payload.azp !== void 0 && payload.azp !== String(client.id)) throw new Error("OIDC authorized party does not match client ID.");
|
|
267
283
|
verifiedClaims = payload;
|
|
268
284
|
verifiedProfile = normalizeProfile(payload);
|
|
269
285
|
},
|
|
@@ -30,7 +30,17 @@ const DEFAULT_GROUP_CONNECTION_POLICY = {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
|
+
/** Accept a value only if it's one of the allowed options, otherwise return the fallback. */
|
|
34
|
+
function oneOf(value, allowed, fallback) {
|
|
35
|
+
return allowed.includes(value) ? value : fallback;
|
|
36
|
+
}
|
|
37
|
+
/** Parse an object whose values are string arrays (e.g. group→roleIds mapping). */
|
|
38
|
+
function parseStringArrayMapping(raw) {
|
|
39
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
40
|
+
return Object.fromEntries(Object.entries(raw).filter(([key, value]) => typeof key === "string" && Array.isArray(value)).map(([key, value]) => [key, Array.from(new Set(value.filter((item) => typeof item === "string" && item.length > 0)))]));
|
|
41
|
+
}
|
|
33
42
|
function normalizeGroupConnectionPolicy(policy) {
|
|
43
|
+
const d = DEFAULT_GROUP_CONNECTION_POLICY;
|
|
34
44
|
const input = asRecord(policy) ?? {};
|
|
35
45
|
const accountLinking = asRecord((asRecord(input.identity) ?? {}).accountLinking) ?? {};
|
|
36
46
|
const provisioning = asRecord(input.provisioning) ?? {};
|
|
@@ -41,34 +51,40 @@ function normalizeGroupConnectionPolicy(policy) {
|
|
|
41
51
|
const groups = asRecord(provisioning.groups) ?? {};
|
|
42
52
|
const roles = asRecord(provisioning.roles) ?? {};
|
|
43
53
|
const extend = asRecord(input.extend) ?? void 0;
|
|
54
|
+
const groupsMapping = parseStringArrayMapping(groups.mapping);
|
|
55
|
+
const rolesMapping = parseStringArrayMapping(roles.mapping);
|
|
44
56
|
return {
|
|
45
57
|
version: 1,
|
|
46
58
|
identity: { accountLinking: {
|
|
47
|
-
oidc: accountLinking.oidc
|
|
48
|
-
saml: accountLinking.saml
|
|
59
|
+
oidc: oneOf(accountLinking.oidc, ["none"], d.identity.accountLinking.oidc),
|
|
60
|
+
saml: oneOf(accountLinking.saml, ["none"], d.identity.accountLinking.saml)
|
|
49
61
|
} },
|
|
50
62
|
provisioning: {
|
|
51
63
|
user: {
|
|
52
|
-
createOnSignIn: typeof user.createOnSignIn === "boolean" ? user.createOnSignIn :
|
|
53
|
-
updateProfileOnLogin: user.updateProfileOnLogin
|
|
54
|
-
updateProfileFromScim: user.updateProfileFromScim
|
|
55
|
-
authority: user.authority
|
|
64
|
+
createOnSignIn: typeof user.createOnSignIn === "boolean" ? user.createOnSignIn : d.provisioning.user.createOnSignIn,
|
|
65
|
+
updateProfileOnLogin: oneOf(user.updateProfileOnLogin, ["never", "always"], d.provisioning.user.updateProfileOnLogin),
|
|
66
|
+
updateProfileFromScim: oneOf(user.updateProfileFromScim, ["never", "missing"], d.provisioning.user.updateProfileFromScim),
|
|
67
|
+
authority: oneOf(user.authority, ["sso", "scim"], d.provisioning.user.authority)
|
|
56
68
|
},
|
|
57
|
-
scimReuse: { user: scimReuse.user
|
|
69
|
+
scimReuse: { user: oneOf(scimReuse.user, ["none"], d.provisioning.scimReuse.user) },
|
|
58
70
|
jit: {
|
|
59
|
-
mode: jit.mode
|
|
60
|
-
|
|
71
|
+
mode: oneOf(jit.mode, [
|
|
72
|
+
"off",
|
|
73
|
+
"createUser",
|
|
74
|
+
"createUserAndMembership"
|
|
75
|
+
], d.provisioning.jit.mode),
|
|
76
|
+
defaultRoleIds: Array.isArray(jit.defaultRoleIds) ? Array.from(new Set(jit.defaultRoleIds.filter((value) => typeof value === "string" && value.length > 0))) : typeof jit.defaultRole === "string" && jit.defaultRole.length > 0 ? [jit.defaultRole] : d.provisioning.jit.defaultRoleIds
|
|
61
77
|
},
|
|
62
|
-
deprovision: { mode: deprovision.mode
|
|
78
|
+
deprovision: { mode: oneOf(deprovision.mode, ["hard"], d.provisioning.deprovision.mode) },
|
|
63
79
|
groups: {
|
|
64
|
-
mode: groups.mode
|
|
80
|
+
mode: oneOf(groups.mode, ["sync"], d.provisioning.groups.mode),
|
|
65
81
|
source: "protocol",
|
|
66
|
-
...
|
|
82
|
+
...groupsMapping ? { mapping: groupsMapping } : {}
|
|
67
83
|
},
|
|
68
84
|
roles: {
|
|
69
|
-
mode: roles.mode
|
|
85
|
+
mode: oneOf(roles.mode, ["map"], d.provisioning.roles.mode),
|
|
70
86
|
source: "protocol",
|
|
71
|
-
...
|
|
87
|
+
...rolesMapping ? { mapping: rolesMapping } : {}
|
|
72
88
|
}
|
|
73
89
|
},
|
|
74
90
|
...extend ? { extend } : {}
|
|
@@ -8,7 +8,7 @@ function getScimConfigShape(scimConfig) {
|
|
|
8
8
|
const convexError = (data) => new ConvexError(data);
|
|
9
9
|
function createGroupScimDomain(deps) {
|
|
10
10
|
const { config, requireEnv, generateRandomString, INVITE_TOKEN_ALPHABET, sha256, loadGroupPolicyOrThrow, recordGroupAuditEvent, emitGroupWebhookDeliveries } = deps;
|
|
11
|
-
const getScimBasePath = (connectionId) => `${requireEnv("CONVEX_SITE_URL")}/
|
|
11
|
+
const getScimBasePath = (connectionId) => `${requireEnv("CONVEX_SITE_URL")}/connections/${connectionId}/scim/v2`;
|
|
12
12
|
const validateScim = async (ctx, connectionId) => {
|
|
13
13
|
const checks = [];
|
|
14
14
|
const connection = await getGroupConnection(ctx, config.component.public, connectionId);
|
package/dist/server/sso/saml.js
CHANGED
|
@@ -6,14 +6,20 @@ import { decodeBase64urlIgnorePadding, encodeBase64urlNoPadding } from "@oslojs/
|
|
|
6
6
|
import { Constants, IdentityProvider, ServiceProvider, setSchemaValidator } from "@robelest/samlify";
|
|
7
7
|
|
|
8
8
|
//#region src/server/sso/saml.ts
|
|
9
|
+
function formDataEntries(formData) {
|
|
10
|
+
return formData;
|
|
11
|
+
}
|
|
9
12
|
const _samlifyPermissiveValidator = { validate: (_xml) => Promise.resolve("OK") };
|
|
10
13
|
function ensureSamlifyValidator() {
|
|
11
14
|
setSchemaValidator(_samlifyPermissiveValidator);
|
|
12
15
|
}
|
|
16
|
+
function escapeHtmlAttribute(value) {
|
|
17
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
18
|
+
}
|
|
13
19
|
/** @internal */
|
|
14
20
|
function createSamlPostBindingResponse(opts) {
|
|
15
|
-
const fields = [`<input type="hidden" name="${opts.parameter}" value="${opts.value
|
|
16
|
-
return new Response(`<!doctype html><html><body><form method="POST" action="${opts.endpoint}">${fields}</form><script>document.forms[0].submit();<\/script></body></html>`, {
|
|
21
|
+
const fields = [`<input type="hidden" name="${opts.parameter}" value="${escapeHtmlAttribute(opts.value)}" />`, opts.relayState ? `<input type="hidden" name="RelayState" value="${escapeHtmlAttribute(opts.relayState)}" />` : ""].join("");
|
|
22
|
+
return new Response(`<!doctype html><html><body><form method="POST" action="${escapeHtmlAttribute(opts.endpoint)}">${fields}</form><script>document.forms[0].submit();<\/script></body></html>`, {
|
|
17
23
|
status: 200,
|
|
18
24
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
19
25
|
});
|
|
@@ -62,9 +68,7 @@ async function readRequestBody(request) {
|
|
|
62
68
|
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
63
69
|
const form = await request.formData();
|
|
64
70
|
const body = {};
|
|
65
|
-
|
|
66
|
-
body[key] = typeof value === "string" ? value : value.name;
|
|
67
|
-
});
|
|
71
|
+
for (const [key, value] of formDataEntries(form)) body[key] = typeof value === "string" ? value : value.name;
|
|
68
72
|
return body;
|
|
69
73
|
}
|
|
70
74
|
return {};
|
|
@@ -89,6 +93,7 @@ function getSamlSecurityConfig(config) {
|
|
|
89
93
|
/** @internal */
|
|
90
94
|
function parseSamlIdpMetadata(metadata) {
|
|
91
95
|
const source = typeof metadata === "string" ? metadata : String(metadata);
|
|
96
|
+
if (/<!DOCTYPE|<!ENTITY/i.test(source)) throw new Error("SAML metadata must not contain DTD or entity declarations.");
|
|
92
97
|
const entityId = source.match(/<[^>]*EntityDescriptor\b[^>]*\bentityID="([^"]+)"/i)?.[1] ?? null;
|
|
93
98
|
if (!entityId) throw new Error("SAML metadata is missing EntityDescriptor@entityID.");
|
|
94
99
|
const parseAttributes = (source$1) => {
|
|
@@ -259,11 +264,13 @@ function verifySamlTimeWindow(notBefore, notOnOrAfter, clockSkewSeconds) {
|
|
|
259
264
|
const drift = clockSkewSeconds * 1e3;
|
|
260
265
|
if (notBefore) {
|
|
261
266
|
const notBeforeTime = new Date(notBefore).getTime();
|
|
262
|
-
if (Number.isFinite(notBeforeTime)
|
|
267
|
+
if (!Number.isFinite(notBeforeTime)) throw new Error("SAML assertion has an invalid NotBefore timestamp.");
|
|
268
|
+
if (now < notBeforeTime - drift) throw new Error("SAML assertion is not yet valid.");
|
|
263
269
|
}
|
|
264
270
|
if (notOnOrAfter) {
|
|
265
271
|
const notOnOrAfterTime = new Date(notOnOrAfter).getTime();
|
|
266
|
-
if (Number.isFinite(notOnOrAfterTime)
|
|
272
|
+
if (!Number.isFinite(notOnOrAfterTime)) throw new Error("SAML assertion has an invalid NotOnOrAfter timestamp.");
|
|
273
|
+
if (now >= notOnOrAfterTime + drift) throw new Error("SAML assertion has expired.");
|
|
267
274
|
}
|
|
268
275
|
}
|
|
269
276
|
/** @internal */
|
|
@@ -273,7 +280,7 @@ function enforceGroupConnectionSamlSecurity(opts) {
|
|
|
273
280
|
const conditions = opts.extract?.conditions;
|
|
274
281
|
if (security.requireSignedAssertions === true && typeof opts.extract?.signature?.signatureAlgorithm !== "string") throw new Error("SAML assertion must be signed.");
|
|
275
282
|
if (security.requireTimestamps === true) {
|
|
276
|
-
if (!conditions?.notBefore
|
|
283
|
+
if (!conditions?.notBefore || !conditions?.notOnOrAfter) throw new Error("SAML assertion missing required timestamp conditions.");
|
|
277
284
|
}
|
|
278
285
|
if (conditions?.notBefore || conditions?.notOnOrAfter) verifySamlTimeWindow(conditions.notBefore, conditions.notOnOrAfter, security.clockSkewSeconds ?? 300);
|
|
279
286
|
}
|
|
@@ -377,10 +384,12 @@ async function parseGroupConnectionSamlLogoutMessage(opts) {
|
|
|
377
384
|
relayState: httpRequest.relayState
|
|
378
385
|
});
|
|
379
386
|
const parsedRequest = httpRequest.hasSamlRequest ? await runtime.sp.parseLogoutRequest(runtime.idp, httpRequest.binding, toSamlHttpRequest(httpRequest)) : void 0;
|
|
387
|
+
const parsedResponse = httpRequest.hasSamlResponse ? await runtime.sp.parseLogoutResponse(runtime.idp, httpRequest.binding, toSamlHttpRequest(httpRequest)) : void 0;
|
|
380
388
|
return {
|
|
381
389
|
...httpRequest,
|
|
382
390
|
runtime,
|
|
383
|
-
parsedRequest
|
|
391
|
+
parsedRequest,
|
|
392
|
+
parsedResponse
|
|
384
393
|
};
|
|
385
394
|
}
|
|
386
395
|
/** @internal */
|
package/dist/server/sso/scim.js
CHANGED
|
@@ -3,8 +3,10 @@ import { SCIM_GROUP_SCHEMA_ID, SCIM_USER_SCHEMA_ID } from "./shared.js";
|
|
|
3
3
|
//#region src/server/sso/scim.ts
|
|
4
4
|
/** @internal */
|
|
5
5
|
function parseScimPath(pathname) {
|
|
6
|
-
const
|
|
7
|
-
|
|
6
|
+
const parts = pathname.split("/").filter(Boolean);
|
|
7
|
+
const connectionsIndex = parts.lastIndexOf("connections");
|
|
8
|
+
const [connectionId, protocol, version, ...rest] = connectionsIndex >= 0 ? parts.slice(connectionsIndex + 1) : [];
|
|
9
|
+
if (connectionsIndex < 0 || !connectionId || connectionId === "setup" || protocol !== "scim" || version !== "v2" || rest.length > 2) return {
|
|
8
10
|
connectionId: "",
|
|
9
11
|
resource: "",
|
|
10
12
|
resourceId: void 0
|
|
@@ -17,8 +19,14 @@ function parseScimPath(pathname) {
|
|
|
17
19
|
}
|
|
18
20
|
/** @internal */
|
|
19
21
|
function parseScimListRequest(url) {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
+
const rawStartIndex = url.searchParams.get("startIndex") ?? "1";
|
|
23
|
+
const rawCount = url.searchParams.get("count") ?? "100";
|
|
24
|
+
const parsedStartIndex = Number(rawStartIndex);
|
|
25
|
+
const parsedCount = Number(rawCount);
|
|
26
|
+
if (!Number.isInteger(parsedStartIndex) || parsedStartIndex < 1) throw new Error("Invalid SCIM pagination.");
|
|
27
|
+
if (!Number.isInteger(parsedCount) || parsedCount < 0) throw new Error("Invalid SCIM pagination.");
|
|
28
|
+
const startIndex = parsedStartIndex;
|
|
29
|
+
const count = Math.min(100, parsedCount);
|
|
22
30
|
const filterParam = url.searchParams.get("filter");
|
|
23
31
|
return {
|
|
24
32
|
startIndex,
|
|
@@ -23,21 +23,21 @@ function groupSamlProviderId(connectionId) {
|
|
|
23
23
|
function getGroupSamlUrls(opts) {
|
|
24
24
|
const root = opts.rootUrl.replace(/\/$/, "");
|
|
25
25
|
return {
|
|
26
|
-
metadataUrl: `${root}/
|
|
27
|
-
acsUrl: `${root}/
|
|
28
|
-
sloUrl: `${root}/
|
|
26
|
+
metadataUrl: `${root}/connections/${opts.source.id}/saml/metadata`,
|
|
27
|
+
acsUrl: `${root}/connections/${opts.source.id}/saml/acs`,
|
|
28
|
+
sloUrl: `${root}/connections/${opts.source.id}/saml/slo`
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
/** @internal */
|
|
32
32
|
function getGroupOidcUrls(opts) {
|
|
33
33
|
const root = opts.rootUrl.replace(/\/$/, "");
|
|
34
34
|
const callbackUrl = (() => {
|
|
35
|
-
if (typeof opts.sharedRedirectURI !== "string") return `${root}/
|
|
35
|
+
if (typeof opts.sharedRedirectURI !== "string") return `${root}/connections/${opts.connectionId}/oidc/callback`;
|
|
36
36
|
if (/^https?:\/\//.test(opts.sharedRedirectURI)) return opts.sharedRedirectURI;
|
|
37
37
|
return `${root}${opts.sharedRedirectURI.startsWith("/") ? "" : "/"}${opts.sharedRedirectURI}`;
|
|
38
38
|
})();
|
|
39
39
|
return {
|
|
40
|
-
signInUrl: `${root}/
|
|
40
|
+
signInUrl: `${root}/connections/${opts.connectionId}/oidc/signin`,
|
|
41
41
|
callbackUrl
|
|
42
42
|
};
|
|
43
43
|
}
|
package/dist/server/telemetry.js
CHANGED
|
@@ -43,12 +43,15 @@ async function buildAuthIdentityAttributes(ctx, config, args) {
|
|
|
43
43
|
if (Object.keys(attributes).length > 0) attributes["auth.identity.mode"] = mode;
|
|
44
44
|
return attributes;
|
|
45
45
|
}
|
|
46
|
+
/** @internal */
|
|
46
47
|
async function buildRefreshIdentityAttributes(ctx, config, args) {
|
|
47
48
|
return await buildAuthIdentityAttributes(ctx, config, args);
|
|
48
49
|
}
|
|
50
|
+
/** @internal */
|
|
49
51
|
async function buildSignInIdentityAttributes(ctx, config, args) {
|
|
50
52
|
return await buildAuthIdentityAttributes(ctx, config, args);
|
|
51
53
|
}
|
|
54
|
+
/** @internal */
|
|
52
55
|
async function buildSignOutIdentityAttributes(ctx, config, args) {
|
|
53
56
|
return await buildAuthIdentityAttributes(ctx, config, args);
|
|
54
57
|
}
|
package/dist/server/tokens.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { generateRandomString } from "./random.js";
|
|
2
|
-
import { requireEnv } from "./env.js";
|
|
2
|
+
import { envOptionalString, readConfigSync, requireEnv } from "./env.js";
|
|
3
3
|
import { withSpan } from "./utils/span.js";
|
|
4
4
|
import { SignJWT, importPKCS8 } from "jose";
|
|
5
5
|
|
|
@@ -34,9 +34,17 @@ const getPrivateKey = () => {
|
|
|
34
34
|
return cachedPrivateKeyPromise;
|
|
35
35
|
};
|
|
36
36
|
const getIssuer = () => {
|
|
37
|
-
if (cachedIssuer === null) cachedIssuer = requireEnv("CONVEX_SITE_URL");
|
|
37
|
+
if (cachedIssuer === null) cachedIssuer = appendAuthPrefix(requireEnv("CONVEX_SITE_URL"), readConfigSync(envOptionalString("CONVEX_AUTH_HTTP_PREFIX")) ?? "/auth");
|
|
38
38
|
return cachedIssuer;
|
|
39
39
|
};
|
|
40
|
+
function appendAuthPrefix(siteUrl, prefix) {
|
|
41
|
+
return `${siteUrl.replace(/\/$/, "")}${normalizeAuthPrefix(prefix)}`;
|
|
42
|
+
}
|
|
43
|
+
function normalizeAuthPrefix(prefix) {
|
|
44
|
+
const trimmed = prefix.trim();
|
|
45
|
+
if (trimmed === "" || trimmed === "/") return "";
|
|
46
|
+
return `/${trimmed.replace(/^\/+|\/+$/g, "")}`;
|
|
47
|
+
}
|
|
40
48
|
try {
|
|
41
49
|
getPrivateKey().catch(() => {});
|
|
42
50
|
} catch {}
|
package/dist/server/totp.js
CHANGED
|
@@ -12,22 +12,20 @@ import { createTOTPKeyURI, verifyTOTPWithGracePeriod } from "@oslojs/otp";
|
|
|
12
12
|
/**
|
|
13
13
|
* Server-side TOTP ceremony logic for two-factor authentication.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Two single-word flows:
|
|
16
|
+
*
|
|
17
|
+
* - `setup` — generate a TOTP secret and `otpauth://` URI for enrollment.
|
|
18
|
+
* - `verify` — consume a TOTP code. Auto-detects:
|
|
19
|
+
* - first-time enrollment confirmation (caller passes `totpId`), or
|
|
20
|
+
* - 2FA sign-in challenge (no `totpId`).
|
|
19
21
|
*/
|
|
20
|
-
const TOTP_FLOWS = [
|
|
21
|
-
"setup",
|
|
22
|
-
"confirm",
|
|
23
|
-
"verify"
|
|
24
|
-
];
|
|
22
|
+
const TOTP_FLOWS = ["setup", "verify"];
|
|
25
23
|
const convexError = (code, message) => toConvexError(authFlowError(code, message));
|
|
26
24
|
const asConvexError = (error, code, message) => error instanceof ConvexError ? error : error instanceof Error ? toConvexError(authFlowError(code, error.message || message)) : convexError(code, message);
|
|
27
25
|
function resolveTotpFlow(params) {
|
|
28
26
|
const flow = params.flow;
|
|
29
27
|
if (typeof flow === "string" && TOTP_FLOWS.includes(flow)) return flow;
|
|
30
|
-
throw convexError("TOTP_MISSING_FLOW", "Missing `flow` parameter. Expected one of:
|
|
28
|
+
throw convexError("TOTP_MISSING_FLOW", "Missing `flow` parameter. Expected one of: " + TOTP_FLOWS.join(", "));
|
|
31
29
|
}
|
|
32
30
|
function requireTotpVerifier(verifier) {
|
|
33
31
|
if (verifier != null) return verifier;
|
|
@@ -37,30 +35,25 @@ function requireTotpCode(params) {
|
|
|
37
35
|
if (typeof params.code === "string") return params.code;
|
|
38
36
|
throw convexError("TOTP_MISSING_CODE", "Missing TOTP code.");
|
|
39
37
|
}
|
|
40
|
-
function requireTotpId(params) {
|
|
41
|
-
if (typeof params.totpId === "string") return params.totpId;
|
|
42
|
-
throw convexError("TOTP_MISSING_ID", "Missing TOTP enrollment ID.");
|
|
43
|
-
}
|
|
44
38
|
function resolveTotpDispatch(params, verifier) {
|
|
45
|
-
|
|
46
|
-
if (flow === "setup") return {
|
|
39
|
+
if (resolveTotpFlow(params) === "setup") return {
|
|
47
40
|
flow: "setup",
|
|
48
41
|
params
|
|
49
42
|
};
|
|
50
|
-
if (flow === "confirm") {
|
|
51
|
-
const resolvedVerifier$1 = requireTotpVerifier(verifier);
|
|
52
|
-
return {
|
|
53
|
-
flow: "confirm",
|
|
54
|
-
code: requireTotpCode(params),
|
|
55
|
-
totpId: requireTotpId(params),
|
|
56
|
-
verifier: resolvedVerifier$1
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
43
|
const resolvedVerifier = requireTotpVerifier(verifier);
|
|
44
|
+
const code = requireTotpCode(params);
|
|
45
|
+
if (typeof params.totpId === "string" && params.totpId.length > 0) return {
|
|
46
|
+
flow: "verify",
|
|
47
|
+
code,
|
|
48
|
+
totpId: params.totpId,
|
|
49
|
+
verifier: resolvedVerifier,
|
|
50
|
+
intent: "enrollment"
|
|
51
|
+
};
|
|
60
52
|
return {
|
|
61
53
|
flow: "verify",
|
|
62
|
-
code
|
|
63
|
-
verifier: resolvedVerifier
|
|
54
|
+
code,
|
|
55
|
+
verifier: resolvedVerifier,
|
|
56
|
+
intent: "challenge"
|
|
64
57
|
};
|
|
65
58
|
}
|
|
66
59
|
async function requireAuthenticatedUserId(ctx) {
|
|
@@ -76,7 +69,7 @@ async function requireAuthenticatedUserId(ctx) {
|
|
|
76
69
|
/** @internal */
|
|
77
70
|
const handleTotp = async (ctx, provider, args) => {
|
|
78
71
|
const dispatch = resolveTotpDispatch(args.params ?? {}, args.verifier);
|
|
79
|
-
const
|
|
72
|
+
const flowHandlers = {
|
|
80
73
|
setup: async () => {
|
|
81
74
|
const { params: setupParams } = dispatch;
|
|
82
75
|
const userId = await requireAuthenticatedUserId(ctx);
|
|
@@ -94,17 +87,6 @@ const handleTotp = async (ctx, provider, args) => {
|
|
|
94
87
|
}
|
|
95
88
|
const uri = createTOTPKeyURI(provider.options.issuer, accountName, secret, provider.options.period, provider.options.digits);
|
|
96
89
|
const base32Secret = encodeBase32LowerCaseNoPadding(secret);
|
|
97
|
-
let verifier;
|
|
98
|
-
try {
|
|
99
|
-
verifier = await callVerifier(ctx, JSON.stringify({
|
|
100
|
-
secret: Array.from(secret),
|
|
101
|
-
userId,
|
|
102
|
-
digits: provider.options.digits,
|
|
103
|
-
period: provider.options.period
|
|
104
|
-
}));
|
|
105
|
-
} catch (error) {
|
|
106
|
-
throw asConvexError(error, "INTERNAL_ERROR", `TOTP setup failed: ${String(error)}`);
|
|
107
|
-
}
|
|
108
90
|
let totpId;
|
|
109
91
|
try {
|
|
110
92
|
totpId = await mutateTotpInsert(ctx, {
|
|
@@ -119,6 +101,19 @@ const handleTotp = async (ctx, provider, args) => {
|
|
|
119
101
|
} catch (error) {
|
|
120
102
|
throw asConvexError(error, "INTERNAL_ERROR", `TOTP setup failed: ${String(error)}`);
|
|
121
103
|
}
|
|
104
|
+
let verifier;
|
|
105
|
+
try {
|
|
106
|
+
verifier = await callVerifier(ctx, JSON.stringify({
|
|
107
|
+
purpose: "totp.setup",
|
|
108
|
+
secret: Array.from(secret),
|
|
109
|
+
userId,
|
|
110
|
+
totpId,
|
|
111
|
+
digits: provider.options.digits,
|
|
112
|
+
period: provider.options.period
|
|
113
|
+
}));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
throw asConvexError(error, "INTERNAL_ERROR", `TOTP setup failed: ${String(error)}`);
|
|
116
|
+
}
|
|
122
117
|
return {
|
|
123
118
|
kind: "totpSetup",
|
|
124
119
|
totpSetup: {
|
|
@@ -129,69 +124,101 @@ const handleTotp = async (ctx, provider, args) => {
|
|
|
129
124
|
verifier
|
|
130
125
|
};
|
|
131
126
|
},
|
|
132
|
-
confirm: async () => {
|
|
133
|
-
const { code, totpId, verifier } = dispatch;
|
|
134
|
-
const userId = await requireAuthenticatedUserId(ctx);
|
|
135
|
-
let doc;
|
|
136
|
-
try {
|
|
137
|
-
doc = await queryTotpById(ctx, totpId);
|
|
138
|
-
} catch {
|
|
139
|
-
throw convexError("TOTP_NOT_FOUND", "TOTP enrollment not found.");
|
|
140
|
-
}
|
|
141
|
-
if (doc === null) throw convexError("TOTP_NOT_FOUND", "TOTP enrollment not found.");
|
|
142
|
-
if (doc.verified) throw convexError("TOTP_ALREADY_VERIFIED", "TOTP enrollment is already verified.");
|
|
143
|
-
if (!verifyTOTPWithGracePeriod(new Uint8Array(doc.secret), provider.options.period, provider.options.digits, code, 30)) throw convexError("TOTP_INVALID_CODE", "Invalid TOTP code.");
|
|
144
|
-
let signInResult;
|
|
145
|
-
try {
|
|
146
|
-
await mutateTotpMarkVerified(ctx, totpId, Date.now());
|
|
147
|
-
await mutateVerifierDelete(ctx, verifier);
|
|
148
|
-
signInResult = await callSignIn(ctx, {
|
|
149
|
-
userId,
|
|
150
|
-
generateTokens: true
|
|
151
|
-
});
|
|
152
|
-
} catch (error) {
|
|
153
|
-
throw asConvexError(error, "INTERNAL_ERROR", String(error));
|
|
154
|
-
}
|
|
155
|
-
return {
|
|
156
|
-
kind: "signedIn",
|
|
157
|
-
session: signInResult
|
|
158
|
-
};
|
|
159
|
-
},
|
|
160
127
|
verify: async () => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
128
|
+
if (dispatch.flow !== "verify") throw convexError("TOTP_MISSING_FLOW", `Unexpected dispatch: ${dispatch.flow}`);
|
|
129
|
+
if (dispatch.intent === "enrollment") return await confirmEnrollment(dispatch.code, dispatch.totpId, dispatch.verifier);
|
|
130
|
+
return await verifyChallenge(dispatch.code, dispatch.verifier);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* `verify` with `totpId`: completes a first-time enrollment after `setup`.
|
|
135
|
+
* Marks the TOTP factor as verified and signs the user in.
|
|
136
|
+
*/
|
|
137
|
+
async function confirmEnrollment(code, totpId, verifier) {
|
|
138
|
+
const userId = await requireAuthenticatedUserId(ctx);
|
|
139
|
+
let verifierDoc;
|
|
140
|
+
try {
|
|
141
|
+
verifierDoc = await queryVerifierById(ctx, verifier);
|
|
142
|
+
} catch {
|
|
143
|
+
throw convexError("TOTP_INVALID_VERIFIER", "Invalid or expired TOTP verifier.");
|
|
144
|
+
}
|
|
145
|
+
if (verifierDoc === null) throw convexError("TOTP_INVALID_VERIFIER", "Invalid or expired TOTP verifier.");
|
|
146
|
+
let verifierData;
|
|
147
|
+
try {
|
|
148
|
+
verifierData = JSON.parse(verifierDoc.signature);
|
|
149
|
+
} catch {
|
|
150
|
+
throw convexError("TOTP_INVALID_VERIFIER", "Invalid or expired TOTP verifier.");
|
|
151
|
+
}
|
|
152
|
+
if (verifierData.purpose !== "totp.setup" || verifierData.userId !== userId || verifierData.totpId !== totpId) throw convexError("TOTP_INVALID_VERIFIER", "Invalid or expired TOTP verifier.");
|
|
153
|
+
let doc;
|
|
154
|
+
try {
|
|
155
|
+
doc = await queryTotpById(ctx, totpId);
|
|
156
|
+
} catch {
|
|
157
|
+
throw convexError("TOTP_NOT_FOUND", "TOTP enrollment not found.");
|
|
158
|
+
}
|
|
159
|
+
if (doc === null) throw convexError("TOTP_NOT_FOUND", "TOTP enrollment not found.");
|
|
160
|
+
if (doc.userId !== userId) throw convexError("TOTP_NOT_FOUND", "TOTP enrollment not found.");
|
|
161
|
+
if (doc.verified) throw convexError("TOTP_ALREADY_VERIFIED", "TOTP enrollment is already verified.");
|
|
162
|
+
if (!verifyTOTPWithGracePeriod(new Uint8Array(doc.secret), provider.options.period, provider.options.digits, code, 30)) throw convexError("TOTP_INVALID_CODE", "Invalid TOTP code.");
|
|
163
|
+
let signInResult;
|
|
164
|
+
try {
|
|
165
|
+
await mutateTotpMarkVerified(ctx, totpId, Date.now());
|
|
166
|
+
await mutateVerifierDelete(ctx, verifier);
|
|
167
|
+
signInResult = await callSignIn(ctx, {
|
|
168
|
+
userId,
|
|
169
|
+
generateTokens: true
|
|
170
|
+
});
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw asConvexError(error, "INTERNAL_ERROR", String(error));
|
|
173
|
+
}
|
|
174
|
+
await ctx.auth.config.callbacks?.after?.(ctx, {
|
|
175
|
+
kind: "totpEnrolled",
|
|
176
|
+
userId,
|
|
177
|
+
totpId
|
|
178
|
+
});
|
|
179
|
+
return {
|
|
180
|
+
kind: "signedIn",
|
|
181
|
+
session: signInResult
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* `verify` without `totpId`: completes a 2FA challenge during sign-in.
|
|
186
|
+
* Looks up the user's verified TOTP factor, validates the code, signs in.
|
|
187
|
+
*/
|
|
188
|
+
async function verifyChallenge(code, verifier) {
|
|
189
|
+
let doc;
|
|
190
|
+
try {
|
|
191
|
+
doc = await queryVerifierById(ctx, verifier);
|
|
192
|
+
} catch {
|
|
193
|
+
throw convexError("TOTP_INVALID_VERIFIER", "Invalid or expired TOTP verifier.");
|
|
194
|
+
}
|
|
195
|
+
if (doc === null) throw convexError("TOTP_INVALID_VERIFIER", "Invalid or expired TOTP verifier.");
|
|
196
|
+
const userId = JSON.parse(doc.signature).userId;
|
|
197
|
+
let totp;
|
|
198
|
+
try {
|
|
199
|
+
totp = await queryTotpVerifiedByUserId(ctx, userId);
|
|
200
|
+
} catch {
|
|
201
|
+
throw convexError("TOTP_NO_ENROLLMENT", "No verified TOTP enrollment found.");
|
|
193
202
|
}
|
|
194
|
-
|
|
203
|
+
if (totp === null) throw convexError("TOTP_NO_ENROLLMENT", "No verified TOTP enrollment found.");
|
|
204
|
+
if (!verifyTOTPWithGracePeriod(new Uint8Array(totp.secret), totp.period, totp.digits, code, 30)) throw convexError("TOTP_INVALID_CODE", "Invalid TOTP code.");
|
|
205
|
+
let signInResult;
|
|
206
|
+
try {
|
|
207
|
+
await mutateTotpUpdateLastUsed(ctx, totp._id, Date.now());
|
|
208
|
+
await mutateVerifierDelete(ctx, verifier);
|
|
209
|
+
signInResult = await callSignIn(ctx, {
|
|
210
|
+
userId,
|
|
211
|
+
generateTokens: true
|
|
212
|
+
});
|
|
213
|
+
} catch (error) {
|
|
214
|
+
throw asConvexError(error, "INTERNAL_ERROR", String(error));
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
kind: "signedIn",
|
|
218
|
+
session: signInResult
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const handler = flowHandlers[dispatch.flow];
|
|
195
222
|
if (!handler) throw convexError("TOTP_MISSING_FLOW", `Unknown TOTP flow: ${dispatch.flow}`);
|
|
196
223
|
return handler();
|
|
197
224
|
};
|