ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
Metadata.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
21namespace ILIAS\Saml;
22
23use SimpleSAML\Error\Exception;
24use SimpleSAML\Store\SQLStore;
25use SimpleSAML\Configuration;
26use ilSamlAuth;
27use SAML2\Constants;
28use Closure;
29
31{
32 public function __construct(private readonly SimpleSamlFactory $create)
33 {
34 }
35
36 public function buildXML(ilSamlAuth $auth): string
37 {
38 $source = $this->create->sourceById($auth->getAuthId());
39 $config = $source->getMetadata();
40 $base_url = rtrim(ILIAS_HTTP_PATH, '/');
41
42 $acs = $this->assertionConsumerServices($config, $this->defaultAssertionConsumerServices($config, $base_url, $auth->getAuthId()));
43
44 $base = [
45 'entityid' => $source->getEntityId(),
46 'metadata-set' => 'saml20-sp-remote',
47 'SingleLogoutService' => $this->singleLogoutService($config, $base_url, $source->getAuthId()),
48 'AssertionConsumerService' => $acs['services'],
49 ];
50
51 $metadata_sp20 = $this->mergeList([
52 $base,
53 $this->nameIdPolicy($config),
54 $this->nameInformation($config),
55 $this->organizationalInformation($config),
56 $this->certificates($config),
57 $this->extensions($config),
58 ]);
59
60 $builder = $this->create->builder($source->getEntityId());
61 $builder->addMetadataSP20($metadata_sp20, $acs['supported_protocols']);
62 $builder->addOrganizationInfo($metadata_sp20);
63
64 $xml = $builder->getEntityDescriptorText();
65 $xml = $this->create->sign($xml, $config->toArray(), 'SAML 2 SP');
66
67 return $xml;
68 }
69
70 private function singleLogoutService(Configuration $config, string $logout_url, string $source_id): array
71 {
72 $logout_url = $logout_url . '/module.php/saml/sp/saml2-logout.php/' . $source_id;
73 $store = $this->create->store();
74
75 $bindings = $config->getOptionalArray('SingleLogoutServiceBinding', [
76 Constants::BINDING_HTTP_REDIRECT,
77 Constants::BINDING_SOAP,
78 ]);
79
80 $bindings = $store instanceof SQLStore ?
81 $bindings :
82 array_values(array_filter($bindings, static fn(string $b): bool => $b !== Constants::BINDING_SOAP));
83
84 return array_map(static fn(string $b): array => [
85 'Binding' => $b,
86 'Location' => $config->getOptionalString('SingleLogoutServiceLocation', $logout_url),
87 ], $bindings);
88 }
89
90 private function assertionConsumerServices(Configuration $config, array $default): array
91 {
92 $services = $config->getOptionalArray('acs.Bindings', array_keys($default));
93
94 $services = array_intersect($services, array_keys($default));
95
96 $services = array_map(static fn(string $service, int $index): array => array_merge($default[$service] ?? [], [
97 'index' => $index,
98 ]), $services, range(0, count($services) - 1));
99
100 return [
101 'services' => array_map($this->removeKey('Protocol'), $services),
102 'supported_protocols' => array_unique(array_values(array_column($services, 'Protocol'))),
103 ];
104 }
105
106 private function defaultAssertionConsumerServices(Configuration $config, string $base_url, string $source_id): array
107 {
108 $default = [
109 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' => [
110 'Binding' => Constants::BINDING_HTTP_POST,
111 'Location' => sprintf('%s/module.php/saml/sp/saml2-acs.php/%s', $base_url, $source_id),
112 'Protocol' => Constants::NS_SAMLP,
113 ],
114 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact' => [
115 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact',
116 'Location' => sprintf('%s/module.php/saml/sp/saml2-acs.php/%s', $base_url, $source_id),
117 'Protocol' => Constants::NS_SAMLP,
118 ],
119 ];
120
121 if ($config->getOptionalString('ProtocolBinding', '') === 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser') {
122 $default['urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser'] = [
123 'Binding' => 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser',
124 'hoksso:ProtocolBinding' => Constants::BINDING_HTTP_REDIRECT,
125 'Location' => sprintf('%s/module.php/saml2-acs/%s', $base_url, $source_id),
126 'Protocol' => Constants::NS_SAMLP,
127 ];
128 }
129
130 return $default;
131 }
132
133 private function nameIdPolicy(Configuration $config): array
134 {
135 $format = $config->getOptionalValue('NameIDPolicy', null);
136 return match (gettype($format)) {
137 'array' => [
138 'NameIDFormat' => $this->create->configFromArray($format)->getString('Format'),
139 ],
140 'string' => ['NameIDFormat' => $format],
141 default => [],
142 };
143 }
144
145 private function nameInformation(Configuration $config): array
146 {
147 if (!$config->hasValue('name') || !$config->hasValue('attributes')) {
148 return [];
149 }
150
151 $information = [
152 'name' => $config->getLocalizedString('name'),
153 'attributes' => $config->getArray('attributes'),
154 ];
155
156 return array_merge($information, $this->mergeListIfExists($config, [
157 ['attributes.required', 'getArray'],
158 ['description', 'getString'],
159 ['attributes.NameFormat', 'getString'],
160 ['attributes.index', 'getInteger'],
161 ['attributes.isDefault', 'getBoolean'],
162 ]));
163 }
164
165 private function organizationalInformation(Configuration $config): array
166 {
167 $array = [];
168 if ($config->hasValue('OrganizationName')) {
169 $array['OrganizationName'] = $config->getLocalizedString('OrganizationName');
170
171 $array = array_merge($array, $this->addIfExists($config, 'OrganizationDisplayName', 'getLocalizedString'));
172
173 if (!$config->hasValue('OrganizationURL')) {
174 throw new Exception('If OrganizationName is set, OrganizationURL must also be set.');
175 }
176 $array['OrganizationURL'] = $config->getLocalizedString('OrganizationURL');
177 }
178
179 foreach ($config->getOptionalArray('contacts', []) as $contact) {
180 $array['contacts'][] = $this->create->contact($contact);
181 }
182
183 // add technical contact
184 if ($config->hasValue('technicalcontact_email') && $config->getString('technicalcontact_email') !== 'na@example.org') {
185 $techcontact = [
186 'emailAddress' => $config->getString('technicalcontact_email'),
187 'name' => $config->getOptionalString('technicalcontact_name', null),
188 'contactType' => 'technical',
189 ];
190 $array['contacts'][] = $this->create->contact($techcontact);
191 }
192
193 return $array;
194 }
195
196 private function certificates(Configuration $config): array
197 {
198 $crypt = $this->create->crypt();
199
200 $key = $this->buildCertData($crypt->loadPublicKey($config, false, 'new_'), true);
201 $has_new_cert = $key !== null;
202
203 $keys = array_values(array_filter([
204 $key,
205 $this->buildCertData($crypt->loadPublicKey($config), !$has_new_cert),
206 ]));
207
208 if (count($keys) === 1) {
209 return ['certData' => $keys[0]['X509Certificate']];
210 } elseif (count($keys) > 1) {
211 return ['keys' => $keys];
212 }
213
214 return [];
215 }
216
217 private function buildCertData(?array $cert_info, bool $encryption): ?array
218 {
219 if ($cert_info['certData'] ?? false) {
220 return [
221 'type' => 'X509Certificate',
222 'signing' => true,
223 'encryption' => $encryption,
224 'X509Certificate' => $cert_info['certData'],
225 ];
226 }
227
228 return null;
229 }
230
231 private function extensions(Configuration $config): array
232 {
233 return $this->mergeListIfExists($config, [
234 ['EntityAttributes', 'getArray'],
235 ['UIInfo', 'getArray'],
236 ['RegistrationInfo', 'getArray'],
237 ['WantAssertionsSigned', 'getBoolean', 'saml20.sign.assertion'],
238 ['redirect.sign', 'getBoolean', 'redirect.validate'],
239 ['sign.authnrequest', 'getBoolean', 'validate.authnrequest'],
240 ]);
241 }
242
247 private function removeKey($key): Closure
248 {
249 return static function (array $a) use ($key) {
250 unset($a[$key]);
251 return $a;
252 };
253 }
254
258 private function mergeList(array $list): array
259 {
260 return array_merge(...$list);
261 }
262
263 private function addIfExists(Configuration $config, string $needle, string $selector = 'getValue', ?string $as_key = null): array
264 {
265 return $config->hasValue($needle) ?
266 [$as_key ?? $needle => $config->$selector($needle)] :
267 [];
268 }
269
273 private function mergeListIfExists(Configuration $config, array $list): array
274 {
275 return $this->mergeList(array_map(
276 fn($pair): array => $this->addIfExists($config, ...(is_array($pair) ? $pair : [$pair])),
277 $list
278 ));
279 }
280}
mergeList(array $list)
Definition: Metadata.php:258
assertionConsumerServices(Configuration $config, array $default)
Definition: Metadata.php:90
__construct(private readonly SimpleSamlFactory $create)
Definition: Metadata.php:32
defaultAssertionConsumerServices(Configuration $config, string $base_url, string $source_id)
Definition: Metadata.php:106
buildXML(ilSamlAuth $auth)
Definition: Metadata.php:36
buildCertData(?array $cert_info, bool $encryption)
Definition: Metadata.php:217
nameInformation(Configuration $config)
Definition: Metadata.php:145
extensions(Configuration $config)
Definition: Metadata.php:231
singleLogoutService(Configuration $config, string $logout_url, string $source_id)
Definition: Metadata.php:70
certificates(Configuration $config)
Definition: Metadata.php:196
organizationalInformation(Configuration $config)
Definition: Metadata.php:165
addIfExists(Configuration $config, string $needle, string $selector='getValue', ?string $as_key=null)
Definition: Metadata.php:263
nameIdPolicy(Configuration $config)
Definition: Metadata.php:133
mergeListIfExists(Configuration $config, array $list)
Definition: Metadata.php:273
$service
Definition: ltiresult.php:36
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples