Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions stepup/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ services:
openconextdev:
volumes:
- ${PWD}/ssp:/var/www/simplesaml/config/cert/
# Local sp.php with mdui:UIInfo (service name) extension support, pending upstream
# inclusion in OpenConext-devssp
- ${PWD}/ssp/sp.php:/var/www/simplesaml/public/sp.php
hostname: ssp.docker


Expand Down
711 changes: 711 additions & 0 deletions stepup/ssp/sp.php

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,38 @@ public function startASfoAuthenticationWithGsspExtension(string $userIdentifier,
$this->minkContext->pressButton('Login');
}

/**
* @When I start an SFO authentication for :arg1 with service name :arg2
*/
public function startASfoAuthenticationWithServiceName(string $userIdentifier, string $serviceName)
{
$this->minkContext->visit($this->spTestUrl);
$this->minkContext->fillField('idp', $this->activeIdp);
$this->minkContext->fillField('sp', $this->activeSp);
$this->minkContext->fillField('loa', $this->requiredLoa);
$this->minkContext->fillField('subject', $userIdentifier);
$this->minkContext->fillField('mdui_displayname', $serviceName);
$this->minkContext->pressButton('Login');
}

/**
* @Then I see service name :arg1 on the GSSP authentication page
*/
public function iSeeServiceNameOnTheGsspAuthenticationPage(string $serviceName)
{
$this->minkContext->assertPageAddress('https://demogssp.dev.openconext.local/authentication');
$this->minkContext->assertPageContainsText($serviceName);
}

/**
* @Then I see service name :arg1 on the GSSP registration page
*/
public function iSeeServiceNameOnTheGsspRegistrationPage(string $serviceName)
{
$this->minkContext->assertPageAddress('https://demogssp.dev.openconext.local/registration');
$this->minkContext->assertPageContainsText($serviceName);
}

/**
* @When I start an SFO authentication for :arg1
*/
Expand Down
33 changes: 33 additions & 0 deletions stepup/tests/behat/features/gssp_service_name.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Tagged SKIP until Stepup-Gateway PR #624 is merged and released in the test image
# with enable_service_name_from_saml_authnrequest enabled, and the mdui-capable
# sp.php is available. Run locally with:
# ./start-dev-env.sh gateway:<Stepup-Gateway checkout> demogssp:<Stepup-gssp-example checkout>
# docker compose exec behat ./vendor/bin/behat --config config/behat.yml features/gssp_service_name.feature
@SKIP
Feature: The GSSP shows the name of the service the user is authenticating for

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What i expect to see here: All 3 of the LoginService::singleSignOn methods should be triggered from here to see if the gateway applies (or discards????) the mdui information.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stepup/tests/behat/features/sfo.feature stepup/tests/behat/features/sfo_gssp_fallback.feature

In order to know which service I am authenticating for
As a user
I want the GSSP authentication page to show the service name from the AuthnRequest

# Covers the cross-repo flow of the mdui:UIInfo service name:
# the SP sends an AuthnRequest with an mdui:UIInfo/mdui:DisplayName extension,
# the Stepup-Gateway (feature flag enable_service_name_from_saml_authnrequest)
# reads it and forwards it in the proxy AuthnRequest to the GSSP, where the
# GSSP (Stepup-gssp-example via Stepup-gssp-bundle and Stepup-saml-bundle)
# displays it on the authentication page.
Scenario: Service name from the AuthnRequest mdui:UIInfo is shown on the GSSP authentication page
Given a service provider configured for second-factor-only
And a user "jane-a-ra" identified by "urn:collab:person:institution-a.example.com:jane-a-ra" from institution "institution-a.example.com" with UUID "00000000-0000-4000-8000-000000000001"
And the user "urn:collab:person:institution-a.example.com:jane-a-ra" has a vetted "demo-gssp" with identifier "gssp-identifier123"
When I start an SFO authentication for "urn:collab:person:institution-a.example.com:jane-a-ra" with service name "Behat Test Service"
Then I see service name "Behat Test Service" on the GSSP authentication page
When I verify the "demo-gssp" second factor
Then I am logged on the service provider

# Reuses the identity vetted in the previous scenario, like sfo.feature does.
Scenario: No service name is shown when the AuthnRequest carries no mdui:UIInfo
Given a service provider configured for second-factor-only
When I start an SFO authentication for "urn:collab:person:institution-a.example.com:jane-a-ra"
Then I should not see "Behat Test Service"
When I verify the "demo-gssp" second factor
Then I am logged on the service provider
5 changes: 5 additions & 0 deletions stepup/tests/playwright/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
test-results/
playwright-report/
.playwright-mcp/
*.png
60 changes: 60 additions & 0 deletions stepup/tests/playwright/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Service name e2e tests (Playwright)

Cross-repo browser test for the "show service name during authentication" feature:
Stepup-Middleware#589, Stepup-Gateway#624, Stepup-saml-bundle#137,
Stepup-gssp-bundle#49, Stepup-gssp-example#141.

## Prerequisites

1. Start the devconf stepup environment (see `../../README.md`), pointing
`middleware` / `gateway` / `demogssp` at your local checkouts if you're
testing branches that aren't in the `test` images yet:

```bash
cd ../..
./start-dev-env.sh -d \
middleware:/path/to/Stepup-Middleware \
gateway:/path/to/Stepup-Gateway \
demogssp:/path/to/Stepup-gssp-example
```

2. `.env` must have `APP_ENV=smoketest` (routes the apps to `*_test` DBs).

3. Run the Behat suite at least once against this stack so the `jane-a-ra`
identity (vetted `demo-gssp` second factor) exists — this test reuses
that fixture data rather than duplicating the bootstrap:

```bash
docker compose exec behat ./vendor/bin/behat --config config/behat.yml \
--tags='~@wip' features/gssp_service_name.feature
```

4. In `Stepup-Gateway`, `enable_service_name_from_saml_authnrequest: true`
must be set (already the case in the devconf parameters).

## Install & run

```bash
npm install
npx playwright install chromium # first time only
NODE_TLS_REJECT_UNAUTHORIZED=0 npx playwright test
```

`NODE_TLS_REJECT_UNAUTHORIZED=0` is needed because the devconf stack uses a
self-signed cert and `lib/middleware.ts` pushes config over `fetch()`
directly (not through Playwright's browser context, which is configured with
`ignoreHTTPSErrors` separately).

## What it does

Each test pushes a config change to Middleware for the `second-sp` SP entity
(`lib/middleware.ts`), then drives the SP debug page
(`https://ssp.dev.openconext.local/simplesaml/sp.php`) through an SFO login
to `demogssp`, asserting on what service name is shown:

- No Middleware `service_name` → the SP's own `mdui:DisplayName` is shown.
- Middleware `service_name` set → it wins, regardless of what the SP sends.
- Neither present → no service name section renders, no error.

Each test resets `second-sp`'s `service_name` back to unset in `afterEach`,
so the shared environment is left as found.
54 changes: 54 additions & 0 deletions stepup/tests/playwright/lib/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import fs from 'node:fs';
import path from 'node:path';

const MIDDLEWARE_CONFIG_URL = 'https://middleware.dev.openconext.local/management/configuration';
const MIDDLEWARE_CONFIG_PATH = path.resolve(__dirname, '../../../middleware/middleware-config.json');
const MANAGEMENT_USER = 'management';
const MANAGEMENT_PASSWORD = 'secret';

type ServiceProvider = { entity_id: string; service_name?: string | null; [key: string]: unknown };
type MiddlewareConfig = { gateway: { service_providers: ServiceProvider[]; [key: string]: unknown }; [key: string]: unknown };

function loadBaseConfig(): MiddlewareConfig {
const raw = fs.readFileSync(MIDDLEWARE_CONFIG_PATH, 'utf-8');
return JSON.parse(raw) as MiddlewareConfig;
}

/**
* Pushes the devconf baseline middleware-config.json, optionally overriding
* `service_name` on one SP entity. Pass `serviceName: null` to push the
* baseline unmodified (no service_name key on that entity).
*/
export async function pushServiceName(entityId: string, serviceName: string | null): Promise<void> {
const config = loadBaseConfig();
const sp = config.gateway.service_providers.find((s) => s.entity_id === entityId);
if (!sp) {
throw new Error(`No service provider with entity_id "${entityId}" found in ${MIDDLEWARE_CONFIG_PATH}`);
}
if (serviceName === null) {
delete sp.service_name;
} else {
sp.service_name = serviceName;
}

const auth = Buffer.from(`${MANAGEMENT_USER}:${MANAGEMENT_PASSWORD}`).toString('base64');
const response = await fetch(MIDDLEWARE_CONFIG_URL, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Basic ${auth}`,
},
body: JSON.stringify(config),
// devconf uses a self-signed cert; Node's fetch needs this at the process level,
// see NODE_TLS_REJECT_UNAUTHORIZED=0 in the npm script / CI invocation.
});

if (!response.ok) {
throw new Error(`Middleware config push failed: HTTP ${response.status} ${await response.text()}`);
}
const body = (await response.json()) as { status?: string };
if (body.status !== 'OK') {
throw new Error(`Middleware config push did not return status OK: ${JSON.stringify(body)}`);
}
}
76 changes: 76 additions & 0 deletions stepup/tests/playwright/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions stepup/tests/playwright/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "stepup-service-name-e2e",
"private": true,
"scripts": {
"test": "playwright test"
},
"devDependencies": {
"@playwright/test": "^1.48.0"
}
}
17 changes: 17 additions & 0 deletions stepup/tests/playwright/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { defineConfig } from '@playwright/test';

// Requires the stepup devconf environment running with the `smoketest` .env profile
// (see ../../README.md) so the app containers use *_test databases and the
// hosts file entries for *.dev.openconext.local resolve to 127.0.0.1.
export default defineConfig({
testDir: './tests',
timeout: 30_000,
fullyParallel: false,
reporter: 'list',
use: {
ignoreHTTPSErrors: true,
baseURL: 'https://ssp.dev.openconext.local',
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
},
});
81 changes: 81 additions & 0 deletions stepup/tests/playwright/tests/service-name.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { test, expect, type Page } from '@playwright/test';
import { pushServiceName } from '../lib/middleware';

/**
* Cross-repo e2e coverage for the "service name during authentication" feature
* (Stepup-Middleware#589, Stepup-Gateway#624, Stepup-saml-bundle#137,
* Stepup-gssp-bundle#49, Stepup-gssp-example#141).
*
* Flow under test: SP (ssp debug SP) --AuthnRequest w/ mdui:UIInfo--> Gateway
* (SFO) --proxy AuthnRequest--> GSSP (demogssp) authentication page.
*
* Priority rule under test (see REFINEMENT_SERVICE_NAME.md):
* Middleware `service_name`, when configured for the SP, always wins over
* any mdui:DisplayName sent by the SP in the AuthnRequest.
*
* Prerequisites:
* - devconf-service-name/stepup environment running (./start-dev-env.sh -d
* with gateway/demogssp/middleware pointed at your local checkouts if you
* want to test in-progress branches).
* - APP_ENV=smoketest in .env (routes the apps to the *_test databases).
* - The Behat suite must have been run at least once against this stack
* (docker compose exec behat ./vendor/bin/behat --config config/behat.yml)
* so the "jane-a-ra" identity with a vetted demo-gssp second factor
* exists — that fixture setup lives in the Behat suite's @BeforeSuite
* hook, not duplicated here.
*
* Run: NODE_TLS_REJECT_UNAUTHORIZED=0 npx playwright test
*/

const SECOND_SP_ENTITY_ID = 'https://ssp.dev.openconext.local/simplesaml/module.php/saml/sp/metadata.php/second-sp';
const VETTED_SUBJECT = 'urn:collab:person:institution-a.example.com:jane-a-ra';

async function startSfoAuthentication(page: Page, mduiDisplayName?: string): Promise<void> {
await page.goto('/simplesaml/sp.php');
await page.locator('#idp').selectOption('OpenConext Stepup Gateway - gateway.dev.openconext.local - SFO');
await page.locator('#sp').selectOption('second-sp');
await page.locator('#loa').selectOption('2');
await page.locator('#subject').fill(VETTED_SUBJECT);
if (mduiDisplayName) {
await page.locator('#mdui_displayname').fill(mduiDisplayName);
} else {
await page.locator('#mdui_displayname').fill('');
}
await page.getByRole('button', { name: 'Login' }).first().click();
await expect(page).toHaveURL('https://demogssp.dev.openconext.local/authentication');
}

test.describe('Service name during authentication', () => {
test.afterEach(async () => {
// Leave the shared devconf environment as we found it.
await pushServiceName(SECOND_SP_ENTITY_ID, null);
});

test('shows the AuthnRequest mdui:DisplayName when Middleware has no service_name configured', async ({ page }) => {
await pushServiceName(SECOND_SP_ENTITY_ID, null);

await startSfoAuthentication(page, 'Behat Test Service');

await expect(page.getByText('Behat Test Service')).toBeVisible();
await page.screenshot({ path: 'screenshots/01-authnrequest-mdui-shown.png', fullPage: true });
});

test('Middleware service_name overrides the AuthnRequest mdui:DisplayName', async ({ page }) => {
await pushServiceName(SECOND_SP_ENTITY_ID, 'Middleware Configured Name');

await startSfoAuthentication(page, 'Behat Test Service');

await expect(page.getByText('Middleware Configured Name')).toBeVisible();
await expect(page.getByText('Behat Test Service')).not.toBeVisible();
await page.screenshot({ path: 'screenshots/02-middleware-overrides-authnrequest.png', fullPage: true });
});

test('renders without error when neither Middleware service_name nor mdui:DisplayName is present', async ({ page }) => {
await pushServiceName(SECOND_SP_ENTITY_ID, null);

await startSfoAuthentication(page);

await expect(page.getByText('Service name')).not.toBeVisible();
await page.screenshot({ path: 'screenshots/03-no-service-name-no-error.png', fullPage: true });
});
});
Loading