1 | <?php |
||
2 | /** |
||
3 | * BEdita, API-first content management framework |
||
4 | * Copyright 2018 ChannelWeb Srl, Chialab Srl |
||
5 | * |
||
6 | * This file is part of BEdita: you can redistribute it and/or modify |
||
7 | * it under the terms of the GNU Lesser General Public License as published |
||
8 | * by the Free Software Foundation, either version 3 of the License, or |
||
9 | * (at your option) any later version. |
||
10 | * |
||
11 | * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details. |
||
12 | */ |
||
13 | |||
14 | namespace BEdita\API\Auth; |
||
15 | |||
16 | use BEdita\Core\Model\Entity\AuthProvider; |
||
17 | use BEdita\Core\State\CurrentApplication; |
||
18 | use Cake\Auth\BaseAuthenticate; |
||
19 | use Cake\Controller\ComponentRegistry; |
||
20 | use Cake\Event\EventDispatcherTrait; |
||
21 | use Cake\Http\Response; |
||
22 | use Cake\Http\ServerRequest; |
||
23 | use Cake\I18n\FrozenTime; |
||
24 | use Cake\ORM\TableRegistry; |
||
25 | use Cake\Utility\Security; |
||
26 | use Cake\Utility\Text; |
||
27 | |||
28 | /** |
||
29 | * Authenticate users via One Time Password. |
||
30 | * |
||
31 | * @since 4.0.0 |
||
32 | */ |
||
33 | class OTPAuthenticate extends BaseAuthenticate |
||
34 | { |
||
35 | use EventDispatcherTrait; |
||
36 | |||
37 | /** |
||
38 | * Default config for this object. |
||
39 | * |
||
40 | * - `authProviders` The AuthProviders entities associated to this authentication component. |
||
41 | * Array formatted with `auth_providers.name` as key, from `AuthProvidersTable::findAuthenticate()` |
||
42 | * - `fields` The fields to use to identify a user by. |
||
43 | * - `userModel` The alias for users table, defaults to Users. |
||
44 | * - `finder` The finder method to use to fetch user record. Defaults to 'all'. |
||
45 | * You can set finder name as string or an array where key is finder name and value |
||
46 | * is an array passed to `Table::find()` options. |
||
47 | * E.g. ['finderName' => ['some_finder_option' => 'some_value']] |
||
48 | * - `passwordHasher` Password hasher class. Can be a string specifying class name |
||
49 | * or an array containing `className` key, any other keys will be passed as |
||
50 | * config to the class. Defaults to 'Default'. |
||
51 | * - 'expiry' Expiry time of a user token, expressed as string expression like `+1 hour`, `+10 minutes` |
||
52 | * - 'generator' Secret token generator, if a valid callable is used instead of default one. |
||
53 | * |
||
54 | * @var array |
||
55 | */ |
||
56 | protected $_defaultConfig = [ |
||
57 | 'authProviders' => [], |
||
58 | 'fields' => [ |
||
59 | 'username' => 'username', |
||
60 | 'password' => null, |
||
61 | ], |
||
62 | 'userModel' => 'Users', |
||
63 | 'finder' => 'loginRoles', |
||
64 | 'passwordHasher' => 'Default', |
||
65 | 'expiry' => '+15 minutes', |
||
66 | 'generator' => null, |
||
67 | ]; |
||
68 | |||
69 | /** |
||
70 | * @inheritDoc |
||
71 | */ |
||
72 | public function __construct(ComponentRegistry $registry, array $config = []) |
||
73 | { |
||
74 | parent::__construct($registry, $config); |
||
75 | // override configuration with `otp` auth provider params ('generator', 'expiry'...) |
||
76 | $authProvider = $this->getConfig('authProviders.otp'); |
||
77 | if ($authProvider && $authProvider instanceof AuthProvider) { |
||
78 | $this->setConfig((array)$authProvider->get('params'), true); |
||
79 | } |
||
80 | } |
||
81 | |||
82 | /** |
||
83 | * @inheritDoc |
||
84 | */ |
||
85 | public function authenticate(ServerRequest $request, Response $response) |
||
86 | { |
||
87 | $username = $request->getData('username'); |
||
88 | if (empty($username) || !is_string($username)) { |
||
89 | return false; |
||
90 | } |
||
91 | |||
92 | $grant = $request->getData('grant_type'); |
||
93 | if ($grant === 'otp') { |
||
94 | return $this->otpAccess($username, $request); |
||
95 | } elseif ($grant === 'otp_request') { |
||
96 | return $this->otpRequest($username); |
||
97 | } |
||
98 | |||
99 | return false; |
||
100 | } |
||
101 | |||
102 | /** |
||
103 | * Retrieve access grant using authorization code and secret token. |
||
104 | * |
||
105 | * @param string $username User name |
||
106 | * @param \Cake\Http\ServerRequest $request Request object |
||
107 | * @return array|bool User data array on success, false on failure |
||
108 | */ |
||
109 | protected function otpAccess($username, ServerRequest $request) |
||
110 | { |
||
111 | if (empty($request->getData('authorization_code')) || empty($request->getData('token'))) { |
||
112 | return false; |
||
113 | } |
||
114 | |||
115 | $result = $this->_findUser($username); |
||
116 | if (empty($result)) { |
||
117 | return false; |
||
118 | } |
||
119 | |||
120 | $data = [ |
||
121 | 'user_id' => $result['id'], |
||
122 | 'application_id' => CurrentApplication::getApplicationId(), |
||
123 | 'client_token' => $request->getData('authorization_code'), |
||
124 | 'secret_token' => $request->getData('token'), |
||
125 | 'token_type' => 'otp', |
||
126 | ]; |
||
127 | |||
128 | $UserTokens = TableRegistry::getTableLocator()->get('UserTokens'); |
||
129 | $userToken = $UserTokens->find('valid')->where($data)->first(); |
||
130 | if (!empty($userToken)) { |
||
131 | $UserTokens->deleteOrFail($userToken); |
||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
132 | |||
133 | return $result; |
||
134 | } |
||
135 | |||
136 | return false; |
||
137 | } |
||
138 | |||
139 | /** |
||
140 | * Generate a new client and secret token upon `otp_request` |
||
141 | * |
||
142 | * @param string $username User name |
||
143 | * @return array|bool Authorization code array on success, false on failure |
||
144 | */ |
||
145 | protected function otpRequest($username) |
||
146 | { |
||
147 | $result = $this->_findUser($username); |
||
148 | if (empty($result)) { |
||
149 | return false; |
||
150 | } |
||
151 | |||
152 | $data = [ |
||
153 | 'user_id' => $result['id'], |
||
154 | 'application_id' => CurrentApplication::getApplicationId(), |
||
155 | 'client_token' => $this->generateClientToken(), |
||
156 | 'secret_token' => $this->generateSecretToken(), |
||
157 | 'token_type' => 'otp', |
||
158 | 'expires' => new FrozenTime($this->getConfig('expiry')), |
||
159 | ]; |
||
160 | |||
161 | $UserTokens = TableRegistry::getTableLocator()->get('UserTokens'); |
||
162 | $entity = $UserTokens->newEntity($data); |
||
163 | $UserTokens->saveOrFail($entity); |
||
164 | $this->dispatchEvent('Auth.userToken', [$entity]); |
||
165 | |||
166 | return ['authorization_code' => $data['client_token']]; |
||
167 | } |
||
168 | |||
169 | /** |
||
170 | * Generate authorization code, aka client token. |
||
171 | * |
||
172 | * @return string The generated token |
||
173 | * @codeCoverageIgnore |
||
174 | */ |
||
175 | public function generateClientToken() |
||
176 | { |
||
177 | return Text::uuid(); |
||
178 | } |
||
179 | |||
180 | /** |
||
181 | * Generate secret token, to be sent separately in a secure way to user |
||
182 | * |
||
183 | * @return string The generated secure token |
||
184 | */ |
||
185 | public function generateSecretToken() |
||
186 | { |
||
187 | $generator = $this->getConfig('generator'); |
||
188 | if (!empty($generator) && is_callable($generator)) { |
||
189 | return call_user_func($generator); |
||
190 | } |
||
191 | |||
192 | return $this->defaultSecretGenerator(); |
||
193 | } |
||
194 | |||
195 | /** |
||
196 | * Super-simple default secret generator: string of 6 random digits |
||
197 | * |
||
198 | * @return string The generated secure token |
||
199 | */ |
||
200 | public static function defaultSecretGenerator() |
||
201 | { |
||
202 | return sprintf('%06d', hexdec(bin2hex(Security::randomBytes(2)))); |
||
203 | } |
||
204 | } |
||
205 |