@o3co/create-auth-provider 0.17.0 → 0.18.0-rc.1

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 (114) hide show
  1. package/README.md +2 -198
  2. package/main.js +4 -0
  3. package/package.json +8 -30
  4. package/README.ja.md +0 -200
  5. package/dist/cli.d.mts +0 -3
  6. package/dist/cli.d.mts.map +0 -1
  7. package/dist/cli.mjs +0 -18
  8. package/dist/index.d.mts +0 -48
  9. package/dist/index.d.mts.map +0 -1
  10. package/dist/index.mjs +0 -325
  11. package/dist/internal/template-filter.d.mts +0 -14
  12. package/dist/internal/template-filter.d.mts.map +0 -1
  13. package/dist/internal/template-filter.mjs +0 -42
  14. package/templates/standalone/.dockerignore +0 -23
  15. package/templates/standalone/.env.example +0 -152
  16. package/templates/standalone/Dockerfile +0 -125
  17. package/templates/standalone/Makefile +0 -12
  18. package/templates/standalone/README.ja.md +0 -912
  19. package/templates/standalone/README.md +0 -1536
  20. package/templates/standalone/config/application.conf +0 -351
  21. package/templates/standalone/config/clients.yaml +0 -4
  22. package/templates/standalone/config/clients.yaml.example +0 -124
  23. package/templates/standalone/config/development.conf +0 -17
  24. package/templates/standalone/config/mailpit.conf +0 -13
  25. package/templates/standalone/config/production.conf +0 -15
  26. package/templates/standalone/config/reference.conf +0 -427
  27. package/templates/standalone/docker-compose.mailpit.yml +0 -39
  28. package/templates/standalone/docker-compose.production.yml +0 -229
  29. package/templates/standalone/docker-compose.test.yml +0 -16
  30. package/templates/standalone/docker-compose.yml +0 -67
  31. package/templates/standalone/gitignore +0 -23
  32. package/templates/standalone/package.json +0 -56
  33. package/templates/standalone/src/__tests__/adapters.test.mts +0 -425
  34. package/templates/standalone/src/__tests__/admission-lifecycle.test.mts +0 -87
  35. package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +0 -1188
  36. package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +0 -285
  37. package/templates/standalone/src/__tests__/all-modules-composition.test.mts +0 -1154
  38. package/templates/standalone/src/__tests__/audit-sink.test.mts +0 -355
  39. package/templates/standalone/src/__tests__/backchannel-peer.fixture.mts +0 -64
  40. package/templates/standalone/src/__tests__/config-notices.test.mts +0 -157
  41. package/templates/standalone/src/__tests__/configPath.test.mts +0 -69
  42. package/templates/standalone/src/__tests__/configured-mfa-mode.fixture.mts +0 -48
  43. package/templates/standalone/src/__tests__/core-federations.test.mts +0 -252
  44. package/templates/standalone/src/__tests__/core-sections.test.mts +0 -192
  45. package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +0 -95
  46. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +0 -1291
  47. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +0 -787
  48. package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +0 -464
  49. package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +0 -141
  50. package/templates/standalone/src/__tests__/federation-types.test.mts +0 -239
  51. package/templates/standalone/src/__tests__/frontchannel-logout-csp.test.mts +0 -168
  52. package/templates/standalone/src/__tests__/google-session.fixture.mts +0 -318
  53. package/templates/standalone/src/__tests__/host-routes.test.mts +0 -134
  54. package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +0 -29
  55. package/templates/standalone/src/__tests__/library-references.fixture.mts +0 -188
  56. package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +0 -217
  57. package/templates/standalone/src/__tests__/listen.test.mts +0 -121
  58. package/templates/standalone/src/__tests__/logger-projection.test.mts +0 -77
  59. package/templates/standalone/src/__tests__/logout-session-cookie.test.mts +0 -113
  60. package/templates/standalone/src/__tests__/mail-sender.test.mts +0 -134
  61. package/templates/standalone/src/__tests__/mailpit-compose.test.mts +0 -393
  62. package/templates/standalone/src/__tests__/metrics.test.mts +0 -253
  63. package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +0 -122
  64. package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +0 -127
  65. package/templates/standalone/src/__tests__/mfa-switch.test.mts +0 -883
  66. package/templates/standalone/src/__tests__/own-modules.test.mts +0 -1480
  67. package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +0 -183
  68. package/templates/standalone/src/__tests__/redis-assume-noeviction.test.mts +0 -283
  69. package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +0 -135
  70. package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +0 -214
  71. package/templates/standalone/src/__tests__/redis-stand-in.fixture.mts +0 -98
  72. package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +0 -249
  73. package/templates/standalone/src/__tests__/replica-safety.test.mts +0 -657
  74. package/templates/standalone/src/__tests__/repositories-logger.test.mts +0 -119
  75. package/templates/standalone/src/__tests__/router-error-answers.test.mts +0 -146
  76. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +0 -610
  77. package/templates/standalone/src/__tests__/sections.test.mts +0 -516
  78. package/templates/standalone/src/__tests__/session-lifecycle.test.mts +0 -213
  79. package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +0 -305
  80. package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +0 -130
  81. package/templates/standalone/src/__tests__/session-requirements.test.mts +0 -382
  82. package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +0 -274
  83. package/templates/standalone/src/__tests__/shutdown.test.mts +0 -622
  84. package/templates/standalone/src/__tests__/smoke.test.mts +0 -642
  85. package/templates/standalone/src/__tests__/startup-log-lines.test.mts +0 -164
  86. package/templates/standalone/src/__tests__/store-credential.test.mts +0 -314
  87. package/templates/standalone/src/__tests__/supertest-loopback.test.mts +0 -296
  88. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +0 -265
  89. package/templates/standalone/src/__tests__/two-phase-config.test.mts +0 -605
  90. package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +0 -385
  91. package/templates/standalone/src/adapters.mts +0 -162
  92. package/templates/standalone/src/app.mts +0 -127
  93. package/templates/standalone/src/bootRefusal.mts +0 -117
  94. package/templates/standalone/src/buildModules.mts +0 -358
  95. package/templates/standalone/src/configPath.mts +0 -487
  96. package/templates/standalone/src/listen.mts +0 -60
  97. package/templates/standalone/src/logger.mts +0 -80
  98. package/templates/standalone/src/metrics.mts +0 -167
  99. package/templates/standalone/src/mfaSwitch.mts +0 -284
  100. package/templates/standalone/src/modules.mts +0 -705
  101. package/templates/standalone/src/rootRenames.mts +0 -95
  102. package/templates/standalone/src/routes.mts +0 -89
  103. package/templates/standalone/src/sections.mts +0 -378
  104. package/templates/standalone/src/securityHeaders.mts +0 -36
  105. package/templates/standalone/src/shutdown.mts +0 -213
  106. package/templates/standalone/tests/Makefile +0 -5
  107. package/templates/standalone/tests/index.test.js +0 -283
  108. package/templates/standalone/tests/package-lock.json +0 -1617
  109. package/templates/standalone/tests/package.json +0 -16
  110. package/templates/standalone/tests/vitest.config.mjs +0 -26
  111. package/templates/standalone/tsconfig.json +0 -31
  112. package/templates/standalone/vitest.config.mts +0 -31
  113. package/templates/standalone/vitest.supertest-loopback.mts +0 -278
  114. package/templates/versions.json +0 -20
@@ -1,239 +0,0 @@
1
- /*
2
- * Copyright 2026 1o1 Co. Ltd.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
-
17
- /**
18
- * The federations the template ships are handled by the modules of their
19
- * types. The template loads the federation types it bundles, `google` and
20
- * `oidc`, whatever the configuration enables, and core dispatches each
21
- * enabled `core.federations` entry to the module of its `type`, under the
22
- * entry's name: two entries of one type are two federations. The template
23
- * reads no entry before boot, so a malformed one is core's to refuse, at its
24
- * path.
25
- */
26
-
27
- import { mkdtempSync, readdirSync, writeFileSync } from "node:fs";
28
- import { tmpdir } from "node:os";
29
- import { join } from "node:path";
30
- import { fileURLToPath } from "node:url";
31
- import request from "supertest";
32
- import { afterEach, describe, expect, it } from "vitest";
33
- import { buildModules } from "#/buildModules.mjs";
34
- import { readOwnLayers, readSwitches, resolveConfigPaths, resolveLayers } from "#/configPath.mjs";
35
- import {
36
- type Composition,
37
- compose,
38
- ISSUER,
39
- ownFiles,
40
- resolveConfig,
41
- SINGLE_ENV,
42
- } from "./all-modules-composition.fixture.mjs";
43
-
44
- const configDir = fileURLToPath(new URL("../../config", import.meta.url));
45
-
46
- /** Google's authorization endpoint, where a Google federation's start sends the browser. */
47
- const GOOGLE_AUTHORIZE = "https://accounts.google.com/o/oauth2/v2/auth";
48
-
49
- let current: Composition | undefined;
50
-
51
- afterEach(async () => {
52
- await current?.handle.dispose();
53
- current = undefined;
54
- });
55
-
56
- /** Boots and remembers the composition, so `afterEach` disposes it. */
57
- async function boot(options: Parameters<typeof compose>[0] = {}): Promise<Composition> {
58
- current = await compose(options);
59
- return current;
60
- }
61
-
62
- /** What `compose` refuses under `options`. */
63
- async function refusal(options: Parameters<typeof compose>[0]): Promise<{
64
- readonly reason?: string;
65
- readonly message: string;
66
- }> {
67
- try {
68
- current = await compose(options);
69
- } catch (err) {
70
- return err as { reason?: string; message: string };
71
- }
72
- throw new Error("the composition booted");
73
- }
74
-
75
- /** `text` in a file of its own, for a layer above the composition's files. */
76
- function hoconFile(text: string): string {
77
- const file = join(mkdtempSync(join(tmpdir(), "federation-types-")), "operator.conf");
78
- writeFileSync(file, text);
79
- return file;
80
- }
81
-
82
- /** The module names `buildModules` lists for `env`, federation ones only. */
83
- const federationModules = (env: Readonly<Record<string, string>>): string[] =>
84
- buildModules(resolveConfig(env))
85
- .map((module) => module.name)
86
- .filter((name) => name.startsWith("federation-") && !name.startsWith("federation-grant"));
87
-
88
- /** Where the start of federation `name` sends the browser. */
89
- async function startOf(composed: Composition, name: string): Promise<URL> {
90
- const res = await request(composed.app).get(`/session/oauth/federation/${name}`);
91
- expect(res.status).toBe(302);
92
- return new URL(res.headers.location as string);
93
- }
94
-
95
- describe("the federation types the template bundles", () => {
96
- it("are listed whatever the configuration enables", () => {
97
- const off = {
98
- ...SINGLE_ENV,
99
- CORE_FEDERATIONS_GOOGLE_ENABLED: "false",
100
- CORE_FEDERATIONS_OIDC_ENABLED: "false",
101
- };
102
- expect(federationModules(off)).toEqual(["federation-google-type", "federation-oidc"]);
103
- expect(federationModules(SINGLE_ENV)).toEqual(["federation-google-type", "federation-oidc"]);
104
- });
105
-
106
- it("handle every entry the template ships, each of which names its type", () => {
107
- const { applicationConfPath } = resolveConfigPaths(configDir, "production");
108
- const resolved = resolveLayers(readOwnLayers([applicationConfPath], { env: {} }), []) as {
109
- core?: { federations?: Record<string, { type?: unknown }> };
110
- };
111
- const types = Object.fromEntries(
112
- Object.entries(resolved.core?.federations ?? {}).map(([name, entry]) => [name, entry.type]),
113
- );
114
- expect(types).toEqual({ google: "google", oidc: "oidc" });
115
- });
116
-
117
- it("boot the shipped Google federation, enabled, as the provider named google that its type built", async () => {
118
- const composed = await boot();
119
- const names = composed.modules.map((module) => module.name);
120
- expect(names).toContain("federation-google-type");
121
- expect(names).not.toContain("federation-google");
122
- expect(composed.handle.components.federationProviders?.get("google")?.name).toBe("google");
123
-
124
- const start = await startOf(composed, "google");
125
- expect(`${start.origin}${start.pathname}`).toBe(GOOGLE_AUTHORIZE);
126
- expect(start.searchParams.get("client_id")).toBe(SINGLE_ENV.CORE_FEDERATIONS_GOOGLE_CLIENT_ID);
127
- expect(start.searchParams.get("redirect_uri")).toBe(
128
- "http://localhost:3000/session/oauth/federation/google/callback",
129
- );
130
- });
131
-
132
- it("boot a second entry of type google, under its own name, beside the shipped one", async () => {
133
- const callbackURL = `${ISSUER}/session/oauth/federation/google-work/callback`;
134
- const composed = await boot({
135
- operatorHocon: `core.federations.google-work {
136
- enabled = true
137
- type = "google"
138
- clientId = "google-work-client"
139
- clientSecret = "google-work-secret"
140
- callbackURL = ${JSON.stringify(callbackURL)}
141
- }
142
- `,
143
- });
144
- const providers = composed.handle.components.federationProviders;
145
- expect([...(providers?.keys() ?? [])].sort()).toEqual(["google", "google-work", "oidc"]);
146
- expect(providers?.get("google-work")?.name).toBe("google-work");
147
-
148
- const work = await startOf(composed, "google-work");
149
- expect(`${work.origin}${work.pathname}`).toBe(GOOGLE_AUTHORIZE);
150
- expect(work.searchParams.get("client_id")).toBe("google-work-client");
151
- expect(work.searchParams.get("redirect_uri")).toBe(callbackURL);
152
- const shipped = await startOf(composed, "google");
153
- expect(shipped.searchParams.get("client_id")).toBe(
154
- SINGLE_ENV.CORE_FEDERATIONS_GOOGLE_CLIENT_ID,
155
- );
156
- });
157
-
158
- it("hand an entry to the type it names, not the type its name reads as", async () => {
159
- // `google` of type `oidc` is a generic OIDC federation named google.
160
- const composed = await boot({
161
- operatorHocon: `core.federations.google { type = "oidc", issuer = ${JSON.stringify(SINGLE_ENV.CORE_FEDERATIONS_OIDC_ISSUER)} }\n`,
162
- });
163
- const start = await startOf(composed, "google");
164
- expect(start.origin).toBe(SINGLE_ENV.CORE_FEDERATIONS_OIDC_ISSUER);
165
- expect(start.searchParams.get("client_id")).toBe(SINGLE_ENV.CORE_FEDERATIONS_GOOGLE_CLIENT_ID);
166
- });
167
-
168
- it("register the type of every entry each shipped configuration file writes", () => {
169
- const registered = new Set(
170
- buildModules(resolveConfig(SINGLE_ENV)).flatMap((module) =>
171
- Object.keys(module.contributes?.federationTypes ?? {}),
172
- ),
173
- );
174
- const written = readdirSync(configDir)
175
- .filter((file) => file.endsWith(".conf"))
176
- .flatMap((file) => {
177
- const resolved = resolveLayers(readOwnLayers([join(configDir, file)], { env: {} }), []) as {
178
- core?: { federations?: Record<string, { type?: unknown }> };
179
- };
180
- return Object.entries(resolved.core?.federations ?? {}).map(([name, entry]) => ({
181
- file,
182
- name,
183
- type: entry.type,
184
- }));
185
- });
186
- expect(written).not.toEqual([]);
187
- expect(written.filter(({ type }) => typeof type !== "string" || !registered.has(type))).toEqual(
188
- [],
189
- );
190
- });
191
- });
192
-
193
- describe("the template reads no federation entry before boot", () => {
194
- it("passes a malformed entry through phase one, and boot refuses it at its path", async () => {
195
- const operatorHocon = 'core.federations.okta { enabled = "maybe", type = "oidc" }\n';
196
- expect(() =>
197
- readSwitches(readOwnLayers([hoconFile(operatorHocon), ...ownFiles()], { env: SINGLE_ENV })),
198
- ).not.toThrow();
199
- const err = await refusal({ operatorHocon });
200
- expect(err.reason).toBe("config-validation-failed");
201
- expect(err.message).toContain("core.federations.okta.enabled");
202
- });
203
-
204
- it.each([
205
- ["accessType", '"sometimes"'],
206
- // A bare string where a list belongs: the natural typo.
207
- ["redirectAllowlist", '"https://app.test/home"'],
208
- ["sessionDomain", "42"],
209
- ["requireAuthorizationResponseIss", '"no"'],
210
- // Only an environment variable's string is read as a flag's spelling.
211
- ["requireAuthorizationResponseIss", "1"],
212
- ["requireAuthorizationResponseIss", "[true]"],
213
- ])(
214
- "leaves %s, a key of the entry's type, to the type's schema, which core refuses it by, at its path",
215
- async (key, value) => {
216
- const err = await refusal({ operatorHocon: `core.federations.google.${key} = ${value}\n` });
217
- expect(err.reason).toBe("config-validation-failed");
218
- expect(err.message).toContain(`core.federations.google.${key}`);
219
- },
220
- );
221
- });
222
-
223
- describe("an enabled Google federation's credentials", () => {
224
- it.each([
225
- ["unset", undefined],
226
- ["exported empty", ""],
227
- ])(
228
- "refuse the boot when the client secret is %s, at its path, quoting no value of the entry",
229
- async (_case, secret) => {
230
- const { CORE_FEDERATIONS_GOOGLE_CLIENT_SECRET: _shipped, ...env } = SINGLE_ENV;
231
- const err = await refusal({
232
- env: secret === undefined ? env : { ...env, CORE_FEDERATIONS_GOOGLE_CLIENT_SECRET: secret },
233
- });
234
- expect(err.reason).toBe("config-validation-failed");
235
- expect(err.message).toContain("core.federations.google.clientSecret");
236
- expect(err.message).not.toContain(SINGLE_ENV.CORE_FEDERATIONS_GOOGLE_CLIENT_ID as string);
237
- },
238
- );
239
- });
@@ -1,168 +0,0 @@
1
- /*
2
- * Copyright 2026 1o1 Co. Ltd.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
-
17
- /**
18
- * The front-channel logout page, served through the template behind its
19
- * security headers, works under the one policy the response carries: its
20
- * relying party's frame and its redirect are allowed, and nothing else is.
21
- * Every other answer keeps the template's policy.
22
- */
23
-
24
- import { createHash } from "node:crypto";
25
- import request from "supertest";
26
- import { describe, expect, it } from "vitest";
27
- import {
28
- authorize,
29
- basic,
30
- codeFrom,
31
- compose,
32
- login,
33
- PKCE,
34
- } from "./all-modules-composition.fixture.mjs";
35
-
36
- const RP = {
37
- id: "rp-fc",
38
- secret: "rp-fc-secret-long-enough",
39
- redirectUri: "https://rp-fc.test/cb",
40
- frontchannelLogoutUri: "https://rp-fc.test:8443/frontchannel-logout",
41
- postLogoutRedirectUri: "https://rp-fc.test/signed-out",
42
- } as const;
43
-
44
- /** A serialized policy as directive name → sources; a directive named twice fails. */
45
- const parsePolicy = (policy: string): Map<string, string[]> => {
46
- const directives = new Map<string, string[]>();
47
- for (const part of policy.split(";")) {
48
- const [name, ...sources] = part.trim().split(/\s+/);
49
- if (name === undefined || name === "") continue;
50
- if (directives.has(name)) throw new Error(`directive ${name} appears twice`);
51
- directives.set(name, sources);
52
- }
53
- return directives;
54
- };
55
-
56
- /** The sources a fetch directive resolves to: its own, else `default-src`'s. */
57
- const effective = (policy: Map<string, string[]>, directive: string): string[] =>
58
- policy.get(directive) ?? policy.get("default-src") ?? [];
59
-
60
- const decode = (value: string): string =>
61
- value
62
- .replace(/&quot;/g, '"')
63
- .replace(/&#39;/g, "'")
64
- .replace(/&lt;/g, "<")
65
- .replace(/&gt;/g, ">")
66
- .replace(/&amp;/g, "&");
67
-
68
- /** How many `Content-Security-Policy` header lines the response carried, as sent. */
69
- const policyHeaderCount = (response: object): number => {
70
- const raw = (response as { res?: { rawHeaders?: unknown } }).res?.rawHeaders;
71
- if (!Array.isArray(raw)) throw new Error("the response carries no raw headers");
72
- return raw.filter(
73
- (name, i) => i % 2 === 0 && String(name).toLowerCase() === "content-security-policy",
74
- ).length;
75
- };
76
-
77
- describe("the front-channel logout page under the template's security headers", () => {
78
- it("is allowed its relying party's frame and its redirect by the one policy it is served with", async () => {
79
- const { app, handle } = await compose({
80
- extraClients: {
81
- [RP.id]: {
82
- tokenEndpointAuthMethod: "client_secret_basic",
83
- clientSecret: RP.secret,
84
- allowedRedirectUris: [RP.redirectUri],
85
- allowedScopes: ["openid"],
86
- allowedGrantTypes: ["authorization_code"],
87
- frontchannelLogoutUri: RP.frontchannelLogoutUri,
88
- postLogoutRedirectUris: [RP.postLogoutRedirectUri],
89
- firstParty: true,
90
- },
91
- },
92
- });
93
- try {
94
- // A session the relying party joined by exchanging a code.
95
- const { cookies } = await login(app);
96
- const exchanged = await request(app)
97
- .post("/oauth/token")
98
- .set("Authorization", basic(RP))
99
- .type("form")
100
- .send({
101
- grant_type: "authorization_code",
102
- code: codeFrom(await authorize(app, cookies, RP)),
103
- redirect_uri: RP.redirectUri,
104
- code_verifier: PKCE.verifier,
105
- });
106
- expect(exchanged.status).toBe(200);
107
-
108
- const res = await request(app)
109
- .get("/oauth/logout")
110
- .set("Accept", "text/html")
111
- .set("Cookie", cookies)
112
- .query({
113
- id_token_hint: exchanged.body.id_token as string,
114
- post_logout_redirect_uri: RP.postLogoutRedirectUri,
115
- state: "s-1",
116
- });
117
-
118
- expect(res.status).toBe(200);
119
- expect(res.headers["content-type"]).toBe("text/html; charset=utf-8");
120
- expect(policyHeaderCount(res)).toBe(1);
121
- const policy = parsePolicy(res.headers["content-security-policy"] as string);
122
-
123
- // The frame: the relying party's, from the one origin the policy allows.
124
- const srcs = [...res.text.matchAll(/<iframe src="([^"]*)"/g)].map((m) => decode(m[1] ?? ""));
125
- expect(srcs).toHaveLength(1);
126
- expect(new URL(srcs[0] ?? "").origin).toBe("https://rp-fc.test:8443");
127
- expect(effective(policy, "frame-src")).toEqual(["https://rp-fc.test:8443"]);
128
-
129
- // The redirect: one inline script, allowed by its hash, sending the
130
- // browser to the registered URI with the state.
131
- const scripts = [
132
- ...res.text.matchAll(/<script data-target="([^"]*)" data-delay="(\d+)">([^<]*)<\/script>/g),
133
- ];
134
- expect(scripts).toHaveLength(1);
135
- expect(res.text.match(/<script/g)).toHaveLength(1);
136
- const [, target, , text] = scripts[0] ?? [];
137
- expect(decode(target ?? "")).toBe(`${RP.postLogoutRedirectUri}?state=s-1`);
138
- const hash = createHash("sha256")
139
- .update(text ?? "", "utf8")
140
- .digest("base64");
141
- expect(effective(policy, "script-src")).toEqual([`'sha256-${hash}'`]);
142
-
143
- // Nothing else.
144
- expect(policy.get("default-src")).toEqual(["'none'"]);
145
- expect(effective(policy, "style-src")).toEqual(["'none'"]);
146
- expect(effective(policy, "connect-src")).toEqual(["'none'"]);
147
- expect(policy.get("frame-ancestors")).toEqual(["'none'"]);
148
- expect(res.headers["cache-control"]).toBe("no-store");
149
- } finally {
150
- await handle.dispose();
151
- }
152
- });
153
-
154
- it("leaves the template's policy on every other answer", async () => {
155
- const { app, handle } = await compose();
156
- try {
157
- const res = await request(app).get("/.well-known/openid-configuration");
158
-
159
- expect(policyHeaderCount(res)).toBe(1);
160
- const policy = parsePolicy(res.headers["content-security-policy"] as string);
161
- expect(policy.get("default-src")).toEqual(["'none'"]);
162
- expect(policy.get("frame-ancestors")).toEqual(["'none'"]);
163
- expect(effective(policy, "frame-src")).toEqual(["'none'"]);
164
- } finally {
165
- await handle.dispose();
166
- }
167
- });
168
- });