Cookie::deleteAllConsents()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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