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',
+
 ];