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 88ac22e..f574104 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 */ @@ -137,6 +155,38 @@ public function startASfoAuthenticationWithGsspExtension(string $userIdentifier, $this->minkContext->pressButton('Login'); } + /** + * @When I start an SFO authentication for :arg1 with service name :arg2 + */ + public function startASfoAuthenticationWithServiceName(string $userIdentifier, string $serviceName) + { + $this->minkContext->visit($this->spTestUrl); + $this->minkContext->fillField('idp', $this->activeIdp); + $this->minkContext->fillField('sp', $this->activeSp); + $this->minkContext->fillField('loa', $this->requiredLoa); + $this->minkContext->fillField('subject', $userIdentifier); + $this->minkContext->fillField('mdui_displayname', $serviceName); + $this->minkContext->pressButton('Login'); + } + + /** + * @Then I see service name :arg1 on the GSSP authentication page + */ + public function iSeeServiceNameOnTheGsspAuthenticationPage(string $serviceName) + { + $this->minkContext->assertPageAddress('https://demogssp.dev.openconext.local/authentication'); + $this->minkContext->assertPageContainsText($serviceName); + } + + /** + * @Then I see service name :arg1 on the GSSP registration page + */ + public function iSeeServiceNameOnTheGsspRegistrationPage(string $serviceName) + { + $this->minkContext->assertPageAddress('https://demogssp.dev.openconext.local/registration'); + $this->minkContext->assertPageContainsText($serviceName); + } + /** * @When I start an SFO authentication for :arg1 */ @@ -342,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'); } @@ -489,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/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'); } 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..0d6c00a --- /dev/null +++ b/stepup/tests/behat/features/gssp_service_name.feature @@ -0,0 +1,49 @@ +# 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 --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 + 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 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. + 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 + + # 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