@axa-fr/oidc-client-service-worker 7.29.7 → 7.30.0

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 (107) hide show
  1. package/README.md +64 -0
  2. package/dist/OidcServiceWorker.d.ts +2 -1
  3. package/dist/OidcServiceWorker.d.ts.map +1 -0
  4. package/dist/OidcServiceWorker.js +995 -765
  5. package/dist/OidcServiceWorker.js.map +1 -1
  6. package/dist/constants.d.ts.map +1 -0
  7. package/dist/crypto.d.ts.map +1 -0
  8. package/dist/dpop.d.ts.map +1 -0
  9. package/dist/jwt.d.ts.map +1 -0
  10. package/dist/oidcConfig.d.ts.map +1 -0
  11. package/dist/protocol.d.ts +140 -2
  12. package/dist/protocol.d.ts.map +1 -0
  13. package/dist/protocol.js +50 -2
  14. package/dist/protocol.js.map +1 -1
  15. package/dist/resource.d.ts +22 -0
  16. package/dist/resource.d.ts.map +1 -0
  17. package/dist/tokenContexts.d.ts +5 -0
  18. package/dist/tokenContexts.d.ts.map +1 -0
  19. package/dist/{src/types.d.ts → types.d.ts} +26 -1
  20. package/dist/types.d.ts.map +1 -0
  21. package/dist/utils/codeVerifier.d.ts.map +1 -0
  22. package/dist/{src/utils → utils}/domains.d.ts +2 -2
  23. package/dist/utils/domains.d.ts.map +1 -0
  24. package/dist/utils/index.d.ts.map +1 -0
  25. package/dist/utils/normalizeUrl.d.ts.map +1 -0
  26. package/dist/utils/serializeHeaders.d.ts.map +1 -0
  27. package/dist/utils/shouldBypassDestination.d.ts.map +1 -0
  28. package/dist/utils/sleep.d.ts.map +1 -0
  29. package/dist/utils/strings.d.ts.map +1 -0
  30. package/dist/{src/utils → utils}/tokens.d.ts +12 -3
  31. package/dist/utils/tokens.d.ts.map +1 -0
  32. package/dist/utils/waitForValidTokens.d.ts.map +1 -0
  33. package/dist/version.d.ts +3 -0
  34. package/dist/version.d.ts.map +1 -0
  35. package/package.json +3 -2
  36. package/src/OidcServiceWorker.ts +290 -34
  37. package/src/__tests__/jwt.spec.ts +17 -0
  38. package/src/__tests__/resource.spec.ts +98 -0
  39. package/src/__tests__/workerResource.spec.ts +919 -0
  40. package/src/jwt.ts +4 -1
  41. package/src/oidcConfig.ts +2 -1
  42. package/src/protocol.ts +16 -1
  43. package/src/resource.ts +104 -0
  44. package/src/tokenContexts.ts +39 -0
  45. package/src/types.ts +27 -1
  46. package/src/utils/domains.ts +20 -2
  47. package/src/utils/tokens.ts +155 -8
  48. package/src/version.ts +1 -1
  49. package/dist/src/OidcServiceWorker.d.ts +0 -2
  50. package/dist/src/OidcServiceWorker.d.ts.map +0 -1
  51. package/dist/src/__tests__/jwt.spec.d.ts +0 -2
  52. package/dist/src/__tests__/jwt.spec.d.ts.map +0 -1
  53. package/dist/src/__tests__/oidcConfig.spec.d.ts +0 -2
  54. package/dist/src/__tests__/oidcConfig.spec.d.ts.map +0 -1
  55. package/dist/src/__tests__/protocol.spec.d.ts +0 -2
  56. package/dist/src/__tests__/protocol.spec.d.ts.map +0 -1
  57. package/dist/src/constants.d.ts.map +0 -1
  58. package/dist/src/crypto.d.ts.map +0 -1
  59. package/dist/src/dpop.d.ts.map +0 -1
  60. package/dist/src/jwt.d.ts.map +0 -1
  61. package/dist/src/oidcConfig.d.ts.map +0 -1
  62. package/dist/src/protocol.d.ts +0 -134
  63. package/dist/src/protocol.d.ts.map +0 -1
  64. package/dist/src/types.d.ts.map +0 -1
  65. package/dist/src/utils/__tests__/codeVerifier.spec.d.ts +0 -2
  66. package/dist/src/utils/__tests__/codeVerifier.spec.d.ts.map +0 -1
  67. package/dist/src/utils/__tests__/domains.spec.d.ts +0 -2
  68. package/dist/src/utils/__tests__/domains.spec.d.ts.map +0 -1
  69. package/dist/src/utils/__tests__/normalizeUrl.spec.d.ts +0 -2
  70. package/dist/src/utils/__tests__/normalizeUrl.spec.d.ts.map +0 -1
  71. package/dist/src/utils/__tests__/serializeHeaders.spec.d.ts +0 -2
  72. package/dist/src/utils/__tests__/serializeHeaders.spec.d.ts.map +0 -1
  73. package/dist/src/utils/__tests__/shouldBypassDestination.spec.d.ts +0 -2
  74. package/dist/src/utils/__tests__/shouldBypassDestination.spec.d.ts.map +0 -1
  75. package/dist/src/utils/__tests__/strings.spec.d.ts +0 -2
  76. package/dist/src/utils/__tests__/strings.spec.d.ts.map +0 -1
  77. package/dist/src/utils/__tests__/testHelper.d.ts +0 -58
  78. package/dist/src/utils/__tests__/testHelper.d.ts.map +0 -1
  79. package/dist/src/utils/__tests__/tokens.spec.d.ts +0 -2
  80. package/dist/src/utils/__tests__/tokens.spec.d.ts.map +0 -1
  81. package/dist/src/utils/__tests__/waitForValidTokens.spec.d.ts +0 -2
  82. package/dist/src/utils/__tests__/waitForValidTokens.spec.d.ts.map +0 -1
  83. package/dist/src/utils/codeVerifier.d.ts.map +0 -1
  84. package/dist/src/utils/domains.d.ts.map +0 -1
  85. package/dist/src/utils/index.d.ts.map +0 -1
  86. package/dist/src/utils/normalizeUrl.d.ts.map +0 -1
  87. package/dist/src/utils/serializeHeaders.d.ts.map +0 -1
  88. package/dist/src/utils/shouldBypassDestination.d.ts.map +0 -1
  89. package/dist/src/utils/sleep.d.ts.map +0 -1
  90. package/dist/src/utils/strings.d.ts.map +0 -1
  91. package/dist/src/utils/tokens.d.ts.map +0 -1
  92. package/dist/src/utils/waitForValidTokens.d.ts.map +0 -1
  93. package/dist/src/version.d.ts +0 -3
  94. package/dist/src/version.d.ts.map +0 -1
  95. /package/dist/{src/constants.d.ts → constants.d.ts} +0 -0
  96. /package/dist/{src/crypto.d.ts → crypto.d.ts} +0 -0
  97. /package/dist/{src/dpop.d.ts → dpop.d.ts} +0 -0
  98. /package/dist/{src/jwt.d.ts → jwt.d.ts} +0 -0
  99. /package/dist/{src/oidcConfig.d.ts → oidcConfig.d.ts} +0 -0
  100. /package/dist/{src/utils → utils}/codeVerifier.d.ts +0 -0
  101. /package/dist/{src/utils → utils}/index.d.ts +0 -0
  102. /package/dist/{src/utils → utils}/normalizeUrl.d.ts +0 -0
  103. /package/dist/{src/utils → utils}/serializeHeaders.d.ts +0 -0
  104. /package/dist/{src/utils → utils}/shouldBypassDestination.d.ts +0 -0
  105. /package/dist/{src/utils → utils}/sleep.d.ts +0 -0
  106. /package/dist/{src/utils → utils}/strings.d.ts +0 -0
  107. /package/dist/{src/utils → utils}/waitForValidTokens.d.ts +0 -0
package/src/jwt.ts CHANGED
@@ -208,11 +208,14 @@ export const generateJwtDemonstratingProofOfPossessionAsync =
208
208
  (w: any) =>
209
209
  (demonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration) =>
210
210
  async (jwk: any, method = 'POST', url: string, extrasClaims = {}) => {
211
+ const target = new URL(url, w.location?.href);
212
+ target.search = '';
213
+ target.hash = '';
211
214
  const claims = {
212
215
  // https://www.rfc-editor.org/rfc/rfc9449.html#name-concept
213
216
  jti: btoa(w.crypto.randomUUID()),
214
217
  htm: method,
215
- htu: url,
218
+ htu: target.href,
216
219
  iat: Math.round(Date.now() / 1000),
217
220
  ...extrasClaims,
218
221
  };
package/src/oidcConfig.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { acceptAnyDomainToken } from './constants';
2
+ import { getTokenContexts } from './tokenContexts';
2
3
  import { Database, Domain, OidcConfig, OidcServerConfiguration, TrustedDomains } from './types';
3
4
  import { getDomains, normalizeUrl } from './utils';
4
5
 
@@ -83,7 +84,7 @@ const shouldBypassNonOidcRequest = (
83
84
 
84
85
  const getMatchingOidcConfigurations = (database: Database, url: string): OidcConfig[] => {
85
86
  const normalizedUrl = normalizeUrl(url);
86
- return Object.values(database).filter(config => {
87
+ return getTokenContexts(database).filter(config => {
87
88
  const { oidcServerConfiguration } = config || {};
88
89
  const { tokenEndpoint, revocationEndpoint } = oidcServerConfiguration || {};
89
90
 
package/src/protocol.ts CHANGED
@@ -37,7 +37,18 @@ export type {
37
37
  * - The `PATCH` component changes for documentation or non-behavioural
38
38
  * tweaks.
39
39
  */
40
- export const PROTOCOL_VERSION = '1.0.0' as const;
40
+ export const PROTOCOL_VERSION = '1.1.0' as const;
41
+
42
+ export type { OidcResourceConfiguration } from './resource';
43
+ export {
44
+ getContextResource,
45
+ getResourceConfiguration,
46
+ getResourceContextName,
47
+ isResourceOriginManaged,
48
+ isResourceUrlAllowed,
49
+ RESOURCE_TOKEN_REQUEST_HEADER,
50
+ validateResourceIdentifier,
51
+ } from './resource';
41
52
 
42
53
  /**
43
54
  * Every supported service worker message type.
@@ -97,6 +108,10 @@ export interface ServiceWorkerMessage<
97
108
  * extend it with additional, well-known fields (see PROTOCOL.md).
98
109
  */
99
110
  export interface ServiceWorkerResponse {
111
+ /** Resource isolation capability provided by protocol 1.1 workers. */
112
+ resourceTokenManagement?: boolean;
113
+ /** Worker-owned context names, without token or key material. */
114
+ resourceContexts?: string[];
100
115
  configurationName?: string;
101
116
  /** Set by the SW when it cannot service the request. */
102
117
  error?: unknown;
@@ -0,0 +1,104 @@
1
+ /** An explicitly permitted OAuth resource and its API destinations. */
2
+ export interface OidcResourceConfiguration {
3
+ resource: string;
4
+ allowedUrlPrefixes: readonly string[];
5
+ scope?: string;
6
+ audiences?: readonly string[];
7
+ }
8
+
9
+ /** Worker-only resource refresh routing hint, removed before the provider request. */
10
+ export const RESOURCE_TOKEN_REQUEST_HEADER = 'x-oidc-resource';
11
+
12
+ /** Builds an isolated storage/worker context without creating another OIDC client. */
13
+ export const getResourceContextName = (configurationName: string, resource: string): string =>
14
+ `${configurationName}#resource=${encodeURIComponent(resource)}`;
15
+
16
+ /** Reads the exact resource identifier from an isolated context name. */
17
+ export const getContextResource = (configurationName: string): string | undefined => {
18
+ const marker = '#resource=';
19
+ const index = configurationName.indexOf(marker);
20
+ return index < 0 ? undefined : decodeURIComponent(configurationName.slice(index + marker.length));
21
+ };
22
+
23
+ /** Validates RFC 8707 identifiers without treating them as API URLs or scopes. */
24
+ export const validateResourceIdentifier = (resource: string): void => {
25
+ if (
26
+ typeof resource !== 'string' ||
27
+ !/^[a-z][a-z\d+.-]*:/i.test(resource) ||
28
+ resource.includes('#') ||
29
+ /\s/.test(resource)
30
+ ) {
31
+ throw new Error('A resource must be an absolute URI without whitespace or a fragment.');
32
+ }
33
+ new URL(resource);
34
+ };
35
+
36
+ const getApiUrl = (value: string): URL => {
37
+ const url = new URL(value);
38
+ if (
39
+ (url.protocol !== 'https:' && url.protocol !== 'http:') ||
40
+ url.username ||
41
+ url.password ||
42
+ url.hash ||
43
+ /%2f|%5c/i.test(url.pathname)
44
+ ) {
45
+ throw new Error(
46
+ 'API destinations must be HTTP(S) URLs without credentials, fragments, or encoded separators.',
47
+ );
48
+ }
49
+ return url;
50
+ };
51
+
52
+ const getApiPrefixUrl = (value: string): URL => {
53
+ const prefix = getApiUrl(value);
54
+ if (prefix.search) throw new Error('An API URL prefix must not contain a query.');
55
+ return prefix;
56
+ };
57
+
58
+ /** Prevents default-token fallback anywhere on an explicitly managed API origin. */
59
+ export const isResourceOriginManaged = (
60
+ configurations: readonly OidcResourceConfiguration[] | undefined,
61
+ destination: string,
62
+ ): boolean => {
63
+ const origin = getApiUrl(destination).origin;
64
+ return (
65
+ configurations?.some(configuration =>
66
+ configuration.allowedUrlPrefixes.some(value => getApiPrefixUrl(value).origin === origin),
67
+ ) ?? false
68
+ );
69
+ };
70
+
71
+ /** Matches API mappings by parsed origin and path boundaries, never by host suffix. */
72
+ export const isResourceUrlAllowed = (
73
+ configuration: OidcResourceConfiguration,
74
+ destination: string,
75
+ ): boolean => {
76
+ const url = getApiUrl(destination);
77
+ return configuration.allowedUrlPrefixes.some(value => {
78
+ const prefix = getApiPrefixUrl(value);
79
+ const path = prefix.pathname.replace(/\/+$/, '');
80
+ return (
81
+ url.origin === prefix.origin &&
82
+ (path === '' || url.pathname === path || url.pathname.startsWith(`${path}/`))
83
+ );
84
+ });
85
+ };
86
+
87
+ /** Validates and retrieves an explicitly registered resource. */
88
+ export const getResourceConfiguration = (
89
+ configurations: readonly OidcResourceConfiguration[] | undefined,
90
+ resource: string,
91
+ ): OidcResourceConfiguration => {
92
+ validateResourceIdentifier(resource);
93
+ const matches =
94
+ configurations?.filter(configuration => configuration.resource === resource) ?? [];
95
+ if (matches.length !== 1 || matches[0].allowedUrlPrefixes.length === 0) {
96
+ throw new Error(
97
+ 'The resource must be registered exactly once with permitted API URL prefixes.',
98
+ );
99
+ }
100
+ for (const prefix of matches[0].allowedUrlPrefixes) {
101
+ isResourceUrlAllowed(matches[0], prefix);
102
+ }
103
+ return matches[0];
104
+ };
@@ -0,0 +1,39 @@
1
+ import { getResourceConfiguration, getResourceContextName } from './resource';
2
+ import type { Database, OidcConfig } from './types';
3
+
4
+ export const getTokenContextEntries = (database: Database): Array<[string, OidcConfig]> =>
5
+ Object.entries(database).flatMap(([key, configuration]): Array<[string, OidcConfig]> => [
6
+ [key, configuration],
7
+ ...Object.entries(configuration.resourceEntries ?? {}),
8
+ ]);
9
+
10
+ export const getTokenContexts = (database: Database): OidcConfig[] =>
11
+ getTokenContextEntries(database).map(([, configuration]) => configuration);
12
+
13
+ export const getOrCreateResourceContext = (
14
+ configuration: OidcConfig,
15
+ resource: string,
16
+ ): OidcConfig => {
17
+ getResourceConfiguration(configuration.oidcConfiguration?.resources, resource);
18
+ configuration.resourceEntries ??= {};
19
+ const name = getResourceContextName(configuration.configurationName, resource);
20
+ configuration.resourceEntries[name] ??= {
21
+ ...configuration,
22
+ configurationName: name,
23
+ resource,
24
+ parentConfiguration: configuration,
25
+ resourceEntries: undefined,
26
+ authorizationScope: undefined,
27
+ requestedScope: undefined,
28
+ demonstratingProofOfPossessionApiNonce: null,
29
+ refreshGrant: undefined,
30
+ tokens: null,
31
+ state: null,
32
+ codeVerifier: null,
33
+ nonce: null,
34
+ status: null,
35
+ hideAccessToken: true,
36
+ generation: 0,
37
+ };
38
+ return configuration.resourceEntries[name];
39
+ };
package/src/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { OidcResourceConfiguration } from './resource';
2
+
1
3
  export type DomainDetails = {
2
4
  domains?: Domain[];
3
5
  oidcDomains?: Domain[];
@@ -42,6 +44,10 @@ export type OidcServerConfiguration = {
42
44
  export type OidcConfiguration = {
43
45
  token_renew_mode: string;
44
46
  demonstrating_proof_of_possession: boolean;
47
+ client_id?: string;
48
+ scope?: string;
49
+ resource?: string | readonly string[];
50
+ resources?: readonly OidcResourceConfiguration[];
45
51
  };
46
52
 
47
53
  export type Status =
@@ -95,6 +101,14 @@ export type Nonce = {
95
101
  } | null;
96
102
 
97
103
  export type OidcConfig = {
104
+ authorizationScope?: string;
105
+ requestedScope?: string;
106
+ demonstratingProofOfPossessionApiNonce?: string | null;
107
+ parentConfiguration?: OidcConfig;
108
+ resource?: string;
109
+ resourceEntries?: Record<string, OidcConfig>;
110
+ refreshGrant?: OidcConfig;
111
+ generation?: number;
98
112
  demonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration | null;
99
113
  configurationName: string;
100
114
  tokens: Tokens | null;
@@ -117,6 +131,7 @@ export type OidcConfig = {
117
131
  };
118
132
 
119
133
  export type IdTokenPayload = {
134
+ sub?: string;
120
135
  iss: string;
121
136
  /**
122
137
  * (Expiration Time) Claim
@@ -130,17 +145,28 @@ export type IdTokenPayload = {
130
145
  };
131
146
 
132
147
  export type AccessTokenPayload = {
148
+ aud?: string | string[];
149
+ iss?: string;
133
150
  exp: number;
134
151
  sub: string;
135
152
  iat: number;
136
153
  };
137
154
 
138
155
  export type Tokens = {
156
+ scope?: string;
157
+ resourceContext?: {
158
+ resource: string;
159
+ issuer: string;
160
+ clientId: string;
161
+ configurationName: string;
162
+ subject: string;
163
+ grantResource: string | null;
164
+ };
139
165
  issued_at: number | string;
140
166
  access_token: string;
141
167
  accessTokenPayload: AccessTokenPayload | null;
142
168
  id_token: null | string;
143
- idTokenPayload: IdTokenPayload;
169
+ idTokenPayload?: IdTokenPayload | null;
144
170
  refresh_token?: string;
145
171
  expiresAt: number;
146
172
  expires_in: number | string;
@@ -1,5 +1,11 @@
1
1
  import { acceptAnyDomainToken, openidWellknownUrlEndWith, scriptFilename } from '../constants';
2
- import { Database, Domain, DomainDetails, OidcConfig, TrustedDomains } from '../types';
2
+ import {
3
+ getResourceConfiguration,
4
+ isResourceOriginManaged,
5
+ isResourceUrlAllowed,
6
+ } from '../resource';
7
+ import { getTokenContextEntries } from '../tokenContexts';
8
+ import { Database, Domain, DomainDetails, TrustedDomains } from '../types';
3
9
  import { normalizeUrl } from './normalizeUrl';
4
10
 
5
11
  export function checkDomain(domains: Domain[], endpoint: string) {
@@ -45,8 +51,20 @@ export const getCurrentDatabaseDomain = (
45
51
  return null;
46
52
  }
47
53
  const datatases = [];
48
- for (const [key, currentDatabase] of Object.entries<OidcConfig>(database)) {
54
+ for (const [key, currentDatabase] of getTokenContextEntries(database)) {
49
55
  const oidcServerConfiguration = currentDatabase.oidcServerConfiguration;
56
+ const resources = currentDatabase.oidcConfiguration?.resources;
57
+ if (currentDatabase.resource) {
58
+ if (!isResourceUrlAllowed(getResourceConfiguration(resources, currentDatabase.resource), url))
59
+ continue;
60
+ } else if (
61
+ resources?.length &&
62
+ (!oidcServerConfiguration?.userInfoEndpoint ||
63
+ url !== normalizeUrl(oidcServerConfiguration.userInfoEndpoint)) &&
64
+ isResourceOriginManaged(resources, url)
65
+ ) {
66
+ continue;
67
+ }
50
68
 
51
69
  if (!oidcServerConfiguration) {
52
70
  continue;
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable simple-import-sort/exports */
2
2
  import { TOKEN, TokenRenewMode } from '../constants';
3
+ import { getResourceConfiguration } from '../resource';
3
4
  import {
4
5
  AccessTokenPayload,
5
6
  IdTokenPayload,
@@ -120,7 +121,20 @@ function _hideTokens(
120
121
  tokens: Tokens,
121
122
  currentDatabaseElement: OidcConfig,
122
123
  configurationName: string,
124
+ isAuthorizationCode = false,
123
125
  ) {
126
+ const hasNewIdToken = typeof tokens.id_token === 'string' && tokens.id_token.length > 0;
127
+ const parent = currentDatabaseElement.parentConfiguration;
128
+ if (
129
+ currentDatabaseElement.resource &&
130
+ currentDatabaseElement.refreshGrant === currentDatabaseElement &&
131
+ tokens.refresh_token &&
132
+ tokens.refresh_token === parent?.tokens?.refresh_token
133
+ ) {
134
+ throw new Error(
135
+ 'The provider reused a refresh token across separate authorization transactions; use a multi-resource authorization grant instead.',
136
+ );
137
+ }
124
138
  if (!tokens.issued_at) {
125
139
  const currentTimeUnixSecond = new Date().getTime() / 1000;
126
140
  tokens.issued_at = currentTimeUnixSecond;
@@ -129,6 +143,15 @@ function _hideTokens(
129
143
  }
130
144
 
131
145
  const accessTokenPayload = extractTokenPayload(tokens.access_token);
146
+ if (
147
+ currentDatabaseElement.resource ||
148
+ currentDatabaseElement.oidcConfiguration?.resources?.length
149
+ ) {
150
+ tokens.scope ??=
151
+ currentDatabaseElement.requestedScope ??
152
+ currentDatabaseElement.tokens?.scope ??
153
+ currentDatabaseElement.oidcConfiguration?.scope;
154
+ }
132
155
  const secureTokens = {
133
156
  ...tokens,
134
157
  accessTokenPayload,
@@ -156,10 +179,24 @@ function _hideTokens(
156
179
  const keyNonce = `${TOKEN.NONCE_TOKEN}_${currentDatabaseElement.configurationName}`;
157
180
  _idTokenPayload.nonce = keyNonce;
158
181
  }
159
- secureTokens.idTokenPayload = _idTokenPayload;
182
+ secureTokens.idTokenPayload =
183
+ currentDatabaseElement.resource && !hasNewIdToken ? null : _idTokenPayload;
184
+ }
185
+ if (
186
+ currentDatabaseElement.resource &&
187
+ isAuthorizationCode &&
188
+ (!hasNewIdToken ||
189
+ typeof tokens.idTokenPayload?.sub !== 'string' ||
190
+ !tokens.idTokenPayload.sub ||
191
+ (currentDatabaseElement.nonce &&
192
+ tokens.idTokenPayload.nonce !== currentDatabaseElement.nonce.nonce))
193
+ ) {
194
+ throw new Error(
195
+ 'Resource authorization requires a fresh ID token with the expected subject and nonce.',
196
+ );
160
197
  }
161
198
  if (tokens.refresh_token) {
162
- secureTokens.refresh_token = `${TOKEN.REFRESH_TOKEN}_${configurationName}`;
199
+ secureTokens.refresh_token = `${TOKEN.REFRESH_TOKEN}_${(currentDatabaseElement.refreshGrant ?? currentDatabaseElement).configurationName}`;
163
200
  }
164
201
 
165
202
  tokens.issued_at = extractedIssueAt(tokens, accessTokenPayload, _idTokenPayload);
@@ -175,8 +212,9 @@ function _hideTokens(
175
212
  : tokens.issued_at + expireIn;
176
213
 
177
214
  let expiresAt: number;
178
- const tokenRenewMode = (currentDatabaseElement.oidcConfiguration as OidcConfiguration)
179
- .token_renew_mode;
215
+ const tokenRenewMode = currentDatabaseElement.resource
216
+ ? TokenRenewMode.access_token_invalid
217
+ : (currentDatabaseElement.oidcConfiguration as OidcConfiguration).token_renew_mode;
180
218
  if (tokenRenewMode === TokenRenewMode.access_token_invalid) {
181
219
  expiresAt = accessTokenExpiresAt;
182
220
  } else if (tokenRenewMode === TokenRenewMode.id_token_invalid) {
@@ -189,13 +227,63 @@ function _hideTokens(
189
227
  tokens.expiresAt = expiresAt;
190
228
  const nonce = currentDatabaseElement.nonce ? currentDatabaseElement.nonce.nonce : null;
191
229
  const { isValid, reason } = isTokensOidcValid(
192
- tokens,
230
+ currentDatabaseElement.resource && !hasNewIdToken
231
+ ? { ...tokens, idTokenPayload: undefined }
232
+ : tokens,
193
233
  nonce as string,
194
234
  currentDatabaseElement.oidcServerConfiguration as OidcServerConfiguration,
195
235
  ); // TODO: Type assertion, could be null.
196
236
  if (!isValid) {
197
237
  throw Error(`Tokens are not OpenID valid, reason: ${reason}`);
198
238
  }
239
+ if (currentDatabaseElement.resource) {
240
+ if (
241
+ typeof tokens.access_token !== 'string' ||
242
+ !tokens.access_token ||
243
+ !Number.isFinite(expiresAt) ||
244
+ !Number.isFinite(tokens.issued_at) ||
245
+ (tokens.refresh_token != null &&
246
+ (typeof tokens.refresh_token !== 'string' || !tokens.refresh_token))
247
+ ) {
248
+ throw new Error('The provider did not return a usable resource token response.');
249
+ }
250
+ const resource = getResourceConfiguration(
251
+ currentDatabaseElement.oidcConfiguration?.resources,
252
+ currentDatabaseElement.resource,
253
+ );
254
+ tokens.scope ??=
255
+ currentDatabaseElement.requestedScope ??
256
+ resource.scope ??
257
+ currentDatabaseElement.oidcConfiguration?.scope;
258
+ secureTokens.scope = tokens.scope;
259
+ if (
260
+ resource.scope &&
261
+ !resource.scope
262
+ .split(/\s+/)
263
+ .filter(Boolean)
264
+ .every(scope => (tokens.scope ?? '').split(/\s+/).includes(scope))
265
+ ) {
266
+ throw new Error('The provider did not grant the required resource scopes.');
267
+ }
268
+ const parentSubject = currentDatabaseElement.parentConfiguration?.tokens?.idTokenPayload?.sub;
269
+ const subject = tokens.idTokenPayload?.sub ?? oldTokens?.idTokenPayload?.sub ?? parentSubject;
270
+ if (typeof parentSubject !== 'string' || subject !== parentSubject) {
271
+ throw new Error('Resource authorization must preserve the authenticated subject.');
272
+ }
273
+ if (
274
+ (accessTokenPayload?.iss !== undefined &&
275
+ accessTokenPayload.iss !== currentDatabaseElement.oidcServerConfiguration?.issuer) ||
276
+ (accessTokenPayload &&
277
+ resource.audiences &&
278
+ !resource.audiences.some(audience =>
279
+ (typeof accessTokenPayload.aud === 'string'
280
+ ? [accessTokenPayload.aud]
281
+ : (accessTokenPayload.aud ?? [])
282
+ ).includes(audience),
283
+ ))
284
+ )
285
+ throw new Error('Resource token issuer or configured audience does not match.');
286
+ }
199
287
 
200
288
  // When refresh_token is not rotated we reuse old refresh_token
201
289
  if (oldTokens != null && 'refresh_token' in oldTokens && !('refresh_token' in tokens)) {
@@ -210,26 +298,85 @@ function _hideTokens(
210
298
  }
211
299
 
212
300
  currentDatabaseElement.status = 'LOGGED_IN';
301
+ if (currentDatabaseElement.resource) {
302
+ const grant = currentDatabaseElement.refreshGrant ?? currentDatabaseElement;
303
+ const subject =
304
+ tokens.idTokenPayload?.sub ??
305
+ grant.tokens?.idTokenPayload?.sub ??
306
+ currentDatabaseElement.parentConfiguration?.tokens?.idTokenPayload?.sub;
307
+ if (typeof subject !== 'string')
308
+ throw new Error('Resource tokens require an authenticated OIDC subject.');
309
+ if (grant !== currentDatabaseElement && tokens.refresh_token && grant.tokens) {
310
+ grant.tokens.refresh_token = tokens.refresh_token;
311
+ }
312
+ const resourceContext = {
313
+ resource: currentDatabaseElement.resource,
314
+ issuer: currentDatabaseElement.oidcServerConfiguration!.issuer,
315
+ clientId: currentDatabaseElement.oidcConfiguration!.client_id!,
316
+ configurationName: currentDatabaseElement.configurationName.split('#')[0],
317
+ subject,
318
+ grantResource: grant.resource ?? null,
319
+ };
320
+ currentDatabaseElement.tokens!.resourceContext = resourceContext;
321
+ secureTokens.resourceContext = resourceContext;
322
+ }
213
323
  return secureTokens;
214
324
  }
215
325
 
216
326
  const demonstratingProofOfPossessionNonceResponseHeader = 'DPoP-Nonce';
217
- function hideTokens(currentDatabaseElement: OidcConfig) {
327
+ function hideTokens(
328
+ currentDatabaseElement: OidcConfig,
329
+ isCurrent: () => boolean = () => true,
330
+ isGrantCurrent: () => boolean = isCurrent,
331
+ isAuthorizationCode = false,
332
+ ) {
218
333
  const configurationName = currentDatabaseElement.configurationName;
219
334
  return (response: Response) => {
220
335
  if (response.status !== 200) {
221
336
  return response;
222
337
  }
223
338
  const newHeaders = new Headers(response.headers);
224
- if (response.headers.has(demonstratingProofOfPossessionNonceResponseHeader)) {
339
+ if (
340
+ response.headers.has(demonstratingProofOfPossessionNonceResponseHeader) &&
341
+ isGrantCurrent()
342
+ ) {
225
343
  currentDatabaseElement.demonstratingProofOfPossessionNonce = response.headers.get(
226
344
  demonstratingProofOfPossessionNonceResponseHeader,
227
345
  );
346
+ if (
347
+ currentDatabaseElement.refreshGrant &&
348
+ currentDatabaseElement.refreshGrant !== currentDatabaseElement
349
+ ) {
350
+ currentDatabaseElement.refreshGrant.demonstratingProofOfPossessionNonce =
351
+ currentDatabaseElement.demonstratingProofOfPossessionNonce;
352
+ }
228
353
  newHeaders.delete(demonstratingProofOfPossessionNonceResponseHeader);
229
354
  }
230
355
 
231
356
  return response.json().then<Response>((tokens: Tokens) => {
232
- const secureTokens = _hideTokens(tokens, currentDatabaseElement, configurationName);
357
+ const grant = currentDatabaseElement.refreshGrant;
358
+ if (
359
+ currentDatabaseElement.resource &&
360
+ grant &&
361
+ isGrantCurrent() &&
362
+ grant.tokens &&
363
+ typeof tokens.refresh_token === 'string' &&
364
+ tokens.refresh_token &&
365
+ (grant !== currentDatabaseElement ||
366
+ tokens.refresh_token !==
367
+ currentDatabaseElement.parentConfiguration?.tokens?.refresh_token)
368
+ ) {
369
+ grant.tokens.refresh_token = tokens.refresh_token;
370
+ }
371
+ if (!isCurrent()) {
372
+ throw new Error('The token transaction was cleared before its response.');
373
+ }
374
+ const secureTokens = _hideTokens(
375
+ tokens,
376
+ currentDatabaseElement,
377
+ configurationName,
378
+ isAuthorizationCode,
379
+ );
233
380
  const body = JSON.stringify(secureTokens);
234
381
  return new Response(body, {
235
382
  status: response.status,
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export default '7.29.7';
1
+ export default '7.30.0';
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=OidcServiceWorker.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"OidcServiceWorker.d.ts","sourceRoot":"","sources":["../../src/OidcServiceWorker.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=jwt.spec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"jwt.spec.d.ts","sourceRoot":"","sources":["../../../src/__tests__/jwt.spec.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=oidcConfig.spec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"oidcConfig.spec.d.ts","sourceRoot":"","sources":["../../../src/__tests__/oidcConfig.spec.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=protocol.spec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"protocol.spec.d.ts","sourceRoot":"","sources":["../../../src/__tests__/protocol.spec.ts"],"names":[],"mappings":""}
@@ -1 +0,0 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,cAAc,0BAA0B,CAAC;AAC/C,QAAA,MAAM,oBAAoB,MAAM,CAAC;AAEjC,KAAK,SAAS,GAAG;IACf,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,QAAA,MAAM,KAAK,EAAE,SAKZ,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACxB,QAAQ,CAAC,gCAAgC,EAAE,MAAM,CAAC;IAClD,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC,CAAC;AAEF,QAAA,MAAM,cAAc,EAAE,kBAIrB,CAAC;AAEF,QAAA,MAAM,yBAAyB,sCAAsC,CAAC;AAEtE,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,cAAc,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/crypto.ts"],"names":[],"mappings":"AAEA,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,2BAQzC;AAED,wBAAgB,mCAAmC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CASjF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"dpop.d.ts","sourceRoot":"","sources":["../../src/dpop.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AASnD,eAAO,MAAM,oBAAoB,GAAI,eAAe,MAAM,EAAE,GAAG,aAAa,4EAa3E,CAAC;AAEF,eAAO,MAAM,gCAAgC,GAAI,eAAe,MAAM,EAAE,GAAG,aAAa,mBAUvF,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/jwt.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,2CAA2C,EAAE,MAAM,SAAS,CAAC;AAiCtE,eAAO,MAAM,gBAAgB,GAAI,OAAO,UAAU,WAOjD,CAAC;AAUF,eAAO,MAAM,kDAAkD,EAAE,2CAc9D,CAAC;AAuEJ,eAAO,MAAM,GAAG;cAnEV,GAAG,MAEL,KAAK,GAAG,EACR,SAAS,GAAG,EACZ,QAAQ,GAAG,EACX,6CAA6C,2CAA2C,EACxF,sBAA0B;CA6DH,CAAC;AAqD5B,eAAO,MAAM,GAAG;oBAtBO,GAAG,MAAY,QAAG,EAAE,iBAAiB,mBAAmB;CAsB9C,CAAC;AAElC,eAAO,MAAM,gBAAgB,GAC1B,GAAG,GAAG,MAAY,sBAAsB,qBAAqB,GAAG,cAAc,iBAO9E,CAAC;AAEJ,eAAO,MAAM,8CAA8C,GACxD,GAAG,GAAG,MACN,6CAA6C,2CAA2C,MAClF,KAAK,GAAG,EAAE,0BAAe,EAAE,KAAK,MAAM,EAAE,iBAAiB,oBAuB/D,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"oidcConfig.d.ts","sourceRoot":"","sources":["../../src/oidcConfig.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAU,UAAU,EAA2B,cAAc,EAAE,MAAM,SAAS,CAAC;AAehG,QAAA,MAAM,mBAAmB,GAAI,UAAU,QAAQ,EAAE,eAAe,MAAM,KAAG,OAWxE,CAAC;AAUF,QAAA,MAAM,0BAA0B,GAC9B,UAAU,QAAQ,EAClB,eAAe,MAAM,EACrB,gBAAgB,cAAc,KAC7B,OAeF,CAAC;AAEF,QAAA,MAAM,0BAA0B,GAC9B,UAAU,QAAQ,EAClB,eAAe,MAAM,EACrB,gBAAgB,cAAc,KAC7B,OAmBF,CAAC;AAEF,QAAA,MAAM,6BAA6B,GAAI,UAAU,QAAQ,EAAE,KAAK,MAAM,KAAG,UAAU,EAWlF,CAAC;AAEF,OAAO,EACL,6BAA6B,IAAI,gCAAgC,EACjE,0BAA0B,EAC1B,mBAAmB,EACnB,0BAA0B,GAC3B,CAAC"}