Completed
Push — 4-cactus ( 5dfe85...ece855 )
by Paolo
18s queued 12s
created

OTPAuthenticate::authenticate()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 9
nc 4
nop 2
dl 0
loc 15
rs 9.6111
c 0
b 0
f 0
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\Time;
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' => 'all',
64
        'contain' => null,
65
        'passwordHasher' => 'Default',
66
        'expiry' => '+15 minutes',
67
        'generator' => null,
68
    ];
69
70
    /**
71
     * {@inheritDoc}
72
     */
73
    public function __construct(ComponentRegistry $registry, array $config = [])
74
    {
75
        parent::__construct($registry, $config);
76
        // override configuration with `otp` auth provider params ('generator', 'expiry'...)
77
        $authProvider = $this->getConfig('authProviders.otp');
78
        if ($authProvider && $authProvider instanceof AuthProvider) {
79
            $this->setConfig((array)$authProvider->get('params'), true);
80
        }
81
    }
82
83
    /**
84
     * {@inheritDoc}
85
     */
86
    public function authenticate(ServerRequest $request, Response $response)
87
    {
88
        $username = $request->getData('username');
89
        if (empty($username) || !is_string($username)) {
90
            return false;
91
        }
92
93
        $grant = $request->getData('grant_type');
94
        if ($grant === 'otp') {
95
            return $this->otpAccess($username, $request);
96
        } elseif ($grant === 'otp_request') {
97
            return $this->otpRequest($username);
98
        }
99
100
        return false;
101
    }
102
103
    /**
104
     * Retrieve access grant using authorization code and secret token.
105
     *
106
     * @param string $username User name
107
     * @param ServerRequest $request Request object
108
     * @return array|bool User data array on success, false on failure
109
     */
110
    protected function otpAccess($username, ServerRequest $request)
111
    {
112
        if (empty($request->getData('authorization_code')) || empty($request->getData('token'))) {
113
            return false;
114
        }
115
116
        $result = $this->_findUser($username);
117
        if (empty($result)) {
118
            return false;
119
        }
120
121
        $data = [
122
            'user_id' => $result['id'],
123
            'application_id' => CurrentApplication::getApplicationId(),
124
            'client_token' => $request->getData('authorization_code'),
125
            'secret_token' => $request->getData('token'),
126
            'token_type' => 'otp',
127
        ];
128
129
        $UserTokens = TableRegistry::get('UserTokens');
130
        $userToken = $UserTokens->find('valid')->where($data)->first();
131
        if (!empty($userToken)) {
132
            $UserTokens->deleteOrFail($userToken);
0 ignored issues
show
Bug introduced by
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

132
            $UserTokens->deleteOrFail(/** @scrutinizer ignore-type */ $userToken);
Loading history...
133
134
            return $result;
135
        }
136
137
        return false;
138
    }
139
140
    /**
141
     * Generate a new client and secret token upon `otp_request`
142
     *
143
     * @param string $username User name
144
     * @return array|bool Authorization code array on success, false on failure
145
     */
146
    protected function otpRequest($username)
147
    {
148
        $result = $this->_findUser($username);
149
        if (empty($result)) {
150
            return false;
151
        }
152
153
        $data = [
154
            'user_id' => $result['id'],
155
            'application_id' => CurrentApplication::getApplicationId(),
156
            'client_token' => $this->generateClientToken(),
157
            'secret_token' => $this->generateSecretToken(),
158
            'token_type' => 'otp',
159
            'expires' => new Time($this->getConfig('expiry')),
160
        ];
161
162
        $UserTokens = TableRegistry::get('UserTokens');
163
        $entity = $UserTokens->newEntity($data);
164
        $UserTokens->saveOrFail($entity);
165
        $this->dispatchEvent('Auth.userToken', [$entity]);
166
167
        return ['authorization_code' => $data['client_token']];
168
    }
169
170
    /**
171
     * Generate authorization code, aka client token.
172
     *
173
     * @return string The generated token
174
     * @codeCoverageIgnore
175
     */
176
    public function generateClientToken()
177
    {
178
        return Text::uuid();
179
    }
180
181
    /**
182
     * Generate secret token, to be sent separately in a secure way to user
183
     *
184
     * @return string The generated secure token
185
     */
186
    public function generateSecretToken()
187
    {
188
        $generator = $this->getConfig('generator');
189
        if (!empty($generator) && is_callable($generator)) {
190
            return call_user_func($generator);
191
        }
192
193
        return $this->defaultSecretGenerator();
194
    }
195
196
    /**
197
     * Super-simple default secret generator: string of 6 random digits
198
     *
199
     * @return string The generated secure token
200
     */
201
    public static function defaultSecretGenerator()
202
    {
203
        return sprintf('%06d', hexdec(bin2hex(Security::randomBytes(2))));
204
    }
205
}
206