475 lines
13 KiB
PHP
Executable File
475 lines
13 KiB
PHP
Executable File
<?php
|
|
/*
|
|
* Name: SAML SSO and SLO
|
|
* Description: replace login and registration with a SAML identity provider.
|
|
* Version: 1.0
|
|
* Author: Ryan <https://friendica.verya.pe/profile/ryan>
|
|
*/
|
|
|
|
use Friendica\App;
|
|
use Friendica\Content\Text\BBCode;
|
|
use Friendica\Core\Hook;
|
|
use Friendica\Core\Logger;
|
|
use Friendica\Core\Renderer;
|
|
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
|
|
|
|
function saml_module($a)
|
|
{
|
|
}
|
|
|
|
function saml_init(App $a)
|
|
{
|
|
if (DI::args()->getArgc() < 2) {
|
|
return;
|
|
}
|
|
|
|
if (!saml_is_configured()) {
|
|
echo 'Please configure the SAML add-on via the admin interface.';
|
|
return;
|
|
}
|
|
|
|
switch (DI::args()->get(1)) {
|
|
case 'metadata.xml':
|
|
saml_metadata();
|
|
break;
|
|
case 'sso':
|
|
saml_sso_reply($a);
|
|
break;
|
|
case 'slo':
|
|
saml_slo_reply();
|
|
break;
|
|
}
|
|
exit();
|
|
}
|
|
|
|
function saml_metadata()
|
|
{
|
|
try {
|
|
$settings = new \OneLogin\Saml2\Settings(saml_settings());
|
|
$metadata = $settings->getSPMetadata();
|
|
$errors = $settings->validateMetadata($metadata);
|
|
|
|
if (empty($errors)) {
|
|
header('Content-Type: text/xml');
|
|
echo $metadata;
|
|
} else {
|
|
throw new \OneLogin\Saml2\Error(
|
|
'Invalid SP metadata: '.implode(', ', $errors),
|
|
\OneLogin\Saml2\Error::METADATA_SP_INVALID
|
|
);
|
|
}
|
|
} catch (Exception $e) {
|
|
Logger::error($e->getMessage());
|
|
}
|
|
}
|
|
|
|
function saml_install()
|
|
{
|
|
Hook::register('login_hook', __FILE__, 'saml_sso_initiate');
|
|
Hook::register('logging_out', __FILE__, 'saml_slo_initiate');
|
|
Hook::register('head', __FILE__, 'saml_head');
|
|
Hook::register('footer', __FILE__, 'saml_footer');
|
|
}
|
|
|
|
function saml_head(App $a, string &$body)
|
|
{
|
|
DI::page()->registerStylesheet(__DIR__ . '/saml.css');
|
|
}
|
|
|
|
function saml_footer(App $a, string &$body)
|
|
{
|
|
$fragment = addslashes(BBCode::convert(DI::config()->get('saml', 'settings_statement')));
|
|
$body .= <<<EOL
|
|
<script>
|
|
var target=$("#settings-nickname-desc");
|
|
if (target.length) { target.append("<p>$fragment</p>"); }
|
|
</script>
|
|
EOL;
|
|
}
|
|
|
|
function saml_is_configured()
|
|
{
|
|
return
|
|
DI::config()->get('saml', 'idp_id') &&
|
|
DI::config()->get('saml', 'client_id') &&
|
|
DI::config()->get('saml', 'sso_url') &&
|
|
DI::config()->get('saml', 'slo_request_url') &&
|
|
DI::config()->get('saml', 'slo_response_url') &&
|
|
DI::config()->get('saml', 'sp_key') &&
|
|
DI::config()->get('saml', 'sp_cert') &&
|
|
DI::config()->get('saml', 'idp_cert');
|
|
}
|
|
|
|
function saml_sso_initiate(App $a, 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, [], 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(App $a)
|
|
{
|
|
$auth = new \OneLogin\Saml2\Auth(saml_settings());
|
|
$requestID = null;
|
|
|
|
if (DI::session()->exists('AuthNRequestID')) {
|
|
$requestID = DI::session()->get('AuthNRequestID');
|
|
}
|
|
|
|
$auth->processResponse($requestID);
|
|
DI::session()->remove('AuthNRequestID');
|
|
|
|
$errors = $auth->getErrors();
|
|
|
|
if (!empty($errors)) {
|
|
echo 'Errors encountered.';
|
|
Logger::error(implode(', ', $errors));
|
|
exit();
|
|
}
|
|
|
|
if (!$auth->isAuthenticated()) {
|
|
echo 'Not authenticated';
|
|
exit();
|
|
}
|
|
|
|
$username = $auth->getNameId();
|
|
$email = $auth->getAttributeWithFriendlyName('email')[0];
|
|
$name = $auth->getAttributeWithFriendlyName('givenName')[0];
|
|
$last_name = $auth->getAttributeWithFriendlyName('surname')[0];
|
|
|
|
if (strlen($last_name)) {
|
|
$name .= " $last_name";
|
|
}
|
|
|
|
if (!DBA::exists('user', ['nickname' => $username])) {
|
|
$user = saml_create_user($username, $email, $name);
|
|
} else {
|
|
$user = User::getByNickname($username);
|
|
}
|
|
|
|
if (!empty($user['uid'])) {
|
|
DI::auth()->setForUser($a, $user);
|
|
}
|
|
|
|
if (isset($_POST['RelayState']) && Utils::getSelfURL() != $_POST['RelayState']) {
|
|
$auth->redirectTo($_POST['RelayState']);
|
|
}
|
|
}
|
|
|
|
function saml_slo_initiate(App $a)
|
|
{
|
|
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();
|
|
DI::session()->set('LogoutRequestID', $auth->getLastRequestID());
|
|
header('Pragma: no-cache');
|
|
header('Cache-Control: no-cache, must-revalidate');
|
|
header('Location: ' . $sloBuiltUrl);
|
|
exit();
|
|
}
|
|
|
|
function saml_slo_reply()
|
|
{
|
|
$auth = new \OneLogin\Saml2\Auth(saml_settings());
|
|
|
|
if (DI::session()->exists('LogoutRequestID')) {
|
|
$requestID = DI::session()->get('LogoutRequestID');
|
|
} else {
|
|
$requestID = null;
|
|
}
|
|
|
|
$auth->processSLO(false, $requestID);
|
|
|
|
$errors = $auth->getErrors();
|
|
|
|
if (empty($errors)) {
|
|
$auth->redirectTo(DI::baseUrl());
|
|
} else {
|
|
Logger::error(implode(', ', $errors));
|
|
}
|
|
}
|
|
|
|
function saml_input($key, $label, $description)
|
|
{
|
|
return [
|
|
'$' . $key => [
|
|
$key,
|
|
$label,
|
|
DI::config()->get('saml', $key),
|
|
$description,
|
|
true, // all the fields are required
|
|
]
|
|
];
|
|
}
|
|
|
|
function saml_addon_admin(App $a, string &$o)
|
|
{
|
|
$form =
|
|
saml_input(
|
|
'settings_statement',
|
|
DI::l10n()->t('Settings statement'),
< |