Subversion Repositories Applications.annuaire

Rev

Rev 536 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
536 mathias 1
<?php
2
 
3
/**
4
 * JSON Web Token implementation, based on this spec:
5
 * http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06
6
 *
7
 * PHP version 5
8
 *
9
 * @category Authentication
10
 * @package  Authentication_JWT
11
 * @author   Neuman Vong <neuman@twilio.com>
12
 * @author   Anant Narayanan <anant@php.net>
13
 * @license  http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
14
 * @link     https://github.com/firebase/php-jwt
15
 */
16
class JWT
17
{
18
    public static $supported_algs = array(
19
        'HS256' => array('hash_hmac', 'SHA256'),
20
        'HS512' => array('hash_hmac', 'SHA512'),
21
        'HS384' => array('hash_hmac', 'SHA384'),
22
        'RS256' => array('openssl', 'SHA256'),
23
    );
24
 
25
    /**
26
     * Decodes a JWT string into a PHP object.
27
     *
28
     * @param string      $jwt           The JWT
29
     * @param string|Array|null $key     The secret key, or map of keys
30
     * @param Array       $allowed_algs  List of supported verification algorithms
31
     *
32
     * @return object      The JWT's payload as a PHP object
33
     *
34
     * @throws DomainException              Algorithm was not provided
35
     * @throws UnexpectedValueException     Provided JWT was invalid
36
     * @throws SignatureInvalidException    Provided JWT was invalid because the signature verification failed
37
     * @throws BeforeValidException         Provided JWT is trying to be used before it's eligible as defined by 'nbf'
38
     * @throws BeforeValidException         Provided JWT is trying to be used before it's been created as defined by 'iat'
39
     * @throws ExpiredException             Provided JWT has since expired, as defined by the 'exp' claim
40
     *
41
     * @uses jsonDecode
42
     * @uses urlsafeB64Decode
43
     */
44
    public static function decode($jwt, $key = null, $allowed_algs = array())
45
    {
46
        $tks = explode('.', $jwt);
47
        if (count($tks) != 3) {
48
            throw new UnexpectedValueException('Wrong number of segments');
49
        }
50
        list($headb64, $bodyb64, $cryptob64) = $tks;
51
        if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64)))) {
52
            throw new UnexpectedValueException('Invalid header encoding');
53
        }
54
        if (null === $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64))) {
55
            throw new UnexpectedValueException('Invalid claims encoding');
56
        }
57
        $sig = JWT::urlsafeB64Decode($cryptob64);
58
        if (isset($key)) {
59
            if (empty($header->alg)) {
60
                throw new DomainException('Empty algorithm');
61
            }
62
            if (empty(self::$supported_algs[$header->alg])) {
63
                throw new DomainException('Algorithm not supported');
64
            }
65
            if (!is_array($allowed_algs) || !in_array($header->alg, $allowed_algs)) {
66
                throw new DomainException('Algorithm not allowed');
67
            }
68
            if (is_array($key)) {
69
                if (isset($header->kid)) {
70
                    $key = $key[$header->kid];
71
                } else {
72
                    throw new DomainException('"kid" empty, unable to lookup correct key');
73
                }
74
            }
75
 
76
            // Check the signature
77
            if (!JWT::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) {
78
                throw new SignatureInvalidException('Signature verification failed');
79
            }
80
 
81
            // Check if the nbf if it is defined. This is the time that the
82
            // token can actually be used. If it's not yet that time, abort.
83
            if (isset($payload->nbf) && $payload->nbf > time()) {
84
                throw new BeforeValidException(
85
                    'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf)
86
                );
87
            }
88
 
89
            // Check that this token has been created before 'now'. This prevents
90
            // using tokens that have been created for later use (and haven't
91
            // correctly used the nbf claim).
92
            if (isset($payload->iat) && $payload->iat > time()) {
93
                throw new BeforeValidException(
94
                    'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat)
95
                );
96
            }
97
 
98
            // Check if this token has expired.
99
            if (isset($payload->exp) && time() >= $payload->exp) {
100
                throw new ExpiredException('Expired token');
101
            }
102
        }
103
 
104
        return $payload;
105
    }
106
 
107
    /**
108
     * Converts and signs a PHP object or array into a JWT string.
109
     *
110
     * @param object|array $payload PHP object or array
111
     * @param string       $key     The secret key
112
     * @param string       $alg     The signing algorithm. Supported
113
     *                              algorithms are 'HS256', 'HS384' and 'HS512'
114
     *
115
     * @return string      A signed JWT
116
     * @uses jsonEncode
117
     * @uses urlsafeB64Encode
118
     */
119
    public static function encode($payload, $key, $alg = 'HS256', $keyId = null)
120
    {
121
        $header = array('typ' => 'JWT', 'alg' => $alg);
122
        if ($keyId !== null) {
123
            $header['kid'] = $keyId;
124
        }
125
        $segments = array();
126
        $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header));
127
        $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($payload));
128
        $signing_input = implode('.', $segments);
129
 
130
        $signature = JWT::sign($signing_input, $key, $alg);
131
        $segments[] = JWT::urlsafeB64Encode($signature);
132
 
133
        return implode('.', $segments);
134
    }
135
 
136
    /**
137
     * Sign a string with a given key and algorithm.
138
     *
139
     * @param string $msg          The message to sign
140
     * @param string|resource $key The secret key
141
     * @param string $alg       The signing algorithm. Supported algorithms
142
     *                               are 'HS256', 'HS384', 'HS512' and 'RS256'
143
     *
144
     * @return string          An encrypted message
145
     * @throws DomainException Unsupported algorithm was specified
146
     */
147
    public static function sign($msg, $key, $alg = 'HS256')
148
    {
149
        if (empty(self::$supported_algs[$alg])) {
150
            throw new DomainException('Algorithm not supported');
151
        }
152
        list($function, $algorithm) = self::$supported_algs[$alg];
153
        switch($function) {
154
            case 'hash_hmac':
155
                return hash_hmac($algorithm, $msg, $key, true);
156
            case 'openssl':
157
                $signature = '';
158
                $success = openssl_sign($msg, $signature, $key, $algorithm);
159
                if (!$success) {
160
                    throw new DomainException("OpenSSL unable to sign data");
161
                } else {
162
                    return $signature;
163
                }
164
        }
165
    }
166
 
167
    /**
168
     * Verify a signature with the mesage, key and method. Not all methods
169
     * are symmetric, so we must have a separate verify and sign method.
170
     * @param string $msg the original message
171
     * @param string $signature
172
     * @param string|resource $key for HS*, a string key works. for RS*, must be a resource of an openssl public key
173
     * @param string $alg
174
     * @return bool
175
     * @throws DomainException Invalid Algorithm or OpenSSL failure
176
     */
177
    private static function verify($msg, $signature, $key, $alg)
178
    {
179
        if (empty(self::$supported_algs[$alg])) {
180
            throw new DomainException('Algorithm not supported');
181
        }
182
 
183
        list($function, $algorithm) = self::$supported_algs[$alg];
184
        switch($function) {
185
            case 'openssl':
186
                $success = openssl_verify($msg, $signature, $key, $algorithm);
187
                if (!$success) {
188
                    throw new DomainException("OpenSSL unable to verify data: " . openssl_error_string());
189
                } else {
190
                    return $signature;
191
                }
192
            case 'hash_hmac':
193
            default:
194
                $hash = hash_hmac($algorithm, $msg, $key, true);
195
                if (function_exists('hash_equals')) {
196
                    return hash_equals($signature, $hash);
197
                }
198
                $len = min(self::safeStrlen($signature), self::safeStrlen($hash));
199
 
200
                $status = 0;
201
                for ($i = 0; $i < $len; $i++) {
202
                    $status |= (ord($signature[$i]) ^ ord($hash[$i]));
203
                }
204
                $status |= (self::safeStrlen($signature) ^ self::safeStrlen($hash));
205
 
206
                return ($status === 0);
207
        }
208
    }
209
 
210
    /**
211
     * Decode a JSON string into a PHP object.
212
     *
213
     * @param string $input JSON string
214
     *
215
     * @return object          Object representation of JSON string
216
     * @throws DomainException Provided string was invalid JSON
217
     */
218
    public static function jsonDecode($input)
219
    {
220
        if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) {
221
            /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you
222
             * to specify that large ints (like Steam Transaction IDs) should be treated as
223
             * strings, rather than the PHP default behaviour of converting them to floats.
224
             */
225
            $obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
226
        } else {
227
            /** Not all servers will support that, however, so for older versions we must
228
             * manually detect large ints in the JSON string and quote them (thus converting
229
             *them to strings) before decoding, hence the preg_replace() call.
230
             */
231
            $max_int_length = strlen((string) PHP_INT_MAX) - 1;
232
            $json_without_bigints = preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input);
233
            $obj = json_decode($json_without_bigints);
234
        }
235
 
236
        if (function_exists('json_last_error') && $errno = json_last_error()) {
237
            JWT::handleJsonError($errno);
238
        } elseif ($obj === null && $input !== 'null') {
239
            throw new DomainException('Null result with non-null input');
240
        }
241
        return $obj;
242
    }
243
 
244
    /**
245
     * Encode a PHP object into a JSON string.
246
     *
247
     * @param object|array $input A PHP object or array
248
     *
249
     * @return string          JSON representation of the PHP object or array
250
     * @throws DomainException Provided object could not be encoded to valid JSON
251
     */
252
    public static function jsonEncode($input)
253
    {
254
        $json = json_encode($input);
255
        if (function_exists('json_last_error') && $errno = json_last_error()) {
256
            JWT::handleJsonError($errno);
257
        } elseif ($json === 'null' && $input !== null) {
258
            throw new DomainException('Null result with non-null input');
259
        }
260
        return $json;
261
    }
262
 
263
    /**
264
     * Decode a string with URL-safe Base64.
265
     *
266
     * @param string $input A Base64 encoded string
267
     *
268
     * @return string A decoded string
269
     */
270
    public static function urlsafeB64Decode($input)
271
    {
272
        $remainder = strlen($input) % 4;
273
        if ($remainder) {
274
            $padlen = 4 - $remainder;
275
            $input .= str_repeat('=', $padlen);
276
        }
277
        return base64_decode(strtr($input, '-_', '+/'));
278
    }
279
 
280
    /**
281
     * Encode a string with URL-safe Base64.
282
     *
283
     * @param string $input The string you want encoded
284
     *
285
     * @return string The base64 encode of what you passed in
286
     */
287
    public static function urlsafeB64Encode($input)
288
    {
289
        return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
290
    }
291
 
292
    /**
293
     * Helper method to create a JSON error.
294
     *
295
     * @param int $errno An error number from json_last_error()
296
     *
297
     * @return void
298
     */
299
    private static function handleJsonError($errno)
300
    {
301
        $messages = array(
302
            JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
303
            JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
304
            JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON'
305
        );
306
        throw new DomainException(
307
            isset($messages[$errno])
308
            ? $messages[$errno]
309
            : 'Unknown JSON error: ' . $errno
310
        );
311
    }
312
 
313
    /**
314
     * Get the number of bytes in cryptographic strings.
315
     *
316
     * @param string
317
     * @return int
318
     */
319
    private static function safeStrlen($str)
320
    {
321
        if (function_exists('mb_strlen')) {
322
            return mb_strlen($str, '8bit');
323
        }
324
        return strlen($str);
325
    }
326
}