From d8803732b3e61a13423abc975ea2c459354526b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=BCdel?= Date: Mon, 1 Mar 2021 15:19:10 +0100 Subject: [PATCH] [Tesla] Add SSO handler to authenticate against Tesla SSO service (#10259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian Güdel --- bundles/org.openhab.binding.tesla/pom.xml | 9 + .../tesla/internal/TeslaBindingConstants.java | 12 +- .../tesla/internal/TeslaHandlerFactory.java | 7 +- .../command/TeslaCommandExtension.java | 90 ++---- .../internal/handler/TeslaAccountHandler.java | 139 +++----- .../internal/handler/TeslaSSOHandler.java | 301 ++++++++++++++++++ .../tesla/internal/protocol/TokenRequest.java | 69 ---- .../protocol/TokenRequestPassword.java | 38 --- .../protocol/TokenRequestRefreshToken.java | 33 -- .../sso/AuthorizationCodeExchangeRequest.java | 35 ++ .../AuthorizationCodeExchangeResponse.java | 27 ++ .../protocol/sso/RefreshTokenRequest.java | 32 ++ .../protocol/sso/TokenExchangeRequest.java | 25 ++ .../protocol/{ => sso}/TokenResponse.java | 2 +- 14 files changed, 504 insertions(+), 315 deletions(-) create mode 100644 bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaSSOHandler.java delete mode 100644 bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenRequest.java delete mode 100644 bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenRequestPassword.java delete mode 100644 bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenRequestRefreshToken.java create mode 100644 bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/AuthorizationCodeExchangeRequest.java create mode 100644 bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/AuthorizationCodeExchangeResponse.java create mode 100644 bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/RefreshTokenRequest.java create mode 100644 bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/TokenExchangeRequest.java rename bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/{ => sso}/TokenResponse.java (92%) diff --git a/bundles/org.openhab.binding.tesla/pom.xml b/bundles/org.openhab.binding.tesla/pom.xml index 33521dcdf68aa..6eda82f6f7f39 100644 --- a/bundles/org.openhab.binding.tesla/pom.xml +++ b/bundles/org.openhab.binding.tesla/pom.xml @@ -14,4 +14,13 @@ openHAB Add-ons :: Bundles :: Tesla Binding + + + org.jsoup + jsoup + 1.8.3 + compile + + + diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaBindingConstants.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaBindingConstants.java index 0e9e7918dc6e6..bd35158403a7b 100644 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaBindingConstants.java +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaBindingConstants.java @@ -31,13 +31,21 @@ public class TeslaBindingConstants { public static final String PATH_DATA_REQUEST = "data_request/{cmd}"; public static final String PATH_VEHICLE_ID = "/{vid}/"; public static final String PATH_WAKE_UP = "wake_up"; - public static final String URI_ACCESS_TOKEN = "oauth/token"; + public static final String PATH_ACCESS_TOKEN = "oauth/token"; public static final String URI_EVENT = "https://streaming.vn.teslamotors.com/stream/"; - public static final String URI_OWNERS = "https://owner-api.teslamotors.com/"; + public static final String URI_OWNERS = "https://owner-api.teslamotors.com"; public static final String VALETPIN = "valetpin"; public static final String VEHICLES = "vehicles"; public static final String VIN = "vin"; + // SSO URI constants + public static final String SSO_SCOPES = "openid email offline_access"; + public static final String URI_SSO = "https://auth.tesla.com/oauth2/v3"; + public static final String PATH_AUTHORIZE = "authorize"; + public static final String PATH_TOKEN = "token"; + public static final String URI_CALLBACK = "https://auth.tesla.com/void/callback"; + public static final String CLIENT_ID = "ownerapi"; + // Tesla REST API commands public static final String COMMAND_ACTUATE_TRUNK = "actuate_trunk"; public static final String COMMAND_AUTO_COND_START = "auto_conditioning_start"; diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaHandlerFactory.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaHandlerFactory.java index f525dbb64ca9c..3b262328d8102 100644 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaHandlerFactory.java +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaHandlerFactory.java @@ -23,6 +23,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.tesla.internal.handler.TeslaAccountHandler; import org.openhab.binding.tesla.internal.handler.TeslaVehicleHandler; +import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingTypeUID; @@ -52,12 +53,14 @@ public class TeslaHandlerFactory extends BaseThingHandlerFactory { THING_TYPE_MODEL3, THING_TYPE_MODELX, THING_TYPE_MODELY); private final ClientBuilder clientBuilder; + private final HttpClientFactory httpClientFactory; @Activate - public TeslaHandlerFactory(@Reference ClientBuilder clientBuilder) { + public TeslaHandlerFactory(@Reference ClientBuilder clientBuilder, @Reference HttpClientFactory httpClientFactory) { this.clientBuilder = clientBuilder // .connectTimeout(EVENT_STREAM_CONNECT_TIMEOUT, TimeUnit.SECONDS) .readTimeout(EVENT_STREAM_READ_TIMEOUT, TimeUnit.SECONDS); + this.httpClientFactory = httpClientFactory; } @Override @@ -70,7 +73,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (thingTypeUID.equals(THING_TYPE_ACCOUNT)) { - return new TeslaAccountHandler((Bridge) thing, clientBuilder.build()); + return new TeslaAccountHandler((Bridge) thing, clientBuilder.build(), httpClientFactory); } else { return new TeslaVehicleHandler(thing, clientBuilder); } diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/command/TeslaCommandExtension.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/command/TeslaCommandExtension.java index 51bfd9db5459e..05765766514dd 100644 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/command/TeslaCommandExtension.java +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/command/TeslaCommandExtension.java @@ -12,42 +12,30 @@ */ package org.openhab.binding.tesla.internal.command; -import static org.openhab.binding.tesla.internal.TeslaBindingConstants.*; - import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.Arrays; import java.util.List; -import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.tesla.internal.TeslaBindingConstants; import org.openhab.binding.tesla.internal.discovery.TeslaAccountDiscoveryService; -import org.openhab.binding.tesla.internal.protocol.TokenRequest; -import org.openhab.binding.tesla.internal.protocol.TokenRequestPassword; -import org.openhab.binding.tesla.internal.protocol.TokenResponse; +import org.openhab.binding.tesla.internal.handler.TeslaSSOHandler; import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.io.console.Console; import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; import org.openhab.core.io.console.extensions.ConsoleCommandExtension; +import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.ThingUID; import org.openhab.core.util.UIDUtils; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.gson.Gson; /** * Console commands for interacting with the Tesla integration @@ -61,19 +49,18 @@ public class TeslaCommandExtension extends AbstractConsoleCommandExtension { private static final String CMD_LOGIN = "login"; - private final Logger logger = LoggerFactory.getLogger(TeslaCommandExtension.class); - @Reference(cardinality = ReferenceCardinality.OPTIONAL) private @Nullable ClientBuilder injectedClientBuilder; - private @Nullable WebTarget tokenTarget; - private final TeslaAccountDiscoveryService teslaAccountDiscoveryService; + private final HttpClientFactory httpClientFactory; @Activate - public TeslaCommandExtension(@Reference TeslaAccountDiscoveryService teslaAccountDiscoveryService) { + public TeslaCommandExtension(@Reference TeslaAccountDiscoveryService teslaAccountDiscoveryService, + @Reference HttpClientFactory httpClientFactory) { super("tesla", "Interact with the Tesla integration."); this.teslaAccountDiscoveryService = teslaAccountDiscoveryService; + this.httpClientFactory = httpClientFactory; } @Override @@ -120,59 +107,20 @@ public List getUsages() { } private void login(Console console, String username, String password) { - try { - Gson gson = new Gson(); - - TokenRequest token = new TokenRequestPassword(username, password); - String payLoad = gson.toJson(token); - - Response response = getTokenTarget().request() - .post(Entity.entity(payLoad, MediaType.APPLICATION_JSON_TYPE)); - - if (response != null) { - if (response.getStatus() == 200 && response.hasEntity()) { - String responsePayLoad = response.readEntity(String.class); - TokenResponse tokenResponse = gson.fromJson(responsePayLoad.trim(), TokenResponse.class); - console.println("Refresh token: " + tokenResponse.refresh_token); - - ThingUID thingUID = new ThingUID(TeslaBindingConstants.THING_TYPE_ACCOUNT, - UIDUtils.encode(username)); - DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel("Tesla Account") - .withProperty(TeslaBindingConstants.CONFIG_REFRESHTOKEN, tokenResponse.refresh_token) - .withProperty(TeslaBindingConstants.CONFIG_USERNAME, username) - .withRepresentationProperty(TeslaBindingConstants.CONFIG_USERNAME).build(); - teslaAccountDiscoveryService.thingDiscovered(result); - } else { - console.println( - "Failure: " + response.getStatus() + " " + response.getStatusInfo().getReasonPhrase()); - } - } - } catch (Exception e) { - console.println("Failed to retrieve token: " + e.getMessage()); - logger.error("Could not get refresh token.", e); - } - } - - private synchronized WebTarget getTokenTarget() { - WebTarget target = this.tokenTarget; - if (target != null) { - return target; + TeslaSSOHandler ssoHandler = new TeslaSSOHandler(httpClientFactory.getCommonHttpClient()); + + String refreshToken = ssoHandler.authenticate(username, password); + if (refreshToken != null) { + console.println("Refresh token: " + refreshToken); + + ThingUID thingUID = new ThingUID(TeslaBindingConstants.THING_TYPE_ACCOUNT, UIDUtils.encode(username)); + DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withLabel("Tesla Account") + .withProperty(TeslaBindingConstants.CONFIG_REFRESHTOKEN, refreshToken) + .withProperty(TeslaBindingConstants.CONFIG_USERNAME, username) + .withRepresentationProperty(TeslaBindingConstants.CONFIG_USERNAME).build(); + teslaAccountDiscoveryService.thingDiscovered(result); } else { - Client client; - try { - client = ClientBuilder.newBuilder().build(); - } catch (Exception e) { - // we seem to have no Jersey, so let's hope for an injected builder by CXF - if (this.injectedClientBuilder != null) { - client = injectedClientBuilder.build(); - } else { - throw new IllegalStateException("No JAX RS Client Builder available."); - } - } - WebTarget teslaTarget = client.target(URI_OWNERS); - target = teslaTarget.path(URI_ACCESS_TOKEN); - this.tokenTarget = target; - return target; + console.println("Failed to retrieve refresh token"); } } } diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java index 9ebe078b18549..053ac4c3d0f91 100644 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java @@ -16,7 +16,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -30,7 +29,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; -import javax.ws.rs.ProcessingException; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientRequestContext; import javax.ws.rs.client.ClientRequestFilter; @@ -42,13 +40,11 @@ import org.openhab.binding.tesla.internal.TeslaBindingConstants; import org.openhab.binding.tesla.internal.discovery.TeslaVehicleDiscoveryService; -import org.openhab.binding.tesla.internal.protocol.TokenRequest; -import org.openhab.binding.tesla.internal.protocol.TokenRequestPassword; -import org.openhab.binding.tesla.internal.protocol.TokenRequestRefreshToken; -import org.openhab.binding.tesla.internal.protocol.TokenResponse; import org.openhab.binding.tesla.internal.protocol.Vehicle; import org.openhab.binding.tesla.internal.protocol.VehicleConfig; +import org.openhab.binding.tesla.internal.protocol.sso.TokenResponse; import org.openhab.core.config.core.Configuration; +import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -85,13 +81,14 @@ public class TeslaAccountHandler extends BaseBridgeHandler { // REST Client API variables private final WebTarget teslaTarget; - private final WebTarget tokenTarget; WebTarget vehiclesTarget; // this cannot be marked final as it is used in the runnable final WebTarget vehicleTarget; final WebTarget dataRequestTarget; final WebTarget commandTarget; final WebTarget wakeUpTarget; + private final TeslaSSOHandler ssoHandler; + // Threading and Job related variables protected ScheduledFuture connectJob; @@ -108,10 +105,11 @@ public class TeslaAccountHandler extends BaseBridgeHandler { private TokenResponse logonToken; private final Set vehicleListeners = new HashSet<>(); - public TeslaAccountHandler(Bridge bridge, Client teslaClient) { + public TeslaAccountHandler(Bridge bridge, Client teslaClient, HttpClientFactory httpClientFactory) { super(bridge); this.teslaTarget = teslaClient.target(URI_OWNERS); - this.tokenTarget = teslaTarget.path(URI_ACCESS_TOKEN); + this.ssoHandler = new TeslaSSOHandler(httpClientFactory.getCommonHttpClient()); + this.vehiclesTarget = teslaTarget.path(API_VERSION).path(VEHICLES); this.vehicleTarget = vehiclesTarget.path(PATH_VEHICLE_ID); this.dataRequestTarget = vehicleTarget.path(PATH_DATA_REQUEST); @@ -272,106 +270,42 @@ private ThingStatusInfo authenticate() { if (hasExpired) { String username = (String) getConfig().get(CONFIG_USERNAME); + String password = (String) getConfig().get(CONFIG_PASSWORD); String refreshToken = (String) getConfig().get(CONFIG_REFRESHTOKEN); - if (refreshToken == null || refreshToken.isEmpty()) { - if (username != null && !username.isEmpty()) { - String password = (String) getConfig().get(CONFIG_PASSWORD); - return authenticate(username, password); - } else { - return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Neither a refresh token nor credentials are provided."); - } - } - - TokenRequestRefreshToken tokenRequest = null; - try { - tokenRequest = new TokenRequestRefreshToken(refreshToken); - } catch (GeneralSecurityException e) { - logger.error("An exception occurred while requesting a new token: '{}'", e.getMessage(), e); - } - String payLoad = gson.toJson(tokenRequest); - Response response = null; - try { - response = tokenTarget.request().post(Entity.entity(payLoad, MediaType.APPLICATION_JSON_TYPE)); - } catch (ProcessingException e) { - return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); - } - - logger.debug("Authenticating: Response: {}:{}", response.getStatus(), response.getStatusInfo()); - - if (response.getStatus() == 200 && response.hasEntity()) { - String responsePayLoad = response.readEntity(String.class); - TokenResponse tokenResponse = gson.fromJson(responsePayLoad.trim(), TokenResponse.class); - if (!refreshToken.equals(tokenResponse.refresh_token)) { - Configuration configuration = editConfiguration(); - configuration.put(CONFIG_REFRESHTOKEN, tokenResponse.refresh_token); - updateConfiguration(configuration); - } - - if (tokenResponse.access_token != null && !tokenResponse.access_token.isEmpty()) { - this.logonToken = tokenResponse; - logger.trace("Access Token is {}", logonToken.access_token); - } - return new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null); - } else if (response.getStatus() == 401) { - if (username != null && !username.isEmpty()) { - String password = (String) getConfig().get(CONFIG_PASSWORD); - return authenticate(username, password); - } else { - return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Refresh token is not valid and no credentials are provided."); - } - } else { - return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "HTTP returncode " + response.getStatus()); - } - } - return new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null); - } - - private ThingStatusInfo authenticate(String username, String password) { - TokenRequest token = null; - try { - token = new TokenRequestPassword(username, password); - } catch (GeneralSecurityException e) { - logger.error("An exception occurred while building a password request token: '{}'", e.getMessage(), e); - } - - if (token != null) { - String payLoad = gson.toJson(token); - - Response response = tokenTarget.request().post(Entity.entity(payLoad, MediaType.APPLICATION_JSON_TYPE)); - - if (response != null) { - logger.debug("Authenticating: Response : {}:{}", response.getStatus(), response.getStatusInfo()); + if (refreshToken == null || refreshToken.isEmpty()) { + if (username != null && !username.isEmpty() && password != null && !password.isEmpty()) { + try { + refreshToken = ssoHandler.authenticate(username, password); + } catch (Exception e) { + logger.error("An exception occurred while obtaining refresh token with username/password: '{}'", + e.getMessage()); + } - if (response.getStatus() == 200 && response.hasEntity()) { - String responsePayLoad = response.readEntity(String.class); - TokenResponse tokenResponse = gson.fromJson(responsePayLoad.trim(), TokenResponse.class); - if (tokenResponse.token_type != null && !tokenResponse.access_token.isEmpty()) { - this.logonToken = tokenResponse; + if (refreshToken != null) { + // store refresh token from SSO endpoint in config, clear the password Configuration cfg = editConfiguration(); - cfg.put(TeslaBindingConstants.CONFIG_REFRESHTOKEN, logonToken.refresh_token); + cfg.put(TeslaBindingConstants.CONFIG_REFRESHTOKEN, refreshToken); cfg.remove(TeslaBindingConstants.CONFIG_PASSWORD); updateConfiguration(cfg); - return new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null); + } else { + return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Failed to obtain refresh token with username/password."); } - } else if (response.getStatus() == 401) { - return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Invalid credentials."); } else { - return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "HTTP returncode " + response.getStatus()); + return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Neither a refresh token nor credentials are provided."); } - } else { - logger.debug("Authenticating: Response was null"); - return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Failed retrieving a response from the server."); + } + + this.logonToken = ssoHandler.getAccessToken(refreshToken); + if (this.logonToken == null) { + return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Failed to obtain access token for API."); } } - return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Cannot build request from credentials."); + + return new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null); } protected String invokeAndParse(String vehicleId, String command, String payLoad, WebTarget target) { @@ -424,7 +358,9 @@ protected String invokeAndParse(String vehicleId, String command, String payLoad try { lock.lock(); - if (getThing().getStatus() != ThingStatus.ONLINE) { + ThingStatusInfo status = getThing().getStatusInfo(); + if (status.getStatus() != ThingStatus.ONLINE + && status.getStatusDetail() != ThingStatusDetail.CONFIGURATION_ERROR) { logger.debug("Setting up an authenticated connection to the Tesla back-end"); ThingStatusInfo authenticationResult = authenticate(); @@ -471,7 +407,12 @@ protected String invokeAndParse(String vehicleId, String command, String payLoad updateStatus(ThingStatus.OFFLINE); } } + } else if (authenticationResult.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR) { + // make sure to set thing to CONFIGURATION_ERROR in case of failed authentication in order not to + // hit request limit on retries on the Tesla SSO endpoints. + updateStatus(ThingStatus.OFFLINE, authenticationResult.getStatusDetail()); } + } } catch (Exception e) { logger.error("An exception occurred while connecting to the Tesla back-end: '{}'", e.getMessage(), e); diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaSSOHandler.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaSSOHandler.java new file mode 100644 index 0000000000000..0cb256a0be1b4 --- /dev/null +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaSSOHandler.java @@ -0,0 +1,301 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tesla.internal.handler; + +import static org.openhab.binding.tesla.internal.TeslaBindingConstants.*; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Iterator; +import java.util.Random; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.util.FormContentProvider; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.util.Fields; +import org.eclipse.jetty.util.Fields.Field; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.openhab.binding.tesla.internal.protocol.sso.AuthorizationCodeExchangeRequest; +import org.openhab.binding.tesla.internal.protocol.sso.AuthorizationCodeExchangeResponse; +import org.openhab.binding.tesla.internal.protocol.sso.RefreshTokenRequest; +import org.openhab.binding.tesla.internal.protocol.sso.TokenExchangeRequest; +import org.openhab.binding.tesla.internal.protocol.sso.TokenResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +/** + * The {@link TeslaSSOHandler} is responsible for authenticating with the Tesla SSO service. + * + * @author Christian Güdel - Initial contribution + */ +@NonNullByDefault +public class TeslaSSOHandler { + + private final HttpClient httpClient; + private final Gson gson = new Gson(); + private final Logger logger = LoggerFactory.getLogger(TeslaSSOHandler.class); + + public TeslaSSOHandler(HttpClient httpClient) { + this.httpClient = httpClient; + } + + @Nullable + public TokenResponse getAccessToken(String refreshToken) { + logger.debug("Exchanging SSO refresh token for API access token"); + + // get a new access token for the owner API token endpoint + RefreshTokenRequest refreshRequest = new RefreshTokenRequest(refreshToken); + String refreshTokenPayload = gson.toJson(refreshRequest); + + final org.eclipse.jetty.client.api.Request request = httpClient.newRequest(URI_SSO + "/" + PATH_TOKEN); + request.content(new StringContentProvider(refreshTokenPayload)); + request.header(HttpHeader.CONTENT_TYPE, "application/json"); + request.method(HttpMethod.POST); + + ContentResponse refreshResponse = executeHttpRequest(request); + + if (refreshResponse != null && refreshResponse.getStatus() == 200) { + String refreshTokenResponse = refreshResponse.getContentAsString(); + TokenResponse tokenResponse = gson.fromJson(refreshTokenResponse.trim(), TokenResponse.class); + + if (tokenResponse != null && tokenResponse.access_token != null && !tokenResponse.access_token.isEmpty()) { + TokenExchangeRequest token = new TokenExchangeRequest(); + String tokenPayload = gson.toJson(token); + + final org.eclipse.jetty.client.api.Request logonRequest = httpClient + .newRequest(URI_OWNERS + "/" + PATH_ACCESS_TOKEN); + logonRequest.content(new StringContentProvider(tokenPayload)); + logonRequest.header(HttpHeader.CONTENT_TYPE, "application/json"); + logonRequest.header(HttpHeader.AUTHORIZATION, "Bearer " + tokenResponse.access_token); + logonRequest.method(HttpMethod.POST); + + ContentResponse logonTokenResponse = executeHttpRequest(logonRequest); + + if (logonTokenResponse != null && logonTokenResponse.getStatus() == 200) { + String tokenResponsePayload = logonTokenResponse.getContentAsString(); + TokenResponse tr = gson.fromJson(tokenResponsePayload.trim(), TokenResponse.class); + + if (tr != null && tr.token_type != null && !tr.access_token.isEmpty()) { + return tr; + } + } else { + logger.debug("An error occurred while exchanging SSO access token for API access token: {}", + (logonTokenResponse != null ? logonTokenResponse.getStatus() : "no response")); + } + } + } else { + logger.debug("An error occurred during refresh of SSO token: {}", + (refreshResponse != null ? refreshResponse.getStatus() : "no response")); + } + + return null; + } + + /** + * Authenticates using username/password against Tesla SSO endpoints. + * + * @param username Username + * @param password Password + * @return Refresh token for use with {@link getAccessToken} + */ + @Nullable + public String authenticate(String username, String password) { + String codeVerifier = generateRandomString(86); + String codeChallenge = null; + String state = generateRandomString(10); + + try { + codeChallenge = getCodeChallenge(codeVerifier); + } catch (NoSuchAlgorithmException e) { + logger.error("An exception occurred while building login page request: '{}'", e.getMessage()); + return null; + } + + final org.eclipse.jetty.client.api.Request loginPageRequest = httpClient + .newRequest(URI_SSO + "/" + PATH_AUTHORIZE); + loginPageRequest.method(HttpMethod.GET); + loginPageRequest.followRedirects(false); + + addQueryParameters(loginPageRequest, codeChallenge, state); + + ContentResponse loginPageResponse = executeHttpRequest(loginPageRequest); + if (loginPageResponse == null + || (loginPageResponse.getStatus() != 200 && loginPageResponse.getStatus() != 302)) { + logger.debug("Failed to obtain SSO login page, response status code: {}", + (loginPageResponse != null ? loginPageResponse.getStatus() : "no response")); + return null; + } + + logger.debug("Obtained SSO login page"); + + String authorizationCode = null; + + if (loginPageResponse.getStatus() == 302) { + String redirectLocation = loginPageResponse.getHeaders().get(HttpHeader.LOCATION); + if (isValidRedirectLocation(redirectLocation)) { + authorizationCode = extractAuthorizationCodeFromUri(redirectLocation); + } else { + logger.debug("Unexpected redirect location received when fetching login page: {}", redirectLocation); + return null; + } + } else { + Fields postData = new Fields(); + + try { + Document doc = Jsoup.parse(loginPageResponse.getContentAsString()); + Element loginForm = doc.getElementsByTag("form").first(); + + Iterator elIt = loginForm.getElementsByTag("input").iterator(); + while (elIt.hasNext()) { + Element input = elIt.next(); + if (input.attr("type").equalsIgnoreCase("hidden")) { + postData.add(input.attr("name"), input.attr("value")); + } + } + } catch (Exception e) { + logger.error("Failed to parse login page: {}", e.getMessage()); + logger.debug("login page response {}", loginPageResponse.getContentAsString()); + return null; + } + + postData.add("identity", username); + postData.add("credential", password); + + final org.eclipse.jetty.client.api.Request formSubmitRequest = httpClient + .newRequest(URI_SSO + "/" + PATH_AUTHORIZE); + formSubmitRequest.method(HttpMethod.POST); + formSubmitRequest.content(new FormContentProvider(postData)); + formSubmitRequest.followRedirects(false); // this should return a 302 ideally, but that location doesn't + // exist + addQueryParameters(formSubmitRequest, codeChallenge, state); + + ContentResponse formSubmitResponse = executeHttpRequest(formSubmitRequest); + if (formSubmitResponse == null || formSubmitResponse.getStatus() != 302) { + logger.debug("Failed to obtain code from SSO login page when submitting form, response status code: {}", + (formSubmitResponse != null ? formSubmitResponse.getStatus() : "no response")); + return null; + } + + String redirectLocation = formSubmitResponse.getHeaders().get(HttpHeader.LOCATION); + if (!isValidRedirectLocation(redirectLocation)) { + logger.debug("Redirect location not set or doesn't match expected callback URI {}: {}", URI_CALLBACK, + redirectLocation); + return null; + } + + logger.debug("Obtained valid redirect location"); + authorizationCode = extractAuthorizationCodeFromUri(redirectLocation); + } + + if (authorizationCode == null) { + logger.debug("Did not receive an authorization code"); + return null; + } + + // exchange authorization code for SSO access + refresh token + AuthorizationCodeExchangeRequest request = new AuthorizationCodeExchangeRequest(authorizationCode, + codeVerifier); + String payload = gson.toJson(request); + + final org.eclipse.jetty.client.api.Request tokenExchangeRequest = httpClient + .newRequest(URI_SSO + "/" + PATH_TOKEN); + tokenExchangeRequest.content(new StringContentProvider(payload)); + tokenExchangeRequest.header(HttpHeader.CONTENT_TYPE, "application/json"); + tokenExchangeRequest.method(HttpMethod.POST); + + ContentResponse response = executeHttpRequest(tokenExchangeRequest); + if (response != null && response.getStatus() == 200) { + String responsePayload = response.getContentAsString(); + AuthorizationCodeExchangeResponse ssoTokenResponse = gson.fromJson(responsePayload.trim(), + AuthorizationCodeExchangeResponse.class); + if (ssoTokenResponse != null && ssoTokenResponse.token_type != null + && !ssoTokenResponse.access_token.isEmpty()) { + logger.debug("Obtained valid SSO refresh token"); + return ssoTokenResponse.refresh_token; + } + } else { + logger.debug("An error occurred while exchanging authorization code for SSO refresh token: {}", + (response != null ? response.getStatus() : "no response")); + } + + return null; + } + + private Boolean isValidRedirectLocation(@Nullable String redirectLocation) { + return redirectLocation != null && redirectLocation.startsWith(URI_CALLBACK); + } + + @Nullable + private String extractAuthorizationCodeFromUri(String uri) { + Field code = httpClient.newRequest(uri).getParams().get("code"); + return code != null ? code.getValue() : null; + } + + private String getCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(codeVerifier.getBytes()); + + StringBuilder hashStr = new StringBuilder(hash.length * 2); + for (byte b : hash) { + hashStr.append(String.format("%02x", b)); + } + + return Base64.getUrlEncoder().encodeToString(hashStr.toString().getBytes()); + } + + private String generateRandomString(int length) { + Random random = new Random(); + + String generatedString = random.ints('a', 'z' + 1).limit(length) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString(); + + return generatedString; + } + + private void addQueryParameters(org.eclipse.jetty.client.api.Request request, String codeChallenge, String state) { + request.param("client_id", CLIENT_ID); + request.param("code_challenge", codeChallenge); + request.param("code_challenge_method", "S256"); + request.param("redirect_uri", URI_CALLBACK); + request.param("response_type", "code"); + request.param("scope", SSO_SCOPES); + request.param("state", state); + } + + @Nullable + private ContentResponse executeHttpRequest(org.eclipse.jetty.client.api.Request request) { + request.timeout(10, TimeUnit.SECONDS); + + ContentResponse response; + try { + response = request.send(); + return response; + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.debug("An exception occurred while invoking a HTTP request: '{}'", e.getMessage()); + return null; + } + } +} diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenRequest.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenRequest.java deleted file mode 100644 index 00b90f79280db..0000000000000 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenRequest.java +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright (c) 2010-2021 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.tesla.internal.protocol; - -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.ShortBufferException; -import javax.crypto.spec.SecretKeySpec; - -import org.openhab.binding.tesla.internal.TeslaBindingConstants; - -/** - * The {@link TokenRequest} is a datastructure to capture - * authentication/credentials required to log into the - * Tesla Remote Service - * - * @author Karel Goderis - Initial contribution - * @author Nicolai Grødum - Adding token based auth - */ -@SuppressWarnings("unused") -public abstract class TokenRequest { - private String client_id; - private String client_secret; - - TokenRequest() throws GeneralSecurityException { - byte[] ci = { 115, -51, 67, -104, -107, 16, -116, -114, -11, -120, 41, 84, -106, -15, -67, 78, -10, -24, -47, - 124, 35, 73, 10, 43, -9, 123, 127, 126, -114, 58, 23, 3, 115, -70, -115, 46, 17, 87, -115, 31, -67, -90, - -107, -100, 59, 18, -19, 91, 95, -52, 82, 91, -37, -83, -74, 39, 12, 59, 14, -81, 3, 95, -111, 72 }; - - byte[] cs = { -28, 97, -94, 108, 69, -40, 111, 53, 88, -57, 82, 111, 57, 98, 116, -63, -75, -37, 16, 95, 2, - -113, -46, -112, 32, 73, -43, 23, -114, 38, -110, -85, -42, 41, 98, 118, 30, -2, -11, 93, 22, 89, 56, - 105, -128, 20, -24, -108, 76, 31, -19, 60, 69, -98, -122, 54, 67, 19, 72, -37, 106, 62, -120, -52 }; - - SecretKeySpec key = new SecretKeySpec(TeslaBindingConstants.API_NAME.getBytes(), "AES"); - Cipher cipher; - try { - cipher = Cipher.getInstance("AES/ECB/NoPadding"); - byte[] plainText = new byte[ci.length]; - cipher.init(Cipher.DECRYPT_MODE, key); - int ptLength = cipher.update(ci, 0, ci.length, plainText, 0); - cipher.doFinal(plainText, ptLength); - this.client_id = new String(plainText); - - cipher.init(Cipher.DECRYPT_MODE, key); - ptLength = cipher.update(cs, 0, cs.length, plainText, 0); - cipher.doFinal(plainText, ptLength); - this.client_secret = new String(plainText); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | ShortBufferException - | IllegalBlockSizeException | BadPaddingException e) { - throw e; - } - } -} diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenRequestPassword.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenRequestPassword.java deleted file mode 100644 index b3360c8cd5ac8..0000000000000 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenRequestPassword.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2010-2021 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.tesla.internal.protocol; - -import java.security.GeneralSecurityException; - -/** - * The {@link TokenRequestPassword} is a datastructure to capture - * authentication/credentials required to log into the - * Tesla Remote Service - * - * @author Karel Goderis - Initial contribution - * @author Nicolai Grødum - Adding token based auth - */ -@SuppressWarnings("unused") -public class TokenRequestPassword extends TokenRequest { - - private String grant_type = "password"; - private String email; - private String password; - - public TokenRequestPassword(String email, String password) throws GeneralSecurityException { - super(); - - this.email = email; - this.password = password; - } -} diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenRequestRefreshToken.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenRequestRefreshToken.java deleted file mode 100644 index 89226919e5454..0000000000000 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenRequestRefreshToken.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2010-2021 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.tesla.internal.protocol; - -import java.security.GeneralSecurityException; - -/** - * The {@link TokenRequestRefreshToken} is a datastructure to capture - * authentication/credentials required to log into the - * Tesla Remote Service - * - * @author Nicolai Grødum - Adding token based auth - */ -public class TokenRequestRefreshToken extends TokenRequest { - - private String grant_type = "refresh_token"; - private String refresh_token; - - public TokenRequestRefreshToken(String refresh_token) throws GeneralSecurityException { - super(); - this.refresh_token = refresh_token; - } -} diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/AuthorizationCodeExchangeRequest.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/AuthorizationCodeExchangeRequest.java new file mode 100644 index 0000000000000..c8fa3303f49e0 --- /dev/null +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/AuthorizationCodeExchangeRequest.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tesla.internal.protocol.sso; + +import static org.openhab.binding.tesla.internal.TeslaBindingConstants.*; + +/** + * The {@link AuthorizationCodeExchangeRequest} is a datastructure to exchange + * the authorization code for an access token on the SSO endpoint + * + * @author Christian Güdel - Initial contribution + */ +@SuppressWarnings("unused") // Unused fields must not be removed since they are used for serialization to JSON +public class AuthorizationCodeExchangeRequest { + private String grant_type = "authorization_code"; + private String client_id = CLIENT_ID; + private String code; + private String code_verifier; + private String redirect_uri = URI_CALLBACK; + + public AuthorizationCodeExchangeRequest(String code, String codeVerifier) { + this.code = code; + this.code_verifier = codeVerifier; + } +} diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/AuthorizationCodeExchangeResponse.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/AuthorizationCodeExchangeResponse.java new file mode 100644 index 0000000000000..14c5fc0d29bf8 --- /dev/null +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/AuthorizationCodeExchangeResponse.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tesla.internal.protocol.sso; + +/** + * The {@link AuthorizationCodeExchangeResponse} is a datastructure to capture + * the response of an {@link AuthorizationCodeExchangeRequest} + * + * @author Christian Güdel - Initial contribution + */ +public class AuthorizationCodeExchangeResponse { + public String access_token; + public String refresh_token; + public String expires_in; + public String state; + public String token_type; +} diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/RefreshTokenRequest.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/RefreshTokenRequest.java new file mode 100644 index 0000000000000..3335001cacf5b --- /dev/null +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/RefreshTokenRequest.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tesla.internal.protocol.sso; + +import static org.openhab.binding.tesla.internal.TeslaBindingConstants.*; + +/** + * The {@link RefreshTokenRequest} is a datastructure to refresh + * the access token for the SSO endpoint + * + * @author Christian Güdel - Initial contribution + */ +public class RefreshTokenRequest { + public String grant_type = "refresh_token"; + public String client_id = CLIENT_ID; + public String refresh_token; + public String scope = SSO_SCOPES; + + public RefreshTokenRequest(String refresh_token) { + this.refresh_token = refresh_token; + } +} diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/TokenExchangeRequest.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/TokenExchangeRequest.java new file mode 100644 index 0000000000000..758953813998f --- /dev/null +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/TokenExchangeRequest.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tesla.internal.protocol.sso; + +/** + * The {@link TokenExchangeRequest} is a datastructure to exchange + * the access token from the SSO endpoint for an owners API access token + * + * @author Christian Güdel - Initial contribution + */ +public class TokenExchangeRequest { + public String grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"; + public String client_id = "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384"; + public String client_secret = "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3"; +} diff --git a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenResponse.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/TokenResponse.java similarity index 92% rename from bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenResponse.java rename to bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/TokenResponse.java index 396b9eac9941d..0e5768c6bcbd5 100644 --- a/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenResponse.java +++ b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/TokenResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.tesla.internal.protocol; +package org.openhab.binding.tesla.internal.protocol.sso; /** * The {@link TokenResponse} is a datastructure to capture