]> git.basschouten.com Git - openhab-addons.git/commitdiff
[Tesla] Add SSO handler to authenticate against Tesla SSO service (#10259)
authorChristian Güdel <cg@dmesg.ch>
Mon, 1 Mar 2021 14:19:10 +0000 (15:19 +0100)
committerGitHub <noreply@github.com>
Mon, 1 Mar 2021 14:19:10 +0000 (15:19 +0100)
Signed-off-by: Christian Güdel <cg@dmesg.ch>
15 files changed:
bundles/org.openhab.binding.tesla/pom.xml
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaBindingConstants.java
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/TeslaHandlerFactory.java
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/command/TeslaCommandExtension.java
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaAccountHandler.java
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/handler/TeslaSSOHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenRequest.java [deleted file]
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenRequestPassword.java [deleted file]
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenRequestRefreshToken.java [deleted file]
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenResponse.java [deleted file]
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/AuthorizationCodeExchangeRequest.java [new file with mode: 0644]
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/AuthorizationCodeExchangeResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/RefreshTokenRequest.java [new file with mode: 0644]
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/TokenExchangeRequest.java [new file with mode: 0644]
bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/TokenResponse.java [new file with mode: 0644]

index 33521dcdf68aa035a0a6b72948f1422a77301c59..6eda82f6f7f3902b89368b0d75554337e0d8c2b2 100644 (file)
 
   <name>openHAB Add-ons :: Bundles :: Tesla Binding</name>
 
+  <dependencies>
+    <dependency>
+      <groupId>org.jsoup</groupId>
+      <artifactId>jsoup</artifactId>
+      <version>1.8.3</version>
+      <scope>compile</scope>
+    </dependency>
+  </dependencies>
+
 </project>
index 0e9e7918dc6e620833077fff93b28797c0ca26ed..bd35158403a7bb2765e44a3ea12b6e425322a82a 100644 (file)
@@ -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";
index f525dbb64ca9cf015cc7a49448383bf56afbb8f3..3b262328d8102c511a2ef3a6ebf52a705cf6d167 100644 (file)
@@ -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);
         }
index 51bfd9db5459e8fd238d766c1bb4a8fcee7cc08f..05765766514dd65a40240ad47332cd2ff2c42265 100644 (file)
  */
 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();
-
-            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");
         }
     }
 }
index 9ebe078b18549a0a97d789a4de12b5a568eca4ce..053ac4c3d0f91f80da3c4eedb1426d6ec09b6c47 100644 (file)
@@ -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<VehicleListener> 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 @@ 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);
-                } 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 @@ 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 (file)
index 0000000..0cb256a
--- /dev/null
@@ -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<Element> 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 (file)
index 00b90f7..0000000
+++ /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 (file)
index b3360c8..0000000
+++ /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 (file)
index 8922691..0000000
+++ /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/TokenResponse.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/TokenResponse.java
deleted file mode 100644 (file)
index 396b9ea..0000000
+++ /dev/null
@@ -1,31 +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;
-
-/**
- * The {@link TokenResponse} is a datastructure to capture
- * authentication response from Tesla Remote Service
- *
- * @author Nicolai Grødum
- */
-public class TokenResponse {
-
-    public String access_token;
-    public String token_type;
-    public Long expires_in;
-    public Long created_at;
-    public String refresh_token;
-
-    public TokenResponse() {
-    }
-}
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 (file)
index 0000000..c8fa330
--- /dev/null
@@ -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 (file)
index 0000000..14c5fc0
--- /dev/null
@@ -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 (file)
index 0000000..3335001
--- /dev/null
@@ -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 (file)
index 0000000..7589538
--- /dev/null
@@ -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/sso/TokenResponse.java b/bundles/org.openhab.binding.tesla/src/main/java/org/openhab/binding/tesla/internal/protocol/sso/TokenResponse.java
new file mode 100644 (file)
index 0000000..0e5768c
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * 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 TokenResponse} is a datastructure to capture
+ * authentication response from Tesla Remote Service
+ *
+ * @author Nicolai Grødum
+ */
+public class TokenResponse {
+
+    public String access_token;
+    public String token_type;
+    public Long expires_in;
+    public Long created_at;
+    public String refresh_token;
+
+    public TokenResponse() {
+    }
+}