From c3bf41f0da67ca92d7ab8abc1ccd0bd8c252f660 Mon Sep 17 00:00:00 2001 From: Jan Gustafsson Date: Fri, 22 Nov 2024 21:41:44 +0100 Subject: [PATCH] [verisure] Fix broken cloud authentication (#17761) * Fix Verisure bridge cannot login (#17730) Signed-off-by: Jan Gustafsson --- .../internal/VerisureBindingConstants.java | 9 +- .../verisure/internal/VerisureSession.java | 338 +++++++++++------- .../VerisureSmartLockThingHandler.java | 4 +- 3 files changed, 217 insertions(+), 134 deletions(-) diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureBindingConstants.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureBindingConstants.java index 3852225a4256d..574b3c89e929c 100644 --- a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureBindingConstants.java +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureBindingConstants.java @@ -132,7 +132,7 @@ public class VerisureBindingConstants { public static final String USERNAME = "username"; public static final String PASSWORD = "password"; public static final String BASE_URL = "https://mypages.verisure.com"; - public static final String LOGON_SUF = BASE_URL + "/j_spring_security_check?locale=en_GB"; + public static final String LOGON_SUF = BASE_URL + "/j_spring_security_check?locale=sv-SE"; public static final String ALARM_COMMAND = BASE_URL + "/remotecontrol/armstatechange.cmd"; public static final String SMARTLOCK_LOCK_COMMAND = BASE_URL + "/remotecontrol/lockunlock.cmd"; public static final String SMARTLOCK_SET_COMMAND = BASE_URL + "/overview/setdoorlock.cmd"; @@ -140,13 +140,14 @@ public class VerisureBindingConstants { public static final String SMARTLOCK_VOLUME_COMMAND = BASE_URL + "/settings/setvolume.cmd"; public static final String SMARTPLUG_COMMAND = BASE_URL + "/settings/smartplug/onoffplug.cmd"; - public static final String START_REDIRECT = "/uk/start.html"; + public static final String START_REDIRECT = "/se/start.html"; public static final String START_SUF = BASE_URL + START_REDIRECT; // GraphQL constants - public static final String STATUS = BASE_URL + "/uk/status"; + public static final String STATUS = BASE_URL + "/se/status"; public static final String EXTEND = BASE_URL + "/session/extend"; - public static final String SETTINGS = BASE_URL + "/uk/settings.html?giid="; + public static final String LOGIN = BASE_URL + "/login.html"; + public static final String SETTINGS = BASE_URL + "/se/settings.html?giid="; public static final String SET_INSTALLATION = BASE_URL + "/setinstallation?giid="; public static final String BASEURL_API = "https://m-api02.verisure.com"; public static final String START_GRAPHQL = "/graphql"; diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureSession.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureSession.java index bdf7503c6afca..181a0c0e611b2 100644 --- a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureSession.java +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureSession.java @@ -27,6 +27,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -40,8 +41,10 @@ import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.util.BytesContentProvider; +import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.util.HttpCookieStore; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; @@ -92,8 +95,10 @@ public class VerisureSession { private static final int REQUEST_TIMEOUT_MS = 10_000; private static final String USER_NAME = "username"; private static final String VID = "vid"; + private static final String JSESSIONID = "JSESSIONID"; private static final String VS_STEPUP = "vs-stepup"; private static final String VS_ACCESS = "vs-access"; + private static final String VS_RESFRESH = "vs-refresh"; private String apiServerInUse = APISERVERLIST.get(apiServerInUseIndex); private String authstring = ""; private @Nullable String csrf; @@ -103,7 +108,9 @@ public class VerisureSession { private String password = ""; private String vid = ""; private String vsAccess = ""; + private String vsRefresh = ""; private String vsStepup = ""; + private String jsessionId = ""; public VerisureSession(HttpClient httpClient) { this.httpClient = httpClient; @@ -115,7 +122,7 @@ public boolean initialize(@Nullable String authstring, @Nullable String pinCode, this.pinCode = pinCode; this.userName = userName; this.password = password; - // Try to login to Verisure + if (logIn()) { return getInstallations(); } else { @@ -137,9 +144,9 @@ public boolean refresh() { public int sendCommand(String url, String data, BigDecimal installationId) { logger.debug("Sending command with URL {} and data {}", url, data); try { - configureInstallationInstance(installationId); int httpResultCode = setSessionCookieAuthLogin(); if (httpResultCode == HttpStatus.OK_200) { + configureInstallationInstance(installationId); return postVerisureAPI(url, data); } else { return httpResultCode; @@ -226,21 +233,27 @@ public void setNumberOfEvents(int numberOfEvents) { public void configureInstallationInstance(BigDecimal installationId) throws ExecutionException, InterruptedException, TimeoutException { - csrf = getCsrfToken(installationId); - logger.debug("Got CSRF: {}", csrf); - // Set installation String url = SET_INSTALLATION + installationId; httpClient.GET(url); } - public @Nullable String getCsrfToken(BigDecimal installationId) - throws ExecutionException, InterruptedException, TimeoutException { + public @Nullable String getCsrfToken() throws ExecutionException, InterruptedException, TimeoutException { String html = null; - String url = SETTINGS + installationId; + CookieStore originalCookieStore = httpClient.getCookieStore(); + httpClient.setCookieStore(new HttpCookieStore.Empty()); + + ContentResponse response = httpClient.newRequest(LOGIN).method(HttpMethod.GET).send(); + html = response.getContentAsString(); + logger.trace("url: {} html: {}", LOGIN, html); - ContentResponse resp = httpClient.GET(url); - html = resp.getContentAsString(); - logger.trace("url: {} html: {}", url, html); + try { + URI authUri = new URI(apiServerInUse); + addRequiredCookies(httpClient.getCookieStore(), originalCookieStore, authUri); + } catch (URISyntaxException e) { + Throwable cause = e.getCause(); + logger.debug("Invalid URI: {}", cause != null ? cause.getMessage() : e.getMessage()); + } + httpClient.setCookieStore(originalCookieStore); Document htmlDocument = Jsoup.parse(html); Element nameInput = htmlDocument.select("input[name=_csrf]").first(); @@ -273,13 +286,44 @@ private void analyzeCookies() { } else if (VS_ACCESS.equals(cookie.getName())) { vsAccess = cookie.getValue(); logger.debug("Fetching vs-access {} from cookie", vsAccess); + } else if (VS_RESFRESH.equals(cookie.getName())) { + vsRefresh = cookie.getValue(); + logger.debug("Fetching vs-refresh {} from cookie", vsRefresh); } else if (VS_STEPUP.equals(cookie.getName())) { vsStepup = cookie.getValue(); logger.debug("Fetching vs-stepup {} from cookie", vsStepup); + } else if (JSESSIONID.equals(cookie.getName())) { + jsessionId = cookie.getValue(); + logger.debug("Fetching JESSIONID {} from cookie", jsessionId); } }); } + private void addCookieIfPresent(Request request, String name, String value) { + if (!value.isEmpty()) { + request.cookie(new HttpCookie(name, value)); + logger.debug("Setting cookie with {} = {}", name, value); + } + } + + private boolean addRequiredCookies(CookieStore from, CookieStore to, URI authUri) { + boolean hasAllRequired = true; + String[] requiredCookies = { VID, USER_NAME, JSESSIONID }; + + for (String cookieName : requiredCookies) { + Optional cookie = from.getCookies().stream().filter(c -> c.getName().equals(cookieName)) + .findFirst(); + if (cookie.isPresent()) { + logger.debug("Adding cookie: {}", cookieName); + to.add(authUri, cookie.get()); + } else { + logger.debug("Missing required cookie: {}", cookieName); + hasAllRequired = false; + } + } + return hasAllRequired; + } + private void logTraceWithPattern(int responseStatus, String content) { if (logger.isTraceEnabled()) { String pattern = "(?m)^\\s*\\r?\\n|\\r?\\n\\s*(?!.*\\r?\\n)"; @@ -289,34 +333,83 @@ private void logTraceWithPattern(int responseStatus, String content) { } private boolean areWeLoggedIn() throws ExecutionException, InterruptedException, TimeoutException { - logger.debug("Checking if we are logged in"); - String url = STATUS; + logger.debug("Checking if session is valid"); + + int statusCode = checkSessionStatus(); + if (statusCode == HttpStatus.OK_200) { + logger.debug("Session is valid"); + return true; + } else if (statusCode == HttpStatus.UNAUTHORIZED_401 && !vsRefresh.isEmpty()) { + logger.debug("Session expired, attempting token refresh"); + if (refreshTokens()) { + statusCode = checkSessionStatus(); + return statusCode == HttpStatus.OK_200; + } else { + logger.debug("Token refresh failed, will need to perform full login"); + vsRefresh = ""; + return false; + } + } else { + logger.debug("Session invalid (status code: {}), need to login", statusCode); + return false; + } + } - ContentResponse response = httpClient.newRequest(url).method(HttpMethod.GET).send(); - String content = response.getContentAsString(); - logTraceWithPattern(response.getStatus(), content); + private int checkSessionStatus() throws ExecutionException, InterruptedException, TimeoutException { + String url = START_GRAPHQL; + String queryQLAccountInstallations = """ + [{ + "operationName": "AccountInstallations", + "variables": { + "email": "%s" + }, + "query": "query AccountInstallations($email: String!) {\\n account(email: $email) {\\n owainstallations {\\n giid\\n alias\\n type\\n subsidiary\\n dealerId\\n __typename\\n }\\n __typename\\n }\\n}\\n" + }] + """ + .formatted(userName); + return postVerisureAPI(url, queryQLAccountInstallations); + } - switch (response.getStatus()) { + private boolean refreshTokens() { + CookieStore originalCookieStore = httpClient.getCookieStore(); + + if (vsRefresh.isEmpty()) { + logger.debug("No refresh token available, need full login"); + return false; + } + + try { + CookieStore tempCookieStore = new HttpCookieStore(); + URI authUri = new URI(apiServerInUse); + if (!addRequiredCookies(originalCookieStore, tempCookieStore, authUri)) { + logger.debug("Missing required Incapsula cookies, might fail"); + } + HttpCookie vsRefreshCookie = new HttpCookie("vs-refresh", vsRefresh); + tempCookieStore.add(authUri, vsRefreshCookie); + httpClient.setCookieStore(tempCookieStore); + } catch (URISyntaxException e) { + Throwable cause = e.getCause(); + logger.debug("Invalid URI: {}", cause != null ? cause.getMessage() : e.getMessage()); + } + + int httpStatusCode = postVerisureAPI(AUTH_TOKEN, "empty"); + switch (httpStatusCode) { case HttpStatus.OK_200: - if (content.contains(" @Nullable T getJSONVerisureAPI(String url, Class jsonClass) @@ -337,29 +430,29 @@ private ContentResponse postVerisureAPI(String url, String data, boolean isJSON) Request request = httpClient.newRequest(url).method(HttpMethod.POST); request.timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS); if (isJSON) { - request.header("content-type", "application/json"); + request.header(HttpHeader.CONTENT_TYPE, "application/json"); } else { if (csrf != null) { request.header("X-CSRF-TOKEN", csrf); } } - request.header("Accept", "application/json"); + request.header(HttpHeader.ACCEPT, "application/json").header(HttpHeader.ORIGIN, "https://mypages.verisure.com"); - if (url.contains(AUTH_LOGIN)) { - request.header("APPLICATION_ID", "OpenHAB Verisure"); - String basicAuhentication = Base64.getEncoder().encodeToString((userName + ":" + password).getBytes()); - request.header("authorization", "Basic " + basicAuhentication); - } else { - if (!vid.isEmpty()) { - request.cookie(new HttpCookie(VID, vid)); - logger.debug("Setting cookie with vid {}", vid); + if (url.contains(AUTH_LOGIN) || url.contains(AUTH_TOKEN)) { + request.header("APPLICATION_ID", "MyPages_Login"); + if (url.contains(AUTH_LOGIN)) { + String basicAuhentication = Base64.getEncoder().encodeToString((userName + ":" + password).getBytes()); + request.header("authorization", "Basic " + basicAuhentication); } - if (!vsAccess.isEmpty()) { - request.cookie(new HttpCookie(VS_ACCESS, vsAccess)); - logger.debug("Setting cookie with vs-access {}", vsAccess); + } else { + if (url.contains(LOGON_SUF)) { + request.header(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded") + .header(HttpHeader.REFERER, "https://mypages.verisure.com/login") + .header("x-vs-refresh", vsRefresh); } - logger.debug("Setting cookie with username {}", userName); - request.cookie(new HttpCookie(USER_NAME, userName)); + addCookieIfPresent(request, VID, vid); + addCookieIfPresent(request, VS_ACCESS, vsAccess); + addCookieIfPresent(request, USER_NAME, userName); } if (!"empty".equals(data)) { @@ -432,87 +525,75 @@ private int postVerisureAPI(String urlString, String data) { logger.debug("Failed to send POST, Http status code: {}", response.getStatus()); } } catch (ExecutionException | InterruptedException | TimeoutException e) { - logger.warn("Failed to send a POST to the API {}", e.getMessage()); + logger.debug("Failed to send a POST to the API {}", e.getMessage()); + return HttpStatus.UNAUTHORIZED_401; } } return HttpStatus.SERVICE_UNAVAILABLE_503; } private int setSessionCookieAuthLogin() throws ExecutionException, InterruptedException, TimeoutException { - // URL to set status which will give us 2 cookies with username and password used for the session - String url = STATUS; - ContentResponse response = httpClient.GET(url); - logTraceWithPattern(response.getStatus(), response.getContentAsString()); - - url = AUTH_LOGIN; + String url = AUTH_LOGIN; int httpStatusCode = postVerisureAPI(url, "empty"); analyzeCookies(); - - // return response.getStatus(); return httpStatusCode; } private boolean getInstallations() { - int httpResultCode = 0; - + String url = START_GRAPHQL; + String queryQLAccountInstallations = """ + [{ + "operationName": "AccountInstallations", + "variables": { + "email": "%s" + }, + "query": "query AccountInstallations($email: String!) {\\n account(email: $email) {\\n owainstallations {\\n giid\\n alias\\n type\\n subsidiary\\n dealerId\\n __typename\\n }\\n __typename\\n }\\n}\\n" + }] + """ + .formatted(userName); try { - httpResultCode = setSessionCookieAuthLogin(); - } catch (ExecutionException | InterruptedException | TimeoutException e) { - logger.warn("Failed to set session cookie {}", e.getMessage()); - return false; - } - - if (httpResultCode == HttpStatus.OK_200) { - String url = START_GRAPHQL; - - String queryQLAccountInstallations = "[{\"operationName\":\"AccountInstallations\",\"variables\":{\"email\":\"" - + userName - + "\"},\"query\":\"query AccountInstallations($email: String!) {\\n account(email: $email) {\\n owainstallations {\\n giid\\n alias\\n type\\n subsidiary\\n dealerId\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"; - try { - VerisureInstallationsDTO installations = postJSONVerisureAPI(url, queryQLAccountInstallations, - VerisureInstallationsDTO.class); - logger.debug("Installation: {}", installations.toString()); - List owaInstList = installations.getData().getAccount().getOwainstallations(); - boolean pinCodesMatchInstallations = true; - List pinCodes = null; - String pinCode = this.pinCode; - if (pinCode != null) { - pinCodes = Arrays.asList(pinCode.split(",")); - if (owaInstList.size() != pinCodes.size()) { - logger.debug("Number of installations {} does not match number of pin codes configured {}", - owaInstList.size(), pinCodes.size()); - pinCodesMatchInstallations = false; - } - } else { - logger.debug("No pin-code defined for user {}", userName); + VerisureInstallationsDTO installations = postJSONVerisureAPI(url, queryQLAccountInstallations, + VerisureInstallationsDTO.class); + logger.debug("Installation: {}", installations.toString()); + List owaInstList = installations.getData().getAccount().getOwainstallations(); + boolean pinCodesMatchInstallations = true; + List pinCodes = null; + String pinCode = this.pinCode; + if (pinCode != null) { + pinCodes = Arrays.asList(pinCode.split(",")); + if (owaInstList.size() != pinCodes.size()) { + logger.debug("Number of installations {} does not match number of pin codes configured {}", + owaInstList.size(), pinCodes.size()); + pinCodesMatchInstallations = false; } + } else { + logger.debug("No pin-code defined for user {}", userName); + } - for (int i = 0; i < owaInstList.size(); i++) { - VerisureInstallation vInst = new VerisureInstallation(); - Owainstallation owaInstallation = owaInstList.get(i); - String installationId = owaInstallation.getGiid(); - if (owaInstallation.getAlias() != null && installationId != null) { - vInst.setInstallationId(new BigDecimal(installationId)); - vInst.setInstallationName(owaInstallation.getAlias()); - if (pinCode != null && pinCodes != null) { - int pinCodeIndex = pinCodesMatchInstallations ? i : 0; - vInst.setPinCode(pinCodes.get(pinCodeIndex)); - logger.debug("Setting configured pincode index[{}] to installation ID {}", pinCodeIndex, - installationId); - } - verisureInstallations.put(new BigDecimal(installationId), vInst); - } else { - logger.warn("Failed to get alias and/or giid"); - return false; + for (int i = 0; i < owaInstList.size(); i++) { + VerisureInstallation vInst = new VerisureInstallation(); + Owainstallation owaInstallation = owaInstList.get(i); + String installationId = owaInstallation.getGiid(); + if (owaInstallation.getAlias() != null && installationId != null) { + vInst.setInstallationId(new BigDecimal(installationId)); + vInst.setInstallationName(owaInstallation.getAlias()); + if (pinCode != null && pinCodes != null) { + int pinCodeIndex = pinCodesMatchInstallations ? i : 0; + vInst.setPinCode(pinCodes.get(pinCodeIndex)); + logger.debug("Setting configured pincode index[{}] to installation ID {}", pinCodeIndex, + installationId); } + verisureInstallations.put(new BigDecimal(installationId), vInst); + } else { + logger.warn("Failed to get alias and/or giid"); + return false; } - } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException - | PostToAPIException e) { - logger.warn("Failed to send a POST to the API {}", e.getMessage()); } - } else { - logger.warn("Failed to set session cookie and auth login, HTTP result code: {}", httpResultCode); - return false; + csrf = getCsrfToken(); + logger.debug("Got CSRF: {}", csrf); + } catch (ExecutionException | InterruptedException | TimeoutException | JsonSyntaxException + | PostToAPIException e) { + logger.warn("Failed to send a POST to the API {}", e.getMessage()); } return true; } @@ -544,6 +625,16 @@ private synchronized boolean logIn() { logger.debug("Failed to login, HTTP status code: {}", httpStatusCode); return false; } + + url = STATUS; + Request request = httpClient.newRequest(url).method(HttpMethod.GET).header(HttpHeader.ACCEPT, + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") + .header(HttpHeader.ACCEPT_LANGUAGE, "sv-SE,sv;q=0.9,en-US;q=0.8,en;q=0.7") + .header(HttpHeader.REFERER, "https://mypages.verisure.com/login").followRedirects(false); + + ContentResponse response = request.send(); + String content = response.getContentAsString(); + logTraceWithPattern(response.getStatus(), content); return true; } else { return true; @@ -583,22 +674,16 @@ private boolean updateStatus() { VerisureInstallation installation = verisureInstallations.getValue(); try { configureInstallationInstance(installation.getInstallationId()); - int httpResultCode = setSessionCookieAuthLogin(); - if (httpResultCode == HttpStatus.OK_200) { - updateAlarmStatus(installation); - updateSmartLockStatus(installation); - updateMiceDetectionStatus(installation); - updateClimateStatus(installation); - updateDoorWindowStatus(installation); - updateUserPresenceStatus(installation); - updateSmartPlugStatus(installation); - updateBroadbandConnectionStatus(installation); - updateEventLogStatus(installation); - updateGatewayStatus(installation); - } else { - logger.debug("Failed to set session cookie and auth login, HTTP result code: {}", httpResultCode); - return false; - } + updateAlarmStatus(installation); + updateSmartLockStatus(installation); + updateMiceDetectionStatus(installation); + updateClimateStatus(installation); + updateDoorWindowStatus(installation); + updateUserPresenceStatus(installation); + updateSmartPlugStatus(installation); + updateBroadbandConnectionStatus(installation); + updateEventLogStatus(installation); + updateGatewayStatus(installation); } catch (ExecutionException | InterruptedException | TimeoutException | PostToAPIException e) { logger.debug("Failed to update status {}", e.getMessage()); return false; @@ -1063,7 +1148,6 @@ public void setInstallationName(@Nullable String installationName) { } private static class OperationDTO { - @SuppressWarnings("unused") private @Nullable String operationName; @SuppressWarnings("unused") @@ -1084,8 +1168,7 @@ public void setQuery(String query) { } } - public static class VariablesDTO { - + private static class VariablesDTO { @SuppressWarnings("unused") private boolean hideNotifications; @SuppressWarnings("unused") @@ -1119,7 +1202,6 @@ public void setGiid(String giid) { } private class PostToAPIException extends Exception { - private static final long serialVersionUID = 1L; public PostToAPIException(String message) { diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureSmartLockThingHandler.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureSmartLockThingHandler.java index 7c8041ae6a92d..be21daf4f76d5 100644 --- a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureSmartLockThingHandler.java +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/handler/VerisureSmartLockThingHandler.java @@ -154,7 +154,7 @@ private void handleAutoRelock(Command command) { if (smartLock != null) { BigDecimal installationId = smartLock.getSiteId(); try { - String csrf = session.getCsrfToken(installationId); + String csrf = session.getCsrfToken(); StringBuilder sb = new StringBuilder(deviceId); sb.insert(4, "+"); String data; @@ -213,7 +213,7 @@ private void handleSmartLockVolumeAndVoiceLevel(Command command, boolean setVolu } BigDecimal installationId = smartLocks.getSiteId(); try { - String csrf = session.getCsrfToken(installationId); + String csrf = session.getCsrfToken(); String url = SMARTLOCK_VOLUME_COMMAND; String data = "keypad.volume=MEDIUM&keypad.beepOnKeypress=true&_keypad.beepOnKeypress=on&siren.volume=MEDIUM&voiceDevice.volume=MEDIUM&doorLock.volume=" + volume + "&doorLock.voiceLevel=" + voiceLevel