Issues (219)

Branch: 4-cactus

plugins/BEdita/API/src/Auth/OTPAuthenticate.php (1 issue)

Labels
Severity
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
It seems like $userToken can also be of type array; however, parameter $entity of Cake\ORM\Table::deleteOrFail() does only seem to accept Cake\Datasource\EntityInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

131
            $UserTokens->deleteOrFail(/** @scrutinizer ignore-type */ $userToken);
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