@payez/next-mvp 4.4.4 → 4.4.5

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.
@@ -1256,7 +1256,26 @@ function createAuthGetHandler(loginPath = '/account-auth/login') {
1256
1256
  return new Response(null, { status: 302, headers: { location: loginPath } });
1257
1257
  }
1258
1258
  }
1259
- // After successful OAuth callback: exchange Google identity for IDP tokens
1259
+ // After successful OAuth callback: exchange the PROVIDER identity for IDP tokens.
1260
+ //
1261
+ // ⚠ WAS "exchange Google identity" — and it meant it literally. This call
1262
+ // site passed no provider, so it took exchangeOAuthForIdpTokens's
1263
+ // `provider: string = 'google'` default and asserted "google" to
1264
+ // /api/ExternalAuth/oauth-callback for EVERY provider. That was harmless
1265
+ // while Google was the only one wired, and became wrong the moment a second
1266
+ // provider shipped: a GitHub sign-in forwarded a GitHub access_token
1267
+ // labelled `provider: 'google'`, so the IDP tried to verify it as a Google
1268
+ // identity and fail-closed with 401 UNVERIFIED_ASSERTION in
1269
+ // `response_time_ms: 0` — it never had to call anyone to know it was wrong.
1270
+ //
1271
+ // The diagnostics made this maximally confusing: `access_token_forwarded:
1272
+ // true` is correct and the token really did arrive. The FIELD was fine; the
1273
+ // LABEL on it was a lie. Nothing on the client half is broken.
1274
+ //
1275
+ // The provider was never unavailable — it is the path segment this very
1276
+ // `if` matches on (/api/auth/callback/github). Derive it instead of
1277
+ // defaulting, because a default here cannot be right for more than one
1278
+ // provider at a time.
1260
1279
  const url = new URL(request.url);
1261
1280
  if (url.pathname.includes('/callback/') && response.status === 302) {
1262
1281
  try {
@@ -1270,8 +1289,23 @@ function createAuthGetHandler(loginPath = '/account-auth/login') {
1270
1289
  headers.set('cookie', cookieHeader);
1271
1290
  const session = await auth.api.getSession({ headers });
1272
1291
  if (session?.session?.token) {
1273
- console.log('[BETTER_AUTH] Got session token from callback:', session.session.token.substring(0, 10), '| email:', session.user?.email);
1274
- await exchangeOAuthForIdpTokens(session.session.token);
1292
+ // /api/auth/callback/<provider>[?query] -> "<provider>"
1293
+ const callbackProvider = url.pathname
1294
+ .split('/callback/')[1]
1295
+ ?.split('/')[0]
1296
+ ?.split('?')[0]
1297
+ ?.trim()
1298
+ .toLowerCase() || '';
1299
+ if (!callbackProvider) {
1300
+ // Do NOT quietly fall back to 'google'. That silent default is the
1301
+ // exact defect this block fixes, and re-introducing it here would
1302
+ // reproduce it for whatever unparseable shape got us here.
1303
+ console.error('[BETTER_AUTH] OAuth callback matched /callback/ but no provider could be derived from', url.pathname, '— refusing to assert a guessed provider to the IDP. No exchange performed.');
1304
+ }
1305
+ else {
1306
+ console.log('[BETTER_AUTH] Got session token from callback:', session.session.token.substring(0, 10), '| provider:', callbackProvider, '| email:', session.user?.email);
1307
+ await exchangeOAuthForIdpTokens(session.session.token, callbackProvider);
1308
+ }
1275
1309
  }
1276
1310
  else {
1277
1311
  console.warn('[BETTER_AUTH] Could not get session after OAuth callback');
@@ -673,7 +673,37 @@ async function fetchConfigFromIDP(idpUrl, clientIdStr) {
673
673
  'X-Client-Id': clientIdStr,
674
674
  'X-Correlation-Id': (0, crypto_1.randomUUID)().replace(/-/g, ''),
675
675
  },
676
- body: JSON.stringify({ client_assertion }),
676
+ // 217917 PHASE 1: explicit opt-in for provider secrets.
677
+ //
678
+ // This broker is the ONLY consumer of oauth_providers[].client_secret
679
+ // (DotNetPert enumerated it: this file feeds auth/providers/oauth.ts,
680
+ // which performs the server-side code exchange). Every OTHER caller of
681
+ // ExternalAuth/client-config - notably every fielded acp-desktop - is
682
+ // pushed the same secrets today and reads none of them: the desktop is
683
+ // PKCE and dropped clientSecret from OAuthProviderConfig entirely.
684
+ //
685
+ // The IDP IGNORES this field today, so shipping it is a no-op and needs
686
+ // no coordination window. Phase 2 (DotNetPert, IDP side) makes
687
+ // client-config emit the secrets ONLY when the opt-in is present and
688
+ // redact otherwise - which is why phase 1 MUST be live first, or the
689
+ // broker loses the field and tenant OAuth sign-in dies.
690
+ //
691
+ // SCOPE, deliberately narrow - do NOT read this as a partial migration:
692
+ // the sibling path fetchConfigFromInternalIDP() hits a DIFFERENT
693
+ // endpoint (InternalClientConfig/{id}, GET) that is already
694
+ // authenticated with CONTAINERS_KEY, is not reachable by a desktop, and
695
+ // is therefore not part of 217917's exposure. It deliberately sends no
696
+ // opt-in. If phase 2's redaction is implemented in shared config-
697
+ // assembly code rather than in ExternalAuthController.GetClientConfig,
698
+ // it will ALSO strip the internal route and break this broker's other
699
+ // path with no opt-in to save it. Phase 2 must gate on the request.
700
+ //
701
+ // NOT A FIX ON ITS OWN: an attacker can set this flag too. Phase 2 ends
702
+ // the AUTOMATIC delivery of every tenant's provider secrets to every
703
+ // user laptop; it does not close disclosure to a determined caller.
704
+ // That residual is card 175047 (unauthenticated mint, global partner
705
+ // key) and 217917 does not close until 175047 lands or Jon accepts it.
706
+ body: JSON.stringify({ client_assertion, include_provider_secrets: true }),
677
707
  cache: 'no-store'
678
708
  });
679
709
  if (!configResp.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payez/next-mvp",
3
- "version": "4.4.4",
3
+ "version": "4.4.5",
4
4
  "sideEffects": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1383,7 +1383,26 @@ export function createAuthGetHandler(loginPath: string = '/account-auth/login')
1383
1383
  }
1384
1384
  }
1385
1385
 
1386
- // After successful OAuth callback: exchange Google identity for IDP tokens
1386
+ // After successful OAuth callback: exchange the PROVIDER identity for IDP tokens.
1387
+ //
1388
+ // ⚠ WAS "exchange Google identity" — and it meant it literally. This call
1389
+ // site passed no provider, so it took exchangeOAuthForIdpTokens's
1390
+ // `provider: string = 'google'` default and asserted "google" to
1391
+ // /api/ExternalAuth/oauth-callback for EVERY provider. That was harmless
1392
+ // while Google was the only one wired, and became wrong the moment a second
1393
+ // provider shipped: a GitHub sign-in forwarded a GitHub access_token
1394
+ // labelled `provider: 'google'`, so the IDP tried to verify it as a Google
1395
+ // identity and fail-closed with 401 UNVERIFIED_ASSERTION in
1396
+ // `response_time_ms: 0` — it never had to call anyone to know it was wrong.
1397
+ //
1398
+ // The diagnostics made this maximally confusing: `access_token_forwarded:
1399
+ // true` is correct and the token really did arrive. The FIELD was fine; the
1400
+ // LABEL on it was a lie. Nothing on the client half is broken.
1401
+ //
1402
+ // The provider was never unavailable — it is the path segment this very
1403
+ // `if` matches on (/api/auth/callback/github). Derive it instead of
1404
+ // defaulting, because a default here cannot be right for more than one
1405
+ // provider at a time.
1387
1406
  const url = new URL(request.url);
1388
1407
  if (url.pathname.includes('/callback/') && response.status === 302) {
1389
1408
  try {
@@ -1399,8 +1418,32 @@ export function createAuthGetHandler(loginPath: string = '/account-auth/login')
1399
1418
 
1400
1419
  const session = await auth.api.getSession({ headers });
1401
1420
  if (session?.session?.token) {
1402
- console.log('[BETTER_AUTH] Got session token from callback:', session.session.token.substring(0, 10), '| email:', session.user?.email);
1403
- await exchangeOAuthForIdpTokens(session.session.token);
1421
+ // /api/auth/callback/<provider>[?query] -> "<provider>"
1422
+ const callbackProvider = url.pathname
1423
+ .split('/callback/')[1]
1424
+ ?.split('/')[0]
1425
+ ?.split('?')[0]
1426
+ ?.trim()
1427
+ .toLowerCase() || '';
1428
+
1429
+ if (!callbackProvider) {
1430
+ // Do NOT quietly fall back to 'google'. That silent default is the
1431
+ // exact defect this block fixes, and re-introducing it here would
1432
+ // reproduce it for whatever unparseable shape got us here.
1433
+ console.error(
1434
+ '[BETTER_AUTH] OAuth callback matched /callback/ but no provider could be derived from',
1435
+ url.pathname,
1436
+ '— refusing to assert a guessed provider to the IDP. No exchange performed.'
1437
+ );
1438
+ } else {
1439
+ console.log(
1440
+ '[BETTER_AUTH] Got session token from callback:',
1441
+ session.session.token.substring(0, 10),
1442
+ '| provider:', callbackProvider,
1443
+ '| email:', session.user?.email
1444
+ );
1445
+ await exchangeOAuthForIdpTokens(session.session.token, callbackProvider);
1446
+ }
1404
1447
  } else {
1405
1448
  console.warn('[BETTER_AUTH] Could not get session after OAuth callback');
1406
1449
  }
@@ -783,7 +783,37 @@ async function fetchConfigFromIDP(idpUrl: string, clientIdStr: string): Promise<
783
783
  'X-Client-Id': clientIdStr,
784
784
  'X-Correlation-Id': randomUUID().replace(/-/g, ''),
785
785
  },
786
- body: JSON.stringify({ client_assertion }),
786
+ // 217917 PHASE 1: explicit opt-in for provider secrets.
787
+ //
788
+ // This broker is the ONLY consumer of oauth_providers[].client_secret
789
+ // (DotNetPert enumerated it: this file feeds auth/providers/oauth.ts,
790
+ // which performs the server-side code exchange). Every OTHER caller of
791
+ // ExternalAuth/client-config - notably every fielded acp-desktop - is
792
+ // pushed the same secrets today and reads none of them: the desktop is
793
+ // PKCE and dropped clientSecret from OAuthProviderConfig entirely.
794
+ //
795
+ // The IDP IGNORES this field today, so shipping it is a no-op and needs
796
+ // no coordination window. Phase 2 (DotNetPert, IDP side) makes
797
+ // client-config emit the secrets ONLY when the opt-in is present and
798
+ // redact otherwise - which is why phase 1 MUST be live first, or the
799
+ // broker loses the field and tenant OAuth sign-in dies.
800
+ //
801
+ // SCOPE, deliberately narrow - do NOT read this as a partial migration:
802
+ // the sibling path fetchConfigFromInternalIDP() hits a DIFFERENT
803
+ // endpoint (InternalClientConfig/{id}, GET) that is already
804
+ // authenticated with CONTAINERS_KEY, is not reachable by a desktop, and
805
+ // is therefore not part of 217917's exposure. It deliberately sends no
806
+ // opt-in. If phase 2's redaction is implemented in shared config-
807
+ // assembly code rather than in ExternalAuthController.GetClientConfig,
808
+ // it will ALSO strip the internal route and break this broker's other
809
+ // path with no opt-in to save it. Phase 2 must gate on the request.
810
+ //
811
+ // NOT A FIX ON ITS OWN: an attacker can set this flag too. Phase 2 ends
812
+ // the AUTOMATIC delivery of every tenant's provider secrets to every
813
+ // user laptop; it does not close disclosure to a determined caller.
814
+ // That residual is card 175047 (unauthenticated mint, global partner
815
+ // key) and 217917 does not close until 175047 lands or Jon accepts it.
816
+ body: JSON.stringify({ client_assertion, include_provider_secrets: true }),
787
817
  cache: 'no-store'
788
818
  } as RequestInit);
789
819