Completed
Push — master ( a1987d...ef66da )
by Terrence
21:32 queued 06:26
created

PortalCookie::getPortalName()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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