]> git.basschouten.com Git - openhab-addons.git/blob
6dd9a85317de4e447d9d654001e40a93e60f73a7
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mybmw.internal.handler;
14
15 import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.*;
16
17 import java.nio.charset.StandardCharsets;
18 import java.security.KeyFactory;
19 import java.security.MessageDigest;
20 import java.security.PublicKey;
21 import java.security.spec.X509EncodedKeySpec;
22 import java.util.Base64;
23 import java.util.Optional;
24 import java.util.concurrent.TimeUnit;
25
26 import javax.crypto.Cipher;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.client.HttpClient;
31 import org.eclipse.jetty.client.HttpResponseException;
32 import org.eclipse.jetty.client.api.ContentResponse;
33 import org.eclipse.jetty.client.api.Request;
34 import org.eclipse.jetty.client.api.Result;
35 import org.eclipse.jetty.client.util.BufferingResponseListener;
36 import org.eclipse.jetty.client.util.StringContentProvider;
37 import org.eclipse.jetty.http.HttpHeader;
38 import org.eclipse.jetty.util.MultiMap;
39 import org.eclipse.jetty.util.UrlEncoded;
40 import org.openhab.binding.mybmw.internal.MyBMWConfiguration;
41 import org.openhab.binding.mybmw.internal.VehicleConfiguration;
42 import org.openhab.binding.mybmw.internal.dto.auth.AuthQueryResponse;
43 import org.openhab.binding.mybmw.internal.dto.auth.AuthResponse;
44 import org.openhab.binding.mybmw.internal.dto.auth.ChinaPublicKeyResponse;
45 import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenExpiration;
46 import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenResponse;
47 import org.openhab.binding.mybmw.internal.dto.network.NetworkError;
48 import org.openhab.binding.mybmw.internal.handler.simulation.Injector;
49 import org.openhab.binding.mybmw.internal.utils.BimmerConstants;
50 import org.openhab.binding.mybmw.internal.utils.Constants;
51 import org.openhab.binding.mybmw.internal.utils.Converter;
52 import org.openhab.binding.mybmw.internal.utils.HTTPConstants;
53 import org.openhab.binding.mybmw.internal.utils.ImageProperties;
54 import org.openhab.core.io.net.http.HttpClientFactory;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 /**
59  * The {@link MyBMWProxy} This class holds the important constants for the BMW Connected Drive Authorization.
60  * They
61  * are taken from the Bimmercode from github {@link https://github.com/bimmerconnected/bimmer_connected}
62  * File defining these constants
63  * {@link https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/account.py}
64  * https://customer.bmwgroup.com/one/app/oauth.js
65  *
66  * @author Bernd Weymann - Initial contribution
67  * @author Norbert Truchsess - edit & send of charge profile
68  */
69 @NonNullByDefault
70 public class MyBMWProxy {
71     private final Logger logger = LoggerFactory.getLogger(MyBMWProxy.class);
72     private Optional<RemoteServiceHandler> remoteServiceHandler = Optional.empty();
73     private final Token token = new Token();
74     private final HttpClient httpClient;
75     private final MyBMWConfiguration configuration;
76
77     /**
78      * URLs taken from https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/const.py
79      */
80     final String vehicleUrl;
81     final String remoteCommandUrl;
82     final String remoteStatusUrl;
83     final String serviceExecutionAPI = "/executeService";
84     final String serviceExecutionStateAPI = "/serviceExecutionStatus";
85     final String remoteServiceEADRXstatusUrl = BimmerConstants.API_REMOTE_SERVICE_BASE_URL
86             + "eventStatus?eventId={event_id}";
87
88     public MyBMWProxy(HttpClientFactory httpClientFactory, MyBMWConfiguration config) {
89         httpClient = httpClientFactory.getCommonHttpClient();
90         configuration = config;
91
92         vehicleUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
93                 + BimmerConstants.API_VEHICLES;
94
95         remoteCommandUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
96                 + BimmerConstants.API_REMOTE_SERVICE_BASE_URL;
97         remoteStatusUrl = remoteCommandUrl + "eventStatus";
98     }
99
100     public synchronized void call(final String url, final boolean post, final @Nullable String encoding,
101             final @Nullable String params, final String brand, final ResponseCallback callback) {
102         // only executed in "simulation mode"
103         // SimulationTest.testSimulationOff() assures Injector is off when releasing
104         if (Injector.isActive()) {
105             if (url.equals(vehicleUrl)) {
106                 ((StringResponseCallback) callback).onResponse(Injector.getDiscovery());
107             } else if (url.endsWith(vehicleUrl)) {
108                 ((StringResponseCallback) callback).onResponse(Injector.getStatus());
109             } else {
110                 logger.debug("Simulation of {} not supported", url);
111             }
112             return;
113         }
114
115         // return in case of unknown brand
116         if (!BimmerConstants.ALL_BRANDS.contains(brand.toLowerCase())) {
117             logger.warn("Unknown Brand {}", brand);
118             return;
119         }
120
121         final Request req;
122         final String completeUrl;
123
124         if (post) {
125             completeUrl = url;
126             req = httpClient.POST(url);
127             if (encoding != null) {
128                 req.header(HttpHeader.CONTENT_TYPE, encoding);
129                 if (CONTENT_TYPE_URL_ENCODED.equals(encoding)) {
130                     req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8));
131                 } else if (CONTENT_TYPE_JSON_ENCODED.equals(encoding)) {
132                     req.content(new StringContentProvider(CONTENT_TYPE_JSON_ENCODED, params, StandardCharsets.UTF_8));
133                 }
134             }
135         } else {
136             completeUrl = params == null ? url : url + Constants.QUESTION + params;
137             req = httpClient.newRequest(completeUrl);
138         }
139         req.header(HttpHeader.AUTHORIZATION, getToken().getBearerToken());
140         req.header(HTTPConstants.X_USER_AGENT,
141                 String.format(BimmerConstants.X_USER_AGENT, brand, configuration.region));
142         req.header(HttpHeader.ACCEPT_LANGUAGE, configuration.language);
143         if (callback instanceof ByteResponseCallback) {
144             req.header(HttpHeader.ACCEPT, "image/png");
145         } else {
146             req.header(HttpHeader.ACCEPT, CONTENT_TYPE_JSON_ENCODED);
147         }
148
149         req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(new BufferingResponseListener() {
150             @NonNullByDefault({})
151             @Override
152             public void onComplete(Result result) {
153                 if (result.getResponse().getStatus() != 200) {
154                     NetworkError error = new NetworkError();
155                     error.url = completeUrl;
156                     error.status = result.getResponse().getStatus();
157                     if (result.getResponse().getReason() != null) {
158                         error.reason = result.getResponse().getReason();
159                     } else {
160                         error.reason = result.getFailure().getMessage();
161                     }
162                     error.params = result.getRequest().getParams().toString();
163                     logger.debug("HTTP Error {}", error.toString());
164                     callback.onError(error);
165                 } else {
166                     if (callback instanceof StringResponseCallback responseCallback) {
167                         responseCallback.onResponse(getContentAsString());
168                     } else if (callback instanceof ByteResponseCallback responseCallback) {
169                         responseCallback.onResponse(getContent());
170                     } else {
171                         logger.error("unexpected reponse type {}", callback.getClass().getName());
172                     }
173                 }
174             }
175         });
176     }
177
178     public void get(String url, @Nullable String coding, @Nullable String params, final String brand,
179             ResponseCallback callback) {
180         call(url, false, coding, params, brand, callback);
181     }
182
183     public void post(String url, @Nullable String coding, @Nullable String params, final String brand,
184             ResponseCallback callback) {
185         call(url, true, coding, params, brand, callback);
186     }
187
188     /**
189      * request all vehicles for one specific brand
190      *
191      * @param brand
192      * @param callback
193      */
194     public void requestVehicles(String brand, StringResponseCallback callback) {
195         // calculate necessary parameters for query
196         MultiMap<String> vehicleParams = new MultiMap<String>();
197         vehicleParams.put(BimmerConstants.TIRE_GUARD_MODE, Constants.ENABLED);
198         vehicleParams.put(BimmerConstants.APP_DATE_TIME, Long.toString(System.currentTimeMillis()));
199         vehicleParams.put(BimmerConstants.APP_TIMEZONE, Integer.toString(Converter.getOffsetMinutes()));
200         String params = UrlEncoded.encode(vehicleParams, StandardCharsets.UTF_8, false);
201         get(vehicleUrl + "?" + params, null, null, brand, callback);
202     }
203
204     /**
205      * request vehicles for all possible brands
206      *
207      * @param callback
208      */
209     public void requestVehicles(StringResponseCallback callback) {
210         BimmerConstants.ALL_BRANDS.forEach(brand -> {
211             requestVehicles(brand, callback);
212         });
213     }
214
215     public void requestImage(VehicleConfiguration config, ImageProperties props, ByteResponseCallback callback) {
216         final String localImageUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
217                 + "/eadrax-ics/v3/presentation/vehicles/" + config.vin + "/images?carView=" + props.viewport;
218         get(localImageUrl, null, null, config.vehicleBrand, callback);
219     }
220
221     /**
222      * request charge statistics for electric vehicles
223      *
224      * @param callback
225      */
226     public void requestChargeStatistics(VehicleConfiguration config, StringResponseCallback callback) {
227         MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
228         chargeStatisticsParams.put("vin", config.vin);
229         chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
230         String params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
231         String chargeStatisticsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
232                 + "/eadrax-chs/v1/charging-statistics?" + params;
233         get(chargeStatisticsUrl, null, null, config.vehicleBrand, callback);
234     }
235
236     /**
237      * request charge statistics for electric vehicles
238      *
239      * @param callback
240      */
241     public void requestChargeSessions(VehicleConfiguration config, StringResponseCallback callback) {
242         MultiMap<String> chargeSessionsParams = new MultiMap<String>();
243         chargeSessionsParams.put("vin", "WBY1Z81040V905639");
244         chargeSessionsParams.put("maxResults", "40");
245         chargeSessionsParams.put("include_date_picker", "true");
246         String params = UrlEncoded.encode(chargeSessionsParams, StandardCharsets.UTF_8, false);
247         String chargeSessionsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
248                 + "/eadrax-chs/v1/charging-sessions?" + params;
249
250         get(chargeSessionsUrl, null, null, config.vehicleBrand, callback);
251     }
252
253     RemoteServiceHandler getRemoteServiceHandler(VehicleHandler vehicleHandler) {
254         remoteServiceHandler = Optional.of(new RemoteServiceHandler(vehicleHandler, this));
255         return remoteServiceHandler.get();
256     }
257
258     // Token handling
259
260     /**
261      * Gets new token if old one is expired or invalid. In case of error the token remains.
262      * So if token refresh fails the corresponding requests will also fail and update the
263      * Thing status accordingly.
264      *
265      * @return token
266      */
267     public Token getToken() {
268         if (!token.isValid()) {
269             boolean tokenUpdateSuccess = false;
270             switch (configuration.region) {
271                 case BimmerConstants.REGION_CHINA:
272                     tokenUpdateSuccess = updateTokenChina();
273                     break;
274                 case BimmerConstants.REGION_NORTH_AMERICA:
275                     tokenUpdateSuccess = updateToken();
276                     break;
277                 case BimmerConstants.REGION_ROW:
278                     tokenUpdateSuccess = updateToken();
279                     break;
280                 default:
281                     logger.warn("Region {} not supported", configuration.region);
282                     break;
283             }
284             if (!tokenUpdateSuccess) {
285                 logger.debug("Authorization failed!");
286             }
287         }
288         return token;
289     }
290
291     /**
292      * Everything is catched by surroundig try catch
293      * - HTTP Exceptions
294      * - JSONSyntax Exceptions
295      * - potential NullPointer Exceptions
296      *
297      * @return
298      */
299     @SuppressWarnings("null")
300     public synchronized boolean updateToken() {
301         try {
302             /*
303              * Step 1) Get basic values for further queries
304              */
305             String authValuesUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region)
306                     + BimmerConstants.API_OAUTH_CONFIG;
307             Request authValuesRequest = httpClient.newRequest(authValuesUrl);
308             authValuesRequest.header(ACP_SUBSCRIPTION_KEY, BimmerConstants.OCP_APIM_KEYS.get(configuration.region));
309             authValuesRequest.header(X_USER_AGENT,
310                     String.format(BimmerConstants.X_USER_AGENT, BimmerConstants.BRAND_BMW, configuration.region));
311
312             ContentResponse authValuesResponse = authValuesRequest.send();
313             if (authValuesResponse.getStatus() != 200) {
314                 throw new HttpResponseException("URL: " + authValuesRequest.getURI() + ", Error: "
315                         + authValuesResponse.getStatus() + ", Message: " + authValuesResponse.getContentAsString(),
316                         authValuesResponse);
317             }
318             AuthQueryResponse aqr = Converter.getGson().fromJson(authValuesResponse.getContentAsString(),
319                     AuthQueryResponse.class);
320
321             /*
322              * Step 2) Calculate values for base parameters
323              */
324             String verfifierBytes = Converter.getRandomString(64);
325             String codeVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(verfifierBytes.getBytes());
326             MessageDigest digest = MessageDigest.getInstance("SHA-256");
327             byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8));
328             String codeChallange = Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
329             String stateBytes = Converter.getRandomString(16);
330             String state = Base64.getUrlEncoder().withoutPadding().encodeToString(stateBytes.getBytes());
331
332             MultiMap<String> baseParams = new MultiMap<String>();
333             baseParams.put(CLIENT_ID, aqr.clientId);
334             baseParams.put(RESPONSE_TYPE, CODE);
335             baseParams.put(REDIRECT_URI, aqr.returnUrl);
336             baseParams.put(STATE, state);
337             baseParams.put(NONCE, BimmerConstants.LOGIN_NONCE);
338             baseParams.put(SCOPE, String.join(Constants.SPACE, aqr.scopes));
339             baseParams.put(CODE_CHALLENGE, codeChallange);
340             baseParams.put(CODE_CHALLENGE_METHOD, "S256");
341
342             /**
343              * Step 3) Authorization with username and password
344              */
345             String loginUrl = aqr.gcdmBaseUrl + BimmerConstants.OAUTH_ENDPOINT;
346             Request loginRequest = httpClient.POST(loginUrl);
347             loginRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED);
348
349             MultiMap<String> loginParams = new MultiMap<String>(baseParams);
350             loginParams.put(GRANT_TYPE, BimmerConstants.AUTHORIZATION_CODE);
351             loginParams.put(USERNAME, configuration.userName);
352             loginParams.put(PASSWORD, configuration.password);
353             loginRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
354                     UrlEncoded.encode(loginParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
355             ContentResponse loginResponse = loginRequest.send();
356             if (loginResponse.getStatus() != 200) {
357                 throw new HttpResponseException("URL: " + loginRequest.getURI() + ", Error: "
358                         + loginResponse.getStatus() + ", Message: " + loginResponse.getContentAsString(),
359                         loginResponse);
360             }
361             String authCode = getAuthCode(loginResponse.getContentAsString());
362
363             /**
364              * Step 4) Authorize with code
365              */
366             Request authRequest = httpClient.POST(loginUrl).followRedirects(false);
367             MultiMap<String> authParams = new MultiMap<String>(baseParams);
368             authParams.put(AUTHORIZATION, authCode);
369             authRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED);
370             authRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
371                     UrlEncoded.encode(authParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
372             ContentResponse authResponse = authRequest.send();
373             if (authResponse.getStatus() != 302) {
374                 throw new HttpResponseException("URL: " + authRequest.getURI() + ", Error: " + authResponse.getStatus()
375                         + ", Message: " + authResponse.getContentAsString(), authResponse);
376             }
377             String code = MyBMWProxy.codeFromUrl(authResponse.getHeaders().get(HttpHeader.LOCATION));
378
379             /**
380              * Step 5) Request token
381              */
382             Request codeRequest = httpClient.POST(aqr.tokenEndpoint);
383             String basicAuth = "Basic "
384                     + Base64.getUrlEncoder().encodeToString((aqr.clientId + ":" + aqr.clientSecret).getBytes());
385             codeRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED);
386             codeRequest.header(AUTHORIZATION, basicAuth);
387
388             MultiMap<String> codeParams = new MultiMap<String>();
389             codeParams.put(CODE, code);
390             codeParams.put(CODE_VERIFIER, codeVerifier);
391             codeParams.put(REDIRECT_URI, aqr.returnUrl);
392             codeParams.put(GRANT_TYPE, BimmerConstants.AUTHORIZATION_CODE);
393             codeRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
394                     UrlEncoded.encode(codeParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
395             ContentResponse codeResponse = codeRequest.send();
396             if (codeResponse.getStatus() != 200) {
397                 throw new HttpResponseException("URL: " + codeRequest.getURI() + ", Error: " + codeResponse.getStatus()
398                         + ", Message: " + codeResponse.getContentAsString(), codeResponse);
399             }
400             AuthResponse ar = Converter.getGson().fromJson(codeResponse.getContentAsString(), AuthResponse.class);
401             token.setType(ar.tokenType);
402             token.setToken(ar.accessToken);
403             token.setExpiration(ar.expiresIn);
404             return true;
405         } catch (Exception e) {
406             logger.warn("Authorization Exception: {}", e.getMessage());
407         }
408         return false;
409     }
410
411     private String getAuthCode(String response) {
412         String[] keys = response.split("&");
413         for (int i = 0; i < keys.length; i++) {
414             if (keys[i].startsWith(AUTHORIZATION)) {
415                 String authCode = keys[i].split("=")[1];
416                 authCode = authCode.split("\"")[0];
417                 return authCode;
418             }
419         }
420         return Constants.EMPTY;
421     }
422
423     public static String codeFromUrl(String encodedUrl) {
424         final MultiMap<String> tokenMap = new MultiMap<String>();
425         UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
426         final StringBuilder codeFound = new StringBuilder();
427         tokenMap.forEach((key, value) -> {
428             if (!value.isEmpty()) {
429                 String val = value.get(0);
430                 if (key.endsWith(CODE)) {
431                     codeFound.append(val);
432                 }
433             }
434         });
435         return codeFound.toString();
436     }
437
438     @SuppressWarnings("null")
439     public synchronized boolean updateTokenChina() {
440         try {
441             /**
442              * Step 1) get public key
443              */
444             String publicKeyUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_CHINA)
445                     + BimmerConstants.CHINA_PUBLIC_KEY;
446             Request oauthQueryRequest = httpClient.newRequest(publicKeyUrl);
447             oauthQueryRequest.header(HttpHeader.USER_AGENT, BimmerConstants.USER_AGENT);
448             oauthQueryRequest.header(X_USER_AGENT,
449                     String.format(BimmerConstants.X_USER_AGENT, BimmerConstants.BRAND_BMW, configuration.region));
450             ContentResponse publicKeyResponse = oauthQueryRequest.send();
451             if (publicKeyResponse.getStatus() != 200) {
452                 throw new HttpResponseException("URL: " + oauthQueryRequest.getURI() + ", Error: "
453                         + publicKeyResponse.getStatus() + ", Message: " + publicKeyResponse.getContentAsString(),
454                         publicKeyResponse);
455             }
456             ChinaPublicKeyResponse pkr = Converter.getGson().fromJson(publicKeyResponse.getContentAsString(),
457                     ChinaPublicKeyResponse.class);
458
459             /**
460              * Step 2) Encode password with public key
461              */
462             // https://www.baeldung.com/java-read-pem-file-keys
463             String publicKeyStr = pkr.data.value;
464             String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "")
465                     .replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "").replace("\\r", "")
466                     .replace("\\n", "").trim();
467             byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
468             X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
469             KeyFactory kf = KeyFactory.getInstance("RSA");
470             PublicKey publicKey = kf.generatePublic(spec);
471             // https://www.thexcoders.net/java-ciphers-rsa/
472             Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
473             cipher.init(Cipher.ENCRYPT_MODE, publicKey);
474             byte[] encryptedBytes = cipher.doFinal(configuration.password.getBytes());
475             String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes);
476
477             /**
478              * Step 3) Send Auth with encoded password
479              */
480             String tokenUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_CHINA)
481                     + BimmerConstants.CHINA_LOGIN;
482             Request loginRequest = httpClient.POST(tokenUrl);
483             loginRequest.header(X_USER_AGENT,
484                     String.format(BimmerConstants.X_USER_AGENT, BimmerConstants.BRAND_BMW, configuration.region));
485             String jsonContent = "{ \"mobile\":\"" + configuration.userName + "\", \"password\":\"" + encodedPassword
486                     + "\"}";
487             loginRequest.content(new StringContentProvider(jsonContent));
488             ContentResponse tokenResponse = loginRequest.send();
489             if (tokenResponse.getStatus() != 200) {
490                 throw new HttpResponseException("URL: " + loginRequest.getURI() + ", Error: "
491                         + tokenResponse.getStatus() + ", Message: " + tokenResponse.getContentAsString(),
492                         tokenResponse);
493             }
494             String authCode = getAuthCode(tokenResponse.getContentAsString());
495
496             /**
497              * Step 4) Decode access token
498              */
499             ChinaTokenResponse cat = Converter.getGson().fromJson(authCode, ChinaTokenResponse.class);
500             String token = cat.data.accessToken;
501             // https://www.baeldung.com/java-jwt-token-decode
502             String[] chunks = token.split("\\.");
503             String tokenJwtDecodeStr = new String(Base64.getUrlDecoder().decode(chunks[1]));
504             ChinaTokenExpiration cte = Converter.getGson().fromJson(tokenJwtDecodeStr, ChinaTokenExpiration.class);
505             Token t = new Token();
506             t.setToken(token);
507             t.setType(cat.data.tokenType);
508             t.setExpirationTotal(cte.exp);
509             return true;
510         } catch (Exception e) {
511             logger.warn("Authorization Exception: {}", e.getMessage());
512         }
513         return false;
514     }
515 }