diff --git a/app/Api/ApiToken.php b/app/Api/ApiToken.php index 4ea12888e..cdcb33a7b 100644 --- a/app/Api/ApiToken.php +++ b/app/Api/ApiToken.php @@ -1,6 +1,8 @@ <?php namespace BookStack\Api; +use BookStack\Auth\User; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class ApiToken extends Model { @@ -8,4 +10,12 @@ class ApiToken extends Model protected $casts = [ 'expires_at' => 'date:Y-m-d' ]; + + /** + * Get the user that this token belongs to. + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index cd3fc83ec..64782fedc 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -1,21 +1,38 @@ <?php namespace BookStack\Http; +use BookStack\Http\Middleware\ApiAuthenticate; use Illuminate\Foundation\Http\Kernel as HttpKernel; class Kernel extends HttpKernel { /** * The application's global HTTP middleware stack. - * * These middleware are run during every request to your application. - * - * @var array */ protected $middleware = [ \BookStack\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \BookStack\Http\Middleware\TrimStrings::class, \BookStack\Http\Middleware\TrustProxies::class, + + ]; + + /** + * The priority ordering of middleware. + */ + protected $middlewarePriority = [ + \BookStack\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \Illuminate\Routing\Middleware\ThrottleRequests::class, + \BookStack\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + \BookStack\Http\Middleware\Localization::class, + \BookStack\Http\Middleware\GlobalViewData::class, + \BookStack\Http\Middleware\Authenticate::class, + \BookStack\Http\Middleware\ApiAuthenticate::class, + \BookStack\Http\Middleware\ConfirmEmails::class, ]; /** @@ -31,12 +48,16 @@ class Kernel extends HttpKernel \Illuminate\View\Middleware\ShareErrorsFromSession::class, \Illuminate\Routing\Middleware\ThrottleRequests::class, \BookStack\Http\Middleware\VerifyCsrfToken::class, - \Illuminate\Routing\Middleware\SubstituteBindings::class, \BookStack\Http\Middleware\Localization::class, \BookStack\Http\Middleware\GlobalViewData::class, + \BookStack\Http\Middleware\ConfirmEmails::class, ], 'api' => [ 'throttle:60,1', + \BookStack\Http\Middleware\EncryptCookies::class, + \Illuminate\Session\Middleware\StartSession::class, + \BookStack\Http\Middleware\ApiAuthenticate::class, + \BookStack\Http\Middleware\ConfirmEmails::class, ], ]; @@ -47,7 +68,6 @@ class Kernel extends HttpKernel */ protected $routeMiddleware = [ 'auth' => \BookStack\Http\Middleware\Authenticate::class, - 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, diff --git a/app/Http/Middleware/ApiAuthenticate.php b/app/Http/Middleware/ApiAuthenticate.php new file mode 100644 index 000000000..3e68cb3ae --- /dev/null +++ b/app/Http/Middleware/ApiAuthenticate.php @@ -0,0 +1,78 @@ +<?php + +namespace BookStack\Http\Middleware; + +use BookStack\Api\ApiToken; +use BookStack\Http\Request; +use Closure; +use Hash; + +class ApiAuthenticate +{ + + /** + * Handle an incoming request. + */ + public function handle(Request $request, Closure $next) + { + // TODO - Look to extract a lot of the logic here into a 'Guard' + // Ideally would like to be able to request API via browser without having to boot + // the session middleware (in Kernel). + +// $sessionCookieName = config('session.cookie'); +// if ($request->cookies->has($sessionCookieName)) { +// $sessionCookie = $request->cookies->get($sessionCookieName); +// $sessionCookie = decrypt($sessionCookie, false); +// dd($sessionCookie); +// } + + // Return if the user is already found to be signed in via session-based auth. + // This is to make it easy to browser the API via browser after just logging into the system. + if (signedInUser()) { + return $next($request); + } + + $authToken = trim($request->header('Authorization', '')); + if (empty($authToken)) { + return $this->unauthorisedResponse(trans('errors.api_no_authorization_found')); + } + + if (strpos($authToken, ':') === false || strpos($authToken, 'Token ') !== 0) { + return $this->unauthorisedResponse(trans('errors.api_bad_authorization_format')); + } + + [$id, $secret] = explode(':', str_replace('Token ', '', $authToken)); + $token = ApiToken::query() + ->where('token_id', '=', $id) + ->with(['user'])->first(); + + if ($token === null) { + return $this->unauthorisedResponse(trans('errors.api_user_token_not_found')); + } + + if (!Hash::check($secret, $token->secret)) { + return $this->unauthorisedResponse(trans('errors.api_incorrect_token_secret')); + } + + if (!$token->user->can('access-api')) { + return $this->unauthorisedResponse(trans('errors.api_user_no_api_permission'), 403); + } + + auth()->login($token->user); + + return $next($request); + } + + /** + * Provide a standard API unauthorised response. + */ + protected function unauthorisedResponse(string $message, int $code = 401) + { + return response()->json([ + 'error' => [ + 'code' => $code, + 'message' => $message, + ] + ], 401); + } +} diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index d840a9b2e..40acc254b 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -2,41 +2,16 @@ namespace BookStack\Http\Middleware; +use BookStack\Http\Request; use Closure; -use Illuminate\Contracts\Auth\Guard; class Authenticate { - /** - * The Guard implementation. - * @var Guard - */ - protected $auth; - - /** - * Create a new filter instance. - * @param Guard $auth - */ - public function __construct(Guard $auth) - { - $this->auth = $auth; - } - /** * Handle an incoming request. - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @return mixed */ - public function handle($request, Closure $next) + public function handle(Request $request, Closure $next) { - if ($this->auth->check()) { - $requireConfirmation = (setting('registration-confirmation') || setting('registration-restrict')); - if ($requireConfirmation && !$this->auth->user()->email_confirmed) { - return redirect('/register/confirm/awaiting'); - } - } - if (!hasAppAccess()) { if ($request->ajax()) { return response('Unauthorized.', 401); diff --git a/app/Http/Middleware/ConfirmEmails.php b/app/Http/Middleware/ConfirmEmails.php new file mode 100644 index 000000000..3700e9973 --- /dev/null +++ b/app/Http/Middleware/ConfirmEmails.php @@ -0,0 +1,60 @@ +<?php + +namespace BookStack\Http\Middleware; + +use BookStack\Http\Request; +use Closure; +use Illuminate\Contracts\Auth\Guard; + +/** + * Confirms the current user's email address. + * Must come after any middleware that may log users in. + */ +class ConfirmEmails +{ + /** + * The Guard implementation. + */ + protected $auth; + + /** + * Create a new ConfirmEmails instance. + */ + public function __construct(Guard $auth) + { + $this->auth = $auth; + } + + /** + * Handle an incoming request. + */ + public function handle(Request $request, Closure $next) + { + if ($this->auth->check()) { + $requireConfirmation = (setting('registration-confirmation') || setting('registration-restrict')); + if ($requireConfirmation && !$this->auth->user()->email_confirmed) { + return $this->errorResponse($request); + } + } + + return $next($request); + } + + /** + * Provide an error response for when the current user's email is not confirmed + * in a system which requires it. + */ + protected function errorResponse(Request $request) + { + if ($request->wantsJson()) { + return response()->json([ + 'error' => [ + 'code' => 401, + 'message' => trans('errors.email_confirmation_awaiting') + ] + ], 401); + } + + return redirect('/register/confirm/awaiting'); + } +} diff --git a/app/helpers.php b/app/helpers.php index 6211f41be..65da1853b 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -42,7 +42,6 @@ function user(): User /** * Check if current user is a signed in user. - * @return bool */ function signedInUser(): bool { @@ -51,7 +50,6 @@ function signedInUser(): bool /** * Check if the current user has general access. - * @return bool */ function hasAppAccess(): bool { @@ -62,9 +60,6 @@ function hasAppAccess(): bool * Check if the current user has a permission. * If an ownable element is passed in the jointPermissions are checked against * that particular item. - * @param string $permission - * @param Ownable $ownable - * @return bool */ function userCan(string $permission, Ownable $ownable = null): bool { diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php index a7c591c5d..85c498f48 100644 --- a/resources/lang/en/errors.php +++ b/resources/lang/en/errors.php @@ -13,6 +13,7 @@ return [ 'email_already_confirmed' => 'Email has already been confirmed, Try logging in.', 'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.', 'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.', + 'email_confirmation_awaiting' => 'The email address for the account in use needs to be confirmed', 'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind', 'ldap_fail_authed' => 'LDAP access failed using given dn & password details', 'ldap_extension_not_installed' => 'LDAP PHP extension not installed', @@ -88,4 +89,11 @@ return [ 'app_down' => ':appName is down right now', 'back_soon' => 'It will be back up soon.', + // API errors + 'api_no_authorization_found' => 'No authorization token found on the request', + 'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect', + 'api_user_token_not_found' => 'No matching API token was found for the provided authorization token', + 'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect', + 'api_user_no_api_permission' => 'The owner of the used API token does not have permission to make API calls', + ];