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