mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Auth: Refactored OIDC RP-logout PR code, Extracted logout
Extracted logout to the login service so the logic can be shared instead of re-implemented at each stage. For this, the SocialAuthService was split so the driver management is in its own class, so it can be used elsewhere without use (or circular dependencies) of the SocialAuthService. During review of #4467
This commit is contained in:
parent
cc10d1ddfc
commit
bba7dcce49
@ -273,11 +273,8 @@ OIDC_USER_TO_GROUPS=false
|
|||||||
OIDC_GROUPS_CLAIM=groups
|
OIDC_GROUPS_CLAIM=groups
|
||||||
OIDC_REMOVE_FROM_GROUPS=false
|
OIDC_REMOVE_FROM_GROUPS=false
|
||||||
OIDC_EXTERNAL_ID_CLAIM=sub
|
OIDC_EXTERNAL_ID_CLAIM=sub
|
||||||
|
|
||||||
# OIDC Logout Feature: Its value should be value of end_session_endpoint from <issuer>/.well-known/openid-configuration
|
|
||||||
OIDC_END_SESSION_ENDPOINT=null
|
OIDC_END_SESSION_ENDPOINT=null
|
||||||
|
|
||||||
|
|
||||||
# Disable default third-party services such as Gravatar and Draw.IO
|
# Disable default third-party services such as Gravatar and Draw.IO
|
||||||
# Service-specific options will override this option
|
# Service-specific options will override this option
|
||||||
DISABLE_EXTERNAL_SERVICES=false
|
DISABLE_EXTERNAL_SERVICES=false
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
namespace BookStack\Access\Controllers;
|
namespace BookStack\Access\Controllers;
|
||||||
|
|
||||||
use BookStack\Access\LoginService;
|
use BookStack\Access\LoginService;
|
||||||
use BookStack\Access\SocialAuthService;
|
use BookStack\Access\SocialDriverManager;
|
||||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||||
use BookStack\Exceptions\LoginAttemptException;
|
use BookStack\Exceptions\LoginAttemptException;
|
||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
@ -17,19 +17,19 @@ class LoginController extends Controller
|
|||||||
{
|
{
|
||||||
use ThrottlesLogins;
|
use ThrottlesLogins;
|
||||||
|
|
||||||
protected SocialAuthService $socialAuthService;
|
protected SocialDriverManager $socialDriverManager;
|
||||||
protected LoginService $loginService;
|
protected LoginService $loginService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new controller instance.
|
* Create a new controller instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(SocialAuthService $socialAuthService, LoginService $loginService)
|
public function __construct(SocialDriverManager $driverManager, LoginService $loginService)
|
||||||
{
|
{
|
||||||
$this->middleware('guest', ['only' => ['getLogin', 'login']]);
|
$this->middleware('guest', ['only' => ['getLogin', 'login']]);
|
||||||
$this->middleware('guard:standard,ldap', ['only' => ['login']]);
|
$this->middleware('guard:standard,ldap', ['only' => ['login']]);
|
||||||
$this->middleware('guard:standard,ldap,oidc', ['only' => ['logout']]);
|
$this->middleware('guard:standard,ldap,oidc', ['only' => ['logout']]);
|
||||||
|
|
||||||
$this->socialAuthService = $socialAuthService;
|
$this->socialDriverManager = $driverManager;
|
||||||
$this->loginService = $loginService;
|
$this->loginService = $loginService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ class LoginController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function getLogin(Request $request)
|
public function getLogin(Request $request)
|
||||||
{
|
{
|
||||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
$socialDrivers = $this->socialDriverManager->getActive();
|
||||||
$authMethod = config('auth.method');
|
$authMethod = config('auth.method');
|
||||||
$preventInitiation = $request->get('prevent_auto_init') === 'true';
|
$preventInitiation = $request->get('prevent_auto_init') === 'true';
|
||||||
|
|
||||||
@ -101,15 +101,9 @@ class LoginController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Logout user and perform subsequent redirect.
|
* Logout user and perform subsequent redirect.
|
||||||
*/
|
*/
|
||||||
public function logout(Request $request)
|
public function logout()
|
||||||
{
|
{
|
||||||
Auth::guard()->logout();
|
return redirect($this->loginService->logout());
|
||||||
$request->session()->invalidate();
|
|
||||||
$request->session()->regenerateToken();
|
|
||||||
|
|
||||||
$redirectUri = $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/';
|
|
||||||
|
|
||||||
return redirect($redirectUri);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -218,16 +212,4 @@ class LoginController extends Controller
|
|||||||
|
|
||||||
redirect()->setIntendedUrl($previous);
|
redirect()->setIntendedUrl($previous);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if login auto-initiate should be valid based upon authentication config.
|
|
||||||
*/
|
|
||||||
protected function shouldAutoInitiate(): bool
|
|
||||||
{
|
|
||||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
|
||||||
$authMethod = config('auth.method');
|
|
||||||
$autoRedirect = config('auth.auto_initiate');
|
|
||||||
|
|
||||||
return $autoRedirect && count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,6 @@ class OidcController extends Controller
|
|||||||
{
|
{
|
||||||
protected OidcService $oidcService;
|
protected OidcService $oidcService;
|
||||||
|
|
||||||
/**
|
|
||||||
* OpenIdController constructor.
|
|
||||||
*/
|
|
||||||
public function __construct(OidcService $oidcService)
|
public function __construct(OidcService $oidcService)
|
||||||
{
|
{
|
||||||
$this->oidcService = $oidcService;
|
$this->oidcService = $oidcService;
|
||||||
@ -65,16 +62,10 @@ class OidcController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OIDC Logout Feature: Start the authorization logout flow via OIDC.
|
* Log the user out then start the OIDC RP-initiated logout process.
|
||||||
*/
|
*/
|
||||||
public function logout()
|
public function logout()
|
||||||
{
|
{
|
||||||
try {
|
return redirect($this->oidcService->logout());
|
||||||
return $this->oidcService->logout();
|
|
||||||
} catch (OidcException $exception) {
|
|
||||||
$this->showErrorNotification($exception->getMessage());
|
|
||||||
return redirect('/logout');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ namespace BookStack\Access\Controllers;
|
|||||||
|
|
||||||
use BookStack\Access\LoginService;
|
use BookStack\Access\LoginService;
|
||||||
use BookStack\Access\RegistrationService;
|
use BookStack\Access\RegistrationService;
|
||||||
use BookStack\Access\SocialAuthService;
|
use BookStack\Access\SocialDriverManager;
|
||||||
use BookStack\Exceptions\StoppedAuthenticationException;
|
use BookStack\Exceptions\StoppedAuthenticationException;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
@ -15,7 +15,7 @@ use Illuminate\Validation\Rules\Password;
|
|||||||
|
|
||||||
class RegisterController extends Controller
|
class RegisterController extends Controller
|
||||||
{
|
{
|
||||||
protected SocialAuthService $socialAuthService;
|
protected SocialDriverManager $socialDriverManager;
|
||||||
protected RegistrationService $registrationService;
|
protected RegistrationService $registrationService;
|
||||||
protected LoginService $loginService;
|
protected LoginService $loginService;
|
||||||
|
|
||||||
@ -23,14 +23,14 @@ class RegisterController extends Controller
|
|||||||
* Create a new controller instance.
|
* Create a new controller instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
SocialAuthService $socialAuthService,
|
SocialDriverManager $socialDriverManager,
|
||||||
RegistrationService $registrationService,
|
RegistrationService $registrationService,
|
||||||
LoginService $loginService
|
LoginService $loginService
|
||||||
) {
|
) {
|
||||||
$this->middleware('guest');
|
$this->middleware('guest');
|
||||||
$this->middleware('guard:standard');
|
$this->middleware('guard:standard');
|
||||||
|
|
||||||
$this->socialAuthService = $socialAuthService;
|
$this->socialDriverManager = $socialDriverManager;
|
||||||
$this->registrationService = $registrationService;
|
$this->registrationService = $registrationService;
|
||||||
$this->loginService = $loginService;
|
$this->loginService = $loginService;
|
||||||
}
|
}
|
||||||
@ -43,7 +43,7 @@ class RegisterController extends Controller
|
|||||||
public function getRegister()
|
public function getRegister()
|
||||||
{
|
{
|
||||||
$this->registrationService->ensureRegistrationAllowed();
|
$this->registrationService->ensureRegistrationAllowed();
|
||||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
$socialDrivers = $this->socialDriverManager->getActive();
|
||||||
|
|
||||||
return view('auth.register', [
|
return view('auth.register', [
|
||||||
'socialDrivers' => $socialDrivers,
|
'socialDrivers' => $socialDrivers,
|
||||||
|
@ -79,7 +79,7 @@ class SocialController extends Controller
|
|||||||
try {
|
try {
|
||||||
return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser);
|
return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser);
|
||||||
} catch (SocialSignInAccountNotUsed $exception) {
|
} catch (SocialSignInAccountNotUsed $exception) {
|
||||||
if ($this->socialAuthService->driverAutoRegisterEnabled($socialDriver)) {
|
if ($this->socialAuthService->drivers()->isAutoRegisterEnabled($socialDriver)) {
|
||||||
return $this->socialRegisterCallback($socialDriver, $socialUser);
|
return $this->socialRegisterCallback($socialDriver, $socialUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ class SocialController extends Controller
|
|||||||
{
|
{
|
||||||
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
|
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
|
||||||
$socialAccount = $this->socialAuthService->newSocialAccount($socialDriver, $socialUser);
|
$socialAccount = $this->socialAuthService->newSocialAccount($socialDriver, $socialUser);
|
||||||
$emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver);
|
$emailVerified = $this->socialAuthService->drivers()->isAutoConfirmEmailEnabled($socialDriver);
|
||||||
|
|
||||||
// Create an array of the user data to create a new user instance
|
// Create an array of the user data to create a new user instance
|
||||||
$userData = [
|
$userData = [
|
||||||
|
@ -16,13 +16,11 @@ class LoginService
|
|||||||
{
|
{
|
||||||
protected const LAST_LOGIN_ATTEMPTED_SESSION_KEY = 'auth-login-last-attempted';
|
protected const LAST_LOGIN_ATTEMPTED_SESSION_KEY = 'auth-login-last-attempted';
|
||||||
|
|
||||||
protected $mfaSession;
|
public function __construct(
|
||||||
protected $emailConfirmationService;
|
protected MfaSession $mfaSession,
|
||||||
|
protected EmailConfirmationService $emailConfirmationService,
|
||||||
public function __construct(MfaSession $mfaSession, EmailConfirmationService $emailConfirmationService)
|
protected SocialDriverManager $socialDriverManager,
|
||||||
{
|
) {
|
||||||
$this->mfaSession = $mfaSession;
|
|
||||||
$this->emailConfirmationService = $emailConfirmationService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -163,4 +161,29 @@ class LoginService
|
|||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the current user out of the application.
|
||||||
|
* Returns an app post-redirect path.
|
||||||
|
*/
|
||||||
|
public function logout(): string
|
||||||
|
{
|
||||||
|
auth()->logout();
|
||||||
|
session()->invalidate();
|
||||||
|
session()->regenerateToken();
|
||||||
|
|
||||||
|
return $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if login auto-initiate should be valid based upon authentication config.
|
||||||
|
*/
|
||||||
|
protected function shouldAutoInitiate(): bool
|
||||||
|
{
|
||||||
|
$socialDrivers = $this->socialDriverManager->getActive();
|
||||||
|
$authMethod = config('auth.method');
|
||||||
|
$autoRedirect = config('auth.auto_initiate');
|
||||||
|
|
||||||
|
return $autoRedirect && count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,11 +217,7 @@ class OidcService
|
|||||||
$settings->keys,
|
$settings->keys,
|
||||||
);
|
);
|
||||||
|
|
||||||
// OIDC Logout Feature: Temporarily save token in session
|
session()->put("oidc_id_token", $idTokenText);
|
||||||
$access_token_for_logout = $idTokenText;
|
|
||||||
session()->put("oidctoken", $access_token_for_logout);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$returnClaims = Theme::dispatch(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $idToken->getAllClaims(), [
|
$returnClaims = Theme::dispatch(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $idToken->getAllClaims(), [
|
||||||
'access_token' => $accessToken->getToken(),
|
'access_token' => $accessToken->getToken(),
|
||||||
@ -291,36 +287,24 @@ class OidcService
|
|||||||
return $this->config()['user_to_groups'] !== false;
|
return $this->config()['user_to_groups'] !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OIDC Logout Feature: Initiate a logout flow.
|
* Start the RP-initiated logout flow if active, otherwise start a standard logout flow.
|
||||||
*
|
* Returns a post-app-logout redirect URL.
|
||||||
* @throws OidcException
|
* Reference: https://openid.net/specs/openid-connect-rpinitiated-1_0.html
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
public function logout() {
|
public function logout(): string
|
||||||
|
{
|
||||||
|
$endSessionEndpoint = $this->config()["end_session_endpoint"];
|
||||||
|
|
||||||
$config = $this->config();
|
// TODO - Add autodiscovery and false/null config value support.
|
||||||
$app_url = env('APP_URL', '');
|
|
||||||
$end_session_endpoint = $config["end_session_endpoint"];
|
|
||||||
|
|
||||||
$oidctoken = session()->get("oidctoken");
|
|
||||||
session()->invalidate();
|
|
||||||
|
|
||||||
if (str_contains($app_url, 'https://')) {
|
|
||||||
$protocol = 'https://';
|
|
||||||
} else {
|
|
||||||
$protocol = 'http://';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return redirect($end_session_endpoint.'?id_token_hint='.$oidctoken."&post_logout_redirect_uri=".$protocol.$_SERVER['HTTP_HOST']."/");
|
|
||||||
|
|
||||||
|
$oidcToken = session()->pull("oidc_id_token");
|
||||||
|
$defaultLogoutUrl = url($this->loginService->logout());
|
||||||
|
$endpointParams = [
|
||||||
|
'id_token_hint' => $oidcToken,
|
||||||
|
'post_logout_redirect_uri' => $defaultLogoutUrl,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $endSessionEndpoint . '?' . http_build_query($endpointParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -71,8 +71,7 @@ class Saml2Service
|
|||||||
throw $error;
|
throw $error;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->actionLogout();
|
$url = $this->loginService->logout();
|
||||||
$url = '/';
|
|
||||||
$id = null;
|
$id = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,20 +139,11 @@ class Saml2Service
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->actionLogout();
|
$this->loginService->logout();
|
||||||
|
|
||||||
return $redirect;
|
return $redirect;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Do the required actions to log a user out.
|
|
||||||
*/
|
|
||||||
protected function actionLogout()
|
|
||||||
{
|
|
||||||
auth()->logout();
|
|
||||||
session()->invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the metadata for this service provider.
|
* Get the metadata for this service provider.
|
||||||
*
|
*
|
||||||
|
@ -2,69 +2,24 @@
|
|||||||
|
|
||||||
namespace BookStack\Access;
|
namespace BookStack\Access;
|
||||||
|
|
||||||
use BookStack\Auth\Access\handler;
|
|
||||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||||
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
use BookStack\Users\Models\User;
|
use BookStack\Users\Models\User;
|
||||||
use Illuminate\Support\Facades\Event;
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||||
use Laravel\Socialite\Contracts\Provider;
|
use Laravel\Socialite\Contracts\Provider;
|
||||||
use Laravel\Socialite\Contracts\User as SocialUser;
|
use Laravel\Socialite\Contracts\User as SocialUser;
|
||||||
use Laravel\Socialite\Two\GoogleProvider;
|
use Laravel\Socialite\Two\GoogleProvider;
|
||||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
|
||||||
class SocialAuthService
|
class SocialAuthService
|
||||||
{
|
{
|
||||||
/**
|
public function __construct(
|
||||||
* The core socialite library used.
|
protected Socialite $socialite,
|
||||||
*
|
protected LoginService $loginService,
|
||||||
* @var Socialite
|
protected SocialDriverManager $driverManager,
|
||||||
*/
|
) {
|
||||||
protected $socialite;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var LoginService
|
|
||||||
*/
|
|
||||||
protected $loginService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default built-in social drivers we support.
|
|
||||||
*
|
|
||||||
* @var string[]
|
|
||||||
*/
|
|
||||||
protected $validSocialDrivers = [
|
|
||||||
'google',
|
|
||||||
'github',
|
|
||||||
'facebook',
|
|
||||||
'slack',
|
|
||||||
'twitter',
|
|
||||||
'azure',
|
|
||||||
'okta',
|
|
||||||
'gitlab',
|
|
||||||
'twitch',
|
|
||||||
'discord',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callbacks to run when configuring a social driver
|
|
||||||
* for an initial redirect action.
|
|
||||||
* Array is keyed by social driver name.
|
|
||||||
* Callbacks are passed an instance of the driver.
|
|
||||||
*
|
|
||||||
* @var array<string, callable>
|
|
||||||
*/
|
|
||||||
protected $configureForRedirectCallbacks = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SocialAuthService constructor.
|
|
||||||
*/
|
|
||||||
public function __construct(Socialite $socialite, LoginService $loginService)
|
|
||||||
{
|
|
||||||
$this->socialite = $socialite;
|
|
||||||
$this->loginService = $loginService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,9 +29,10 @@ class SocialAuthService
|
|||||||
*/
|
*/
|
||||||
public function startLogIn(string $socialDriver): RedirectResponse
|
public function startLogIn(string $socialDriver): RedirectResponse
|
||||||
{
|
{
|
||||||
$driver = $this->validateDriver($socialDriver);
|
$socialDriver = trim(strtolower($socialDriver));
|
||||||
|
$this->driverManager->ensureDriverActive($socialDriver);
|
||||||
|
|
||||||
return $this->getDriverForRedirect($driver)->redirect();
|
return $this->getDriverForRedirect($socialDriver)->redirect();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,9 +42,10 @@ class SocialAuthService
|
|||||||
*/
|
*/
|
||||||
public function startRegister(string $socialDriver): RedirectResponse
|
public function startRegister(string $socialDriver): RedirectResponse
|
||||||
{
|
{
|
||||||
$driver = $this->validateDriver($socialDriver);
|
$socialDriver = trim(strtolower($socialDriver));
|
||||||
|
$this->driverManager->ensureDriverActive($socialDriver);
|
||||||
|
|
||||||
return $this->getDriverForRedirect($driver)->redirect();
|
return $this->getDriverForRedirect($socialDriver)->redirect();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -119,9 +76,10 @@ class SocialAuthService
|
|||||||
*/
|
*/
|
||||||
public function getSocialUser(string $socialDriver): SocialUser
|
public function getSocialUser(string $socialDriver): SocialUser
|
||||||
{
|
{
|
||||||
$driver = $this->validateDriver($socialDriver);
|
$socialDriver = trim(strtolower($socialDriver));
|
||||||
|
$this->driverManager->ensureDriverActive($socialDriver);
|
||||||
|
|
||||||
return $this->socialite->driver($driver)->user();
|
return $this->socialite->driver($socialDriver)->user();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -131,6 +89,7 @@ class SocialAuthService
|
|||||||
*/
|
*/
|
||||||
public function handleLoginCallback(string $socialDriver, SocialUser $socialUser)
|
public function handleLoginCallback(string $socialDriver, SocialUser $socialUser)
|
||||||
{
|
{
|
||||||
|
$socialDriver = trim(strtolower($socialDriver));
|
||||||
$socialId = $socialUser->getId();
|
$socialId = $socialUser->getId();
|
||||||
|
|
||||||
// Get any attached social accounts or users
|
// Get any attached social accounts or users
|
||||||
@ -181,76 +140,11 @@ class SocialAuthService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the social driver is correct and supported.
|
* Get the social driver manager used by this service.
|
||||||
*
|
|
||||||
* @throws SocialDriverNotConfigured
|
|
||||||
*/
|
*/
|
||||||
protected function validateDriver(string $socialDriver): string
|
public function drivers(): SocialDriverManager
|
||||||
{
|
{
|
||||||
$driver = trim(strtolower($socialDriver));
|
return $this->driverManager;
|
||||||
|
|
||||||
if (!in_array($driver, $this->validSocialDrivers)) {
|
|
||||||
abort(404, trans('errors.social_driver_not_found'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->checkDriverConfigured($driver)) {
|
|
||||||
throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => Str::title($socialDriver)]));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $driver;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check a social driver has been configured correctly.
|
|
||||||
*/
|
|
||||||
protected function checkDriverConfigured(string $driver): bool
|
|
||||||
{
|
|
||||||
$lowerName = strtolower($driver);
|
|
||||||
$configPrefix = 'services.' . $lowerName . '.';
|
|
||||||
$config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
|
|
||||||
|
|
||||||
return !in_array(false, $config) && !in_array(null, $config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the names of the active social drivers.
|
|
||||||
* @returns array<string, string>
|
|
||||||
*/
|
|
||||||
public function getActiveDrivers(): array
|
|
||||||
{
|
|
||||||
$activeDrivers = [];
|
|
||||||
|
|
||||||
foreach ($this->validSocialDrivers as $driverKey) {
|
|
||||||
if ($this->checkDriverConfigured($driverKey)) {
|
|
||||||
$activeDrivers[$driverKey] = $this->getDriverName($driverKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $activeDrivers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the presentational name for a driver.
|
|
||||||
*/
|
|
||||||
public function getDriverName(string $driver): string
|
|
||||||
{
|
|
||||||
return config('services.' . strtolower($driver) . '.name');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the current config for the given driver allows auto-registration.
|
|
||||||
*/
|
|
||||||
public function driverAutoRegisterEnabled(string $driver): bool
|
|
||||||
{
|
|
||||||
return config('services.' . strtolower($driver) . '.auto_register') === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the current config for the given driver allow email address auto-confirmation.
|
|
||||||
*/
|
|
||||||
public function driverAutoConfirmEmailEnabled(string $driver): bool
|
|
||||||
{
|
|
||||||
return config('services.' . strtolower($driver) . '.auto_confirm') === true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -284,33 +178,8 @@ class SocialAuthService
|
|||||||
$driver->with(['prompt' => 'select_account']);
|
$driver->with(['prompt' => 'select_account']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($this->configureForRedirectCallbacks[$driverName])) {
|
$this->driverManager->getConfigureForRedirectCallback($driverName)($driver);
|
||||||
$this->configureForRedirectCallbacks[$driverName]($driver);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $driver;
|
return $driver;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a custom socialite driver to be used.
|
|
||||||
* Driver name should be lower_snake_case.
|
|
||||||
* Config array should mirror the structure of a service
|
|
||||||
* within the `Config/services.php` file.
|
|
||||||
* Handler should be a Class@method handler to the SocialiteWasCalled event.
|
|
||||||
*/
|
|
||||||
public function addSocialDriver(
|
|
||||||
string $driverName,
|
|
||||||
array $config,
|
|
||||||
string $socialiteHandler,
|
|
||||||
callable $configureForRedirect = null
|
|
||||||
) {
|
|
||||||
$this->validSocialDrivers[] = $driverName;
|
|
||||||
config()->set('services.' . $driverName, $config);
|
|
||||||
config()->set('services.' . $driverName . '.redirect', url('/login/service/' . $driverName . '/callback'));
|
|
||||||
config()->set('services.' . $driverName . '.name', $config['name'] ?? $driverName);
|
|
||||||
Event::listen(SocialiteWasCalled::class, $socialiteHandler);
|
|
||||||
if (!is_null($configureForRedirect)) {
|
|
||||||
$this->configureForRedirectCallbacks[$driverName] = $configureForRedirect;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
147
app/Access/SocialDriverManager.php
Normal file
147
app/Access/SocialDriverManager.php
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Access;
|
||||||
|
|
||||||
|
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||||
|
|
||||||
|
class SocialDriverManager
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The default built-in social drivers we support.
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
protected array $validDrivers = [
|
||||||
|
'google',
|
||||||
|
'github',
|
||||||
|
'facebook',
|
||||||
|
'slack',
|
||||||
|
'twitter',
|
||||||
|
'azure',
|
||||||
|
'okta',
|
||||||
|
'gitlab',
|
||||||
|
'twitch',
|
||||||
|
'discord',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callbacks to run when configuring a social driver
|
||||||
|
* for an initial redirect action.
|
||||||
|
* Array is keyed by social driver name.
|
||||||
|
* Callbacks are passed an instance of the driver.
|
||||||
|
*
|
||||||
|
* @var array<string, callable>
|
||||||
|
*/
|
||||||
|
protected array $configureForRedirectCallbacks = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current config for the given driver allows auto-registration.
|
||||||
|
*/
|
||||||
|
public function isAutoRegisterEnabled(string $driver): bool
|
||||||
|
{
|
||||||
|
return $this->getDriverConfigProperty($driver, 'auto_register') === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current config for the given driver allow email address auto-confirmation.
|
||||||
|
*/
|
||||||
|
public function isAutoConfirmEmailEnabled(string $driver): bool
|
||||||
|
{
|
||||||
|
return $this->getDriverConfigProperty($driver, 'auto_confirm') === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the names of the active social drivers, keyed by driver id.
|
||||||
|
* @returns array<string, string>
|
||||||
|
*/
|
||||||
|
public function getActive(): array
|
||||||
|
{
|
||||||
|
$activeDrivers = [];
|
||||||
|
|
||||||
|
foreach ($this->validDrivers as $driverKey) {
|
||||||
|
if ($this->checkDriverConfigured($driverKey)) {
|
||||||
|
$activeDrivers[$driverKey] = $this->getName($driverKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $activeDrivers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the configure-for-redirect callback for the given driver.
|
||||||
|
* This is a callable that allows modification of the driver at redirect time.
|
||||||
|
* Commonly used to perform custom dynamic configuration where required.
|
||||||
|
* The callback is passed a \Laravel\Socialite\Contracts\Provider instance.
|
||||||
|
*/
|
||||||
|
public function getConfigureForRedirectCallback(string $driver): callable
|
||||||
|
{
|
||||||
|
return $this->configureForRedirectCallbacks[$driver] ?? (fn() => true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a custom socialite driver to be used.
|
||||||
|
* Driver name should be lower_snake_case.
|
||||||
|
* Config array should mirror the structure of a service
|
||||||
|
* within the `Config/services.php` file.
|
||||||
|
* Handler should be a Class@method handler to the SocialiteWasCalled event.
|
||||||
|
*/
|
||||||
|
public function addSocialDriver(
|
||||||
|
string $driverName,
|
||||||
|
array $config,
|
||||||
|
string $socialiteHandler,
|
||||||
|
callable $configureForRedirect = null
|
||||||
|
) {
|
||||||
|
$this->validDrivers[] = $driverName;
|
||||||
|
config()->set('services.' . $driverName, $config);
|
||||||
|
config()->set('services.' . $driverName . '.redirect', url('/login/service/' . $driverName . '/callback'));
|
||||||
|
config()->set('services.' . $driverName . '.name', $config['name'] ?? $driverName);
|
||||||
|
Event::listen(SocialiteWasCalled::class, $socialiteHandler);
|
||||||
|
if (!is_null($configureForRedirect)) {
|
||||||
|
$this->configureForRedirectCallbacks[$driverName] = $configureForRedirect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the presentational name for a driver.
|
||||||
|
*/
|
||||||
|
protected function getName(string $driver): string
|
||||||
|
{
|
||||||
|
return $this->getDriverConfigProperty($driver, 'name') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDriverConfigProperty(string $driver, string $property): mixed
|
||||||
|
{
|
||||||
|
return config("services.{$driver}.{$property}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the social driver is correct and supported.
|
||||||
|
*
|
||||||
|
* @throws SocialDriverNotConfigured
|
||||||
|
*/
|
||||||
|
public function ensureDriverActive(string $driverName): void
|
||||||
|
{
|
||||||
|
if (!in_array($driverName, $this->validDrivers)) {
|
||||||
|
abort(404, trans('errors.social_driver_not_found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->checkDriverConfigured($driverName)) {
|
||||||
|
throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => Str::title($driverName)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check a social driver has been configured correctly.
|
||||||
|
*/
|
||||||
|
protected function checkDriverConfigured(string $driver): bool
|
||||||
|
{
|
||||||
|
$lowerName = strtolower($driver);
|
||||||
|
$configPrefix = 'services.' . $lowerName . '.';
|
||||||
|
$config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
|
||||||
|
|
||||||
|
return !in_array(false, $config) && !in_array(null, $config);
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\App\Providers;
|
namespace BookStack\App\Providers;
|
||||||
|
|
||||||
use BookStack\Access\SocialAuthService;
|
use BookStack\Access\SocialDriverManager;
|
||||||
use BookStack\Activity\Tools\ActivityLogger;
|
use BookStack\Activity\Tools\ActivityLogger;
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Models\Bookshelf;
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
@ -36,7 +36,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
public $singletons = [
|
public $singletons = [
|
||||||
'activity' => ActivityLogger::class,
|
'activity' => ActivityLogger::class,
|
||||||
SettingService::class => SettingService::class,
|
SettingService::class => SettingService::class,
|
||||||
SocialAuthService::class => SocialAuthService::class,
|
SocialDriverManager::class => SocialDriverManager::class,
|
||||||
CspService::class => CspService::class,
|
CspService::class => CspService::class,
|
||||||
HttpRequestService::class => HttpRequestService::class,
|
HttpRequestService::class => HttpRequestService::class,
|
||||||
];
|
];
|
||||||
|
@ -36,6 +36,12 @@ return [
|
|||||||
'authorization_endpoint' => env('OIDC_AUTH_ENDPOINT', null),
|
'authorization_endpoint' => env('OIDC_AUTH_ENDPOINT', null),
|
||||||
'token_endpoint' => env('OIDC_TOKEN_ENDPOINT', null),
|
'token_endpoint' => env('OIDC_TOKEN_ENDPOINT', null),
|
||||||
|
|
||||||
|
// OIDC RP-Initiated Logout endpoint
|
||||||
|
// A null value gets the URL from discovery, if active.
|
||||||
|
// A false value force-disables RP-Initiated Logout.
|
||||||
|
// A string value forces the given URL to be used.
|
||||||
|
'end_session_endpoint' => env('OIDC_END_SESSION_ENDPOINT', null),
|
||||||
|
|
||||||
// Add extra scopes, upon those required, to the OIDC authentication request
|
// Add extra scopes, upon those required, to the OIDC authentication request
|
||||||
// Multiple values can be provided comma seperated.
|
// Multiple values can be provided comma seperated.
|
||||||
'additional_scopes' => env('OIDC_ADDITIONAL_SCOPES', null),
|
'additional_scopes' => env('OIDC_ADDITIONAL_SCOPES', null),
|
||||||
@ -45,11 +51,6 @@ return [
|
|||||||
'user_to_groups' => env('OIDC_USER_TO_GROUPS', false),
|
'user_to_groups' => env('OIDC_USER_TO_GROUPS', false),
|
||||||
// Attribute, within a OIDC ID token, to find group names within
|
// Attribute, within a OIDC ID token, to find group names within
|
||||||
'groups_claim' => env('OIDC_GROUPS_CLAIM', 'groups'),
|
'groups_claim' => env('OIDC_GROUPS_CLAIM', 'groups'),
|
||||||
// When syncing groups, remove any groups that no longer match. Otherwise sync only adds new groups.
|
// When syncing groups, remove any groups that no longer match. Otherwise, sync only adds new groups.
|
||||||
'remove_from_groups' => env('OIDC_REMOVE_FROM_GROUPS', false),
|
'remove_from_groups' => env('OIDC_REMOVE_FROM_GROUPS', false),
|
||||||
|
|
||||||
// OIDC Logout Feature: OAuth2 end_session_endpoint
|
|
||||||
'end_session_endpoint' => env('OIDC_END_SESSION_ENDPOINT', null),
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Theming;
|
namespace BookStack\Theming;
|
||||||
|
|
||||||
use BookStack\Access\SocialAuthService;
|
use BookStack\Access\SocialDriverManager;
|
||||||
use BookStack\Exceptions\ThemeException;
|
use BookStack\Exceptions\ThemeException;
|
||||||
use Illuminate\Console\Application;
|
use Illuminate\Console\Application;
|
||||||
use Illuminate\Console\Application as Artisan;
|
use Illuminate\Console\Application as Artisan;
|
||||||
@ -82,11 +82,11 @@ class ThemeService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see SocialAuthService::addSocialDriver
|
* @see SocialDriverManager::addSocialDriver
|
||||||
*/
|
*/
|
||||||
public function addSocialDriver(string $driverName, array $config, string $socialiteHandler, callable $configureForRedirect = null): void
|
public function addSocialDriver(string $driverName, array $config, string $socialiteHandler, callable $configureForRedirect = null): void
|
||||||
{
|
{
|
||||||
$socialAuthService = app()->make(SocialAuthService::class);
|
$driverManager = app()->make(SocialDriverManager::class);
|
||||||
$socialAuthService->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect);
|
$driverManager->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Users\Controllers;
|
namespace BookStack\Users\Controllers;
|
||||||
|
|
||||||
use BookStack\Access\SocialAuthService;
|
use BookStack\Access\SocialDriverManager;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Permissions\PermissionApplicator;
|
use BookStack\Permissions\PermissionApplicator;
|
||||||
use BookStack\Settings\UserNotificationPreferences;
|
use BookStack\Settings\UserNotificationPreferences;
|
||||||
@ -161,7 +161,7 @@ class UserAccountController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Show the view for the "Access & Security" account options.
|
* Show the view for the "Access & Security" account options.
|
||||||
*/
|
*/
|
||||||
public function showAuth(SocialAuthService $socialAuthService)
|
public function showAuth(SocialDriverManager $socialDriverManager)
|
||||||
{
|
{
|
||||||
$mfaMethods = user()->mfaValues()->get()->groupBy('method');
|
$mfaMethods = user()->mfaValues()->get()->groupBy('method');
|
||||||
|
|
||||||
@ -171,7 +171,7 @@ class UserAccountController extends Controller
|
|||||||
'category' => 'auth',
|
'category' => 'auth',
|
||||||
'mfaMethods' => $mfaMethods,
|
'mfaMethods' => $mfaMethods,
|
||||||
'authMethod' => config('auth.method'),
|
'authMethod' => config('auth.method'),
|
||||||
'activeSocialDrivers' => $socialAuthService->getActiveDrivers(),
|
'activeSocialDrivers' => $socialDriverManager->getActive(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Users\Controllers;
|
namespace BookStack\Users\Controllers;
|
||||||
|
|
||||||
use BookStack\Access\SocialAuthService;
|
use BookStack\Access\SocialDriverManager;
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
use BookStack\Exceptions\UserUpdateException;
|
use BookStack\Exceptions\UserUpdateException;
|
||||||
use BookStack\Http\Controller;
|
use BookStack\Http\Controller;
|
||||||
@ -101,7 +101,7 @@ class UserController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Show the form for editing the specified user.
|
* Show the form for editing the specified user.
|
||||||
*/
|
*/
|
||||||
public function edit(int $id, SocialAuthService $socialAuthService)
|
public function edit(int $id, SocialDriverManager $socialDriverManager)
|
||||||
{
|
{
|
||||||
$this->checkPermission('users-manage');
|
$this->checkPermission('users-manage');
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ class UserController extends Controller
|
|||||||
$user->load(['apiTokens', 'mfaValues']);
|
$user->load(['apiTokens', 'mfaValues']);
|
||||||
$authMethod = ($user->system_name) ? 'system' : config('auth.method');
|
$authMethod = ($user->system_name) ? 'system' : config('auth.method');
|
||||||
|
|
||||||
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
|
$activeSocialDrivers = $socialDriverManager->getActive();
|
||||||
$mfaMethods = $user->mfaValues->groupBy('method');
|
$mfaMethods = $user->mfaValues->groupBy('method');
|
||||||
$this->setPageTitle(trans('settings.user_profile'));
|
$this->setPageTitle(trans('settings.user_profile'));
|
||||||
$roles = Role::query()->orderBy('display_name', 'asc')->get();
|
$roles = Role::query()->orderBy('display_name', 'asc')->get();
|
||||||
|
@ -29,28 +29,20 @@
|
|||||||
</li>
|
</li>
|
||||||
<li><hr></li>
|
<li><hr></li>
|
||||||
<li>
|
<li>
|
||||||
<?php
|
@php
|
||||||
// OIDC Logout Feature: Use /oidc/logout if authentication method is oidc.
|
$logoutPath = match (config('auth.method')) {
|
||||||
if (config('auth.method') === 'oidc') {
|
'saml2' => '/saml2/logout',
|
||||||
?>
|
'oidc' => '/oidc/logout',
|
||||||
<form action="/oidc/logout"
|
default => '/logout',
|
||||||
method="get">
|
}
|
||||||
<?php
|
@endphp
|
||||||
// OIDC Logout Feature: Use /oidc/logout if authentication method is oidc.
|
<form action="{{ url($logoutPath) }}" method="post">
|
||||||
} else {
|
{{ csrf_field() }}
|
||||||
?>
|
<button class="icon-item" data-shortcut="logout">
|
||||||
<form action="{{ url(config('auth.method') === 'saml2' ? '/saml2/logout' : '/logout') }}"
|
@icon('logout')
|
||||||
method="post">
|
<div>{{ trans('auth.logout') }}</div>
|
||||||
<?php
|
</button>
|
||||||
// OIDC Logout Feature: Use /oidc/logout if authentication method is oidc.
|
</form>
|
||||||
}
|
|
||||||
?>
|
|
||||||
{{ csrf_field() }}
|
|
||||||
<button class="icon-item" data-shortcut="logout">
|
|
||||||
@icon('logout')
|
|
||||||
<div>{{ trans('auth.logout') }}</div>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
@ -332,8 +332,7 @@ Route::get('/saml2/acs', [AccessControllers\Saml2Controller::class, 'processAcs'
|
|||||||
// OIDC routes
|
// OIDC routes
|
||||||
Route::post('/oidc/login', [AccessControllers\OidcController::class, 'login']);
|
Route::post('/oidc/login', [AccessControllers\OidcController::class, 'login']);
|
||||||
Route::get('/oidc/callback', [AccessControllers\OidcController::class, 'callback']);
|
Route::get('/oidc/callback', [AccessControllers\OidcController::class, 'callback']);
|
||||||
// OIDC Logout Feature: Added to cater OIDC logout
|
Route::post('/oidc/logout', [AccessControllers\OidcController::class, 'logout']);
|
||||||
Route::get('/oidc/logout', [AccessControllers\OidcController::class, 'logout']);
|
|
||||||
|
|
||||||
// User invitation routes
|
// User invitation routes
|
||||||
Route::get('/register/invite/{token}', [AccessControllers\UserInviteController::class, 'showSetPassword']);
|
Route::get('/register/invite/{token}', [AccessControllers\UserInviteController::class, 'showSetPassword']);
|
||||||
|
Loading…
Reference in New Issue
Block a user