@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.
Files changed (88) hide show
  1. package/README.md +3 -5
  2. package/dist/bin.js +6488 -1571
  3. package/dist/browser/index.js +10 -7
  4. package/dist/browser/locks.js +3 -5
  5. package/dist/browser/navigation.js +7 -10
  6. package/dist/browser/runtime.js +35 -33
  7. package/dist/client/core/types.js +17 -0
  8. package/dist/client/factors/device.js +26 -19
  9. package/dist/client/index.js +151 -163
  10. package/dist/client/runtime/proxy.js +6 -6
  11. package/dist/client/services/adapters.js +3 -7
  12. package/dist/client/services/http.js +2 -5
  13. package/dist/client/services/resolve.js +5 -11
  14. package/dist/client/services/runtime.js +2 -5
  15. package/dist/component/_generated/component.d.ts +46 -0
  16. package/dist/component/index.d.ts +3 -3
  17. package/dist/component/model.d.ts +25 -25
  18. package/dist/component/public/identity/sessions.js +38 -1
  19. package/dist/component/public/identity/tokens.js +81 -3
  20. package/dist/component/public/identity/verifiers.js +9 -3
  21. package/dist/component/public.js +3 -3
  22. package/dist/component/schema.d.ts +320 -320
  23. package/dist/core/index.d.ts +380 -0
  24. package/dist/core/index.js +83 -0
  25. package/dist/otel.d.ts +13 -17
  26. package/dist/otel.js +39 -49
  27. package/dist/providers/email.d.ts +2 -2
  28. package/dist/providers/password.js +8 -16
  29. package/dist/providers/phone.js +2 -9
  30. package/dist/server/auth-context.d.ts +204 -0
  31. package/dist/server/auth-context.js +76 -0
  32. package/dist/server/auth.d.ts +25 -187
  33. package/dist/server/auth.js +5 -96
  34. package/dist/server/componentContext.d.ts +12 -0
  35. package/dist/server/componentContext.js +1 -0
  36. package/dist/server/config.js +1 -12
  37. package/dist/server/constants.js +6 -0
  38. package/dist/server/contract.d.ts +1 -1
  39. package/dist/server/core.js +5 -14
  40. package/dist/server/crypto.js +26 -18
  41. package/dist/server/db.js +6 -1
  42. package/dist/server/device.js +88 -78
  43. package/dist/server/http.d.ts +4 -3
  44. package/dist/server/http.js +74 -86
  45. package/dist/server/index.d.ts +2 -1
  46. package/dist/server/limits.js +22 -15
  47. package/dist/server/mounts.d.ts +103 -103
  48. package/dist/server/mutations/account.js +6 -4
  49. package/dist/server/mutations/invalidate.js +3 -6
  50. package/dist/server/mutations/oauth.js +86 -88
  51. package/dist/server/mutations/refresh.js +45 -87
  52. package/dist/server/mutations/register.js +19 -19
  53. package/dist/server/mutations/retrieve.js +17 -15
  54. package/dist/server/mutations/signature.js +9 -13
  55. package/dist/server/mutations/signin.js +7 -3
  56. package/dist/server/mutations/signout.js +10 -15
  57. package/dist/server/mutations/store.js +22 -12
  58. package/dist/server/mutations/verifier.js +11 -6
  59. package/dist/server/mutations/verify.js +55 -46
  60. package/dist/server/oauth/runtime.js +27 -25
  61. package/dist/server/passkey.js +299 -250
  62. package/dist/server/prefetch.js +283 -281
  63. package/dist/server/refresh.js +7 -60
  64. package/dist/server/runtime.d.ts +82 -206
  65. package/dist/server/runtime.js +63 -56
  66. package/dist/server/services/config.js +5 -3
  67. package/dist/server/services/logger.js +2 -4
  68. package/dist/server/services/providers.js +2 -4
  69. package/dist/server/services/refresh.js +2 -4
  70. package/dist/server/services/resolve.js +15 -14
  71. package/dist/server/services/signin.js +2 -4
  72. package/dist/server/sessions.js +32 -33
  73. package/dist/server/signin.js +177 -142
  74. package/dist/server/sso/domain.d.ts +20 -68
  75. package/dist/server/sso/domain.js +444 -413
  76. package/dist/server/sso/http.js +53 -59
  77. package/dist/server/sso/oidc.js +94 -80
  78. package/dist/server/tokens.js +13 -3
  79. package/dist/server/totp.js +153 -116
  80. package/dist/server/types.d.ts +2 -2
  81. package/dist/server/users.js +18 -23
  82. package/dist/server/utils/cache.js +51 -0
  83. package/dist/server/utils/dispatch.js +36 -0
  84. package/dist/server/utils/retry.js +24 -0
  85. package/dist/server/utils/span.js +32 -0
  86. package/dist/shared/errors.js +9 -3
  87. package/dist/shared/log.js +20 -22
  88. package/package.json +41 -33
@@ -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 { redirectAbsoluteUrl, setURLSearchParam } from "../redirects.js";
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) => Effect.runPromise(Effect.gen(function* () {
193
- if (runtimeRoute.protocol !== "saml" || runtimeRoute.rest.length !== 1 || runtimeRoute.rest[0] !== "acs") return yield* Effect.fail(convexError("INVALID_PARAMETERS", "Invalid connection runtime path."));
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 } = yield* Effect.tryPromise({
196
- try: () => loadActiveConnectionSamlOrThrow(ctx, connectionId),
197
- catch: (error) => error
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
- catch: (error) => convexError("OAUTH_PROVIDER_ERROR", `SAML response parse failed: ${error instanceof Error ? error.message : String(error)}`)
210
- });
211
- yield* Effect.try({
212
- try: () => enforceGroupConnectionSamlSecurity({
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
- catch: (error) => convexError("OAUTH_PROVIDER_ERROR", error instanceof Error ? error.message : "SAML assertion failed security validation.")
217
- });
218
- yield* Effect.try({
219
- try: () => validateGroupConnectionSamlLoginRelayState({
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
- catch: () => convexError("OAUTH_INVALID_STATE", "SAML RelayState did not match the pending login request.")
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 = yield* Effect.tryPromise({
242
- try: () => callUserOAuth(ctx, {
243
- provider: groupSamlProviderId(connection._id),
244
- providerAccountId: profile.id,
245
- profile,
246
- signature: parsedResponse.relayState.signature,
247
- accountExtend: {
248
- identity: {
249
- protocol: "saml",
250
- connectionId: connection._id,
251
- subject: profile.id,
252
- entityId: typeof saml.entityId === "string" ? saml.entityId : void 0
253
- },
254
- saml: {
255
- attributes: samlAttributes,
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(yield* Effect.tryPromise({
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
- return Match.value(parsedMessage).pipe(Match.when({ hasSamlRequest: true }, (parsedMessage$1) => {
287
- if (!parsedMessage$1.parsedRequest) throw convexError("INVALID_PARAMETERS", "Missing SAML logout payload.");
288
- const responseContext = parsedMessage$1.runtime.sp.createLogoutResponse(parsedMessage$1.runtime.idp, parsedMessage$1.parsedRequest.extract, parsedMessage$1.binding, parsedMessage$1.relayState ?? "");
289
- return parsedMessage$1.binding === "redirect" ? new Response(null, {
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
- }) : createSamlPostBindingResponse({
286
+ });
287
+ return createSamlPostBindingResponse({
293
288
  endpoint: responseContext.entityEndpoint,
294
289
  parameter: "SAMLResponse",
295
290
  value: responseContext.context,
296
- relayState: parsedMessage$1.relayState
291
+ relayState: parsedMessage.relayState
297
292
  });
298
- }), Match.when({ hasSamlResponse: true }, () => new Response(null, { status: 204 })), Match.orElse(() => {
299
- throw convexError("INVALID_PARAMETERS", "Missing SAML logout payload.");
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 Effect.runPromise(handleOAuthCallback(providerId, {
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) {
@@ -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 = Effect.runSync(Cache.make({
13
+ const OIDC_JWKS_CACHE = createCache({
12
14
  capacity: 128,
13
- timeToLive: Duration.hours(1),
14
- lookup: (cacheKey) => Effect.sync(() => {
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 Effect.tryPromise({
53
- try: async () => {
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 Schema.decodeUnknownSync(OidcDiscoverySchema)(await response.json());
57
- },
58
- catch: asError
59
- }).pipe(Effect.retry({ schedule: NETWORK_RETRY_SCHEDULE }), Effect.withSpan("convex-auth.sso.oidc.discovery"));
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 Effect.runSync(Cache.get(OIDC_JWKS_CACHE, cacheKey));
110
+ return OIDC_JWKS_CACHE.get(cacheKey);
96
111
  }
97
- function userInfoProfileFx(opts) {
98
- return Effect.tryPromise({
99
- try: async () => {
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
- return Schema.decodeUnknownSync(OidcUserInfoSchema)(await response.json());
103
- },
104
- catch: (error) => ({
105
- kind: "transport",
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
- return userInfoSubject !== void 0 && tokenSubject !== void 0 && userInfoSubject !== tokenSubject ? Effect.fail({ kind: "subject-mismatch" }) : Effect.succeed({
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
- }), Effect.catch((failure) => Match.value(failure).pipe(Match.when({ kind: "transport" }, () => Effect.succeed(null)), Match.orElse(() => Effect.fail(/* @__PURE__ */ new Error("OIDC userinfo subject does not match ID token subject."))))), Effect.withSpan("convex-auth.sso.oidc.userinfo"));
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 Effect.runPromise(discoverOidcConfiguration(config));
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
- const verified = await Effect.runPromise(Effect.gen(function* () {
222
- if (ctx.nonce === void 0) return yield* Effect.fail(/* @__PURE__ */ new Error("OIDC nonce is required."));
223
- const idToken = tokens.idToken;
224
- if (idToken === void 0) return yield* Effect.fail(/* @__PURE__ */ new Error("OIDC response is missing id_token."));
225
- const verifiedIdToken = idToken;
226
- const tokenAlg = decodeProtectedHeader(verifiedIdToken).alg;
227
- const useSymmetricValidation = typeof tokenAlg === "string" && (tokenAlg === "HS256" || tokenAlg === "HS384" || tokenAlg === "HS512") && supportedIdTokenSigningAlgs.includes(tokenAlg);
228
- const verificationOptions = {
229
- audience: expectedAudience,
230
- requiredClaims: [
231
- "iss",
232
- "sub",
233
- "aud",
234
- "exp",
235
- "iat"
236
- ],
237
- clockTolerance: clockToleranceSeconds
238
- };
239
- const payload = (yield* Effect.tryPromise({
240
- try: () => useSymmetricValidation ? jwtVerify(verifiedIdToken, (() => {
241
- if (typeof client.secret !== "string") throw new Error("OIDC provider uses symmetric ID token signatures but clientSecret is missing.");
242
- return new TextEncoder().encode(client.secret);
243
- })(), verificationOptions) : jwtVerify(verifiedIdToken, jwks, verificationOptions),
244
- catch: asError
245
- })).payload;
246
- const tokenIssuerRaw = typeof payload.iss === "string" ? payload.iss : void 0;
247
- const tokenIssuer = typeof tokenIssuerRaw === "string" ? tokenIssuerRaw.replace(/\/$/, "") : void 0;
248
- if (!tokenIssuer || !expectedIssuers.includes(tokenIssuer)) return yield* Effect.fail(/* @__PURE__ */ new Error(`OIDC token issuer mismatch. Received: ${tokenIssuer ?? "<missing>"}. Expected one of: ${expectedIssuers.join(", ")}`));
249
- if (payload.nonce !== ctx.nonce) return yield* Effect.fail(/* @__PURE__ */ new Error("OIDC nonce mismatch."));
250
- if (Array.isArray(payload.aud) && payload.aud.length > 1 && payload.azp !== String(client.id)) return yield* Effect.fail(/* @__PURE__ */ new Error("OIDC authorized party does not match client ID."));
251
- return payload;
252
- }));
253
- verifiedClaims = verified;
254
- verifiedProfile = normalizeProfile(verified);
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 Effect.runPromise(userInfoProfileFx({
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;
@@ -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 importPKCS8(requireEnv("JWT_PRIVATE_KEY"), "RS256");
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(requireEnv("CONVEX_SITE_URL")).setAudience("convex").setExpirationTime(expirationTime).sign(privateKey);
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