3 namespace BookStack\Api;
5 use BookStack\Exceptions\ApiAuthException;
6 use Illuminate\Auth\GuardHelpers;
7 use Illuminate\Contracts\Auth\Authenticatable;
8 use Illuminate\Contracts\Auth\Guard;
9 use Illuminate\Support\Carbon;
10 use Illuminate\Support\Facades\Hash;
11 use Symfony\Component\HttpFoundation\Request;
13 class ApiTokenGuard implements Guard
18 * The request instance.
23 * The last auth exception thrown in this request.
25 * @var ApiAuthException
27 protected $lastAuthException;
30 * ApiTokenGuard constructor.
32 public function __construct(Request $request)
34 $this->request = $request;
40 public function user()
42 // Return the user if we've already retrieved them.
43 // Effectively a request-instance cache for this method.
44 if (!is_null($this->user)) {
51 $user = $this->getAuthorisedUserFromRequest();
52 } catch (ApiAuthException $exception) {
53 $this->lastAuthException = $exception;
62 * Determine if current user is authenticated. If not, throw an exception.
64 * @throws ApiAuthException
66 * @return \Illuminate\Contracts\Auth\Authenticatable
68 public function authenticate()
70 if (!is_null($user = $this->user())) {
74 if ($this->lastAuthException) {
75 throw $this->lastAuthException;
78 throw new ApiAuthException('Unauthorized');
82 * Check the API token in the request and fetch a valid authorised user.
84 * @throws ApiAuthException
86 protected function getAuthorisedUserFromRequest(): Authenticatable
88 $authToken = trim($this->request->headers->get('Authorization', ''));
89 $this->validateTokenHeaderValue($authToken);
91 [$id, $secret] = explode(':', str_replace('Token ', '', $authToken));
92 $token = ApiToken::query()
93 ->where('token_id', '=', $id)
94 ->with(['user'])->first();
96 $this->validateToken($token, $secret);
102 * Validate the format of the token header value string.
104 * @throws ApiAuthException
106 protected function validateTokenHeaderValue(string $authToken): void
108 if (empty($authToken)) {
109 throw new ApiAuthException(trans('errors.api_no_authorization_found'));
112 if (strpos($authToken, ':') === false || strpos($authToken, 'Token ') !== 0) {
113 throw new ApiAuthException(trans('errors.api_bad_authorization_format'));
118 * Validate the given secret against the given token and ensure the token
119 * currently has access to the instance API.
121 * @throws ApiAuthException
123 protected function validateToken(?ApiToken $token, string $secret): void
125 if ($token === null) {
126 throw new ApiAuthException(trans('errors.api_user_token_not_found'));
129 if (!Hash::check($secret, $token->secret)) {
130 throw new ApiAuthException(trans('errors.api_incorrect_token_secret'));
133 $now = Carbon::now();
134 if ($token->expires_at <= $now) {
135 throw new ApiAuthException(trans('errors.api_user_token_expired'), 403);
138 if (!$token->user->can('access-api')) {
139 throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);
146 public function validate(array $credentials = [])
148 if (empty($credentials['id']) || empty($credentials['secret'])) {
152 $token = ApiToken::query()
153 ->where('token_id', '=', $credentials['id'])
154 ->with(['user'])->first();
156 if ($token === null) {
160 return Hash::check($credentials['secret'], $token->secret);
164 * "Log out" the currently authenticated user.
166 public function logout()