517 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			517 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;
 | 
						|
 | 
						|
/**
 | 
						|
 * 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;
 | 
						|
    }
 | 
						|
}
 |