@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/dist/server/oauth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { AuthError } from "./authError.js";
|
|
2
1
|
import { isLocalHost, logWithLevel } from "./utils.js";
|
|
3
2
|
import { SHARED_COOKIE_OPTIONS } from "./cookies.js";
|
|
4
3
|
import { Fx } from "@robelest/fx";
|
|
4
|
+
import { Cv } from "@robelest/fx/convex";
|
|
5
5
|
import * as arctic from "arctic";
|
|
6
6
|
|
|
7
7
|
//#region src/server/oauth.ts
|
|
@@ -10,7 +10,7 @@ import * as arctic from "arctic";
|
|
|
10
10
|
*
|
|
11
11
|
* Uses Arctic for OAuth provider integration.
|
|
12
12
|
*
|
|
13
|
-
* All functions return `Fx<A,
|
|
13
|
+
* All functions return `Fx<A, ConvexError<any>>` composed via `Fx.gen` pipelines.
|
|
14
14
|
*
|
|
15
15
|
* @internal
|
|
16
16
|
* @module
|
|
@@ -59,15 +59,24 @@ function isPKCEProvider(provider) {
|
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
61
|
* Exchange the authorization code for tokens via Arctic.
|
|
62
|
-
* Maps Arctic-specific errors to typed `
|
|
62
|
+
* Maps Arctic-specific errors to typed `ConvexError<any>` failures.
|
|
63
63
|
*/
|
|
64
64
|
function exchangeCode(arcticProvider, code, codeVerifier) {
|
|
65
65
|
return Fx.from({
|
|
66
66
|
ok: () => isPKCEProvider(arcticProvider) ? arcticProvider.validateAuthorizationCode(code, codeVerifier) : arcticProvider.validateAuthorizationCode(code),
|
|
67
67
|
err: (e) => {
|
|
68
|
-
if (e instanceof arctic.OAuth2RequestError) return
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
if (e instanceof arctic.OAuth2RequestError) return Cv.error({
|
|
69
|
+
code: "OAUTH_PROVIDER_ERROR",
|
|
70
|
+
message: `Token exchange failed: ${e.code}`
|
|
71
|
+
});
|
|
72
|
+
if (e instanceof arctic.ArcticFetchError) return Cv.error({
|
|
73
|
+
code: "OAUTH_PROVIDER_ERROR",
|
|
74
|
+
message: `Network error during token exchange: ${e.message}`
|
|
75
|
+
});
|
|
76
|
+
return Cv.error({
|
|
77
|
+
code: "OAUTH_PROVIDER_ERROR",
|
|
78
|
+
message: `Unexpected error during token exchange: ${e instanceof Error ? e.message : String(e)}`
|
|
79
|
+
});
|
|
71
80
|
}
|
|
72
81
|
}).pipe(Fx.chain((tokens) => {
|
|
73
82
|
return Fx.succeed(tokens);
|
|
@@ -83,7 +92,10 @@ function extractProfile(providerId, oauthConfig, tokens) {
|
|
|
83
92
|
return Fx.match(profileSource, profileSource.source, {
|
|
84
93
|
callback: (_profileSource) => Fx.from({
|
|
85
94
|
ok: () => oauthConfig.profile(tokens),
|
|
86
|
-
err: (e) =>
|
|
95
|
+
err: (e) => Cv.error({
|
|
96
|
+
code: "OAUTH_INVALID_PROFILE",
|
|
97
|
+
message: `Profile callback threw: ${e instanceof Error ? e.message : String(e)}`
|
|
98
|
+
})
|
|
87
99
|
}),
|
|
88
100
|
idToken: (_profileSource) => {
|
|
89
101
|
const claims = arctic.decodeIdToken(tokens.idToken());
|
|
@@ -94,14 +106,20 @@ function extractProfile(providerId, oauthConfig, tokens) {
|
|
|
94
106
|
image: claims.picture ?? void 0
|
|
95
107
|
});
|
|
96
108
|
},
|
|
97
|
-
missing: (_profileSource) =>
|
|
109
|
+
missing: (_profileSource) => Cv.fail({
|
|
110
|
+
code: "OAUTH_INVALID_PROFILE",
|
|
111
|
+
message: `Provider "${providerId}" does not return an ID token. Add a \`profile\` callback in the OAuth() config to extract user info from the access token.`
|
|
112
|
+
})
|
|
98
113
|
});
|
|
99
114
|
}
|
|
100
115
|
/**
|
|
101
116
|
* Validate that the profile has a non-empty string `id`.
|
|
102
117
|
*/
|
|
103
118
|
function validateProfileId(providerId, profile) {
|
|
104
|
-
return typeof profile.id === "string" && profile.id ? Fx.succeed(profile) :
|
|
119
|
+
return typeof profile.id === "string" && profile.id ? Fx.succeed(profile) : Cv.fail({
|
|
120
|
+
code: "OAUTH_INVALID_PROFILE",
|
|
121
|
+
message: `The profile callback for "${providerId}" must return an object with a string \`id\` field.`
|
|
122
|
+
});
|
|
105
123
|
}
|
|
106
124
|
/**
|
|
107
125
|
* Create an OAuth authorization URL using an Arctic provider.
|
|
@@ -145,7 +163,7 @@ async function createOAuthAuthorizationURL(providerId, arcticProvider, oauthConf
|
|
|
145
163
|
* Handle the OAuth callback: validate state, exchange code for tokens,
|
|
146
164
|
* extract profile.
|
|
147
165
|
*
|
|
148
|
-
* Returns `Fx<CallbackResult,
|
|
166
|
+
* Returns `Fx<CallbackResult, ConvexError<any>>` composed via `Fx.gen`.
|
|
149
167
|
*/
|
|
150
168
|
/** @internal */
|
|
151
169
|
function handleOAuthCallback(providerId, arcticProvider, oauthConfig, params, cookies) {
|
|
@@ -153,7 +171,10 @@ function handleOAuthCallback(providerId, arcticProvider, oauthConfig, params, co
|
|
|
153
171
|
const resCookies = [];
|
|
154
172
|
const storedState = cookies[oauthCookieName("state", providerId)];
|
|
155
173
|
const returnedState = params.state;
|
|
156
|
-
yield* Fx.guard(!storedState || !returnedState || storedState !== returnedState,
|
|
174
|
+
yield* Fx.guard(!storedState || !returnedState || storedState !== returnedState, Cv.fail({
|
|
175
|
+
code: "OAUTH_INVALID_STATE",
|
|
176
|
+
message: "Invalid OAuth state. Please try signing in again."
|
|
177
|
+
}));
|
|
157
178
|
resCookies.push(clearCookie("state", providerId));
|
|
158
179
|
if (params.error) {
|
|
159
180
|
const cause = {
|
|
@@ -162,25 +183,41 @@ function handleOAuthCallback(providerId, arcticProvider, oauthConfig, params, co
|
|
|
162
183
|
error_description: params.error_description
|
|
163
184
|
};
|
|
164
185
|
logWithLevel("DEBUG", "OAuthCallbackError", cause);
|
|
165
|
-
yield*
|
|
186
|
+
yield* Cv.fail({
|
|
187
|
+
code: "OAUTH_PROVIDER_ERROR",
|
|
188
|
+
message: "OAuth provider returned an error",
|
|
189
|
+
cause: JSON.stringify(cause)
|
|
190
|
+
});
|
|
166
191
|
}
|
|
167
|
-
const code = yield* params.code != null ? Fx.succeed(params.code) :
|
|
192
|
+
const code = yield* params.code != null ? Fx.succeed(params.code) : Cv.fail({
|
|
193
|
+
code: "OAUTH_PROVIDER_ERROR",
|
|
194
|
+
message: "Missing authorization code in callback"
|
|
195
|
+
});
|
|
168
196
|
let codeVerifier;
|
|
169
197
|
if (isPKCEProvider(arcticProvider)) {
|
|
170
198
|
const pkceCookieName = oauthCookieName("pkce", providerId);
|
|
171
|
-
codeVerifier = yield* cookies[pkceCookieName] != null ? Fx.succeed(cookies[pkceCookieName]) :
|
|
199
|
+
codeVerifier = yield* cookies[pkceCookieName] != null ? Fx.succeed(cookies[pkceCookieName]) : Cv.fail({
|
|
200
|
+
code: "OAUTH_MISSING_VERIFIER",
|
|
201
|
+
message: "Missing PKCE verifier cookie for OAuth callback"
|
|
202
|
+
});
|
|
172
203
|
resCookies.push(clearCookie("pkce", providerId));
|
|
173
204
|
}
|
|
174
205
|
let nonce;
|
|
175
206
|
if (oauthConfig.nonce === true) {
|
|
176
207
|
const nonceCookieName = oauthCookieName("nonce", providerId);
|
|
177
|
-
nonce = yield* cookies[nonceCookieName] != null ? Fx.succeed(cookies[nonceCookieName]) :
|
|
208
|
+
nonce = yield* cookies[nonceCookieName] != null ? Fx.succeed(cookies[nonceCookieName]) : Cv.fail({
|
|
209
|
+
code: "OAUTH_PROVIDER_ERROR",
|
|
210
|
+
message: "Missing nonce cookie for OAuth callback"
|
|
211
|
+
});
|
|
178
212
|
resCookies.push(clearCookie("nonce", providerId));
|
|
179
213
|
}
|
|
180
214
|
const tokens = yield* exchangeCode(arcticProvider, code, codeVerifier);
|
|
181
215
|
if (oauthConfig.validateTokens !== void 0) yield* Fx.from({
|
|
182
216
|
ok: () => oauthConfig.validateTokens(tokens, { nonce }),
|
|
183
|
-
err: (e) =>
|
|
217
|
+
err: (e) => Cv.error({
|
|
218
|
+
code: "OAUTH_PROVIDER_ERROR",
|
|
219
|
+
message: `Token validation failed: ${e instanceof Error ? e.message : String(e)}`
|
|
220
|
+
})
|
|
184
221
|
});
|
|
185
222
|
const profile = yield* validateProfileId(providerId, yield* extractProfile(providerId, oauthConfig, tokens));
|
|
186
223
|
logWithLevel("DEBUG", "OAuth callback profile extracted", {
|
package/dist/server/oauth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.js","names":[],"sources":["../../src/server/oauth.ts"],"sourcesContent":["/**\n * Arctic-based OAuth flow implementation.\n *\n * Uses Arctic for OAuth provider integration.\n *\n * All functions return `Fx<A, AuthError>` composed via `Fx.gen` pipelines.\n *\n * @internal\n * @module\n */\n\nimport { Fx } from \"@robelest/fx\";\nimport * as arctic from \"arctic\";\n\nimport { SHARED_COOKIE_OPTIONS } from \"./cookies\";\nimport { AuthError } from \"./authError\";\nimport type { OAuthProfile } from \"./types\";\nimport { logWithLevel } from \"./utils\";\nimport { isLocalHost } from \"./utils\";\n\ntype OAuthProviderConfigLike = {\n scopes?: string[];\n profile?: (tokens: arctic.OAuth2Tokens) => Promise<OAuthProfile>;\n nonce?: boolean;\n validateTokens?: (\n tokens: arctic.OAuth2Tokens,\n ctx: { nonce?: string },\n ) => Promise<void>;\n};\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/** A cookie to be set on the HTTP response. */\n/** @internal */\nexport interface OAuthCookie {\n name: string;\n value: string;\n options: Record<string, unknown>;\n}\n\n/** Result of creating an authorization URL. */\n/** @internal */\nexport interface AuthorizationResult {\n redirect: string;\n cookies: OAuthCookie[];\n signature: string;\n}\n\n/** Result of handling an OAuth callback. */\n/** @internal */\nexport interface CallbackResult {\n profile: OAuthProfile;\n providerAccountId: string;\n cookies: OAuthCookie[];\n signature: string;\n}\n\n// ============================================================================\n// Cookie helpers\n// ============================================================================\n\nconst COOKIE_TTL = 60 * 15; // 15 minutes\n\nfunction oauthCookieName(type: \"state\" | \"pkce\" | \"nonce\", providerId: string) {\n const prefix = !isLocalHost(process.env.CONVEX_SITE_URL) ? \"__Host-\" : \"\";\n return prefix + providerId + \"OAuth\" + type;\n}\n\nfunction createCookie(\n type: \"state\" | \"pkce\" | \"nonce\",\n providerId: string,\n value: string,\n): OAuthCookie {\n const expires = new Date();\n expires.setTime(expires.getTime() + COOKIE_TTL * 1000);\n return {\n name: oauthCookieName(type, providerId),\n value,\n options: { ...SHARED_COOKIE_OPTIONS, expires },\n };\n}\n\nfunction clearCookie(\n type: \"state\" | \"pkce\" | \"nonce\",\n providerId: string,\n): OAuthCookie {\n return {\n name: oauthCookieName(type, providerId),\n value: \"\",\n options: { ...SHARED_COOKIE_OPTIONS, maxAge: 0 },\n };\n}\n\n// ============================================================================\n// Signature (ConvexAuth-specific verifier mechanism)\n// ============================================================================\n\n/**\n * Creates a signature string from the OAuth state parameters.\n * This is stored in the verifier table and validated during callback.\n */\n/** @internal */\nexport function getAuthorizationSignature({\n codeVerifier,\n state,\n}: {\n codeVerifier?: string;\n state?: string;\n}) {\n return [codeVerifier, state].filter((param) => param !== undefined).join(\" \");\n}\n\n// ============================================================================\n// PKCE Detection\n// ============================================================================\n\n/**\n * Detect whether an Arctic provider uses PKCE by checking the arity\n * of `createAuthorizationURL`. PKCE providers take 3 args\n * (state, codeVerifier, scopes), non-PKCE take 2 (state, scopes).\n */\nfunction isPKCEProvider(provider: any): boolean {\n return (\n typeof provider.createAuthorizationURL === \"function\" &&\n provider.createAuthorizationURL.length >= 3\n );\n}\n\n// ============================================================================\n// Token exchange — wraps Arctic's validateAuthorizationCode\n// ============================================================================\n\n/**\n * Exchange the authorization code for tokens via Arctic.\n * Maps Arctic-specific errors to typed `AuthError` failures.\n */\nfunction exchangeCode(\n arcticProvider: any,\n code: string,\n codeVerifier: string | undefined,\n): Fx<arctic.OAuth2Tokens, AuthError> {\n return Fx.from({\n ok: () =>\n isPKCEProvider(arcticProvider)\n ? arcticProvider.validateAuthorizationCode(code, codeVerifier)\n : arcticProvider.validateAuthorizationCode(code),\n err: (e) => {\n if (e instanceof arctic.OAuth2RequestError) {\n return new AuthError(\n \"OAUTH_PROVIDER_ERROR\",\n `Token exchange failed: ${e.code}`,\n );\n }\n if (e instanceof arctic.ArcticFetchError) {\n return new AuthError(\n \"OAUTH_PROVIDER_ERROR\",\n `Network error during token exchange: ${e.message}`,\n );\n }\n // Unknown error — treat as unrecoverable defect; we surface it as\n // an AuthError here so the pipeline type stays Fx<_, AuthError>.\n // The original `throw e` re-throw is replicated via Fx.fatal below.\n return new AuthError(\n \"OAUTH_PROVIDER_ERROR\",\n `Unexpected error during token exchange: ${e instanceof Error ? e.message : String(e)}`,\n );\n },\n }).pipe(\n Fx.chain((tokens) => {\n // If the original error was neither OAuth2RequestError nor\n // ArcticFetchError the old code re-threw it raw. We replicate that\n // by checking whether we created an \"Unexpected\" marker message\n // — but since `Fx.from` already mapped it, we just pass through.\n return Fx.succeed(tokens);\n }),\n );\n}\n\n/**\n * Extract the user profile from tokens using the config callback,\n * OIDC auto-decode, or fail if neither is available.\n */\nfunction extractProfile(\n providerId: string,\n oauthConfig: OAuthProviderConfigLike,\n tokens: arctic.OAuth2Tokens,\n): Fx<OAuthProfile, AuthError> {\n const hasIdToken =\n \"id_token\" in tokens.data &&\n typeof (tokens.data as any).id_token === \"string\";\n const profileSource = oauthConfig.profile\n ? { source: \"callback\" as const }\n : hasIdToken\n ? { source: \"idToken\" as const }\n : { source: \"missing\" as const };\n\n return Fx.match(profileSource, profileSource.source, {\n callback: (_profileSource) =>\n Fx.from({\n ok: () => oauthConfig.profile!(tokens),\n err: (e) =>\n new AuthError(\n \"OAUTH_INVALID_PROFILE\",\n `Profile callback threw: ${e instanceof Error ? e.message : String(e)}`,\n ),\n }),\n idToken: (_profileSource) => {\n const claims = arctic.decodeIdToken(tokens.idToken()) as Record<\n string,\n unknown\n >;\n return Fx.succeed({\n id: (claims.sub as string) ?? crypto.randomUUID(),\n name: (claims.name as string) ?? undefined,\n email: (claims.email as string) ?? undefined,\n image: (claims.picture as string) ?? undefined,\n });\n },\n missing: (_profileSource) =>\n Fx.fail(\n new AuthError(\n \"OAUTH_INVALID_PROFILE\",\n `Provider \"${providerId}\" does not return an ID token. ` +\n `Add a \\`profile\\` callback in the OAuth() config to extract user info from the access token.`,\n ),\n ),\n });\n}\n\n/**\n * Validate that the profile has a non-empty string `id`.\n */\nfunction validateProfileId(\n providerId: string,\n profile: OAuthProfile,\n): Fx<OAuthProfile, AuthError> {\n return typeof profile.id === \"string\" && profile.id\n ? Fx.succeed(profile)\n : Fx.fail(\n new AuthError(\n \"OAUTH_INVALID_PROFILE\",\n `The profile callback for \"${providerId}\" must return an object with a string \\`id\\` field.`,\n ),\n );\n}\n\n// ============================================================================\n// Authorization URL creation\n// ============================================================================\n\n/**\n * Create an OAuth authorization URL using an Arctic provider.\n *\n * Handles PKCE detection, state generation, and cookie creation.\n */\n/** @internal */\nexport async function createOAuthAuthorizationURL(\n providerId: string,\n arcticProvider: any,\n oauthConfig: OAuthProviderConfigLike,\n): Promise<AuthorizationResult> {\n const state = arctic.generateState();\n const cookies: OAuthCookie[] = [];\n let codeVerifier: string | undefined;\n\n const scopes = oauthConfig.scopes ?? [];\n\n let url: URL;\n\n if (isPKCEProvider(arcticProvider)) {\n codeVerifier = arctic.generateCodeVerifier();\n url = arcticProvider.createAuthorizationURL(state, codeVerifier, scopes);\n cookies.push(createCookie(\"pkce\", providerId, codeVerifier));\n } else {\n url = arcticProvider.createAuthorizationURL(state, scopes);\n }\n\n cookies.push(createCookie(\"state\", providerId, state));\n\n if (oauthConfig.nonce === true) {\n const nonce = arctic.generateState();\n url.searchParams.set(\"nonce\", nonce);\n cookies.push(createCookie(\"nonce\", providerId, nonce));\n }\n\n logWithLevel(\"DEBUG\", \"OAuth authorization URL created\", {\n url: url.toString(),\n providerId,\n hasPKCE: !!codeVerifier,\n });\n\n const signature = getAuthorizationSignature({ codeVerifier, state });\n\n return {\n redirect: url.toString(),\n cookies,\n signature,\n };\n}\n\n// ============================================================================\n// OAuth callback handling\n// ============================================================================\n\n/**\n * Handle the OAuth callback: validate state, exchange code for tokens,\n * extract profile.\n *\n * Returns `Fx<CallbackResult, AuthError>` composed via `Fx.gen`.\n */\n/** @internal */\nexport function handleOAuthCallback(\n providerId: string,\n arcticProvider: any,\n oauthConfig: OAuthProviderConfigLike,\n params: Record<string, string>,\n cookies: Record<string, string | undefined>,\n): Fx<CallbackResult, AuthError> {\n return Fx.gen(function* () {\n const resCookies: OAuthCookie[] = [];\n\n // 1. Validate state\n const stateCookieName = oauthCookieName(\"state\", providerId);\n const storedState = cookies[stateCookieName];\n const returnedState = params.state;\n\n yield* Fx.guard(\n !storedState || !returnedState || storedState !== returnedState,\n Fx.fail(new AuthError(\"OAUTH_INVALID_STATE\")),\n );\n resCookies.push(clearCookie(\"state\", providerId));\n\n // Check for error from provider\n if (params.error) {\n const cause = {\n providerId,\n error: params.error,\n error_description: params.error_description,\n };\n logWithLevel(\"DEBUG\", \"OAuthCallbackError\", cause);\n yield* Fx.fail(\n new AuthError(\n \"OAUTH_PROVIDER_ERROR\",\n \"OAuth provider returned an error\",\n {\n cause: JSON.stringify(cause),\n },\n ),\n );\n }\n\n // 2. Get code\n const code = yield* params.code != null\n ? Fx.succeed(params.code)\n : Fx.fail(\n new AuthError(\n \"OAUTH_PROVIDER_ERROR\",\n \"Missing authorization code in callback\",\n ),\n );\n\n // 3. Read PKCE verifier from cookie if applicable\n let codeVerifier: string | undefined;\n if (isPKCEProvider(arcticProvider)) {\n const pkceCookieName = oauthCookieName(\"pkce\", providerId);\n codeVerifier = yield* cookies[pkceCookieName] != null\n ? Fx.succeed(cookies[pkceCookieName]!)\n : Fx.fail(\n new AuthError(\n \"OAUTH_MISSING_VERIFIER\",\n \"Missing PKCE verifier cookie for OAuth callback\",\n ),\n );\n resCookies.push(clearCookie(\"pkce\", providerId));\n }\n\n let nonce: string | undefined;\n if (oauthConfig.nonce === true) {\n const nonceCookieName = oauthCookieName(\"nonce\", providerId);\n nonce = yield* cookies[nonceCookieName] != null\n ? Fx.succeed(cookies[nonceCookieName]!)\n : Fx.fail(\n new AuthError(\n \"OAUTH_PROVIDER_ERROR\",\n \"Missing nonce cookie for OAuth callback\",\n ),\n );\n resCookies.push(clearCookie(\"nonce\", providerId));\n }\n\n // 4. Exchange code for tokens\n const tokens = yield* exchangeCode(arcticProvider, code, codeVerifier);\n\n if (oauthConfig.validateTokens !== undefined) {\n yield* Fx.from({\n ok: () => oauthConfig.validateTokens!(tokens, { nonce }),\n err: (e) =>\n new AuthError(\n \"OAUTH_PROVIDER_ERROR\",\n `Token validation failed: ${e instanceof Error ? e.message : String(e)}`,\n ),\n });\n }\n\n // 5. Extract profile\n const rawProfile = yield* extractProfile(providerId, oauthConfig, tokens);\n const profile = yield* validateProfileId(providerId, rawProfile);\n\n logWithLevel(\"DEBUG\", \"OAuth callback profile extracted\", {\n providerId,\n profileId: profile.id,\n });\n\n // 6. Compute signature for verifier validation\n const state = storedState!;\n const signature = getAuthorizationSignature({ codeVerifier, state });\n\n return {\n profile,\n providerAccountId: profile.id,\n cookies: resCookies,\n signature,\n };\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA+DA,MAAM,aAAa;AAEnB,SAAS,gBAAgB,MAAkC,YAAoB;AAE7E,SADe,CAAC,YAAY,QAAQ,IAAI,gBAAgB,GAAG,YAAY,MACvD,aAAa,UAAU;;AAGzC,SAAS,aACP,MACA,YACA,OACa;CACb,MAAM,0BAAU,IAAI,MAAM;AAC1B,SAAQ,QAAQ,QAAQ,SAAS,GAAG,aAAa,IAAK;AACtD,QAAO;EACL,MAAM,gBAAgB,MAAM,WAAW;EACvC;EACA,SAAS;GAAE,GAAG;GAAuB;GAAS;EAC/C;;AAGH,SAAS,YACP,MACA,YACa;AACb,QAAO;EACL,MAAM,gBAAgB,MAAM,WAAW;EACvC,OAAO;EACP,SAAS;GAAE,GAAG;GAAuB,QAAQ;GAAG;EACjD;;;;;;;AAYH,SAAgB,0BAA0B,EACxC,cACA,SAIC;AACD,QAAO,CAAC,cAAc,MAAM,CAAC,QAAQ,UAAU,UAAU,OAAU,CAAC,KAAK,IAAI;;;;;;;AAY/E,SAAS,eAAe,UAAwB;AAC9C,QACE,OAAO,SAAS,2BAA2B,cAC3C,SAAS,uBAAuB,UAAU;;;;;;AAY9C,SAAS,aACP,gBACA,MACA,cACoC;AACpC,QAAO,GAAG,KAAK;EACb,UACE,eAAe,eAAe,GAC1B,eAAe,0BAA0B,MAAM,aAAa,GAC5D,eAAe,0BAA0B,KAAK;EACpD,MAAM,MAAM;AACV,OAAI,aAAa,OAAO,mBACtB,QAAO,IAAI,UACT,wBACA,0BAA0B,EAAE,OAC7B;AAEH,OAAI,aAAa,OAAO,iBACtB,QAAO,IAAI,UACT,wBACA,wCAAwC,EAAE,UAC3C;AAKH,UAAO,IAAI,UACT,wBACA,2CAA2C,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,GACtF;;EAEJ,CAAC,CAAC,KACD,GAAG,OAAO,WAAW;AAKnB,SAAO,GAAG,QAAQ,OAAO;GACzB,CACH;;;;;;AAOH,SAAS,eACP,YACA,aACA,QAC6B;CAC7B,MAAM,aACJ,cAAc,OAAO,QACrB,OAAQ,OAAO,KAAa,aAAa;CAC3C,MAAM,gBAAgB,YAAY,UAC9B,EAAE,QAAQ,YAAqB,GAC/B,aACE,EAAE,QAAQ,WAAoB,GAC9B,EAAE,QAAQ,WAAoB;AAEpC,QAAO,GAAG,MAAM,eAAe,cAAc,QAAQ;EACnD,WAAW,mBACT,GAAG,KAAK;GACN,UAAU,YAAY,QAAS,OAAO;GACtC,MAAM,MACJ,IAAI,UACF,yBACA,2BAA2B,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,GACtE;GACJ,CAAC;EACJ,UAAU,mBAAmB;GAC3B,MAAM,SAAS,OAAO,cAAc,OAAO,SAAS,CAAC;AAIrD,UAAO,GAAG,QAAQ;IAChB,IAAK,OAAO,OAAkB,OAAO,YAAY;IACjD,MAAO,OAAO,QAAmB;IACjC,OAAQ,OAAO,SAAoB;IACnC,OAAQ,OAAO,WAAsB;IACtC,CAAC;;EAEJ,UAAU,mBACR,GAAG,KACD,IAAI,UACF,yBACA,aAAa,WAAW,6HAEzB,CACF;EACJ,CAAC;;;;;AAMJ,SAAS,kBACP,YACA,SAC6B;AAC7B,QAAO,OAAO,QAAQ,OAAO,YAAY,QAAQ,KAC7C,GAAG,QAAQ,QAAQ,GACnB,GAAG,KACD,IAAI,UACF,yBACA,6BAA6B,WAAW,qDACzC,CACF;;;;;;;;AAaP,eAAsB,4BACpB,YACA,gBACA,aAC8B;CAC9B,MAAM,QAAQ,OAAO,eAAe;CACpC,MAAM,UAAyB,EAAE;CACjC,IAAI;CAEJ,MAAM,SAAS,YAAY,UAAU,EAAE;CAEvC,IAAI;AAEJ,KAAI,eAAe,eAAe,EAAE;AAClC,iBAAe,OAAO,sBAAsB;AAC5C,QAAM,eAAe,uBAAuB,OAAO,cAAc,OAAO;AACxE,UAAQ,KAAK,aAAa,QAAQ,YAAY,aAAa,CAAC;OAE5D,OAAM,eAAe,uBAAuB,OAAO,OAAO;AAG5D,SAAQ,KAAK,aAAa,SAAS,YAAY,MAAM,CAAC;AAEtD,KAAI,YAAY,UAAU,MAAM;EAC9B,MAAM,QAAQ,OAAO,eAAe;AACpC,MAAI,aAAa,IAAI,SAAS,MAAM;AACpC,UAAQ,KAAK,aAAa,SAAS,YAAY,MAAM,CAAC;;AAGxD,cAAa,SAAS,mCAAmC;EACvD,KAAK,IAAI,UAAU;EACnB;EACA,SAAS,CAAC,CAAC;EACZ,CAAC;CAEF,MAAM,YAAY,0BAA0B;EAAE;EAAc;EAAO,CAAC;AAEpE,QAAO;EACL,UAAU,IAAI,UAAU;EACxB;EACA;EACD;;;;;;;;;AAcH,SAAgB,oBACd,YACA,gBACA,aACA,QACA,SAC+B;AAC/B,QAAO,GAAG,IAAI,aAAa;EACzB,MAAM,aAA4B,EAAE;EAIpC,MAAM,cAAc,QADI,gBAAgB,SAAS,WAAW;EAE5D,MAAM,gBAAgB,OAAO;AAE7B,SAAO,GAAG,MACR,CAAC,eAAe,CAAC,iBAAiB,gBAAgB,eAClD,GAAG,KAAK,IAAI,UAAU,sBAAsB,CAAC,CAC9C;AACD,aAAW,KAAK,YAAY,SAAS,WAAW,CAAC;AAGjD,MAAI,OAAO,OAAO;GAChB,MAAM,QAAQ;IACZ;IACA,OAAO,OAAO;IACd,mBAAmB,OAAO;IAC3B;AACD,gBAAa,SAAS,sBAAsB,MAAM;AAClD,UAAO,GAAG,KACR,IAAI,UACF,wBACA,oCACA,EACE,OAAO,KAAK,UAAU,MAAM,EAC7B,CACF,CACF;;EAIH,MAAM,OAAO,OAAO,OAAO,QAAQ,OAC/B,GAAG,QAAQ,OAAO,KAAK,GACvB,GAAG,KACD,IAAI,UACF,wBACA,yCACD,CACF;EAGL,IAAI;AACJ,MAAI,eAAe,eAAe,EAAE;GAClC,MAAM,iBAAiB,gBAAgB,QAAQ,WAAW;AAC1D,kBAAe,OAAO,QAAQ,mBAAmB,OAC7C,GAAG,QAAQ,QAAQ,gBAAiB,GACpC,GAAG,KACD,IAAI,UACF,0BACA,kDACD,CACF;AACL,cAAW,KAAK,YAAY,QAAQ,WAAW,CAAC;;EAGlD,IAAI;AACJ,MAAI,YAAY,UAAU,MAAM;GAC9B,MAAM,kBAAkB,gBAAgB,SAAS,WAAW;AAC5D,WAAQ,OAAO,QAAQ,oBAAoB,OACvC,GAAG,QAAQ,QAAQ,iBAAkB,GACrC,GAAG,KACD,IAAI,UACF,wBACA,0CACD,CACF;AACL,cAAW,KAAK,YAAY,SAAS,WAAW,CAAC;;EAInD,MAAM,SAAS,OAAO,aAAa,gBAAgB,MAAM,aAAa;AAEtE,MAAI,YAAY,mBAAmB,OACjC,QAAO,GAAG,KAAK;GACb,UAAU,YAAY,eAAgB,QAAQ,EAAE,OAAO,CAAC;GACxD,MAAM,MACJ,IAAI,UACF,wBACA,4BAA4B,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,GACvE;GACJ,CAAC;EAKJ,MAAM,UAAU,OAAO,kBAAkB,YADtB,OAAO,eAAe,YAAY,aAAa,OAAO,CACT;AAEhE,eAAa,SAAS,oCAAoC;GACxD;GACA,WAAW,QAAQ;GACpB,CAAC;EAIF,MAAM,YAAY,0BAA0B;GAAE;GAAc,OAD9C;GACqD,CAAC;AAEpE,SAAO;GACL;GACA,mBAAmB,QAAQ;GAC3B,SAAS;GACT;GACD;GACD"}
|
|
1
|
+
{"version":3,"file":"oauth.js","names":[],"sources":["../../src/server/oauth.ts"],"sourcesContent":["/**\n * Arctic-based OAuth flow implementation.\n *\n * Uses Arctic for OAuth provider integration.\n *\n * All functions return `Fx<A, ConvexError<any>>` composed via `Fx.gen` pipelines.\n *\n * @internal\n * @module\n */\n\nimport { Fx } from \"@robelest/fx\";\nimport { Cv } from \"@robelest/fx/convex\";\nimport * as arctic from \"arctic\";\nimport type { ConvexError } from \"convex/values\";\n\nimport { SHARED_COOKIE_OPTIONS } from \"./cookies\";\nimport type { OAuthProfile } from \"./types\";\nimport { logWithLevel } from \"./utils\";\nimport { isLocalHost } from \"./utils\";\n\ntype OAuthProviderConfigLike = {\n scopes?: string[];\n profile?: (tokens: arctic.OAuth2Tokens) => Promise<OAuthProfile>;\n nonce?: boolean;\n validateTokens?: (\n tokens: arctic.OAuth2Tokens,\n ctx: { nonce?: string },\n ) => Promise<void>;\n};\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/** A cookie to be set on the HTTP response. */\n/** @internal */\nexport interface OAuthCookie {\n name: string;\n value: string;\n options: Record<string, unknown>;\n}\n\n/** Result of creating an authorization URL. */\n/** @internal */\nexport interface AuthorizationResult {\n redirect: string;\n cookies: OAuthCookie[];\n signature: string;\n}\n\n/** Result of handling an OAuth callback. */\n/** @internal */\nexport interface CallbackResult {\n profile: OAuthProfile;\n providerAccountId: string;\n cookies: OAuthCookie[];\n signature: string;\n}\n\n// ============================================================================\n// Cookie helpers\n// ============================================================================\n\nconst COOKIE_TTL = 60 * 15; // 15 minutes\n\nfunction oauthCookieName(type: \"state\" | \"pkce\" | \"nonce\", providerId: string) {\n const prefix = !isLocalHost(process.env.CONVEX_SITE_URL) ? \"__Host-\" : \"\";\n return prefix + providerId + \"OAuth\" + type;\n}\n\nfunction createCookie(\n type: \"state\" | \"pkce\" | \"nonce\",\n providerId: string,\n value: string,\n): OAuthCookie {\n const expires = new Date();\n expires.setTime(expires.getTime() + COOKIE_TTL * 1000);\n return {\n name: oauthCookieName(type, providerId),\n value,\n options: { ...SHARED_COOKIE_OPTIONS, expires },\n };\n}\n\nfunction clearCookie(\n type: \"state\" | \"pkce\" | \"nonce\",\n providerId: string,\n): OAuthCookie {\n return {\n name: oauthCookieName(type, providerId),\n value: \"\",\n options: { ...SHARED_COOKIE_OPTIONS, maxAge: 0 },\n };\n}\n\n// ============================================================================\n// Signature (ConvexAuth-specific verifier mechanism)\n// ============================================================================\n\n/**\n * Creates a signature string from the OAuth state parameters.\n * This is stored in the verifier table and validated during callback.\n */\n/** @internal */\nexport function getAuthorizationSignature({\n codeVerifier,\n state,\n}: {\n codeVerifier?: string;\n state?: string;\n}) {\n return [codeVerifier, state].filter((param) => param !== undefined).join(\" \");\n}\n\n// ============================================================================\n// PKCE Detection\n// ============================================================================\n\n/**\n * Detect whether an Arctic provider uses PKCE by checking the arity\n * of `createAuthorizationURL`. PKCE providers take 3 args\n * (state, codeVerifier, scopes), non-PKCE take 2 (state, scopes).\n */\nfunction isPKCEProvider(provider: any): boolean {\n return (\n typeof provider.createAuthorizationURL === \"function\" &&\n provider.createAuthorizationURL.length >= 3\n );\n}\n\n// ============================================================================\n// Token exchange — wraps Arctic's validateAuthorizationCode\n// ============================================================================\n\n/**\n * Exchange the authorization code for tokens via Arctic.\n * Maps Arctic-specific errors to typed `ConvexError<any>` failures.\n */\nfunction exchangeCode(\n arcticProvider: any,\n code: string,\n codeVerifier: string | undefined,\n): Fx<arctic.OAuth2Tokens, ConvexError<any>> {\n return Fx.from({\n ok: () =>\n isPKCEProvider(arcticProvider)\n ? arcticProvider.validateAuthorizationCode(code, codeVerifier)\n : arcticProvider.validateAuthorizationCode(code),\n err: (e) => {\n if (e instanceof arctic.OAuth2RequestError) {\n return Cv.error({\n code: \"OAUTH_PROVIDER_ERROR\",\n message: `Token exchange failed: ${e.code}`,\n });\n }\n if (e instanceof arctic.ArcticFetchError) {\n return Cv.error({\n code: \"OAUTH_PROVIDER_ERROR\",\n message: `Network error during token exchange: ${e.message}`,\n });\n }\n // Unknown error — treat as unrecoverable defect; we surface it as\n // an ConvexError<any> here so the pipeline type stays Fx<_, ConvexError<any>>.\n // The original `throw e` re-throw is replicated via Fx.fatal below.\n return Cv.error({\n code: \"OAUTH_PROVIDER_ERROR\",\n message: `Unexpected error during token exchange: ${e instanceof Error ? e.message : String(e)}`,\n });\n },\n }).pipe(\n Fx.chain((tokens) => {\n // If the original error was neither OAuth2RequestError nor\n // ArcticFetchError the old code re-threw it raw. We replicate that\n // by checking whether we created an \"Unexpected\" marker message\n // — but since `Fx.from` already mapped it, we just pass through.\n return Fx.succeed(tokens);\n }),\n );\n}\n\n/**\n * Extract the user profile from tokens using the config callback,\n * OIDC auto-decode, or fail if neither is available.\n */\nfunction extractProfile(\n providerId: string,\n oauthConfig: OAuthProviderConfigLike,\n tokens: arctic.OAuth2Tokens,\n): Fx<OAuthProfile, ConvexError<any>> {\n const hasIdToken =\n \"id_token\" in tokens.data &&\n typeof (tokens.data as any).id_token === \"string\";\n const profileSource = oauthConfig.profile\n ? { source: \"callback\" as const }\n : hasIdToken\n ? { source: \"idToken\" as const }\n : { source: \"missing\" as const };\n\n return Fx.match(profileSource, profileSource.source, {\n callback: (_profileSource) =>\n Fx.from({\n ok: () => oauthConfig.profile!(tokens),\n err: (e) =>\n Cv.error({\n code: \"OAUTH_INVALID_PROFILE\",\n message: `Profile callback threw: ${e instanceof Error ? e.message : String(e)}`,\n }),\n }),\n idToken: (_profileSource) => {\n const claims = arctic.decodeIdToken(tokens.idToken()) as Record<\n string,\n unknown\n >;\n return Fx.succeed({\n id: (claims.sub as string) ?? crypto.randomUUID(),\n name: (claims.name as string) ?? undefined,\n email: (claims.email as string) ?? undefined,\n image: (claims.picture as string) ?? undefined,\n });\n },\n missing: (_profileSource) =>\n Cv.fail({\n code: \"OAUTH_INVALID_PROFILE\",\n message:\n `Provider \"${providerId}\" does not return an ID token. ` +\n `Add a \\`profile\\` callback in the OAuth() config to extract user info from the access token.`,\n }),\n });\n}\n\n/**\n * Validate that the profile has a non-empty string `id`.\n */\nfunction validateProfileId(\n providerId: string,\n profile: OAuthProfile,\n): Fx<OAuthProfile, ConvexError<any>> {\n return typeof profile.id === \"string\" && profile.id\n ? Fx.succeed(profile)\n : Cv.fail({\n code: \"OAUTH_INVALID_PROFILE\",\n message: `The profile callback for \"${providerId}\" must return an object with a string \\`id\\` field.`,\n });\n}\n\n// ============================================================================\n// Authorization URL creation\n// ============================================================================\n\n/**\n * Create an OAuth authorization URL using an Arctic provider.\n *\n * Handles PKCE detection, state generation, and cookie creation.\n */\n/** @internal */\nexport async function createOAuthAuthorizationURL(\n providerId: string,\n arcticProvider: any,\n oauthConfig: OAuthProviderConfigLike,\n): Promise<AuthorizationResult> {\n const state = arctic.generateState();\n const cookies: OAuthCookie[] = [];\n let codeVerifier: string | undefined;\n\n const scopes = oauthConfig.scopes ?? [];\n\n let url: URL;\n\n if (isPKCEProvider(arcticProvider)) {\n codeVerifier = arctic.generateCodeVerifier();\n url = arcticProvider.createAuthorizationURL(state, codeVerifier, scopes);\n cookies.push(createCookie(\"pkce\", providerId, codeVerifier));\n } else {\n url = arcticProvider.createAuthorizationURL(state, scopes);\n }\n\n cookies.push(createCookie(\"state\", providerId, state));\n\n if (oauthConfig.nonce === true) {\n const nonce = arctic.generateState();\n url.searchParams.set(\"nonce\", nonce);\n cookies.push(createCookie(\"nonce\", providerId, nonce));\n }\n\n logWithLevel(\"DEBUG\", \"OAuth authorization URL created\", {\n url: url.toString(),\n providerId,\n hasPKCE: !!codeVerifier,\n });\n\n const signature = getAuthorizationSignature({ codeVerifier, state });\n\n return {\n redirect: url.toString(),\n cookies,\n signature,\n };\n}\n\n// ============================================================================\n// OAuth callback handling\n// ============================================================================\n\n/**\n * Handle the OAuth callback: validate state, exchange code for tokens,\n * extract profile.\n *\n * Returns `Fx<CallbackResult, ConvexError<any>>` composed via `Fx.gen`.\n */\n/** @internal */\nexport function handleOAuthCallback(\n providerId: string,\n arcticProvider: any,\n oauthConfig: OAuthProviderConfigLike,\n params: Record<string, string>,\n cookies: Record<string, string | undefined>,\n): Fx<CallbackResult, ConvexError<any>> {\n return Fx.gen(function* () {\n const resCookies: OAuthCookie[] = [];\n\n // 1. Validate state\n const stateCookieName = oauthCookieName(\"state\", providerId);\n const storedState = cookies[stateCookieName];\n const returnedState = params.state;\n\n yield* Fx.guard(\n !storedState || !returnedState || storedState !== returnedState,\n Cv.fail({\n code: \"OAUTH_INVALID_STATE\",\n message: \"Invalid OAuth state. Please try signing in again.\",\n }),\n );\n resCookies.push(clearCookie(\"state\", providerId));\n\n // Check for error from provider\n if (params.error) {\n const cause = {\n providerId,\n error: params.error,\n error_description: params.error_description,\n };\n logWithLevel(\"DEBUG\", \"OAuthCallbackError\", cause);\n yield* Cv.fail({\n code: \"OAUTH_PROVIDER_ERROR\",\n message: \"OAuth provider returned an error\",\n cause: JSON.stringify(cause),\n });\n }\n\n // 2. Get code\n const code = yield* params.code != null\n ? Fx.succeed(params.code)\n : Cv.fail({\n code: \"OAUTH_PROVIDER_ERROR\",\n message: \"Missing authorization code in callback\",\n });\n\n // 3. Read PKCE verifier from cookie if applicable\n let codeVerifier: string | undefined;\n if (isPKCEProvider(arcticProvider)) {\n const pkceCookieName = oauthCookieName(\"pkce\", providerId);\n codeVerifier = yield* cookies[pkceCookieName] != null\n ? Fx.succeed(cookies[pkceCookieName]!)\n : Cv.fail({\n code: \"OAUTH_MISSING_VERIFIER\",\n message: \"Missing PKCE verifier cookie for OAuth callback\",\n });\n resCookies.push(clearCookie(\"pkce\", providerId));\n }\n\n let nonce: string | undefined;\n if (oauthConfig.nonce === true) {\n const nonceCookieName = oauthCookieName(\"nonce\", providerId);\n nonce = yield* cookies[nonceCookieName] != null\n ? Fx.succeed(cookies[nonceCookieName]!)\n : Cv.fail({\n code: \"OAUTH_PROVIDER_ERROR\",\n message: \"Missing nonce cookie for OAuth callback\",\n });\n resCookies.push(clearCookie(\"nonce\", providerId));\n }\n\n // 4. Exchange code for tokens\n const tokens = yield* exchangeCode(arcticProvider, code, codeVerifier);\n\n if (oauthConfig.validateTokens !== undefined) {\n yield* Fx.from({\n ok: () => oauthConfig.validateTokens!(tokens, { nonce }),\n err: (e) =>\n Cv.error({\n code: \"OAUTH_PROVIDER_ERROR\",\n message: `Token validation failed: ${e instanceof Error ? e.message : String(e)}`,\n }),\n });\n }\n\n // 5. Extract profile\n const rawProfile = yield* extractProfile(providerId, oauthConfig, tokens);\n const profile = yield* validateProfileId(providerId, rawProfile);\n\n logWithLevel(\"DEBUG\", \"OAuth callback profile extracted\", {\n providerId,\n profileId: profile.id,\n });\n\n // 6. Compute signature for verifier validation\n const state = storedState!;\n const signature = getAuthorizationSignature({ codeVerifier, state });\n\n return {\n profile,\n providerAccountId: profile.id,\n cookies: resCookies,\n signature,\n };\n });\n}\n"],"mappings":"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"}
|
package/dist/server/passkey.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AuthDataModel, GenericActionCtxWithAuthConfig, PasskeyProviderConfig, SessionInfo } from "./types.js";
|
|
2
|
-
import { AuthError } from "./authError.js";
|
|
3
2
|
import { Fx } from "@robelest/fx";
|
|
3
|
+
import { ConvexError } from "convex/values";
|
|
4
4
|
|
|
5
5
|
//#region src/server/passkey.d.ts
|
|
6
6
|
type EnrichedActionCtx = GenericActionCtxWithAuthConfig<AuthDataModel>;
|
|
@@ -21,7 +21,7 @@ type PasskeyResult = {
|
|
|
21
21
|
declare function handlePasskeyFx(ctx: EnrichedActionCtx, provider: PasskeyProviderConfig, args: {
|
|
22
22
|
params?: Record<string, any>;
|
|
23
23
|
verifier?: string;
|
|
24
|
-
}): Fx<PasskeyResult,
|
|
24
|
+
}): Fx<PasskeyResult, ConvexError<any>>;
|
|
25
25
|
//#endregion
|
|
26
26
|
export { handlePasskeyFx };
|
|
27
27
|
//# sourceMappingURL=passkey.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"passkey.d.ts","names":[],"sources":["../../src/server/passkey.ts"],"mappings":";;;;;KAmEK,iBAAA,GAAoB,8BAAA,CAA+B,aAAA;;
|
|
1
|
+
{"version":3,"file":"passkey.d.ts","names":[],"sources":["../../src/server/passkey.ts"],"mappings":";;;;;KAmEK,iBAAA,GAAoB,8BAAA,CAA+B,aAAA;;KAqLnD,aAAA;EACC,IAAA;EAAkB,QAAA,EAAU,WAAA;AAAA;EAC5B,IAAA;EAAwB,OAAA,EAAS,MAAA;EAAqB,QAAA;AAAA;;;;;;iBAkD5C,eAAA,CACd,GAAA,EAAK,iBAAA,EACL,QAAA,EAAU,qBAAA,EACV,IAAA;EACE,MAAA,GAAS,MAAA;EACT,QAAA;AAAA,IAED,EAAA,CAAO,aAAA,EAAe,WAAA"}
|
package/dist/server/passkey.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { AuthError } from "./authError.js";
|
|
2
1
|
import { userIdFromIdentitySubject } from "./identity.js";
|
|
3
2
|
import { authDb } from "./db.js";
|
|
4
3
|
import { callVerifierSignature } from "./mutations/signature.js";
|
|
@@ -6,6 +5,7 @@ import { callSignIn } from "./mutations/signin.js";
|
|
|
6
5
|
import { callVerifier } from "./mutations/verifier.js";
|
|
7
6
|
import { mutatePasskeyInsert, mutatePasskeyUpdateCounter, mutateVerifierDelete, queryPasskeyByCredentialId, queryPasskeysByUserId, queryUserById, queryUserByVerifiedEmail, queryVerifierById } from "./types.js";
|
|
8
7
|
import { Fx } from "@robelest/fx";
|
|
8
|
+
import { Cv } from "@robelest/fx/convex";
|
|
9
9
|
import { sha256 } from "@oslojs/crypto/sha2";
|
|
10
10
|
import { decodeBase64urlIgnorePadding, encodeBase64urlNoPadding } from "@oslojs/encoding";
|
|
11
11
|
import { decodePKIXECDSASignature, decodeSEC1PublicKey, p256, verifyECDSASignature } from "@oslojs/crypto/ecdsa";
|
|
@@ -25,14 +25,14 @@ import { COSEKeyType, ClientDataType, coseAlgorithmES256, coseAlgorithmRS256, cr
|
|
|
25
25
|
* Uses `@oslojs/webauthn` for attestation/assertion parsing and
|
|
26
26
|
* `@oslojs/crypto` for signature verification.
|
|
27
27
|
*
|
|
28
|
-
* All functions return `Fx<A,
|
|
28
|
+
* All functions return `Fx<A, ConvexError<any>>` composed via `Fx.chain` pipelines.
|
|
29
29
|
*
|
|
30
30
|
* @module
|
|
31
31
|
*/
|
|
32
32
|
/**
|
|
33
33
|
* Resolve passkey relying party options from provider config and environment.
|
|
34
34
|
*
|
|
35
|
-
* Returns `Fx<RpOptions,
|
|
35
|
+
* Returns `Fx<RpOptions, ConvexError<any>>` — fails if neither SITE_URL nor rpId
|
|
36
36
|
* is configured.
|
|
37
37
|
*/
|
|
38
38
|
const resolveRpOptionsFx = (provider) => {
|
|
@@ -43,7 +43,10 @@ const resolveRpOptionsFx = (provider) => {
|
|
|
43
43
|
siteUrl,
|
|
44
44
|
hasSiteUrl,
|
|
45
45
|
hasRpId
|
|
46
|
-
}).pipe(Fx.chain(({ siteUrl: siteUrl$1, hasSiteUrl: hasSiteUrl$1, hasRpId: hasRpId$1 }) => !hasSiteUrl$1 && !hasRpId$1 ?
|
|
46
|
+
}).pipe(Fx.chain(({ siteUrl: siteUrl$1, hasSiteUrl: hasSiteUrl$1, hasRpId: hasRpId$1 }) => !hasSiteUrl$1 && !hasRpId$1 ? Cv.fail({
|
|
47
|
+
code: "PASSKEY_MISSING_CONFIG",
|
|
48
|
+
message: "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."
|
|
49
|
+
}) : Fx.succeed(siteUrl$1)), Fx.map((siteUrl$1) => {
|
|
47
50
|
const siteHostname = siteUrl$1 ? new URL(siteUrl$1).hostname : void 0;
|
|
48
51
|
return {
|
|
49
52
|
rpName: provider.options.rpName ?? siteHostname ?? "localhost",
|
|
@@ -59,27 +62,51 @@ const resolveRpOptionsFx = (provider) => {
|
|
|
59
62
|
}));
|
|
60
63
|
};
|
|
61
64
|
/** Verify client data type matches expected WebAuthn ceremony type. */
|
|
62
|
-
const verifyClientDataType = (expectedType, label) => (clientData) => clientData.type === expectedType ? Fx.succeed(clientData) :
|
|
65
|
+
const verifyClientDataType = (expectedType, label) => (clientData) => clientData.type === expectedType ? Fx.succeed(clientData) : Cv.fail({
|
|
66
|
+
code: "PASSKEY_INVALID_CLIENT_DATA",
|
|
67
|
+
message: `Invalid client data type: expected ${label}`
|
|
68
|
+
});
|
|
63
69
|
/** Verify origin is in the allowed list. */
|
|
64
70
|
const verifyOrigin = (rp) => (clientData) => {
|
|
65
71
|
const allowed = Array.isArray(rp.origin) ? rp.origin : [rp.origin];
|
|
66
|
-
return allowed.includes(clientData.origin) ? Fx.succeed(clientData) :
|
|
72
|
+
return allowed.includes(clientData.origin) ? Fx.succeed(clientData) : Cv.fail({
|
|
73
|
+
code: "PASSKEY_INVALID_ORIGIN",
|
|
74
|
+
message: `Invalid origin: ${clientData.origin}, expected one of: ${allowed.join(", ")}`
|
|
75
|
+
});
|
|
67
76
|
};
|
|
68
77
|
/** Verify the challenge hash matches the stored verifier, then delete verifier. */
|
|
69
78
|
const verifyAndConsumeChallenge = (ctx, verifierValue) => (clientData) => {
|
|
70
79
|
const challengeHash = encodeBase64urlNoPadding(new Uint8Array(sha256(clientData.challenge)));
|
|
71
80
|
return Fx.from({
|
|
72
81
|
ok: () => queryVerifierById(ctx, verifierValue),
|
|
73
|
-
err: () =>
|
|
74
|
-
|
|
82
|
+
err: () => Cv.error({
|
|
83
|
+
code: "PASSKEY_INVALID_CHALLENGE",
|
|
84
|
+
message: "Invalid or expired passkey challenge."
|
|
85
|
+
})
|
|
86
|
+
}).pipe(Fx.chain((doc) => !doc || doc.signature !== challengeHash ? Cv.fail({
|
|
87
|
+
code: "PASSKEY_INVALID_CHALLENGE",
|
|
88
|
+
message: "Invalid or expired passkey challenge."
|
|
89
|
+
}) : Fx.succeed(doc)), Fx.chain(() => Fx.from({
|
|
75
90
|
ok: () => mutateVerifierDelete(ctx, verifierValue),
|
|
76
|
-
err: () =>
|
|
91
|
+
err: () => Cv.error({
|
|
92
|
+
code: "PASSKEY_INVALID_CHALLENGE",
|
|
93
|
+
message: "Invalid or expired passkey challenge."
|
|
94
|
+
})
|
|
77
95
|
})), Fx.map(() => clientData));
|
|
78
96
|
};
|
|
79
97
|
/** Verify RP ID hash matches. */
|
|
80
|
-
const verifyRpId = (rpId) => (authData) => authData.verifyRelyingPartyIdHash(rpId) ? Fx.succeed(authData) :
|
|
98
|
+
const verifyRpId = (rpId) => (authData) => authData.verifyRelyingPartyIdHash(rpId) ? Fx.succeed(authData) : Cv.fail({
|
|
99
|
+
code: "PASSKEY_RP_MISMATCH",
|
|
100
|
+
message: "Relying party ID mismatch."
|
|
101
|
+
});
|
|
81
102
|
/** Verify user presence and (optionally) user verification flags. */
|
|
82
|
-
const verifyUserFlags = (rp) => (authData) => !authData.userPresent ?
|
|
103
|
+
const verifyUserFlags = (rp) => (authData) => !authData.userPresent ? Cv.fail({
|
|
104
|
+
code: "PASSKEY_USER_PRESENCE",
|
|
105
|
+
message: "User presence flag not set."
|
|
106
|
+
}) : rp.userVerification === "required" && !authData.userVerified ? Cv.fail({
|
|
107
|
+
code: "PASSKEY_USER_VERIFICATION",
|
|
108
|
+
message: "User verification required but not performed."
|
|
109
|
+
}) : Fx.succeed(authData);
|
|
83
110
|
const PASSKEY_FLOW = {
|
|
84
111
|
registerOptions: "registerOptions",
|
|
85
112
|
registerVerify: "registerVerify",
|
|
@@ -94,9 +121,15 @@ const PASSKEY_FLOWS = [
|
|
|
94
121
|
];
|
|
95
122
|
const resolvePasskeyDispatchFx = (params) => {
|
|
96
123
|
const flow = params.flow;
|
|
97
|
-
return typeof flow === "string" && PASSKEY_FLOWS.includes(flow) ? Fx.succeed({ flow }) :
|
|
124
|
+
return typeof flow === "string" && PASSKEY_FLOWS.includes(flow) ? Fx.succeed({ flow }) : Cv.fail({
|
|
125
|
+
code: "PASSKEY_MISSING_FLOW",
|
|
126
|
+
message: "Missing `flow` parameter. Expected one of: registerOptions, registerVerify, authOptions, authVerify"
|
|
127
|
+
});
|
|
98
128
|
};
|
|
99
|
-
const requirePasskeyVerifierFx = (verifier) => verifier != null ? Fx.succeed(verifier) :
|
|
129
|
+
const requirePasskeyVerifierFx = (verifier) => verifier != null ? Fx.succeed(verifier) : Cv.fail({
|
|
130
|
+
code: "PASSKEY_MISSING_VERIFIER",
|
|
131
|
+
message: "Missing verifier for passkey operation."
|
|
132
|
+
});
|
|
100
133
|
/**
|
|
101
134
|
* Main passkey handler dispatched from signIn.ts.
|
|
102
135
|
*
|
|
@@ -108,8 +141,14 @@ function handlePasskeyFx(ctx, provider, args) {
|
|
|
108
141
|
return Fx.match(dispatch).on("flow", {
|
|
109
142
|
registerOptions: (_) => Fx.zip(Fx.from({
|
|
110
143
|
ok: () => ctx.auth.getUserIdentity(),
|
|
111
|
-
err: () =>
|
|
112
|
-
|
|
144
|
+
err: () => Cv.error({
|
|
145
|
+
code: "PASSKEY_AUTH_REQUIRED",
|
|
146
|
+
message: "Sign in first, then add a passkey to your account."
|
|
147
|
+
})
|
|
148
|
+
}).pipe(Fx.chain((id) => id === null ? Cv.fail({
|
|
149
|
+
code: "PASSKEY_AUTH_REQUIRED",
|
|
150
|
+
message: "Sign in first, then add a passkey to your account."
|
|
151
|
+
}) : Fx.succeed(userIdFromIdentitySubject(id.subject)))), resolveRpOptionsFx(provider)).pipe(Fx.chain(([userId, rp]) => {
|
|
113
152
|
const challenge = new Uint8Array(32);
|
|
114
153
|
crypto.getRandomValues(challenge);
|
|
115
154
|
const challengeHash = encodeBase64urlNoPadding(new Uint8Array(sha256(challenge)));
|
|
@@ -158,18 +197,30 @@ function handlePasskeyFx(ctx, provider, args) {
|
|
|
158
197
|
verifier
|
|
159
198
|
};
|
|
160
199
|
},
|
|
161
|
-
err: () =>
|
|
200
|
+
err: () => Cv.error({
|
|
201
|
+
code: "INTERNAL_ERROR",
|
|
202
|
+
message: "An unexpected error occurred."
|
|
203
|
+
})
|
|
162
204
|
});
|
|
163
205
|
})),
|
|
164
206
|
registerVerify: (_) => Fx.zip(Fx.from({
|
|
165
207
|
ok: () => ctx.auth.getUserIdentity(),
|
|
166
|
-
err: () =>
|
|
167
|
-
|
|
208
|
+
err: () => Cv.error({
|
|
209
|
+
code: "PASSKEY_AUTH_REQUIRED",
|
|
210
|
+
message: "Sign in first, then add a passkey to your account."
|
|
211
|
+
})
|
|
212
|
+
}).pipe(Fx.chain((id) => id === null ? Cv.fail({
|
|
213
|
+
code: "PASSKEY_AUTH_REQUIRED",
|
|
214
|
+
message: "Sign in first, then add a passkey to your account."
|
|
215
|
+
}) : Fx.succeed(userIdFromIdentitySubject(id.subject)))), resolveRpOptionsFx(provider)).pipe(Fx.chain(([userId, rp]) => requirePasskeyVerifierFx(args.verifier).pipe(Fx.chain((verifier) => {
|
|
168
216
|
const clientData = parseClientDataJSON(decodeBase64urlIgnorePadding(params.clientDataJSON));
|
|
169
217
|
return Fx.succeed(clientData).pipe(Fx.chain(verifyClientDataType(ClientDataType.Create, "webauthn.create")), Fx.chain(verifyOrigin(rp)), Fx.chain(verifyAndConsumeChallenge(ctx, verifier)), Fx.map(() => {
|
|
170
218
|
return parseAttestationObject(decodeBase64urlIgnorePadding(params.attestationObject)).authenticatorData;
|
|
171
219
|
})).pipe(Fx.chain(verifyRpId(rp.rpId)), Fx.chain(verifyUserFlags(rp)), Fx.chain((authData) => {
|
|
172
|
-
if (authData.credential == null) return
|
|
220
|
+
if (authData.credential == null) return Cv.fail({
|
|
221
|
+
code: "PASSKEY_NO_CREDENTIAL",
|
|
222
|
+
message: "No credential in attestation."
|
|
223
|
+
});
|
|
173
224
|
return Fx.succeed({
|
|
174
225
|
authData,
|
|
175
226
|
credential: authData.credential
|
|
@@ -210,7 +261,10 @@ function handlePasskeyFx(ctx, provider, args) {
|
|
|
210
261
|
return Fx.succeed(rsaPubKey.encodePKCS1());
|
|
211
262
|
}
|
|
212
263
|
}[algorithm];
|
|
213
|
-
return (handler ? handler() :
|
|
264
|
+
return (handler ? handler() : Cv.fail({
|
|
265
|
+
code: "PASSKEY_UNSUPPORTED_ALGORITHM",
|
|
266
|
+
message: `Unsupported algorithm: ${algorithm}`
|
|
267
|
+
})).pipe(Fx.chain((publicKeyBytes) => Fx.from({
|
|
214
268
|
ok: async () => {
|
|
215
269
|
const deviceType = params.deviceType ?? "single-device";
|
|
216
270
|
const backedUp = params.backedUp ?? false;
|
|
@@ -239,7 +293,10 @@ function handlePasskeyFx(ctx, provider, args) {
|
|
|
239
293
|
})
|
|
240
294
|
};
|
|
241
295
|
},
|
|
242
|
-
err: () =>
|
|
296
|
+
err: () => Cv.error({
|
|
297
|
+
code: "INTERNAL_ERROR",
|
|
298
|
+
message: "An unexpected error occurred."
|
|
299
|
+
})
|
|
243
300
|
})));
|
|
244
301
|
}));
|
|
245
302
|
})))),
|
|
@@ -279,16 +336,28 @@ function handlePasskeyFx(ctx, provider, args) {
|
|
|
279
336
|
verifier
|
|
280
337
|
};
|
|
281
338
|
},
|
|
282
|
-
err: () =>
|
|
339
|
+
err: () => Cv.error({
|
|
340
|
+
code: "INTERNAL_ERROR",
|
|
341
|
+
message: "An unexpected error occurred."
|
|
342
|
+
})
|
|
283
343
|
});
|
|
284
344
|
})),
|
|
285
345
|
authVerify: (_) => Fx.zip(resolveRpOptionsFx(provider), requirePasskeyVerifierFx(args.verifier)).pipe(Fx.chain(([rp, verifier]) => {
|
|
286
346
|
const clientDataJSON = decodeBase64urlIgnorePadding(params.clientDataJSON);
|
|
287
347
|
const clientData = parseClientDataJSON(clientDataJSON);
|
|
288
|
-
return Fx.succeed(clientData).pipe(Fx.chain(verifyClientDataType(ClientDataType.Get, "webauthn.get")), Fx.chain(verifyOrigin(rp)), Fx.chain(verifyAndConsumeChallenge(ctx, verifier)), Fx.chain(() => params.credentialId != null ? Fx.succeed(params.credentialId) :
|
|
348
|
+
return Fx.succeed(clientData).pipe(Fx.chain(verifyClientDataType(ClientDataType.Get, "webauthn.get")), Fx.chain(verifyOrigin(rp)), Fx.chain(verifyAndConsumeChallenge(ctx, verifier)), Fx.chain(() => params.credentialId != null ? Fx.succeed(params.credentialId) : Cv.fail({
|
|
349
|
+
code: "PASSKEY_UNKNOWN_CREDENTIAL",
|
|
350
|
+
message: "Missing credential ID"
|
|
351
|
+
}))).pipe(Fx.chain((credentialId) => Fx.from({
|
|
289
352
|
ok: () => queryPasskeyByCredentialId(ctx, credentialId),
|
|
290
|
-
err: () =>
|
|
291
|
-
|
|
353
|
+
err: () => Cv.error({
|
|
354
|
+
code: "PASSKEY_UNKNOWN_CREDENTIAL",
|
|
355
|
+
message: "Unknown passkey credential."
|
|
356
|
+
})
|
|
357
|
+
}).pipe(Fx.chain((passkey) => passkey ? Fx.succeed(passkey) : Cv.fail({
|
|
358
|
+
code: "PASSKEY_UNKNOWN_CREDENTIAL",
|
|
359
|
+
message: "Unknown credential"
|
|
360
|
+
})))), Fx.chain((passkey) => {
|
|
292
361
|
const authenticatorDataBytes = decodeBase64urlIgnorePadding(params.authenticatorData);
|
|
293
362
|
const authenticatorData = parseAuthenticatorData(authenticatorDataBytes);
|
|
294
363
|
const signature = decodeBase64urlIgnorePadding(params.signature);
|
|
@@ -297,14 +366,26 @@ function handlePasskeyFx(ctx, provider, args) {
|
|
|
297
366
|
const storedPublicKeyBytes = new Uint8Array(passkey.publicKey);
|
|
298
367
|
const handler = {
|
|
299
368
|
[coseAlgorithmES256]: () => {
|
|
300
|
-
return verifyECDSASignature(decodeSEC1PublicKey(p256, storedPublicKeyBytes), messageHash, decodePKIXECDSASignature(signature)) ? Fx.succeed(void 0) :
|
|
369
|
+
return verifyECDSASignature(decodeSEC1PublicKey(p256, storedPublicKeyBytes), messageHash, decodePKIXECDSASignature(signature)) ? Fx.succeed(void 0) : Cv.fail({
|
|
370
|
+
code: "PASSKEY_INVALID_SIGNATURE",
|
|
371
|
+
message: "Invalid passkey signature."
|
|
372
|
+
});
|
|
301
373
|
},
|
|
302
374
|
[coseAlgorithmRS256]: () => {
|
|
303
|
-
return verifyRSASSAPKCS1v15Signature(decodePKCS1RSAPublicKey(storedPublicKeyBytes), sha256ObjectIdentifier, messageHash, signature) ? Fx.succeed(void 0) :
|
|
375
|
+
return verifyRSASSAPKCS1v15Signature(decodePKCS1RSAPublicKey(storedPublicKeyBytes), sha256ObjectIdentifier, messageHash, signature) ? Fx.succeed(void 0) : Cv.fail({
|
|
376
|
+
code: "PASSKEY_INVALID_SIGNATURE",
|
|
377
|
+
message: "Invalid passkey signature."
|
|
378
|
+
});
|
|
304
379
|
}
|
|
305
380
|
}[passkey.algorithm];
|
|
306
|
-
return handler ? handler() :
|
|
307
|
-
|
|
381
|
+
return handler ? handler() : Cv.fail({
|
|
382
|
+
code: "PASSKEY_UNSUPPORTED_ALGORITHM",
|
|
383
|
+
message: `Unsupported algorithm: ${passkey.algorithm}`
|
|
384
|
+
});
|
|
385
|
+
})).pipe(Fx.chain(() => passkey.counter !== 0 && authenticatorData.signatureCounter !== 0 && authenticatorData.signatureCounter <= passkey.counter ? Cv.fail({
|
|
386
|
+
code: "PASSKEY_COUNTER_ERROR",
|
|
387
|
+
message: "Authenticator counter did not increase — possible credential cloning detected."
|
|
388
|
+
}) : Fx.succeed(authenticatorData))).pipe(Fx.chain(() => Fx.from({
|
|
308
389
|
ok: async () => {
|
|
309
390
|
await mutatePasskeyUpdateCounter(ctx, passkey._id, authenticatorData.signatureCounter, Date.now());
|
|
310
391
|
return {
|
|
@@ -315,7 +396,10 @@ function handlePasskeyFx(ctx, provider, args) {
|
|
|
315
396
|
})
|
|
316
397
|
};
|
|
317
398
|
},
|
|
318
|
-
err: () =>
|
|
399
|
+
err: () => Cv.error({
|
|
400
|
+
code: "INTERNAL_ERROR",
|
|
401
|
+
message: "An unexpected error occurred."
|
|
402
|
+
})
|
|
319
403
|
})));
|
|
320
404
|
}));
|
|
321
405
|
}))
|