]> BookStack Code Mirror - bookstack/blob - app/Api/ApiTokenGuard.php
Added testing for our request method overrides
[bookstack] / app / Api / ApiTokenGuard.php
1 <?php
2
3 namespace BookStack\Api;
4
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;
13
14 class ApiTokenGuard implements Guard
15 {
16     use GuardHelpers;
17
18     /**
19      * The request instance.
20      */
21     protected $request;
22
23     /**
24      * @var LoginService
25      */
26     protected $loginService;
27
28     /**
29      * The last auth exception thrown in this request.
30      *
31      * @var ApiAuthException
32      */
33     protected $lastAuthException;
34
35     /**
36      * ApiTokenGuard constructor.
37      */
38     public function __construct(Request $request, LoginService $loginService)
39     {
40         $this->request = $request;
41         $this->loginService = $loginService;
42     }
43
44     /**
45      * {@inheritdoc}
46      */
47     public function user()
48     {
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)) {
52             return $this->user;
53         }
54
55         $user = null;
56
57         try {
58             $user = $this->getAuthorisedUserFromRequest();
59         } catch (ApiAuthException $exception) {
60             $this->lastAuthException = $exception;
61         }
62
63         $this->user = $user;
64
65         return $user;
66     }
67
68     /**
69      * Determine if current user is authenticated. If not, throw an exception.
70      *
71      * @throws ApiAuthException
72      *
73      * @return \Illuminate\Contracts\Auth\Authenticatable
74      */
75     public function authenticate()
76     {
77         if (!is_null($user = $this->user())) {
78             return $user;
79         }
80
81         if ($this->lastAuthException) {
82             throw $this->lastAuthException;
83         }
84
85         throw new ApiAuthException('Unauthorized');
86     }
87
88     /**
89      * Check the API token in the request and fetch a valid authorised user.
90      *
91      * @throws ApiAuthException
92      */
93     protected function getAuthorisedUserFromRequest(): Authenticatable
94     {
95         $authToken = trim($this->request->headers->get('Authorization', ''));
96         $this->validateTokenHeaderValue($authToken);
97
98         [$id, $secret] = explode(':', str_replace('Token ', '', $authToken));
99         $token = ApiToken::query()
100             ->where('token_id', '=', $id)
101             ->with(['user'])->first();
102
103         $this->validateToken($token, $secret);
104
105         if ($this->loginService->awaitingEmailConfirmation($token->user)) {
106             throw new ApiAuthException(trans('errors.email_confirmation_awaiting'));
107         }
108
109         return $token->user;
110     }
111
112     /**
113      * Validate the format of the token header value string.
114      *
115      * @throws ApiAuthException
116      */
117     protected function validateTokenHeaderValue(string $authToken): void
118     {
119         if (empty($authToken)) {
120             throw new ApiAuthException(trans('errors.api_no_authorization_found'));
121         }
122
123         if (strpos($authToken, ':') === false || strpos($authToken, 'Token ') !== 0) {
124             throw new ApiAuthException(trans('errors.api_bad_authorization_format'));
125         }
126     }
127
128     /**
129      * Validate the given secret against the given token and ensure the token
130      * currently has access to the instance API.
131      *
132      * @throws ApiAuthException
133      */
134     protected function validateToken(?ApiToken $token, string $secret): void
135     {
136         if ($token === null) {
137             throw new ApiAuthException(trans('errors.api_user_token_not_found'));
138         }
139
140         if (!Hash::check($secret, $token->secret)) {
141             throw new ApiAuthException(trans('errors.api_incorrect_token_secret'));
142         }
143
144         $now = Carbon::now();
145         if ($token->expires_at <= $now) {
146             throw new ApiAuthException(trans('errors.api_user_token_expired'), 403);
147         }
148
149         if (!$token->user->can('access-api')) {
150             throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);
151         }
152     }
153
154     /**
155      * {@inheritdoc}
156      */
157     public function validate(array $credentials = [])
158     {
159         if (empty($credentials['id']) || empty($credentials['secret'])) {
160             return false;
161         }
162
163         $token = ApiToken::query()
164             ->where('token_id', '=', $credentials['id'])
165             ->with(['user'])->first();
166
167         if ($token === null) {
168             return false;
169         }
170
171         return Hash::check($credentials['secret'], $token->secret);
172     }
173
174     /**
175      * "Log out" the currently authenticated user.
176      */
177     public function logout()
178     {
179         $this->user = null;
180     }
181 }
Morty Proxy This is a proxified and sanitized view of the page, visit original site.