diff --git a/bundles/org.openhab.binding.tesla/pom.xml b/bundles/org.openhab.binding.tesla/pom.xml
index 33521dcdf68..6eda82f6f7f 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 0e9e7918dc6..bd35158403a 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 f525dbb64ca..3b262328d81 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.NonNullByDefault;
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 class TeslaHandlerFactory extends BaseThingHandlerFactory {
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 51bfd9db545..05765766514 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 class TeslaCommandExtension extends AbstractConsoleCommandExtension {
}
private void login(Console console, String username, String password) {
- try {
- Gson gson = new Gson();
+ TeslaSSOHandler ssoHandler = new TeslaSSOHandler(httpClientFactory.getCommonHttpClient());
- TokenRequest token = new TokenRequestPassword(username, password);
- String payLoad = gson.toJson(token);
+ String refreshToken = ssoHandler.authenticate(username, password);
+ if (refreshToken != null) {
+ console.println("Refresh token: " + refreshToken);
- 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;
+ 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 9ebe078b185..053ac4c3d0f 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 static org.openhab.binding.tesla.internal.TeslaBindingConstants.*;
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.ScheduledFuture;
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 javax.ws.rs.core.Response;
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,108 +270,44 @@ public class TeslaAccountHandler extends BaseBridgeHandler {
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);
+ 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 (refreshToken != null) {
+ // store refresh token from SSO endpoint in config, clear the password
+ Configuration cfg = editConfiguration();
+ cfg.put(TeslaBindingConstants.CONFIG_REFRESHTOKEN, refreshToken);
+ cfg.remove(TeslaBindingConstants.CONFIG_PASSWORD);
+ updateConfiguration(cfg);
+ } else {
+ return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "Failed to obtain refresh token with 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());
+ 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.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 (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;
- Configuration cfg = editConfiguration();
- cfg.put(TeslaBindingConstants.CONFIG_REFRESHTOKEN, logonToken.refresh_token);
- cfg.remove(TeslaBindingConstants.CONFIG_PASSWORD);
- updateConfiguration(cfg);
- return new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
- }
- } 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());
- }
- } else {
- logger.debug("Authenticating: Response was null");
- return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
- "Failed retrieving a response from the server.");
- }
- }
- return new ThingStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
- "Cannot build request from credentials.");
- }
-
protected String invokeAndParse(String vehicleId, String command, String payLoad, WebTarget target) {
logger.debug("Invoking: {}", command);
@@ -424,7 +358,9 @@ public class TeslaAccountHandler extends BaseBridgeHandler {
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 @@ public class TeslaAccountHandler extends BaseBridgeHandler {
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 00000000000..0cb256a0be1
--- /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 00b90f79280..00000000000
--- 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 b3360c8cd5a..00000000000
--- 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 89226919e54..00000000000
--- 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 00000000000..c8fa3303f49
--- /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 00000000000..14c5fc0d29b
--- /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 00000000000..3335001cacf
--- /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 00000000000..75895381399
--- /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 396b9eac994..0e5768c6bcb 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