@o3co/create-auth-provider 0.9.0 → 0.10.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 (31) hide show
  1. package/dist/index.d.mts +20 -0
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +99 -6
  4. package/package.json +4 -4
  5. package/templates/standalone/.env.example +43 -4
  6. package/templates/standalone/Dockerfile +51 -16
  7. package/templates/standalone/README.ja.md +79 -12
  8. package/templates/standalone/README.md +345 -17
  9. package/templates/standalone/config/application.conf +121 -4
  10. package/templates/standalone/config/clients.yaml.example +41 -0
  11. package/templates/standalone/docker-compose.production.yml +116 -0
  12. package/templates/standalone/package.json +14 -18
  13. package/templates/standalone/src/__tests__/audit-sink.test.mts +343 -0
  14. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +419 -0
  15. package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +114 -0
  16. package/templates/standalone/src/__tests__/metrics.test.mts +231 -0
  17. package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +76 -1
  18. package/templates/standalone/src/__tests__/smoke.test.mts +177 -11
  19. package/templates/standalone/src/__tests__/terminal-error.test.mts +96 -0
  20. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +117 -7
  21. package/templates/standalone/src/app.mts +67 -8
  22. package/templates/standalone/src/buildModules.mts +43 -1
  23. package/templates/standalone/src/configPath.mts +2 -2
  24. package/templates/standalone/src/logger.mts +90 -2
  25. package/templates/standalone/src/metrics.mts +190 -0
  26. package/templates/standalone/src/modules.mts +281 -33
  27. package/templates/standalone/src/terminalError.mts +60 -0
  28. package/templates/standalone/tests/index.test.js +69 -5
  29. package/templates/standalone/tsconfig.json +9 -1
  30. package/templates/standalone/vitest.config.mts +8 -0
  31. package/templates/versions.json +11 -11
@@ -29,6 +29,23 @@ const client = axios.create({
29
29
 
30
30
  const _extractCookies = (res) => (res.headers["set-cookie"] ?? []).join("; ");
31
31
 
32
+ /**
33
+ * The server's own origin, as a browser would report it.
34
+ *
35
+ * Since #272 `POST /session/login` and `POST /session/logout` reject a request
36
+ * that carries neither a same-origin `Origin`/`Referer` nor a valid
37
+ * double-submit CSRF token. A browser sets `Origin` on its own; a scripted
38
+ * client like this one has to say it.
39
+ */
40
+ const SERVER_ORIGIN = new URL(BASE_URL).origin;
41
+
42
+ /** POST to a state-changing session route the way a same-origin browser would. */
43
+ const sessionPost = (path, data, config = {}) =>
44
+ client.post(path, data, {
45
+ ...config,
46
+ headers: { Origin: SERVER_ORIGIN, ...(config.headers ?? {}) },
47
+ });
48
+
32
49
  describe("POST /oauth/introspect", () => {
33
50
  // Helper: self-introspect sends the token as both Bearer credential and body parameter
34
51
  const selfIntrospect = (token, contentType = "application/json") => {
@@ -109,12 +126,12 @@ describe("POST /oauth/introspect", () => {
109
126
 
110
127
  describe("POST /session/login", () => {
111
128
  it("returns 400 when no credentials are provided", async () => {
112
- const res = await client.post("/session/login", {});
129
+ const res = await sessionPost("/session/login", {});
113
130
  expect(res.status).toBe(400);
114
131
  });
115
132
 
116
133
  it("returns 400 when redirect_to has an invalid scheme", async () => {
117
- const res = await client.post("/session/login", {
134
+ const res = await sessionPost("/session/login", {
118
135
  username: "testuser",
119
136
  password: "testpass",
120
137
  redirect_to: "javascript:alert(1)",
@@ -124,7 +141,7 @@ describe("POST /session/login", () => {
124
141
  });
125
142
 
126
143
  it("returns 400 when redirect_to is not a valid URL", async () => {
127
- const res = await client.post("/session/login", {
144
+ const res = await sessionPost("/session/login", {
128
145
  username: "testuser",
129
146
  password: "testpass",
130
147
  redirect_to: "not-a-url",
@@ -134,7 +151,7 @@ describe("POST /session/login", () => {
134
151
  });
135
152
 
136
153
  it("returns 400 when redirect_to exceeds 2048 characters", async () => {
137
- const res = await client.post("/session/login", {
154
+ const res = await sessionPost("/session/login", {
138
155
  username: "testuser",
139
156
  password: "testpass",
140
157
  redirect_to: `https://example.com/${"a".repeat(2048)}`,
@@ -147,7 +164,7 @@ describe("POST /session/login", () => {
147
164
  // redirect_to validation runs before authentication, so even with
148
165
  // invalid credentials, a valid redirect_to should not cause a 400
149
166
  // with error: "invalid_redirect". It should fail with 401 (auth failure).
150
- const res = await client.post("/session/login", {
167
+ const res = await sessionPost("/session/login", {
151
168
  username: "testuser",
152
169
  password: "testpass",
153
170
  redirect_to: "http://localhost:3000/oauth/authorize",
@@ -157,6 +174,53 @@ describe("POST /session/login", () => {
157
174
  });
158
175
  });
159
176
 
177
+ describe("CSRF on the state-changing session routes (#272)", () => {
178
+ it("rejects a login carrying neither an origin signal nor a token", async () => {
179
+ // The bypass this replaced: omitting `Origin` skipped the check entirely.
180
+ const res = await client.post("/session/login", {
181
+ username: "testuser",
182
+ password: "testpass",
183
+ });
184
+ expect(res.status).toBe(403);
185
+ expect(res.data.error).toBe("access_denied");
186
+ });
187
+
188
+ it("rejects a logout carrying neither an origin signal nor a token", async () => {
189
+ const res = await client.post("/session/logout", {});
190
+ expect(res.status).toBe(403);
191
+ expect(res.data.error).toBe("access_denied");
192
+ });
193
+
194
+ it("rejects a login from a foreign origin", async () => {
195
+ const res = await client.post(
196
+ "/session/login",
197
+ { username: "testuser", password: "testpass" },
198
+ { headers: { Origin: "https://evil.example.com" } },
199
+ );
200
+ expect(res.status).toBe(403);
201
+ });
202
+
203
+ it("accepts a header-less client that presents a token from GET /session/csrf", async () => {
204
+ const issued = await client.get("/session/csrf");
205
+ expect(issued.status).toBe(200);
206
+ expect(typeof issued.data.csrf_token).toBe("string");
207
+
208
+ const res = await client.post(
209
+ "/session/login",
210
+ { username: "testuser", password: "testpass" },
211
+ {
212
+ headers: {
213
+ Cookie: _extractCookies(issued).split(";")[0],
214
+ [issued.data.header_name]: issued.data.csrf_token,
215
+ },
216
+ },
217
+ );
218
+ // Credentials are still wrong in this environment — the point is that the
219
+ // request got past the CSRF guard rather than stopping at 403.
220
+ expect(res.status).not.toBe(403);
221
+ });
222
+ });
223
+
160
224
  describe("POST /oauth/token", () => {
161
225
  it("returns 401 for grant_type=session without an authenticated session", async () => {
162
226
  const res = await client.post("/oauth/token", "grant_type=session", {
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "esnext",
3
+ // `es2023` target (NOT esnext) with `lib: esnext` — mirrors the monorepo's
4
+ // tsconfig.base.json. This template is copied verbatim into scaffolded
5
+ // consumer projects (so it must stay self-contained and cannot `extends`
6
+ // ../../tsconfig.base.json). Capping the emit target at ES2023 — the newest
7
+ // edition Node 22 (this template's engine floor) parses natively — makes tsc
8
+ // downlevel any newer syntax (e.g. `await using`) into helpers, so the built
9
+ // server actually runs on the Node it advertises. Do NOT raise to `esnext`
10
+ // without also raising the engine floor.
11
+ "target": "es2023",
4
12
  "lib": ["esnext"],
5
13
  "module": "nodenext",
6
14
  "moduleResolution": "nodenext",
@@ -3,5 +3,13 @@ import { defineConfig } from "vitest/config";
3
3
  export default defineConfig({
4
4
  test: {
5
5
  include: ["src/**/__tests__/**/*.test.mts"],
6
+ // #357: the workspace-wide deadline floor, repeated as literals rather
7
+ // than imported from vitest.shared.mts — this template is copied
8
+ // verbatim into scaffolded projects, where that file does not exist.
9
+ // Rationale lives there: under a loaded parallel run, transform+import
10
+ // alone can exceed vitest's 5s default, and a real hang still fails at
11
+ // 20s.
12
+ testTimeout: 20_000,
13
+ hookTimeout: 20_000,
6
14
  },
7
15
  });
@@ -1,13 +1,13 @@
1
1
  {
2
- "@o3co/auth-provider-core": "0.9.0",
3
- "@o3co/auth-provider-dpop": "0.9.0",
4
- "@o3co/auth-provider-mtls": "0.9.0",
5
- "@o3co/auth-provider-federation-google": "0.9.0",
6
- "@o3co/auth-provider-federation-github": "0.9.0",
7
- "@o3co/auth-provider-oauth": "0.9.0",
8
- "@o3co/auth-provider-oauth-token-exchange": "0.9.0",
9
- "@o3co/auth-provider-session": "0.9.0",
10
- "@o3co/auth-provider-foundation": "0.9.0",
11
- "@o3co/auth-provider-redis": "0.9.0",
12
- "@o3co/auth-provider-webauthn": "0.9.0"
2
+ "@o3co/auth-provider-core": "0.10.0",
3
+ "@o3co/auth-provider-dpop": "0.10.0",
4
+ "@o3co/auth-provider-mtls": "0.10.0",
5
+ "@o3co/auth-provider-federation-google": "0.10.0",
6
+ "@o3co/auth-provider-federation-github": "0.10.0",
7
+ "@o3co/auth-provider-oauth": "0.10.0",
8
+ "@o3co/auth-provider-oauth-token-exchange": "0.10.0",
9
+ "@o3co/auth-provider-session": "0.10.0",
10
+ "@o3co/auth-provider-foundation": "0.10.0",
11
+ "@o3co/auth-provider-redis": "0.10.0",
12
+ "@o3co/auth-provider-webauthn": "0.10.0"
13
13
  }