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