]> git.basschouten.com Git - openhab-addons.git/blob
b341f1acafb523efce5c1904b328c994f3c68c93
[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.auth;
14
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;
25
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;
32
33 import javax.crypto.Cipher;
34
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;
60
61 /**
62  * The {@link AuthTest} test authorization flow
63  *
64  * @author Bernd Weymann - Initial contribution
65  * @author Martin Grassl - moved to other package and updated for v2
66  */
67 @NonNullByDefault
68 class AuthTest {
69     private final Logger logger = LoggerFactory.getLogger(AuthTest.class);
70
71     @Test
72     public void testAuth() {
73         String user = "usr";
74         String pwd = "pwd";
75
76         SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
77         HttpClient authHttpClient = new HttpClient(sslContextFactory);
78         try {
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)");
86
87             ContentResponse firstResponse = firstRequest.send();
88             logger.info(firstResponse.getContentAsString());
89             AuthQueryResponse aqr = JsonStringDeserializer.deserializeString(firstResponse.getContentAsString(),
90                     AuthQueryResponse.class);
91
92             String verifierBytes = StringUtils.getRandomAlphabetic(64).toLowerCase();
93             String codeVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(verifierBytes.getBytes());
94
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);
98
99             String stateBytes = StringUtils.getRandomAlphabetic(16).toLowerCase();
100             String state = Base64.getUrlEncoder().withoutPadding().encodeToString(stateBytes.getBytes());
101
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");
106
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");
116
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);
127
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);
139             logger.info("Auth");
140
141             logger.info(aqr.tokenEndpoint);
142
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);
149
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(),
160                     AuthResponse.class);
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());
166
167             /**
168              * REQUEST CONTENT
169              */
170             HttpClient apiHttpClient = new HttpClient(sslContextFactory);
171             apiHttpClient.start();
172
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");
177
178             String params = UrlEncoded.encode(vehicleParams, StandardCharsets.UTF_8, false);
179
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
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)");
189
190             ContentResponse vehicleResponse = vehicleRequest.send();
191             logger.info("Vehicle Status {} {}", vehicleResponse.getStatus(), vehicleResponse.getContentAsString());
192
193             /**
194              * CHARGE STATISTICS
195              */
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);
200
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);
206
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");
211
212             logger.info("{}", params);
213             chargeStatisticsRequest
214                     .content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8));
215
216             ContentResponse chargeStatisticsResponse = chargeStatisticsRequest.send();
217             logger.info("{}", chargeStatisticsResponse.getStatus());
218             logger.info("{}", chargeStatisticsResponse.getReason());
219             logger.info("{}", chargeStatisticsResponse.getContentAsString());
220
221             /**
222              * CHARGE SESSIONS
223              */
224             MultiMap<String> chargeSessionsParams = new MultiMap<String>();
225             chargeSessionsParams.put("vin", "WBY1Z81040V905639");
226             chargeSessionsParams.put("maxResults", "40");
227             chargeSessionsParams.put("include_date_picker", "true");
228
229             params = UrlEncoded.encode(chargeSessionsParams, StandardCharsets.UTF_8, false);
230
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);
235
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");
240
241             logger.info("{}", params);
242
243             ContentResponse chargeSessionsResponse = chargeSessionsRequest.send();
244             logger.info("{}", chargeSessionsResponse.getStatus());
245             logger.info("{}", chargeSessionsResponse.getReason());
246             logger.info("{}", chargeSessionsResponse.getContentAsString());
247
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);
252
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);
258
259         } catch (Exception e) {
260             logger.error("{}", e.getMessage());
261         }
262     }
263
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];
270                 return authCode;
271             }
272         }
273         return Constants.EMPTY;
274     }
275
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);
285                 }
286             }
287         });
288         return codeFound.toString();
289     }
290
291     @Test
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);
296
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();
304         t.setToken(token);
305         t.setType(cat.data.tokenType);
306         t.setExpirationTotal(cte.exp);
307         assertEquals(
308                 "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJEVU1NWSQxJEEkMTYzNzcwNzkxNjc4MiIsIm5iZiI6MTYzNzcwNzkxNiwiZXhwIjoxNjM3NzExMjE2LCJpYXQiOjE2Mzc3MDc5MTZ9.hpi-P97W68g7avGwu9dcBRapIsaG4F8MwOdPHe6PuTA",
309                 t.getBearerToken(), "Token");
310     }
311
312     public void testChina() {
313         SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
314         HttpClient authHttpClient = new HttpClient(sslContextFactory);
315         try {
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());
329         }
330     }
331
332     @Test
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);
343         KeyFactory kf;
344         try {
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());
355         }
356     }
357
358     public void testChinaToken() {
359         SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
360         HttpClient authHttpClient = new HttpClient(sslContextFactory);
361         try {
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));
368
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
373
374             String publicKeyStr = pkr.data.value;
375
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);
380
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());
391         }
392     }
393 }