diff --git a/Makefile b/Makefile index 6c37cc1..9ab7e7e 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,13 @@ build: setup cd repos/auth.proxy && pnpm install && pnpm run build cd repos/auth.policy-verifier && pnpm install && pnpm run build +# One definition of the shared HS256 secret, interpolated into the containers +# by docker compose and exported to the test processes, which mint their own +# tokens with it. Defining it twice is how the suite drifted before: the tests +# fell back to a stale literal and every negative case failed as a 401 that +# read like a policy failure. auth.provider#282 requires >=32 decoded bytes. +export OAUTH_JWT_SECRET := qmV+afsq/SMZ7hPGs9edVQDvPzNmjXemJNjqti181v0= + .PHONY: test-e2e test-e2e: build docker compose -f tests/docker-compose.yml up -d --build --wait diff --git a/tests/abac/index.test.js b/tests/abac/index.test.js index 5eeb46b..03706b7 100644 --- a/tests/abac/index.test.js +++ b/tests/abac/index.test.js @@ -41,14 +41,19 @@ function signToken(claims, options = {}) { /** Grant carrying `read:project`. */ let projectGrant; -/** Grant carrying only `read:project_member` — used for the deny case. */ +/** + * Grant carrying only `read:project.member` — used for the deny case, and for + * the nested-resource allow. The scope name tracks the resource type the + * verifier derives: auth.policy-verifier#117 stopped rewriting the `.` + * separator to `_`, so `project:1.member:2` now derives `project.member`. + */ let memberGrant; beforeAll(async () => { const session = await login(); expect(session.status).toBe(200); projectGrant = await codeFlow({ cookie: session.cookie, scope: 'openid read:project' }); - memberGrant = await codeFlow({ cookie: session.cookie, scope: 'openid read:project_member' }); + memberGrant = await codeFlow({ cookie: session.cookie, scope: 'openid read:project.member' }); }, 30_000); describe('ABAC: POST /verify with provider-issued access tokens', () => { diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index c6c8bef..237b278 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -25,12 +25,24 @@ services: - ./provider/clients.yaml:/home/node/templates/standalone/config/clients.yaml:ro - ./provider/users.yaml:/home/node/templates/standalone/config/users.yaml:ro environment: - - OAUTH_JWT_SECRET=test-secret-for-e2e + # auth.provider#282 made EdDSA the default and removed the HS256 + # fallback, so the algorithm must be named explicitly. The E2E stays on + # HS256 deliberately: the verifier requires https for a JWKS URI, with an + # http carve-out only for loopback (auth.policy-verifier#109), and here it + # would reach the provider at http://provider:3000 — a compose service + # name, not loopback. A JWKS-based E2E would fail the verifier's boot + # check, so the two services share a secret instead. + - OAUTH_JWT_ALGORITHM=HS256 + # #282 also put a >=32-byte floor on the decoded secret, measured at the + # smallest plausible reading. The value is defined once in the Makefile + # and interpolated here, because the ABAC tests mint tokens with the same + # secret — a second literal is how the two drifted apart before. + - OAUTH_JWT_SECRET=${OAUTH_JWT_SECRET:?run via make test-e2e} # auth.provider requires a canonical issuer and never derives one from the # request (auth.provider#266). It is an identifier, not a URL anything # fetches, so the E2E pins a fixed one both services agree on. - OAUTH_JWT_ISSUER=https://auth.e2e.test - - SESSION_SECRET=test-session-secret + - SESSION_SECRET=lO0QH09fuKSGuViZ9myJbH3jsgai99A2GpC3RYRuy6Y= - SESSION_SECURE=false # __Host- prefixed default requires secure=true; plain-HTTP E2E needs both overrides - SESSION_NAME=auth.session @@ -138,7 +150,9 @@ services: volumes: - ./abac/application.conf:/home/node/templates/standalone/config/application.conf:ro environment: - - OAUTH_JWT_SECRET=test-secret-for-e2e + # Same interpolated secret as the provider — under HS256 the verifier + # holds the shared secret it verifies with. + - OAUTH_JWT_SECRET=${OAUTH_JWT_SECRET:?run via make test-e2e} # auth.policy-verifier validates iss / aud / typ per RFC 9068 §4 and # refuses to boot without them (auth.policy-verifier#105). The issuer must # match what the provider stamps, and the audience must match what the diff --git a/tests/provider/clients.yaml b/tests/provider/clients.yaml index 50f0898..40bd669 100644 --- a/tests/provider/clients.yaml +++ b/tests/provider/clients.yaml @@ -26,7 +26,7 @@ e2e-app: - "openid" - "email" - "read:project" - - "read:project_member" + - "read:project.member" allowedAudiences: - "https://api.e2e.test" allowedGrantTypes: diff --git a/tests/shared/oauthFlow.js b/tests/shared/oauthFlow.js index 12568fa..219930c 100644 --- a/tests/shared/oauthFlow.js +++ b/tests/shared/oauthFlow.js @@ -20,7 +20,25 @@ export const VERIFIER_URL = process.env.VERIFIER_URL || 'http://localhost:3097'; export const ISSUER = process.env.OAUTH_JWT_ISSUER || 'https://auth.e2e.test'; export const AUDIENCE = process.env.OAUTH_JWT_AUDIENCE || 'https://api.e2e.test'; -export const JWT_SECRET = process.env.OAUTH_JWT_SECRET || 'test-secret-for-e2e'; +/** + * Must equal the containers' `OAUTH_JWT_SECRET` in tests/docker-compose.yml. + * The negative ABAC tests mint their own tokens with it, so a value that + * merely looks plausible produces 401s that read like a policy failure. + * There is deliberately no fallback: auth.provider#282 put a >=32-byte floor + * on the secret, and a default here would silently drift from compose again + * the next time the floor or the value changes. + */ +export const JWT_SECRET = requireEnv('OAUTH_JWT_SECRET'); + +function requireEnv(name) { + const value = process.env[name]; + if (!value) { + throw new Error( + `${name} is not set. Run the suite through \`make test-e2e\`, which exports it from tests/docker-compose.yml.`, + ); + } + return value; +} /** Marked `firstParty: true` in tests/provider/clients.yaml. */ export const CLIENT_ID = 'e2e-app';