3 namespace BookStack\Api;
5 use BookStack\Auth\Access\LoginService;
6 use BookStack\Exceptions\ApiAuthException;
7 use Illuminate\Auth\GuardHelpers;
8 use Illuminate\Contracts\Auth\Authenticatable;
9 use Illuminate\Contracts\Auth\Guard;
10 use Illuminate\Support\Carbon;
11 use Illuminate\Support\Facades\Hash;
12 use Symfony\Component\HttpFoundation\Request;
14 class ApiTokenGuard implements Guard
19 * The request instance.
26 protected $loginService;
29 * The last auth exception thrown in this request.
31 * @var ApiAuthException
33 protected $lastAuthException;
36 * ApiTokenGuard constructor.
38 public function __construct(Request $request, LoginService $loginService)
40 $this->request = $request;
41 $this->loginService = $loginService;
47 public function user()
49 // Return the user if we've already retrieved them.
50 // Effectively a request-instance cache for this method.
51 if (!is_null($this->user)) {
58 $user = $this->getAuthorisedUserFromRequest();
59 } catch (ApiAuthException $exception) {
60 $this->lastAuthException = $exception;
69 * Determine if current user is authenticated. If not, throw an exception.
71 * @throws ApiAuthException
73 * @return \Illuminate\Contracts\Auth\Authenticatable
75 public function authenticate()
77 if (!is_null($user = $this->user())) {
81 if ($this->lastAuthException) {
82 throw $this->lastAuthException;
85 throw new ApiAuthException('Unauthorized');
89 * Check the API token in the request and fetch a valid authorised user.
91 * @throws ApiAuthException
93 protected function getAuthorisedUserFromRequest(): Authenticatable
95 $authToken = trim($this->request->headers->get('Authorization', ''));
96 $this->validateTokenHeaderValue($authToken);
98 [$id, $secret] = explode(':', str_replace('Token ', '', $authToken));
99 $token = ApiToken::query()
100 ->where('token_id', '=', $id)
101 ->with(['user'])->first();
103 $this->validateToken($token, $secret);
105 if ($this->loginService->awaitingEmailConfirmation($token->user)) {
106 throw new ApiAuthException(trans('errors.email_confirmation_awaiting'));
113 * Validate the format of the token header value string.
115 * @throws ApiAuthException
117 protected function validateTokenHeaderValue(string $authToken): void
119 if (empty($authToken)) {
120 throw new ApiAuthException(trans('errors.api_no_authorization_found'));
123 if (strpos($authToken, ':') === false || strpos($authToken, 'Token ') !== 0) {
124 throw new ApiAuthException(trans('errors.api_bad_authorization_format'));
129 * Validate the given secret against the given token and ensure the token
130 * currently has access to the instance API.
132 * @throws ApiAuthException
134 protected function validateToken(?ApiToken $token, string $secret): void
136 if ($token === null) {
137 throw new ApiAuthException(trans('errors.api_user_token_not_found'));
140 if (!Hash::check($secret, $token->secret)) {
141 throw new ApiAuthException(trans('errors.api_incorrect_token_secret'));
144 $now = Carbon::now();
145 if ($token->expires_at <= $now) {
146 throw new ApiAuthException(trans('errors.api_user_token_expired'), 403);
149 if (!$token->user->can('access-api')) {
150 throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);
157 public function validate(array $credentials = [])
159 if (empty($credentials['id']) || empty($credentials['secret'])) {
163 $token = ApiToken::query()
164 ->where('token_id', '=', $credentials['id'])
165 ->with(['user'])->first();
167 if ($token === null) {
171 return Hash::check($credentials['secret'], $token->secret);
175 * "Log out" the currently authenticated user.
177 public function logout()