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.auth;
15 import static org.junit.jupiter.api.Assertions.assertEquals;
16 import static org.junit.jupiter.api.Assertions.assertNotNull;
17 import static org.junit.jupiter.api.Assertions.assertTrue;
18 import static org.mockito.Mockito.mock;
19 import static org.mockito.Mockito.when;
20 import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.AUTHORIZATION;
21 import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CODE;
22 import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CONTENT_TYPE_JSON;
23 import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.CONTENT_TYPE_URL_ENCODED;
24 import static org.openhab.binding.mybmw.internal.utils.HTTPConstants.HEADER_X_USER_AGENT;
26 import java.nio.charset.StandardCharsets;
27 import java.security.KeyFactory;
28 import java.security.MessageDigest;
29 import java.security.PublicKey;
30 import java.security.spec.X509EncodedKeySpec;
31 import java.util.Base64;
33 import javax.crypto.Cipher;
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jetty.client.HttpClient;
37 import org.eclipse.jetty.client.api.ContentResponse;
38 import org.eclipse.jetty.client.api.Request;
39 import org.eclipse.jetty.client.util.StringContentProvider;
40 import org.eclipse.jetty.http.HttpHeader;
41 import org.eclipse.jetty.util.MultiMap;
42 import org.eclipse.jetty.util.UrlEncoded;
43 import org.eclipse.jetty.util.ssl.SslContextFactory;
44 import org.junit.jupiter.api.Test;
45 import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration;
46 import org.openhab.binding.mybmw.internal.dto.auth.AuthQueryResponse;
47 import org.openhab.binding.mybmw.internal.dto.auth.AuthResponse;
48 import org.openhab.binding.mybmw.internal.dto.auth.ChinaPublicKeyResponse;
49 import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenExpiration;
50 import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenResponse;
51 import org.openhab.binding.mybmw.internal.handler.backend.JsonStringDeserializer;
52 import org.openhab.binding.mybmw.internal.util.FileReader;
53 import org.openhab.binding.mybmw.internal.utils.BimmerConstants;
54 import org.openhab.binding.mybmw.internal.utils.Constants;
55 import org.openhab.binding.mybmw.internal.utils.Converter;
56 import org.openhab.core.io.net.http.HttpClientFactory;
57 import org.openhab.core.util.StringUtils;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
62 * The {@link AuthTest} test authorization flow
64 * @author Bernd Weymann - Initial contribution
65 * @author Martin Grassl - moved to other package and updated for v2
69 private final Logger logger = LoggerFactory.getLogger(AuthTest.class);
72 public void testAuth() {
76 SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
77 HttpClient authHttpClient = new HttpClient(sslContextFactory);
79 authHttpClient.start();
80 Request firstRequest = authHttpClient
81 .newRequest("https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
82 + "/eadrax-ucs/v1/presentation/oauth/config");
83 firstRequest.header("ocp-apim-subscription-key",
84 BimmerConstants.OCP_APIM_KEYS.get(BimmerConstants.REGION_ROW));
85 firstRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
87 ContentResponse firstResponse = firstRequest.send();
88 logger.info(firstResponse.getContentAsString());
89 AuthQueryResponse aqr = JsonStringDeserializer.deserializeString(firstResponse.getContentAsString(),
90 AuthQueryResponse.class);
92 String verifierBytes = StringUtils.getRandomAlphabetic(64).toLowerCase();
93 String codeVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(verifierBytes.getBytes());
95 MessageDigest digest = MessageDigest.getInstance("SHA-256");
96 byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8));
97 String codeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
99 String stateBytes = StringUtils.getRandomAlphabetic(16).toLowerCase();
100 String state = Base64.getUrlEncoder().withoutPadding().encodeToString(stateBytes.getBytes());
102 String authUrl = aqr.gcdmBaseUrl + BimmerConstants.OAUTH_ENDPOINT;
103 logger.info(authUrl);
104 Request loginRequest = authHttpClient.POST(authUrl);
105 loginRequest.header("Content-Type", "application/x-www-form-urlencoded");
107 MultiMap<String> baseParams = new MultiMap<String>();
108 baseParams.put("client_id", aqr.clientId);
109 baseParams.put("response_type", "code");
110 baseParams.put("redirect_uri", aqr.returnUrl);
111 baseParams.put("state", state);
112 baseParams.put("nonce", "login_nonce");
113 baseParams.put("scope", String.join(" ", aqr.scopes));
114 baseParams.put("code_challenge", codeChallenge);
115 baseParams.put("code_challenge_method", "S256");
117 MultiMap<String> loginParams = new MultiMap<String>(baseParams);
118 loginParams.put("grant_type", "authorization_code");
119 loginParams.put("username", user);
120 loginParams.put("password", pwd);
121 loginRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
122 UrlEncoded.encode(loginParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
123 ContentResponse secondResonse = loginRequest.send();
124 logger.info(secondResonse.getContentAsString());
125 String authCode = getAuthCode(secondResonse.getContentAsString());
126 logger.info(authCode);
128 MultiMap<String> authParams = new MultiMap<String>(baseParams);
129 authParams.put("authorization", authCode);
130 Request authRequest = authHttpClient.POST(authUrl).followRedirects(false);
131 authRequest.header("Content-Type", "application/x-www-form-urlencoded");
132 authRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
133 UrlEncoded.encode(authParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
134 ContentResponse authResponse = authRequest.send();
135 logger.info("{}", authResponse.getHeaders());
136 logger.info("Response " + authResponse.getHeaders().get(HttpHeader.LOCATION));
137 String code = AuthTest.codeFromUrl(authResponse.getHeaders().get(HttpHeader.LOCATION));
138 logger.info("Code " + code);
141 logger.info(aqr.tokenEndpoint);
143 Request codeRequest = authHttpClient.POST(aqr.tokenEndpoint);
144 String basicAuth = "Basic "
145 + Base64.getUrlEncoder().encodeToString((aqr.clientId + ":" + aqr.clientSecret).getBytes());
146 logger.info(basicAuth);
147 codeRequest.header("Content-Type", "application/x-www-form-urlencoded");
148 codeRequest.header(AUTHORIZATION, basicAuth);
150 MultiMap<String> codeParams = new MultiMap<String>();
151 codeParams.put("code", code);
152 codeParams.put("code_verifier", codeVerifier);
153 codeParams.put("redirect_uri", aqr.returnUrl);
154 codeParams.put("grant_type", "authorization_code");
155 codeRequest.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED,
156 UrlEncoded.encode(codeParams, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8));
157 ContentResponse codeResponse = codeRequest.send();
158 logger.info(codeResponse.getContentAsString());
159 AuthResponse ar = JsonStringDeserializer.deserializeString(codeResponse.getContentAsString(),
161 Token t = new Token();
162 t.setType(ar.tokenType);
163 t.setToken(ar.accessToken);
164 t.setExpiration(ar.expiresIn);
165 logger.info(t.getBearerToken());
170 HttpClient apiHttpClient = new HttpClient(sslContextFactory);
171 apiHttpClient.start();
173 MultiMap<String> vehicleParams = new MultiMap<String>();
174 vehicleParams.put("tireGuardMode", "ENABLED");
175 vehicleParams.put("appDateTime", Long.toString(System.currentTimeMillis()));
176 vehicleParams.put("apptimezone", "60");
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);
185 vehicleRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
186 vehicleRequest.header("accept", "application/json");
187 vehicleRequest.header("accept-language", "de");
188 vehicleRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
190 ContentResponse vehicleResponse = vehicleRequest.send();
191 logger.info("Vehicle Status {} {}", vehicleResponse.getStatus(), vehicleResponse.getContentAsString());
196 MultiMap<String> chargeStatisticsParams = new MultiMap<String>();
197 chargeStatisticsParams.put("vin", "WBY1Z81040V905639");
198 chargeStatisticsParams.put("currentDate", Converter.getCurrentISOTime());
199 params = UrlEncoded.encode(chargeStatisticsParams, StandardCharsets.UTF_8, false);
201 String chargeStatisticsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
202 + "/eadrax-chs/v1/charging-statistics";
203 Request chargeStatisticsRequest = apiHttpClient.newRequest(chargeStatisticsUrl)
204 .param("vin", "WBY1Z81040V905639").param("currentDate", Converter.getCurrentISOTime());
205 logger.info("{}", chargeStatisticsUrl);
207 chargeStatisticsRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
208 chargeStatisticsRequest.header("accept", "application/json");
209 chargeStatisticsRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
210 chargeStatisticsRequest.header("accept-language", "de");
212 logger.info("{}", params);
213 chargeStatisticsRequest
214 .content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8));
216 ContentResponse chargeStatisticsResponse = chargeStatisticsRequest.send();
217 logger.info("{}", chargeStatisticsResponse.getStatus());
218 logger.info("{}", chargeStatisticsResponse.getReason());
219 logger.info("{}", chargeStatisticsResponse.getContentAsString());
224 MultiMap<String> chargeSessionsParams = new MultiMap<String>();
225 chargeSessionsParams.put("vin", "WBY1Z81040V905639");
226 chargeSessionsParams.put("maxResults", "40");
227 chargeSessionsParams.put("include_date_picker", "true");
229 params = UrlEncoded.encode(chargeSessionsParams, StandardCharsets.UTF_8, false);
231 String chargeSessionsUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
232 + "/eadrax-chs/v1/charging-sessions";
233 Request chargeSessionsRequest = apiHttpClient.newRequest(chargeSessionsUrl + "?" + params);
234 logger.info("{}", chargeSessionsUrl);
236 chargeSessionsRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
237 chargeSessionsRequest.header("accept", "application/json");
238 chargeSessionsRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
239 chargeSessionsRequest.header("accept-language", "de");
241 logger.info("{}", params);
243 ContentResponse chargeSessionsResponse = chargeSessionsRequest.send();
244 logger.info("{}", chargeSessionsResponse.getStatus());
245 logger.info("{}", chargeSessionsResponse.getReason());
246 logger.info("{}", chargeSessionsResponse.getContentAsString());
248 String chargingControlUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_ROW)
249 + "/eadrax-vrccs/v2/presentation/remote-commands/WBY1Z81040V905639/charging-control";
250 Request chargingControlRequest = apiHttpClient.POST(chargingControlUrl);
251 logger.info("{}", chargingControlUrl);
253 chargingControlRequest.header(HttpHeader.AUTHORIZATION, t.getBearerToken());
254 chargingControlRequest.header("accept", "application/json");
255 chargingControlRequest.header("x-user-agent", "android(v1.07_20200330);bmw;1.7.0(11152)");
256 chargingControlRequest.header("accept-language", "de");
257 chargingControlRequest.header("Content-Type", CONTENT_TYPE_JSON);
259 } catch (Exception e) {
260 logger.error("{}", e.getMessage());
264 private String getAuthCode(String response) {
265 String[] keys = response.split("&");
266 for (int i = 0; i < keys.length; i++) {
267 if (keys[i].startsWith(AUTHORIZATION)) {
268 String authCode = keys[i].split("=")[1];
269 authCode = authCode.split("\"")[0];
273 return Constants.EMPTY;
276 public static String codeFromUrl(String encodedUrl) {
277 final MultiMap<String> tokenMap = new MultiMap<String>();
278 UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII);
279 final StringBuilder codeFound = new StringBuilder();
280 tokenMap.forEach((key, value) -> {
281 if (!value.isEmpty()) {
282 String val = value.get(0);
283 if (key.endsWith(CODE)) {
284 codeFound.append(val);
288 return codeFound.toString();
292 public void testJWTDeserialze() {
293 String accessTokenResponseStr = FileReader.fileToString("responses/auth/auth_cn_login_pwd.json");
294 ChinaTokenResponse cat = JsonStringDeserializer.deserializeString(accessTokenResponseStr,
295 ChinaTokenResponse.class);
297 // https://www.baeldung.com/java-jwt-token-decode
298 String token = cat.data.accessToken;
299 String[] chunks = token.split("\\.");
300 String tokenJwtDecodeStr = new String(Base64.getUrlDecoder().decode(chunks[1]));
301 ChinaTokenExpiration cte = JsonStringDeserializer.deserializeString(tokenJwtDecodeStr,
302 ChinaTokenExpiration.class);
303 Token t = new Token();
305 t.setType(cat.data.tokenType);
306 t.setExpirationTotal(cte.exp);
308 "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJEVU1NWSQxJEEkMTYzNzcwNzkxNjc4MiIsIm5iZiI6MTYzNzcwNzkxNiwiZXhwIjoxNjM3NzExMjE2LCJpYXQiOjE2Mzc3MDc5MTZ9.hpi-P97W68g7avGwu9dcBRapIsaG4F8MwOdPHe6PuTA",
309 t.getBearerToken(), "Token");
312 public void testChina() {
313 SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
314 HttpClient authHttpClient = new HttpClient(sslContextFactory);
316 authHttpClient.start();
317 HttpClientFactory mockHCF = mock(HttpClientFactory.class);
318 when(mockHCF.getCommonHttpClient()).thenReturn(authHttpClient);
319 MyBMWBridgeConfiguration config = new MyBMWBridgeConfiguration();
320 config.region = BimmerConstants.REGION_CHINA;
321 config.userName = "Hello User";
322 config.password = "Hello Password";
323 MyBMWTokenController tokenHandler = new MyBMWTokenController(config, authHttpClient);
324 Token token = tokenHandler.getToken();
325 assertNotNull(token);
326 assertNotNull(token.getBearerToken());
327 } catch (Exception e) {
328 logger.warn("Exception: " + e.getMessage());
333 public void testPublicKey() {
334 String publicKeyResponseStr = FileReader.fileToString("responses/auth/china-key.json");
335 ChinaPublicKeyResponse pkr = JsonStringDeserializer.deserializeString(publicKeyResponseStr,
336 ChinaPublicKeyResponse.class);
337 String publicKeyStr = pkr.data.value;
338 String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "")
339 .replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "").replace("\\r", "")
340 .replace("\\n", "").trim();
341 byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
342 X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
345 kf = KeyFactory.getInstance("RSA");
346 PublicKey publicKey = kf.generatePublic(spec);
347 // https://www.thexcoders.net/java-ciphers-rsa/
348 Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
349 cipher.init(Cipher.ENCRYPT_MODE, publicKey);
350 byte[] encryptedBytes = cipher.doFinal("Hello World".getBytes());
351 String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes);
352 logger.info(encodedPassword);
353 } catch (Exception e) {
354 assertTrue(false, "Excpetion: " + e.getMessage());
358 public void testChinaToken() {
359 SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
360 HttpClient authHttpClient = new HttpClient(sslContextFactory);
362 authHttpClient.start();
363 String url = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(BimmerConstants.REGION_CHINA)
364 + BimmerConstants.CHINA_PUBLIC_KEY;
365 Request oauthQueryRequest = authHttpClient.newRequest(url);
366 oauthQueryRequest.header(HEADER_X_USER_AGENT,
367 String.format(BimmerConstants.BRAND_BMW, BimmerConstants.BRAND_BMW, BimmerConstants.REGION_ROW));
369 ContentResponse publicKeyResponse = oauthQueryRequest.send();
370 ChinaPublicKeyResponse pkr = JsonStringDeserializer
371 .deserializeString(publicKeyResponse.getContentAsString(), ChinaPublicKeyResponse.class);
372 // https://stackoverflow.com/questions/11410770/load-rsa-public-key-from-file
374 String publicKeyStr = pkr.data.value;
376 String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "")
377 .replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "");
378 byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
379 X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
381 KeyFactory kf = KeyFactory.getInstance("RSA");
382 PublicKey publicKey = kf.generatePublic(spec);
383 // https://www.thexcoders.net/java-ciphers-rsa/
384 Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
385 cipher.init(Cipher.ENCRYPT_MODE, publicKey);
386 byte[] encryptedBytes = cipher.doFinal("Hello World".getBytes());
387 String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes);
388 logger.info(encodedPassword);
389 } catch (Exception e) {
390 assertTrue(false, "Excpetion: " + e.getMessage());