oidc-spa 8.6.10 → 8.6.11

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.
@@ -2,6 +2,8 @@ import { OidcInitializationError } from "./OidcInitializationError";
2
2
  import { isKeycloak, createKeycloakUtils } from "../keycloak";
3
3
  import { getIsValidRemoteJson } from "../tools/getIsValidRemoteJson";
4
4
  import { WELL_KNOWN_PATH } from "./OidcMetadata";
5
+ import { assert } from "../tools/tsafe/assert";
6
+ import { getDoMatchWildcardsPattern } from "../tools/wildcardsMatch";
5
7
 
6
8
  export async function createWellKnownOidcConfigurationEndpointUnreachableInitializationError(params: {
7
9
  issuerUri: string;
@@ -90,8 +92,9 @@ export async function createIframeTimeoutInitializationError(params: {
90
92
  redirectUri: string;
91
93
  issuerUri: string;
92
94
  clientId: string;
95
+ authorizationEndpointUrl: string;
93
96
  }): Promise<OidcInitializationError> {
94
- const { redirectUri, issuerUri, clientId } = params;
97
+ const { redirectUri, issuerUri, clientId, authorizationEndpointUrl } = params;
95
98
 
96
99
  check_if_well_known_endpoint_is_reachable: {
97
100
  const isValid = await getIsValidRemoteJson(`${issuerUri}${WELL_KNOWN_PATH}`);
@@ -103,7 +106,8 @@ export async function createIframeTimeoutInitializationError(params: {
103
106
  return createWellKnownOidcConfigurationEndpointUnreachableInitializationError({ issuerUri });
104
107
  }
105
108
 
106
- iframe_blocked: {
109
+ // Investigate if framing was prevented by some header defined policies
110
+ {
107
111
  const headersOrError = await fetch(redirectUri).then(
108
112
  response => {
109
113
  if (!response.ok) {
@@ -131,62 +135,165 @@ export async function createIframeTimeoutInitializationError(params: {
131
135
 
132
136
  const headers = headersOrError;
133
137
 
134
- let key_problem = (() => {
135
- block: {
136
- const key = "Content-Security-Policy" as const;
138
+ content_security_policy_issue: {
139
+ const cspHeaderValue = headers["Content-Security-Policy"];
137
140
 
138
- const header = headers[key];
141
+ if (cspHeaderValue === null) {
142
+ break content_security_policy_issue;
143
+ }
139
144
 
140
- if (header === null) {
141
- break block;
145
+ const csp_parsed: Record<string, string[] | undefined> = Object.fromEntries(
146
+ cspHeaderValue
147
+ .split(";")
148
+ .filter(part => part !== "")
149
+ .map(statement => {
150
+ const [directive, ...values] = statement.split(" ");
151
+ assert(directive !== undefined);
152
+ assert(values.length !== 0);
153
+ return [directive, values];
154
+ })
155
+ );
156
+
157
+ frame_src_issue: {
158
+ const frameSrcValues = csp_parsed["frame-src"];
159
+
160
+ if (frameSrcValues === undefined) {
161
+ break frame_src_issue;
142
162
  }
143
163
 
144
- const hasFrameAncestorsNone = header
145
- .replace(/["']/g, "")
146
- .replace(/\s+/g, " ")
147
- .toLowerCase()
148
- .includes("frame-ancestors none");
149
-
150
- if (!hasFrameAncestorsNone) {
151
- break block;
164
+ const hasIssue = (() => {
165
+ for (const frameSrcValue of frameSrcValues) {
166
+ if (frameSrcValue === "'none'") {
167
+ return true;
168
+ }
169
+
170
+ const origin_authorizationEndpoint = new URL(authorizationEndpointUrl).origin;
171
+
172
+ if (frameSrcValue === "'self'") {
173
+ const origin_app = new URL(location.href).origin;
174
+
175
+ if (origin_app === origin_authorizationEndpoint) {
176
+ return false;
177
+ }
178
+ }
179
+
180
+ if (
181
+ getDoMatchWildcardsPattern({
182
+ candidate: origin_authorizationEndpoint,
183
+ stringWithWildcards: frameSrcValue
184
+ })
185
+ ) {
186
+ return false;
187
+ }
188
+ }
189
+
190
+ return true;
191
+ })();
192
+
193
+ if (!hasIssue) {
194
+ break frame_src_issue;
152
195
  }
153
196
 
154
- return key;
197
+ const recommendedValue = (() => {
198
+ const hostname_app = new URL(location.href).hostname;
199
+ const {
200
+ hostname: hostname_authorizationEndpoint,
201
+ origin: origin_authorizationEndpoint
202
+ } = new URL(authorizationEndpointUrl);
203
+
204
+ if (hostname_app === hostname_authorizationEndpoint) {
205
+ return "'self'";
206
+ }
207
+
208
+ const [lvl1, lvl2] = hostname_app.split(".").reverse();
209
+
210
+ if (!lvl2) {
211
+ return origin_authorizationEndpoint;
212
+ }
213
+
214
+ if (hostname_authorizationEndpoint.endsWith(`.${lvl2}.${lvl1}`)) {
215
+ return `https://*.${lvl2}.${lvl1}`;
216
+ }
217
+
218
+ return origin_authorizationEndpoint;
219
+ })();
220
+
221
+ return new OidcInitializationError({
222
+ isAuthServerLikelyDown: false,
223
+ messageOrCause: [
224
+ `Session restoration via iframe failed due to the following HTTP header on GET ${redirectUri}:`,
225
+ `\nContent-Security-Policy “frame-src”: ${frameSrcValues.join("; ")}`,
226
+ `\nThis header prevents opening an iframe to ${authorizationEndpointUrl}.`,
227
+ `\nTo fix this:`,
228
+ `\n - Update your CSP to: frame-src ${[
229
+ ...frameSrcValues.filter(v => v !== "'none'"),
230
+ recommendedValue
231
+ ]}`,
232
+ `\n - OR remove the frame-src directive from your CSP`,
233
+ `\n - OR, if you cannot change your CSP, call bootstrapOidc/createOidc with sessionRestorationMethod: "full page redirect"`,
234
+ `\n\nMore info: https://docs.oidc-spa.dev/v/v8/resources/csp-configuration`
235
+ ].join(" ")
236
+ });
155
237
  }
156
238
 
157
- block: {
158
- const key = "X-Frame-Options" as const;
159
-
160
- const header = headers[key];
239
+ frame_ancestor_issue: {
240
+ const frameAncestorsValues = csp_parsed["frame-ancestors"];
161
241
 
162
- if (header === null) {
163
- break block;
242
+ if (frameAncestorsValues === undefined) {
243
+ break frame_ancestor_issue;
164
244
  }
165
245
 
166
- const hasFrameAncestorsNone = header.toLowerCase().includes("deny");
246
+ const hasIssue =
247
+ frameAncestorsValues.includes("'none'") || !frameAncestorsValues.includes("'self'");
167
248
 
168
- if (!hasFrameAncestorsNone) {
169
- break block;
249
+ if (!hasIssue) {
250
+ break frame_ancestor_issue;
170
251
  }
171
252
 
172
- return key;
253
+ return new OidcInitializationError({
254
+ isAuthServerLikelyDown: false,
255
+ messageOrCause: [
256
+ `Session restoration via iframe failed due to the following HTTP header on GET ${redirectUri}:`,
257
+ `\nContent-Security-Policy “frame-ancestors”: ${frameAncestorsValues.join(
258
+ "; "
259
+ )}`,
260
+ `\nThis header prevents your app from being iframed by itself.`,
261
+ `\nTo fix this:`,
262
+ `\n - Update your CSP to: frame-ancestors 'self'`,
263
+ `\n - OR remove the frame-ancestors directive from your CSP`,
264
+ `\n - OR, if you cannot modify your CSP, call bootstrapOidc/createOidc with sessionRestorationMethod: "full page redirect"`,
265
+ `\n\nMore info: https://docs.oidc-spa.dev/v/v8/resources/csp-configuration`
266
+ ].join(" ")
267
+ });
173
268
  }
269
+ }
174
270
 
175
- return undefined;
176
- })();
271
+ x_frame_option_header_issue: {
272
+ const key = "X-Frame-Options" as const;
177
273
 
178
- if (key_problem === undefined) {
179
- break iframe_blocked;
180
- }
274
+ const value = headers[key];
181
275
 
182
- return new OidcInitializationError({
183
- isAuthServerLikelyDown: false,
184
- messageOrCause: [
185
- `${redirectUri} is currently served by your web server with the HTTP header \`${key_problem}: ${headers[key_problem]}\`.\n`,
186
- "This header prevents the silent sign-in process from working.\n",
187
- "Refer to this documentation page to fix this issue: https://docs.oidc-spa.dev/v/v8/resources/third-party-cookies-and-session-restoration"
188
- ].join(" ")
189
- });
276
+ if (value === null) {
277
+ break x_frame_option_header_issue;
278
+ }
279
+
280
+ const hasFrameAncestorsNone = value.toLowerCase().includes("deny");
281
+
282
+ if (!hasFrameAncestorsNone) {
283
+ break x_frame_option_header_issue;
284
+ }
285
+
286
+ return new OidcInitializationError({
287
+ isAuthServerLikelyDown: false,
288
+ messageOrCause: [
289
+ `Session restoration via iframe failed due to the following HTTP header on GET ${redirectUri}:`,
290
+ `\n${key}: ${value}`,
291
+ `\nThis header prevents your app from being framed by itself.`,
292
+ `\nTo fix this, remove the ${key} header and rely on Content-Security-Policy if you need to restrict framing.`,
293
+ `\n\nMore info: https://docs.oidc-spa.dev/v/v8/resources/csp-configuration`
294
+ ].join(" ")
295
+ });
296
+ }
190
297
  }
191
298
 
192
299
  // Here we know that the server is not down and that the issuer_uri is correct
@@ -225,8 +332,8 @@ export async function createIframeTimeoutInitializationError(params: {
225
332
  ];
226
333
  })(),
227
334
  "\n\n",
228
- "If nothing works, you can try disabling the use of iframe: https://docs.oidc-spa.dev/resources/iframe-related-issues\n",
229
- "with some OIDC provider it might solve the issue."
335
+ `If nothing works, or if you see in the console a message mentioning 'refused to frame' there might be a problem with your CSP.`,
336
+ `Read more: https://docs.oidc-spa.dev/v/v8/resources/csp-configuration`
230
337
  ].join(" ")
231
338
  });
232
339
  }
@@ -0,0 +1,16 @@
1
+ export function getDoMatchWildcardsPattern(params: {
2
+ stringWithWildcards: string;
3
+ candidate: string;
4
+ }): boolean {
5
+ const { stringWithWildcards, candidate } = params;
6
+
7
+ if (!stringWithWildcards.includes("*")) {
8
+ return stringWithWildcards === candidate;
9
+ }
10
+
11
+ const escapedRegex = stringWithWildcards
12
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
13
+ .replace(/\\\*/g, ".*");
14
+
15
+ return new RegExp(`^${escapedRegex}$`).test(candidate);
16
+ }
@@ -0,0 +1,4 @@
1
+ export declare function getDoMatchWildcardsPattern(params: {
2
+ stringWithWildcards: string;
3
+ candidate: string;
4
+ }): boolean;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDoMatchWildcardsPattern = getDoMatchWildcardsPattern;
4
+ function getDoMatchWildcardsPattern(params) {
5
+ const { stringWithWildcards, candidate } = params;
6
+ if (!stringWithWildcards.includes("*")) {
7
+ return stringWithWildcards === candidate;
8
+ }
9
+ const escapedRegex = stringWithWildcards
10
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
11
+ .replace(/\\\*/g, ".*");
12
+ return new RegExp(`^${escapedRegex}$`).test(candidate);
13
+ }
14
+ //# sourceMappingURL=wildcardsMatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wildcardsMatch.js","sourceRoot":"","sources":["../src/tools/wildcardsMatch.ts"],"names":[],"mappings":";;AAAA,gEAeC;AAfD,SAAgB,0BAA0B,CAAC,MAG1C;IACG,MAAM,EAAE,mBAAmB,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC;IAElD,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,OAAO,mBAAmB,KAAK,SAAS,CAAC;IAC7C,CAAC;IAED,MAAM,YAAY,GAAG,mBAAmB;SACnC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC;SACtC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAE5B,OAAO,IAAI,MAAM,CAAC,IAAI,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAC3D,CAAC"}