mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-08-25 03:35:35 +00:00
Merge pull request #1866 from BookStackApp/auth_alignment
Auth service alignment
This commit is contained in:
commit
e15fcf5b50
@ -121,7 +121,7 @@ STORAGE_S3_ENDPOINT=https://my-custom-s3-compatible.service.com:8001
|
||||
STORAGE_URL=false
|
||||
|
||||
# Authentication method to use
|
||||
# Can be 'standard' or 'ldap'
|
||||
# Can be 'standard', 'ldap' or 'saml2'
|
||||
AUTH_METHOD=standard
|
||||
|
||||
# Social authentication configuration
|
||||
@ -205,8 +205,6 @@ LDAP_REMOVE_FROM_GROUPS=false
|
||||
# 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
|
||||
|
@ -64,10 +64,8 @@ class ExternalAuthService
|
||||
|
||||
/**
|
||||
* Sync the groups to the user roles for the current user
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @param array $userGroups
|
||||
*/
|
||||
public function syncWithGroups(User $user, array $userGroups)
|
||||
public function syncWithGroups(User $user, array $userGroups): void
|
||||
{
|
||||
// Get the ids for the roles from the names
|
||||
$groupsAsRoles = $this->matchGroupsToSystemsRoles($userGroups);
|
||||
@ -75,7 +73,7 @@ class ExternalAuthService
|
||||
// Sync groups
|
||||
if ($this->config['remove_from_groups']) {
|
||||
$user->roles()->sync($groupsAsRoles);
|
||||
$this->userRepo->attachDefaultRole($user);
|
||||
$user->attachDefaultRole();
|
||||
} else {
|
||||
$user->roles()->syncWithoutDetaching($groupsAsRoles);
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Providers;
|
||||
namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Auth\Access\LdapService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Contracts\Auth\UserProvider;
|
||||
|
||||
class LdapUserProvider implements UserProvider
|
||||
class ExternalBaseUserProvider implements UserProvider
|
||||
{
|
||||
|
||||
/**
|
||||
@ -16,21 +15,13 @@ class LdapUserProvider implements UserProvider
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* @var \BookStack\Auth\LdapService
|
||||
*/
|
||||
protected $ldapService;
|
||||
|
||||
|
||||
/**
|
||||
* LdapUserProvider constructor.
|
||||
* @param $model
|
||||
* @param \BookStack\Auth\LdapService $ldapService
|
||||
*/
|
||||
public function __construct($model, LdapService $ldapService)
|
||||
public function __construct(string $model)
|
||||
{
|
||||
$this->model = $model;
|
||||
$this->ldapService = $ldapService;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -44,7 +35,6 @@ class LdapUserProvider implements UserProvider
|
||||
return new $class;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a user by their unique identifier.
|
||||
*
|
||||
@ -65,12 +55,7 @@ class LdapUserProvider implements UserProvider
|
||||
*/
|
||||
public function retrieveByToken($identifier, $token)
|
||||
{
|
||||
$model = $this->createModel();
|
||||
|
||||
return $model->newQuery()
|
||||
->where($model->getAuthIdentifierName(), $identifier)
|
||||
->where($model->getRememberTokenName(), $token)
|
||||
->first();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -83,10 +68,7 @@ class LdapUserProvider implements UserProvider
|
||||
*/
|
||||
public function updateRememberToken(Authenticatable $user, $token)
|
||||
{
|
||||
if ($user->exists) {
|
||||
$user->setRememberToken($token);
|
||||
$user->save();
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,27 +79,11 @@ class LdapUserProvider implements UserProvider
|
||||
*/
|
||||
public function retrieveByCredentials(array $credentials)
|
||||
{
|
||||
// Get user via LDAP
|
||||
$userDetails = $this->ldapService->getUserDetails($credentials['username']);
|
||||
if ($userDetails === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Search current user base by looking up a uid
|
||||
$model = $this->createModel();
|
||||
$currentUser = $model->newQuery()
|
||||
->where('external_auth_id', $userDetails['uid'])
|
||||
return $model->newQuery()
|
||||
->where('external_auth_id', $credentials['external_auth_id'])
|
||||
->first();
|
||||
|
||||
if ($currentUser !== null) {
|
||||
return $currentUser;
|
||||
}
|
||||
|
||||
$model->name = $userDetails['name'];
|
||||
$model->external_auth_id = $userDetails['uid'];
|
||||
$model->email = $userDetails['email'];
|
||||
$model->email_confirmed = false;
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -129,6 +95,7 @@ class LdapUserProvider implements UserProvider
|
||||
*/
|
||||
public function validateCredentials(Authenticatable $user, array $credentials)
|
||||
{
|
||||
return $this->ldapService->validateUserCredentials($user, $credentials['username'], $credentials['password']);
|
||||
// Should be done in the guard.
|
||||
return false;
|
||||
}
|
||||
}
|
305
app/Auth/Access/Guards/ExternalBaseSessionGuard.php
Normal file
305
app/Auth/Access/Guards/ExternalBaseSessionGuard.php
Normal file
@ -0,0 +1,305 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access\Guards;
|
||||
|
||||
use BookStack\Auth\Access\RegistrationService;
|
||||
use Illuminate\Auth\GuardHelpers;
|
||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||
use Illuminate\Contracts\Auth\StatefulGuard;
|
||||
use Illuminate\Contracts\Auth\UserProvider;
|
||||
use Illuminate\Contracts\Session\Session;
|
||||
|
||||
/**
|
||||
* Class BaseSessionGuard
|
||||
* A base implementation of a session guard. Is a copy of the default Laravel
|
||||
* guard with 'remember' functionality removed. Basic auth and event emission
|
||||
* has also been removed to keep this simple. Designed to be extended by external
|
||||
* Auth Guards.
|
||||
*
|
||||
* @package Illuminate\Auth
|
||||
*/
|
||||
class ExternalBaseSessionGuard implements StatefulGuard
|
||||
{
|
||||
use GuardHelpers;
|
||||
|
||||
/**
|
||||
* The name of the Guard. Typically "session".
|
||||
*
|
||||
* Corresponds to guard name in authentication configuration.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The user we last attempted to retrieve.
|
||||
*
|
||||
* @var \Illuminate\Contracts\Auth\Authenticatable
|
||||
*/
|
||||
protected $lastAttempted;
|
||||
|
||||
/**
|
||||
* The session used by the guard.
|
||||
*
|
||||
* @var \Illuminate\Contracts\Session\Session
|
||||
*/
|
||||
protected $session;
|
||||
|
||||
/**
|
||||
* Indicates if the logout method has been called.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $loggedOut = false;
|
||||
|
||||
/**
|
||||
* Service to handle common registration actions.
|
||||
*
|
||||
* @var RegistrationService
|
||||
*/
|
||||
protected $registrationService;
|
||||
|
||||
/**
|
||||
* Create a new authentication guard.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(string $name, UserProvider $provider, Session $session, RegistrationService $registrationService)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->session = $session;
|
||||
$this->provider = $provider;
|
||||
$this->registrationService = $registrationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently authenticated user.
|
||||
*
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
if ($this->loggedOut) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we've already retrieved the user for the current request we can just
|
||||
// return it back immediately. We do not want to fetch the user data on
|
||||
// every call to this method because that would be tremendously slow.
|
||||
if (! is_null($this->user)) {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
$id = $this->session->get($this->getName());
|
||||
|
||||
// First we will try to load the user using the
|
||||
// identifier in the session if one exists.
|
||||
if (! is_null($id)) {
|
||||
$this->user = $this->provider->retrieveById($id);
|
||||
}
|
||||
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID for the currently authenticated user.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function id()
|
||||
{
|
||||
if ($this->loggedOut) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->user()
|
||||
? $this->user()->getAuthIdentifier()
|
||||
: $this->session->get($this->getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a user into the application without sessions or cookies.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @return bool
|
||||
*/
|
||||
public function once(array $credentials = [])
|
||||
{
|
||||
if ($this->validate($credentials)) {
|
||||
$this->setUser($this->lastAttempted);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the given user ID into the application without sessions or cookies.
|
||||
*
|
||||
* @param mixed $id
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable|false
|
||||
*/
|
||||
public function onceUsingId($id)
|
||||
{
|
||||
if (! is_null($user = $this->provider->retrieveById($id))) {
|
||||
$this->setUser($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a user's credentials.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @return bool
|
||||
*/
|
||||
public function validate(array $credentials = [])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attempt to authenticate a user using the given credentials.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @param bool $remember
|
||||
* @return bool
|
||||
*/
|
||||
public function attempt(array $credentials = [], $remember = false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the given user ID into the application.
|
||||
*
|
||||
* @param mixed $id
|
||||
* @param bool $remember
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable|false
|
||||
*/
|
||||
public function loginUsingId($id, $remember = false)
|
||||
{
|
||||
if (! is_null($user = $this->provider->retrieveById($id))) {
|
||||
$this->login($user, $remember);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a user into the application.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param bool $remember
|
||||
* @return void
|
||||
*/
|
||||
public function login(AuthenticatableContract $user, $remember = false)
|
||||
{
|
||||
$this->updateSession($user->getAuthIdentifier());
|
||||
|
||||
$this->setUser($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the session with the given ID.
|
||||
*
|
||||
* @param string $id
|
||||
* @return void
|
||||
*/
|
||||
protected function updateSession($id)
|
||||
{
|
||||
$this->session->put($this->getName(), $id);
|
||||
|
||||
$this->session->migrate(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the user out of the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
$this->clearUserDataFromStorage();
|
||||
|
||||
// Now we will clear the users out of memory so they are no longer available
|
||||
// as the user is no longer considered as being signed into this
|
||||
// application and should not be available here.
|
||||
$this->user = null;
|
||||
|
||||
$this->loggedOut = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the user data from the session and cookies.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function clearUserDataFromStorage()
|
||||
{
|
||||
$this->session->remove($this->getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last user we attempted to authenticate.
|
||||
*
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable
|
||||
*/
|
||||
public function getLastAttempted()
|
||||
{
|
||||
return $this->lastAttempted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a unique identifier for the auth session value.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'login_'.$this->name.'_'.sha1(static::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user was authenticated via "remember me" cookie.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function viaRemember()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the currently cached user.
|
||||
*
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current user.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @return $this
|
||||
*/
|
||||
public function setUser(AuthenticatableContract $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
$this->loggedOut = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
114
app/Auth/Access/Guards/LdapSessionGuard.php
Normal file
114
app/Auth/Access/Guards/LdapSessionGuard.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access\Guards;
|
||||
|
||||
use BookStack\Auth\Access\LdapService;
|
||||
use BookStack\Auth\Access\RegistrationService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\LdapException;
|
||||
use BookStack\Exceptions\LoginAttemptException;
|
||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use Illuminate\Contracts\Auth\UserProvider;
|
||||
use Illuminate\Contracts\Session\Session;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
{
|
||||
|
||||
protected $ldapService;
|
||||
|
||||
/**
|
||||
* LdapSessionGuard constructor.
|
||||
*/
|
||||
public function __construct($name,
|
||||
UserProvider $provider,
|
||||
Session $session,
|
||||
LdapService $ldapService,
|
||||
RegistrationService $registrationService
|
||||
)
|
||||
{
|
||||
$this->ldapService = $ldapService;
|
||||
parent::__construct($name, $provider, $session, $registrationService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate a user using the given credentials.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @param bool $remember
|
||||
* @return bool
|
||||
* @throws LoginAttemptEmailNeededException
|
||||
* @throws LoginAttemptException
|
||||
* @throws LdapException
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
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->createNewFromLdapAndCreds($userDetails, $credentials);
|
||||
}
|
||||
|
||||
// Sync LDAP groups if required
|
||||
if ($this->ldapService->shouldSyncGroups()) {
|
||||
$this->ldapService->syncGroups($user, $username);
|
||||
}
|
||||
|
||||
$this->login($user, $remember);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user from the given ldap credentials and login credentials
|
||||
* @throws LoginAttemptEmailNeededException
|
||||
* @throws LoginAttemptException
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
protected function createNewFromLdapAndCreds(array $ldapUserDetails, array $credentials): User
|
||||
{
|
||||
$email = trim($ldapUserDetails['email'] ?: ($credentials['email'] ?? ''));
|
||||
|
||||
if (empty($email)) {
|
||||
throw new LoginAttemptEmailNeededException();
|
||||
}
|
||||
|
||||
$details = [
|
||||
'name' => $ldapUserDetails['name'],
|
||||
'email' => $ldapUserDetails['email'] ?: $credentials['email'],
|
||||
'external_auth_id' => $ldapUserDetails['uid'],
|
||||
'password' => Str::random(32),
|
||||
];
|
||||
|
||||
return $this->registrationService->registerUser($details, null, false);
|
||||
}
|
||||
|
||||
}
|
40
app/Auth/Access/Guards/Saml2SessionGuard.php
Normal file
40
app/Auth/Access/Guards/Saml2SessionGuard.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access\Guards;
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
/**
|
||||
* Validate a user's credentials.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @return bool
|
||||
*/
|
||||
public function validate(array $credentials = [])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate a user using the given credentials.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @param bool $remember
|
||||
* @return bool
|
||||
*/
|
||||
public function attempt(array $credentials = [], $remember = false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\LdapException;
|
||||
use ErrorException;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
/**
|
||||
* Class LdapService
|
||||
@ -16,17 +14,15 @@ class LdapService extends ExternalAuthService
|
||||
protected $ldap;
|
||||
protected $ldapConnection;
|
||||
protected $config;
|
||||
protected $userRepo;
|
||||
protected $enabled;
|
||||
|
||||
/**
|
||||
* LdapService constructor.
|
||||
*/
|
||||
public function __construct(Ldap $ldap, UserRepo $userRepo)
|
||||
public function __construct(Ldap $ldap)
|
||||
{
|
||||
$this->ldap = $ldap;
|
||||
$this->config = config('services.ldap');
|
||||
$this->userRepo = $userRepo;
|
||||
$this->enabled = config('auth.method') === 'ldap';
|
||||
}
|
||||
|
||||
@ -106,20 +102,15 @@ class LdapService extends ExternalAuthService
|
||||
* Check if the given credentials are valid for the given user.
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function validateUserCredentials(Authenticatable $user, string $username, string $password): bool
|
||||
public function validateUserCredentials(array $ldapUserDetails, string $username, string $password): bool
|
||||
{
|
||||
$ldapUser = $this->getUserDetails($username);
|
||||
if ($ldapUser === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($ldapUser['uid'] !== $user->external_auth_id) {
|
||||
if ($ldapUserDetails === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ldapConnection = $this->getConnection();
|
||||
try {
|
||||
$ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password);
|
||||
$ldapBind = $this->ldap->bind($ldapConnection, $ldapUserDetails['dn'], $password);
|
||||
} catch (ErrorException $e) {
|
||||
$ldapBind = false;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Auth\SocialAccount;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use Exception;
|
||||
@ -20,41 +21,55 @@ class RegistrationService
|
||||
$this->emailConfirmationService = $emailConfirmationService;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check whether or not registrations are allowed in the app settings.
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function checkRegistrationAllowed()
|
||||
public function ensureRegistrationAllowed()
|
||||
{
|
||||
if (!setting('registration-enabled') || config('auth.method') === 'ldap') {
|
||||
if (!$this->registrationAllowed()) {
|
||||
throw new UserRegistrationException(trans('auth.registrations_disabled'), '/login');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if standard BookStack User registrations are currently allowed.
|
||||
* Does not prevent external-auth based registration.
|
||||
*/
|
||||
protected function registrationAllowed(): bool
|
||||
{
|
||||
$authMethod = config('auth.method');
|
||||
$authMethodsWithRegistration = ['standard'];
|
||||
return in_array($authMethod, $authMethodsWithRegistration) && setting('registration-enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* The registrations flow for all users.
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function registerUser(array $userData, ?SocialAccount $socialAccount = null, bool $emailVerified = false)
|
||||
public function registerUser(array $userData, ?SocialAccount $socialAccount = null, bool $emailConfirmed = false): User
|
||||
{
|
||||
$registrationRestrict = setting('registration-restrict');
|
||||
$userEmail = $userData['email'];
|
||||
|
||||
if ($registrationRestrict) {
|
||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
|
||||
$userEmailDomain = $domain = mb_substr(mb_strrchr($userData['email'], "@"), 1);
|
||||
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
|
||||
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
|
||||
}
|
||||
// Email restriction
|
||||
$this->ensureEmailDomainAllowed($userEmail);
|
||||
|
||||
// Ensure user does not already exist
|
||||
$alreadyUser = !is_null($this->userRepo->getByEmail($userEmail));
|
||||
if ($alreadyUser) {
|
||||
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $userEmail]));
|
||||
}
|
||||
|
||||
$newUser = $this->userRepo->registerNew($userData, $emailVerified);
|
||||
// Create the user
|
||||
$newUser = $this->userRepo->registerNew($userData, $emailConfirmed);
|
||||
|
||||
// Assign social account if given
|
||||
if ($socialAccount) {
|
||||
$newUser->socialAccounts()->save($socialAccount);
|
||||
}
|
||||
|
||||
if ($this->emailConfirmationService->confirmationRequired() && !$emailVerified) {
|
||||
// Start email confirmation flow if required
|
||||
if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {
|
||||
$newUser->save();
|
||||
$message = '';
|
||||
|
||||
@ -67,7 +82,37 @@ class RegistrationService
|
||||
throw new UserRegistrationException($message, '/register/confirm');
|
||||
}
|
||||
|
||||
auth()->login($newUser);
|
||||
return $newUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the given email meets any active email domain registration restrictions.
|
||||
* Throws if restrictions are active and the email does not match an allowed domain.
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
protected function ensureEmailDomainAllowed(string $userEmail): void
|
||||
{
|
||||
$registrationRestrict = setting('registration-restrict');
|
||||
|
||||
if (!$registrationRestrict) {
|
||||
return;
|
||||
}
|
||||
|
||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
|
||||
$userEmailDomain = $domain = mb_substr(mb_strrchr($userEmail, "@"), 1);
|
||||
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
|
||||
$redirect = $this->registrationAllowed() ? '/register' : '/login';
|
||||
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), $redirect);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias to the UserRepo method of the same name.
|
||||
* Attaches the default system role, if configured, to the given user.
|
||||
*/
|
||||
public function attachDefaultRole(User $user): void
|
||||
{
|
||||
$this->userRepo->attachDefaultRole($user);
|
||||
}
|
||||
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\JsonDebugException;
|
||||
use BookStack\Exceptions\SamlException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use Exception;
|
||||
use Illuminate\Support\Str;
|
||||
use OneLogin\Saml2\Auth;
|
||||
@ -18,19 +18,17 @@ use OneLogin\Saml2\ValidationError;
|
||||
class Saml2Service extends ExternalAuthService
|
||||
{
|
||||
protected $config;
|
||||
protected $userRepo;
|
||||
protected $registrationService;
|
||||
protected $user;
|
||||
protected $enabled;
|
||||
|
||||
/**
|
||||
* Saml2Service constructor.
|
||||
*/
|
||||
public function __construct(UserRepo $userRepo, User $user)
|
||||
public function __construct(RegistrationService $registrationService, User $user)
|
||||
{
|
||||
$this->config = config('saml2');
|
||||
$this->userRepo = $userRepo;
|
||||
$this->registrationService = $registrationService;
|
||||
$this->user = $user;
|
||||
$this->enabled = config('saml2.enabled') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -80,6 +78,7 @@ class Saml2Service extends ExternalAuthService
|
||||
* @throws SamlException
|
||||
* @throws ValidationError
|
||||
* @throws JsonDebugException
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function processAcsResponse(?string $requestId): ?User
|
||||
{
|
||||
@ -204,7 +203,7 @@ class Saml2Service extends ExternalAuthService
|
||||
*/
|
||||
protected function shouldSyncGroups(): bool
|
||||
{
|
||||
return $this->enabled && $this->config['user_to_groups'] !== false;
|
||||
return $this->config['user_to_groups'] !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -248,7 +247,7 @@ class Saml2Service extends ExternalAuthService
|
||||
/**
|
||||
* 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);
|
||||
@ -310,43 +309,26 @@ class Saml2Service extends ExternalAuthService
|
||||
return $defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a user that is authenticated but not already registered.
|
||||
*/
|
||||
protected function registerUser(array $userDetails): User
|
||||
{
|
||||
// Create an array of the user data to create a new user instance
|
||||
$userData = [
|
||||
'name' => $userDetails['name'],
|
||||
'email' => $userDetails['email'],
|
||||
'password' => Str::random(32),
|
||||
'external_auth_id' => $userDetails['external_id'],
|
||||
'email_confirmed' => true,
|
||||
];
|
||||
|
||||
$existingUser = $this->user->newQuery()->where('email', '=', $userDetails['email'])->first();
|
||||
if ($existingUser) {
|
||||
throw new SamlException(trans('errors.saml_email_exists', ['email' => $userDetails['email']]));
|
||||
}
|
||||
|
||||
$user = $this->user->forceCreate($userData);
|
||||
$this->userRepo->attachDefaultRole($user);
|
||||
$this->userRepo->downloadAndAssignUserAvatar($user);
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user from the database for the specified details.
|
||||
* @throws SamlException
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
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) {
|
||||
$user = $this->registerUser($userDetails);
|
||||
if (is_null($user)) {
|
||||
$userData = [
|
||||
'name' => $userDetails['name'],
|
||||
'email' => $userDetails['email'],
|
||||
'password' => Str::random(32),
|
||||
'external_auth_id' => $userDetails['external_id'],
|
||||
];
|
||||
|
||||
$user = $this->registrationService->registerUser($userData, null, false);
|
||||
}
|
||||
|
||||
return $user;
|
||||
@ -357,6 +339,7 @@ class Saml2Service extends ExternalAuthService
|
||||
* they exist, optionally registering them automatically.
|
||||
* @throws SamlException
|
||||
* @throws JsonDebugException
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function processLoginCallback(string $samlID, array $samlAttributes): User
|
||||
{
|
||||
|
@ -64,7 +64,7 @@ class SocialAuthService
|
||||
|
||||
if ($this->userRepo->getByEmail($socialUser->getEmail())) {
|
||||
$email = $socialUser->getEmail();
|
||||
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver, 'email' => $email]), '/login');
|
||||
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $email]), '/login');
|
||||
}
|
||||
|
||||
return $socialUser;
|
||||
@ -124,7 +124,7 @@ class SocialAuthService
|
||||
|
||||
// Otherwise let the user know this social account is not used by anyone.
|
||||
$message = trans('errors.social_account_not_used', ['socialAccount' => $titleCaseDriver]);
|
||||
if (setting('registration-enabled') && config('auth.method') !== 'ldap') {
|
||||
if (setting('registration-enabled') && config('auth.method') !== 'ldap' && config('auth.method') !== 'saml2') {
|
||||
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => $titleCaseDriver]);
|
||||
}
|
||||
|
||||
|
@ -116,6 +116,17 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
return $this->roles->pluck('system_name')->contains($role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach the default system role to this user.
|
||||
*/
|
||||
public function attachDefaultRole(): void
|
||||
{
|
||||
$roleId = setting('registration-role');
|
||||
if ($roleId && $this->roles()->where('id', '=', $roleId)->count() === 0) {
|
||||
$this->roles()->attach($roleId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all permissions belonging to a the current user.
|
||||
* @param bool $cache
|
||||
@ -153,16 +164,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function attachRole(Role $role)
|
||||
{
|
||||
$this->attachRoleId($role->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a role id to this user.
|
||||
* @param $id
|
||||
*/
|
||||
public function attachRoleId($id)
|
||||
{
|
||||
$this->roles()->attach($id);
|
||||
$this->roles()->attach($role->id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,10 +29,9 @@ class UserRepo
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $email
|
||||
* @return User|null
|
||||
* Get a user by their email address.
|
||||
*/
|
||||
public function getByEmail($email)
|
||||
public function getByEmail(string $email): ?User
|
||||
{
|
||||
return $this->user->where('email', '=', $email)->first();
|
||||
}
|
||||
@ -78,31 +77,16 @@ class UserRepo
|
||||
|
||||
/**
|
||||
* Creates a new user and attaches a role to them.
|
||||
* @param array $data
|
||||
* @param boolean $verifyEmail
|
||||
* @return User
|
||||
*/
|
||||
public function registerNew(array $data, $verifyEmail = false)
|
||||
public function registerNew(array $data, bool $emailConfirmed = false): User
|
||||
{
|
||||
$user = $this->create($data, $verifyEmail);
|
||||
$this->attachDefaultRole($user);
|
||||
$user = $this->create($data, $emailConfirmed);
|
||||
$user->attachDefaultRole();
|
||||
$this->downloadAndAssignUserAvatar($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Give a user the default role. Used when creating a new user.
|
||||
* @param User $user
|
||||
*/
|
||||
public function attachDefaultRole(User $user)
|
||||
{
|
||||
$roleId = setting('registration-role');
|
||||
if ($roleId !== false && $user->roles()->where('id', '=', $roleId)->count() === 0) {
|
||||
$user->attachRoleId($roleId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a user to a system-level role.
|
||||
* @param User $user
|
||||
@ -172,17 +156,15 @@ class UserRepo
|
||||
|
||||
/**
|
||||
* Create a new basic instance of user.
|
||||
* @param array $data
|
||||
* @param boolean $verifyEmail
|
||||
* @return User
|
||||
*/
|
||||
public function create(array $data, $verifyEmail = false)
|
||||
public function create(array $data, bool $emailConfirmed = false): User
|
||||
{
|
||||
return $this->user->forceCreate([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => bcrypt($data['password']),
|
||||
'email_confirmed' => $verifyEmail
|
||||
'email_confirmed' => $emailConfirmed,
|
||||
'external_auth_id' => $data['external_auth_id'] ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -11,14 +11,14 @@
|
||||
return [
|
||||
|
||||
// Method of authentication to use
|
||||
// Options: standard, ldap
|
||||
// Options: standard, ldap, saml2
|
||||
'method' => env('AUTH_METHOD', 'standard'),
|
||||
|
||||
// Authentication Defaults
|
||||
// This option controls the default authentication "guard" and password
|
||||
// reset options for your application.
|
||||
'defaults' => [
|
||||
'guard' => 'web',
|
||||
'guard' => env('AUTH_METHOD', 'standard'),
|
||||
'passwords' => 'users',
|
||||
],
|
||||
|
||||
@ -26,13 +26,20 @@ return [
|
||||
// All authentication drivers have a user provider. This defines how the
|
||||
// users are actually retrieved out of your database or other storage
|
||||
// mechanisms used by this application to persist your user's data.
|
||||
// Supported: "session", "token"
|
||||
// Supported drivers: "session", "api-token", "ldap-session"
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'standard' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
|
||||
'ldap' => [
|
||||
'driver' => 'ldap-session',
|
||||
'provider' => 'external',
|
||||
],
|
||||
'saml2' => [
|
||||
'driver' => 'saml2-session',
|
||||
'provider' => 'external',
|
||||
],
|
||||
'api' => [
|
||||
'driver' => 'api-token',
|
||||
],
|
||||
@ -42,17 +49,15 @@ return [
|
||||
// All authentication drivers have a user provider. This defines how the
|
||||
// users are actually retrieved out of your database or other storage
|
||||
// mechanisms used by this application to persist your user's data.
|
||||
// Supported: database, eloquent, ldap
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => env('AUTH_METHOD', 'standard') === 'standard' ? 'eloquent' : env('AUTH_METHOD'),
|
||||
'driver' => 'eloquent',
|
||||
'model' => \BookStack\Auth\User::class,
|
||||
],
|
||||
'external' => [
|
||||
'driver' => 'external-users',
|
||||
'model' => \BookStack\Auth\User::class,
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
// Resetting Passwords
|
||||
|
@ -4,10 +4,6 @@ return [
|
||||
|
||||
// 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),
|
||||
|
@ -1,6 +0,0 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class AuthException extends PrettyException
|
||||
{
|
||||
|
||||
}
|
@ -57,7 +57,10 @@ class Handler extends ExceptionHandler
|
||||
// Handle notify exceptions which will redirect to the
|
||||
// specified location then show a notification message.
|
||||
if ($this->isExceptionType($e, NotifyException::class)) {
|
||||
session()->flash('error', $this->getOriginalMessage($e));
|
||||
$message = $this->getOriginalMessage($e);
|
||||
if (!empty($message)) {
|
||||
session()->flash('error', $message);
|
||||
}
|
||||
return redirect($e->redirectLocation);
|
||||
}
|
||||
|
||||
|
6
app/Exceptions/LoginAttemptEmailNeededException.php
Normal file
6
app/Exceptions/LoginAttemptEmailNeededException.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class LoginAttemptEmailNeededException extends LoginAttemptException
|
||||
{
|
||||
|
||||
}
|
6
app/Exceptions/LoginAttemptException.php
Normal file
6
app/Exceptions/LoginAttemptException.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class LoginAttemptException extends \Exception
|
||||
{
|
||||
|
||||
}
|
@ -30,6 +30,7 @@ class ForgotPasswordController extends Controller
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest');
|
||||
$this->middleware('guard:standard');
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
@ -2,12 +2,11 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Auth\Access\LdapService;
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\AuthException;
|
||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||
use BookStack\Exceptions\LoginAttemptException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@ -27,32 +26,23 @@ class LoginController extends Controller
|
||||
use AuthenticatesUsers;
|
||||
|
||||
/**
|
||||
* Where to redirect users after login.
|
||||
*
|
||||
* @var string
|
||||
* Redirection paths
|
||||
*/
|
||||
protected $redirectTo = '/';
|
||||
|
||||
protected $redirectPath = '/';
|
||||
protected $redirectAfterLogout = '/login';
|
||||
|
||||
protected $socialAuthService;
|
||||
protected $ldapService;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @param \BookStack\Auth\\BookStack\Auth\Access\SocialAuthService $socialAuthService
|
||||
* @param LdapService $ldapService
|
||||
* @param \BookStack\Auth\UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(SocialAuthService $socialAuthService, LdapService $ldapService, UserRepo $userRepo)
|
||||
public function __construct(SocialAuthService $socialAuthService)
|
||||
{
|
||||
$this->middleware('guest', ['only' => ['getLogin', 'postLogin']]);
|
||||
$this->middleware('guest', ['only' => ['getLogin', 'login']]);
|
||||
$this->middleware('guard:standard,ldap', ['only' => ['login', 'logout']]);
|
||||
|
||||
$this->socialAuthService = $socialAuthService;
|
||||
$this->ldapService = $ldapService;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->redirectPath = url('/');
|
||||
$this->redirectAfterLogout = url('/login');
|
||||
parent::__construct();
|
||||
@ -64,47 +54,11 @@ class LoginController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the action when a user is authenticated.
|
||||
* If the user authenticated but does not exist in the user table we create them.
|
||||
* @throws AuthException
|
||||
* @throws \BookStack\Exceptions\LdapException
|
||||
* Get the needed authorization credentials from the request.
|
||||
*/
|
||||
protected function authenticated(Request $request, Authenticatable $user)
|
||||
protected function credentials(Request $request)
|
||||
{
|
||||
// Explicitly log them out for now if they do no exist.
|
||||
if (!$user->exists) {
|
||||
auth()->logout($user);
|
||||
}
|
||||
|
||||
if (!$user->exists && $user->email === null && !$request->filled('email')) {
|
||||
$request->flash();
|
||||
session()->flash('request-email', true);
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
if (!$user->exists && $user->email === null && $request->filled('email')) {
|
||||
$user->email = $request->get('email');
|
||||
}
|
||||
|
||||
if (!$user->exists) {
|
||||
// Check for users with same email already
|
||||
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
|
||||
if ($alreadyUser) {
|
||||
throw new AuthException(trans('errors.error_user_exists_different_creds', ['email' => $user->email]));
|
||||
}
|
||||
|
||||
$user->save();
|
||||
$this->userRepo->attachDefaultRole($user);
|
||||
$this->userRepo->downloadAndAssignUserAvatar($user);
|
||||
auth()->login($user);
|
||||
}
|
||||
|
||||
// Sync LDAP groups if required
|
||||
if ($this->ldapService->shouldSyncGroups()) {
|
||||
$this->ldapService->syncGroups($user, $request->get($this->username()));
|
||||
}
|
||||
|
||||
return redirect()->intended('/');
|
||||
return $request->only('username', 'email', 'password');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -114,7 +68,6 @@ class LoginController extends Controller
|
||||
{
|
||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
||||
$authMethod = config('auth.method');
|
||||
$samlEnabled = config('saml2.enabled') === true;
|
||||
|
||||
if ($request->has('email')) {
|
||||
session()->flashInput([
|
||||
@ -126,22 +79,87 @@ class LoginController extends Controller
|
||||
return view('auth.login', [
|
||||
'socialDrivers' => $socialDrivers,
|
||||
'authMethod' => $authMethod,
|
||||
'samlEnabled' => $samlEnabled,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the user out of the application.
|
||||
* Handle a login request to the application.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function logout(Request $request)
|
||||
public function login(Request $request)
|
||||
{
|
||||
if (config('saml2.enabled') && session()->get('last_login_type') === 'saml2') {
|
||||
return redirect('/saml2/logout');
|
||||
$this->validateLogin($request);
|
||||
|
||||
// If the class is using the ThrottlesLogins trait, we can automatically throttle
|
||||
// the login attempts for this application. We'll key this by the username and
|
||||
// the IP address of the client making these requests into this application.
|
||||
if (method_exists($this, 'hasTooManyLoginAttempts') &&
|
||||
$this->hasTooManyLoginAttempts($request)) {
|
||||
$this->fireLockoutEvent($request);
|
||||
|
||||
return $this->sendLockoutResponse($request);
|
||||
}
|
||||
|
||||
$this->guard()->logout();
|
||||
$request->session()->invalidate();
|
||||
try {
|
||||
if ($this->attemptLogin($request)) {
|
||||
return $this->sendLoginResponse($request);
|
||||
}
|
||||
} catch (LoginAttemptException $exception) {
|
||||
return $this->sendLoginAttemptExceptionResponse($exception, $request);
|
||||
}
|
||||
|
||||
return $this->loggedOut($request) ?: redirect('/');
|
||||
// If the login attempt was unsuccessful we will increment the number of attempts
|
||||
// to login and redirect the user back to the login form. Of course, when this
|
||||
// user surpasses their maximum number of attempts they will get locked out.
|
||||
$this->incrementLoginAttempts($request);
|
||||
|
||||
return $this->sendFailedLoginResponse($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the user login request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return void
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
protected function validateLogin(Request $request)
|
||||
{
|
||||
$rules = ['password' => 'required|string'];
|
||||
$authMethod = config('auth.method');
|
||||
|
||||
if ($authMethod === 'standard') {
|
||||
$rules['email'] = 'required|email';
|
||||
}
|
||||
|
||||
if ($authMethod === 'ldap') {
|
||||
$rules['username'] = 'required|string';
|
||||
$rules['email'] = 'email';
|
||||
}
|
||||
|
||||
$request->validate($rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a response when a login attempt exception occurs.
|
||||
*/
|
||||
protected function sendLoginAttemptExceptionResponse(LoginAttemptException $exception, Request $request)
|
||||
{
|
||||
if ($exception instanceof LoginAttemptEmailNeededException) {
|
||||
$request->flash();
|
||||
session()->flash('request-email', true);
|
||||
}
|
||||
|
||||
if ($message = $exception->getMessage()) {
|
||||
$this->showWarningNotification($message);
|
||||
}
|
||||
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -43,7 +43,8 @@ class RegisterController extends Controller
|
||||
*/
|
||||
public function __construct(SocialAuthService $socialAuthService, RegistrationService $registrationService)
|
||||
{
|
||||
$this->middleware('guest')->only(['getRegister', 'postRegister']);
|
||||
$this->middleware('guest');
|
||||
$this->middleware('guard:standard');
|
||||
|
||||
$this->socialAuthService = $socialAuthService;
|
||||
$this->registrationService = $registrationService;
|
||||
@ -73,12 +74,10 @@ class RegisterController extends Controller
|
||||
*/
|
||||
public function getRegister()
|
||||
{
|
||||
$this->registrationService->checkRegistrationAllowed();
|
||||
$this->registrationService->ensureRegistrationAllowed();
|
||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
||||
$samlEnabled = (config('saml2.enabled') === true) && (config('saml2.auto_register') === true);
|
||||
return view('auth.register', [
|
||||
'socialDrivers' => $socialDrivers,
|
||||
'samlEnabled' => $samlEnabled,
|
||||
]);
|
||||
}
|
||||
|
||||
@ -88,12 +87,13 @@ class RegisterController extends Controller
|
||||
*/
|
||||
public function postRegister(Request $request)
|
||||
{
|
||||
$this->registrationService->checkRegistrationAllowed();
|
||||
$this->registrationService->ensureRegistrationAllowed();
|
||||
$this->validator($request->all())->validate();
|
||||
$userData = $request->all();
|
||||
|
||||
try {
|
||||
$this->registrationService->registerUser($userData);
|
||||
$user = $this->registrationService->registerUser($userData);
|
||||
auth()->login($user);
|
||||
} catch (UserRegistrationException $exception) {
|
||||
if ($exception->getMessage()) {
|
||||
$this->showErrorNotification($exception->getMessage());
|
||||
|
@ -31,6 +31,7 @@ class ResetPasswordController extends Controller
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest');
|
||||
$this->middleware('guard:standard');
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
@ -17,15 +17,7 @@ class Saml2Controller extends Controller
|
||||
{
|
||||
parent::__construct();
|
||||
$this->samlService = $samlService;
|
||||
|
||||
// SAML2 access middleware
|
||||
$this->middleware(function ($request, $next) {
|
||||
if (!config('saml2.enabled')) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
});
|
||||
$this->middleware('guard:saml2');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -89,7 +81,6 @@ class Saml2Controller extends Controller
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
session()->put('last_login_type', 'saml2');
|
||||
return redirect()->intended();
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,7 @@ class SocialController extends Controller
|
||||
*/
|
||||
public function socialRegister(string $socialDriver)
|
||||
{
|
||||
$this->registrationService->checkRegistrationAllowed();
|
||||
$this->registrationService->ensureRegistrationAllowed();
|
||||
session()->put('social-callback', 'register');
|
||||
return $this->socialAuthService->startRegister($socialDriver);
|
||||
}
|
||||
@ -78,7 +78,7 @@ class SocialController extends Controller
|
||||
|
||||
// Attempt login or fall-back to register if allowed.
|
||||
$socialUser = $this->socialAuthService->getSocialUser($socialDriver);
|
||||
if ($action == 'login') {
|
||||
if ($action === 'login') {
|
||||
try {
|
||||
return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser);
|
||||
} catch (SocialSignInAccountNotUsed $exception) {
|
||||
@ -89,7 +89,7 @@ class SocialController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if ($action == 'register') {
|
||||
if ($action === 'register') {
|
||||
return $this->socialRegisterCallback($socialDriver, $socialUser);
|
||||
}
|
||||
|
||||
@ -108,7 +108,6 @@ class SocialController extends Controller
|
||||
|
||||
/**
|
||||
* Register a new user after a registration callback.
|
||||
* @return RedirectResponse|Redirector
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
|
||||
@ -121,17 +120,11 @@ class SocialController extends Controller
|
||||
$userData = [
|
||||
'name' => $socialUser->getName(),
|
||||
'email' => $socialUser->getEmail(),
|
||||
'password' => Str::random(30)
|
||||
'password' => Str::random(32)
|
||||
];
|
||||
|
||||
try {
|
||||
$this->registrationService->registerUser($userData, $socialAccount, $emailVerified);
|
||||
} catch (UserRegistrationException $exception) {
|
||||
if ($exception->getMessage()) {
|
||||
$this->showErrorNotification($exception->getMessage());
|
||||
}
|
||||
return redirect($exception->redirectLocation);
|
||||
}
|
||||
$user = $this->registrationService->registerUser($userData, $socialAccount, $emailVerified);
|
||||
auth()->login($user);
|
||||
|
||||
$this->showSuccessNotification(trans('auth.register_success'));
|
||||
return redirect('/');
|
||||
|
@ -8,11 +8,9 @@ use BookStack\Exceptions\UserTokenExpiredException;
|
||||
use BookStack\Exceptions\UserTokenNotFoundException;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class UserInviteController extends Controller
|
||||
{
|
||||
@ -21,22 +19,20 @@ class UserInviteController extends Controller
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @param UserInviteService $inviteService
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(UserInviteService $inviteService, UserRepo $userRepo)
|
||||
{
|
||||
$this->middleware('guest');
|
||||
$this->middleware('guard:standard');
|
||||
|
||||
$this->inviteService = $inviteService;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->middleware('guest');
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the page for the user to set the password for their account.
|
||||
* @param string $token
|
||||
* @return Factory|View|RedirectResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
public function showSetPassword(string $token)
|
||||
@ -54,9 +50,6 @@ class UserInviteController extends Controller
|
||||
|
||||
/**
|
||||
* Sets the password for an invited user and then grants them access.
|
||||
* @param Request $request
|
||||
* @param string $token
|
||||
* @return RedirectResponse|Redirector
|
||||
* @throws Exception
|
||||
*/
|
||||
public function setPassword(Request $request, string $token)
|
||||
@ -85,7 +78,6 @@ class UserInviteController extends Controller
|
||||
|
||||
/**
|
||||
* Check and validate the exception thrown when checking an invite token.
|
||||
* @param Exception $exception
|
||||
* @return RedirectResponse|Redirector
|
||||
* @throws Exception
|
||||
*/
|
||||
|
@ -5,8 +5,6 @@ use BookStack\Notifications\TestEmail;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use BookStack\Uploads\ImageService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Setting;
|
||||
|
||||
class SettingController extends Controller
|
||||
{
|
||||
@ -14,7 +12,6 @@ class SettingController extends Controller
|
||||
|
||||
/**
|
||||
* SettingController constructor.
|
||||
* @param $imageRepo
|
||||
*/
|
||||
public function __construct(ImageRepo $imageRepo)
|
||||
{
|
||||
@ -22,10 +19,8 @@ class SettingController extends Controller
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display a listing of the settings.
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
@ -43,8 +38,6 @@ class SettingController extends Controller
|
||||
|
||||
/**
|
||||
* Update the specified settings in storage.
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
@ -78,12 +71,12 @@ class SettingController extends Controller
|
||||
}
|
||||
|
||||
$this->showSuccessNotification(trans('settings.settings_save_success'));
|
||||
return redirect('/settings');
|
||||
$redirectLocation = '/settings#' . $request->get('section', '');
|
||||
return redirect(rtrim($redirectLocation, '#'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the page for application maintenance.
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showMaintenance()
|
||||
{
|
||||
@ -98,9 +91,6 @@ class SettingController extends Controller
|
||||
|
||||
/**
|
||||
* Action to clean-up images in the system.
|
||||
* @param Request $request
|
||||
* @param ImageService $imageService
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function cleanupImages(Request $request, ImageService $imageService)
|
||||
{
|
||||
@ -127,11 +117,8 @@ class SettingController extends Controller
|
||||
|
||||
/**
|
||||
* Action to send a test e-mail to the current user.
|
||||
* @param Request $request
|
||||
* @param User $user
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function sendTestEmail(Request $request)
|
||||
public function sendTestEmail()
|
||||
{
|
||||
$this->checkPermission('settings-manage');
|
||||
|
||||
|
@ -48,7 +48,8 @@ class Kernel extends HttpKernel
|
||||
'auth' => \BookStack\Http\Middleware\Authenticate::class,
|
||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||
'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'perm' => \BookStack\Http\Middleware\PermissionMiddleware::class
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'perm' => \BookStack\Http\Middleware\PermissionMiddleware::class,
|
||||
'guard' => \BookStack\Http\Middleware\CheckGuard::class,
|
||||
];
|
||||
}
|
||||
|
27
app/Http/Middleware/CheckGuard.php
Normal file
27
app/Http/Middleware/CheckGuard.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
|
||||
class CheckGuard
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @param string $allowedGuards
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next, ...$allowedGuards)
|
||||
{
|
||||
$activeGuard = config('auth.method');
|
||||
if (!in_array($activeGuard, $allowedGuards)) {
|
||||
session()->flash('error', trans('errors.permission'));
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@ class PermissionMiddleware
|
||||
{
|
||||
|
||||
if (!$request->user() || !$request->user()->can($permission)) {
|
||||
Session::flash('error', trans('errors.permission'));
|
||||
session()->flash('error', trans('errors.permission'));
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,12 @@ namespace BookStack\Providers;
|
||||
|
||||
use Auth;
|
||||
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\Access\RegistrationService;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
@ -19,6 +24,27 @@ class AuthServiceProvider extends ServiceProvider
|
||||
Auth::extend('api-token', function ($app, $name, array $config) {
|
||||
return new ApiTokenGuard($app['request']);
|
||||
});
|
||||
|
||||
Auth::extend('ldap-session', function ($app, $name, array $config) {
|
||||
$provider = Auth::createUserProvider($config['provider']);
|
||||
return new LdapSessionGuard(
|
||||
$name,
|
||||
$provider,
|
||||
$this->app['session.store'],
|
||||
$app[LdapService::class],
|
||||
$app[RegistrationService::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[RegistrationService::class]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -28,8 +54,8 @@ class AuthServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
Auth::provider('ldap', function ($app, array $config) {
|
||||
return new LdapUserProvider($config['model'], $app[LdapService::class]);
|
||||
Auth::provider('external-users', function ($app, array $config) {
|
||||
return new ExternalBaseUserProvider($config['model']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ return [
|
||||
'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
|
||||
'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.',
|
||||
'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
|
||||
'saml_email_exists' => 'Registration unsuccessful since a user already exists with email address ":email"',
|
||||
'social_no_action_defined' => 'No action defined',
|
||||
'social_login_bad_response' => "Error received during :socialAccount login: \n:error",
|
||||
'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',
|
||||
|
@ -56,7 +56,7 @@ return [
|
||||
'reg_enable_toggle' => 'Enable registration',
|
||||
'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',
|
||||
'reg_default_role' => 'Default user role after registration',
|
||||
'reg_enable_ldap_warning' => 'The option above is not used while LDAP authentication is active. User accounts for non-existing members will be auto-created if authentication, against the LDAP system in use, is successful.',
|
||||
'reg_enable_external_warning' => 'The option above is ignore while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.',
|
||||
'reg_email_confirmation' => 'Email Confirmation',
|
||||
'reg_email_confirmation_toggle' => 'Require email confirmation',
|
||||
'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and this option will be ignored.',
|
||||
@ -131,7 +131,7 @@ return [
|
||||
'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',
|
||||
'users_send_invite_option' => 'Send user invite email',
|
||||
'users_external_auth_id' => 'External Authentication ID',
|
||||
'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your LDAP system.',
|
||||
'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your external authentication system.',
|
||||
'users_password_warning' => 'Only fill the below if you would like to change your password.',
|
||||
'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',
|
||||
'users_delete' => 'Delete User',
|
||||
|
@ -1,19 +1,28 @@
|
||||
<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 class="form-group text-right pt-s">
|
||||
<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>
|
||||
</form>
|
11
resources/views/auth/forms/login/saml2.blade.php
Normal file
11
resources/views/auth/forms/login/saml2.blade.php
Normal file
@ -0,0 +1,11 @@
|
||||
<form action="{{ url('/saml2/login') }}" method="POST" id="login-form" class="mt-l">
|
||||
{!! csrf_field() !!}
|
||||
|
||||
<div>
|
||||
<button id="saml-login" class="button outline block svg">
|
||||
@icon('saml2')
|
||||
{{ trans('auth.log_in_with', ['socialDriver' => config('saml2.name')]) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
@ -1,12 +1,36 @@
|
||||
<div class="form-group">
|
||||
<label for="email">{{ trans('auth.email') }}</label>
|
||||
@include('form.text', ['name' => 'email', 'autofocus' => true])
|
||||
</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="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>
|
||||
|
@ -9,29 +9,7 @@
|
||||
<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">
|
||||
@ -45,17 +23,7 @@
|
||||
@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>
|
||||
|
@ -50,15 +50,6 @@
|
||||
@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
|
||||
|
@ -42,7 +42,7 @@
|
||||
@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>
|
||||
@ -64,7 +64,11 @@
|
||||
<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>
|
||||
|
@ -15,9 +15,10 @@
|
||||
</div>
|
||||
|
||||
<div class="card content-wrap auto-height">
|
||||
<h2 class="list-heading">{{ trans('settings.app_features_security') }}</h2>
|
||||
<h2 id="features" class="list-heading">{{ trans('settings.app_features_security') }}</h2>
|
||||
<form action="{{ url("/settings") }}" method="POST">
|
||||
{!! csrf_field() !!}
|
||||
<input type="hidden" name="section" value="features">
|
||||
|
||||
<div class="setting-list">
|
||||
|
||||
@ -79,9 +80,10 @@
|
||||
</div>
|
||||
|
||||
<div class="card content-wrap auto-height">
|
||||
<h2 class="list-heading">{{ trans('settings.app_customization') }}</h2>
|
||||
<h2 id="customization" class="list-heading">{{ trans('settings.app_customization') }}</h2>
|
||||
<form action="{{ url("/settings") }}" method="POST" enctype="multipart/form-data">
|
||||
{!! csrf_field() !!}
|
||||
<input type="hidden" name="section" value="customization">
|
||||
|
||||
<div class="setting-list">
|
||||
|
||||
@ -202,9 +204,10 @@
|
||||
</div>
|
||||
|
||||
<div class="card content-wrap auto-height">
|
||||
<h2 class="list-heading">{{ trans('settings.reg_settings') }}</h2>
|
||||
<h2 id="registration" class="list-heading">{{ trans('settings.reg_settings') }}</h2>
|
||||
<form action="{{ url("/settings") }}" method="POST">
|
||||
{!! csrf_field() !!}
|
||||
<input type="hidden" name="section" value="registration">
|
||||
|
||||
<div class="setting-list">
|
||||
<div class="grid half gap-xl">
|
||||
@ -219,8 +222,8 @@
|
||||
'label' => trans('settings.reg_enable_toggle')
|
||||
])
|
||||
|
||||
@if(config('auth.method') === 'ldap')
|
||||
<div class="text-warn text-small mb-l">{{ trans('settings.reg_enable_ldap_warning') }}</div>
|
||||
@if(in_array(config('auth.method'), ['ldap', 'saml2']))
|
||||
<div class="text-warn text-small mb-l">{{ trans('settings.reg_enable_external_warning') }}</div>
|
||||
@endif
|
||||
|
||||
<label for="setting-registration-role">{{ trans('settings.reg_default_role') }}</label>
|
||||
|
@ -19,7 +19,7 @@
|
||||
@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'])
|
||||
|
@ -25,7 +25,7 @@
|
||||
</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>
|
||||
|
@ -210,7 +210,9 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
// Social auth routes
|
||||
Route::get('/login/service/{socialDriver}', 'Auth\SocialController@getSocialLogin');
|
||||
Route::get('/login/service/{socialDriver}/callback', 'Auth\SocialController@socialCallback');
|
||||
Route::get('/login/service/{socialDriver}/detach', 'Auth\SocialController@detachSocialAccount');
|
||||
Route::group(['middleware' => 'auth'], function () {
|
||||
Route::get('/login/service/{socialDriver}/detach', 'Auth\SocialController@detachSocialAccount');
|
||||
});
|
||||
Route::get('/register/service/{socialDriver}', 'Auth\SocialController@socialRegister');
|
||||
|
||||
// Login/Logout routes
|
||||
@ -225,7 +227,7 @@ Route::get('/register/confirm/{token}', 'Auth\ConfirmEmailController@confirm');
|
||||
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');
|
||||
|
@ -20,7 +20,7 @@ class ApiAuthTest extends TestCase
|
||||
$resp = $this->get($this->endpoint);
|
||||
$resp->assertStatus(401);
|
||||
|
||||
$this->actingAs($viewer, 'web');
|
||||
$this->actingAs($viewer, 'standard');
|
||||
|
||||
$resp = $this->get($this->endpoint);
|
||||
$resp->assertStatus(200);
|
||||
@ -72,11 +72,11 @@ class ApiAuthTest extends TestCase
|
||||
public function test_api_access_permission_required_to_access_api_with_session_auth()
|
||||
{
|
||||
$editor = $this->getEditor();
|
||||
$this->actingAs($editor, 'web');
|
||||
$this->actingAs($editor, 'standard');
|
||||
|
||||
$resp = $this->get($this->endpoint);
|
||||
$resp->assertStatus(200);
|
||||
auth('web')->logout();
|
||||
auth('standard')->logout();
|
||||
|
||||
$accessApiPermission = RolePermission::getByName('access-api');
|
||||
$editorRole = $this->getEditor()->roles()->first();
|
||||
@ -84,7 +84,7 @@ class ApiAuthTest extends TestCase
|
||||
|
||||
$editor = User::query()->where('id', '=', $editor->id)->first();
|
||||
|
||||
$this->actingAs($editor, 'web');
|
||||
$this->actingAs($editor, 'standard');
|
||||
$resp = $this->get($this->endpoint);
|
||||
$resp->assertStatus(403);
|
||||
$resp->assertJson($this->errorResponse("The owner of the used API token does not have permission to make API calls", 403));
|
||||
|
@ -1,4 +1,5 @@
|
||||
<?php namespace Tests;
|
||||
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Auth\Access\Ldap;
|
||||
use BookStack\Auth\User;
|
||||
@ -21,12 +22,16 @@ class LdapTest extends BrowserKitTest
|
||||
if (!defined('LDAP_OPT_REFERRALS')) define('LDAP_OPT_REFERRALS', 1);
|
||||
app('config')->set([
|
||||
'auth.method' => 'ldap',
|
||||
'auth.defaults.guard' => 'ldap',
|
||||
'services.ldap.base_dn' => 'dc=ldap,dc=local',
|
||||
'services.ldap.email_attribute' => 'mail',
|
||||
'services.ldap.display_name_attribute' => 'cn',
|
||||
'services.ldap.id_attribute' => 'uid',
|
||||
'services.ldap.user_to_groups' => false,
|
||||
'auth.providers.users.driver' => 'ldap',
|
||||
'services.ldap.version' => '3',
|
||||
'services.ldap.user_filter' => '(&(uid=${user}))',
|
||||
'services.ldap.follow_referrals' => false,
|
||||
'services.ldap.tls_insecure' => false,
|
||||
]);
|
||||
$this->mockLdap = \Mockery::mock(Ldap::class);
|
||||
$this->app[Ldap::class] = $this->mockLdap;
|
||||
@ -60,16 +65,16 @@ class LdapTest extends BrowserKitTest
|
||||
{
|
||||
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setVersion')->once();
|
||||
$this->mockLdap->shouldReceive('setOption')->times(4);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
|
||||
$this->mockLdap->shouldReceive('setOption')->times(2);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
|
||||
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'uid' => [$this->mockUser->name],
|
||||
'cn' => [$this->mockUser->name],
|
||||
'dn' => ['dc=test' . config('services.ldap.base_dn')]
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
|
||||
$this->mockEscapes(4);
|
||||
$this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
|
||||
$this->mockEscapes(2);
|
||||
|
||||
$this->mockUserLogin()
|
||||
->seePageIs('/login')->see('Please enter an email to use for this account.');
|
||||
@ -81,21 +86,53 @@ class LdapTest extends BrowserKitTest
|
||||
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name]);
|
||||
}
|
||||
|
||||
public function test_email_domain_restriction_active_on_new_ldap_login()
|
||||
{
|
||||
$this->setSettings([
|
||||
'registration-restrict' => 'testing.com'
|
||||
]);
|
||||
|
||||
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setVersion')->once();
|
||||
$this->mockLdap->shouldReceive('setOption')->times(2);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
|
||||
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'uid' => [$this->mockUser->name],
|
||||
'cn' => [$this->mockUser->name],
|
||||
'dn' => ['dc=test' . config('services.ldap.base_dn')]
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
|
||||
$this->mockEscapes(2);
|
||||
|
||||
$this->mockUserLogin()
|
||||
->seePageIs('/login')
|
||||
->see('Please enter an email to use for this account.');
|
||||
|
||||
$email = 'tester@invaliddomain.com';
|
||||
|
||||
$this->type($email, '#email')
|
||||
->press('Log In')
|
||||
->seePageIs('/login')
|
||||
->see('That email domain does not have access to this application')
|
||||
->dontSeeInDatabase('users', ['email' => $email]);
|
||||
}
|
||||
|
||||
public function test_login_works_when_no_uid_provided_by_ldap_server()
|
||||
{
|
||||
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setVersion')->once();
|
||||
$ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
|
||||
$this->mockLdap->shouldReceive('setOption')->times(2);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
|
||||
$this->mockLdap->shouldReceive('setOption')->times(1);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
|
||||
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'cn' => [$this->mockUser->name],
|
||||
'dn' => $ldapDn,
|
||||
'mail' => [$this->mockUser->email]
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
|
||||
$this->mockEscapes(2);
|
||||
$this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true);
|
||||
$this->mockEscapes(1);
|
||||
|
||||
$this->mockUserLogin()
|
||||
->seePageIs('/')
|
||||
@ -109,8 +146,8 @@ class LdapTest extends BrowserKitTest
|
||||
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setVersion')->once();
|
||||
$ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
|
||||
$this->mockLdap->shouldReceive('setOption')->times(2);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
|
||||
$this->mockLdap->shouldReceive('setOption')->times(1);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
|
||||
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'cn' => [$this->mockUser->name],
|
||||
@ -120,8 +157,8 @@ class LdapTest extends BrowserKitTest
|
||||
]]);
|
||||
|
||||
|
||||
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
|
||||
$this->mockEscapes(2);
|
||||
$this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true);
|
||||
$this->mockEscapes(1);
|
||||
|
||||
$this->mockUserLogin()
|
||||
->seePageIs('/')
|
||||
@ -133,16 +170,16 @@ class LdapTest extends BrowserKitTest
|
||||
{
|
||||
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setVersion')->once();
|
||||
$this->mockLdap->shouldReceive('setOption')->times(2);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
|
||||
$this->mockLdap->shouldReceive('setOption')->times(1);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
|
||||
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'uid' => [$this->mockUser->name],
|
||||
'cn' => [$this->mockUser->name],
|
||||
'dn' => ['dc=test' . config('services.ldap.base_dn')]
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false);
|
||||
$this->mockEscapes(2);
|
||||
$this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true, false);
|
||||
$this->mockEscapes(1);
|
||||
|
||||
$this->mockUserLogin()
|
||||
->seePageIs('/login')->see('These credentials do not match our records.')
|
||||
@ -201,10 +238,10 @@ class LdapTest extends BrowserKitTest
|
||||
'services.ldap.group_attribute' => 'memberOf',
|
||||
'services.ldap.remove_from_groups' => false,
|
||||
]);
|
||||
$this->mockLdap->shouldReceive('connect')->times(2)->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setVersion')->times(2);
|
||||
$this->mockLdap->shouldReceive('setOption')->times(5);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(5)
|
||||
$this->mockLdap->shouldReceive('connect')->times(1)->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setVersion')->times(1);
|
||||
$this->mockLdap->shouldReceive('setOption')->times(4);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
|
||||
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'uid' => [$this->mockUser->name],
|
||||
@ -217,8 +254,8 @@ class LdapTest extends BrowserKitTest
|
||||
1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
|
||||
]
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
|
||||
$this->mockEscapes(5);
|
||||
$this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
|
||||
$this->mockEscapes(4);
|
||||
$this->mockExplodes(6);
|
||||
|
||||
$this->mockUserLogin()->seePageIs('/');
|
||||
@ -250,10 +287,10 @@ class LdapTest extends BrowserKitTest
|
||||
'services.ldap.group_attribute' => 'memberOf',
|
||||
'services.ldap.remove_from_groups' => true,
|
||||
]);
|
||||
$this->mockLdap->shouldReceive('connect')->times(2)->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setVersion')->times(2);
|
||||
$this->mockLdap->shouldReceive('setOption')->times(4);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
|
||||
$this->mockLdap->shouldReceive('connect')->times(1)->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setVersion')->times(1);
|
||||
$this->mockLdap->shouldReceive('setOption')->times(3);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(3)
|
||||
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'uid' => [$this->mockUser->name],
|
||||
@ -265,8 +302,8 @@ class LdapTest extends BrowserKitTest
|
||||
0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
|
||||
]
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
|
||||
$this->mockEscapes(4);
|
||||
$this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
|
||||
$this->mockEscapes(3);
|
||||
$this->mockExplodes(2);
|
||||
|
||||
$this->mockUserLogin()->seePageIs('/');
|
||||
@ -299,10 +336,10 @@ class LdapTest extends BrowserKitTest
|
||||
'services.ldap.group_attribute' => 'memberOf',
|
||||
'services.ldap.remove_from_groups' => true,
|
||||
]);
|
||||
$this->mockLdap->shouldReceive('connect')->times(2)->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setVersion')->times(2);
|
||||
$this->mockLdap->shouldReceive('setOption')->times(4);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
|
||||
$this->mockLdap->shouldReceive('connect')->times(1)->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setVersion')->times(1);
|
||||
$this->mockLdap->shouldReceive('setOption')->times(3);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(3)
|
||||
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'uid' => [$this->mockUser->name],
|
||||
@ -314,8 +351,8 @@ class LdapTest extends BrowserKitTest
|
||||
0 => "cn=ex-auth-a,ou=groups,dc=example,dc=com",
|
||||
]
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
|
||||
$this->mockEscapes(4);
|
||||
$this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
|
||||
$this->mockEscapes(3);
|
||||
$this->mockExplodes(2);
|
||||
|
||||
$this->mockUserLogin()->seePageIs('/');
|
||||
@ -344,10 +381,10 @@ class LdapTest extends BrowserKitTest
|
||||
'services.ldap.group_attribute' => 'memberOf',
|
||||
'services.ldap.remove_from_groups' => true,
|
||||
]);
|
||||
$this->mockLdap->shouldReceive('connect')->times(2)->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setVersion')->times(2);
|
||||
$this->mockLdap->shouldReceive('setOption')->times(5);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(5)
|
||||
$this->mockLdap->shouldReceive('connect')->times(1)->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setVersion')->times(1);
|
||||
$this->mockLdap->shouldReceive('setOption')->times(4);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
|
||||
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'uid' => [$this->mockUser->name],
|
||||
@ -360,8 +397,8 @@ class LdapTest extends BrowserKitTest
|
||||
1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
|
||||
]
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
|
||||
$this->mockEscapes(5);
|
||||
$this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
|
||||
$this->mockEscapes(4);
|
||||
$this->mockExplodes(6);
|
||||
|
||||
$this->mockUserLogin()->seePageIs('/');
|
||||
@ -385,8 +422,8 @@ class LdapTest extends BrowserKitTest
|
||||
|
||||
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setVersion')->once();
|
||||
$this->mockLdap->shouldReceive('setOption')->times(4);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
|
||||
$this->mockLdap->shouldReceive('setOption')->times(2);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
|
||||
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'uid' => [$this->mockUser->name],
|
||||
@ -394,8 +431,8 @@ class LdapTest extends BrowserKitTest
|
||||
'dn' => ['dc=test' . config('services.ldap.base_dn')],
|
||||
'displayname' => 'displayNameAttribute'
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
|
||||
$this->mockEscapes(4);
|
||||
$this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
|
||||
$this->mockEscapes(2);
|
||||
|
||||
$this->mockUserLogin()
|
||||
->seePageIs('/login')->see('Please enter an email to use for this account.');
|
||||
@ -415,16 +452,16 @@ class LdapTest extends BrowserKitTest
|
||||
|
||||
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setVersion')->once();
|
||||
$this->mockLdap->shouldReceive('setOption')->times(4);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
|
||||
$this->mockLdap->shouldReceive('setOption')->times(2);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
|
||||
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'uid' => [$this->mockUser->name],
|
||||
'cn' => [$this->mockUser->name],
|
||||
'dn' => ['dc=test' . config('services.ldap.base_dn')]
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
|
||||
$this->mockEscapes(4);
|
||||
$this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
|
||||
$this->mockEscapes(2);
|
||||
|
||||
$this->mockUserLogin()
|
||||
->seePageIs('/login')->see('Please enter an email to use for this account.');
|
||||
@ -444,14 +481,14 @@ class LdapTest extends BrowserKitTest
|
||||
|
||||
// Standard mocks
|
||||
$this->mockLdap->shouldReceive('setVersion')->once();
|
||||
$this->mockLdap->shouldReceive('setOption')->times(2);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)->andReturn(['count' => 1, 0 => [
|
||||
$this->mockLdap->shouldReceive('setOption')->times(1);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)->andReturn(['count' => 1, 0 => [
|
||||
'uid' => [$this->mockUser->name],
|
||||
'cn' => [$this->mockUser->name],
|
||||
'dn' => ['dc=test' . config('services.ldap.base_dn')]
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
|
||||
$this->mockEscapes(2);
|
||||
$this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true);
|
||||
$this->mockEscapes(1);
|
||||
|
||||
$this->mockLdap->shouldReceive('connect')->once()
|
||||
->with($expectedHost, $expectedPort)->andReturn($this->resourceId);
|
||||
@ -473,4 +510,37 @@ class LdapTest extends BrowserKitTest
|
||||
{
|
||||
$this->checkLdapReceivesCorrectDetails('ldap.bookstack.com', 'ldap.bookstack.com', 389);
|
||||
}
|
||||
|
||||
public function test_forgot_password_routes_inaccessible()
|
||||
{
|
||||
$resp = $this->get('/password/email');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$resp = $this->post('/password/email');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$resp = $this->get('/password/reset/abc123');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$resp = $this->post('/password/reset');
|
||||
$this->assertPermissionError($resp);
|
||||
}
|
||||
|
||||
public function test_user_invite_routes_inaccessible()
|
||||
{
|
||||
$resp = $this->get('/register/invite/abc123');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$resp = $this->post('/register/invite/abc123');
|
||||
$this->assertPermissionError($resp);
|
||||
}
|
||||
|
||||
public function test_user_register_routes_inaccessible()
|
||||
{
|
||||
$resp = $this->get('/register');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$resp = $this->post('/register');
|
||||
$this->assertPermissionError($resp);
|
||||
}
|
||||
}
|
||||
|
@ -11,9 +11,9 @@ class Saml2Test extends TestCase
|
||||
parent::setUp();
|
||||
// Set default config for SAML2
|
||||
config()->set([
|
||||
'auth.method' => 'saml2',
|
||||
'auth.defaults.guard' => 'saml2',
|
||||
'saml2.name' => 'SingleSignOn-Testing',
|
||||
'saml2.enabled' => true,
|
||||
'saml2.auto_register' => true,
|
||||
'saml2.email_attribute' => 'email',
|
||||
'saml2.display_name_attributes' => ['first_name', 'last_name'],
|
||||
'saml2.external_id_attribute' => 'uid',
|
||||
@ -53,27 +53,12 @@ class Saml2Test extends TestCase
|
||||
{
|
||||
$req = $this->get('/login');
|
||||
$req->assertSeeText('SingleSignOn-Testing');
|
||||
$req->assertElementExists('a[href$="/saml2/login"]');
|
||||
}
|
||||
|
||||
public function test_login_option_shows_on_register_page_only_when_auto_register_enabled()
|
||||
{
|
||||
$this->setSettings(['app-public' => 'true', 'registration-enabled' => 'true']);
|
||||
|
||||
$req = $this->get('/register');
|
||||
$req->assertSeeText('SingleSignOn-Testing');
|
||||
$req->assertElementExists('a[href$="/saml2/login"]');
|
||||
|
||||
config()->set(['saml2.auto_register' => false]);
|
||||
|
||||
$req = $this->get('/register');
|
||||
$req->assertDontSeeText('SingleSignOn-Testing');
|
||||
$req->assertElementNotExists('a[href$="/saml2/login"]');
|
||||
$req->assertElementExists('form[action$="/saml2/login"][method=POST] button');
|
||||
}
|
||||
|
||||
public function test_login()
|
||||
{
|
||||
$req = $this->get('/saml2/login');
|
||||
$req = $this->post('/saml2/login');
|
||||
$redirect = $req->headers->get('location');
|
||||
$this->assertStringStartsWith('http://saml.local/saml2/idp/SSOService.php', $redirect, 'Login redirects to SSO location');
|
||||
|
||||
@ -88,7 +73,7 @@ class Saml2Test extends TestCase
|
||||
$this->assertDatabaseHas('users', [
|
||||
'email' => 'user@example.com',
|
||||
'external_auth_id' => 'user',
|
||||
'email_confirmed' => true,
|
||||
'email_confirmed' => false,
|
||||
'name' => 'Barry Scott'
|
||||
]);
|
||||
|
||||
@ -138,20 +123,15 @@ class Saml2Test extends TestCase
|
||||
});
|
||||
}
|
||||
|
||||
public function test_logout_redirects_to_saml_logout_when_active_saml_session()
|
||||
public function test_logout_link_directs_to_saml_path()
|
||||
{
|
||||
config()->set([
|
||||
'saml2.onelogin.strict' => false,
|
||||
]);
|
||||
|
||||
$this->withPost(['SAMLResponse' => $this->acsPostData], function () {
|
||||
$acsPost = $this->post('/saml2/acs');
|
||||
$lastLoginType = session()->get('last_login_type');
|
||||
$this->assertEquals('saml2', $lastLoginType);
|
||||
|
||||
$req = $this->get('/logout');
|
||||
$req->assertRedirect('/saml2/logout');
|
||||
});
|
||||
$resp = $this->actingAs($this->getEditor())->get('/');
|
||||
$resp->assertElementExists('a[href$="/saml2/logout"]');
|
||||
$resp->assertElementContains('a[href$="/saml2/logout"]', 'Logout');
|
||||
}
|
||||
|
||||
public function test_logout_sls_flow()
|
||||
@ -229,32 +209,86 @@ class Saml2Test extends TestCase
|
||||
$acsPost = $this->post('/saml2/acs');
|
||||
$acsPost->assertRedirect('/');
|
||||
$errorMessage = session()->get('error');
|
||||
$this->assertEquals('Registration unsuccessful since a user already exists with email address "user@example.com"', $errorMessage);
|
||||
$this->assertEquals('A user with the email user@example.com already exists but with different credentials.', $errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
public function test_saml_routes_are_only_active_if_saml_enabled()
|
||||
{
|
||||
config()->set(['saml2.enabled' => false]);
|
||||
$getRoutes = ['/login', '/logout', '/metadata', '/sls'];
|
||||
config()->set(['auth.method' => 'standard']);
|
||||
$getRoutes = ['/logout', '/metadata', '/sls'];
|
||||
foreach ($getRoutes as $route) {
|
||||
$req = $this->get('/saml2' . $route);
|
||||
$req->assertRedirect('/');
|
||||
$error = session()->get('error');
|
||||
$this->assertStringStartsWith('You do not have permission to access', $error);
|
||||
session()->flush();
|
||||
$this->assertPermissionError($req);
|
||||
}
|
||||
|
||||
$postRoutes = ['/acs'];
|
||||
$postRoutes = ['/login', '/acs'];
|
||||
foreach ($postRoutes as $route) {
|
||||
$req = $this->post('/saml2' . $route);
|
||||
$req->assertRedirect('/');
|
||||
$error = session()->get('error');
|
||||
$this->assertStringStartsWith('You do not have permission to access', $error);
|
||||
session()->flush();
|
||||
$this->assertPermissionError($req);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_forgot_password_routes_inaccessible()
|
||||
{
|
||||
$resp = $this->get('/password/email');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$resp = $this->post('/password/email');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$resp = $this->get('/password/reset/abc123');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$resp = $this->post('/password/reset');
|
||||
$this->assertPermissionError($resp);
|
||||
}
|
||||
|
||||
public function test_standard_login_routes_inaccessible()
|
||||
{
|
||||
$resp = $this->post('/login');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$resp = $this->get('/logout');
|
||||
$this->assertPermissionError($resp);
|
||||
}
|
||||
|
||||
public function test_user_invite_routes_inaccessible()
|
||||
{
|
||||
$resp = $this->get('/register/invite/abc123');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$resp = $this->post('/register/invite/abc123');
|
||||
$this->assertPermissionError($resp);
|
||||
}
|
||||
|
||||
public function test_user_register_routes_inaccessible()
|
||||
{
|
||||
$resp = $this->get('/register');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$resp = $this->post('/register');
|
||||
$this->assertPermissionError($resp);
|
||||
}
|
||||
|
||||
public function test_email_domain_restriction_active_on_new_saml_login()
|
||||
{
|
||||
$this->setSettings([
|
||||
'registration-restrict' => 'testing.com'
|
||||
]);
|
||||
config()->set([
|
||||
'saml2.onelogin.strict' => false,
|
||||
]);
|
||||
|
||||
$this->withPost(['SAMLResponse' => $this->acsPostData], function () {
|
||||
$acsPost = $this->post('/saml2/acs');
|
||||
$acsPost->assertRedirect('/login');
|
||||
$errorMessage = session()->get('error');
|
||||
$this->assertStringContainsString('That email domain does not have access to this application', $errorMessage);
|
||||
$this->assertDatabaseMissing('users', ['email' => 'user@example.com']);
|
||||
});
|
||||
}
|
||||
|
||||
protected function withGet(array $options, callable $callback)
|
||||
{
|
||||
return $this->withGlobal($_GET, $options, $callback);
|
||||
|
@ -262,4 +262,19 @@ trait SharedTestHelpers
|
||||
self::assertThat($passed, self::isTrue(), "Failed asserting that given map:\n\n{$toCheckStr}\n\nincludes:\n\n{$toIncludeStr}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a permission error has occurred.
|
||||
*/
|
||||
protected function assertPermissionError($response)
|
||||
{
|
||||
if ($response instanceof BrowserKitTest) {
|
||||
$response = \Illuminate\Foundation\Testing\TestResponse::fromBaseResponse($response->response);
|
||||
}
|
||||
|
||||
$response->assertRedirect('/');
|
||||
$this->assertSessionHas('error');
|
||||
$error = session()->pull('error');
|
||||
$this->assertStringStartsWith('You do not have permission to access', $error);
|
||||
}
|
||||
|
||||
}
|
@ -16,19 +16,6 @@ abstract class TestCase extends BaseTestCase
|
||||
*/
|
||||
protected $baseUrl = 'http://localhost';
|
||||
|
||||
/**
|
||||
* Assert a permission error has occurred.
|
||||
* @param TestResponse $response
|
||||
* @return TestCase
|
||||
*/
|
||||
protected function assertPermissionError(TestResponse $response)
|
||||
{
|
||||
$response->assertRedirect('/');
|
||||
$this->assertSessionHas('error');
|
||||
session()->remove('error');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the session contains a specific entry.
|
||||
* @param string $key
|
||||
|
Loading…
Reference in New Issue
Block a user