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