From 7e493c5e9c91830f8ddb7d4ce9517084325d48b6 Mon Sep 17 00:00:00 2001 From: Bas Strooband Date: Tue, 2 Mar 2021 15:06:12 +0100 Subject: [PATCH] Add remote vetting POC --- .travis.yml | 5 + CHANGELOG.md | 3 + bin/extract-translations.sh | 2 +- composer.json | 8 +- composer.lock | 368 ++++++++------ config/legacy/parameters.yaml.dist | 98 ++++ config/legacy/samlstepupproviders.yaml | 142 ++---- config/packages/jms_translation.yaml | 4 +- config/packages/translation.yaml | 2 +- config/routes/dev/remote_vetting.yaml | 4 + config/routes/test/remote_vetting.yaml | 4 + config/services.yaml | 2 +- config/services_dev.yaml | 28 ++ config/services_test.yaml | 29 +- public/images/remote-vetting/idin.png | Bin 0 -> 5035 bytes public/images/remote-vetting/irma.png | Bin 0 -> 14036 bytes public/images/remote-vetting/mock.png | Bin 0 -> 11929 bytes public/images/remote-vetting/on-premise.png | Bin 0 -> 11851 bytes public/images/remote-vetting/readid.png | Bin 0 -> 28551 bytes public/scss/application.scss | 26 +- .../Command/RemoteVetCommand.php | 32 ++ .../Command/RemoteVetValidationCommand.php | 46 ++ .../Registration/GssfController.php | 2 +- .../Controller/Registration/SmsController.php | 2 +- .../Registration/YubikeyController.php | 2 +- .../Controller/RegistrationController.php | 16 +- .../Controller/RemoteVettingController.php | 225 +++++++++ .../Controller/SecondFactorController.php | 1 + .../InvalidRemoteVettingContextException.php | 24 + ...RemoteVettingIdentityProviderException.php | 27 + .../InvalidRemoteVettingMappingException.php | 24 + .../InvalidRemoteVettingResponseException.php | 24 + .../InvalidRemoteVettingStateException.php | 24 + .../Form/Type/RemoteVetAssertionMatchType.php | 109 ++++ .../Form/Type/RemoteVetFeedbackType.php | 75 +++ .../Form/Type/RemoteVetValidationType.php | 74 +++ .../Mock/RemoteVetting/MockConfiguration.php | 106 ++++ .../Mock/RemoteVetting/MockGateway.php | 435 ++++++++++++++++ .../RemoteVetting/MockRemoteVetController.php | 227 +++++++++ .../Resources/config/remote_vetting.yml | 65 +++ .../Resources/config/routing.yml | 20 + .../Resources/config/services.yml | 8 + .../Resources/views/form/fields.html.twig | 15 + .../display_vetting_types.html.twig | 61 +++ .../views/remote_vetting/validation.html.twig | 81 +++ .../views/second_factor/list.html.twig | 8 +- .../Resources/views/translations.twig | 12 + .../Authentication/Provider/SamlProvider.php | 1 + .../Authentication/Token/SamlToken.php | 2 + .../Service/ApplicationHelper.php | 50 ++ ...InstitutionConfigurationOptionsService.php | 2 +- .../Service/RemoteVetting/AttributeMapper.php | 87 ++++ .../RemoteVettingConfiguration.php | 99 ++++ .../RemoteVetting/Dto/AttributeListDto.php | 147 ++++++ .../Dto/RemoteVettingIdenityProviderDto.php | 168 +++++++ .../Dto/RemoteVettingProcessDto.php | 198 ++++++++ .../Dto/RemoteVettingTokenDto.php | 109 ++++ .../RemoteVetting/Encryption/IdentityData.php | 92 ++++ .../Encryption/IdentityEncrypter.php | 117 +++++ .../Encryption/IdentityEncrypterInterface.php | 34 ++ .../Encryption/IdentityFilesystemWriter.php | 51 ++ .../Encryption/IdentityWriterInterface.php | 29 ++ .../RemoteVetting/IdentityProviderFactory.php | 67 +++ .../RemoteVetting/RemoteVettingContext.php | 170 +++++++ .../RemoteVetting/RemoteVettingViewHelper.php | 47 ++ .../RemoteVetting/SamlCalloutHelper.php | 130 +++++ .../RemoteVetting/ServiceProviderFactory.php | 62 +++ .../State/AbstractRemoteVettingState.php | 66 +++ .../State/RemoteVettingState.php | 81 +++ .../State/RemoteVettingStateDone.php | 39 ++ .../State/RemoteVettingStateInitialised.php | 39 ++ .../State/RemoteVettingStateValidated.php | 36 ++ .../State/RemoteVettingStateValidating.php | 45 ++ .../Service/RemoteVetting/Value/Attribute.php | 67 +++ .../Value/AttributeCollection.php | 61 +++ .../Value/AttributeCollectionAggregate.php | 56 +++ .../Value/AttributeCollectionInterface.php | 30 ++ .../RemoteVetting/Value/AttributeMatch.php | 95 ++++ .../Value/AttributeMatchCollection.php | 74 +++ .../Value/FeedbackCollection.php | 66 +++ .../Service/RemoteVetting/Value/ProcessId.php | 65 +++ .../Service/RemoteVettingService.php | 201 ++++++++ .../Service/SecondFactorService.php | 17 + .../MockRemoteVetControllerTest.php | 422 ++++++++++++++++ .../Tests/Resources/encryption.crt | 18 + .../Tests/Resources/encryption.key | 27 + .../Tests/Resources/test.crt | 23 + .../Tests/Resources/test.key | 28 ++ .../Security/Session/SessionStorageTest.php | 8 + .../RemoteVetting/AttributeMapperTest.php | 143 ++++++ .../Dto/AttributeListDtoTest.php | 109 ++++ .../Dto/RemoteVettingProcessDtoTest.php | 67 +++ .../RemoteVetting/Encryption/Decrypter.php | 69 +++ .../Encryption/FakeIdentityEncrypter.php | 47 ++ .../Encryption/FakeIdentityWriter.php | 36 ++ .../IdentityEncrypterIntegrationTest.php | 80 +++ .../Encryption/IdentityEncrypterTest.php | 150 ++++++ .../RemoteVettingContextTest.php | 358 +++++++++++++ ...buteCollectionInterfaceIntegrationTest.php | 78 +++ .../Service/RemoteVettingServiceTestTest.php | 213 ++++++++ symfony.lock | 12 + templates/dev/mock-acs-post.html.twig | 21 + templates/dev/mock-acs.html.twig | 41 ++ translations/messages.en_GB.xliff | 467 ++++++++++++----- translations/messages.nl_NL.xliff | 473 +++++++++++++----- translations/validators.en_GB.xliff | 86 +--- translations/validators.nl_NL.xliff | 86 +--- 107 files changed, 7544 insertions(+), 688 deletions(-) create mode 100644 config/routes/dev/remote_vetting.yaml create mode 100644 config/routes/test/remote_vetting.yaml create mode 100644 config/services_dev.yaml create mode 100644 public/images/remote-vetting/idin.png create mode 100644 public/images/remote-vetting/irma.png create mode 100644 public/images/remote-vetting/mock.png create mode 100644 public/images/remote-vetting/on-premise.png create mode 100644 public/images/remote-vetting/readid.png create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Command/RemoteVetCommand.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Command/RemoteVetValidationCommand.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/RemoteVettingController.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Exception/InvalidRemoteVettingContextException.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Exception/InvalidRemoteVettingIdentityProviderException.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Exception/InvalidRemoteVettingMappingException.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Exception/InvalidRemoteVettingResponseException.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Exception/InvalidRemoteVettingStateException.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetAssertionMatchType.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetValidationType.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Mock/RemoteVetting/MockConfiguration.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Mock/RemoteVetting/MockGateway.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Mock/RemoteVetting/MockRemoteVetController.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/config/remote_vetting.yml create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/ApplicationHelper.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/AttributeMapper.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Configuration/RemoteVettingConfiguration.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Dto/AttributeListDto.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Dto/RemoteVettingIdenityProviderDto.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Dto/RemoteVettingProcessDto.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Dto/RemoteVettingTokenDto.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityData.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityEncrypter.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityEncrypterInterface.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityFilesystemWriter.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityWriterInterface.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/IdentityProviderFactory.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/RemoteVettingContext.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/RemoteVettingViewHelper.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/SamlCalloutHelper.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/ServiceProviderFactory.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/AbstractRemoteVettingState.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/RemoteVettingState.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/RemoteVettingStateDone.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/RemoteVettingStateInitialised.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/RemoteVettingStateValidated.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/RemoteVettingStateValidating.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/Attribute.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeCollection.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeCollectionAggregate.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeCollectionInterface.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeMatch.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeMatchCollection.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/FeedbackCollection.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/ProcessId.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVettingService.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Mock/RemoteVetting/MockRemoteVetControllerTest.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/encryption.crt create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/encryption.key create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/test.crt create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/test.key create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/AttributeMapperTest.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Dto/AttributeListDtoTest.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Dto/RemoteVettingProcessDtoTest.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/Decrypter.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/FakeIdentityEncrypter.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/FakeIdentityWriter.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/IdentityEncrypterIntegrationTest.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/IdentityEncrypterTest.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/RemoteVettingContextTest.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Value/AttributeCollectionInterfaceIntegrationTest.php create mode 100644 src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVettingServiceTestTest.php create mode 100644 templates/dev/mock-acs-post.html.twig create mode 100644 templates/dev/mock-acs.html.twig diff --git a/.travis.yml b/.travis.yml index 81e31e08b..85ae05063 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,6 +32,7 @@ before_script: - curl --compressed -o- -L https://yarnpkg.com/install.sh | bash - export PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH" - composer install --prefer-dist + - composer frontend-install script: - composer test @@ -40,3 +41,7 @@ branches: only: - master - develop + - remote-vetting + - /^feature\/(.*)$/ + - /^hotfix\/(.*)$/ + - /^bugfix\/(.*)$/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 87d5a213d..12411c625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## Remote Vetting (RC) +Remote vetting will allow configured Remote Vetting SAML IdP's to vet tokens. + ## 3.0.3: * Add GSSP UserAttributes extensions to registration-SAMLAuthnRequest to pass user-information to GSSP's * Update dependencies diff --git a/bin/extract-translations.sh b/bin/extract-translations.sh index 755b91c9a..a0e767ab6 100755 --- a/bin/extract-translations.sh +++ b/bin/extract-translations.sh @@ -1,2 +1,2 @@ #!/bin/bash -app/console translation:extract --config=default --env=dev +bin/console translation:extract --config=default --env=dev diff --git a/composer.json b/composer.json index fd9bde864..5ead50816 100644 --- a/composer.json +++ b/composer.json @@ -10,9 +10,11 @@ "src/Kernel.php" ] }, - "minimum-stability": "stable", + "minimum-stability": "dev", + "prefer-stable": true, "require": { "php": "~7.2", + "ext-json": "*", "guzzlehttp/guzzle": "^6", "incenteev/composer-parameter-handler": "~2.0", "jms/translation-bundle": "^1.3.0", @@ -21,7 +23,7 @@ "openconext/monitor-bundle": "^2.0", "sensio/framework-extra-bundle": "^5.0", "surfnet/stepup-bundle": "^4.0", - "surfnet/stepup-middleware-client-bundle": "^4.0", + "surfnet/stepup-middleware-client-bundle": "dev-remote-vetting", "surfnet/stepup-saml-bundle": "^4.2.1", "symfony/console": "4.4.*", "symfony/expression-language": "4.4.*", @@ -48,6 +50,8 @@ "sebastian/exporter": "^3", "sebastian/phpcpd": "^4", "squizlabs/php_codesniffer": "^3.4", + "symfony/browser-kit": "4.4.*", + "symfony/css-selector": "4.4.*", "symfony/phpunit-bridge": "^3.0" }, "scripts": { diff --git a/composer.lock b/composer.lock index 3791cda7a..75fbdba11 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8ed50bb502fb0373a2ad4280da49399b", + "content-hash": "a32635dd4a0c678aa0a7ca84f5d1c20c", "packages": [ { "name": "beberlei/assert", @@ -59,10 +59,6 @@ "assertion", "validation" ], - "support": { - "issues": "https://github.com/beberlei/assert/issues", - "source": "https://github.com/beberlei/assert/tree/v2.9.9" - }, "time": "2019-05-28T15:27:37+00:00" }, { @@ -762,10 +758,6 @@ "bcmath", "math" ], - "support": { - "issues": "https://github.com/ramsey/moontoast-math/issues", - "source": "https://github.com/ramsey/moontoast-math" - }, "abandoned": "brick/math", "time": "2020-01-05T04:49:34+00:00" }, @@ -835,11 +827,6 @@ "php", "utf-8" ], - "support": { - "docs": "http://mpdf.github.io", - "issues": "https://github.com/mpdf/mpdf/issues", - "source": "https://github.com/mpdf/mpdf" - }, "time": "2019-02-06T13:32:19+00:00" }, { @@ -1143,11 +1130,6 @@ "hex2bin", "rfc4648" ], - "support": { - "email": "info@paragonie.com", - "issues": "https://github.com/paragonie/constant_time_encoding/issues", - "source": "https://github.com/paragonie/constant_time_encoding" - }, "time": "2020-12-06T15:14:20+00:00" }, { @@ -1255,11 +1237,6 @@ "math", "polyfill" ], - "support": { - "email": "terrafrost@php.net", - "issues": "https://github.com/phpseclib/bcmath_compat/issues", - "source": "https://github.com/phpseclib/bcmath_compat" - }, "time": "2020-12-22T16:38:51+00:00" }, { @@ -1353,10 +1330,6 @@ "x.509", "x509" ], - "support": { - "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.5" - }, "funding": [ { "url": "https://github.com/terrafrost", @@ -1417,9 +1390,6 @@ "psr", "psr-6" ], - "support": { - "source": "https://github.com/php-fig/cache/tree/master" - }, "time": "2016-08-06T20:24:11+00:00" }, { @@ -1469,10 +1439,6 @@ "container-interop", "psr" ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/master" - }, "time": "2017-02-14T16:28:37+00:00" }, { @@ -1523,9 +1489,6 @@ "request", "response" ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/master" - }, "time": "2016-08-06T14:39:51+00:00" }, { @@ -1616,10 +1579,6 @@ } ], "description": "A polyfill for getallheaders.", - "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" - }, "time": "2019-03-08T08:55:37+00:00" }, { @@ -1882,10 +1841,6 @@ "fpdi", "pdf" ], - "support": { - "issues": "https://github.com/Setasign/FPDI/issues", - "source": "https://github.com/Setasign/FPDI/tree/master" - }, "time": "2017-05-11T14:25:49+00:00" }, { @@ -1943,10 +1898,6 @@ } ], "description": "SAML2 PHP library from SimpleSAMLphp", - "support": { - "issues": "https://github.com/simplesamlphp/saml2/issues", - "source": "https://github.com/simplesamlphp/saml2/tree/master" - }, "time": "2018-11-20T11:11:28+00:00" }, { @@ -2013,20 +1964,21 @@ }, { "name": "surfnet/stepup-middleware-client-bundle", - "version": "4.1.0", + "version": "dev-remote-vetting", "source": { "type": "git", "url": "https://github.com/OpenConext/Stepup-Middleware-clientbundle.git", - "reference": "5bbae33bc70d99549b1458d1af353796042b287a" + "reference": "f60177a32d98534751bad81fcf3c38581896dde1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/OpenConext/Stepup-Middleware-clientbundle/zipball/5bbae33bc70d99549b1458d1af353796042b287a", - "reference": "5bbae33bc70d99549b1458d1af353796042b287a", + "url": "https://api.github.com/repos/OpenConext/Stepup-Middleware-clientbundle/zipball/f60177a32d98534751bad81fcf3c38581896dde1", + "reference": "f60177a32d98534751bad81fcf3c38581896dde1", "shasum": "" }, "require": { "beberlei/assert": "~2.0", + "ext-json": "*", "guzzlehttp/guzzle": "^6.0", "moontoast/math": "~1.1", "php": "^7.0", @@ -2063,11 +2015,7 @@ "Apache-2.0" ], "description": "Symfony2 bundle for consuming the Step-up Middleware API.", - "support": { - "issues": "https://github.com/OpenConext/Stepup-Middleware-clientbundle/issues", - "source": "https://github.com/OpenConext/Stepup-Middleware-clientbundle/tree/4.1.0" - }, - "time": "2020-07-29T08:44:23+00:00" + "time": "2021-02-18T09:36:30+00:00" }, { "name": "surfnet/stepup-saml-bundle", @@ -2269,9 +2217,6 @@ "caching", "psr6" ], - "support": { - "source": "https://github.com/symfony/cache/tree/v4.4.19" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -2424,9 +2369,6 @@ ], "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/config/tree/v4.4.19" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -2582,9 +2524,6 @@ ], "description": "Provides tools to ease debugging PHP code", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/debug/tree/v4.4.19" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -2667,9 +2606,6 @@ ], "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v4.4.19" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -2736,9 +2672,6 @@ ], "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/error-handler/tree/v4.4.19" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -2819,9 +2752,6 @@ ], "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.19" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3023,9 +2953,6 @@ ], "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/filesystem/tree/v4.4.19" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3084,9 +3011,6 @@ ], "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/finder/tree/v4.4.19" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3393,9 +3317,6 @@ ], "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v4.4.19" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3539,9 +3460,6 @@ ], "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/http-foundation/tree/v4.4.19" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3643,9 +3561,6 @@ ], "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/http-kernel/tree/v4.4.19" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3874,9 +3789,6 @@ "mime", "mime-type" ], - "support": { - "source": "https://github.com/symfony/mime/tree/v4.4.19" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4179,9 +4091,6 @@ "polyfill", "portable" ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4353,9 +4262,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.22.1" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4437,9 +4343,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.1" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4517,9 +4420,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4593,9 +4493,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.22.1" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4672,9 +4569,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.22.1" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4755,9 +4649,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.22.1" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4922,9 +4813,6 @@ "uri", "url" ], - "support": { - "source": "https://github.com/symfony/routing/tree/v4.4.19" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -6000,9 +5888,6 @@ ], "description": "Provides tools to validate values", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/validator/tree/v4.4.19" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -6089,9 +5974,6 @@ "debug", "dump" ], - "support": { - "source": "https://github.com/symfony/var-dumper/tree/v4.4.19" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -6161,9 +6043,6 @@ "instantiate", "serialize" ], - "support": { - "source": "https://github.com/symfony/var-exporter/tree/v4.4.19" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -6453,10 +6332,6 @@ "i18n", "text" ], - "support": { - "issues": "https://github.com/twigphp/Twig-extensions/issues", - "source": "https://github.com/twigphp/Twig-extensions/tree/master" - }, "abandoned": true, "time": "2018-12-05T18:34:18+00:00" }, @@ -6728,8 +6603,8 @@ "validate" ], "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.9.1" + "issues": "https://github.com/webmozart/assert/issues", + "source": "https://github.com/webmozart/assert/tree/master" }, "time": "2020-07-08T17:02:28+00:00" } @@ -7803,10 +7678,6 @@ "keywords": [ "template" ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" - }, "time": "2015-06-21T13:50:34+00:00" }, { @@ -8851,10 +8722,6 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/master" - }, "time": "2016-10-03T07:35:21+00:00" }, { @@ -8913,6 +8780,206 @@ }, "time": "2020-10-23T02:01:07+00:00" }, + { + "name": "symfony/browser-kit", + "version": "v4.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/browser-kit.git", + "reference": "f6f060bdc473c3f3b1f00e2ebdeb3d02eda77f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/f6f060bdc473c3f3b1f00e2ebdeb3d02eda77f82", + "reference": "f6f060bdc473c3f3b1f00e2ebdeb3d02eda77f82", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/dom-crawler": "^3.4|^4.0|^5.0" + }, + "require-dev": { + "symfony/css-selector": "^3.4|^4.0|^5.0", + "symfony/http-client": "^4.3|^5.0", + "symfony/mime": "^4.3|^5.0", + "symfony/process": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\BrowserKit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", + "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-27T09:09:26+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v4.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "f907d3e53ecb2a5fad8609eb2f30525287a734c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/f907d3e53ecb2a5fad8609eb2f30525287a734c8", + "reference": "f907d3e53ecb2a5fad8609eb2f30525287a734c8", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-27T09:09:26+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v4.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "21032c566558255e551d23f4a516434c9e3a9a78" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/21032c566558255e551d23f4a516434c9e3a9a78", + "reference": "21032c566558255e551d23f4a516434c9e3a9a78", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "masterminds/html5": "<2.6" + }, + "require-dev": { + "masterminds/html5": "^2.6", + "symfony/css-selector": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/css-selector": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases DOM navigation for HTML and XML documents", + "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-27T09:09:26+00:00" + }, { "name": "symfony/phpunit-bridge", "version": "v3.4.47", @@ -9030,10 +9097,6 @@ ], "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.", "homepage": "https://github.com/theseer/fDOMDocument", - "support": { - "issues": "https://github.com/theseer/fDOMDocument/issues", - "source": "https://github.com/theseer/fDOMDocument/tree/master" - }, "time": "2017-06-30T11:53:12+00:00" }, { @@ -9088,16 +9151,19 @@ } ], "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, + "minimum-stability": "dev", + "stability-flags": { + "surfnet/stepup-middleware-client-bundle": 20 + }, + "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "~7.2" + "php": "~7.2", + "ext-json": "*" }, "platform-dev": [], "platform-overrides": { "php": "7.2" }, - "plugin-api-version": "2.0.0" + "plugin-api-version": "1.1.0" } diff --git a/config/legacy/parameters.yaml.dist b/config/legacy/parameters.yaml.dist index 50a7b3995..a8b1f7661 100644 --- a/config/legacy/parameters.yaml.dist +++ b/config/legacy/parameters.yaml.dist @@ -59,3 +59,101 @@ parameters: session_max_absolute_lifetime: 3600 # 1 hours * 60 minutes * 60 seconds session_max_relative_lifetime: 600 # 10 minutes * 60 seconds + + # remote vetting + + remote_vetting_entity_id: https://selfservice.stepup.example.com/rv/metadata + + # For each remote vetting IdP we require some parameters. + remote_vetting_idps: + # The display name + - name: "IRMA" + # The logo that is displayed in the UI, should be a transparent PNG, preferably monochrome coloured + logo: "/images/remote-vetting/irma.png" + # This discription is presented to the user choosing a remote vetting IdP, make sure this description amply + # describes the service. + description: + nl_NL: Met IRMA kunt u op een privacy-vriendelijke, beveiligde manier eigenschappen (attributen) van uzelf onthullen. + en_GB: IRMA is a unique privacy-friendly identity platform for both authentication and signing. + # The slug is used in the URL to keep track of which remote vetting IdP was chosen by the user. Should not contain + # special characters, alphanumeric (lower case) characters and dashes are allowed. + slug: irma + # The entity id of the remote vetting IdP + entityId: https://selfservice.stepup.example.com/mock/metadata + # The SSO url of the remote vetting IdP + ssoUrl: https://selfservice.stepup.example.com/second-factor/mock/sso + # Certificates for the remote vetting IdP + certificateFile: "%saml_rv_publickey%" + # The attribute mapping should map the institute IdP attributes with the ones received from the remote vetting + # IdP. + attributeMapping: + givenName: firstName + surname: lastName + - name: "ReadId" + logo: "/images/remote-vetting/readid.png" + description: + nl_NL: ReadID® verifieert identiteitsbewijzen uitgerust met NFC op een smartphone. + en_GB: ReadID® verifies identity documents with NFC on a smartphone + slug: readid + entityId: https://selfservice.stepup.example.com/mock/metadata + ssoUrl: https://selfservice.stepup.example.com/second-factor/mock/sso + certificateFile: "%saml_rv_publickey%" + attributeMapping: + givenName: firstName + surname: lastName + - name: "iDIN" + logo: "/images/remote-vetting/idin.png" + description: + nl_NL: iDIN is een dienst van de banken waarmee consumenten zich bij andere organisaties met de veilige en vertrouwde inlogmiddelen van hun eigen bank kunnen identificeren. + en_GB: iDIN is a service offered by banks which allows consumers to use their bank’s secure and reliable login methods to carry out the following actions on the websites of other organisations. + slug: idin + entityId: https://selfservice.stepup.example.com/mock/metadata + ssoUrl: https://selfservice.stepup.example.com/second-factor/mock/sso + certificateFile: '%saml_rv_publickey%' + attributeMapping: + givenName: firstName + surname: lastName + - slug: mock + name: 'Mock IDP' + logo: /images/remote-vetting/mock.png + description: + nl_NL: 'This is an integration test IdP.' + en_GB: 'This is an integration test IdP.' + entityId: 'https://selfservice.stepup.example.com/mock/metadata' + ssoUrl: 'https://selfservice.stepup.example.com/second-factor/mock/sso' + certificateFile: '%saml_rv_publickey%' + attributeMapping: + givenName: firstName + surname: lastName + + identity_encryption_configuration: + # The public key used to encrypt the remote vetting user data. The private key matching this is used to decrypt, + # but is not configured in the application. The application can not be used to decrypt the data. + encryption_public_key: | + -----BEGIN CERTIFICATE----- + MIIC6jCCAdICCQC9cRx5wiwWOjANBgkqhkiG9w0BAQsFADA3MRwwGgYDVQQDDBNT + ZWxmU2VydmljZSBTQU1MIFNQMRcwFQYDVQQKDA5EZXZlbG9wbWVudCBWTTAeFw0x + ODA3MzAxMjMwNDdaFw0yMzA3MjkxMjMwNDdaMDcxHDAaBgNVBAMME1NlbGZTZXJ2 + aWNlIFNBTUwgU1AxFzAVBgNVBAoMDkRldmVsb3BtZW50IFZNMIIBIjANBgkqhkiG + 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqhbI0Xy682DuvWchg6FYnI+DNwLXef2XExM4 + YVRBaMMsOZ3rBtQUTMSqYan6SK/BOEXLs0rNiJjyM0dn+F98wg3fv5zIADlvfk3L + BVdcGsrpVfFUWtSa73yMgbROy8/RJADbUJE/HUB3ZmdjdiuD2Cui2aoWwT2HR8uk + Jwmoxiu45IWFPbqPQ7/1mH644JPOWTPLTv4OGGLQo8MNrP1oRCiZ0IEL4CQeGOOj + u5rfIJ0bTVm0UmelT4hGaqZovBMwXp3QV41akJ7UEMEBK2YMnLQy47Xuzi7aTDhJ + lvHcJ8mfH2NbjRh7hJoACVRTvQloxajgkr1iGMiWiiqT0e+YYwIDAQABMA0GCSqG + SIb3DQEBCwUAA4IBAQBwZ0gRHvR8B8KivrXrhWNL9uLvWhEAH7OiDqo+fywkBp5K + EuDJcbbvEPftHunSAGylg7M2xKuBIGamFpp74WDJccrtZ1jJ4qqnacUDRQrTLqqM + ZKqGpFOU0xjKkSxSGRuMtGN9/7er/TeonjQ0XBvjYvTomy3b5aCLVWRvEfKu2g1s + Dd8uhr62RY/HfMgidEt7LHDolkCVg+6JzY3OTcgeHga3cvYObOYPplxw1YPq5+Bq + qxaUW4nfb5DtK33bZBYMeyV6BZtSggc5Z/19aPx/s0bf6ySTUyB3lRqe5d3etCns + 4bGidORCl/6EZiXwVcPvmYmxYXqmuNWfps7isUvo + -----END CERTIFICATE----- + + # The location on disk where the encrypted remote vetting user data is stored + storage_location: '%kernel.project_dir%/var/rv' + + + # Saml Remote Vetting SP public key + saml_rv_publickey: '%kernel.project_dir%/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/test.crt' + # Saml Remote Vetting Mock IdP private key (used for development and testing, this value should be omitted in production) + saml_rv_privatekey: '%kernel.project_dir%/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/test.key' \ No newline at end of file diff --git a/config/legacy/samlstepupproviders.yaml b/config/legacy/samlstepupproviders.yaml index 1b9bcfa57..e1efe352d 100644 --- a/config/legacy/samlstepupproviders.yaml +++ b/config/legacy/samlstepupproviders.yaml @@ -7,56 +7,6 @@ surfnet_stepup_self_service_saml_stepup_provider: metadata: ss_registration_gssf_saml_metadata providers: - tiqr: - hosted: - service_provider: - public_key: "%gssp_tiqr_sp_publickey%" - private_key: "%gssp_tiqr_sp_privatekey%" - metadata: - public_key: "%gssp_tiqr_metadata_publickey%" - private_key: "%gssp_tiqr_metadata_privatekey%" - remote: - entity_id: "%gssp_tiqr_remote_entity_id%" - sso_url: "%gssp_tiqr_remote_sso_url%" - certificate: "%gssp_tiqr_remote_certificate%" - view_config: - loa: "%gssp_tiqr_loa%" - logo: "%gssp_tiqr_logo%" - alt: "%gssp_tiqr_alt%" - title: "%gssp_tiqr_title%" - description: "%gssp_tiqr_description%" - button_use: "%gssp_tiqr_button_use%" - initiate_title: "%gssp_tiqr_initiate_title%" - initiate_button: "%gssp_tiqr_initiate_button%" - explanation: "%gssp_tiqr_explanation%" - authn_failed: "%gssp_tiqr_authn_failed%" - pop_failed: "%gssp_tiqr_pop_failed%" - app_android_url: "%gssp_tiqr_app_android_url%" - app_ios_url: "%gssp_tiqr_app_ios_url%" - biometric: - hosted: - service_provider: - public_key: "%gssp_biometric_sp_publickey%" - private_key: "%gssp_biometric_sp_privatekey%" - metadata: - public_key: "%gssp_biometric_metadata_publickey%" - private_key: "%gssp_biometric_metadata_privatekey%" - remote: - entity_id: "%gssp_biometric_remote_entity_id%" - sso_url: "%gssp_biometric_remote_sso_url%" - certificate: "%gssp_biometric_remote_certificate%" - view_config: - loa: "%gssp_biometric_loa%" - logo: "%gssp_biometric_logo%" - alt: "%gssp_biometric_alt%" - title: "%gssp_biometric_title%" - description: "%gssp_biometric_description%" - button_use: "%gssp_biometric_button_use%" - initiate_title: "%gssp_biometric_initiate_title%" - initiate_button: "%gssp_biometric_initiate_button%" - explanation: "%gssp_biometric_explanation%" - authn_failed: "%gssp_biometric_authn_failed%" - pop_failed: "%gssp_biometric_pop_failed%" webauthn: hosted: service_provider: @@ -83,32 +33,58 @@ surfnet_stepup_self_service_saml_stepup_provider: pop_failed: "%gssp_webauthn_pop_failed%" app_android_url: "%gssp_webauthn_app_android_url%" app_ios_url: "%gssp_webauthn_app_ios_url%" - azuremfa: + demo_gssp_2: hosted: service_provider: - public_key: "%gssp_azuremfa_sp_publickey%" - private_key: "%gssp_azuremfa_sp_privatekey%" + public_key: "%gssp_demo_gssp_2_sp_publickey%" + private_key: "%gssp_demo_gssp_2_sp_privatekey%" metadata: - public_key: "%gssp_azuremfa_metadata_publickey%" - private_key: "%gssp_azuremfa_metadata_privatekey%" + public_key: "%gssp_demo_gssp_2_metadata_publickey%" + private_key: "%gssp_demo_gssp_2_metadata_privatekey%" remote: - entity_id: "%gssp_azuremfa_remote_entity_id%" - sso_url: "%gssp_azuremfa_remote_sso_url%" - certificate: "%gssp_azuremfa_remote_certificate%" + entity_id: "%gssp_demo_gssp_2_remote_entity_id%" + sso_url: "%gssp_demo_gssp_2_remote_sso_url%" + certificate: "%gssp_demo_gssp_2_remote_certificate%" + view_config: + loa: 3 + logo: "%gssp_demo_gssp_2_logo%" + alt: "%gssp_demo_gssp_2_alt%" + title: "%gssp_demo_gssp_2_title%" + description: "%gssp_demo_gssp_2_description%" + button_use: "%gssp_demo_gssp_2_button_use%" + initiate_title: "%gssp_demo_gssp_2_initiate_title%" + initiate_button: "%gssp_demo_gssp_2_initiate_button%" + explanation: "%gssp_demo_gssp_2_explanation%" + authn_failed: "%gssp_demo_gssp_2_authn_failed%" + pop_failed: "%gssp_demo_gssp_2_pop_failed%" + app_android_url: "%gssp_demo_gssp_2_app_android_url%" + app_ios_url: "%gssp_demo_gssp_2_app_ios_url%" + tiqr: + hosted: + service_provider: + public_key: "%gssp_tiqr_sp_publickey%" + private_key: "%gssp_tiqr_sp_privatekey%" + metadata: + public_key: "%gssp_tiqr_metadata_publickey%" + private_key: "%gssp_tiqr_metadata_privatekey%" + remote: + entity_id: "%gssp_tiqr_remote_entity_id%" + sso_url: "%gssp_tiqr_remote_sso_url%" + certificate: "%gssp_tiqr_remote_certificate%" view_config: loa: 2 - logo: "%gssp_azuremfa_logo%" - alt: "%gssp_azuremfa_alt%" - title: "%gssp_azuremfa_title%" - description: "%gssp_azuremfa_description%" - button_use: "%gssp_azuremfa_button_use%" - initiate_title: "%gssp_azuremfa_initiate_title%" - initiate_button: "%gssp_azuremfa_initiate_button%" - explanation: "%gssp_azuremfa_explanation%" - authn_failed: "%gssp_azuremfa_authn_failed%" - pop_failed: "%gssp_azuremfa_pop_failed%" - app_android_url: "%gssp_azuremfa_app_android_url%" - app_ios_url: "%gssp_azuremfa_app_ios_url%" + logo: "%gssp_tiqr_logo%" + alt: "%gssp_tiqr_alt%" + title: "%gssp_tiqr_title%" + description: "%gssp_tiqr_description%" + button_use: "%gssp_tiqr_button_use%" + initiate_title: "%gssp_tiqr_initiate_title%" + initiate_button: "%gssp_tiqr_initiate_button%" + explanation: "%gssp_tiqr_explanation%" + authn_failed: "%gssp_tiqr_authn_failed%" + pop_failed: "%gssp_tiqr_pop_failed%" + app_android_url: "%gssp_tiqr_app_android_url%" + app_ios_url: "%gssp_tiqr_app_ios_url%" demo_gssp: hosted: service_provider: @@ -135,29 +111,3 @@ surfnet_stepup_self_service_saml_stepup_provider: pop_failed: "%gssp_demo_gssp_pop_failed%" app_android_url: "%gssp_demo_gssp_app_android_url%" app_ios_url: "%gssp_demo_gssp_app_ios_url%" - demo_gssp_2: - hosted: - service_provider: - public_key: "%gssp_demo_gssp_2_sp_publickey%" - private_key: "%gssp_demo_gssp_2_sp_privatekey%" - metadata: - public_key: "%gssp_demo_gssp_2_metadata_publickey%" - private_key: "%gssp_demo_gssp_2_metadata_privatekey%" - remote: - entity_id: "%gssp_demo_gssp_2_remote_entity_id%" - sso_url: "%gssp_demo_gssp_2_remote_sso_url%" - certificate: "%gssp_demo_gssp_2_remote_certificate%" - view_config: - loa: 3 - logo: "%gssp_demo_gssp_2_logo%" - alt: "%gssp_demo_gssp_2_alt%" - title: "%gssp_demo_gssp_2_title%" - description: "%gssp_demo_gssp_2_description%" - button_use: "%gssp_demo_gssp_2_button_use%" - initiate_title: "%gssp_demo_gssp_2_initiate_title%" - initiate_button: "%gssp_demo_gssp_2_initiate_button%" - explanation: "%gssp_demo_gssp_2_explanation%" - authn_failed: "%gssp_demo_gssp_2_authn_failed%" - pop_failed: "%gssp_demo_gssp_2_pop_failed%" - app_android_url: "%gssp_demo_gssp_2_app_android_url%" - app_ios_url: "%gssp_demo_gssp_2_app_ios_url%" diff --git a/config/packages/jms_translation.yaml b/config/packages/jms_translation.yaml index 2007ca4d7..ba9ac2df1 100644 --- a/config/packages/jms_translation.yaml +++ b/config/packages/jms_translation.yaml @@ -2,8 +2,8 @@ jms_translation: locales: "%locales%" configs: default: - dirs: ["%kernel.root_dir%/../src", "%kernel.root_dir%", "%kernel.root_dir%/../vendor/surfnet"] - output_dir: "%kernel.root_dir%/translations" + dirs: ["%kernel.project_dir%/src", "%kernel.project_dir%/templates", "%kernel.project_dir%/vendor/surfnet"] + output_dir: "%kernel.project_dir%/translations" ignored_domains: [] excluded_names: ['*TestCase.php', '*Test.php'] excluded_dirs: [cache, data, logs, Tests] diff --git a/config/packages/translation.yaml b/config/packages/translation.yaml index 36f2c93bb..7139193b9 100644 --- a/config/packages/translation.yaml +++ b/config/packages/translation.yaml @@ -1,6 +1,6 @@ framework: default_locale: "%default_locale%" translator: - default_path: '%kernel.project_dir%/translations' + default_path: "%kernel.project_dir%/translations" fallbacks: - "%default_locale%" diff --git a/config/routes/dev/remote_vetting.yaml b/config/routes/dev/remote_vetting.yaml new file mode 100644 index 000000000..7cb009db1 --- /dev/null +++ b/config/routes/dev/remote_vetting.yaml @@ -0,0 +1,4 @@ +ss_second_factor_remote_vet_mock: + path: /second-factor/mock/sso + methods: [GET,POST] + defaults: { _controller: Surfnet\StepupSelfService\SelfServiceBundle\Mock\RemoteVetting\MockRemoteVetController::ssoAction } diff --git a/config/routes/test/remote_vetting.yaml b/config/routes/test/remote_vetting.yaml new file mode 100644 index 000000000..7cb009db1 --- /dev/null +++ b/config/routes/test/remote_vetting.yaml @@ -0,0 +1,4 @@ +ss_second_factor_remote_vet_mock: + path: /second-factor/mock/sso + methods: [GET,POST] + defaults: { _controller: Surfnet\StepupSelfService\SelfServiceBundle\Mock\RemoteVetting\MockRemoteVetController::ssoAction } diff --git a/config/services.yaml b/config/services.yaml index b36a0ab77..84c870b70 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,5 +1,5 @@ imports: - - { resource: 'legacy/samlstepupproviders_parameters.yaml' } +# - { resource: 'legacy/samlstepupproviders_parameters.yaml' } - { resource: 'legacy/global_view_parameters.yaml' } - { resource: 'legacy/parameters.yaml' } - { resource: 'legacy/samlstepupproviders.yaml' } diff --git a/config/services_dev.yaml b/config/services_dev.yaml new file mode 100644 index 000000000..409535619 --- /dev/null +++ b/config/services_dev.yaml @@ -0,0 +1,28 @@ +# Use this service definition file to override services and parameters in the dev environment. +# For example to mock certain services, or override a password for test. +services: + # A remote vetting mock IdP is used in order to mock the attributes released from a remote RV (Remote Vetting) IdP. + # This IdP is utilized as a drop in replacement to test the attribute matching between a remote IdP to vet with and the local + # OpenConext IdP. + Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\Configuration\RemoteVettingConfiguration: + arguments: + $privateKey: '%saml_rv_privatekey%' + $configurationSettings: '%identity_encryption_configuration%' + $remoteVettingIdpConfig: '%remote_vetting_idps%' + + Surfnet\StepupSelfService\SelfServiceBundle\Mock\RemoteVetting\MockConfiguration: + arguments: + $identityProviderEntityId: 'https://selfservice.stepup.example.com/mock/idp/metadata' + $serviceProviderEntityId: 'https://selfservice.stepup.example.com/rv/metadata' + $privateKeyPath: '%saml_rv_privatekey%' + $publicCertPath: '%saml_rv_publickey%' + + Surfnet\StepupSelfService\SelfServiceBundle\Mock\RemoteVetting\MockRemoteVetController: + public: true + arguments: + - '@Surfnet\StepupSelfService\SelfServiceBundle\Mock\RemoteVetting\MockGateway' + - '@twig' + + Surfnet\StepupSelfService\SelfServiceBundle\Mock\RemoteVetting\MockGateway: + arguments: + - '@Surfnet\StepupSelfService\SelfServiceBundle\Mock\RemoteVetting\MockConfiguration' \ No newline at end of file diff --git a/config/services_test.yaml b/config/services_test.yaml index 0315850ec..e95bc7d05 100644 --- a/config/services_test.yaml +++ b/config/services_test.yaml @@ -28,4 +28,31 @@ services: class: GuzzleHttp\Client factory: ['Surfnet\StepupSelfService\SelfServiceBundle\Tests\TestDouble\Factory\GuzzleApiFactory', createCommandGuzzleClient] arguments: - - "%middleware_url_command_api%" \ No newline at end of file + - "%middleware_url_command_api%" + + + # A remote vetting mock IdP is used in order to mock the attributes released from a remote RV (Remote Vetting) IdP. + # This IdP is utilized as a drop in replacement to test the attribute matching between a remote IdP to vet with and the local + # OpenConext IdP. + Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\Configuration\RemoteVettingConfiguration: + arguments: + $privateKey: '%saml_rv_privatekey%' + $configurationSettings: '%identity_encryption_configuration%' + $remoteVettingIdpConfig: '%remote_vetting_idps%' + + Surfnet\StepupSelfService\SelfServiceBundle\Mock\RemoteVetting\MockConfiguration: + arguments: + $identityProviderEntityId: 'https://selfservice.stepup.example.com/mock/idp/metadata' + $serviceProviderEntityId: 'https://selfservice.stepup.example.com/rv/metadata' + $privateKeyPath: '%saml_rv_privatekey%' + $publicCertPath: '%saml_rv_publickey%' + + Surfnet\StepupSelfService\SelfServiceBundle\Mock\RemoteVetting\MockRemoteVetController: + public: true + arguments: + - '@Surfnet\StepupSelfService\SelfServiceBundle\Mock\RemoteVetting\MockGateway' + - '@twig' + + Surfnet\StepupSelfService\SelfServiceBundle\Mock\RemoteVetting\MockGateway: + arguments: + - '@Surfnet\StepupSelfService\SelfServiceBundle\Mock\RemoteVetting\MockConfiguration' \ No newline at end of file diff --git a/public/images/remote-vetting/idin.png b/public/images/remote-vetting/idin.png new file mode 100644 index 0000000000000000000000000000000000000000..01da5993cd06f7dd3ba7f730ea070fc96db0bee0 GIT binary patch literal 5035 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 RL;QmVP>@lTu9Y+k`(MlKsQUl_ literal 0 HcmV?d00001 diff --git a/public/images/remote-vetting/mock.png b/public/images/remote-vetting/mock.png new file mode 100644 index 0000000000000000000000000000000000000000..c04d2a1393744abd8521071578c885bee22596c1 GIT binary patch literal 11929 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 literal 0 HcmV?d00001 diff --git a/public/images/remote-vetting/on-premise.png b/public/images/remote-vetting/on-premise.png new file mode 100644 index 0000000000000000000000000000000000000000..61cfe237adba6c54bc4acdec5597609b2c564465 GIT binary patch literal 11851 zcmV-RF0|2!P)0Eb>&XjTWLG)jR`y(j!0z*sHZ3?2eym@nQV4FOQcle#^pLXzm}eL z1u+fOID(;JcN|(AJWRodxG`Oyk6XCk(zC806$3SnU})GKhZYA9Q}7{fOxNe*7Vfw7 ztSd;xK#hY^4H;xm>fU}1WMD!P1w=Gao(jbmqJBKzFL>&mJJiJu#>i@opNT71h;RrH zMLYm26knqH@qEAFsdw&B7dIGVCzn69A*1Rwg>4%LSVP9QL=|FNoR`noh&mvvhwUfW%W#A!+$l1?pp_~Q^;tWQ7Fc0w5TMKWyFb@J6DnmnY#%3-I%V=NkV&aW4w;# zPc>we9Hg*q<45Q``Hx!>WH6O|#W33YsvPnVtrTNf~l z&p6z82E&A>tHv)OF&HEY!pLN;2c1%&{r zAO>t4n=4>mGI0nBZpsmd7RsF$Pr^g-Lx8mW;DBL^aR>*^JJpbe6{({XIN;ilMsSUT z^C&*aaM(l?3^&Aw`hKAo|8`@zJ{v0p4*@K$a>XDRuyHWD_=JjENhLq2iVjP{D9Re5mgidck+9A%omV-P_IqS3~;Q$tTi@ z!+k{0pW7SP0BvslVYv?R$mXYGDJoZpfQOJc+(-2MxxM-G{Th`&)sUWyrQYr3K%|EB z8f>hMkT2_oj&a1rh(U;H`AMn7P>qXj;Nf^-h(XTbFtLmy4mk$l4J?1EA>(8ug>MT7 zw1$jnpv% zTvEt3azJUwn5J*R))kpoKiK%ip<>0Yj0l!Hixo&oBg8J17HDe)C`-oF+slB>mvx2W z0^kQ5zc?frm4Lr8CRkqbHYttpCX_$bkha=WR|#++a+JO0`SMkS?~ln)JX%ah3^<1T z)`9(e`Tm%k#G}Q8#DHVSZyh+0uT(>B9oy5C5^%uPkVaCC!zB>{hX9Q4Lw&yxje|>g z{5xUHglHxmgKr!rh#ett2oQ-DjTJUM-NbVF;wXA?5~7({2H!YL5IaKP5FipQ8Y{Z| z6>f^Jrw=E6*Vfi(Wo5Uy3xpBBg@py0o}T94%a<={c6OE) z78hAS5nf(aMwOLSR9ja|yLRoO=H_NGAX9Y;aIL+wNZ?EIR-vzpP3Vdw8$xXo*pR##S75T8GPPWSHIqx<*o(fz)D8Xg{| zxw$!7T2flkblL0c935S9rc8C&l@%4NEvu`lsI{$?4jec@hYug7ciwrI4jno~d-v`! zUAavguI(?s%7xB{W>YLb-OR`4-b2gb-F*3XyoPkU zpZfayXl!hZyJy&vNgpg}H8wTUqeqYE>C>ky)DZNy-hxo>rS|rA)1|}qOrOnjAhW)n z&Ds{E2wMGPT~1Oy*!Y#htjIW#f*ung2rSqQD%tgQg0>qR9HgsPuhJiX{6W{RU#BNe zo^W?PGdsif;?mL*t*@`MJy}RXupjH{>bR?hZQ0q`Ne2%cpbtJcPM`e8f6$SmN2s}} ziE65ANG{ae%JQQuae7INN81CrC68O+vbAGfPD~$c{K~PJ^6yv;3E^5@TcejRUeM6s zAli4o%NKO?$WiL< z>ZIDbI;yPPu`Jynhl(4mLbYnQjai>{ImudMPJumtD3)nKno6k2`Unayd^3a11-jma zxjB0H-~oO2-FI~5$`$JC@28=mA;Kv7RXxzOFbftJm+1N6AlqVU_RZ|NCCSBGNx$>b>ij%&`W z8CO?ne0-c9^!L-{%a`e=3m53-ty?rcF+ub5^A7Kd?SXlKAJTzf&d<+LZEZEJuC6eA z|Ni~ly;oIL6+7uSf?h>%FPdLsU$AYsZUY7()Fv1;HrPWqHiwCzQi{qOgaM=FzP>&> zfBrmOx_FVEJbg-&lamDPwN(!WQ|s$%tdalqukUDdbd0qo^7qzTZxIC26vr#CuR;qc zKgwt6x`dG@JpLrU!S|RG#or)GTs+Z)fs6#B8Mle=X46C$#9;Y%jE0oaG;FyC4<69( zmoL-Bix=s}jT^MMxX9W`3b<8o>())0nw;Dg%^1a#x%qjT zfAWNCYHR4w!Gly=TT7=-oub<68lG?u3riH06vhG>l62q5iD^Q%1!LeQkd!t|h3wI=z_)Dq&lk6Cfd~eGe-7&glXh%$XU;E=44Gj;| z{K7&>bk#mnW@cuo?|vWKw*CDNXn16VyKx^c^+$7n1rjYrwHoUJI$DY;B{n-tg)Vq z`NyrC-tj9hpMO2OPAjV`Y*IiVZ{E7eJB+ZziCsnMlbZvMKuU{{wju2WCW$diK+gmi z)#|3A31Ov0R$GDys=Xo@-7ZEw(= z#x`a>m#qBKccFa3w=n;ObMn^Q$n6cW%%UroRKr%*zax;L9Bv~3m4{Usba(geVH?@~ zlJ2T~RxK>UTKcO#3Kpg>dU)Y|raZi164^n->ucj~@*; z!O8&*AmI>5XqD*~FL;p9-`CF*&>ErSU!4DyRhoS{OW3mZd}xUFS5#6}Rkh`Bj`E{` zh8xCVB6#I@;c@6LmPf+o%5gDPzK9;F&D4L%YDfs<+}u1(O=6GZ2-{@KB{t3D)r0HB z(TVkSnxC8Ft#N}xLwsbyMM`}>2Z}0?Xqot-L8(TiAObf57QxGp4Om`Urpbv3nwS{p zX&($0#0K*sm9!UAR)fPsG&DR!i;D}EIf_AE-fj62U~D%R?8OOtu?xuT1VXT`08E;Aw|n5dwB&TN~Ab=k-eVJw!iQkt~xvgD(VhNTB{pRzqSN7k1I$2m;(Y^})y* zGX*bSyyVl6JGC5Jm2Y+G&=c6aiJM7DCE?EZcgkzo;(+~CusF29cwNQUh(@EuM|9H@-z%(tXWB>7D}^`aAkoFGEHCqX zWGUonBY9X`TW8yGd08DY+X#*AjDrRREsssnFriQ{IGbKyUq^Ly^+i9QUcs%$G?ipU zS8EJLv5A5+k%RMz1{idBElh$Ox>=(2wRI(oVk-&aj(V$|<=9fD<1+8Ex_kF7-MDd+j~V0o85}Ww@BR0Ao)woUKx5*# za50_==(+fY=Z7eN-W|vxovE^Fsp%GL0AaeQL>vRCtgcFUuTC-8QG6O)j;bm?V*@uz zYWSC<{G1b4u?031NPrNKe0y-K3KfDzx9vjV3l)hCWZ7kf4bk6x^9^0SdW}X$N4fjH zed`uIeE5*oSJx=Jo~46_4)R?{k_+2+S_oV=9i5T^C2*i@e*N!3%YNF;{gu3Yu-GQAv$>QU{N$B z3uIVF8qxI_6>LK`NNgj!O<%=#Y(hJjHciDg;BbqMWk631a%TU)8Et&LA1N`;o%8?ylr#^I4+ z7Dm>LmoD=CNI}gg`NpVwZhnrvEzK<~l;{octEo}c*lplB@{#Zq(OCJJg zxO#=+JE1{OhJ{gS#yfQWn{No3@!69n+)W2HV=xwsLTJh#e)xg62I3ARlyP_WZfa<# zj32}0D1QlS$V%?!THD&Fv8jpg%~@VrGB%9Em=gBj6k_-8ZvNrCcBwsLGvd0!+qdt~ z&%gZ4!ibUe>hcP$=UJ3sO*uD5yLL6u{+fjmM@1lveC#4Jg)k7^J#G(~nwliudk_#J zlZyoPA1RRG@ zr%uy5@4Uk^BWk3rN@~ih9mXG#K*?%I$ty0M$Nh`LLqqh(^*`zP^FiMI6KiNo#BF|a zzKuHnhN^RM7)w!Kv`KcE-lmW@DO2wTiJ{k`P>qA zRVC%CXhz)jhN}oa`sgD%dh`f&baZTL-k8_2Wt1FhaJfV|ClgdeOV7H3(G1i$f}zTd z3OMxETYS#BtGk;99zCMjnHg^3VeHa@V7ltm>C^mcZjre;m!YQE#$odh!NE=^tcd)- z@4u&q1CLm>9z1l2{@4Hf8y!7*lxk~gsjO11{vf8+jFwK&h16vy{;iug>Fcl0)1}{k z=Sx^2jD>2(o}L~$bLI?v@x>P`l;{!ANR&JUXSX8?X!NNTGzS;o6L4I*renWjj;_QiL!~kJnV1O=PzCu4 zUsp@3E313~e@km?KnUWKfq-LU#PHBCchf(^X1w$}-MfF6FY#Jm7omx#fe>iMy1H8G z>DkZ1`0>XdvoK=A1#9d?pHGQTgV(ZUW<6gEvt%UmZi~0!%4I{v<;F8@$QzXl9blui zp`cw|UG(XvpYk-si>WETXiBIFekr^mjIgKBjU73Bm^wQ;^n^HgAURh2h$_F)i~|Go z?YH0ZHPyJ76#|L}pF3GuT%xg&QTp=BuXsRFQ(H^;a)i*g^T;+m2qR`GaB;!c=f9!f zG0*t?84F_}no$X35B2s!Gk!^@PM_j=N2?iaEl90ftJ&rUSW)FKc@2r#)v`>6=Oo`h z_8xcF0}mha4Ypt~t}Q8rN3BrQuz`QJx0ia)oT0sY_Oi{FB!5B8`0(KX-+lS(ufNit zfBs1`FJGGTI2ep9%rEdI7#P9hBk+d~9^xwiql7|f#$~=(6}Jrk^0S&}ynpW=t*)&S zf5|LC29;*4BlJ}0ra$>aX~siu9a5SRyRwS#ut3@tY^duI?^+|GoEV?0<~$Jms&y{YLZ8o(0m~J}2@A8w%a@e}4L(bhh^_ zwY0PZ2b+|u{16;m6aDbv1N!#c|3eT)z8v8tjy|lIg29dTlg@wrHEYJf=g(Of3)74c#sk#b+e=@3@i|{L*xlVtt8x|9+XfY1|BqvpDE#gAdA!-{|NFEiJ1ux3_n&P+)Td*3HYx%Xzenm94?SA-Z|< zCXc4kO=F%Bt1n4~5t^~Su8#H}P{JrSBR(L-!U%KAwt!$(H{@o4jM>1fzHu?8>&uyvqMRX~3v)^!ewXvj*wx z>Qig{GcVU+ayz#;nohpz(POf>yoy9p}CPa@_?;z(1hrwF%Q>!rk9Q!IYL#~L=iJk zi%SdLHI4~jnkUHx{@9(fwzk9?@!>v>`yJrXn{gb|t%!id3Uge=AtFELyRq@k?atZBNwic@qAx=BXjsSv*|EWULi zs_Qev%&m2HepJy9vH<%FPaZ!>&5g~}+0n_%UO)W%2O1c7#NAa=#@kd2b3qv2efM29 z@c;VPPuaGGro<6*;A)t$&Jbl7u11`JzzyJN; z+%hOG##h!qk{dSjmP)X9_FMPmSi<+Cyv_#3pTKo@g)5FVH! z7)?XiAV|3$gyV-D-pk8Ngd2P@3P-uIg=}EpAwioaZJtBHcrdu}#()Pm?EZ26I#2#$ zY6BIUv@(KpeK&-$tFwa+A3j2-&zzz6Klp&&K6;e)?cK*yF0L@fl+S>T!z6T2bjEY{uPivpx+AV~> zth|h;ldw8eU0uZ=)WoDS@7if=qkVh#^4c^6^2pJnw7a{THKrZ4NUjQhV?QC*g7HN# zXgh5Ex_j>)jgE{cO&K^o5Jg=yCCZ5T*+L)Dj2Nk({ZMSilRU?1%?N2og;GzBuWqgo zQJ3^$MuVa%1>Gd$;R~K2rx9&K3Q;hmT!%}+^;shnaM(7`l0Ex+Xjj87dhghKJjlVV zW7yw^u5e^%h_weQOg`cc4GL|7DGYS~=sH_koB7kj-CbR@d-opN*V99L_Uz$#MhJOx zOACJ_3HbaJq5Q}{1pnZ{L-ggBUs8Kp8(#svytv5b$C@|&T?Qi2_?A*CSr_pPgdWtW&ia?HxvIfDG%d`AJ z_a)Xw5C|iXEiEjN=&C>m8*H($1NJF|CR8DkiA}sQXIW8E#%U-)&(iq#7z+YM$oQ@z zx@9z;Vm^*$Jo_Pi^6|%X^5iMr`7N4J6hSf5jicoXq~Wx7=$!aAogpSUjX|&tN7|Pp zU?M6~vEF3)Fn1wJK5)qZ1gy43eN9VGUo0*xvat{2AC-&G8;gy`YvyID%dX@uyP5|| znA5ARt7TzDS1u*sjGKHrP{W>$l;3E|LxA5xlT5N^JamXh z=I73x#O-(+D#X>q@Nbnya1g=mD0l;0Id$>nSCE-TAWrc9lJ!{jbMMt~9)uu%Mj zWOJdhAe5C6&ZS^`83Y2Kt-}LNIXYGxO^L<@VT8?y)um59{)DG?Fx4ZPQTNlivRm}9 zNP#rzy_g9vDGKGJ@1SP<{M_fP89O>V9GlT< z#;q%Vq(CMuREqcNH~=k$T~65Vh><>?@v(7!-@6Y@Ikq92($|bwfx!Ugz`+A*^~DTe zUR~L8QPu+Kwg9jFn&GUAm5ZzjGjxnY$I*nabqY`HJfm%*I-(ojHRxn)Z?*--+deKe z8$lEFvn!Ohqx_;NF%hq9%63}+Cd=REez^fn3C)NJ_zyokOXtpgPA5*BBxpun3e^1G z&hnR}hQvfR<_MP-muPXx&K>gV#p)`pt*-IM(}9N{7|1IrH8ni-gIz%w4db#ZRA@0C zT)HoJJ>ts>SVy0mol_I55pe`ECBM*sZhKe@qT z>sr3icYQ-WAE&?o=GfyOESbSx!fgupjf@QQCez93X~x0XY*c`(v|!qY1#-6%NX$F-^z7q-05)M@ z!W?sZ&=NR`T2WcSniD&Pcg(#Msi~1#AdsDE1qoM-;KX2iYa7ouHZ`lfA&%P46{>0j!0EzF zWf`?WQ`XdQ!-b|qmG?nYj*S_9m`Fri=K6*Ptr^v58r?KT)25qt{4BNWmP2$CMp|R8A1p_hjyNU08UTyB>D&%#t0h52(K5* z^&xm;84`CSLQSB8rHid7N0(i}_gi8e9C^bu4m2gUsNvkriQ~t49lp7#nZu2Sk{a2t9_+0(`zd_t>a|g=1 zZ5k32)AI}SyfX*u&CoWu&g0gt+jRTpE&k9DYeBgvzCdn4K+(lvO&d#pon4(2C6Evt z2oeMvc@lwyMuetD#X}%*HX7YEgmt42(t#N9lC z;889aeTz-m*Vkt>CCa}Gnvn-EAMt+1lP6BFNdx-1(uhlYE8wn%G?Ho@ZXQD55PjT3vpn-ORU{<5cuB_yN z1}Y0-s9dQW(YCTl0oqao6kYb>(h>`+N~%Wf000gKNkl=1{hgfQZQ1!%_qI`;M!~mSLqgD?c^*H0%%AVQeEBk;gd2D`VC+K4@Mhi+ zFtG*Y)`!i|D^^@|srca>5V~l5=v_?g8%mLWK zlM|C{u1ri$Qe(p|I{v{2)O)sf$J^Oo1<11EKUIm@ZdRETDg$GN8Cm1F3prYWg zRxwDyZT>jpfm4I`@86?|@d>UJw0N-z3r~Az#=vwC>{@K%c>C?6boT6r^!LC2osJ%T zJ9P196nDj@#79ce$e*)0Fv>l{kt2tB<8XU>JKNU2aQpOpf8iXBqYwx8Q^;=J9pl(i zk89uV-nq+1JTG3nNY}4l=YfKstvAV!xz^D|+}#74b&7il49L(pvCa&eQ%s8}z8IUb zy^RNJSaHFQd5qe-ySupSE?|WqDv2$Hsr`^T$y`=TvRuVc$Z!{R8jW1QVNz%gYr_Fi}H(2eKU zmhU&j6FcH4D7c%t={N)TWoBNzhJ?L@5i3s9;f%*`7u8K$7`3L4<-qFd8m+F*)6n1$ zPryUK_UzrmAAHB9PUzNiJu-+>13A5MSR^1<#1acDi<*T*IaFD!%gMJ7Hh$$WULqDo zOh;U~qO>D!-AXm1!%fG7xmFB#?)UZ4cmMj1&uOa(aBx*tQ=oCwE^ZCgG!$HW%*)n? z8;-hv)%9;Op|fsvB{o_rU@%v&Y?O^NA(+HQ*Yn4ge#1BYaK>m$#z(ex08({%pNhLMyS><)C1fj@iQpDHp%O$A3(@U=zZV$B*g4 zj~Dm@JQz)mhtw2xI$RD^)YnIF$m$7gvsx#*RIjkKd#ZlNSj zb8RF3$*C!xv%PfjB5yK*kYer;tmo;Rz%rw(Hu>q8YVrbPV~Gbg-ne+i_%jIM`UjEN zU=JM|2ze7>mZ&(F`k5lA#eaO`dVFG>FUQ0U|5zqWg)zXS0-V^{gpDQ-9`w_bCr|k# z;?lBvBU%CEN*+~p*$`zskleuIdCk~c5XB6TQ@gMWuoVkSVL0H9nRd zc{qTE410Wf`UUm(_tDwD!DIAqz#vrHptn7Rvber}!~KYB!u1|IP!TTf_u^ipr39Ka49Oq=01J3ABT ztw2fFi&T@b-V|KG^7hj%X8m(kB%!#8PH4t}?HkNb(KRIYIAU8FMu+qBrE&jvu)sUj zt)_vVF<+rA@fC(bKK-^+%FP04^`3R107lSd6eOjx0b-^|2w&>-{P{C_HmI(ZGXbe% zQyjn&ERIWzjgG1Pq6>?B=*;PXO!XNDrzt*p`x&ut>jI9gW<3g)h&CWp|89cOnlM-N zqHD;-#RawQh!W}fDBcn7rJi^CV13{^?2pqsX(yVRpZUsTp;PD5tE|x|L zyll92DMA6H1Wr3Y?a`5piF}!0<$&gN1TsNdu_(N|htDxD^EG!#zYrBmFb#<00E7^| z5axT=ihNgOWP)1^ESf+<0I}U{Wv#HUif(avcYJc_b(qiidTWe8njJu*BDH3+n(NiD zK@^}-eA<3g)?!#=hjW21?iA?}8*;sX8~=-x^czJ6e1?`R^aDscg-Plw429-ant~S( z%)(##Yyk&kJs5AJlC(oKqhv$C;@5AfDHGibkM&AK55Z4XG8U#U1aye@LW37gLmD}h zw7krw-Xd}!D2zoUTMDGqsY4N>uqa38(d-si5jeVzo0LQ};S`#W1K_p%Li-_gx8n-SfuZNXD&@G+~si}rH ziAr+myI34}oq1f5Tb@PVNeff{xE;u^Goy+nOvR#Oc`bDW%5FWGlYJi8c8DrKCea^v$mp6FbXx&<}1AZ zk%Co3Dt@f3sma^?>5LWg)FA{CcV**pBz#Z0v9Xc;o8WyKHG%_GRh87-+(ftr8W*s> zZjbJ=A)&VUK(a-;QDFaQgzrbozz-K4;Y+MNJ^N|jz8-Z8*XzcyZ7dIXj0^8~cXd&B zS0{hX!CGeFjJdTM4z~~&bEEZd=t1>m)kwoil?hd%rjB3*q)v%o(Kc6zP~yu2@4ovk zUrdVg*x0*=Ee>hg@Qv;>y}k6|M<4O`Zt$gB^hm*;P3WTeixnp>4)eP&8!WT?uXJ+}pJ2vvBPjU_@&DhMJ z5&6qs{z9itpQg5scAm|M62|0|OB1vFAOHBrKcq($EvQ;o;DWPALm;X^uqD-K8`s7) zH8m5yhk|Y!hm!C~IJ?U^Dt-zJasa(cXJ-eU?d_$%e)=hW`srWky<^At`#TjCl~(zg z`cXBf?(6|2S5;+E!Lav(iRdu`W9xv;)W1s8P~enIF0{k=y?nr1bPb7aq`bU_?VK}b z%K00ANT{QugFnMIJ3Ggk5`xLu)~x}Vlww5W^R)+|{CF_W2p(d_0^0Gw{{8%EkJG15 z(c5po&0hk8puVn8Cd;3pQUw%&C`NH{9}TwIJNSH%KSYG@6(Wpp@!~rU zb-Rj77&pCgZK=1Q;VpTV3X(AKN4^a`iLJCoTu%WkrYlX*m zM1&0Sgf~9(fvGcm^$PmWnE!(DLDtWp{vXOxNciZpdTlSyzyd zff@&R@rncz2_qehu-DhuXm)0XkH_M}Nce1<`Y_Tu&nbbC_%VJk$DW(>t)~l2kO-Fu z4F^coNJ$O0pUut_SRhVjN=#Hh!#@ENtb;FO3 zx#xdWidG*>4!pP_a0rkmsY3CEoFC8kZ-GEcR@B?ifbY+Z?S@AA6uq5XA;D7R=1ve( z+&jPg{H??Ig8K!Og6nr|L>*ukD>;I1qj}T<>dtG?f*49K1Q5)E-tkmah!t>;Y-~gw zkd|LDOjl=;`j4C#_t>6#Qx0qy2h`VKV_SN3KJuaa;nr_mpzQjh(#nk?Cmij}n!=CO zXi-Td%ZL**cdo+aviR1OELj)d$prIHg}+YaPc@`p%c*}`IpBUTTXdFbn9}*{JjR%I zMkjbS(?(0?+Ztg_s41I#x+z((bpgZpjKhs*Fidz_e&&$jhtDtZYlI@yf2tvcr|G*S zIH10lowo%@UKL`@-$`OYa%9R&S!b9;x0EKD5#vv3o z_RdQt4ne_7vHYoq+?Xh&v0f1eTpQ9fDC6K{#V2_Po05XzhWJq5FZANyZVcCFV};-$ zfW=j=7z6`04n`NBP;pBMs9?AuKGgRMz4*5q!}Zx%A$SO2ag{3u!GMi}(ZwfJ+)@H6 z7;cCU_5DIG_)axskQ=Fc+d1HBNIyIIL^^S}kLdYxd*d3Q&8#L2`?Cbn(cwrzXDiEZ1qZCexD6HM$$-u+(H`}w-+*16~W=-%DE zvAXx#5sLB>2(UP?0000%N>WrA004si0s+tvpohK_p3{GC#xfG3fUmH)=|0dKjH9HM z3jhFz`ri)>keQ7I`UvGJB_{@T1jL3SLiwF3Zz04Y%+RnLvf9-C-V8^4psDUKbE zqQX1nl@&c-u9Y^I92N_Ep2Mlw{`x6zL))p}wYSXLn!ye5SQ99BP%i<^DEcr~$Oot> zD5;taKT@-h>-#4?ruuP$Q%Ceo;%@PN%w`|v^80eRH8LM2CL|!=lK(h}86U5%zOL+c z)lln;V;kFEahn?V%zF{|n!1OA`+r-MOlW8i1SW2hiNoP=Zv#eIXnk7$;2=ssNJvNu z0D}RyPxGBY_GQe+ZRGFE1;A%XUmFe=OcT_A^`M5b=d-T?_OcN2#SSJ+knfdq^`|J# zQ!;E0|0fT{%C3LhU!L33f8>C#>YkfA)?&~|-|V!@H*Uia3AQa7U!T{f%#C37oC&*; z*&a<#e{KSxnw0`$UHqM8l2}fNWJ}Q;lVB9g`HhW?$ojno8NhBFXgyIQk#{+u;1OZvn|3dVQP3g zs&;s10KcJohg^s5KQJqlNxPgG>ajgl+H^QqUzXB#VzDyS4o`g`_2!jV7TxkGe;}1> zB1L#q!r?3D0 zwJ)?Z)#7y@9kz-@ZCe*#M5;i1BnsmE%Tu@?J0dCeYFH)lBQHFh&sdzdaj<_-Iv_2w z531jQ3*+Itm!SB4Y?BCu8!E?6%r$x41#ac`*5am=#g=?4DuasS0#JxQ3S( zto5&3-9sI``w-3HwkgN%lobj#rUBK!?20)?aD z2{HOnV)X=gwLQq0ez@&gu zd+59_gy77OMMFebKtg~5vusX+%amn>CO7ZWm+!Ou%d*VVD-a zI{XQ4(GSQA2&Wm2eu4N5XV~BvlmYR1DPS>!8_l2fn{%=;?j^OO=PehThZGIm|a6B^9bAX2$|q)NWEA{oPp89 zilU;!e;SfzKYL$Ua*@7R00Kt4svym#jk~sSu50u6tn%RDmb5qHc+l_7>1glN>*m10 z0h%E`pv_KuIGubRDY|-L7RR%yAajr?xQbGbQX`X~vyq^83b8nfFx&_;xcZ{sK@y3G zM|(+of0X@aFPb)Mc{>sI@W%dxRaCuFYX2`9z|xvQTB*^Gg)`v^YmdNkc{$t8+nYyparTM;-Ms2)e7 zJM~^$wB_wjr3 z_2%fVY`f~nD22SuPB;0nah~w3971kOW>uilj6s7ZREolH_djM8=q$qtGdTJl-$RNR zi${lZHvY{a14Oam`Cq?!49-ayMDASf#y+)BOoORa{}k7u{fNbpyEgq*8XA#4AFo{k zlI{MDC=OM!23eMmvg3RjUbB$xfcmmVU^AytpVd#VdcGG8YzboC#>qpU% zbalEzKKQw$)K6>_|7%%UfmLio&^o27sAfE>+X%y~0C9=%`LUm`GI$^!|KLMt(8MpR z&wkgC28nK;+AXCl=Ujhfvmr(_M_lyG|b_tm*GwO!`+o*9o#C@cUHh@1{NGDa9AuR zD?&PxghSoQMY94E_7O+I!s7ch>WmT5?TjYYabUbOxs!4ovd) zd~LP2kYaU~04*)Ea>2uj;LBuMAgtM$_>AR6fF-joExbJJ=M@3C3;+}3obIco%;VqB z1sg5Q(~+kSJ48hJnY4UcGMp+|QMvtuD9|R+k2;@)Qa=ouuOapbLJ_CGApFRS-z+u1 z8HWY+kp}#N4)|N=d&IKE5frqi@5#X_SzEHnigCKZpbziJ8@X_F7MiMkaFTp3oHCif zBOAJ-C}EWzR+0Cz(~r*?G+F-udrNw9+e~rGL36vz`1L-@Qv33}i93hV?i3TXLJs zm=hcQw!fOOz8=C+C}I2TM|2-jZ`$^(`#oLuR7aee2CE`$kn{LB7;DC~X!Xrmc($sU zGc78IvJi`dUB;_EN0UOOd6g<2TdbGn7!_Ui4UmW>8ls>`ZW7M%kIjtzGR?R z474DN`>0NWvCh#EF=how%{|V<#YmKBmce>&@cS}(qH7pI>yvQPF>&=!+e^@Gg&xxa;LMnymwz$tWO)zIcb)?04-LHM+OoC<7E^t4eF`R0*u(v&V4 zvpWgL9WjG#h?d~EAbfUV3grOAHk9=9u4P-5r0Q zqw&p`wF74(L_L*m0nf(8-{)E=4L2F+z>9SISuFnSr}_EAOmQ{Sg5pfkbtl=;4}0k2Aw)S5JmI)AX~l76U8nh6aT%hO z#kiNwC`lETfJP;4A%&ARNQk(de_qv##~D`7xi(ZG@#fyLDTuSN;Z5ihi_ zaRzB$4!p+YNxZ#9vlNyob# zxv9jnGn+Y>`;~@-y389zV|s)r%n`$w&Z!-{-Olwu?68KYQ-J8!-+z%+QWl5|$rQ)X z1hV*g$dB)U57QWcW!Y7Gj%}G7PAh~bL2)3t!&xZ1PAo*eq>eX*oNM7Bt^9w{8Jz`k zR`3Ph^bCR-C82#E{@C%(v4N43>qUSa*BE9GeSog@+;qD*$JQn!2gs=rOlbaDEY^xX z;iO_4RV%4RjG`6&18VVE7^LPP=!(t?yhdgsrlOm+13?&+%w{)^t7x4=I=ZWU0Rc3^ z@-B2tW}zhRhM+16*?Hr_=IJd^Ol~QnrT(4k?W@*|@=f#-ndKm9iU_n1)nEc#X<+$D z*QQsdG*KR~Kvcc*d zvTBX!xJF3k1&Gam5tfm`4`rIi;?otBKNgX(*4F#+*i z=9B%G%${S?0U9SKw~6jsQX3n3&n6m4*o973`)Uq3kjl)=%FN1y9C;O|%KA??dAY!0 z;Cf172|YC=bo%%-ewP$TZ2e?WZsK{r6Aq~B{Dm=)=RVjVmGAZNqk&8X{Eh-|J#^oS z(6LGi)wFr}4vdhAqW^ihpCPO+FD@)AD=R#mo*uk@R`k;CWd#yji&7{{Y(y+L-pOXb;`jmP2IeLLD@heb7{L_%+caE|H_G&ZmLoj! zcqCf3G8W}Nmlhd9D9EV0=Mj4P;d&H9o%;g<#%cN34#!Rh9RKToe<7B~2xQ#JO&BEe z2kSLT(7d%y{TH@5jz9H}skwZ;PxPZ=D90riOf6XmS(+k;2>!8{LSoDjkSojZRSXXe z<9FTup2ux1qMa1R|3ipt`?xJ+qFBSZnK3+ZNXvJWNfr-;>XN6d(E%GQ8VXT*KuDA&Wm7Wd*QMd>cFsJGJ5oIGKvF@$tIa(j zep`P%-u?5&iMDl850Q8i)+?QwajXCYr$6DGLkC~pj zJVUp20t%h38I1V~eJGHJ(s+hv`Fc&hk5`kOQj$I-7Hi;Q{7s4Qn zJ}D_S!=t}UuoF%(QLV7AYUBg=A^`T%NfvM)c+vlt#k`AUeZu`tePbGl!Rws4-er?Pv<*vPPCz-ouDY;rf*f0dNk3VMVcKY=R)FDe0a zWW((U#BBuur*Dmvhc6J_=GKsLb`XxU`;11zXS3xs9*Op?ITNmQ;(LQfS7Zb^Y+ti> z8(W16Kl(PBa#R}$b9hdk_83IKj1w4cwm;s#t?e;cdw@> zUuhEZ!3w%kLE&qn5N{AEG}W}4)I~{itTvc*+bC?Q5wcsuF~^b!$Mw~i<${p%lp^SM zAv#3JP7N21pYxxXvX<(1{o()yJbNd~|2#kp&pO8JAAZ0RZ%=+lAhaR-uk+05`}t+~ zw-mAQGOZ33+*dnHEZ$%o{du8Z=Ek$g3H1*@#z6h#5}?goEowvD53;b)?SEDr*2 z8?t^I`wr%7{;WJ+-W1w1o0#v~zn+ZkOyrl!h0Kfm{o~7NP~#PRgZCsa8|G7iL(US$ zP*1og#jVMH3+HD=VZ(;7qf*u(4P-!$%MD7~I%K4%Q5?T7kXTt+2UU{>`^>~i&3@-g zbrhfOAk=Y1Z6VF~G2dLVMFZ#kF?5m62` zJP#8sVlZBU_lU}uaT^vCI>3Aj869Db>POfXFzPB)ORw`^1HLbZorg7Wyv;hdfXM)8 z_B~#SlKvNJDnfk#B4aPAuj}e`j%gXkm>?v5ZXJ_{+=dDeQ1}W}EV#TszIEf2Ou?1aa-*9SS9r~ZME9-hL2jdvc-m4jvF z-I0ID%`xtO{$Slern`b=`0Gfvb?)Z2>V_OAuqBc)Ho6J|wjT&e);!i@AhokUwrN)= z`+4M1HLYu*hv7g;#4CYAvc z!~`R?VoM6JDj#X6812v$1Z+sPS($4b{=EKoE9W-Tg~F2xU=Kg1#{{b%aaY$2?1Hwi zqZ2&Fx_Rp8Bj{>|3D>fR?mY9P9F~XsZtW7WK7BI8t*2?F`14p8E*bJ6)|Dh82_9Sa zb62Ze*fnOuYj*Dj3L>>de1Kj zo{QltPac~_cAA?)h&8_nx&E>_aMp}#hIxy2rxs*hI(x^tW?pm_=!$~C^l@6=40Oni zXtLFiz+8X!Ug9x>d^r@hoyhOH5@osks!rUmhoNKvk0zZ{CEqri7ws3NTE}#%IKUJ{ zjAdaJYhHSc52?D>l?#U}{O+63Alx1w>1t=(c&VS(DQZ5VNd`AWjrrhb_t$g>t0aKm zzF9U8A$VYo7~?(W`KfcleKG=2X~2HRK(vTaY)*7K(YU*rtU%11cOASgWQni0-<$mTjLn7uS30aj4(%NW&I`+@|G zrOgxsF2=eFVchb(}UoIHO_#l539RBMQczk+LY%2W^{^Cj9qB{Ky+ zMoG#@jhT&x>R3pM$!tWh8;E9DO09kP(~l&T*)h-WIq01>7a26SjS>41koVY{;q6JK zsj%62cAfi5{_fddpar9Ez1!$j>I0Og6E~6-mzMp3nTBugR8I^84~@m(#*$()=|arU zqWxt{8Jcu&{i;QT5Q%=iv<~!@#n3gkG9)0GIU=uNno(Gma9Z|1V))o1v)}wtW4{8^ zcN>Y?|D^sqa>FW%ZlBY}b1;M6t(H<3$Ga6_S0qhjD!fVP)d2DBS-mWRXnJ{Z+^9vIue&;>R%)#n$*;L975)<{e(XJBO!CFjwzo_ByhSI;B02c^!+4KW*g+Mb@hEE=|7T`$a<~XV zdg>lM{*yWqxsGkaTrmoyk{xED32a!QyDxnnE9I`y)(2sXq!;y<-x-AfV>q8IcEuXU0}rFHh_pHrpptrhJ;`xd1ausoBKpCc6aG5O zY@fKJ@pE-Lc>`Y-HuC)4$m>x!X6}1O=>D-kubaV%p@tNL*`9-68$2ae)!I$z5HMxF zV1XU^sH+M!{vQ`03%s}WhKB!8gy1kK{hGf4l6eW58O0l#+vgSDKtOxw-avJvlZvQV zjaG4-+Ke)P{QG6CYCEkrbZLo5U+azO#&|MI*N^oMM^(kH!mh2~4v>Y!u4q)|K<2g@qtS#eRs~v`{tcq#(<&xYGeIdK&czk01@}#D}(|^BR zD3^Y&^DCmfZOghdRGrfS#B2*oSa`+2%C( z6g37BR=@-`Q>#{P*q~v?0*6!Eb9ymHXOnh0Aa3%2Hx9X>fL^620FJ?t%hmy`M#qrO zR*~6G-um<)&7xmVE@lSp&63MYfPX(*@2J3lM*A4u6V2mSJOK<)sp_pR2#7|-coP3T zzN{ANA;Jhjlr;9AKi@NaE-bjL?EG+%Tp0aN8no}YEPN3AvNQRt)aWhJF)jtEh1ipc zx~mRxKYhBoSGngR0li+dmtpMdF%JRH52s!+^gLS!pWhg(IsoGsXwtA&x52w4r^rxS zdpJ;+30YtZX-}1%iMk_?)Q1pSYViCe zGG*G%y?BuXzak0(3kxXOvl8x2{?0s9>IIt?q;@9hF1iD_?c?J!0K zCLlm5rCR6d(^lCCq6>mX!v6l2bIV z1N=^mBusp9c!{p_iYEyQp?Bz1J`X%!Ql@9(`yCno*mg(#u5pks@iDT(1X!F6o?|~g z;#uZtVQtG|?I~okB`ZJIcLg!P68YYWXup)uqb)~~c_)1p?=LQs(bDe)^MB-$@Yl>v zlbxoUltaW6j5O8YBe8>A$&I|azYe+=gm#dao!n%uj(+l{-<(TH_4^}I;`;SY)GY!9QZ|euHaM~}L#UJ(7k>p)TS3)FFKtUAOE`)3%B2llc0 z(PAMJFq*Y>lFPZr?%|oU>G>qt%j=adg#|G2c0JEuXB(2RvP+BCe?3LN5;8&b>fb)) z|ALznvY=wfZ*aN5x{6l6&v#~>uyhM4iq zN|K3_-wjvJ*^Lm*ekuIrg4n~Q$TwRkj zMcb&d+gbZ%kP@Ql)t>XU@|?8QA@|1tZfpP}w`I4n?IU)G?IXQ>pQmDoNAO^2wM;|F zm-~wns4KyOy`~#7tB6X%k~i=`$q3o`F5NqY`Tc;+69 zwM@oaU2HW~_|ne-lo1R&5RWEp7S`>Gt~K@*Wo0!p+Z92d6dhWzu;}@rb|=$ae=_MV z)F-CRGmfKUXvM|(gW@#*#*@Gg3dJY0nUi#n-(ernC5b2{E3TK-f<3imeC$F^L|D#^ z6j!9sEt*|WWhD{EhiLv;Q{I2-^7wq`mH9hd5bJB#=eok5nP^#T!1bbDFtatUtl4tG z%;FmkWmc35@kb`nL_4{4DVeCAnJ&8l6J>3(qvdgzm5!vHq(>~k&*kmg-#fD2>fJ*+ z``jeIzWW23n?$O7FiRja+A3#x?5i*74;RbW<79}^ik>9}I9L`OG-Uq6CZKU7L4VU$ zcgO8_bqpv@a7Av&;?bv?Fv|EwlCyX=p?t#3~&J`&HqH zjfI;g@?GuSHOKhRRr*@&TVp7e!2zE?dU+YZ@K_Hb{d9Z+J-q&HnSHu^Mt^^ahRi(= zSit3fK>QU=lO9c3k z2GW{*E45}PpA*kw_po`=Un;ZG3aR7yirPrp(TbMKMZMqfro@}m2cUo#+vqTrw}ioeQMK3-#?|z>uRgyllfEh zL%|2P;MtOy99fHwTeEM^(G_(fTw&rBMPTIWUI;Jj6c z%Mrhl4K8njul|C-?dF(jqWW}(DI@Zp^6v2JduIaEdS1RPg&e5z zvq#0?SK)vrOJSVoKCzS4g0aUjRgjJ>);veVU;~wZY?Z#S8E@uq8L*U1WfDx`h*TGg zJHV&B5I}&L2F%?n$74?I#=svxk9+sC>?SsK@_Q%Wg%e7jktLHEg-%=$>>K#0ax!dyuasamr&}IWC zDaQ+2@^$C3X)Yej_pOwIA*Z6_lx5Md0S##``CAFyI;+;{JnMF_p1V3TDHZWAsTD~y z4JWzQb6G)f37Xcm)GLK`4)MGDc-MAesI+@%4u`MLCkKcJ!c&&dfl{)uc5=xTuWmvn zaKwm{lV@~~jv1m=G3svmgydUji zR)5uI|6)G=Do&+$J&An+S;NcPj5@p^lew&9Uh6sa<)YR=NPh3;ILG;Hdn)19unUPp zW6fseVaqmd62%`*up>@HdYy5X^Ano($u1%PcZIMphE(_Vs$C3?xm9S@gm36aaCBn% z19bMcS}n}1wRZ5u+PDYVShnbX0~0V~edWt}JuvMbmrVS>f(g24Qg$Xyl9%eTeTjo% zszA9%Z*l&J5Iu;dK#z_apAGlqq<9u$My}|>jPj;J52u7b+3Cslf})+s8pdG1O6?ag z=o~g(qDlyz*}wkwBWkQ|zX-n`!|dho&ZfM5H7ZbSSlPd{Wrw3BTsimabWg3^bAe;u zB#(9b4)$Si1&Vk>m}|&8(+XlxC<=Dy`yhjn!r|)5%YbEOGw~U}kx%?C>{(paSK^hG zqe)OORSs4~f3czvjWcb3cFCB(JWmMnBaOSwIF;L7B=4^zqzT?m;`Mr(ZhHEc$dM&}$w6`P)< zt*0z()^0hVTT8pHBa@F89Fl1@Eo` zhBYIjv<_-Sj6|`=#ty>_^`_El+E~Zw{7!>kE{Z+-NqVTuEY2^%>A6b&U-4Z-1~0y(F8@vsZ1Oe+^O0jyO2cgL3G$xw$cgbR`OPIJ(%Y zcfi6wiUM3hYZSUsFd0GO3A4=*F$<=m=3ZXs?_Dw$A*@+L{O@R4fI%)R#@vYq1>EQn zm*9bq3}bODw3cvTEYA}*UjMan$r=o*$;UB(`5!}Cifi>Qo!(Y%uyL2{FNU+NPecmH z7pod$FLs=>g)s|ND{4Km35+aPIjnRlK+l~r%C}L(;@lF3Un>1YnezOG{6fYE!jtmUe;XJYEKGji$JT7{!73{RVqAGfKUW&SKLc*eWpx z9kQpxjW8$0)jEyd<4ldu%VddNkAGsRLx|*5w2{WDUn`y}1M+cDR>0HzpkDLmM*90l zF7~IZWSZ03K`Eyh}Nk*uM1m-PG7o zLmuQ-@~xyx-qL6DU{k!T7!5=y<)$z0QN%_F+2>vgQ^rS%q{QQdQocNX`*|E$CeMyJ zr%T-4FvgDlBDy#0XHyL5ZI-aTuSgh$g|c3OTnwcWwo+~?Z1(Ksi1RfULc5mcsL1t_ z=Z0IdZ5j-yG*;{cYnS9zVWYeU&qAVjC5x|{6Rl|t=Y1r9oY#WI z*Ya*nay-7=8pwSCpI;m6vTi~-E!v&F=|F1kjJ%{;#$M5S_da4OsL>Hb4Jz_#-ZmC6{UV4z#?vXD755t*%+fMIa$A#J zIw}-RUW9en3B{h0CoSM|r4rv&Pa5!`uR=fxq7LCq|W9~Tr2CS+fdZxaa-D3rVM7;5sp}5S~ zh(BuB?SGo-LRElac5XW_bG zE;7wpIj6K+TlW-)0Fg?`>?}bQ(tP9W<4_*l7_^^(tn;R!^4Zigt!gt^&Fa$VsiAo! zkPqkBL3Ov&%fsCi3-_53`pJT{&UD7Y-jd*Fb@X77^#-hpU59nPPxt2r%Cc+Bdf8FX zf}u58tE%wf^QGbd+G20*I4-qgZf%QmIhv1x-Sj z_B8?+a-1fSch<{Itgl-rWpf{2=pqbTjgbzR85{enP7u3jbnA>!`{$DpEJ6lciNbd@ zN6o2#<6`8avz$Mm06y(FV3z%G&-~mkN8Ua)w_K-nvO7Dc+=z=Gc`PCT)PAOA$J>n< z98SvR!Qbs{2W78iFX1BmM;@;RQZr_o-AGuALl(A~E>j;q_nOG{3h<@-j}MrIj@xbt z&FxHwV5-^y3bL6q9Jhk%8EMOyHqb@d_uony&$IP~Q0O}`rZ~l`##$nJ+>ie>WoC@g zNe|5}zPCAk(uD~5!Wrw~beI1dNbwjDH8Vbf7o%M~So@NwLJBY!2IuH}UNHDP%)9Jc z$e+>c?f2F0KN{9h*S0Yjb}t%@vP|+0;s~JN^y&AA4N1-#H1QrTZcko+8H8`*lR))D zC(CYy%V%X=p+vK*&2hAf>gTSOB&;kJ+8(84D{%F_Dg|*WIWGLuSC#EL8xQF{0L`*J zhApZ_R$7^hb(s<3rE&S6!>eP7g$%!d(;KdIDi)ML;RIRCzB*Dca@I$k{kbRiFjq^b z5E~htZmsD(NCEM<-2*T%rX++c`!sxkPwY1n{47|(SJ4W{4s-Kv{48_W8v zCIxx|%;(by9`Z<#OADpllWUlB$4pEpX@E4kE$a_Yk8gQ>i}kEE>nY@LB|>~+?OL#u zFbet2vSQ;2H)4$@pN|6ii2!oeXouEL26z108+He$3kdHvO`yTv?qS&L`vlNtS=~OC~xGw|OTvWhy@lGS;JFLfp-P7h1xULS zBt6Cuib_YbN898)m$ziShM3{Cm^wb-Tq>Ar@XEQFV7qsrw9Bp+Yirhd^_f7$ zo84!NKh)t`PsZHLLqb(Hk((J|)MZe))x4Qlo(ei~_~|btCo2c2^+H1SEvzIDP}3m~ zW1c@|K+%A(MOM_{hx0I{Rt$g@NHKF*HL#ld5q`kFI(Li_C=<}8KB=1b^tww?{q!Lp z_iJoccTHo%BKz^T+ccz*Ty)|f`=TQ$pkM$!zdAEj$pX%wbokg@D3;PI4Ox-ocpxddkt$8#TTsYHMoh?rA@_t{l} z&sQlA&^?XG9Kmln!+Z8%qBv+QmK1hB|I0s(_K8h~J*LM<3PQ!STN0hoE9`}*RdicP-%DJA5P1^aOy9;hWsWMjo6 zX`&H?hJ|jwPb)}X=3V*Vcx>Qndd}(a- zd9^rPBL-15pMJnjUvW+>%aIA|>YOlM0FQ0@FIc3vjT7keBJ8RziP86^ zk)iPKUcwCqC3u}wGyoKb>-8-bx&ErsB4@&iKTm~?Q?&5S9_o0zJwn#TfgmnRBK3H! z28?_vq_MqEn=IxZoD2}EtHHJ?Y>LBeQA!RG>H(t=L}E;+z={mKnvZ==Cp`Q7zM^#U+Y*$p7 zV7U>`(gl^@OYWw`qO@J1xJv%)Ix|T^#xyllhGAyW#HfJ4XE*+>zI_mt0=o0r_B)1-Zi(eALS>I&#QKkHdL&5iK*sccGcxaq>&7O+!lrm`=sGJXw-_}lNJ4qx z5bUPktbOpwncaL&Sr+Qr03c>mx`Wxjpjj9-%R3bTuD&5CUgm$VOCXv8-9CX05QmZQ zR|=-O?5f=nOQlXyJo0L6zDyE3o%_0xL&@#7@6!Yg3gyX*gj%ZMXFn%J;&ox9(0Lv! zoxr5^T!F?Fi)U&*Hd2BbSGwB||0M|C1e5)HY~KXa3lc!oz3YdH@o1T9{_dekE(+G6 z1C*9qC8kRN-O~wlzNb9Q(tJ~8^#X{`h=Rpd#~VW0U!;&$X3jBx_ZryP!CCj`+tR-Q zZ*wY1_B5DubVy@zW=a?0C9fc598&W|!_G}hCzo5)w^8hg^?p93ghkO3;v<#?3)Rny zIZa?Y9wm8g%R={@!?@&YJQOb}YvfKz2pqYY`c`C(35#9{>P;sL?yPP-H=n*(X+GmW zR~vDIDmyq=Sr(-ZR#s-zu#5;OX^TQ;A+6Al1FXOqIPkXLBwRG`5cD$}i%a;ZbFv_B zG5`a&wkie$2J$4BAkQ!PGJ%S*(pZ4ux93h5TzSvIP`n3JzN4J&=2gB>;n+ybgat$V z9C65k!l7NiO;@D;c|9VrQw$K5=Z=>!p4EGtYE=@^J%M}~ybp^BBffwD>C>$7sKow zaDqq7N+L}266+FXRn4HWF~No(Jp0=M`HC{g7)#?E)D6ikQHW7LJUS-ucQR<2=yr5@ z7(*&&M6z#+XK4}?NiQ2SLUzoyjnhD&4WZQa{P#g%yEAok)zg6$T7O0GKsD<)HSTTU zH)%V|?|ToEFm$8Fl;C3uA0nS3pCF#glr$K!Z|>p?3$)V+F@B{GCBQE-BbQN zffbLZTZ`6jJ(hTBkV|yECA<0wgO?f=tt+KZe^kQEXp}Uu)92v8D>H^hYDC?HNGeAQ z&)ow+*9x%x6FF-z$#DuVa(fh@daPN}eD=9#f(n@QGLc5J_KbX|bH^C)YXJ@;%|Z#& z-?6>6u#P9jzur_@w|@A97g}*YM)h_dtArf4D`|RR#zjbBC&g#V3ccb|qrP0AmI&!# zE&qdk5UD21eWuht7mvGzn^SFuv1pPp3#w`3sp) z0y3tcxNYatp9peLPB1-jSBtTn9ny|dr$ugXq>m;pDH@U5hIoW-E+WW0~EFl=mM|j(3jmY@lKURX<)jXwpOPhny%w zNOze~p5PyKoBg(a2;r-(ol*Mvz3?OB$VuBlrX{|_N2pb&x%@;Q2?%MXWE6jm8#q&K_VjLRd#BSV1panby;mRX~;?f zf52s=_z`>e&GQUxP~?~z8I133)&x|ChE%2&1q({lc!t>t*36WwP>_@L&AH45{2v!! z-YJPa%prp(fV5y) zxnP6&dhp6%XlVM2G<$bijp=y1=s2H{zcbQkj}7zG9!hrkQw#X;#95 z+<{Y3aKm*#;suP+0k-cU_YZf+(C3)2_*9V)_K;?vF>$yVXy*Ufcyaol-oElHt|oYM zW^i|RcXubjU4y$5g1ZmF-Q5Y6KyZS?;O->2TW}37JMVA*fIVl=p8YUi?&<07r>d)N z-R`QVHN=oU_n94kYJExjU=9%HS`L}(5cH+W1xPbTa=XAkO1ejGw!pevxjd1`?EDNA z3r(C}D*efhfcBq(+EM-#g12yEtg&a^Fn11rfi!hJ@c4=D*Zo>{aj*R|Yq1wLM(}|= z2~%QDr^Ss84S$C@kR(~6;w8Pwpow6p-4i;=t-MxL@a+Bg*t%R zGXj+mQ@Bv{q}SXvVOGKuv1ZaO+7m}Ip}HK2odQmsWDM{3Rc*^DE?y|a<6j$JZ6Ax0 zjvl(t1QjGABa}e1!VXkEog4)VHB!cscw$*Gk~XgDy*QL!js`(N@jgQx)bfn)p5}GR zK}8{)ClLM+dze;58AiVn=t+1PUMn3KB@ISU3CXEzn3q#V@~|%*s2l3uSDfSZTv0jz zdu#LOYb8%-71r*g2+)=qS8P_R{udTxkPbXg9~(paN0`JbU|!>IUKJ6T00%9q4J=&9 zLWR!*->r~@JTIp{06Wb=j_(Op8T_QhV8(!1_?<>*0_wH2Mc*F?Y9Qg8YqQ^NU(^VI z)$>RXJJ@|q0fJ|Ad7B?uf9RB0%>31V45`zB$0g~o49obA`lw7Q^a=Ko2a2>tIFMfx zRB&zUKnJe-Y=Bk!;;&xFtw)O*L$$ZigpG$o$krLI)JKk$BtCEN)4c19I^0AY3nrKA z8ydURQ{D5*iT8CUc+cuGr!8b!+LrcTY_K zO8AE>1~D4cn7|K2SRm&{nqC~P3@IXJQ`8#QK@T!$kTNh+8b*4}mBPTs0G>*LVZ-qG z_{BMqF1+`&&N=YxJAYwgTYHQoWs|)0-={f-PI#k6*h^-0N!hQ{RNp>FXXu_SiW%B$ z`HNC<$`o<%BbZmU z4VvuoOME>C7(`2L0|4Q8KqVQt{if}!{zucAW8ZjH}(CWUmgN~dPY1*)H3TgTZzJpTs)8dUylI7~2W4%nZRs|nh4 z!^Xl%YW-A2^MykSdp9fYgMnw00?Cu9x*ZKL&xnp1Q!HViE`XrN8foiOCsnz6`ue9p zL>b*l=KY<-2C+f%ca*0BHBu{j9K0DExpT{0*nn`1*8r!j2ipVsMUKyMn>!yZsonT= zq_#x@uddFSexPL>n{q`{+B_|mzc-t|TNed!VSOir7Oy@qpUpL3fd>_^KDwTc2Za#S z*Z;9-++){hBOH}PE3K8f*FGrA0GJ#Z4vGKc_T(xv%Z3^q>^Q>)Zj|A!4!6pr! zf60$7{u|qk{O{X*;LkvSo}Q7<-T^1H?z-ggR@!4F4O-E{%F6z7wFv%^0~W&8@U06* z0v9%7fx`#)@qr*AR~|gqfn@T6pt8L1@Ig310@^@(v%%WtjQx#Hagi7QBO}dH+&IX8jQ_m8T%lG`zV+%|3cA2CHN3uMJgqwRwWf{qKWM4${MeLEkuz*$k+ zgVZkYz7pc?w*P4(of^P4M6KO@i_{ZzP}}m~Jg78;fmKEWw=!xE)V)x?n!zf!mv|f( z_K2OR={#`#{|Fd19ml06Le-QMOLz$R#Tn+Zu_6ry zCQS!h!p7uQQ3VEC{Wm0DC}R#dawm$WWDe=UmJMiBz&YKwqkkWfT2+sHbP`0)<__JX zg8s2GD=J!fUp}m)@cthI)cKl6wJcA!v~GFJ(PQ(ai3hHmp8G?6lQyi`)p6K6s5pOt zXiO6AHu+|+3aUq-UZ?P}DJ8udE43{peBb+V%I5fgb@gLeNZ8@-PbB^HXr^V6p>r0r z@H_I4aWRRnXn?msXp}*YV*y{Ps&rA10dS*asAV3&&wPBm08kUu3-0h>ndtycnz0~s z)L?QXgBI7b{kp6QKZNMHsrSu|wjp^p`d%TG4yovY1Qc;M;nL? z>8(dyb|k^^5SN`y%m+(x*t#BA)g%ny3ljx)zxsd|;-rjyV)%ET_)pl6WF;t>&*31ET zH-Ot(nlTUeOu^o`K>32|HEo0$PfO}fQq+MqV!w>0CBQP_)pKJDc*#SCaMb~Xt7mEb zgYP!j1Zr!ZjRPh2=P&lmfZb<*6We@Mk4^Nh-XSv=EV9et z4G^cGU`V44F>AvQkPE3JfQtH@?DE>gm>OBcT>jLc`^t!6`;I>ry%uEzzgInhoBme# zoH^EOeO(^(CWHsJ4Ez0jLw*wkE5BsoDDZ8hv7x5$92Z1`2n_8(7_zKQDqTU6^M8mX z@k#~T3u=j&-fI8}FSdhC1-^!z*|--s+kD)#YB)3KU_|0dz%#&#NJ6Y{Qb^2kKltEFo!)P>Z{NOO^%IUgX`zdEt(rQp{rkJKO%BE35OLwiI zQyrp2DUC=Kdb6*-xP>^g3WwR@*to-<4bQS~ET#sx<>P^ptm=tl#iOXNQTp~tC!j} zg^I=iWN33CUbyWL5Fl;g>)@t|cx|#*Hz+!8_Ht8FrXU`0|et8M3!1h%dD8osqre5!?~@Q6u8Fjbdmc^ zxM%A~?M5yyNdZ7o>}2SAg+e*w`2m;+Ayn#JK}sZcWUZ^9c`Nof(kU(?idekwqhyV3d|gD%3zP z7r|?<&}Oe7^WA*dtWJlLDrkxUh9a^v#(b@Vq`j7!WuKuopjk{?R>auG zY5j$YcZgqBd65e;cRb)P{^I&^o)_gVPru2ZNm67Tt_V~lN|}g#a}Yn}*EABBPe)Rw zklGxJo8LJqzfwwfQa&nAuNeK?8v#Bcq#a&_;tlI-Z|u6K?P#_qeOLH-H%2oc4Q5Z6 zti+yZj|87issNi#i|bQ`#SiUD`A>=|(fmKK!==7U!#pWP9uuz>!XnBlC+XcBpQni& zv1RE)^{w_;=2-JCe#rvp48Cc>*AMV4K}*yq{`^B*`4h8tDd`?94*j>V`awdGrg%2` zhG(zdgQ=g`E)wiuvUWD_Wn)gBskok{O@xP7s1nStx!CL7Xg=4Eq~%jt+LoXa2@v2a z_d@%yR{1loC_ATNz#1`@d^fVbIVv2D6>Ak zqh->3uo}^zRGXSqn%WypP9@b>KwAsZNB}ZO!~5Ozw4MzZUnF?1_Y@S`JV|^9Fv=yr z_d5!6I`}0Aeb`DH)htq;)W+fKkWK%rU5t4!K$gag7D)=jUjj4iKPkyiJ7ui4@Tzd3 zvtx6v^Y~=a9SOx?dz)BpwCeGCA98Ncr=FUiX&9DQ5`(qai+FdgLdZzaQ+tNAq|n8AIrCta^@c~y!WLl2 zmOwEPdjXyM2QhR#z(MPPagi&v$P;sB*6u>|bDJCkHX3QDW5Y7TM%G`^v5y{^+}4U> z!;~ae%E;5L#SYjhn2Vl+MB>*TO%7%WYg>u!8^Oc+^2+p1J_0Y%Yk55R@LR#a%SJgh zzyz?n_%}-+pK5E);vHa5uNzO*6`q1Z;#wu8X^&CU4Mo!hMI+?v-l#%Ev0`|rF}&O& z7or~#CjV5sH-r0Z&z`ZhX3goVnhON9l?+4KcHz7xa3%HCU-)bMhok~QJ;By2EUvFz z}hL%W5!doqMJ2k`6~i1w|Yx}%8%4Z z_pbBw^Azt{=O*8bg7na`4VhAMX@1tn?ytXKtE6NkKNltrWtZ>r>9eio#wr(KD|`>{ z(<;wq&UT>FdYe*qN?IwcOpHcCQ!Z*sf-N>Z6_P@0v_DzsSrEpq10G?30=sW&bk$o~ zA6j{4XwQzo-bW(`|QPOCHQ?9=mY-pCfzu>Vs6Z}z2oT?e` zGUm0ytBzRin(z2f9xUH{C^jaAOr&}`ic33!Cm57MkUxFaW>kj^f52R6ve&dN@GZ}- znBx9WE8;-|l8wLPazlFqfKB1Wd)8yid$`VOa%6bqqN!!HN-4`Kx2)pS`h0ex51~Jn z^CFA-&RBmN^wvXkjNn}9Rm1Cx2i-a6!;QO&8^FuQiv$VsCun$0-bXbrF-}B&7%DpX zO|*}x!SK(fZuYKl?F}*klHb2{YU&ihdn+VHIT-iVwF9c%= zwuPormk-`hp`&62VEZVmGk~g1-tfd0C4~Zld~;8FU?8o6C6>MMK2drZx?Q(4#aJOVx72tskz>AGA+MopbY2eju?tH#n z3A{TllV(!qs8RvQS0J9DKtXZtik8XberbL(vS6%R1irrwd7ZLp)$RB#|2BPM@N~bA z%-gT$MmB$!`{mg^I2_{&YwE^zYVFWr`f|Q=gv4kIZ)1!3WV9WLjm! zXgp=4w&_}nehwvOmG}rJ;(s!dH+wnQ%q<)HTLiz5TO3$y>)7NtkeCCT0l&VPYEo0f2NZ1iiVsSxT-|7Pj?g?bA=+VLZSS0933)AL zMcaL)h916CUth|n3RsVo@1^+@MZh6z{JOwLeVd-CuYf`5jUzoiC~ycICA@pWfxi0M zN&HTxjPG;Ym%H{ix#rEqx-H2@E+ytbGp0XIobu$|GHf6A*du~7E57qc#j>!%gdjtb zce|j{L+BvD*FO!ua0WaxbsR5LX6HboQ@k+^g@zOtWxD}nP8~Zqsa-b@^lkr@n0gi5 zP)970kX2<4|ElPWj^p*KcdB=cG9)e2oX+vVc+C^jWr5vyc@&7*jcP7pF1PI?W8ogo z#dS`{f;|=C9B!qP|7IemnslUq|GCV^Mo@xS;SL!-`)m`zBrDvgjPIyY?_MY=k%)?- zW&%fdDTqStw406+abQMb(agO`{(O|z1Bin|^hG(cT;DCD3om%oLD18GBm2tvlM_R& zWXe3kL#jxm2-y4Sc)L27GjHEgboyp>EjY4DnM)}Y9g7{E=(A`qPghrT?jtGZFy5b| za|T}MH*^7m9vF!p9O>e6c}XXgP@?6^oW`V;!EHTyuZv0@rcN7&Xj~8f`ps}CHlCLz z6s=3fy2Ek>om_Q3-_5CmXG7CsFh6tDO!2t)C;ceQdmDxV;EsB<#FP=+O|+N6Cuq;b zdG#MZ9Oi?s%Uy1&s?9g~(=%vaus`gx871O;O}24B z&$FOk#)AtUP|0cB-gkT>Pa@U$ za&axAqig<**JzJA zb>e21X3RruSS5_V#s6L;H{)v@G6ZSss0P88p-&u7Xq`83RB3ikrFTk*Y1%Vy#z|jA zbRn z8IJQ$8Z3RTZP~$}yCNxT8|&;DQQ`Nhyght-SBX%&?|x%5Q@486Ly{Dj4tkJ&$QSJ= zX0A=%XA&3fdokTI?3|!07r=ujLLFk)jkZhOEp+nj(=u9#VuppKG4byy^oAUZy=x{= z7anpbnEq20L5VIcyzG$6(myNR8^Kg!`5a49u8<>$|17Q10MRMk&fTSMhc5uA9VbK zU(`pKYcFttB@FyCMF4yNty}M~v8uANV_gaA6Rrea#+w8`PW-yK(*?Xt%%pD$cY~7l zd2PYR?3;q6le{dVV_Gi|e3kqIv>MJ2ftC*SKE|z&74U1cCs@7O5kV{0Wt^3bhPH?U77HJ~xEYIQDXwF1YzayD0S^xhQ{+#$ zR8)896(Vxf1~i|pbDp=YG)a6`Ee8n!zL<1;@ZW8ZaD3M|Qty;<(M$rBYD+U=dJJi3j2Ht*Z=U>E?Dk?q|WvL8=s9?1LXJ&`A zqJwC8M#RUw15q+qtA3hk%FSzBj4QZbMc6|tvWqP_^!a_kF@rN8?`GvNEtLfTy|Pvq>CSaky|A++^Xb=5A`U%3!^LWa$pEpp_1a@-9!&T1dsYcRLX zFZ~Q-ync}sO|tSsO=L^~A8c=O@@aLQgq=EVM63Job2i1EhGv)5R+O0%zhsMW(w8q& zKd-dkakHtMZm>;{#jKs>c|=N{l1fPN5lCUb)He;Qbxs)?t+@TupSn4f?Q^Z86H!OB zZ`^PPs9sS`-AK_C1bV7PN-XfrOA3wRLpBPV=B>U~8KT1|UE3c&1SIYkz=jEu&<89= z|3weVtv8{^^_kp(% zCgaiMVw3B&Y3uwrxUf-lJn{M^3JyY|Ja-!CxZ9U8Z$ZKO1XQ2%fak0M0MKv$yCWbq zA}y5_@EWpN80($kzgRfjLE`J|8qPxV1606ZhS&Nb)#>NK_VhNj;KBoE;m#wQ`G8Zv z_OYxC6$XO20D`nP8~VLfg$Xl*G2lsR${(V1PK ztEjNImq9L^;enPTf%NUdCB%UQ3&obv-K?TeXl|oC&jZLQ^?d2=E}R6!7Ho8+u&W7 z$hyASeIu6LQyRrX-<0nX;BipJ;Xdc>Dio|VftL<1e9})A>@B}<*Hi*d#Tv#JiJ!v7 zsCI-mdyBc<;>G&aTnbKa75o$?l6O0dYoP zLu6@-LVzEM1Hul;>ssYP7k$EY+eQK|Vj^Cl5NNhw!M!JCD>W{$!i#Nvu+w5-vdfmv z0S9GHwQrd`fA8)vj!$bV0i+=m{ZzC?DzP2PTxzlOo&{YPB4+6qSA5-5tJU|)!4xf- z_qF||Q2&AL1nAn?;HsOVB|yyDLp~H#iSCQydyj&@QFh5!>YVz$aY-?CzqtKhr&t0- z7swt}FJa1ceRw9tAJdWPkV~%=wm^eOlp9?rU^x%8d~S*25BGZPZ>Pa05Ef!ROz3w&2kdvPKo-?!c!EkI`J zc*IttTd9zK0GUCxrmd^xWvJ&g`ruEs^^WQ9j0{VlD8Jx~z^{`oyhVn$-#QR_(3&bY zQ~%&#&ny$Z{gG`h`Wfo!j8H?5%W@1lJ&68;5L5^%dUn%zcc|96hKt-)aF{U9}1B67w*dfi^Kv^4Rhhk>7pB!!gO* z^L_Nn#P(&fV*-U%8V(ge;f<|O?h<3sE#Z1|Lwv5#fYk4 z0OG((KuiffRQC)j0MuXMWoS?t&4cRr-x!}m!)O0|8u|mri7(snw;Mg!lGSikca{(m zO-mmkzxshl2^b}VDZL1~b9_LduK!ibodKeBrGyQO;@|0}ln&D<1r^182Q=UEHWju>4U{^E~lC@epaMEa4p&s*_5C8#CS zgCT&$gg3L{Vmgf`!!DWfdwiQ*wOj6LwTaq$N2IB+fR+P;??pfrKFS2wBy8j7B&KI# z7*529^ivWwweDK3e}m)auzV?d&0P0UGBtIQM-nd((*gte<(hcAEE%E-LNJgT$KP9N zQ|xt+7fa=s7cnYoCjBHIiv|&Ee8C?3^}|y~1%VZBp$Id=r-?B_&i1>Sox0v6ff7$Hd+>}J5EWmhFU}n1V!(fQp#rKjhoEx zhqPR?vYJW4(1eDlsNmsezbjRGPAqt1kowRpXlWqeoTDIXQtV83%1W#IWJNar4?CegW^ncE}2aPMVW67A{8XC;f4ZrIr%Yv)5Fb5LO&kf?005DGWWwBQ?n@Yu* zzTICnA#8kbbp1zvq2_AGHWnnnmoJ^?5^?ZvX|9Pt#%Fe<816eRKvflWJCiz}zECu% zXkACcqg~_wU>8Cjc{Km*ErpwoFvxN=(TrdT z8w<-6!|Phdv`tpeua#zL1!Q#{?;MO{ou?iW#{|mfuv+UxVUd^1nzTSmuK|&kz-sPM zKL0TjlMBGhLU;GNE>NC+L^f7Bg*sj?A`cntmIr5+*Ktqm>3?oZ@Wx9{Uh+KdXX{u@ z{b7jLDt?NPKwMp4lbq1KR9Hhf(mLbq7F>m1A7W=O|Q4UPp7&ro_|6JbXK)Y67u%xVdvzQH_)DI_SL3@`)7oZu+Q{eg8*N^Dyp z(X5u|!v6QV`@l(aZ^V*AZsv|?Afh=(zf&F^L_BZT&8jmpSXo-WvROZU9Z^z}^T1Za zkW{tEZ7no7LJv-IIUfbHK{Yl3dMHVopC=thxt)LFwSyBS+Km(L0U4D#AxU2fxd-T<->9^pcA57 z?>tjJpwEpsI9X+8$@Pvhdm8fCRtUGn@*2Wlpd%@nkHqdCf}$+iFBPET-+WC8nwhM4 zXT~&F;~M0(9py=o(>7dct-&Fk;oj0ccl>^3gn+2O%|5UEgyUee1>yi*dY-M0!3+Vw zh(|LeD!kv|N zfL9n^Y3T@)@5lmUQXd%$R+K&GIIeq>RZ}_3SO*HEocteu&F>-VaFytRtYsD=@)WuT zn`j*mO1KZyU=<&-afv9J;8AVK{>Cu6`GP>SOVYl%(j0-dy+qB7>88>?e*u=0Mm=X zghrgnRb_R$XchCg_D-1y9$GsEyRg^IdBHAMTk8tY zW%DG@1aAT?{Qz~enyd2H@jJaiTJ-4$)4zjnwpliwMTs#D2<(6XoPj^YWL!fu;Y!lv zYPk-mdKJB*-uhMv4r^8sCo7C&Q??6Oog;jDzseO-HySvsIo8SAdv|1JNF}WcS;6Gvw5HAdb`eAc2fR!vX@X@~ zAuyLu1d}Bmc3l0do%W_Y-UViQ0FR48H|V`?C!#M;w{fiX6Z)wY5Glc9^ivU(1O9m% z8J*UClIz#G1=e>pLz!isjlrn*FBk8du>2`16fFhN6V#ML+(efQQE?7cZue^pHe25x%6U0Ld-ysE7 zVa|%tEkaI637&u?)H+3TyNUk?TZwd{E3*?QTeTuj=akVngk(OF8}2_XCyc!s~{xtE6ug3+HWuZqIVAzmevk z)WrjfM-QJ%Q6+knR-hzgt$_XjVlXaO@Xs@EfL(sD_4^EV!~c$uOBlzZciv%syRsMY VrsN+$ugwD}$f!!!d^G#|zW`;co|pgt literal 0 HcmV?d00001 diff --git a/public/scss/application.scss b/public/scss/application.scss index 9c0b90990..5e4d3dc1d 100644 --- a/public/scss/application.scss +++ b/public/scss/application.scss @@ -19,14 +19,8 @@ } } +.remote-vetting, .second-factors { - - @media (min-width: $medium) { - > div:nth-child(3n+4) { - clear: left; - } - } - .second-factor-selector { text-align: center; @@ -147,3 +141,21 @@ span.label { .alert-notice { @extend .alert-info; } + +/** + * This are custom RV form layout styles + */ +.rv-form { + #ss_remote_vet_validation_feedback { + .form-check { + display: inline-block; + margin-right: 1rem; + } + + legend { + font-size: 14px; + font-weight: bold; + border: none; + } + } +} \ No newline at end of file diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Command/RemoteVetCommand.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Command/RemoteVetCommand.php new file mode 100644 index 000000000..a924a37c3 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Command/RemoteVetCommand.php @@ -0,0 +1,32 @@ +matches = $matches; + $this->feedback = $feedback; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/Registration/GssfController.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/Registration/GssfController.php index 7770cdd77..601cbc95e 100644 --- a/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/Registration/GssfController.php +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/Registration/GssfController.php @@ -174,7 +174,7 @@ public function consumeAssertionAction(Request $httpRequest, $provider) ); } else { return $this->redirectToRoute( - 'ss_registration_registration_email_sent', + 'ss_second_factor_remote_vetting_types', ['secondFactorId' => $secondFactorId] ); } diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/Registration/SmsController.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/Registration/SmsController.php index 39db8f56b..924baaf8b 100644 --- a/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/Registration/SmsController.php +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/Registration/SmsController.php @@ -110,7 +110,7 @@ public function provePossessionAction(Request $request) ); } else { return $this->redirectToRoute( - 'ss_registration_registration_email_sent', + 'ss_second_factor_remote_vetting_types', ['secondFactorId' => $result->getSecondFactorId()] ); } diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/Registration/YubikeyController.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/Registration/YubikeyController.php index a208c71c5..4af2f0dee 100644 --- a/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/Registration/YubikeyController.php +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/Registration/YubikeyController.php @@ -56,7 +56,7 @@ public function provePossessionAction(Request $request) ); } else { return $this->redirectToRoute( - 'ss_registration_registration_email_sent', + 'ss_second_factor_remote_vetting_types', ['secondFactorId' => $result->getSecondFactorId()] ); } diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/RegistrationController.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/RegistrationController.php index a559926c1..0c4edba5e 100644 --- a/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/RegistrationController.php +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/RegistrationController.php @@ -24,6 +24,7 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; use Surfnet\StepupSelfService\SelfServiceBundle\Service\RaLocationService; use Surfnet\StepupSelfService\SelfServiceBundle\Service\RaService; +use Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\RemoteVettingViewHelper; use Surfnet\StepupSelfService\SelfServiceBundle\Service\SecondFactorService; use Surfnet\StepupSelfService\SelfServiceBundle\Value\AvailableTokenCollection; use Symfony\Component\HttpFoundation\Request; @@ -86,6 +87,19 @@ public function displaySecondFactorTypesAction() ]; } + /** + * @Template + * @param string $secondFactorId + */ + public function displayVettingTypesAction($secondFactorId) + { + return [ + 'identityProviders' => $this->get(RemoteVettingViewHelper::class)->getIdentityProviders(), + 'verifyEmail' => $this->emailVerificationIsRequired(), + 'secondFactorId' => $secondFactorId, + ]; + } + /** * @Template */ @@ -116,7 +130,7 @@ public function verifyEmailAction(Request $request) if ($service->verifyEmail($identityId, $nonce)) { return $this->redirectToRoute( - 'ss_registration_registration_email_sent', + 'ss_second_factor_remote_vetting_types', ['secondFactorId' => $secondFactor->id] ); } diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/RemoteVettingController.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/RemoteVettingController.php new file mode 100644 index 000000000..37d6bfabf --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/RemoteVettingController.php @@ -0,0 +1,225 @@ +secondFactorService = $secondFactorService; + $this->remoteVettingService = $remoteVettingService; + $this->samlCalloutHelper = $samlCalloutHelper; + $this->expirationHelper = $expirationHelper; + $this->logger = $logger; + $this->remoteVettingViewHelper = $remoteVettingViewHelper; + } + + /** + * @Template + * @param Request $request + * @param string $secondFactorId + * @param string $identityProviderSlug + * @return array|Response + */ + public function remoteVetAction(Request $request, $secondFactorId, $identityProviderSlug) + { + $identity = $this->getIdentity(); + + $secondFactor = $this->secondFactorService->findOneVerified($secondFactorId); + if ($secondFactor === null || + $secondFactor->identityId != $identity->id || + $this->expirationHelper->hasExpired($secondFactor->registrationRequestedAt) + ) { + throw new NotFoundHttpException( + sprintf("No %s second factor with id '%s' exists.", 'verified', $secondFactorId) + ); + } + + $token = RemoteVettingTokenDto::create( + $identity->id, + $secondFactor->id + ); + + $this->remoteVettingService->start($identityProviderSlug, $token); + + return new RedirectResponse($this->samlCalloutHelper->createAuthnRequest($identityProviderSlug)); + } + + /** + * @param Request $request + * @return mixed + */ + public function acsAction(Request $request) + { + $this->logger->info('Receiving a remote vetting response from the remote IdP'); + + /** @var FlashBagInterface $flashBag */ + $flashBag = $this->get('session')->getFlashBag(); + + try { + $processId = $this->samlCalloutHelper->handleResponse($request, $this->remoteVettingService->getActiveIdentityProviderSlug()); + } catch (InvalidRemoteVettingStateException $e) { + $this->logger->error($e->getMessage()); + $flashBag->add('error', 'ss.second_factor.revoke.alert.remote_vetting_failed'); + return $this->redirectToRoute('ss_second_factor_list'); + } catch (PreconditionNotMetException $e) { + $this->logger->error($e->getMessage()); + $flashBag->add('error', 'ss.second_factor.revoke.alert.remote_vetting_failed'); + return $this->redirectToRoute('ss_second_factor_list'); + } catch (InvalidRemoteVettingContextException $e) { + $this->logger->error($e->getMessage()); + $flashBag->add('error', 'ss.second_factor.revoke.alert.remote_vetting_invalid_context'); + return $this->redirectToRoute('ss_second_factor_list'); + } + + return $this->redirectToRoute('ss_second_factor_remote_vet_match', [ + 'processId' => $processId->getProcessId(), + ]); + + return $this->redirectToRoute('ss_second_factor_list'); + } + + /** + * @param Request $request + * @param $processId + * @return Response + */ + public function remoteVetMatchAction(Request $request, $processId) + { + $this->logger->info('Start matching the remote vetting attributes'); + + /** @var FlashBagInterface $flashBag */ + $flashBag = $this->get('session')->getFlashBag(); + + /** @var SamlToken $samlToken */ + $samlToken = $this->container->get('security.token_storage')->getToken(); + + $localAttributes = AttributeListDto::fromAttributeSet($samlToken->getAttribute(SamlToken::ATTRIBUTE_SET)); + + try { + $matches = $this->remoteVettingService->getAttributeMatchCollection($localAttributes); + $command = new RemoteVetValidationCommand($matches, new FeedbackCollection()); + + $form = $this->createForm(RemoteVetValidationType::class, $command)->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $this->logger->info('Finishing matching the remote vetting attributes'); + + /** @var RemoteVetValidationCommand $command */ + $command = $form->getData(); + + $token = $this->remoteVettingService->done( + ProcessId::create($processId), + $this->getIdentity(), + $localAttributes, + $command->matches, + $command->feedback + ); + + $command = new RemoteVetCommand(); + $command->identity = $token->getIdentityId(); + $command->secondFactor = $token->getSecondFactorId(); + + if ($this->secondFactorService->remoteVet($command)) { + $this->logger->info('Remote vetting succeeded'); + $flashBag->add('success', 'ss.second_factor.revoke.alert.remote_vetting_successful'); + } else { + $this->logger->info('Remote vetting failed'); + $flashBag->add('error', 'ss.second_factor.revoke.alert.remote_vetting_failed'); + } + + return $this->redirectToRoute('ss_second_factor_list'); + } + + $provider = $this->remoteVettingViewHelper->getIdentityProvider($this->remoteVettingService->getActiveIdentityProviderSlug()); + + return $this->render('SurfnetStepupSelfServiceSelfServiceBundle:remote_vetting:validation.html.twig', [ + 'provider' => $provider, + 'identity' => $this->getIdentity(), + 'form' => $form->createView(), + ]); + } catch (InvalidRemoteVettingStateException $e) { + $this->logger->error($e->getMessage()); + $flashBag->add('error', 'ss.second_factor.revoke.alert.remote_vetting_failed'); + return $this->redirectToRoute('ss_second_factor_list'); + } catch (InvalidRemoteVettingContextException $e) { + $this->logger->error($e->getMessage()); + $flashBag->add('error', 'ss.second_factor.revoke.alert.remote_vetting_invalid_context'); + return $this->redirectToRoute('ss_second_factor_list'); + } + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/SecondFactorController.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/SecondFactorController.php index 338bfd0f9..876de1251 100644 --- a/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/SecondFactorController.php +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/SecondFactorController.php @@ -18,6 +18,7 @@ namespace Surfnet\StepupSelfService\SelfServiceBundle\Controller; +use LogicException; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; use Surfnet\StepupSelfService\SelfServiceBundle\Command\RevokeCommand; use Surfnet\StepupSelfService\SelfServiceBundle\Form\Type\RevokeSecondFactorType; diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Exception/InvalidRemoteVettingContextException.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Exception/InvalidRemoteVettingContextException.php new file mode 100644 index 000000000..9d60ceedf --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Exception/InvalidRemoteVettingContextException.php @@ -0,0 +1,24 @@ +add('valid', ChoiceType::class, [ + 'label' => false, + 'required' => true, + 'choices' => array( + 'ss.form.ss_remote_vet_feedback.yes' => true, + 'ss.form.ss_remote_vet_feedback.no' => false, + ), + 'multiple' => false, + 'expanded' => true, + ]); + + $builder->add('remarks', TextareaType::class, [ + 'label' => false, + 'required' => false, + ]); + + $builder->setDataMapper($this); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => AttributeMatch::class, + 'attr' => ['class' => 'form-inline'], + ]); + } + + public function getBlockPrefix() + { + return 'ss_remote_vet_assertion'; + } + + /** + * @param $viewData AttributeMatch + * @inheritDoc + */ + public function mapDataToForms($viewData, $forms) + { + // there is no data yet, so nothing to prepopulate + if (null === $viewData) { + return; + } + + // invalid data type + if (!$viewData instanceof AttributeMatch) { + throw new UnexpectedTypeException($viewData, AttributeMatch::class); + } + + /** @var FormInterface[] $forms */ + $forms = iterator_to_array($forms); + + // Set VO values to form + $forms['valid']->setData($viewData->isValid()); + $forms['remarks']->setData($viewData->getRemarks()); + } + + /** + * @param $viewData AttributeMatch + * @inheritDoc + */ + public function mapFormsToData($forms, &$viewData) + { + /** @var FormInterface[] $forms */ + $forms = iterator_to_array($forms); + + // Keep the name from the original object + $viewData = new AttributeMatch( + $viewData->getLocalAttribute(), + $viewData->getRemoteAttribute(), + $forms['valid']->getData(), + (string)$forms['remarks']->getData() + ); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php new file mode 100644 index 000000000..999f2af34 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php @@ -0,0 +1,75 @@ +add('used-before', ChoiceType::class, [ + 'label' => 'ss.form.ss_remote_vet_feedback.used-before', + 'required' => true, + 'choices' => array( + 'ss.form.ss_remote_vet_feedback.yes' => 'yes', + 'ss.form.ss_remote_vet_feedback.no' => 'no', + ), + 'multiple' => false, + 'expanded' => true, + ]); + + $builder->add('rating', ChoiceType::class, [ + 'label' => 'ss.form.ss_remote_vet_feedback.rating', + 'required' => true, + 'choices' => array( + '1' => '1', + '2' => '3', + '4' => '4', + '5' => '5', + '6' => '6', + '7' => '7', + '8' => '8', + '9' => '9', + '10' => '10', + ), + 'label_attr' => array('class' => 'checkbox-inline'), + 'multiple' => false, + 'expanded' => true, + ]); + + $builder->add('rating-explanation', TextareaType::class, [ + 'label' => 'ss.form.ss_remote_vet_feedback.rating-explanation', + 'required' => false, + ]); + + $builder->add('remarks', TextareaType::class, [ + 'label' => 'ss.second_factor.remote_vet.remarks', + 'required' => false, + ]); + } + + public function getBlockPrefix() + { + return 'ss_remote_vet_feedback'; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetValidationType.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetValidationType.php new file mode 100644 index 000000000..91f82474c --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetValidationType.php @@ -0,0 +1,74 @@ +add('matches', CollectionType::class, [ + 'label' => false, + // each entry in the array will be an "match" field + 'entry_type' => RemoteVetAssertionMatchType::class, + // these options are passed to each "match" type + 'entry_options' => [ + 'attr' => ['class' => 'assertion_match'], + 'label' => false, + ], + ]); + + $builder->add('feedback', RemoteVetFeedbackType::class, [ + 'label' => 'ss.form.ss_remote_vet_second_factor.feedback', + ]); + + $builder->add('cancel', AnchorType::class, [ + 'label' => 'ss.form.ss_remote_vet_second_factor.cancel', + 'attr' => [ 'class' => 'btn pull-right' ], + 'route' => 'ss_second_factor_list', + ]); + + $builder->add('validate', SubmitType::class, [ + 'label' => 'ss.form.ss_remote_vet_second_factor.validate', + 'attr' => [ 'class' => 'btn btn-primary pull-right' ], + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => RemoteVetValidationCommand::class, + ]); + } + + public function getBlockPrefix() + { + return 'ss_remote_vet_validation'; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Mock/RemoteVetting/MockConfiguration.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Mock/RemoteVetting/MockConfiguration.php new file mode 100644 index 000000000..a9daf2648 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Mock/RemoteVetting/MockConfiguration.php @@ -0,0 +1,106 @@ +identityProviderEntityId = $identityProviderEntityId; + $this->serviceProviderEntityId = $serviceProviderEntityId; + $this->privateKeyPemFile = $privateKeyPath; + $this->publicKeyCertDataFile = $publicCertPath; + $this->privateKeyPem = file_get_contents($privateKeyPath); + $this->publicKeyCertData = file_get_contents($publicCertPath); + } + + /** + * @return string + */ + public function getIdentityProviderEntityId() + { + return $this->identityProviderEntityId; + } + + /** + * @return string + */ + public function getIdentityProviderPublicKeyCertData() + { + return $this->publicKeyCertData; + } + + /** + * @return string + */ + public function getIdentityProviderGetPrivateKeyPem() + { + return $this->privateKeyPem; + } + + /** + * @return string + */ + public function getServiceProviderEntityId() + { + return $this->serviceProviderEntityId; + } + + /** + * @return string + */ + public function getPublicKeyCertDataFile() + { + return $this->publicKeyCertDataFile; + } + + /** + * @return string + */ + public function getPrivateKeyPemFile() + { + return $this->privateKeyPemFile; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Mock/RemoteVetting/MockGateway.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Mock/RemoteVetting/MockGateway.php new file mode 100644 index 000000000..dec7fcbf4 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Mock/RemoteVetting/MockGateway.php @@ -0,0 +1,435 @@ +gatewayConfiguration = $gatewayConfiguration; + $this->currentTime = new DateTime(); + } + + /** + * @param Request $request + * @param string $fullRequestUri + * @return Response + */ + public function handleSsoSuccess(Request $request, $fullRequestUri, array $attributes) + { + // parse the authnRequest + $authnRequest = $this->parseRequest($request, $fullRequestUri); + + // get parameters from authnRequest + $nameId = $authnRequest->getNameId()->value; + $destination = $authnRequest->getAssertionConsumerServiceURL(); + $authnContextClassRef = current($authnRequest->getRequestedAuthnContext()['AuthnContextClassRef']); + $requestId = $authnRequest->getId(); + + // handle success + return $this->createSecondFactorOnlyResponse( + $nameId, + $destination, + $authnContextClassRef, + $requestId, + $attributes + ); + } + + /** + * @param Request $request + * @param string $fullRequestUri + * @param string $status + * @param string $subStatus + * @param string $message + * @return Response + */ + public function handleSsoFailure(Request $request, $fullRequestUri, $status, $subStatus, $message = '') + { + // parse the authnRequest + $authnRequest = $this->parseRequest($request, $fullRequestUri); + + // get parameters from authnRequest + $destination = $authnRequest->getAssertionConsumerServiceURL(); + $requestId = $authnRequest->getId(); + + return $this->createFailureResponse($destination, $requestId, $status, $subStatus, $message); + } + + + /** + * @param string $nameId + * @param string $destination The ACS location + * @param string|null $authnContextClassRef The loa level + * @param string $requestId The requestId + * @param array $attributes All new attributes, as an associative array. + * @return Response + */ + private function createSecondFactorOnlyResponse($nameId, $destination, $authnContextClassRef, $requestId, $attributes) + { + $assertion = $this->createNewAssertion( + $nameId, + $authnContextClassRef, + $destination, + $requestId + ); + + $assertion->setAttributes($attributes); + + return $this->createNewAuthnResponse( + $assertion, + $destination, + $requestId + ); + } + + /** + * @param string $samlRequest + * @param string $fullRequestUri + * @return SAML2AuthnRequest + * @throws \Exception + */ + private function parseRequest(Request $request, $fullRequestUri) + { + // the GET parameter is already urldecoded by Symfony, so we should not do it again. + $requestData = $request->get(self::PARAMETER_REQUEST); + + if (empty($requestData)) { + throw new BadRequestHttpException('Missing a request, did not receive a request or request was empty'); + } + + $samlRequest = base64_decode($requestData, true); + if ($samlRequest === false) { + throw new BadRequestHttpException('Failed decoding the request, did not receive a valid base64 string'); + } + + // Catch any errors gzinflate triggers + $errorNo = $errorMessage = null; + set_error_handler(function ($number, $message) use (&$errorNo, &$errorMessage) { + $errorNo = $number; + $errorMessage = $message; + }); + $samlRequest = gzinflate($samlRequest); + restore_error_handler(); + + if ($samlRequest === false) { + throw new BadRequestHttpException(sprintf( + 'Failed inflating the request; error "%d": "%s"', + $errorNo, + $errorMessage + )); + } + + // 1. Parse to xml object + // additional security against XXE Processing vulnerability + $previous = libxml_disable_entity_loader(true); + $document = DOMDocumentFactory::fromString($samlRequest); + libxml_disable_entity_loader($previous); + + // 2. Parse saml request + $authnRequest = Message::fromXML($document->firstChild); + + if (!$authnRequest instanceof SAML2AuthnRequest) { + throw new RuntimeException(sprintf( + 'The received request is not an AuthnRequest, "%s" received instead', + substr(get_class($authnRequest), strrpos($authnRequest, '_') + 1) + )); + } + + // 3. Validate destination + if (!$authnRequest->getDestination() === $fullRequestUri) { + throw new BadRequestHttpException(sprintf( + 'Actual Destination "%s" does not match the AuthnRequest Destination "%s"', + $fullRequestUri, + $authnRequest->getDestination() + )); + } + + // 4. Validate issuer + if (!$this->gatewayConfiguration->getServiceProviderEntityId() === $authnRequest->getIssuer()) { + throw new BadRequestHttpException(sprintf( + 'Actual issuer "%s" does not match the AuthnRequest Issuer "%s"', + $this->gatewayConfiguration->getServiceProviderEntityId(), + $authnRequest->getIssuer() + )); + } + + // 5. Validate key + $key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, array('type' => 'public')); + $key->loadKey($this->gatewayConfiguration->getIdentityProviderPublicKeyCertData()); + + // The query string to validate needs to be urlencoded again because Symfony has already decoded this for us + $query = self::PARAMETER_REQUEST . '=' . urlencode($requestData); + $query .= '&' . self::PARAMETER_SIGNATURE_ALGORITHM . '=' . urlencode($request->get(self::PARAMETER_SIGNATURE_ALGORITHM)); + + $signature = base64_decode($request->get(self::PARAMETER_SIGNATURE)); + + if (!$key->verifySignature($query, $signature)) { + throw new BadRequestHttpException( + 'Validation of the signature in the AuthnRequest failed' + ); + } + + return $authnRequest; + } + + /** + * @param Response $response + * @return string + */ + public function parsePostResponse(Response $response) + { + return $response->toUnsignedXML()->ownerDocument->saveXML(); + } + + /** + * @param string $destination The ACS location + * @param string $requestId The requestId + * @param string $status The response status (see \SAML2\Constants) + * @param string|null $subStatus An optional substatus (see \SAML2\Constants) + * @param string|null $message The textual message + * @return Response + */ + private function createFailureResponse($destination, $requestId, $status, $subStatus = null, $message = null) + { + $response = new Response(); + $response->setDestination($destination); + $response->setIssuer($this->gatewayConfiguration->getIdentityProviderEntityId()); + $response->setIssueInstant($this->getTimestamp()); + $response->setInResponseTo($requestId); + + + if (!$this->isValidResponseStatus($status)) { + throw new LogicException(sprintf('Trying to set invalid Response Status')); + } + + if ($subStatus && !$this->isValidResponseSubStatus($subStatus)) { + throw new LogicException(sprintf('Trying to set invalid Response SubStatus')); + } + + $status = ['Code' => $status]; + if ($subStatus) { + $status['SubCode'] = $subStatus; + } + if ($message) { + $status['Message'] = $message; + } + + $response->setStatus($status); + + return $response; + } + + /** + * @param Assertion $newAssertion + * @param string $destination The ACS location + * @param string $requestId The requestId + * @return Response + */ + private function createNewAuthnResponse(Assertion $newAssertion, $destination, $requestId) + { + $response = new Response(); + $response->setAssertions([$newAssertion]); + $response->setIssuer($this->gatewayConfiguration->getIdentityProviderEntityId()); + $response->setIssueInstant($this->getTimestamp()); + $response->setDestination($destination); + $response->setInResponseTo($requestId); + + return $response; + } + + /** + * @param string $nameId + * @param string $authnContextClassRef + * @param string $destination The ACS location + * @param string $requestId The requestId + * @return Assertion + */ + private function createNewAssertion($nameId, $authnContextClassRef, $destination, $requestId) + { + $newAssertion = new Assertion(); + $newAssertion->setNotBefore($this->currentTime->getTimestamp()); + $newAssertion->setNotOnOrAfter($this->getTimestamp('PT5M')); + $newAssertion->setIssuer($this->gatewayConfiguration->getIdentityProviderEntityId()); + $newAssertion->setIssueInstant($this->getTimestamp()); + $this->signAssertion($newAssertion); + $this->addSubjectConfirmationFor($newAssertion, $destination, $requestId); + $newAssertion->setNameId([ + 'Format' => Constants::NAMEID_UNSPECIFIED, + 'Value' => $nameId, + ]); + $newAssertion->setValidAudiences([$this->gatewayConfiguration->getServiceProviderEntityId()]); + $this->addAuthenticationStatementTo($newAssertion, $authnContextClassRef); + + return $newAssertion; + } + + /** + * @param Assertion $newAssertion + * @param string $destination The ACS location + * @param string $requestId The requestId + */ + private function addSubjectConfirmationFor(Assertion $newAssertion, $destination, $requestId) + { + $confirmation = new SubjectConfirmation(); + $confirmation->Method = Constants::CM_BEARER; + + $confirmationData = new SubjectConfirmationData(); + $confirmationData->InResponseTo = $requestId; + $confirmationData->Recipient = $destination; + $confirmationData->NotOnOrAfter = $newAssertion->getNotOnOrAfter(); + + $confirmation->SubjectConfirmationData = $confirmationData; + + $newAssertion->setSubjectConfirmation([$confirmation]); + } + + /** + * @param Assertion $assertion + * @param $authnContextClassRef + */ + private function addAuthenticationStatementTo(Assertion $assertion, $authnContextClassRef) + { + $assertion->setAuthnInstant($this->getTimestamp()); + $assertion->setAuthnContextClassRef($authnContextClassRef); + $assertion->setAuthenticatingAuthority([$this->gatewayConfiguration->getIdentityProviderEntityId()]); + } + + /** + * @param string $interval a DateInterval compatible interval to skew the time with + * @return int + */ + private function getTimestamp($interval = null) + { + $time = clone $this->currentTime; + + if ($interval) { + $time->add(new DateInterval($interval)); + } + + return $time->getTimestamp(); + } + + /** + * @param Assertion $assertion + * @return Assertion + */ + private function signAssertion(Assertion $assertion) + { + $assertion->setSignatureKey($this->loadPrivateKey()); + $assertion->setCertificates([$this->getPublicCertificate()]); + + return $assertion; + } + + /** + * @return XMLSecurityKey + */ + private function loadPrivateKey() + { + $xmlSecurityKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'private']); + $xmlSecurityKey->loadKey($this->gatewayConfiguration->getIdentityProviderGetPrivateKeyPem()); + + return $xmlSecurityKey; + } + + /** + * @return string + */ + private function getPublicCertificate() + { + return $this->gatewayConfiguration->getIdentityProviderPublicKeyCertData(); + } + + private function isValidResponseStatus($status) + { + return in_array($status, [ + Constants::STATUS_SUCCESS, // weeee! + Constants::STATUS_REQUESTER, // Something is wrong with the AuthnRequest + Constants::STATUS_RESPONDER, // Something went wrong with the Response + Constants::STATUS_VERSION_MISMATCH, // The version of the request message was incorrect + ]); + } + + private function isValidResponseSubStatus($subStatus) + { + return in_array($subStatus, [ + Constants::STATUS_AUTHN_FAILED, // failed authentication + Constants::STATUS_INVALID_ATTR, + Constants::STATUS_INVALID_NAMEID_POLICY, + Constants::STATUS_NO_AUTHN_CONTEXT, // insufficient Loa or Loa cannot be met + Constants::STATUS_NO_AVAILABLE_IDP, + Constants::STATUS_NO_PASSIVE, + Constants::STATUS_NO_SUPPORTED_IDP, + Constants::STATUS_PARTIAL_LOGOUT, + Constants::STATUS_PROXY_COUNT_EXCEEDED, + Constants::STATUS_REQUEST_DENIED, + Constants::STATUS_REQUEST_UNSUPPORTED, + Constants::STATUS_REQUEST_VERSION_DEPRECATED, + Constants::STATUS_REQUEST_VERSION_TOO_HIGH, + Constants::STATUS_REQUEST_VERSION_TOO_LOW, + Constants::STATUS_RESOURCE_NOT_RECOGNIZED, + Constants::STATUS_TOO_MANY_RESPONSES, + Constants::STATUS_UNKNOWN_ATTR_PROFILE, + Constants::STATUS_UNKNOWN_PRINCIPAL, + Constants::STATUS_UNSUPPORTED_BINDING, + ]); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Mock/RemoteVetting/MockRemoteVetController.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Mock/RemoteVetting/MockRemoteVetController.php new file mode 100644 index 000000000..3ba798d9b --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Mock/RemoteVetting/MockRemoteVetController.php @@ -0,0 +1,227 @@ +mockGateway = $mockGateway; + $this->twig = $twig; + } + + /** + * This is the sso action used to mock a RV IdP callout + * + * @param Request $request + * @return string|Response + */ + public function ssoAction(Request $request) + { + if (!in_array($this->getParameter('kernel.environment'), ['test', 'dev'])) { + throw new Exception('Invalid environment encountered.'); + } + + try { + $status = $request->get('status'); + + // Check binding + if (!$request->isMethod(Request::METHOD_GET) && !$status) { + throw new BadRequestHttpException(sprintf( + 'Could not receive AuthnRequest from HTTP Request: expected a GET method, got %s', + $request->getMethod() + )); + } + + // show possible saml response status to return + if (!$status) { + // Present response + $body = $this->twig->render( + 'dev/mock-acs.html.twig', + [ + 'action' => $request->getUri(), + 'responses' => [ + 'success', + 'user-cancelled', + 'unknown', + ], + ] + ); + return new Response($body); + } + + // Parse available responses + $response = $this->getSelectedResponse($request, $status); + + // Present response + $body = $this->twig->render( + 'dev/mock-acs-post.html.twig', + [ + 'response' => $response, + ] + ); + + return new Response($body); + } catch (BadRequestHttpException $e) { + return new Response($e->getMessage(), $e->getStatusCode()); + } catch (Exception $e) { + return new Response($e->getMessage(), 500); + } + } + + /** + * @param Request $request + * @param string $status + * @return array + */ + private function getSelectedResponse(Request $request, $status) + { + switch (true) { + case ($status == 'success'): + // Parse successful + $rawAttributes = $request->get('attributes'); + $attributes = $this->parseAttributes($rawAttributes); + + $samlResponse = $this->mockGateway->handleSsoSuccess($request, $this->getFullRequestUri($request), $attributes); + return $this->getResponseData($request, $samlResponse); + + case ($status == 'user-cancelled'): + // Parse user cancelled + $samlResponse = $this->mockGateway->handleSsoFailure( + $request, + $this->getFullRequestUri($request), + Constants::STATUS_RESPONDER, + Constants::STATUS_AUTHN_FAILED, + 'Authentication cancelled by user' + ); + return $this->getResponseData($request, $samlResponse); + + case ($status == 'unknown'): + // Parse unknown + $samlResponse = $this->mockGateway->handleSsoFailure( + $request, + $this->getFullRequestUri($request), + Constants::STATUS_RESPONDER, + Constants::STATUS_AUTHN_FAILED + ); + return $this->getResponseData($request, $samlResponse); + default: + throw new BadRequestHttpException(sprintf( + 'Could not create a response for status %s', + $status + )); + } + } + + /** + * @param Request $request + * @param SamlResponse $samlResponse + * @return array + */ + private function getResponseData(Request $request, SamlResponse $samlResponse) + { + $rawResponse = $this->mockGateway->parsePostResponse($samlResponse); + + return [ + 'acu' => $samlResponse->getDestination(), + 'rawResponse' => $rawResponse, + 'encodedResponse' => base64_encode($rawResponse), + 'relayState' => $request->request->get(MockGateway::PARAMETER_RELAY_STATE), + ]; + } + + /** + * @param Request $request + * @return string + */ + private function getFullRequestUri(Request $request) + { + return $request->getSchemeAndHttpHost() . $request->getBasePath() . $request->getPathInfo(); + } + + /** + * @param string $data + * @return array + */ + private function parseAttributes($data) + { + json_decode($data); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new BadRequestHttpException(sprintf( + 'Could not parse the attributes because no valid json was given %s', + $data + )); + } + + $data = json_decode($data, true); + + $result = []; + foreach ($data as $attr) { + if (!array_key_exists('name', $attr)) { + throw new BadRequestHttpException(sprintf( + 'Could not parse the attributes because no valid name was given %s', + json_encode($data) + )); + } + if (!array_key_exists('value', $attr)) { + throw new BadRequestHttpException(sprintf( + 'Could not parse the attributes because no valid value was given %s', + json_encode($data) + )); + } + + if (!is_array($attr['value'])) { + throw new BadRequestHttpException(sprintf( + 'Could not parse the attributes because a value should be an array with strings %s', + json_encode($data) + )); + } + + foreach ($attr['value'] as $value) { + if (!is_string($value)) { + throw new BadRequestHttpException(sprintf( + 'Could not parse the attributes because if a value is an array it should consist of strings %s', + json_encode($data) + )); + } + } + + $result[$attr['name']] = $attr['value']; + } + + return $result; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/config/remote_vetting.yml b/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/config/remote_vetting.yml new file mode 100644 index 000000000..af1328df6 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/config/remote_vetting.yml @@ -0,0 +1,65 @@ +services: + Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\IdentityProviderFactory: + public: true + arguments: + $configuration: '@Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\Configuration\RemoteVettingConfiguration' + + Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\ServiceProviderFactory: + arguments: + $router: '@router' + $entityId: "%remote_vetting_entity_id%" + $assertionConsumerUrlSlug: "ss_second_factor_remote_vet_acs" + $privateKey: "%saml_rv_privatekey%" + + Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\Configuration\RemoteVettingConfiguration: + arguments: + $privateKey: '%saml_rv_privatekey%' + $configurationSettings: '%identity_encryption_configuration%' + $remoteVettingIdpConfig: '%remote_vetting_idps%' + + Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\RemoteVettingContext: + arguments: + $session: '@session' + + Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\SamlCalloutHelper: + $identityProviderFactory: '@Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\IdentityProviderFactory' + $serviceProviderFactory: '@Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\ServiceProviderFactory' + $postBinding: '@surfnet_saml.http.post_binding' + $remoteVettingService: '@Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVettingService' + $logger: '@logger' + + Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\AttributeMapper: + $configuration: '@Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\Configuration\RemoteVettingConfiguration' + + Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVettingService: + public: true + arguments: + $remoteVettingContext: '@Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\RemoteVettingContext' + $attributeMapper: '@Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\AttributeMapper' + $identityEncrypter: '@Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\Encryption\IdentityEncrypter' + $applicationHelper: '@self_service.service.application' + $logger: '@logger' + + Surfnet\StepupSelfService\SelfServiceBundle\Controller\RemoteVettingController: + public: true + arguments: + - '@Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVettingService' + - '@Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\RemoteVettingViewHelper' + - '@surfnet_stepup_self_service_self_service.service.second_factor' + - '@Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\SamlCalloutHelper' + - '@surfnet_stepup.registration_expiration_helper' + - '@logger' + + Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\Encryption\IdentityFilesystemWriter: + arguments: + - '@Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\Configuration\RemoteVettingConfiguration' + + Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\RemoteVettingViewHelper: + public: true + arguments: + $configuration: '@Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\Configuration\RemoteVettingConfiguration' + + Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\Encryption\IdentityEncrypter: + arguments: + $configuration: '@Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\Configuration\RemoteVettingConfiguration' + $writer: '@Surfnet\StepupSelfService\SelfServiceBundle\Service\RemoteVetting\Encryption\IdentityFilesystemWriter' \ No newline at end of file diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/config/routing.yml b/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/config/routing.yml index 24863931d..9f5b973bc 100644 --- a/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/config/routing.yml +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/config/routing.yml @@ -20,6 +20,26 @@ ss_second_factor_revoke: requirements: state: '^(unverified|verified|vetted)$' +ss_second_factor_remote_vetting_types: + path: /second-factor/{secondFactorId}/vetting-types + methods: [GET] + defaults: { _controller: SurfnetStepupSelfServiceSelfServiceBundle:Registration:displayVettingTypes } + +ss_second_factor_remote_vet: + path: /second-factor/{secondFactorId}/remote-vet/{identityProviderSlug} + methods: [GET,POST] + defaults: { _controller: SurfnetStepupSelfServiceSelfServiceBundle:RemoteVetting:remoteVet } + +ss_second_factor_remote_vet_acs: + path: /second-factor/acs + methods: [GET,POST] + defaults: { _controller: SurfnetStepupSelfServiceSelfServiceBundle:RemoteVetting:acs } + +ss_second_factor_remote_vet_match: + path: /second-factor/remote-vetting/match/{processId} + methods: [GET,POST] + defaults: { _controller: SurfnetStepupSelfServiceSelfServiceBundle:RemoteVetting:remoteVetMatch } + ss_registration_display_types: path: /registration/select-token methods: [GET] diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/config/services.yml b/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/config/services.yml index 957cbee6a..1b0752565 100644 --- a/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/config/services.yml +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/config/services.yml @@ -1,3 +1,6 @@ +imports: + - { resource: 'remote_vetting.yml' } + services: # To be compatible with loading services from the container instead of using DI, TODO, Utilize DI throughout the # application. @@ -81,6 +84,11 @@ services: arguments: - "@surfnet_stepup_self_service_self_service.service.command" + self_service.service.application: + class: Surfnet\StepupSelfService\SelfServiceBundle\Service\ApplicationHelper + arguments: + $kernelProjectDir: "%kernel.project_dir%" + self_service.service.identity: class: Surfnet\StepupSelfService\SelfServiceBundle\Service\IdentityService arguments: diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/form/fields.html.twig b/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/form/fields.html.twig index eeecbe714..3d0414a17 100644 --- a/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/form/fields.html.twig +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/form/fields.html.twig @@ -64,3 +64,18 @@ {% endspaceless %} {%- endblock otp_widget -%} + +{# Show value in the remote vetting attribute match. #} +{% block ss_remote_vet_assertion_widget %} + {% spaceless %} + {% set attributeKey = 'ss.second_factor.remote_vet.attribute.' ~ form.vars.data.localAttribute.name %} + + {{ attributeKey |trans }} + {{ form.vars.data.localAttribute.value|join(',
') }} + {{ form.vars.data.remoteAttribute.value|join(',
') }} + {{ form_widget(form.valid) }} + {{ form_widget(form.remarks) }} + + {% endspaceless %} +{% endblock %} + diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig b/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig new file mode 100644 index 000000000..aac43e855 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig @@ -0,0 +1,61 @@ +{% extends "base.html.twig" %} + +{% block page_title %}{{ 'ss.registration.vetting_type.title'|trans }}{% endblock %} + +{% block page_header %} + {{ parent() }} + + {% if verifyEmail %} + {% include 'SurfnetStepupSelfServiceSelfServiceBundle:registration/partial:progress_bar.html.twig' with {'progress':75, 'step': 4, verifyEmail: true} only %} + {% else %} + {% include 'SurfnetStepupSelfServiceSelfServiceBundle:registration/partial:progress_bar.html.twig' with {'progress': 65, 'step': 3, verifyEmail: false} only %} + {% endif %} +{% endblock %} + +{% block content %} +

{{ block('page_title') }}

+ + +
+
+

{{ 'ss.registration.vetting_type.description.vetting'|trans }}

+
+
+ +
+ +
+
+ {{ ('ss.registration.selector.on-premise.alt')|trans }} +

{{ 'ss.registration.vetting_type.title.ra_vetting'|trans }}

+
+

{{ 'ss.registration.vetting_type.description.ra_vetting'|trans }}

+

+

+ +

+
+
+
+ + {% for identityProvider in identityProviders %} +
+
+ Logo of {{ identityProvider.name }} +

{{ identityProvider.name }}

+
+

+ {{ identityProvider.description(app.request.locale) }} +

+

+

+ +
+

+
+
+ {% endfor %} + +
+{% endblock %} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig b/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig new file mode 100644 index 000000000..1f101343a --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig @@ -0,0 +1,81 @@ +{% extends "base.html.twig" %} + +{% block page_title %}{{ 'ss.second_factor.remote_vet_validation.title'|trans }}{% endblock %} + +{% form_theme form _self %} + +{#Add basic layout for this form#} +{%- block form_row -%} +
+
+ {{- form_label(form) -}} +
+
+ {{- form_help(form) -}} + {{- form_errors(form) -}} + {{- form_widget(form) -}} +
+
+{%- endblock form_row -%} + +{#Add layout for choice widget (radio/checkbox collections)#} + {% block choice_widget %} + {% for child in form.children %} + {{- form_widget(child) -}} + {{- form_label(child, null) -}} + {% endfor %} + {% endblock choice_widget %} + +{% block content %} + +

{{ block('page_title') }}

+ + {% set organizationKey = 'ss.second_factor.remote_vet.schachome.' ~ identity.institution %} + {% set organization = organizationKey | trans %} + +

{{ 'ss.second_factor.remote_vet.text.does_it_match'|trans({ + '%organization%': organization, + '%provider%': provider.name, + }) | nl2br }}

+ + {{ form_start(form, {'attr':{'class': 'form-horizontal rv-form'}}) }} + {{ form_errors(form) }} +
+
+

{{ 'ss.second_factor.remote_vet.personal_data'|trans }}

+ + + + + + + + + + + + + {{ form_widget(form.matches) }} + +
{{ 'ss.second_factor.remote_vet.attribute_name'|trans }}{{ organization }}{{ provider.name }}{{ 'ss.second_factor.remote_vet.is_attribute_valid'|trans }}{{ 'ss.second_factor.remote_vet.remarks'|trans }}
+
+ + +
+

{{ 'ss.second_factor.remote_vet.feedback'|trans }}

+ + {{ form_widget(form.feedback) }} + +
+ + {{ form_end(form) }} +
+ + + +{% endblock %} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig b/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig index 0159a1ed6..91ddacb43 100644 --- a/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig @@ -66,8 +66,14 @@ {% endif %} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig b/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig index 36de0b24c..e74cbb180 100644 --- a/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig @@ -40,3 +40,15 @@ {# SamlController flash messages #} {{ 'ss.test_second_factor.verification_successful'|trans }} {{ 'ss.test_second_factor.verification_failed'|trans }} + +{# RemoteVettingController flash messages #} +{{ 'ss.second_factor.revoke.alert.remote_vetting_successful'|trans }} +{{ 'ss.second_factor.revoke.alert.remote_vetting_failed'|trans }} +{{ 'ss.second_factor.revoke.alert.remote_vetting_invalid_context'|trans }} + +{# RemoteVetting Schachomes #} +{{ 'ss.second_factor.remote_vet.schachome.institution-d.example.com'|trans }} + +{# RemoteVetting attributes #} +{{ 'ss.second_factor.remote_vet.attribute.givenName'|trans }} +{{ 'ss.second_factor.remote_vet.attribute.surname'|trans }} \ No newline at end of file diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Security/Authentication/Provider/SamlProvider.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Security/Authentication/Provider/SamlProvider.php index 1918d6394..b5779c5f4 100644 --- a/src/Surfnet/StepupSelfService/SelfServiceBundle/Security/Authentication/Provider/SamlProvider.php +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Security/Authentication/Provider/SamlProvider.php @@ -100,6 +100,7 @@ public function authenticate(TokenInterface $token) $authenticatedToken = new SamlToken(['ROLE_USER']); $authenticatedToken->setUser($identity); + $authenticatedToken->setAttribute(SamlToken::ATTRIBUTE_SET, $translatedAssertion->getAttributeSet()); return $authenticatedToken; } diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Security/Authentication/Token/SamlToken.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Security/Authentication/Token/SamlToken.php index d523b12e9..38449e8e8 100644 --- a/src/Surfnet/StepupSelfService/SelfServiceBundle/Security/Authentication/Token/SamlToken.php +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Security/Authentication/Token/SamlToken.php @@ -22,6 +22,8 @@ class SamlToken extends AbstractToken { + const ATTRIBUTE_SET = 'attribute_set'; + /** * @var \SAML2\Assertion */ diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/ApplicationHelper.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/ApplicationHelper.php new file mode 100644 index 000000000..ad0a03250 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/ApplicationHelper.php @@ -0,0 +1,50 @@ +kernelProjectDir = $kernelProjectDir; + } + + /** + * In stepup application, the installation path includes the software version. This is the version that can be + * read on the `/info` endpoint. + * + * For development builds, this will probably simply be Stepup-SelfService + * + * @return string + */ + public function getApplicationVersion() + { + // The buildPath (version string) is the installation directory of the project. And is derived from the + // kernel.project_dir (which is the app folder). + return basename(realpath($this->kernelProjectDir)); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/InstitutionConfigurationOptionsService.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/InstitutionConfigurationOptionsService.php index cadd7dbc9..577a54ccd 100644 --- a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/InstitutionConfigurationOptionsService.php +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/InstitutionConfigurationOptionsService.php @@ -21,7 +21,7 @@ use Surfnet\StepupMiddlewareClientBundle\Configuration\Dto\InstitutionConfigurationOptions; use Surfnet\StepupMiddlewareClientBundle\Configuration\Service\InstitutionConfigurationOptionsService as ApiInstitutionConfigurationOptionsService; -final class InstitutionConfigurationOptionsService +class InstitutionConfigurationOptionsService { /** * @var ApiInstitutionConfigurationOptionsService diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/AttributeMapper.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/AttributeMapper.php new file mode 100644 index 000000000..3a09069b1 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/AttributeMapper.php @@ -0,0 +1,87 @@ +configuration = $configuration; + } + + /** + * @param string $identityProviderName + * @param AttributeListDto $localAttributes + * @param AttributeListDto $remoteAttributes + * @return AttributeMatchCollection + * @throws InvalidRemoteVettingMappingException + */ + public function map($identityProviderName, AttributeListDto $localAttributes, AttributeListDto $remoteAttributes) + { + $attributeMapping = $this->configuration->getAttributeMapping($identityProviderName); + + $localMap = $this->attributeListToMap($localAttributes); + $remoteMap = $this->attributeListToMap($remoteAttributes); + + $matchCollection = new AttributeMatchCollection(); + foreach ($attributeMapping as $localName => $remoteName) { + if (!array_key_exists($localName, $localMap)) { + throw new InvalidRemoteVettingMappingException(sprintf( + 'Invalid remote vetting attribute mapping, local attribute with name "%s" not found', + $localName + )); + } + + if (!array_key_exists($remoteName, $remoteMap)) { + throw new InvalidRemoteVettingMappingException(sprintf( + 'Invalid remote vetting attribute mapping, remote attribute with name "%s" not found', + $remoteName + )); + } + + $attributeMatch = new AttributeMatch($localMap[$localName], $remoteMap[$remoteName], false, ''); + $matchCollection->add($localName, $attributeMatch); + }; + + return $matchCollection; + } + + /** + * @param AttributeListDto $attributeList + * @return array + */ + private function attributeListToMap(AttributeListDto $attributeList) + { + $result = []; + foreach ($attributeList->getAttributeCollection() as $attribute) { + $result[$attribute->getName()] = $attribute; + } + return $result; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Configuration/RemoteVettingConfiguration.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Configuration/RemoteVettingConfiguration.php new file mode 100644 index 000000000..b28c826ba --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Configuration/RemoteVettingConfiguration.php @@ -0,0 +1,99 @@ +publicKey = $configurationSettings['encryption_public_key']; + $this->location = $configurationSettings['storage_location']; + + foreach ($remoteVettingIdpConfig as $idpConfig) { + $idpConfig['privateKey'] = $privateKey; + + $idp = RemoteVettingIdenityProviderDto::create($idpConfig); + $this->idps[$idp->getSlug()] = $idp; + + Assert::keyExists($idpConfig, 'attributeMapping', sprintf('attributeMapping should be set: %s', $idp->getSlug())); + Assert::isArray($idpConfig['attributeMapping'], 'attributeMapping should be an array'); + Assert::allString($idpConfig['attributeMapping'], 'attributeMapping should consist of strings'); + + $this->attributeMapping[$idp->getSlug()] = $idpConfig['attributeMapping']; + } + } + + public function getLocation() + { + return $this->location; + } + + public function getPublicKey() + { + return $this->publicKey; + } + + /** + * @return RemoteVettingIdenityProviderDto[] + */ + public function getRemoteVettingIdps(): array + { + return $this->idps; + } + + public function getRemoteVettingIdp(string $name): RemoteVettingIdenityProviderDto + { + if (array_key_exists($name, $this->idps)) { + return $this->idps[$name]; + } + + throw new InvalidRemoteVettingIdentityProviderException(sprintf("Invalid IdP requested '%s'", $name)); + } + + /** + * @param string + * @return array + */ + public function getAttributeMapping($name) + { + if (array_key_exists($name, $this->attributeMapping)) { + return $this->attributeMapping[$name]; + } + + throw new InvalidRemoteVettingIdentityProviderException(sprintf("Invalid IdP requested '%s'", $name)); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Dto/AttributeListDto.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Dto/AttributeListDto.php new file mode 100644 index 000000000..7aa217953 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Dto/AttributeListDto.php @@ -0,0 +1,147 @@ +attributes = new AttributeCollection($attributes); + $this->nameId = $nameId; + } + + /** + * @param AttributeSet $attributeSet + * @return AttributeListDto + */ + public static function fromAttributeSet(AttributeSet $attributeSet) + { + $attributes = []; + $nameID = ''; + /** @var SAMLAttribute $attribute */ + foreach ($attributeSet as $attribute) { + $name = $attribute->getAttributeDefinition()->getName(); + $values = $attribute->getValue(); + foreach ($values as $value) { + if ($value instanceof NameID) { + $nameID = (string)$value->value; + continue; + } + + $attributes[$name] = $values; + } + } + + return new self($attributes, $nameID); + } + + /** + * @param string $serialized + * @return AttributeListDto + */ + public static function deserialize($serialized) + { + $instance = new self([], ''); + $instance->unserialize($serialized); + return $instance; + } + + /** + * @return AttributeListDto + */ + public static function notSet() + { + return new self([], ''); + } + + /** + * @return AttributeCollection|Attribute[] + */ + public function getAttributeCollection() + { + return $this->attributes; + } + + /** + * @inheritDoc + */ + public function serialize() + { + return json_encode($this->getAttributes()); + } + + /** + * @inheritDoc + */ + public function unserialize($serialized) + { + $data = json_decode($serialized, true); + + $this->nameId = $data['nameId']; + $this->attributes = new AttributeCollection($data['attributes']); + } + + public function getAttributes() + { + $attributes = []; + foreach ($this->attributes as $item) { + $attributes[$item->getName()] = $item->getValue(); + } + + return [ + 'nameId' => $this->nameId, + 'attributes' => $attributes, + ]; + } + + /** + * @return string + */ + public function getNameId() + { + return $this->nameId; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Dto/RemoteVettingIdenityProviderDto.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Dto/RemoteVettingIdenityProviderDto.php new file mode 100644 index 000000000..a4e75dc07 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Dto/RemoteVettingIdenityProviderDto.php @@ -0,0 +1,168 @@ +name = $configData['name']; + $identityProvider->descriptions = $configData['description']; + $identityProvider->logo = $configData['logo']; + $identityProvider->slug = $configData['slug']; + + $identityProvider->entityId = $configData['entityId']; + $identityProvider->ssoUrl = $configData['ssoUrl']; + $identityProvider->privateKey = $configData['privateKey']; + $identityProvider->certificateFile = $configData['certificateFile']; + + return $identityProvider; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string[] + */ + public function getDescription($lang) + { + Assert::keyExists($this->descriptions, $lang); + return $this->descriptions[$lang]; + } + + /** + * @return string + */ + public function getLogo() + { + return $this->logo; + } + + /** + * @return string + */ + public function getSlug() + { + return $this->slug; + } + + /** + * @return string + */ + public function getSsoUrl(): string + { + return $this->ssoUrl; + } + + /** + * @return string + */ + public function getPrivateKey(): string + { + return $this->privateKey; + } + + /** + * @return string + */ + public function getCertificateFile(): string + { + return $this->certificateFile; + } + + /** + * @return string + */ + public function getEntityId(): string + { + return $this->entityId; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Dto/RemoteVettingProcessDto.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Dto/RemoteVettingProcessDto.php new file mode 100644 index 000000000..4924c1292 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Dto/RemoteVettingProcessDto.php @@ -0,0 +1,198 @@ +unserialize($serialized); + return $instance; + } + + /** + * @param RemoteVettingProcessDto $process + * @param RemoteVettingState $state + * @return RemoteVettingProcessDto + */ + public static function updateState(RemoteVettingProcessDto $process, RemoteVettingState $state) + { + return new self($process->getProcessId(), $process->getToken(), $state, $process->getAttributes(), $process->getIdentityProviderName()); + } + + /** + * @param ProcessId $processId + * @param RemoteVettingTokenDto $token + * @param RemoteVettingState $state + * @param AttributeListDto $attributes + * @param string $identityProviderName + * @throws \Assert\AssertionFailedException + */ + private function __construct( + ProcessId $processId, + RemoteVettingTokenDto $token, + RemoteVettingState $state, + AttributeListDto $attributes, + $identityProviderName + ) { + Assert::string($identityProviderName, 'The $identityProviderName in an RemoteVettingProcessDto must be a string value'); + + $this->processId = $processId; + $this->token = $token; + $this->state = $state; + $this->attributes = $attributes; + $this->identityProviderName = $identityProviderName; + } + + /** + * @return ProcessId + */ + public function getProcessId() + { + return $this->processId; + } + + /** + * @return RemoteVettingTokenDto + */ + public function getToken() + { + return $this->token; + } + + /** + * @return RemoteVettingState + */ + public function getState() + { + return $this->state; + } + + /** + * @return AttributeListDto + */ + public function getAttributes() + { + return $this->attributes; + } + + /** + * @param AttributeListDto $attributes + */ + public function setAttributes($attributes) + { + $this->attributes = $attributes; + } + + /** + * @return string + */ + public function getIdentityProviderName() + { + return $this->identityProviderName; + } + + /** + * @inheritDoc + */ + public function serialize() + { + $stateClass = !is_null($this->state) ? get_class($this->state) : null; + + $data = [ + 'processId' => json_encode($this->processId->getProcessId()), + 'token' => $this->token->serialize(), + 'state' => json_encode($stateClass), + 'attributes' => $this->attributes->serialize(), + 'identityProvider' => json_encode($this->identityProviderName), + ]; + + $params = []; + foreach ($data as $key => $value) { + $params[] = json_encode($key).":{$value}"; + } + + return '{'.implode(',', $params).'}'; + } + + /** + * @inheritDoc + */ + public function unserialize($serialized) + { + $data = json_decode($serialized, true); + + $stateClass = !is_null($data['state']) ? new $data['state']() : null; + + $this->processId = ProcessId::create($data['processId']); + $this->token = RemoteVettingTokenDto::deserialize(json_encode($data['token'])); + $this->state = $stateClass; + $this->attributes = AttributeListDto::deserialize(json_encode($data['attributes'])); + $this->identityProviderName = $data['identityProvider']; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Dto/RemoteVettingTokenDto.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Dto/RemoteVettingTokenDto.php new file mode 100644 index 000000000..043b873ed --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Dto/RemoteVettingTokenDto.php @@ -0,0 +1,109 @@ +unserialize($serialized); + return $instance; + } + + /** + * @param string $identityId + * @param string $secondFactorId + */ + private function __construct($identityId, $secondFactorId) + { + $this->identityId = $identityId; + $this->secondFactorId = $secondFactorId; + } + + /** + * @return string + */ + public function getIdentityId() + { + return $this->identityId; + } + + /** + * @return string + */ + public function getSecondFactorId() + { + return $this->secondFactorId; + } + + /** + * @inheritDoc + */ + public function serialize() + { + return json_encode([ + 'identityId' => $this->identityId, + 'secondFactorId' => $this->secondFactorId, + ]); + } + + /** + * @inheritDoc + */ + public function unserialize($serialized) + { + $data = json_decode($serialized, true); + + $this->identityId = $data['identityId']; + $this->secondFactorId = $data['secondFactorId']; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityData.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityData.php new file mode 100644 index 000000000..2ba71a39b --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityData.php @@ -0,0 +1,92 @@ +attributeCollectionAggregate = $attributeCollectionAggregate; + $this->nameId = $nameId; + $this->applicationVersion = $applicationVersion; + $this->institution = $institution; + $this->remoteVettingSource = $remoteVettingSource; + } + + public function serialize() + { + return json_encode( + [ + 'attribute-data' => $this->attributeCollectionAggregate->getAttributes(), + 'name-id' => $this->nameId, + 'institution' => $this->institution, + 'remote-vetting-source' => $this->remoteVettingSource, + 'application-version' => $this->applicationVersion, + 'time' => DateTime::now()->format('c'), + ] + ); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityEncrypter.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityEncrypter.php new file mode 100644 index 000000000..6d8b5a7de --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityEncrypter.php @@ -0,0 +1,117 @@ +configuration = $configuration; + $this->writer = $writer; + } + + public function encrypt($data) + { + $rsaPublicKey = $this->configuration->getPublicKey(); + + if (!is_string($data) || !is_string($rsaPublicKey)) { + // Invalid argument + throw new InvalidArgumentException('Invalid input was provided to the encrypt method'); + } + + // Use AES-256 in GCM + $symmetricAlgorithm = 'aes-256-gcm'; + + // Generate initialisation vector for the symmetric encryption algorithm + $ivLength = openssl_cipher_iv_length($symmetricAlgorithm); + if (false === $ivLength) { + // Error generating key + throw new InvalidArgumentException( + 'Unable to generate an initialization vector (iv) based on the selected symmetric encryption algorithm' + ); + } + + $iv = openssl_random_pseudo_bytes($ivLength); + if (false === $iv) { + // Error generating key + throw new InvalidArgumentException('Unable to generate a correct initialization vector (iv)'); + } + + // Generate a 256 bits AES key + $secretKey = openssl_random_pseudo_bytes(256 / 8); + if (false === $secretKey) { + // Error generating key + throw new InvalidArgumentException('Unable to generate the secret key'); + } + + // Encrypt the data + $tag = ''; + $ciphertext = openssl_encrypt($data, $symmetricAlgorithm, $secretKey, 0, $iv, $tag); + if (false === $ciphertext) { + // Encryption failed + throw new InvalidArgumentException( + sprintf('Unable to encrypt the data, ssl error: "%s"', openssl_error_string()) + ); + } + + // Encrypt symmetric key + $rsaPublicKeyHandle = openssl_pkey_get_public($rsaPublicKey); + if (false === $rsaPublicKeyHandle) { + // Reading RSA public key failed + throw new InvalidArgumentException('Reading RSA public key failed'); + } + $encryptedKey = ''; + + $res = openssl_public_encrypt($secretKey, $encryptedKey, $rsaPublicKeyHandle, OPENSSL_PKCS1_OAEP_PADDING); + if (false === $res) { + // Key encryption failed + openssl_pkey_free($rsaPublicKeyHandle); + throw new InvalidArgumentException('Key encryption failed'); + } + + openssl_pkey_free($rsaPublicKeyHandle); + $output = json_encode( + [ + 'algorithm' => $symmetricAlgorithm, + 'iv' => base64_encode($iv), + 'tag' => base64_encode($tag), + 'ciphertext' => base64_encode($ciphertext), + 'encrypted_key' => base64_encode($encryptedKey), + ] + ); + + $this->writer->write($output); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityEncrypterInterface.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityEncrypterInterface.php new file mode 100644 index 000000000..1439cd8de --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityEncrypterInterface.php @@ -0,0 +1,34 @@ +configuration = $configuration; + } + + /** + * Writes identity data to a data store. The data should be passed as a string. + * + * @param string $data + */ + public function write($data) + { + $fileName = $this->createFileName($this->configuration->getLocation()); + file_put_contents($fileName, $data."\n"); + } + + private function createFileName($location) + { + $fileName = uniqid('identity_', true); + return "{$location}/{$fileName}"; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityWriterInterface.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityWriterInterface.php new file mode 100644 index 000000000..ba082a99b --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Encryption/IdentityWriterInterface.php @@ -0,0 +1,29 @@ +getRemoteVettingIdps() as $idp) { + Assert::file($idp->getPrivateKey(), 'privateKey should be a file'); + Assert::file($idp->getCertificateFile(), 'certificateFile should be a file'); + + $idpConfiguration = [ + 'name' => $idp->getName(), + 'entityId' => $idp->getEntityId(), + 'ssoUrl' => $idp->getSsoUrl(), + 'certificateFile' => $idp->getCertificateFile(), + 'privateKeys' => [new PrivateKey($idp->getPrivateKey(), PrivateKey::NAME_DEFAULT)], + ]; + + // set idp + $this->identityProviders[$idp->getSlug()] = new IdentityProvider($idpConfiguration); + } + } + + /** + * @param string $name + * @return IdentityProvider + */ + public function create($name) + { + if (array_key_exists($name, $this->identityProviders)) { + return $this->identityProviders[$name]; + } + + throw new InvalidRemoteVettingIdentityProviderException(sprintf("Invalid IdP requested '%s'", $name)); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/RemoteVettingContext.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/RemoteVettingContext.php new file mode 100644 index 000000000..abcc30edd --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/RemoteVettingContext.php @@ -0,0 +1,170 @@ +session = $session; + $this->state = new RemoteVettingStateInitialised(); + } + + /** + * Do not use this method directly, this method is used to control state in the RemoteVettingState implementations. + * This is done in order to comply with the state machine design pattern. + * + * @param RemoteVettingState $newState + */ + public function setState(RemoteVettingState $newState) + { + $this->state = $newState; + } + + /** + * @param string $identityProviderName + * @param RemoteVettingTokenDto $token + */ + public function initialize($identityProviderName, RemoteVettingTokenDto $token) + { + $process = $this->state->handleInitialise($this, $identityProviderName, $token); + $this->saveProcess($process); + } + + /** + * @param ProcessId $processId + */ + public function validating(ProcessId $processId) + { + $process = $this->loadProcess(); + $process = $this->state->handleValidating($this, $process, $processId); + $this->saveProcess($process); + } + + /** + * @param ProcessId $processId + * @param AttributeListDto $xexternalAttributes + */ + public function validated(ProcessId $processId, AttributeListDto $xexternalAttributes) + { + $process = $this->loadProcess(); + $process = $this->state->handleValidated($this, $process, $processId, $xexternalAttributes); + $this->saveProcess($process); + } + + /** + * @param ProcessId $processId + * @return RemoteVettingProcessDto + */ + public function done(ProcessId $processId) + { + $process = $this->loadProcess(); + $token = $this->state->handleDone($this, $process, $processId); + $this->saveProcess($process); + return $token; + } + + /** + * @return RemoteVettingTokenDto + */ + public function getValidatedToken() + { + $process = $this->loadProcess(); + return $this->state->getValidatedToken($process); + } + + /** + * @return AttributeListDto + */ + public function getAttributes() + { + $process = $this->loadProcess(); + return $this->state->getAttributes($process); + } + + /** + * @return string + */ + public function getIdentityProviderSlug() + { + $process = $this->loadProcess(); + return $process->getIdentityProviderName(); + } + + /** + * @return string + */ + public function getTokenId() + { + $process = $this->loadProcess(); + return $process->getToken()->getSecondFactorId(); + } + + /** + * @return RemoteVettingProcessDto + */ + private function loadProcess() + { + // get active process + $serialized = $this->session->get(self::SESSION_KEY, null); + if ($serialized == null) { + throw new InvalidRemoteVettingContextException('No remote vetting process found'); + } + + $process = RemoteVettingProcessDto::deserialize($serialized); + + // update state from session + $this->state = $process->getState(); + + return $process; + } + + /** + * @param RemoteVettingProcessDto $process + * @return void + */ + private function saveProcess(RemoteVettingProcessDto $process) + { + // save state in session + $process = RemoteVettingProcessDto::updateState($process, $this->state); + $this->session->set(self::SESSION_KEY, $process->serialize()); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/RemoteVettingViewHelper.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/RemoteVettingViewHelper.php new file mode 100644 index 000000000..2a642b803 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/RemoteVettingViewHelper.php @@ -0,0 +1,47 @@ +configuration = $configuration; + } + + public function getIdentityProvider(string $slug): RemoteVettingIdenityProviderDto + { + return $this->configuration->getRemoteVettingIdp($slug); + } + + /** + * @return RemoteVettingIdenityProviderDto[] + */ + public function getIdentityProviders(): array + { + return $this->configuration->getRemoteVettingIdps(); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/SamlCalloutHelper.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/SamlCalloutHelper.php new file mode 100644 index 000000000..629e78b0b --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/SamlCalloutHelper.php @@ -0,0 +1,130 @@ +identityProviderFactory = $identityProviderFactory; + $this->serviceProvider = $serviceProviderFactory->create(); + $this->postBinding = $postBinding; + $this->remoteVettingService = $remoteVettingService; + $this->logger = $logger; + } + + /** + * @param string $identityProviderSlug + * @return string + */ + public function createAuthnRequest($identityProviderSlug) + { + $this->logger->info(sprintf('Creating a SAML2 AuthnRequest to send to the %s remote vetting IdP', $identityProviderSlug)); + + $identityProvider = $this->identityProviderFactory->create($identityProviderSlug); + $authnRequest = AuthnRequestFactory::createNewRequest($this->serviceProvider, $identityProvider); + + // Set NameId + $authnRequest->setSubject('', Constants::NAMEID_UNSPECIFIED); + + // Set AuthnContextClassRef + $authnRequest->setAuthenticationContextClassRef(Constants::AC_UNSPECIFIED); + + // Handle validating state + $this->remoteVettingService->startValidation(ProcessId::create($authnRequest->getRequestId())); + + // Create redirect response. + $query = $authnRequest->buildRequestQuery(); + + return sprintf( + '%s?%s', + $identityProvider->getSsoUrl(), + $query + ); + } + + /** + * @param Request $request + * @param string $identityProviderSlug + * @return ProcessId + */ + public function handleResponse(Request $request, $identityProviderSlug) + { + $identityProvider = $this->identityProviderFactory->create($identityProviderSlug); + + $this->logger->info(sprintf('Process the SAML Respons received from the %s remote vetting IdP', $identityProviderSlug)); + + $assertion = $this->postBinding->processResponse( + $request, + $identityProvider, + $this->serviceProvider + ); + + /** @var SubjectConfirmation $subjectConfirmation */ + $subjectConfirmation = $assertion->getSubjectConfirmation()[0]; + $requestId = $subjectConfirmation->SubjectConfirmationData->InResponseTo; + + // Create log DTO in order to store + $attributeLogDto = new AttributeListDto( + $assertion->getAttributes(), + (string)$subjectConfirmation->NameID + ); + + // Handle validated state + $processId = ProcessId::create($requestId); + $this->remoteVettingService->finishValidation($processId, $attributeLogDto); + return $processId; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/ServiceProviderFactory.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/ServiceProviderFactory.php new file mode 100644 index 000000000..d033c4487 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/ServiceProviderFactory.php @@ -0,0 +1,62 @@ +generate($assertionConsumerUrlSlug, [], UrlGeneratorInterface::ABSOLUTE_URL); + $configuration['privateKey'] = $privateKey; + + Assert::url($configuration['entityId']); + Assert::url($configuration['assertionConsumerUrl']); + Assert::file($configuration['privateKey']); + + $configuration['privateKeys'] = [new PrivateKey($configuration['privateKey'], PrivateKey::NAME_DEFAULT)]; + unset($configuration['privateKey']); + + $this->serviceProvider = new ServiceProvider($configuration); + } + + /** + * @return ServiceProvider + */ + public function create() + { + return $this->serviceProvider; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/AbstractRemoteVettingState.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/AbstractRemoteVettingState.php new file mode 100644 index 000000000..a765a0ec9 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/AbstractRemoteVettingState.php @@ -0,0 +1,66 @@ +getToken(); + } + + public function getAttributes(RemoteVettingProcessDto $process) + { + return $process->getAttributes(); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/RemoteVettingStateInitialised.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/RemoteVettingStateInitialised.php new file mode 100644 index 000000000..2feba9bef --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/RemoteVettingStateInitialised.php @@ -0,0 +1,39 @@ +setState(new RemoteVettingStateValidating()); + + return RemoteVettingProcessDto::create($id, $process->getToken(), $process->getIdentityProviderName()); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/RemoteVettingStateValidated.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/RemoteVettingStateValidated.php new file mode 100644 index 000000000..358927688 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/RemoteVettingStateValidated.php @@ -0,0 +1,36 @@ +setState(new RemoteVettingStateDone()); + return $process; + } + + public function getAttributes(RemoteVettingProcessDto $process) + { + return $process->getAttributes(); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/RemoteVettingStateValidating.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/RemoteVettingStateValidating.php new file mode 100644 index 000000000..2fb04017f --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/State/RemoteVettingStateValidating.php @@ -0,0 +1,45 @@ +getProcessId()->isValid($id)) { + throw new InvalidRemoteVettingContextException('Invalid remote vetting context found'); + } + + $process->setAttributes($externalAttributes); + + $context->setState(new RemoteVettingStateValidated()); + + return $process; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/Attribute.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/Attribute.php new file mode 100644 index 000000000..2595b0ea7 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/Attribute.php @@ -0,0 +1,67 @@ +name = $name; + $this->value = array_values($value); + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string[] + */ + public function getValue() + { + return $this->value; + } + + public function jsonSerialize() + { + return ['name' => $this->name, 'value' => $this->value]; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeCollection.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeCollection.php new file mode 100644 index 000000000..bf60329ba --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeCollection.php @@ -0,0 +1,61 @@ + $attributeValue) { + $this->add(new Attribute($attributeName, $attributeValue)); + } + } + + private function add(Attribute $attribute) + { + // Attributes are not + $this->attributes[] = $attribute; + } + + public function getIterator() + { + return new ArrayIterator($this->attributes); + } + + public function getAttributes() + { + $output = []; + foreach ($this->attributes as $attribute) { + $output[$attribute->getName()] = $attribute; + } + return $output; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeCollectionAggregate.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeCollectionAggregate.php new file mode 100644 index 000000000..8d35ff59f --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeCollectionAggregate.php @@ -0,0 +1,56 @@ +attributeCollections, + $name, + sprintf('The collection named: "%s" is already present in the collection', $name) + ); + $this->attributeCollections[$name] = $collection; + } + + /** + * @return AttributeListDto[] + */ + public function getAttributes() + { + $output = []; + foreach ($this->attributeCollections as $name => $collection) { + $output[$name] = $collection->getAttributes(); + } + return $output; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeCollectionInterface.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeCollectionInterface.php new file mode 100644 index 000000000..4020369d2 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeCollectionInterface.php @@ -0,0 +1,30 @@ +localAttribute = $localAttribute; + $this->remoteAttribute = $remoteAttribute; + $this->valid = $valid; + $this->remarks = $remarks; + } + + /** + * @return bool + */ + public function isValid() + { + return $this->valid; + } + + /** + * @return string + */ + public function getRemarks() + { + return $this->remarks; + } + + /** + * @return Attribute + */ + public function getLocalAttribute() + { + return $this->localAttribute; + } + + /** + * @return Attribute + */ + public function getRemoteAttribute() + { + return $this->remoteAttribute; + } + + public function jsonSerialize() + { + return ['local' => $this->localAttribute, 'remote' => $this->remoteAttribute, 'is-valid' => $this->valid, 'remarks' => $this->remarks]; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeMatchCollection.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeMatchCollection.php new file mode 100644 index 000000000..04216b845 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/AttributeMatchCollection.php @@ -0,0 +1,74 @@ +matches[$key] = $attributeMatch; + } + + public function getIterator() + { + return new ArrayIterator($this->matches); + } + + public function offsetSet($offset, $value) + { + if (is_null($offset)) { + $this->matches[] = $value; + } else { + $this->matches[$offset] = $value; + } + } + + public function offsetExists($offset) + { + return isset($this->matches[$offset]); + } + + public function offsetUnset($offset) + { + unset($this->matches[$offset]); + } + + public function offsetGet($offset) + { + return isset($this->matches[$offset]) ? $this->matches[$offset] : null; + } + + public function getAttributes() + { + return $this->matches; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/FeedbackCollection.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/FeedbackCollection.php new file mode 100644 index 000000000..99cc6e7d7 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/FeedbackCollection.php @@ -0,0 +1,66 @@ +feedback); + } + + public function offsetSet($offset, $value) + { + if (is_null($offset)) { + $this->feedback[] = $value; + } else { + $this->feedback[$offset] = $value; + } + } + + public function offsetExists($offset) + { + return isset($this->feedback[$offset]); + } + + public function offsetUnset($offset) + { + unset($this->feedback[$offset]); + } + + public function offsetGet($offset) + { + return isset($this->feedback[$offset]) ? $this->feedback[$offset] : null; + } + + public function getAttributes() + { + return $this->feedback; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/ProcessId.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/ProcessId.php new file mode 100644 index 000000000..42343e4f4 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVetting/Value/ProcessId.php @@ -0,0 +1,65 @@ +processId = $processId; + } + + /** + * @return mixed + */ + public function getProcessId() + { + return $this->processId; + } + + public function isValid(ProcessId $id) + { + if (empty($this->processId)) { + return false; + } + return $this->processId == $id->getProcessId(); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVettingService.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVettingService.php new file mode 100644 index 000000000..b61e8e35f --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/RemoteVettingService.php @@ -0,0 +1,201 @@ +remoteVettingContext = $remoteVettingContext; + $this->logger = $logger; + $this->identityEncrypter = $identityEncrypter; + $this->attributeMapper = $attributeMapper; + $this->applicationHelper = $applicationHelper; + } + + /** + * @param string $identityProviderSlug + * @param RemoteVettingTokenDto $remoteVettingToken + */ + public function start($identityProviderSlug, RemoteVettingTokenDto $remoteVettingToken) + { + $this->logger->notice('Starting a remote vetting process', [ + 'second-factor' => $remoteVettingToken->getSecondFactorId(), + 'identity' => $remoteVettingToken->getIdentityId(), + 'provider' => $identityProviderSlug, + ]); + + $this->remoteVettingContext->initialize($identityProviderSlug, $remoteVettingToken); + } + + /** + * @param ProcessId $processId + */ + public function startValidation(ProcessId $processId) + { + $this->logger->notice('Starting a remote vetting authentication', [ + 'second-factor' => $this->remoteVettingContext->getTokenId(), + 'process' => $processId->getProcessId(), + ]); + + $this->remoteVettingContext->validating($processId); + } + + /** + * @param ProcessId $processId + * @param AttributeListDto $externalAttributes + */ + public function finishValidation(ProcessId $processId, AttributeListDto $externalAttributes) + { + $this->logger->notice('Finishing a remote vetting authentication', [ + 'second-factor' => $this->remoteVettingContext->getTokenId(), + 'process' => $processId->getProcessId(), + ]); + + $this->remoteVettingContext->validated($processId, $externalAttributes); + } + + /** + * @param ProcessId $processId + * @param Identity $identity + * @param AttributeListDto $localAttributes + * @param AttributeMatchCollection $attributeMatches + * @param FeedbackCollection $feedback + * @return RemoteVettingTokenDto + */ + public function done( + ProcessId $processId, + Identity $identity, + AttributeListDto $localAttributes, + AttributeMatchCollection $attributeMatches, + FeedbackCollection $feedback + ) { + $this->remoteVettingContext->done($processId); + $this->logger->notice('Saving the encrypted match data to the filesystem', [ + 'second-factor' => $this->remoteVettingContext->getTokenId(), + 'process' => $processId->getProcessId(), + ]); + + $identityData = $this->aggregateIdentityData($identity, $localAttributes, $attributeMatches, $feedback); + $this->identityEncrypter->encrypt($identityData->serialize()); + + $this->logger->notice('Finished the remote vetting process', [ + 'second-factor' => $this->remoteVettingContext->getTokenId(), + 'process' => $processId->getProcessId(), + ]); + + return $this->remoteVettingContext->getValidatedToken(); + } + + /** + * @param AttributeListDto $localAttributes + * @return AttributeMatchCollection + */ + public function getAttributeMatchCollection(AttributeListDto $localAttributes) + { + $externalAttributes = $this->remoteVettingContext->getAttributes(); + $identityProviderSlug = $this->remoteVettingContext->getIdentityProviderSlug(); + + return $this->attributeMapper->map($identityProviderSlug, $localAttributes, $externalAttributes); + } + + /** + * @return string + */ + public function getActiveIdentityProviderSlug() + { + return $this->remoteVettingContext->getIdentityProviderSlug(); + } + + /** + * @param Identity $identity + * @param AttributeListDto $localAttributes + * @param AttributeMatchCollection $attributeMatches + * @param FeedbackCollection $feedback + * @return IdentityData + */ + private function aggregateIdentityData( + Identity $identity, + AttributeListDto $localAttributes, + AttributeMatchCollection $attributeMatches, + FeedbackCollection $feedback + ) { + $nameId = $identity->nameId; + $institution = $identity->institution; + $version = $this->applicationHelper->getApplicationVersion(); + $remoteVettingSource = $this->remoteVettingContext->getIdentityProviderSlug(); + + $attributeCollectionAggregate = new AttributeCollectionAggregate(); + $attributeCollectionAggregate->add('local-attributes', $localAttributes); + $attributeCollectionAggregate->add('remote-attributes', $this->remoteVettingContext->getAttributes()); + $attributeCollectionAggregate->add('matching-results', $attributeMatches); + $attributeCollectionAggregate->add('feedback', $feedback); + + return new IdentityData( + $attributeCollectionAggregate, + $nameId, + $version, + $institution, + $remoteVettingSource + ); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/SecondFactorService.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/SecondFactorService.php index 734004c26..4795395ed 100644 --- a/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/SecondFactorService.php +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Service/SecondFactorService.php @@ -22,6 +22,7 @@ use Surfnet\StepupMiddlewareClient\Identity\Dto\VerifiedSecondFactorOfIdentitySearchQuery; use Surfnet\StepupMiddlewareClient\Identity\Dto\VettedSecondFactorSearchQuery; use Surfnet\StepupMiddlewareClientBundle\Dto\CollectionDto; +use Surfnet\StepupMiddlewareClientBundle\Identity\Command\RemoteVetSecondFactorCommand; use Surfnet\StepupMiddlewareClientBundle\Identity\Command\RevokeOwnSecondFactorCommand; use Surfnet\StepupMiddlewareClientBundle\Identity\Command\VerifyEmailCommand; use Surfnet\StepupMiddlewareClientBundle\Identity\Dto\UnverifiedSecondFactor; @@ -31,6 +32,7 @@ use Surfnet\StepupMiddlewareClientBundle\Identity\Dto\VettedSecondFactor; use Surfnet\StepupMiddlewareClientBundle\Identity\Dto\VettedSecondFactorCollection; use Surfnet\StepupMiddlewareClientBundle\Identity\Service\SecondFactorService as MiddlewareSecondFactorService; +use Surfnet\StepupSelfService\SelfServiceBundle\Command\RemoteVetCommand; use Surfnet\StepupSelfService\SelfServiceBundle\Command\RevokeCommand; use Surfnet\StepupSelfService\SelfServiceBundle\Exception\LogicException; @@ -96,6 +98,21 @@ public function revoke(RevokeCommand $command) return $result->isSuccessful(); } + /** + * @param RemoteVetCommand $command + * @return bool + */ + public function remoteVet(RemoteVetCommand $command) + { + $apiCommand = new RemoteVetSecondFactorCommand(); + $apiCommand->identityId = $command->identity; + $apiCommand->secondFactorId = $command->secondFactor; + + $result = $this->commandService->execute($apiCommand); + + return $result->isSuccessful(); + } + /** * Returns whether the given registrant has registered second factors with Step-up. The state of the second factor * is irrelevant. diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Mock/RemoteVetting/MockRemoteVetControllerTest.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Mock/RemoteVetting/MockRemoteVetControllerTest.php new file mode 100644 index 000000000..f70cc8688 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Mock/RemoteVetting/MockRemoteVetControllerTest.php @@ -0,0 +1,422 @@ +client = static::createClient(); + $this->client->followRedirects(true); + $this->client->disableReboot(); + + $this->remoteVettingService = $this->client->getKernel()->getContainer()->get(RemoteVettingService::class); + + // Mock second factor service + $this->secondFactorService = m::mock(SecondFactorService::class); + $this->client->getKernel()->getContainer()->set('surfnet_stepup_self_service_self_service.service.second_factor', $this->secondFactorService); + + $this->mockSecondFactorOverviewPage(); + + $projectDir = self::$kernel->getProjectDir(); + $keyPath = '/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources'; + $this->publicKey = $projectDir . $keyPath . '/test.crt'; + $this->privateKey = $projectDir . $keyPath . '/test.key'; + + $this->samlCalloutHelper = $this->setupSamlCalloutHelper(); + + $this->localAttributeSet = AttributeSet::create([ + new Attribute(new AttributeDefinition('givenName', 'urn:mace:firstName', 'urn:oid:0.2.1'), ['John']), + new Attribute(new AttributeDefinition('surname', 'urn:mace:lastName', 'urn:oid:0.2.2'), ['Doe']), + new Attribute(new AttributeDefinition('isMemberOf', 'urn:mace:isMemberOf', 'urn:oid:0.2.7'), ['team-a', 'a-team']), + new Attribute(new AttributeDefinition('nameId', 'urn:mace:nameId', 'urn:oid:0.2.7'), [NameID::fromArray(['Value' => 'johndoe.example.com', 'Format' => 'unspecified'])]), + ]); + } + + protected function tearDown(): void + { + parent::tearDown(); + + m::close(); + } + + + /** + * @test + * @group rv + */ + public function the_mock_remote_vetting_idp_should_present_us_with_possible_results_for_testing_purposes() + { + $this->logIn(); + $this->remoteVettingService->start('mock', RemoteVettingTokenDto::create('identity-id-123456', 'second-factor-id-56789')); + $authnRequestUrl = $this->samlCalloutHelper->createAuthnRequest('mock'); + + $crawler = $this->client->request('GET', $authnRequestUrl); + + // Test if on decision page + $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); + $this->assertStringContainsString('Select response', $crawler->filter('h2')->text()); + } + + /** + * @test + * @group rv + */ + public function a_successful_response_from_a_remote_vetting_idp_should_succeed() + { + $this->logIn(); + $this->remoteVettingService->start('mock', RemoteVettingTokenDto::create('identity-id-123456', 'second-factor-id-56789')); + $authnRequestUrl = $this->samlCalloutHelper->createAuthnRequest('mock'); + + $crawler = $this->client->request('GET', $authnRequestUrl); + + // Test valid response + $this->postMockIdpForm($crawler, 'success'); + + // Test if on manual matching form + $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); + $this->assertStringStartsWith('https://selfservice.stepup.example.com/second-factor/remote-vetting/match/', $this->client->getRequest()->getUri()); + $this->assertStringContainsString('Questions for you', $this->client->getResponse()->getContent()); + } + + /** + * @test + * @group rv + */ + public function a_user_cancelled_response_from_a_remote_vetting_idp_should_fail() + { + $this->logIn(); + $this->remoteVettingService->start('mock', RemoteVettingTokenDto::create('identity-id-123456', 'second-factor-id-56789')); + $authnRequestUrl = $this->samlCalloutHelper->createAuthnRequest('mock'); + + $crawler = $this->client->request('GET', $authnRequestUrl); + + // Test user cancelled response + $this->postMockIdpForm($crawler, 'user-cancelled'); + + // Test if on sp acs + $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); // this could be enabled if the request to MW are mocked + $this->assertEquals('https://selfservice.stepup.example.com/overview', $this->client->getRequest()->getUri()); + $this->assertStringContainsString('Unable to validate the information', $this->client->getResponse()->getContent()); + } + + /** + * @test + * @group rv + */ + public function an_unsuccessful_response_from_a_remote_vetting_idp_should_fail() + { + $this->logIn(); + $this->remoteVettingService->start('mock', RemoteVettingTokenDto::create('identity-id-123456', 'second-factor-id-56789')); + $authnRequestUrl = $this->samlCalloutHelper->createAuthnRequest('mock'); + + $crawler = $this->client->request('GET', $authnRequestUrl); + + // Test unknown response + $this->postMockIdpForm($crawler, 'unknown'); + + // Test if on sp acs + $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); // this could be enabled if the request to MW are mocked + $this->assertEquals('https://selfservice.stepup.example.com/overview', $this->client->getRequest()->getUri()); + $this->assertStringContainsString('Unable to validate the information', $this->client->getResponse()->getContent()); + } + + + /** + * @test + * @group rv + */ + public function a_verified_token_must_be_vetted_with_external_idp_e2e() + { + // Mock remote vet vetting + $remoteVetCommand = new RemoteVetCommand(); + $remoteVetCommand->identity = "identity-id-123456"; + $remoteVetCommand->secondFactor = "second-factor-id-56789"; + + $this->secondFactorService + ->shouldReceive('remoteVet') + ->with(IsEqual::equalTo($remoteVetCommand))->once() + ->andReturn(true); + + // Login + $this->logIn(); + + // On second factor overview page start vetting + $crawler = $this->client->request('GET', 'https://selfservice.stepup.example.com/overview'); + $link = $crawler->selectLink('Validate identity')->link(); + $this->assertSame('https://selfservice.stepup.example.com/second-factor/second-factor-id-56789/vetting-types', $link->getUri()); + $crawler = $this->client->click($link); + + // Select 'mock' as vetting type + $this->assertSame('https://selfservice.stepup.example.com/second-factor/second-factor-id-56789/vetting-types', $this->client->getRequest()->getUri()); + $button = $crawler->selectButton('select-rv-idp-mock'); + $form = $button->form(); + $crawler = $this->client->submit($form); + + // Handle IdP callout + $this->assertSame('https://selfservice.stepup.example.com/second-factor/mock/sso', $this->client->getRequest()->getSchemeAndHttpHost() . $this->client->getRequest()->getPathInfo()); + $crawler = $this->postMockIdpForm($crawler, 'success'); + + // Test if on manual matching form + $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); + $this->assertStringStartsWith('https://selfservice.stepup.example.com/second-factor/remote-vetting/match/', $this->client->getRequest()->getUri()); + $this->assertStringContainsString('Questions for you', $this->client->getResponse()->getContent()); + + // Set response attributes and post form + $form = $crawler->selectButton('ss_remote_vet_validation[validate]')->form(); + $crawler = $this->client->submit($form, [ + 'ss_remote_vet_validation[matches][surname][valid]' => '1', + 'ss_remote_vet_validation[matches][givenName][remarks]' => 'This is not my full first name', + 'ss_remote_vet_validation[feedback][remarks]' => 'All other info seems valid', + ]); + + // Check if on overview page with success flashbag message + $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); + $this->assertStringStartsWith('https://selfservice.stepup.example.com/overview', $this->client->getRequest()->getUri()); + $this->assertStringContainsString('Your identity information was validated successfully', $this->client->getResponse()->getContent()); + } + + /** + * @return SamlCalloutHelper + */ + private function setupSamlCalloutHelper() + { + $identityProviderFactory = $this->client->getKernel()->getContainer()->get(IdentityProviderFactory::class); + + $serviceProviderFactory = m::mock(ServiceProviderFactory::class); + $serviceProviderFactory->shouldReceive('create') + ->withNoArgs() + ->once() + ->andReturn($this->createServiceProvider()); + + $logger = new NullLogger(); + + $responseProcessor = new Processor($logger); + $keyLoader = new KeyLoader(); + $signatureVerifier = new SignatureVerifier($keyLoader, $logger); + $postBinding = new PostBinding($responseProcessor, $logger, $signatureVerifier); + + return new SamlCalloutHelper( + $identityProviderFactory, + $serviceProviderFactory, + $postBinding, + $this->remoteVettingService, + $logger + ); + } + + private function createServiceProvider() + { + return new ServiceProvider( + [ + 'entityId' => 'https://selfservice.stepup.example.com/rv/metadata', + 'assertionConsumerUrl' => 'https://selfservice.stepup.example.com/second-factor/acs', + 'certificateFile' => $this->publicKey, + 'privateKeys' => [ + new PrivateKey( + $this->privateKey, + 'default' + ), + ], + 'sharedKey' => $this->publicKey, + ] + ); + } + + /** + * @param Crawler $crawler + * @param string $state State button to press + * @return Crawler + */ + private function postMockIdpForm(Crawler $crawler, $state) + { + $data = '[ + {"name":"firstName","value":["john"]}, + {"name":"lastName","value":["doe"]}, + {"name":"eduPersonAffiliation","value":["users","role1"]} + ]'; + + // Test if on decision page + $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); + $this->assertStringContainsString('Select response', $crawler->filter('h2')->text()); + + // Set response attributes and post form + $form = $crawler->selectButton($state)->form(); + $form->get('attributes')->setValue($data); + $crawler = $this->client->submit($form); + + // Post response + $form = $crawler->selectButton('Post')->form(); + + return $this->client->submit($form); + } + + private function logIn() + { + $session = $this->client->getKernel()->getContainer()->get('session'); + + $firewallContext = 'saml_based'; + + $user = Identity::fromData([ + 'id' => 'identity-id-123456', + 'name_id' => 'name-id', + 'institution' => 'institution', + 'email' => 'name@institution.tld', + 'common_name' => 'Common Name', + 'preferred_locale' => 'en_GB', + ]); + + $token = new SamlToken(['ROLE_USER']); + $token->setUser($user); + + $token->setAttribute(SamlToken::ATTRIBUTE_SET, $this->localAttributeSet); + + $session->set('_security_' . $firewallContext, serialize($token)); + $session->save(); + + $cookie = new Cookie($session->getName(), $session->getId()); + $this->client->getCookieJar()->set($cookie); + } + + private function mockSecondFactorOverviewPage() + { + // Mock institution configuration for second factor overview page + $this->institutionConfigurationOptionsService = m::mock(InstitutionConfigurationOptionsService::class); + $this->client->getKernel()->getContainer()->set('self_service.service.institution_configuration_options', $this->institutionConfigurationOptionsService); + + $verifiedResult = json_decode('{ + "collection": { + "total_items": 1, + "page": 1, + "page_size": 1 + }, + "items": [ + { + "id": "second-factor-id-56789", + "type": "yubikey", + "second_factor_identifier": "2340897143", + "registration_code": "DMHKJKH8", + "registration_requested_at": "' . date(DateTime::ISO8601) . '", + "identity_id": "identity-id-123456", + "institution": "institution-f.example.com", + "common_name": "joe-f3 Institution-f.example.com" + } + ], + "filters": [] + }', true); + + $emptyResult = json_decode('{ + "collection": { + "total_items": 0, + "page": 0, + "page_size": 0 + }, + "items": [], + "filters": [] + }', true); + + $tokenCollection = new SecondFactorTypeCollection(); + $tokenCollection->verified = VerifiedSecondFactorCollection::fromData($verifiedResult); + $tokenCollection->unverified = UnverifiedSecondFactorCollection::fromData($emptyResult); + $tokenCollection->vetted = VettedSecondFactorCollection::fromData($emptyResult); + + $this->secondFactorService + ->shouldReceive('getSecondFactorsForIdentity') + ->zeroOrMoreTimes() + ->andReturn($tokenCollection); + + $this->secondFactorService->shouldReceive('findOneVerified') + ->andReturn($tokenCollection->verified->getOnlyElement()); + + // Mock institution configuration + $institutionConfigurationOptions = new InstitutionConfigurationOptions(); + + $this->institutionConfigurationOptionsService + ->shouldReceive('getInstitutionConfigurationOptionsFor') + ->with('institution') + ->zeroOrMoreTimes() + ->andReturn($institutionConfigurationOptions); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/encryption.crt b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/encryption.crt new file mode 100644 index 000000000..0859998f9 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/encryption.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC6jCCAdICCQC9cRx5wiwWOjANBgkqhkiG9w0BAQsFADA3MRwwGgYDVQQDDBNT +ZWxmU2VydmljZSBTQU1MIFNQMRcwFQYDVQQKDA5EZXZlbG9wbWVudCBWTTAeFw0x +ODA3MzAxMjMwNDdaFw0yMzA3MjkxMjMwNDdaMDcxHDAaBgNVBAMME1NlbGZTZXJ2 +aWNlIFNBTUwgU1AxFzAVBgNVBAoMDkRldmVsb3BtZW50IFZNMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqhbI0Xy682DuvWchg6FYnI+DNwLXef2XExM4 +YVRBaMMsOZ3rBtQUTMSqYan6SK/BOEXLs0rNiJjyM0dn+F98wg3fv5zIADlvfk3L +BVdcGsrpVfFUWtSa73yMgbROy8/RJADbUJE/HUB3ZmdjdiuD2Cui2aoWwT2HR8uk +Jwmoxiu45IWFPbqPQ7/1mH644JPOWTPLTv4OGGLQo8MNrP1oRCiZ0IEL4CQeGOOj +u5rfIJ0bTVm0UmelT4hGaqZovBMwXp3QV41akJ7UEMEBK2YMnLQy47Xuzi7aTDhJ +lvHcJ8mfH2NbjRh7hJoACVRTvQloxajgkr1iGMiWiiqT0e+YYwIDAQABMA0GCSqG +SIb3DQEBCwUAA4IBAQBwZ0gRHvR8B8KivrXrhWNL9uLvWhEAH7OiDqo+fywkBp5K +EuDJcbbvEPftHunSAGylg7M2xKuBIGamFpp74WDJccrtZ1jJ4qqnacUDRQrTLqqM +ZKqGpFOU0xjKkSxSGRuMtGN9/7er/TeonjQ0XBvjYvTomy3b5aCLVWRvEfKu2g1s +Dd8uhr62RY/HfMgidEt7LHDolkCVg+6JzY3OTcgeHga3cvYObOYPplxw1YPq5+Bq +qxaUW4nfb5DtK33bZBYMeyV6BZtSggc5Z/19aPx/s0bf6ySTUyB3lRqe5d3etCns +4bGidORCl/6EZiXwVcPvmYmxYXqmuNWfps7isUvo +-----END CERTIFICATE----- diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/encryption.key b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/encryption.key new file mode 100644 index 000000000..a7ab91723 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/encryption.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAqhbI0Xy682DuvWchg6FYnI+DNwLXef2XExM4YVRBaMMsOZ3r +BtQUTMSqYan6SK/BOEXLs0rNiJjyM0dn+F98wg3fv5zIADlvfk3LBVdcGsrpVfFU +WtSa73yMgbROy8/RJADbUJE/HUB3ZmdjdiuD2Cui2aoWwT2HR8ukJwmoxiu45IWF +PbqPQ7/1mH644JPOWTPLTv4OGGLQo8MNrP1oRCiZ0IEL4CQeGOOju5rfIJ0bTVm0 +UmelT4hGaqZovBMwXp3QV41akJ7UEMEBK2YMnLQy47Xuzi7aTDhJlvHcJ8mfH2Nb +jRh7hJoACVRTvQloxajgkr1iGMiWiiqT0e+YYwIDAQABAoIBAF+J5Msm0Kwcan2h +DEYvvuJSClZAFmDDfLSOO0EQXp1F4/WJKpbvUWe9oCazn45sio/dRIo1HjX4EzOS +jGgK2rz1phSvL/hQSrwbXkplw6qZB2/q2oMaoNycjR/d89Svqr4abRZYP6diqq6u +rEOYNbqa6CJzU8y/jtlZHZ9/4XlN8035QNJ3YIi3qVe3cCr6IOahUGOayWNaW+0q +vLBhWdbaER5aHiUdcZPrJfNhepb2Ob9djizqpWo8u9WyYNpiExjm1Ov6IAQhxkc7 +uAvJIE7W39Ag4wHNHHj+WkctG+KBEym3/i2SDAddUP5H6FGMzPQPdoJK2XArrE0B +p5Tun0ECgYEA1ot9Vz7YbMOqGvok/GQyVuV8MTRC12iPlwoOV3HKNG9TfclglRzg +csp83rJ13tz8NyN93GQpjOkCvdQinJGk/kR6h9eCi2l2HPGNMrZH7qY+2cQvf6J5 +KTGI1sAi4DqHJ9u0AyaQdu2ieh3HwgI8+PWBFn3dBR5xKeHIh/59hRsCgYEAyvRG +W+xpVRlM1XoLPMn5Z2yUpI6mieaD3jmNQSC0OuxdxlIZVtyqBF3rFQw1V/74bS3X +aOxtwelGQ2PfWnjo4uLoWqUoIN0ZAn+9yKzMla/5y1jEhyFcaUQc8QGmp+wOjDgQ +NHM23VSAr7Q+G3EMQmjlURC45Il66mnrkcZUFlkCgYEAoAMzPZHauuwH/8zXLwLP +5K2Nvej7fUs35O+UGLX+mLL7M1KxXSVHZXYOQc4aSVjKJ5mp8mkl8DmNWOVR1zJt +O1L5jD042R+T/yxNIih/Z8fIEoTW5DvaX9XY+Eoe+NvOF/UtwjfOAVVlG+0AInum +3AvG9m5zHLFCt3j1JjCxj0cCgYEAv4IrFjiJ2DwsbVBhZDYt+nLR/EmDSqLTEhH6 +gVcr2mIJxsbXlEhawg4hctX3TBaTMurL1f0rQIwvug12yDdJgjadDFPF/uTC4cHK +Qp8T2beZHVGg+OX4/nfAW4a0TMYJoDSSzftd7RH88E9DP7+30r6KjKkb3sL/0kyq +df7Qf9kCgYAi1vf0bc6GgWf0CA+7NtZivl4Pw1aZEZI7tKY2cC95KKTycPhxSpq5 +g72XdHAp+gaJoSBledEYMJfE5Xsdf5r0F1v5xDe87Dn+zT7UXpw4JrDE16jBKwv1 +pTLyJ51aerY27qJEtZ3JqbCux853aa2cxLIoje+5Kxso33bPe0EXGg== +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/test.crt b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/test.crt new file mode 100644 index 000000000..96cbe1e26 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/test.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID4TCCAsmgAwIBAgIJAMRJjoU8qOWkMA0GCSqGSIb3DQEBCwUAMIGFMQswCQYD +VQQGEwJubDEQMA4GA1UECAwHemVlbGFuZDETMBEGA1UEBwwKdmxpc3NpbmdlbjET +MBEGA1UECgwKb3BlbmNvbmV4dDELMAkGA1UECwwCaXQxDTALBgNVBAMMBHRlc3Qx +HjAcBgkqhkiG9w0BCQEWD3Rlc3RAZG9tYWluLnRsZDAgFw0yMDAyMTgxNTQxMDJa +GA8yMTIwMDEyNTE1NDEwMlowgYUxCzAJBgNVBAYTAm5sMRAwDgYDVQQIDAd6ZWVs +YW5kMRMwEQYDVQQHDAp2bGlzc2luZ2VuMRMwEQYDVQQKDApvcGVuY29uZXh0MQsw +CQYDVQQLDAJpdDENMAsGA1UEAwwEdGVzdDEeMBwGCSqGSIb3DQEJARYPdGVzdEBk +b21haW4udGxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtyqrduin +yfu78sRFHd/x2dMwMpWNOSDbSh+f5ThPe9M3r0KvEyAqHMqnvpk+TEOrmmKthnV9 +RWABPIDFU9A8xVV3RgkUzZeFmqkUrooQydGGofkTbpYvVnMVVxU7Ju62t2fJNp25 +/IM1rgcq8ycU3CW9mYyTqGD8hWje6/n/KVGeZOzkLRVc+JIVxEuZof29VRCRBaK/ +CbHYehwk0f6KtTefFiM2It9G9TwGce5bu2tYoQx782MKfUjXaAGiB2oy5u0wNxs5 +aeQVDbElJVbioM0CTtZfVroTz2ttZzSIctZFNET4rsbOBJVrPI1GGCgWLCwwcux8 +loINgIXrLzrtpQIDAQABo1AwTjAdBgNVHQ4EFgQUQWcfYW0gfNZcYfk/dMaYQjxv +GqEwHwYDVR0jBBgwFoAUQWcfYW0gfNZcYfk/dMaYQjxvGqEwDAYDVR0TBAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAQEAex22VMVCMY+P/ZYWPwgTVQq0OxkbOmEBE5zK +A2SjgfaqYxPiwnpJpX7tMCC0oCvtahLUTz+cx5XAPtPrAdZ/vKcxSH5R/fTpy/z8 +BBlnP+SVrfqnyMa8ZQrKPH8QE475umVhGyLYeDV63WhhyPJ0usu9Sf5QWJRDGpTt +qGKNCtGgLut8Y/ONPXFYv7QDmoOGiVNWGphaOUR2x+yEe15FiTonmpVvXkRbj+bj +xklg2rq1svVyM/wEB/fIZkeLqypExl7TIQya5GafTP2Gfu+xcsKcn/DipAp/MWKt +UKS62o7JFpdHHUwT5hUlWM7fMunOszrUbqsQ/eo7yLfm1IwFAw== +-----END CERTIFICATE----- diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/test.key b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/test.key new file mode 100644 index 000000000..977b486ba --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/test.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC3Kqt26KfJ+7vy +xEUd3/HZ0zAylY05INtKH5/lOE970zevQq8TICocyqe+mT5MQ6uaYq2GdX1FYAE8 +gMVT0DzFVXdGCRTNl4WaqRSuihDJ0Yah+RNuli9WcxVXFTsm7ra3Z8k2nbn8gzWu +ByrzJxTcJb2ZjJOoYPyFaN7r+f8pUZ5k7OQtFVz4khXES5mh/b1VEJEFor8Jsdh6 +HCTR/oq1N58WIzYi30b1PAZx7lu7a1ihDHvzYwp9SNdoAaIHajLm7TA3Gzlp5BUN +sSUlVuKgzQJO1l9WuhPPa21nNIhy1kU0RPiuxs4ElWs8jUYYKBYsLDBy7HyWgg2A +hesvOu2lAgMBAAECggEAKgmX+p1gzOGCfDABcN7Rwd1PRXy/QapBydxCNThcPZ33 +PLkAN1J9d8gnAX5oJtatDqyxCJf0XTSwc/NV1muHVv7Ldf5lkN3CowFFJt23BqqY +zJm+lPXQ/XVCI49S2ReWRbeDHY7LfaGbc0pf2BhTGEdtIU17IfzIV9H+v4fLwO5/ +5q1Jtb3USO/e5aQM2lr1Hzm4iKn5f2+nN9ujlChNtuNg8zliyCKKdAwiRkJYGtkH +f0LqXw21OI+82tHq50/gHj21Lh4ZXAnmjMwOtfhsLEBZQ4WXUsInsQJCWY5B+bjZ +OnPGD8zyNB/s6rIJiBNVknxTzW2eC37XsxaVBK4N4QKBgQDmWv6T+KnVt7VdXDPV +QWfqIy0ZyqbUu7+R+NapzmGz4CN2ZyiHyIbjvCOG0soJlAM3Rf5DTZ6QlmQkH9Cd +EAeJ4StX4RU2tJb2diqjLaRsBgqqxw+TJ5MzCWezk5H2u2WLvKI7MJwDfRlIFyhS +YHyy/q4YPx/nYD1NDjkhuv7SCQKBgQDLjtIsI3nOywMWKXFNOrf57fsYA17eHG6l +wg1xWaJsicwMHQrzxOt/7Q6E7NzDFE7QwqZwTc5X22fMpkhgML8+FTtCdai/xACR +5DmRZ4Rng2GxWQ8IokA/VapY1QDaDXI0wZI4z6AcZRoeGPhtfqMjfcuZcMIXJFBw +9q2HEi41vQKBgQDCVNGgE49zdN/UOwyfQ+ZeZ/6MW6ISpbEffEXcSvexv80q/iv7 +IPhq9zUhcIJxQiTUZH/0V28Fm1ov/4cGeZdigksGgCRdWkxg759YuhCT8STencBN +7H0J+U06+auR8tXf5OsX9BIp/0dswdyKYkvQ1XoZimdn0pDkiLM5+X8vwQKBgQDB +BCE4Zz82HfxERHHCGF6hJ87DO1b2Z6FnnbTfeW3xW0xXZCahdWUVPXRaCLtgY15E +x33I2Y5CcrJfNIFGbJK6zKkPYL/tbm5X6D/KJ40+Fi9JWjhOKIOhSpqndvpCySM0 +8SO0qPOaWSfFyEmwkbchjUPEsE3qYa3BH7b1cADPRQKBgAwbHOIK6ogb8HcnHx64 +hqgXfeQz5A/5XLWgzOWwj/EDAoc35MEwWFQgOxX7xmcpAKmJuJ+zW6vz/pEZjWe9 +BfRDeE2Tk90Wx1i9wXjot0mSYQWezxelhLiynoyyeozOLVM9q3GM7ZWO0JM+xjEL +luTesf6wfI9O8/ooOSwar6eL +-----END PRIVATE KEY----- diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Security/Session/SessionStorageTest.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Security/Session/SessionStorageTest.php index 236189465..cc67925d7 100644 --- a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Security/Session/SessionStorageTest.php +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Security/Session/SessionStorageTest.php @@ -47,6 +47,8 @@ public function the_authentication_moment_can_be_logged() $sessionStorage = new SessionStorage(new FakeSession()); $sessionStorage->logAuthenticationMoment(); + + $this->assertInstanceOf(SessionStorage::class, $sessionStorage); } /** @@ -122,6 +124,8 @@ public function an_interaction_can_be_logged() $sessionStorage = new SessionStorage(new FakeSession()); $sessionStorage->updateLastInteractionMoment(); + + $this->assertInstanceOf(SessionStorage::class, $sessionStorage); } /** @@ -293,6 +297,8 @@ public function a_session_can_be_invalidated() $sessionStorage = new SessionStorage($session); $sessionStorage->invalidate(); + + $this->assertInstanceOf(SessionStorage::class, $sessionStorage); } /** @@ -309,6 +315,8 @@ public function a_session_can_be_migrated() $sessionStorage = new SessionStorage($session); $sessionStorage->migrate(); + + $this->assertInstanceOf(SessionStorage::class, $sessionStorage); } /** diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/AttributeMapperTest.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/AttributeMapperTest.php new file mode 100644 index 000000000..1e077dd33 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/AttributeMapperTest.php @@ -0,0 +1,143 @@ +configuration = m::mock(RemoteVettingConfiguration::class); + $this->attributeMapper = new AttributeMapper($this->configuration); + } + + public function test_attribute_mapping() + { + $config = [ + 'foo2' => 'baz2', + 'foo3' => 'baz1', + ]; + + $nameId = 'foo@bar.baz'; + $local = [ + 'foo1' => ['bar1'], + 'foo2' => ['bar2'], + 'foo3' => ['bar3'], + ]; + $external = [ + 'baz1' => ['foobar1'], + 'baz2' => ['foobar2'], + 'baz3' => ['foobar3'], + ]; + + $this->configuration->shouldReceive('getAttributeMapping') + ->with('idp-name') + ->andReturn($config); + + $localAttributes = new AttributeListDto($local, $nameId); + $externalAttributes = new AttributeListDto($external, ''); + + $attributeMatchCollection = $this->attributeMapper->map('idp-name', $localAttributes, $externalAttributes); + + $result = json_encode($attributeMatchCollection->getAttributes()); + + $this->assertSame('{"foo2":{"local":{"name":"foo2","value":["bar2"]},"remote":{"name":"baz2","value":["foobar2"]},"is-valid":false,"remarks":""},"foo3":{"local":{"name":"foo3","value":["bar3"]},"remote":{"name":"baz1","value":["foobar1"]},"is-valid":false,"remarks":""}}', $result); + } + + + public function test_attribute_mapping_missing_local() + { + $this->expectException(InvalidRemoteVettingMappingException::class); + $this->expectExceptionMessage('Invalid remote vetting attribute mapping, local attribute with name "MISSING" not found'); + + $config = [ + 'MISSING' => 'baz2', + 'foo3' => 'baz1', + ]; + + $nameId = 'foo@bar.baz'; + $local = [ + 'foo1' => ['bar1'], + 'foo2' => ['bar2'], + 'foo3' => ['bar3'], + ]; + $external = [ + 'baz1' => ['foobar1'], + 'baz2' => ['foobar2'], + 'baz3' => ['foobar3'], + ]; + + $this->configuration->shouldReceive('getAttributeMapping') + ->with('idp-name') + ->andReturn($config); + + $localAttributes = new AttributeListDto($local, $nameId); + $externalAttributes = new AttributeListDto($external, ''); + + $this->attributeMapper->map('idp-name', $localAttributes, $externalAttributes); + } + + + + public function test_attribute_mapping_missing_external() + { + $this->expectException(InvalidRemoteVettingMappingException::class); + $this->expectExceptionMessage('Invalid remote vetting attribute mapping, remote attribute with name "MISSING" not found'); + + $config = [ + 'foo2' => 'MISSING', + 'foo3' => 'baz1', + ]; + + $nameId = 'foo@bar.baz'; + $local = [ + 'foo1' => ['bar1'], + 'foo2' => ['bar2'], + 'foo3' => ['bar3'], + ]; + $external = [ + 'baz1' => ['foobar1'], + 'baz2' => ['foobar2'], + 'baz3' => ['foobar3'], + ]; + + $this->configuration->shouldReceive('getAttributeMapping') + ->with('idp-name') + ->andReturn($config); + + $localAttributes = new AttributeListDto($local, $nameId); + $externalAttributes = new AttributeListDto($external, ''); + + $mappedAttributes = $this->attributeMapper->map('idp-name', $localAttributes, $externalAttributes); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Dto/AttributeListDtoTest.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Dto/AttributeListDtoTest.php new file mode 100644 index 000000000..467076f75 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Dto/AttributeListDtoTest.php @@ -0,0 +1,109 @@ +assertEquals($expected, $attribute->serialize()); + } + + /** + * @dataProvider provideValidAttributes + */ + public function test_allows_loading_attributes_from_serialised_string($nameId, $attributes, $expected) + { + $attribute = new AttributeListDto($attributes, $nameId); + $this->assertEquals($expected, $attribute->serialize()); + + $newAttribute = AttributeListDto::deserialize($attribute->serialize()); + $this->assertEquals($expected, $newAttribute->serialize()); + } + + /** + * @dataProvider provideInvalidAttributes + */ + public function test_disallows_non_scalar_types($nameId, $attributes, $expectedExceptionMessage) + { + $this->expectException(AssertionFailedException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + $attribute = new AttributeListDto($attributes, $nameId); + } + + public function test_can_be_created_from_attribute_set() + { + $attributes = [ + new Attribute(new AttributeDefinition('firstName', 'urn:mace:firstName', 'urn:oid:0.2.1'), ['John']), + new Attribute(new AttributeDefinition('lastName', 'urn:mace:lastName', 'urn:oid:0.2.2'), ['Doe']), + new Attribute(new AttributeDefinition('isMemberOf', 'urn:mace:isMemberOf', 'urn:oid:0.2.7'), ['team-a', 'a-team']), + new Attribute(new AttributeDefinition('nameId', 'urn:mace:nameId', 'urn:oid:0.2.7'), [NameID::fromArray(['Value' => 'johndoe.example.com', 'Format' => 'unspecified'])]), + ]; + $set = AttributeSet::create($attributes); + + $dto = AttributeListDto::fromAttributeSet($set); + + $convertedAttributes = $dto->getAttributeCollection()->getAttributes(); + + $this->assertCount(3, $dto->getAttributeCollection()); + $this->assertEquals(['John'], $convertedAttributes['firstName']->getValue()); + $this->assertEquals(['Doe'], $convertedAttributes['lastName']->getValue()); + $this->assertEquals(['team-a', 'a-team'], $convertedAttributes['isMemberOf']->getValue()); + $this->assertEquals('johndoe.example.com', $dto->getNameId()); + } + + public function provideValidAttributes() + { + yield ['foobar', ['foo' => ['bar']], '{"nameId":"foobar","attributes":{"foo":["bar"]}}']; + yield ['foobar', ['foo' => ['bar', 'foo2' => '2']], '{"nameId":"foobar","attributes":{"foo":["bar","2"]}}']; + yield ['foobar', ['foo' => ['bar', 'foo2' => 'bar2', 'foo3' => 'bar3']], '{"nameId":"foobar","attributes":{"foo":["bar","bar2","bar3"]}}']; + yield ['foobar', ['foo' => ['bar'], 'foo2' => ['bar', 'bar2']], '{"nameId":"foobar","attributes":{"foo":["bar"],"foo2":["bar","bar2"]}}']; + } + + public function provideInvalidAttributes() + { + yield ['foobar', [true, 2], 'The $name of an Attribute must be a scalar value']; + yield ['foobar', [true], 'The $name of an Attribute must be a scalar value']; + yield ['foobar', [['foobar']], 'The $name of an Attribute must be a scalar value']; + yield ['foobar', [['foo'=>'bar']], 'The $name of an Attribute must be a scalar value']; + yield ['foobar', ['valid', new stdClass()], 'The $name of an Attribute must be a scalar value']; + + yield ['foobar', ["foobar" => 0], 'The $value of an Attribute must be an array with strings']; + yield ['foobar', ["foobar" => "0"], 'The $value of an Attribute must be an array with strings']; + yield ['foobar', ["foobar" => [0]], 'The $value of an Attribute must be an array with strings']; + yield ['foobar', ["foobar" => [[]]], 'The $value of an Attribute must be an array with strings']; + + yield [1, ['foobar'], 'The $nameId in an AttributeListDto must be a string value']; + yield [false, ['foobar'], 'The $nameId in an AttributeListDto must be a string value']; + yield [['foobar'], ['foobar'], 'The $nameId in an AttributeListDto must be a string value']; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Dto/RemoteVettingProcessDtoTest.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Dto/RemoteVettingProcessDtoTest.php new file mode 100644 index 000000000..9ad3202a8 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Dto/RemoteVettingProcessDtoTest.php @@ -0,0 +1,67 @@ + ['bar']], 'nameId'); + $expected = sprintf('{"processId":"process id","token":{"identityId":"identityId","secondFactorId":"secondFactorId"},"state":%s,"attributes":{"nameId":"nameId","attributes":{"foo":["bar"]}},"identityProvider":"IRMA"}', json_encode($expected)); + + $processId = ProcessId::create('process id'); + $token = RemoteVettingTokenDto::create('identityId', 'secondFactorId'); + + $processDto = RemoteVettingProcessDto::create($processId, $token, 'IRMA'); + + $processDto = RemoteVettingProcessDto::updateState($processDto, $state); + + $processDto->setAttributes($attributes); + + $serialized = $processDto->serialize(); + + $this->assertSame($expected, $serialized); + + $newProcessDto = RemoteVettingProcessDto::deserialize($serialized); + + $this->assertEquals($processDto, $newProcessDto); + $this->assertSame($expected, $newProcessDto->serialize()); + } + + function provideState() + { + yield [ 'Initialized', new RemoteVettingStateInitialised(), RemoteVettingStateInitialised::class ]; + yield [ 'Validating', new RemoteVettingStateValidating(), RemoteVettingStateValidating::class]; + yield [ 'Validated', new RemoteVettingStateValidated(), RemoteVettingStateValidated::class]; + yield [ 'Done', new RemoteVettingStateDone(), RemoteVettingStateDone::class]; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/Decrypter.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/Decrypter.php new file mode 100644 index 000000000..4ea5b3a86 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/Decrypter.php @@ -0,0 +1,69 @@ +configuration = $configuration; + $this->writer = $writer; + } + + public function encrypt($data) + { + $this->writer->write($data); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/FakeIdentityWriter.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/FakeIdentityWriter.php new file mode 100644 index 000000000..61b7112c1 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/FakeIdentityWriter.php @@ -0,0 +1,36 @@ +data = $data; + } + + public function getData() + { + return $this->data; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/IdentityEncrypterIntegrationTest.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/IdentityEncrypterIntegrationTest.php new file mode 100644 index 000000000..4d79fc35e --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/IdentityEncrypterIntegrationTest.php @@ -0,0 +1,80 @@ +publicKey = file_get_contents($testKeyPath. '/encryption.crt'); + $this->privateKey = file_get_contents($testKeyPath . '/encryption.key'); + + $rvPrivateKey = realpath($testKeyPath . '/test.key'); + $config = [ + 'encryption_public_key' => $this->publicKey, + 'storage_location' => '/tmp', + ]; + $this->config = new RemoteVettingConfiguration($rvPrivateKey, $config, []); + $this->writer = new FakeIdentityWriter(); + $this->encrypter = new IdentityEncrypter($this->config, $this->writer); + } + + /** + * Create a simple identity DTO, encrypt and write it. The decrypted data should match that + * of the data set on the DTO. + */ + public function test_happy_flow() + { + $nameId = 'a-random-nameid@something.else'; + + $data = new AttributeListDto(['email' => ['johndoe@example.com'], 'firstName' => ['John']], $nameId); + $this->encrypter->encrypt($data->serialize()); + + $writtenData = $this->writer->getData(); + + // Now decrypt the data with the private key to prove the data is actually retrievable + $result = Decrypter::decrypt($writtenData, $this->privateKey); + + $serialized = json_decode($result, true); + + $this->assertEquals(['johndoe@example.com'], $serialized['attributes']['email']); + $this->assertEquals(['John'], $serialized['attributes']['firstName']); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/IdentityEncrypterTest.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/IdentityEncrypterTest.php new file mode 100644 index 000000000..587028ff0 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Encryption/IdentityEncrypterTest.php @@ -0,0 +1,150 @@ +publicKey = file_get_contents($testKeyPath. '/encryption.crt'); + $this->privateKey = file_get_contents($testKeyPath . '/encryption.key'); + + $this->config = m::mock(RemoteVettingConfiguration::class); + $this->writer = m::mock(IdentityWriterInterface::class); + $this->encrypter = new IdentityEncrypter($this->config, $this->writer); + } + + /** + * @test + */ + public function happy_flow_should_succeed() + { + $this->config + ->shouldReceive('getPublicKey') + ->andReturn($this->publicKey); + + $this->writer + ->shouldReceive('write') + ->withArgs(function ($data) use (&$encryptedData ){ + $encryptedData = $data; + return true; + }); + + $nameId = 'a-random-nameid@something.else'; + + $data = new AttributeListDto(['email' => ['johndoe@example.com'], 'firstName' => ['John']], $nameId); + $this->encrypter->encrypt($data->serialize()); + + // Assert result + $decryptedData = Decrypter::decrypt($encryptedData, $this->privateKey); + $this->assertSame($data->serialize(), $decryptedData); + } + + /** + * @test + */ + public function a_large_chunk_should_succeed() + { + $this->config + ->shouldReceive('getPublicKey') + ->andReturn($this->publicKey); + + $this->writer + ->shouldReceive('write') + ->withArgs(function ($data) use (&$encryptedData ){ + $encryptedData = $data; + return true; + }); + + $nameId = 'a-random-nameid@something.else'; + + $data = new AttributeListDto(['email' => ['johndoe@example.com'], 'firstName' => ['John']], $nameId); + $this->encrypter->encrypt($data->serialize()); + + // Assert result + $decryptedData = Decrypter::decrypt($encryptedData, $this->privateKey); + $this->assertSame($data->serialize(), $decryptedData); + } + + /** + * @test + */ + public function an_invalid_key_should_fail_non_string() + { + $this->config + ->shouldReceive('getPublicKey') + ->andReturn(8373292782); + + $nameId = 'a-random-nameid@something.else'; + $data = new AttributeListDto(['email' => ['johndoe@example.com'], 'firstName' => ['John']], $nameId); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid input was provided to the encrypt method'); + $this->encrypter->encrypt($data->serialize()); + } + + /** + * @test + */ + public function an_invalid_key_should_fail_bad_key_format() + { + $this->config + ->shouldReceive('getPublicKey') + ->andReturn('invalid key'); + + $nameId = 'a-random-nameid@something.else'; + + $data = new AttributeListDto(['email' => ['johndoe@example.com'], 'firstName' => ['John'],'a-lot-of-data' => [$this->generateRandomString(5000)]], $nameId); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Reading RSA public key failed'); + $this->encrypter->encrypt($data->serialize()); + } + + private function generateRandomString($length) + { + $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"\\}\''; + $charactersLength = strlen($characters); + $randomString = ''; + for ($i = 0; $i < $length; $i++) { + $randomString .= $characters[rand(0, $charactersLength - 1)]; + } + return $randomString; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/RemoteVettingContextTest.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/RemoteVettingContextTest.php new file mode 100644 index 000000000..1ad7f6e6b --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/RemoteVettingContextTest.php @@ -0,0 +1,358 @@ +session = new Session(new MockArraySessionStorage()); + } + + /** + * @test + * @group rv + */ + public function a_new_remote_vetting_process_can_always_be_started() + { + $token = $this->createToken(); + $token2 = $this->createToken(); + $token3 = $this->createToken(); + + $context = new RemoteVettingContext($this->session); + + $context->initialize('IRMA', $token, AttributeListDto::notSet()); + $context->initialize('IRMA', $token2, AttributeListDto::notSet()); + $context->initialize('IRMA', $token3, AttributeListDto::notSet()); + + $this->assertInstanceOf(RemoteVettingContext::class, $context); + } + + /** + * @test + * @group rv + */ + public function a_succesfull_remote_vetting_process_will_result_in_a_validated_token() + { + $token = $this->createToken(); + $processId = ProcessId::create('12345'); + + $context = new RemoteVettingContext($this->session); + + $context->initialize('IRMA', $token, AttributeListDto::notSet()); + $context->validating($processId); + $context->validated($processId, AttributeListDto::notSet()); + $context->done($processId); + + $validatedToken = $context->getValidatedToken(); + + $this->assertEquals($token, $validatedToken); + } + + /** + * @test + * @group rv + */ + public function a_incomplete_remote_vetting_process_wil_never_result_in_a_validated_token_0() + { + $this->expectException(InvalidRemoteVettingStateException::class); + $this->expectExceptionMessage('Unable to find a validated token'); + + $token = $this->createToken(); + $processId = ProcessId::create('12345'); + + $context = new RemoteVettingContext($this->session); + + $context->initialize('IRMA', $token, AttributeListDto::notSet()); + $context->validating($processId); + $context->validated($processId, AttributeListDto::notSet()); + + $context->getValidatedToken(); + } + + + /** + * @test + * @group rv + */ + public function a_incomplete_remote_vetting_process_wil_never_result_in_a_validated_token_1() + { + $this->expectException(InvalidRemoteVettingStateException::class); + $this->expectExceptionMessage('Unable to find a validated token'); + + $token = $this->createToken(); + $processId = ProcessId::create('12345'); + + $context = new RemoteVettingContext($this->session); + + $context->initialize('IRMA', $token, AttributeListDto::notSet()); + $context->validating($processId); + $context->validated($processId, AttributeListDto::notSet()); + + $context->getValidatedToken(); + } + + /** + * @test + * @group rv + */ + public function a_incomplete_remote_vetting_process_wil_never_result_in_a_validated_token_2() + { + $this->expectException(InvalidRemoteVettingStateException::class); + $this->expectExceptionMessage('Unable to find a validated token'); + + $token = $this->createToken(); + $processId = ProcessId::create('12345'); + + $context = new RemoteVettingContext($this->session); + + $context->initialize('IRMA', $token, AttributeListDto::notSet()); + $context->validating($processId); + + $context->getValidatedToken(); + } + + /** + * @test + * @group rv + */ + public function a_incomplete_remote_vetting_process_wil_never_result_in_a_validated_token_3() + { + $this->expectException(InvalidRemoteVettingStateException::class); + $this->expectExceptionMessage('Unable to find a validated token'); + + $token = $this->createToken(); + + $context = new RemoteVettingContext($this->session); + + $context->initialize('IRMA', $token, AttributeListDto::notSet()); + + $context->getValidatedToken(); + } + + /** + * @test + * @group rv + */ + public function a_incomplete_remote_vetting_process_wil_never_result_in_a_validated_token_4() + { + $this->expectException(InvalidRemoteVettingContextException::class); + $this->expectExceptionMessage('No remote vetting process found'); + + $context = new RemoteVettingContext($this->session); + + $context->getValidatedToken(); + } + + /** + * @test + * @group rv + */ + public function a_remote_vetting_process_which_is_not_initialized_could_not_be_validating() + { + $this->expectException(InvalidRemoteVettingContextException::class); + $this->expectExceptionMessage('No remote vetting process found'); + + $token = $this->createToken(); + $processId = ProcessId::create('12345'); + + $context = new RemoteVettingContext($this->session); + + $context->validating($processId); + } + + /** + * @test + * @group rv + */ + public function a_remote_vetting_process_which_is_not_validating_could_not_be_validated() + { + $this->expectException(InvalidRemoteVettingStateException::class); + $this->expectExceptionMessage('Unable to finish validation of a token'); + + $token = $this->createToken(); + $processId = ProcessId::create('12345'); + + $context = new RemoteVettingContext($this->session); + + $context->initialize('IRMA', $token, AttributeListDto::notSet()); + //$context->validating($processId); + $context->validated($processId, AttributeListDto::notSet()); + } + + /** + * @test + * @group rv + */ + public function a_remote_vetting_process_which_is_not_validated_should_never_result_in_a_validated_token() + { + $this->expectException(InvalidRemoteVettingStateException::class); + $this->expectExceptionMessage('Unable to end the validation of a token'); + + $token = $this->createToken(); + $processId = ProcessId::create('12345'); + + $context = new RemoteVettingContext($this->session); + + $context->initialize('IRMA', $token, AttributeListDto::notSet()); + $context->validating($processId); + //$context->validated($processId); + $context->done($processId); + } + + /** + * @test + * @group rv + */ + public function a_remote_vetting_process_validation_will_ony_work_with_a_matching_process_id() + { + $this->expectException(InvalidRemoteVettingContextException::class); + $this->expectExceptionMessage('Invalid remote vetting context found'); + + $token = $this->createToken(); + $processId = ProcessId::create('12345'); + + $wrongProcessId = ProcessId::create('wrong'); + + $context = new RemoteVettingContext($this->session); + + $context->initialize('IRMA', $token, AttributeListDto::notSet()); + $context->validating($processId); + $context->validated($wrongProcessId, AttributeListDto::notSet()); + } + + /** + * @test + * @group rv + */ + public function a_remote_vetting_process_finish_will_ony_work_with_a_matching_process_id() + { + $this->expectException(InvalidRemoteVettingContextException::class); + $this->expectExceptionMessage('Invalid remote vetting context found'); + + $token = $this->createToken(); + $processId = ProcessId::create('12345'); + + $wrongProcessId = ProcessId::create('wrong'); + + $context = new RemoteVettingContext($this->session); + + $context->initialize('IRMA', $token, AttributeListDto::notSet()); + $context->validating($processId); + $context->validated($wrongProcessId, AttributeListDto::notSet()); + $context->validated($processId, AttributeListDto::notSet()); + $context->matched($processId, AttributeListDto::notSet()); + $context->done($wrongProcessId); + } + + /** + * @test + * @group rv + */ + public function a_remote_vetting_process_could_not_be_started_with_A_method_other_then_initialize_1() + { + $this->expectException(InvalidRemoteVettingContextException::class); + $this->expectExceptionMessage('No remote vetting process found'); + + $processId = ProcessId::create('12345'); + + $context = new RemoteVettingContext($this->session); + + $context->validating($processId); + } + + /** + * @test + * @group rv + */ + public function a_remote_vetting_process_could_not_be_started_with_A_method_other_then_initialize_2() + { + $this->expectException(InvalidRemoteVettingContextException::class); + $this->expectExceptionMessage('No remote vetting process found'); + + $processId = ProcessId::create('No remote vetting process found'); + + $context = new RemoteVettingContext($this->session); + + $context->validated($processId, AttributeListDto::notSet()); + } + + /** + * @test + * @group rv + */ + public function a_remote_vetting_process_could_not_be_started_with_A_method_other_then_initialize_3() + { + $this->expectException(InvalidRemoteVettingContextException::class); + $this->expectExceptionMessage('No remote vetting process found'); + + $processId = ProcessId::create('12345'); + + $context = new RemoteVettingContext($this->session); + + $context->done($processId); + } + + + /** + * @test + * @group rv + */ + public function a_remote_vetting_process_holds_the_name_of_the_identity_provider() + { + $token = $this->createToken(); + + $context = new RemoteVettingContext($this->session); + + $context->initialize('IRMA', $token, AttributeListDto::notSet()); + + + $this->assertSame('IRMA', $context->getIdentityProviderSlug()); + } + + + /** + * @return RemoteVettingTokenDto + */ + private function createToken() + { + $identityId = Uuid::generate(); + $secondFactorId = Uuid::generate(); + + return RemoteVettingTokenDto::create($identityId, $secondFactorId); + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Value/AttributeCollectionInterfaceIntegrationTest.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Value/AttributeCollectionInterfaceIntegrationTest.php new file mode 100644 index 000000000..88d717ed6 --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVetting/Value/AttributeCollectionInterfaceIntegrationTest.php @@ -0,0 +1,78 @@ + ['John'], + 'lastName' => ['Doe'], + 'shacHomeOrganization' => ['institution-a.example.com'] + ]; + + $institutionAttributes = new AttributeListDto($attributes, 'johndoe.institution-a.example.com'); + $remoteVettingAttributes = new AttributeListDto(array_merge($attributes, ['documentNumber' => ["1234567890"]]), 'identifier-at-rv-idp'); + + $attributeCollection = new AttributeCollection($attributes); + $attributeMatches = new AttributeMatchCollection(); + foreach ($attributeCollection as $attribute) { + $attributeMatches->add('name', new AttributeMatch($attribute, $attribute, false, '')); + } + + $aggregate = new AttributeCollectionAggregate(); + $aggregate->add('local-attributes', $institutionAttributes); + $aggregate->add('remote-attributes', $remoteVettingAttributes); + $aggregate->add('attribute-matches', $attributeMatches); + + $smashed = $aggregate->getAttributes(); + + $this->assertTrue($this->isJsonSerializable($smashed)); + $this->assertArrayHasKey('local-attributes', $smashed); + $this->assertArrayHasKey('remote-attributes', $smashed); + $this->assertArrayHasKey('attribute-matches', $smashed); + } + + /** + * Simple validation logic to ensure no objects reside in the aggregated collections + * @param $value + * @return bool + */ + private function isJsonSerializable($value) + { + $return = true; + $wrapped = array($value); + array_walk_recursive($wrapped, function ($element) use (&$return) { + if (is_object($element) && !$element instanceof JsonSerializable) { + var_dump($element); die; + $return = false; + } + }); + return $return; + } +} diff --git a/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVettingServiceTestTest.php b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVettingServiceTestTest.php new file mode 100644 index 000000000..2a13faadc --- /dev/null +++ b/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Service/RemoteVettingServiceTestTest.php @@ -0,0 +1,213 @@ +publicKey = file_get_contents($kernelRootPath. '/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/encryption.crt'); + $this->privateKey = file_get_contents($kernelRootPath . '/src/Surfnet/StepupSelfService/SelfServiceBundle/Tests/Resources/encryption.key'); + + $this->remoteVettingConfiguration = m::mock(RemoteVettingConfiguration::class); + $this->applicationHelper = new ApplicationHelper($kernelRootPath); + $this->fakeIdentityWriter = new FakeIdentityWriter(); + $this->logger = new TestLogger(); + $session = new Session(new MockFileSessionStorage()); + + $identityEncrypter = new IdentityEncrypter($this->remoteVettingConfiguration, $this->fakeIdentityWriter); + $this->attributeMapper = new AttributeMapper($this->remoteVettingConfiguration); + $remoteVettingContext = new RemoteVettingContext($session); + + $now = new DateTime(); + $mockTime = new DateTime('@1614004537',$now->getTimezone()); + DateTimeHelper::setCurrentTime($mockTime); + + $this->service = new RemoteVettingService( + $remoteVettingContext, + $this->attributeMapper, + $identityEncrypter, + $this->applicationHelper, + $this->logger + ); + } + + public function test_happy_flow() + { + $processId = '4a46493c-7387-4c04-b491-a41f7323d73a'; + $identityId = '95515f44-71a2-4dc1-8e8f-e7e4021ee65b'; + $secondFactorId = 'dace3819-35e9-4205-8538-04bb09bdd479'; + + $nameId = "john.doe@example.com"; + $institution = "stepup.example.com"; + $email = "johndoe@example.com"; + $commonName = "John Doe"; + $preferredLocale = "nl_NL"; + + $processId = ProcessId::create($processId); + + $identity = Identity::fromData([ + 'id' => $identityId, + 'name_id' => $nameId, + 'institution' => $institution, + 'email' => $email, + 'common_name' => $commonName, + 'preferred_locale' => $preferredLocale, + ]); + + $localAttributes = new AttributeListDto(['email' => ['john@example.com'], 'firstName' => ['Johnie'], "familyName" => ["Doe"], "telephone" => ["0612345678"]], $nameId); + $externalAttributes = new AttributeListDto(['emailAddress' => ['johndoe@example.com'], 'givenName' => ['John'], "familyName" => ["Doe"], "fullName" => ["John Doe"]], $nameId); + $remarks = "This seems a pretty decent match"; + + $this->remoteVettingConfiguration->shouldReceive("getAttributeMapping") + ->with('mock') + ->andReturn([ + 'email' => 'emailAddress', + 'firstName' => 'givenName', + 'familyName' => 'familyName', + ]); + + $this->remoteVettingConfiguration->shouldReceive("getPublicKey") + ->andReturn($this->publicKey); + + $matches = $this->attributeMapper->map('mock', $localAttributes, $externalAttributes); + + // Update match + $match = $matches['email']; + $matches['email'] = new AttributeMatch($match->getLocalAttribute(), $match->getRemoteAttribute(), true, "This email is valid"); + + $feedbackCollection = new FeedbackCollection(); + $feedbackCollection["key1"] = ["val1"]; + $feedbackCollection["key2"] = ["val2"]; + + $token = RemoteVettingTokenDto::create($identityId, $secondFactorId); + $this->service->start('mock', $token); + $this->service->startValidation($processId); + $this->service->finishValidation($processId, $externalAttributes); + $remoteVettingToken = $this->service->done($processId, $identity, $localAttributes, $matches, $feedbackCollection); + + // test token result + $this->assertSame($identityId, $remoteVettingToken->getIdentityId()); + $this->assertSame($secondFactorId, $remoteVettingToken->getSecondFactorId()); + + // test encrypted result + $result = Decrypter::decrypt($this->fakeIdentityWriter->getData(), $this->privateKey); + $this->assertSame('{"attribute-data":{"local-attributes":{"nameId":"john.doe@example.com","attributes":{"email":["john@example.com"],"firstName":["Johnie"],"familyName":["Doe"],"telephone":["0612345678"]}},"remote-attributes":{"nameId":"john.doe@example.com","attributes":{"emailAddress":["johndoe@example.com"],"givenName":["John"],"familyName":["Doe"],"fullName":["John Doe"]}},"matching-results":{"email":{"local":{"name":"email","value":["john@example.com"]},"remote":{"name":"emailAddress","value":["johndoe@example.com"]},"is-valid":true,"remarks":"This email is valid"},"firstName":{"local":{"name":"firstName","value":["Johnie"]},"remote":{"name":"givenName","value":["John"]},"is-valid":false,"remarks":""},"familyName":{"local":{"name":"familyName","value":["Doe"]},"remote":{"name":"familyName","value":["Doe"]},"is-valid":false,"remarks":""}},"feedback":{"key1":["val1"],"key2":["val2"]}},"name-id":"john.doe@example.com","institution":"stepup.example.com","remote-vetting-source":"mock","application-version":"Stepup-SelfService","time":"2021-02-22T14:35:37+00:00"}', $result); + + // test logs + $this->assertSame([ + [ + 'level' => 'notice', + 'message' => 'Starting a remote vetting process', + 'context' => [ + 'second-factor' => 'dace3819-35e9-4205-8538-04bb09bdd479', + 'identity' => '95515f44-71a2-4dc1-8e8f-e7e4021ee65b', + 'provider' => 'mock', + ], + ],[ + 'level' => 'notice', + 'message' => 'Starting a remote vetting authentication', + 'context' => [ + 'second-factor' => 'dace3819-35e9-4205-8538-04bb09bdd479', + 'process' => '4a46493c-7387-4c04-b491-a41f7323d73a', + ], + ],[ + 'level' => 'notice', + 'message' => 'Finishing a remote vetting authentication', + 'context' => [ + 'second-factor' => 'dace3819-35e9-4205-8538-04bb09bdd479', + 'process' => '4a46493c-7387-4c04-b491-a41f7323d73a', + ], + ],[ + 'level' => 'notice', + 'message' => 'Saving the encrypted match data to the filesystem', + 'context' => [ + 'second-factor' => 'dace3819-35e9-4205-8538-04bb09bdd479', + 'process' => '4a46493c-7387-4c04-b491-a41f7323d73a', + ], + ],[ + 'level' => 'notice', + 'message' => 'Finished the remote vetting process', + 'context' => [ + 'second-factor' => 'dace3819-35e9-4205-8538-04bb09bdd479', + 'process' => '4a46493c-7387-4c04-b491-a41f7323d73a', + ], + ], + ], $this->logger->records); + } +} diff --git a/symfony.lock b/symfony.lock index 4259633df..b8d8b977f 100644 --- a/symfony.lock +++ b/symfony.lock @@ -101,6 +101,9 @@ "phar-io/version": { "version": "3.1.0" }, + "php": { + "version": "7.2" + }, "phpdocumentor/reflection-common": { "version": "2.2.0" }, @@ -256,6 +259,9 @@ "symfony/asset": { "version": "v4.4.19" }, + "symfony/browser-kit": { + "version": "v4.4.19" + }, "symfony/cache": { "version": "v4.4.19" }, @@ -278,12 +284,18 @@ "config/bootstrap.php" ] }, + "symfony/css-selector": { + "version": "v4.4.19" + }, "symfony/debug": { "version": "v4.4.19" }, "symfony/dependency-injection": { "version": "v4.4.19" }, + "symfony/dom-crawler": { + "version": "v4.4.19" + }, "symfony/error-handler": { "version": "v4.4.19" }, diff --git a/templates/dev/mock-acs-post.html.twig b/templates/dev/mock-acs-post.html.twig new file mode 100644 index 000000000..a959b70b9 --- /dev/null +++ b/templates/dev/mock-acs-post.html.twig @@ -0,0 +1,21 @@ + + +

Post SAML response back to SSO

+
+ + {% if response.relayState|length > 0 %} + + {% endif %} + +
+ + + diff --git a/templates/dev/mock-acs.html.twig b/templates/dev/mock-acs.html.twig new file mode 100644 index 000000000..e197b4d1a --- /dev/null +++ b/templates/dev/mock-acs.html.twig @@ -0,0 +1,41 @@ + +

Select response

+
+ +

Attributes

+ +
+ + {% for id, response in responses %} +
+ {% endfor %} +
+ diff --git a/translations/messages.en_GB.xliff b/translations/messages.en_GB.xliff index f6e231cf5..b20d8854f 100644 --- a/translations/messages.en_GB.xliff +++ b/translations/messages.en_GB.xliff @@ -1,668 +1,861 @@ - +
The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message.
+ + 1 + 1 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + 10 + 10 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + 2 + 2 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + 4 + 4 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + 5 + 5 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + 6 + 6 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + 612345678 612345678 - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/SendSmsChallengeType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/SendSmsChallengeType.php + + + 7 + 7 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + 8 + 8 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + 9 + 9 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php app.name Registration Portal - /Resources/views/base.html.twig - /Resources/views/base.html.twig + /templates/base.html.twig + /templates/base.html.twig + /templates/pdf.html.twig + /templates/pdf.html.twig button.logout Sign out - /Resources/views/base.html.twig + /templates/base.html.twig country code country code - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/SendSmsChallengeType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/SendSmsChallengeType.php locale.en_GB English - /../vendor/surfnet/stepup-bundle/src/Resources/views/translations.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/translations.twig locale.nl_NL Nederlands - /../vendor/surfnet/stepup-bundle/src/Resources/views/translations.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/translations.twig ss.flash.error_while_switching_locale Due to an unknown reason, switching locales failed. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/LocaleController.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/LocaleController.php ss.flash.invalid_switch_locale_form Due to an unknown reason, switching locales failed. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/LocaleController.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/LocaleController.php + + + ss.form.ss_remote_vet_feedback.no + No + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetAssertionMatchType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + ss.form.ss_remote_vet_feedback.rating + Rating + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + ss.form.ss_remote_vet_feedback.rating-explanation + Rating explanation + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + ss.form.ss_remote_vet_feedback.used-before + Used the provider before? + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + ss.form.ss_remote_vet_feedback.yes + Yes + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetAssertionMatchType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + ss.form.ss_remote_vet_second_factor.cancel + Cancel + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetValidationType.php + + + ss.form.ss_remote_vet_second_factor.feedback + ss.form.ss_remote_vet_second_factor.feedback + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetValidationType.php + + + ss.form.ss_remote_vet_second_factor.validate + Confirm + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetValidationType.php ss.form.ss_revoke_second_factor.cancel Cancel - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RevokeSecondFactorType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RevokeSecondFactorType.php ss.form.ss_revoke_second_factor.revoke Remove - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RevokeSecondFactorType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RevokeSecondFactorType.php ss.form.ss_send_sms_challenge.button.send_challenge Send code - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/SendSmsChallengeType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/SendSmsChallengeType.php ss.form.ss_verify_email.button.verify_email Verify e-mail - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifyEmailType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifyEmailType.php ss.form.ss_verify_email.text.verification_code Verification code - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifyEmailType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifyEmailType.php ss.form.ss_verify_sms_challenge.button.resend_challenge Send new code - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifySmsChallengeType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifySmsChallengeType.php ss.form.ss_verify_sms_challenge.button.verify_challenge Verify - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifySmsChallengeType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifySmsChallengeType.php ss.form.ss_verify_sms_challenge.text.challenge Code - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifySmsChallengeType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifySmsChallengeType.php ss.prove_phone_possession.challenge_expired Your code has expired. Please request a new code. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.prove_phone_possession.challenge_request_limit_reached You have exceeded the limit of three codes; you can no longer request any more codes. Contact your helpdesk or try again later. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.prove_phone_possession.challenge_response_incorrect The code you entered does not match. Please try again or request a new code. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.prove_phone_possession.incorrect_challenge_response The code you entered does not match. Please try again or request a new code. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.prove_phone_possession.proof_of_possession_failed The token could not be created due to unknown reasons. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.prove_phone_possession.send_sms_challenge_failed Sending the SMS failed. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.prove_phone_possession.too_many_attempts You have exceeded the limit of ten attempts; you can no longer attempt verification of any more codes. Contact your helpdesk or try again later. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.prove_yubikey_possession.proof_of_possession_failed The token could not be created due to unknown reasons. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.email_verification_email_sent.text.email_verification_has_been_sent Check your inbox. A verification e-mail has been sent to the e-mail address %email%. Please follow the instructions in this e-mail to continue the registration process. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/emailVerificationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/email_verification_email_sent.html.twig ss.registration.email_verification_email_sent.title Verify your e-mail - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/emailVerificationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/email_verification_email_sent.html.twig ss.registration.progress_bar.confirm_second_factor Confirm - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/partial/progressBar.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/progress_bar.html.twig ss.registration.progress_bar.link_second_factor Link token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/partial/progressBar.html.twig - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/partial/progressBar.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/progress_bar.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/progress_bar.html.twig ss.registration.progress_bar.register_second_factor Activate token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/partial/progressBar.html.twig - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/partial/progressBar.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/progress_bar.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/progress_bar.html.twig ss.registration.progress_bar.select_second_factor Select token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/partial/progressBar.html.twig - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/partial/progressBar.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/progress_bar.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/progress_bar.html.twig ss.registration.registration_email_sent.label.expiration_date The activation code is valid until and including %expirationDate%. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.label.registration_code Activation code - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.activation_instructions Visit the location below to get your token activated. Please bring the following: - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.activation_instructions_item_1 Your token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.activation_instructions_item_2 A valid proof of identity (passport, driver's license or national ID-card) - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.activation_instructions_item_3 Your activation code - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.no_ra_locations_for_your_institution There are no locations available within your institution to activate your token. Please contact your helpdesk for support. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.no_ras_for_your_institution There are no locations available within your institution to activate your token. Please contact your helpdesk for support. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.registration_code_has_been_sent An e-mail containing these instructions and your activation code has also been sent to the e-mail address %email%. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.registration_code_has_been_sent_no_email If you do not have access (yet) to your e-mail address you can also print or download the instructions and your activation code. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.registration_code_has_been_sent_pdf PDF - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.registration_code_has_been_sent_print Print - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.thank_you_for_registration Thank you for registering your token. Your token is almost ready to use. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.title Activation code - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/registration_email_sent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/registration_email_sent_pdf.html.twig ss.registration.registration_email_sent.title.list_of_ra_locations Location(s) to activate your token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.title.list_of_ras Location(s) to activate your token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig + + + ss.registration.selector.on-premise.alt + On premise + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig ss.registration.selector.sms.alt SMS security token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.selector.sms.button.use Select - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.selector.sms.description Log in with a one time SMS code. For all mobile phones. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.selector.sms.title SMS - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.selector.title.welcome Select token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/displaySecondFactorTypes.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_second_factor_types.html.twig ss.registration.selector.yubikey.alt YubiKey token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.selector.yubikey.button.use Select - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.selector.yubikey.description Log in with a USB hardware token. For all devices with a USB port. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.selector.yubikey.title YubiKey - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.sms.alert.no_verification_state Your session has expired. Please request a new code. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.sms.challenge_body Your SMS code: %challenge% - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Service/SmsSecondFactorService.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Service/SmsSecondFactorService.php ss.registration.sms.prove_possession.title.page Enter SMS code - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Sms/provePossession.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/sms/prove_possession.html.twig ss.registration.sms.send_challenge.title.page Send SMS code - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Sms/sendChallenge.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/sms/send_challenge.html.twig ss.registration.sms.text.ensure_phone_has_signal Please ensure your mobile phone has a signal and can receive text messages. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Sms/sendChallenge.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/sms/send_challenge.html.twig ss.registration.sms.text.enter_challenge_below Enter the code that was sent to your phone and click 'Verify' - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Sms/provePossession.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/sms/prove_possession.html.twig ss.registration.sms.text.enter_phone_number_below Please enter your mobile phone number below: - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Sms/sendChallenge.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/sms/send_challenge.html.twig ss.registration.sms.text.help_user_enter_challenge Please validate that you can receive SMS messages on this phone: - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Sms/provePossession.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/sms/prove_possession.html.twig ss.registration.sms.text.otp_requests_remaining Attempts remaining: %count% - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Sms/sendChallenge.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/sms/send_challenge.html.twig ss.registration.sms.text.retry_if_not_received Click 'Send new code' if you did not receive a code. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Sms/provePossession.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/sms/prove_possession.html.twig ss.registration.verify_email.text.verification_failed E-mail verification failed. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/verifyEmail.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/verify_email.html.twig ss.registration.verify_email.title Verify your e-mail address - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/verifyEmail.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/verify_email.html.twig + + + ss.registration.vetting_type.button.ra_vetting + Continue + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig + + + ss.registration.vetting_type.description.ra_vetting + Vet your second factor token at one of your registration authority locations. + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig + + + ss.registration.vetting_type.description.vetting + Prove your identity using one of our verification methods. Physical vetting at one of your registration authority locations, ReadId document scanner (read your passport, drivers licence or ID card using the ReadId connect app and a NFF compatible document), IRMA (RMA offers a way for privacy-friendly authentication. When authenticating, you as a user reveal only relevant properties (attributes) of yourself, using an IRMA app on your mobile phone — for instance that you are older than 16.) or iDIN (bank). + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig + + + ss.registration.vetting_type.title + Choose your prefered vetting method + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig + + + ss.registration.vetting_type.title.ra_vetting + RA vetting + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig ss.registration.yubikey.text.connect_yubikey_to_pc Put your YubiKey in a USB port on your computer. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Yubikey/provePossession.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/yubikey/prove_possession.html.twig ss.registration.yubikey.text.ensure_form_field_focus Please ensure that the form field below has focus. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Yubikey/provePossession.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/yubikey/prove_possession.html.twig ss.registration.yubikey.text.press_once_form_auto_submitted Press and hold the button on your Yubikey. A One Time Password will be entered in the field below automatically. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Yubikey/provePossession.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/yubikey/prove_possession.html.twig ss.registration.yubikey.title.enter_challenge Link your YubiKey - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Yubikey/provePossession.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/yubikey/prove_possession.html.twig ss.second_factor.list.button.register_second_factor Add token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor.list.text.no_second_factors There are no tokens registered for your account. Click on 'Add token' to register a new token. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor.list.text.unverified For the token(s) below the e-mail address must be verified. An e-mail was sent to '%email%'. Please follow the instructions in this e-mail to continue with the registration. Didn't receive an e-mail? Remove the token to try again. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor.list.text.verified The following tokens are registered for your account, but not yet activated. An e-mail with your activation code has been sent to the e-mail address %email%. Please follow the instructions in the e-mail on how to proceed. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor.list.text.vetted The following tokens are registered for your account. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor.list.title Token Overview - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig + + + ss.second_factor.remote_vet.attribute.givenName + Given name + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + + + ss.second_factor.remote_vet.attribute.surname + Surname + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + + + ss.second_factor.remote_vet.attribute_name + Name + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig + + + ss.second_factor.remote_vet.feedback + ss.second_factor.remote_vet.feedback + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig + + + ss.second_factor.remote_vet.is_attribute_valid + The attribute is valid + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig + + + ss.second_factor.remote_vet.personal_data + ss.second_factor.remote_vet.personal_data + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig + + + ss.second_factor.remote_vet.remarks + Remarks + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig + + + ss.second_factor.remote_vet.schachome.institution-d.example.com + Institution D + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + + + ss.second_factor.remote_vet.text.does_it_match + Is the supplied information correct? + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig + + + ss.second_factor.remote_vet_validation.title + Questions for you + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig + + + ss.second_factor.revoke.alert.remote_vetting_failed + Unable to validate the information + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + + + ss.second_factor.revoke.alert.remote_vetting_invalid_context + Unable to find an active remote vetting token process + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + + + ss.second_factor.revoke.alert.remote_vetting_successful + Your identity information was validated successfully + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor.revoke.alert.revocation_failed Token revocation failed - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor.revoke.alert.revocation_successful Your token has been removed. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + + + ss.second_factor.revoke.button.remote_vet + Validate identity + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor.revoke.button.revoke Remove - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor.revoke.button.test Test a token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor.revoke.second_factor_type.sms SMS - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor.revoke.second_factor_type.yubikey YubiKey - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor.revoke.table_header.second_factor.identifier ID - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/revoke.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/revoke.html.twig ss.second_factor.revoke.table_header.type Token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/revoke.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/revoke.html.twig ss.second_factor.revoke.text.are_you_sure You are about to remove the following token. Are you sure? - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/revoke.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/revoke.html.twig ss.second_factor.revoke.title Remove token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/revoke.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/revoke.html.twig ss.second_factor.type.sms SMS - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor.type.yubikey YubiKey - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor_list.header.expiration_date Expiration date - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor_list.header.expired_explanation The token registration period has expired. Please remove your token and restart the registration process. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor_list.header.expired_warning Expired - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor_list.header.second_factor_identifier ID - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor_list.header.type Token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.security.session_expired.click_to_login Click here to log in again - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Security/sessionExpired.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/security/session_expired.html.twig ss.security.session_expired.explanation Your session has expired due to either prolonged inactivity or being logged in for too long. You have been automatically logged out and are required to log in again to be able to continue. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Security/sessionExpired.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/security/session_expired.html.twig ss.security.session_expired.page_title Session Expired - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Security/sessionExpired.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/security/session_expired.html.twig ss.support_url_text Help - /Resources/views/base.html.twig + /templates/base.html.twig ss.test_second_factor.verification_failed The test with your token failed. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.test_second_factor.verification_successful The test with your token was successful. You can login with your token. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.verify_yubikey_command.otp.otp_invalid This YubiKey code was invalid. Please try again. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.verify_yubikey_command.otp.verification_error The verification of the YubiKey code failed due to unknown reasons. Please try again. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig stepup.error.authentication_error.description Sign in unsuccessful. Please try again. - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.authentication_error.title Sign in - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.authn_failed.description Sign in unsuccessful. Please try again. - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.authn_failed.title Sign in - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.error_code Error code - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig stepup.error.generic_error.description Something went wrong. Please try again. - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.generic_error.title Oops! - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.hostname Application - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig stepup.error.ip_address IP address - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig stepup.error.missing_required_attribute.title Missing required attribute - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/ExceptionController.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/ExceptionController.php stepup.error.page_not_found.text The page you requested was not found. Please try again or go back to Home. - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error404.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error404.html.twig stepup.error.page_not_found.title Page not found - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error404.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error404.html.twig stepup.error.precondition_not_met.description You are not authorised to sign in - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.precondition_not_met.title Not authorised to sign in - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.request_id Request ID - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig stepup.error.signature_validation_failed.description The SAML request has been signed but the signature could not be validated. - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.signature_validation_failed.title Signature validation failed - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.support_page.text the support page if this does not fix your problem. On this page you will find more information about possible causes of the error and how to contact the support team.]]> - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error404.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error404.html.twig stepup.error.timestamp Time - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig stepup.error.unknown_service_provider.title Unknown service provider - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.unsigned_request.description The SAML request is expected to be signed but it was not - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.unsigned_request.title Unsigned request - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.unsupported_signature.description The SAMLRequest has been signed, but the signature format is not supported - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.unsupported_signature.title Unsupported signature format - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.user_agent User agent - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig stepup_middleware_client.form.switch_locale.switch Switch - /../vendor/surfnet/stepup-bundle/src/Form/Type/SwitchLocaleType.php + /vendor/surfnet/stepup-bundle/src/Form/Type/SwitchLocaleType.php subscriberNumber Number - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/SendSmsChallengeType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/SendSmsChallengeType.php
diff --git a/translations/messages.nl_NL.xliff b/translations/messages.nl_NL.xliff index 6537ed2ba..1e4b78e70 100644 --- a/translations/messages.nl_NL.xliff +++ b/translations/messages.nl_NL.xliff @@ -1,666 +1,865 @@ - +
The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message.
+ + 1 + 1 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + 10 + 10 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + 2 + 2 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + 4 + 4 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + 5 + 5 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + 6 + 6 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + 612345678 612345678 - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/SendSmsChallengeType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/SendSmsChallengeType.php + + + 7 + 7 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + 8 + 8 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + 9 + 9 + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php app.name Registratieportal - /Resources/views/base.html.twig - /Resources/views/base.html.twig + /templates/base.html.twig + /templates/base.html.twig + /templates/pdf.html.twig + /templates/pdf.html.twig button.logout Uitloggen - /Resources/views/base.html.twig + /templates/base.html.twig country code land code - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/SendSmsChallengeType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/SendSmsChallengeType.php locale.en_GB English - /../vendor/surfnet/stepup-bundle/src/Resources/views/translations.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/translations.twig locale.nl_NL Nederlands - /../vendor/surfnet/stepup-bundle/src/Resources/views/translations.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/translations.twig ss.flash.error_while_switching_locale Door een onbekende oorzaak is het wisselen van taal mislukt. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/LocaleController.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/LocaleController.php ss.flash.invalid_switch_locale_form Door een onbekende oorzaak is het wisselen van taal mislukt. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/LocaleController.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/LocaleController.php + + + ss.form.ss_remote_vet_feedback.no + Nee + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetAssertionMatchType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + ss.form.ss_remote_vet_feedback.rating + Hoe makkelijk vond je het gebruik van de dienst (1=heel moeilijk, 10=heel makkelijk) + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + ss.form.ss_remote_vet_feedback.rating-explanation + Toelichting + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + ss.form.ss_remote_vet_feedback.used-before + Heb je SurfSecureID eerder gebruikt om via de servicedesk van je instelling een token te activeren + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + ss.form.ss_remote_vet_feedback.yes + Ja + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetAssertionMatchType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + + + ss.form.ss_remote_vet_second_factor.cancel + Annuleren + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetValidationType.php + + + ss.form.ss_remote_vet_second_factor.feedback + Over je ervaringen + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetValidationType.php + + + ss.form.ss_remote_vet_second_factor.validate + Bevestig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetValidationType.php ss.form.ss_revoke_second_factor.cancel Annuleren - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RevokeSecondFactorType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RevokeSecondFactorType.php ss.form.ss_revoke_second_factor.revoke Verwijderen - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RevokeSecondFactorType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RevokeSecondFactorType.php ss.form.ss_send_sms_challenge.button.send_challenge Verstuur code - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/SendSmsChallengeType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/SendSmsChallengeType.php ss.form.ss_verify_email.button.verify_email E-mail bevestigen - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifyEmailType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifyEmailType.php ss.form.ss_verify_email.text.verification_code Verificatiecode - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifyEmailType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifyEmailType.php ss.form.ss_verify_sms_challenge.button.resend_challenge Stuur een nieuwe code - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifySmsChallengeType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifySmsChallengeType.php ss.form.ss_verify_sms_challenge.button.verify_challenge Controleren - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifySmsChallengeType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifySmsChallengeType.php ss.form.ss_verify_sms_challenge.text.challenge Code - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifySmsChallengeType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/VerifySmsChallengeType.php ss.prove_phone_possession.challenge_expired Deze code is verlopen. Vraag een nieuwe code aan. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.prove_phone_possession.challenge_request_limit_reached Je hebt de limiet van drie codes bereikt; je kunt geen codes meer aanvragen. Neem contact op met de helpdesk van je instelling of probeer het later nog eens. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.prove_phone_possession.challenge_response_incorrect De code die je ingevoerd hebt komt niet overeen met de code die je hebt ontvangen. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.prove_phone_possession.incorrect_challenge_response De ingevoerde code is onjuist. Probeer het nog eens, of vraag een nieuwe code op. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.prove_phone_possession.proof_of_possession_failed Het token kon wegens een onbekende reden niet aangemaakt worden. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.prove_phone_possession.send_sms_challenge_failed Het versturen van de code per SMS is mislukt. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.prove_phone_possession.too_many_attempts U heeft de limiet van tien pogingen bereikt; u kunt geen codes meer verifiëren. Neem contact op met uw helpdesk of probeer het later nog eens. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.prove_yubikey_possession.proof_of_possession_failed Het token kon wegens een onbekende reden niet aangemaakt worden. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.email_verification_email_sent.text.email_verification_has_been_sent Controleer je inbox. Er is een verificatie e-mail verstuurd naar het e-mailadres '%email%'. Volg de instructies in deze e-mail om het registratieproces te vervolgen. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/emailVerificationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/email_verification_email_sent.html.twig ss.registration.email_verification_email_sent.title Bevestig je e-mailadres - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/emailVerificationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/email_verification_email_sent.html.twig ss.registration.progress_bar.confirm_second_factor Bevestig e-email - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/partial/progressBar.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/progress_bar.html.twig ss.registration.progress_bar.link_second_factor Koppel token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/partial/progressBar.html.twig - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/partial/progressBar.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/progress_bar.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/progress_bar.html.twig ss.registration.progress_bar.register_second_factor Activeer token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/partial/progressBar.html.twig - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/partial/progressBar.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/progress_bar.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/progress_bar.html.twig ss.registration.progress_bar.select_second_factor Selecteer token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/partial/progressBar.html.twig - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/partial/progressBar.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/progress_bar.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/progress_bar.html.twig ss.registration.registration_email_sent.label.expiration_date Je activatiecode is geldig tot en met %expirationDate%. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.label.registration_code Activatiecode - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.activation_instructions Ga naar onderstaande locatie om je token te laten activeren. Neem daarbij het volgende mee: - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.activation_instructions_item_1 Je token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.activation_instructions_item_2 Een geldig legitimatiebewijs (paspoort, rijbewijs of nationale ID-kaart) - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.activation_instructions_item_3 Je activatiecode - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.no_ra_locations_for_your_institution Er zijn geen locaties beschikbaar binnen je instelling om je token te activeren. Neem contact op met je helpdesk voor support. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.no_ras_for_your_institution Er zijn geen locaties beschikbaar binnen je instelling om je token te activeren. Neem contact op met je helpdesk voor support. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.registration_code_has_been_sent Een e-mail met deze instructies en je activatiecode is ook naar het e-mailadres ‘%email%’ verstuurd. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.registration_code_has_been_sent_no_email Mocht je (nog) geen toegang hebben tot dit e-mailadres dan kun je de instructies en activatiecode ook printen of downloaden. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.registration_code_has_been_sent_pdf PDF - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.registration_code_has_been_sent_print Print - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.text.thank_you_for_registration Bedankt voor het registreren van je token. Je token is nu bijna klaar voor gebruik. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.title Activatiecode - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/registration_email_sent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/registration_email_sent_pdf.html.twig ss.registration.registration_email_sent.title.list_of_ra_locations Locatie(s) om je token te activeren - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig ss.registration.registration_email_sent.title.list_of_ras Locatie(s) om je token te activeren - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/registrationEmailSent.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/partial/registration_email_sent.html.twig + + + ss.registration.selector.on-premise.alt + Op locatie + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig ss.registration.selector.sms.alt SMS-beveiligingstoken - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.selector.sms.button.use Selecteer - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.selector.sms.description Log in met een eenmalige SMS-code. Geschikt voor alle mobiele telefoons. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.selector.sms.title SMS - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.selector.title.welcome Selecteer token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/displaySecondFactorTypes.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_second_factor_types.html.twig ss.registration.selector.yubikey.alt YubiKey-token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.selector.yubikey.button.use Selecteer - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.selector.yubikey.description Log in met een USB hardware token. Geschikt voor alle devices met een USB-poort. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.selector.yubikey.title YubiKey - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.sms.alert.no_verification_state Uw sessie is verlopen. Vraag een nieuwe code aan. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.registration.sms.challenge_body Uw SMS-code: %challenge% - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Service/SmsSecondFactorService.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Service/SmsSecondFactorService.php ss.registration.sms.prove_possession.title.page SMS-code invoeren - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Sms/provePossession.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/sms/prove_possession.html.twig ss.registration.sms.send_challenge.title.page SMS-code versturen - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Sms/sendChallenge.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/sms/send_challenge.html.twig ss.registration.sms.text.ensure_phone_has_signal Zorg dat je mobiele telefoon bereik heeft en SMS-berichten kan ontvangen - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Sms/sendChallenge.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/sms/send_challenge.html.twig ss.registration.sms.text.enter_challenge_below Voer de code in die naar je mobiele telefoon is gestuurd en klik op 'Controleren' - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Sms/provePossession.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/sms/prove_possession.html.twig ss.registration.sms.text.enter_phone_number_below Vul hieronder je mobiele nummer in en klik op 'Verstuur code' - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Sms/sendChallenge.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/sms/send_challenge.html.twig ss.registration.sms.text.help_user_enter_challenge Bewijs dat je op deze telefoon SMS-berichten kunt ontvangen. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Sms/provePossession.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/sms/prove_possession.html.twig ss.registration.sms.text.otp_requests_remaining Aantal resterende pogingen: %count% - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Sms/sendChallenge.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/sms/send_challenge.html.twig ss.registration.sms.text.retry_if_not_received Geen code ontvangen? Klik dan op 'Stuur een nieuwe code' - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Sms/provePossession.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/sms/prove_possession.html.twig ss.registration.verify_email.text.verification_failed De e-mailverificatie is om onbekende reden mislukt. Probeer het opnieuw of neem contact op met een beheerder. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/verifyEmail.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/verify_email.html.twig ss.registration.verify_email.title E-mail verifiëren - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/verifyEmail.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/verify_email.html.twig + + + ss.registration.vetting_type.button.ra_vetting + Verder + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig + + + ss.registration.vetting_type.description.ra_vetting + Activeer je token bij een van de RA locaties. + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig + + + ss.registration.vetting_type.description.vetting + Activeer je token met behulp van een van de beschikbare remote vetting identificatiemiddelen (Fysiek op een RA-locatie, iDIN via bank, IRMA via app of ReadID scan een NFC id-bewijs) + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig + + + ss.registration.vetting_type.title + Kies een activatiemethode + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig + + + ss.registration.vetting_type.title.ra_vetting + RA vetting + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/display_vetting_types.html.twig ss.registration.yubikey.text.connect_yubikey_to_pc Stop je YubiKey in een USB-poort van je computer. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Yubikey/provePossession.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/yubikey/prove_possession.html.twig ss.registration.yubikey.text.ensure_form_field_focus Zorg dat het invulveld hieronder actief is. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Yubikey/provePossession.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/yubikey/prove_possession.html.twig ss.registration.yubikey.text.press_once_form_auto_submitted Druk op de knop van je YubiKey en houd even vast. Er verschijnt automatisch een eenmalige code in het invulveld. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Yubikey/provePossession.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/yubikey/prove_possession.html.twig ss.registration.yubikey.title.enter_challenge Koppel je YubiKey - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Registration/Yubikey/provePossession.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/registration/yubikey/prove_possession.html.twig ss.second_factor.list.button.register_second_factor Token toevoegen - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor.list.text.no_second_factors Er zijn geen tokens geregistreerd voor jouw account. Klik op 'Token toevoegen' om een nieuw token te registreren. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor.list.text.unverified Voor de onderstaande token(s) moet het e-mailadres nog bevestigd worden. Er is een e-mail verstuurd naar '%email%'. Volg de instructies in deze e-mail om verder te gaan met de registratie. Geen e-mail ontvangen? Verwijder het token om het opnieuw te proberen. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor.list.text.verified De volgende token(s) zijn geregistreerd voor jouw account, maar nog niet geactiveerd. Er is een e-mail met activatiecode gestuurd naar het e-mailadres %email%. Volg de instructies uit de e-mail om je token te activeren. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor.list.text.vetted De volgende token(s) zijn geregistreerd voor jouw account. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor.list.title Overzicht tokens - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig + + + ss.second_factor.remote_vet.attribute.givenName + Voornaam + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + + + ss.second_factor.remote_vet.attribute.surname + Achternaam + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + + + ss.second_factor.remote_vet.attribute_name + Persoonsgegeven + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig + + + ss.second_factor.remote_vet.feedback + Over je ervaringen + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig + + + ss.second_factor.remote_vet.is_attribute_valid + Komen overeen + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig + + + ss.second_factor.remote_vet.personal_data + Persoonsgegevens + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig + + + ss.second_factor.remote_vet.remarks + Wat wil je verder nog aan ons kwijt? + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/RemoteVetFeedbackType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig + + + ss.second_factor.remote_vet.schachome.institution-d.example.com + Instelling D + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + + + ss.second_factor.remote_vet.text.does_it_match + + Hieronder vind je zowel de identiteitsgegevens die we van je instelling %organization% ontvangen hebben als de identiteitsgegevens die we van %provider% ontvangen hebben. Uiteindelijk willen we het systeem deze identiteitsgegevens automatisch met elkaar laten vergelijken om te bepalen of het om de zelfde persoon gaat en je token te activeren. + + Vaak komen gegevens van dezelfde persoon niet helemaal overeen. Daarom vragen we jou om de vergelijking nu zelf te doen en om dingen die jou opvallen in je eigen gegevens te melden zodat wij hiervan kunnen leren. We stellen je ook een aantal vragen over je ervaringen tijdens het gebruik van de deze proof-of-concept van de remote vetting functie van SURFsecureID. + + Als je op "bewaar" klikt worden de gegevens die je op dit scherm versleuteld opgeslagen. We gebruiken deze gegevens alleen voor het ontwikkelen en verbeteren van de SURFsecureID en zullen deze aan het einde van de proof of concept, uiterlijk 1-10-2021, vernietigen. Wil je niet dat je gegevens worden bewaard, kies dan voor "Annuleer" of sluit dit scherm. + + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig + + + ss.second_factor.remote_vet_validation.title + Vragen aan jou + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/remote_vetting/validation.html.twig + + + ss.second_factor.revoke.alert.remote_vetting_failed + De identiteitsinformatie kon niet worden gevalideerd + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + + + ss.second_factor.revoke.alert.remote_vetting_invalid_context + Er kon geen actief remote vetting proces gevonden worden + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + + + ss.second_factor.revoke.alert.remote_vetting_successful + De identiteitsinformatie is succesvol gevalideerd + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor.revoke.alert.revocation_failed Token intrekken is mislukt - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor.revoke.alert.revocation_successful Je token is verwijderd. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + + + ss.second_factor.revoke.button.remote_vet + Valideer identiteit + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor.revoke.button.revoke Verwijderen - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor.revoke.button.test Test een token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor.revoke.second_factor_type.sms SMS - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor.revoke.second_factor_type.yubikey YubiKey - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor.revoke.table_header.second_factor.identifier ID - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/revoke.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/revoke.html.twig ss.second_factor.revoke.table_header.type Token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/revoke.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/revoke.html.twig ss.second_factor.revoke.text.are_you_sure Je gaat het volgende token verwijderen. Weet je het zeker? - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/revoke.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/revoke.html.twig ss.second_factor.revoke.title Verwijder token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/revoke.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/revoke.html.twig ss.second_factor.type.sms SMS - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor.type.yubikey YubiKey - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.second_factor_list.header.expiration_date Verloopdatum - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor_list.header.expired_explanation De uiterste registratiedatum is verlopen. Registreer het token opnieuw door deze te verwijderen en het registratieproces opnieuw te starten. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor_list.header.expired_warning Verlopen - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor_list.header.second_factor_identifier ID - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.second_factor_list.header.type Token - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/SecondFactor/list.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/second_factor/list.html.twig ss.security.session_expired.click_to_login Klik hier om opnieuw in te loggen - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Security/sessionExpired.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/security/session_expired.html.twig ss.security.session_expired.explanation Uw sessie is verlopen. Dit kan komen doordat er te lang geen activiteit is geweest, of doordat u te lang bent ingelogd. U bent automatisch uitgelogd en om verder te gaan dient u eerst opnieuw in te loggen. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Security/sessionExpired.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/security/session_expired.html.twig ss.security.session_expired.page_title Sessie Verlopen - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/Security/sessionExpired.html.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/security/session_expired.html.twig ss.support_url_text Help - /Resources/views/base.html.twig + /templates/base.html.twig ss.test_second_factor.verification_failed De test met je token is mislukt. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.test_second_factor.verification_successful De test met je token is geslaagd. Je kunt inloggen met je token. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.verify_yubikey_command.otp.otp_invalid Deze YubiKey code was ongeldig. Probeer het nog eens. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig ss.verify_yubikey_command.otp.verification_error Het verifiëren van de YubiKey-code is wegens een onbekende reden nigelukt. Probeer het opnieuw. - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig + /src/Surfnet/StepupSelfService/SelfServiceBundle/Resources/views/translations.twig stepup.error.authentication_error.description Inloggen mislukt. Probeer het nog eens. - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.authentication_error.title Inloggen - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.authn_failed.description Inloggen mislukt. Probeer het nog eens. - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.authn_failed.title Inloggen - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.error_code Foutcode - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig stepup.error.generic_error.description Er is iets mis gegaan. Probeer het opnieuw. - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.generic_error.title Oeps! - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.hostname Applicatie - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig stepup.error.ip_address IP-adres - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig stepup.error.missing_required_attribute.title Attribuut ontbreekt - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/ExceptionController.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/ExceptionController.php stepup.error.page_not_found.text De pagina die je zocht kan niet gevonden worden. Probeer het nog eens, of ga terug naar Home. - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error404.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error404.html.twig stepup.error.page_not_found.title Pagina niet gevonden - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error404.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error404.html.twig stepup.error.precondition_not_met.description Je hebt niet de juiste rechten om in te mogen loggen. - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.precondition_not_met.title Onvoldoende rechten om in te loggen - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.request_id Request ID - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig stepup.error.signature_validation_failed.description Het SAML bericht is ondertekend maar de signature kan niet gevalideerd worden - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.signature_validation_failed.title Verificatie van signature mislukt - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.support_page.text de support pagina als dit je probleem niet oplost. Op deze pagina vind je meer informatie over de mogelijk oorzaken en hoe je contact kan opnemen met het supportteam.]]> - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error404.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error404.html.twig stepup.error.timestamp Tijd - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig stepup.error.unknown_service_provider.title Onbekende serviceprovider - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.unsigned_request.description Het SAML bericht moet ondertekend zijn maar bevat geen signature - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.unsigned_request.title Geen signature in SAML bericht - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.unsupported_signature.description Het SAML bericht is ondertekend, maar het signature formaat wordt niet ondersteund - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.unsupported_signature.title Signature formaat wordt niet ondersteund - /../vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php + /vendor/surfnet/stepup-bundle/src/Controller/ExceptionController.php stepup.error.user_agent User agent - /../vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig + /vendor/surfnet/stepup-bundle/src/Resources/views/Exception/error.html.twig stepup_middleware_client.form.switch_locale.switch Vertalen - /../vendor/surfnet/stepup-bundle/src/Form/Type/SwitchLocaleType.php + /vendor/surfnet/stepup-bundle/src/Form/Type/SwitchLocaleType.php subscriberNumber Nummer - /../src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/SendSmsChallengeType.php + /src/Surfnet/StepupSelfService/SelfServiceBundle/Form/Type/SendSmsChallengeType.php
diff --git a/translations/validators.en_GB.xliff b/translations/validators.en_GB.xliff index 1f059b6df..7833b1b55 100644 --- a/translations/validators.en_GB.xliff +++ b/translations/validators.en_GB.xliff @@ -1,87 +1,19 @@ - +
The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message.
- - Register request AppID may not be empty - Register request AppID may not be empty - - - Register request AppID must be a string - Register request AppID must be a string - - - Register request challenge may not be empty - Register request challenge may not be empty - - - Register request challenge must be a string - Register request challenge must be a string - - - Register request error code may not be empty - Register request error code may not be empty - - - Register request error code must be numeric - Register request error code must be numeric - - - Register request version may not be empty - Register request version may not be empty - - - Register request version must be a string - Register request version must be a string - - - Sign request AppID may not be empty - Sign request AppID may not be empty - - - Sign request AppID must be a string - Sign request AppID must be a string - - - Sign request challenge may not be empty - Sign request challenge may not be empty - - - Sign request challenge must be a string - Sign request challenge must be a string - - - Sign request error code may not be empty - Sign request error code may not be empty - - - Sign request error code must be numeric - Sign request error code must be numeric - - - Sign request key handle may not be empty - Sign request key handle may not be empty - - - Sign request key handle must be a string - Sign request key handle must be a string - - - Sign request version may not be empty - Sign request version may not be empty - - - Sign request version must be a string - Sign request version must be a string - middleware_client.dto.configuration.allowed_second_factors.must_be_array middleware_client.dto.configuration.allowed_second_factors.must_be_array + + middleware_client.dto.configuration.number_of_tokens_per_identity.must_be_integer + middleware_client.dto.configuration.number_of_tokens_per_identity.must_be_integer + middleware_client.dto.configuration.show_raa_contact_information.must_be_boolean middleware_client.dto.configuration.show_raa_contact_information.must_be_boolean @@ -190,6 +122,14 @@ middleware_client.dto.ra_candidate.name_id.must_not_be_blank middleware_client.dto.ra_candidate.name_id.must_not_be_blank + + middleware_client.dto.ra_candidate_institution.institution.must_be_string + middleware_client.dto.ra_candidate_institution.institution.must_be_string + + + middleware_client.dto.ra_candidate_institution.institution.must_not_be_blank + middleware_client.dto.ra_candidate_institution.institution.must_not_be_blank + middleware_client.dto.ra_credentials.common_name.must_be_null_or_string middleware_client.dto.ra_credentials.common_name.must_be_null_or_string diff --git a/translations/validators.nl_NL.xliff b/translations/validators.nl_NL.xliff index 188bdb199..79c476948 100644 --- a/translations/validators.nl_NL.xliff +++ b/translations/validators.nl_NL.xliff @@ -1,87 +1,19 @@ - +
The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message.
- - Register request AppID may not be empty - Register request AppID may not be empty - - - Register request AppID must be a string - Register request AppID must be a string - - - Register request challenge may not be empty - Register request challenge may not be empty - - - Register request challenge must be a string - Register request challenge must be a string - - - Register request error code may not be empty - Register request error code may not be empty - - - Register request error code must be numeric - Register request error code must be numeric - - - Register request version may not be empty - Register request version may not be empty - - - Register request version must be a string - Register request version must be a string - - - Sign request AppID may not be empty - Sign request AppID may not be empty - - - Sign request AppID must be a string - Sign request AppID must be a string - - - Sign request challenge may not be empty - Sign request challenge may not be empty - - - Sign request challenge must be a string - Sign request challenge must be a string - - - Sign request error code may not be empty - Sign request error code may not be empty - - - Sign request error code must be numeric - Sign request error code must be numeric - - - Sign request key handle may not be empty - Sign request key handle may not be empty - - - Sign request key handle must be a string - Sign request key handle must be a string - - - Sign request version may not be empty - Sign request version may not be empty - - - Sign request version must be a string - Sign request version must be a string - middleware_client.dto.configuration.allowed_second_factors.must_be_array middleware_client.dto.configuration.allowed_second_factors.must_be_array + + middleware_client.dto.configuration.number_of_tokens_per_identity.must_be_integer + middleware_client.dto.configuration.number_of_tokens_per_identity.must_be_integer + middleware_client.dto.configuration.show_raa_contact_information.must_be_boolean middleware_client.dto.configuration.show_raa_contact_information.must_be_boolean @@ -190,6 +122,14 @@ middleware_client.dto.ra_candidate.name_id.must_not_be_blank middleware_client.dto.ra_candidate.name_id.must_not_be_blank + + middleware_client.dto.ra_candidate_institution.institution.must_be_string + middleware_client.dto.ra_candidate_institution.institution.must_be_string + + + middleware_client.dto.ra_candidate_institution.institution.must_not_be_blank + middleware_client.dto.ra_candidate_institution.institution.must_not_be_blank + middleware_client.dto.ra_credentials.common_name.must_be_null_or_string middleware_client.dto.ra_credentials.common_name.must_be_null_or_string