Advanced Access Manager - Version 6.9.0

Version Description

  • Fixed: Revoking JWT token via UI causes current user to logout https://github.com/aamplugin/advanced-access-manager/issues/224
  • Fixed: Notice: Undefined variable: cache https://github.com/aamplugin/advanced-access-manager/issues/223
  • Changed: Update JWT vendor https://github.com/aamplugin/advanced-access-manager/issues/221
Download this release

Release Info

Developer vasyltech
Plugin Icon 128x128 Advanced Access Manager
Version 6.9.0
Comparing to
See all releases

Code changes from version 6.8.5 to 6.9.0

aam.php CHANGED
@@ -3,7 +3,7 @@
3
  /**
4
  * Plugin Name: Advanced Access Manager
5
  * Description: Collection of features to manage your WordPress website authentication, authorization and monitoring
6
- * Version: 6.8.5
7
  * Author: Vasyl Martyniuk <vasyl@vasyltech.com>
8
  * Author URI: https://vasyltech.com
9
  * Text Domain: advanced-access-manager
@@ -264,7 +264,7 @@ if (defined('ABSPATH')) {
264
  // Define few common constants
265
  define('AAM_MEDIA', plugins_url('/media', __FILE__));
266
  define('AAM_KEY', 'advanced-access-manager');
267
- define('AAM_VERSION', '6.8.5');
268
  define('AAM_BASEDIR', __DIR__);
269
 
270
  // Load vendor
3
  /**
4
  * Plugin Name: Advanced Access Manager
5
  * Description: Collection of features to manage your WordPress website authentication, authorization and monitoring
6
+ * Version: 6.9.0
7
  * Author: Vasyl Martyniuk <vasyl@vasyltech.com>
8
  * Author URI: https://vasyltech.com
9
  * Text Domain: advanced-access-manager
264
  // Define few common constants
265
  define('AAM_MEDIA', plugins_url('/media', __FILE__));
266
  define('AAM_KEY', 'advanced-access-manager');
267
+ define('AAM_VERSION', '6.9.0');
268
  define('AAM_BASEDIR', __DIR__);
269
 
270
  // Load vendor
application/Backend/Feature/Main/Jwt.php CHANGED
@@ -10,11 +10,12 @@
10
  /**
11
  * JWT UI manager
12
  *
 
13
  * @since 6.7.9 https://github.com/aamplugin/advanced-access-manager/issues/192
14
  * @since 6.0.0 Initial implementation of the class
15
  *
16
  * @package AAM
17
- * @version 6.7.9
18
  */
19
  class AAM_Backend_Feature_Main_Jwt
20
  extends AAM_Backend_Feature_Abstract implements AAM_Backend_Feature_ISubjectAware
@@ -54,8 +55,11 @@ class AAM_Backend_Feature_Main_Jwt
54
  *
55
  * @return string
56
  *
 
 
 
57
  * @access public
58
- * @version 6.0.0
59
  */
60
  public function generate()
61
  {
@@ -75,7 +79,8 @@ class AAM_Backend_Feature_Main_Jwt
75
  $claims = array(
76
  'userId' => $user->ID,
77
  'revocable' => true,
78
- 'refreshable' => ($refresh === true)
 
79
  );
80
 
81
  // If token also should contains the trigger action when it is expires,
@@ -86,13 +91,11 @@ class AAM_Backend_Feature_Main_Jwt
86
 
87
  try {
88
  if ($max >= AAM_Core_API::maxLevel($user->allcaps)) {
89
- $jwt = AAM_Core_Jwt_Issuer::getInstance()->issueToken(
90
- $claims, new DateTime('@' . $expires)
91
- );
92
 
93
  if ($register === true) {
94
  $status = AAM_Service_Jwt::getInstance()->registerToken(
95
- $user->ID, $jwt->token
96
  );
97
  } else {
98
  $status = true;
@@ -100,16 +103,20 @@ class AAM_Backend_Feature_Main_Jwt
100
 
101
  $result = array(
102
  'status' => (!empty($status) ? 'success' : 'failure'),
103
- 'jwt' => $jwt->token
104
  );
105
  } else {
106
- $result['reason'] = 'You are not allowed to generate JWT for this user';
 
 
107
  }
108
  } catch (Exception $ex) {
109
  $result['reason'] = $ex->getMessage();
110
  }
111
  } else {
112
- $result['reason'] = 'You are not allowed to manage JWT tokens';
 
 
113
  }
114
 
115
  return wp_json_encode($result);
@@ -178,8 +185,11 @@ class AAM_Backend_Feature_Main_Jwt
178
  *
179
  * @return array
180
  *
 
 
 
181
  * @access protected
182
- * @version 6.0.0
183
  */
184
  protected function retrieveList()
185
  {
@@ -194,12 +204,12 @@ class AAM_Backend_Feature_Main_Jwt
194
  'data' => array(),
195
  );
196
 
197
- $issuer = AAM_Core_Jwt_Issuer::getInstance();
198
 
199
  foreach ($tokens as $token) {
200
- $claims = $issuer->validateToken($token);
201
 
202
- if ($claims->isValid) {
203
  $expires = new DateTime('@' . $claims->exp, new DateTimeZone('UTC'));
204
  $details = $expires->format('m/d/Y, H:i O');
205
  } else {
@@ -209,7 +219,7 @@ class AAM_Backend_Feature_Main_Jwt
209
  $response['data'][] = array(
210
  $token,
211
  add_query_arg('aam-jwt', $token, site_url()),
212
- $claims->isValid,
213
  $details,
214
  'view,delete'
215
  );
10
  /**
11
  * JWT UI manager
12
  *
13
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/221
14
  * @since 6.7.9 https://github.com/aamplugin/advanced-access-manager/issues/192
15
  * @since 6.0.0 Initial implementation of the class
16
  *
17
  * @package AAM
18
+ * @version 6.9.0
19
  */
20
  class AAM_Backend_Feature_Main_Jwt
21
  extends AAM_Backend_Feature_Abstract implements AAM_Backend_Feature_ISubjectAware
55
  *
56
  * @return string
57
  *
58
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/221
59
+ * @since 6.0.0 Initial implementation of the method
60
+ *
61
  * @access public
62
+ * @version 6.9.0
63
  */
64
  public function generate()
65
  {
79
  $claims = array(
80
  'userId' => $user->ID,
81
  'revocable' => true,
82
+ 'refreshable' => ($refresh === true),
83
+ 'exp' => (new DateTime('@' . $expires))->getTimestamp()
84
  );
85
 
86
  // If token also should contains the trigger action when it is expires,
91
 
92
  try {
93
  if ($max >= AAM_Core_API::maxLevel($user->allcaps)) {
94
+ $jwt = AAM_Core_Jwt_Manager::getInstance()->encode($claims);
 
 
95
 
96
  if ($register === true) {
97
  $status = AAM_Service_Jwt::getInstance()->registerToken(
98
+ $user->ID, $jwt
99
  );
100
  } else {
101
  $status = true;
103
 
104
  $result = array(
105
  'status' => (!empty($status) ? 'success' : 'failure'),
106
+ 'jwt' => $jwt
107
  );
108
  } else {
109
+ $result['reason'] = __(
110
+ 'You are not allowed to generate JWT for this user', AAM_KEY
111
+ );
112
  }
113
  } catch (Exception $ex) {
114
  $result['reason'] = $ex->getMessage();
115
  }
116
  } else {
117
+ $result['reason'] = __(
118
+ 'You are not allowed to manage JWT tokens', AAM_KEY
119
+ );
120
  }
121
 
122
  return wp_json_encode($result);
185
  *
186
  * @return array
187
  *
188
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/221
189
+ * @since 6.0.0 Initial implementation of the method
190
+ *
191
  * @access protected
192
+ * @version 6.9.0
193
  */
194
  protected function retrieveList()
195
  {
204
  'data' => array(),
205
  );
206
 
207
+ $issuer = AAM_Core_Jwt_Manager::getInstance();
208
 
209
  foreach ($tokens as $token) {
210
+ $claims = $issuer->validate($token);
211
 
212
+ if (!is_wp_error($claims)) {
213
  $expires = new DateTime('@' . $claims->exp, new DateTimeZone('UTC'));
214
  $details = $expires->format('m/d/Y, H:i O');
215
  } else {
219
  $response['data'][] = array(
220
  $token,
221
  add_query_arg('aam-jwt', $token, site_url()),
222
+ !is_wp_error($claims),
223
  $details,
224
  'view,delete'
225
  );
application/Core/Jwt/Issuer.php CHANGED
@@ -10,6 +10,8 @@
10
  /**
11
  * AAM JWT Issuer
12
  *
 
 
13
  * @since 6.7.2 https://github.com/aamplugin/advanced-access-manager/issues/165
14
  * @since 6.1.0 Enriched error response with more details
15
  * @since 6.0.4 Bug fixing. Timezone was handled incorrectly and ttl did not take in
@@ -17,7 +19,10 @@
17
  * @since 6.0.0 Initial implementation of the class
18
  *
19
  * @package AAM
20
- * @version 6.7.2
 
 
 
21
  */
22
  class AAM_Core_Jwt_Issuer
23
  {
@@ -31,15 +36,22 @@ class AAM_Core_Jwt_Issuer
31
  *
32
  * @return object
33
  *
 
34
  * @since 6.1.0 Enriched error response with more details
35
  * @since 6.0.4 Making sure that JWT expiration is checked with UTC timezone
36
  * @since 6.0.0 Initial implementation of the method
37
  *
38
  * @access public
39
- * @version 6.1.0
40
  */
41
  public function validateToken($token)
42
  {
 
 
 
 
 
 
43
  try {
44
  $headers = $this->extractTokenHeaders($token);
45
 
@@ -98,16 +110,23 @@ class AAM_Core_Jwt_Issuer
98
  *
99
  * @return object
100
  *
 
101
  * @since 6.0.4 Fixed the bug when `authentication.jwt.expires` is defined in
102
  * seconds
103
  * @since 6.0.0 Initial implementation of the method
104
  *
105
  * @access public
106
  * @throws Exception
107
- * @version 6.0.4
108
  */
109
  public function issueToken($args = array(), $expires = null)
110
  {
 
 
 
 
 
 
111
  if (!empty($expires)) {
112
  $time = $expires;
113
  } else {
@@ -169,11 +188,20 @@ class AAM_Core_Jwt_Issuer
169
  *
170
  * @return object
171
  *
 
 
 
172
  * @access public
173
- * @version 6.0.0
174
  */
175
  public function extractTokenClaims($token)
176
  {
 
 
 
 
 
 
177
  $parts = explode('.', $token);
178
  $claims = array();
179
 
10
  /**
11
  * AAM JWT Issuer
12
  *
13
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/221
14
+ * @since 6.8.6 https://github.com/aamplugin/advanced-access-manager/issues/221
15
  * @since 6.7.2 https://github.com/aamplugin/advanced-access-manager/issues/165
16
  * @since 6.1.0 Enriched error response with more details
17
  * @since 6.0.4 Bug fixing. Timezone was handled incorrectly and ttl did not take in
19
  * @since 6.0.0 Initial implementation of the class
20
  *
21
  * @package AAM
22
+ * @version 6.9.0
23
+ *
24
+ * @deprecated 6.8.5 Will be removed from AAM in 7.0.0 version
25
+ * @todo Remove this class in 7.0.0 release
26
  */
27
  class AAM_Core_Jwt_Issuer
28
  {
36
  *
37
  * @return object
38
  *
39
+ * @since 6.8.6 https://github.com/aamplugin/advanced-access-manager/issues/221
40
  * @since 6.1.0 Enriched error response with more details
41
  * @since 6.0.4 Making sure that JWT expiration is checked with UTC timezone
42
  * @since 6.0.0 Initial implementation of the method
43
  *
44
  * @access public
45
+ * @version 6.8.6
46
  */
47
  public function validateToken($token)
48
  {
49
+ _deprecated_function(
50
+ __CLASS__ . '::' . __FUNCTION__,
51
+ '6.8.5',
52
+ 'AAM_Core_Jwt_Manager::validate'
53
+ );
54
+
55
  try {
56
  $headers = $this->extractTokenHeaders($token);
57
 
110
  *
111
  * @return object
112
  *
113
+ * @since 6.8.6 https://github.com/aamplugin/advanced-access-manager/issues/221
114
  * @since 6.0.4 Fixed the bug when `authentication.jwt.expires` is defined in
115
  * seconds
116
  * @since 6.0.0 Initial implementation of the method
117
  *
118
  * @access public
119
  * @throws Exception
120
+ * @version 6.8.6
121
  */
122
  public function issueToken($args = array(), $expires = null)
123
  {
124
+ _deprecated_function(
125
+ __CLASS__ . '::' . __FUNCTION__,
126
+ '6.8.5',
127
+ 'AAM_Core_Jwt_Manager::encode'
128
+ );
129
+
130
  if (!empty($expires)) {
131
  $time = $expires;
132
  } else {
188
  *
189
  * @return object
190
  *
191
+ * @since 6.8.6 https://github.com/aamplugin/advanced-access-manager/issues/221
192
+ * @since 6.0.0 Initial implementation of the method
193
+ *
194
  * @access public
195
+ * @version 6.8.6
196
  */
197
  public function extractTokenClaims($token)
198
  {
199
+ _deprecated_function(
200
+ __CLASS__ . '::' . __FUNCTION__,
201
+ '6.8.5',
202
+ 'AAM_Core_Jwt_Manager::decode'
203
+ );
204
+
205
  $parts = explode('.', $token);
206
  $claims = array();
207
 
application/Core/Jwt/Manager.php ADDED
@@ -0,0 +1,667 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * ======================================================================
5
+ * LICENSE: This file is subject to the terms and conditions defined in *
6
+ * file 'license.txt', which is part of this source code package. *
7
+ * ======================================================================
8
+ */
9
+
10
+ /**
11
+ * AAM JWT Manager
12
+ *
13
+ * Majority of the work is taken from the Firebase PHP JWT library. The code was
14
+ * adopted to work with PHP 5.6.0+.
15
+ *
16
+ * @package AAM
17
+ * @since 6.9.0
18
+ *
19
+ * @link https://github.com/firebase/php-jwt
20
+ */
21
+ class AAM_Core_Jwt_Manager
22
+ {
23
+
24
+ use AAM_Core_Contract_SingletonTrait;
25
+
26
+ /**
27
+ * When checking nbf, iat or expiration times,
28
+ * we want to provide some extra leeway time to
29
+ * account for clock skew.
30
+ *
31
+ * @var int
32
+ * @version 6.9.0
33
+ */
34
+ private $_leeway = 0;
35
+
36
+ /**
37
+ * Collection of supported signing algorithms
38
+ *
39
+ * @var array
40
+ *
41
+ * @access private
42
+ * @version 6.9.0
43
+ */
44
+ private $_supported_algs = array(
45
+ 'ES384' => array('openssl', 'SHA384'),
46
+ 'ES256' => array('openssl', 'SHA256'),
47
+ 'HS256' => array('hash_hmac', 'SHA256'),
48
+ 'HS384' => array('hash_hmac', 'SHA384'),
49
+ 'HS512' => array('hash_hmac', 'SHA512'),
50
+ 'RS256' => array('openssl', 'SHA256'),
51
+ 'RS384' => array('openssl', 'SHA384'),
52
+ 'RS512' => array('openssl', 'SHA512'),
53
+ 'EdDSA' => array('sodium_crypto', 'EdDSA'),
54
+ );
55
+
56
+ /**
57
+ * Verify that the provided token is valid
58
+ *
59
+ * @param string $token
60
+ *
61
+ * @return boolean
62
+ *
63
+ * @access public
64
+ * @version 6.9.0
65
+ */
66
+ public function validate($token)
67
+ {
68
+ try {
69
+ // Validating headers segment. Make sure that all necessary properties
70
+ // are defined correctly
71
+ $this->validateHeaders($token);
72
+
73
+ // Get signing attributes
74
+ $attrs = $this->getSigningAttributes();
75
+
76
+ // Verify the signature
77
+ $this->validateSignature($token, $attrs->key);
78
+
79
+ $timestamp = (new DateTime('now', new DateTimeZone('UTC')))->getTimestamp();
80
+ $payload = $this->decodeSegment($token, 1);
81
+
82
+ // Check the nbf if it is defined. This is the time that the
83
+ // token can actually be used. If it's not yet that time, abort.
84
+ if (isset($payload->nbf) && $payload->nbf > ($timestamp + $this->_leeway)) {
85
+ throw new Exception(
86
+ 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf)
87
+ );
88
+ }
89
+
90
+ // Check that this token has been created before 'now'. This prevents
91
+ // using tokens that have been created for later use (and haven't
92
+ // correctly used the nbf claim).
93
+ if (isset($payload->iat) && $payload->iat > ($timestamp + $this->_leeway)) {
94
+ throw new Exception(
95
+ 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat)
96
+ );
97
+ }
98
+
99
+ // Check if this token has expired.
100
+ if (isset($payload->exp) && ($timestamp - $this->_leeway) >= $payload->exp) {
101
+ throw new Exception('Expired token');
102
+ }
103
+ } catch (Exception $e) {
104
+ $payload = new WP_Error(1, $e->getMessage(), $e);
105
+ }
106
+
107
+ return $payload;
108
+ }
109
+
110
+ /**
111
+ * Encode payload
112
+ *
113
+ * @param array $payload
114
+ *
115
+ * @return string
116
+ * @version 6.9.0
117
+ */
118
+ public function encode($payload)
119
+ {
120
+ $attrs = $this->getSigningAttributes();
121
+
122
+ // Encode the JWT headers first
123
+ $segments = array($this->urlsafeB64Encode($this->jsonEncode(array(
124
+ 'typ' => 'JWT',
125
+ 'alg' => $attrs->alg
126
+ ))));
127
+
128
+
129
+ $ttl = AAM_Core_Config::get('authentication.jwt.expires', '+24 hours');
130
+
131
+ if (is_numeric($ttl)) {
132
+ $ttl = "+{$ttl} seconds";
133
+ }
134
+
135
+ $time = new DateTime($ttl, new DateTimeZone('UTC'));
136
+
137
+ $claims = apply_filters(
138
+ 'aam_jwt_claims_filter',
139
+ array_merge(
140
+ array(
141
+ "iat" => time(),
142
+ 'iss' => get_site_url(),
143
+ 'exp' => $time->getTimestamp(),
144
+ 'jti' => $this->generateUuid()
145
+ ),
146
+ $payload
147
+ )
148
+ );
149
+
150
+ // Next, let's encode the payload
151
+ array_push($segments, $this->urlsafeB64Encode($this->jsonEncode($claims)));
152
+
153
+ // Adding signature to as the last segment
154
+ array_push($segments, $this->urlsafeB64Encode($this->sign(
155
+ implode('.', $segments),
156
+ $attrs->key,
157
+ $attrs->alg
158
+ )));
159
+
160
+ return implode('.', $segments);
161
+ }
162
+
163
+ /**
164
+ * Decode token
165
+ *
166
+ * @param string $token
167
+ *
168
+ * @return WP_Error|stdClass
169
+ * @version 6.9.0
170
+ */
171
+ public function decode($token)
172
+ {
173
+ return $this->validate($token, $this->getSigningAttributes()->key);
174
+ }
175
+
176
+ /**
177
+ * Generate random uuid
178
+ *
179
+ * @return string
180
+ *
181
+ * @access protected
182
+ * @version 6.9.0
183
+ */
184
+ protected function generateUuid()
185
+ {
186
+ return sprintf(
187
+ '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
188
+ // 32 bits for "time_low"
189
+ mt_rand(0, 0xffff),
190
+ mt_rand(0, 0xffff),
191
+
192
+ // 16 bits for "time_mid"
193
+ mt_rand(0, 0xffff),
194
+
195
+ // 16 bits for "time_hi_and_version",
196
+ // four most significant bits holds version number 4
197
+ mt_rand(0, 0x0fff) | 0x4000,
198
+
199
+ // 16 bits, 8 bits for "clk_seq_hi_res",
200
+ // 8 bits for "clk_seq_low",
201
+ // two most significant bits holds zero and one for variant DCE1.1
202
+ mt_rand(0, 0x3fff) | 0x8000,
203
+
204
+ // 48 bits for "node"
205
+ mt_rand(0, 0xffff),
206
+ mt_rand(0, 0xffff),
207
+ mt_rand(0, 0xffff)
208
+ );
209
+ }
210
+
211
+ /**
212
+ * Sign a string with a given key and algorithm.
213
+ *
214
+ * @param string $msg The message to sign
215
+ * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
216
+ * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
217
+ * 'HS512', 'RS256', 'RS384', and 'RS512'
218
+ *
219
+ * @return string An encrypted message
220
+ * @version 6.9.0
221
+ *
222
+ * @throws DomainException Unsupported algorithm or bad key was specified
223
+ */
224
+ protected function sign($msg, $key, $alg)
225
+ {
226
+ $signature = null;
227
+
228
+ if (empty($this->_supported_algs[$alg])) {
229
+ throw new DomainException('Algorithm not supported');
230
+ } elseif (empty($key) || !is_string($key)) {
231
+ throw new InvalidArgumentException('The signing key must be a string');
232
+ }
233
+
234
+ list($function, $algorithm) = $this->_supported_algs[$alg];
235
+
236
+ if ($function === 'hash_hmac') {
237
+ $signature = hash_hmac($algorithm, $msg, $key, true);
238
+ } elseif ($function === 'openssl') {
239
+ $success = openssl_sign($msg, $signature, $key, $algorithm);
240
+
241
+ if (!$success) {
242
+ throw new DomainException('OpenSSL unable to sign data');
243
+ }
244
+ if ($alg === 'ES256') {
245
+ $signature = $this->signatureFromDER($signature, 256);
246
+ } elseif ($alg === 'ES384') {
247
+ $signature = $this->signatureFromDER($signature, 384);
248
+ }
249
+ } elseif ($function === 'sodium_crypto') {
250
+ if (!function_exists('sodium_crypto_sign_detached')) {
251
+ throw new DomainException('libsodium is not available');
252
+ }
253
+
254
+ // The last non-empty line is used as the key.
255
+ $lines = array_filter(explode("\n", $key));
256
+ $key = base64_decode((string) end($lines));
257
+ $signature = sodium_crypto_sign_detached($msg, $key);
258
+ } else {
259
+ throw new DomainException('Algorithm not supported');
260
+ }
261
+
262
+ return $signature;
263
+ }
264
+
265
+ /**
266
+ * Encode a string with URL-safe Base64.
267
+ *
268
+ * @param string $input The string you want encoded
269
+ *
270
+ * @return string The base64 encode of what you passed in
271
+ * @version 6.9.0
272
+ */
273
+ protected function urlsafeB64Encode($input)
274
+ {
275
+ return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
276
+ }
277
+
278
+ /**
279
+ * Encode a PHP array into a JSON string.
280
+ *
281
+ * @param array<mixed> $input A PHP array
282
+ *
283
+ * @return string JSON representation of the PHP array
284
+ * @version 6.9.0
285
+ *
286
+ * @throws DomainException Provided object could not be encoded to valid JSON
287
+ */
288
+ protected function jsonEncode(array $input): string
289
+ {
290
+ if (PHP_VERSION_ID >= 50400) {
291
+ $json = json_encode($input, JSON_UNESCAPED_SLASHES);
292
+ } else {
293
+ // PHP 5.3 only
294
+ $json = json_encode($input);
295
+ }
296
+
297
+ if ($errno = json_last_error()) {
298
+ throw new DomainException('Failed to encode JSON with error ' . $errno);
299
+ } elseif ($json === false) {
300
+ throw new DomainException('Provided object could not be encoded to valid JSON');
301
+ }
302
+
303
+ return $json;
304
+ }
305
+
306
+ /**
307
+ * Get token signing attributes like algorithm and key
308
+ *
309
+ * @return stdClass
310
+ *
311
+ * @access protected
312
+ * @version 6.9.0
313
+ */
314
+ protected function getSigningAttributes()
315
+ {
316
+ $alg = strtoupper(
317
+ AAM_Core_Config::get('authentication.jwt.algorithm', 'HS256')
318
+ );
319
+
320
+ if (strpos($alg, 'RS') === 0) {
321
+ $path = AAM_Core_Config::get('authentication.jwt.privateKeyPath');
322
+ $key = (is_readable($path) ? file_get_contents($path) : null);
323
+ $passphrase = AAM_Core_Config::get('authentication.jwt.passphrase', false);
324
+
325
+ if ($passphrase && extension_loaded('openssl')) {
326
+ $key = openssl_pkey_get_private($key, $passphrase);
327
+ }
328
+ } else {
329
+ $key = AAM_Core_Config::get('authentication.jwt.secret', SECURE_AUTH_KEY);
330
+ }
331
+
332
+ return (object) array(
333
+ 'alg' => $alg,
334
+ 'key' => $key
335
+ );
336
+ }
337
+
338
+ /**
339
+ * Validate token headers
340
+ *
341
+ * @param string $token
342
+ *
343
+ * @return void
344
+ *
345
+ * @access protected
346
+ * @version 6.9.0
347
+ */
348
+ protected function validateHeaders($token)
349
+ {
350
+ $headers = $this->decodeSegment($token);
351
+
352
+ if (empty($headers->alg)) {
353
+ throw new UnexpectedValueException('Empty algorithm');
354
+ }
355
+
356
+ if (empty($this->_supported_algs[$headers->alg])) {
357
+ throw new UnexpectedValueException('Algorithm not supported');
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Validate token's signature
363
+ *
364
+ * @param string $token
365
+ * @param string $key
366
+ *
367
+ * @return void
368
+ *
369
+ * @access protected
370
+ * @version 6.9.0
371
+ */
372
+ protected function validateSignature($token, $key)
373
+ {
374
+ $headers = $this->decodeSegment($token);
375
+ $signature = $this->decodeSegment($token, 2, true);
376
+
377
+ if ($headers->alg === 'ES256' || $headers->alg === 'ES384') {
378
+ // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures
379
+ $signature = $this->signatureToDER($signature);
380
+ }
381
+
382
+ // Now verifying the token
383
+ list($a, $b) = explode('.', $token);
384
+ list($function, $algorithm) = $this->_supported_algs[$headers->alg];
385
+ $msg = "{$a}.{$b}";
386
+
387
+ if ($function === 'openssl') {
388
+ $success = openssl_verify($msg, $signature, $key, $algorithm) === 1;
389
+ } else if ($function === 'sodium_crypto') {
390
+ if (!function_exists('sodium_crypto_sign_verify_detached')) {
391
+ throw new DomainException('libsodium is not available');
392
+ }
393
+
394
+ // The last non-empty line is used as the key.
395
+ $lines = array_filter(explode("\n", $key));
396
+ $key = base64_decode((string) end($lines));
397
+ $success = sodium_crypto_sign_verify_detached($signature, $msg, $key);
398
+ } else {
399
+ $hash = hash_hmac($algorithm, $msg, $key, true);
400
+ $success = $this->constantTimeEquals($hash, $signature);
401
+ }
402
+
403
+ if ($success !== true) {
404
+ throw new Exception('Invalid token signature');
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Comparing hashes
410
+ *
411
+ * @param string $left The string of known length to compare against
412
+ * @param string $right The user-supplied string
413
+ *
414
+ * @return bool
415
+ *
416
+ * @access protected
417
+ * @version 6.9.0
418
+ */
419
+ protected function constantTimeEquals($left, $right)
420
+ {
421
+ $response = false;
422
+
423
+ if (function_exists('hash_equals')) {
424
+ $response = hash_equals($left, $right);
425
+ } else {
426
+ $len = min($this->safeStrlen($left), $this->safeStrlen($right));
427
+
428
+ $status = 0;
429
+ for ($i = 0; $i < $len; $i++) {
430
+ $status |= (ord($left[$i]) ^ ord($right[$i]));
431
+ }
432
+
433
+ $status |= ($this->safeStrlen($left) ^ $this->safeStrlen($right));
434
+
435
+ $response = $status === 0;
436
+ }
437
+
438
+ return $response;
439
+ }
440
+
441
+ /**
442
+ * Get the number of bytes in cryptographic strings.
443
+ *
444
+ * @param string $str
445
+ *
446
+ * @return int
447
+ * @version 6.9.0
448
+ */
449
+ protected function safeStrlen($str)
450
+ {
451
+ $response = 0;
452
+
453
+ if (function_exists('mb_strlen')) {
454
+ $response = mb_strlen($str, '8bit');
455
+ } else {
456
+ $response = strlen($str);
457
+ }
458
+
459
+ return $response;
460
+ }
461
+
462
+ /**
463
+ * Decode a token's segment
464
+ *
465
+ * @param string $token
466
+ * @param integer $segment
467
+ * @param boolean $returnRaw
468
+ *
469
+ * @return mixed
470
+ *
471
+ * @access protected
472
+ * @version 6.9.0
473
+ */
474
+ protected function decodeSegment($token, $segment = 0, $returnRaw = false)
475
+ {
476
+ $segments = explode('.', $token);
477
+
478
+ if (count($segments) !== 3) {
479
+ throw new UnexpectedValueException('Wrong number of segments');
480
+ }
481
+
482
+ // Base64 decode the value
483
+ $decoded = $this->urlsafeB64Decode($segments[$segment]);
484
+
485
+ return $returnRaw ? $decoded : $this->jsonDecode($decoded);
486
+ }
487
+
488
+ /**
489
+ * Decode a string with URL-safe Base64.
490
+ *
491
+ * @param string $input A Base64 encoded string
492
+ *
493
+ * @return string A decoded string
494
+ *
495
+ * @throws InvalidArgumentException invalid base64 characters
496
+ * @version 6.9.0
497
+ */
498
+ protected function urlsafeB64Decode($input)
499
+ {
500
+ $remainder = strlen($input) % 4;
501
+
502
+ if ($remainder) {
503
+ $input .= str_repeat('=', 4 - $remainder);
504
+ }
505
+
506
+ return base64_decode(strtr($input, '-_', '+/'));
507
+ }
508
+
509
+ /**
510
+ * Decode a JSON string into a PHP object.
511
+ *
512
+ * @param string $input JSON string
513
+ *
514
+ * @return mixed The decoded JSON string
515
+ * @version 6.9.0
516
+ *
517
+ * @throws DomainException Provided string was invalid JSON
518
+ */
519
+ protected function jsonDecode($input)
520
+ {
521
+ $obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
522
+
523
+ if (is_array($obj)) { // Covering scenario when value is empty array
524
+ $obj = (object) $obj;
525
+ }
526
+
527
+ if ($errno = json_last_error()) {
528
+ throw new DomainException('Failed to decode JSON with error ' . $errno);
529
+ } elseif (!is_a($obj, stdClass::class)) {
530
+ throw new UnexpectedValueException('Unexpected segment value');
531
+ }
532
+
533
+ return $obj;
534
+ }
535
+
536
+ /**
537
+ * Convert an ECDSA signature to an ASN.1 DER sequence
538
+ *
539
+ * @param string $sig The ECDSA signature to convert
540
+ *
541
+ * @return string The encoded DER object
542
+ * @version 6.9.0
543
+ */
544
+ protected function signatureToDER($sig)
545
+ {
546
+ // Separate the signature into r-value and s-value
547
+ $length = max(1, (int) (strlen($sig) / 2));
548
+ list($r, $s) = str_split($sig, $length);
549
+
550
+ // Trim leading zeros
551
+ $r = ltrim($r, "\x00");
552
+ $s = ltrim($s, "\x00");
553
+
554
+ // Convert r-value and s-value from unsigned big-endian integers to
555
+ // signed two's complement
556
+ if (ord($r[0]) > 0x7f) {
557
+ $r = "\x00" . $r;
558
+ }
559
+
560
+ if (ord($s[0]) > 0x7f) {
561
+ $s = "\x00" . $s;
562
+ }
563
+
564
+ return self::encodeDER(
565
+ 0x10,
566
+ self::encodeDER(0x02, $r) .
567
+ self::encodeDER(0x02, $s)
568
+ );
569
+ }
570
+
571
+ /**
572
+ * Encodes signature from a DER object.
573
+ *
574
+ * @param string $der binary signature in DER format
575
+ * @param int $keySize the number of bits in the key
576
+ *
577
+ * @return string the signature
578
+ * @version 6.9.0
579
+ */
580
+ protected function signatureFromDER($der, $keySize)
581
+ {
582
+ // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE
583
+ list($offset, $_) = $this->readDER($der);
584
+ list($offset, $r) = $this->readDER($der, $offset);
585
+ list($offset, $s) = $this->readDER($der, $offset);
586
+
587
+ // Convert r-value and s-value from signed two's compliment to unsigned
588
+ // big-endian integers
589
+ $r = ltrim($r, "\x00");
590
+ $s = ltrim($s, "\x00");
591
+
592
+ // Pad out r and s so that they are $keySize bits long
593
+ $r = str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT);
594
+ $s = str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT);
595
+
596
+ return $r . $s;
597
+ }
598
+
599
+ /**
600
+ * Reads binary DER-encoded data and decodes into a single object
601
+ *
602
+ * @param string $der the binary data in DER format
603
+ * @param int $offset the offset of the data stream containing the object
604
+ * to decode
605
+ *
606
+ * @return array{int, string|null} the new offset and the decoded object
607
+ * @version 6.9.0
608
+ */
609
+ protected function readDER($der, $offset = 0)
610
+ {
611
+ $pos = $offset;
612
+ $size = strlen($der);
613
+ $constructed = (ord($der[$pos]) >> 5) & 0x01;
614
+ $type = ord($der[$pos++]) & 0x1f;
615
+
616
+ // Length
617
+ $len = ord($der[$pos++]);
618
+ if ($len & 0x80) {
619
+ $n = $len & 0x1f;
620
+ $len = 0;
621
+ while ($n-- && $pos < $size) {
622
+ $len = ($len << 8) | ord($der[$pos++]);
623
+ }
624
+ }
625
+
626
+ // Value
627
+ if ($type === 0x03) {
628
+ $pos++; // Skip the first contents octet (padding indicator)
629
+ $data = substr($der, $pos, $len - 1);
630
+ $pos += $len - 1;
631
+ } elseif (!$constructed) {
632
+ $data = substr($der, $pos, $len);
633
+ $pos += $len;
634
+ } else {
635
+ $data = null;
636
+ }
637
+
638
+ return array($pos, $data);
639
+ }
640
+
641
+ /**
642
+ * Encodes a value into a DER object.
643
+ *
644
+ * @param int $type DER tag
645
+ * @param string $value the value to encode
646
+ *
647
+ * @return string the encoded object
648
+ * @version 6.9.0
649
+ */
650
+ protected function encodeDER($type, $value)
651
+ {
652
+ $tag_header = 0;
653
+
654
+ if ($type === 0x10) {
655
+ $tag_header |= 0x20;
656
+ }
657
+
658
+ // Type
659
+ $der = chr($tag_header | $type);
660
+
661
+ // Length
662
+ $der .= chr(strlen($value));
663
+
664
+ return $der . $value;
665
+ }
666
+
667
+ }
application/Core/Redirect.php CHANGED
@@ -100,7 +100,12 @@ class AAM_Core_Redirect
100
  array('reason' => 'restricted'),
101
  wp_login_url(AAM_Core_Request::server('REQUEST_URI'))
102
  ));
103
- exit;
 
 
 
 
 
104
  }
105
 
106
  /**
@@ -125,7 +130,13 @@ class AAM_Core_Redirect
125
  $code = !empty($meta['code']) ? $meta['code'] : 307;
126
 
127
  if (!empty($dest) && (empty($current) || ($current->ID !== intval($dest)))) {
128
- wp_safe_redirect(get_page_link($dest), $code); exit;
 
 
 
 
 
 
129
  }
130
  }
131
 
@@ -151,7 +162,13 @@ class AAM_Core_Redirect
151
  $code = !empty($meta['code']) ? $meta['code'] : 307;
152
 
153
  if ($dest !== AAM_Core_Request::server('REQUEST_URI')) {
154
- wp_safe_redirect($dest, $code); exit;
 
 
 
 
 
 
155
  }
156
  }
157
 
100
  array('reason' => 'restricted'),
101
  wp_login_url(AAM_Core_Request::server('REQUEST_URI'))
102
  ));
103
+
104
+ // Halt the execution. Redirect should carry user away if this is not
105
+ // a CLI execution (e.g. Unit Test)
106
+ if (php_sapi_name() !== 'cli') {
107
+ exit;
108
+ }
109
  }
110
 
111
  /**
130
  $code = !empty($meta['code']) ? $meta['code'] : 307;
131
 
132
  if (!empty($dest) && (empty($current) || ($current->ID !== intval($dest)))) {
133
+ wp_safe_redirect(get_page_link($dest), $code);
134
+
135
+ // Halt the execution. Redirect should carry user away if this is not
136
+ // a CLI execution (e.g. Unit Test)
137
+ if (php_sapi_name() !== 'cli') {
138
+ exit;
139
+ }
140
  }
141
  }
142
 
162
  $code = !empty($meta['code']) ? $meta['code'] : 307;
163
 
164
  if ($dest !== AAM_Core_Request::server('REQUEST_URI')) {
165
+ wp_safe_redirect($dest, $code);
166
+
167
+ // Halt the execution. Redirect should carry user away if this is not
168
+ // a CLI execution (e.g. Unit Test)
169
+ if (php_sapi_name() !== 'cli') {
170
+ exit;
171
+ }
172
  }
173
  }
174
 
application/Service/Jwt.php CHANGED
@@ -10,6 +10,8 @@
10
  /**
11
  * JWT Token service
12
  *
 
 
13
  * @since 6.6.2 https://github.com/aamplugin/advanced-access-manager/issues/139
14
  * @since 6.6.1 https://github.com/aamplugin/advanced-access-manager/issues/136
15
  * @since 6.6.0 https://github.com/aamplugin/advanced-access-manager/issues/129
@@ -26,7 +28,7 @@
26
  * @since 6.0.0 Initial implementation of the class
27
  *
28
  * @package AAM
29
- * @version 6.6.2
30
  */
31
  class AAM_Service_Jwt
32
  {
@@ -255,24 +257,40 @@ class AAM_Service_Jwt
255
  *
256
  * @return WP_REST_Response
257
  *
 
258
  * @since 6.1.0 Enriched error response with more details
259
  * @since 6.0.0 Initial implementation of the method
260
  *
261
  * @access public
262
- * @version 6.1.0
263
  */
264
  public function validateToken(WP_REST_Request $request)
265
  {
266
  $jwt = $request->get_param('jwt');
267
- $result = AAM_Core_Jwt_Issuer::getInstance()->validateToken($jwt);
268
 
269
- if ($result->isValid === true) {
270
  $response = new WP_REST_Response($result);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  } else {
272
  $response = new WP_REST_Response(array(
273
  'code' => 'rest_jwt_validation_failure',
274
- 'reason' => $result->reason
275
- ), $result->status);
276
  }
277
 
278
  return $response;
@@ -287,7 +305,7 @@ class AAM_Service_Jwt
287
  *
288
  * @return WP_REST_Response
289
  * @version 6.0.0
290
- * @todo Remove in 6.5.0
291
  */
292
  public function refreshTokenDeprecated(WP_REST_Request $request)
293
  {
@@ -303,18 +321,19 @@ class AAM_Service_Jwt
303
  *
304
  * @return WP_REST_Response
305
  *
 
306
  * @since 6.1.0 Enriched error response with more details
307
  * @since 6.0.0 Initial implementation of the method
308
  *
309
  * @access public
310
- * @version 6.1.0
311
  */
312
  public function refreshToken(WP_REST_Request $request)
313
  {
314
  $jwt = $request->get_param('jwt');
315
- $result = AAM_Core_Jwt_Issuer::getInstance()->validateToken($jwt);
316
 
317
- if ($result->isValid === true) {
318
  if (!empty($result->refreshable)) {
319
  // calculate the new expiration
320
  $issuedAt = new DateTime();
@@ -324,11 +343,11 @@ class AAM_Service_Jwt
324
  $exp = new DateTime();
325
  $exp->add($issuedAt->diff($expires));
326
 
327
- $new = $this->issueToken($result->userId, $jwt, $exp, true);
328
 
329
  $response = new WP_REST_Response(array(
330
- 'token' => $new->token,
331
- 'token_expires' => $new->claims['exp'],
332
  ));
333
  } else {
334
  $response = new WP_REST_Response(array(
@@ -339,8 +358,8 @@ class AAM_Service_Jwt
339
  } else {
340
  $response = new WP_REST_Response(array(
341
  'code' => 'rest_jwt_validation_failure',
342
- 'reason' => $result->reason
343
- ), $result->status);
344
  }
345
 
346
  return $response;
@@ -353,18 +372,19 @@ class AAM_Service_Jwt
353
  *
354
  * @return WP_REST_Response
355
  *
 
356
  * @since 6.1.0 Enriched error response with more details
357
  * @since 6.0.0 Initial implementation of the method
358
  *
359
  * @access public
360
- * @version 6.1.0
361
  */
362
  public function revokeToken(WP_REST_Request $request)
363
  {
364
  $jwt = $request->get_param('jwt');
365
- $claims = AAM_Core_Jwt_Issuer::getInstance()->validateToken($jwt);
366
 
367
- if ($claims->isValid === true) {
368
  if ($this->revokeUserToken($claims->userId, $jwt)) {
369
  $response = new WP_REST_Response(
370
  array('message' => 'Token revoked successfully'), 200
@@ -378,8 +398,8 @@ class AAM_Service_Jwt
378
  } else {
379
  $response = new WP_REST_Response(array(
380
  'code' => 'rest_jwt_validation_failure',
381
- 'reason' => $claims->reason
382
- ), $claims->status);
383
  }
384
 
385
  return $response;
@@ -397,13 +417,14 @@ class AAM_Service_Jwt
397
  *
398
  * @return array
399
  *
 
400
  * @since 6.6.2 https://github.com/aamplugin/advanced-access-manager/issues/139
401
  * @since 6.6.0 https://github.com/aamplugin/advanced-access-manager/issues/100
402
  * @since 6.4.0 Added the ability to issue refreshable token
403
  * @since 6.0.0 Initial implementation of the method
404
  *
405
  * @access public
406
- * @version 6.6.2
407
  */
408
  public function prepareLoginResponse(
409
  array $response, WP_REST_Request $request, $user
@@ -435,8 +456,7 @@ class AAM_Service_Jwt
435
  $jwt = $this->issueToken($user->ID, null, null, $refreshable);
436
 
437
  $response['jwt'] = array(
438
- 'token' => $jwt->token,
439
- 'token_expires' => $jwt->claims['exp']
440
  );
441
  }
442
 
@@ -446,15 +466,18 @@ class AAM_Service_Jwt
446
  /**
447
  * Issue JWT token
448
  *
449
- * @param int $userId
450
- * @param string $replace
451
- * @param string $expires
452
- * @param boolean $refreshable
453
  *
454
  * @return object
455
  *
 
 
 
456
  * @access public
457
- * @version 6.0.0
458
  */
459
  public function issueToken(
460
  $userId,
@@ -462,19 +485,22 @@ class AAM_Service_Jwt
462
  $expires = null,
463
  $refreshable = false
464
  ) {
465
- $result = AAM_Core_Jwt_Issuer::getInstance()->issueToken(
466
- array(
467
- 'userId' => $userId,
468
- 'revocable' => true,
469
- 'refreshable' => $refreshable
470
- ),
471
- $expires
472
  );
473
 
 
 
 
 
 
 
474
  // Finally register token so it can be revoked
475
- $this->registerToken($userId, $result->token, $replace);
476
 
477
- return $result;
478
  }
479
 
480
  /**
@@ -548,11 +574,12 @@ class AAM_Service_Jwt
548
  *
549
  * @return bool
550
  *
 
551
  * @since 6.6.0 https://github.com/aamplugin/advanced-access-manager/issues/118
552
  * @since 6.0.0 Initial implementation of the method
553
  *
554
  * @access public
555
- * @version 6.6.0
556
  */
557
  public function revokeUserToken($userId, $token)
558
  {
@@ -561,7 +588,7 @@ class AAM_Service_Jwt
561
  foreach($this->getTokenRegistry($userId) as $item) {
562
  if ($token !== $item) {
563
  $filtered[] = $item;
564
- } else {
565
  // Also delete user session if any is active. The downside here is
566
  // that if user logged in with different token, he still is going to
567
  // be logged out because AAM does not track the token that user used
@@ -581,8 +608,11 @@ class AAM_Service_Jwt
581
  *
582
  * @return int
583
  *
 
 
 
584
  * @access public
585
- * @version 6.0.0
586
  */
587
  public function determineUser($userId)
588
  {
@@ -590,9 +620,9 @@ class AAM_Service_Jwt
590
  $token = $this->extractToken();
591
 
592
  if (!empty($token)) {
593
- $result = AAM_Core_Jwt_Issuer::getInstance()->validateToken($token->jwt);
594
 
595
- if ($result->isValid === true) {
596
  // Verify that user is can be logged in
597
  $user = apply_filters(
598
  'aam_verify_user_filter', new WP_User($result->userId)
@@ -621,19 +651,24 @@ class AAM_Service_Jwt
621
  *
622
  * @return mixed
623
  *
 
 
 
624
  * @access public
625
- * @version 6.0.0
626
  */
627
  public function getJwtClaim($value, $prop)
628
  {
629
  $token = $this->extractToken();
630
 
631
  if ($token) {
632
- $claims = AAM_Core_Jwt_Issuer::getInstance()->extractTokenClaims(
633
  $token->jwt
634
  );
635
 
636
- $value = (property_exists($claims, $prop) ? $claims->$prop : null);
 
 
637
  }
638
 
639
  return $value;
@@ -644,22 +679,27 @@ class AAM_Service_Jwt
644
  *
645
  * @return void
646
  *
 
647
  * @since 6.5.2 Fixed https://github.com/aamplugin/advanced-access-manager/issues/117
648
  * @since 6.5.0 Fixed https://github.com/aamplugin/advanced-access-manager/issues/98
649
  * @since 6.0.0 Initial implementation of the method
650
  *
651
  * @access public
652
- * @version 6.5.2
653
  */
654
  public function authenticateUser()
655
  {
656
  $token = $this->extractToken();
657
- $claims = AAM_Core_Jwt_Issuer::getInstance()->extractTokenClaims($token->jwt);
658
 
659
- // Check if account is active
660
- $user = apply_filters('aam_verify_user_filter', new WP_User($claims->userId));
 
 
 
 
661
 
662
- if (!is_wp_error($user)) {
663
  wp_set_current_user($claims->userId);
664
  wp_set_auth_cookie($claims->userId);
665
 
10
  /**
11
  * JWT Token service
12
  *
13
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/221
14
+ * https://github.com/aamplugin/advanced-access-manager/issues/224
15
  * @since 6.6.2 https://github.com/aamplugin/advanced-access-manager/issues/139
16
  * @since 6.6.1 https://github.com/aamplugin/advanced-access-manager/issues/136
17
  * @since 6.6.0 https://github.com/aamplugin/advanced-access-manager/issues/129
28
  * @since 6.0.0 Initial implementation of the class
29
  *
30
  * @package AAM
31
+ * @version 6.9.0
32
  */
33
  class AAM_Service_Jwt
34
  {
257
  *
258
  * @return WP_REST_Response
259
  *
260
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/221
261
  * @since 6.1.0 Enriched error response with more details
262
  * @since 6.0.0 Initial implementation of the method
263
  *
264
  * @access public
265
+ * @version 6.9.0
266
  */
267
  public function validateToken(WP_REST_Request $request)
268
  {
269
  $jwt = $request->get_param('jwt');
270
+ $result = AAM_Core_Jwt_Manager::getInstance()->validate($jwt);
271
 
272
+ if (!is_wp_error($result)) {
273
  $response = new WP_REST_Response($result);
274
+
275
+ // If token is "revocable", make sure that claimed user still has
276
+ // the token in the meta
277
+ if (!empty($result->revocable)) {
278
+ $tokens = get_user_option(
279
+ AAM_Service_Jwt::DB_OPTION, $result->userId
280
+ );
281
+
282
+ if (!is_array($tokens) || !in_array($jwt, $tokens, true)) {
283
+ $response = new WP_REST_Response(array(
284
+ 'code' => 'rest_jwt_validation_failure',
285
+ 'reason' => __('Token has been revoked', AAM_KEY)
286
+ ), 410);
287
+ }
288
+ }
289
  } else {
290
  $response = new WP_REST_Response(array(
291
  'code' => 'rest_jwt_validation_failure',
292
+ 'reason' => $result->get_error_message()
293
+ ), 400);
294
  }
295
 
296
  return $response;
305
  *
306
  * @return WP_REST_Response
307
  * @version 6.0.0
308
+ * @todo Remove in 7.0.0
309
  */
310
  public function refreshTokenDeprecated(WP_REST_Request $request)
311
  {
321
  *
322
  * @return WP_REST_Response
323
  *
324
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/221
325
  * @since 6.1.0 Enriched error response with more details
326
  * @since 6.0.0 Initial implementation of the method
327
  *
328
  * @access public
329
+ * @version 6.9.0
330
  */
331
  public function refreshToken(WP_REST_Request $request)
332
  {
333
  $jwt = $request->get_param('jwt');
334
+ $result = AAM_Core_Jwt_Manager::getInstance()->validate($jwt);
335
 
336
+ if (!is_wp_error($result)) {
337
  if (!empty($result->refreshable)) {
338
  // calculate the new expiration
339
  $issuedAt = new DateTime();
343
  $exp = new DateTime();
344
  $exp->add($issuedAt->diff($expires));
345
 
346
+ $token = $this->issueToken($result->userId, $jwt, $exp, true);
347
 
348
  $response = new WP_REST_Response(array(
349
+ 'token' => $token,
350
+ 'token_expires' => $exp->getTimestamp(),
351
  ));
352
  } else {
353
  $response = new WP_REST_Response(array(
358
  } else {
359
  $response = new WP_REST_Response(array(
360
  'code' => 'rest_jwt_validation_failure',
361
+ 'reason' => $result->get_error_message()
362
+ ), 400);
363
  }
364
 
365
  return $response;
372
  *
373
  * @return WP_REST_Response
374
  *
375
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/221
376
  * @since 6.1.0 Enriched error response with more details
377
  * @since 6.0.0 Initial implementation of the method
378
  *
379
  * @access public
380
+ * @version 6.9.0
381
  */
382
  public function revokeToken(WP_REST_Request $request)
383
  {
384
  $jwt = $request->get_param('jwt');
385
+ $claims = AAM_Core_Jwt_Manager::getInstance()->validate($jwt);
386
 
387
+ if (!is_wp_error($claims)) {
388
  if ($this->revokeUserToken($claims->userId, $jwt)) {
389
  $response = new WP_REST_Response(
390
  array('message' => 'Token revoked successfully'), 200
398
  } else {
399
  $response = new WP_REST_Response(array(
400
  'code' => 'rest_jwt_validation_failure',
401
+ 'reason' => $claims->get_error_message()
402
+ ), 400);
403
  }
404
 
405
  return $response;
417
  *
418
  * @return array
419
  *
420
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/221
421
  * @since 6.6.2 https://github.com/aamplugin/advanced-access-manager/issues/139
422
  * @since 6.6.0 https://github.com/aamplugin/advanced-access-manager/issues/100
423
  * @since 6.4.0 Added the ability to issue refreshable token
424
  * @since 6.0.0 Initial implementation of the method
425
  *
426
  * @access public
427
+ * @version 6.9.0
428
  */
429
  public function prepareLoginResponse(
430
  array $response, WP_REST_Request $request, $user
456
  $jwt = $this->issueToken($user->ID, null, null, $refreshable);
457
 
458
  $response['jwt'] = array(
459
+ 'token' => $jwt
 
460
  );
461
  }
462
 
466
  /**
467
  * Issue JWT token
468
  *
469
+ * @param int $userId
470
+ * @param string $replace
471
+ * @param DateTime $expires
472
+ * @param boolean $refreshable
473
  *
474
  * @return object
475
  *
476
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/221
477
+ * @since 6.0.0 Initial implementation of the method
478
+ *
479
  * @access public
480
+ * @version 6.9.0
481
  */
482
  public function issueToken(
483
  $userId,
485
  $expires = null,
486
  $refreshable = false
487
  ) {
488
+ $claims = array(
489
+ 'userId' => $userId,
490
+ 'revocable' => true,
491
+ 'refreshable' => $refreshable
 
 
 
492
  );
493
 
494
+ if (is_a($expires, DateTime::class)) {
495
+ $claims['exp'] = $expires->getTimestamp();
496
+ }
497
+
498
+ $token = AAM_Core_Jwt_Manager::getInstance()->encode($claims);
499
+
500
  // Finally register token so it can be revoked
501
+ $this->registerToken($userId, $token, $replace);
502
 
503
+ return $token;
504
  }
505
 
506
  /**
574
  *
575
  * @return bool
576
  *
577
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/224
578
  * @since 6.6.0 https://github.com/aamplugin/advanced-access-manager/issues/118
579
  * @since 6.0.0 Initial implementation of the method
580
  *
581
  * @access public
582
+ * @version 6.9.0
583
  */
584
  public function revokeUserToken($userId, $token)
585
  {
588
  foreach($this->getTokenRegistry($userId) as $item) {
589
  if ($token !== $item) {
590
  $filtered[] = $item;
591
+ } elseif (get_current_user_id() !== $userId) {
592
  // Also delete user session if any is active. The downside here is
593
  // that if user logged in with different token, he still is going to
594
  // be logged out because AAM does not track the token that user used
608
  *
609
  * @return int
610
  *
611
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/221
612
+ * @since 6.0.0 Initial implementation of the method
613
+ *
614
  * @access public
615
+ * @version 6.9.0
616
  */
617
  public function determineUser($userId)
618
  {
620
  $token = $this->extractToken();
621
 
622
  if (!empty($token)) {
623
+ $result = AAM_Core_Jwt_Manager::getInstance()->validate($token->jwt);
624
 
625
+ if (!is_wp_error($result)) {
626
  // Verify that user is can be logged in
627
  $user = apply_filters(
628
  'aam_verify_user_filter', new WP_User($result->userId)
651
  *
652
  * @return mixed
653
  *
654
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/221
655
+ * @since 6.0.0 Initial implementation of the method
656
+ *
657
  * @access public
658
+ * @version 6.9.0
659
  */
660
  public function getJwtClaim($value, $prop)
661
  {
662
  $token = $this->extractToken();
663
 
664
  if ($token) {
665
+ $claims = AAM_Core_Jwt_Manager::getInstance()->validate(
666
  $token->jwt
667
  );
668
 
669
+ if (!is_wp_error($claims)) {
670
+ $value = (property_exists($claims, $prop) ? $claims->$prop : null);
671
+ }
672
  }
673
 
674
  return $value;
679
  *
680
  * @return void
681
  *
682
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/221
683
  * @since 6.5.2 Fixed https://github.com/aamplugin/advanced-access-manager/issues/117
684
  * @since 6.5.0 Fixed https://github.com/aamplugin/advanced-access-manager/issues/98
685
  * @since 6.0.0 Initial implementation of the method
686
  *
687
  * @access public
688
+ * @version 6.9.0
689
  */
690
  public function authenticateUser()
691
  {
692
  $token = $this->extractToken();
693
+ $claims = AAM_Core_Jwt_Manager::getInstance()->validate($token->jwt);
694
 
695
+ if (!is_wp_error($claims)) {
696
+ // Check if account is active
697
+ $user = apply_filters(
698
+ 'aam_verify_user_filter', new WP_User($claims->userId)
699
+ );
700
+ }
701
 
702
+ if (isset($user) && !is_wp_error($user)) {
703
  wp_set_current_user($claims->userId);
704
  wp_set_auth_cookie($claims->userId);
705
 
application/Service/Toolbar.php CHANGED
@@ -10,11 +10,12 @@
10
  /**
11
  * Toolbar service
12
  *
13
- * @since 6.4.0 Fixed https://github.com/aamplugin/advanced-access-manager/issues/76
 
14
  * @since 6.0.0 Initial implementation of the class
15
  *
16
  * @package AAM
17
- * @version 6.4.0
18
  */
19
  class AAM_Service_Toolbar
20
  {
@@ -86,15 +87,19 @@ class AAM_Service_Toolbar
86
  *
87
  * @return void
88
  *
 
 
 
89
  * @access public
90
  * @global object $wp_admin_bar
91
- * @version 6.0.0
92
  */
93
  public function cacheAdminBar()
94
  {
95
  global $wp_admin_bar;
96
 
97
  $reflection = new ReflectionClass(get_class($wp_admin_bar));
 
98
 
99
  if ($reflection->hasProperty('nodes')) {
100
  $prop = $reflection->getProperty('nodes');
@@ -103,7 +108,6 @@ class AAM_Service_Toolbar
103
  $nodes = $prop->getValue($wp_admin_bar);
104
 
105
  if (isset($nodes['root'])) {
106
- $cache = array();
107
  foreach ($nodes['root']->children as $node) {
108
  $cache = array_merge($cache, $node->children);
109
  }
@@ -116,12 +120,6 @@ class AAM_Service_Toolbar
116
  }
117
  AAM_Core_API::updateOption(self::DB_OPTION, $cache);
118
  }
119
- } else {
120
- _doing_it_wrong(
121
- __CLASS__ . '::' . __METHOD__,
122
- 'Toolbar object does not have "nodes" property',
123
- AAM_VERSION
124
- );
125
  }
126
 
127
  return $cache;
10
  /**
11
  * Toolbar service
12
  *
13
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/223
14
+ * @since 6.4.0 https://github.com/aamplugin/advanced-access-manager/issues/76
15
  * @since 6.0.0 Initial implementation of the class
16
  *
17
  * @package AAM
18
+ * @version 6.9.0
19
  */
20
  class AAM_Service_Toolbar
21
  {
87
  *
88
  * @return void
89
  *
90
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/223
91
+ * @since 6.0.0 Initial implementation of the method
92
+ *
93
  * @access public
94
  * @global object $wp_admin_bar
95
+ * @version 6.9.0
96
  */
97
  public function cacheAdminBar()
98
  {
99
  global $wp_admin_bar;
100
 
101
  $reflection = new ReflectionClass(get_class($wp_admin_bar));
102
+ $cache = array();
103
 
104
  if ($reflection->hasProperty('nodes')) {
105
  $prop = $reflection->getProperty('nodes');
108
  $nodes = $prop->getValue($wp_admin_bar);
109
 
110
  if (isset($nodes['root'])) {
 
111
  foreach ($nodes['root']->children as $node) {
112
  $cache = array_merge($cache, $node->children);
113
  }
120
  }
121
  AAM_Core_API::updateOption(self::DB_OPTION, $cache);
122
  }
 
 
 
 
 
 
123
  }
124
 
125
  return $cache;
autoloader.php CHANGED
@@ -5,15 +5,17 @@
5
  * LICENSE: This file is subject to the terms and conditions defined in *
6
  * file 'license.txt', which is part of this source code package. *
7
  * ======================================================================
8
- *
9
- * @version 6.0.0
10
  */
11
 
12
  /**
13
  * Project auto-loader
14
  *
15
  * @package AAM
16
- * @version 6.0.0
 
 
 
 
17
  */
18
  class AAM_Autoloader
19
  {
@@ -23,10 +25,17 @@ class AAM_Autoloader
23
  *
24
  * @var array
25
  *
 
 
 
26
  * @access protected
27
- * @version 6.0.0
 
 
28
  */
29
- protected static $class_map = array();
 
 
30
 
31
  /**
32
  * Add new index
5
  * LICENSE: This file is subject to the terms and conditions defined in *
6
  * file 'license.txt', which is part of this source code package. *
7
  * ======================================================================
 
 
8
  */
9
 
10
  /**
11
  * Project auto-loader
12
  *
13
  * @package AAM
14
+ *
15
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/221
16
+ * @since 6.0.0 Initial implementation of the class
17
+ *
18
+ * @version 6.9.0
19
  */
20
  class AAM_Autoloader
21
  {
25
  *
26
  * @var array
27
  *
28
+ * @since 6.9.0 https://github.com/aamplugin/advanced-access-manager/issues/221
29
+ * @since 6.0.0 Initial implementation of the property
30
+ *
31
  * @access protected
32
+ * @version 6.9.0
33
+ *
34
+ * @todo Remove in 7.0.0
35
  */
36
+ protected static $class_map = array(
37
+ 'Firebase\JWT\JWT' => __DIR__ . '/firebase/JWT.php',
38
+ );
39
 
40
  /**
41
  * Add new index
readme.txt CHANGED
@@ -4,7 +4,7 @@ Tags: access control, membership, backend menu, user role, restricted content, s
4
  Requires at least: 4.7.0
5
  Requires PHP: 5.6.0
6
  Tested up to: 6.0.2
7
- Stable tag: 6.8.5
8
 
9
  All you need to manage access to WordPress websites on the frontend, backend and API levels for any role, user or visitors.
10
 
@@ -91,6 +91,11 @@ We take security and privacy very seriously, that is why there are several non-n
91
 
92
  == Changelog ==
93
 
 
 
 
 
 
94
  = 6.8.5 =
95
  * Fixed: Redirect may not always work [https://github.com/aamplugin/advanced-access-manager/issues/214](https://github.com/aamplugin/advanced-access-manager/issues/214)
96
  * Fixed: PHP Notice: Undefined index: 404.redirect.** [https://github.com/aamplugin/advanced-access-manager/issues/215](https://github.com/aamplugin/advanced-access-manager/issues/215)
4
  Requires at least: 4.7.0
5
  Requires PHP: 5.6.0
6
  Tested up to: 6.0.2
7
+ Stable tag: 6.9.0
8
 
9
  All you need to manage access to WordPress websites on the frontend, backend and API levels for any role, user or visitors.
10
 
91
 
92
  == Changelog ==
93
 
94
+ = 6.9.0 =
95
+ * Fixed: Revoking JWT token via UI causes current user to logout [https://github.com/aamplugin/advanced-access-manager/issues/224](https://github.com/aamplugin/advanced-access-manager/issues/224)
96
+ * Fixed: Notice: Undefined variable: cache [https://github.com/aamplugin/advanced-access-manager/issues/223](https://github.com/aamplugin/advanced-access-manager/issues/223)
97
+ * Changed: Update JWT vendor [https://github.com/aamplugin/advanced-access-manager/issues/221](https://github.com/aamplugin/advanced-access-manager/issues/221)
98
+
99
  = 6.8.5 =
100
  * Fixed: Redirect may not always work [https://github.com/aamplugin/advanced-access-manager/issues/214](https://github.com/aamplugin/advanced-access-manager/issues/214)
101
  * Fixed: PHP Notice: Undefined index: 404.redirect.** [https://github.com/aamplugin/advanced-access-manager/issues/215](https://github.com/aamplugin/advanced-access-manager/issues/215)
vendor/autoload.php CHANGED
@@ -7,18 +7,6 @@
7
  * ======================================================================
8
  */
9
 
10
- // Firebase for JWT token handling
11
- if (!class_exists('Firebase\JWT')) {
12
- spl_autoload_register(function($class_name) {
13
- if (strpos($class_name, 'Firebase\JWT') === 0) {
14
- require __DIR__ . '/firebase/BeforeValidException.php';
15
- require __DIR__ . '/firebase/ExpiredException.php';
16
- require __DIR__ . '/firebase/SignatureInvalidException.php';
17
- require __DIR__ . '/firebase/JWT.php';
18
- }
19
- });
20
- }
21
-
22
  //Composer Semver for Policy dependency versioning
23
  if (!class_exists('Composer\Semver')) {
24
  spl_autoload_register(function($class_name) {
@@ -32,7 +20,7 @@ if (!class_exists('Composer\Semver')) {
32
  }
33
 
34
  if (!empty($filename) && file_exists($filename)) {
35
- require $filename;
36
  }
37
  });
38
  }
7
  * ======================================================================
8
  */
9
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  //Composer Semver for Policy dependency versioning
11
  if (!class_exists('Composer\Semver')) {
12
  spl_autoload_register(function($class_name) {
20
  }
21
 
22
  if (!empty($filename) && file_exists($filename)) {
23
+ require_once $filename;
24
  }
25
  });
26
  }
vendor/firebase/BeforeValidException.php DELETED
@@ -1,7 +0,0 @@
1
- <?php
2
- namespace Firebase\JWT;
3
-
4
- class BeforeValidException extends \UnexpectedValueException
5
- {
6
-
7
- }
 
 
 
 
 
 
 
vendor/firebase/ExpiredException.php DELETED
@@ -1,7 +0,0 @@
1
- <?php
2
- namespace Firebase\JWT;
3
-
4
- class ExpiredException extends \UnexpectedValueException
5
- {
6
-
7
- }
 
 
 
 
 
 
 
vendor/firebase/JWT.php CHANGED
@@ -1,389 +1,63 @@
1
  <?php
2
 
 
 
 
 
 
 
 
3
  namespace Firebase\JWT;
4
- use \DomainException;
5
- use \InvalidArgumentException;
6
- use \UnexpectedValueException;
7
- use \DateTime;
8
 
9
  /**
10
- * JSON Web Token implementation, based on this spec:
11
- * https://tools.ietf.org/html/rfc7519
12
  *
13
- * PHP version 5
14
  *
15
- * @category Authentication
16
- * @package Authentication_JWT
17
- * @author Neuman Vong <neuman@twilio.com>
18
- * @author Anant Narayanan <anant@php.net>
19
- * @license https://opensource.org/licenses/BSD-3-Clause 3-clause BSD
20
- * @link https://github.com/firebase/php-jwt
21
  */
22
  class JWT
23
  {
24
 
25
  /**
26
- * When checking nbf, iat or expiration times,
27
- * we want to provide some extra leeway time to
28
- * account for clock skew.
29
- */
30
- public static $leeway = 0;
31
-
32
- /**
33
- * Allow the current timestamp to be specified.
34
- * Useful for fixing a value within unit testing.
35
- *
36
- * Will default to PHP time() value if null.
37
- */
38
- public static $timestamp = null;
39
-
40
- public static $supported_algs = array(
41
- 'HS256' => array('hash_hmac', 'SHA256'),
42
- 'HS512' => array('hash_hmac', 'SHA512'),
43
- 'HS384' => array('hash_hmac', 'SHA384'),
44
- 'RS256' => array('openssl', 'SHA256'),
45
- 'RS384' => array('openssl', 'SHA384'),
46
- 'RS512' => array('openssl', 'SHA512'),
47
- );
48
-
49
- /**
50
- * Decodes a JWT string into a PHP object.
51
- *
52
- * @param string $jwt The JWT
53
- * @param string|array $key The key, or map of keys.
54
- * If the algorithm used is asymmetric, this is the public key
55
- * @param array $allowed_algs List of supported verification algorithms
56
- * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256'
57
- *
58
- * @return object The JWT's payload as a PHP object
59
- *
60
- * @throws UnexpectedValueException Provided JWT was invalid
61
- * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed
62
- * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf'
63
- * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat'
64
- * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim
65
- *
66
- * @uses jsonDecode
67
- * @uses urlsafeB64Decode
68
- */
69
- public static function decode($jwt, $key, array $allowed_algs = array())
70
- {
71
- $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp;
72
-
73
- if (empty($key)) {
74
- throw new InvalidArgumentException('Key may not be empty');
75
- }
76
- $tks = explode('.', $jwt);
77
- if (count($tks) != 3) {
78
- throw new UnexpectedValueException('Wrong number of segments');
79
- }
80
-
81
- if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($tks[0])))) {
82
- throw new UnexpectedValueException('Invalid header encoding');
83
- }
84
- if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($tks[1]))) {
85
- throw new UnexpectedValueException('Invalid claims encoding');
86
- }
87
- if (false === ($sig = static::urlsafeB64Decode($tks[2]))) {
88
- throw new UnexpectedValueException('Invalid signature encoding');
89
- }
90
- if (empty($header->alg)) {
91
- throw new UnexpectedValueException('Empty algorithm');
92
- }
93
- if (empty(static::$supported_algs[$header->alg])) {
94
- throw new UnexpectedValueException('Algorithm not supported');
95
- }
96
- if (!in_array($header->alg, $allowed_algs)) {
97
- throw new UnexpectedValueException('Algorithm not allowed');
98
- }
99
- if (is_array($key) || $key instanceof \ArrayAccess) {
100
- if (isset($header->kid)) {
101
- if (!isset($key[$header->kid])) {
102
- throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
103
- }
104
- $key = $key[$header->kid];
105
- } else {