mirror of
https://github.com/snipe/snipe-it.git
synced 2024-12-26 22:19:41 -08:00
Merge pull request #8023 from johnson-yi/features/saml_auth
Added #542: add saml authentication
This commit is contained in:
commit
c3d8024e1f
|
@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\LdapAd;
|
use App\Services\LdapAd;
|
||||||
|
use App\Services\Saml;
|
||||||
use Com\Tecnick\Barcode\Barcode;
|
use Com\Tecnick\Barcode\Barcode;
|
||||||
use Google2FA;
|
use Google2FA;
|
||||||
use Illuminate\Foundation\Auth\ThrottlesLogins;
|
use Illuminate\Foundation\Auth\ThrottlesLogins;
|
||||||
|
@ -44,26 +45,38 @@ class LoginController extends Controller
|
||||||
*/
|
*/
|
||||||
protected $ldap;
|
protected $ldap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Saml
|
||||||
|
*/
|
||||||
|
protected $saml;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new authentication controller instance.
|
* Create a new authentication controller instance.
|
||||||
*
|
*
|
||||||
* @param LdapAd $ldap
|
* @param LdapAd $ldap
|
||||||
|
* @param Saml $saml
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function __construct(LdapAd $ldap)
|
public function __construct(LdapAd $ldap, Saml $saml)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->middleware('guest', ['except' => ['logout','postTwoFactorAuth','getTwoFactorAuth','getTwoFactorEnroll']]);
|
$this->middleware('guest', ['except' => ['logout','postTwoFactorAuth','getTwoFactorAuth','getTwoFactorEnroll']]);
|
||||||
Session::put('backUrl', \URL::previous());
|
Session::put('backUrl', \URL::previous());
|
||||||
$this->ldap = $ldap;
|
$this->ldap = $ldap;
|
||||||
|
$this->saml = $saml;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLoginForm(Request $request)
|
function showLoginForm(Request $request)
|
||||||
{
|
{
|
||||||
$this->loginViaRemoteUser($request);
|
$this->loginViaRemoteUser($request);
|
||||||
|
$this->loginViaSaml($request);
|
||||||
if (Auth::check()) {
|
if (Auth::check()) {
|
||||||
return redirect()->intended('dashboard');
|
return redirect()->intended('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->saml->isEnabled() && Setting::getSettings()->saml_forcelogin == "1" && !($request->has('nosaml') || $request->session()->has('error'))) {
|
||||||
|
return redirect()->route('saml.login');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Setting::getSettings()->login_common_disabled == "1") {
|
if (Setting::getSettings()->login_common_disabled == "1") {
|
||||||
|
@ -73,6 +86,47 @@ class LoginController extends Controller
|
||||||
return view('auth.login');
|
return view('auth.login');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log in a user by SAML
|
||||||
|
*
|
||||||
|
* @author Johnson Yi <jyi.dev@outlook.com>
|
||||||
|
*
|
||||||
|
* @since 5.0.0
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
*
|
||||||
|
* @return User
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function loginViaSaml(Request $request)
|
||||||
|
{
|
||||||
|
$saml = $this->saml;
|
||||||
|
$samlData = $request->session()->get('saml_login');
|
||||||
|
if ($saml->isEnabled() && !empty($samlData)) {
|
||||||
|
try {
|
||||||
|
LOG::debug("Attempting to log user in by SAML authentication.");
|
||||||
|
$user = $saml->samlLogin($samlData);
|
||||||
|
if(!is_null($user)) {
|
||||||
|
Auth::login($user, true);
|
||||||
|
} else {
|
||||||
|
$username = $saml->getUsername();
|
||||||
|
LOG::debug("SAML user '$username' could not be found in database.");
|
||||||
|
$request->session()->flash('error', trans('auth/message.signin.error'));
|
||||||
|
$saml->clearData();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user = Auth::user()) {
|
||||||
|
$user->last_login = \Carbon::now();
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
LOG::debug("There was an error authenticating the SAML user: " . $e->getMessage());
|
||||||
|
throw new \Exception($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log in a user by LDAP
|
* Log in a user by LDAP
|
||||||
*
|
*
|
||||||
|
@ -309,17 +363,40 @@ class LoginController extends Controller
|
||||||
*/
|
*/
|
||||||
public function logout(Request $request)
|
public function logout(Request $request)
|
||||||
{
|
{
|
||||||
|
$settings = Setting::getSettings();
|
||||||
|
$saml = $this->saml;
|
||||||
|
$sloRedirectUrl = null;
|
||||||
|
$sloRequestUrl = null;
|
||||||
|
|
||||||
|
if ($saml->isEnabled()) {
|
||||||
|
$auth = $saml->getAuth();
|
||||||
|
$sloRedirectUrl = $request->session()->get('saml_slo_redirect_url');
|
||||||
|
|
||||||
|
if (!empty($auth->getSLOurl()) && $settings->saml_slo == '1' && $saml->isAuthenticated() && empty($sloRedirectUrl)) {
|
||||||
|
$sloRequestUrl = $auth->logout(null, array(), $saml->getNameId(), $saml->getSessionIndex(), true, $saml->getNameIdFormat(), $saml->getNameIdNameQualifier(), $saml->getNameIdSPNameQualifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
$saml->clearData();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($sloRequestUrl)) {
|
||||||
|
return redirect()->away($sloRequestUrl);
|
||||||
|
}
|
||||||
|
|
||||||
$request->session()->forget('2fa_authed');
|
$request->session()->forget('2fa_authed');
|
||||||
|
|
||||||
Auth::logout();
|
Auth::logout();
|
||||||
|
|
||||||
$settings = Setting::getSettings();
|
if (!empty($sloRedirectUrl)) {
|
||||||
|
return redirect()->away($sloRedirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
$customLogoutUrl = $settings->login_remote_user_custom_logout_url ;
|
$customLogoutUrl = $settings->login_remote_user_custom_logout_url ;
|
||||||
if ($settings->login_remote_user_enabled == '1' && $customLogoutUrl != '') {
|
if ($settings->login_remote_user_enabled == '1' && $customLogoutUrl != '') {
|
||||||
return redirect()->away($customLogoutUrl);
|
return redirect()->away($customLogoutUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->route('login')->with('success', trans('auth/message.logout.success'));
|
return redirect()->route('login')->with(['success' => trans('auth/message.logout.success'), 'loggedout' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
140
app/Http/Controllers/Auth/SamlController.php
Normal file
140
app/Http/Controllers/Auth/SamlController.php
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\Saml;
|
||||||
|
use Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This controller provides the endpoint for SAML communication and metadata.
|
||||||
|
*
|
||||||
|
* @author Johnson Yi <jyi.dev@outlook.com>
|
||||||
|
*
|
||||||
|
* @since 5.0.0
|
||||||
|
*/
|
||||||
|
class SamlController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Saml
|
||||||
|
*/
|
||||||
|
protected $saml;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new authentication controller instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Saml $saml)
|
||||||
|
{
|
||||||
|
$this->saml = $saml;
|
||||||
|
|
||||||
|
$this->middleware('guest', ['except' => ['metadata','sls']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return SAML SP metadata for Snipe-IT
|
||||||
|
*
|
||||||
|
* /saml/metadata
|
||||||
|
*
|
||||||
|
* @author Johnson Yi <jyi.dev@outlook.com>
|
||||||
|
*
|
||||||
|
* @since 5.0.0
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function metadata(Request $request)
|
||||||
|
{
|
||||||
|
$metadata = $this->saml->getSPMetadata();
|
||||||
|
|
||||||
|
if (empty($metadata)) {
|
||||||
|
return response()->view('errors.403', [], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response($metadata)->header('Content-Type', 'text/xml');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin the SP-Initiated SSO by sending AuthN to the IdP.
|
||||||
|
*
|
||||||
|
* /login/saml
|
||||||
|
*
|
||||||
|
* @author Johnson Yi <jyi.dev@outlook.com>
|
||||||
|
*
|
||||||
|
* @since 5.0.0
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
*
|
||||||
|
* @return Redirect
|
||||||
|
*/
|
||||||
|
public function login(Request $request)
|
||||||
|
{
|
||||||
|
$auth = $this->saml->getAuth();
|
||||||
|
$ssoUrl = $auth->login(null, array(), false, false, false, false);
|
||||||
|
return redirect()->away($ssoUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives, parses the assertion from IdP and flashes SAML data
|
||||||
|
* back to the LoginController for authentication.
|
||||||
|
*
|
||||||
|
* /saml/acs
|
||||||
|
*
|
||||||
|
* @author Johnson Yi <jyi.dev@outlook.com>
|
||||||
|
*
|
||||||
|
* @since 5.0.0
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
*
|
||||||
|
* @return Redirect
|
||||||
|
*/
|
||||||
|
public function acs(Request $request)
|
||||||
|
{
|
||||||
|
$saml = $this->saml;
|
||||||
|
$auth = $saml->getAuth();
|
||||||
|
$auth->processResponse();
|
||||||
|
$errors = $auth->getErrors();
|
||||||
|
|
||||||
|
if (!empty($errors)) {
|
||||||
|
Log::debug("There was an error with SAML ACS: " . implode(', ', $errors));
|
||||||
|
Log::debug("Reason: " . $auth->getLastErrorReason());
|
||||||
|
return redirect()->route('login')->with('error', trans('auth/message.signin.error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$samlData = $saml->extractData();
|
||||||
|
|
||||||
|
return redirect()->route('login')->with('saml_login', $samlData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives LogoutRequest/LogoutResponse from IdP and flashes
|
||||||
|
* back to the LoginController for logging out.
|
||||||
|
*
|
||||||
|
* /saml/slo
|
||||||
|
*
|
||||||
|
* @author Johnson Yi <jyi.dev@outlook.com>
|
||||||
|
*
|
||||||
|
* @since 5.0.0
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
*
|
||||||
|
* @return Redirect
|
||||||
|
*/
|
||||||
|
public function sls(Request $request)
|
||||||
|
{
|
||||||
|
$auth = $this->saml->getAuth();
|
||||||
|
$sloUrl = $auth->processSLO(true, null, null, null, true);
|
||||||
|
$errors = $auth->getErrors();
|
||||||
|
|
||||||
|
if (!empty($errors)) {
|
||||||
|
Log::debug("There was an error with SAML SLS: " . implode(', ', $errors));
|
||||||
|
Log::debug("Reason: " . $auth->getLastErrorReason());
|
||||||
|
return view('errors.403');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('logout')->with('saml_slo_redirect_url', $sloUrl);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ namespace App\Http\Controllers;
|
||||||
use enshrined\svgSanitize\Sanitizer;
|
use enshrined\svgSanitize\Sanitizer;
|
||||||
use App\Helpers\Helper;
|
use App\Helpers\Helper;
|
||||||
use App\Http\Requests\ImageUploadRequest;
|
use App\Http\Requests\ImageUploadRequest;
|
||||||
|
use App\Http\Requests\SettingsSamlRequest;
|
||||||
use App\Http\Requests\SetupUserRequest;
|
use App\Http\Requests\SetupUserRequest;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
@ -1002,6 +1003,56 @@ class SettingsController extends Controller
|
||||||
return redirect()->back()->withInput()->withErrors($setting->getErrors());
|
return redirect()->back()->withInput()->withErrors($setting->getErrors());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a form to allow a super admin to update settings.
|
||||||
|
*
|
||||||
|
* @author Johnson Yi <jyi.dev@outlook.com>
|
||||||
|
*
|
||||||
|
* @since v5.0.0
|
||||||
|
*
|
||||||
|
* @return View
|
||||||
|
*/
|
||||||
|
public function getSamlSettings()
|
||||||
|
{
|
||||||
|
$setting = Setting::getSettings();
|
||||||
|
|
||||||
|
return view('settings.saml', compact('setting'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves settings from form.
|
||||||
|
*
|
||||||
|
* @author Johnson Yi <jyi.dev@outlook.com>
|
||||||
|
*
|
||||||
|
* @since v5.0.0
|
||||||
|
*
|
||||||
|
* @return View
|
||||||
|
*/
|
||||||
|
public function postSamlSettings(SettingsSamlRequest $request)
|
||||||
|
{
|
||||||
|
if (is_null($setting = Setting::getSettings())) {
|
||||||
|
return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$setting->saml_enabled = $request->input('saml_enabled', '0');
|
||||||
|
$setting->saml_idp_metadata = $request->input('saml_idp_metadata');
|
||||||
|
$setting->saml_attr_mapping_username = $request->input('saml_attr_mapping_username');
|
||||||
|
$setting->saml_forcelogin = $request->input('saml_forcelogin', '0');
|
||||||
|
$setting->saml_slo = $request->input('saml_slo', '0');
|
||||||
|
if (!empty($request->input('saml_sp_privatekey'))) {
|
||||||
|
$setting->saml_sp_x509cert = $request->input('saml_sp_x509cert');
|
||||||
|
$setting->saml_sp_privatekey = $request->input('saml_sp_privatekey');
|
||||||
|
}
|
||||||
|
$setting->saml_custom_settings = $request->input('saml_custom_settings');
|
||||||
|
|
||||||
|
if ($setting->save()) {
|
||||||
|
return redirect()->route('settings.saml.index')
|
||||||
|
->with('success', trans('admin/settings/message.update.success'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()->withInput()->withErrors($setting->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the listing of backups.
|
* Show the listing of backups.
|
||||||
*
|
*
|
||||||
|
|
113
app/Http/Requests/SettingsSamlRequest.php
Normal file
113
app/Http/Requests/SettingsSamlRequest.php
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use OneLogin\Saml2\IdPMetadataParser as OneLogin_Saml2_IdPMetadataParser;
|
||||||
|
use OneLogin\Saml2\Utils as OneLogin_Saml2_Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This handles validating and cleaning SAML settings provided by the user.
|
||||||
|
*
|
||||||
|
* @author Johnson Yi <jyi.dev@outlook.com>
|
||||||
|
*
|
||||||
|
* @since 5.0.0
|
||||||
|
*/
|
||||||
|
class SettingsSamlRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withValidator($validator)
|
||||||
|
{
|
||||||
|
$validator->after(function ($validator) {
|
||||||
|
if ($this->input('saml_enabled') == '1') {
|
||||||
|
|
||||||
|
$idpMetadata = $this->input('saml_idp_metadata');
|
||||||
|
if (!empty($idpMetadata)) {
|
||||||
|
try {
|
||||||
|
if (filter_var($idpMetadata, FILTER_VALIDATE_URL)) {
|
||||||
|
$metadataInfo = OneLogin_Saml2_IdPMetadataParser::parseRemoteXML($idpMetadata);
|
||||||
|
} else {
|
||||||
|
$metadataInfo = OneLogin_Saml2_IdPMetadataParser::parseXML($idpMetadata);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$validator->errors()->add('saml_idp_metadata', trans('validation.url', ['attribute' => 'Metadata']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->input('saml_sp_regenerate_keypair') == '1' || !$this->has('saml_sp_x509cert')) {
|
||||||
|
$dn = [
|
||||||
|
"countryName" => "US",
|
||||||
|
"stateOrProvinceName" => "N/A",
|
||||||
|
"localityName" => "N/A",
|
||||||
|
"organizationName" => "Snipe-IT",
|
||||||
|
"commonName" => "Snipe-IT",
|
||||||
|
];
|
||||||
|
|
||||||
|
$pkey = openssl_pkey_new([
|
||||||
|
"private_key_bits" => 2048,
|
||||||
|
"private_key_type" => OPENSSL_KEYTYPE_RSA,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$csr = openssl_csr_new($dn, $pkey, ['digest_alg' => 'sha256']);
|
||||||
|
|
||||||
|
$x509 = openssl_csr_sign($csr, null, $pkey, 3650, ['digest_alg' => 'sha256']);
|
||||||
|
|
||||||
|
openssl_x509_export($x509, $x509cert);
|
||||||
|
openssl_pkey_export($pkey, $privateKey);
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
while (($error = openssl_error_string() !== false)) {
|
||||||
|
$errors[] = $error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(empty($x509cert) && empty($privateKey))) {
|
||||||
|
$this->merge([
|
||||||
|
'saml_sp_x509cert' => $x509cert,
|
||||||
|
'saml_sp_privatekey' => $privateKey,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($this->input('saml_custom_settings'))) {
|
||||||
|
$req_custom_settings = preg_split('/\r\n|\r|\n/', $this->input('saml_custom_settings'));
|
||||||
|
$custom_settings = [];
|
||||||
|
|
||||||
|
foreach ($req_custom_settings as $custom_setting) {
|
||||||
|
$split = explode('=', $custom_setting, 2);
|
||||||
|
|
||||||
|
if (count($split) == 2) {
|
||||||
|
$split[0] = trim($split[0]);
|
||||||
|
$split[1] = trim($split[1]);
|
||||||
|
|
||||||
|
if (!empty($split[0])) {
|
||||||
|
$custom_settings[] = implode('=', $split);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->merge(['saml_custom_settings' => implode(PHP_EOL, $custom_settings) . PHP_EOL]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
69
app/Providers/SamlServiceProvider.php
Normal file
69
app/Providers/SamlServiceProvider.php
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Services\Saml;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
class SamlServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Bootstrap the application services.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function boot()
|
||||||
|
{
|
||||||
|
$this->app->singleton(Saml::class, Saml::class);
|
||||||
|
|
||||||
|
Route::group(['namespace'=> 'App\Http\Controllers'], function () {
|
||||||
|
Route::group(['prefix'=> 'saml'], function () {
|
||||||
|
Route::get(
|
||||||
|
'metadata',
|
||||||
|
[
|
||||||
|
'as' => 'saml.metadata',
|
||||||
|
'uses' => 'Auth\SamlController@metadata' ]
|
||||||
|
);
|
||||||
|
|
||||||
|
Route::match(
|
||||||
|
['get', 'post'],
|
||||||
|
'acs',
|
||||||
|
[
|
||||||
|
'as' => 'saml.acs',
|
||||||
|
'uses' => 'Auth\SamlController@acs' ]
|
||||||
|
);
|
||||||
|
|
||||||
|
Route::get(
|
||||||
|
'sls',
|
||||||
|
[
|
||||||
|
'as' => 'saml.sls',
|
||||||
|
'uses' => 'Auth\SamlController@sls' ]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::get(
|
||||||
|
'login/saml',
|
||||||
|
[
|
||||||
|
'as' => 'saml.login',
|
||||||
|
'uses' => 'Auth\SamlController@login' ]
|
||||||
|
);
|
||||||
|
|
||||||
|
Route::group(['prefix' => 'admin','middleware' => ['auth', 'authorize:superuser']], function () {
|
||||||
|
|
||||||
|
Route::get('saml', ['as' => 'settings.saml.index','uses' => 'SettingsController@getSamlSettings' ]);
|
||||||
|
Route::post('saml', ['as' => 'settings.saml.save','uses' => 'SettingsController@postSamlSettings' ]);
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the application services.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
487
app/Services/Saml.php
Normal file
487
app/Services/Saml.php
Normal file
|
@ -0,0 +1,487 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
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 App\Models\Setting;
|
||||||
|
use App\Models\User;
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Session;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
function __construct()
|
||||||
|
{
|
||||||
|
$this->loadSettings();
|
||||||
|
|
||||||
|
if ($this->isEnabled()) {
|
||||||
|
$this->loadDataFromSession();
|
||||||
|
} else {
|
||||||
|
$this->clearData();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->_auth = new OneLogin_Saml2_Auth($this->_settings);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->_enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds settings from Snipe-IT for OneLogin_Saml2_Auth.
|
||||||
|
*
|
||||||
|
* @author Johnson Yi <jyi.dev@outlook.com>
|
||||||
|
*
|
||||||
|
* @since 5.0.0
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function loadSettings()
|
||||||
|
{
|
||||||
|
$setting = Setting::getSettings();
|
||||||
|
$settings = [];
|
||||||
|
|
||||||
|
$this->_enabled = $setting->saml_enabled == '1';
|
||||||
|
|
||||||
|
if ($this->isEnabled()) {
|
||||||
|
data_set($settings, 'sp.entityId', 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);
|
||||||
|
data_set($settings, 'security.wantAssertionsSigned', true);
|
||||||
|
data_set($settings, 'security.requestedAuthnContext', false);
|
||||||
|
|
||||||
|
if (!empty(data_get($settings, 'sp.privateKey'))) {
|
||||||
|
data_set($settings, 'security.logoutRequestSigned', true);
|
||||||
|
data_set($settings, 'security.logoutResponseSigned', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$idpMetadata = $setting->saml_idp_metadata;
|
||||||
|
$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,
|
||||||
|
], 604800);
|
||||||
|
} 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) {
|
||||||
|
$setting = Setting::getSettings();
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,6 +45,7 @@
|
||||||
"maknz/slack": "^1.7",
|
"maknz/slack": "^1.7",
|
||||||
"neitanod/forceutf8": "^2.0",
|
"neitanod/forceutf8": "^2.0",
|
||||||
"nesbot/carbon": "^2.32",
|
"nesbot/carbon": "^2.32",
|
||||||
|
"onelogin/php-saml": "^3.4",
|
||||||
"paragonie/constant_time_encoding": "^2.3",
|
"paragonie/constant_time_encoding": "^2.3",
|
||||||
"patchwork/utf8": "^1.3",
|
"patchwork/utf8": "^1.3",
|
||||||
"phpdocumentor/reflection-docblock": "^5.1",
|
"phpdocumentor/reflection-docblock": "^5.1",
|
||||||
|
|
90
composer.lock
generated
90
composer.lock
generated
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "693916c94ecc3dc06b80b9f83d65e4cb",
|
"content-hash": "3fe8a441e49d1299687346810b350e00",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "adldap2/adldap2",
|
"name": "adldap2/adldap2",
|
||||||
|
@ -3595,6 +3595,56 @@
|
||||||
],
|
],
|
||||||
"time": "2019-09-05T13:24:16+00:00"
|
"time": "2019-09-05T13:24:16+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "onelogin/php-saml",
|
||||||
|
"version": "3.4.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/onelogin/php-saml.git",
|
||||||
|
"reference": "5fbf3486704ac9835b68184023ab54862c95f213"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/onelogin/php-saml/zipball/5fbf3486704ac9835b68184023ab54862c95f213",
|
||||||
|
"reference": "5fbf3486704ac9835b68184023ab54862c95f213",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=5.4",
|
||||||
|
"robrichards/xmlseclibs": ">=3.0.4"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"pdepend/pdepend": "^2.5.0",
|
||||||
|
"php-coveralls/php-coveralls": "^1.0.2 || ^2.0",
|
||||||
|
"phploc/phploc": "^2.1 || ^3.0 || ^4.0",
|
||||||
|
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1",
|
||||||
|
"sebastian/phpcpd": "^2.0 || ^3.0 || ^4.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.1.1"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-curl": "Install curl lib to be able to use the IdPMetadataParser for parsing remote XMLs",
|
||||||
|
"ext-gettext": "Install gettext and php5-gettext libs to handle translations",
|
||||||
|
"ext-openssl": "Install openssl lib in order to handle with x509 certs (require to support sign and encryption)"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"OneLogin\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "OneLogin PHP SAML Toolkit",
|
||||||
|
"homepage": "https://developers.onelogin.com/saml/php",
|
||||||
|
"keywords": [
|
||||||
|
"SAML2",
|
||||||
|
"onelogin",
|
||||||
|
"saml"
|
||||||
|
],
|
||||||
|
"time": "2019-11-25T17:30:07+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "opis/closure",
|
"name": "opis/closure",
|
||||||
"version": "3.5.1",
|
"version": "3.5.1",
|
||||||
|
@ -4904,6 +4954,44 @@
|
||||||
],
|
],
|
||||||
"time": "2020-02-21T04:36:14+00:00"
|
"time": "2020-02-21T04:36:14+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "robrichards/xmlseclibs",
|
||||||
|
"version": "3.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/robrichards/xmlseclibs.git",
|
||||||
|
"reference": "8d8e56ca7914440a8c60caff1a865e7dff1d9a5a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/8d8e56ca7914440a8c60caff1a865e7dff1d9a5a",
|
||||||
|
"reference": "8d8e56ca7914440a8c60caff1a865e7dff1d9a5a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-openssl": "*",
|
||||||
|
"php": ">= 5.4"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"RobRichards\\XMLSecLibs\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"BSD-3-Clause"
|
||||||
|
],
|
||||||
|
"description": "A PHP library for XML Security",
|
||||||
|
"homepage": "https://github.com/robrichards/xmlseclibs",
|
||||||
|
"keywords": [
|
||||||
|
"security",
|
||||||
|
"signature",
|
||||||
|
"xml",
|
||||||
|
"xmldsig"
|
||||||
|
],
|
||||||
|
"time": "2020-04-22T17:19:51+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "rollbar/rollbar",
|
"name": "rollbar/rollbar",
|
||||||
"version": "v2.0.0",
|
"version": "v2.0.0",
|
||||||
|
|
|
@ -339,6 +339,7 @@ return [
|
||||||
*/
|
*/
|
||||||
App\Providers\MacroServiceProvider::class,
|
App\Providers\MacroServiceProvider::class,
|
||||||
App\Providers\LdapServiceProvider::class,
|
App\Providers\LdapServiceProvider::class,
|
||||||
|
App\Providers\SamlServiceProvider::class,
|
||||||
|
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class AddSamlFieldsToSettings extends Migration {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('settings', function(Blueprint $table)
|
||||||
|
{
|
||||||
|
$table->boolean('saml_enabled')->default(0);
|
||||||
|
$table->text('saml_idp_metadata')->nullable()->default(NULL);
|
||||||
|
$table->string('saml_attr_mapping_username')->nullable()->default(NULL);
|
||||||
|
$table->boolean('saml_forcelogin')->default(0);
|
||||||
|
$table->boolean('saml_slo')->default(0);
|
||||||
|
$table->text('saml_sp_x509cert')->nullable()->default(NULL);
|
||||||
|
$table->text('saml_sp_privatekey')->nullable()->default(NULL);
|
||||||
|
$table->text('saml_custom_settings')->nullable()->default(NULL);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('settings', function(Blueprint $table)
|
||||||
|
{
|
||||||
|
$table->dropColumn('saml_enabled');
|
||||||
|
$table->dropColumn('saml_idp_metadata');
|
||||||
|
$table->dropColumn('saml_attr_mapping_username');
|
||||||
|
$table->dropColumn('saml_forcelogin');
|
||||||
|
$table->dropColumn('saml_slo');
|
||||||
|
$table->dropColumn('saml_sp_x509cert');
|
||||||
|
$table->dropColumn('saml_sp_privatekey');
|
||||||
|
$table->dropColumn('saml_custom_settings');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -118,6 +118,20 @@ return array(
|
||||||
'pwd_secure_uncommon_help' => 'This will disallow users from using common passwords from the top 10,000 passwords reported in breaches.',
|
'pwd_secure_uncommon_help' => 'This will disallow users from using common passwords from the top 10,000 passwords reported in breaches.',
|
||||||
'qr_help' => 'Enable QR Codes first to set this',
|
'qr_help' => 'Enable QR Codes first to set this',
|
||||||
'qr_text' => 'QR Code Text',
|
'qr_text' => 'QR Code Text',
|
||||||
|
'saml_enabled' => 'SAML enabled',
|
||||||
|
'saml_integration' => 'SAML Integration',
|
||||||
|
'saml_idp_metadata' => 'SAML IdP Metadata',
|
||||||
|
'saml_idp_metadata_help' => 'You can specify the IdP metadata using a URL or XML file.',
|
||||||
|
'saml_attr_mapping_username' => 'Attribute Mapping - Username',
|
||||||
|
'saml_attr_mapping_username_help' => 'NameID will be used if attribute mapping is unspecified or invalid.',
|
||||||
|
'saml_forcelogin_label' => 'SAML Force Login',
|
||||||
|
'saml_forcelogin' => 'Make SAML the primary login',
|
||||||
|
'saml_forcelogin_help' => 'You can use \'/login?nosaml\' to get to the normal login page.',
|
||||||
|
'saml_slo_label' => 'SAML Single Log Out',
|
||||||
|
'saml_slo' => 'Send a LogoutRequest to IdP on Logout',
|
||||||
|
'saml_slo_help' => 'This will cause the user to be first redirected to the Idp on logout. Leave unchecked if the IdP doesn\'t correctly support SP-initiated SAML SLO.',
|
||||||
|
'saml_custom_settings' => 'SAML Custom Settings',
|
||||||
|
'saml_custom_settings_help' => 'You can specify additional settings to the onelogin/php-saml library. Use at your own risk.',
|
||||||
'setting' => 'Setting',
|
'setting' => 'Setting',
|
||||||
'settings' => 'Settings',
|
'settings' => 'Settings',
|
||||||
'show_alerts_in_menu' => 'Show alerts in top menu',
|
'show_alerts_in_menu' => 'Show alerts in top menu',
|
||||||
|
|
|
@ -4,6 +4,7 @@ return [
|
||||||
'send_password_link' => 'Send Password Reset Link',
|
'send_password_link' => 'Send Password Reset Link',
|
||||||
'email_reset_password' => 'Email Password Reset',
|
'email_reset_password' => 'Email Password Reset',
|
||||||
'reset_password' => 'Reset Password',
|
'reset_password' => 'Reset Password',
|
||||||
|
'saml_login' => 'Login via SAML',
|
||||||
'login' => 'Login',
|
'login' => 'Login',
|
||||||
'login_prompt' => 'Please Login',
|
'login_prompt' => 'Please Login',
|
||||||
'forgot_password' => 'I forgot my password',
|
'forgot_password' => 'I forgot my password',
|
||||||
|
|
|
@ -61,6 +61,14 @@
|
||||||
</div> <!-- end col-md-12 -->
|
</div> <!-- end col-md-12 -->
|
||||||
|
|
||||||
</div> <!-- end row -->
|
</div> <!-- end row -->
|
||||||
|
|
||||||
|
@if ($snipeSettings->saml_enabled)
|
||||||
|
<div class="row ">
|
||||||
|
<div class="col-md-12 text-right">
|
||||||
|
<a href="{{ route('saml.login') }}">{{ trans('auth/general.saml_login') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="box-footer">
|
<div class="box-footer">
|
||||||
<button class="btn btn-lg btn-primary btn-block">{{ trans('auth/general.login') }}</button>
|
<button class="btn btn-lg btn-primary btn-block">{{ trans('auth/general.login') }}</button>
|
||||||
|
|
|
@ -222,6 +222,21 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4 col-lg-3 col-sm-6 col-xl-1">
|
||||||
|
<div class="box box-default">
|
||||||
|
<div class="box-body text-center">
|
||||||
|
<h5>
|
||||||
|
<a href="{{ route('settings.saml.index') }}">
|
||||||
|
<i class="fa fa-sign-in fa-4x" aria-hidden="true"></i>
|
||||||
|
<br><br>
|
||||||
|
<span class="name">SAML</span>
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<p class="help-block">SAML settings</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4 col-lg-3 col-sm-6 col-xl-1">
|
<div class="col-md-4 col-lg-3 col-sm-6 col-xl-1">
|
||||||
<div class="box box-default">
|
<div class="box box-default">
|
||||||
<div class="box-body text-center">
|
<div class="box-body text-center">
|
||||||
|
|
177
resources/views/settings/saml.blade.php
Normal file
177
resources/views/settings/saml.blade.php
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
@extends('layouts/default')
|
||||||
|
|
||||||
|
{{-- Page title --}}
|
||||||
|
@section('title')
|
||||||
|
Update SAML Settings
|
||||||
|
@parent
|
||||||
|
@stop
|
||||||
|
|
||||||
|
@section('header_right')
|
||||||
|
<a href="{{ route('settings.index') }}" class="btn btn-default"> {{ trans('general.back') }}</a>
|
||||||
|
@stop
|
||||||
|
|
||||||
|
|
||||||
|
{{-- Page content --}}
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.checkbox label {
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
{{ Form::open(['method' => 'POST', 'files' => false, 'autocomplete' => 'false', 'class' => 'form-horizontal', 'role' => 'form']) }}
|
||||||
|
<!-- CSRF Token -->
|
||||||
|
{{csrf_field()}}
|
||||||
|
|
||||||
|
<!-- this is a hack to prevent Chrome from trying to autocomplete fields -->
|
||||||
|
<input type="text" name="prevent_autofill" id="prevent_autofill" value="" style="display:none;" />
|
||||||
|
<input type="password" name="password_fake" id="password_fake" value="" style="display:none;" />
|
||||||
|
|
||||||
|
|
||||||
|
@if (!empty($setting->saml_sp_x509cert))
|
||||||
|
{{ Form::hidden('saml_sp_x509cert', $setting->saml_sp_x509cert) }}
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="panel box box-default">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h2 class="box-title">
|
||||||
|
<i class="fa fa-sign-in"></i> SAML
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-md-11 col-md-offset-1">
|
||||||
|
|
||||||
|
<!-- Enable SAML -->
|
||||||
|
<div class="form-group {{ $errors->has('saml_integration') ? 'error' : '' }}">
|
||||||
|
<div class="col-md-3">
|
||||||
|
{{ Form::label('saml_integration', trans('admin/settings/general.saml_integration')) }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
{{ Form::checkbox('saml_enabled', '1', Request::old('saml_enabled', $setting->saml_enabled), ['class' => 'minimal '. $setting->demoMode, $setting->demoMode]) }}
|
||||||
|
{{ trans('admin/settings/general.saml_enabled') }}
|
||||||
|
@if ($setting->saml_enabled)
|
||||||
|
<p class="help-block"><a href="{{ route('saml.metadata') }}" target="_blank">{{ route('saml.metadata') }}</a></p>
|
||||||
|
@endif
|
||||||
|
{!! $errors->first('saml_enabled', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SAML IdP Metadata -->
|
||||||
|
<div class="form-group {{ $errors->has('saml_idp_metadata') ? 'error' : '' }}">
|
||||||
|
<div class="col-md-3">
|
||||||
|
{{ Form::label('saml_idp_metadata', trans('admin/settings/general.saml_idp_metadata')) }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
{{ Form::textarea('saml_idp_metadata', old('saml_idp_metadata', $setting->saml_idp_metadata), ['class' => 'form-control','placeholder' => 'https://example.com/idp/metadata', 'wrap' => 'off', $setting->demoMode]) }}
|
||||||
|
{!! $errors->first('saml_idp_metadata', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}<br>
|
||||||
|
<button type="button" class="btn btn-default" id="saml_idp_metadata_upload_btn">{{ trans('button.select_file') }}</button>
|
||||||
|
<input type="file" class="js-uploadFile" id="saml_idp_metadata_upload"
|
||||||
|
data-maxsize="{{ \App\Helpers\Helper::file_upload_max_size() }}"
|
||||||
|
accept="text/*" style="display:none; max-width: 90%">
|
||||||
|
|
||||||
|
<p class="help-block">{{ trans('admin/settings/general.saml_idp_metadata_help') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SAML Attribute Mapping Username -->
|
||||||
|
<div class="form-group {{ $errors->has('saml_attr_mapping_username') ? 'error' : '' }}">
|
||||||
|
<div class="col-md-3">
|
||||||
|
{{ Form::label('saml_attr_mapping_username', trans('admin/settings/general.saml_attr_mapping_username')) }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
{{ Form::text('saml_attr_mapping_username', Request::old('saml_attr_mapping_username', $setting->saml_attr_mapping_username), ['class' => 'form-control','placeholder' => '', $setting->demoMode]) }}
|
||||||
|
<p class="help-block">{{ trans('admin/settings/general.saml_attr_mapping_username_help') }}</p>
|
||||||
|
{!! $errors->first('saml_attr_mapping_username', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
|
||||||
|
</div>
|
||||||
|
</div><!-- AD Domain -->
|
||||||
|
|
||||||
|
<!-- SAML Force Login -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-3">
|
||||||
|
{{ Form::label('saml_forcelogin', trans('admin/settings/general.saml_forcelogin_label')) }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
{{ Form::checkbox('saml_forcelogin', '1', Request::old('saml_forcelogin', $setting->saml_forcelogin),['class' => 'minimal '. $setting->demoMode, $setting->demoMode]) }}
|
||||||
|
{{ trans('admin/settings/general.saml_forcelogin') }}
|
||||||
|
<p class="help-block">{{ trans('admin/settings/general.saml_forcelogin_help') }}</p>
|
||||||
|
<p class="help-block">{{ route('login', ['nosaml']) }}</p>
|
||||||
|
{!! $errors->first('saml_forcelogin', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SAML Single Log Out -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-3">
|
||||||
|
{{ Form::label('saml_slo', trans('admin/settings/general.saml_slo_label')) }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
{{ Form::checkbox('saml_slo', '1', Request::old('saml_slo', $setting->saml_slo),['class' => 'minimal '. $setting->demoMode, $setting->demoMode]) }}
|
||||||
|
{{ trans('admin/settings/general.saml_slo') }}
|
||||||
|
<p class="help-block">{{ trans('admin/settings/general.saml_slo_help') }}</p>
|
||||||
|
{!! $errors->first('saml_slo', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SAML Custom Options -->
|
||||||
|
<div class="form-group {{ $errors->has('saml_custom_settings') ? 'error' : '' }}">
|
||||||
|
<div class="col-md-3">
|
||||||
|
{{ Form::label('saml_custom_settings', trans('admin/settings/general.saml_custom_settings')) }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9">
|
||||||
|
{{ Form::textarea('saml_custom_settings', old('saml_custom_settings', $setting->saml_custom_settings), ['class' => 'form-control','placeholder' => 'example.option=false', 'wrap' => 'off', $setting->demoMode]) }}
|
||||||
|
<p class="help-block">{{ trans('admin/settings/general.saml_custom_settings_help') }}</p>
|
||||||
|
{!! $errors->first('saml_custom_settings', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div> <!--/.box-body-->
|
||||||
|
<div class="box-footer">
|
||||||
|
<div class="text-left col-md-6">
|
||||||
|
<a class="btn btn-link text-left" href="{{ route('settings.index') }}">{{ trans('button.cancel') }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="text-right col-md-6">
|
||||||
|
<button type="submit" class="btn btn-primary"><i class="fa fa-check icon-white" aria-hidden="true"></i> {{ trans('general.save') }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div> <!-- /box -->
|
||||||
|
|
||||||
|
</div> <!-- /.col-md-8-->
|
||||||
|
</div> <!-- /.row-->
|
||||||
|
|
||||||
|
{{Form::close()}}
|
||||||
|
|
||||||
|
|
||||||
|
@stop
|
||||||
|
|
||||||
|
@push('js')
|
||||||
|
<script nonce="{{ csrf_token() }}">
|
||||||
|
|
||||||
|
$('#saml_idp_metadata_upload_btn').click(function() {
|
||||||
|
$('#saml_idp_metadata_upload').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#saml_idp_metadata_upload').on('change', function () {
|
||||||
|
var fr = new FileReader();
|
||||||
|
|
||||||
|
fr.onload = function(e) {
|
||||||
|
$('#saml_idp_metadata').text(e.target.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
fr.readAsText(this.files[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue