Skip to content

Add GSSP fallback implementation #447

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/test-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,9 @@ jobs:
cd ci/docker && docker compose exec -T gateway bash -c '
composer check
'

- name: Output logs on failure
if: failure()
run: |
cd ci/docker
docker compose logs
20 changes: 20 additions & 0 deletions ci/docker/compose.override.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
services:
haproxy:
networks:
openconextdev:
aliases:
- ra.dev.openconext.local
- ssp.dev.openconext.local
- selfservice.dev.openconext.local
- middleware.dev.openconext.local
- gateway.dev.openconext.local
- demogssp.dev.openconext.local
- webauthn.dev.openconext.local
- tiqr.dev.openconext.local
- azuremfa.dev.openconext.local
- mailcatcher.dev.openconext.local
gateway:
image: ghcr.io/openconext/openconext-basecontainers/php82-apache2-node20-composer2:latest
container_name: gateway
Expand All @@ -14,3 +28,9 @@ services:
mariadb:
volumes:
- ../../devconf/stepup/dbschema/:/docker-entrypoint-initdb.d/
azuremfa:
image: ghcr.io/openconext/stepup-azuremfa/stepup-azuremfa:test
environment:
APP_ENV: smoketest
volumes:
- ../../devconf/stepup/:/config
7 changes: 4 additions & 3 deletions ci/docker/init.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env bash
cp ../../devconf/stepup/gateway/surfnet_yubikey.yaml.dist ../../devconf/stepup/gateway/surfnet_yubikey.yaml
echo "pulling the images"
docker compose pull gateway selenium haproxy ssp mariadb
docker compose pull gateway selenium haproxy ssp mariadb azuremfa chrome
echo "starting the images"
docker compose up gateway selenium haproxy ssp mariadb -d
docker compose up gateway selenium haproxy ssp mariadb azuremfa chrome -d
echo "intialising the environment"
docker compose exec -T gateway bash -c '
cp /var/www/html/devconf/stepup/gateway/surfnet_yubikey.yaml.dist /var/www/html/devconf/stepup/gateway/surfnet_yubikey.yaml && \
Expand All @@ -14,5 +14,6 @@ docker compose exec -T gateway bash -c '
composer frontend-install && \global_view_parameters.yaml.dist
./bin/console assets:install --env=smoketest --verbose && \
./bin/console cache:clear --env=smoketest && \
chown -R www-data:www-data /var/www/html/var/
mkdir /var/www/html/var/cache/smoketest/sessions && \
chown -R www-data /var/www/html/var/
'
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@
"@phpcs",
"@phpmd",
"@test",
"@behat"
"@behat",
"@behat-functional"
],
"phplint": "./ci/qa/phplint",
"validate-lockfile": "./ci/qa/validate",
Expand Down
11 changes: 11 additions & 0 deletions config/openconext/parameters.yaml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,14 @@ parameters:
#
# Example value: 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
sso_encryption_key: 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f

# The GSSP ID from samlstepupproviders.yaml to use as fallback GSSP
# Set fallback_gssp to false to disable the fallback_gssp functionality
# fallback_gssp: false
fallback_gssp: 'azuremfa'

# The user attribute to use in the Subject of the AuthnRequest to the fallback GSSP
fallback_gssp_subject_attribute: 'urn:mace:dir:attribute-def:mail'

# The user attribute to use to determine the user's home institution
fallback_gssp_institution_attribute: 'urn:mace:terena.org:attribute-def:schacHomeOrganization'
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
use Surfnet\StepupGateway\GatewayBundle\Form\Type\VerifySmsChallengeType;
use Surfnet\StepupGateway\GatewayBundle\Form\Type\VerifyYubikeyOtpType;
use Surfnet\StepupGateway\GatewayBundle\Saml\ResponseContext;
use Surfnet\StepupGateway\GatewayBundle\Service\SecondFactor\SecondFactorInterface;
use Surfnet\StepupGateway\GatewayBundle\Service\SecondFactorService;
use Surfnet\StepupGateway\GatewayBundle\Sso2fa\CookieService;
use Surfnet\StepupGateway\SamlStepupProviderBundle\Controller\SamlProxyController;
use Surfnet\StepupGateway\SecondFactorOnlyBundle\Service\Gateway\GsspFallbackService;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
Expand Down Expand Up @@ -144,6 +146,19 @@ public function selectSecondFactorForVerification(
}
}

// Determine if the GSSP fallback flow is allowed so we can continue without a previous registered token
if ($this->getGsspFallbackService()->determineGsspFallbackNeeded(
$identityNameId,
$authenticationMode,
$requiredLoa,
$this->get('gateway.service.whitelist'),
$logger,
$request->getLocale(),
)) {
$secondFactor = $this->getGsspFallbackService()->createSecondFactor();
return $this->selectAndRedirectTo($secondFactor, $context, $authenticationMode);
}

$secondFactorCollection = $this
->getStepupService()
->determineViableSecondFactors(
Expand All @@ -154,7 +169,6 @@ public function selectSecondFactorForVerification(
switch (count($secondFactorCollection)) {
case 0:
$logger->notice('No second factors can give the determined Loa');

return $this->forward(
'Surfnet\StepupGateway\GatewayBundle\Controller\GatewayController::sendLoaCannotBeGiven',
['authenticationMode' => $authenticationMode],
Expand Down Expand Up @@ -338,7 +352,6 @@ public function verifyGssf(Request $request): Response

/** @var SecondFactorService $secondFactorService */
$secondFactorService = $this->get('gateway.service.second_factor_service');
/** @var SecondFactor $secondFactor */
$secondFactor = $secondFactorService->findByUuid($selectedSecondFactor);
if (!$secondFactor) {
throw new RuntimeException(
Expand All @@ -355,8 +368,8 @@ public function verifyGssf(Request $request): Response
return $this->forward(
SamlProxyController::class . '::sendSecondFactorVerificationAuthnRequest',
[
'provider' => $secondFactor->secondFactorType,
'subjectNameId' => $secondFactor->secondFactorIdentifier,
'provider' => $secondFactor->getSecondFactorType(),
'subjectNameId' => $secondFactor->getSecondFactorIdentifier(),
'responseContextServiceId' => $responseContextServiceId,
'relayState' => $context->getRelayState(),
],
Expand All @@ -377,8 +390,13 @@ public function gssfVerified(Request $request): Response

$selectedSecondFactor = $this->getSelectedSecondFactor($context, $logger);

/** @var SecondFactor $secondFactor */
$secondFactor = $this->get('gateway.service.second_factor_service')->findByUuid($selectedSecondFactor);
if (!$this->getGsspFallbackService()->isSecondFactorFallback()) {
/** @var SecondFactor $secondFactor */
$secondFactor = $this->get('gateway.service.second_factor_service')->findByUuid($selectedSecondFactor);
} else {
$secondFactor = $this->getGsspFallbackService()->createSecondFactor();
}

if (!$secondFactor) {
throw new RuntimeException(
sprintf(
Expand Down Expand Up @@ -682,6 +700,11 @@ private function getSecondFactorService(): SecondFactorService
return $this->get('gateway.service.second_factor_service');
}

private function getGsspFallbackService(): GsspFallbackService
{
return $this->get('second_factor_only.gssp_fallback_service');
}

private function getSelectedSecondFactor(ResponseContext $context, LoggerInterface $logger): string
{
$selectedSecondFactor = $context->getSelectedSecondFactor();
Expand All @@ -696,16 +719,16 @@ private function getSelectedSecondFactor(ResponseContext $context, LoggerInterfa
}

private function selectAndRedirectTo(
SecondFactor $secondFactor,
SecondFactorInterface $secondFactor,
ResponseContext $context,
$authenticationMode,
): RedirectResponse {
$context->saveSelectedSecondFactor($secondFactor);

$this->getStepupService()->clearSmsVerificationState($secondFactor->secondFactorId);
$this->getStepupService()->clearSmsVerificationState($secondFactor->getSecondFactorId());

$secondFactorTypeService = $this->get('surfnet_stepup.service.second_factor_type');
$secondFactorType = new SecondFactorType($secondFactor->secondFactorType);
$secondFactorType = new SecondFactorType($secondFactor->getSecondFactorType());

$route = 'gateway_verify_second_factor_';
if ($secondFactorTypeService->isGssf($secondFactorType)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ public function getAllMatchingFor(Loa $highestLoa, $identityNameId, SecondFactor
return $matches;
}

public function hasTokens(string $identityNameId): bool
{
$secondFactors = $this->findAllByIdentityNameId($identityNameId);
return count($secondFactors) > 0;
}

public function findOneBySecondFactorId($secondFactorId)
{
if (!isset($this->secondFactorsById[$secondFactorId])) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ public function getAllMatchingFor(Loa $highestLoa, $identityNameId, SecondFactor
return $enabledSecondFactors;
}

public function hasTokens($identityNameId): bool
{
return $this->secondFactorRepository->hasTokens($identityNameId);
}

public function findOneBySecondFactorId($secondFactorId)
{
$secondFactor = $this->secondFactorRepository->findOneBySecondFactorId($secondFactorId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,17 @@ class InstitutionConfiguration
*/
public $ssoOn2faEnabled;

private function __construct(string $institution, bool $ssoOn2faEnabled)
/**
* * @ORM\Column(type="boolean")
*
* @var bool is the SSO registration bypass feature enabled?
*/
public bool $ssoRegistrationBypass;

private function __construct(string $institution, bool $ssoOn2faEnabled, bool $ssoRegistrationBypass)
{
$this->institution = $institution;
$this->ssoOn2faEnabled = $ssoOn2faEnabled;
$this->ssoRegistrationBypass = $ssoRegistrationBypass;
}
}
28 changes: 27 additions & 1 deletion src/Surfnet/StepupGateway/GatewayBundle/Entity/SecondFactor.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Surfnet\StepupBundle\Value\Loa;
use Surfnet\StepupBundle\Value\SecondFactorType;
use Surfnet\StepupBundle\Value\VettingType;
use Surfnet\StepupGateway\GatewayBundle\Service\SecondFactor\SecondFactorInterface;

/**
* WARNING: Any schema change made to this entity should also be applied to the Middleware SecondFactor entity!
Expand All @@ -37,7 +38,7 @@
* }
* )
*/
class SecondFactor
class SecondFactor implements SecondFactorInterface
{
/**
* @var int
Expand Down Expand Up @@ -144,4 +145,29 @@ private function determineVettingType(bool $identityVetted): VettingType
}
return new VettingType(VettingType::TYPE_SELF_ASSERTED_REGISTRATION);
}

public function getSecondFactorId(): string
{
return $this->secondFactorId;
}

public function getSecondFactorType(): string
{
return $this->secondFactorType;
}

public function getDisplayLocale(): string
{
return $this->displayLocale;
}

public function getSecondFactorIdentifier(): string
{
return $this->secondFactorIdentifier;
}

public function getInstitution(): string
{
return $this->institution;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ interface SecondFactorRepository
*/
public function getAllMatchingFor(Loa $highestLoa, $identityNameId, SecondFactorTypeService $service);

public function hasTokens(string $identityNameId): bool;

/**
* Loads a second factor by its ID. Subsequent calls do not hit the database.
*
Expand Down
Loading