ILIAS  trunk Revision v11.0_alpha-1713-gd8962da2f67
All Data Structures Namespaces Files Functions Variables Enumerations Enumerator Modules Pages
Metadata.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
21 namespace ILIAS\Saml;
22 
26 use ilSamlAuth;
27 use SAML2\Constants;
28 use Closure;
29 
30 class Metadata
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 }
mergeListIfExists(Configuration $config, array $list)
Definition: Metadata.php:273
assertionConsumerServices(Configuration $config, array $default)
Definition: Metadata.php:90
nameIdPolicy(Configuration $config)
Definition: Metadata.php:133
certificates(Configuration $config)
Definition: Metadata.php:196
addIfExists(Configuration $config, string $needle, string $selector='getValue', ?string $as_key=null)
Definition: Metadata.php:263
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
buildCertData(?array $cert_info, bool $encryption)
Definition: Metadata.php:217
mergeList(array $list)
Definition: Metadata.php:258
organizationalInformation(Configuration $config)
Definition: Metadata.php:165
buildXML(ilSamlAuth $auth)
Definition: Metadata.php:36
extensions(Configuration $config)
Definition: Metadata.php:231
singleLogoutService(Configuration $config, string $logout_url, string $source_id)
Definition: Metadata.php:70
__construct(private readonly SimpleSamlFactory $create)
Definition: Metadata.php:32
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples
$service
Definition: ltiservices.php:40
nameInformation(Configuration $config)
Definition: Metadata.php:145
defaultAssertionConsumerServices(Configuration $config, string $base_url, string $source_id)
Definition: Metadata.php:106