[Tesla] Add SSO handler to authenticate against Tesla SSO service (#10259)
Signed-off-by: Christian Güdel <cg@dmesg.ch>pull/10265/head
parent
9c112a9a1d
commit
62e829f968
|
@ -14,4 +14,13 @@
|
|||
|
||||
<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>
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
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_REFRESHTOKEN, refreshToken)
|
||||
.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;
|
||||
} 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,108 +270,44 @@ public class TeslaAccountHandler extends BaseBridgeHandler {
|
|||
|
||||
if (hasExpired) {
|
||||
String username = (String) getConfig().get(CONFIG_USERNAME);
|
||||
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);
|
||||
String refreshToken = (String) getConfig().get(CONFIG_REFRESHTOKEN);
|
||||
|
||||
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 (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 {
|
||||
this.logonToken = ssoHandler.getAccessToken(refreshToken);
|
||||
if (this.logonToken == null) {
|
||||
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());
|
||||
"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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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
|
Loading…
Reference in New Issue