Skip to content

Commit 83ce3b6

Browse files
authored
Merge pull request #215 from OpenConext/feature/extend-config-update-logging
Allow using multiple keys from the metadata endpoint
2 parents a8e62f6 + 212c840 commit 83ce3b6

7 files changed

Lines changed: 77 additions & 9 deletions

File tree

config/openconext/institutions.yaml.dist

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ parameters:
7575
email_domains: # A list of email domains that are used to identify registering users (addresses must match the email domain of the institution)
7676
- 'institution-c.example.com'
7777
is_azure_ad: true # AzureAD (Entra) does not accept a SAML subject, ADFS does require this
78+
institution-d.example.com: # The institution identifier (schacHomeOrganization)
79+
metadata_url: 'https://azuremfa.dev.openconext.local/mock/metadata-rollover'
80+
email_domains: # A list of email domains that are used to identify registering users (addresses must match the email domain of the institution)
81+
- 'institution-d.example.com'
82+
is_azure_ad: true # AzureAD (Entra) does not accept a SAML subject, ADFS does require this
7883
harting-college.nl:
7984
sso_location: 'https://adfs.harting-college.nl/adfs/ls/'
8085
entity_id: 'https://azuremfa.dev.openconext.local/mock/metadata' # The Entity Id of the remote Azure MFA endpoint

dev/Controller/MockAzureMfaController.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,28 @@ public function metadata(Request $request): SymfonyResponse
106106
$body = $this->twig->render(
107107
'dev/mock-metadata.xml.twig',
108108
[
109-
'publickey' => $cert->getCertData(),
109+
'publickeys' => [
110+
$cert->getCertData(),
111+
]
112+
]
113+
);
114+
return new Response($body);
115+
}
116+
117+
/**
118+
* This is the metadata action used
119+
*/
120+
#[Route(path: '/mock/metadata-rollover', name: 'mock_metadata_rollover')]
121+
public function metadataRollover(Request $request): SymfonyResponse
122+
{
123+
$cert = new Certificate($this->mockStepupGateway->getPublicCertificate());
124+
$body = $this->twig->render(
125+
'dev/mock-metadata.xml.twig',
126+
[
127+
'publickeys' => [
128+
"MIIEJTCCAw2gAwIBAgIJANug+o++1X5IMA0GCSqGSIb3DQEBCwUAMIGoMQswCQYDVQQGEwJOTDEQMA4GA1UECAwHVXRyZWNodDEQMA4GA1UEBwwHVXRyZWNodDEVMBMGA1UECgwMU1VSRm5ldCBCLlYuMRMwEQYDVQQLDApTVVJGY29uZXh0MRwwGgYDVQQDDBNTVVJGbmV0IERldmVsb3BtZW50MSswKQYJKoZIhvcNAQkBFhxzdXJmY29uZXh0LWJlaGVlckBzdXJmbmV0Lm5sMB4XDTE0MTAyMDEyMzkxMVoXDTE0MTExOTEyMzkxMVowgagxCzAJBgNVBAYTAk5MMRAwDgYDVQQIDAdVdHJlY2h0MRAwDgYDVQQHDAdVdHJlY2h0MRUwEwYDVQQKDAxTVVJGbmV0IEIuVi4xEzARBgNVBAsMClNVUkZjb25leHQxHDAaBgNVBAMME1NVUkZuZXQgRGV2ZWxvcG1lbnQxKzApBgkqhkiG9w0BCQEWHHN1cmZjb25leHQtYmVoZWVyQHN1cmZuZXQubmwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDXuSSBeNJY3d4p060oNRSuAER5nLWT6AIVbv3XrXhcgSwc9m2b8u3ksp14pi8FbaNHAYW3MjlKgnLlopYIylzKD/6Ut/clEx67aO9Hpqsc0HmIP0It6q2bf5yUZ71E4CN2HtQceO5DsEYpe5M7D5i64kS2A7e2NYWVdA5Z01DqUpQGRBc+uMzOwyif6StBiMiLrZH3n2r5q5aVaXU4Vy5EE4VShv3Mp91sgXJj/v155fv0wShgl681v8yf2u2ZMb7NKnQRA4zM2Ng2EUAyy6PQ+Jbn+rALSm1YgiJdVuSlTLhvgwbiHGO2XgBi7bTHhlqSrJFK3Gs4zwIsop/XqQRBAgMBAAGjUDBOMB0GA1UdDgQWBBQCJmcoa/F7aM3jIFN7Bd4uzWRgzjAfBgNVHSMEGDAWgBQCJmcoa/F7aM3jIFN7Bd4uzWRgzjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBd80GpWKjp1J+Dgp0blVAox1s/WPWQlex9xrx1GEYbc5elp3svS+S82s7dFm2llHrrNOBt1HZVC+TdW4f+MR1xq8O5lOYjDRsosxZc/u9jVsYWYc3M9bQAx8VyJ8VGpcAK+fLqRNabYlqTnj/t9bzX8fS90sp8JsALV4g84Aj0G8RpYJokw+pJUmOpuxsZN5U84MmLPnVfmrnuCVh/HkiLNV2c8Pk8LSomg6q1M1dQUTsz/HVxcOhHLj/owwh3IzXf/KXV/E8vSYW8o4WWCAnruYOWdJMI4Z8NG1Mfv7zvb7U3FL1C/KLV04DqzALXGj+LVmxtDvuxqC042apoIDQV",
129+
$cert->getCertData(),
130+
],
110131
]
111132
);
112133
return new Response($body);

src/Surfnet/AzureMfa/Infrastructure/Entity/AzureMfaIdentityProvider.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020

2121
namespace Surfnet\AzureMfa\Infrastructure\Entity;
2222

23+
use SAML2\Certificate\X509;
2324
use Surfnet\AzureMfa\Domain\Institution\Collection\CertificateCollection;
25+
use Surfnet\AzureMfa\Domain\Institution\ValueObject\Certificate;
2426
use Surfnet\AzureMfa\Domain\Institution\ValueObject\Destination;
2527
use Surfnet\AzureMfa\Domain\Institution\ValueObject\EntityId;
2628
use Surfnet\AzureMfa\Domain\Institution\ValueObject\IdentityProviderInterface;
@@ -34,11 +36,17 @@ public function __construct(
3436
private readonly CertificateCollection $certificates,
3537
bool $isAzureAD
3638
) {
39+
$keys = array_map(fn (Certificate $cert) => [
40+
'encryption' => false,
41+
'signing' => true,
42+
'type' => 'X509Certificate',
43+
'X509Certificate' => $cert->getCertData(),
44+
], $certificates->getCertificates());
45+
3746
$configuration = [
38-
// The entityId is not configured in the
3947
'entityId' => $entityId->getEntityId(),
4048
'ssoUrl' => $destination->getUrl(),
41-
'certificateData' => $certificates->first()->getCertData(),
49+
'keys' => $keys,
4250
'isAzureAD' => $isAzureAD,
4351
];
4452

src/Surfnet/AzureMfa/Infrastructure/Factory/IdentityProviderFactory.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,16 @@ public function __construct(ConfigurationFactory $configurationFactory, Metadata
5656
public function build(
5757
InstitutionName $institutionName,
5858
): IdentityProviderInterface {
59+
$this->logger->info(sprintf('Start updating the identity configuration for: %s', $institutionName->getInstitutionName()));
60+
5961
$entity = $this->configurationFactory->getEntity($institutionName);
6062

6163
if (!$entity instanceof InstitutionConfigurationData) {
6264
throw new InstitutionNotFoundException('The institution with name "' . $institutionName->getInstitutionName() . '" was not found in the configuration.');
6365
}
6466

6567
if ($entity->hasMetadataUrl()) {
66-
$this->logger->info(sprintf('Fetching metadata for institution: %s', $institutionName->getInstitutionName()));
68+
$this->logger->info(sprintf('Update configuration for institution with the metadata url: %s', $institutionName->getInstitutionName()));
6769

6870
try {
6971
$identityProvider = $this->metadataIdentityProviderService->fetch($entity);
@@ -72,11 +74,13 @@ public function build(
7274
throw $e;
7375
}
7476

75-
$this->logger->info(sprintf('Successfully fetched metadata for institution: %s', $institutionName->getInstitutionName()));
77+
$this->logger->info(sprintf('Successfully updated with metadata for institution: %s, %d certificates found', $institutionName->getInstitutionName(), count($identityProvider->getCertificates()->getCertificates())));
7678

7779
return $identityProvider;
7880
}
7981

82+
$this->logger->info(sprintf('Update configuration for institution with the application config: %s', $institutionName->getInstitutionName()));
83+
8084
if (!$entity->hasCertificates()) {
8185
throw new InvalidCertificateException('The entity provider must have at least one certificate.');
8286
}
@@ -86,6 +90,10 @@ public function build(
8690
$certificates = CertificateCollection::fromStringArray($entity->getCertificates());
8791
$isAzureAD = $entity->isAzureAd();
8892

89-
return new AzureMfaIdentityProvider($entityId, $ssoLocation, $certificates, $isAzureAD);
93+
$identityProvider = new AzureMfaIdentityProvider($entityId, $ssoLocation, $certificates, $isAzureAD);
94+
95+
$this->logger->info(sprintf('Successfully updated with config for institution: %s, %d certificates found', $institutionName->getInstitutionName(), count($identityProvider->getCertificates()->getCertificates())));
96+
97+
return $identityProvider;
9098
}
9199
}

templates/dev/mock-metadata.xml.twig

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<EntityDescriptor ID="_a35331bd-8793-48f6-ba2f-617c3300d903"
3-
entityID="https://institution-c.example.com/"
3+
entityID="https://institution-metadata.example.com/"
44
xmlns="urn:oasis:names:tc:SAML:2.0:metadata">
55
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
6+
{% for publickey in publickeys %}
67
<KeyDescriptor use="signing">
78
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
89
<X509Data>
@@ -12,6 +13,7 @@
1213
</X509Data>
1314
</KeyInfo>
1415
</KeyDescriptor>
16+
{% endfor %}
1517
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
1618
Location="https://azuremfa.dev.openconext.local/mock/sso"/>
1719
</IDPSSODescriptor>

tests/Functional/Features/Context/WebContext.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,11 @@ public function iHaveNoCertificateDataForInstitution($institution)
236236
/**
237237
* @Given /^I have an invalid cached identity provider for "([^"]*)"$/
238238
*/
239-
public function iHaveOldCertificateDataForInstitution($institution) {
239+
public function iHaveAnInvalidCertificateDataForInstitution($institution) {
240240
file_put_contents(__DIR__ . '/../../../../federation-metadata/' . $institution . '.cache',
241241
'{
242242
"updated": "2025-06-05T15:49:55+02:00",
243-
"entity_id": "https:\/\/institution-c.example.com\/",
243+
"entity_id": "https:\/\/'.$institution.'\/",
244244
"sso_location": "https:\/\/azuremfa.dev.openconext.local\/mock\/sso",
245245
"certificates": [
246246
"MIIEEzCCAnsCFEow2E90q1t\/\/LDuqkgF2zo7VNo4MA0GCSqGSIb3DQEBCwUAMEYxGzAZBgNVBAMMEkF6dXJlLU1GQSBHU1NQIElkUDEnMCUGA1UECgweRGV2ZWxvcG1lbnQgRG9ja2VyIGVudmlyb25tZW50MB4XDTIzMDUyNTA5MzMyM1oXDTI4MDUyMzA5MzMyM1owRjEbMBkGA1UEAwwSQXp1cmUtTUZBIEdTU1AgSWRQMScwJQYDVQQKDB5EZXZlbG9wbWVudCBEb2NrZXIgZW52aXJvbm1lbnQwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCWaoXdTdU3N0RL2jK\/88PEN3jwyyz7AFJX64Rfx48CtCsI3Hze+0i+0KQgILsVU91kKujllFBM6N4V5PKQ+9Z5zafJeuhT80zQ9jcHVxyQoKi30438fBGzlAKD9hGojG7DwjKopK+96Eawvu90KCxf8q7STh50n8dO6hnxWtE8RGk5a9R2cMDxEuOlvrW2B8Ih+EVCT3OmOsCQdp31TuTt5x3xLxmY\/04mGGPpQi9PBV38O2uTd4G2mbqGqNGx6S6iPAMgh6u4NVmg03iqBKkFJgQvNRCdif+gMQTKEW0mJwr62PrEQrPBoBphgCpJNF9pnEy\/+mdWiKCo8lvVxiPGQaaKyoNvZEt1IROwp8Ga2gLEoFjtcMcodnLgudusDOCH6Idp0CtuTkrf3hLIxKjQMOFTCiCmOCtMlJZa9+l7LbhzEGcJUcHH0i1k+ufqUhOSBrrfKoiohixAnW+bayqymef+Zy32YoT+\/LDjoP\/vyMrNnRwpwqguPMwBF+HWgwUCAwEAATANBgkqhkiG9w0BAQsFAAOCAYEAReFJH\/X+PyA8cFe6RdCgyTbuRuq2rTgadKpqfhhbXlwcOTh8rEpevqFf8tequegCj7fFZgz+hIL075ZsEcZwk2N8F8m32cVjmYHar2rLsYEkqhEc\/yCUjyGffqUeZBVmdUnUM6ggGsIHqcjTvrNhmFrh3ManebvZkjvDyJCkrwUOGYvCpbFjXa4CW1Rp+I0+e7HnQeyFW3p+3T0SAmdo3eJEZLhRsMm\/YLcyCW7IRTVvpTvGoxhbvQU1k6EtkhLcahA+MWVzNbgiIdHP\/otSQnaLW243sxoxYm7EiuAihnQ0iRaNEzsFrx\/W06G0e5rmTbWPGc4LZj6YDKd7531SGIwqOOC1wrzrZ36iuwPm5PrZReCWH3ptR6bSszQerbQsx6wkumYN7iDZg9EK9ADHRzfovbqOPad2s+N5iVWAOfEXGqItZcrLdW53vUOqbfXXuFt7szhtdvTWRWWQQJryrg61UmLgJcLb3xMMdZZ+D6mcXqa3v2cSzGdfO9123456"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Feature: When an user needs to authenticate
2+
As a service provider
3+
I need to send an AuthnRequest with a nameID to the Azure MFA GSSP IdP
4+
5+
Scenario: The user authenticates successfully if IdP is not cached
6+
Given I have no cached identity provider for "institution-d.example.com"
7+
And I send an authentication request to "https://azuremfa.dev.openconext.local/saml/sso" with NameID "q2b27d-0000|user@institution-d.example.com"
8+
And the login with Azure MFA succeeds and the following attributes are released:
9+
| name | value |
10+
| urn:mace:dir:attribute-def:mail | user@institution-d.example.com |
11+
| http://schemas.microsoft.com/claims/authnmethodsreferences | http://schemas.microsoft.com/claims/multipleauthn |
12+
Then I should be on "https://azuremfa.dev.openconext.local/saml/sso_return"
13+
And the SAML Response should contain element "StatusCode" with attribute "Value" with attribute value "urn:oasis:names:tc:SAML:2.0:status:Success"
14+
And the SAML Response should contain element "NameID" with value "q2b27d-0000|user@institution-d.example.com"
15+
16+
Scenario: The user authenticates successfully if IdP is cached
17+
Given I send an authentication request to "https://azuremfa.dev.openconext.local/saml/sso" with NameID "q2b27d-0000|user@institution-d.example.com"
18+
And the login with Azure MFA succeeds and the following attributes are released:
19+
| name | value |
20+
| urn:mace:dir:attribute-def:mail | user@institution-d.example.com |
21+
| http://schemas.microsoft.com/claims/authnmethodsreferences | http://schemas.microsoft.com/claims/multipleauthn |
22+
Then I should be on "https://azuremfa.dev.openconext.local/saml/sso_return"
23+
And the SAML Response should contain element "StatusCode" with attribute "Value" with attribute value "urn:oasis:names:tc:SAML:2.0:status:Success"
24+
And the SAML Response should contain element "NameID" with value "q2b27d-0000|user@institution-d.example.com"

0 commit comments

Comments
 (0)