From a0c1c0baa57c84cb14fcd38e79a7ee5bf5b024b1 Mon Sep 17 00:00:00 2001
From: Kay Joosten
Date: Mon, 6 Jul 2026 08:59:50 +0200
Subject: [PATCH 1/7] Add full-flow behat coverage for GSSP service name from
AuthnRequest mdui:UIInfo
Adds a behat feature that exercises the cross-repo service name flow:
the SP includes an mdui:UIInfo/mdui:DisplayName extension in the SFO
AuthnRequest, the Stepup-Gateway (with feature flag
enable_service_name_from_saml_authnrequest enabled) forwards it in the
proxy AuthnRequest to the GSSP, and the demo GSSP displays the service
name on its authentication page.
The devssp test SP does not support the mdui:UIInfo extension yet, so a
patched sp.php with an mdui_displayname form field is mounted over the
one in the devssp container, pending upstream inclusion in
OpenConext-devssp.
---
stepup/docker-compose.yml | 3 +
stepup/ssp/sp.php | 711 ++++++++++++++++++
.../bootstrap/SecondFactorAuthContext.php | 23 +
.../behat/features/gssp_service_name.feature | 33 +
4 files changed, 770 insertions(+)
create mode 100644 stepup/ssp/sp.php
create mode 100644 stepup/tests/behat/features/gssp_service_name.feature
diff --git a/stepup/docker-compose.yml b/stepup/docker-compose.yml
index 047710b..0d779ae 100644
--- a/stepup/docker-compose.yml
+++ b/stepup/docker-compose.yml
@@ -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
diff --git a/stepup/ssp/sp.php b/stepup/ssp/sp.php
new file mode 100644
index 0000000..704a3cf
--- /dev/null
+++ b/stepup/ssp/sp.php
@@ -0,0 +1,711 @@
+isAuthenticated();
+
+// Build return URL. This is where ask simplesamlPHP to direct the browser to after login or logout
+// Point to this script, but without any request parameters so we won't trigger an login again (and again, and again, and ...)
+$returnURL = ($_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
+$returnURL .= $_SERVER['HTTP_HOST'];
+$returnURL .= $_SERVER['SCRIPT_NAME'];
+$returnURL .= '?sp='.urlencode($sp);
+
+// Process login and logout actions. Neither login nor logout return
+if (isset($_REQUEST['action']) && $_REQUEST['action'] == 'login' ) {
+
+ // Save submitted form in session
+ $params_to_save=$_REQUEST;
+ unset($params_to_save['action']);
+ $session->setData('array', 'SSP_DEMO_SP_FORM_DATA', $params_to_save);
+
+ // Unset existing RequiredAuthnContextClassRef first
+ $session->deleteData('string', 'RequiredAuthnContextClassRef');
+ $bForceAuthn = false;
+ if ( (isset($_REQUEST['forceauthn'])) && ($_REQUEST['forceauthn'] == 'true') )
+ $bForceAuthn = true;
+
+ // For use by SAML2Keeper callback function
+ $session->setData('string', 'SAML2Keeper_ReturnTo', $returnURL);
+
+ $context = array(
+ 'ReturnTo' => $returnURL,
+ 'ReturnCallback' => array('sspmod_saml2keeper_SAML2Keeper','loginCallback'),
+ 'ForceAuthn' => $bForceAuthn,
+ 'saml:NameIDPolicy' => null,
+ );
+
+ // IdP
+ if ( (isset($_REQUEST['idp'])) ) {
+ $context['saml:idp'] = $_REQUEST['idp'];
+ }
+
+ // LOA
+ if ( isset($_REQUEST['loa']) && isset($_REQUEST['idp']) && isset($gIDPmap[$_REQUEST['idp']]['loa'][$_REQUEST['loa']]) ) {
+ $loa = $gIDPmap[$_REQUEST['idp']]['loa'][$_REQUEST['loa']];
+ // Store the requested LOA in the session so we can verify it later
+ $session->setData('string', 'RequiredAuthnContextClassRef', $loa);
+ $context['saml:AuthnContextClassRef'] = $loa; // Specify LOA
+ }
+
+ // Scoping IdPList
+ if ( isset($_REQUEST['scopingIDP']) && strlen($_REQUEST['scopingIDP']) > 0 ) {
+ $context['saml:IDPList'] = array($_REQUEST['scopingIDP']);
+
+ if ( isset($_REQUEST['scopingIDP2']) && strlen($_REQUEST['scopingIDP2']) > 0 ) {
+ $context['saml:IDPList'][]=$_REQUEST['scopingIDP2'];
+ }
+ }
+
+ // RequesterID
+ if ( isset($_REQUEST['requesterid']) && strlen($_REQUEST['requesterid']) > 0 ) {
+ $context['saml:RequesterID'] = array($_REQUEST['requesterid']);
+
+ if ( isset($_REQUEST['requesterid2']) && strlen($_REQUEST['requesterid2']) > 0 ) {
+ $context['saml:RequesterID'][] = $_REQUEST['requesterid2'];
+ }
+ }
+
+ // NameIDPolicy
+ if ( isset($_REQUEST['nameidpolicy']) && strlen($_REQUEST['nameidpolicy']) > 0 ) {
+ $context['saml:NameIDPolicy'] = $_REQUEST['nameidpolicy'];
+ }
+
+ // Subject NameID
+ if ( isset($_REQUEST['subject']) && strlen($_REQUEST['subject']) > 0 ) {
+ $nameId = new \SAML2\XML\saml\NameID();
+ $nameId->setValue($_REQUEST['subject']); // Use value of "NameID" attribute
+ $nameId->setFormat(\SAML2\Constants::NAMEID_UNSPECIFIED); // Unspecified NameID
+ $context['saml:NameID'] = $nameId;
+ }
+
+ // AssertionConsumerServiceURL
+ if ( isset($_REQUEST['acsurl']) && strlen($_REQUEST['acsurl']) > 0 ) {
+ $context['debugsp:AssertionConsumerServiceURL'] = $_REQUEST['acsurl'];
+ }
+
+ // Emulate an authentication request by the SURF MFA extension for ADFS. See: https://github.com/SURFnet/ADFS-MFA-SAML2.0-Extension
+ // The second factor only (SFO) endpoint of the Stepup-Gateway (see: https://github.com/OpenConext/Stepup-Gateway) will give a different
+ // SAML Response when this is enabled. This option allows testing this behaviour without having to install an ADFS server.
+ // The trigger for this behaviour in the stepup-gateway is the presence of the 'Context' and 'AuthMethod' POST variables. This also means that a HTTP-POST
+ // binding must be used.
+ // Additionally the SURF MFA extension for ADFS sets the ACS location in the SAML Request to the URL where the response must be posted.
+ // We add some dummy URL parameters to the ACS location for this purpose.
+ // Because the actual SAML response is posted in the "_SAMLResponse" parameter (instead of "SAMLResponse") the acs location of debugsp module
+ // must be used to validate the response as that will rename the "_SAMLResponse" POST variable back to "SAMLResponse" so that
+ // SimpleSAMLphp can then process it normally.
+ // For more information See: https://github.com/OpenConext/Stepup-Gateway/blob/main/docs/SFO.md#adfs-mfa-extension
+ if ( (isset($_REQUEST['emulateadfs'])) && ($_REQUEST['emulateadfs'] == 'true') )
+ {
+ // Switch binding to HTTP-POST
+ $ssobinding = $GLOBALS['gSP_SSOBinding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST';
+
+ // Set AuthMethod and mock Context like the SURF MFA extension for ADFS would do
+ $context['debugsp:extraPOSTvars'] = array(
+ 'AuthMethod' => 'ADFS.SCSA',
+ 'Context' => '1C63828278F1B0AC2FE61429E099FFA7AC94917CQe1t1xgG78zLHhUxBXm0ous4yl0zfQumsKI79lrHMOIjdTdeF/i1Yx+pQ+mgnubT9mh+DfBYMs7wU1g+eXiAs2gnwKWmnMzeuxgG+m5Nky5Wd63NcEgLZ2zNTYuW70X514HMtLAw+l1H8cptQMXfXt9ageHOdY+65eq4IsNwnB0mPhRkua58R9xO3I4MfBzy90GqwgjmDeZAo5vsKgk0iZRgZ1CS4hPyIWX+ryU2tnYp5UEuDE9gGlR9cQr2uHW10LOG22ZfEy8rJie2T2A2bCQVyF47nmBnvoKYV6YyEDpozSYJpUqHmIgvaWgFu5dvDvZ0fvrVQaQ1ZUKHTT76Cg==6SO/qvyH0bmayeNGyzqAy/Oim2UAOvhxm18rTs+72Qm2fSK6Pfo1ZEDNKmLRk6IemCvkUYWMa4VmxIdATswREx/aSrp4YS3QejDBoZlCwz4LqFWJMiqTPxJfWhahP0hBNEORN8cU5vBQXXIahWqlkaHzs6IPjH4WoMe5vsSKVTetaOMbMC3ZML67BWpAnEXKWoR/gar1jH5v961ljdKJozzgwsJIAY4TNSoB+AEzRd4C3wLSTCott1DyRtMmEmS5DpaDOaxmZ/X+z16t1hb9VKgEqt1xZJ0uw451d5oeuisN9zSqbWQzyiJdkk6k11YU9q2rvg342qLJk6xeTtRc6+DLQ24vZIHC8RU2jcHveLDJvOq89BBJ0LHtnV/7PJpb4PGf1OUqWZidnRAS0/dqprEVzPEnvdzIJ8vPRGzE0dkQhgzDi+cbMsuZrDqYWaMuodvDbGrETxZ9hu0MI3l9pgjuIh8xF7TT/6qTJnGExRaGFebcjMXC99thZ3A7XeJESDNXNxgDgFQf6OwHLjLuhw==va+cZ6Y7NIyBU9vCVb+qRGSx0Yk=DNF3KVEb8ju/T+ise1j0QBS2OYsepwzgWaUtOASvUPI6NPlvyQIHxX1Py6oHcUkbWP1jaVTzwEGadaq428nPMWSeU/MDWqyz2jyrwuIUWglc64AMlcXd0BOdT1I6khKMsUGY8CSa1tRD2arcIH1TUrrk7jY3qfAGtgNbFlElPwc/2l4dkN7QXdHRcmntFp4D/9yEG9FkWzTyXLvCvGqcQeu8L1fKTwq8Upqk9iT2PKnmT/gH+IUt3votmCMV9bxYols0aQWfv2RX2HX3Gow9xKZuOn+ckjZRqBJ1Kp9wGMAB65XQPli5UQzezEHX28oUPH/PEgnu6RKDgsN55h22ag==DB540D051F8F73EA2F3B5190BFC0F349E595EB34 SAMLRequest: PHNhbWwycDpBdXRoblJlcXVlc3QgeG1sbnM6c2FtbDJwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iXzkwZWFkMWNlLTM3NmMtNGE1ZC05ODAzLWQ5Y2M4MDA2M2Q0ZiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTgtMDQtMjNUMTM6NDk6NTBaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9zYS1ndy50ZXN0Mi5zdXJmY29uZXh0Lm5sL3NlY29uZC1mYWN0b3Itb25seS9zaW5nbGUtc2lnbi1vbiIgQXNzZXJ0aW9uQ29uc3VtZXJTZXJ2aWNlVVJMPSJodHRwczovL2FkZnMtMjAxMi50ZXN0Mi5zdXJmY29uZXh0Lm5sOjQ0My9hZGZzL2xzLz9TQU1MUmVxdWVzdD1wVkpOanhNeERQMHJvOXpubzVscFlhTzJVdGtLVVdtQmFsczRjRUZ1eGtNalpaSWhkbUQ1OTZRcGlJVkRMNXdTUGZzOVB6OTVTVERhU1cwaW45MGpmbzFJWER5TjFwSEtoWldJd1NrUFpFZzVHSkVVYTNYWXZIMVFzbXJVRkR4NzdhMTRScm5OQUNJTWJMd1R4VzY3RXAlMkZiMmVtdTBYTG90SnpMUmklMkJHV1R1SHJwJTJGTCUyQlFKMXYyaFBKOTNjZFMwTUlJcVBHQ2d4VnlJSkpUcFJ4SjBqQnNjSmFtWXZ5NllyWlh1Y3RhcGJxUGJGSjFGczB6YkdBV2ZXbVhraVZkZlFEMVNtZmxseEtzdUtZaGkwZCUyRmpFbGJPNVdsdXFSYkg1YmZYZU80b2poZ09HYjBiamg4ZUhQMktUUWNaUUFaaXM0ekNMa0Jrbml6bkE4MVNQdm84V3E4djNBdFYwZldVSm1qTGE0d0RSY2ttVEtQYSUyRkluMWxYRyUyRmNsOXRwbnE1TnBONGNqJTJGdHklMkYlMkY1d0ZPdmxSVnZsZE1MNlAyMk95TkFEd3o4dWwlMkZYekdjdnJCYjFMN25iYnZiZEclMkZ5aGUlMkJ6QUMzelolMkZRVXhmRHJsVmNRQkhCaDJuNEszMTMlMkI4REF1TktjSWdvNnZWMTVOOTN1djRKJmFtcDtSZWxheVN0YXRlPWh0dHBzJTNBJTJGJTJGcGlldGVyLmFhaS5zdXJmbmV0Lm5sJTJGc2ltcGxlc2FtbHBocCUyRnNwLnBocCUzRnNwJTNEZGVmYXVsdC1zcCZhbXA7U2lnQWxnPWh0dHAlM0ElMkYlMkZ3d3cudzMub3JnJTJGMjAwMSUyRjA0JTJGeG1sZHNpZy1tb3JlJTIzcnNhLXNoYTI1NiZhbXA7U2lnbmF0dXJlPUt6TzhYV0liVTZGdUVWZVZ4RFlNZzJ1T2xoZTlBQVAwd09uWlZVM3RVMU1ibWNKUDlXa3Q1Z0R3a2RKcXhDbUlJWGVDdnBhNDVLWUdlTzNFNWppampSOHlMUFpTalJUalJRem81V2h5bzJTaXRjTkxOZzZ4WFdZY0Z6bmdIcEdKeGRyJTJGVmxjSTR0RXFUNFZSN0VwbXp3amJtd1RaRGMyOW9hdEtZRGNUUjBjTjh2M0VtMVRIR0ZOc1B0bERvRVd5c0laWjFONkRpRVAxYmE4aTE5OVhoRiUyQldXZzVzNHdqMXltMnBDendQelJkYyUyRmpreDRQcG1MTyUyQjNVY3R3amoySG5RNmNiJTJCeHBsJTJCJTJCVUFwalZ1a3ZlNWdiempMbzI4JTJCUGklMkZXQmJ0Ym0lMkZrMzE5UlZ2dWFEdVVvJTJGM3VTUU1BJTJCbHR6b0ZIOGEwRTJ0Q0pNVGg1TWRnUm10ZyUzRCUzRCI+DQogIDxzYW1sMjpJc3N1ZXI+aHR0cDovL2FkZnMtMjAxMi50ZXN0Mi5zdXJmY29uZXh0Lm5sPC9zYW1sMjpJc3N1ZXI+PFNpZ25hdHVyZSB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PFNpZ25lZEluZm8+PENhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiIC8+PFNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiIC8+PFJlZmVyZW5jZSBVUkk9IiNfOTBlYWQxY2UtMzc2Yy00YTVkLTk4MDMtZDljYzgwMDYzZDRmIj48VHJhbnNmb3Jtcz48VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiIC8+PFRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIgLz48L1RyYW5zZm9ybXM+PERpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiIgLz48RGlnZXN0VmFsdWU+Q2ZKV1hpTXJLU0g0b3pPVWZTL0VNOXBXbFRSc3p2QkpMN2Y3MUZrZ3ZCbz08L0RpZ2VzdFZhbHVlPjwvUmVmZXJlbmNlPjwvU2lnbmVkSW5mbz48U2lnbmF0dXJlVmFsdWU+YzFqakFhdnVjMi9TSHZQYzdJdktJVkRSZUZuZVAzbTlITkZzTzErZEx2bWhxUXl5NkhoN3ZRMlFJalVGb2FybkYxZnJpOUY1RlNxL2JsR1I4RENvNEpndUlxKzNnRXBjN1JYR2EyTWc4dE5CV2tWdUg2UnZBTGM1Qk5DSDNtODVTTHgwcklGbERWc0tzZi9IQ0lTc2taV3Z6VVJGVVRFZnZjRVljWjZZZjNXYURmK1YvbE5jYXpCeVQ0L3RmNVVFN0VIM1JYckc2dUFqbHhjekN4a09UUE1SMnIvNmxwaEF1UmIxcjY1bThYQXFZNVFnbG5SVXBOM3U3ZEt4bGN5VUVaY2xKTW5JN21ya2NyMUdwODV6allkK0N4TmFwanNEQXplVjdSSVNLdHNaY0NFYWhncStIdTZVOEtBUmZHZmdFQ1dZNGY0c2lWRGp2d0NVaTlUME13PT08L1NpZ25hdHVyZVZhbHVlPjxLZXlJbmZvPjxYNTA5RGF0YT48WDUwOUNlcnRpZmljYXRlPk1JSURFekNDQWZ1Z0F3SUJBZ0lRU044elc2Q1lsSUpPQUZ6ZFN0VFQrakFOQmdrcWhraUc5dzBCQVFzRkFEQXNNU293S0FZRFZRUUREQ0Z6YVdkdWFXNW5MbVF5TURFeUxuUmxjM1F5TG5OMWNtWmpiMjVsZUhRdWJtd3dIaGNOTVRjd09USTBNVFl6TkRFM1doY05Nakl3T1RJMU1UWXpOREUzV2pBc01Tb3dLQVlEVlFRRERDRnphV2R1YVc1bkxtUXlNREV5TG5SbGMzUXlMbk4xY21aamIyNWxlSFF1Ym13d2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNkL1RXMUpvY05PV2w1aUZsN1JrWTQwa1A1U2NpQllCNnRJRXl3ZmxGNHRrRUR3S1Jxc1EyRXNaOTN1aWdnQi9wWFVhNlRHdlM3dnpRalJxZGhGZ0Nwb2htaGVwUzlQd3UvL0krcDY4VlpDdHNsdDFVSkd0NjJBRk9ad2FUU1FQbjRlR2RoRHI0c1g5TXIrdVVPU1plZWlEdHVFaGlNSWprZDJJYWJPeVNkOUxTK05Nc29pY1NoWEd5MERZR05yN2gyaHl1L2xUK3VMSnZsUFJ0V29aNDdpS1kwVUdpcVNJN013WlNQQjBoT2p5ZW9wbCsvWExENGhEKzNWVUFDMkttSDZaTzBBYWErc1JRZStNVFlVNHZvN29YTitkaEZoOG9VcFFrNjN4MjBtYTE1N3RSU1lqQlVwTURkclMvdk4vNWQ3c21URnQ1dFV4dlVHTGNGR0E4SkFnTUJBQUdqTVRBdk1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBZEJnTlZIUTRFRmdRVUZ2ZzR3d3ByTDBlWi9ZL09zSDkyVDMrK3RsVXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBRXEvQ2p5N2J0THFmWnp4dlZwUEJYZnVmYk5RSHJBeFY1QnA2a0QyY0NYUzlWQ0swdUdKdkZsemRVMDNDTE9uME4xYTJBUU5JSmZPZVg2dXlTQ1F1WXE0aDRWeUxVSVUyWE1QS1V3OGF2cWhuM0pxbGx4WUJuOE9XdENiRS9BWTdLU2lMWHk5V1BYcHRhdFpyeTV4T0x1SzYxZCtsSzJrdTlVc2xVcTY5b3BIZHhNZ3VqV0EvOUV1SkRINEVEblJ0c0lDT1oyZmxibEllQng4VU5zemExWjJ3NlIxUmtWQVN3YVZDL3JXOVpJaXhkTjdyQzUxQU1qU2YzUnBUc0cvT1Y2blNFcHJNa2hVWWRoSFdSb09XZk8rUGJmZGE1Sm85SHMyeHZENE43L2hPSjQzdC8wV0o1bng3NkNxMTNHcGlFYmlIbXZIQU1jS3R4aHVBS2M4NEk0PTwvWDUwOUNlcnRpZmljYXRlPjwvWDUwOURhdGE+PC9LZXlJbmZvPjwvU2lnbmF0dXJlPg0KICA8c2FtbDI6U3ViamVjdD4NCiAgICA8c2FtbDI6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6dW5zcGVjaWZpZWQiPnVybjpjb2xsYWI6cGVyc29uOmluc3RpdHV0aW9uLWEubmw6cGlldGVyLWExPC9zYW1sMjpOYW1lSUQ+DQogIDwvc2FtbDI6U3ViamVjdD4NCiAgPHNhbWwycDpSZXF1ZXN0ZWRBdXRobkNvbnRleHQgQ29tcGFyaXNvbj0iZXhhY3QiPg0KICAgIDxzYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj5odHRwOi8vdGVzdDIuc3VyZmNvbmV4dC5ubC9hc3N1cmFuY2Uvc2ZvLWxldmVsMjwvc2FtbDI6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogIDwvc2FtbDJwOlJlcXVlc3RlZEF1dGhuQ29udGV4dD4NCjwvc2FtbDJwOkF1dGhuUmVxdWVzdD4='
+ );
+ if (! isset($context['debugsp:AssertionConsumerServiceURL']) ) {
+ // Add some GET request parameters to the ACL URL. The IdP that can work with the ADFS SFO endpoint must use
+ // this exact URL so these parameters are returned. We do not actually verify this currently
+ $sspDebugSPACSURL = SimpleSAML\Module::getModuleURL('debugsp/acs/');
+ $sp=$_REQUEST['sp'];
+ $context['debugsp:AssertionConsumerServiceURL'] = $sspDebugSPACSURL.$sp.'?SAMLRequest=request_that_must_be_kept&Context=context_value_that_must_be_kept';
+ }
+ }
+
+ // SAML Extensions
+ $extensionChunks = array();
+
+ // SAML Extensions (email and SHO)
+ if ( ( isset($_REQUEST['email_extension']) && strlen($_REQUEST['email_extension']) > 0 ) ||
+ ( isset($_REQUEST['sho_extension']) && strlen($_REQUEST['sho_extension']) > 0 ) )
+ {
+ $attributes = array(); // Array of attribute name => attribute value
+ if ( isset($_REQUEST['email_extension']) && strlen($_REQUEST['email_extension']) > 0 ) {
+ $attributes['urn:mace:dir:attribute-def:mail'] = $_REQUEST['email_extension'];
+ }
+ if ( isset($_REQUEST['sho_extension']) && strlen($_REQUEST['sho_extension']) > 0 ) {
+ $attributes['urn:mace:terena.org:attribute-def:schacHomeOrganization'] = $_REQUEST['sho_extension'];
+ }
+
+ // A DOMDocument to hold the DOM structure and to serve as a factory object for DOMElements
+ // This is the method used in the Stepup-saml-bundle:
+ // https://github.com/OpenConext/Stepup-saml-bundle/blob/main/src/SAML2/Extensions/GsspUserAttributesChunk.php
+ // I'm not sure why this method is used over using new DOMElement() directly, and not using a DOMDocument at all
+ $dom = new DOMDocument('1.0', 'UTF-8');
+
+ // Create the UserAttributes extension element
+ $userAttributes = $dom->createElementNS('urn:mace:surf.nl:stepup:gssp-extensions', 'gssp:UserAttributes');
+ $userAttributes->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
+ $userAttributes->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xs', 'http://www.w3.org/2001/XMLSchema');
+
+ // Add the attributes to the extension
+ foreach ($attributes as $attributeName => $attributeValue) {
+ // Create the saml:Attribute element
+ $attribute = $dom->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:Attribute');
+ $attribute->setAttribute('NameFormat', 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri');
+ $attribute->setAttribute('Name', $attributeName);
+
+ // Create the saml:AttributeValue element
+ $attributeValue = $dom->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:AttributeValue', $attributeValue);
+ $attributeValue->setAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'xsi:type', 'xs:string');
+
+ // Append the saml:AttributeValue to saml:Attribute
+ $attribute->appendChild($attributeValue);
+
+ // Append the saml:Attribute to gssp:UserAttributes
+ $userAttributes->appendChild($attribute);
+ }
+
+ // Append the root element to the document
+ $dom->appendChild($userAttributes);
+
+ // Create a Chunk from the gssp:UserAttributes DOMElement
+ $userAttributesChunk = new \SAML2\XML\Chunk($userAttributes);
+
+ // Collect the gssp:UserAttributes chunk
+ $extensionChunks[] = $userAttributesChunk;
+ }
+
+ // mdui:UIInfo extension with a mdui:DisplayName (service name), as sent by e.g. OpenConext
+ // EngineBlock when initiating a Stepup callout. The Stepup-Gateway can read the DisplayName
+ // and forward it to the GSSP so the GSSP can show which service the user is authenticating for.
+ if ( isset($_REQUEST['mdui_displayname']) && strlen($_REQUEST['mdui_displayname']) > 0 )
+ {
+ $mduiDom = new DOMDocument('1.0', 'UTF-8');
+ $uiInfo = $mduiDom->createElementNS('urn:oasis:names:tc:SAML:metadata:ui', 'mdui:UIInfo');
+ $displayName = $mduiDom->createElementNS('urn:oasis:names:tc:SAML:metadata:ui', 'mdui:DisplayName');
+ $displayName->setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:lang', 'en');
+ $displayName->textContent = $_REQUEST['mdui_displayname'];
+ $uiInfo->appendChild($displayName);
+ $mduiDom->appendChild($uiInfo);
+ $extensionChunks[] = new \SAML2\XML\Chunk($uiInfo);
+ }
+
+ // Add the collected extensions to the SAML request
+ // The SAML2 library uses the 'saml:Extensions' element to hold the extensions which is an array of
+ // SAML2\XML\Chunk objects, one for each extension to add.
+ // The SSP will then add the extensions to the SAML request by calling setExtensions(array $extensions) : void
+ // on the SAML2\AuthnRequest object with the array of Chunk objects.
+ if ( count($extensionChunks) > 0 ) {
+ $context['saml:Extensions'] = $extensionChunks;
+ }
+
+ // login
+ $as->login( $context );
+
+ exit; // Added for clarity
+}
+
+if( isset($_REQUEST['action']) && $_REQUEST['action'] == 'logout' ) {
+ $as->logout( array (
+ 'ReturnTo' => $returnURL,
+ ) ); // Process logout
+ exit; // Added for clarity
+}
+
+if( isset($_REQUEST['action']) && $_REQUEST['action'] == 'reset' ) {
+ $session->deleteData('array', 'SSP_DEMO_SP_FORM_DATA');
+ $_REQUEST=array();
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Output HTML
+
+$saved_parameters = $session->getData('array', 'SSP_DEMO_SP_FORM_DATA');
+if (!is_array($saved_parameters)) {
+ $saved_parameters = array();
+}
+if (!isset($_REQUEST['idp'])) {
+ $_REQUEST=array_merge($saved_parameters, $_REQUEST);
+}
+
+$idp=htmlentities(isset($_REQUEST['idp']) ? $_REQUEST['idp'] : "");
+$loa=htmlentities(isset($_REQUEST['loa']) ? $_REQUEST['loa'] : "");
+$nameidpolicy=htmlentities(isset($_REQUEST['nameidpolicy']) ? $_REQUEST['nameidpolicy'] : "");
+$ssobinding=htmlentities(isset($_REQUEST['ssobinding']) ? $_REQUEST['ssobinding'] : "");
+$requesterid=htmlentities(isset($_REQUEST['requesterid']) ? $_REQUEST['requesterid'] : "");
+$requesterid2=htmlentities(isset($_REQUEST['requesterid2']) ? $_REQUEST['requesterid2'] : "");
+$email_extension=htmlentities(isset($_REQUEST['email_extension']) ? $_REQUEST['email_extension'] : "");
+$sho_extension=htmlentities(isset($_REQUEST['sho_extension']) ? $_REQUEST['sho_extension'] : "");
+$mdui_displayname=htmlentities(isset($_REQUEST['mdui_displayname']) ? $_REQUEST['mdui_displayname'] : "");
+$scopingIDP=htmlentities(isset($_REQUEST['scopingIDP']) ? $_REQUEST['scopingIDP'] : "");
+$scopingIDP2=htmlentities(isset($_REQUEST['scopingIDP2']) ? $_REQUEST['scopingIDP2'] : "");
+$sp=htmlentities(isset($_REQUEST['sp']) ? $_REQUEST['sp'] : "default-sp");
+$subject=htmlentities(isset($_REQUEST['subject']) ? $_REQUEST['subject'] : "");
+$acsurl=htmlentities(isset($_REQUEST['acsurl']) ? $_REQUEST['acsurl'] : "");
+
+echo <<
+
+
+
+
+ simpleSAMLphp Test SP
+
+
+
';
+ }
+ else
+ {
+ echo 'Error decoding SAMLResponse (invalid base64) ';
+ }
+}
+
+echo <<
+
+html;
+
diff --git a/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php b/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php
index 88ac22e..6aca6b5 100644
--- a/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php
+++ b/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php
@@ -137,6 +137,29 @@ 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);
+ }
+
/**
* @When I start an SFO authentication for :arg1
*/
diff --git a/stepup/tests/behat/features/gssp_service_name.feature b/stepup/tests/behat/features/gssp_service_name.feature
new file mode 100644
index 0000000..e99f3bf
--- /dev/null
+++ b/stepup/tests/behat/features/gssp_service_name.feature
@@ -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: demogssp:
+# 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
+ 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
From b8904f00dd2501bfed4f001d6702d837fbd3ad58 Mon Sep 17 00:00:00 2001
From: Kay Joosten
Date: Wed, 22 Jul 2026 11:45:28 +0200
Subject: [PATCH 2/7] Add playwright coverage and GSSP registration-page step
for service name
Playwright suite exercises the mdui:UIInfo service-name flow end to end
against the running devconf stack, complementing the existing behat
scenario. Also adds a missing "on the GSSP registration page" assertion
step alongside the existing authentication-page one.
---
.../bootstrap/SecondFactorAuthContext.php | 9 +++
stepup/tests/playwright/.gitignore | 5 ++
stepup/tests/playwright/README.md | 60 ++++++++++++++
stepup/tests/playwright/lib/middleware.ts | 54 +++++++++++++
stepup/tests/playwright/package-lock.json | 76 +++++++++++++++++
stepup/tests/playwright/package.json | 10 +++
stepup/tests/playwright/playwright.config.ts | 17 ++++
.../playwright/tests/service-name.spec.ts | 81 +++++++++++++++++++
8 files changed, 312 insertions(+)
create mode 100644 stepup/tests/playwright/.gitignore
create mode 100644 stepup/tests/playwright/README.md
create mode 100644 stepup/tests/playwright/lib/middleware.ts
create mode 100644 stepup/tests/playwright/package-lock.json
create mode 100644 stepup/tests/playwright/package.json
create mode 100644 stepup/tests/playwright/playwright.config.ts
create mode 100644 stepup/tests/playwright/tests/service-name.spec.ts
diff --git a/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php b/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php
index 6aca6b5..2372956 100644
--- a/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php
+++ b/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php
@@ -160,6 +160,15 @@ public function iSeeServiceNameOnTheGsspAuthenticationPage(string $serviceName)
$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
*/
diff --git a/stepup/tests/playwright/.gitignore b/stepup/tests/playwright/.gitignore
new file mode 100644
index 0000000..da0275b
--- /dev/null
+++ b/stepup/tests/playwright/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+test-results/
+playwright-report/
+.playwright-mcp/
+*.png
diff --git a/stepup/tests/playwright/README.md b/stepup/tests/playwright/README.md
new file mode 100644
index 0000000..d4c05df
--- /dev/null
+++ b/stepup/tests/playwright/README.md
@@ -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.
diff --git a/stepup/tests/playwright/lib/middleware.ts b/stepup/tests/playwright/lib/middleware.ts
new file mode 100644
index 0000000..1577c9e
--- /dev/null
+++ b/stepup/tests/playwright/lib/middleware.ts
@@ -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 {
+ 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)}`);
+ }
+}
diff --git a/stepup/tests/playwright/package-lock.json b/stepup/tests/playwright/package-lock.json
new file mode 100644
index 0000000..6b95315
--- /dev/null
+++ b/stepup/tests/playwright/package-lock.json
@@ -0,0 +1,76 @@
+{
+ "name": "stepup-service-name-e2e",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "stepup-service-name-e2e",
+ "devDependencies": {
+ "@playwright/test": "^1.48.0"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
+ "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.61.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
+ "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.61.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
+ "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ }
+ }
+}
diff --git a/stepup/tests/playwright/package.json b/stepup/tests/playwright/package.json
new file mode 100644
index 0000000..64a687d
--- /dev/null
+++ b/stepup/tests/playwright/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "stepup-service-name-e2e",
+ "private": true,
+ "scripts": {
+ "test": "playwright test"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.48.0"
+ }
+}
diff --git a/stepup/tests/playwright/playwright.config.ts b/stepup/tests/playwright/playwright.config.ts
new file mode 100644
index 0000000..0e0f9b8
--- /dev/null
+++ b/stepup/tests/playwright/playwright.config.ts
@@ -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',
+ },
+});
diff --git a/stepup/tests/playwright/tests/service-name.spec.ts b/stepup/tests/playwright/tests/service-name.spec.ts
new file mode 100644
index 0000000..ceb69ea
--- /dev/null
+++ b/stepup/tests/playwright/tests/service-name.spec.ts
@@ -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 {
+ 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 });
+ });
+});
From fa374f03880576914c1056418682e10132254a12 Mon Sep 17 00:00:00 2001
From: Kay Joosten
Date: Wed, 22 Jul 2026 12:27:52 +0200
Subject: [PATCH 3/7] Drop local sp.php override now that the fix moved
upstream to devssp
mdui_displayname support is now a real patch against
OpenConext-devssp's sp.php instead of a full-file mount, so the
711-line local copy and its docker-compose override are no longer
needed here. Not pushed yet: this depends on the devssp image being
rebuilt with that patch, so the test/behat run against the current
:latest image will not have the mdui field until then.
---
stepup/docker-compose.yml | 3 -
stepup/ssp/sp.php | 711 ------------------
.../behat/features/gssp_service_name.feature | 7 +-
.../playwright/tests/service-name.spec.ts | 5 +-
4 files changed, 5 insertions(+), 721 deletions(-)
delete mode 100644 stepup/ssp/sp.php
diff --git a/stepup/docker-compose.yml b/stepup/docker-compose.yml
index 0d779ae..047710b 100644
--- a/stepup/docker-compose.yml
+++ b/stepup/docker-compose.yml
@@ -97,9 +97,6 @@ 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
diff --git a/stepup/ssp/sp.php b/stepup/ssp/sp.php
deleted file mode 100644
index 704a3cf..0000000
--- a/stepup/ssp/sp.php
+++ /dev/null
@@ -1,711 +0,0 @@
-isAuthenticated();
-
-// Build return URL. This is where ask simplesamlPHP to direct the browser to after login or logout
-// Point to this script, but without any request parameters so we won't trigger an login again (and again, and again, and ...)
-$returnURL = ($_SERVER['HTTPS'] == 'on') ? 'https://' : 'http://';
-$returnURL .= $_SERVER['HTTP_HOST'];
-$returnURL .= $_SERVER['SCRIPT_NAME'];
-$returnURL .= '?sp='.urlencode($sp);
-
-// Process login and logout actions. Neither login nor logout return
-if (isset($_REQUEST['action']) && $_REQUEST['action'] == 'login' ) {
-
- // Save submitted form in session
- $params_to_save=$_REQUEST;
- unset($params_to_save['action']);
- $session->setData('array', 'SSP_DEMO_SP_FORM_DATA', $params_to_save);
-
- // Unset existing RequiredAuthnContextClassRef first
- $session->deleteData('string', 'RequiredAuthnContextClassRef');
- $bForceAuthn = false;
- if ( (isset($_REQUEST['forceauthn'])) && ($_REQUEST['forceauthn'] == 'true') )
- $bForceAuthn = true;
-
- // For use by SAML2Keeper callback function
- $session->setData('string', 'SAML2Keeper_ReturnTo', $returnURL);
-
- $context = array(
- 'ReturnTo' => $returnURL,
- 'ReturnCallback' => array('sspmod_saml2keeper_SAML2Keeper','loginCallback'),
- 'ForceAuthn' => $bForceAuthn,
- 'saml:NameIDPolicy' => null,
- );
-
- // IdP
- if ( (isset($_REQUEST['idp'])) ) {
- $context['saml:idp'] = $_REQUEST['idp'];
- }
-
- // LOA
- if ( isset($_REQUEST['loa']) && isset($_REQUEST['idp']) && isset($gIDPmap[$_REQUEST['idp']]['loa'][$_REQUEST['loa']]) ) {
- $loa = $gIDPmap[$_REQUEST['idp']]['loa'][$_REQUEST['loa']];
- // Store the requested LOA in the session so we can verify it later
- $session->setData('string', 'RequiredAuthnContextClassRef', $loa);
- $context['saml:AuthnContextClassRef'] = $loa; // Specify LOA
- }
-
- // Scoping IdPList
- if ( isset($_REQUEST['scopingIDP']) && strlen($_REQUEST['scopingIDP']) > 0 ) {
- $context['saml:IDPList'] = array($_REQUEST['scopingIDP']);
-
- if ( isset($_REQUEST['scopingIDP2']) && strlen($_REQUEST['scopingIDP2']) > 0 ) {
- $context['saml:IDPList'][]=$_REQUEST['scopingIDP2'];
- }
- }
-
- // RequesterID
- if ( isset($_REQUEST['requesterid']) && strlen($_REQUEST['requesterid']) > 0 ) {
- $context['saml:RequesterID'] = array($_REQUEST['requesterid']);
-
- if ( isset($_REQUEST['requesterid2']) && strlen($_REQUEST['requesterid2']) > 0 ) {
- $context['saml:RequesterID'][] = $_REQUEST['requesterid2'];
- }
- }
-
- // NameIDPolicy
- if ( isset($_REQUEST['nameidpolicy']) && strlen($_REQUEST['nameidpolicy']) > 0 ) {
- $context['saml:NameIDPolicy'] = $_REQUEST['nameidpolicy'];
- }
-
- // Subject NameID
- if ( isset($_REQUEST['subject']) && strlen($_REQUEST['subject']) > 0 ) {
- $nameId = new \SAML2\XML\saml\NameID();
- $nameId->setValue($_REQUEST['subject']); // Use value of "NameID" attribute
- $nameId->setFormat(\SAML2\Constants::NAMEID_UNSPECIFIED); // Unspecified NameID
- $context['saml:NameID'] = $nameId;
- }
-
- // AssertionConsumerServiceURL
- if ( isset($_REQUEST['acsurl']) && strlen($_REQUEST['acsurl']) > 0 ) {
- $context['debugsp:AssertionConsumerServiceURL'] = $_REQUEST['acsurl'];
- }
-
- // Emulate an authentication request by the SURF MFA extension for ADFS. See: https://github.com/SURFnet/ADFS-MFA-SAML2.0-Extension
- // The second factor only (SFO) endpoint of the Stepup-Gateway (see: https://github.com/OpenConext/Stepup-Gateway) will give a different
- // SAML Response when this is enabled. This option allows testing this behaviour without having to install an ADFS server.
- // The trigger for this behaviour in the stepup-gateway is the presence of the 'Context' and 'AuthMethod' POST variables. This also means that a HTTP-POST
- // binding must be used.
- // Additionally the SURF MFA extension for ADFS sets the ACS location in the SAML Request to the URL where the response must be posted.
- // We add some dummy URL parameters to the ACS location for this purpose.
- // Because the actual SAML response is posted in the "_SAMLResponse" parameter (instead of "SAMLResponse") the acs location of debugsp module
- // must be used to validate the response as that will rename the "_SAMLResponse" POST variable back to "SAMLResponse" so that
- // SimpleSAMLphp can then process it normally.
- // For more information See: https://github.com/OpenConext/Stepup-Gateway/blob/main/docs/SFO.md#adfs-mfa-extension
- if ( (isset($_REQUEST['emulateadfs'])) && ($_REQUEST['emulateadfs'] == 'true') )
- {
- // Switch binding to HTTP-POST
- $ssobinding = $GLOBALS['gSP_SSOBinding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST';
-
- // Set AuthMethod and mock Context like the SURF MFA extension for ADFS would do
- $context['debugsp:extraPOSTvars'] = array(
- 'AuthMethod' => 'ADFS.SCSA',
- 'Context' => '1C63828278F1B0AC2FE61429E099FFA7AC94917CQe1t1xgG78zLHhUxBXm0ous4yl0zfQumsKI79lrHMOIjdTdeF/i1Yx+pQ+mgnubT9mh+DfBYMs7wU1g+eXiAs2gnwKWmnMzeuxgG+m5Nky5Wd63NcEgLZ2zNTYuW70X514HMtLAw+l1H8cptQMXfXt9ageHOdY+65eq4IsNwnB0mPhRkua58R9xO3I4MfBzy90GqwgjmDeZAo5vsKgk0iZRgZ1CS4hPyIWX+ryU2tnYp5UEuDE9gGlR9cQr2uHW10LOG22ZfEy8rJie2T2A2bCQVyF47nmBnvoKYV6YyEDpozSYJpUqHmIgvaWgFu5dvDvZ0fvrVQaQ1ZUKHTT76Cg==6SO/qvyH0bmayeNGyzqAy/Oim2UAOvhxm18rTs+72Qm2fSK6Pfo1ZEDNKmLRk6IemCvkUYWMa4VmxIdATswREx/aSrp4YS3QejDBoZlCwz4LqFWJMiqTPxJfWhahP0hBNEORN8cU5vBQXXIahWqlkaHzs6IPjH4WoMe5vsSKVTetaOMbMC3ZML67BWpAnEXKWoR/gar1jH5v961ljdKJozzgwsJIAY4TNSoB+AEzRd4C3wLSTCott1DyRtMmEmS5DpaDOaxmZ/X+z16t1hb9VKgEqt1xZJ0uw451d5oeuisN9zSqbWQzyiJdkk6k11YU9q2rvg342qLJk6xeTtRc6+DLQ24vZIHC8RU2jcHveLDJvOq89BBJ0LHtnV/7PJpb4PGf1OUqWZidnRAS0/dqprEVzPEnvdzIJ8vPRGzE0dkQhgzDi+cbMsuZrDqYWaMuodvDbGrETxZ9hu0MI3l9pgjuIh8xF7TT/6qTJnGExRaGFebcjMXC99thZ3A7XeJESDNXNxgDgFQf6OwHLjLuhw==va+cZ6Y7NIyBU9vCVb+qRGSx0Yk=DNF3KVEb8ju/T+ise1j0QBS2OYsepwzgWaUtOASvUPI6NPlvyQIHxX1Py6oHcUkbWP1jaVTzwEGadaq428nPMWSeU/MDWqyz2jyrwuIUWglc64AMlcXd0BOdT1I6khKMsUGY8CSa1tRD2arcIH1TUrrk7jY3qfAGtgNbFlElPwc/2l4dkN7QXdHRcmntFp4D/9yEG9FkWzTyXLvCvGqcQeu8L1fKTwq8Upqk9iT2PKnmT/gH+IUt3votmCMV9bxYols0aQWfv2RX2HX3Gow9xKZuOn+ckjZRqBJ1Kp9wGMAB65XQPli5UQzezEHX28oUPH/PEgnu6RKDgsN55h22ag==DB540D051F8F73EA2F3B5190BFC0F349E595EB34 SAMLRequest: PHNhbWwycDpBdXRoblJlcXVlc3QgeG1sbnM6c2FtbDJwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iXzkwZWFkMWNlLTM3NmMtNGE1ZC05ODAzLWQ5Y2M4MDA2M2Q0ZiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTgtMDQtMjNUMTM6NDk6NTBaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9zYS1ndy50ZXN0Mi5zdXJmY29uZXh0Lm5sL3NlY29uZC1mYWN0b3Itb25seS9zaW5nbGUtc2lnbi1vbiIgQXNzZXJ0aW9uQ29uc3VtZXJTZXJ2aWNlVVJMPSJodHRwczovL2FkZnMtMjAxMi50ZXN0Mi5zdXJmY29uZXh0Lm5sOjQ0My9hZGZzL2xzLz9TQU1MUmVxdWVzdD1wVkpOanhNeERQMHJvOXpubzVscFlhTzJVdGtLVVdtQmFsczRjRUZ1eGtNalpaSWhkbUQ1OTZRcGlJVkRMNXdTUGZzOVB6OTVTVERhU1cwaW45MGpmbzFJWER5TjFwSEtoWldJd1NrUFpFZzVHSkVVYTNYWXZIMVFzbXJVRkR4NzdhMTRScm5OQUNJTWJMd1R4VzY3RXAlMkZiMmVtdTBYTG90SnpMUmklMkJHV1R1SHJwJTJGTCUyQlFKMXYyaFBKOTNjZFMwTUlJcVBHQ2d4VnlJSkpUcFJ4SjBqQnNjSmFtWXZ5NllyWlh1Y3RhcGJxUGJGSjFGczB6YkdBV2ZXbVhraVZkZlFEMVNtZmxseEtzdUtZaGkwZCUyRmpFbGJPNVdsdXFSYkg1YmZYZU80b2poZ09HYjBiamg4ZUhQMktUUWNaUUFaaXM0ekNMa0Jrbml6bkE4MVNQdm84V3E4djNBdFYwZldVSm1qTGE0d0RSY2ttVEtQYSUyRkluMWxYRyUyRmNsOXRwbnE1TnBONGNqJTJGdHklMkYlMkY1d0ZPdmxSVnZsZE1MNlAyMk95TkFEd3o4dWwlMkZYekdjdnJCYjFMN25iYnZiZEclMkZ5aGUlMkJ6QUMzelolMkZRVXhmRHJsVmNRQkhCaDJuNEszMTMlMkI4REF1TktjSWdvNnZWMTVOOTN1djRKJmFtcDtSZWxheVN0YXRlPWh0dHBzJTNBJTJGJTJGcGlldGVyLmFhaS5zdXJmbmV0Lm5sJTJGc2ltcGxlc2FtbHBocCUyRnNwLnBocCUzRnNwJTNEZGVmYXVsdC1zcCZhbXA7U2lnQWxnPWh0dHAlM0ElMkYlMkZ3d3cudzMub3JnJTJGMjAwMSUyRjA0JTJGeG1sZHNpZy1tb3JlJTIzcnNhLXNoYTI1NiZhbXA7U2lnbmF0dXJlPUt6TzhYV0liVTZGdUVWZVZ4RFlNZzJ1T2xoZTlBQVAwd09uWlZVM3RVMU1ibWNKUDlXa3Q1Z0R3a2RKcXhDbUlJWGVDdnBhNDVLWUdlTzNFNWppampSOHlMUFpTalJUalJRem81V2h5bzJTaXRjTkxOZzZ4WFdZY0Z6bmdIcEdKeGRyJTJGVmxjSTR0RXFUNFZSN0VwbXp3amJtd1RaRGMyOW9hdEtZRGNUUjBjTjh2M0VtMVRIR0ZOc1B0bERvRVd5c0laWjFONkRpRVAxYmE4aTE5OVhoRiUyQldXZzVzNHdqMXltMnBDendQelJkYyUyRmpreDRQcG1MTyUyQjNVY3R3amoySG5RNmNiJTJCeHBsJTJCJTJCVUFwalZ1a3ZlNWdiempMbzI4JTJCUGklMkZXQmJ0Ym0lMkZrMzE5UlZ2dWFEdVVvJTJGM3VTUU1BJTJCbHR6b0ZIOGEwRTJ0Q0pNVGg1TWRnUm10ZyUzRCUzRCI+DQogIDxzYW1sMjpJc3N1ZXI+aHR0cDovL2FkZnMtMjAxMi50ZXN0Mi5zdXJmY29uZXh0Lm5sPC9zYW1sMjpJc3N1ZXI+PFNpZ25hdHVyZSB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PFNpZ25lZEluZm8+PENhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiIC8+PFNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiIC8+PFJlZmVyZW5jZSBVUkk9IiNfOTBlYWQxY2UtMzc2Yy00YTVkLTk4MDMtZDljYzgwMDYzZDRmIj48VHJhbnNmb3Jtcz48VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiIC8+PFRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIgLz48L1RyYW5zZm9ybXM+PERpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiIgLz48RGlnZXN0VmFsdWU+Q2ZKV1hpTXJLU0g0b3pPVWZTL0VNOXBXbFRSc3p2QkpMN2Y3MUZrZ3ZCbz08L0RpZ2VzdFZhbHVlPjwvUmVmZXJlbmNlPjwvU2lnbmVkSW5mbz48U2lnbmF0dXJlVmFsdWU+YzFqakFhdnVjMi9TSHZQYzdJdktJVkRSZUZuZVAzbTlITkZzTzErZEx2bWhxUXl5NkhoN3ZRMlFJalVGb2FybkYxZnJpOUY1RlNxL2JsR1I4RENvNEpndUlxKzNnRXBjN1JYR2EyTWc4dE5CV2tWdUg2UnZBTGM1Qk5DSDNtODVTTHgwcklGbERWc0tzZi9IQ0lTc2taV3Z6VVJGVVRFZnZjRVljWjZZZjNXYURmK1YvbE5jYXpCeVQ0L3RmNVVFN0VIM1JYckc2dUFqbHhjekN4a09UUE1SMnIvNmxwaEF1UmIxcjY1bThYQXFZNVFnbG5SVXBOM3U3ZEt4bGN5VUVaY2xKTW5JN21ya2NyMUdwODV6allkK0N4TmFwanNEQXplVjdSSVNLdHNaY0NFYWhncStIdTZVOEtBUmZHZmdFQ1dZNGY0c2lWRGp2d0NVaTlUME13PT08L1NpZ25hdHVyZVZhbHVlPjxLZXlJbmZvPjxYNTA5RGF0YT48WDUwOUNlcnRpZmljYXRlPk1JSURFekNDQWZ1Z0F3SUJBZ0lRU044elc2Q1lsSUpPQUZ6ZFN0VFQrakFOQmdrcWhraUc5dzBCQVFzRkFEQXNNU293S0FZRFZRUUREQ0Z6YVdkdWFXNW5MbVF5TURFeUxuUmxjM1F5TG5OMWNtWmpiMjVsZUhRdWJtd3dIaGNOTVRjd09USTBNVFl6TkRFM1doY05Nakl3T1RJMU1UWXpOREUzV2pBc01Tb3dLQVlEVlFRRERDRnphV2R1YVc1bkxtUXlNREV5TG5SbGMzUXlMbk4xY21aamIyNWxlSFF1Ym13d2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNkL1RXMUpvY05PV2w1aUZsN1JrWTQwa1A1U2NpQllCNnRJRXl3ZmxGNHRrRUR3S1Jxc1EyRXNaOTN1aWdnQi9wWFVhNlRHdlM3dnpRalJxZGhGZ0Nwb2htaGVwUzlQd3UvL0krcDY4VlpDdHNsdDFVSkd0NjJBRk9ad2FUU1FQbjRlR2RoRHI0c1g5TXIrdVVPU1plZWlEdHVFaGlNSWprZDJJYWJPeVNkOUxTK05Nc29pY1NoWEd5MERZR05yN2gyaHl1L2xUK3VMSnZsUFJ0V29aNDdpS1kwVUdpcVNJN013WlNQQjBoT2p5ZW9wbCsvWExENGhEKzNWVUFDMkttSDZaTzBBYWErc1JRZStNVFlVNHZvN29YTitkaEZoOG9VcFFrNjN4MjBtYTE1N3RSU1lqQlVwTURkclMvdk4vNWQ3c21URnQ1dFV4dlVHTGNGR0E4SkFnTUJBQUdqTVRBdk1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBZEJnTlZIUTRFRmdRVUZ2ZzR3d3ByTDBlWi9ZL09zSDkyVDMrK3RsVXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBRXEvQ2p5N2J0THFmWnp4dlZwUEJYZnVmYk5RSHJBeFY1QnA2a0QyY0NYUzlWQ0swdUdKdkZsemRVMDNDTE9uME4xYTJBUU5JSmZPZVg2dXlTQ1F1WXE0aDRWeUxVSVUyWE1QS1V3OGF2cWhuM0pxbGx4WUJuOE9XdENiRS9BWTdLU2lMWHk5V1BYcHRhdFpyeTV4T0x1SzYxZCtsSzJrdTlVc2xVcTY5b3BIZHhNZ3VqV0EvOUV1SkRINEVEblJ0c0lDT1oyZmxibEllQng4VU5zemExWjJ3NlIxUmtWQVN3YVZDL3JXOVpJaXhkTjdyQzUxQU1qU2YzUnBUc0cvT1Y2blNFcHJNa2hVWWRoSFdSb09XZk8rUGJmZGE1Sm85SHMyeHZENE43L2hPSjQzdC8wV0o1bng3NkNxMTNHcGlFYmlIbXZIQU1jS3R4aHVBS2M4NEk0PTwvWDUwOUNlcnRpZmljYXRlPjwvWDUwOURhdGE+PC9LZXlJbmZvPjwvU2lnbmF0dXJlPg0KICA8c2FtbDI6U3ViamVjdD4NCiAgICA8c2FtbDI6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6dW5zcGVjaWZpZWQiPnVybjpjb2xsYWI6cGVyc29uOmluc3RpdHV0aW9uLWEubmw6cGlldGVyLWExPC9zYW1sMjpOYW1lSUQ+DQogIDwvc2FtbDI6U3ViamVjdD4NCiAgPHNhbWwycDpSZXF1ZXN0ZWRBdXRobkNvbnRleHQgQ29tcGFyaXNvbj0iZXhhY3QiPg0KICAgIDxzYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj5odHRwOi8vdGVzdDIuc3VyZmNvbmV4dC5ubC9hc3N1cmFuY2Uvc2ZvLWxldmVsMjwvc2FtbDI6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogIDwvc2FtbDJwOlJlcXVlc3RlZEF1dGhuQ29udGV4dD4NCjwvc2FtbDJwOkF1dGhuUmVxdWVzdD4='
- );
- if (! isset($context['debugsp:AssertionConsumerServiceURL']) ) {
- // Add some GET request parameters to the ACL URL. The IdP that can work with the ADFS SFO endpoint must use
- // this exact URL so these parameters are returned. We do not actually verify this currently
- $sspDebugSPACSURL = SimpleSAML\Module::getModuleURL('debugsp/acs/');
- $sp=$_REQUEST['sp'];
- $context['debugsp:AssertionConsumerServiceURL'] = $sspDebugSPACSURL.$sp.'?SAMLRequest=request_that_must_be_kept&Context=context_value_that_must_be_kept';
- }
- }
-
- // SAML Extensions
- $extensionChunks = array();
-
- // SAML Extensions (email and SHO)
- if ( ( isset($_REQUEST['email_extension']) && strlen($_REQUEST['email_extension']) > 0 ) ||
- ( isset($_REQUEST['sho_extension']) && strlen($_REQUEST['sho_extension']) > 0 ) )
- {
- $attributes = array(); // Array of attribute name => attribute value
- if ( isset($_REQUEST['email_extension']) && strlen($_REQUEST['email_extension']) > 0 ) {
- $attributes['urn:mace:dir:attribute-def:mail'] = $_REQUEST['email_extension'];
- }
- if ( isset($_REQUEST['sho_extension']) && strlen($_REQUEST['sho_extension']) > 0 ) {
- $attributes['urn:mace:terena.org:attribute-def:schacHomeOrganization'] = $_REQUEST['sho_extension'];
- }
-
- // A DOMDocument to hold the DOM structure and to serve as a factory object for DOMElements
- // This is the method used in the Stepup-saml-bundle:
- // https://github.com/OpenConext/Stepup-saml-bundle/blob/main/src/SAML2/Extensions/GsspUserAttributesChunk.php
- // I'm not sure why this method is used over using new DOMElement() directly, and not using a DOMDocument at all
- $dom = new DOMDocument('1.0', 'UTF-8');
-
- // Create the UserAttributes extension element
- $userAttributes = $dom->createElementNS('urn:mace:surf.nl:stepup:gssp-extensions', 'gssp:UserAttributes');
- $userAttributes->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
- $userAttributes->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xs', 'http://www.w3.org/2001/XMLSchema');
-
- // Add the attributes to the extension
- foreach ($attributes as $attributeName => $attributeValue) {
- // Create the saml:Attribute element
- $attribute = $dom->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:Attribute');
- $attribute->setAttribute('NameFormat', 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri');
- $attribute->setAttribute('Name', $attributeName);
-
- // Create the saml:AttributeValue element
- $attributeValue = $dom->createElementNS('urn:oasis:names:tc:SAML:2.0:assertion', 'saml:AttributeValue', $attributeValue);
- $attributeValue->setAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'xsi:type', 'xs:string');
-
- // Append the saml:AttributeValue to saml:Attribute
- $attribute->appendChild($attributeValue);
-
- // Append the saml:Attribute to gssp:UserAttributes
- $userAttributes->appendChild($attribute);
- }
-
- // Append the root element to the document
- $dom->appendChild($userAttributes);
-
- // Create a Chunk from the gssp:UserAttributes DOMElement
- $userAttributesChunk = new \SAML2\XML\Chunk($userAttributes);
-
- // Collect the gssp:UserAttributes chunk
- $extensionChunks[] = $userAttributesChunk;
- }
-
- // mdui:UIInfo extension with a mdui:DisplayName (service name), as sent by e.g. OpenConext
- // EngineBlock when initiating a Stepup callout. The Stepup-Gateway can read the DisplayName
- // and forward it to the GSSP so the GSSP can show which service the user is authenticating for.
- if ( isset($_REQUEST['mdui_displayname']) && strlen($_REQUEST['mdui_displayname']) > 0 )
- {
- $mduiDom = new DOMDocument('1.0', 'UTF-8');
- $uiInfo = $mduiDom->createElementNS('urn:oasis:names:tc:SAML:metadata:ui', 'mdui:UIInfo');
- $displayName = $mduiDom->createElementNS('urn:oasis:names:tc:SAML:metadata:ui', 'mdui:DisplayName');
- $displayName->setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:lang', 'en');
- $displayName->textContent = $_REQUEST['mdui_displayname'];
- $uiInfo->appendChild($displayName);
- $mduiDom->appendChild($uiInfo);
- $extensionChunks[] = new \SAML2\XML\Chunk($uiInfo);
- }
-
- // Add the collected extensions to the SAML request
- // The SAML2 library uses the 'saml:Extensions' element to hold the extensions which is an array of
- // SAML2\XML\Chunk objects, one for each extension to add.
- // The SSP will then add the extensions to the SAML request by calling setExtensions(array $extensions) : void
- // on the SAML2\AuthnRequest object with the array of Chunk objects.
- if ( count($extensionChunks) > 0 ) {
- $context['saml:Extensions'] = $extensionChunks;
- }
-
- // login
- $as->login( $context );
-
- exit; // Added for clarity
-}
-
-if( isset($_REQUEST['action']) && $_REQUEST['action'] == 'logout' ) {
- $as->logout( array (
- 'ReturnTo' => $returnURL,
- ) ); // Process logout
- exit; // Added for clarity
-}
-
-if( isset($_REQUEST['action']) && $_REQUEST['action'] == 'reset' ) {
- $session->deleteData('array', 'SSP_DEMO_SP_FORM_DATA');
- $_REQUEST=array();
-}
-
-
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-// Output HTML
-
-$saved_parameters = $session->getData('array', 'SSP_DEMO_SP_FORM_DATA');
-if (!is_array($saved_parameters)) {
- $saved_parameters = array();
-}
-if (!isset($_REQUEST['idp'])) {
- $_REQUEST=array_merge($saved_parameters, $_REQUEST);
-}
-
-$idp=htmlentities(isset($_REQUEST['idp']) ? $_REQUEST['idp'] : "");
-$loa=htmlentities(isset($_REQUEST['loa']) ? $_REQUEST['loa'] : "");
-$nameidpolicy=htmlentities(isset($_REQUEST['nameidpolicy']) ? $_REQUEST['nameidpolicy'] : "");
-$ssobinding=htmlentities(isset($_REQUEST['ssobinding']) ? $_REQUEST['ssobinding'] : "");
-$requesterid=htmlentities(isset($_REQUEST['requesterid']) ? $_REQUEST['requesterid'] : "");
-$requesterid2=htmlentities(isset($_REQUEST['requesterid2']) ? $_REQUEST['requesterid2'] : "");
-$email_extension=htmlentities(isset($_REQUEST['email_extension']) ? $_REQUEST['email_extension'] : "");
-$sho_extension=htmlentities(isset($_REQUEST['sho_extension']) ? $_REQUEST['sho_extension'] : "");
-$mdui_displayname=htmlentities(isset($_REQUEST['mdui_displayname']) ? $_REQUEST['mdui_displayname'] : "");
-$scopingIDP=htmlentities(isset($_REQUEST['scopingIDP']) ? $_REQUEST['scopingIDP'] : "");
-$scopingIDP2=htmlentities(isset($_REQUEST['scopingIDP2']) ? $_REQUEST['scopingIDP2'] : "");
-$sp=htmlentities(isset($_REQUEST['sp']) ? $_REQUEST['sp'] : "default-sp");
-$subject=htmlentities(isset($_REQUEST['subject']) ? $_REQUEST['subject'] : "");
-$acsurl=htmlentities(isset($_REQUEST['acsurl']) ? $_REQUEST['acsurl'] : "");
-
-echo <<
-
-
-
-
- simpleSAMLphp Test SP
-
-
-
';
- }
- else
- {
- echo 'Error decoding SAMLResponse (invalid base64) ';
- }
-}
-
-echo <<
-
-html;
-
diff --git a/stepup/tests/behat/features/gssp_service_name.feature b/stepup/tests/behat/features/gssp_service_name.feature
index e99f3bf..c47c1da 100644
--- a/stepup/tests/behat/features/gssp_service_name.feature
+++ b/stepup/tests/behat/features/gssp_service_name.feature
@@ -1,6 +1,7 @@
-# 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:
+# Tagged SKIP until both of these are merged and released in their test images:
+# - Stepup-Gateway PR #624 (enable_service_name_from_saml_authnrequest)
+# - OpenConext-devssp PR adding the mdui_displayname field to sp.php
+# Until then, run locally with:
# ./start-dev-env.sh gateway: demogssp:
# docker compose exec behat ./vendor/bin/behat --config config/behat.yml features/gssp_service_name.feature
@SKIP
diff --git a/stepup/tests/playwright/tests/service-name.spec.ts b/stepup/tests/playwright/tests/service-name.spec.ts
index ceb69ea..581b566 100644
--- a/stepup/tests/playwright/tests/service-name.spec.ts
+++ b/stepup/tests/playwright/tests/service-name.spec.ts
@@ -9,7 +9,7 @@ import { pushServiceName } from '../lib/middleware';
* 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):
+ * Priority rule under test:
* Middleware `service_name`, when configured for the SP, always wins over
* any mdui:DisplayName sent by the SP in the AuthnRequest.
*
@@ -57,7 +57,6 @@ test.describe('Service name during authentication', () => {
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 }) => {
@@ -67,7 +66,6 @@ test.describe('Service name during authentication', () => {
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 }) => {
@@ -76,6 +74,5 @@ test.describe('Service name during authentication', () => {
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 });
});
});
From 983e49caf470f04986515eb798923d0d794fe22c Mon Sep 17 00:00:00 2001
From: Kay Joosten
Date: Mon, 3 Aug 2026 15:06:01 +0200
Subject: [PATCH 4/7] Add SSO-flow coverage and fix service_name shape, per
review feedback
Gateway has three independent LoginService::singleSignOn implementations that
each read the mdui:UIInfo extension behind the same feature flag:
GatewayBundle (plain SSO), SecondFactorOnlyBundle, and SamlStepupProviderBundle
(the latter two already exercised by the existing SFO scenarios). Per
review feedback on this PR, add a third scenario that drives the plain SSO
flow (second-sp -> default-sp) so a regression in that specific copy would
be caught even with the SFO scenarios passing.
Also fixes lib/middleware.ts's pushServiceName(), which was sending
service_name as a bare string. Middleware/Gateway expect a locale => name
map (SamlEntity::fromConfiguration coerces anything that isn't an array to
[]), so the value was silently discarded rather than actually overriding
the AuthnRequest's mdui:DisplayName.
Adds seed-test-identity.sh: seeds a fully vetted Demo GSSP identity directly
via Middleware's command API (the same mechanism FeatureContext's "has a
vetted" step uses), so testing this flow manually no longer requires the
registration UI, RA app, or any second-factor hardware.
---
stepup/seed-test-identity.sh | 70 +++++++++++++++++++
.../bootstrap/SecondFactorAuthContext.php | 32 ++++++++-
.../behat/features/gssp_service_name.feature | 15 ++++
stepup/tests/playwright/lib/middleware.ts | 11 ++-
4 files changed, 124 insertions(+), 4 deletions(-)
create mode 100755 stepup/seed-test-identity.sh
diff --git a/stepup/seed-test-identity.sh b/stepup/seed-test-identity.sh
new file mode 100755
index 0000000..1ddfce9
--- /dev/null
+++ b/stepup/seed-test-identity.sh
@@ -0,0 +1,70 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Seeds a fully vetted Demo GSSP identity directly via Middleware's command API,
+# bypassing registration UI, RA app, e-mail, and any physical/virtual second factor
+# hardware entirely. Mirrors the "has a vetted demo-gssp" step in
+# tests/behat/features/bootstrap/FeatureContext.php (theUserHasAVettedWithIdentifier),
+# adapted from the smoketest DB to this environment's real dev DB/credentials.
+#
+# Usage: ./seed-test-identity.sh [institution] [gssf-id]
+#
+# After running, log in via the ssp test SP (https://ssp.dev.openconext.local/simplesaml/sp.php)
+# as /, request an LoA that Demo GSSP satisfies, and pick "Demo GSSP" as the
+# second factor -- no registration/vetting/hardware step needed.
+
+SLUG="${1:?Usage: $0 [institution] [gssf-id]}"
+INSTITUTION="${2:-dev.openconext.local}"
+GSSF_ID="${3:-seed-$SLUG}"
+NAME_ID="urn:collab:person:${INSTITUTION}:${SLUG}"
+IDENTITY_ID=$(uuidgen | tr 'A-Z' 'a-z')
+SECOND_FACTOR_ID=$(uuidgen | tr 'A-Z' 'a-z')
+
+# Real SRAA identity in this environment's dev DB (has RA authority everywhere).
+# Look it up fresh rather than hardcoding, in case the admin identity_id ever changes.
+ACTOR_ID=$(docker exec stepup-mariadb-1 mysql -uroot -psecret middleware -N -B \
+ -e "SELECT id FROM identity WHERE name_id='urn:collab:person:dev.openconext.local:admin';")
+
+if [ -z "$ACTOR_ID" ]; then
+ echo "Could not find the admin/SRAA identity in the middleware DB -- is the environment bootstrapped?" >&2
+ exit 1
+fi
+
+MW=https://middleware.dev.openconext.local
+DB="docker exec stepup-mariadb-1 mysql -uroot -psecret -N -B middleware"
+
+post() {
+ local user=$1 pass=$2 body=$3
+ curl -sk -u "$user:$pass" -H 'Content-Type: application/json' -H 'Accept: application/json' -X POST "$MW/command" -d "$body"
+ echo
+}
+
+echo "== Creating identity $NAME_ID ($IDENTITY_ID) =="
+post ss sa_secret "$(printf '{"meta":{"actor_id":null,"actor_institution":null},"command":{"name":"Identity:CreateIdentity","uuid":"%s","payload":{"id":"%s","name_id":"%s","institution":"%s","email":"%s@dev.openconext.local","common_name":"%s","preferred_locale":"en_GB"}}}' \
+ "$(uuidgen)" "$IDENTITY_ID" "$NAME_ID" "$INSTITUTION" "$SLUG" "$SLUG")"
+
+echo "== Proving possession of Demo GSSP token (gssf_id=$GSSF_ID) =="
+post ss sa_secret "$(printf '{"meta":{"actor_id":"%s","actor_institution":"%s"},"command":{"name":"Identity:ProveGssfPossession","uuid":"%s","payload":{"identity_id":"%s","second_factor_id":"%s","stepup_provider":"demo_gssp","gssf_id":"%s"}}}' \
+ "$IDENTITY_ID" "$INSTITUTION" "$(uuidgen)" "$IDENTITY_ID" "$SECOND_FACTOR_ID" "$GSSF_ID")"
+
+# Unlike yubikey/sms, GSSF possession (Identity:ProveGssfPossession) is proven-and-verified
+# in a single event (GssfPossessionProvenAndVerifiedEvent) -- no separate e-mail/nonce step.
+REG_CODE=$($DB -e "SELECT registration_code FROM verified_second_factor WHERE identity_id='$IDENTITY_ID' ORDER BY registration_requested_at DESC LIMIT 1;")
+if [ -z "$REG_CODE" ]; then
+ echo "No verified_second_factor row found for $IDENTITY_ID -- VerifyEmail likely failed, see output above." >&2
+ exit 1
+fi
+
+echo "== Vetting (registration code $REG_CODE, authority $ACTOR_ID) =="
+post ra ra_secret "$(printf '{"meta":{"actor_id":"%s","actor_institution":"%s"},"command":{"name":"Identity:VetSecondFactor","uuid":"%s","payload":{"authority_id":"%s","identity_id":"%s","second_factor_id":"%s","registration_code":"%s","second_factor_type":"demo_gssp","second_factor_identifier":"%s","document_number":"123456","identity_verified":true}}}' \
+ "$ACTOR_ID" "$INSTITUTION" "$(uuidgen)" "$ACTOR_ID" "$IDENTITY_ID" "$SECOND_FACTOR_ID" "$REG_CODE" "$GSSF_ID")"
+
+VETTED=$($DB -e "SELECT id FROM vetted_second_factor WHERE identity_id='$IDENTITY_ID';")
+echo
+if [ -n "$VETTED" ]; then
+ echo "Done. $SLUG now has a vetted Demo GSSP token (second_factor_id=$VETTED)."
+ echo "Log in at https://ssp.dev.openconext.local/simplesaml/sp.php as ${SLUG}/${SLUG}, pick a Request LOA Demo GSSP satisfies, and select Demo GSSP as the second factor."
+else
+ echo "Vetting did not produce a vetted_second_factor row -- check the command output above for an error." >&2
+ exit 1
+fi
diff --git a/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php b/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php
index 2372956..3e86410 100644
--- a/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php
+++ b/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php
@@ -120,6 +120,24 @@ public function visitServiceProvider()
}
}
+ /**
+ * @When I visit the service provider with service name :arg1
+ */
+ public function visitServiceProviderWithServiceName(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('mdui_displayname', $serviceName);
+
+ if ($this->activeIdp === self::SFO_IDP) {
+ $this->minkContext->fillField('subject', self::TEST_NAMEID);
+ }
+ $this->minkContext->pressButton('Login');
+ }
+
/**
* @When I start an SFO authentication for :arg1 with GSSP extension subject :arg2 and institution :arg3
*/
@@ -521,8 +539,20 @@ public function authenticateWithIdentityProviderFor($userName)
$this->minkContext->fillField('password', $userName);
$this->minkContext->pressButton('Login');
- $this->minkContext->pressButton('Yes, continue');
+ $this->pressConsentIfShown();
+ }
+ /**
+ * SimpleSAMLphp's consent module remembers a given SP+attribute-set combination for the
+ * browser session, so a consent screen may or may not appear depending on what earlier
+ * scenarios in the same feature already consented to.
+ */
+ private function pressConsentIfShown(): void
+ {
+ try {
+ $this->minkContext->pressButton('Yes, continue');
+ } catch (ElementNotFoundException $e) {
+ }
}
public function authenticateWithIdentityProviderForWithStepup($userName)
diff --git a/stepup/tests/behat/features/gssp_service_name.feature b/stepup/tests/behat/features/gssp_service_name.feature
index c47c1da..b2284ee 100644
--- a/stepup/tests/behat/features/gssp_service_name.feature
+++ b/stepup/tests/behat/features/gssp_service_name.feature
@@ -32,3 +32,18 @@ Feature: The GSSP shows the name of the service the user is authenticating for
Then I should not see "Behat Test Service"
When I verify the "demo-gssp" second factor
Then I am logged on the service provider
+
+ # Gateway has three independent LoginService::singleSignOn implementations that each
+ # read the mdui:UIInfo extension behind the same feature flag: GatewayBundle (plain
+ # SSO, exercised here), SecondFactorOnlyBundle, and SamlStepupProviderBundle (both
+ # exercised by the SFO scenarios above). Without this scenario, a regression in the
+ # SSO copy specifically would go undetected even with the SFO scenarios passing.
+ Scenario: Service name from the AuthnRequest mdui:UIInfo is shown on the GSSP authentication page via the plain SSO flow
+ Given a service provider configured for single-signon
+ And a user "Jane Toppan" identified by "urn:collab:person:institution-a.example.com:jane-a2" from institution "institution-a.example.com"
+ And the user "urn:collab:person:institution-a.example.com:jane-a2" has a vetted "demo-gssp" with identifier "gssp-identifier-sso1"
+ When I visit the service provider with service name "SSO Flow Service Name"
+ And I authenticate as "jane-a2" with the identity provider
+ Then I see service name "SSO Flow Service Name" on the GSSP authentication page
+ When I verify the "demo-gssp" second factor
+ Then I am logged on the service provider
diff --git a/stepup/tests/playwright/lib/middleware.ts b/stepup/tests/playwright/lib/middleware.ts
index 1577c9e..c11ae59 100644
--- a/stepup/tests/playwright/lib/middleware.ts
+++ b/stepup/tests/playwright/lib/middleware.ts
@@ -6,7 +6,11 @@ const MIDDLEWARE_CONFIG_PATH = path.resolve(__dirname, '../../../middleware/midd
const MANAGEMENT_USER = 'management';
const MANAGEMENT_PASSWORD = 'secret';
-type ServiceProvider = { entity_id: string; service_name?: string | null; [key: string]: unknown };
+// service_name is a locale => name map (e.g. { en_GB: "Name" }), per
+// ServiceProviderConfigurationValidator / SamlEntity::fromConfiguration on the
+// Middleware and Gateway side. A bare string is silently coerced to `[]` by
+// `is_array($serviceName) ? $serviceName : []`, so it is never actually applied.
+type ServiceProvider = { entity_id: string; service_name?: Record | null; [key: string]: unknown };
type MiddlewareConfig = { gateway: { service_providers: ServiceProvider[]; [key: string]: unknown }; [key: string]: unknown };
function loadBaseConfig(): MiddlewareConfig {
@@ -17,7 +21,8 @@ function loadBaseConfig(): 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).
+ * baseline unmodified (no service_name key on that entity), or a plain string
+ * to set it for locale "en_GB" (Gateway's default_locale in this environment).
*/
export async function pushServiceName(entityId: string, serviceName: string | null): Promise {
const config = loadBaseConfig();
@@ -28,7 +33,7 @@ export async function pushServiceName(entityId: string, serviceName: string | nu
if (serviceName === null) {
delete sp.service_name;
} else {
- sp.service_name = serviceName;
+ sp.service_name = { en_GB: serviceName };
}
const auth = Buffer.from(`${MANAGEMENT_USER}:${MANAGEMENT_PASSWORD}`).toString('base64');
From bb1f6352bfee53d8b2212eae58f638c7c5208ed5 Mon Sep 17 00:00:00 2001
From: Kay Joosten
Date: Mon, 3 Aug 2026 15:06:01 +0200
Subject: [PATCH 5/7] fix(behat): update stale Yubikey OTP field/button ids
VerifyYubikeyOtpType's field is named yubikeyInput with block prefix
gateway_verify_yubikey (see Stepup-Gateway's Form/Type/VerifyYubikeyOtpType.php),
not the gateway_verify_yubikey_otp_otp / gateway_verify_yubikey_otp_submit ids
these helpers were still using. Every scenario that verifies a Yubikey second
factor or logs into the RA app was failing on
"Form field ... gateway_verify_yubikey_otp_otp not found" as a result.
---
.../behat/features/bootstrap/SecondFactorAuthContext.php | 6 +++---
.../tests/behat/features/bootstrap/SelfServiceContext.php | 4 ++--
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php b/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php
index 3e86410..f574104 100644
--- a/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php
+++ b/stepup/tests/behat/features/bootstrap/SecondFactorAuthContext.php
@@ -392,13 +392,13 @@ public function authenticateUserYubikeyInGateway()
$this->minkContext->assertPageAddress('https://gateway.dev.openconext.local/verify-second-factor/sfo/yubikey');
}
// Give an OTP
- $this->minkContext->fillField('gateway_verify_yubikey_otp_otp', 'ccccccdhgrbtucnfhrhltvfkchlnnrndcbnfnnljjdgf');
+ $this->minkContext->fillField('gateway_verify_yubikey_yubikeyInput', 'ccccccdhgrbtucnfhrhltvfkchlnnrndcbnfnnljjdgf');
// Simulate the enter press the yubikey otp generator
- $form = $this->minkContext->getSession()->getPage()->find('css', '[id="gateway_verify_yubikey_otp_otp"]');
+ $form = $this->minkContext->getSession()->getPage()->find('css', '[id="gateway_verify_yubikey_yubikeyInput"]');
if (!$form) {
throw new ElementNotFoundException('Yubikey OTP Submit form could not be found on the page');
}
- $this->minkContext->pressButton('gateway_verify_yubikey_otp_submit');
+ $this->minkContext->pressButton('gateway_verify_yubikey_submit');
// Pass through the 'return to sp' redirection page.
$this->minkContext->pressButton('Submit');
}
diff --git a/stepup/tests/behat/features/bootstrap/SelfServiceContext.php b/stepup/tests/behat/features/bootstrap/SelfServiceContext.php
index 12cc2d1..c912cf9 100644
--- a/stepup/tests/behat/features/bootstrap/SelfServiceContext.php
+++ b/stepup/tests/behat/features/bootstrap/SelfServiceContext.php
@@ -491,9 +491,9 @@ public function removeRecoveryToken(string $recoveryTokenType)
private function performYubikeyAuthentication()
{
- $this->minkContext->fillField('gateway_verify_yubikey_otp_otp', 'ccccccdhgrbtfddefpkffhkkukbgfcdilhiltrrncmig');
+ $this->minkContext->fillField('gateway_verify_yubikey_yubikeyInput', 'ccccccdhgrbtfddefpkffhkkukbgfcdilhiltrrncmig');
$page = $this->minkContext->getSession()->getPage();
- $form = $page->find('css', 'form[name="gateway_verify_yubikey_otp"]');
+ $form = $page->find('css', 'form[name="gateway_verify_yubikey"]');
$form->submit();
$this->minkContext->pressButton('Submit');
}
From b5f3269486bf3369d92b5fe708eb87c7046fe822 Mon Sep 17 00:00:00 2001
From: Kay Joosten
Date: Mon, 3 Aug 2026 15:06:01 +0200
Subject: [PATCH 6/7] docs: fix stale references in gssp_service_name docs
- The feature flag is append_service_name_to_authnrequest, not
enable_service_name_from_saml_authnrequest (that name never existed in
Stepup-Gateway's config; only the FeatureConfiguration method uses that
wording).
- OpenConext-devssp's mdui_displayname field merged upstream, so it's no
longer a pending dependency for lifting @SKIP -- only Gateway#624 is.
- The documented `./vendor/bin/behat --config config/behat.yml
features/gssp_service_name.feature` invocation doesn't actually work: the
suite's default tag filter excludes @SKIP, so it reports "No
specifications found" unless --tags='~@wip' is passed to override it.
- append_service_name_to_authnrequest defaults to false and devconf has no
parameters override enabling it, so "already the case in the devconf
parameters" was incorrect -- documented the manual step needed instead.
- Documented seed-test-identity.sh as an alternative to the Behat-bootstrap
dependency in the Playwright README.
---
.../behat/features/gssp_service_name.feature | 10 +++++-----
stepup/tests/playwright/README.md | 19 +++++++++++++++++--
2 files changed, 22 insertions(+), 7 deletions(-)
diff --git a/stepup/tests/behat/features/gssp_service_name.feature b/stepup/tests/behat/features/gssp_service_name.feature
index b2284ee..0d6c00a 100644
--- a/stepup/tests/behat/features/gssp_service_name.feature
+++ b/stepup/tests/behat/features/gssp_service_name.feature
@@ -1,9 +1,9 @@
-# Tagged SKIP until both of these are merged and released in their test images:
-# - Stepup-Gateway PR #624 (enable_service_name_from_saml_authnrequest)
-# - OpenConext-devssp PR adding the mdui_displayname field to sp.php
+# Tagged SKIP until Stepup-Gateway PR #624 (append_service_name_to_authnrequest) is merged
+# and released in the test image. (OpenConext-devssp's mdui_displayname field already merged
+# and is in the stock devssp image, so no local sp.php override is needed anymore.)
# Until then, run locally with:
# ./start-dev-env.sh gateway: demogssp:
-# docker compose exec behat ./vendor/bin/behat --config config/behat.yml features/gssp_service_name.feature
+# docker compose exec behat ./vendor/bin/behat --config config/behat.yml --tags='~@wip' features/gssp_service_name.feature
@SKIP
Feature: The GSSP shows the name of the service the user is authenticating for
In order to know which service I am authenticating for
@@ -12,7 +12,7 @@ Feature: The GSSP shows the name of the service the user is authenticating for
# 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)
+ # the Stepup-Gateway (feature flag append_service_name_to_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.
diff --git a/stepup/tests/playwright/README.md b/stepup/tests/playwright/README.md
index d4c05df..f01ea2f 100644
--- a/stepup/tests/playwright/README.md
+++ b/stepup/tests/playwright/README.md
@@ -29,8 +29,23 @@ Stepup-gssp-bundle#49, Stepup-gssp-example#141.
--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).
+ Alternatively, skip the Behat dependency entirely and seed the same
+ `jane-a-ra` identity directly via Middleware's command API:
+
+ ```bash
+ cd ../.. && ./seed-test-identity.sh jane-a-ra institution-a.example.com
+ ```
+
+4. `append_service_name_to_authnrequest` defaults to `false`
+ (`Stepup-Gateway/config/openconext/parameters.yaml.dist`) and devconf does
+ not override it, so it must be enabled manually against the running
+ container, e.g.:
+
+ ```bash
+ docker compose exec gateway sed -i \
+ 's/append_service_name_to_authnrequest: false/append_service_name_to_authnrequest: true/' \
+ config/openconext/parameters.yaml
+ ```
## Install & run
From d86182b29dcbfac6a6d3db71d5d7e9390cae33d2 Mon Sep 17 00:00:00 2001
From: Kay Joosten
Date: Mon, 3 Aug 2026 15:06:01 +0200
Subject: [PATCH 7/7] Remove Playwright suite from this PR
Not meant to be pushed to GitHub -- the Behat coverage (gssp_service_name.feature)
is the test suite of record for this feature. Local node_modules/test-results/
screenshots were already gitignored and never tracked.
---
stepup/tests/playwright/.gitignore | 5 --
stepup/tests/playwright/README.md | 75 ------------------
stepup/tests/playwright/lib/middleware.ts | 59 --------------
stepup/tests/playwright/package-lock.json | 76 ------------------
stepup/tests/playwright/package.json | 10 ---
stepup/tests/playwright/playwright.config.ts | 17 ----
.../playwright/tests/service-name.spec.ts | 78 -------------------
7 files changed, 320 deletions(-)
delete mode 100644 stepup/tests/playwright/.gitignore
delete mode 100644 stepup/tests/playwright/README.md
delete mode 100644 stepup/tests/playwright/lib/middleware.ts
delete mode 100644 stepup/tests/playwright/package-lock.json
delete mode 100644 stepup/tests/playwright/package.json
delete mode 100644 stepup/tests/playwright/playwright.config.ts
delete mode 100644 stepup/tests/playwright/tests/service-name.spec.ts
diff --git a/stepup/tests/playwright/.gitignore b/stepup/tests/playwright/.gitignore
deleted file mode 100644
index da0275b..0000000
--- a/stepup/tests/playwright/.gitignore
+++ /dev/null
@@ -1,5 +0,0 @@
-node_modules/
-test-results/
-playwright-report/
-.playwright-mcp/
-*.png
diff --git a/stepup/tests/playwright/README.md b/stepup/tests/playwright/README.md
deleted file mode 100644
index f01ea2f..0000000
--- a/stepup/tests/playwright/README.md
+++ /dev/null
@@ -1,75 +0,0 @@
-# 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
- ```
-
- Alternatively, skip the Behat dependency entirely and seed the same
- `jane-a-ra` identity directly via Middleware's command API:
-
- ```bash
- cd ../.. && ./seed-test-identity.sh jane-a-ra institution-a.example.com
- ```
-
-4. `append_service_name_to_authnrequest` defaults to `false`
- (`Stepup-Gateway/config/openconext/parameters.yaml.dist`) and devconf does
- not override it, so it must be enabled manually against the running
- container, e.g.:
-
- ```bash
- docker compose exec gateway sed -i \
- 's/append_service_name_to_authnrequest: false/append_service_name_to_authnrequest: true/' \
- config/openconext/parameters.yaml
- ```
-
-## 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.
diff --git a/stepup/tests/playwright/lib/middleware.ts b/stepup/tests/playwright/lib/middleware.ts
deleted file mode 100644
index c11ae59..0000000
--- a/stepup/tests/playwright/lib/middleware.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-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';
-
-// service_name is a locale => name map (e.g. { en_GB: "Name" }), per
-// ServiceProviderConfigurationValidator / SamlEntity::fromConfiguration on the
-// Middleware and Gateway side. A bare string is silently coerced to `[]` by
-// `is_array($serviceName) ? $serviceName : []`, so it is never actually applied.
-type ServiceProvider = { entity_id: string; service_name?: Record | 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), or a plain string
- * to set it for locale "en_GB" (Gateway's default_locale in this environment).
- */
-export async function pushServiceName(entityId: string, serviceName: string | null): Promise {
- 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 = { en_GB: 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)}`);
- }
-}
diff --git a/stepup/tests/playwright/package-lock.json b/stepup/tests/playwright/package-lock.json
deleted file mode 100644
index 6b95315..0000000
--- a/stepup/tests/playwright/package-lock.json
+++ /dev/null
@@ -1,76 +0,0 @@
-{
- "name": "stepup-service-name-e2e",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "stepup-service-name-e2e",
- "devDependencies": {
- "@playwright/test": "^1.48.0"
- }
- },
- "node_modules/@playwright/test": {
- "version": "1.61.1",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
- "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "playwright": "1.61.1"
- },
- "bin": {
- "playwright": "cli.js"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
- "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/playwright": {
- "version": "1.61.1",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
- "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "playwright-core": "1.61.1"
- },
- "bin": {
- "playwright": "cli.js"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "fsevents": "2.3.2"
- }
- },
- "node_modules/playwright-core": {
- "version": "1.61.1",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
- "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "playwright-core": "cli.js"
- },
- "engines": {
- "node": ">=18"
- }
- }
- }
-}
diff --git a/stepup/tests/playwright/package.json b/stepup/tests/playwright/package.json
deleted file mode 100644
index 64a687d..0000000
--- a/stepup/tests/playwright/package.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "stepup-service-name-e2e",
- "private": true,
- "scripts": {
- "test": "playwright test"
- },
- "devDependencies": {
- "@playwright/test": "^1.48.0"
- }
-}
diff --git a/stepup/tests/playwright/playwright.config.ts b/stepup/tests/playwright/playwright.config.ts
deleted file mode 100644
index 0e0f9b8..0000000
--- a/stepup/tests/playwright/playwright.config.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-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',
- },
-});
diff --git a/stepup/tests/playwright/tests/service-name.spec.ts b/stepup/tests/playwright/tests/service-name.spec.ts
deleted file mode 100644
index 581b566..0000000
--- a/stepup/tests/playwright/tests/service-name.spec.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-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:
- * 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 {
- 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();
- });
-
- 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();
- });
-
- 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();
- });
-});