namespace BookStack\Api;
+use BookStack\Auth\Access\LoginService;
use BookStack\Exceptions\ApiAuthException;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
+use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
use Symfony\Component\HttpFoundation\Request;
class ApiTokenGuard implements Guard
{
-
use GuardHelpers;
/**
*/
protected $request;
+ /**
+ * @var LoginService
+ */
+ protected $loginService;
/**
* The last auth exception thrown in this request.
+ *
* @var ApiAuthException
*/
protected $lastAuthException;
/**
* ApiTokenGuard constructor.
*/
- public function __construct(Request $request)
+ public function __construct(Request $request, LoginService $loginService)
{
$this->request = $request;
+ $this->loginService = $loginService;
}
-
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
public function user()
{
}
$user = null;
+
try {
$user = $this->getAuthorisedUserFromRequest();
} catch (ApiAuthException $exception) {
}
$this->user = $user;
+
return $user;
}
/**
* Determine if current user is authenticated. If not, throw an exception.
*
- * @return \Illuminate\Contracts\Auth\Authenticatable
- *
* @throws ApiAuthException
+ *
+ * @return \Illuminate\Contracts\Auth\Authenticatable
*/
public function authenticate()
{
- if (! is_null($user = $this->user())) {
+ if (!is_null($user = $this->user())) {
return $user;
}
/**
* Check the API token in the request and fetch a valid authorised user.
+ *
* @throws ApiAuthException
*/
protected function getAuthorisedUserFromRequest(): Authenticatable
{
$authToken = trim($this->request->headers->get('Authorization', ''));
+ $this->validateTokenHeaderValue($authToken);
+
+ [$id, $secret] = explode(':', str_replace('Token ', '', $authToken));
+ $token = ApiToken::query()
+ ->where('token_id', '=', $id)
+ ->with(['user'])->first();
+
+ $this->validateToken($token, $secret);
+
+ if ($this->loginService->awaitingEmailConfirmation($token->user)) {
+ throw new ApiAuthException(trans('errors.email_confirmation_awaiting'));
+ }
+
+ return $token->user;
+ }
+
+ /**
+ * Validate the format of the token header value string.
+ *
+ * @throws ApiAuthException
+ */
+ protected function validateTokenHeaderValue(string $authToken): void
+ {
if (empty($authToken)) {
throw new ApiAuthException(trans('errors.api_no_authorization_found'));
}
if (strpos($authToken, ':') === false || strpos($authToken, 'Token ') !== 0) {
throw new ApiAuthException(trans('errors.api_bad_authorization_format'));
}
+ }
- [$id, $secret] = explode(':', str_replace('Token ', '', $authToken));
- $token = ApiToken::query()
- ->where('token_id', '=', $id)
- ->with(['user'])->first();
-
+ /**
+ * Validate the given secret against the given token and ensure the token
+ * currently has access to the instance API.
+ *
+ * @throws ApiAuthException
+ */
+ protected function validateToken(?ApiToken $token, string $secret): void
+ {
if ($token === null) {
throw new ApiAuthException(trans('errors.api_user_token_not_found'));
}
throw new ApiAuthException(trans('errors.api_incorrect_token_secret'));
}
+ $now = Carbon::now();
+ if ($token->expires_at <= $now) {
+ throw new ApiAuthException(trans('errors.api_user_token_expired'), 403);
+ }
+
if (!$token->user->can('access-api')) {
throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);
}
-
- return $token->user;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
public function validate(array $credentials = [])
{
return Hash::check($credentials['secret'], $token->secret);
}
-}
\ No newline at end of file
+ /**
+ * "Log out" the currently authenticated user.
+ */
+ public function logout()
+ {
+ $this->user = null;
+ }
+}