csrf.php 7.11 KB
Newer Older
Alexis Ragot's avatar
Alexis Ragot committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
/*
 * Copyright (C) 2018 Maarch
 *
 * This file is part of bundle Auth.
 *
 * Bundle Auth is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Bundle Auth is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with bundle Auth.  If not, see <http://www.gnu.org/licenses/>.
 */
namespace presentation\maarchRM\Observer;

/**
 * Service for Cross Site Request Forgery protection
 *
 * @package Auth
 * @author Maarch Alexis Ragot <alexis.ragot@maarch.org>
 */
class csrf
{
    protected $sdoFactory;
    protected $config;
    protected $whiteList;
Cyril Vazquez's avatar
Cyril Vazquez committed
33
34

    protected $account;
35
    protected $accountTokens = [];
Cyril Vazquez's avatar
Cyril Vazquez committed
36
37
38
39
40
41
    
    protected $requestToken;
    protected $requestTokenTime;

    protected $responseToken;
    protected $responseTokenTime;
Alexis Ragot's avatar
Alexis Ragot committed
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

    /**
     * Construct the observer
     * @param object $sdoFactory The user model
     */
    public function __construct(\dependency\sdo\Factory $sdoFactory)
    {
        $this->sdoFactory = $sdoFactory;
        $this->config = \laabs::configuration("auth")["csrfConfig"];
        $this->whiteList = \laabs::configuration("auth")["csrfWhiteList"];
    }

    /**
     * Observer for the CSRF protection
     * @param \core\Reflection\Command $userCommand
     * @param array                    $args
     *
     * @subject LAABS_USER_COMMAND
     */
    public function check(&$userCommand, array &$args = null)
    {
Alexis Ragot's avatar
Alexis Ragot committed
63
        if (empty(\laabs::kernel()->request->uri) || in_array(\laabs::kernel()->request->uri, $this->whiteList)) {
Alexis Ragot's avatar
Alexis Ragot committed
64
65
66
            return;
        }

67
        $requestToken = \laabs::getToken("Csrf", LAABS_IN_HEADER);
Cyril Vazquez's avatar
Cyril Vazquez committed
68

69
        // Get account with LOCK
Cyril Vazquez's avatar
Cyril Vazquez committed
70
71
72
        $this->sdoFactory->beginTransaction();
        
        $account = $this->getAccount(true);
73
        if (!$account) {
Cyril Vazquez's avatar
Cyril Vazquez committed
74
75
            $this->sdoFactory->rollback();

76
            return;
77
        }
78
        
79
80
        $accountTokens = $account->authentication->csrf;
        
Alexis Ragot's avatar
Alexis Ragot committed
81
82
83
84
        switch ($userCommand->method) {
            case "create":
            case "update":
            case "delete":
85
86
87
                if (empty($requestToken)) {
                    throw new \core\Exception('Attempt to access without a valid token 1', 412);
                }
Alexis Ragot's avatar
Alexis Ragot committed
88

89
                $requestTokenTime = $this->checkToken($requestToken, $accountTokens);
90
91
                $accountTokens = $this->shiftTokens($requestTokenTime, $accountTokens);
                $accountTokens = $this->addToken($accountTokens);
Alexis Ragot's avatar
Alexis Ragot committed
92
                break;
Cyril Vazquez's avatar
Cyril Vazquez committed
93

Alexis Ragot's avatar
Alexis Ragot committed
94
            default:
95
                if (empty($accountTokens)) {
Alexis Ragot's avatar
Alexis Ragot committed
96
                    $accountTokens = $this->addToken([]);
Cyril Vazquez's avatar
Cyril Vazquez committed
97
                }
Alexis Ragot's avatar
Alexis Ragot committed
98
                break;
99
100
101
        }

        $account->authentication->csrf = $accountTokens;
Alexis Ragot's avatar
Alexis Ragot committed
102

103
        // Set account and COMMIT
Cyril Vazquez's avatar
Cyril Vazquez committed
104
105
106
107
108
109
        try {
            $this->updateAccount($account);
            $this->sdoFactory->commit();
        } catch (\Exception $exception) {
            $this->sdoFactory->rollback();
        }
Alexis Ragot's avatar
Alexis Ragot committed
110

Cyril Vazquez's avatar
Cyril Vazquez committed
111
        return true;
Alexis Ragot's avatar
Alexis Ragot committed
112
113
114
115
116
117
118
119
    }

    /**
     * Observer for the CSRF protection
     * @param \core\Response\HttpResponse
     *
     * @subject LAABS_RESPONSE
     */
120
121
    public function setResponseToken(&$response)
    {
Cyril Vazquez's avatar
Cyril Vazquez committed
122
        $account = $this->getAccount(false);
123
124
125
126
        if (!$account) {
            return;
        }

127
128
129
130
        $accountTokens = $account->authentication->csrf;
        
        $responseToken = $this->getLastToken($accountTokens);
        
131
        \laabs::setToken($this->config["cookieName"], $responseToken, null, true);
132
133
    }

Cyril Vazquez's avatar
Cyril Vazquez committed
134
    /**
135
     * Retrieves the account information with a LOCK on database
Cyril Vazquez's avatar
Cyril Vazquez committed
136
     * @param bool $lock Lock user
137
     *
138
     * @return auth/userAccount
Cyril Vazquez's avatar
Cyril Vazquez committed
139
     */
Cyril Vazquez's avatar
Cyril Vazquez committed
140
    private function getAccount($lock = false)
Cyril Vazquez's avatar
Cyril Vazquez committed
141
142
143
    {
        $accountToken = \laabs::getToken('AUTH');

144
        if (!$accountToken) {
Cyril Vazquez's avatar
Cyril Vazquez committed
145
146
147
148
149
            $accountToken = \laabs::getToken('TEMP-AUTH');

            if (!$accountToken) {
                return false;
            }
150
151
        }

Cyril Vazquez's avatar
Cyril Vazquez committed
152
        $account = $this->sdoFactory->read('auth/account', $accountToken->accountId, $lock);
153
        $account->authentication = json_decode($account->authentication);
Cyril Vazquez's avatar
Cyril Vazquez committed
154

155
156
157
158
159
        if (empty($account->authentication)) {
            $account->authentication = new \stdClass();
            $account->authentication->csrf = [];

            return $account;
Cyril Vazquez's avatar
Cyril Vazquez committed
160
161
        }

162
163
        if (!is_object($account->authentication->csrf)) {
            $account->authentication->csrf = [];
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
            return $account;
        }

        $account->authentication->csrf = get_object_vars($account->authentication->csrf);

        $lifetime = '3600';
        if (isset($this->config['lifetime'])) {
            $lifetime = $this->config['lifetime'];
        }
        $duration = \laabs::newDuration('PT'.$lifetime.'S');
        $now = \laabs::newTimestamp();

        foreach ($account->authentication->csrf as $time => $token) {
            $timestamp = \laabs::newTimestamp($time);
            $expiration = $timestamp->add($duration);

            if ($now->diff($expiration)->invert == 1) {
                unset($account->authentication->csrf[$time]);
            }
183
        }
Cyril Vazquez's avatar
Cyril Vazquez committed
184

185

186
        return $account;
Alexis Ragot's avatar
Alexis Ragot committed
187
188
    }

189
190
191
192
193
194
195
    /**
     * Adds a token to the current array of tokens
     * @param array $accountTokens
     * 
     * @return array
     */
    private function addToken($accountTokens)
Alexis Ragot's avatar
Alexis Ragot committed
196
197
198
    {
        $tokenLength = 32;

Alexis Ragot's avatar
Alexis Ragot committed
199
        if (!empty($this->config["cookieName"])) {
Alexis Ragot's avatar
Alexis Ragot committed
200
            $tokenLength = $this->config["tokenLength"];
Alexis Ragot's avatar
Alexis Ragot committed
201
202
        }

Alexis Ragot's avatar
Alexis Ragot committed
203
        if (function_exists("openssl_random_pseudo_bytes")) {
204
            $token = bin2hex(openssl_random_pseudo_bytes($tokenLength));
Alexis Ragot's avatar
Alexis Ragot committed
205
        } elseif (function_exists("random_bytes")) {
206
            $token = bin2hex(random_bytes($tokenLength));
Alexis Ragot's avatar
Alexis Ragot committed
207
        } else {
208
            $token = \laabs::newId();
Alexis Ragot's avatar
Alexis Ragot committed
209
210
        }

211
        $time = (string) \laabs::newTimestamp();
Alexis Ragot's avatar
Alexis Ragot committed
212

213
        $accountTokens[$time] = $token;
Alexis Ragot's avatar
Alexis Ragot committed
214

215
        return $accountTokens;
Alexis Ragot's avatar
Alexis Ragot committed
216
217
    }

218
    private function getLastToken($accountTokens)
Alexis Ragot's avatar
Alexis Ragot committed
219
    {
220
        ksort($accountTokens);
Cyril Vazquez's avatar
Cyril Vazquez committed
221

222
        return end($accountTokens);
Cyril Vazquez's avatar
Cyril Vazquez committed
223
224
    }

225
    private function checkToken($requestToken, $accountTokens)
Cyril Vazquez's avatar
Cyril Vazquez committed
226
    {
227
        $requestTokenTime = array_search($requestToken, $accountTokens);
Cyril Vazquez's avatar
Cyril Vazquez committed
228

229
230
        if (empty($requestTokenTime)) {
            $requestToken = null;
Cyril Vazquez's avatar
Cyril Vazquez committed
231
232
233
234
235
236

            $e = new \core\Exception('Attempt to access without a valid token', 412);

            throw $e;
        }

237
        return $requestTokenTime;
Alexis Ragot's avatar
Alexis Ragot committed
238
    }
Cyril Vazquez's avatar
Cyril Vazquez committed
239

240
    private function shiftTokens($requestTokenTime, $accountTokens)
Cyril Vazquez's avatar
Cyril Vazquez committed
241
    {
242
        foreach ($accountTokens as $time => $token) {
243
            if ($time <= $requestTokenTime) {
244
                unset($accountTokens[$time]);
Cyril Vazquez's avatar
Cyril Vazquez committed
245
246
            }
        }
247
248

        return $accountTokens;
Cyril Vazquez's avatar
Cyril Vazquez committed
249
250
    }

251
    private function updateAccount($account)
Cyril Vazquez's avatar
Cyril Vazquez committed
252
    {
253
        $account->authentication = json_encode($account->authentication);
Cyril Vazquez's avatar
Cyril Vazquez committed
254

255
        $this->sdoFactory->update($account, "auth/account");
Cyril Vazquez's avatar
Cyril Vazquez committed
256
    }
Alexis Ragot's avatar
Alexis Ragot committed
257
}