@robelest/convex-auth 0.0.4-preview.27 → 0.0.4-preview.28
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 +3 -5
- package/dist/bin.js +6488 -1571
- package/dist/browser/index.js +10 -7
- package/dist/browser/locks.js +3 -5
- package/dist/browser/navigation.js +7 -10
- package/dist/browser/runtime.js +35 -33
- package/dist/client/core/types.js +17 -0
- package/dist/client/factors/device.js +26 -19
- package/dist/client/index.js +151 -163
- package/dist/client/runtime/proxy.js +6 -6
- package/dist/client/services/adapters.js +3 -7
- package/dist/client/services/http.js +2 -5
- package/dist/client/services/resolve.js +5 -11
- package/dist/client/services/runtime.js +2 -5
- package/dist/component/_generated/component.d.ts +46 -0
- package/dist/component/index.d.ts +3 -3
- package/dist/component/model.d.ts +25 -25
- package/dist/component/public/identity/sessions.js +38 -1
- package/dist/component/public/identity/tokens.js +81 -3
- package/dist/component/public/identity/verifiers.js +9 -3
- package/dist/component/public.js +3 -3
- package/dist/component/schema.d.ts +320 -320
- package/dist/core/index.d.ts +380 -0
- package/dist/core/index.js +83 -0
- package/dist/otel.d.ts +13 -17
- package/dist/otel.js +39 -49
- package/dist/providers/email.d.ts +2 -2
- package/dist/providers/password.js +8 -16
- package/dist/providers/phone.js +2 -9
- package/dist/server/auth-context.d.ts +204 -0
- package/dist/server/auth-context.js +76 -0
- package/dist/server/auth.d.ts +25 -187
- package/dist/server/auth.js +5 -96
- package/dist/server/componentContext.d.ts +12 -0
- package/dist/server/componentContext.js +1 -0
- package/dist/server/config.js +1 -12
- package/dist/server/constants.js +6 -0
- package/dist/server/contract.d.ts +1 -1
- package/dist/server/core.js +5 -14
- package/dist/server/crypto.js +26 -18
- package/dist/server/db.js +6 -1
- package/dist/server/device.js +88 -78
- package/dist/server/http.d.ts +4 -3
- package/dist/server/http.js +74 -86
- package/dist/server/index.d.ts +2 -1
- package/dist/server/limits.js +22 -15
- package/dist/server/mounts.d.ts +103 -103
- package/dist/server/mutations/account.js +6 -4
- package/dist/server/mutations/invalidate.js +3 -6
- package/dist/server/mutations/oauth.js +86 -88
- package/dist/server/mutations/refresh.js +45 -87
- package/dist/server/mutations/register.js +19 -19
- package/dist/server/mutations/retrieve.js +17 -15
- package/dist/server/mutations/signature.js +9 -13
- package/dist/server/mutations/signin.js +7 -3
- package/dist/server/mutations/signout.js +10 -15
- package/dist/server/mutations/store.js +22 -12
- package/dist/server/mutations/verifier.js +11 -6
- package/dist/server/mutations/verify.js +55 -46
- package/dist/server/oauth/runtime.js +27 -25
- package/dist/server/passkey.js +299 -250
- package/dist/server/prefetch.js +283 -281
- package/dist/server/refresh.js +7 -60
- package/dist/server/runtime.d.ts +82 -206
- package/dist/server/runtime.js +63 -56
- package/dist/server/services/config.js +5 -3
- package/dist/server/services/logger.js +2 -4
- package/dist/server/services/providers.js +2 -4
- package/dist/server/services/refresh.js +2 -4
- package/dist/server/services/resolve.js +15 -14
- package/dist/server/services/signin.js +2 -4
- package/dist/server/sessions.js +32 -33
- package/dist/server/signin.js +177 -142
- package/dist/server/sso/domain.d.ts +20 -68
- package/dist/server/sso/domain.js +444 -413
- package/dist/server/sso/http.js +53 -59
- package/dist/server/sso/oidc.js +94 -80
- package/dist/server/tokens.js +13 -3
- package/dist/server/totp.js +153 -116
- package/dist/server/types.d.ts +2 -2
- package/dist/server/users.js +18 -23
- package/dist/server/utils/cache.js +51 -0
- package/dist/server/utils/dispatch.js +36 -0
- package/dist/server/utils/retry.js +24 -0
- package/dist/server/utils/span.js +32 -0
- package/dist/shared/errors.js +9 -3
- package/dist/shared/log.js +20 -22
- package/package.json +41 -33
package/dist/server/sso/http.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { redirectToParamCookie, useRedirectToParam } from "../cookies.js";
|
|
2
1
|
import { deleteScimIdentity, getScimIdentity, getScimIdentityByConnectionAndUser, getScimIdentityByMappedGroup, insertAccount, insertUser, listScimIdentitiesByConnection, patchUser, upsertScimIdentity } from "../contract.js";
|
|
3
2
|
import { finalizeNormalizedProfile, normalizeStringArray } from "./profile.js";
|
|
4
3
|
import { SCIM_GROUP_SCHEMA_ID, SCIM_USER_SCHEMA_ID, decodeGroupOidcState, encodeGroupOidcState, groupOidcProviderId, groupSamlProviderId } from "./shared.js";
|
|
5
4
|
import { createGroupConnectionOidcRuntime } from "./oidc.js";
|
|
6
5
|
import { resolveProvisionedRoleIds } from "./policy.js";
|
|
7
|
-
import {
|
|
6
|
+
import { redirectToParamCookie, useRedirectToParam } from "../cookies.js";
|
|
8
7
|
import { addSSORoutes, convertErrorsToResponse, getCookies } from "../http.js";
|
|
9
8
|
import { createOAuthAuthorizationURL, handleOAuthCallback } from "../oauth/runtime.js";
|
|
9
|
+
import { redirectAbsoluteUrl, setURLSearchParam } from "../redirects.js";
|
|
10
10
|
import { parseScimListRequest, scimError, scimJson, serializeScimGroup, serializeScimUser } from "./scim.js";
|
|
11
11
|
import { createGroupConnectionSamlMetadataXml, createGroupConnectionSamlSignInRequest, createSamlPostBindingResponse, encodeGroupSamlRelayState, enforceGroupConnectionSamlSecurity, parseGroupConnectionSamlLoginResponse, parseGroupConnectionSamlLogoutMessage, profileFromSamlExtract, validateGroupConnectionSamlLoginRelayState } from "./saml.js";
|
|
12
12
|
import { ConvexError } from "convex/values";
|
|
13
|
-
import { Effect, Match } from "effect";
|
|
14
13
|
import { serialize } from "cookie";
|
|
15
14
|
|
|
16
15
|
//#region src/server/sso/http.ts
|
|
@@ -189,15 +188,13 @@ function addGroupHttpRuntime(deps) {
|
|
|
189
188
|
return null;
|
|
190
189
|
};
|
|
191
190
|
const readScimJson = async (request) => await request.json();
|
|
192
|
-
const handleSamlAcs = async (ctx, request, runtimeRoute) =>
|
|
193
|
-
if (runtimeRoute.protocol !== "saml" || runtimeRoute.rest.length !== 1 || runtimeRoute.rest[0] !== "acs")
|
|
191
|
+
const handleSamlAcs = async (ctx, request, runtimeRoute) => {
|
|
192
|
+
if (runtimeRoute.protocol !== "saml" || runtimeRoute.rest.length !== 1 || runtimeRoute.rest[0] !== "acs") throw convexError("INVALID_PARAMETERS", "Invalid connection runtime path.");
|
|
194
193
|
const connectionId = runtimeRoute.connectionId;
|
|
195
|
-
const { loaded, connection, saml } =
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const parsedResponse = yield* Effect.tryPromise({
|
|
200
|
-
try: () => parseGroupConnectionSamlLoginResponse({
|
|
194
|
+
const { loaded, connection, saml } = await loadActiveConnectionSamlOrThrow(ctx, connectionId);
|
|
195
|
+
let parsedResponse;
|
|
196
|
+
try {
|
|
197
|
+
parsedResponse = await parseGroupConnectionSamlLoginResponse({
|
|
201
198
|
request,
|
|
202
199
|
rootUrl: requireEnv("CONVEX_SITE_URL"),
|
|
203
200
|
source: {
|
|
@@ -205,27 +202,30 @@ function addGroupHttpRuntime(deps) {
|
|
|
205
202
|
id: connection._id
|
|
206
203
|
},
|
|
207
204
|
config: loaded.config
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
205
|
+
});
|
|
206
|
+
} catch (error) {
|
|
207
|
+
throw convexError("OAUTH_PROVIDER_ERROR", `SAML response parse failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
enforceGroupConnectionSamlSecurity({
|
|
213
211
|
extract: parsedResponse.parsed.extract,
|
|
214
212
|
config: loaded.config
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
throw convexError("OAUTH_PROVIDER_ERROR", error instanceof Error ? error.message : "SAML assertion failed security validation.");
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
validateGroupConnectionSamlLoginRelayState({
|
|
220
219
|
relayState: parsedResponse.relayState,
|
|
221
220
|
source: {
|
|
222
221
|
kind: "connection",
|
|
223
222
|
id: connection._id
|
|
224
223
|
},
|
|
225
224
|
inResponseTo: parsedResponse.parsed.extract?.response?.inResponseTo
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
|
|
225
|
+
});
|
|
226
|
+
} catch {
|
|
227
|
+
throw convexError("OAUTH_INVALID_STATE", "SAML RelayState did not match the pending login request.");
|
|
228
|
+
}
|
|
229
229
|
const { samlAttributes, samlSessionIndex, ...userProfile } = profileFromSamlExtract(parsedResponse.parsed.extract, saml.profile?.mapping ?? {});
|
|
230
230
|
const profile = userProfile;
|
|
231
231
|
const extraFields = typeof saml.profile === "object" && saml.profile !== null ? saml.profile.extraFields : void 0;
|
|
@@ -238,31 +238,25 @@ function addGroupHttpRuntime(deps) {
|
|
|
238
238
|
if (Object.keys(extend).length > 0) profile.extend = extend;
|
|
239
239
|
}
|
|
240
240
|
const maybeRedirectTo = useRedirectToParam(groupSamlProviderId(connection._id), getCookies(request));
|
|
241
|
-
const verificationCode =
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
sessionIndex: samlSessionIndex
|
|
257
|
-
}
|
|
241
|
+
const verificationCode = await callUserOAuth(ctx, {
|
|
242
|
+
provider: groupSamlProviderId(connection._id),
|
|
243
|
+
providerAccountId: profile.id,
|
|
244
|
+
profile,
|
|
245
|
+
signature: parsedResponse.relayState.signature,
|
|
246
|
+
accountExtend: {
|
|
247
|
+
identity: {
|
|
248
|
+
protocol: "saml",
|
|
249
|
+
connectionId: connection._id,
|
|
250
|
+
subject: profile.id,
|
|
251
|
+
entityId: typeof saml.entityId === "string" ? saml.entityId : void 0
|
|
252
|
+
},
|
|
253
|
+
saml: {
|
|
254
|
+
attributes: samlAttributes,
|
|
255
|
+
sessionIndex: samlSessionIndex
|
|
258
256
|
}
|
|
259
|
-
}
|
|
260
|
-
catch: (error) => error
|
|
257
|
+
}
|
|
261
258
|
});
|
|
262
|
-
const vurl = setURLSearchParam(
|
|
263
|
-
try: () => redirectAbsoluteUrl(config, { redirectTo: maybeRedirectTo?.redirectTo ?? (typeof parsedResponse.relayState.redirectTo === "string" ? parsedResponse.relayState.redirectTo : void 0) }),
|
|
264
|
-
catch: (error) => error
|
|
265
|
-
}), "code", verificationCode);
|
|
259
|
+
const vurl = setURLSearchParam(await redirectAbsoluteUrl(config, { redirectTo: maybeRedirectTo?.redirectTo ?? (typeof parsedResponse.relayState.redirectTo === "string" ? parsedResponse.relayState.redirectTo : void 0) }), "code", verificationCode);
|
|
266
260
|
const vheaders = new Headers({ Location: vurl });
|
|
267
261
|
vheaders.set("Cache-Control", "must-revalidate");
|
|
268
262
|
for (const { name, value, options } of maybeRedirectTo !== null ? [maybeRedirectTo.updatedCookie] : []) vheaders.append("Set-Cookie", serialize(name, value, options));
|
|
@@ -270,7 +264,7 @@ function addGroupHttpRuntime(deps) {
|
|
|
270
264
|
status: 302,
|
|
271
265
|
headers: vheaders
|
|
272
266
|
});
|
|
273
|
-
}
|
|
267
|
+
};
|
|
274
268
|
const handleSamlSlo = async (ctx, request, runtimeRoute) => {
|
|
275
269
|
if (runtimeRoute.protocol !== "saml" || runtimeRoute.rest.length !== 1 || runtimeRoute.rest[0] !== "slo") throw convexError("INVALID_PARAMETERS", "Invalid connection runtime path.");
|
|
276
270
|
const { loaded, connection } = await loadActiveConnectionSamlOrThrow(ctx, runtimeRoute.connectionId);
|
|
@@ -283,21 +277,21 @@ function addGroupHttpRuntime(deps) {
|
|
|
283
277
|
},
|
|
284
278
|
config: loaded.config
|
|
285
279
|
});
|
|
286
|
-
|
|
287
|
-
if (!parsedMessage
|
|
288
|
-
const responseContext = parsedMessage
|
|
289
|
-
|
|
280
|
+
if (parsedMessage.hasSamlRequest) {
|
|
281
|
+
if (!parsedMessage.parsedRequest) throw convexError("INVALID_PARAMETERS", "Missing SAML logout payload.");
|
|
282
|
+
const responseContext = parsedMessage.runtime.sp.createLogoutResponse(parsedMessage.runtime.idp, parsedMessage.parsedRequest.extract, parsedMessage.binding, parsedMessage.relayState ?? "");
|
|
283
|
+
if (parsedMessage.binding === "redirect") return new Response(null, {
|
|
290
284
|
status: 302,
|
|
291
285
|
headers: { Location: responseContext.context }
|
|
292
|
-
})
|
|
286
|
+
});
|
|
287
|
+
return createSamlPostBindingResponse({
|
|
293
288
|
endpoint: responseContext.entityEndpoint,
|
|
294
289
|
parameter: "SAMLResponse",
|
|
295
290
|
value: responseContext.context,
|
|
296
|
-
relayState: parsedMessage
|
|
291
|
+
relayState: parsedMessage.relayState
|
|
297
292
|
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
}));
|
|
293
|
+
} else if (parsedMessage.hasSamlResponse) return new Response(null, { status: 204 });
|
|
294
|
+
else throw convexError("INVALID_PARAMETERS", "Missing SAML logout payload.");
|
|
301
295
|
};
|
|
302
296
|
const handleScimRequest = async (ctx, request) => {
|
|
303
297
|
try {
|
|
@@ -907,10 +901,10 @@ function addGroupHttpRuntime(deps) {
|
|
|
907
901
|
const cookies = getCookies(request);
|
|
908
902
|
const maybeRedirectTo = useRedirectToParam(providerId, cookies);
|
|
909
903
|
const destinationUrl = await redirectAbsoluteUrl(config, { redirectTo: maybeRedirectTo?.redirectTo });
|
|
910
|
-
const result = await
|
|
904
|
+
const result = await handleOAuthCallback(providerId, {
|
|
911
905
|
...oauthConfig,
|
|
912
906
|
provider
|
|
913
|
-
}, Object.fromEntries(url.searchParams.entries()), cookies)
|
|
907
|
+
}, Object.fromEntries(url.searchParams.entries()), cookies);
|
|
914
908
|
const extraFields = typeof oidc.profile === "object" && oidc.profile !== null ? oidc.profile.extraFields : void 0;
|
|
915
909
|
let profile = result.profile;
|
|
916
910
|
if (extraFields && typeof profile === "object" && profile) {
|
package/dist/server/sso/oidc.js
CHANGED
|
@@ -1,39 +1,52 @@
|
|
|
1
1
|
import { log } from "../log.js";
|
|
2
|
+
import { createCache } from "../utils/cache.js";
|
|
3
|
+
import { retryWithBackoff } from "../utils/retry.js";
|
|
4
|
+
import { withSpan } from "../utils/span.js";
|
|
2
5
|
import { finalizeNormalizedProfile, normalizeStringArray } from "./profile.js";
|
|
3
6
|
import { getGroupOidcUrls, groupOidcProviderId } from "./shared.js";
|
|
4
|
-
import { Cache, Duration, Effect, Match, Schedule, Schema } from "effect";
|
|
5
7
|
import { sha256 } from "@oslojs/crypto/sha2";
|
|
6
8
|
import { encodeBase64urlNoPadding } from "@oslojs/encoding";
|
|
7
9
|
import { createRemoteJWKSet, customFetch, decodeProtectedHeader, jwtVerify } from "jose";
|
|
8
10
|
import { decodeIdToken } from "arctic";
|
|
9
11
|
|
|
10
12
|
//#region src/server/sso/oidc.ts
|
|
11
|
-
const OIDC_JWKS_CACHE =
|
|
13
|
+
const OIDC_JWKS_CACHE = createCache({
|
|
12
14
|
capacity: 128,
|
|
13
|
-
|
|
14
|
-
lookup: (cacheKey) =>
|
|
15
|
+
timeToLiveMs: 3600 * 1e3,
|
|
16
|
+
lookup: (cacheKey) => {
|
|
15
17
|
const key = JSON.parse(cacheKey);
|
|
16
18
|
const fetchImpl = key.runtimeOrigin !== void 0 || key.externalHost !== void 0 ? createGroupConnectionOidcFetchFromParts(key.runtimeOrigin, key.externalHost) : void 0;
|
|
17
19
|
return fetchImpl ? createRemoteJWKSet(new URL(key.url), { [customFetch]: fetchImpl }) : createRemoteJWKSet(new URL(key.url));
|
|
18
|
-
}
|
|
19
|
-
}));
|
|
20
|
-
const NETWORK_RETRY_SCHEDULE = Schedule.both(Schedule.jittered(Schedule.exponential("200 millis")), Schedule.recurs(2));
|
|
21
|
-
const OidcDiscoverySchema = Schema.Struct({
|
|
22
|
-
issuer: Schema.String,
|
|
23
|
-
authorization_endpoint: Schema.String,
|
|
24
|
-
token_endpoint: Schema.String,
|
|
25
|
-
jwks_uri: Schema.String,
|
|
26
|
-
userinfo_endpoint: Schema.optional(Schema.String),
|
|
27
|
-
token_endpoint_auth_methods_supported: Schema.optional(Schema.Array(Schema.String)),
|
|
28
|
-
id_token_signing_alg_values_supported: Schema.optional(Schema.Array(Schema.String))
|
|
29
|
-
});
|
|
30
|
-
const OidcUserInfoSchema = Schema.Struct({
|
|
31
|
-
sub: Schema.optional(Schema.String),
|
|
32
|
-
email: Schema.optional(Schema.String),
|
|
33
|
-
email_verified: Schema.optional(Schema.Boolean),
|
|
34
|
-
name: Schema.optional(Schema.String),
|
|
35
|
-
picture: Schema.optional(Schema.String)
|
|
20
|
+
}
|
|
36
21
|
});
|
|
22
|
+
function validateOidcDiscovery(data) {
|
|
23
|
+
if (typeof data !== "object" || data === null) throw new Error("OIDC discovery response is not an object.");
|
|
24
|
+
const obj = data;
|
|
25
|
+
if (typeof obj.issuer !== "string") throw new Error("OIDC discovery is missing 'issuer'.");
|
|
26
|
+
if (typeof obj.authorization_endpoint !== "string") throw new Error("OIDC discovery is missing 'authorization_endpoint'.");
|
|
27
|
+
if (typeof obj.token_endpoint !== "string") throw new Error("OIDC discovery is missing 'token_endpoint'.");
|
|
28
|
+
if (typeof obj.jwks_uri !== "string") throw new Error("OIDC discovery is missing 'jwks_uri'.");
|
|
29
|
+
return {
|
|
30
|
+
issuer: obj.issuer,
|
|
31
|
+
authorization_endpoint: obj.authorization_endpoint,
|
|
32
|
+
token_endpoint: obj.token_endpoint,
|
|
33
|
+
jwks_uri: obj.jwks_uri,
|
|
34
|
+
userinfo_endpoint: typeof obj.userinfo_endpoint === "string" ? obj.userinfo_endpoint : void 0,
|
|
35
|
+
token_endpoint_auth_methods_supported: Array.isArray(obj.token_endpoint_auth_methods_supported) ? obj.token_endpoint_auth_methods_supported.filter((v) => typeof v === "string") : void 0,
|
|
36
|
+
id_token_signing_alg_values_supported: Array.isArray(obj.id_token_signing_alg_values_supported) ? obj.id_token_signing_alg_values_supported.filter((v) => typeof v === "string") : void 0
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function validateOidcUserInfo(data) {
|
|
40
|
+
if (typeof data !== "object" || data === null) return {};
|
|
41
|
+
const obj = data;
|
|
42
|
+
return {
|
|
43
|
+
sub: typeof obj.sub === "string" ? obj.sub : void 0,
|
|
44
|
+
email: typeof obj.email === "string" ? obj.email : void 0,
|
|
45
|
+
email_verified: typeof obj.email_verified === "boolean" ? obj.email_verified : void 0,
|
|
46
|
+
name: typeof obj.name === "string" ? obj.name : void 0,
|
|
47
|
+
picture: typeof obj.picture === "string" ? obj.picture : void 0
|
|
48
|
+
};
|
|
49
|
+
}
|
|
37
50
|
const asError = (error) => error instanceof Error ? error : new Error(String(error));
|
|
38
51
|
function getOidcSections(config) {
|
|
39
52
|
return {
|
|
@@ -44,19 +57,21 @@ function getOidcSections(config) {
|
|
|
44
57
|
profile: typeof config.profile === "object" && config.profile !== null ? config.profile : {}
|
|
45
58
|
};
|
|
46
59
|
}
|
|
47
|
-
function discoverOidcConfiguration(config) {
|
|
60
|
+
async function discoverOidcConfiguration(config) {
|
|
48
61
|
const { discovery } = getOidcSections(config);
|
|
49
62
|
const discoveryUrl = typeof discovery.discoveryUrl === "string" ? discovery.discoveryUrl : typeof discovery.issuer === "string" ? `${discovery.issuer.replace(/\/$/, "")}/.well-known/openid-configuration` : null;
|
|
50
63
|
if (!discoveryUrl) throw new Error("Group connection OIDC requires an issuer or discoveryUrl.");
|
|
51
64
|
const oidcFetch = createGroupConnectionOidcFetch(config, typeof discovery.issuer === "string" ? discovery.issuer : void 0);
|
|
52
|
-
return
|
|
53
|
-
|
|
65
|
+
return withSpan("convex-auth.sso.oidc.discovery", {}, async () => {
|
|
66
|
+
return retryWithBackoff(async () => {
|
|
54
67
|
const response = await oidcFetch(discoveryUrl, { signal: AbortSignal.timeout(1e4) });
|
|
55
68
|
if (!response.ok) throw new Error(`Failed to discover OIDC configuration: ${response.status}`);
|
|
56
|
-
return
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
return validateOidcDiscovery(await response.json());
|
|
70
|
+
}, {
|
|
71
|
+
maxRetries: 2,
|
|
72
|
+
baseMs: 200
|
|
73
|
+
});
|
|
74
|
+
});
|
|
60
75
|
}
|
|
61
76
|
function createGroupConnectionOidcFetch(config, discoveredIssuer) {
|
|
62
77
|
const { discovery } = getOidcSections(config);
|
|
@@ -92,35 +107,34 @@ function getOidcJwks(url, runtimeOrigin, externalHost, fetchImpl) {
|
|
|
92
107
|
runtimeOrigin: fetchImpl ? runtimeOrigin : void 0,
|
|
93
108
|
externalHost: fetchImpl ? externalHost : void 0
|
|
94
109
|
});
|
|
95
|
-
return
|
|
110
|
+
return OIDC_JWKS_CACHE.get(cacheKey);
|
|
96
111
|
}
|
|
97
|
-
function userInfoProfileFx(opts) {
|
|
98
|
-
return
|
|
99
|
-
|
|
112
|
+
async function userInfoProfileFx(opts) {
|
|
113
|
+
return withSpan("convex-auth.sso.oidc.userinfo", {}, async () => {
|
|
114
|
+
let userInfo;
|
|
115
|
+
try {
|
|
100
116
|
const response = await (opts.fetchImpl ?? fetch)(opts.endpoint, { headers: { Authorization: `Bearer ${opts.accessToken}` } });
|
|
101
117
|
if (!response.ok) throw new Error(`OIDC userinfo request failed: ${response.status}`);
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
error
|
|
107
|
-
})
|
|
108
|
-
}).pipe(Effect.flatMap((userInfo) => {
|
|
118
|
+
userInfo = validateOidcUserInfo(await response.json());
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
109
122
|
const userInfoSubject = typeof userInfo.sub === "string" ? userInfo.sub : void 0;
|
|
110
123
|
const tokenSubject = typeof opts.verifiedClaims.sub === "string" ? opts.verifiedClaims.sub : void 0;
|
|
111
|
-
|
|
124
|
+
if (userInfoSubject !== void 0 && tokenSubject !== void 0 && userInfoSubject !== tokenSubject) throw new Error("OIDC userinfo subject does not match ID token subject.");
|
|
125
|
+
return {
|
|
112
126
|
id: userInfoSubject ?? (typeof opts.verifiedClaims.sub === "string" ? opts.verifiedClaims.sub : void 0) ?? crypto.randomUUID(),
|
|
113
127
|
email: typeof userInfo.email === "string" ? userInfo.email : opts.verifiedProfile.email,
|
|
114
128
|
emailVerified: typeof userInfo.email_verified === "boolean" ? userInfo.email_verified : opts.verifiedProfile.emailVerified,
|
|
115
129
|
name: typeof userInfo.name === "string" ? userInfo.name : opts.verifiedProfile.name,
|
|
116
130
|
image: typeof userInfo.picture === "string" ? userInfo.picture : opts.verifiedProfile.image
|
|
117
|
-
}
|
|
118
|
-
})
|
|
131
|
+
};
|
|
132
|
+
});
|
|
119
133
|
}
|
|
120
134
|
/** @internal */
|
|
121
135
|
async function createGroupConnectionOidcProvider(config, redirectUri) {
|
|
122
136
|
const { discovery: discoveryConfig, client, request, security, profile } = getOidcSections(config);
|
|
123
|
-
const discovery = await
|
|
137
|
+
const discovery = await discoverOidcConfiguration(config);
|
|
124
138
|
const discoveredIssuer = typeof discovery.issuer === "string" ? discovery.issuer.replace(/\/$/, "") : "";
|
|
125
139
|
const expectedIssuer = typeof discoveryConfig.issuer === "string" ? discoveryConfig.issuer.replace(/\/$/, "") : discoveredIssuer;
|
|
126
140
|
const strictIssuer = security.strictIssuer === true;
|
|
@@ -218,40 +232,40 @@ async function createGroupConnectionOidcProvider(config, redirectUri) {
|
|
|
218
232
|
scopes,
|
|
219
233
|
nonce: true,
|
|
220
234
|
validateTokens: async (tokens, ctx) => {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
verifiedClaims =
|
|
254
|
-
verifiedProfile = normalizeProfile(
|
|
235
|
+
if (ctx.nonce === void 0) throw new Error("OIDC nonce is required.");
|
|
236
|
+
const idToken = tokens.idToken;
|
|
237
|
+
if (idToken === void 0) throw new Error("OIDC response is missing id_token.");
|
|
238
|
+
const verifiedIdToken = idToken;
|
|
239
|
+
const tokenAlg = decodeProtectedHeader(verifiedIdToken).alg;
|
|
240
|
+
const useSymmetricValidation = typeof tokenAlg === "string" && (tokenAlg === "HS256" || tokenAlg === "HS384" || tokenAlg === "HS512") && supportedIdTokenSigningAlgs.includes(tokenAlg);
|
|
241
|
+
const verificationOptions = {
|
|
242
|
+
audience: expectedAudience,
|
|
243
|
+
requiredClaims: [
|
|
244
|
+
"iss",
|
|
245
|
+
"sub",
|
|
246
|
+
"aud",
|
|
247
|
+
"exp",
|
|
248
|
+
"iat"
|
|
249
|
+
],
|
|
250
|
+
clockTolerance: clockToleranceSeconds
|
|
251
|
+
};
|
|
252
|
+
let verification;
|
|
253
|
+
try {
|
|
254
|
+
verification = await (useSymmetricValidation ? jwtVerify(verifiedIdToken, (() => {
|
|
255
|
+
if (typeof client.secret !== "string") throw new Error("OIDC provider uses symmetric ID token signatures but clientSecret is missing.");
|
|
256
|
+
return new TextEncoder().encode(client.secret);
|
|
257
|
+
})(), verificationOptions) : jwtVerify(verifiedIdToken, jwks, verificationOptions));
|
|
258
|
+
} catch (error) {
|
|
259
|
+
throw asError(error);
|
|
260
|
+
}
|
|
261
|
+
const payload = verification.payload;
|
|
262
|
+
const tokenIssuerRaw = typeof payload.iss === "string" ? payload.iss : void 0;
|
|
263
|
+
const tokenIssuer = typeof tokenIssuerRaw === "string" ? tokenIssuerRaw.replace(/\/$/, "") : void 0;
|
|
264
|
+
if (!tokenIssuer || !expectedIssuers.includes(tokenIssuer)) throw new Error(`OIDC token issuer mismatch. Received: ${tokenIssuer ?? "<missing>"}. Expected one of: ${expectedIssuers.join(", ")}`);
|
|
265
|
+
if (payload.nonce !== ctx.nonce) throw new Error("OIDC nonce mismatch.");
|
|
266
|
+
if (Array.isArray(payload.aud) && payload.aud.length > 1 && payload.azp !== String(client.id)) throw new Error("OIDC authorized party does not match client ID.");
|
|
267
|
+
verifiedClaims = payload;
|
|
268
|
+
verifiedProfile = normalizeProfile(payload);
|
|
255
269
|
},
|
|
256
270
|
accountLinking: config.accountLinking,
|
|
257
271
|
profile: async (tokens) => {
|
|
@@ -262,13 +276,13 @@ async function createGroupConnectionOidcProvider(config, redirectUri) {
|
|
|
262
276
|
verifiedProfile = normalizeProfile(claims);
|
|
263
277
|
}
|
|
264
278
|
if (userinfoEndpoint && typeof tokens.accessToken === "string") {
|
|
265
|
-
const userInfoProfile = await
|
|
279
|
+
const userInfoProfile = await userInfoProfileFx({
|
|
266
280
|
endpoint: userinfoEndpoint,
|
|
267
281
|
accessToken: tokens.accessToken,
|
|
268
282
|
verifiedClaims,
|
|
269
283
|
verifiedProfile,
|
|
270
284
|
fetchImpl: oidcFetch
|
|
271
|
-
})
|
|
285
|
+
});
|
|
272
286
|
if (userInfoProfile !== null) return userInfoProfile;
|
|
273
287
|
}
|
|
274
288
|
return verifiedProfile;
|
package/dist/server/tokens.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { requireEnv } from "./env.js";
|
|
2
1
|
import { generateRandomString } from "./random.js";
|
|
2
|
+
import { requireEnv } from "./env.js";
|
|
3
3
|
import { SignJWT, importPKCS8 } from "jose";
|
|
4
4
|
|
|
5
5
|
//#region src/server/tokens.ts
|
|
@@ -7,11 +7,21 @@ const TOKEN_SUB_CLAIM_DIVIDER = "|";
|
|
|
7
7
|
const DEFAULT_JWT_DURATION_MS = 1e3 * 60 * 60;
|
|
8
8
|
const TOKEN_JTI_LENGTH = 24;
|
|
9
9
|
const TOKEN_JTI_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
10
|
+
let cachedPrivateKeyPromise = null;
|
|
11
|
+
let cachedIssuer = null;
|
|
12
|
+
const getPrivateKey = () => {
|
|
13
|
+
if (cachedPrivateKeyPromise === null) cachedPrivateKeyPromise = importPKCS8(requireEnv("JWT_PRIVATE_KEY"), "RS256");
|
|
14
|
+
return cachedPrivateKeyPromise;
|
|
15
|
+
};
|
|
16
|
+
const getIssuer = () => {
|
|
17
|
+
if (cachedIssuer === null) cachedIssuer = requireEnv("CONVEX_SITE_URL");
|
|
18
|
+
return cachedIssuer;
|
|
19
|
+
};
|
|
10
20
|
/** @internal */
|
|
11
21
|
async function generateToken(args, config) {
|
|
12
|
-
const privateKey = await
|
|
22
|
+
const privateKey = await getPrivateKey();
|
|
13
23
|
const expirationTime = new Date(Date.now() + (config.jwt?.durationMs ?? DEFAULT_JWT_DURATION_MS));
|
|
14
|
-
return await new SignJWT({ sub: args.userId + TOKEN_SUB_CLAIM_DIVIDER + args.sessionId }).setProtectedHeader({ alg: "RS256" }).setIssuedAt().setJti(generateRandomString(TOKEN_JTI_LENGTH, TOKEN_JTI_ALPHABET)).setIssuer(
|
|
24
|
+
return await new SignJWT({ sub: args.userId + TOKEN_SUB_CLAIM_DIVIDER + args.sessionId }).setProtectedHeader({ alg: "RS256" }).setIssuedAt().setJti(generateRandomString(TOKEN_JTI_LENGTH, TOKEN_JTI_ALPHABET)).setIssuer(getIssuer()).setAudience("convex").setExpirationTime(expirationTime).sign(privateKey);
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
//#endregion
|