Issues (141)

src/Service/PortalCookie.php (2 issues)

Labels
Severity
1
<?php
2
3
namespace CILogon\Service;
4
5
use CILogon\Service\Util;
6
use CILogon\Service\Content;
7
8
/**
9
 * PortalCookie
10
 *
11
 * This class is used by the 'CILogon Delegate Service'
12
 * and the CILogon OIDC 'authorize' endpoint to keep track of
13
 * user-selected  attributes such as lifetime (in hours) of the
14
 * delegated certificate and if the user clicked the 'Always Allow'
15
 * button to remember the allowed delegation upon future accesses.
16
 * The information related to certificate 'lifetime' and 'remember' the
17
 * delegation settings is stored in a single cookie.  Since the data
18
 * is actually a two dimensional array (first element is the name of
19
 * the portal, the second element is an array of the various
20
 * attributes), the stored cookie is actually a base64-encoded
21
 * serialization of the 2D array.  This class provides methods to
22
 * read/write the cookie, and to get/set the values for a given portal.
23
 *
24
 * Example usage:
25
 *    require_once 'PortalCookie.php';
26
 *    $pc = new PortalCookie();  // Automatically reads the cookie
27
 *    // Assume the callbackuri or redirect_uri for the portal has
28
 *    // been set in the PHP session.
29
 *    $lifetime = $pc->get('lifetime');
30
 *    if ($lifetime < 1) {
31
 *        $lifetime = 1;
32
 *    } elseif ($lifetime > 240) {
33
 *        $lifetime = 240;
34
 *    }
35
 *    $pc->set('remember',1);
36
 *    $pc->write();  // Must be done before any HTML output
37
 */
38
class PortalCookie
39
{
40
    /**
41
     * @var string COOKIENAME The token name is const to be accessible from
42
     *      removeTheCookie.
43
     */
44
    public const COOKIENAME = "portalparams";
45
46
    /**
47
     * @var array $portalarray An array of arrays. First index is portal name.
48
     */
49
    public $portalarray = array();
50
51
    /**
52
     * __construct
53
     *
54
     * Default constructor.  This reads the current portal cookie into
55
     * the class $portalarray arary.
56
     */
57
    public function __construct()
58
    {
59
        $this->read();
60
    }
61
62
    /**
63
     * read
64
     *
65
     * This method reads the portal cookie, decodes the base64 string,
66
     * decrypts the AES-128-CBC string, and unserializes the 2D array.
67
     * This is stored in the class $portalarray array.
68
     */
69
    public function read()
70
    {
71
        if (isset($_COOKIE[static::COOKIENAME])) {
72
            $cookievar = $_COOKIE[static::COOKIENAME];
73
            $serial = $cookievar;
74
75
            // Attempt to un-base64 and decrypt portal array from cookie
76
            if (defined('OPENSSL_KEY') && (!empty(OPENSSL_KEY))) {
0 ignored issues
show
The constant CILogon\Service\OPENSSL_KEY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
77
                $b64 = base64_decode($cookievar);
78
                if ($b64 !== false) {
79
                    $iv = substr($b64, 0, 16); // IV prepended to encrypted data
80
                    $b64a = substr($b64, 16);  // IV is 16 bytes, rest is data
81
                    if ((strlen($iv) > 0) && (strlen($b64a) > 0)) {
82
                        $serial = openssl_decrypt(
83
                            $b64a,
84
                            'AES-128-CBC',
85
                            OPENSSL_KEY,
86
                            OPENSSL_RAW_DATA,
87
                            $iv
88
                        );
89
                    }
90
                }
91
            }
92
93
            // Unserialize the cookie data back into the portalarray
94
            if (strlen($serial) > 0) {
95
                $unserial = unserialize($serial);
96
                if ($unserial !== false) {
97
                    $this->portalarray = $unserial;
98
                }
99
            }
100
        }
101
    }
102
103
    /**
104
     * write
105
     *
106
     * This method writes the class $portalarray to a cookie.  In
107
     * order to store the 2D array as a cookie, the array is first
108
     * serialized, then encrypted with AES-128-CBC, and then base64-
109
     * encoded.
110
     */
111
    public function write()
112
    {
113
        if (!empty($this->portalarray)) {
114
            $this->set('ut', time()); // Save update time
115
            $serial = serialize($this->portalarray);
116
            // Special check: If the serialization of the cookie is
117
            // more than 2500 bytes, the resulting base64-encoded string
118
            // may be too big (>4K). So scan through all portal entries
119
            // and delete the oldest one until the size is small enough.
120
            while (strlen($serial) > 2500) {
121
                $smallvalue = 5000000000; // Unix time = Jun 11, 2128
122
                $smallportal = '';
123
                foreach ($this->portalarray as $k => $v) {
124
                    if (isset($v['ut'])) {
125
                        if ($v['ut'] < $smallvalue) {
126
                            $smallvalue = $v['ut'];
127
                            $smallportal = $k;
128
                        }
129
                    } else { // 'ut' not set, delete it
130
                        $smallportal = $k;
131
                        break;
132
                    }
133
                }
134
                if (strlen($smallportal) > 0) {
135
                    unset($this->portalarray[$smallportal]);
136
                } else {
137
                    break; // Should never get here, but just in case
138
                }
139
                $serial = serialize($this->portalarray);
140
            }
141
            $cookievar = $serial;
142
143
            // Attempt to encrypt and base64 the serialized portal array
144
            if (defined('OPENSSL_KEY') && (!empty(OPENSSL_KEY))) {
0 ignored issues
show
The constant CILogon\Service\OPENSSL_KEY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
145
                $iv = openssl_random_pseudo_bytes(16);  // IV is 16 bytes
146
                if (strlen($iv) > 0) {
147
                    $data = openssl_encrypt(
148
                        $cookievar,
149
                        'AES-128-CBC',
150
                        OPENSSL_KEY,
151
                        OPENSSL_RAW_DATA,
152
                        $iv
153
                    );
154
                    if (strlen($data) > 0) {
155
                        $b64 = base64_encode($iv . $data); // Prepend IV to data
156
                        if ($b64 !== false) {
157
                            $cookievar = $b64;
158
                        }
159
                    }
160
                }
161
            }
162
            Util::setCookieVar(static::COOKIENAME, $cookievar);
163
        }
164
    }
165
166
    /**
167
     * getPortalName
168
     *
169
     * This method looks in the PHP session for one of 'callbackuri'
170
     * (in the OAuth 1.0a 'delegate' case) or various $clientparams
171
     * (in the OIDC 'authorize' case).This is used as the key for the
172
     * $portalarray. If neither of these session variables is set,
173
     * return empty string.
174
     *
175
     * @return string The name of the portal, which is either the
176
     *         OAuth 1.0a 'callbackuri' or the OIDC client info.
177
     */
178
    public function getPortalName()
179
    {
180
        // Check the OAuth 1.0a 'delegate' 'callbackuri'
181
        $retval = Util::getSessionVar('callbackuri');
182
        if (strlen($retval) == 0) {
183
            // Next, check the OAuth 2.0 'authorize' $clientparams[]
184
            $clientparams = json_decode(
185
                Util::getSessionVar('clientparams'),
186
                true
187
            );
188
            if (
189
                (isset($clientparams['client_id'])) &&
190
                (isset($clientparams['redirect_uri'])) &&
191
                (isset($clientparams['scope']))
192
            ) {
193
                // Use the first element of the idphint list as the selected_idp.
194
                $selected_idp = '';
195
                $idphintlist = Content::getIdphintList();
196
                if (!empty($idphintlist)) {
197
                    $selected_idp = $idphintlist[0];
198
                }
199
                $retval = $clientparams['client_id'] . ';' .
200
                          $clientparams['redirect_uri'] . ';' .
201
                          $clientparams['scope'] .
202
                          (empty($selected_idp) ? '' : ';' . $selected_idp);
203
            }
204
        }
205
        return $retval;
206
    }
207
208
    /**
209
     * removeTheCookie
210
     *
211
     * This method unsets the portal cookie in the user's browser.
212
     * This should be called before any HTML is output.
213
     */
214
    public static function removeTheCookie()
215
    {
216
        Util::unsetCookieVar(static::COOKIENAME);
217
    }
218
219
    /**
220
     * get
221
     *
222
     * This method is a generalized getter to fetch the value of a
223
     * parameter for a given portal.  In other words, this method
224
     * returns $this->portalarray[$param], where $param is something
225
     * like 'lifetime' or 'remember'. If the portal name is not set,
226
     * or the requested parameter is missing from the cookie, return
227
     * empty string.
228
     *
229
     * @param string $param The attribute of the portal to get.  Should be
230
     *        something like 'lifetime' or 'remember'.
231
     * @return string The value of the $param for the portal.
232
     */
233
    public function get($param)
234
    {
235
        $retval = '';
236
        $name = $this->getPortalName();
237
        if (strlen($name) > 0) {
238
            if (
239
                (isset($this->portalarray[$name])) &&
240
                (isset($this->portalarray[$name][$param]))
241
            ) {
242
                $retval = $this->portalarray[$name][$param];
243
            } elseif ($param == 'providerId') {
244
                // CIL-719 If there is no portal cookie set for this
245
                // particular 'portal name', then attempt to read the
246
                // 'providerId' value from the most recent portal cookie.
247
                $pa = $this->portalarray; // Make a copy of the portalarary
248
                // Ascending sort the array by 'ut'
249
                uasort($pa, function ($a, $b) {
250
                    return ($a['ut'] < $b['ut']) ? -1 : (($a['ut'] > $b['ut']) ? 1 : 0);
251
                });
252
                // Get the last (most recent) element of the array
253
                $name = @array_key_last($pa);
254
                if (
255
                    (strlen($name) > 0) &&
256
                    (isset($pa[$name])) &&
257
                    (isset($pa[$name][$param]))
258
                ) {
259
                    $retval = $pa[$name][$param];
260
                }
261
            }
262
        }
263
        return $retval;
264
    }
265
266
    /**
267
     * set
268
     *
269
     * This method sets a portal's parameter to a given value.  Note
270
     * that $value should be an integer or character value.
271
     *
272
     * @param string $param The parameter of the portal to set.  Should be
273
     *        something like 'lifetime' or 'remember'.
274
     * @param string $value The value to set for the parameter.
275
     */
276
    public function set($param, $value)
277
    {
278
        $name = $this->getPortalName();
279
        if (strlen($name) > 0) {
280
            $this->portalarray[$name][$param] = $value;
281
        }
282
    }
283
284
    /**
285
     * __toString
286
     *
287
     * This function returns a string representation of the object.
288
     * The format is 'portal=...,lifetime=...,remember=...'. Multiple
289
     * portals are separated by a newline character.
290
     *
291
     * @return string A 'pretty print' representation of the class
292
     *         portalarray.
293
     */
294
    public function __toString()
295
    {
296
        $retval = '';
297
        $first = true;
298
        foreach ($this->portalarray as $key => $value) {
299
            if (!$first) {
300
                $retval .= "\n";
301
            }
302
            $first = false;
303
            $retval .= 'portal=' . $key;
304
            ksort($value);
305
            foreach ($value as $key2 => $value2) {
306
                $retval .= ", $key2=$value2";
307
            }
308
        }
309
        return $retval;
310
    }
311
}
312