@robelest/convex-auth 0.0.4-preview.21 → 0.0.4-preview.23
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/authorization/index.d.ts +1 -1
- package/dist/authorization/index.js +1 -1
- package/dist/authorization/index.js.map +1 -1
- package/dist/client/index.d.ts +1 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +36 -39
- package/dist/client/index.js.map +1 -1
- package/dist/component/client/index.d.ts +1 -2
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/model.d.ts +5 -5
- package/dist/component/model.d.ts.map +1 -1
- package/dist/component/public/enterprise/audit.d.ts.map +1 -1
- package/dist/component/public/enterprise/audit.js.map +1 -1
- package/dist/component/public/enterprise/core.d.ts.map +1 -1
- package/dist/component/public/enterprise/core.js.map +1 -1
- package/dist/component/public/enterprise/domains.d.ts.map +1 -1
- package/dist/component/public/enterprise/domains.js.map +1 -1
- package/dist/component/public/enterprise/scim.d.ts.map +1 -1
- package/dist/component/public/enterprise/scim.js.map +1 -1
- package/dist/component/public/enterprise/secrets.d.ts.map +1 -1
- package/dist/component/public/enterprise/secrets.js.map +1 -1
- package/dist/component/public/enterprise/webhooks.d.ts.map +1 -1
- package/dist/component/public/enterprise/webhooks.js.map +1 -1
- package/dist/component/public/factors/devices.d.ts.map +1 -1
- package/dist/component/public/factors/devices.js.map +1 -1
- package/dist/component/public/factors/passkeys.d.ts.map +1 -1
- package/dist/component/public/factors/passkeys.js.map +1 -1
- package/dist/component/public/factors/totp.d.ts.map +1 -1
- package/dist/component/public/factors/totp.js.map +1 -1
- package/dist/component/public/groups/core.js.map +1 -1
- package/dist/component/public/groups/invites.d.ts.map +1 -1
- package/dist/component/public/groups/invites.js.map +1 -1
- package/dist/component/public/groups/members.d.ts.map +1 -1
- package/dist/component/public/groups/members.js.map +1 -1
- package/dist/component/public/identity/accounts.d.ts.map +1 -1
- package/dist/component/public/identity/accounts.js.map +1 -1
- package/dist/component/public/identity/codes.d.ts.map +1 -1
- package/dist/component/public/identity/codes.js.map +1 -1
- package/dist/component/public/identity/sessions.d.ts.map +1 -1
- package/dist/component/public/identity/sessions.js.map +1 -1
- package/dist/component/public/identity/tokens.d.ts.map +1 -1
- package/dist/component/public/identity/tokens.js.map +1 -1
- package/dist/component/public/identity/users.d.ts.map +1 -1
- package/dist/component/public/identity/users.js.map +1 -1
- package/dist/component/public/identity/verifiers.d.ts.map +1 -1
- package/dist/component/public/identity/verifiers.js.map +1 -1
- package/dist/component/public/security/keys.d.ts.map +1 -1
- package/dist/component/public/security/keys.js.map +1 -1
- package/dist/component/public/security/limits.d.ts.map +1 -1
- package/dist/component/public/security/limits.js.map +1 -1
- package/dist/component/schema.d.ts +39 -39
- package/dist/component/server/auth.d.ts +95 -52
- package/dist/component/server/auth.d.ts.map +1 -1
- package/dist/component/server/auth.js +63 -43
- package/dist/component/server/auth.js.map +1 -1
- package/dist/component/server/core.js +116 -235
- package/dist/component/server/core.js.map +1 -1
- package/dist/component/server/crypto.js +25 -7
- package/dist/component/server/crypto.js.map +1 -1
- package/dist/component/server/device.js +58 -15
- package/dist/component/server/device.js.map +1 -1
- package/dist/component/server/enterprise/domain.js +148 -59
- package/dist/component/server/enterprise/domain.js.map +1 -1
- package/dist/component/server/enterprise/http.js +36 -15
- package/dist/component/server/enterprise/http.js.map +1 -1
- package/dist/component/server/enterprise/oidc.js +1 -1
- package/dist/component/server/http.js +26 -21
- package/dist/component/server/http.js.map +1 -1
- package/dist/component/server/identity.js +5 -2
- package/dist/component/server/identity.js.map +1 -1
- package/dist/component/server/limits.js +21 -30
- package/dist/component/server/limits.js.map +1 -1
- package/dist/component/server/mutations/account.js +12 -10
- package/dist/component/server/mutations/account.js.map +1 -1
- package/dist/component/server/mutations/code.js +5 -2
- package/dist/component/server/mutations/code.js.map +1 -1
- package/dist/component/server/mutations/invalidate.js +1 -1
- package/dist/component/server/mutations/invalidate.js.map +1 -1
- package/dist/component/server/mutations/oauth.js +10 -4
- package/dist/component/server/mutations/oauth.js.map +1 -1
- package/dist/component/server/mutations/refresh.js +2 -2
- package/dist/component/server/mutations/refresh.js.map +1 -1
- package/dist/component/server/mutations/register.js +46 -42
- package/dist/component/server/mutations/register.js.map +1 -1
- package/dist/component/server/mutations/retrieve.js +21 -25
- package/dist/component/server/mutations/retrieve.js.map +1 -1
- package/dist/component/server/mutations/signature.js +10 -4
- package/dist/component/server/mutations/signature.js.map +1 -1
- package/dist/component/server/mutations/signout.js.map +1 -1
- package/dist/component/server/mutations/store.js +9 -24
- package/dist/component/server/mutations/store.js.map +1 -1
- package/dist/component/server/mutations/verifier.js.map +1 -1
- package/dist/component/server/mutations/verify.js +1 -1
- package/dist/component/server/mutations/verify.js.map +1 -1
- package/dist/component/server/oauth.js +53 -16
- package/dist/component/server/oauth.js.map +1 -1
- package/dist/component/server/passkey.js +115 -31
- package/dist/component/server/passkey.js.map +1 -1
- package/dist/component/server/redirects.js +9 -3
- package/dist/component/server/redirects.js.map +1 -1
- package/dist/component/server/refresh.js +10 -7
- package/dist/component/server/refresh.js.map +1 -1
- package/dist/component/server/runtime.d.ts +3 -3
- package/dist/component/server/runtime.d.ts.map +1 -1
- package/dist/component/server/runtime.js +62 -20
- package/dist/component/server/runtime.js.map +1 -1
- package/dist/component/server/signin.js +34 -10
- package/dist/component/server/signin.js.map +1 -1
- package/dist/component/server/totp.js +79 -19
- package/dist/component/server/totp.js.map +1 -1
- package/dist/component/server/types.d.ts +12 -20
- package/dist/component/server/types.d.ts.map +1 -1
- package/dist/component/server/types.js.map +1 -1
- package/dist/component/server/users.js +6 -3
- package/dist/component/server/users.js.map +1 -1
- package/dist/component/server/utils.js +10 -4
- package/dist/component/server/utils.js.map +1 -1
- package/dist/core/types.d.ts +14 -22
- package/dist/core/types.d.ts.map +1 -1
- package/dist/factors/device.js +8 -9
- package/dist/factors/device.js.map +1 -1
- package/dist/factors/passkey.js +18 -21
- package/dist/factors/passkey.js.map +1 -1
- package/dist/providers/password.js +66 -81
- package/dist/providers/password.js.map +1 -1
- package/dist/runtime/invite.js +2 -8
- package/dist/runtime/invite.js.map +1 -1
- package/dist/server/auth.d.ts +95 -52
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +63 -43
- package/dist/server/auth.js.map +1 -1
- package/dist/server/core.d.ts +71 -159
- package/dist/server/core.d.ts.map +1 -1
- package/dist/server/core.js +116 -235
- package/dist/server/core.js.map +1 -1
- package/dist/server/crypto.d.ts.map +1 -1
- package/dist/server/crypto.js +25 -7
- package/dist/server/crypto.js.map +1 -1
- package/dist/server/device.js +58 -15
- package/dist/server/device.js.map +1 -1
- package/dist/server/enterprise/domain.d.ts +0 -8
- package/dist/server/enterprise/domain.d.ts.map +1 -1
- package/dist/server/enterprise/domain.js +148 -59
- package/dist/server/enterprise/domain.js.map +1 -1
- package/dist/server/enterprise/http.d.ts.map +1 -1
- package/dist/server/enterprise/http.js +35 -14
- package/dist/server/enterprise/http.js.map +1 -1
- package/dist/server/http.d.ts +2 -2
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +25 -20
- package/dist/server/http.js.map +1 -1
- package/dist/server/identity.js +5 -2
- package/dist/server/identity.js.map +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/server/limits.js +21 -30
- package/dist/server/limits.js.map +1 -1
- package/dist/server/mounts.d.ts +26 -64
- package/dist/server/mounts.d.ts.map +1 -1
- package/dist/server/mounts.js +45 -106
- package/dist/server/mounts.js.map +1 -1
- package/dist/server/mutations/account.d.ts +8 -9
- package/dist/server/mutations/account.d.ts.map +1 -1
- package/dist/server/mutations/account.js +11 -9
- package/dist/server/mutations/account.js.map +1 -1
- package/dist/server/mutations/code.d.ts +13 -13
- package/dist/server/mutations/code.d.ts.map +1 -1
- package/dist/server/mutations/code.js +5 -2
- package/dist/server/mutations/code.js.map +1 -1
- package/dist/server/mutations/invalidate.d.ts +4 -4
- package/dist/server/mutations/invalidate.d.ts.map +1 -1
- package/dist/server/mutations/invalidate.js.map +1 -1
- package/dist/server/mutations/oauth.d.ts +12 -10
- package/dist/server/mutations/oauth.d.ts.map +1 -1
- package/dist/server/mutations/oauth.js +9 -3
- package/dist/server/mutations/oauth.js.map +1 -1
- package/dist/server/mutations/refresh.d.ts +3 -3
- package/dist/server/mutations/refresh.d.ts.map +1 -1
- package/dist/server/mutations/refresh.js +1 -1
- package/dist/server/mutations/refresh.js.map +1 -1
- package/dist/server/mutations/register.d.ts +11 -11
- package/dist/server/mutations/register.d.ts.map +1 -1
- package/dist/server/mutations/register.js +45 -41
- package/dist/server/mutations/register.js.map +1 -1
- package/dist/server/mutations/retrieve.d.ts +6 -6
- package/dist/server/mutations/retrieve.d.ts.map +1 -1
- package/dist/server/mutations/retrieve.js +20 -24
- package/dist/server/mutations/retrieve.js.map +1 -1
- package/dist/server/mutations/signature.d.ts +6 -7
- package/dist/server/mutations/signature.d.ts.map +1 -1
- package/dist/server/mutations/signature.js +9 -3
- package/dist/server/mutations/signature.js.map +1 -1
- package/dist/server/mutations/signin.d.ts +5 -5
- package/dist/server/mutations/signin.d.ts.map +1 -1
- package/dist/server/mutations/signout.js.map +1 -1
- package/dist/server/mutations/store.d.ts +97 -97
- package/dist/server/mutations/store.d.ts.map +1 -1
- package/dist/server/mutations/store.js +8 -23
- package/dist/server/mutations/store.js.map +1 -1
- package/dist/server/mutations/verifier.js.map +1 -1
- package/dist/server/mutations/verify.d.ts +10 -10
- package/dist/server/mutations/verify.d.ts.map +1 -1
- package/dist/server/mutations/verify.js.map +1 -1
- package/dist/server/oauth.js +53 -16
- package/dist/server/oauth.js.map +1 -1
- package/dist/server/passkey.d.ts +2 -2
- package/dist/server/passkey.d.ts.map +1 -1
- package/dist/server/passkey.js +114 -30
- package/dist/server/passkey.js.map +1 -1
- package/dist/server/redirects.js +9 -3
- package/dist/server/redirects.js.map +1 -1
- package/dist/server/refresh.js +10 -7
- package/dist/server/refresh.js.map +1 -1
- package/dist/server/runtime.d.ts +14 -14
- package/dist/server/runtime.d.ts.map +1 -1
- package/dist/server/runtime.js +61 -19
- package/dist/server/runtime.js.map +1 -1
- package/dist/server/signin.js +34 -10
- package/dist/server/signin.js.map +1 -1
- package/dist/server/ssr.d.ts.map +1 -1
- package/dist/server/ssr.js +175 -184
- package/dist/server/ssr.js.map +1 -1
- package/dist/server/totp.js +78 -18
- package/dist/server/totp.js.map +1 -1
- package/dist/server/types.d.ts +13 -21
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/dist/server/users.js +6 -3
- package/dist/server/users.js.map +1 -1
- package/dist/server/utils.js +10 -4
- package/dist/server/utils.js.map +1 -1
- package/package.json +2 -6
- package/src/authorization/index.ts +1 -1
- package/src/cli/index.ts +1 -1
- package/src/client/core/types.ts +14 -14
- package/src/client/factors/device.ts +10 -12
- package/src/client/factors/passkey.ts +23 -26
- package/src/client/index.ts +54 -64
- package/src/client/runtime/invite.ts +5 -7
- package/src/component/index.ts +1 -0
- package/src/component/public/enterprise/audit.ts +6 -1
- package/src/component/public/enterprise/core.ts +1 -0
- package/src/component/public/enterprise/domains.ts +5 -1
- package/src/component/public/enterprise/scim.ts +1 -0
- package/src/component/public/enterprise/secrets.ts +1 -0
- package/src/component/public/enterprise/webhooks.ts +1 -0
- package/src/component/public/factors/devices.ts +1 -0
- package/src/component/public/factors/passkeys.ts +1 -0
- package/src/component/public/factors/totp.ts +1 -0
- package/src/component/public/groups/core.ts +1 -1
- package/src/component/public/groups/invites.ts +7 -1
- package/src/component/public/groups/members.ts +1 -0
- package/src/component/public/identity/accounts.ts +1 -0
- package/src/component/public/identity/codes.ts +1 -0
- package/src/component/public/identity/sessions.ts +1 -0
- package/src/component/public/identity/tokens.ts +1 -0
- package/src/component/public/identity/users.ts +1 -0
- package/src/component/public/identity/verifiers.ts +1 -0
- package/src/component/public/security/keys.ts +1 -0
- package/src/component/public/security/limits.ts +1 -0
- package/src/providers/password.ts +89 -110
- package/src/server/auth.ts +177 -111
- package/src/server/core.ts +197 -233
- package/src/server/crypto.ts +31 -29
- package/src/server/device.ts +65 -32
- package/src/server/enterprise/domain.ts +158 -170
- package/src/server/enterprise/http.ts +46 -39
- package/src/server/http.ts +36 -30
- package/src/server/identity.ts +5 -5
- package/src/server/index.ts +2 -0
- package/src/server/limits.ts +53 -80
- package/src/server/mounts.ts +47 -74
- package/src/server/mutations/account.ts +22 -36
- package/src/server/mutations/code.ts +6 -6
- package/src/server/mutations/invalidate.ts +1 -1
- package/src/server/mutations/oauth.ts +14 -8
- package/src/server/mutations/refresh.ts +5 -4
- package/src/server/mutations/register.ts +87 -132
- package/src/server/mutations/retrieve.ts +44 -44
- package/src/server/mutations/signature.ts +13 -6
- package/src/server/mutations/signout.ts +1 -1
- package/src/server/mutations/store.ts +16 -31
- package/src/server/mutations/verifier.ts +1 -1
- package/src/server/mutations/verify.ts +3 -5
- package/src/server/oauth.ts +60 -69
- package/src/server/passkey.ts +567 -517
- package/src/server/redirects.ts +10 -6
- package/src/server/refresh.ts +14 -18
- package/src/server/runtime.ts +70 -55
- package/src/server/signin.ts +44 -37
- package/src/server/ssr.ts +390 -407
- package/src/server/totp.ts +85 -35
- package/src/server/types.ts +19 -22
- package/src/server/users.ts +7 -6
- package/src/server/utils.ts +10 -12
- package/dist/component/server/authError.js +0 -34
- package/dist/component/server/authError.js.map +0 -1
- package/dist/component/server/errors.d.ts +0 -1
- package/dist/component/server/errors.js +0 -137
- package/dist/component/server/errors.js.map +0 -1
- package/dist/server/authError.d.ts +0 -46
- package/dist/server/authError.d.ts.map +0 -1
- package/dist/server/authError.js +0 -34
- package/dist/server/authError.js.map +0 -1
- package/dist/server/errors.d.ts +0 -177
- package/dist/server/errors.d.ts.map +0 -1
- package/dist/server/errors.js +0 -212
- package/dist/server/errors.js.map +0 -1
- package/src/server/authError.ts +0 -44
- package/src/server/errors.ts +0 -290
package/src/server/passkey.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Uses `@oslojs/webauthn` for attestation/assertion parsing and
|
|
11
11
|
* `@oslojs/crypto` for signature verification.
|
|
12
12
|
*
|
|
13
|
-
* All functions return `Fx<A,
|
|
13
|
+
* All functions return `Fx<A, ConvexError<any>>` composed via `Fx.chain` pipelines.
|
|
14
14
|
*
|
|
15
15
|
* @module
|
|
16
16
|
*/
|
|
@@ -43,11 +43,11 @@ import {
|
|
|
43
43
|
COSEKeyType,
|
|
44
44
|
} from "@oslojs/webauthn";
|
|
45
45
|
import type { Fx as FxType } from "@robelest/fx";
|
|
46
|
-
|
|
47
|
-
import { authDb } from "./db";
|
|
48
46
|
import { Fx } from "@robelest/fx";
|
|
47
|
+
import { Cv } from "@robelest/fx/convex";
|
|
48
|
+
import type { ConvexError } from "convex/values";
|
|
49
49
|
|
|
50
|
-
import {
|
|
50
|
+
import { authDb } from "./db";
|
|
51
51
|
import { userIdFromIdentitySubject } from "./identity";
|
|
52
52
|
import { callSignIn, callVerifier } from "./mutations/index";
|
|
53
53
|
import { callVerifierSignature } from "./mutations/signature";
|
|
@@ -87,12 +87,12 @@ interface RpOptions {
|
|
|
87
87
|
/**
|
|
88
88
|
* Resolve passkey relying party options from provider config and environment.
|
|
89
89
|
*
|
|
90
|
-
* Returns `Fx<RpOptions,
|
|
90
|
+
* Returns `Fx<RpOptions, ConvexError<any>>` — fails if neither SITE_URL nor rpId
|
|
91
91
|
* is configured.
|
|
92
92
|
*/
|
|
93
93
|
const resolveRpOptionsFx = (
|
|
94
94
|
provider: PasskeyProviderConfig,
|
|
95
|
-
): FxType<RpOptions,
|
|
95
|
+
): FxType<RpOptions, ConvexError<any>> => {
|
|
96
96
|
const siteUrl = process.env.SITE_URL;
|
|
97
97
|
const hasSiteUrl = siteUrl !== undefined && siteUrl !== "";
|
|
98
98
|
const hasRpId = provider.options.rpId !== undefined;
|
|
@@ -100,14 +100,13 @@ const resolveRpOptionsFx = (
|
|
|
100
100
|
return Fx.succeed({ siteUrl, hasSiteUrl, hasRpId }).pipe(
|
|
101
101
|
Fx.chain(({ siteUrl, hasSiteUrl, hasRpId }) =>
|
|
102
102
|
!hasSiteUrl && !hasRpId
|
|
103
|
-
?
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
? Cv.fail({
|
|
104
|
+
code: "PASSKEY_MISSING_CONFIG",
|
|
105
|
+
message:
|
|
106
106
|
"Passkey provider requires SITE_URL env var (your frontend URL) " +
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
107
|
+
"or explicit rpId / origin in the provider config. " +
|
|
108
|
+
"CONVEX_SITE_URL cannot be used because WebAuthn RP ID must match the frontend domain.",
|
|
109
|
+
})
|
|
111
110
|
: Fx.succeed(siteUrl),
|
|
112
111
|
),
|
|
113
112
|
Fx.map((siteUrl) => {
|
|
@@ -132,7 +131,7 @@ const resolveRpOptionsFx = (
|
|
|
132
131
|
};
|
|
133
132
|
|
|
134
133
|
// ============================================================================
|
|
135
|
-
// Composable validators — small functions (A) => Fx<B,
|
|
134
|
+
// Composable validators — small functions (A) => Fx<B, ConvexError<any>>
|
|
136
135
|
// ============================================================================
|
|
137
136
|
|
|
138
137
|
/** Verify client data type matches expected WebAuthn ceremony type. */
|
|
@@ -141,29 +140,27 @@ const verifyClientDataType =
|
|
|
141
140
|
expectedType: ClientDataType,
|
|
142
141
|
label: string,
|
|
143
142
|
) =>
|
|
144
|
-
(clientData: T): FxType<T,
|
|
143
|
+
(clientData: T): FxType<T, ConvexError<any>> =>
|
|
145
144
|
clientData.type === expectedType
|
|
146
145
|
? Fx.succeed(clientData)
|
|
147
|
-
:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
),
|
|
152
|
-
);
|
|
146
|
+
: Cv.fail({
|
|
147
|
+
code: "PASSKEY_INVALID_CLIENT_DATA",
|
|
148
|
+
message: `Invalid client data type: expected ${label}`,
|
|
149
|
+
});
|
|
153
150
|
|
|
154
151
|
/** Verify origin is in the allowed list. */
|
|
155
152
|
const verifyOrigin =
|
|
156
153
|
(rp: RpOptions) =>
|
|
157
|
-
<T extends { origin: string }>(
|
|
154
|
+
<T extends { origin: string }>(
|
|
155
|
+
clientData: T,
|
|
156
|
+
): FxType<T, ConvexError<any>> => {
|
|
158
157
|
const allowed = Array.isArray(rp.origin) ? rp.origin : [rp.origin];
|
|
159
158
|
return allowed.includes(clientData.origin)
|
|
160
159
|
? Fx.succeed(clientData)
|
|
161
|
-
:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
),
|
|
166
|
-
);
|
|
160
|
+
: Cv.fail({
|
|
161
|
+
code: "PASSKEY_INVALID_ORIGIN",
|
|
162
|
+
message: `Invalid origin: ${clientData.origin}, expected one of: ${allowed.join(", ")}`,
|
|
163
|
+
});
|
|
167
164
|
};
|
|
168
165
|
|
|
169
166
|
/** Verify the challenge hash matches the stored verifier, then delete verifier. */
|
|
@@ -171,23 +168,34 @@ const verifyAndConsumeChallenge =
|
|
|
171
168
|
(ctx: EnrichedActionCtx, verifierValue: string) =>
|
|
172
169
|
<T extends { challenge: Uint8Array }>(
|
|
173
170
|
clientData: T,
|
|
174
|
-
): FxType<T,
|
|
171
|
+
): FxType<T, ConvexError<any>> => {
|
|
175
172
|
const challengeHash = encodeBase64urlNoPadding(
|
|
176
173
|
new Uint8Array(sha256(clientData.challenge)),
|
|
177
174
|
);
|
|
178
175
|
return Fx.from({
|
|
179
176
|
ok: () => queryVerifierById(ctx, verifierValue),
|
|
180
|
-
err: () =>
|
|
177
|
+
err: () =>
|
|
178
|
+
Cv.error({
|
|
179
|
+
code: "PASSKEY_INVALID_CHALLENGE",
|
|
180
|
+
message: "Invalid or expired passkey challenge.",
|
|
181
|
+
}),
|
|
181
182
|
}).pipe(
|
|
182
183
|
Fx.chain((doc) =>
|
|
183
184
|
!doc || doc.signature !== challengeHash
|
|
184
|
-
?
|
|
185
|
+
? Cv.fail({
|
|
186
|
+
code: "PASSKEY_INVALID_CHALLENGE",
|
|
187
|
+
message: "Invalid or expired passkey challenge.",
|
|
188
|
+
})
|
|
185
189
|
: Fx.succeed(doc),
|
|
186
190
|
),
|
|
187
191
|
Fx.chain(() =>
|
|
188
192
|
Fx.from({
|
|
189
193
|
ok: () => mutateVerifierDelete(ctx, verifierValue),
|
|
190
|
-
err: () =>
|
|
194
|
+
err: () =>
|
|
195
|
+
Cv.error({
|
|
196
|
+
code: "PASSKEY_INVALID_CHALLENGE",
|
|
197
|
+
message: "Invalid or expired passkey challenge.",
|
|
198
|
+
}),
|
|
191
199
|
}),
|
|
192
200
|
),
|
|
193
201
|
Fx.map(() => clientData),
|
|
@@ -199,21 +207,30 @@ const verifyRpId =
|
|
|
199
207
|
(rpId: string) =>
|
|
200
208
|
<T extends { verifyRelyingPartyIdHash: (id: string) => boolean }>(
|
|
201
209
|
authData: T,
|
|
202
|
-
): FxType<T,
|
|
210
|
+
): FxType<T, ConvexError<any>> =>
|
|
203
211
|
authData.verifyRelyingPartyIdHash(rpId)
|
|
204
212
|
? Fx.succeed(authData)
|
|
205
|
-
:
|
|
213
|
+
: Cv.fail({
|
|
214
|
+
code: "PASSKEY_RP_MISMATCH",
|
|
215
|
+
message: "Relying party ID mismatch.",
|
|
216
|
+
});
|
|
206
217
|
|
|
207
218
|
/** Verify user presence and (optionally) user verification flags. */
|
|
208
219
|
const verifyUserFlags =
|
|
209
220
|
(rp: RpOptions) =>
|
|
210
221
|
<T extends { userPresent: boolean; userVerified: boolean }>(
|
|
211
222
|
authData: T,
|
|
212
|
-
): FxType<T,
|
|
223
|
+
): FxType<T, ConvexError<any>> =>
|
|
213
224
|
!authData.userPresent
|
|
214
|
-
?
|
|
225
|
+
? Cv.fail({
|
|
226
|
+
code: "PASSKEY_USER_PRESENCE",
|
|
227
|
+
message: "User presence flag not set.",
|
|
228
|
+
})
|
|
215
229
|
: rp.userVerification === "required" && !authData.userVerified
|
|
216
|
-
?
|
|
230
|
+
? Cv.fail({
|
|
231
|
+
code: "PASSKEY_USER_VERIFICATION",
|
|
232
|
+
message: "User verification required but not performed.",
|
|
233
|
+
})
|
|
217
234
|
: Fx.succeed(authData);
|
|
218
235
|
|
|
219
236
|
// ============================================================================
|
|
@@ -255,24 +272,26 @@ type PasskeyDispatch =
|
|
|
255
272
|
|
|
256
273
|
const resolvePasskeyDispatchFx = (
|
|
257
274
|
params: Record<string, unknown>,
|
|
258
|
-
): FxType<PasskeyDispatch,
|
|
275
|
+
): FxType<PasskeyDispatch, ConvexError<any>> => {
|
|
259
276
|
const flow = params.flow;
|
|
260
277
|
return typeof flow === "string" && PASSKEY_FLOWS.includes(flow as never)
|
|
261
278
|
? Fx.succeed({ flow: flow as (typeof PASSKEY_FLOWS)[number] })
|
|
262
|
-
:
|
|
263
|
-
|
|
264
|
-
|
|
279
|
+
: Cv.fail({
|
|
280
|
+
code: "PASSKEY_MISSING_FLOW",
|
|
281
|
+
message:
|
|
265
282
|
"Missing `flow` parameter. Expected one of: registerOptions, registerVerify, authOptions, authVerify",
|
|
266
|
-
|
|
267
|
-
);
|
|
283
|
+
});
|
|
268
284
|
};
|
|
269
285
|
|
|
270
286
|
const requirePasskeyVerifierFx = (
|
|
271
287
|
verifier: string | undefined,
|
|
272
|
-
): FxType<string,
|
|
288
|
+
): FxType<string, ConvexError<any>> =>
|
|
273
289
|
verifier != null
|
|
274
290
|
? Fx.succeed(verifier)
|
|
275
|
-
:
|
|
291
|
+
: Cv.fail({
|
|
292
|
+
code: "PASSKEY_MISSING_VERIFIER",
|
|
293
|
+
message: "Missing verifier for passkey operation.",
|
|
294
|
+
});
|
|
276
295
|
|
|
277
296
|
/**
|
|
278
297
|
* Main passkey handler dispatched from signIn.ts.
|
|
@@ -286,498 +305,529 @@ export function handlePasskeyFx(
|
|
|
286
305
|
params?: Record<string, any>;
|
|
287
306
|
verifier?: string;
|
|
288
307
|
},
|
|
289
|
-
): FxType<PasskeyResult,
|
|
308
|
+
): FxType<PasskeyResult, ConvexError<any>> {
|
|
290
309
|
const params = (args.params ?? {}) as Record<string, any>;
|
|
291
310
|
|
|
292
311
|
return resolvePasskeyDispatchFx(params).pipe(
|
|
293
312
|
Fx.chain((dispatch) => {
|
|
294
|
-
const flowFx: FxType<PasskeyResult,
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
313
|
+
const flowFx: FxType<PasskeyResult, ConvexError<any>> = Fx.match(
|
|
314
|
+
dispatch,
|
|
315
|
+
).on("flow", {
|
|
316
|
+
registerOptions: (_) =>
|
|
317
|
+
Fx.zip(
|
|
318
|
+
Fx.from({
|
|
319
|
+
ok: () => ctx.auth.getUserIdentity(),
|
|
320
|
+
err: () =>
|
|
321
|
+
Cv.error({
|
|
322
|
+
code: "PASSKEY_AUTH_REQUIRED",
|
|
323
|
+
message: "Sign in first, then add a passkey to your account.",
|
|
324
|
+
}),
|
|
325
|
+
}).pipe(
|
|
326
|
+
Fx.chain((id) =>
|
|
327
|
+
id === null
|
|
328
|
+
? Cv.fail({
|
|
329
|
+
code: "PASSKEY_AUTH_REQUIRED",
|
|
330
|
+
message:
|
|
331
|
+
"Sign in first, then add a passkey to your account.",
|
|
332
|
+
})
|
|
333
|
+
: Fx.succeed(userIdFromIdentitySubject(id.subject)),
|
|
308
334
|
),
|
|
309
|
-
resolveRpOptionsFx(provider),
|
|
310
|
-
).pipe(
|
|
311
|
-
Fx.chain(([userId, rp]) => {
|
|
312
|
-
const challenge = new Uint8Array(32);
|
|
313
|
-
crypto.getRandomValues(challenge);
|
|
314
|
-
const challengeHash = encodeBase64urlNoPadding(
|
|
315
|
-
new Uint8Array(sha256(challenge)),
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
return Fx.from({
|
|
319
|
-
ok: async () => {
|
|
320
|
-
const verifier = await callVerifier(ctx);
|
|
321
|
-
await callVerifierSignature(ctx, {
|
|
322
|
-
verifier,
|
|
323
|
-
signature: challengeHash,
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
const user = await queryUserById(ctx, userId);
|
|
327
|
-
const userName = params.userName ?? user?.email ?? "user";
|
|
328
|
-
const userDisplayName =
|
|
329
|
-
params.userDisplayName ?? user?.name ?? userName;
|
|
330
|
-
|
|
331
|
-
const existing = await queryPasskeysByUserId(ctx, userId);
|
|
332
|
-
const excludeCredentials = existing.map((pk) => ({
|
|
333
|
-
id: pk.credentialId,
|
|
334
|
-
transports: pk.transports,
|
|
335
|
-
}));
|
|
336
|
-
|
|
337
|
-
const userHandle = encodeBase64urlNoPadding(
|
|
338
|
-
new TextEncoder().encode(userId),
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
const options = {
|
|
342
|
-
rp: { name: rp.rpName, id: rp.rpId },
|
|
343
|
-
user: {
|
|
344
|
-
id: userHandle,
|
|
345
|
-
name: userName,
|
|
346
|
-
displayName: userDisplayName,
|
|
347
|
-
},
|
|
348
|
-
challenge: encodeBase64urlNoPadding(challenge),
|
|
349
|
-
pubKeyCredParams: rp.algorithms.map((alg) => ({
|
|
350
|
-
type: "public-key" as const,
|
|
351
|
-
alg,
|
|
352
|
-
})),
|
|
353
|
-
timeout: rp.challengeExpirationMs,
|
|
354
|
-
attestation: rp.attestation,
|
|
355
|
-
authenticatorSelection: {
|
|
356
|
-
residentKey: rp.residentKey,
|
|
357
|
-
requireResidentKey: rp.residentKey === "required",
|
|
358
|
-
userVerification: rp.userVerification,
|
|
359
|
-
...(rp.authenticatorAttachment
|
|
360
|
-
? {
|
|
361
|
-
authenticatorAttachment:
|
|
362
|
-
rp.authenticatorAttachment,
|
|
363
|
-
}
|
|
364
|
-
: {}),
|
|
365
|
-
},
|
|
366
|
-
excludeCredentials,
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
return {
|
|
370
|
-
kind: "passkeyOptions" as const,
|
|
371
|
-
options,
|
|
372
|
-
verifier,
|
|
373
|
-
};
|
|
374
|
-
},
|
|
375
|
-
err: () => new AuthError("INTERNAL_ERROR"),
|
|
376
|
-
});
|
|
377
|
-
}),
|
|
378
335
|
),
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
)
|
|
336
|
+
resolveRpOptionsFx(provider),
|
|
337
|
+
).pipe(
|
|
338
|
+
Fx.chain(([userId, rp]) => {
|
|
339
|
+
const challenge = new Uint8Array(32);
|
|
340
|
+
crypto.getRandomValues(challenge);
|
|
341
|
+
const challengeHash = encodeBase64urlNoPadding(
|
|
342
|
+
new Uint8Array(sha256(challenge)),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
return Fx.from({
|
|
346
|
+
ok: async () => {
|
|
347
|
+
const verifier = await callVerifier(ctx);
|
|
348
|
+
await callVerifierSignature(ctx, {
|
|
349
|
+
verifier,
|
|
350
|
+
signature: challengeHash,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const user = await queryUserById(ctx, userId);
|
|
354
|
+
const userName = params.userName ?? user?.email ?? "user";
|
|
355
|
+
const userDisplayName =
|
|
356
|
+
params.userDisplayName ?? user?.name ?? userName;
|
|
357
|
+
|
|
358
|
+
const existing = await queryPasskeysByUserId(ctx, userId);
|
|
359
|
+
const excludeCredentials = existing.map((pk) => ({
|
|
360
|
+
id: pk.credentialId,
|
|
361
|
+
transports: pk.transports,
|
|
362
|
+
}));
|
|
363
|
+
|
|
364
|
+
const userHandle = encodeBase64urlNoPadding(
|
|
365
|
+
new TextEncoder().encode(userId),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const options = {
|
|
369
|
+
rp: { name: rp.rpName, id: rp.rpId },
|
|
370
|
+
user: {
|
|
371
|
+
id: userHandle,
|
|
372
|
+
name: userName,
|
|
373
|
+
displayName: userDisplayName,
|
|
374
|
+
},
|
|
375
|
+
challenge: encodeBase64urlNoPadding(challenge),
|
|
376
|
+
pubKeyCredParams: rp.algorithms.map((alg) => ({
|
|
377
|
+
type: "public-key" as const,
|
|
378
|
+
alg,
|
|
379
|
+
})),
|
|
380
|
+
timeout: rp.challengeExpirationMs,
|
|
381
|
+
attestation: rp.attestation,
|
|
382
|
+
authenticatorSelection: {
|
|
383
|
+
residentKey: rp.residentKey,
|
|
384
|
+
requireResidentKey: rp.residentKey === "required",
|
|
385
|
+
userVerification: rp.userVerification,
|
|
386
|
+
...(rp.authenticatorAttachment
|
|
387
|
+
? {
|
|
388
|
+
authenticatorAttachment: rp.authenticatorAttachment,
|
|
389
|
+
}
|
|
390
|
+
: {}),
|
|
391
|
+
},
|
|
392
|
+
excludeCredentials,
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
kind: "passkeyOptions" as const,
|
|
397
|
+
options,
|
|
398
|
+
verifier,
|
|
399
|
+
};
|
|
400
|
+
},
|
|
401
|
+
err: () =>
|
|
402
|
+
Cv.error({
|
|
403
|
+
code: "INTERNAL_ERROR",
|
|
404
|
+
message: "An unexpected error occurred.",
|
|
405
|
+
}),
|
|
406
|
+
});
|
|
407
|
+
}),
|
|
408
|
+
),
|
|
409
|
+
registerVerify: (_) =>
|
|
410
|
+
Fx.zip(
|
|
411
|
+
Fx.from({
|
|
412
|
+
ok: () => ctx.auth.getUserIdentity(),
|
|
413
|
+
err: () =>
|
|
414
|
+
Cv.error({
|
|
415
|
+
code: "PASSKEY_AUTH_REQUIRED",
|
|
416
|
+
message: "Sign in first, then add a passkey to your account.",
|
|
417
|
+
}),
|
|
418
|
+
}).pipe(
|
|
419
|
+
Fx.chain((id) =>
|
|
420
|
+
id === null
|
|
421
|
+
? Cv.fail({
|
|
422
|
+
code: "PASSKEY_AUTH_REQUIRED",
|
|
423
|
+
message:
|
|
424
|
+
"Sign in first, then add a passkey to your account.",
|
|
425
|
+
})
|
|
426
|
+
: Fx.succeed(userIdFromIdentitySubject(id.subject)),
|
|
390
427
|
),
|
|
391
|
-
|
|
392
|
-
)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
428
|
+
),
|
|
429
|
+
resolveRpOptionsFx(provider),
|
|
430
|
+
).pipe(
|
|
431
|
+
Fx.chain(([userId, rp]) =>
|
|
432
|
+
requirePasskeyVerifierFx(args.verifier).pipe(
|
|
433
|
+
Fx.chain((verifier) => {
|
|
434
|
+
const clientDataJSON = decodeBase64urlIgnorePadding(
|
|
435
|
+
params.clientDataJSON,
|
|
436
|
+
);
|
|
437
|
+
const clientData = parseClientDataJSON(clientDataJSON);
|
|
438
|
+
|
|
439
|
+
const verifiedClientDataFx = Fx.succeed(clientData).pipe(
|
|
440
|
+
Fx.chain(
|
|
441
|
+
verifyClientDataType(
|
|
442
|
+
ClientDataType.Create,
|
|
443
|
+
"webauthn.create",
|
|
407
444
|
),
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
return Fx.succeed({
|
|
432
|
-
authData,
|
|
433
|
-
credential: authData.credential,
|
|
445
|
+
),
|
|
446
|
+
Fx.chain(verifyOrigin(rp)),
|
|
447
|
+
Fx.chain(verifyAndConsumeChallenge(ctx, verifier)),
|
|
448
|
+
Fx.map(() => {
|
|
449
|
+
const attestationObjectBytes =
|
|
450
|
+
decodeBase64urlIgnorePadding(params.attestationObject);
|
|
451
|
+
const attestation = parseAttestationObject(
|
|
452
|
+
attestationObjectBytes,
|
|
453
|
+
);
|
|
454
|
+
return attestation.authenticatorData;
|
|
455
|
+
}),
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
return verifiedClientDataFx.pipe(
|
|
459
|
+
Fx.chain(verifyRpId(rp.rpId)),
|
|
460
|
+
Fx.chain(verifyUserFlags(rp)),
|
|
461
|
+
Fx.chain((authData) => {
|
|
462
|
+
if (authData.credential == null) {
|
|
463
|
+
return Cv.fail({
|
|
464
|
+
code: "PASSKEY_NO_CREDENTIAL",
|
|
465
|
+
message: "No credential in attestation.",
|
|
434
466
|
});
|
|
435
|
-
}
|
|
436
|
-
Fx.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
[
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
467
|
+
}
|
|
468
|
+
return Fx.succeed({
|
|
469
|
+
authData,
|
|
470
|
+
credential: authData.credential,
|
|
471
|
+
});
|
|
472
|
+
}),
|
|
473
|
+
Fx.chain(({ authData, credential }) => {
|
|
474
|
+
const credentialId = encodeBase64urlNoPadding(
|
|
475
|
+
credential.id,
|
|
476
|
+
);
|
|
477
|
+
const publicKey = credential.publicKey;
|
|
478
|
+
|
|
479
|
+
let algorithm: number;
|
|
480
|
+
if (publicKey.isAlgorithmDefined()) {
|
|
481
|
+
algorithm = publicKey.algorithm();
|
|
482
|
+
} else {
|
|
483
|
+
const keyType = publicKey.type();
|
|
484
|
+
algorithm =
|
|
485
|
+
keyType === COSEKeyType.EC2
|
|
486
|
+
? coseAlgorithmES256
|
|
487
|
+
: keyType === COSEKeyType.RSA
|
|
488
|
+
? coseAlgorithmRS256
|
|
489
|
+
: coseAlgorithmES256;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const handlers: Record<
|
|
493
|
+
number,
|
|
494
|
+
(() => FxType<Uint8Array, ConvexError<any>>) | undefined
|
|
495
|
+
> = {
|
|
496
|
+
[coseAlgorithmES256]: () => {
|
|
497
|
+
const ec2 = publicKey.ec2();
|
|
498
|
+
const xBytes = new Uint8Array(32);
|
|
499
|
+
let vx = ec2.x;
|
|
500
|
+
for (let i = 31; i >= 0; i--) {
|
|
501
|
+
xBytes[i] = Number(vx & 0xffn);
|
|
502
|
+
vx >>= 8n;
|
|
503
|
+
}
|
|
504
|
+
const yBytes = new Uint8Array(32);
|
|
505
|
+
let vy = ec2.y;
|
|
506
|
+
for (let i = 31; i >= 0; i--) {
|
|
507
|
+
yBytes[i] = Number(vy & 0xffn);
|
|
508
|
+
vy >>= 8n;
|
|
509
|
+
}
|
|
510
|
+
const bytes = new Uint8Array(65);
|
|
511
|
+
bytes[0] = 0x04;
|
|
512
|
+
bytes.set(xBytes, 1);
|
|
513
|
+
bytes.set(yBytes, 33);
|
|
514
|
+
return Fx.succeed(bytes);
|
|
515
|
+
},
|
|
516
|
+
[coseAlgorithmRS256]: () => {
|
|
517
|
+
const rsa = publicKey.rsa();
|
|
518
|
+
const rsaPubKey = new RSAPublicKey(rsa.n, rsa.e);
|
|
519
|
+
return Fx.succeed(rsaPubKey.encodePKCS1());
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const handler = handlers[algorithm];
|
|
524
|
+
return (
|
|
525
|
+
handler
|
|
526
|
+
? handler()
|
|
527
|
+
: Cv.fail({
|
|
528
|
+
code: "PASSKEY_UNSUPPORTED_ALGORITHM",
|
|
529
|
+
message: `Unsupported algorithm: ${algorithm}`,
|
|
530
|
+
})
|
|
531
|
+
).pipe(
|
|
532
|
+
Fx.chain((publicKeyBytes) =>
|
|
533
|
+
Fx.from({
|
|
534
|
+
ok: async () => {
|
|
535
|
+
const deviceType =
|
|
536
|
+
params.deviceType ?? "single-device";
|
|
537
|
+
const backedUp = params.backedUp ?? false;
|
|
538
|
+
|
|
539
|
+
const db = authDb(ctx, ctx.auth.config);
|
|
540
|
+
await db.accounts.create({
|
|
541
|
+
userId,
|
|
542
|
+
provider: provider.id,
|
|
543
|
+
providerAccountId: credentialId,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
await mutatePasskeyInsert(ctx, {
|
|
547
|
+
userId,
|
|
548
|
+
credentialId,
|
|
549
|
+
publicKey: publicKeyBytes.buffer.slice(
|
|
550
|
+
publicKeyBytes.byteOffset,
|
|
551
|
+
publicKeyBytes.byteOffset +
|
|
552
|
+
publicKeyBytes.byteLength,
|
|
494
553
|
),
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
createdAt: Date.now(),
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
const signInResult = await callSignIn(ctx, {
|
|
529
|
-
userId,
|
|
530
|
-
generateTokens: true,
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
return {
|
|
534
|
-
kind: "signedIn" as const,
|
|
535
|
-
signedIn: signInResult,
|
|
536
|
-
};
|
|
537
|
-
},
|
|
538
|
-
err: () => new AuthError("INTERNAL_ERROR"),
|
|
539
|
-
}),
|
|
540
|
-
),
|
|
541
|
-
);
|
|
542
|
-
}),
|
|
543
|
-
);
|
|
544
|
-
}),
|
|
545
|
-
),
|
|
554
|
+
algorithm,
|
|
555
|
+
counter: authData.signatureCounter,
|
|
556
|
+
transports: params.transports,
|
|
557
|
+
deviceType,
|
|
558
|
+
backedUp,
|
|
559
|
+
name: params.passkeyName,
|
|
560
|
+
createdAt: Date.now(),
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
const signInResult = await callSignIn(ctx, {
|
|
564
|
+
userId,
|
|
565
|
+
generateTokens: true,
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
kind: "signedIn" as const,
|
|
570
|
+
signedIn: signInResult,
|
|
571
|
+
};
|
|
572
|
+
},
|
|
573
|
+
err: () =>
|
|
574
|
+
Cv.error({
|
|
575
|
+
code: "INTERNAL_ERROR",
|
|
576
|
+
message: "An unexpected error occurred.",
|
|
577
|
+
}),
|
|
578
|
+
}),
|
|
579
|
+
),
|
|
580
|
+
);
|
|
581
|
+
}),
|
|
582
|
+
);
|
|
583
|
+
}),
|
|
546
584
|
),
|
|
547
585
|
),
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
)
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
586
|
+
),
|
|
587
|
+
authOptions: (_) =>
|
|
588
|
+
resolveRpOptionsFx(provider).pipe(
|
|
589
|
+
Fx.chain((rp) => {
|
|
590
|
+
const challenge = new Uint8Array(32);
|
|
591
|
+
crypto.getRandomValues(challenge);
|
|
592
|
+
const challengeHash = encodeBase64urlNoPadding(
|
|
593
|
+
new Uint8Array(sha256(challenge)),
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
return Fx.from({
|
|
597
|
+
ok: async () => {
|
|
598
|
+
const verifier = await callVerifier(ctx);
|
|
599
|
+
await callVerifierSignature(ctx, {
|
|
600
|
+
verifier,
|
|
601
|
+
signature: challengeHash,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
let allowCredentials:
|
|
605
|
+
| Array<{
|
|
606
|
+
type: string;
|
|
607
|
+
id: string;
|
|
608
|
+
transports?: string[];
|
|
609
|
+
}>
|
|
610
|
+
| undefined;
|
|
611
|
+
if (params.email) {
|
|
612
|
+
const user = await queryUserByVerifiedEmail(
|
|
613
|
+
ctx,
|
|
614
|
+
params.email,
|
|
615
|
+
);
|
|
616
|
+
if (user) {
|
|
617
|
+
const passkeys = await queryPasskeysByUserId(
|
|
574
618
|
ctx,
|
|
575
|
-
|
|
619
|
+
user._id,
|
|
576
620
|
);
|
|
577
|
-
if (
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
allowCredentials = passkeys.map((pk) => ({
|
|
584
|
-
type: "public-key",
|
|
585
|
-
id: pk.credentialId,
|
|
586
|
-
transports: pk.transports,
|
|
587
|
-
}));
|
|
588
|
-
}
|
|
621
|
+
if (passkeys.length > 0) {
|
|
622
|
+
allowCredentials = passkeys.map((pk) => ({
|
|
623
|
+
type: "public-key",
|
|
624
|
+
id: pk.credentialId,
|
|
625
|
+
transports: pk.transports,
|
|
626
|
+
}));
|
|
589
627
|
}
|
|
590
628
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const options: Record<string, any> = {
|
|
632
|
+
challenge: encodeBase64urlNoPadding(challenge),
|
|
633
|
+
timeout: rp.challengeExpirationMs,
|
|
634
|
+
rpId: rp.rpId,
|
|
635
|
+
userVerification: rp.userVerification,
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
if (allowCredentials) {
|
|
639
|
+
options.allowCredentials = allowCredentials;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
kind: "passkeyOptions" as const,
|
|
644
|
+
options,
|
|
645
|
+
verifier,
|
|
646
|
+
};
|
|
647
|
+
},
|
|
648
|
+
err: () =>
|
|
649
|
+
Cv.error({
|
|
650
|
+
code: "INTERNAL_ERROR",
|
|
651
|
+
message: "An unexpected error occurred.",
|
|
652
|
+
}),
|
|
653
|
+
});
|
|
654
|
+
}),
|
|
655
|
+
),
|
|
656
|
+
authVerify: (_) =>
|
|
657
|
+
Fx.zip(
|
|
658
|
+
resolveRpOptionsFx(provider),
|
|
659
|
+
requirePasskeyVerifierFx(args.verifier),
|
|
660
|
+
).pipe(
|
|
661
|
+
Fx.chain(([rp, verifier]) => {
|
|
662
|
+
const clientDataJSON = decodeBase64urlIgnorePadding(
|
|
663
|
+
params.clientDataJSON,
|
|
664
|
+
);
|
|
665
|
+
const clientData = parseClientDataJSON(clientDataJSON);
|
|
666
|
+
|
|
667
|
+
const verifiedClientDataFx = Fx.succeed(clientData).pipe(
|
|
668
|
+
Fx.chain(
|
|
669
|
+
verifyClientDataType(ClientDataType.Get, "webauthn.get"),
|
|
670
|
+
),
|
|
671
|
+
Fx.chain(verifyOrigin(rp)),
|
|
672
|
+
Fx.chain(verifyAndConsumeChallenge(ctx, verifier)),
|
|
673
|
+
Fx.chain(() =>
|
|
674
|
+
params.credentialId != null
|
|
675
|
+
? Fx.succeed(params.credentialId as string)
|
|
676
|
+
: Cv.fail({
|
|
677
|
+
code: "PASSKEY_UNKNOWN_CREDENTIAL",
|
|
678
|
+
message: "Missing credential ID",
|
|
679
|
+
}),
|
|
680
|
+
),
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
return verifiedClientDataFx.pipe(
|
|
684
|
+
Fx.chain((credentialId) =>
|
|
685
|
+
Fx.from({
|
|
686
|
+
ok: () => queryPasskeyByCredentialId(ctx, credentialId),
|
|
687
|
+
err: () =>
|
|
688
|
+
Cv.error({
|
|
689
|
+
code: "PASSKEY_UNKNOWN_CREDENTIAL",
|
|
690
|
+
message: "Unknown passkey credential.",
|
|
691
|
+
}),
|
|
692
|
+
}).pipe(
|
|
693
|
+
Fx.chain((passkey) =>
|
|
694
|
+
passkey
|
|
695
|
+
? Fx.succeed(passkey)
|
|
696
|
+
: Cv.fail({
|
|
697
|
+
code: "PASSKEY_UNKNOWN_CREDENTIAL",
|
|
698
|
+
message: "Unknown credential",
|
|
699
|
+
}),
|
|
658
700
|
),
|
|
659
701
|
),
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
)
|
|
702
|
+
),
|
|
703
|
+
Fx.chain((passkey) => {
|
|
704
|
+
const authenticatorDataBytes = decodeBase64urlIgnorePadding(
|
|
705
|
+
params.authenticatorData,
|
|
706
|
+
);
|
|
707
|
+
const authenticatorData = parseAuthenticatorData(
|
|
708
|
+
authenticatorDataBytes,
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
const signature = decodeBase64urlIgnorePadding(
|
|
712
|
+
params.signature,
|
|
713
|
+
);
|
|
714
|
+
const signatureMessage = createAssertionSignatureMessage(
|
|
715
|
+
authenticatorDataBytes,
|
|
716
|
+
clientDataJSON,
|
|
717
|
+
);
|
|
718
|
+
const messageHash = sha256(signatureMessage);
|
|
719
|
+
|
|
720
|
+
const checkedAuthenticatorFx = Fx.succeed(
|
|
721
|
+
authenticatorData,
|
|
722
|
+
).pipe(
|
|
723
|
+
Fx.chain(verifyRpId(rp.rpId)),
|
|
724
|
+
Fx.chain(verifyUserFlags(rp)),
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
const signatureVerifiedFx = checkedAuthenticatorFx.pipe(
|
|
728
|
+
Fx.chain(() => {
|
|
729
|
+
const storedPublicKeyBytes = new Uint8Array(
|
|
730
|
+
passkey.publicKey,
|
|
731
|
+
);
|
|
732
|
+
const algorithmHandlers: Record<
|
|
733
|
+
number,
|
|
734
|
+
(() => FxType<void, ConvexError<any>>) | undefined
|
|
735
|
+
> = {
|
|
736
|
+
[coseAlgorithmES256]: () => {
|
|
737
|
+
const ecPublicKey = decodeSEC1PublicKey(
|
|
738
|
+
p256,
|
|
739
|
+
storedPublicKeyBytes,
|
|
740
|
+
);
|
|
741
|
+
const ecdsaSignature =
|
|
742
|
+
decodePKIXECDSASignature(signature);
|
|
743
|
+
const valid = verifyECDSASignature(
|
|
744
|
+
ecPublicKey,
|
|
745
|
+
messageHash,
|
|
746
|
+
ecdsaSignature,
|
|
747
|
+
);
|
|
748
|
+
return valid
|
|
749
|
+
? Fx.succeed(undefined as void)
|
|
750
|
+
: Cv.fail({
|
|
751
|
+
code: "PASSKEY_INVALID_SIGNATURE",
|
|
752
|
+
message: "Invalid passkey signature.",
|
|
753
|
+
});
|
|
754
|
+
},
|
|
755
|
+
[coseAlgorithmRS256]: () => {
|
|
756
|
+
const rsaPublicKey =
|
|
757
|
+
decodePKCS1RSAPublicKey(storedPublicKeyBytes);
|
|
758
|
+
const valid = verifyRSASSAPKCS1v15Signature(
|
|
759
|
+
rsaPublicKey,
|
|
760
|
+
sha256ObjectIdentifier,
|
|
761
|
+
messageHash,
|
|
762
|
+
signature,
|
|
763
|
+
);
|
|
764
|
+
return valid
|
|
765
|
+
? Fx.succeed(undefined as void)
|
|
766
|
+
: Cv.fail({
|
|
767
|
+
code: "PASSKEY_INVALID_SIGNATURE",
|
|
768
|
+
message: "Invalid passkey signature.",
|
|
769
|
+
});
|
|
770
|
+
},
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
const handler = algorithmHandlers[passkey.algorithm];
|
|
774
|
+
return handler
|
|
775
|
+
? handler()
|
|
776
|
+
: Cv.fail({
|
|
777
|
+
code: "PASSKEY_UNSUPPORTED_ALGORITHM",
|
|
778
|
+
message: `Unsupported algorithm: ${passkey.algorithm}`,
|
|
779
|
+
});
|
|
780
|
+
}),
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
const counterValidatedFx = signatureVerifiedFx.pipe(
|
|
784
|
+
Fx.chain(() =>
|
|
785
|
+
passkey.counter !== 0 &&
|
|
786
|
+
authenticatorData.signatureCounter !== 0 &&
|
|
787
|
+
authenticatorData.signatureCounter <= passkey.counter
|
|
788
|
+
? Cv.fail({
|
|
789
|
+
code: "PASSKEY_COUNTER_ERROR",
|
|
790
|
+
message:
|
|
791
|
+
"Authenticator counter did not increase — possible credential cloning detected.",
|
|
792
|
+
})
|
|
793
|
+
: Fx.succeed(authenticatorData),
|
|
794
|
+
),
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
return counterValidatedFx.pipe(
|
|
798
|
+
Fx.chain(() =>
|
|
799
|
+
Fx.from({
|
|
800
|
+
ok: async () => {
|
|
801
|
+
await mutatePasskeyUpdateCounter(
|
|
802
|
+
ctx,
|
|
803
|
+
passkey._id,
|
|
804
|
+
authenticatorData.signatureCounter,
|
|
805
|
+
Date.now(),
|
|
806
|
+
);
|
|
683
807
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
decodePKIXECDSASignature(signature);
|
|
700
|
-
const valid = verifyECDSASignature(
|
|
701
|
-
ecPublicKey,
|
|
702
|
-
messageHash,
|
|
703
|
-
ecdsaSignature,
|
|
704
|
-
);
|
|
705
|
-
return valid
|
|
706
|
-
? Fx.succeed(undefined as void)
|
|
707
|
-
: Fx.fail(
|
|
708
|
-
new AuthError("PASSKEY_INVALID_SIGNATURE"),
|
|
709
|
-
);
|
|
710
|
-
},
|
|
711
|
-
[coseAlgorithmRS256]: () => {
|
|
712
|
-
const rsaPublicKey =
|
|
713
|
-
decodePKCS1RSAPublicKey(storedPublicKeyBytes);
|
|
714
|
-
const valid = verifyRSASSAPKCS1v15Signature(
|
|
715
|
-
rsaPublicKey,
|
|
716
|
-
sha256ObjectIdentifier,
|
|
717
|
-
messageHash,
|
|
718
|
-
signature,
|
|
719
|
-
);
|
|
720
|
-
return valid
|
|
721
|
-
? Fx.succeed(undefined as void)
|
|
722
|
-
: Fx.fail(
|
|
723
|
-
new AuthError("PASSKEY_INVALID_SIGNATURE"),
|
|
724
|
-
);
|
|
725
|
-
},
|
|
726
|
-
};
|
|
727
|
-
|
|
728
|
-
const handler = algorithmHandlers[passkey.algorithm];
|
|
729
|
-
return handler
|
|
730
|
-
? handler()
|
|
731
|
-
: Fx.fail(
|
|
732
|
-
new AuthError(
|
|
733
|
-
"PASSKEY_UNSUPPORTED_ALGORITHM",
|
|
734
|
-
`Unsupported algorithm: ${passkey.algorithm}`,
|
|
735
|
-
),
|
|
736
|
-
);
|
|
808
|
+
const signInResult = await callSignIn(ctx, {
|
|
809
|
+
userId: passkey.userId,
|
|
810
|
+
generateTokens: true,
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
return {
|
|
814
|
+
kind: "signedIn" as const,
|
|
815
|
+
signedIn: signInResult,
|
|
816
|
+
};
|
|
817
|
+
},
|
|
818
|
+
err: () =>
|
|
819
|
+
Cv.error({
|
|
820
|
+
code: "INTERNAL_ERROR",
|
|
821
|
+
message: "An unexpected error occurred.",
|
|
822
|
+
}),
|
|
737
823
|
}),
|
|
738
|
-
)
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
? Fx.fail(new AuthError("PASSKEY_COUNTER_ERROR"))
|
|
746
|
-
: Fx.succeed(authenticatorData),
|
|
747
|
-
),
|
|
748
|
-
);
|
|
749
|
-
|
|
750
|
-
return counterValidatedFx.pipe(
|
|
751
|
-
Fx.chain(() =>
|
|
752
|
-
Fx.from({
|
|
753
|
-
ok: async () => {
|
|
754
|
-
await mutatePasskeyUpdateCounter(
|
|
755
|
-
ctx,
|
|
756
|
-
passkey._id,
|
|
757
|
-
authenticatorData.signatureCounter,
|
|
758
|
-
Date.now(),
|
|
759
|
-
);
|
|
760
|
-
|
|
761
|
-
const signInResult = await callSignIn(ctx, {
|
|
762
|
-
userId: passkey.userId,
|
|
763
|
-
generateTokens: true,
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
return {
|
|
767
|
-
kind: "signedIn" as const,
|
|
768
|
-
signedIn: signInResult,
|
|
769
|
-
};
|
|
770
|
-
},
|
|
771
|
-
err: () => new AuthError("INTERNAL_ERROR"),
|
|
772
|
-
}),
|
|
773
|
-
),
|
|
774
|
-
);
|
|
775
|
-
}),
|
|
776
|
-
);
|
|
777
|
-
}),
|
|
778
|
-
),
|
|
779
|
-
},
|
|
780
|
-
);
|
|
824
|
+
),
|
|
825
|
+
);
|
|
826
|
+
}),
|
|
827
|
+
);
|
|
828
|
+
}),
|
|
829
|
+
),
|
|
830
|
+
});
|
|
781
831
|
return flowFx;
|
|
782
832
|
}),
|
|
783
833
|
);
|