Jwt_Token_Service.php
13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
<?php
namespace Wpo\Services;
use WP_Error;
use \Wpo\Core\WordPress_Helpers;
use \Wpo\Services\Log_Service;
use \Wpo\Services\Options_Service;
// Prevent public access to this script
defined('ABSPATH') or die();
if (!class_exists('\Wpo\Services\Jwt_Token_Service')) {
class Jwt_Token_Service
{
/**
* Allow the current timestamp to be specified.
* Useful for fixing a value within unit testing.
*
* Will default to PHP time() value if null.
*/
public static $timestamp = null;
/**
* @param string $token
* @return WP_Error|object WP_Error when an error occurred, otherwise the token's claims.
*/
public static function validate_signature($token, $retry = false)
{
Log_Service::write_log('DEBUG', '##### -> ' . __METHOD__);
if (empty($token)) {
return new WP_Error(
'ArgumentException',
__METHOD__ . ' -> JWT token not found.'
);
}
$token_arr = explode('.', $token);
// Token should explored in three segments header, body, signature
if (sizeof($token_arr) != 3) {
return new WP_Error(
'ArgumentException',
__METHOD__ . ' -> JWT token does not contain the expected 3 segments header, body and signature.'
);
}
// Header
$headers_enc = $token_arr[0];
$header = \json_decode(WordPress_Helpers::base64_url_decode($headers_enc));
if (\json_last_error() !== JSON_ERROR_NONE || !\property_exists($header, 'kid')) {
return new WP_Error(
'ArgumentException',
__METHOD__ . ' -> Failed to retrieve expected JWT token header and corresponding kid property.'
);
}
// Payload (claims)
$claims_enc = $token_arr[1];
$claims = \json_decode(WordPress_Helpers::base64_url_decode($claims_enc));
// Signature
$sig_enc = $token_arr[2];
$sig = WordPress_Helpers::base64_url_decode($sig_enc);
// Try get the public keys
$key = self::get_key_from_set($header->kid);
if (empty($key)) {
return new WP_Error(
'WebKeySetNotFoundException',
__METHOD__ . ' -> Could not retrieve a tenant and application specific JSON Web Key Set and thus the JWT token cannot be verified successfully.'
);
}
// create Crypt_RSA
$rsa = new \phpseclib\Crypt\RSA();
// load public key with modulus and exponent
$public = [
'n' => new \phpseclib\Math\BigInteger(WordPress_Helpers::base64_url_decode($key->n), 256),
'e' => new \phpseclib\Math\BigInteger(WordPress_Helpers::base64_url_decode($key->e), 256),
];
$rsa->loadKey($public);
// set hash algorithm
$rsa->setHash('sha256');
$rsa->setSignatureMode(\phpseclib\Crypt\RSA::SIGNATURE_PKCS1);
// Verify
try {
$verified = $rsa->verify($headers_enc . '.' . $claims_enc, $sig);
} catch (\Exception $e) {
$verified = false;
}
if (!$verified) {
delete_option('wpo365_msft_key');
if (!$retry) {
Log_Service::write_log('WARN', __METHOD__ . ' -> Verification of the signature of the JWT token failed a first time. Cached tokens have been deleted and a 2nd attempt will be made.');
return self::validate_signature($token, true);
}
return new WP_Error(
'SignatureValidationException',
__METHOD__ . ' -> Verification of the signature of the JWT token failed a second time. No more attempts will be made.'
);
}
// Check nbf, iat and exp
$timestamp = is_null(static::$timestamp) ? time() : static::$timestamp;
$leeway = Options_Service::get_global_numeric_var('leeway');
$leeway = empty($leeway) ? 300 : $leeway;
// Check if the nbf if it is defined. This is the time that the
// token can actually be used. If it's not yet that time, abort.
if (isset($claims->nbf) && $claims->nbf > ($timestamp + $leeway)) {
return new WP_Error(
'SignatureValidationException',
__METHOD__ . ' -> Cannot handle JWT token prior to ' . date(\DateTime::ISO8601, $claims->nbf) . '. Please check the system clock of your WordPress server.'
);
}
// Check that this token has been created before 'now'. This prevents
// using tokens that have been created for later use (and haven't
// correctly used the nbf claim).
if (isset($claims->iat) && $claims->iat > ($timestamp + $leeway)) {
return new WP_Error(
'SignatureValidationException',
__METHOD__ . ' -> Cannot handle JWT token prior to ' . date(\DateTime::ISO8601, $claims->iat) . '. Please check the system clock of your WordPress server.'
);
}
// Check if this token has expired.
if (isset($claims->exp) && ($timestamp - $leeway) >= $claims->exp) {
return new WP_Error(
'SignatureValidationException',
__METHOD__ . ' -> Cannot handle expired JWT token after ' . date(\DateTime::ISO8601, $claims->exp) . '. Please check the system clock of your WordPress server.'
);
}
Log_Service::write_log('DEBUG', __METHOD__ . ' -> Verification of signature of the JWT token was successful');
return $claims;
}
/**
* Tries to retrieve the JSON Web Key Set issued for the current tenant and application either from cache or
* by loading the JWKS from the jwks_uri specified in the open-id configuration for the current tenant.
*
* @since 14.0
*
* @return stdClass JSON Web Key Set to be used to verify a JWT token issued for the current tenant and registered application as a typical PHP stdClass.
*/
private static function get_key_from_set($kid)
{
Log_Service::write_log('DEBUG', '##### -> ' . __METHOD__);
/**
* Try get the key from cache.
*/
$cached_key = get_site_option('wpo365_msft_key');
if (!empty($cached_key)) {
if (\is_object($cached_key) && \property_exists($cached_key, 'kid') && $cached_key->kid == $kid && \property_exists($cached_key, 'e') && \property_exists($cached_key, 'n')) {
Log_Service::write_log('DEBUG', __METHOD__ . ' -> Found cached JSON Web Key Set to verify the JWT token signature');
return $cached_key;
}
Log_Service::write_log('DEBUG', __METHOD__ . ' -> Deleted cached JSON Web Key Set to verify the JWT token signature');
delete_option('wpo365_msft_key');
}
/**
* Get the JSON Web Key Sets
*/
$jwks_uri = self::get_json_web_key_sets_uri();
$skip_ssl_verify = !Options_Service::get_global_boolean_var('skip_host_verification');
$response = wp_remote_get(
$jwks_uri,
array(
'method' => 'GET',
'timeout' => 15,
'sslverify' => $skip_ssl_verify,
)
);
if (is_wp_error($response)) {
$warning = 'Error occured whilst trying to retrieve JSON Web Key Sets: ' . $response->get_error_message();
Log_Service::write_log('ERROR', __METHOD__ . " -> $warning");
return null;
}
$body = wp_remote_retrieve_body($response);
$keys = \json_decode($body);
if (\json_last_error() !== JSON_ERROR_NONE) {
Log_Service::write_log('ERROR', __METHOD__ . ' -> Error when trying to decode the JSON Web Key Sets [ ' . \json_last_error_msg() . ' ]');
Log_Service::write_log('DEBUG', $keys);
return null;
}
if (\is_object($keys) && \property_exists($keys, 'keys')) {
$keys = $keys->keys;
}
$keys_arr = \is_array($keys) ? $keys : array($keys);
foreach ($keys_arr as $key) {
if (\property_exists($key, 'kid') && $key->kid == $kid && \property_exists($key, 'n') && \property_exists($key, 'e')) {
update_site_option('wpo365_msft_key', $key);
return $key;
}
}
return null;
}
/**
* Get the JSON Web Key Sets URI for the given tenant and app. In case of multi-tenancy enabled the generic keys will be returned.
*
* @since 14.1
*/
private static function get_json_web_key_sets_uri()
{
Log_Service::write_log('DEBUG', '##### -> ' . __METHOD__);
if (Options_Service::get_global_boolean_var('multi_tenanted')) {
$jwks_uri = 'https://login.microsoftonline.com/common/discovery/v2.0/keys';
Log_Service::write_log('DEBUG', __METHOD__ . " -> Trying to retrieve the Open ID configuration for the designated tenant and application $jwks_uri");
return $jwks_uri;
}
/**
* Get the JSON Web Key Sets URI (jwks_uri) from the openid configuration.
*/
$request_service = Request_Service::get_instance();
$request = $request_service->get_request($GLOBALS['WPO_CONFIG']['request_id']);
$mode = $request->get_item('mode');
$use_mail_config = $mode == 'mailAuthorize';
$directory_id = $use_mail_config ? Options_Service::get_aad_option('mail_tenant_id') : Options_Service::get_aad_option('tenant_id');
$application_id = $use_mail_config ? Options_Service::get_aad_option('mail_application_id') : Options_Service::get_aad_option('application_id');
if (!$use_mail_config && Options_Service::get_global_boolean_var('use_b2c') && \class_exists('\Wpo\Services\Id_Token_Service_B2c')) {
$b2c_domain_name = Options_Service::get_global_string_var('b2c_domain_name');
$b2c_policy_name = Options_Service::get_global_string_var('b2c_policy_name');
/**
* @since 20.x Support for custom b2c login domain e.g. login.contoso.com
*/
$b2c_domain = Options_Service::get_global_string_var('b2c_custom_domain');
if (empty($b2c_domain)) {
$b2c_domain = sprintf('https://%s.b2clogin.com/', $b2c_domain_name);
} else {
$b2c_domain = sprintf('https://%s', trailingslashit($b2c_domain));
}
$open_id_config_url = "$b2c_domain$directory_id/$b2c_policy_name/v2.0/.well-known/openid-configuration";
} else {
$open_id_config_url = "https://login.microsoftonline.com/$directory_id/v2.0/.well-known/openid-configuration?appid=$application_id";
}
Log_Service::write_log('DEBUG', __METHOD__ . " -> Trying to retrieve the Open ID configuration for the designated tenant and application $open_id_config_url");
$skip_ssl_verify = !Options_Service::get_global_boolean_var('skip_host_verification');
$response = wp_remote_get(
$open_id_config_url,
array(
'method' => 'GET',
'timeout' => 15,
'sslverify' => $skip_ssl_verify,
)
);
if (is_wp_error($response)) {
$warning = 'Error occured whilst getting JSON Web Key Sets URI: ' . $response->get_error_message();
Log_Service::write_log('ERROR', __METHOD__ . " -> $warning");
return null;
}
$body = wp_remote_retrieve_body($response);
$open_id_config = json_decode($body);
if (\json_last_error() !== JSON_ERROR_NONE || !isset($open_id_config->jwks_uri)) {
Log_Service::write_log('ERROR', __METHOD__ . ' -> jwks_uri property not found [ ' . \json_last_error_msg() . ' ]');
Log_Service::write_log('DEBUG', $open_id_config);
return null;
}
return $open_id_config->jwks_uri;
}
}
}