/*
* Name: SAML SSO and SLO
* Description: replace login and registration with a SAML identity provider.
- * Version: 0.0
+ * Version: 1.0
* Author: Ryan <https://friendica.verya.pe/profile/ryan>
*/
+
use Friendica\Content\Text\BBCode;
use Friendica\Core\Hook;
use Friendica\Core\Logger;
use Friendica\Core\Renderer;
-use Friendica\Core\Session;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\User;
-use Friendica\Util\Strings;
+use OneLogin\Saml2\Utils;
require_once(__DIR__ . '/vendor/autoload.php');
-define("PW_LEN", 32); // number of characters to use for random passwords
+define('PW_LEN', 32); // number of characters to use for random passwords
-function saml_module($a)
-{
-}
+function saml_module() {}
-function saml_init($a)
+function saml_init()
{
- if ($a->argc < 2) {
+ if (DI::args()->getArgc() < 2) {
+ return;
+ }
+
+ if (!saml_is_configured()) {
+ echo 'Please configure the SAML add-on via the admin interface.';
return;
}
- switch ($a->argv[1]) {
- case "metadata.xml":
+ switch (DI::args()->get(1)) {
+ case 'metadata.xml':
saml_metadata();
break;
- case "sso":
- saml_sso_reply($a);
+ case 'sso':
+ saml_sso_reply();
break;
- case "slo":
+ case 'slo':
saml_slo_reply();
break;
}
Hook::register('footer', __FILE__, 'saml_footer');
}
-function saml_head(&$a, &$b)
+function saml_head(string &$body)
{
DI::page()->registerStylesheet(__DIR__ . '/saml.css');
}
-function saml_footer(&$a, &$b)
+function saml_footer(string &$body)
{
- $fragment = addslashes(BBCode::convert(DI::config()->get('saml', 'settings_statement')));
- $b .= <<<EOL
+ $fragment = addslashes(BBCode::convertForUriId(User::getSystemUriId(), DI::config()->get('saml', 'settings_statement')));
+ $body .= <<<EOL
<script>
var target=$("#settings-nickname-desc");
if (target.length) { target.append("<p>$fragment</p>"); }
DI::config()->get('saml', 'idp_cert');
}
-function saml_sso_initiate(&$a, &$b)
+function saml_sso_initiate(string &$body)
{
if (!saml_is_configured()) {
+ Logger::warning('SAML SSO tried to trigger, but the SAML addon is not configured yet!');
return;
}
$auth = new \OneLogin\Saml2\Auth(saml_settings());
- $ssoBuiltUrl = $auth->login(null, array(), false, false, true);
- $_SESSION['AuthNRequestID'] = $auth->getLastRequestID();
+ $ssoBuiltUrl = $auth->login(null, [], false, false, true);
+ DI::session()->set('AuthNRequestID', $auth->getLastRequestID());
header('Pragma: no-cache');
header('Cache-Control: no-cache, must-revalidate');
header('Location: ' . $ssoBuiltUrl);
exit();
}
-function saml_sso_reply($a)
+function saml_sso_reply()
{
$auth = new \OneLogin\Saml2\Auth(saml_settings());
$requestID = null;
- if (isset($_SESSION) && isset($_SESSION['AuthNRequestID'])) {
- $requestID = $_SESSION['AuthNRequestID'];
+ if (DI::session()->exists('AuthNRequestID')) {
+ $requestID = DI::session()->get('AuthNRequestID');
}
$auth->processResponse($requestID);
- unset($_SESSION['AuthNRequestID']);
+ DI::session()->remove('AuthNRequestID');
$errors = $auth->getErrors();
if (!empty($errors)) {
- echo "Errors encountered.";
+ echo 'Errors encountered.';
Logger::error(implode(', ', $errors));
exit();
}
if (!$auth->isAuthenticated()) {
- echo "Not authenticated";
+ echo 'Not authenticated';
exit();
}
}
if (!empty($user['uid'])) {
- DI::auth()->setForUser($a, $user);
+ DI::auth()->setForUser(DI::app(), $user);
}
- if (isset($_POST['RelayState'])
- && \OneLogin\Saml2\Utils::getSelfURL() != $_POST['RelayState']) {
+ if (isset($_POST['RelayState']) && Utils::getSelfURL() != $_POST['RelayState']) {
$auth->redirectTo($_POST['RelayState']);
}
}
-function saml_slo_initiate(&$a, &$b)
+function saml_slo_initiate()
{
+ if (!saml_is_configured()) {
+ Logger::warning('SAML SLO tried to trigger, but the SAML addon is not configured yet!');
+ return;
+ }
+
$auth = new \OneLogin\Saml2\Auth(saml_settings());
$sloBuiltUrl = $auth->logout();
- $_SESSION['LogoutRequestID'] = $auth->getLastRequestID();
+ DI::session()->set('LogoutRequestID', $auth->getLastRequestID());
header('Pragma: no-cache');
header('Cache-Control: no-cache, must-revalidate');
header('Location: ' . $sloBuiltUrl);
{
$auth = new \OneLogin\Saml2\Auth(saml_settings());
- if (isset($_SESSION) && isset($_SESSION['LogoutRequestID'])) {
- $requestID = $_SESSION['LogoutRequestID'];
+ if (DI::session()->exists('LogoutRequestID')) {
+ $requestID = DI::session()->get('LogoutRequestID');
} else {
$requestID = null;
}
$label,
DI::config()->get('saml', $key),
$description,
- true, // all the fields are required
+ true, // all the fields are required
]
];
}
-function saml_addon_admin(&$a, &$o)
+function saml_addon_admin(string &$o)
{
$form =
saml_input(
[
'$submit' => DI::l10n()->t('Save Settings'),
];
- $t = Renderer::getMarkupTemplate("admin.tpl", "addon/saml/");
+ $t = Renderer::getMarkupTemplate('admin.tpl', 'addon/saml/');
$o = Renderer::replaceMacros($t, $form);
}
-function saml_addon_admin_post(&$a)
+function saml_addon_admin_post()
{
- $safeset = function ($key) {
- $val = (!empty($_POST[$key]) ? Strings::escapeTags(trim($_POST[$key])) : '');
+ $set = function ($key) {
+ $val = (!empty($_POST[$key]) ? trim($_POST[$key]) : '');
DI::config()->set('saml', $key, $val);
};
- $safeset('idp_id');
- $safeset('client_id');
- $safeset('sso_url');
- $safeset('slo_request_url');
- $safeset('slo_response_url');
- $safeset('sp_key');
- $safeset('sp_cert');
- $safeset('idp_cert');
-
- // Not using safeset here since settings_statement is *meant* to include HTML tags.
- DI::config()->set('saml', 'settings_statement', $_POST['settings_statement']);
+ $set('idp_id');
+ $set('client_id');
+ $set('sso_url');
+ $set('slo_request_url');
+ $set('slo_response_url');
+ $set('sp_key');
+ $set('sp_cert');
+ $set('idp_cert');
+ $set('settings_statement');
}
function saml_create_user($username, $email, $name)
'nickname' => $username,
'email' => $email,
'password' => base64_encode($bytes), // should be at least PW_LEN long
- 'verified' => true
+ 'verified' => true
]);
return $user;
function saml_settings()
{
- return array(
+ return [
+
// If 'strict' is True, then the PHP Toolkit will reject unsigned
// or unencrypted messages if it expects them to be signed or encrypted.
// Also it will reject the messages if the SAML standard is not strictly
// the BaseURL of the view that process the SAML Message.
// Ex http://sp.example.com/
// http://example.com/sp/
- 'baseurl' => DI::baseUrl() . "/saml",
+ 'baseurl' => DI::baseUrl() . '/saml',
// Service Provider Data that we are deploying.
- 'sp' => array(
+ 'sp' => [
+
// Identifier of the SP entity (must be a URI)
'entityId' => DI::config()->get('saml', 'client_id'),
+
// Specifies info about where and how the <AuthnResponse> message MUST be
// returned to the requester, in this case our SP.
- 'assertionConsumerService' => array(
+ 'assertionConsumerService' => [
+
// URL Location where the <Response> from the IdP will be returned
- 'url' => DI::baseUrl() . "/saml/sso",
+ 'url' => DI::baseUrl() . '/saml/sso',
+
// SAML protocol binding to be used when returning the <Response>
// message. OneLogin Toolkit supports this endpoint for the
// HTTP-POST binding only.
'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
- ),
+ ],
+
// If you need to specify requested attributes, set a
// attributeConsumingService. nameFormat, attributeValue and
// friendlyName can be omitted
- "attributeConsumingService"=> array(
- "serviceName" => "Friendica SAML SSO and SLO Addon",
- "serviceDescription" => "SLO and SSO support for Friendica",
- "requestedAttributes" => array(
- array(
- "uid" => "",
- "isRequired" => false,
- )
- )
- ),
+ 'attributeConsumingService'=> [
+ 'serviceName' => 'Friendica SAML SSO and SLO Addon',
+ 'serviceDescription' => 'SLO and SSO support for Friendica',
+ 'requestedAttributes' => [
+ [
+ 'uid' => '',
+ 'isRequired' => false,
+ ]
+ ]
+ ],
+
// Specifies info about where and how the <Logout Response> message MUST be
// returned to the requester, in this case our SP.
- 'singleLogoutService' => array(
+ 'singleLogoutService' => [
+
// URL Location where the <Response> from the IdP will be returned
- 'url' => DI::baseUrl() . "/saml/slo",
+ 'url' => DI::baseUrl() . '/saml/slo',
+
// SAML protocol binding to be used when returning the <Response>
// message. OneLogin Toolkit supports the HTTP-Redirect binding
// only for this endpoint.
'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
- ),
+ ],
+
// Specifies the constraints on the name identifier to be used to
// represent the requested subject.
// Take a look on lib/Saml2/Constants.php to see the NameIdFormat supported.
'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified',
+
// Usually x509cert and privateKey of the SP are provided by files placed at
// the certs folder. But we can also provide them with the following parameters
'x509cert' => DI::config()->get('saml', 'sp_cert'),
'privateKey' => DI::config()->get('saml', 'sp_key'),
- ),
+ ],
// Identity Provider Data that we want connected with our SP.
- 'idp' => array(
+ 'idp' => [
+
// Identifier of the IdP entity (must be a URI)
'entityId' => DI::config()->get('saml', 'idp_id'),
+
// SSO endpoint info of the IdP. (Authentication Request protocol)
- 'singleSignOnService' => array(
+ 'singleSignOnService' => [
+
// URL Target of the IdP where the Authentication Request Message
// will be sent.
'url' => DI::config()->get('saml', 'sso_url'),
+
// SAML protocol binding to be used when returning the <Response>
// message. OneLogin Toolkit supports the HTTP-Redirect binding
// only for this endpoint.
'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
- ),
+ ],
+
// SLO endpoint info of the IdP.
- 'singleLogoutService' => array(
+ 'singleLogoutService' => [
+
// URL Location of the IdP where SLO Request will be sent.
'url' => DI::config()->get('saml', 'slo_request_url'),
+
// URL location of the IdP where SLO Response will be sent (ResponseLocation)
// if not set, url for the SLO Request will be used
'responseUrl' => DI::config()->get('saml', 'slo_response_url'),
+
// SAML protocol binding to be used when returning the <Response>
// message. OneLogin Toolkit supports the HTTP-Redirect binding
// only for this endpoint.
'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
- ),
- // Public x509 certificate of the IdP
- 'x509cert' => DI::config()->get('saml', 'idp_cert'),
- ),
- 'security' => array (
- 'wantXMLValidation' => false,
-
- // Indicates whether the <samlp:AuthnRequest> messages sent by this SP
- // will be signed. [Metadata of the SP will offer this info]
- 'authnRequestsSigned' => true,
-
- // Indicates whether the <samlp:logoutRequest> messages sent by this SP
- // will be signed.
- 'logoutRequestSigned' => true,
-
- // Indicates whether the <samlp:logoutResponse> messages sent by this SP
- // will be signed.
- 'logoutResponseSigned' => true,
-
- /* Sign the Metadata */
- 'signMetadata' => true,
- )
- );
+ ],
+
+ // Public x509 certificate of the IdP
+ 'x509cert' => DI::config()->get('saml', 'idp_cert'),
+ ],
+ 'security' => [
+ 'wantXMLValidation' => false,
+
+ // Indicates whether the <samlp:AuthnRequest> messages sent by this SP
+ // will be signed. [Metadata of the SP will offer this info]
+ 'authnRequestsSigned' => true,
+
+ // Indicates whether the <samlp:logoutRequest> messages sent by this SP
+ // will be signed.
+ 'logoutRequestSigned' => true,
+
+ // Indicates whether the <samlp:logoutResponse> messages sent by this SP
+ // will be signed.
+ 'logoutResponseSigned' => true,
+
+ // Sign the Metadata
+ 'signMetadata' => true,
+ ]
+ ];
}