diff --git a/bun.lock b/bun.lock index 8f70f68c..7e063659 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ }, "auth-server": { "name": "@schemavaults/auth-server", - "version": "0.20.52", + "version": "0.21.0", "dependencies": { "@hookform/resolvers": "3.9.0", "@schemavaults/app-definitions": "workspace:*", @@ -70,7 +70,7 @@ }, "packages/auth-client-sdk": { "name": "@schemavaults/auth-client-sdk", - "version": "0.9.26", + "version": "0.9.28", "dependencies": { "@schemavaults/app-definitions": "workspace:*", "@schemavaults/auth-common": "workspace:*", @@ -109,7 +109,7 @@ }, "packages/auth-react-provider": { "name": "@schemavaults/auth-react-provider", - "version": "0.10.14", + "version": "0.10.17", "dependencies": { "@schemavaults/app-definitions": "workspace:*", "@schemavaults/auth-client-sdk": "workspace:*", @@ -141,9 +141,10 @@ }, "packages/auth-resource-server-codegen-templates": { "name": "@schemavaults/auth-resource-server-codegen-templates", - "version": "0.0.17", + "version": "0.0.19", "devDependencies": { "@eslint/js": "9.39.1", + "@schemavaults/auth-client-sdk": "workspace:*", "@schemavaults/auth-common": "workspace:*", "@schemavaults/auth-react-provider": "workspace:*", "@schemavaults/jwt": "workspace:*", @@ -165,7 +166,7 @@ }, "packages/auth-server-sdk": { "name": "@schemavaults/auth-server-sdk", - "version": "0.21.14", + "version": "0.21.19", "bin": { "auth-server-sdk": "./dist/cli.cjs", }, @@ -191,7 +192,7 @@ }, "packages/auth-ui": { "name": "@schemavaults/auth-ui", - "version": "0.6.59", + "version": "0.6.61", "dependencies": { "@hookform/resolvers": "3.9.0", "@schemavaults/app-definitions": "workspace:*", @@ -226,7 +227,7 @@ }, "packages/jwt": { "name": "@schemavaults/jwt", - "version": "0.6.36", + "version": "0.6.37", "dependencies": { "@schemavaults/app-definitions": "workspace:*", "@schemavaults/auth-common": "workspace:*", @@ -246,7 +247,7 @@ }, "tests/cypress-e2e-auth-tests-helper-commands": { "name": "@schemavaults/cypress-e2e-auth-tests-helper-commands", - "version": "0.1.2", + "version": "0.1.3", "dependencies": { "@schemavaults/app-definitions": "workspace:*", "@schemavaults/auth-common": "workspace:*", @@ -261,7 +262,7 @@ }, "tests/e2e-auth-tests": { "name": "@schemavaults/e2e-auth-tests", - "version": "0.2.8", + "version": "0.2.17", "dependencies": { "@schemavaults/app-definitions": "workspace:*", "@schemavaults/auth-common": "workspace:*", @@ -277,7 +278,7 @@ }, "tests/example-nextjs-resource-server": { "name": "@schemavaults/example-nextjs-resource-server", - "version": "0.0.5", + "version": "0.1.4", "dependencies": { "@schemavaults/auth-client-sdk": "workspace:*", "@schemavaults/auth-react-provider": "workspace:*", diff --git a/tests/e2e-auth-tests/cypress/e2e/example_resource_server/ExampleResourceServer.cy.ts b/tests/e2e-auth-tests/cypress/e2e/example_resource_server/ExampleResourceServer.cy.ts index 0425c441..6196f1c1 100644 --- a/tests/e2e-auth-tests/cypress/e2e/example_resource_server/ExampleResourceServer.cy.ts +++ b/tests/e2e-auth-tests/cypress/e2e/example_resource_server/ExampleResourceServer.cy.ts @@ -1,7 +1,102 @@ describe("ExampleResourceServer", () => { - it("can visit the example resource server", async () => { - cy.visit("http://example-nextjs-resource-server:3007"); - cy.url().should("include", "example-nextjs-resource-server"); - cy.contains("h1", "@schemavaults/example-nextjs-resource-server"); + const exampleAppUrl: string = + Cypress.env("EXAMPLE_NEXTJS_RESOURCE_SERVER_URL") || + "http://example-nextjs-resource-server:3007"; + // Normalize origin to strip default port 80 — cy.origin() requires + // the argument to match the browser's normalised origin exactly. + const exampleAppOrigin: string = new URL(exampleAppUrl).origin; + + it("can visit the example resource server", () => { + cy.origin(exampleAppOrigin, () => { + cy.visit("/"); + cy.url().should("include", "example-nextjs-resource-server"); + cy.contains("h1", "@schemavaults/example-nextjs-resource-server"); + }); + }); + + it("can register a new user through the full OAuth2 PKCE flow and access the protected /account route", () => { + // Step 1: Login as admin and create an invite code for the new user + cy.create_and_login_as_superuser().then((success: boolean) => { + if (!success) { + throw new Error("Failed to login as superuser"); + } + + cy.generate_random_code(24).then((inviteCode: string) => { + cy.create_invite_code(inviteCode, 1).then((created: boolean) => { + if (!created) { + throw new Error("Failed to create invite code"); + } + + // Step 2: Logout from admin session + cy.logout(); + + // Step 3: Visit example app and click "Register". + // Wrap in cy.origin() since example app is cross-origin + // from the auth server base URL. cy.visit() inside + // cy.origin() is relative to the given origin. + cy.origin(exampleAppOrigin, () => { + cy.visit("/"); + cy.contains("h1", "@schemavaults/example-nextjs-resource-server"); + cy.contains("button", "Register").click(); + }); + + // Step 4: Example app's /auth/register generates PKCE params and + // redirects to auth server's /auth/register with code_challenge, + // redirect_uri, and app_id query parameters. + // After the redirect we're back on the auth server (base URL) origin. + cy.url({ timeout: 20000 }).should("include", "/auth/register"); + cy.url().should("include", "code_challenge"); + cy.wait_for_page_hydration(); + + // Step 5: Fill the registration form on the auth server + cy.generate_random_code(12).then((suffix: string) => { + const email = `pkce-reg-test-${suffix}@example.com`; + const password = "TestPassword123!"; + + cy.get("input[name='email']") + .should("be.visible") + .type(email, { force: true }); + cy.get("input[name='password']") + .should("be.visible") + .type(password, { force: true }); + cy.get("input[name='confirm']") + .should("be.visible") + .type(password, { force: true }); + cy.get("input[name='invite_code']") + .should("not.be.disabled") + .type(inviteCode, { force: true }); + + cy.get("button[type='submit']") + .should("not.be.disabled") + .click(); + + // Step 6: Consent screen appears because the example app is not a + // hardcoded app — AppAuthorizationConsentScreen renders in + // authorize-only mode. Click "Authorize & Continue" to approve. + cy.contains("Authorize & Continue", { timeout: 15000 }) + .should("be.visible") + .click(); + + // Step 7: Auth server redirects back to example app's + // /auth/authorize?authorization_code=...&challenge_time=... + // The example app's useTradeAuthorizationCodeForTokensEffect + // exchanges the auth code + stored code_verifier for tokens, + // then redirects to /account. + + // Step 8: Verify the protected /account page renders successfully. + // We're crossing back to the example app origin from the auth server. + cy.origin(exampleAppOrigin, () => { + cy.url({ timeout: 30000 }).should("include", "/account"); + cy.contains("Example Account Page", { + timeout: 15000, + }).should("be.visible"); + cy.contains( + "If you're seeing this it means that you were not redirected because you are logged in!", + ).should("be.visible"); + }); + }); + }); + }); + }); }); }); diff --git a/tests/e2e-auth-tests/docker-compose.yml b/tests/e2e-auth-tests/docker-compose.yml index c28ba490..d04f746a 100644 --- a/tests/e2e-auth-tests/docker-compose.yml +++ b/tests/e2e-auth-tests/docker-compose.yml @@ -15,7 +15,7 @@ services: CYPRESS_BASE_URL: http://schemavaults-auth:80 SCHEMAVAULTS_APP_ENVIRONMENT: test TEST_SUITE_NAME: ${TEST_SUITE_NAME:?error} - EXAMPLE_NEXTJS_RESOURCE_SERVER_URL: http://example-nextjs-resource-server:3007 + EXAMPLE_NEXTJS_RESOURCE_SERVER_URL: http://example-nextjs-resource-server EXAMPLE_NEXTJS_RESOURCE_SERVER_JWKS_ACCESS_PUBLIC_KEY: ${EXAMPLE_NEXTJS_RESOURCE_SERVER_JWKS_ACCESS_PUBLIC_KEY:-null} profiles: - e2e @@ -47,9 +47,9 @@ services: SCHEMAVAULTS_CLIENT_APP_ID: 00000000-0000-0000-0000-000000000000 SCHEMAVAULTS_AUTH_JWKS_ACCESS_PRIVATE_KEY: ${EXAMPLE_NEXTJS_RESOURCE_SERVER_JWKS_ACCESS_PRIVATE_KEY:-null} expose: - - 3007 + - 80 ports: - - 3007:3007 + - 3007:80 profiles: - e2e_with_resource_server