Cookie::setConsentCookie()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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