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