3 * Name: SAML SSO and SLO
4 * Description: replace login and registration with a SAML identity provider.
6 * Author: Ryan <https://friendica.verya.pe/profile/ryan>
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;
17 use Friendica\Model\User;
18 use Friendica\Util\Strings;
19 use OneLogin\Saml2\Utils;
21 require_once(__DIR__ . '/vendor/autoload.php');
23 define('PW_LEN', 32); // number of characters to use for random passwords
25 function saml_module($a)
29 function saml_init($a)
31 if (DI::args()->getArgc() < 2) {
35 if (!saml_is_configured()) {
36 echo 'Please configure the SAML add-on via the admin interface.';
40 switch (DI::args()->get(1)) {
54 function saml_metadata()
57 $settings = new \OneLogin\Saml2\Settings(saml_settings());
58 $metadata = $settings->getSPMetadata();
59 $errors = $settings->validateMetadata($metadata);
62 header('Content-Type: text/xml');
65 throw new \OneLogin\Saml2\Error(
66 'Invalid SP metadata: '.implode(', ', $errors),
67 \OneLogin\Saml2\Error::METADATA_SP_INVALID
70 } catch (Exception $e) {
71 Logger::error($e->getMessage());
75 function saml_install()
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');
83 function saml_head(App $a, array &$b)
85 DI::page()->registerStylesheet(__DIR__ . '/saml.css');
88 function saml_footer(App $a, array &$b)
90 $fragment = addslashes(BBCode::convert(DI::config()->get('saml', 'settings_statement')));
93 var target=$("#settings-nickname-desc");
94 if (target.length) { target.append("<p>$fragment</p>"); }
99 function saml_is_configured()
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');
112 function saml_sso_initiate(App $a, array &$b)
114 if (!saml_is_configured()) {
115 Logger::warning('SAML SSO tried to trigger, but the SAML addon is not configured yet!');
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);
128 function saml_sso_reply($a)
130 $auth = new \OneLogin\Saml2\Auth(saml_settings());
133 if (isset($_SESSION) && isset($_SESSION['AuthNRequestID'])) {
134 $requestID = $_SESSION['AuthNRequestID'];
137 $auth->processResponse($requestID);
138 unset($_SESSION['AuthNRequestID']);
140 $errors = $auth->getErrors();
142 if (!empty($errors)) {
143 echo 'Errors encountered.';
144 Logger::error(implode(', ', $errors));
148 if (!$auth->isAuthenticated()) {
149 echo 'Not authenticated';
153 $username = $auth->getNameId();
154 $email = $auth->getAttributeWithFriendlyName('email')[0];
155 $name = $auth->getAttributeWithFriendlyName('givenName')[0];
156 $last_name = $auth->getAttributeWithFriendlyName('surname')[0];
158 if (strlen($last_name)) {
159 $name .= " $last_name";
162 if (!DBA::exists('user', ['nickname' => $username])) {
163 $user = saml_create_user($username, $email, $name);
165 $user = User::getByNickname($username);
168 if (!empty($user['uid'])) {
169 DI::auth()->setForUser($a, $user);
172 if (isset($_POST['RelayState']) && Utils::getSelfURL() != $_POST['RelayState']) {
173 $auth->redirectTo($_POST['RelayState']);
177 function saml_slo_initiate(App $a, array &$b)
179 if (!saml_is_configured()) {
180 Logger::warning('SAML SLO tried to trigger, but the SAML addon is not configured yet!');
184 $auth = new \OneLogin\Saml2\Auth(saml_settings());
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);
194 function saml_slo_reply()
196 $auth = new \OneLogin\Saml2\Auth(saml_settings());
198 if (isset($_SESSION) && isset($_SESSION['LogoutRequestID'])) {
199 $requestID = $_SESSION['LogoutRequestID'];
204 $auth->processSLO(false, $requestID);
206 $errors = $auth->getErrors();
208 if (empty($errors)) {
209 $auth->redirectTo(DI::baseUrl());
211 Logger::error(implode(', ', $errors));
215 function saml_input($key, $label, $description)
221 DI::config()->get('saml', $key),
223 true, // all the fields are required
228 function saml_addon_admin(App $a, &$o)
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.')
239 DI::l10n()->t('IdP ID'),
240 DI::l10n()->t('Identity provider (IdP) entity URI (e.g., https://example.com/auth/realms/user).')
244 DI::l10n()->t('Client ID'),
245 DI::l10n()->t('Identifier assigned to client by the identity provider (IdP).')
249 DI::l10n()->t('IdP SSO URL'),
250 DI::l10n()->t('The URL for your identity provider\'s SSO endpoint.')
254 DI::l10n()->t('IdP SLO request URL'),
255 DI::l10n()->t('The URL for your identity provider\'s SLO request endpoint.')
259 DI::l10n()->t('IdP SLO response URL'),
260 DI::l10n()->t('The URL for your identity provider\'s SLO response endpoint.')
264 DI::l10n()->t('SP private key'),
265 DI::l10n()->t('The private key the addon should use to authenticate.')
269 DI::l10n()->t('SP certificate'),
270 DI::l10n()->t('The certficate for the addon\'s private key.')
274 DI::l10n()->t('IdP certificate'),
275 DI::l10n()->t('The x509 certficate for your identity provider.')
278 '$submit' => DI::l10n()->t('Save Settings'),
280 $t = Renderer::getMarkupTemplate('admin.tpl', 'addon/saml/');
281 $o = Renderer::replaceMacros($t, $form);
284 function saml_addon_admin_post(App $a)
286 $set = function ($key) {
287 $val = (!empty($_POST[$key]) ? trim($_POST[$key]) : '');
288 DI::config()->set('saml', $key, $val);
293 $set('slo_request_url');
294 $set('slo_response_url');
298 $set('settings_statement');
301 function saml_create_user($username, $email, $name)
303 if (!strlen($email) || !strlen($name)) {
304 Logger::error('Could not create user: no email or username given.');
310 $bytes = openssl_random_pseudo_bytes(intval(ceil(PW_LEN * 0.75)), $strong);
313 throw new Exception('Strong algorithm not available for PRNG.');
316 $user = User::create([
318 'nickname' => $username,
320 'password' => base64_encode($bytes), // should be at least PW_LEN long
325 } catch (Exception $e) {
327 'Exception while creating user',
329 'username' => $username,
332 'exception' => $e->getMessage(),
333 'trace' => $e->getTraceAsString()
341 function saml_settings()
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!
352 // Enable debug mode (to print errors).
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',
361 // Service Provider Data that we are deploying.
364 // Identifier of the SP entity (must be a URI)
365 'entityId' => DI::config()->get('saml', 'client_id'),
367 // Specifies info about where and how the <AuthnResponse> message MUST be
368 // returned to the requester, in this case our SP.
369 'assertionConsumerService' => [
371 // URL Location where the <Response> from the IdP will be returned
372 'url' => DI::baseUrl() . '/saml/sso',
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',
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' => [
389 'isRequired' => false,
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' => [
398 // URL Location where the <Response> from the IdP will be returned
399 'url' => DI::baseUrl() . '/saml/slo',
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',
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',
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'),
418 // Identity Provider Data that we want connected with our SP.
421 // Identifier of the IdP entity (must be a URI)
422 'entityId' => DI::config()->get('saml', 'idp_id'),
424 // SSO endpoint info of the IdP. (Authentication Request protocol)
425 'singleSignOnService' => [
427 // URL Target of the IdP where the Authentication Request Message
429 'url' => DI::config()->get('saml', 'sso_url'),
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',
437 // SLO endpoint info of the IdP.
438 'singleLogoutService' => [
440 // URL Location of the IdP where SLO Request will be sent.
441 'url' => DI::config()->get('saml', 'slo_request_url'),
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'),
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',
453 // Public x509 certificate of the IdP
454 'x509cert' => DI::config()->get('saml', 'idp_cert'),
457 'wantXMLValidation' => false,
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,
463 // Indicates whether the <samlp:logoutRequest> messages sent by this SP
465 'logoutRequestSigned' => true,
467 // Indicates whether the <samlp:logoutResponse> messages sent by this SP
469 'logoutResponseSigned' => true,
472 'signMetadata' => true,