oidc-spa 8.1.12 → 8.1.14

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.
@@ -1062,6 +1062,27 @@ export async function createOidc_nonMemoized<
1062
1062
  log
1063
1063
  });
1064
1064
 
1065
+ detect_useless_idleSessionLifetimeInSeconds: {
1066
+ if (idleSessionLifetimeInSeconds === undefined) {
1067
+ break detect_useless_idleSessionLifetimeInSeconds;
1068
+ }
1069
+
1070
+ if (currentTokens.refreshTokenExpirationTime === undefined) {
1071
+ break detect_useless_idleSessionLifetimeInSeconds;
1072
+ }
1073
+
1074
+ console.warn(
1075
+ [
1076
+ "oidc-spa: You've specified idleSessionLifetimeInSeconds,",
1077
+ "but your auth server issues a refresh_token with a known expiration time.",
1078
+ "idleSessionLifetimeInSeconds should only be used as a fallback",
1079
+ "for auth servers that don't specify when an inactive session expires.",
1080
+ "The auth server, not your code, is the source of truth.",
1081
+ "See: https://docs.oidc-spa.dev/v/v8/auto-logout"
1082
+ ].join(" ")
1083
+ );
1084
+ }
1085
+
1065
1086
  {
1066
1087
  if (getPersistedAuthState({ configId }) !== undefined) {
1067
1088
  persistAuthState({ configId, state: undefined });
@@ -1558,6 +1579,18 @@ export async function createOidc_nonMemoized<
1558
1579
  return;
1559
1580
  }
1560
1581
 
1582
+ const msBeforeExpiration_idleSessionLifetimeInSeconds =
1583
+ idleSessionLifetimeInSeconds === undefined ? undefined : idleSessionLifetimeInSeconds * 1000;
1584
+
1585
+ const msBeforeExpiration_refreshToken =
1586
+ currentTokens.refreshTokenExpirationTime === undefined
1587
+ ? undefined
1588
+ : currentTokens.refreshTokenExpirationTime - currentTokens.getServerDateNow();
1589
+ const msBeforeExpiration_accessToken =
1590
+ currentTokens.accessTokenExpirationTime - currentTokens.getServerDateNow();
1591
+
1592
+ let isRefreshTokenNeverExpiring = false;
1593
+
1561
1594
  if (
1562
1595
  currentTokens.refreshTokenExpirationTime !== undefined &&
1563
1596
  currentTokens.refreshTokenExpirationTime >= INFINITY_TIME
@@ -1573,74 +1606,117 @@ export async function createOidc_nonMemoized<
1573
1606
  if (warningLines.length > 0) {
1574
1607
  warningLines.push(
1575
1608
  ...[
1576
- "Misconfiguration: offline_access is for native apps, not SPAs. ",
1609
+ "Misconfiguration: offline_access is for native apps, not for web apps like yours. ",
1577
1610
  "You lose SSO and users must log in after every reload."
1578
1611
  ]
1579
1612
  );
1613
+ console.warn(`oidc-spa: ${warningLines.join(" ")}`);
1614
+ return;
1580
1615
  }
1581
1616
 
1582
- const logMessage = [
1583
- "Refresh token never expires → no need to ping server.",
1584
- "The backend session will not expire.",
1585
- ...warningLines
1586
- ].join(" ");
1617
+ isRefreshTokenNeverExpiring = true;
1618
+ }
1587
1619
 
1588
- if (warningLines.length > 0) {
1589
- console.warn(`oidc-spa: ${logMessage}`);
1590
- } else {
1591
- log?.(logMessage);
1620
+ const RENEW_MS_BEFORE_EXPIRES = 30_000;
1621
+ const MIN_ACCEPTABLE_MS_BEFORE_EXPIRATION = RENEW_MS_BEFORE_EXPIRES + 15_000;
1622
+
1623
+ detect_session_reached_max_life: {
1624
+ if (msBeforeExpiration_refreshToken === undefined) {
1625
+ break detect_session_reached_max_life;
1626
+ }
1627
+
1628
+ if (msBeforeExpiration_refreshToken > MIN_ACCEPTABLE_MS_BEFORE_EXPIRATION) {
1629
+ break detect_session_reached_max_life;
1592
1630
  }
1631
+
1632
+ console.warn(
1633
+ [
1634
+ "oidc-spa: The session is nearing its maximum lifetime, and the user will soon need to log in again,",
1635
+ `or you've configured a refresh_token with a TTL of ${toHumanReadableDuration(
1636
+ msBeforeExpiration_refreshToken
1637
+ )}.`,
1638
+ `If it's the latter, the TTL is too short, it must be at least ${toHumanReadableDuration(
1639
+ MIN_ACCEPTABLE_MS_BEFORE_EXPIRATION
1640
+ )} for reliable operation.`,
1641
+ "Shorter lifetimes can cause unpredictable session expirations and are usually a misconfiguration.",
1642
+ "\nIn either case, oidc-spa will not ping the auth server to keep the session alive."
1643
+ ].join(" ")
1644
+ );
1645
+
1593
1646
  return;
1594
1647
  }
1595
1648
 
1596
- const msBeforeExpiration =
1597
- (currentTokens.refreshTokenExpirationTime ?? currentTokens.accessTokenExpirationTime) -
1598
- currentTokens.getServerDateNow();
1649
+ let msBeforeExpiration = (() => {
1650
+ if (msBeforeExpiration_refreshToken !== undefined && !isRefreshTokenNeverExpiring) {
1651
+ log?.(
1652
+ [
1653
+ toHumanReadableDuration(msBeforeExpiration_refreshToken),
1654
+ `before expiration of the refresh_token.`,
1655
+ `Scheduling renewal of the tokens ${toHumanReadableDuration(
1656
+ RENEW_MS_BEFORE_EXPIRES
1657
+ )} before expiration as a way to keep the session alive on the OIDC server.`
1658
+ ].join(" ")
1659
+ );
1599
1660
 
1600
- const typeOfTheTokenWeGotTheTtlFrom =
1601
- currentTokens.refreshTokenExpirationTime !== undefined ? "refresh" : "access";
1661
+ return msBeforeExpiration_refreshToken;
1662
+ }
1602
1663
 
1603
- const RENEW_MS_BEFORE_EXPIRES = 30_000;
1664
+ if (msBeforeExpiration_idleSessionLifetimeInSeconds !== undefined) {
1665
+ if (
1666
+ msBeforeExpiration_idleSessionLifetimeInSeconds < MIN_ACCEPTABLE_MS_BEFORE_EXPIRATION
1667
+ ) {
1668
+ throw new Error(
1669
+ [
1670
+ `oidc-spa: The configured idleSessionLifetimeInSeconds (${toHumanReadableDuration(
1671
+ msBeforeExpiration_idleSessionLifetimeInSeconds
1672
+ )}) is too short.`,
1673
+ `For reliability, it must be at least ${toHumanReadableDuration(
1674
+ MIN_ACCEPTABLE_MS_BEFORE_EXPIRATION
1675
+ )}.`,
1676
+ "Very short session idle lifetimes are usually a misconfiguration, even for ultra sensitive apps."
1677
+ ].join(" ")
1678
+ );
1679
+ }
1680
+
1681
+ log?.(
1682
+ [
1683
+ `You've set idleSessionLifetimeInSeconds to ${toHumanReadableDuration(
1684
+ msBeforeExpiration_idleSessionLifetimeInSeconds
1685
+ )}.`,
1686
+ `This means the user session will expire after ${toHumanReadableDuration(
1687
+ msBeforeExpiration_idleSessionLifetimeInSeconds
1688
+ )} of inactivity (assuming you're right).`,
1689
+ `Scheduling token renewal ${toHumanReadableDuration(
1690
+ RENEW_MS_BEFORE_EXPIRES
1691
+ )} before expiration to keep the session active on the OIDC server.`
1692
+ ].join(" ")
1693
+ );
1694
+
1695
+ return msBeforeExpiration_idleSessionLifetimeInSeconds;
1696
+ }
1697
+
1698
+ const msBeforeExpiration =
1699
+ msBeforeExpiration_accessToken > MIN_ACCEPTABLE_MS_BEFORE_EXPIRATION
1700
+ ? msBeforeExpiration_accessToken
1701
+ : 3_600_000;
1604
1702
 
1605
- if (msBeforeExpiration <= RENEW_MS_BEFORE_EXPIRES) {
1606
1703
  log?.(
1607
1704
  [
1608
- "Session keep-alive disabled. We just got fresh tokens",
1609
- (() => {
1610
- switch (typeOfTheTokenWeGotTheTtlFrom) {
1611
- case "refresh":
1612
- return [
1613
- " and the refresh token is already about to expires.",
1614
- "This means that we have reached the max session lifespan, we can't keep",
1615
- "the session alive any longer.",
1616
- "(This can also mean that the refresh token was configured with a TTL,",
1617
- "aka the idle session lifespan, too low to make sense)"
1618
- ].join(" ");
1619
- case "access":
1620
- return [
1621
- currentTokens.hasRefreshToken
1622
- ? ", we can't read the expiration time of the refresh token"
1623
- : ", we don't have a refresh token",
1624
- ` and the access token is already about to expire`,
1625
- "we would spam the auth server by constantly renewing the access token in the background",
1626
- "avoiding to do so."
1627
- ].join(" ");
1628
- }
1629
- })()
1630
- ].join(" ")
1705
+ "The auth server's idle session timeout is unknown.",
1706
+ isRefreshTokenNeverExpiring && "(The refresh token never expires)",
1707
+ `Assuming a default idle session TTL of ${toHumanReadableDuration(
1708
+ msBeforeExpiration
1709
+ )}.`,
1710
+ `Scheduling token renewal ${toHumanReadableDuration(
1711
+ RENEW_MS_BEFORE_EXPIRES
1712
+ )} before expiration to keep the session active on the OIDC server.`
1713
+ ]
1714
+ .filter(line => typeof line === "string")
1715
+ .join(" ")
1631
1716
  );
1632
- return;
1633
- }
1634
1717
 
1635
- log?.(
1636
- [
1637
- toHumanReadableDuration(msBeforeExpiration),
1638
- `before expiration of the ${typeOfTheTokenWeGotTheTtlFrom} token.`,
1639
- `Scheduling renewal ${toHumanReadableDuration(
1640
- RENEW_MS_BEFORE_EXPIRES
1641
- )} before expiration to keep the session alive on the OIDC server.`
1642
- ].join(" ")
1643
- );
1718
+ return msBeforeExpiration;
1719
+ })();
1644
1720
 
1645
1721
  const timer = setTimeout(
1646
1722
  async () => {
@@ -1670,7 +1746,7 @@ export async function createOidc_nonMemoized<
1670
1746
  }
1671
1747
 
1672
1748
  log?.(
1673
- `Renewing the tokens now as the ${typeOfTheTokenWeGotTheTtlFrom} token will expire in ${toHumanReadableDuration(
1749
+ `Renewing the tokens now as otherwise the session will be terminated by the auth server in ${toHumanReadableDuration(
1674
1750
  RENEW_MS_BEFORE_EXPIRES
1675
1751
  )}`
1676
1752
  );
@@ -1695,19 +1771,22 @@ export async function createOidc_nonMemoized<
1695
1771
 
1696
1772
  auto_logout: {
1697
1773
  const getCurrentRefreshTokenTtlInSeconds = () => {
1698
- if (idleSessionLifetimeInSeconds !== undefined) {
1774
+ if (currentTokens.refreshTokenExpirationTime === undefined) {
1699
1775
  return idleSessionLifetimeInSeconds;
1700
1776
  }
1701
1777
 
1702
- if (currentTokens.refreshTokenExpirationTime === undefined) {
1703
- return undefined;
1778
+ if (currentTokens.refreshTokenExpirationTime >= INFINITY_TIME) {
1779
+ return idleSessionLifetimeInSeconds ?? 0;
1704
1780
  }
1705
1781
 
1706
- if (currentTokens.refreshTokenExpirationTime >= INFINITY_TIME) {
1707
- return 0;
1782
+ const ttlInSeconds =
1783
+ (currentTokens.refreshTokenExpirationTime - currentTokens.issuedAtTime) / 1000;
1784
+
1785
+ if (idleSessionLifetimeInSeconds !== undefined) {
1786
+ return Math.min(idleSessionLifetimeInSeconds, ttlInSeconds);
1708
1787
  }
1709
1788
 
1710
- return (currentTokens.refreshTokenExpirationTime - currentTokens.issuedAtTime) / 1000;
1789
+ return ttlInSeconds;
1711
1790
  };
1712
1791
 
1713
1792
  if (getCurrentRefreshTokenTtlInSeconds() === 0) {
@@ -9,16 +9,15 @@ export function createCreateValidateAndGetAccessTokenClaims_rfc9068<
9
9
  >(params: {
10
10
  accessTokenClaimsSchema?: ZodSchemaLike<AccessTokenClaims_RFC9068, AccessTokenClaims>;
11
11
  accessTokenClaims_mock?: AccessTokenClaims;
12
- expectedAudience?:
13
- | string
14
- | ((params: {
15
- paramsOfBootstrap: ParamsOfBootstrap<boolean, Record<string, unknown>, AccessTokenClaims>;
16
- }) => string);
12
+ expectedAudience?: (params: {
13
+ paramsOfBootstrap: ParamsOfBootstrap.Real<boolean>;
14
+ process: { env: Record<string, string> };
15
+ }) => string;
17
16
  }) {
18
17
  const {
19
18
  accessTokenClaimsSchema,
20
19
  accessTokenClaims_mock,
21
- expectedAudience: expectedAudienceOrGetter
20
+ expectedAudience: expectedAudienceGetter
22
21
  } = params;
23
22
 
24
23
  const createValidateAndGetAccessTokenClaims: CreateValidateAndGetAccessTokenClaims<
@@ -66,13 +65,56 @@ export function createCreateValidateAndGetAccessTokenClaims_rfc9068<
66
65
  })();
67
66
 
68
67
  const expectedAudience = (() => {
69
- if (expectedAudienceOrGetter === undefined) {
68
+ if (expectedAudienceGetter === undefined) {
70
69
  return undefined;
71
70
  }
72
- if (typeof expectedAudienceOrGetter === "function") {
73
- return expectedAudienceOrGetter({ paramsOfBootstrap });
71
+
72
+ const missingEnvNames = new Set<string>();
73
+
74
+ const env_proxy = new Proxy<Record<string, string>>(
75
+ {},
76
+ {
77
+ get: (...[, envName]) => {
78
+ assert(typeof envName === "string");
79
+
80
+ const value = process.env[envName];
81
+
82
+ if (value === undefined) {
83
+ missingEnvNames.add(envName);
84
+ return "";
85
+ }
86
+
87
+ return value;
88
+ },
89
+ has: (...[, envName]) => {
90
+ assert(typeof envName === "string");
91
+ return true;
92
+ }
93
+ }
94
+ );
95
+
96
+ const expectedAudience = expectedAudienceGetter?.({
97
+ paramsOfBootstrap,
98
+ process: { env: env_proxy }
99
+ });
100
+
101
+ if (!expectedAudience) {
102
+ throw new Error(
103
+ [
104
+ "oidc-spa: The expectedAudience() you provided returned empty.",
105
+ "If you specified the expectedAudience in withAccessTokenValidation",
106
+ "it's probably and error.",
107
+ missingEnvNames.size !== 0 &&
108
+ `Did you forget to set the env var: ${Array.from(missingEnvNames).join(
109
+ ", "
110
+ )} ?`
111
+ ]
112
+ .filter(line => typeof line === "string")
113
+ .join(" ")
114
+ );
74
115
  }
75
- return expectedAudienceOrGetter;
116
+
117
+ return expectedAudience;
76
118
  })();
77
119
 
78
120
  return {
@@ -41,15 +41,10 @@ export type OidcSpaApiBuilder<
41
41
  accessTokenClaimsSchema?: ZodSchemaLike<AccessTokenClaims_RFC9068, AccessTokenClaims>;
42
42
  accessTokenClaims_mock?: NoInfer<AccessTokenClaims>;
43
43
 
44
- expectedAudience?:
45
- | string
46
- | ((params: {
47
- paramsOfBootstrap: ParamsOfBootstrap<
48
- boolean,
49
- Record<string, unknown>,
50
- AccessTokenClaims
51
- >;
52
- }) => string);
44
+ expectedAudience?: (params: {
45
+ paramsOfBootstrap: ParamsOfBootstrap.Real<boolean>;
46
+ process: { env: Record<string, string> };
47
+ }) => string;
53
48
  }): OidcSpaApiBuilder<
54
49
  AutoLogin,
55
50
  DecodedIdToken,
@@ -663,7 +663,9 @@ export function createOidcSpaApi<
663
663
  noIframe: paramsOfBootstrap.noIframe,
664
664
  debugLogs: paramsOfBootstrap.debugLogs,
665
665
  __unsafe_clientSecret: paramsOfBootstrap.__unsafe_clientSecret,
666
- __metadata: paramsOfBootstrap.__metadata
666
+ __metadata: paramsOfBootstrap.__metadata,
667
+ __unsafe_useIdTokenAsAccessToken:
668
+ paramsOfBootstrap.__unsafe_useIdTokenAsAccessToken
667
669
  });
668
670
  } catch (error) {
669
671
  if (!(error instanceof OidcInitializationError)) {
@@ -22,7 +22,7 @@ export type CreateOidcComponent<DecodedIdToken> = <
22
22
  export namespace CreateOidcComponent {
23
23
  export type WithAutoLogin<DecodedIdToken> = <Props>(params: {
24
24
  pendingComponent?: (params: NoInfer<Props>) => ReactNode;
25
- component: (props: Props) => ReactNode;
25
+ component: (props: Props) => any;
26
26
  }) => ((props: Props) => ReactNode) & {
27
27
  useOidc: () => Oidc.LoggedIn<DecodedIdToken>;
28
28
  };
@@ -338,6 +338,13 @@ export namespace ParamsOfBootstrap {
338
338
  * or non-standard deployments), and you cannot fix the server-side configuration.
339
339
  */
340
340
  __metadata?: Partial<OidcMetadata>;
341
+
342
+ /**
343
+ * WARNING: Setting this to true is a workaround for provider
344
+ * like Google OAuth that don't support JWT access token.
345
+ * Use at your own risk, this is a hack.
346
+ */
347
+ __unsafe_useIdTokenAsAccessToken?: boolean;
341
348
  } & (AutoLogin extends true ? {} : {});
342
349
 
343
350
  export type Mock<AutoLogin, DecodedIdToken, AccessTokenClaims> = {