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.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
53 * The {@link AuthTest} test authorization flow
55 * @author Bernd Weymann - Initial contribution
59 private final Logger logger = LoggerFactory.getLogger(AuthTest.class);
65 SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
66 HttpClient authHttpClient = new HttpClient(sslContextFactory);
68 authHttpClient.start();
69 Request firstRequest = authHttpClient
70 .newRequest("https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
71 + "/eadrax-ucs/v1/presentation/oauth/config");
72 firstRequest.header("ocp-apim-subscription-key",
73 BimmerConstants.OCP_APIM_KEYS.get(BimmerConstants.REGION_ROW));
74 firstRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
76 ContentResponse firstResponse = firstRequest.send();
77 logger.info(firstResponse.getContentAsString());
78 AuthQueryResponse aqr = Converter.getGson().fromJson(firstResponse.getContentAsString(),
79 AuthQueryResponse.class);
81 // String verifier_bytes = RandomStringUtils.randomAlphanumeric(64);
82 String verifierBytes = Converter.getRandomString(64);
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 state_bytes = RandomStringUtils.randomAlphanumeric(16);
90 String stateBytes = Converter.getRandomString(16);
91 String state = Base64.getUrlEncoder().withoutPadding().encodeToString(stateBytes.getBytes());
93 String authUrl = aqr.gcdmBaseUrl + BimmerConstants.OAUTH_ENDPOINT;
95 Request loginRequest = authHttpClient.POST(authUrl);
96 loginRequest.header("Content-Type", "application/x-www-form-urlencoded");
98 MultiMap<String> baseParams = new MultiMap<String>();
99 baseParams.put("client_id", aqr.clientId);
100 baseParams.put("response_type", "code");
101 baseParams.put("redirect_uri", aqr.returnUrl);
102 baseParams.put("state", state);
103 baseParams.put("nonce", "login_nonce");
104 baseParams.put("scope", String.join(" ", aqr.scopes));
105 baseParams.put("code_challenge", codeChallenge);
106 baseParams.put("code_challenge_method", "S256");
108 MultiMap<String> loginParams = new MultiMap<String>(baseParams);
109 loginParams.put("grant_type", "authorization_code");
110 loginParams.put("username", user);
111 loginParams.put("password", pwd);
112 loginRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
113 UrlEncoded.encode(loginParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
114 ContentResponse secondResonse = loginRequest.send();
115 logger.info(secondResonse.getContentAsString());
116 String authCode = getAuthCode(secondResonse.getContentAsString());
117 logger.info(authCode);
119 MultiMap<String> authParams = new MultiMap<String>(baseParams);
120 authParams.put("authorization", authCode);
121 Request authRequest = authHttpClient.POST(authUrl).followRedirects(false);
122 authRequest.header("Content-Type", "application/x-www-form-urlencoded");
123 authRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
124 UrlEncoded.encode(authParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
125 ContentResponse authResponse = authRequest.send();
126 logger.info("{}", authResponse.getHeaders());
127 logger.info("Response " + authResponse.getHeaders().get(HttpHeader.LOCATION));
128 String code = AuthTest.codeFromUrl(authResponse.getHeaders().get(HttpHeader.LOCATION));
129 logger.info("Code " + code);
132 logger.info(aqr.tokenEndpoint);
133 // AuthenticationStore authenticationStore = authHttpClient.getAuthenticationStore();
134 // BasicAuthentication ba = new BasicAuthentication(new URI(aqr.tokenEndpoint), Authentication.ANY_REALM,
135 // aqr.clientId, aqr.clientSecret);
136 // authenticationStore.addAuthentication(ba);
137 Request codeRequest = authHttpClient.POST(aqr.tokenEndpoint);
138 String basicAuth = "Basic "
139 + Base64.getUrlEncoder().encodeToString((aqr.clientId + ":" + aqr.clientSecret).getBytes());
140 logger.info(basicAuth);
141 codeRequest.header("Content-Type", "application/x-www-form-urlencoded");
142 codeRequest.header(AUTHORIZATION, basicAuth);
144 MultiMap<String> codeParams = new MultiMap<String>();
145 codeParams.put("code", code);
146 codeParams.put("code_verifier", codeVerifier);
147 codeParams.put("redirect_uri", aqr.returnUrl);
148 codeParams.put("grant_type", "authorization_code");
149 codeRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
150 UrlEncoded.encode(codeParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
151 ContentResponse codeResponse = codeRequest.send();
152 logger.info(codeResponse.getContentAsString());
153 AuthResponse ar = Converter.getGson().fromJson(codeResponse.getContentAsString(), AuthResponse.class);
154 Token t = new Token();
155 t.setType(ar.tokenType);
156 t.setToken(ar.accessToken);
157 t.setExpiration(ar.expiresIn);
158 logger.info(t.getBearerToken());
163 HttpClient apiHttpClient = new HttpClient(sslContextFactory);
164 apiHttpClient.start();
166 MultiMap<String> vehicleParams = new MultiMap<String>();
167 vehicleParams.put("tireGuardMode", "ENABLED");
168 vehicleParams.put("appDateTime", Long.toString(System.currentTimeMillis()));
169 vehicleParams.put("apptimezone", "60");
170 // vehicleRequest.param("tireGuardMode", "ENABLED");
171 // vehicleRequest.param("appDateTime", Long.toString(System.currentTimeMillis()));
172 // vehicleRequest.param("apptimezone", "60.0");
174 // // logger.info(vehicleParams);
175 // vehicleRequest.content(new StringContentProvider(CONTENT_TYPE_JSON_ENCODED, vehicleParams.toString(),
176 // StandardCharsets.UTF_8));
177 // logger.info(vehicleRequest.getHeaders());
178 String params = UrlEncoded.encode(vehicleParams, StandardCharsets.UTF_8, false);
180 String vehicleUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
181 + "/eadrax-vcs/v1/vehicles";
182 logger.info(vehicleUrl);
183 Request vehicleRequest = apiHttpClient.newRequest(vehicleUrl + "?" + params);//
184 // .param("tireGuardMode", "ENABLED")
185 // .param("appDateTime", Long.toString(System.currentTimeMillis())).param("apptimezone", "60.0");
186 // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded");
187 vehicleRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
188 vehicleRequest.header("accept", "application/json");
189 vehicleRequest.header("accept-language", "de");
190 vehicleRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
191 // vehicleRequest.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded");
192 // vehicleRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
193 // UrlEncoded.encode(vehicleParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
194 ContentResponse vehicleResponse = vehicleRequest.send();
195 logger.info("Vehicle Status {} {}", vehicleResponse.getStatus(), vehicleResponse.getContentAsString());
200 MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
201 chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
202 chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
203 params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
205 String chargeStatisticsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
206 + "/eadrax-chs/v1/charging-statistics";
207 Request chargeStatisticsRequest = apiHttpClient.newRequest(chargeStatisticsUrl)
208 .param("vin", "WBY1Z81040V905639").param("currentDate", Converter.getCurrentISOTime());
209 logger.info("{}", chargeStatisticsUrl);
210 // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded");
211 chargeStatisticsRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
212 chargeStatisticsRequest.header("accept", "application/json");
213 chargeStatisticsRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
214 chargeStatisticsRequest.header("accept-language", "de");
216 // MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
217 // chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
218 // chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
220 // params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
221 logger.info("{}", params);
222 chargeStatisticsRequest
223 .content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8));
225 ContentResponse chargeStatisticsResponse = chargeStatisticsRequest.send();
226 logger.info("{}", chargeStatisticsResponse.getStatus());
227 logger.info("{}", chargeStatisticsResponse.getReason());
228 logger.info("{}", chargeStatisticsResponse.getContentAsString());
233 MultiMap<String> chargeSessionsParams = new MultiMap<String>();
234 chargeSessionsParams.put("vin", "WBY1Z81040V905639");
235 chargeSessionsParams.put("maxResults", "40");
236 chargeSessionsParams.put("include_date_picker", "true");
238 params = UrlEncoded.encode(chargeSessionsParams, StandardCharsets.UTF_8, false);
240 String chargeSessionsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
241 + "/eadrax-chs/v1/charging-sessions";
242 Request chargeSessionsRequest = apiHttpClient.newRequest(chargeSessionsUrl + "?" + params);
243 logger.info("{}", chargeSessionsUrl);
244 // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded");
245 chargeSessionsRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
246 chargeSessionsRequest.header("accept", "application/json");
247 chargeSessionsRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
248 chargeSessionsRequest.header("accept-language", "de");
250 // MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
251 // chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
252 // chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
254 // params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
255 logger.info("{}", params);
256 // chargeStatisticsRequest
257 // .content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8));
259 ContentResponse chargeSessionsResponse = chargeSessionsRequest.send();
260 logger.info("{}", chargeSessionsResponse.getStatus());
261 logger.info("{}", chargeSessionsResponse.getReason());
262 logger.info("{}", chargeSessionsResponse.getContentAsString());
264 String chargingControlUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
265 + "/eadrax-vrccs/v2/presentation/remote-commands/WBY1Z81040V905639/charging-control";
266 Request chargingControlRequest = apiHttpClient.POST(chargingControlUrl);
267 logger.info("{}", chargingControlUrl);
268 // vehicleRequest.header("Content-Type", "application/x-www-form-urlencoded");
269 chargingControlRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
270 chargingControlRequest.header("accept", "application/json");
271 chargingControlRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
272 chargingControlRequest.header("accept-language", "de");
273 chargingControlRequest.header("Content-Type", CONTENT_TYPE_JSON_ENCODED);
275 // String content = FileReader.readFileInString("src/test/resources/responses/charging-profile.json");
276 // logger.info("{}", content);
277 // ChargeProfile cpc = Converter.getGson().fromJson(content, ChargeProfile.class);
278 // String contentTranfsorm = Converter.getGson().toJson(cpc);
279 // String profile = "{chargingProfile:" + contentTranfsorm + "}";
280 // logger.info("{}", profile);
281 // chargingControlRequest
282 // .content(new StringContentProvider(CONTENT_TYPE_JSON_ENCODED, params, StandardCharsets.UTF_8));
284 // chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
285 // chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
287 // params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
289 // ContentResponse chargingControlResponse = chargingControlRequest.send();
290 // logger.info("{}", chargingControlResponse.getStatus());
291 // logger.info("{}", chargingControlResponse.getReason());
292 // logger.info("{}", chargingControlResponse.getContentAsString());
294 } catch (Exception e) {
295 logger.error("{}", e.getMessage());
299 private String getAuthCode(String response) {
300 String[] keys = response.split("&");
301 for (int i = 0; i < keys.length; i++) {
302 if (keys[i].startsWith(AUTHORIZATION)) {
303 String authCode = keys[i].split("=")[1];
304 authCode = authCode.split("\"")[0];
308 return Constants.EMPTY;
311 public static String codeFromUrl(String encodedUrl) {
312 final MultiMap<String> tokenMap = new MultiMap<String>();
313 UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
314 final StringBuilder codeFound = new StringBuilder();
315 tokenMap.forEach((key, value) -> {
316 if (!value.isEmpty()) {
317 String val = value.get(0);
318 if (key.endsWith(CODE)) {
319 codeFound.append(val);
323 return codeFound.toString();
327 public void testJWTDeserialze() {
328 String accessTokenResponseStr = FileReader
329 .readFileInString("src/test/resources/responses/auth/auth_cn_login_pwd.json");
330 ChinaTokenResponse cat = Converter.getGson().fromJson(accessTokenResponseStr, ChinaTokenResponse.class);
332 // https://www.baeldung.com/java-jwt-token-decode
333 String token = cat.data.accessToken;
334 String[] chunks = token.split("\\.");
335 String tokenJwtDecodeStr = new String(Base64.getUrlDecoder().decode(chunks[1]));
336 ChinaTokenExpiration cte = Converter.getGson().fromJson(tokenJwtDecodeStr, ChinaTokenExpiration.class);
337 Token t = new Token();
339 t.setType(cat.data.tokenType);
340 t.setExpirationTotal(cte.exp);
342 "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJEVU1NWSQxJEEkMTYzNzcwNzkxNjc4MiIsIm5iZiI6MTYzNzcwNzkxNiwiZXhwIjoxNjM3NzExMjE2LCJpYXQiOjE2Mzc3MDc5MTZ9.hpi-P97W68g7avGwu9dcBRapIsaG4F8MwOdPHe6PuTA",
343 t.getBearerToken(), "Token");
346 public void testChina() {
347 SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
348 HttpClient authHttpClient = new HttpClient(sslContextFactory);
350 authHttpClient.start();
351 HttpClientFactory mockHCF = mock(HttpClientFactory.class);
352 when(mockHCF.getCommonHttpClient()).thenReturn(authHttpClient);
353 MyBMWConfiguration config = new MyBMWConfiguration();
354 config.region = BimmerConstants.REGION_CHINA;
355 config.userName = "Hello User";
356 config.password = "Hello Password";
357 MyBMWProxy bmwProxy = new MyBMWProxy(mockHCF, config);
358 bmwProxy.updateTokenChina();
359 } catch (Exception e) {
360 logger.warn("Exception: " + e.getMessage());
365 public void testPublicKey() {
366 String publicKeyResponseStr = FileReader.readFileInString("src/test/resources/responses/auth/china-key.json");
367 ChinaPublicKeyResponse pkr = Converter.getGson().fromJson(publicKeyResponseStr, ChinaPublicKeyResponse.class);
368 String publicKeyStr = pkr.data.value;
369 String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "")
370 .replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "").replace("\\r", "")
371 .replace("\\n", "").trim();
372 byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
373 X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
376 kf = KeyFactory.getInstance("RSA");
377 PublicKey publicKey = kf.generatePublic(spec);
378 // https://www.thexcoders.net/java-ciphers-rsa/
379 Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
380 cipher.init(Cipher.ENCRYPT_MODE, publicKey);
381 byte[] encryptedBytes = cipher.doFinal("Hello World".getBytes());
382 String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes);
383 logger.info(encodedPassword);
384 } catch (Exception e) {
385 assertTrue(false, "Excpetion: " + e.getMessage());
389 public void testChinaToken() {
390 SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
391 HttpClient authHttpClient = new HttpClient(sslContextFactory);
393 authHttpClient.start();
394 String url = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_CHINA)
395 + BimmerConstants.CHINA_PUBLIC_KEY;
396 Request oauthQueryRequest = authHttpClient.newRequest(url);
397 oauthQueryRequest.header(X_USER_AGENT,
398 String.format(BimmerConstants.BRAND_BMW, BimmerConstants.BRAND_BMW, BimmerConstants.REGION_ROW));
400 ContentResponse publicKeyResponse = oauthQueryRequest.send();
401 ChinaPublicKeyResponse pkr = Converter.getGson().fromJson(publicKeyResponse.getContentAsString(),
402 ChinaPublicKeyResponse.class);
403 // https://stackoverflow.com/questions/11410770/load-rsa-public-key-from-file
405 String publicKeyStr = pkr.data.value;
406 // String cleanPublicKeyStr = pkr.data.value.replaceAll("(\r\n|\n)", Constants.EMPTY);
407 String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "")
408 .replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "");
409 byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
410 X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
412 KeyFactory kf = KeyFactory.getInstance("RSA");
413 PublicKey publicKey = kf.generatePublic(spec);
414 // https://www.thexcoders.net/java-ciphers-rsa/
415 Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
416 cipher.init(Cipher.ENCRYPT_MODE, publicKey);
417 byte[] encryptedBytes = cipher.doFinal("Hello World".getBytes());
418 String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes);
419 logger.info(encodedPassword);
420 } catch (Exception e) {
421 assertTrue(false, "Excpetion: " + e.getMessage());