<?php namespace BookStack\Api;
+use BookStack\Auth\User;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
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);
+ }
}
<?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,
];
/**
\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,
],
];
*/
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,
--- /dev/null
+<?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);
+ }
+}
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);
--- /dev/null
+<?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');
+ }
+}
/**
* Check if current user is a signed in user.
- * @return bool
*/
function signedInUser(): bool
{
/**
* Check if the current user has general access.
- * @return bool
*/
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
{
'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',
'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',
+
];