snipe-it/app/Services/Saml.php
snipe fb233c0aa4 Cleaned up facade names and references
Signed-off-by: snipe <snipe@snipe.net>
2024-05-29 12:38:15 +01:00

518 lines
14 KiB
PHP

<?php
namespace App\Services;
use App\Models\Setting;
use App\Models\User;
use Exception;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Session;
use OneLogin\Saml2\Auth as OneLogin_Saml2_Auth;
use OneLogin\Saml2\IdPMetadataParser as OneLogin_Saml2_IdPMetadataParser;
use OneLogin\Saml2\Settings as OneLogin_Saml2_Settings;
use OneLogin\Saml2\Utils as OneLogin_Saml2_Utils;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Support\Facades\Log;
/**
* SAML Singleton that builds the settings and loads the onelogin/php-saml library.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*/
class Saml
{
public const DATA_SESSION_KEY = '_samlData';
/**
* OneLogin_Saml2_Auth instance.
*
* @var OneLogin\Saml2\Auth
*/
private $_auth;
/**
* if SAML is enabled and has valid settings.
*
* @var bool
*/
private $_enabled = false;
/**
* Settings to be passed to OneLogin_Saml2_Auth.
*
* @var array
*/
private $_settings = [];
/**
* User attributes data.
*
* @var array
*/
private $_attributes = [];
/**
* User attributes data with FriendlyName index.
*
* @var array
*/
private $_attributesWithFriendlyName = [];
/**
* NameID
*
* @var string
*/
private $_nameid;
/**
* NameID Format
*
* @var string
*/
private $_nameidFormat;
/**
* NameID NameQualifier
*
* @var string
*/
private $_nameidNameQualifier;
/**
* NameID SP NameQualifier
*
* @var string
*/
private $_nameidSPNameQualifier;
/**
* If user is authenticated.
*
* @var bool
*/
private $_authenticated = false;
/**
* SessionIndex. When the user is logged, this stored it
* from the AuthnStatement of the SAML Response
*
* @var string
*/
private $_sessionIndex;
/**
* SessionNotOnOrAfter. When the user is logged, this stored it
* from the AuthnStatement of the SAML Response
*
* @var int|null
*/
private $_sessionExpiration;
/**
* Initializes the SAML service and builds the OneLogin_Saml2_Auth instance.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @throws Exception
* @throws Error
*/
public function __construct()
{
$this->loadSettings();
if ($this->isEnabled()) {
$this->loadDataFromSession();
} else {
$this->clearData();
}
try {
$this->_auth = new OneLogin_Saml2_Auth($this->_settings);
} catch (Exception $e) {
if ( $this->isEnabled() ) { // $this->loadSettings() initializes this to true if SAML is enabled by settings.
Log::warning('Trying OneLogin_Saml2_Auth failed. Setting SAML enabled to false. OneLogin_Saml2_Auth error message is: '. $e->getMessage());
}
$this->_enabled = false;
}
}
/**
* Builds settings from Snipe-IT for OneLogin_Saml2_Auth.
*
* @author Johnson Yi <jyi.dev@outlook.com>
* @author Michael Pietsch <skywalker-11@mi-pietsch.de>
*
* @since 5.0.0
*
* @return void
*/
private function loadSettings()
{
$setting = Setting::getSettings();
$settings = [];
$this->_enabled = $setting->saml_enabled == '1';
if ($this->isEnabled()) {
//Let onelogin/php-saml know to use 'X-Forwarded-*' headers if it is from a trusted proxy
OneLogin_Saml2_Utils::setProxyVars(request()->isFromTrustedProxy());
data_set($settings, 'sp.entityId', config('app.url'));
data_set($settings, 'sp.assertionConsumerService.url', route('saml.acs'));
data_set($settings, 'sp.singleLogoutService.url', route('saml.sls'));
data_set($settings, 'sp.x509cert', $setting->saml_sp_x509cert);
data_set($settings, 'sp.privateKey', $setting->saml_sp_privatekey);
if (! empty($setting->saml_sp_x509certNew)) {
data_set($settings, 'sp.x509certNew', $setting->saml_sp_x509certNew);
} else {
data_set($settings, 'sp.x509certNew', '');
}
if (! empty(data_get($settings, 'sp.privateKey'))) {
data_set($settings, 'security.logoutRequestSigned', true);
data_set($settings, 'security.logoutResponseSigned', true);
}
$idpMetadata = $setting->saml_idp_metadata;
if (! empty($idpMetadata)) {
$updatedAt = $setting->updated_at->timestamp;
$metadataCache = Cache::get('saml_idp_metadata_cache');
try {
$url = null;
$metadataInfo = null;
if (empty($metadataCache) || $metadataCache['updated_at'] != $updatedAt) {
if (filter_var($idpMetadata, FILTER_VALIDATE_URL)) {
$url = $idpMetadata;
$metadataInfo = OneLogin_Saml2_IdPMetadataParser::parseRemoteXML($idpMetadata);
} else {
$metadataInfo = OneLogin_Saml2_IdPMetadataParser::parseXML($idpMetadata);
}
Cache::put('saml_idp_metadata_cache', [
'updated_at' => $updatedAt,
'url' => $url,
'metadata_info' => $metadataInfo,
]);
} else {
$metadataInfo = $metadataCache['metadata_info'];
}
$settings = OneLogin_Saml2_IdPMetadataParser::injectIntoSettings($settings, $metadataInfo);
} catch (Exception $e) {
}
}
$custom_settings = preg_split('/\r\n|\r|\n/', $setting->saml_custom_settings);
if ($custom_settings) {
foreach ($custom_settings as $custom_setting) {
$split = explode('=', $custom_setting, 2);
if (count($split) == 2) {
$boolValue = filter_var($split[1], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if (! is_null($boolValue)) {
$split[1] = $boolValue;
}
data_set($settings, $split[0], $split[1]);
}
}
}
$this->_settings = $settings;
}
}
/**
* Load SAML data from Session.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @return void
*/
private function loadDataFromSession()
{
$samlData = collect(session(self::DATA_SESSION_KEY));
$this->_authenticated = ! $samlData->isEmpty();
$this->_nameid = $samlData->get('nameId');
$this->_nameidFormat = $samlData->get('nameIdFormat');
$this->_nameidNameQualifier = $samlData->get('nameIdNameQualifier');
$this->_nameidSPNameQualifier = $samlData->get('nameIdSPNameQualifier');
$this->_sessionIndex = $samlData->get('sessionIndex');
$this->_sessionExpiration = $samlData->get('sessionExpiration');
$this->_attributes = $samlData->get('attributes');
$this->_attributesWithFriendlyName = $samlData->get('attributesWithFriendlyName');
}
/**
* Save SAML data to Session.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @param string $data
*
* @return void
*/
private function saveDataToSession($data)
{
return session([self::DATA_SESSION_KEY => $data]);
}
/**
* Check to see if SAML is enabled and has valid settings.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @return bool
*/
public function isEnabled()
{
return $this->_enabled;
}
/**
* Clear SAML data from session.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @return void
*/
public function clearData()
{
Session::forget(self::DATA_SESSION_KEY);
$this->loadDataFromSession();
}
/**
* Find user from SAML data.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @param string $data
*
* @return \App\Models\User
*/
public function samlLogin($data)
{
$this->saveDataToSession($data);
$this->loadDataFromSession();
$username = $this->getUsername();
return User::where('username', '=', $username)->whereNull('deleted_at')->where('activated', '=', '1')->first();
}
/**
* Returns the OneLogin_Saml2_Auth instance.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @return OneLogin\Saml2\Auth
*/
public function getAuth()
{
if (! $this->isEnabled()) {
throw new HttpException(403, 'SAML not enabled.');
}
return $this->_auth;
}
/**
* Get a setting.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @param string|array|int $key
* @param mixed $default
*
* @return void
*/
public function getSetting($key, $default = null)
{
return data_get($this->_settings, $key, $default);
}
/**
* Gets the SP metadata. The XML representation.
*
* @param bool $alwaysPublishEncryptionCert When 'true', the returned
* metadata will always include an 'encryption' KeyDescriptor. Otherwise,
* the 'encryption' KeyDescriptor will only be included if
* $advancedSettings['security']['wantNameIdEncrypted'] or
* $advancedSettings['security']['wantAssertionsEncrypted'] are enabled.
* @param int|null $validUntil Metadata's valid time
* @param int|null $cacheDuration Duration of the cache in seconds
*
* @return string SP metadata (xml)
*/
public function getSPMetadata($alwaysPublishEncryptionCert = false, $validUntil = null, $cacheDuration = null)
{
try {
$settings = new OneLogin_Saml2_Settings($this->_settings, true);
$metadata = $settings->getSPMetadata($alwaysPublishEncryptionCert, $validUntil, $cacheDuration);
return $metadata;
} catch (Exception $e) {
return '';
}
}
/**
* Extract data from SAML Response.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @return array
*/
public function extractData()
{
$auth = $this->getAuth();
return [
'attributes' => $auth->getAttributes(),
'attributesWithFriendlyName' => $auth->getAttributesWithFriendlyName(),
'nameId' => $auth->getNameId(),
'nameIdFormat' => $auth->getNameIdFormat(),
'nameIdNameQualifier' => $auth->getNameIdNameQualifier(),
'nameIdSPNameQualifier' => $auth->getNameIdSPNameQualifier(),
'sessionIndex' => $auth->getSessionIndex(),
'sessionExpiration' => $auth->getSessionExpiration(),
'nonce' => $auth->getLastAssertionId(),
'assertionNotOnOrAfter' => $auth->getLastAssertionNotOnOrAfter(),
];
}
/**
* Checks if the user is authenticated or not.
*
* @return bool True if the user is authenticated
*/
public function isAuthenticated()
{
return $this->_authenticated;
}
/**
* Returns the set of SAML attributes.
*
* @return array Attributes of the user.
*/
public function getAttributes()
{
return $this->_attributes;
}
/**
* Returns the set of SAML attributes indexed by FriendlyName
*
* @return array Attributes of the user.
*/
public function getAttributesWithFriendlyName()
{
return $this->_attributesWithFriendlyName;
}
/**
* Returns the nameID
*
* @return string The nameID of the assertion
*/
public function getNameId()
{
return $this->_nameid;
}
/**
* Returns the nameID Format
*
* @return string The nameID Format of the assertion
*/
public function getNameIdFormat()
{
return $this->_nameidFormat;
}
/**
* Returns the nameID NameQualifier
*
* @return string The nameID NameQualifier of the assertion
*/
public function getNameIdNameQualifier()
{
return $this->_nameidNameQualifier;
}
/**
* Returns the nameID SP NameQualifier
*
* @return string The nameID SP NameQualifier of the assertion
*/
public function getNameIdSPNameQualifier()
{
return $this->_nameidSPNameQualifier;
}
/**
* Returns the SessionIndex
*
* @return string|null The SessionIndex of the assertion
*/
public function getSessionIndex()
{
return $this->_sessionIndex;
}
/**
* Returns the SessionNotOnOrAfter
*
* @return int|null The SessionNotOnOrAfter of the assertion
*/
public function getSessionExpiration()
{
return $this->_sessionExpiration;
}
/**
* Returns the correct username from SAML Response.
*
* @author Johnson Yi <jyi.dev@outlook.com>
*
* @since 5.0.0
*
* @return string
*/
public function getUsername()
{
$setting = Setting::getSettings();
$username = $this->getNameId();
if (! empty($setting->saml_attr_mapping_username)) {
$attributes = $this->getAttributes();
if (isset($attributes[$setting->saml_attr_mapping_username])) {
$username = $attributes[$setting->saml_attr_mapping_username][0];
}
}
return $username;
}
}