STORAGE_URL=false
# Authentication method to use
-# Can be 'standard' or 'ldap'
+# Can be 'standard', 'ldap' or 'saml2'
AUTH_METHOD=standard
# Social authentication configuration
# SAML authentication configuration
# Refer to https://www.bookstackapp.com/docs/admin/saml2-auth/
SAML2_NAME=SSO
-SAML2_ENABLED=false
-SAML2_AUTO_REGISTER=true
SAML2_EMAIL_ATTRIBUTE=email
SAML2_DISPLAY_NAME_ATTRIBUTES=username
SAML2_EXTERNAL_ID_ATTRIBUTE=null
namespace BookStack\Auth\Access\Guards;
+use BookStack\Auth\User;
+use BookStack\Auth\UserRepo;
+use BookStack\Exceptions\LoginAttemptEmailNeededException;
+use BookStack\Exceptions\LoginAttemptException;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\StatefulGuard;
*/
protected $loggedOut = false;
+ /**
+ * Repository to perform user-specific actions.
+ *
+ * @var UserRepo
+ */
+ protected $userRepo;
+
/**
* Create a new authentication guard.
*
- * @param string $name
- * @param \Illuminate\Contracts\Auth\UserProvider $provider
- * @param \Illuminate\Contracts\Session\Session $session
* @return void
*/
- public function __construct($name,
- UserProvider $provider,
- Session $session)
+ public function __construct(string $name, UserProvider $provider, Session $session, UserRepo $userRepo)
{
$this->name = $name;
$this->session = $session;
$this->provider = $provider;
+ $this->userRepo = $userRepo;
}
/**
{
protected $ldapService;
- protected $userRepo;
/**
* LdapSessionGuard constructor.
)
{
$this->ldapService = $ldapService;
- $this->userRepo = $userRepo;
- parent::__construct($name, $provider, $session);
+ parent::__construct($name, $provider, $session, $userRepo);
}
/**
namespace BookStack\Auth\Access\Guards;
-use BookStack\Auth\Access\LdapService;
-use BookStack\Auth\User;
-use BookStack\Auth\UserRepo;
-use BookStack\Exceptions\LdapException;
-use BookStack\Exceptions\LoginAttemptException;
-use BookStack\Exceptions\LoginAttemptEmailNeededException;
-use Illuminate\Contracts\Auth\UserProvider;
-use Illuminate\Contracts\Session\Session;
-
-class LdapSessionGuard extends ExternalBaseSessionGuard
+/**
+ * Saml2 Session Guard
+ *
+ * The saml2 login process is async in nature meaning it does not fit very well
+ * into the default laravel 'Guard' auth flow. Instead most of the logic is done
+ * via the Saml2 controller & Saml2Service. This class provides a safer, thin
+ * version of SessionGuard.
+ *
+ * @package BookStack\Auth\Access\Guards
+ */
+class Saml2SessionGuard extends ExternalBaseSessionGuard
{
-
- protected $ldapService;
-
- /**
- * LdapSessionGuard constructor.
- */
- public function __construct($name,
- UserProvider $provider,
- Session $session,
- LdapService $ldapService,
- UserRepo $userRepo
- )
- {
- $this->ldapService = $ldapService;
- parent::__construct($name, $provider, $session, $userRepo);
- }
-
/**
* Validate a user's credentials.
*
* @param array $credentials
* @return bool
- * @throws LdapException
*/
public function validate(array $credentials = [])
{
- $userDetails = $this->ldapService->getUserDetails($credentials['username']);
- $this->lastAttempted = $this->provider->retrieveByCredentials([
- 'external_auth_id' => $userDetails['uid']
- ]);
-
- return $this->ldapService->validateUserCredentials($userDetails, $credentials['username'], $credentials['password']);
+ return false;
}
/**
* @param array $credentials
* @param bool $remember
* @return bool
- * @throws LoginAttemptEmailNeededException
- * @throws LoginAttemptException
- * @throws LdapException
*/
public function attempt(array $credentials = [], $remember = false)
{
- $username = $credentials['username'];
- $userDetails = $this->ldapService->getUserDetails($username);
- $this->lastAttempted = $user = $this->provider->retrieveByCredentials([
- 'external_auth_id' => $userDetails['uid']
- ]);
-
- if (!$this->ldapService->validateUserCredentials($userDetails, $username, $credentials['password'])) {
- return false;
- }
-
- if (is_null($user)) {
- $user = $this->freshUserInstanceFromLdapUserDetails($userDetails);
- }
-
- $this->checkForUserEmail($user, $credentials['email'] ?? '');
- $this->saveIfNew($user);
-
- // Sync LDAP groups if required
- if ($this->ldapService->shouldSyncGroups()) {
- $this->ldapService->syncGroups($user, $username);
- }
-
- $this->login($user, $remember);
- return true;
- }
-
- /**
- * Create a fresh user instance from details provided by a LDAP lookup.
- */
- protected function freshUserInstanceFromLdapUserDetails(array $ldapUserDetails): User
- {
- $user = new User();
-
- $user->name = $ldapUserDetails['name'];
- $user->external_auth_id = $ldapUserDetails['uid'];
- $user->email = $ldapUserDetails['email'];
- $user->email_confirmed = false;
-
- return $user;
+ return false;
}
}
$this->emailConfirmationService = $emailConfirmationService;
}
-
/**
* Check whether or not registrations are allowed in the app settings.
* @throws UserRegistrationException
*/
public function checkRegistrationAllowed()
{
- if (!setting('registration-enabled') || config('auth.method') === 'ldap') {
+ $authMethod = config('auth.method');
+ $authMethodsWithRegistration = ['standard'];
+ if (!setting('registration-enabled') || !in_array($authMethod, $authMethodsWithRegistration)) {
throw new UserRegistrationException(trans('auth.registrations_disabled'), '/login');
}
}
protected $config;
protected $userRepo;
protected $user;
- protected $enabled;
/**
* Saml2Service constructor.
$this->config = config('saml2');
$this->userRepo = $userRepo;
$this->user = $user;
- $this->enabled = config('saml2.enabled') === true;
}
/**
*/
protected function shouldSyncGroups(): bool
{
- return $this->enabled && $this->config['user_to_groups'] !== false;
+ return $this->config['user_to_groups'] !== false;
}
/**
/**
* Extract the details of a user from a SAML response.
*/
- public function getUserDetails(string $samlID, $samlAttributes): array
+ protected function getUserDetails(string $samlID, $samlAttributes): array
{
$emailAttr = $this->config['email_attribute'];
$externalId = $this->getExternalId($samlAttributes, $samlID);
throw new SamlException(trans('errors.saml_email_exists', ['email' => $userDetails['email']]));
}
- $user = $this->user->forceCreate($userData);
+ $user = $this->user->newQuery()->forceCreate($userData);
$this->userRepo->attachDefaultRole($user);
$this->userRepo->downloadAndAssignUserAvatar($user);
return $user;
/**
* Get the user from the database for the specified details.
+ * @throws SamlException
*/
protected function getOrRegisterUser(array $userDetails): ?User
{
- $isRegisterEnabled = $this->config['auto_register'] === true;
- $user = $this->user
- ->where('external_auth_id', $userDetails['external_id'])
+ $user = $this->user->newQuery()
+ ->where('external_auth_id', '=', $userDetails['external_id'])
->first();
- if ($user === null && $isRegisterEnabled) {
+ if (is_null($user)) {
$user = $this->registerUser($userDetails);
}
],
'ldap' => [
'driver' => 'ldap-session',
- 'provider' => 'external'
+ 'provider' => 'external',
+ ],
+ 'saml2' => [
+ 'driver' => 'saml2-session',
+ 'provider' => 'external',
],
'api' => [
'driver' => 'api-token',
// Display name, shown to users, for SAML2 option
'name' => env('SAML2_NAME', 'SSO'),
- // Toggle whether the SAML2 option is active
- 'enabled' => env('SAML2_ENABLED', false),
- // Enable registration via SAML2 authentication
- 'auto_register' => env('SAML2_AUTO_REGISTER', true),
// Dump user details after a login request for debugging purposes
'dump_user_details' => env('SAML2_DUMP_USER_DETAILS', false),
{
$socialDrivers = $this->socialAuthService->getActiveDrivers();
$authMethod = config('auth.method');
- $samlEnabled = config('saml2.enabled') === true;
if ($request->has('email')) {
session()->flashInput([
return view('auth.login', [
'socialDrivers' => $socialDrivers,
'authMethod' => $authMethod,
- 'samlEnabled' => $samlEnabled,
]);
}
*/
protected function validateLogin(Request $request)
{
- $rules = [];
+ $rules = ['password' => 'required|string'];
$authMethod = config('auth.method');
if ($authMethod === 'standard') {
- $rules = [
- 'email' => 'required|string|email',
- 'password' => 'required|string'
- ];
+ $rules['email'] = 'required|email';
}
if ($authMethod === 'ldap') {
- $rules = [
- 'username' => 'required|string',
- 'password' => 'required|string',
- 'email' => 'email',
- ];
- }
-
- if ($authMethod === 'saml2') {
- $rules = [
- 'email' => 'email',
- ];
+ $rules['username'] = 'required|string';
+ $rules['email'] = 'email';
}
$request->validate($rules);
*/
public function logout(Request $request)
{
- if (config('saml2.enabled') && session()->get('last_login_type') === 'saml2') {
- return redirect('/saml2/logout');
- }
-
$this->guard()->logout();
$request->session()->invalidate();
{
$this->registrationService->checkRegistrationAllowed();
$socialDrivers = $this->socialAuthService->getActiveDrivers();
- $samlEnabled = (config('saml2.enabled') === true) && (config('saml2.auto_register') === true);
return view('auth.register', [
'socialDrivers' => $socialDrivers,
- 'samlEnabled' => $samlEnabled,
]);
}
// SAML2 access middleware
$this->middleware(function ($request, $next) {
- if (!config('saml2.enabled')) {
+
+ if (config('auth.method') !== 'saml2') {
$this->showPermissionError();
}
use BookStack\Api\ApiTokenGuard;
use BookStack\Auth\Access\ExternalBaseUserProvider;
use BookStack\Auth\Access\Guards\LdapSessionGuard;
+use BookStack\Auth\Access\Guards\Saml2SessionGuard;
use BookStack\Auth\Access\LdapService;
use BookStack\Auth\UserRepo;
use Illuminate\Support\ServiceProvider;
$app[UserRepo::class]
);
});
+
+ Auth::extend('saml2-session', function ($app, $name, array $config) {
+ $provider = Auth::createUserProvider($config['provider']);
+ return new Saml2SessionGuard(
+ $name,
+ $provider,
+ $this->app['session.store'],
+ $app[UserRepo::class]
+ );
+ });
}
/**
-<div class="form-group">
- <label for="username">{{ trans('auth.username') }}</label>
- @include('form.text', ['name' => 'username', 'autofocus' => true])
-</div>
+<form action="{{ url('/login') }}" method="POST" id="login-form" class="mt-l">
+ {!! csrf_field() !!}
-@if(session('request-email', false) === true)
- <div class="form-group">
- <label for="email">{{ trans('auth.email') }}</label>
- @include('form.text', ['name' => 'email'])
- <span class="text-neg">
- {{ trans('auth.ldap_email_hint') }}
- </span>
+ <div class="stretch-inputs">
+ <div class="form-group">
+ <label for="username">{{ trans('auth.username') }}</label>
+ @include('form.text', ['name' => 'username', 'autofocus' => true])
+ </div>
+
+ @if(session('request-email', false) === true)
+ <div class="form-group">
+ <label for="email">{{ trans('auth.email') }}</label>
+ @include('form.text', ['name' => 'email'])
+ <span class="text-neg">{{ trans('auth.ldap_email_hint') }}</span>
+ </div>
+ @endif
+
+ <div class="form-group">
+ <label for="password">{{ trans('auth.password') }}</label>
+ @include('form.password', ['name' => 'password'])
+ </div>
+ </div>
+
+ <div class="grid half collapse-xs gap-xl v-center">
+ <div class="text-right">
+ <button class="button">{{ Str::title(trans('auth.log_in')) }}</button>
+ </div>
</div>
-@endif
-<div class="form-group">
- <label for="password">{{ trans('auth.password') }}</label>
- @include('form.password', ['name' => 'password'])
-</div>
\ No newline at end of file
+</form>
\ No newline at end of file
-<form action="{{ url('/login') }}" method="POST" id="login-form" class="mt-l">
+<form action="{{ url('/saml2/login') }}" method="POST" id="login-form" class="mt-l">
{!! csrf_field() !!}
- <div class="stretch-inputs">
- <div class="form-group">
- <label for="username">{{ trans('auth.username') }}</label>
- @include('form.text', ['name' => 'username', 'autofocus' => true])
- </div>
-
- @if(session('request-email', false) === true)
- <div class="form-group">
- <label for="email">{{ trans('auth.email') }}</label>
- @include('form.text', ['name' => 'email'])
- <span class="text-neg">{{ trans('auth.ldap_email_hint') }}</span>
- </div>
- @endif
-
- <div class="form-group">
- <label for="password">{{ trans('auth.password') }}</label>
- @include('form.password', ['name' => 'password'])
- </div>
- </div>
-
- <div class="grid half collapse-xs gap-xl v-center">
- <div class="text-right">
- <button class="button">{{ Str::title(trans('auth.log_in')) }}</button>
- </div>
+ <div>
+ <button id="saml-login" class="button outline block svg">
+ @icon('saml2')
+ {{ trans('auth.log_in_with', ['socialDriver' => config('saml2.name')]) }}
+ </button>
</div>
</form>
\ No newline at end of file
-<div class="form-group">
- <label for="email">{{ trans('auth.email') }}</label>
- @include('form.text', ['name' => 'email', 'autofocus' => true])
-</div>
-
-<div class="form-group">
- <label for="password">{{ trans('auth.password') }}</label>
- @include('form.password', ['name' => 'password'])
- <span class="block small mt-s">
- <a href="{{ url('/password/email') }}">{{ trans('auth.forgot_password') }}</a>
- </span>
-</div>
+<form action="{{ url('/login') }}" method="POST" id="login-form" class="mt-l">
+ {!! csrf_field() !!}
+
+ <div class="stretch-inputs">
+ <div class="form-group">
+ <label for="email">{{ trans('auth.email') }}</label>
+ @include('form.text', ['name' => 'email', 'autofocus' => true])
+ </div>
+
+ <div class="form-group">
+ <label for="password">{{ trans('auth.password') }}</label>
+ @include('form.password', ['name' => 'password'])
+ <div class="small mt-s">
+ <a href="{{ url('/password/email') }}">{{ trans('auth.forgot_password') }}</a>
+ </div>
+ </div>
+ </div>
+
+ <div class="grid half collapse-xs gap-xl v-center">
+ <div class="text-left ml-xxs">
+ @include('components.custom-checkbox', [
+ 'name' => 'remember',
+ 'checked' => false,
+ 'value' => 'on',
+ 'label' => trans('auth.remember_me'),
+ ])
+ </div>
+
+ <div class="text-right">
+ <button class="button">{{ Str::title(trans('auth.log_in')) }}</button>
+ </div>
+ </div>
+
+</form>
+
+
<div class="card content-wrap auto-height">
<h1 class="list-heading">{{ Str::title(trans('auth.log_in')) }}</h1>
- <form action="{{ url('/login') }}" method="POST" id="login-form" class="mt-l">
- {!! csrf_field() !!}
-
- <div class="stretch-inputs">
- @include('auth.forms.login.' . $authMethod)
- </div>
-
- <div class="grid half collapse-xs gap-xl v-center">
- <div class="text-left ml-xxs">
- @include('components.custom-checkbox', [
- 'name' => 'remember',
- 'checked' => false,
- 'value' => 'on',
- 'label' => trans('auth.remember_me'),
- ])
- </div>
-
- <div class="text-right">
- <button class="button">{{ Str::title(trans('auth.log_in')) }}</button>
- </div>
- </div>
-
- </form>
+ @include('auth.forms.login.' . $authMethod)
@if(count($socialDrivers) > 0)
<hr class="my-l">
@endforeach
@endif
- @if($samlEnabled)
- <hr class="my-l">
- <div>
- <a id="saml-login" class="button outline block svg" href="{{ url("/saml2/login") }}">
- @icon('saml2')
- {{ trans('auth.log_in_with', ['socialDriver' => config('saml2.name')]) }}
- </a>
- </div>
- @endif
-
- @if(setting('registration-enabled') && config('auth.method') !== 'ldap')
+ @if(setting('registration-enabled') && config('auth.method') === 'standard')
<div class="text-center pb-s">
<hr class="my-l">
<a href="{{ url('/register') }}">{{ trans('auth.dont_have_account') }}</a>
@endforeach
@endif
- @if($samlEnabled)
- <hr class="my-l">
- <div>
- <a id="saml-login" class="button outline block svg" href="{{ url("/saml2/login") }}">
- @icon('saml2')
- {{ trans('auth.log_in_with', ['socialDriver' => config('saml2.name')]) }}
- </a>
- </div>
- @endif
</div>
</div>
@stop
@endif
@if(!signedInUser())
- @if(setting('registration-enabled') && config('auth.method') !== 'ldap')
+ @if(setting('registration-enabled') && config('auth.method') === 'standard')
<a href="{{ url('/register') }}">@icon('new-user') {{ trans('auth.sign_up') }}</a>
@endif
<a href="{{ url('/login') }}">@icon('login') {{ trans('auth.log_in') }}</a>
<a href="{{ url("/settings/users/{$currentUser->id}") }}">@icon('edit'){{ trans('common.edit_profile') }}</a>
</li>
<li>
- <a href="{{ url('/logout') }}">@icon('logout'){{ trans('auth.logout') }}</a>
+ @if(config('auth.method') === 'saml2')
+ <a href="{{ url('/saml2/logout') }}">@icon('logout'){{ trans('auth.logout') }}</a>
+ @else
+ <a href="{{ url('/logout') }}">@icon('logout'){{ trans('auth.logout') }}</a>
+ @endif
</li>
</ul>
</div>
@include('form.text', ['name' => 'description'])
</div>
- @if(config('auth.method') === 'ldap' || config('saml2.enabled') === true)
+ @if(config('auth.method') === 'ldap' || config('auth.method') === 'saml2')
<div class="form-group">
<label for="name">{{ trans('settings.role_external_auth_id') }}</label>
@include('form.text', ['name' => 'external_auth_id'])
</div>
</div>
-@if(($authMethod === 'ldap' || config('saml2.enabled') === true) && userCan('users-manage'))
+@if(($authMethod === 'ldap' || $authMethod === 'saml2') && userCan('users-manage'))
<div class="grid half gap-xl v-center">
<div>
<label class="setting-list-label">{{ trans('settings.users_external_auth_id') }}</label>
Route::post('/register', 'Auth\RegisterController@postRegister');
// SAML routes
-Route::get('/saml2/login', 'Auth\Saml2Controller@login');
+Route::post('/saml2/login', 'Auth\Saml2Controller@login');
Route::get('/saml2/logout', 'Auth\Saml2Controller@logout');
Route::get('/saml2/metadata', 'Auth\Saml2Controller@metadata');
Route::get('/saml2/sls', 'Auth\Saml2Controller@sls');