2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mybmw.internal.handler;
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.mockito.Mockito.*;
17 import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.*;
19 import java.nio.charset.StandardCharsets;
20 import java.security.KeyFactory;
21 import java.security.MessageDigest;
22 import java.security.PublicKey;
23 import java.security.spec.X509EncodedKeySpec;
24 import java.util.Base64;
26 import javax.crypto.Cipher;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.eclipse.jetty.client.api.ContentResponse;
31 import org.eclipse.jetty.client.api.Request;
32 import org.eclipse.jetty.client.util.StringContentProvider;
33 import org.eclipse.jetty.http.HttpHeader;
34 import org.eclipse.jetty.util.MultiMap;
35 import org.eclipse.jetty.util.UrlEncoded;
36 import org.eclipse.jetty.util.ssl.SslContextFactory;
37 import org.junit.jupiter.api.Test;
38 import org.openhab.binding.mybmw.internal.MyBMWConfiguration;
39 import org.openhab.binding.mybmw.internal.dto.auth.AuthQueryResponse;
40 import org.openhab.binding.mybmw.internal.dto.auth.AuthResponse;
41 import org.openhab.binding.mybmw.internal.dto.auth.ChinaPublicKeyResponse;
42 import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenExpiration;
43 import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenResponse;
44 import org.openhab.binding.mybmw.internal.util.FileReader;
45 import org.openhab.binding.mybmw.internal.utils.BimmerConstants;
46 import org.openhab.binding.mybmw.internal.utils.Constants;
47 import org.openhab.binding.mybmw.internal.utils.Converter;
48 import org.openhab.core.io.net.http.HttpClientFactory;
49 import org.openhab.core.util.StringUtils;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
54 * The {@link AuthTest} test authorization flow
56 * @author Bernd Weymann - Initial contribution
60 private final Logger logger = LoggerFactory.getLogger(AuthTest.class);
66 SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
67 HttpClient authHttpClient = new HttpClient(sslContextFactory);
69 authHttpClient.start();
70 Request firstRequest = authHttpClient
71 .newRequest("https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
72 + "/eadrax-ucs/v1/presentation/oauth/config");
73 firstRequest.header("ocp-apim-subscription-key",
74 BimmerConstants.OCP_APIM_KEYS.get(BimmerConstants.REGION_ROW));
75 firstRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
77 ContentResponse firstResponse = firstRequest.send();
78 logger.info(firstResponse.getContentAsString());
79 AuthQueryResponse aqr = Converter.getGson().fromJson(firstResponse.getContentAsString(),
80 AuthQueryResponse.class);
82 String verifierBytes = StringUtils.getRandomAlphabetic(64).toLowerCase();
83 String codeVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(verifierBytes.getBytes());
85 MessageDigest digest = MessageDigest.getInstance("SHA-256");
86 byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8));
87 String codeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
89 String stateBytes = StringUtils.getRandomAlphabetic(16).toLowerCase();
90 String state = Base64.getUrlEncoder().withoutPadding().encodeToString(stateBytes.getBytes());
92 String authUrl = aqr.gcdmBaseUrl + BimmerConstants.OAUTH_ENDPOINT;
94 Request loginRequest = authHttpClient.POST(authUrl);
95 loginRequest.header("Content-Type", "application/x-www-form-urlencoded");
97 MultiMap<String> baseParams = new MultiMap<String>();
98 baseParams.put("client_id", aqr.clientId);
99 baseParams.put("response_type", "code");
100 baseParams.put("redirect_uri", aqr.returnUrl);
101 baseParams.put("state", state);
102 baseParams.put("nonce", "login_nonce");
103 baseParams.put("scope", String.join(" ", aqr.scopes));
104 baseParams.put("code_challenge", codeChallenge);
105 baseParams.put("code_challenge_method", "S256");
107 MultiMap<String> loginParams = new MultiMap<String>(baseParams);
108 loginParams.put("grant_type", "authorization_code");
109 loginParams.put("username", user);
110 loginParams.put("password", pwd);
111 loginRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
112 UrlEncoded.encode(loginParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
113 ContentResponse secondResonse = loginRequest.send();
114 logger.info(secondResonse.getContentAsString());
115 String authCode = getAuthCode(secondResonse.getContentAsString());
116 logger.info(authCode);
118 MultiMap<String> authParams = new MultiMap<String>(baseParams);
119 authParams.put("authorization", authCode);
120 Request authRequest = authHttpClient.POST(authUrl).followRedirects(false);
121 authRequest.header("Content-Type", "application/x-www-form-urlencoded");
122 authRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
123 UrlEncoded.encode(authParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
124 ContentResponse authResponse = authRequest.send();
125 logger.info("{}", authResponse.getHeaders());
126 logger.info("Response " + authResponse.getHeaders().get(HttpHeader.LOCATION));
127 String code = AuthTest.codeFromUrl(authResponse.getHeaders().get(HttpHeader.LOCATION));
128 logger.info("Code " + code);
131 logger.info(aqr.tokenEndpoint);
132 // AuthenticationStore authenticationStore = authHttpClient.getAuthenticationStore();
133 // BasicAuthentication ba = new BasicAuthentication(new URI(aqr.tokenEndpoint), Authentication.ANY_REALM,
134 // aqr.clientId, aqr.clientSecret);
135 // authenticationStore.addAuthentication(ba);
136 Request codeRequest = authHttpClient.POST(aqr.tokenEndpoint);
137 String basicAuth = "Basic "
138 + Base64.getUrlEncoder().encodeToString((aqr.clientId + ":" + aqr.clientSecret).getBytes());
139 logger.info(basicAuth);
140 codeRequest.header("Content-Type", "application/x-www-form-urlencoded");
141 codeRequest.header(AUTHORIZATION, basicAuth);
143 MultiMap<String> codeParams = new MultiMap<String>();
144 codeParams.put("code", code);
145 codeParams.put("code_verifier", codeVerifier);
146 codeParams.put("redirect_uri", aqr.returnUrl);
147 codeParams.put("grant_type", "authorization_code");
148 codeRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
149 UrlEncoded.encode(codeParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
150 ContentResponse codeResponse = codeRequest.send();
151 logger.info(codeResponse.getContentAsString());
152 AuthResponse ar = Converter.getGson().fromJson(codeResponse.getContentAsString(), AuthResponse.class);
153 Token t = new Token();
154 t.setType(ar.tokenType);
155 t.setToken(ar.accessToken);
156 t.setExpiration(ar.expiresIn);
157 logger.info(t.getBearerToken());
162 HttpClient apiHttpClient = new HttpClient(sslContextFactory);
163 apiHttpClient.start();
165 MultiMap<String> vehicleParams = new MultiMap<String>();
166 vehicleParams.put("tireGuardMode", "ENABLED");
167 vehicleParams.put("appDateTime", Long.toString(System.currentTimeMillis()));
168 vehicleParams.put("apptimezone", "60");
169 // vehicleRequest.param("tireGuardMode", "ENABLED");
170 // vehicleRequest.param("appDateTime", Long.toString(System.currentTimeMillis()));
171 // vehicleRequest.param("apptimezone", "60.0");
173 // // logger.info(vehicleParams);
174 // vehicleRequest.content(new StringContentProvider(CONTENT_TYPE_JSON_ENCODED, vehicleParams.toString(),
175 // StandardCharsets.UTF_8));
176 // logger.info(vehicleRequest.getHeaders());
177 String params = UrlEncoded.encode(vehicleParams, StandardCharsets.UTF_8, false);
179 String vehicleUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
180 + "/eadrax-vcs/v1/vehicles";
181 logger.info(vehicleUrl);
182 Request vehicleRequest = apiHttpClient.newRequest(vehicleUrl + "?" + params);//
183 // .param("tireGuardMode", "ENABLED")
184 // .param("appDateTime", Long.toString(System.currentTimeMillis())).param("apptimezone", "60.0");
185 // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded");
186 vehicleRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
187 vehicleRequest.header("accept", "application/json");
188 vehicleRequest.header("accept-language", "de");
189 vehicleRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
190 // vehicleRequest.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded");
191 // vehicleRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
192 // UrlEncoded.encode(vehicleParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
193 ContentResponse vehicleResponse = vehicleRequest.send();
194 logger.info("Vehicle Status {} {}", vehicleResponse.getStatus(), vehicleResponse.getContentAsString());
199 MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
200 chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
201 chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
202 params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
204 String chargeStatisticsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
205 + "/eadrax-chs/v1/charging-statistics";
206 Request chargeStatisticsRequest = apiHttpClient.newRequest(chargeStatisticsUrl)
207 .param("vin", "WBY1Z81040V905639").param("currentDate", Converter.getCurrentISOTime());
208 logger.info("{}", chargeStatisticsUrl);
209 // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded");
210 chargeStatisticsRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
211 chargeStatisticsRequest.header("accept", "application/json");
212 chargeStatisticsRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
213 chargeStatisticsRequest.header("accept-language", "de");
215 // MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
216 // chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
217 // chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
219 // params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
220 logger.info("{}", params);
221 chargeStatisticsRequest
222 .content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8));
224 ContentResponse chargeStatisticsResponse = chargeStatisticsRequest.send();
225 logger.info("{}", chargeStatisticsResponse.getStatus());
226 logger.info("{}", chargeStatisticsResponse.getReason());
227 logger.info("{}", chargeStatisticsResponse.getContentAsString());
232 MultiMap<String> chargeSessionsParams = new MultiMap<String>();
233 chargeSessionsParams.put("vin", "WBY1Z81040V905639");
234 chargeSessionsParams.put("maxResults", "40");
235 chargeSessionsParams.put("include_date_picker", "true");
237 params = UrlEncoded.encode(chargeSessionsParams, StandardCharsets.UTF_8, false);
239 String chargeSessionsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
240 + "/eadrax-chs/v1/charging-sessions";
241 Request chargeSessionsRequest = apiHttpClient.newRequest(chargeSessionsUrl + "?" + params);
242 logger.info("{}", chargeSessionsUrl);
243 // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded");
244 chargeSessionsRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
245 chargeSessionsRequest.header("accept", "application/json");
246 chargeSessionsRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
247 chargeSessionsRequest.header("accept-language", "de");
249 // MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
250 // chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
251 // chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
253 // params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
254 logger.info("{}", params);
255 // chargeStatisticsRequest
256 // .content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8));
258 ContentResponse chargeSessionsResponse = chargeSessionsRequest.send();
259 logger.info("{}", chargeSessionsResponse.getStatus());
260 logger.info("{}", chargeSessionsResponse.getReason());
261 logger.info("{}", chargeSessionsResponse.getContentAsString());
263 String chargingControlUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
264 + "/eadrax-vrccs/v2/presentation/remote-commands/WBY1Z81040V905639/charging-control";
265 Request chargingControlRequest = apiHttpClient.POST(chargingControlUrl);
266 logger.info("{}", chargingControlUrl);
267 // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded");
268 chargingControlRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
269 chargingControlRequest.header("accept", "application/json");
270 chargingControlRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
271 chargingControlRequest.header("accept-language", "de");
272 chargingControlRequest.header("Content-Type", CONTENT_TYPE_JSON_ENCODED);
274 // String content = FileReader.readFileInString("src/test/resources/responses/charging-profile.json");
275 // logger.info("{}", content);
276 // ChargeProfile cpc = Converter.getGson().fromJson(content, ChargeProfile.class);
277 // String contentTranfsorm = Converter.getGson().toJson(cpc);
278 // String profile = "{chargingProfile:" + contentTranfsorm + "}";
279 // logger.info("{}", profile);
280 // chargingControlRequest
281 // .content(new StringContentProvider(CONTENT_TYPE_JSON_ENCODED, params, StandardCharsets.UTF_8));
283 // chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
284 // chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
286 // params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
288 // ContentResponse chargingControlResponse = chargingControlRequest.send();
289 // logger.info("{}", chargingControlResponse.getStatus());
290 // logger.info("{}", chargingControlResponse.getReason());
291 // logger.info("{}", chargingControlResponse.getContentAsString());
293 } catch (Exception e) {
294 logger.error("{}", e.getMessage());
298 private String getAuthCode(String response) {
299 String[] keys = response.split("&");
300 for (int i = 0; i < keys.length; i++) {
301 if (keys[i].startsWith(AUTHORIZATION)) {
302 String authCode = keys[i].split("=")[1];
303 authCode = authCode.split("\"")[0];
307 return Constants.EMPTY;
310 public static String codeFromUrl(String encodedUrl) {
311 final MultiMap<String> tokenMap = new MultiMap<String>();
312 UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
313 final StringBuilder codeFound = new StringBuilder();
314 tokenMap.forEach((key, value) -> {
315 if (!value.isEmpty()) {
316 String val = value.get(0);
317 if (key.endsWith(CODE)) {
318 codeFound.append(val);
322 return codeFound.toString();
326 public void testJWTDeserialze() {
327 String accessTokenResponseStr = FileReader
328 .readFileInString("src/test/resources/responses/auth/auth_cn_login_pwd.json");
329 ChinaTokenResponse cat = Converter.getGson().fromJson(accessTokenResponseStr, ChinaTokenResponse.class);
331 // https://www.baeldung.com/java-jwt-token-decode
332 String token = cat.data.accessToken;
333 String[] chunks = token.split("\\.");
334 String tokenJwtDecodeStr = new String(Base64.getUrlDecoder().decode(chunks[1]));
335 ChinaTokenExpiration cte = Converter.getGson().fromJson(tokenJwtDecodeStr, ChinaTokenExpiration.class);
336 Token t = new Token();
338 t.setType(cat.data.tokenType);
339 t.setExpirationTotal(cte.exp);
341 "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJEVU1NWSQxJEEkMTYzNzcwNzkxNjc4MiIsIm5iZiI6MTYzNzcwNzkxNiwiZXhwIjoxNjM3NzExMjE2LCJpYXQiOjE2Mzc3MDc5MTZ9.hpi-P97W68g7avGwu9dcBRapIsaG4F8MwOdPHe6PuTA",
342 t.getBearerToken(), "Token");
345 public void testChina() {
346 SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
347 HttpClient authHttpClient = new HttpClient(sslContextFactory);
349 authHttpClient.start();
350 HttpClientFactory mockHCF = mock(HttpClientFactory.class);
351 when(mockHCF.getCommonHttpClient()).thenReturn(authHttpClient);
352 MyBMWConfiguration config = new MyBMWConfiguration();
353 config.region = BimmerConstants.REGION_CHINA;
354 config.userName = "Hello User";
355 config.password = "Hello Password";
356 MyBMWProxy bmwProxy = new MyBMWProxy(mockHCF, config);
357 bmwProxy.updateTokenChina();
358 } catch (Exception e) {
359 logger.warn("Exception: " + e.getMessage());
364 public void testPublicKey() {
365 String publicKeyResponseStr = FileReader.readFileInString("src/test/resources/responses/auth/china-key.json");
366 ChinaPublicKeyResponse pkr = Converter.getGson().fromJson(publicKeyResponseStr, ChinaPublicKeyResponse.class);
367 String publicKeyStr = pkr.data.value;
368 String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "")
369 .replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "").replace("\\r", "")
370 .replace("\\n", "").trim();
371 byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
372 X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
375 kf = KeyFactory.getInstance("RSA");
376 PublicKey publicKey = kf.generatePublic(spec);
377 // https://www.thexcoders.net/java-ciphers-rsa/
378 Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
379 cipher.init(Cipher.ENCRYPT_MODE, publicKey);
380 byte[] encryptedBytes = cipher.doFinal("Hello World".getBytes());
381 String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes);
382 logger.info(encodedPassword);
383 } catch (Exception e) {
384 assertTrue(false, "Excpetion: " + e.getMessage());
388 public void testChinaToken() {
389 SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
390 HttpClient authHttpClient = new HttpClient(sslContextFactory);
392 authHttpClient.start();
393 String url = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_CHINA)
394 + BimmerConstants.CHINA_PUBLIC_KEY;
395 Request oauthQueryRequest = authHttpClient.newRequest(url);
396 oauthQueryRequest.header(X_USER_AGENT,
397 String.format(BimmerConstants.BRAND_BMW, BimmerConstants.BRAND_BMW, BimmerConstants.REGION_ROW));
399 ContentResponse publicKeyResponse = oauthQueryRequest.send();
400 ChinaPublicKeyResponse pkr = Converter.getGson().fromJson(publicKeyResponse.getContentAsString(),
401 ChinaPublicKeyResponse.class);
402 // https://stackoverflow.com/questions/11410770/load-rsa-public-key-from-file
404 String publicKeyStr = pkr.data.value;
405 // String cleanPublicKeyStr = pkr.data.value.replaceAll("(\r\n|\n)", Constants.EMPTY);
406 String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "")
407 .replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "");
408 byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
409 X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
411 KeyFactory kf = KeyFactory.getInstance("RSA");
412 PublicKey publicKey = kf.generatePublic(spec);
413 // https://www.thexcoders.net/java-ciphers-rsa/
414 Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
415 cipher.init(Cipher.ENCRYPT_MODE, publicKey);
416 byte[] encryptedBytes = cipher.doFinal("Hello World".getBytes());
417 String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes);
418 logger.info(encodedPassword);
419 } catch (Exception e) {
420 assertTrue(false, "Excpetion: " + e.getMessage());