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