@robelest/convex-auth 0.0.4-preview.30 → 0.0.4-preview.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +125 -36
- package/dist/browser/index.d.ts +3 -13
- package/dist/browser/index.js +47 -12
- package/dist/browser/navigation.js +1 -1
- package/dist/browser/passkey.js +7 -7
- package/dist/browser/runtime.js +13 -15
- package/dist/client/core/types.d.ts +179 -63
- package/dist/client/core/types.js +6 -0
- package/dist/client/factors/totp.js +1 -1
- package/dist/client/index.d.ts +5 -4
- package/dist/client/index.js +115 -56
- package/dist/client/runtime/mutex.js +3 -2
- package/dist/component/_generated/component.d.ts +40 -0
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/http.js +9 -0
- package/dist/component/index.d.ts +1 -1
- package/dist/component/model.d.ts +25 -25
- package/dist/component/model.js +2 -1
- package/dist/component/modules.js +1 -0
- package/dist/component/public/factors/passkeys.js +31 -1
- package/dist/component/public/identity/codes.js +1 -1
- package/dist/component/public/identity/tokens.js +2 -1
- package/dist/component/public/identity/verifiers.js +15 -5
- package/dist/component/public.js +2 -2
- package/dist/component/schema.d.ts +292 -290
- package/dist/component/schema.js +2 -1
- package/dist/core/index.d.ts +8 -3
- package/dist/core/index.js +7 -2
- package/dist/expo/index.d.ts +21 -0
- package/dist/expo/index.js +148 -0
- package/dist/expo/passkey.js +174 -0
- package/dist/providers/apple.d.ts +1 -1
- package/dist/providers/apple.js +6 -8
- package/dist/providers/custom.d.ts +1 -1
- package/dist/providers/custom.js +4 -7
- package/dist/providers/github.d.ts +1 -1
- package/dist/providers/github.js +5 -8
- package/dist/providers/google.d.ts +1 -1
- package/dist/providers/google.js +5 -8
- package/dist/providers/microsoft.d.ts +1 -1
- package/dist/providers/microsoft.js +5 -9
- package/dist/providers/password.d.ts +18 -37
- package/dist/providers/password.js +170 -115
- package/dist/providers/redirect.d.ts +1 -0
- package/dist/providers/redirect.js +20 -0
- package/dist/server/auth.d.ts +6 -7
- package/dist/server/auth.js +3 -2
- package/dist/server/{ctxCache.js → cache/context.js} +2 -2
- package/dist/server/{componentContext.d.ts → component/context.d.ts} +2 -2
- package/dist/server/context.js +3 -10
- package/dist/server/contract.d.ts +2 -87
- package/dist/server/contract.js +1 -1
- package/dist/server/cookies.js +25 -1
- package/dist/server/core.js +1 -1
- package/dist/server/errors.js +24 -1
- package/dist/server/{auth-context.d.ts → facade.d.ts} +3 -45
- package/dist/server/{auth-context.js → facade.js} +11 -12
- package/dist/server/http.d.ts +7 -7
- package/dist/server/http.js +36 -7
- package/dist/server/{convexIdentity.d.ts → identity/convex.d.ts} +3 -3
- package/dist/server/index.d.ts +5 -3
- package/dist/server/index.js +3 -2
- package/dist/server/mounts.d.ts +175 -22
- package/dist/server/mutations/code.js +7 -1
- package/dist/server/mutations/{credentialsSignIn.js → credentials/signin.js} +10 -10
- package/dist/server/mutations/index.js +1 -1
- package/dist/server/mutations/invalidate.js +11 -1
- package/dist/server/mutations/oauth.js +25 -27
- package/dist/server/mutations/signin.js +6 -0
- package/dist/server/mutations/signout.js +5 -0
- package/dist/server/mutations/store.js +1 -1
- package/dist/server/oauth/factory.js +11 -3
- package/dist/server/passkey.js +126 -110
- package/dist/server/prefetch.js +8 -1
- package/dist/server/redirects.js +11 -3
- package/dist/server/refresh.js +6 -1
- package/dist/server/runtime.d.ts +58 -36
- package/dist/server/runtime.js +333 -36
- package/dist/server/services/group.js +4 -0
- package/dist/server/sessions.js +1 -0
- package/dist/server/signin.js +8 -6
- package/dist/server/sso/domain.d.ts +159 -16
- package/dist/server/sso/domain.js +1 -1
- package/dist/server/sso/http.js +144 -60
- package/dist/server/sso/oidc.js +28 -12
- package/dist/server/sso/policy.js +30 -14
- package/dist/server/sso/provision.js +1 -1
- package/dist/server/sso/saml.js +18 -9
- package/dist/server/sso/scim.js +12 -4
- package/dist/server/sso/shared.js +5 -5
- package/dist/server/telemetry.js +3 -0
- package/dist/server/tokens.js +10 -2
- package/dist/server/totp.js +127 -100
- package/dist/server/types.d.ts +224 -151
- package/dist/server/url.js +1 -1
- package/dist/server/users.js +93 -53
- package/dist/server/wellknown.d.ts +130 -0
- package/dist/server/wellknown.js +195 -0
- package/dist/shared/errors.js +0 -1
- package/package.json +36 -4
- package/dist/server/oauth/index.js +0 -12
- package/dist/server/utils/dispatch.js +0 -36
- package/dist/shared/authResults.d.ts +0 -16
- /package/dist/server/{componentContext.js → component/context.js} +0 -0
- /package/dist/server/{convexIdentity.js → identity/convex.js} +0 -0
package/dist/server/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LOG_LEVELS } from "../shared/log.js";
|
|
2
2
|
import { generateRandomString, sha256 } from "./random.js";
|
|
3
3
|
import { createCoreDomains } from "./core.js";
|
|
4
|
-
import { requireEnv } from "./env.js";
|
|
4
|
+
import { envOptionalString, readConfigSync, requireEnv } from "./env.js";
|
|
5
5
|
import { log, logError } from "./log.js";
|
|
6
6
|
import { callModifyAccount } from "./mutations/account.js";
|
|
7
7
|
import { callInvalidateSessions } from "./mutations/invalidate.js";
|
|
@@ -14,8 +14,10 @@ import { callVerifierSignature } from "./mutations/signature.js";
|
|
|
14
14
|
import { callSignOut } from "./mutations/signout.js";
|
|
15
15
|
import { storeArgs, storeImpl } from "./mutations/store.js";
|
|
16
16
|
import { siteUrlsFromEnv } from "./url.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { decodeOAuthState, encodeOAuthState } from "./cookies.js";
|
|
18
|
+
import { FlowSignal } from "./errors.js";
|
|
19
|
+
import { addAuthRoutes, addOpenIdRoutes, addWebAuthnRoute, convertErrorsToResponse, createHttpAction, createHttpContext, createHttpRoute, getCookies } from "./http.js";
|
|
20
|
+
import { generateWebAuthnConfig } from "./wellknown.js";
|
|
19
21
|
import { createOAuthAuthorizationURL, handleOAuthCallback } from "./oauth/runtime.js";
|
|
20
22
|
import { redirectAbsoluteUrl, setURLSearchParam } from "./redirects.js";
|
|
21
23
|
import { encryptSecret } from "./secret.js";
|
|
@@ -24,11 +26,61 @@ import { resolveServerServices } from "./services/resolve.js";
|
|
|
24
26
|
import { createGroupConnectionDomain } from "./sso/domain.js";
|
|
25
27
|
import { addGroupHttpRuntime } from "./sso/http.js";
|
|
26
28
|
import { ConvexError, v } from "convex/values";
|
|
27
|
-
import { actionGeneric, internalMutationGeneric } from "convex/server";
|
|
29
|
+
import { actionGeneric, httpRouter, internalMutationGeneric } from "convex/server";
|
|
28
30
|
import { serialize } from "cookie";
|
|
29
31
|
|
|
30
32
|
//#region src/server/runtime.ts
|
|
31
33
|
const GROUP_CONNECTION_OIDC_CLIENT_SECRET_KIND = "oidc_client_secret";
|
|
34
|
+
const vHeaderPairs = v.array(v.array(v.string()));
|
|
35
|
+
function formDataEntries(formData) {
|
|
36
|
+
return formData;
|
|
37
|
+
}
|
|
38
|
+
const vHttpDelegateResponse = v.object({
|
|
39
|
+
status: v.number(),
|
|
40
|
+
statusText: v.string(),
|
|
41
|
+
headers: vHeaderPairs,
|
|
42
|
+
body: v.string()
|
|
43
|
+
});
|
|
44
|
+
function normalizeRoutePrefix(prefix) {
|
|
45
|
+
if (prefix === void 0 || prefix === "" || prefix === "/") return "";
|
|
46
|
+
return (prefix.startsWith("/") ? prefix : `/${prefix}`).replace(/\/$/, "");
|
|
47
|
+
}
|
|
48
|
+
function appendRoutePrefix(siteUrl, prefix) {
|
|
49
|
+
return `${siteUrl.replace(/\/$/, "")}${prefix}`;
|
|
50
|
+
}
|
|
51
|
+
function authSiteUrlFromEnv() {
|
|
52
|
+
return appendRoutePrefix(readConfigSync(envOptionalString("CONVEX_AUTH_SITE_URL")) ?? requireEnv("CONVEX_SITE_URL"), normalizeRoutePrefix(readConfigSync(envOptionalString("CONVEX_AUTH_HTTP_PREFIX")) ?? "/auth"));
|
|
53
|
+
}
|
|
54
|
+
function createInMemoryHttpRouter() {
|
|
55
|
+
const routes = [];
|
|
56
|
+
return {
|
|
57
|
+
route: (spec) => {
|
|
58
|
+
routes.push(spec);
|
|
59
|
+
},
|
|
60
|
+
lookup(pathname, method) {
|
|
61
|
+
const exact = routes.find((route) => route.method === method && route.path === pathname);
|
|
62
|
+
if (exact) return exact;
|
|
63
|
+
return routes.filter((route) => route.method === method && route.pathPrefix !== void 0 && pathname.startsWith(route.pathPrefix)).sort((a, b) => (b.pathPrefix?.length ?? 0) - (a.pathPrefix?.length ?? 0))[0] ?? null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async function invokeHttpHandler(handler, ctx, request) {
|
|
68
|
+
const maybeHandler = handler;
|
|
69
|
+
if (typeof maybeHandler._handler === "function") return await maybeHandler._handler(ctx, request);
|
|
70
|
+
throw new Error("Invalid HTTP action handler.");
|
|
71
|
+
}
|
|
72
|
+
async function serializeHttpResponse(response) {
|
|
73
|
+
const headers = [];
|
|
74
|
+
response.headers.forEach((value, name) => headers.push([name, value]));
|
|
75
|
+
const setCookies = response.headers.getSetCookie?.();
|
|
76
|
+
if (setCookies !== void 0) for (const cookie of setCookies) headers.push(["set-cookie", cookie]);
|
|
77
|
+
return {
|
|
78
|
+
status: response.status,
|
|
79
|
+
statusText: response.statusText,
|
|
80
|
+
headers,
|
|
81
|
+
body: await response.text()
|
|
82
|
+
};
|
|
83
|
+
}
|
|
32
84
|
const convexError = (data) => new ConvexError(data);
|
|
33
85
|
/**
|
|
34
86
|
* Configure the Convex Auth library. Returns an object with
|
|
@@ -42,7 +94,10 @@ const convexError = (data) => new ConvexError(data);
|
|
|
42
94
|
* export const auth = createAuth(components.auth, {
|
|
43
95
|
* providers: [],
|
|
44
96
|
* });
|
|
45
|
-
* export const
|
|
97
|
+
* export const signIn = auth.signIn;
|
|
98
|
+
* export const signOut = auth.signOut;
|
|
99
|
+
* export const store = auth.store;
|
|
100
|
+
* export const http = auth.http;
|
|
46
101
|
* ```
|
|
47
102
|
*
|
|
48
103
|
* @returns An object with fields you should reexport from your
|
|
@@ -57,11 +112,12 @@ function Auth(config_) {
|
|
|
57
112
|
const getProviderOrThrow = services.providerRegistry.getProviderOrThrow;
|
|
58
113
|
const INVITE_TOKEN_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
59
114
|
const INVITE_TOKEN_LENGTH = 48;
|
|
60
|
-
const GROUP_CONNECTION_ROUTE_BASE = "/
|
|
115
|
+
const GROUP_CONNECTION_ROUTE_BASE = "/connections";
|
|
61
116
|
const group = createGroupService({
|
|
62
117
|
config,
|
|
63
118
|
sha256
|
|
64
119
|
});
|
|
120
|
+
const authRequireEnv = (name) => name === "CONVEX_SITE_URL" ? authSiteUrlFromEnv() : requireEnv(name);
|
|
65
121
|
const authBase = {
|
|
66
122
|
...createCoreDomains({
|
|
67
123
|
config,
|
|
@@ -71,7 +127,26 @@ function Auth(config_) {
|
|
|
71
127
|
callModifyAccount,
|
|
72
128
|
getEnrichCtx: () => enrichCtx,
|
|
73
129
|
inviteTokenAlphabet: INVITE_TOKEN_ALPHABET,
|
|
74
|
-
inviteTokenLength: INVITE_TOKEN_LENGTH
|
|
130
|
+
inviteTokenLength: INVITE_TOKEN_LENGTH,
|
|
131
|
+
signInForProvider: async (ctx, providerConfig, args) => {
|
|
132
|
+
const materialized = typeof providerConfig === "function" ? providerConfig() : providerConfig;
|
|
133
|
+
const result = await services.signIn.signIn(enrichCtx(ctx), materialized, args, {
|
|
134
|
+
generateTokens: false,
|
|
135
|
+
allowExtraProviders: true
|
|
136
|
+
});
|
|
137
|
+
if (result.kind === "signedIn") {
|
|
138
|
+
const session = result.session;
|
|
139
|
+
if (session === null || session === void 0) return null;
|
|
140
|
+
const userId = session.userId;
|
|
141
|
+
const sessionId = session.sessionId ?? session._id;
|
|
142
|
+
if (userId === void 0 || sessionId === void 0) return null;
|
|
143
|
+
return {
|
|
144
|
+
userId,
|
|
145
|
+
sessionId
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
throw new FlowSignal(result);
|
|
149
|
+
}
|
|
75
150
|
}),
|
|
76
151
|
sso: createGroupConnectionDomain({
|
|
77
152
|
config,
|
|
@@ -82,7 +157,7 @@ function Auth(config_) {
|
|
|
82
157
|
emitGroupWebhookDeliveries: group.emitGroupWebhookDeliveries,
|
|
83
158
|
connectionNotFoundError: "Connection not found.",
|
|
84
159
|
GROUP_CONNECTION_OIDC_CLIENT_SECRET_KIND,
|
|
85
|
-
requireEnv,
|
|
160
|
+
requireEnv: authRequireEnv,
|
|
86
161
|
generateRandomString,
|
|
87
162
|
INVITE_TOKEN_ALPHABET,
|
|
88
163
|
sha256,
|
|
@@ -91,19 +166,144 @@ function Auth(config_) {
|
|
|
91
166
|
})
|
|
92
167
|
};
|
|
93
168
|
const getDefaultCorsOrigins = () => siteUrlsFromEnv().allowedUrls.map((u) => new URL(u).origin);
|
|
94
|
-
const
|
|
95
|
-
|
|
169
|
+
const getAuthSiteUrl = (_ctx) => authSiteUrlFromEnv();
|
|
170
|
+
const createProtocolRouter = (authSiteUrl) => {
|
|
171
|
+
const protocolHttp = createInMemoryHttpRouter();
|
|
172
|
+
const protocolRequireEnv = (name) => name === "CONVEX_SITE_URL" ? authSiteUrl : requireEnv(name);
|
|
173
|
+
addOpenIdRoutes(protocolHttp, {
|
|
174
|
+
getIssuer: () => authSiteUrl,
|
|
175
|
+
getJwks: () => requireEnv("JWKS")
|
|
176
|
+
});
|
|
177
|
+
addWebAuthnRoute(protocolHttp, { getResponse: () => {
|
|
178
|
+
const r = generateWebAuthnConfig();
|
|
179
|
+
return r === null ? null : {
|
|
180
|
+
body: r.body,
|
|
181
|
+
headers: r.headers
|
|
182
|
+
};
|
|
183
|
+
} });
|
|
184
|
+
addGroupHttpRuntime({
|
|
185
|
+
http: protocolHttp,
|
|
186
|
+
hasSSO,
|
|
187
|
+
auth: authBase,
|
|
188
|
+
config,
|
|
189
|
+
routeBase: "/connections",
|
|
190
|
+
requireEnv: protocolRequireEnv,
|
|
191
|
+
loadActiveConnectionSamlOrThrow: group.loadActiveConnectionSamlOrThrow,
|
|
192
|
+
loadConnectionOidcOrThrow: group.loadConnectionOidcOrThrow,
|
|
193
|
+
getGroupConnectionScimContext: group.getGroupConnectionScimContext,
|
|
194
|
+
loadGroupPolicyOrThrow: group.loadGroupPolicyOrThrow,
|
|
195
|
+
normalizeGroupConnectionPolicy,
|
|
196
|
+
recordGroupAuditEvent: group.recordGroupAuditEvent,
|
|
197
|
+
emitGroupWebhookDeliveries: group.emitGroupWebhookDeliveries,
|
|
198
|
+
generateRandomString,
|
|
199
|
+
inviteTokenAlphabet: INVITE_TOKEN_ALPHABET,
|
|
200
|
+
callUserOAuth,
|
|
201
|
+
callVerifierSignature,
|
|
202
|
+
sharedOidcRedirectURI: ssoProvider?.redirectURI
|
|
203
|
+
});
|
|
204
|
+
if (hasOAuth) addAuthRoutes(protocolHttp, {
|
|
205
|
+
routeBase: "",
|
|
206
|
+
handleSignIn: convertErrorsToResponse(400, async (ctx, request$1) => {
|
|
207
|
+
const url = new URL(request$1.url);
|
|
208
|
+
const pathParts = url.pathname.split("/");
|
|
209
|
+
const providerId = pathParts[pathParts.length - 1];
|
|
210
|
+
if (providerId === null) throw convexError({
|
|
211
|
+
code: "OAUTH_MISSING_PROVIDER",
|
|
212
|
+
message: "Missing OAuth provider ID."
|
|
213
|
+
});
|
|
214
|
+
const verifier = url.searchParams.get("code");
|
|
215
|
+
if (verifier === null) throw convexError({
|
|
216
|
+
code: "OAUTH_MISSING_VERIFIER",
|
|
217
|
+
message: "Missing sign-in verifier."
|
|
218
|
+
});
|
|
219
|
+
const oauthConfig = getProviderOrThrow(providerId);
|
|
220
|
+
const redirectTo = url.searchParams.get("redirectTo");
|
|
221
|
+
const { redirect, cookies, signature } = await createOAuthAuthorizationURL(providerId, oauthConfig, { stateTransform: (state) => encodeOAuthState(state, redirectTo) });
|
|
222
|
+
await callVerifierSignature(ctx, {
|
|
223
|
+
verifier,
|
|
224
|
+
signature
|
|
225
|
+
});
|
|
226
|
+
const headers = new Headers({ Location: redirect });
|
|
227
|
+
for (const { name, value, options } of cookies) headers.append("Set-Cookie", serialize(name, value, options));
|
|
228
|
+
return new Response(null, {
|
|
229
|
+
status: 302,
|
|
230
|
+
headers
|
|
231
|
+
});
|
|
232
|
+
}),
|
|
233
|
+
handleCallback: async (ctx, request$1) => {
|
|
234
|
+
const url = new URL(request$1.url);
|
|
235
|
+
const callbackPathParts = new URL(request$1.url).pathname.split("/");
|
|
236
|
+
const providerId = callbackPathParts[callbackPathParts.length - 1];
|
|
237
|
+
if (!providerId) throw convexError({
|
|
238
|
+
code: "OAUTH_MISSING_PROVIDER",
|
|
239
|
+
message: "Missing OAuth provider ID."
|
|
240
|
+
});
|
|
241
|
+
log(LOG_LEVELS.DEBUG, "Handling OAuth callback for provider:", providerId);
|
|
242
|
+
const provider = getProviderOrThrow(providerId);
|
|
243
|
+
const cookies = getCookies(request$1);
|
|
244
|
+
const params = url.searchParams;
|
|
245
|
+
if (request$1.headers.get("Content-Type")?.includes("application/x-www-form-urlencoded")) {
|
|
246
|
+
const formData = await request$1.formData();
|
|
247
|
+
for (const [key, value] of formDataEntries(formData)) if (typeof value === "string") params.append(key, value);
|
|
248
|
+
}
|
|
249
|
+
const fallbackDestinationUrl = await redirectAbsoluteUrl(ctx, config, { redirectTo: void 0 });
|
|
250
|
+
try {
|
|
251
|
+
const result = await handleOAuthCallback(providerId, provider, Object.fromEntries(params.entries()), cookies);
|
|
252
|
+
const oauthCookies = result.cookies;
|
|
253
|
+
const { id: profileId, ...profileData } = result.profile;
|
|
254
|
+
const { signature } = result;
|
|
255
|
+
const { redirectTo: stateRedirectTo } = decodeOAuthState(params.get("state") ?? "");
|
|
256
|
+
const redirUrl = setURLSearchParam(await redirectAbsoluteUrl(ctx, config, { redirectTo: stateRedirectTo ?? void 0 }), "code", await callUserOAuth(ctx, {
|
|
257
|
+
provider: providerId,
|
|
258
|
+
providerAccountId: profileId,
|
|
259
|
+
profile: profileData,
|
|
260
|
+
signature
|
|
261
|
+
}));
|
|
262
|
+
const redirHeaders = new Headers({ Location: redirUrl });
|
|
263
|
+
redirHeaders.set("Cache-Control", "must-revalidate");
|
|
264
|
+
for (const { name, value, options } of oauthCookies) redirHeaders.append("Set-Cookie", serialize(name, value, options));
|
|
265
|
+
return new Response(null, {
|
|
266
|
+
status: 302,
|
|
267
|
+
headers: redirHeaders
|
|
268
|
+
});
|
|
269
|
+
} catch (error) {
|
|
270
|
+
logError(error);
|
|
271
|
+
return new Response(null, {
|
|
272
|
+
status: 302,
|
|
273
|
+
headers: { Location: fallbackDestinationUrl }
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
return protocolHttp;
|
|
279
|
+
};
|
|
280
|
+
const request = {
|
|
281
|
+
add: (http$1, options) => {
|
|
282
|
+
const routePrefix = normalizeRoutePrefix(options?.prefix);
|
|
283
|
+
const authSiteUrl = () => appendRoutePrefix(requireEnv("CONVEX_SITE_URL"), routePrefix);
|
|
284
|
+
const protocolRequireEnv = (name) => name === "CONVEX_SITE_URL" ? authSiteUrl() : requireEnv(name);
|
|
96
285
|
addOpenIdRoutes(http$1, {
|
|
97
|
-
|
|
286
|
+
routeBase: routePrefix,
|
|
287
|
+
getIssuer: authSiteUrl,
|
|
98
288
|
getJwks: () => requireEnv("JWKS")
|
|
99
289
|
});
|
|
290
|
+
addWebAuthnRoute(http$1, {
|
|
291
|
+
routeBase: routePrefix,
|
|
292
|
+
getResponse: () => {
|
|
293
|
+
const r = generateWebAuthnConfig();
|
|
294
|
+
return r === null ? null : {
|
|
295
|
+
body: r.body,
|
|
296
|
+
headers: r.headers
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
});
|
|
100
300
|
addGroupHttpRuntime({
|
|
101
301
|
http: http$1,
|
|
102
302
|
hasSSO,
|
|
103
303
|
auth: authBase,
|
|
104
304
|
config,
|
|
105
|
-
routeBase: GROUP_CONNECTION_ROUTE_BASE
|
|
106
|
-
requireEnv,
|
|
305
|
+
routeBase: `${routePrefix}${GROUP_CONNECTION_ROUTE_BASE}`,
|
|
306
|
+
requireEnv: protocolRequireEnv,
|
|
107
307
|
loadActiveConnectionSamlOrThrow: group.loadActiveConnectionSamlOrThrow,
|
|
108
308
|
loadConnectionOidcOrThrow: group.loadConnectionOidcOrThrow,
|
|
109
309
|
getGroupConnectionScimContext: group.getGroupConnectionScimContext,
|
|
@@ -118,8 +318,9 @@ function Auth(config_) {
|
|
|
118
318
|
sharedOidcRedirectURI: ssoProvider?.redirectURI
|
|
119
319
|
});
|
|
120
320
|
if (hasOAuth) addAuthRoutes(http$1, {
|
|
121
|
-
|
|
122
|
-
|
|
321
|
+
routeBase: routePrefix,
|
|
322
|
+
handleSignIn: convertErrorsToResponse(400, async (ctx, request$1) => {
|
|
323
|
+
const url = new URL(request$1.url);
|
|
123
324
|
const pathParts = url.pathname.split("/");
|
|
124
325
|
const providerId = pathParts[pathParts.length - 1];
|
|
125
326
|
if (providerId === null) throw convexError({
|
|
@@ -131,23 +332,23 @@ function Auth(config_) {
|
|
|
131
332
|
code: "OAUTH_MISSING_VERIFIER",
|
|
132
333
|
message: "Missing sign-in verifier."
|
|
133
334
|
});
|
|
134
|
-
const
|
|
335
|
+
const oauthConfig = getProviderOrThrow(providerId);
|
|
336
|
+
const redirectTo = url.searchParams.get("redirectTo");
|
|
337
|
+
const { redirect, cookies, signature } = await createOAuthAuthorizationURL(providerId, oauthConfig, { stateTransform: (state) => encodeOAuthState(state, redirectTo) });
|
|
135
338
|
await callVerifierSignature(ctx, {
|
|
136
339
|
verifier,
|
|
137
340
|
signature
|
|
138
341
|
});
|
|
139
|
-
const redirectTo = url.searchParams.get("redirectTo");
|
|
140
|
-
if (redirectTo !== null) cookies.push(redirectToParamCookie(providerId, redirectTo));
|
|
141
342
|
const headers = new Headers({ Location: redirect });
|
|
142
|
-
for (const { name, value, options } of cookies) headers.append("Set-Cookie", serialize(name, value, options));
|
|
343
|
+
for (const { name, value, options: options$1 } of cookies) headers.append("Set-Cookie", serialize(name, value, options$1));
|
|
143
344
|
return new Response(null, {
|
|
144
345
|
status: 302,
|
|
145
346
|
headers
|
|
146
347
|
});
|
|
147
348
|
}),
|
|
148
|
-
handleCallback: async (ctx, request) => {
|
|
149
|
-
const url = new URL(request.url);
|
|
150
|
-
const callbackPathParts = new URL(request.url).pathname.split("/");
|
|
349
|
+
handleCallback: async (ctx, request$1) => {
|
|
350
|
+
const url = new URL(request$1.url);
|
|
351
|
+
const callbackPathParts = new URL(request$1.url).pathname.split("/");
|
|
151
352
|
const providerId = callbackPathParts[callbackPathParts.length - 1];
|
|
152
353
|
if (!providerId) throw convexError({
|
|
153
354
|
code: "OAUTH_MISSING_PROVIDER",
|
|
@@ -155,19 +356,20 @@ function Auth(config_) {
|
|
|
155
356
|
});
|
|
156
357
|
log(LOG_LEVELS.DEBUG, "Handling OAuth callback for provider:", providerId);
|
|
157
358
|
const provider = getProviderOrThrow(providerId);
|
|
158
|
-
const cookies = getCookies(request);
|
|
159
|
-
const maybeRedirectTo = useRedirectToParam(provider.id, cookies);
|
|
160
|
-
const destinationUrl = await redirectAbsoluteUrl(config, { redirectTo: maybeRedirectTo?.redirectTo });
|
|
359
|
+
const cookies = getCookies(request$1);
|
|
161
360
|
const params = url.searchParams;
|
|
162
|
-
if (request.headers.get("Content-Type")?.includes("application/x-www-form-urlencoded"))
|
|
163
|
-
|
|
164
|
-
|
|
361
|
+
if (request$1.headers.get("Content-Type")?.includes("application/x-www-form-urlencoded")) {
|
|
362
|
+
const formData = await request$1.formData();
|
|
363
|
+
for (const [key, value] of formDataEntries(formData)) if (typeof value === "string") params.append(key, value);
|
|
364
|
+
}
|
|
365
|
+
const fallbackDestinationUrl = await redirectAbsoluteUrl(ctx, config, { redirectTo: void 0 });
|
|
165
366
|
try {
|
|
166
367
|
const result = await handleOAuthCallback(providerId, provider, Object.fromEntries(params.entries()), cookies);
|
|
167
368
|
const oauthCookies = result.cookies;
|
|
168
369
|
const { id: profileId, ...profileData } = result.profile;
|
|
169
370
|
const { signature } = result;
|
|
170
|
-
const
|
|
371
|
+
const { redirectTo: stateRedirectTo } = decodeOAuthState(params.get("state") ?? "");
|
|
372
|
+
const redirUrl = setURLSearchParam(await redirectAbsoluteUrl(ctx, config, { redirectTo: stateRedirectTo ?? void 0 }), "code", await callUserOAuth(ctx, {
|
|
171
373
|
provider: providerId,
|
|
172
374
|
providerAccountId: profileId,
|
|
173
375
|
profile: profileData,
|
|
@@ -175,34 +377,126 @@ function Auth(config_) {
|
|
|
175
377
|
}));
|
|
176
378
|
const redirHeaders = new Headers({ Location: redirUrl });
|
|
177
379
|
redirHeaders.set("Cache-Control", "must-revalidate");
|
|
178
|
-
for (const { name, value, options } of
|
|
380
|
+
for (const { name, value, options: options$1 } of oauthCookies) redirHeaders.append("Set-Cookie", serialize(name, value, options$1));
|
|
179
381
|
return new Response(null, {
|
|
180
382
|
status: 302,
|
|
181
383
|
headers: redirHeaders
|
|
182
384
|
});
|
|
183
385
|
} catch (error) {
|
|
184
386
|
logError(error);
|
|
185
|
-
const respHeaders = new Headers({ Location: destinationUrl });
|
|
186
|
-
for (const { name, value, options } of maybeRedirectTo !== null ? [maybeRedirectTo.updatedCookie] : []) respHeaders.append("Set-Cookie", serialize(name, value, options));
|
|
187
387
|
return new Response(null, {
|
|
188
388
|
status: 302,
|
|
189
|
-
headers:
|
|
389
|
+
headers: { Location: fallbackDestinationUrl }
|
|
190
390
|
});
|
|
191
391
|
}
|
|
192
392
|
}
|
|
193
393
|
});
|
|
194
394
|
},
|
|
395
|
+
router: (options) => {
|
|
396
|
+
const http$1 = httpRouter();
|
|
397
|
+
request.add(http$1, options ?? { prefix: "/auth" });
|
|
398
|
+
return http$1;
|
|
399
|
+
},
|
|
195
400
|
context: createHttpContext(authBase),
|
|
196
401
|
action: createHttpAction(authBase, getDefaultCorsOrigins),
|
|
197
402
|
route: createHttpRoute(createHttpAction(authBase, getDefaultCorsOrigins), getDefaultCorsOrigins)
|
|
198
403
|
};
|
|
199
|
-
const auth = Object.assign(authBase, {
|
|
404
|
+
const auth = Object.assign(authBase, { request });
|
|
405
|
+
const httpDelegate = actionGeneric({
|
|
406
|
+
args: {
|
|
407
|
+
authSiteUrl: v.string(),
|
|
408
|
+
routePath: v.string(),
|
|
409
|
+
url: v.string(),
|
|
410
|
+
method: v.string(),
|
|
411
|
+
headers: vHeaderPairs,
|
|
412
|
+
body: v.optional(v.string())
|
|
413
|
+
},
|
|
414
|
+
returns: vHttpDelegateResponse,
|
|
415
|
+
handler: async (ctx, args) => {
|
|
416
|
+
const incomingUrl = new URL(args.url);
|
|
417
|
+
const requestUrl = new URL(args.authSiteUrl.replace(/\/$/, "") + args.routePath);
|
|
418
|
+
requestUrl.search = incomingUrl.search;
|
|
419
|
+
const method = args.method.toUpperCase();
|
|
420
|
+
const request$1 = new Request(requestUrl, {
|
|
421
|
+
method,
|
|
422
|
+
headers: new Headers(args.headers),
|
|
423
|
+
body: method === "GET" || method === "HEAD" ? void 0 : args.body ?? ""
|
|
424
|
+
});
|
|
425
|
+
const route = createProtocolRouter(args.authSiteUrl.replace(/\/$/, "")).lookup(args.routePath, method);
|
|
426
|
+
if (!route) return await serializeHttpResponse(new Response(null, { status: 404 }));
|
|
427
|
+
return await serializeHttpResponse(await invokeHttpHandler(route.handler, ctx, request$1));
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
const http = Object.assign((options) => request.router(options), httpDelegate);
|
|
431
|
+
const accountUnlink = async (ctx, args) => {
|
|
432
|
+
const accountDoc = await ctx.runQuery(config.component.public.accountGetById, { accountId: args.accountId });
|
|
433
|
+
if (accountDoc === null) throw convexError({
|
|
434
|
+
code: "ACCOUNT_NOT_FOUND",
|
|
435
|
+
message: "Account not found."
|
|
436
|
+
});
|
|
437
|
+
await ctx.runMutation(config.component.public.accountDelete, { accountId: args.accountId });
|
|
438
|
+
const userId = accountDoc.userId;
|
|
439
|
+
const provider = accountDoc.provider;
|
|
440
|
+
await config.callbacks?.after?.(ctx, {
|
|
441
|
+
kind: "accountUnlinked",
|
|
442
|
+
userId,
|
|
443
|
+
accountId: args.accountId,
|
|
444
|
+
provider
|
|
445
|
+
});
|
|
446
|
+
return {
|
|
447
|
+
accountId: args.accountId,
|
|
448
|
+
userId,
|
|
449
|
+
provider
|
|
450
|
+
};
|
|
451
|
+
};
|
|
452
|
+
const passkeyDelete = async (ctx, args) => {
|
|
453
|
+
const passkeyDoc = await ctx.runQuery(config.component.public.passkeyGetById, { passkeyId: args.passkeyId });
|
|
454
|
+
if (passkeyDoc === null) throw convexError({
|
|
455
|
+
code: "PASSKEY_NOT_FOUND",
|
|
456
|
+
message: "Passkey not found."
|
|
457
|
+
});
|
|
458
|
+
await ctx.runMutation(config.component.public.passkeyDelete, { passkeyId: args.passkeyId });
|
|
459
|
+
const userId = passkeyDoc.userId;
|
|
460
|
+
await config.callbacks?.after?.(ctx, {
|
|
461
|
+
kind: "passkeyRemoved",
|
|
462
|
+
userId,
|
|
463
|
+
passkeyId: args.passkeyId
|
|
464
|
+
});
|
|
465
|
+
return {
|
|
466
|
+
passkeyId: args.passkeyId,
|
|
467
|
+
userId
|
|
468
|
+
};
|
|
469
|
+
};
|
|
470
|
+
const totpDelete = async (ctx, args) => {
|
|
471
|
+
const totpDoc = await ctx.runQuery(config.component.public.totpGetById, { totpId: args.totpId });
|
|
472
|
+
if (totpDoc === null) throw convexError({
|
|
473
|
+
code: "TOTP_NOT_FOUND",
|
|
474
|
+
message: "TOTP factor not found."
|
|
475
|
+
});
|
|
476
|
+
await ctx.runMutation(config.component.public.totpDelete, { totpId: args.totpId });
|
|
477
|
+
const userId = totpDoc.userId;
|
|
478
|
+
await config.callbacks?.after?.(ctx, {
|
|
479
|
+
kind: "totpRemoved",
|
|
480
|
+
userId,
|
|
481
|
+
totpId: args.totpId
|
|
482
|
+
});
|
|
483
|
+
return {
|
|
484
|
+
totpId: args.totpId,
|
|
485
|
+
userId
|
|
486
|
+
};
|
|
487
|
+
};
|
|
488
|
+
const enrichedAccount = Object.assign({}, auth.account, { unlink: accountUnlink });
|
|
489
|
+
const passkeyHelpers = { delete: passkeyDelete };
|
|
490
|
+
const totpHelpers = { delete: totpDelete };
|
|
200
491
|
const enrichCtx = (ctx) => ({
|
|
201
492
|
...ctx,
|
|
202
493
|
auth: {
|
|
203
494
|
...ctx.auth,
|
|
495
|
+
getUserIdentity: ctx.auth.getUserIdentity.bind(ctx.auth),
|
|
204
496
|
config,
|
|
205
|
-
account:
|
|
497
|
+
account: enrichedAccount,
|
|
498
|
+
passkey: passkeyHelpers,
|
|
499
|
+
totp: totpHelpers,
|
|
206
500
|
session: auth.session,
|
|
207
501
|
member: auth.member,
|
|
208
502
|
provider: auth.provider
|
|
@@ -221,9 +515,11 @@ function Auth(config_) {
|
|
|
221
515
|
handler: async (ctx, args) => {
|
|
222
516
|
if (args.calledBy !== void 0) log("INFO", `\`auth:signIn\` called by ${args.calledBy}`);
|
|
223
517
|
const provider = args.provider !== void 0 ? getProviderOrThrow(args.provider) : null;
|
|
518
|
+
const authSiteUrl = provider?.type === "oauth" || provider?.type === "sso" ? getAuthSiteUrl(ctx) : void 0;
|
|
224
519
|
const result = await services.signIn.signIn(enrichCtx(ctx), provider, args, {
|
|
225
520
|
generateTokens: true,
|
|
226
521
|
allowExtraProviders: false,
|
|
522
|
+
authSiteUrl,
|
|
227
523
|
resolveSsoProtocol: group.resolveGroupConnectionSsoProtocolOrThrow
|
|
228
524
|
});
|
|
229
525
|
const handler = {
|
|
@@ -253,7 +549,8 @@ function Auth(config_) {
|
|
|
253
549
|
handler: async (ctx, args) => {
|
|
254
550
|
return storeImpl(ctx, args, services);
|
|
255
551
|
}
|
|
256
|
-
})
|
|
552
|
+
}),
|
|
553
|
+
http
|
|
257
554
|
};
|
|
258
555
|
}
|
|
259
556
|
|
|
@@ -182,6 +182,10 @@ function createGroupService(deps) {
|
|
|
182
182
|
code: "INVALID_PARAMETERS",
|
|
183
183
|
message: connectionNotFoundError
|
|
184
184
|
});
|
|
185
|
+
if (connection.status !== "active") throw convexError({
|
|
186
|
+
code: "INVALID_PARAMETERS",
|
|
187
|
+
message: "Connection is not active."
|
|
188
|
+
});
|
|
185
189
|
return {
|
|
186
190
|
scimConfig,
|
|
187
191
|
connection,
|
package/dist/server/sessions.js
CHANGED
|
@@ -9,6 +9,7 @@ import { generateToken } from "./tokens.js";
|
|
|
9
9
|
|
|
10
10
|
//#region src/server/sessions.ts
|
|
11
11
|
const DEFAULT_SESSION_TOTAL_DURATION_MS = 1e3 * 60 * 60 * 24 * 30;
|
|
12
|
+
/** @internal */
|
|
12
13
|
const sessionExpirationTime = (config, now = Date.now()) => now + (config.session?.totalDurationMs ?? readConfigSync(envOptionalNumber("AUTH_SESSION_TOTAL_DURATION_MS")) ?? DEFAULT_SESSION_TOTAL_DURATION_MS);
|
|
13
14
|
const encodeRefreshToken = (refreshTokenId, sessionId) => `${refreshTokenId}${REFRESH_TOKEN_DIVIDER}${sessionId}`;
|
|
14
15
|
function buildSessionIdentity(userId, sessionId, user) {
|
package/dist/server/signin.js
CHANGED
|
@@ -8,9 +8,9 @@ import { callRefreshSession } from "./mutations/refresh.js";
|
|
|
8
8
|
import { callSignIn } from "./mutations/signin.js";
|
|
9
9
|
import { callVerifier } from "./mutations/verifier.js";
|
|
10
10
|
import { callVerifyCodeAndSignIn } from "./mutations/verify.js";
|
|
11
|
-
import { redirectAbsoluteUrl, setURLSearchParam } from "./redirects.js";
|
|
12
11
|
import { authFlowError } from "../shared/errors.js";
|
|
13
|
-
import { toConvexError } from "./errors.js";
|
|
12
|
+
import { FlowSignal, toConvexError } from "./errors.js";
|
|
13
|
+
import { redirectAbsoluteUrl, setURLSearchParam } from "./redirects.js";
|
|
14
14
|
import { queryTotpVerifiedByUserId } from "./types.js";
|
|
15
15
|
import { handleDevice } from "./device.js";
|
|
16
16
|
import { handlePasskeyFx } from "./passkey.js";
|
|
@@ -146,7 +146,7 @@ async function handleEmailAndPhoneProviderFx(ctx, provider, args, options) {
|
|
|
146
146
|
}
|
|
147
147
|
let destination;
|
|
148
148
|
try {
|
|
149
|
-
destination = await redirectAbsoluteUrl(ctx.auth.config, args.params ?? {});
|
|
149
|
+
destination = await redirectAbsoluteUrl(ctx, ctx.auth.config, args.params ?? {});
|
|
150
150
|
} catch (error) {
|
|
151
151
|
throw asConvexError(error, "INVALID_REDIRECT", "Failed to resolve redirect URL.");
|
|
152
152
|
}
|
|
@@ -182,6 +182,7 @@ async function handleCredentialsFx(ctx, provider, args, options) {
|
|
|
182
182
|
try {
|
|
183
183
|
result = await withSpan("convex-auth.signin.credentials.authorize", { providerId: provider.id }, () => provider.authorize(args.params ?? {}, ctx));
|
|
184
184
|
} catch (error) {
|
|
185
|
+
if (error instanceof FlowSignal) return error.result;
|
|
185
186
|
throw asCredentialsError(error);
|
|
186
187
|
}
|
|
187
188
|
if (result === null) return {
|
|
@@ -272,7 +273,8 @@ async function handleOAuthProviderFx(ctx, provider, args, options) {
|
|
|
272
273
|
session: result
|
|
273
274
|
};
|
|
274
275
|
}
|
|
275
|
-
const
|
|
276
|
+
const authSiteUrl = options.authSiteUrl ?? readConfigSync(envOptionalString("CUSTOM_AUTH_SITE_URL")) ?? requireEnv("CONVEX_SITE_URL");
|
|
277
|
+
const redirect = new URL(`${authSiteUrl.replace(/\/$/, "")}/signin/${provider.id}`);
|
|
276
278
|
let verifier;
|
|
277
279
|
try {
|
|
278
280
|
verifier = await callVerifier(ctx);
|
|
@@ -315,8 +317,8 @@ async function handleSsoProviderFx(ctx, args, options) {
|
|
|
315
317
|
} catch (error) {
|
|
316
318
|
throw asConvexError(error, "INTERNAL_ERROR", "Failed to create verifier.");
|
|
317
319
|
}
|
|
318
|
-
const siteUrl = readConfigSync(envOptionalString("CUSTOM_AUTH_SITE_URL")) ?? requireEnv("CONVEX_SITE_URL");
|
|
319
|
-
const redirect = new URL(`${siteUrl}/
|
|
320
|
+
const siteUrl = options.authSiteUrl ?? readConfigSync(envOptionalString("CUSTOM_AUTH_SITE_URL")) ?? requireEnv("CONVEX_SITE_URL");
|
|
321
|
+
const redirect = new URL(`${siteUrl.replace(/\/$/, "")}/connections/${connectionId}/${protocol}/signin`);
|
|
320
322
|
redirect.searchParams.set("code", verifier);
|
|
321
323
|
if (typeof args.params?.redirectTo === "string") redirect.searchParams.set("redirectTo", args.params.redirectTo);
|
|
322
324
|
if (typeof normalizedParams.loginHint === "string") redirect.searchParams.set("loginHint", normalizedParams.loginHint);
|