]> git.mxchange.org Git - friendica-addons.git/blob - saml/saml.php
d39ef003c4a9ce7fd98c1e11c68ad19b27c28e2c
[friendica-addons.git] / saml / saml.php
1 <?php
2 /*
3  * Name: SAML SSO and SLO
4  * Description: replace login and registration with a SAML identity provider.
5  * Version: 1.0
6  * Author: Ryan <https://friendica.verya.pe/profile/ryan>
7  */
8 use Friendica\Content\Text\BBCode;
9 use Friendica\Core\Hook;
10 use Friendica\Core\Logger;
11 use Friendica\Core\Renderer;
12 use Friendica\Core\Session;
13 use Friendica\Database\DBA;
14 use Friendica\DI;
15 use Friendica\Model\User;
16 use Friendica\Util\Strings;
17
18 require_once(__DIR__ . '/vendor/autoload.php');
19
20 define('PW_LEN', 32); // number of characters to use for random passwords
21
22 function saml_module($a)
23 {
24 }
25
26 function saml_init($a)
27 {
28         if ($a->argc < 2) {
29                 return;
30         }
31
32         if (!saml_is_configured()) {
33                 echo 'Please configure the SAML add-on via the admin interface.';
34                 return;
35         }
36
37         switch ($a->argv[1]) {
38                 case 'metadata.xml':
39                         saml_metadata();
40                         break;
41                 case 'sso':
42                         saml_sso_reply($a);
43                         break;
44                 case 'slo':
45                         saml_slo_reply();
46                         break;
47         }
48         exit();
49 }
50
51 function saml_metadata()
52 {
53         try {
54                 $settings = new \OneLogin\Saml2\Settings(saml_settings());
55                 $metadata = $settings->getSPMetadata();
56                 $errors = $settings->validateMetadata($metadata);
57
58                 if (empty($errors)) {
59                         header('Content-Type: text/xml');
60                         echo $metadata;
61                 } else {
62                         throw new \OneLogin\Saml2\Error(
63                                 'Invalid SP metadata: '.implode(', ', $errors),
64                                 \OneLogin\Saml2\Error::METADATA_SP_INVALID
65                         );
66                 }
67         } catch (Exception $e) {
68                 Logger::error($e->getMessage());
69         }
70 }
71
72 function saml_install()
73 {
74         Hook::register('login_hook', __FILE__, 'saml_sso_initiate');
75         Hook::register('logging_out', __FILE__, 'saml_slo_initiate');
76         Hook::register('head', __FILE__, 'saml_head');
77         Hook::register('footer', __FILE__, 'saml_footer');
78 }
79
80 function saml_head(&$a, &$b)
81 {
82         DI::page()->registerStylesheet(__DIR__ . '/saml.css');
83 }
84
85 function saml_footer(&$a, &$b)
86 {
87         $fragment = addslashes(BBCode::convert(DI::config()->get('saml', 'settings_statement')));
88         $b .= <<<EOL
89 <script>
90 var target=$("#settings-nickname-desc");
91 if (target.length) { target.append("<p>$fragment</p>"); }
92 </script>
93 EOL;
94 }
95
96 function saml_is_configured()
97 {
98         return
99                 DI::config()->get('saml', 'idp_id') &&
100                 DI::config()->get('saml', 'client_id') &&
101                 DI::config()->get('saml', 'sso_url') &&
102                 DI::config()->get('saml', 'slo_request_url') &&
103                 DI::config()->get('saml', 'slo_response_url') &&
104                 DI::config()->get('saml', 'sp_key') &&
105                 DI::config()->get('saml', 'sp_cert') &&
106                 DI::config()->get('saml', 'idp_cert');
107 }
108
109 function saml_sso_initiate(&$a, &$b)
110 {
111         if (!saml_is_configured()) {
112                 Logger::warning('SAML SSO tried to trigger, but the SAML addon is not configured yet!');
113                 return;
114         }
115
116         $auth = new \OneLogin\Saml2\Auth(saml_settings());
117         $ssoBuiltUrl = $auth->login(null, [], false, false, true);
118         $_SESSION['AuthNRequestID'] = $auth->getLastRequestID();
119         header('Pragma: no-cache');
120         header('Cache-Control: no-cache, must-revalidate');
121         header('Location: ' . $ssoBuiltUrl);
122         exit();
123 }
124
125 function saml_sso_reply($a)
126 {
127         $auth = new \OneLogin\Saml2\Auth(saml_settings());
128         $requestID = null;
129
130         if (isset($_SESSION) && isset($_SESSION['AuthNRequestID'])) {
131                 $requestID = $_SESSION['AuthNRequestID'];
132         }
133
134         $auth->processResponse($requestID);
135         unset($_SESSION['AuthNRequestID']);
136
137         $errors = $auth->getErrors();
138
139         if (!empty($errors)) {
140                 echo 'Errors encountered.';
141                 Logger::error(implode(', ', $errors));
142                 exit();
143         }
144
145         if (!$auth->isAuthenticated()) {
146                 echo 'Not authenticated';
147                 exit();
148         }
149
150         $username = $auth->getNameId();
151         $email = $auth->getAttributeWithFriendlyName('email')[0];
152         $name = $auth->getAttributeWithFriendlyName('givenName')[0];
153         $last_name = $auth->getAttributeWithFriendlyName('surname')[0];
154
155         if (strlen($last_name)) {
156                 $name .= " $last_name";
157         }
158
159         if (!DBA::exists('user', ['nickname' => $username])) {
160                 $user = saml_create_user($username, $email, $name);
161         } else {
162                 $user = User::getByNickname($username);
163         }
164
165         if (!empty($user['uid'])) {
166                 DI::auth()->setForUser($a, $user);
167         }
168
169         if (isset($_POST['RelayState'])
170                 && \OneLogin\Saml2\Utils::getSelfURL() != $_POST['RelayState']) {
171                 $auth->redirectTo($_POST['RelayState']);
172         }
173 }
174
175 function saml_slo_initiate(&$a, &$b)
176 {
177         if (!saml_is_configured()) {
178                 Logger::warning('SAML SLO tried to trigger, but the SAML addon is not configured yet!');
179                 return;
180         }
181
182         $auth = new \OneLogin\Saml2\Auth(saml_settings());
183
184         $sloBuiltUrl = $auth->logout();
185         $_SESSION['LogoutRequestID'] = $auth->getLastRequestID();
186         header('Pragma: no-cache');
187         header('Cache-Control: no-cache, must-revalidate');
188         header('Location: ' . $sloBuiltUrl);
189         exit();
190 }
191
192 function saml_slo_reply()
193 {
194         $auth = new \OneLogin\Saml2\Auth(saml_settings());
195
196         if (isset($_SESSION) && isset($_SESSION['LogoutRequestID'])) {
197                 $requestID = $_SESSION['LogoutRequestID'];
198         } else {
199                 $requestID = null;
200         }
201
202         $auth->processSLO(false, $requestID);
203
204         $errors = $auth->getErrors();
205
206         if (empty($errors)) {
207                 $auth->redirectTo(DI::baseUrl());
208         } else {
209                 Logger::error(implode(', ', $errors));
210         }
211 }
212
213 function saml_input($key, $label, $description)
214 {
215         return [
216                 '$' . $key => [
217                         $key,
218                         $label,
219                         DI::config()->get('saml', $key),
220                         $description,
221                         true, // all the fields are required
222                 ]
223         ];
224 }
225
226 function saml_addon_admin(&$a, &$o)
227 {
228         $form =
229                 saml_input(
230                         'settings_statement',
231                         DI::l10n()->t('Settings statement'),
232                         DI::l10n()->t('A statement on the settings page explaining where the user should go to change '
233                                         . 'their e-mail and password. BBCode allowed.')
234                 ) +
235                 saml_input(
236                         'idp_id',
237                         DI::l10n()->t('IdP ID'),
238                         DI::l10n()->t('Identity provider (IdP) entity URI (e.g., https://example.com/auth/realms/user).')
239                 ) +
240                 saml_input(
241                         'client_id',
242                         DI::l10n()->t('Client ID'),
243                         DI::l10n()->t('Identifier assigned to client by the identity provider (IdP).')
244                 ) +
245                 saml_input(
246                         'sso_url',
247                         DI::l10n()->t('IdP SSO URL'),
248                         DI::l10n()->t('The URL for your identity provider\'s SSO endpoint.')
249                 ) +
250                 saml_input(
251                         'slo_request_url',
252                         DI::l10n()->t('IdP SLO request URL'),
253                         DI::l10n()->t('The URL for your identity provider\'s SLO request endpoint.')
254                 ) +
255                 saml_input(
256                         'slo_response_url',
257                         DI::l10n()->t('IdP SLO response URL'),
258                         DI::l10n()->t('The URL for your identity provider\'s SLO response endpoint.')
259                 ) +
260                 saml_input(
261                         'sp_key',
262                         DI::l10n()->t('SP private key'),
263                         DI::l10n()->t('The private key the addon should use to authenticate.')
264                 ) +
265                 saml_input(
266                         'sp_cert',
267                         DI::l10n()->t('SP certificate'),
268                         DI::l10n()->t('The certficate for the addon\'s private key.')
269                 ) +
270                 saml_input(
271                         'idp_cert',
272                         DI::l10n()->t('IdP certificate'),
273                         DI::l10n()->t('The x509 certficate for your identity provider.')
274                 ) +
275                 [
276                         '$submit'  => DI::l10n()->t('Save Settings'),
277                 ];
278         $t = Renderer::getMarkupTemplate('admin.tpl', 'addon/saml/');
279         $o = Renderer::replaceMacros($t, $form);
280 }
281
282 function saml_addon_admin_post(&$a)
283 {
284         $set = function ($key) {
285                 $val = (!empty($_POST[$key]) ? trim($_POST[$key]) : '');
286                 DI::config()->set('saml', $key, $val);
287         };
288         $set('idp_id');
289         $set('client_id');
290         $set('sso_url');
291         $set('slo_request_url');
292         $set('slo_response_url');
293         $set('sp_key');
294         $set('sp_cert');
295         $set('idp_cert');
296         $set('settings_statement');
297 }
298
299 function saml_create_user($username, $email, $name)
300 {
301         if (!strlen($email) || !strlen($name)) {
302                 Logger::error('Could not create user: no email or username given.');
303                 return false;
304         }
305
306         try {
307                 $strong = false;
308                 $bytes = openssl_random_pseudo_bytes(intval(ceil(PW_LEN * 0.75)), $strong);
309
310                 if (!$strong) {
311                         throw new Exception('Strong algorithm not available for PRNG.');
312                 }
313
314                 $user = User::create([
315                         'username' => $name,
316                         'nickname' => $username,
317                         'email' => $email,
318                         'password' => base64_encode($bytes), // should be at least PW_LEN long
319                         'verified' => true
320                 ]);
321
322                 return $user;
323         } catch (Exception $e) {
324                 Logger::error(
325                         'Exception while creating user',
326                         [
327                                 'username'  => $username,
328                                 'email'  => $email,
329                                 'name'    => $name,
330                                 'exception' => $e->getMessage(),
331                                 'trace'  => $e->getTraceAsString()
332                         ]
333                 );
334
335                 return false;
336         }
337 }
338
339 function saml_settings()
340 {
341         return [
342
343                 // If 'strict' is True, then the PHP Toolkit will reject unsigned
344                 // or unencrypted messages if it expects them to be signed or encrypted.
345                 // Also it will reject the messages if the SAML standard is not strictly
346                 // followed: Destination, NameId, Conditions ... are validated too.
347                 // Should never be set to anything else in production!
348                 'strict' => true,
349
350                 // Enable debug mode (to print errors).
351                 'debug' => false,
352
353                 // Set a BaseURL to be used instead of try to guess
354                 // the BaseURL of the view that process the SAML Message.
355                 // Ex http://sp.example.com/
356                 //      http://example.com/sp/
357                 'baseurl' => DI::baseUrl() . '/saml',
358
359                 // Service Provider Data that we are deploying.
360                 'sp' => [
361
362                         // Identifier of the SP entity  (must be a URI)
363                         'entityId' => DI::config()->get('saml', 'client_id'),
364
365                         // Specifies info about where and how the <AuthnResponse> message MUST be
366                         // returned to the requester, in this case our SP.
367                         'assertionConsumerService' => [
368
369                                 // URL Location where the <Response> from the IdP will be returned
370                                 'url' => DI::baseUrl() . '/saml/sso',
371
372                                 // SAML protocol binding to be used when returning the <Response>
373                                 // message. OneLogin Toolkit supports this endpoint for the
374                                 // HTTP-POST binding only.
375                                 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
376                         ],
377
378                         // If you need to specify requested attributes, set a
379                         // attributeConsumingService. nameFormat, attributeValue and
380                         // friendlyName can be omitted
381                         'attributeConsumingService'=> [
382                                 'serviceName' => 'Friendica SAML SSO and SLO Addon',
383                                 'serviceDescription' => 'SLO and SSO support for Friendica',
384                                 'requestedAttributes' => [
385                                         [
386                                                 'uid' => '',
387                                                 'isRequired' => false,
388                                         ]
389                                 ]
390                         ],
391
392                         // Specifies info about where and how the <Logout Response> message MUST be
393                         // returned to the requester, in this case our SP.
394                         'singleLogoutService' => [
395
396                                 // URL Location where the <Response> from the IdP will be returned
397                                 'url' => DI::baseUrl() . '/saml/slo',
398
399                                 // SAML protocol binding to be used when returning the <Response>
400                                 // message. OneLogin Toolkit supports the HTTP-Redirect binding
401                                 // only for this endpoint.
402                                 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
403                         ],
404
405                         // Specifies the constraints on the name identifier to be used to
406                         // represent the requested subject.
407                         // Take a look on lib/Saml2/Constants.php to see the NameIdFormat supported.
408                         'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified',
409
410                         // Usually x509cert and privateKey of the SP are provided by files placed at
411                         // the certs folder. But we can also provide them with the following parameters
412                         'x509cert' => DI::config()->get('saml', 'sp_cert'),
413                         'privateKey' => DI::config()->get('saml', 'sp_key'),
414                 ],
415
416                 // Identity Provider Data that we want connected with our SP.
417                 'idp' => [
418
419                         // Identifier of the IdP entity  (must be a URI)
420                         'entityId' => DI::config()->get('saml', 'idp_id'),
421
422                         // SSO endpoint info of the IdP. (Authentication Request protocol)
423                         'singleSignOnService' => [
424
425                                 // URL Target of the IdP where the Authentication Request Message
426                                 // will be sent.
427                                 'url' => DI::config()->get('saml', 'sso_url'),
428
429                                 // SAML protocol binding to be used when returning the <Response>
430                                 // message. OneLogin Toolkit supports the HTTP-Redirect binding
431                                 // only for this endpoint.
432                                 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
433                         ],
434
435                         // SLO endpoint info of the IdP.
436                         'singleLogoutService' => [
437
438                                 // URL Location of the IdP where SLO Request will be sent.
439                                 'url' => DI::config()->get('saml', 'slo_request_url'),
440
441                                 // URL location of the IdP where SLO Response will be sent (ResponseLocation)
442                                 // if not set, url for the SLO Request will be used
443                                 'responseUrl' => DI::config()->get('saml', 'slo_response_url'),
444
445                                 // SAML protocol binding to be used when returning the <Response>
446                                 // message. OneLogin Toolkit supports the HTTP-Redirect binding
447                                 // only for this endpoint.
448                                 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
449                         ],
450
451                         // Public x509 certificate of the IdP
452                         'x509cert' => DI::config()->get('saml', 'idp_cert'),
453                 ],
454                 'security' => [
455                         'wantXMLValidation' => false,
456
457                         // Indicates whether the <samlp:AuthnRequest> messages sent by this SP
458                         // will be signed.  [Metadata of the SP will offer this info]
459                         'authnRequestsSigned' => true,
460
461                         // Indicates whether the <samlp:logoutRequest> messages sent by this SP
462                         // will be signed.
463                         'logoutRequestSigned' => true,
464
465                         // Indicates whether the <samlp:logoutResponse> messages sent by this SP
466                         // will be signed.
467                         'logoutResponseSigned' => true,
468
469                         // Sign the Metadata
470                         'signMetadata' => true,
471                 ]
472         ];
473 }