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