mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-01-03 23:42:28 +03:00
Merge branch 'development' into lukeshu/oidc-development
This commit is contained in:
@@ -19,20 +19,25 @@ class MfaTotpController extends Controller
|
||||
|
||||
protected const SETUP_SECRET_SESSION_KEY = 'mfa-setup-totp-secret';
|
||||
|
||||
public function __construct(
|
||||
protected TotpService $totp
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a view that generates and displays a TOTP QR code.
|
||||
*/
|
||||
public function generate(TotpService $totp)
|
||||
public function generate()
|
||||
{
|
||||
if (session()->has(static::SETUP_SECRET_SESSION_KEY)) {
|
||||
$totpSecret = decrypt(session()->get(static::SETUP_SECRET_SESSION_KEY));
|
||||
} else {
|
||||
$totpSecret = $totp->generateSecret();
|
||||
$totpSecret = $this->totp->generateSecret();
|
||||
session()->put(static::SETUP_SECRET_SESSION_KEY, encrypt($totpSecret));
|
||||
}
|
||||
|
||||
$qrCodeUrl = $totp->generateUrl($totpSecret, $this->currentOrLastAttemptedUser());
|
||||
$svg = $totp->generateQrCodeSvg($qrCodeUrl);
|
||||
$qrCodeUrl = $this->totp->generateUrl($totpSecret, $this->currentOrLastAttemptedUser());
|
||||
$svg = $this->totp->generateQrCodeSvg($qrCodeUrl);
|
||||
|
||||
$this->setPageTitle(trans('auth.mfa_gen_totp_title'));
|
||||
|
||||
@@ -56,7 +61,7 @@ class MfaTotpController extends Controller
|
||||
'code' => [
|
||||
'required',
|
||||
'max:12', 'min:4',
|
||||
new TotpValidationRule($totpSecret),
|
||||
new TotpValidationRule($totpSecret, $this->totp),
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -87,7 +92,7 @@ class MfaTotpController extends Controller
|
||||
'code' => [
|
||||
'required',
|
||||
'max:12', 'min:4',
|
||||
new TotpValidationRule($totpSecret),
|
||||
new TotpValidationRule($totpSecret, $this->totp),
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
@@ -2,36 +2,26 @@
|
||||
|
||||
namespace BookStack\Access\Mfa;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
|
||||
class TotpValidationRule implements Rule
|
||||
class TotpValidationRule implements ValidationRule
|
||||
{
|
||||
protected $secret;
|
||||
protected $totpService;
|
||||
|
||||
/**
|
||||
* Create a new rule instance.
|
||||
* Takes the TOTP secret that must be system provided, not user provided.
|
||||
*/
|
||||
public function __construct(string $secret)
|
||||
{
|
||||
$this->secret = $secret;
|
||||
$this->totpService = app()->make(TotpService::class);
|
||||
public function __construct(
|
||||
protected string $secret,
|
||||
protected TotpService $totpService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*/
|
||||
public function passes($attribute, $value)
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
return $this->totpService->verifyCode($value, $this->secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*/
|
||||
public function message()
|
||||
{
|
||||
return trans('validation.totp');
|
||||
$passes = $this->totpService->verifyCode($value, $this->secret);
|
||||
if (!$passes) {
|
||||
$fail(trans('validation.totp'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,15 +83,9 @@ class OidcOAuthProvider extends AbstractProvider
|
||||
|
||||
/**
|
||||
* Checks a provider response for errors.
|
||||
*
|
||||
* @param ResponseInterface $response
|
||||
* @param array|string $data Parsed response data
|
||||
*
|
||||
* @throws IdentityProviderException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function checkResponse(ResponseInterface $response, $data)
|
||||
protected function checkResponse(ResponseInterface $response, $data): void
|
||||
{
|
||||
if ($response->getStatusCode() >= 400 || isset($data['error'])) {
|
||||
throw new IdentityProviderException(
|
||||
@@ -105,13 +99,8 @@ class OidcOAuthProvider extends AbstractProvider
|
||||
/**
|
||||
* Generates a resource owner object from a successful resource owner
|
||||
* details request.
|
||||
*
|
||||
* @param array $response
|
||||
* @param AccessToken $token
|
||||
*
|
||||
* @return ResourceOwnerInterface
|
||||
*/
|
||||
protected function createResourceOwner(array $response, AccessToken $token)
|
||||
protected function createResourceOwner(array $response, AccessToken $token): ResourceOwnerInterface
|
||||
{
|
||||
return new GenericResourceOwner($response, '');
|
||||
}
|
||||
@@ -121,14 +110,18 @@ class OidcOAuthProvider extends AbstractProvider
|
||||
*
|
||||
* The grant that was used to fetch the response can be used to provide
|
||||
* additional context.
|
||||
*
|
||||
* @param array $response
|
||||
* @param AbstractGrant $grant
|
||||
*
|
||||
* @return OidcAccessToken
|
||||
*/
|
||||
protected function createAccessToken(array $response, AbstractGrant $grant)
|
||||
protected function createAccessToken(array $response, AbstractGrant $grant): OidcAccessToken
|
||||
{
|
||||
return new OidcAccessToken($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the method used for PKCE code verifier hashing, which is passed
|
||||
* in the "code_challenge_method" parameter in the authorization request.
|
||||
*/
|
||||
protected function getPkceMethod(): string
|
||||
{
|
||||
return static::PKCE_METHOD_S256;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ class OidcService
|
||||
|
||||
/**
|
||||
* Initiate an authorization flow.
|
||||
* Provides back an authorize redirect URL, in addition to other
|
||||
* details which may be required for the auth flow.
|
||||
*
|
||||
* @throws OidcException
|
||||
*
|
||||
@@ -42,8 +44,12 @@ class OidcService
|
||||
{
|
||||
$settings = $this->getProviderSettings();
|
||||
$provider = $this->getProvider($settings);
|
||||
|
||||
$url = $provider->getAuthorizationUrl();
|
||||
session()->put('oidc_pkce_code', $provider->getPkceCode() ?? '');
|
||||
|
||||
return [
|
||||
'url' => $provider->getAuthorizationUrl(),
|
||||
'url' => $url,
|
||||
'state' => $provider->getState(),
|
||||
];
|
||||
}
|
||||
@@ -63,6 +69,10 @@ class OidcService
|
||||
$settings = $this->getProviderSettings();
|
||||
$provider = $this->getProvider($settings);
|
||||
|
||||
// Set PKCE code flashed at login
|
||||
$pkceCode = session()->pull('oidc_pkce_code', '');
|
||||
$provider->setPkceCode($pkceCode);
|
||||
|
||||
// Try to exchange authorization code for access token
|
||||
$accessToken = $provider->getAccessToken('authorization_code', [
|
||||
'code' => $authorizationCode,
|
||||
|
||||
@@ -14,20 +14,14 @@ use Illuminate\Support\Str;
|
||||
|
||||
class RegistrationService
|
||||
{
|
||||
protected $userRepo;
|
||||
protected $emailConfirmationService;
|
||||
|
||||
/**
|
||||
* RegistrationService constructor.
|
||||
*/
|
||||
public function __construct(UserRepo $userRepo, EmailConfirmationService $emailConfirmationService)
|
||||
{
|
||||
$this->userRepo = $userRepo;
|
||||
$this->emailConfirmationService = $emailConfirmationService;
|
||||
public function __construct(
|
||||
protected UserRepo $userRepo,
|
||||
protected EmailConfirmationService $emailConfirmationService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether or not registrations are allowed in the app settings.
|
||||
* Check if registrations are allowed in the app settings.
|
||||
*
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
@@ -84,6 +78,7 @@ class RegistrationService
|
||||
public function registerUser(array $userData, ?SocialAccount $socialAccount = null, bool $emailConfirmed = false): User
|
||||
{
|
||||
$userEmail = $userData['email'];
|
||||
$authSystem = $socialAccount ? $socialAccount->driver : auth()->getDefaultDriver();
|
||||
|
||||
// Email restriction
|
||||
$this->ensureEmailDomainAllowed($userEmail);
|
||||
@@ -94,6 +89,12 @@ class RegistrationService
|
||||
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $userEmail]), '/login');
|
||||
}
|
||||
|
||||
/** @var ?bool $shouldRegister */
|
||||
$shouldRegister = Theme::dispatch(ThemeEvents::AUTH_PRE_REGISTER, $authSystem, $userData);
|
||||
if ($shouldRegister === false) {
|
||||
throw new UserRegistrationException(trans('errors.auth_pre_register_theme_prevention'), '/login');
|
||||
}
|
||||
|
||||
// Create the user
|
||||
$newUser = $this->userRepo->createWithoutActivity($userData, $emailConfirmed);
|
||||
$newUser->attachDefaultRole();
|
||||
@@ -104,7 +105,7 @@ class RegistrationService
|
||||
}
|
||||
|
||||
Activity::add(ActivityType::AUTH_REGISTER, $socialAccount ?? $newUser);
|
||||
Theme::dispatch(ThemeEvents::AUTH_REGISTER, $socialAccount ? $socialAccount->driver : auth()->getDefaultDriver(), $newUser);
|
||||
Theme::dispatch(ThemeEvents::AUTH_REGISTER, $authSystem, $newUser);
|
||||
|
||||
// Start email confirmation flow if required
|
||||
if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {
|
||||
@@ -138,7 +139,7 @@ class RegistrationService
|
||||
}
|
||||
|
||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
|
||||
$userEmailDomain = $domain = mb_substr(mb_strrchr($userEmail, '@'), 1);
|
||||
$userEmailDomain = mb_substr(mb_strrchr($userEmail, '@'), 1);
|
||||
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
|
||||
$redirect = $this->registrationAllowed() ? '/register' : '/login';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user