@caffeinebounce/identity 0.12.1 → 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.mjs CHANGED
@@ -1,6 +1,43 @@
1
1
  import { NextResponse } from 'next/server';
2
2
  export { generateSecureToken, getClientIP, getGeolocationFromIP, hashString } from '@caffeinebounce/shared-utils';
3
3
 
4
+ // src/handlers/callback.ts
5
+
6
+ // src/utils/redirect.ts
7
+ var INTERNAL_REDIRECT_BASE = new URL("https://identity.invalid");
8
+ var ENCODED_PATH_SEPARATOR = /%(?:2f|5c)/i;
9
+ function hasControlCharacter(value) {
10
+ return Array.from(value).some((character) => {
11
+ const code = character.charCodeAt(0);
12
+ return code <= 31 || code === 127;
13
+ });
14
+ }
15
+ function isStrictInternalRedirect(candidate) {
16
+ if (!candidate?.startsWith("/") || hasControlCharacter(candidate)) {
17
+ return false;
18
+ }
19
+ const pathEnd = candidate.search(/[?#]/);
20
+ const pathname = pathEnd === -1 ? candidate : candidate.slice(0, pathEnd);
21
+ if (pathname.includes("\\") || ENCODED_PATH_SEPARATOR.test(pathname)) {
22
+ return false;
23
+ }
24
+ try {
25
+ const target = new URL(candidate, INTERNAL_REDIRECT_BASE);
26
+ return target.origin === INTERNAL_REDIRECT_BASE.origin;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+ function getSafeInternalRedirect(candidate, fallback) {
32
+ if (isStrictInternalRedirect(candidate)) {
33
+ return candidate;
34
+ }
35
+ if (fallback === null) {
36
+ return null;
37
+ }
38
+ return isStrictInternalRedirect(fallback) ? fallback : "/";
39
+ }
40
+
4
41
  // src/handlers/callback.ts
5
42
  var POST_AUTH_HOOK_ERROR_MESSAGE = "Authentication completed, but setup failed. Please try again.";
6
43
  var DEFAULT_LINKING_ACCOUNT_ERROR_MESSAGE = "This account is already connected to another account. Each external account can only be linked to one account.";
@@ -12,12 +49,6 @@ var EMAIL_OTP_TYPES = [
12
49
  "email_change",
13
50
  "email"
14
51
  ];
15
- function getSafeRedirectPath(candidate, fallback) {
16
- if (candidate?.startsWith("/") && !candidate.startsWith("//")) {
17
- return candidate;
18
- }
19
- return fallback;
20
- }
21
52
  function getEmailOtpType(value) {
22
53
  if (!value) {
23
54
  return null;
@@ -108,8 +139,9 @@ function createRedirectResponse(target, origin) {
108
139
  if (target instanceof URL) {
109
140
  return NextResponse.redirect(target.toString());
110
141
  }
111
- if (target.startsWith("/") && !target.startsWith("//")) {
112
- return NextResponse.redirect(`${origin}${target}`);
142
+ const safeTarget = getSafeInternalRedirect(target, null);
143
+ if (safeTarget) {
144
+ return NextResponse.redirect(`${origin}${safeTarget}`);
113
145
  }
114
146
  return null;
115
147
  }
@@ -171,15 +203,20 @@ function createAuthCallbackHandler({
171
203
  isLinkingFlow = isDefaultLinkingFlow,
172
204
  resolveLinkingErrorMessage
173
205
  }) {
206
+ const safeDefaultRedirect = getSafeInternalRedirect(
207
+ defaultRedirect,
208
+ "/dashboard"
209
+ );
210
+ const safeSignInPath = getSafeInternalRedirect(signInPath, "/signin");
174
211
  return async function GET(request) {
175
212
  const requestUrl = new URL(request.url);
176
213
  const code = requestUrl.searchParams.get("code");
177
214
  const tokenHash = requestUrl.searchParams.get("token_hash");
178
215
  const rawOtpType = requestUrl.searchParams.get("type");
179
216
  const otpType = getEmailOtpType(rawOtpType);
180
- const next = getSafeRedirectPath(
217
+ const next = getSafeInternalRedirect(
181
218
  requestUrl.searchParams.get("next") ?? requestUrl.searchParams.get("redirect_to"),
182
- defaultRedirect
219
+ safeDefaultRedirect
183
220
  );
184
221
  const origin = requestUrl.origin;
185
222
  const error = requestUrl.searchParams.get("error");
@@ -201,7 +238,7 @@ function createAuthCallbackHandler({
201
238
  return linkingErrorRedirect;
202
239
  }
203
240
  return NextResponse.redirect(
204
- `${origin}${signInPath}?error=${encodeURIComponent(message)}`
241
+ `${origin}${safeSignInPath}?error=${encodeURIComponent(message)}`
205
242
  );
206
243
  }
207
244
  if (code) {
@@ -216,7 +253,7 @@ function createAuthCallbackHandler({
216
253
  flow: "oauth",
217
254
  postAuthHook,
218
255
  postAuthHookErrorMode,
219
- signInPath,
256
+ signInPath: safeSignInPath,
220
257
  resolveSuccessRedirect
221
258
  });
222
259
  }
@@ -236,13 +273,13 @@ function createAuthCallbackHandler({
236
273
  return linkingErrorRedirect;
237
274
  }
238
275
  return NextResponse.redirect(
239
- `${origin}${signInPath}?error=${encodeURIComponent(errorMessage)}`
276
+ `${origin}${safeSignInPath}?error=${encodeURIComponent(errorMessage)}`
240
277
  );
241
278
  }
242
279
  if (tokenHash || rawOtpType) {
243
280
  if (!tokenHash || !otpType) {
244
281
  return NextResponse.redirect(
245
- `${origin}${signInPath}?error=${encodeURIComponent("Invalid verification link")}`
282
+ `${origin}${safeSignInPath}?error=${encodeURIComponent("Invalid verification link")}`
246
283
  );
247
284
  }
248
285
  const supabase = await createClient();
@@ -260,16 +297,16 @@ function createAuthCallbackHandler({
260
297
  otpType,
261
298
  postAuthHook,
262
299
  postAuthHookErrorMode,
263
- signInPath,
300
+ signInPath: safeSignInPath,
264
301
  resolveSuccessRedirect
265
302
  });
266
303
  }
267
304
  return NextResponse.redirect(
268
- `${origin}${signInPath}?error=${encodeURIComponent(verifyError.message)}`
305
+ `${origin}${safeSignInPath}?error=${encodeURIComponent(verifyError.message)}`
269
306
  );
270
307
  }
271
308
  return NextResponse.redirect(
272
- `${origin}${signInPath}?error=No authorization code received`
309
+ `${origin}${safeSignInPath}?error=No authorization code received`
273
310
  );
274
311
  };
275
312
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caffeinebounce/identity",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
4
4
  "description": "Authentication components and handlers for Caffeine Bounce projects",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -61,7 +61,7 @@
61
61
  "dependencies": {
62
62
  "@caffeinebounce/logger": "^0.10.0",
63
63
  "@caffeinebounce/shared-utils": "^0.7.136",
64
- "@caffeinebounce/ui": "^0.62.2",
64
+ "@caffeinebounce/ui": "^0.62.3",
65
65
  "@supabase/ssr": "^0.8.0",
66
66
  "@supabase/supabase-js": "^2.49.4",
67
67
  "input-otp": "^1.4.2",