Passed
Push — master ( 452cd3...08361e )
by Tim
02:33
created

Cookie::deleteConsent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 2
b 0
f 0
nc 1
nop 2
dl 0
loc 4
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\consent\Consent\Store;
6
7
use Exception;
8
use SimpleSAML\Configuration;
9
use SimpleSAML\Error;
10
use SimpleSAML\Logger;
11
use SimpleSAML\Utils;
12
13
/**
14
 * Cookie storage for consent
15
 *
16
 * This class implements a consent store which stores the consent information in cookies on the users computer.
17
 *
18
 * Example - Consent module with cookie store:
19
 *
20
 * <code>
21
 * 'authproc' => array(
22
 *   array(
23
 *     'consent:Consent',
24
 *     'store' => 'consent:Cookie',
25
 *     ),
26
 *   ),
27
 * </code>
28
 *
29
 * @package SimpleSAMLphp
30
 */
31
32
class Cookie extends \SimpleSAML\Module\consent\Store
33
{
34
    /**
35
     * @var string Cookie name prefix
36
     */
37
    private $name;
38
39
    /**
40
     * @var int Cookie lifetime
41
     */
42
    private $lifetime;
43
44
    /**
45
     * @var string Cookie path
46
     */
47
    private $path;
48
49
    /**
50
     * @var string Cookie domain
51
     */
52
    private $domain = '';
53
54
    /**
55
     * @var bool Cookie secure flag
56
     */
57
    private $secure;
58
59
    /**
60
     * @var string|null Cookie samesite flag
61
     */
62
    private $samesite = null;
63
64
    /**
65
     * Parse configuration.
66
     *
67
     * This constructor parses the configuration.
68
     *
69
     * @param array $config Configuration for database consent store.
70
     *
71
     * @throws \Exception in case of a configuration error.
72
     */
73
    public function __construct(array $config)
74
    {
75
        parent::__construct($config);
76
77
        if (array_key_exists('name', $config)) {
78
            $this->name = $config['name'];
79
        } else {
80
            $this->name = '\SimpleSAML\Module\consent';
81
        }
82
83
        if (array_key_exists('lifetime', $config)) {
84
            $this->lifetime = (int) $config['lifetime'];
85
        } else {
86
            $this->lifetime = 7776000; // (90*24*60*60)
87
        }
88
89
        if (array_key_exists('path', $config)) {
90
            $this->path = $config['path'];
91
        } else {
92
            $globalConfig = Configuration::getInstance();
93
            $this->path = $globalConfig->getBasePath();
94
        }
95
96
        if (array_key_exists('domain', $config)) {
97
            $this->domain = $config['domain'];
98
        }
99
100
        if (array_key_exists('secure', $config)) {
101
            $this->secure = (bool) $config['secure'];
102
        } else {
103
            $this->secure = Utils\HTTP::isHTTPS();
0 ignored issues
show
Bug Best Practice introduced by
The method SimpleSAML\Utils\HTTP::isHTTPS() is not static, but was called statically. ( Ignorable by Annotation )

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

103
            /** @scrutinizer ignore-call */ 
104
            $this->secure = Utils\HTTP::isHTTPS();
Loading history...
104
        }
105
106
        if (array_key_exists('samesite', $config)) {
107
            $this->samesite = $config['samesite'];
108
        }
109
    }
110
111
    /**
112
     * Check for consent.
113
     *
114
     * This function checks whether a given user has authorized the release of the attributes identified by
115
     * $attributeSet from $source to $destination.
116
     *
117
     * @param string $userId        The hash identifying the user at an IdP.
118
     * @param string $destinationId A string which identifies the destination.
119
     * @param string $attributeSet  A hash which identifies the attributes.
120
     *
121
     * @return bool True if the user has given consent earlier, false if not (or on error).
122
     */
123
    public function hasConsent(string $userId, string $destinationId, string $attributeSet): bool
124
    {
125
        $cookieName = $this->getCookieName($userId, $destinationId);
126
127
        $data = $userId . ':' . $attributeSet . ':' . $destinationId;
128
129
        Logger::debug('Consent cookie - Get [' . $data . ']');
130
131
        if (!array_key_exists($cookieName, $_COOKIE)) {
132
            Logger::debug(
133
                'Consent cookie - no cookie with name \'' . $cookieName . '\'.'
134
            );
135
            return false;
136
        }
137
        if (!is_string($_COOKIE[$cookieName])) {
138
            Logger::warning(
139
                'Value of consent cookie wasn\'t a string. Was: ' .
140
                var_export($_COOKIE[$cookieName], true)
141
            );
142
            return false;
143
        }
144
145
        $data = self::sign($data);
146
147
        if ($_COOKIE[$cookieName] !== $data) {
148
            Logger::info(
149
                'Attribute set changed from the last time consent was given.'
150
            );
151
            return false;
152
        }
153
154
        Logger::debug(
155
            'Consent cookie - found cookie with correct name and value.'
156
        );
157
158
        return true;
159
    }
160
161
162
    /**
163
     * Save consent.
164
     *
165
     * Called when the user asks for the consent to be saved. If consent information for the given user and destination
166
     * already exists, it should be overwritten.
167
     *
168
     * @param string $userId        The hash identifying the user at an IdP.
169
     * @param string $destinationId A string which identifies the destination.
170
     * @param string $attributeSet  A hash which identifies the attributes.
171
     *
172
     * @return bool
173
     */
174
    public function saveConsent(string $userId, string $destinationId, string $attributeSet): bool
175
    {
176
        $name = $this->getCookieName($userId, $destinationId);
177
        $value = $userId . ':' . $attributeSet . ':' . $destinationId;
178
179
        Logger::debug('Consent cookie - Set [' . $value . ']');
180
181
        $value = self::sign($value);
182
        return $this->setConsentCookie($name, $value);
183
    }
184
185
186
    /**
187
     * Delete consent.
188
     *
189
     * Called when a user revokes consent for a given destination.
190
     *
191
     * @param string $userId        The hash identifying the user at an IdP.
192
     * @param string $destinationId A string which identifies the destination.
193
     *
194
     */
195
    public function deleteConsent(string $userId, string $destinationId): void
196
    {
197
        $name = $this->getCookieName($userId, $destinationId);
198
        $this->setConsentCookie($name, null);
199
    }
200
201
202
    /**
203
     * Delete consent.
204
     *
205
     * @param string $userId The hash identifying the user at an IdP.
206
     *
207
     *
208
     * @throws \Exception This method always throws an exception indicating that it is not possible to delete all given
209
     * consents with this handler.
210
     */
211
    public function deleteAllConsents(string $userId): void
212
    {
213
        throw new Exception(
214
            'The cookie consent handler does not support delete of all consents...'
215
        );
216
    }
217
218
219
    /**
220
     * Retrieve consents.
221
     *
222
     * This function should return a list of consents the user has saved.
223
     *
224
     * @param string $userId The hash identifying the user at an IdP.
225
     *
226
     * @return array Array of all destination ids the user has given consent for.
227
     */
228
    public function getConsents(string $userId): array
229
    {
230
        $ret = [];
231
232
        $cookieNameStart = $this->name . ':';
233
        $cookieNameStartLen = strlen($cookieNameStart);
234
        foreach ($_COOKIE as $name => $value) {
235
            if (substr($name, 0, $cookieNameStartLen) !== $cookieNameStart) {
236
                continue;
237
            }
238
239
            $value = self::verify($value);
240
            if ($value === false) {
241
                continue;
242
            }
243
244
            $tmp = explode(':', $value, 3);
245
            if (count($tmp) !== 3) {
246
                Logger::warning(
247
                    'Consent cookie with invalid value: ' . $value
248
                );
249
                continue;
250
            }
251
252
            if ($userId !== $tmp[0]) {
253
                // Wrong user
254
                continue;
255
            }
256
257
            $destination = $tmp[2];
258
            $ret[] = $destination;
259
        }
260
261
        return $ret;
262
    }
263
264
265
    /**
266
     * Calculate a signature of some data.
267
     *
268
     * This function calculates a signature of the data.
269
     *
270
     * @param string $data The data which should be signed.
271
     *
272
     * @return string The signed data.
273
     */
274
    private static function sign(string $data): string
275
    {
276
        $configUtils = new Utils\Config();
277
        $secretSalt = $configUtils->getSecretSalt();
278
279
        return sha1($secretSalt . $data . $secretSalt) . ':' . $data;
280
    }
281
282
283
    /**
284
     * Verify signed data.
285
     *
286
     * This function verifies signed data.
287
     *
288
     * @param string $signedData The data which is signed.
289
     *
290
     * @return string|false The data, or false if the signature is invalid.
291
     */
292
    private static function verify(string $signedData)
293
    {
294
        $data = explode(':', $signedData, 2);
295
        if (count($data) !== 2) {
296
            Logger::warning('Consent cookie: Missing signature.');
297
            return false;
298
        }
299
        $data = $data[1];
300
301
        $newSignedData = self::sign($data);
302
        if ($newSignedData !== $signedData) {
303
            Logger::warning('Consent cookie: Invalid signature.');
304
            return false;
305
        }
306
307
        return $data;
308
    }
309
310
311
    /**
312
     * Get cookie name.
313
     *
314
     * This function gets the cookie name for the given user & destination.
315
     *
316
     * @param string $userId        The hash identifying the user at an IdP.
317
     * @param string $destinationId A string which identifies the destination.
318
     *
319
     * @return string The cookie name
320
     */
321
    private function getCookieName(string $userId, string $destinationId): string
322
    {
323
        return $this->name . ':' . sha1($userId . ':' . $destinationId);
324
    }
325
326
327
    /**
328
     * Helper function for setting a cookie.
329
     *
330
     * @param string      $name  Name of the cookie.
331
     * @param string|null $value Value of the cookie. Set this to null to delete the cookie.
332
     *
333
     * @return bool
334
     */
335
    private function setConsentCookie(string $name, ?string $value): bool
336
    {
337
        $globalConfig = Configuration::getInstance();
0 ignored issues
show
Unused Code introduced by
The assignment to $globalConfig is dead and can be removed.
Loading history...
338
        $httpUtils = new Utils\HTTP();
339
340
        $params = [
341
            'lifetime' => $this->lifetime,
342
            'path' => $this->path,
343
            'domain' => $this->domain,
344
            'httponly' => true,
345
            'secure' => $httpUtils->isHTTPS(),
346
            'samesite' => $this->samesite,
347
        ];
348
349
        try {
350
            $httpUtils->setCookie($name, $value, $params, false);
351
            return true;
352
        } catch (Error\CannotSetCookie $e) {
353
            return false;
354
        }
355
    }
356
}
357