@robelest/convex-auth 0.0.4-preview.13 → 0.0.4-preview.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -9
- package/dist/bin.cjs +5957 -5478
- package/dist/client/index.d.ts +3 -7
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +27 -26
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +14 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +1672 -24
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/index.d.ts +1 -1
- package/dist/component/index.js +2 -2
- package/dist/component/model.d.ts +153 -0
- package/dist/component/model.d.ts.map +1 -0
- package/dist/component/model.js +343 -0
- package/dist/component/model.js.map +1 -0
- package/dist/component/providers/sso.d.ts +1 -1
- package/dist/component/public/enterprise.d.ts +54 -0
- package/dist/component/public/enterprise.d.ts.map +1 -0
- package/dist/component/public/enterprise.js +515 -0
- package/dist/component/public/enterprise.js.map +1 -0
- package/dist/component/public/factors.d.ts +52 -0
- package/dist/component/public/factors.d.ts.map +1 -0
- package/dist/component/public/factors.js +285 -0
- package/dist/component/public/factors.js.map +1 -0
- package/dist/component/public/groups.d.ts +116 -0
- package/dist/component/public/groups.d.ts.map +1 -0
- package/dist/component/public/groups.js +596 -0
- package/dist/component/public/groups.js.map +1 -0
- package/dist/component/public/identity.d.ts +93 -0
- package/dist/component/public/identity.d.ts.map +1 -0
- package/dist/component/public/identity.js +426 -0
- package/dist/component/public/identity.js.map +1 -0
- package/dist/component/public/keys.d.ts +41 -0
- package/dist/component/public/keys.d.ts.map +1 -0
- package/dist/component/public/keys.js +157 -0
- package/dist/component/public/keys.js.map +1 -0
- package/dist/component/public/shared.d.ts +26 -0
- package/dist/component/public/shared.d.ts.map +1 -0
- package/dist/component/public/shared.js +32 -0
- package/dist/component/public/shared.js.map +1 -0
- package/dist/component/public.d.ts +9 -321
- package/dist/component/public.d.ts.map +1 -1
- package/dist/component/public.js +6 -2145
- package/dist/component/schema.d.ts +406 -260
- package/dist/component/schema.js +37 -32
- package/dist/component/schema.js.map +1 -1
- package/dist/component/server/auth.d.ts +161 -15
- package/dist/component/server/auth.d.ts.map +1 -1
- package/dist/component/server/auth.js +100 -7
- package/dist/component/server/auth.js.map +1 -1
- package/dist/component/server/cookies.js +3 -0
- package/dist/component/server/cookies.js.map +1 -1
- package/dist/component/server/db.js +1 -0
- package/dist/component/server/db.js.map +1 -1
- package/dist/component/server/device.js +3 -1
- package/dist/component/server/device.js.map +1 -1
- package/dist/component/server/domains/core.js +629 -0
- package/dist/component/server/domains/core.js.map +1 -0
- package/dist/component/server/domains/sso.js +884 -0
- package/dist/component/server/domains/sso.js.map +1 -0
- package/dist/component/server/factory.d.ts +136 -0
- package/dist/component/server/factory.d.ts.map +1 -0
- package/dist/component/server/factory.js +1134 -0
- package/dist/component/server/factory.js.map +1 -0
- package/dist/component/server/fx.js +2 -1
- package/dist/component/server/fx.js.map +1 -1
- package/dist/component/server/http.js +287 -0
- package/dist/component/server/http.js.map +1 -0
- package/dist/component/server/identity.js +13 -0
- package/dist/component/server/identity.js.map +1 -0
- package/dist/component/server/keys.js +4 -0
- package/dist/component/server/keys.js.map +1 -1
- package/dist/component/server/mutations/account.js +1 -1
- package/dist/component/server/mutations/index.js +2 -2
- package/dist/component/server/mutations/index.js.map +1 -1
- package/dist/component/server/mutations/invalidate.js +1 -1
- package/dist/component/server/mutations/oauth.js +10 -7
- package/dist/component/server/mutations/oauth.js.map +1 -1
- package/dist/component/server/mutations/refresh.js +1 -1
- package/dist/component/server/mutations/register.js +1 -1
- package/dist/component/server/mutations/retrieve.js +1 -1
- package/dist/component/server/mutations/signature.js +1 -1
- package/dist/component/server/mutations/store.js +6 -3
- package/dist/component/server/mutations/store.js.map +1 -1
- package/dist/component/server/mutations/verify.js +1 -1
- package/dist/component/server/oauth.js +3 -0
- package/dist/component/server/oauth.js.map +1 -1
- package/dist/component/server/passkey.js +3 -2
- package/dist/component/server/passkey.js.map +1 -1
- package/dist/component/server/provider.js +2 -0
- package/dist/component/server/provider.js.map +1 -1
- package/dist/component/server/providers.js +10 -0
- package/dist/component/server/providers.js.map +1 -1
- package/dist/component/server/ratelimit.js +3 -0
- package/dist/component/server/ratelimit.js.map +1 -1
- package/dist/component/server/redirects.js +2 -0
- package/dist/component/server/redirects.js.map +1 -1
- package/dist/component/server/refresh.js +5 -0
- package/dist/component/server/refresh.js.map +1 -1
- package/dist/component/server/sessions.js +5 -0
- package/dist/component/server/sessions.js.map +1 -1
- package/dist/component/server/signin.js +2 -1
- package/dist/component/server/signin.js.map +1 -1
- package/dist/component/server/sso.js +166 -19
- package/dist/component/server/sso.js.map +1 -1
- package/dist/component/server/tokens.js +1 -0
- package/dist/component/server/tokens.js.map +1 -1
- package/dist/component/server/totp.js +4 -2
- package/dist/component/server/totp.js.map +1 -1
- package/dist/component/server/types.d.ts +106 -38
- 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 +1 -0
- package/dist/component/server/users.js.map +1 -1
- package/dist/component/server/utils.js +44 -2
- package/dist/component/server/utils.js.map +1 -1
- package/dist/providers/anonymous.d.ts +1 -1
- package/dist/providers/credentials.d.ts +1 -1
- package/dist/providers/password.d.ts +1 -1
- package/dist/providers/sso.d.ts +1 -1
- package/dist/providers/sso.js.map +1 -1
- package/dist/server/auth.d.ts +163 -17
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +100 -7
- package/dist/server/auth.js.map +1 -1
- package/dist/server/cookies.d.ts +1 -38
- package/dist/server/cookies.js +3 -0
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/db.d.ts +1 -125
- package/dist/server/db.js +1 -0
- package/dist/server/db.js.map +1 -1
- package/dist/server/device.d.ts +1 -24
- package/dist/server/device.js +3 -1
- package/dist/server/device.js.map +1 -1
- package/dist/server/domains/core.d.ts +434 -0
- package/dist/server/domains/core.d.ts.map +1 -0
- package/dist/server/domains/core.js +629 -0
- package/dist/server/domains/core.js.map +1 -0
- package/dist/server/domains/sso.d.ts +409 -0
- package/dist/server/domains/sso.d.ts.map +1 -0
- package/dist/server/domains/sso.js +884 -0
- package/dist/server/domains/sso.js.map +1 -0
- package/dist/server/enterpriseValidators.d.ts +1 -0
- package/dist/server/enterpriseValidators.js +60 -0
- package/dist/server/enterpriseValidators.js.map +1 -0
- package/dist/server/factory.d.ts +136 -0
- package/dist/server/factory.d.ts.map +1 -0
- package/dist/server/factory.js +1134 -0
- package/dist/server/factory.js.map +1 -0
- package/dist/server/fx.d.ts +1 -16
- package/dist/server/fx.d.ts.map +1 -1
- package/dist/server/fx.js +1 -0
- package/dist/server/fx.js.map +1 -1
- package/dist/server/http.d.ts +59 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +287 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/identity.d.ts +1 -0
- package/dist/server/identity.js +13 -0
- package/dist/server/identity.js.map +1 -0
- package/dist/server/index.d.ts +468 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +530 -36
- package/dist/server/index.js.map +1 -1
- package/dist/server/keys.d.ts +1 -57
- package/dist/server/keys.js +4 -0
- package/dist/server/keys.js.map +1 -1
- package/dist/server/mutations/account.d.ts +7 -7
- package/dist/server/mutations/account.d.ts.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/index.d.ts +107 -107
- package/dist/server/mutations/index.d.ts.map +1 -1
- package/dist/server/mutations/index.js +1 -1
- package/dist/server/mutations/index.js.map +1 -1
- package/dist/server/mutations/invalidate.d.ts +5 -5
- package/dist/server/mutations/invalidate.d.ts.map +1 -1
- package/dist/server/mutations/oauth.d.ts +10 -10
- package/dist/server/mutations/oauth.d.ts.map +1 -1
- package/dist/server/mutations/oauth.js +9 -6
- package/dist/server/mutations/oauth.js.map +1 -1
- package/dist/server/mutations/refresh.d.ts +4 -4
- package/dist/server/mutations/register.d.ts +12 -12
- package/dist/server/mutations/register.d.ts.map +1 -1
- package/dist/server/mutations/retrieve.d.ts +7 -7
- package/dist/server/mutations/signature.d.ts +5 -5
- package/dist/server/mutations/signin.d.ts +6 -6
- package/dist/server/mutations/signin.d.ts.map +1 -1
- package/dist/server/mutations/signout.d.ts +1 -1
- package/dist/server/mutations/store.d.ts +3 -2
- package/dist/server/mutations/store.d.ts.map +1 -1
- package/dist/server/mutations/store.js +6 -3
- package/dist/server/mutations/store.js.map +1 -1
- package/dist/server/mutations/verifier.d.ts +1 -1
- package/dist/server/mutations/verify.d.ts +11 -11
- package/dist/server/mutations/verify.d.ts.map +1 -1
- package/dist/server/oauth.d.ts +1 -59
- package/dist/server/oauth.js +3 -0
- package/dist/server/oauth.js.map +1 -1
- package/dist/server/passkey.d.ts.map +1 -1
- package/dist/server/passkey.js +3 -2
- package/dist/server/passkey.js.map +1 -1
- package/dist/server/provider.d.ts +1 -14
- package/dist/server/provider.d.ts.map +1 -1
- package/dist/server/provider.js +2 -0
- package/dist/server/provider.js.map +1 -1
- package/dist/server/providers.js +10 -0
- package/dist/server/providers.js.map +1 -1
- package/dist/server/ratelimit.d.ts +1 -22
- package/dist/server/ratelimit.js +3 -0
- package/dist/server/ratelimit.js.map +1 -1
- package/dist/server/redirects.d.ts +1 -10
- package/dist/server/redirects.js +2 -0
- package/dist/server/redirects.js.map +1 -1
- package/dist/server/refresh.d.ts +1 -37
- package/dist/server/refresh.js +5 -0
- package/dist/server/refresh.js.map +1 -1
- package/dist/server/sessions.d.ts +1 -28
- package/dist/server/sessions.js +5 -0
- package/dist/server/sessions.js.map +1 -1
- package/dist/server/signin.d.ts +1 -55
- package/dist/server/signin.js +2 -1
- package/dist/server/signin.js.map +1 -1
- package/dist/server/sso.d.ts +1 -348
- package/dist/server/sso.js +165 -18
- package/dist/server/sso.js.map +1 -1
- package/dist/server/templates.d.ts +1 -21
- package/dist/server/templates.js +1 -0
- package/dist/server/templates.js.map +1 -1
- package/dist/server/tokens.d.ts +1 -11
- package/dist/server/tokens.js +1 -0
- package/dist/server/tokens.js.map +1 -1
- package/dist/server/totp.d.ts +1 -23
- package/dist/server/totp.js +4 -2
- package/dist/server/totp.js.map +1 -1
- package/dist/server/types.d.ts +114 -77
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/dist/server/users.d.ts +1 -31
- package/dist/server/users.js +1 -0
- package/dist/server/users.js.map +1 -1
- package/dist/server/utils.d.ts +1 -27
- package/dist/server/utils.js +44 -2
- package/dist/server/utils.js.map +1 -1
- package/dist/server/version.d.ts +1 -1
- package/dist/server/version.js +1 -1
- package/dist/server/version.js.map +1 -1
- package/package.json +4 -5
- package/src/cli/bin.ts +5 -0
- package/src/cli/index.ts +22 -9
- package/src/cli/keys.ts +3 -0
- package/src/client/index.ts +36 -37
- package/src/component/_generated/api.ts +14 -0
- package/src/component/_generated/component.ts +2106 -9
- package/src/component/index.ts +3 -1
- package/src/component/model.ts +441 -0
- package/src/component/public/enterprise.ts +753 -0
- package/src/component/public/factors.ts +332 -0
- package/src/component/public/groups.ts +932 -0
- package/src/component/public/identity.ts +566 -0
- package/src/component/public/keys.ts +209 -0
- package/src/component/public/shared.ts +119 -0
- package/src/component/public.ts +5 -2965
- package/src/component/schema.ts +68 -63
- package/src/providers/sso.ts +1 -1
- package/src/server/auth.ts +413 -18
- package/src/server/cookies.ts +3 -0
- package/src/server/db.ts +3 -0
- package/src/server/device.ts +3 -1
- package/src/server/domains/core.ts +1071 -0
- package/src/server/domains/sso.ts +1749 -0
- package/src/server/enterpriseValidators.ts +93 -0
- package/src/server/factory.ts +2181 -0
- package/src/server/fx.ts +1 -0
- package/src/server/http.ts +529 -0
- package/src/server/identity.ts +18 -0
- package/src/server/index.ts +806 -40
- package/src/server/keys.ts +4 -0
- package/src/server/mutations/index.ts +1 -1
- package/src/server/mutations/oauth.ts +36 -8
- package/src/server/mutations/store.ts +6 -3
- package/src/server/oauth.ts +6 -0
- package/src/server/passkey.ts +3 -2
- package/src/server/provider.ts +2 -0
- package/src/server/providers.ts +20 -0
- package/src/server/ratelimit.ts +3 -0
- package/src/server/redirects.ts +2 -0
- package/src/server/refresh.ts +5 -0
- package/src/server/sessions.ts +5 -0
- package/src/server/signin.ts +1 -0
- package/src/server/sso.ts +259 -17
- package/src/server/templates.ts +1 -0
- package/src/server/tokens.ts +1 -0
- package/src/server/totp.ts +4 -2
- package/src/server/types.ts +178 -83
- package/src/server/users.ts +1 -0
- package/src/server/utils.ts +71 -1
- package/src/server/version.ts +1 -1
- package/dist/component/public.js.map +0 -1
- package/dist/component/server/implementation.d.ts +0 -1264
- package/dist/component/server/implementation.d.ts.map +0 -1
- package/dist/component/server/implementation.js +0 -2365
- package/dist/component/server/implementation.js.map +0 -1
- package/dist/server/cookies.d.ts.map +0 -1
- package/dist/server/db.d.ts.map +0 -1
- package/dist/server/device.d.ts.map +0 -1
- package/dist/server/implementation.d.ts +0 -1264
- package/dist/server/implementation.d.ts.map +0 -1
- package/dist/server/implementation.js +0 -2365
- package/dist/server/implementation.js.map +0 -1
- package/dist/server/keys.d.ts.map +0 -1
- package/dist/server/oauth.d.ts.map +0 -1
- package/dist/server/ratelimit.d.ts.map +0 -1
- package/dist/server/redirects.d.ts.map +0 -1
- package/dist/server/refresh.d.ts.map +0 -1
- package/dist/server/sessions.d.ts.map +0 -1
- package/dist/server/signin.d.ts.map +0 -1
- package/dist/server/sso.d.ts.map +0 -1
- package/dist/server/templates.d.ts.map +0 -1
- package/dist/server/tokens.d.ts.map +0 -1
- package/dist/server/totp.d.ts.map +0 -1
- package/dist/server/users.d.ts.map +0 -1
- package/dist/server/utils.d.ts.map +0 -1
- package/src/server/implementation.ts +0 -5336
|
@@ -0,0 +1,1134 @@
|
|
|
1
|
+
import { isAuthError } from "./errors.js";
|
|
2
|
+
import { AuthError, Fx } from "./fx.js";
|
|
3
|
+
import { LOG_LEVELS, decryptSecret, encryptSecret, generateRandomString, logError, logWithLevel, requireEnv, sha256 } from "./utils.js";
|
|
4
|
+
import { redirectToParamCookie, useRedirectToParam } from "./cookies.js";
|
|
5
|
+
import { configDefaults, listAvailableProviders } from "./providers.js";
|
|
6
|
+
import { callModifyAccount } from "./mutations/account.js";
|
|
7
|
+
import { callInvalidateSessions } from "./mutations/invalidate.js";
|
|
8
|
+
import { SCIM_GROUP_SCHEMA_ID, SCIM_USER_SCHEMA_ID, createEnterpriseOidcRuntime, createEnterpriseSamlMetadataXml, createEnterpriseSamlSignInRequest, createSamlPostBindingResponse, createServiceProviderMetadata, encodeEnterpriseSamlRelayState, enterpriseOidcProviderId, enterpriseSamlProviderId, getEnterpriseOidcUrls, getOidcConfig, getPublicOidcConfig, getSamlConfig, getSamlServiceProviderOptions, isEnterpriseSamlSourceActive, normalizeDomain, normalizeEnterprisePolicy, parseEnterpriseSamlLoginResponse, parseEnterpriseSamlLogoutMessage, parseSamlIdpMetadata, parseScimListRequest, parseScimPath, patchEnterprisePolicy, profileFromSamlExtract, scimError, scimJson, serializeScimGroup, serializeScimUser, upsertProtocolConfig, validateEnterpriseSamlLoginRelayState, withOidcSecretState } from "./sso.js";
|
|
9
|
+
import { callUserOAuth } from "./mutations/oauth.js";
|
|
10
|
+
import { callCreateAccountFromCredentials } from "./mutations/register.js";
|
|
11
|
+
import { callRetrieveAccountWithCredentials } from "./mutations/retrieve.js";
|
|
12
|
+
import { callVerifierSignature } from "./mutations/signature.js";
|
|
13
|
+
import { callSignOut } from "./mutations/signout.js";
|
|
14
|
+
import { storeArgs, storeImpl } from "./mutations/index.js";
|
|
15
|
+
import { redirectAbsoluteUrl, setURLSearchParam } from "./redirects.js";
|
|
16
|
+
import { signInImpl } from "./signin.js";
|
|
17
|
+
import { createCoreDomains } from "./domains/core.js";
|
|
18
|
+
import { createSsoDomain } from "./domains/sso.js";
|
|
19
|
+
import { addAuthRoutes, addOpenIdRoutes, addSSORoutes, convertErrorsToResponse, createHttpAction, createHttpRoute, getCookies } from "./http.js";
|
|
20
|
+
import { createOAuthAuthorizationURL, handleOAuthCallback } from "./oauth.js";
|
|
21
|
+
import { actionGeneric, internalMutationGeneric } from "convex/server";
|
|
22
|
+
import { v } from "convex/values";
|
|
23
|
+
import { serialize } from "cookie";
|
|
24
|
+
|
|
25
|
+
//#region src/server/factory.ts
|
|
26
|
+
const ENTERPRISE_OIDC_CLIENT_SECRET_KIND = "oidc_client_secret";
|
|
27
|
+
/**
|
|
28
|
+
* Configure the Convex Auth library. Returns an object with
|
|
29
|
+
* functions and `auth` helper. You must export the functions
|
|
30
|
+
* from `convex/auth.ts` to make them callable:
|
|
31
|
+
*
|
|
32
|
+
* ```ts filename="convex/auth.ts"
|
|
33
|
+
* import { createAuth } from "@robelest/convex-auth/component";
|
|
34
|
+
* import { components } from "./_generated/api";
|
|
35
|
+
*
|
|
36
|
+
* export const auth = createAuth(components.auth, {
|
|
37
|
+
* providers: [],
|
|
38
|
+
* });
|
|
39
|
+
* export const { signIn, signOut, store } = auth;
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @returns An object with fields you should reexport from your
|
|
43
|
+
* `convex/auth.ts` file.
|
|
44
|
+
*/
|
|
45
|
+
function Auth(config_) {
|
|
46
|
+
const config = configDefaults(config_);
|
|
47
|
+
const hasOAuth = config.providers.some((provider) => provider.type === "oauth");
|
|
48
|
+
const hasSSO = config.providers.some((provider) => provider.type === "sso");
|
|
49
|
+
const getProviderOrThrow = (id, allowExtraProviders = false) => {
|
|
50
|
+
const provider = config.providers.find((configuredProvider) => configuredProvider.id === id) ?? (allowExtraProviders ? config.extraProviders.find((configuredProvider) => configuredProvider.id === id) : void 0);
|
|
51
|
+
if (provider === void 0) {
|
|
52
|
+
const detail = `Provider \`${id}\` is not configured, available providers are ${listAvailableProviders(config, allowExtraProviders)}.`;
|
|
53
|
+
logWithLevel(LOG_LEVELS.ERROR, detail);
|
|
54
|
+
throw new AuthError("PROVIDER_NOT_CONFIGURED", detail, { provider: id }).toConvexError();
|
|
55
|
+
}
|
|
56
|
+
return provider;
|
|
57
|
+
};
|
|
58
|
+
const getEnterpriseSecret = async (ctx, enterpriseId, kind) => {
|
|
59
|
+
return await ctx.runQuery(config.component.public.enterpriseSecretGet, {
|
|
60
|
+
enterpriseId,
|
|
61
|
+
kind
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
const getEnterpriseOidcConfigWithSecret = async (ctx, enterprise) => {
|
|
65
|
+
const oidc = getOidcConfig(enterprise.config);
|
|
66
|
+
const secret = await getEnterpriseSecret(ctx, enterprise._id, ENTERPRISE_OIDC_CLIENT_SECRET_KIND);
|
|
67
|
+
return {
|
|
68
|
+
...oidc,
|
|
69
|
+
...secret ? { clientSecret: await decryptSecret(secret.ciphertext) } : {}
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const INVITE_TOKEN_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
73
|
+
const INVITE_TOKEN_LENGTH = 48;
|
|
74
|
+
const enterpriseNotFoundError = "Enterprise not found.";
|
|
75
|
+
const ENTERPRISE_CONTROL_ROUTE_BASE = "/api/auth/sso";
|
|
76
|
+
const getPolicyFromEnterprise = (enterprise) => normalizeEnterprisePolicy(enterprise.policy);
|
|
77
|
+
const loadEnterpriseOrThrow = async (ctx, enterpriseId) => {
|
|
78
|
+
const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
|
|
79
|
+
if (!enterprise) throw new AuthError("INVALID_PARAMETERS", enterpriseNotFoundError).toConvexError();
|
|
80
|
+
return enterprise;
|
|
81
|
+
};
|
|
82
|
+
const loadActiveEnterpriseOrThrow = async (ctx, enterpriseId) => {
|
|
83
|
+
const enterprise = await loadEnterpriseOrThrow(ctx, enterpriseId);
|
|
84
|
+
if (enterprise.status !== "active") throw new AuthError("INVALID_PARAMETERS", "Enterprise connection is not active.").toConvexError();
|
|
85
|
+
return enterprise;
|
|
86
|
+
};
|
|
87
|
+
const loadActiveEnterpriseSamlOrThrow = async (ctx, enterpriseId) => {
|
|
88
|
+
const enterprise = await loadEnterpriseOrThrow(ctx, enterpriseId);
|
|
89
|
+
const loaded = {
|
|
90
|
+
source: {
|
|
91
|
+
kind: "enterprise",
|
|
92
|
+
id: enterpriseId
|
|
93
|
+
},
|
|
94
|
+
config: enterprise.config,
|
|
95
|
+
status: enterprise.status,
|
|
96
|
+
enterprise
|
|
97
|
+
};
|
|
98
|
+
if (!isEnterpriseSamlSourceActive(loaded)) throw new AuthError("INVALID_PARAMETERS", "Enterprise connection is not active.").toConvexError();
|
|
99
|
+
const saml = getSamlConfig(loaded.config);
|
|
100
|
+
if (!saml.idp?.metadataXml) throw new AuthError("PROVIDER_NOT_CONFIGURED", "SAML is not configured for this enterprise.").toConvexError();
|
|
101
|
+
return {
|
|
102
|
+
loaded,
|
|
103
|
+
enterprise,
|
|
104
|
+
saml
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
const loadEnterpriseOidcOrThrow = async (ctx, enterpriseId) => {
|
|
108
|
+
const enterprise = await loadActiveEnterpriseOrThrow(ctx, enterpriseId);
|
|
109
|
+
const oidc = await getEnterpriseOidcConfigWithSecret(ctx, enterprise);
|
|
110
|
+
if (oidc.enabled !== true) throw new AuthError("PROVIDER_NOT_CONFIGURED", "OIDC is not configured for this enterprise.").toConvexError();
|
|
111
|
+
return {
|
|
112
|
+
enterprise,
|
|
113
|
+
oidc
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
const validateEnterprisePolicy = (policy) => {
|
|
117
|
+
const checks = [];
|
|
118
|
+
checks.push({
|
|
119
|
+
name: "policy_version",
|
|
120
|
+
ok: policy.version === 1
|
|
121
|
+
});
|
|
122
|
+
checks.push({
|
|
123
|
+
name: "jit_default_role_ids_present",
|
|
124
|
+
ok: policy.provisioning.jit.mode !== "createUserAndMembership" || policy.provisioning.jit.defaultRoleIds.length > 0,
|
|
125
|
+
message: policy.provisioning.jit.mode === "createUserAndMembership" && policy.provisioning.jit.defaultRoleIds.length === 0 ? "At least one default roleId is required when JIT membership provisioning is enabled." : void 0
|
|
126
|
+
});
|
|
127
|
+
checks.push({
|
|
128
|
+
name: "jit_default_role_ids_known",
|
|
129
|
+
ok: policy.provisioning.jit.defaultRoleIds.every((roleId) => config.authorization.roles[roleId] !== void 0),
|
|
130
|
+
message: policy.provisioning.jit.defaultRoleIds.every((roleId) => config.authorization.roles[roleId] !== void 0) ? void 0 : "JIT defaultRoleIds contains unknown roleIds."
|
|
131
|
+
});
|
|
132
|
+
checks.push({
|
|
133
|
+
name: "scim_reuse_supported",
|
|
134
|
+
ok: policy.provisioning.scimReuse.user === "externalId" || policy.provisioning.scimReuse.user === "none"
|
|
135
|
+
});
|
|
136
|
+
return checks;
|
|
137
|
+
};
|
|
138
|
+
const recordEnterpriseAuditEvent = async (ctx, data) => {
|
|
139
|
+
const { ok, ...rest } = data;
|
|
140
|
+
return await ctx.runMutation(config.component.public.enterpriseAuditEventCreate, {
|
|
141
|
+
...rest,
|
|
142
|
+
status: ok ? "success" : "failure",
|
|
143
|
+
occurredAt: Date.now()
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
const emitEnterpriseWebhookDeliveries = async (ctx, data) => {
|
|
147
|
+
const endpoints = await ctx.runQuery(config.component.public.enterpriseWebhookEndpointList, { enterpriseId: data.enterpriseId });
|
|
148
|
+
for (const endpoint of endpoints) {
|
|
149
|
+
if (endpoint.status !== "active" || !endpoint.subscriptions.includes(data.eventType)) continue;
|
|
150
|
+
await ctx.runMutation(config.component.public.enterpriseWebhookDeliveryEnqueue, {
|
|
151
|
+
enterpriseId: data.enterpriseId,
|
|
152
|
+
endpointId: endpoint._id,
|
|
153
|
+
auditEventId: data.auditEventId,
|
|
154
|
+
eventType: data.eventType,
|
|
155
|
+
payload: data.payload,
|
|
156
|
+
nextAttemptAt: Date.now()
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const getEnterpriseScimContext = async (ctx, request) => {
|
|
161
|
+
const authHeader = request.headers.get("Authorization");
|
|
162
|
+
if (!authHeader?.startsWith("Bearer ")) throw new AuthError("MISSING_BEARER_TOKEN").toConvexError();
|
|
163
|
+
const token = authHeader.slice(7);
|
|
164
|
+
const scimConfig = await ctx.runQuery(config.component.public.enterpriseScimConfigGetByTokenHash, { tokenHash: await sha256(token) });
|
|
165
|
+
if (!scimConfig || scimConfig.status !== "active") throw new AuthError("INVALID_API_KEY", "Invalid SCIM token.").toConvexError();
|
|
166
|
+
const parsedPath = parseScimPath(new URL(request.url).pathname);
|
|
167
|
+
if (parsedPath.enterpriseId !== scimConfig.enterpriseId) throw new AuthError("INVALID_API_KEY", "SCIM token/tenant mismatch.").toConvexError();
|
|
168
|
+
const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: scimConfig.enterpriseId });
|
|
169
|
+
if (enterprise === null) throw new AuthError("INVALID_PARAMETERS", "Enterprise not found.").toConvexError();
|
|
170
|
+
return {
|
|
171
|
+
scimConfig,
|
|
172
|
+
enterprise,
|
|
173
|
+
parsedPath
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
const SCIM_SCHEMAS = [{
|
|
177
|
+
id: SCIM_USER_SCHEMA_ID,
|
|
178
|
+
name: "User",
|
|
179
|
+
description: "User Account",
|
|
180
|
+
attributes: [
|
|
181
|
+
{
|
|
182
|
+
name: "userName",
|
|
183
|
+
type: "string",
|
|
184
|
+
required: true
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: "displayName",
|
|
188
|
+
type: "string"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "active",
|
|
192
|
+
type: "boolean"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: "emails",
|
|
196
|
+
type: "complex",
|
|
197
|
+
multiValued: true
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
}, {
|
|
201
|
+
id: SCIM_GROUP_SCHEMA_ID,
|
|
202
|
+
name: "Group",
|
|
203
|
+
description: "Group",
|
|
204
|
+
attributes: [{
|
|
205
|
+
name: "displayName",
|
|
206
|
+
type: "string",
|
|
207
|
+
required: true
|
|
208
|
+
}, {
|
|
209
|
+
name: "members",
|
|
210
|
+
type: "complex",
|
|
211
|
+
multiValued: true
|
|
212
|
+
}]
|
|
213
|
+
}];
|
|
214
|
+
const SCIM_RESOURCE_TYPES = [{
|
|
215
|
+
id: "User",
|
|
216
|
+
name: "User",
|
|
217
|
+
endpoint: "/Users",
|
|
218
|
+
schema: SCIM_USER_SCHEMA_ID
|
|
219
|
+
}, {
|
|
220
|
+
id: "Group",
|
|
221
|
+
name: "Group",
|
|
222
|
+
endpoint: "/Groups",
|
|
223
|
+
schema: SCIM_GROUP_SCHEMA_ID
|
|
224
|
+
}];
|
|
225
|
+
const handleStaticScimCollection = (items, resourceId, opts) => {
|
|
226
|
+
if (resourceId !== void 0) {
|
|
227
|
+
const item = items.find((entry) => entry[opts.by] === decodeURIComponent(resourceId));
|
|
228
|
+
return item ? scimJson(item) : scimError(404, "notFound", opts.notFound);
|
|
229
|
+
}
|
|
230
|
+
return scimJson({
|
|
231
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
|
232
|
+
Resources: items,
|
|
233
|
+
totalResults: items.length,
|
|
234
|
+
startIndex: 1,
|
|
235
|
+
itemsPerPage: items.length
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
const filterScimCollection = (items, filter, filters) => {
|
|
239
|
+
if (!filter) return items;
|
|
240
|
+
const predicate = filters[filter.attribute];
|
|
241
|
+
if (!predicate) throw new Error("Unsupported SCIM filter.");
|
|
242
|
+
return items.filter((item) => predicate(item, filter.value));
|
|
243
|
+
};
|
|
244
|
+
const paginateScimCollection = (items, listRequest) => {
|
|
245
|
+
const start = listRequest.startIndex - 1;
|
|
246
|
+
return items.slice(start, start + listRequest.count);
|
|
247
|
+
};
|
|
248
|
+
const requireScimResourceId = (resourceId, label) => {
|
|
249
|
+
if (!resourceId) return scimError(400, "invalidPath", `${label} resource ID is required.`);
|
|
250
|
+
return null;
|
|
251
|
+
};
|
|
252
|
+
const readScimJson = async (request) => await request.json();
|
|
253
|
+
let auth;
|
|
254
|
+
auth = {
|
|
255
|
+
...createCoreDomains({
|
|
256
|
+
config,
|
|
257
|
+
getAuth: () => auth,
|
|
258
|
+
callInvalidateSessions,
|
|
259
|
+
callCreateAccountFromCredentials,
|
|
260
|
+
callRetrieveAccountWithCredentials,
|
|
261
|
+
callModifyAccount,
|
|
262
|
+
getEnrichCtx: () => enrichCtx,
|
|
263
|
+
inviteTokenAlphabet: INVITE_TOKEN_ALPHABET,
|
|
264
|
+
inviteTokenLength: INVITE_TOKEN_LENGTH
|
|
265
|
+
}),
|
|
266
|
+
sso: createSsoDomain({
|
|
267
|
+
config,
|
|
268
|
+
getAuth: () => auth,
|
|
269
|
+
normalizeEnterprisePolicy,
|
|
270
|
+
normalizeDomain,
|
|
271
|
+
getEnterpriseSecret,
|
|
272
|
+
loadEnterpriseOrThrow,
|
|
273
|
+
validateEnterprisePolicy,
|
|
274
|
+
recordEnterpriseAuditEvent,
|
|
275
|
+
emitEnterpriseWebhookDeliveries,
|
|
276
|
+
enterpriseNotFoundError,
|
|
277
|
+
ENTERPRISE_OIDC_CLIENT_SECRET_KIND,
|
|
278
|
+
requireEnv,
|
|
279
|
+
generateRandomString,
|
|
280
|
+
INVITE_TOKEN_ALPHABET,
|
|
281
|
+
sha256,
|
|
282
|
+
encryptSecret,
|
|
283
|
+
upsertProtocolConfig,
|
|
284
|
+
parseSamlIdpMetadata,
|
|
285
|
+
createServiceProviderMetadata,
|
|
286
|
+
getSamlServiceProviderOptions,
|
|
287
|
+
getPublicOidcConfig,
|
|
288
|
+
withOidcSecretState,
|
|
289
|
+
getOidcConfig,
|
|
290
|
+
getEnterpriseOidcUrls,
|
|
291
|
+
enterpriseOidcProviderId,
|
|
292
|
+
getPolicyFromEnterprise,
|
|
293
|
+
patchEnterprisePolicy
|
|
294
|
+
}),
|
|
295
|
+
http: {
|
|
296
|
+
add: (http) => {
|
|
297
|
+
addOpenIdRoutes(http, {
|
|
298
|
+
getIssuer: () => requireEnv("CONVEX_SITE_URL"),
|
|
299
|
+
getJwks: () => requireEnv("JWKS")
|
|
300
|
+
});
|
|
301
|
+
if (hasSSO) {
|
|
302
|
+
const handleSamlAcs = async (ctx, request, runtimeRoute) => Fx.run(Fx.gen(function* () {
|
|
303
|
+
yield* Fx.guard(runtimeRoute.protocol !== "saml" || runtimeRoute.rest.length !== 1 || runtimeRoute.rest[0] !== "acs", Fx.fail(new AuthError("INVALID_PARAMETERS", "Invalid enterprise runtime path.").toConvexError()));
|
|
304
|
+
const enterpriseId = runtimeRoute.enterpriseId;
|
|
305
|
+
const { loaded, enterprise, saml } = yield* Fx.from({
|
|
306
|
+
ok: () => loadActiveEnterpriseSamlOrThrow(ctx, enterpriseId),
|
|
307
|
+
err: (e) => e
|
|
308
|
+
});
|
|
309
|
+
const parsedResponse = yield* Fx.from({
|
|
310
|
+
ok: () => parseEnterpriseSamlLoginResponse({
|
|
311
|
+
request,
|
|
312
|
+
rootUrl: requireEnv("CONVEX_SITE_URL"),
|
|
313
|
+
source: {
|
|
314
|
+
kind: "enterprise",
|
|
315
|
+
id: enterprise._id
|
|
316
|
+
},
|
|
317
|
+
config: loaded.config
|
|
318
|
+
}),
|
|
319
|
+
err: (e) => new AuthError("OAUTH_PROVIDER_ERROR", `SAML response parse failed: ${e instanceof Error ? e.message : String(e)}`).toConvexError()
|
|
320
|
+
});
|
|
321
|
+
yield* Fx.from({
|
|
322
|
+
ok: () => {
|
|
323
|
+
validateEnterpriseSamlLoginRelayState({
|
|
324
|
+
relayState: parsedResponse.relayState,
|
|
325
|
+
source: {
|
|
326
|
+
kind: "enterprise",
|
|
327
|
+
id: enterprise._id
|
|
328
|
+
},
|
|
329
|
+
inResponseTo: parsedResponse.parsed.extract?.response?.inResponseTo
|
|
330
|
+
});
|
|
331
|
+
return Promise.resolve();
|
|
332
|
+
},
|
|
333
|
+
err: () => new AuthError("OAUTH_INVALID_STATE", "SAML RelayState did not match the pending login request.").toConvexError()
|
|
334
|
+
});
|
|
335
|
+
const { samlAttributes, samlSessionIndex, ...userProfile } = profileFromSamlExtract(parsedResponse.parsed.extract, saml.attributeMapping);
|
|
336
|
+
const profile = userProfile;
|
|
337
|
+
const maybeRedirectTo = useRedirectToParam(enterpriseSamlProviderId(enterprise._id), getCookies(request));
|
|
338
|
+
const verificationCode = yield* Fx.from({
|
|
339
|
+
ok: () => callUserOAuth(ctx, {
|
|
340
|
+
provider: enterpriseSamlProviderId(enterprise._id),
|
|
341
|
+
providerAccountId: profile.id,
|
|
342
|
+
profile,
|
|
343
|
+
signature: parsedResponse.relayState.signature,
|
|
344
|
+
accountExtend: {
|
|
345
|
+
identity: {
|
|
346
|
+
protocol: "saml",
|
|
347
|
+
enterpriseId: enterprise._id,
|
|
348
|
+
subject: profile.id,
|
|
349
|
+
entityId: typeof saml.entityId === "string" ? saml.entityId : void 0
|
|
350
|
+
},
|
|
351
|
+
saml: {
|
|
352
|
+
attributes: samlAttributes,
|
|
353
|
+
sessionIndex: samlSessionIndex
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}),
|
|
357
|
+
err: (e) => e
|
|
358
|
+
});
|
|
359
|
+
const vurl = setURLSearchParam(yield* Fx.from({
|
|
360
|
+
ok: () => redirectAbsoluteUrl(config, { redirectTo: maybeRedirectTo?.redirectTo ?? (typeof parsedResponse.relayState.redirectTo === "string" ? parsedResponse.relayState.redirectTo : void 0) }),
|
|
361
|
+
err: (e) => e
|
|
362
|
+
}), "code", verificationCode);
|
|
363
|
+
const vheaders = new Headers({ Location: vurl });
|
|
364
|
+
vheaders.set("Cache-Control", "must-revalidate");
|
|
365
|
+
for (const { name, value, options } of maybeRedirectTo !== null ? [maybeRedirectTo.updatedCookie] : []) vheaders.append("Set-Cookie", serialize(name, value, options));
|
|
366
|
+
return new Response(null, {
|
|
367
|
+
status: 302,
|
|
368
|
+
headers: vheaders
|
|
369
|
+
});
|
|
370
|
+
}).pipe(Fx.recover((e) => Fx.fatal(e))));
|
|
371
|
+
const handleSamlSlo = async (ctx, request, runtimeRoute) => {
|
|
372
|
+
if (runtimeRoute.protocol !== "saml" || runtimeRoute.rest.length !== 1 || runtimeRoute.rest[0] !== "slo") throw new AuthError("INVALID_PARAMETERS", "Invalid enterprise runtime path.").toConvexError();
|
|
373
|
+
const { loaded, enterprise } = await loadActiveEnterpriseSamlOrThrow(ctx, runtimeRoute.enterpriseId);
|
|
374
|
+
const parsedMessage = await parseEnterpriseSamlLogoutMessage({
|
|
375
|
+
request,
|
|
376
|
+
rootUrl: requireEnv("CONVEX_SITE_URL"),
|
|
377
|
+
source: {
|
|
378
|
+
kind: "enterprise",
|
|
379
|
+
id: enterprise._id
|
|
380
|
+
},
|
|
381
|
+
config: loaded.config
|
|
382
|
+
});
|
|
383
|
+
if (parsedMessage.hasSamlRequest && parsedMessage.parsedRequest) {
|
|
384
|
+
const responseContext = parsedMessage.runtime.sp.createLogoutResponse(parsedMessage.runtime.idp, parsedMessage.parsedRequest.extract, parsedMessage.binding, parsedMessage.relayState ?? "");
|
|
385
|
+
if (parsedMessage.binding === "redirect") return new Response(null, {
|
|
386
|
+
status: 302,
|
|
387
|
+
headers: { Location: responseContext.context }
|
|
388
|
+
});
|
|
389
|
+
return createSamlPostBindingResponse({
|
|
390
|
+
endpoint: responseContext.entityEndpoint,
|
|
391
|
+
parameter: "SAMLResponse",
|
|
392
|
+
value: responseContext.context,
|
|
393
|
+
relayState: parsedMessage.relayState
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
if (parsedMessage.hasSamlResponse) return new Response(null, { status: 204 });
|
|
397
|
+
throw new AuthError("INVALID_PARAMETERS", "Missing SAML logout payload.").toConvexError();
|
|
398
|
+
};
|
|
399
|
+
const handleScimRequest = async (ctx, request) => {
|
|
400
|
+
try {
|
|
401
|
+
const { scimConfig, enterprise, parsedPath } = await getEnterpriseScimContext(ctx, request);
|
|
402
|
+
const state = {
|
|
403
|
+
ctx,
|
|
404
|
+
request,
|
|
405
|
+
url: new URL(request.url),
|
|
406
|
+
parsedPath,
|
|
407
|
+
enterprise,
|
|
408
|
+
scimConfig,
|
|
409
|
+
policy: getPolicyFromEnterprise(enterprise),
|
|
410
|
+
recordScimEvent: async (eventType, ok, subjectType, subjectId, metadata) => {
|
|
411
|
+
const auditEventId = await recordEnterpriseAuditEvent(ctx, {
|
|
412
|
+
enterpriseId: enterprise._id,
|
|
413
|
+
groupId: enterprise.groupId,
|
|
414
|
+
eventType,
|
|
415
|
+
actorType: "scim",
|
|
416
|
+
subjectType,
|
|
417
|
+
subjectId,
|
|
418
|
+
ok,
|
|
419
|
+
metadata
|
|
420
|
+
});
|
|
421
|
+
await emitEnterpriseWebhookDeliveries(ctx, {
|
|
422
|
+
enterpriseId: enterprise._id,
|
|
423
|
+
eventType,
|
|
424
|
+
auditEventId,
|
|
425
|
+
payload: {
|
|
426
|
+
enterpriseId: enterprise._id,
|
|
427
|
+
subjectId,
|
|
428
|
+
metadata
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
const handleUsersGet = async (state$1) => {
|
|
434
|
+
const members = await auth.member.list(state$1.ctx, {
|
|
435
|
+
where: { groupId: state$1.enterprise.groupId },
|
|
436
|
+
limit: 100
|
|
437
|
+
});
|
|
438
|
+
const identities = await state$1.ctx.runQuery(config.component.public.enterpriseScimIdentityListByEnterprise, { enterpriseId: state$1.enterprise._id });
|
|
439
|
+
const identityByUserId = new Map(identities.filter((identity) => identity.userId !== void 0).map((identity) => [identity.userId, identity]));
|
|
440
|
+
const users = (await Promise.all(members.items.map(async (member) => {
|
|
441
|
+
const user = await auth.user.get(state$1.ctx, member.userId);
|
|
442
|
+
return user ? {
|
|
443
|
+
user,
|
|
444
|
+
member,
|
|
445
|
+
identity: identityByUserId.get(user._id)
|
|
446
|
+
} : null;
|
|
447
|
+
}))).filter(Boolean);
|
|
448
|
+
const listRequest = parseScimListRequest(state$1.url);
|
|
449
|
+
const filtered = filterScimCollection(users, listRequest.filter, {
|
|
450
|
+
id: (item, value) => item.user._id === value,
|
|
451
|
+
externalId: (item, value) => item.identity?.externalId === value,
|
|
452
|
+
userName: (item, value) => item.user.email === value,
|
|
453
|
+
"emails.value": (item, value) => item.user.email === value,
|
|
454
|
+
active: (item, value) => String(item.identity?.active ?? item.member.status === "active") === value
|
|
455
|
+
});
|
|
456
|
+
if (state$1.parsedPath.resourceId) {
|
|
457
|
+
const resource = filtered.find(({ user }) => user._id === state$1.parsedPath.resourceId);
|
|
458
|
+
return resource ? scimJson(serializeScimUser({
|
|
459
|
+
id: resource.user._id,
|
|
460
|
+
user: resource.user,
|
|
461
|
+
externalId: resource.identity?.externalId,
|
|
462
|
+
location: `${state$1.url.origin}${state$1.url.pathname.replace(/\/[^/]+$/, "")}/${resource.user._id}`,
|
|
463
|
+
active: resource.identity?.active ?? resource.member.status === "active"
|
|
464
|
+
}), 200, { Location: `${state$1.url.origin}${state$1.url.pathname.replace(/\/[^/]+$/, "")}/${resource.user._id}` }) : scimError(404, "notFound", "User not found.");
|
|
465
|
+
}
|
|
466
|
+
const paged = paginateScimCollection(filtered, listRequest);
|
|
467
|
+
await state$1.recordScimEvent("enterprise.scim.read", true, "enterprise_scim", state$1.scimConfig._id);
|
|
468
|
+
return scimJson({
|
|
469
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
|
470
|
+
Resources: paged.map(({ user, identity, member }) => serializeScimUser({
|
|
471
|
+
id: user._id,
|
|
472
|
+
user,
|
|
473
|
+
externalId: identity?.externalId,
|
|
474
|
+
location: `${state$1.url.origin}${state$1.url.pathname}/${user._id}`,
|
|
475
|
+
active: identity?.active ?? member.status === "active"
|
|
476
|
+
})),
|
|
477
|
+
totalResults: filtered.length,
|
|
478
|
+
startIndex: listRequest.startIndex,
|
|
479
|
+
itemsPerPage: paged.length
|
|
480
|
+
});
|
|
481
|
+
};
|
|
482
|
+
const handleUsersPost = async (state$1) => {
|
|
483
|
+
const body = await readScimJson(state$1.request);
|
|
484
|
+
const primaryEmail = Array.isArray(body.emails) ? body.emails.find((entry) => entry.primary === true)?.value ?? body.emails[0]?.value : void 0;
|
|
485
|
+
const phone = Array.isArray(body.phoneNumbers) ? body.phoneNumbers[0]?.value : void 0;
|
|
486
|
+
const userId = await state$1.ctx.runMutation(config.component.public.userInsert, { data: {
|
|
487
|
+
name: body.displayName ?? body.name?.formatted,
|
|
488
|
+
email: primaryEmail ?? body.userName,
|
|
489
|
+
...typeof (primaryEmail ?? body.userName) === "string" ? { emailVerificationTime: Date.now() } : {},
|
|
490
|
+
phone,
|
|
491
|
+
...typeof phone === "string" ? { phoneVerificationTime: Date.now() } : {}
|
|
492
|
+
} });
|
|
493
|
+
try {
|
|
494
|
+
await auth.member.create(state$1.ctx, {
|
|
495
|
+
groupId: state$1.enterprise.groupId,
|
|
496
|
+
userId,
|
|
497
|
+
roleIds: state$1.policy.provisioning.jit.defaultRoleIds,
|
|
498
|
+
status: body.active === false ? "inactive" : "active"
|
|
499
|
+
});
|
|
500
|
+
} catch {}
|
|
501
|
+
if (typeof body.externalId === "string") await state$1.ctx.runMutation(config.component.public.enterpriseScimIdentityUpsert, {
|
|
502
|
+
enterpriseId: state$1.enterprise._id,
|
|
503
|
+
groupId: state$1.enterprise.groupId,
|
|
504
|
+
resourceType: "user",
|
|
505
|
+
externalId: body.externalId,
|
|
506
|
+
userId,
|
|
507
|
+
active: body.active !== false,
|
|
508
|
+
raw: body,
|
|
509
|
+
lastProvisionedAt: Date.now()
|
|
510
|
+
});
|
|
511
|
+
await state$1.recordScimEvent("enterprise.scim.user.created", true, "user", userId);
|
|
512
|
+
const createdUser = await auth.user.get(state$1.ctx, userId);
|
|
513
|
+
const location = `${state$1.url.origin}${state$1.url.pathname}/${userId}`;
|
|
514
|
+
return scimJson(serializeScimUser({
|
|
515
|
+
id: userId,
|
|
516
|
+
user: createdUser ?? {},
|
|
517
|
+
externalId: body.externalId,
|
|
518
|
+
location,
|
|
519
|
+
active: body.active !== false
|
|
520
|
+
}), 201, { Location: location });
|
|
521
|
+
};
|
|
522
|
+
const handleUsersUpsert = async (state$1) => {
|
|
523
|
+
const missing = requireScimResourceId(state$1.parsedPath.resourceId, "User");
|
|
524
|
+
if (missing) return missing;
|
|
525
|
+
const userId = state$1.parsedPath.resourceId;
|
|
526
|
+
const existingUser = await auth.user.get(state$1.ctx, userId);
|
|
527
|
+
if (!existingUser) return scimError(404, "notFound", "User not found.");
|
|
528
|
+
const body = await readScimJson(state$1.request);
|
|
529
|
+
const patchData = {};
|
|
530
|
+
let nextActive;
|
|
531
|
+
if (state$1.request.method === "PUT") {
|
|
532
|
+
patchData.name = body.displayName ?? body.name?.formatted;
|
|
533
|
+
patchData.email = body.userName ?? (Array.isArray(body.emails) ? body.emails[0]?.value : void 0);
|
|
534
|
+
patchData.phone = Array.isArray(body.phoneNumbers) ? body.phoneNumbers[0]?.value : void 0;
|
|
535
|
+
if (typeof patchData.email === "string") patchData.emailVerificationTime = Date.now();
|
|
536
|
+
if (typeof patchData.phone === "string") patchData.phoneVerificationTime = Date.now();
|
|
537
|
+
} else for (const operation of Array.isArray(body.Operations) ? body.Operations : []) {
|
|
538
|
+
if (operation.path === "active") nextActive = operation.value;
|
|
539
|
+
if (operation.path === "displayName" || operation.path === "name.formatted") patchData.name = operation.value;
|
|
540
|
+
if (operation.path === "userName" || operation.path === "emails.value") {
|
|
541
|
+
patchData.email = operation.value;
|
|
542
|
+
if (typeof operation.value === "string") patchData.emailVerificationTime = Date.now();
|
|
543
|
+
}
|
|
544
|
+
if (operation.path === "phoneNumbers.value") {
|
|
545
|
+
patchData.phone = operation.value;
|
|
546
|
+
if (typeof operation.value === "string") patchData.phoneVerificationTime = Date.now();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
await state$1.ctx.runMutation(config.component.public.userPatch, {
|
|
550
|
+
userId,
|
|
551
|
+
data: patchData
|
|
552
|
+
});
|
|
553
|
+
const membership = await auth.member.getByUserAndGroup(state$1.ctx, {
|
|
554
|
+
groupId: state$1.enterprise.groupId,
|
|
555
|
+
userId
|
|
556
|
+
});
|
|
557
|
+
if (membership) await auth.member.update(state$1.ctx, membership._id, { status: body.active === false || nextActive === false ? "inactive" : "active" });
|
|
558
|
+
await state$1.ctx.runMutation(config.component.public.enterpriseScimIdentityUpsert, {
|
|
559
|
+
enterpriseId: state$1.enterprise._id,
|
|
560
|
+
groupId: state$1.enterprise.groupId,
|
|
561
|
+
resourceType: "user",
|
|
562
|
+
externalId: typeof body.externalId === "string" ? body.externalId : (await state$1.ctx.runQuery(config.component.public.enterpriseScimIdentityGetByEnterpriseAndUser, {
|
|
563
|
+
enterpriseId: state$1.enterprise._id,
|
|
564
|
+
userId
|
|
565
|
+
}))?.externalId ?? userId,
|
|
566
|
+
userId,
|
|
567
|
+
active: body.active !== false && nextActive !== false,
|
|
568
|
+
raw: body,
|
|
569
|
+
lastProvisionedAt: Date.now()
|
|
570
|
+
});
|
|
571
|
+
await state$1.recordScimEvent("enterprise.scim.user.updated", true, "user", userId);
|
|
572
|
+
const updatedUser = await auth.user.get(state$1.ctx, userId);
|
|
573
|
+
const location = `${state$1.url.origin}${state$1.url.pathname}`;
|
|
574
|
+
return scimJson(serializeScimUser({
|
|
575
|
+
id: userId,
|
|
576
|
+
user: updatedUser ?? existingUser,
|
|
577
|
+
externalId: typeof body.externalId === "string" ? body.externalId : void 0,
|
|
578
|
+
location,
|
|
579
|
+
active: body.active !== false && nextActive !== false
|
|
580
|
+
}), 200, { Location: location });
|
|
581
|
+
};
|
|
582
|
+
const handleUsersDelete = async (state$1) => {
|
|
583
|
+
const missing = requireScimResourceId(state$1.parsedPath.resourceId, "User");
|
|
584
|
+
if (missing) return missing;
|
|
585
|
+
const userId = state$1.parsedPath.resourceId;
|
|
586
|
+
const membership = await auth.member.getByUserAndGroup(state$1.ctx, {
|
|
587
|
+
groupId: state$1.enterprise.groupId,
|
|
588
|
+
userId
|
|
589
|
+
});
|
|
590
|
+
if (membership) await auth.member.delete(state$1.ctx, membership._id);
|
|
591
|
+
const identity = await state$1.ctx.runQuery(config.component.public.enterpriseScimIdentityGetByEnterpriseAndUser, {
|
|
592
|
+
enterpriseId: state$1.enterprise._id,
|
|
593
|
+
userId
|
|
594
|
+
});
|
|
595
|
+
if (identity) if (state$1.policy.provisioning.deprovision.mode === "hard") await state$1.ctx.runMutation(config.component.public.enterpriseScimIdentityDelete, { identityId: identity._id });
|
|
596
|
+
else await state$1.ctx.runMutation(config.component.public.enterpriseScimIdentityUpsert, {
|
|
597
|
+
enterpriseId: identity.enterpriseId,
|
|
598
|
+
groupId: identity.groupId,
|
|
599
|
+
resourceType: identity.resourceType,
|
|
600
|
+
externalId: identity.externalId,
|
|
601
|
+
userId: identity.userId,
|
|
602
|
+
mappedGroupId: identity.mappedGroupId,
|
|
603
|
+
active: false,
|
|
604
|
+
raw: identity.raw,
|
|
605
|
+
lastProvisionedAt: Date.now()
|
|
606
|
+
});
|
|
607
|
+
await state$1.recordScimEvent("enterprise.scim.user.deleted", true, "user", userId);
|
|
608
|
+
return new Response(null, { status: 204 });
|
|
609
|
+
};
|
|
610
|
+
const handleGroupsGet = async (state$1) => {
|
|
611
|
+
const groupsList = await auth.group.list(state$1.ctx, {
|
|
612
|
+
where: { parentGroupId: state$1.enterprise.groupId },
|
|
613
|
+
limit: 100
|
|
614
|
+
});
|
|
615
|
+
const identities = await state$1.ctx.runQuery(config.component.public.enterpriseScimIdentityListByEnterprise, { enterpriseId: state$1.enterprise._id });
|
|
616
|
+
const identityByGroupId = new Map(identities.filter((identity) => identity.mappedGroupId !== void 0).map((identity) => [identity.mappedGroupId, identity]));
|
|
617
|
+
const groups = groupsList.items.map((group) => ({
|
|
618
|
+
group,
|
|
619
|
+
identity: identityByGroupId.get(group._id)
|
|
620
|
+
}));
|
|
621
|
+
const listRequest = parseScimListRequest(state$1.url);
|
|
622
|
+
const filtered = filterScimCollection(groups, listRequest.filter, {
|
|
623
|
+
id: (item, value) => item.group._id === value,
|
|
624
|
+
externalId: (item, value) => item.identity?.externalId === value,
|
|
625
|
+
displayName: (item, value) => item.group.name === value
|
|
626
|
+
});
|
|
627
|
+
if (state$1.parsedPath.resourceId) {
|
|
628
|
+
const resource = filtered.find(({ group }) => group._id === state$1.parsedPath.resourceId);
|
|
629
|
+
if (!resource) return scimError(404, "notFound", "Group not found.");
|
|
630
|
+
const members = (await auth.member.list(state$1.ctx, {
|
|
631
|
+
where: {
|
|
632
|
+
groupId: resource.group._id,
|
|
633
|
+
status: "active"
|
|
634
|
+
},
|
|
635
|
+
limit: 100
|
|
636
|
+
})).items.map((member) => ({ value: member.userId }));
|
|
637
|
+
const location = `${state$1.url.origin}${state$1.url.pathname.replace(/\/[^/]+$/, "")}/${resource.group._id}`;
|
|
638
|
+
return scimJson(serializeScimGroup({
|
|
639
|
+
id: resource.group._id,
|
|
640
|
+
group: resource.group,
|
|
641
|
+
externalId: resource.identity?.externalId,
|
|
642
|
+
location,
|
|
643
|
+
members
|
|
644
|
+
}), 200, { Location: location });
|
|
645
|
+
}
|
|
646
|
+
const paged = paginateScimCollection(filtered, listRequest);
|
|
647
|
+
return scimJson({
|
|
648
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
|
|
649
|
+
Resources: paged.map(({ group, identity }) => serializeScimGroup({
|
|
650
|
+
id: group._id,
|
|
651
|
+
group,
|
|
652
|
+
externalId: identity?.externalId,
|
|
653
|
+
location: `${state$1.url.origin}${state$1.url.pathname}/${group._id}`
|
|
654
|
+
})),
|
|
655
|
+
totalResults: filtered.length,
|
|
656
|
+
startIndex: listRequest.startIndex,
|
|
657
|
+
itemsPerPage: paged.length
|
|
658
|
+
});
|
|
659
|
+
};
|
|
660
|
+
const handleGroupsPost = async (state$1) => {
|
|
661
|
+
const body = await readScimJson(state$1.request);
|
|
662
|
+
const { groupId } = await auth.group.create(state$1.ctx, {
|
|
663
|
+
name: String(body.displayName ?? "Group"),
|
|
664
|
+
parentGroupId: state$1.enterprise.groupId,
|
|
665
|
+
type: "organization"
|
|
666
|
+
});
|
|
667
|
+
await state$1.ctx.runMutation(config.component.public.enterpriseScimIdentityUpsert, {
|
|
668
|
+
enterpriseId: state$1.enterprise._id,
|
|
669
|
+
groupId: state$1.enterprise.groupId,
|
|
670
|
+
resourceType: "group",
|
|
671
|
+
externalId: body.externalId ?? groupId,
|
|
672
|
+
mappedGroupId: groupId,
|
|
673
|
+
active: true,
|
|
674
|
+
raw: body,
|
|
675
|
+
lastProvisionedAt: Date.now()
|
|
676
|
+
});
|
|
677
|
+
for (const member of Array.isArray(body.members) ? body.members : []) try {
|
|
678
|
+
await auth.member.create(state$1.ctx, {
|
|
679
|
+
groupId,
|
|
680
|
+
userId: String(member.value),
|
|
681
|
+
roleIds: state$1.policy.provisioning.jit.defaultRoleIds,
|
|
682
|
+
status: "active"
|
|
683
|
+
});
|
|
684
|
+
} catch {}
|
|
685
|
+
await state$1.recordScimEvent("enterprise.scim.group.created", true, "group", groupId);
|
|
686
|
+
const group = await auth.group.get(state$1.ctx, groupId);
|
|
687
|
+
const location = `${state$1.url.origin}${state$1.url.pathname}/${groupId}`;
|
|
688
|
+
return scimJson(serializeScimGroup({
|
|
689
|
+
id: groupId,
|
|
690
|
+
group: group ?? {},
|
|
691
|
+
externalId: body.externalId,
|
|
692
|
+
location,
|
|
693
|
+
members: (await auth.member.list(state$1.ctx, {
|
|
694
|
+
where: {
|
|
695
|
+
groupId,
|
|
696
|
+
status: "active"
|
|
697
|
+
},
|
|
698
|
+
limit: 100
|
|
699
|
+
})).items.map((member) => ({ value: member.userId }))
|
|
700
|
+
}), 201, { Location: location });
|
|
701
|
+
};
|
|
702
|
+
const handleGroupsPatch = async (state$1) => {
|
|
703
|
+
const missing = requireScimResourceId(state$1.parsedPath.resourceId, "Group");
|
|
704
|
+
if (missing) return missing;
|
|
705
|
+
const groupId = state$1.parsedPath.resourceId;
|
|
706
|
+
const body = await readScimJson(state$1.request);
|
|
707
|
+
for (const operation of Array.isArray(body.Operations) ? body.Operations : []) {
|
|
708
|
+
if (operation.path === "displayName") await auth.group.update(state$1.ctx, groupId, { name: operation.value });
|
|
709
|
+
if (operation.path === "members" && operation.op === "add") for (const member of Array.isArray(operation.value) ? operation.value : []) try {
|
|
710
|
+
await auth.member.create(state$1.ctx, {
|
|
711
|
+
groupId,
|
|
712
|
+
userId: String(member.value),
|
|
713
|
+
roleIds: state$1.policy.provisioning.jit.defaultRoleIds,
|
|
714
|
+
status: "active"
|
|
715
|
+
});
|
|
716
|
+
} catch {}
|
|
717
|
+
if (operation.path === "members" && operation.op === "replace") {
|
|
718
|
+
const currentMembers = (await auth.member.list(state$1.ctx, {
|
|
719
|
+
where: {
|
|
720
|
+
groupId,
|
|
721
|
+
status: "active"
|
|
722
|
+
},
|
|
723
|
+
limit: 100
|
|
724
|
+
})).items;
|
|
725
|
+
const currentUserIds = new Set(currentMembers.map((member) => member.userId));
|
|
726
|
+
const nextUserIds = new Set((Array.isArray(operation.value) ? operation.value : []).map((member) => String(member.value)));
|
|
727
|
+
for (const member of currentMembers) if (!nextUserIds.has(member.userId)) await auth.member.delete(state$1.ctx, member._id);
|
|
728
|
+
for (const userId of nextUserIds.values()) if (!currentUserIds.has(userId)) try {
|
|
729
|
+
await auth.member.create(state$1.ctx, {
|
|
730
|
+
groupId,
|
|
731
|
+
userId,
|
|
732
|
+
roleIds: state$1.policy.provisioning.jit.defaultRoleIds,
|
|
733
|
+
status: "active"
|
|
734
|
+
});
|
|
735
|
+
} catch {}
|
|
736
|
+
}
|
|
737
|
+
if (typeof operation.path === "string" && operation.op === "remove" && operation.path.startsWith("members[")) {
|
|
738
|
+
const userId = operation.path.match(/^members\[value eq "([^"]+)"\]$/)?.[1];
|
|
739
|
+
if (userId) {
|
|
740
|
+
const membership = await auth.member.getByUserAndGroup(state$1.ctx, {
|
|
741
|
+
groupId,
|
|
742
|
+
userId
|
|
743
|
+
});
|
|
744
|
+
if (membership) await auth.member.delete(state$1.ctx, membership._id);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
await state$1.recordScimEvent("enterprise.scim.group.updated", true, "group", groupId);
|
|
749
|
+
const group = await auth.group.get(state$1.ctx, groupId);
|
|
750
|
+
const location = `${state$1.url.origin}${state$1.url.pathname}`;
|
|
751
|
+
const members = (await auth.member.list(state$1.ctx, {
|
|
752
|
+
where: {
|
|
753
|
+
groupId,
|
|
754
|
+
status: "active"
|
|
755
|
+
},
|
|
756
|
+
limit: 100
|
|
757
|
+
})).items;
|
|
758
|
+
return scimJson(serializeScimGroup({
|
|
759
|
+
id: groupId,
|
|
760
|
+
group: group ?? {},
|
|
761
|
+
location,
|
|
762
|
+
members: members.map((member) => ({ value: member.userId }))
|
|
763
|
+
}), 200, { Location: location });
|
|
764
|
+
};
|
|
765
|
+
const handleGroupsDelete = async (state$1) => {
|
|
766
|
+
const missing = requireScimResourceId(state$1.parsedPath.resourceId, "Group");
|
|
767
|
+
if (missing) return missing;
|
|
768
|
+
const groupId = state$1.parsedPath.resourceId;
|
|
769
|
+
await auth.group.delete(state$1.ctx, groupId);
|
|
770
|
+
const identity = await state$1.ctx.runQuery(config.component.public.enterpriseScimIdentityGetByMappedGroup, { mappedGroupId: groupId });
|
|
771
|
+
if (identity) await state$1.ctx.runMutation(config.component.public.enterpriseScimIdentityDelete, { identityId: identity._id });
|
|
772
|
+
await state$1.recordScimEvent("enterprise.scim.group.deleted", true, "group", groupId);
|
|
773
|
+
return new Response(null, { status: 204 });
|
|
774
|
+
};
|
|
775
|
+
const handler = {
|
|
776
|
+
ServiceProviderConfig: { GET: async () => scimJson({
|
|
777
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
|
|
778
|
+
patch: { supported: true },
|
|
779
|
+
bulk: {
|
|
780
|
+
supported: false,
|
|
781
|
+
maxOperations: 0,
|
|
782
|
+
maxPayloadSize: 0
|
|
783
|
+
},
|
|
784
|
+
filter: {
|
|
785
|
+
supported: true,
|
|
786
|
+
maxResults: 100
|
|
787
|
+
},
|
|
788
|
+
changePassword: { supported: false },
|
|
789
|
+
sort: { supported: false },
|
|
790
|
+
etag: { supported: false },
|
|
791
|
+
authenticationSchemes: [{
|
|
792
|
+
type: "oauthbearertoken",
|
|
793
|
+
name: "Bearer Token",
|
|
794
|
+
description: "Use the SCIM token generated by Convex Auth enterprise."
|
|
795
|
+
}]
|
|
796
|
+
}) },
|
|
797
|
+
Schemas: { GET: async (state$1) => handleStaticScimCollection(SCIM_SCHEMAS, state$1.parsedPath.resourceId, {
|
|
798
|
+
by: "id",
|
|
799
|
+
notFound: "Schema not found."
|
|
800
|
+
}) },
|
|
801
|
+
ResourceTypes: { GET: async (state$1) => handleStaticScimCollection(SCIM_RESOURCE_TYPES, state$1.parsedPath.resourceId, {
|
|
802
|
+
by: "name",
|
|
803
|
+
notFound: "Resource type not found."
|
|
804
|
+
}) },
|
|
805
|
+
Users: {
|
|
806
|
+
GET: handleUsersGet,
|
|
807
|
+
POST: handleUsersPost,
|
|
808
|
+
PATCH: handleUsersUpsert,
|
|
809
|
+
PUT: handleUsersUpsert,
|
|
810
|
+
DELETE: handleUsersDelete
|
|
811
|
+
},
|
|
812
|
+
Groups: {
|
|
813
|
+
GET: handleGroupsGet,
|
|
814
|
+
POST: handleGroupsPost,
|
|
815
|
+
PATCH: handleGroupsPatch,
|
|
816
|
+
DELETE: handleGroupsDelete
|
|
817
|
+
}
|
|
818
|
+
}[state.parsedPath.resource]?.[state.request.method];
|
|
819
|
+
return handler ? await handler(state) : scimError(404, "notFound", "SCIM resource not found.");
|
|
820
|
+
} catch (error) {
|
|
821
|
+
if (error instanceof Error && error.message === "Unsupported SCIM filter.") return scimError(400, "invalidFilter", error.message);
|
|
822
|
+
if (isAuthError(error)) {
|
|
823
|
+
const code = error.data.code;
|
|
824
|
+
return scimError(code === "MISSING_BEARER_TOKEN" || code === "INVALID_API_KEY" ? 401 : 400, code, error.data.message);
|
|
825
|
+
}
|
|
826
|
+
throw error;
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
addSSORoutes(http, {
|
|
830
|
+
routeBase: ENTERPRISE_CONTROL_ROUTE_BASE,
|
|
831
|
+
convertErrorsToResponse,
|
|
832
|
+
handleSamlMetadata: async (ctx, _request, runtimeRoute) => {
|
|
833
|
+
const { loaded } = await loadActiveEnterpriseSamlOrThrow(ctx, runtimeRoute.enterpriseId);
|
|
834
|
+
return new Response(createEnterpriseSamlMetadataXml({
|
|
835
|
+
rootUrl: requireEnv("CONVEX_SITE_URL"),
|
|
836
|
+
source: loaded.source,
|
|
837
|
+
config: loaded.config
|
|
838
|
+
}), {
|
|
839
|
+
status: 200,
|
|
840
|
+
headers: { "Content-Type": "application/xml" }
|
|
841
|
+
});
|
|
842
|
+
},
|
|
843
|
+
handleSamlSignIn: async (ctx, request, runtimeRoute) => {
|
|
844
|
+
const url = new URL(request.url);
|
|
845
|
+
const verifier = url.searchParams.get("code");
|
|
846
|
+
if (!verifier) throw new AuthError("OAUTH_MISSING_VERIFIER").toConvexError();
|
|
847
|
+
const { loaded, enterprise } = await loadActiveEnterpriseSamlOrThrow(ctx, runtimeRoute.enterpriseId);
|
|
848
|
+
const state = generateRandomString(24, INVITE_TOKEN_ALPHABET);
|
|
849
|
+
const signInRequest = createEnterpriseSamlSignInRequest({
|
|
850
|
+
rootUrl: requireEnv("CONVEX_SITE_URL"),
|
|
851
|
+
source: {
|
|
852
|
+
kind: "enterprise",
|
|
853
|
+
id: enterprise._id
|
|
854
|
+
},
|
|
855
|
+
config: loaded.config,
|
|
856
|
+
state,
|
|
857
|
+
signature: `saml ${enterprise._id} pending ${state}`,
|
|
858
|
+
redirectTo: url.searchParams.get("redirectTo") ?? void 0
|
|
859
|
+
});
|
|
860
|
+
const signature = `saml ${enterprise._id} ${signInRequest.requestId} ${state}`;
|
|
861
|
+
await callVerifierSignature(ctx, {
|
|
862
|
+
verifier,
|
|
863
|
+
signature
|
|
864
|
+
});
|
|
865
|
+
const redirectTo = url.searchParams.get("redirectTo");
|
|
866
|
+
const redirectCookies = redirectTo !== null ? [redirectToParamCookie(enterpriseSamlProviderId(enterprise._id), redirectTo)] : [];
|
|
867
|
+
const relayState = encodeEnterpriseSamlRelayState({
|
|
868
|
+
source: {
|
|
869
|
+
kind: "enterprise",
|
|
870
|
+
id: enterprise._id
|
|
871
|
+
},
|
|
872
|
+
signature,
|
|
873
|
+
requestId: signInRequest.requestId,
|
|
874
|
+
state,
|
|
875
|
+
redirectTo: url.searchParams.get("redirectTo") ?? void 0
|
|
876
|
+
});
|
|
877
|
+
if (signInRequest.binding === "redirect" && signInRequest.redirectUrl) {
|
|
878
|
+
const redirectUrl = new URL(signInRequest.redirectUrl);
|
|
879
|
+
redirectUrl.searchParams.set("RelayState", relayState);
|
|
880
|
+
const headers = new Headers({ Location: redirectUrl.toString() });
|
|
881
|
+
for (const { name, value, options } of redirectCookies) headers.append("Set-Cookie", serialize(name, value, options));
|
|
882
|
+
return new Response(null, {
|
|
883
|
+
status: 302,
|
|
884
|
+
headers
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
const response = createSamlPostBindingResponse({
|
|
888
|
+
endpoint: signInRequest.post.endpoint,
|
|
889
|
+
parameter: "SAMLRequest",
|
|
890
|
+
value: signInRequest.post.value,
|
|
891
|
+
relayState
|
|
892
|
+
});
|
|
893
|
+
for (const { name, value, options } of redirectCookies) response.headers.append("Set-Cookie", serialize(name, value, options));
|
|
894
|
+
return response;
|
|
895
|
+
},
|
|
896
|
+
handleOidcSignIn: async (ctx, request, runtimeRoute) => {
|
|
897
|
+
const url = new URL(request.url);
|
|
898
|
+
const verifier = url.searchParams.get("code");
|
|
899
|
+
if (!verifier) throw new AuthError("OAUTH_MISSING_VERIFIER").toConvexError();
|
|
900
|
+
const { enterprise, oidc } = await loadEnterpriseOidcOrThrow(ctx, runtimeRoute.enterpriseId);
|
|
901
|
+
const { providerId, provider, oauthConfig } = await createEnterpriseOidcRuntime({
|
|
902
|
+
rootUrl: requireEnv("CONVEX_SITE_URL"),
|
|
903
|
+
enterpriseId: enterprise._id,
|
|
904
|
+
oidc
|
|
905
|
+
});
|
|
906
|
+
const { redirect, cookies, signature } = await createOAuthAuthorizationURL(providerId, provider, oauthConfig);
|
|
907
|
+
await callVerifierSignature(ctx, {
|
|
908
|
+
verifier,
|
|
909
|
+
signature
|
|
910
|
+
});
|
|
911
|
+
const redirectTo = url.searchParams.get("redirectTo");
|
|
912
|
+
const headers_ = new Headers({ Location: redirect });
|
|
913
|
+
for (const { name, value, options } of [...cookies, ...redirectTo !== null ? [redirectToParamCookie(providerId, redirectTo)] : []]) headers_.append("Set-Cookie", serialize(name, value, options));
|
|
914
|
+
return new Response(null, {
|
|
915
|
+
status: 302,
|
|
916
|
+
headers: headers_
|
|
917
|
+
});
|
|
918
|
+
},
|
|
919
|
+
handleOidcCallback: async (ctx, request, runtimeRoute) => {
|
|
920
|
+
const url = new URL(request.url);
|
|
921
|
+
const { enterprise, oidc } = await loadEnterpriseOidcOrThrow(ctx, runtimeRoute.enterpriseId);
|
|
922
|
+
const { providerId, provider, oauthConfig } = await createEnterpriseOidcRuntime({
|
|
923
|
+
rootUrl: requireEnv("CONVEX_SITE_URL"),
|
|
924
|
+
enterpriseId: enterprise._id,
|
|
925
|
+
oidc
|
|
926
|
+
});
|
|
927
|
+
const cookies = getCookies(request);
|
|
928
|
+
const maybeRedirectTo = useRedirectToParam(providerId, cookies);
|
|
929
|
+
const destinationUrl = await redirectAbsoluteUrl(config, { redirectTo: maybeRedirectTo?.redirectTo });
|
|
930
|
+
const params = url.searchParams;
|
|
931
|
+
const result = await Fx.run(handleOAuthCallback(providerId, provider, oauthConfig, Object.fromEntries(params.entries()), cookies));
|
|
932
|
+
const extraFields = oidc.extraFields;
|
|
933
|
+
let profile = result.profile;
|
|
934
|
+
if (extraFields && typeof profile === "object" && profile) {
|
|
935
|
+
const extend = {};
|
|
936
|
+
for (const [claimName, fieldName] of Object.entries(extraFields)) if (claimName in profile) extend[fieldName] = profile[claimName];
|
|
937
|
+
if (Object.keys(extend).length > 0) profile = {
|
|
938
|
+
...profile,
|
|
939
|
+
extend
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
const verificationCode = await callUserOAuth(ctx, {
|
|
943
|
+
provider: providerId,
|
|
944
|
+
providerAccountId: result.providerAccountId,
|
|
945
|
+
profile,
|
|
946
|
+
signature: result.signature,
|
|
947
|
+
accountExtend: { identity: {
|
|
948
|
+
protocol: "oidc",
|
|
949
|
+
enterpriseId: enterprise._id,
|
|
950
|
+
subject: result.providerAccountId,
|
|
951
|
+
issuer: typeof oidc.issuer === "string" ? oidc.issuer : void 0,
|
|
952
|
+
discoveryUrl: typeof oidc.discoveryUrl === "string" ? oidc.discoveryUrl : void 0
|
|
953
|
+
} }
|
|
954
|
+
});
|
|
955
|
+
const headers = new Headers({ Location: setURLSearchParam(destinationUrl, "code", verificationCode) });
|
|
956
|
+
for (const { name, value, options } of result.cookies) headers.append("Set-Cookie", serialize(name, value, options));
|
|
957
|
+
if (maybeRedirectTo) headers.append("Set-Cookie", serialize(maybeRedirectTo.updatedCookie.name, maybeRedirectTo.updatedCookie.value, maybeRedirectTo.updatedCookie.options));
|
|
958
|
+
return new Response(null, {
|
|
959
|
+
status: 302,
|
|
960
|
+
headers
|
|
961
|
+
});
|
|
962
|
+
},
|
|
963
|
+
handleSamlAcs,
|
|
964
|
+
handleSamlSlo,
|
|
965
|
+
handleScimRequest,
|
|
966
|
+
scimError
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
if (hasOAuth) addAuthRoutes(http, {
|
|
970
|
+
handleSignIn: convertErrorsToResponse(400, async (ctx, request) => {
|
|
971
|
+
const url = new URL(request.url);
|
|
972
|
+
const providerId = url.pathname.split("/").at(-1);
|
|
973
|
+
if (providerId === null) throw new AuthError("OAUTH_MISSING_PROVIDER").toConvexError();
|
|
974
|
+
const verifier = url.searchParams.get("code");
|
|
975
|
+
if (verifier === null) throw new AuthError("OAUTH_MISSING_VERIFIER").toConvexError();
|
|
976
|
+
const oauthConfig = getProviderOrThrow(providerId);
|
|
977
|
+
const { redirect, cookies, signature } = await createOAuthAuthorizationURL(providerId, oauthConfig.provider, oauthConfig);
|
|
978
|
+
await callVerifierSignature(ctx, {
|
|
979
|
+
verifier,
|
|
980
|
+
signature
|
|
981
|
+
});
|
|
982
|
+
const redirectTo = url.searchParams.get("redirectTo");
|
|
983
|
+
if (redirectTo !== null) cookies.push(redirectToParamCookie(providerId, redirectTo));
|
|
984
|
+
const headers = new Headers({ Location: redirect });
|
|
985
|
+
for (const { name, value, options } of cookies) headers.append("Set-Cookie", serialize(name, value, options));
|
|
986
|
+
return new Response(null, {
|
|
987
|
+
status: 302,
|
|
988
|
+
headers
|
|
989
|
+
});
|
|
990
|
+
}),
|
|
991
|
+
handleCallback: async (ctx, request) => {
|
|
992
|
+
const url = new URL(request.url);
|
|
993
|
+
const providerId = new URL(request.url).pathname.split("/").at(-1);
|
|
994
|
+
if (!providerId) throw new AuthError("OAUTH_MISSING_PROVIDER").toConvexError();
|
|
995
|
+
logWithLevel(LOG_LEVELS.DEBUG, "Handling OAuth callback for provider:", providerId);
|
|
996
|
+
const provider = getProviderOrThrow(providerId);
|
|
997
|
+
const cookies = getCookies(request);
|
|
998
|
+
const maybeRedirectTo = useRedirectToParam(provider.id, cookies);
|
|
999
|
+
const destinationUrl = await redirectAbsoluteUrl(config, { redirectTo: maybeRedirectTo?.redirectTo });
|
|
1000
|
+
const params = url.searchParams;
|
|
1001
|
+
if (request.headers.get("Content-Type") === "application/x-www-form-urlencoded") (await request.formData()).forEach((value, key) => {
|
|
1002
|
+
if (typeof value === "string") params.append(key, value);
|
|
1003
|
+
});
|
|
1004
|
+
return Fx.run(Fx.from({
|
|
1005
|
+
ok: async () => {
|
|
1006
|
+
const oauthConfig = provider;
|
|
1007
|
+
const result = await Fx.run(handleOAuthCallback(providerId, oauthConfig.provider, oauthConfig, Object.fromEntries(params.entries()), cookies));
|
|
1008
|
+
const oauthCookies = result.cookies;
|
|
1009
|
+
const { id: profileId, ...profileData } = result.profile;
|
|
1010
|
+
const { signature } = result;
|
|
1011
|
+
const redirUrl = setURLSearchParam(destinationUrl, "code", await callUserOAuth(ctx, {
|
|
1012
|
+
provider: providerId,
|
|
1013
|
+
providerAccountId: profileId,
|
|
1014
|
+
profile: profileData,
|
|
1015
|
+
signature
|
|
1016
|
+
}));
|
|
1017
|
+
const redirHeaders = new Headers({ Location: redirUrl });
|
|
1018
|
+
redirHeaders.set("Cache-Control", "must-revalidate");
|
|
1019
|
+
for (const { name, value, options } of [...oauthCookies, ...maybeRedirectTo !== null ? [maybeRedirectTo.updatedCookie] : []]) redirHeaders.append("Set-Cookie", serialize(name, value, options));
|
|
1020
|
+
return new Response(null, {
|
|
1021
|
+
status: 302,
|
|
1022
|
+
headers: redirHeaders
|
|
1023
|
+
});
|
|
1024
|
+
},
|
|
1025
|
+
err: (error) => error
|
|
1026
|
+
}).pipe(Fx.recover((error) => {
|
|
1027
|
+
logError(error);
|
|
1028
|
+
const respHeaders = new Headers({ Location: destinationUrl });
|
|
1029
|
+
for (const { name, value, options } of maybeRedirectTo !== null ? [maybeRedirectTo.updatedCookie] : []) respHeaders.append("Set-Cookie", serialize(name, value, options));
|
|
1030
|
+
return Fx.succeed(new Response(null, {
|
|
1031
|
+
status: 302,
|
|
1032
|
+
headers: respHeaders
|
|
1033
|
+
}));
|
|
1034
|
+
})));
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
},
|
|
1038
|
+
action: createHttpAction(auth),
|
|
1039
|
+
route: createHttpRoute(createHttpAction(auth))
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
const enrichCtx = (ctx) => ({
|
|
1043
|
+
...ctx,
|
|
1044
|
+
auth: {
|
|
1045
|
+
...ctx.auth,
|
|
1046
|
+
config,
|
|
1047
|
+
account: auth.account,
|
|
1048
|
+
session: auth.session,
|
|
1049
|
+
access: auth.access,
|
|
1050
|
+
provider: auth.provider
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
return {
|
|
1054
|
+
auth,
|
|
1055
|
+
signIn: actionGeneric({
|
|
1056
|
+
args: {
|
|
1057
|
+
provider: v.optional(v.string()),
|
|
1058
|
+
params: v.optional(v.any()),
|
|
1059
|
+
verifier: v.optional(v.string()),
|
|
1060
|
+
refreshToken: v.optional(v.string()),
|
|
1061
|
+
calledBy: v.optional(v.string())
|
|
1062
|
+
},
|
|
1063
|
+
handler: async (ctx, args) => {
|
|
1064
|
+
if (args.calledBy !== void 0) logWithLevel("INFO", `\`auth:signIn\` called by ${args.calledBy}`);
|
|
1065
|
+
const provider = args.provider !== void 0 ? getProviderOrThrow(args.provider) : null;
|
|
1066
|
+
const result = await signInImpl(enrichCtx(ctx), provider, args, {
|
|
1067
|
+
generateTokens: true,
|
|
1068
|
+
allowExtraProviders: false
|
|
1069
|
+
});
|
|
1070
|
+
return Fx.run(Fx.match(result, result.kind, {
|
|
1071
|
+
redirect: (r) => Fx.succeed({
|
|
1072
|
+
kind: "redirect",
|
|
1073
|
+
redirect: r.redirect,
|
|
1074
|
+
verifier: r.verifier
|
|
1075
|
+
}),
|
|
1076
|
+
signedIn: (r) => Fx.succeed({
|
|
1077
|
+
kind: "signedIn",
|
|
1078
|
+
tokens: r.signedIn?.tokens ?? null
|
|
1079
|
+
}),
|
|
1080
|
+
refreshTokens: (r) => Fx.succeed({
|
|
1081
|
+
kind: "signedIn",
|
|
1082
|
+
tokens: r.signedIn?.tokens ?? null
|
|
1083
|
+
}),
|
|
1084
|
+
started: () => Fx.succeed({ kind: "started" }),
|
|
1085
|
+
passkeyOptions: (r) => Fx.succeed({
|
|
1086
|
+
kind: "passkeyOptions",
|
|
1087
|
+
options: r.options,
|
|
1088
|
+
verifier: r.verifier
|
|
1089
|
+
}),
|
|
1090
|
+
totpRequired: (r) => Fx.succeed({
|
|
1091
|
+
kind: "totpRequired",
|
|
1092
|
+
verifier: r.verifier
|
|
1093
|
+
}),
|
|
1094
|
+
totpSetup: (r) => Fx.succeed({
|
|
1095
|
+
kind: "totpSetup",
|
|
1096
|
+
totpSetup: {
|
|
1097
|
+
uri: r.uri,
|
|
1098
|
+
secret: r.secret,
|
|
1099
|
+
totpId: r.totpId
|
|
1100
|
+
},
|
|
1101
|
+
verifier: r.verifier
|
|
1102
|
+
}),
|
|
1103
|
+
deviceCode: (r) => Fx.succeed({
|
|
1104
|
+
kind: "deviceCode",
|
|
1105
|
+
deviceCode: {
|
|
1106
|
+
deviceCode: r.deviceCode,
|
|
1107
|
+
userCode: r.userCode,
|
|
1108
|
+
verificationUri: r.verificationUri,
|
|
1109
|
+
verificationUriComplete: r.verificationUriComplete,
|
|
1110
|
+
expiresIn: r.expiresIn,
|
|
1111
|
+
interval: r.interval
|
|
1112
|
+
}
|
|
1113
|
+
})
|
|
1114
|
+
}));
|
|
1115
|
+
}
|
|
1116
|
+
}),
|
|
1117
|
+
signOut: actionGeneric({
|
|
1118
|
+
args: {},
|
|
1119
|
+
handler: async (ctx) => {
|
|
1120
|
+
await callSignOut(ctx);
|
|
1121
|
+
}
|
|
1122
|
+
}),
|
|
1123
|
+
store: internalMutationGeneric({
|
|
1124
|
+
args: storeArgs,
|
|
1125
|
+
handler: async (ctx, args) => {
|
|
1126
|
+
return storeImpl(ctx, args, getProviderOrThrow, config);
|
|
1127
|
+
}
|
|
1128
|
+
})
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
//#endregion
|
|
1133
|
+
export { Auth };
|
|
1134
|
+
//# sourceMappingURL=factory.js.map
|