Skip to content

Commit

Permalink
[boschindego] Refactor OAuth2 implementation (openhab#14950)
Browse files Browse the repository at this point in the history
* Delete OAuth2 token when thing is removed
* Fix reinitialization
* Introduce abstraction for OAuthClientService
* Improve thing status synchronization

---------

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
Signed-off-by: Matt Myers <mmyers75@icloud.com>
  • Loading branch information
jlaur authored and matchews committed Aug 9, 2023
1 parent 145c02d commit c844569
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 87 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2023 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.boschindego.internal;

import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*;

import java.io.IOException;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;

/**
* The {@link AuthorizationController} acts as a bridge between
* {@link OAuthClientService} and {@link IndegoController}.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class AuthorizationController implements AuthorizationProvider {

private static final String BEARER = "Bearer ";

private final AuthorizationListener listener;

private OAuthClientService oAuthClientService;

public AuthorizationController(OAuthClientService oAuthClientService, AuthorizationListener listener) {
this.oAuthClientService = oAuthClientService;
this.listener = listener;
}

public void setOAuthClientService(OAuthClientService oAuthClientService) {
this.oAuthClientService = oAuthClientService;
}

public String getAuthorizationHeader() throws IndegoAuthenticationException {
final AccessTokenResponse accessTokenResponse;
try {
accessTokenResponse = getAccessToken();
} catch (OAuthException | OAuthResponseException e) {
var throwable = new IndegoAuthenticationException(
"Error fetching access token. Invalid authcode? Please generate a new one -> "
+ getAuthorizationUrl(),
e);
listener.onFailedAuthorization(throwable);
throw throwable;
} catch (IOException e) {
var throwable = new IndegoAuthenticationException("An unexpected IOException occurred: " + e.getMessage(),
e);
listener.onFailedAuthorization(throwable);
throw throwable;
}

String accessToken = accessTokenResponse.getAccessToken();
if (accessToken == null || accessToken.isEmpty()) {
var throwable = new IndegoAuthenticationException(
"No access token. Is this thing authorized? -> " + getAuthorizationUrl());
listener.onFailedAuthorization(throwable);
throw throwable;
}
if (accessTokenResponse.getRefreshToken() == null || accessTokenResponse.getRefreshToken().isEmpty()) {
var throwable = new IndegoAuthenticationException(
"No refresh token. Please reauthorize -> " + getAuthorizationUrl());
listener.onFailedAuthorization(throwable);
throw throwable;
}

listener.onSuccessfulAuthorization();

return BEARER + accessToken;
}

public AccessTokenResponse getAccessToken() throws OAuthException, OAuthResponseException, IOException {
AccessTokenResponse accessTokenResponse = oAuthClientService.getAccessTokenResponse();
if (accessTokenResponse == null) {
throw new OAuthException("No access token response");
}

return accessTokenResponse;
}

private String getAuthorizationUrl() {
try {
return oAuthClientService.getAuthorizationUrl(BSK_REDIRECT_URI, BSK_SCOPE, null);
} catch (OAuthException e) {
return "";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2023 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.boschindego.internal;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* {@link} AuthorizationListener} is used for notifying {@link BoschAccountHandler}
* when authorization state has changed and for notifying {@link BoschIndegoHandler}
* when authorization flow is completed.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public interface AuthorizationListener {
/**
* Called upon successful OAuth authorization.
*/
void onSuccessfulAuthorization();

/**
* Called upon failed OAuth authorization.
*/
void onFailedAuthorization(Throwable throwable);

/**
* Called upon successful completion of OAuth authorization flow.
*/
void onAuthorizationFlowCompleted();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2023 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.boschindego.internal;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschindego.internal.exceptions.IndegoException;

/**
* The {@link AuthorizationProvider} is responsible for providing
* authorization headers needed for communicating with the Bosch Indego
* cloud services.
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public interface AuthorizationProvider {
/**
* Get HTTP authorization header for authenticating with Bosch Indego services.
*
* @return the header contents
* @throws IndegoException if not authorized
*/
String getAuthorizationHeader() throws IndegoException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@
*/
package org.openhab.binding.boschindego.internal;

import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*;

import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
Expand All @@ -41,10 +38,6 @@
import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidResponseException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.openhab.core.library.types.RawType;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
Expand All @@ -66,23 +59,22 @@ public class IndegoController {

private static final String BASE_URL = "https://api.indego-cloud.iot.bosch-si.com/api/v1/";
private static final String CONTENT_TYPE_HEADER = "application/json";
private static final String BEARER = "Bearer ";

private final Logger logger = LoggerFactory.getLogger(IndegoController.class);
private final Gson gson = new GsonBuilder().registerTypeAdapter(Instant.class, new InstantDeserializer()).create();
private final HttpClient httpClient;
private final OAuthClientService oAuthClientService;
private final AuthorizationProvider authorizationProvider;
private final String userAgent;

/**
* Initialize the controller instance.
*
* @param httpClient the HttpClient for communicating with the service
* @param oAuthClientService the OAuthClientService for authorization
* @param authorizationProvider the AuthorizationProvider for authenticating with the service
*/
public IndegoController(HttpClient httpClient, OAuthClientService oAuthClientService) {
public IndegoController(HttpClient httpClient, AuthorizationProvider authorizationProvider) {
this.httpClient = httpClient;
this.oAuthClientService = oAuthClientService;
this.authorizationProvider = authorizationProvider;
userAgent = "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString();
}

Expand Down Expand Up @@ -112,39 +104,6 @@ public DevicePropertiesResponse getDeviceProperties(String serialNumber)
return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/", DevicePropertiesResponse.class);
}

private String getAuthorizationUrl() {
try {
return oAuthClientService.getAuthorizationUrl(BSK_REDIRECT_URI, BSK_SCOPE, null);
} catch (OAuthException e) {
return "";
}
}

private String getAuthorizationHeader() throws IndegoException {
final AccessTokenResponse accessTokenResponse;
try {
accessTokenResponse = oAuthClientService.getAccessTokenResponse();
} catch (OAuthException | OAuthResponseException e) {
logger.debug("Error fetching access token: {}", e.getMessage(), e);
throw new IndegoAuthenticationException(
"Error fetching access token. Invalid authcode? Please generate a new one -> "
+ getAuthorizationUrl(),
e);
} catch (IOException e) {
throw new IndegoException("An unexpected IOException occurred: " + e.getMessage(), e);
}
if (accessTokenResponse == null || accessTokenResponse.getAccessToken() == null
|| accessTokenResponse.getAccessToken().isEmpty()) {
throw new IndegoAuthenticationException(
"No access token. Is this thing authorized? -> " + getAuthorizationUrl());
}
if (accessTokenResponse.getRefreshToken() == null || accessTokenResponse.getRefreshToken().isEmpty()) {
throw new IndegoAuthenticationException("No refresh token. Please reauthorize -> " + getAuthorizationUrl());
}

return BEARER + accessTokenResponse.getAccessToken();
}

/**
* Sends a GET request to the server and returns the deserialized JSON response.
*
Expand All @@ -160,7 +119,7 @@ protected <T> T getRequest(String path, Class<? extends T> dtoClass)
int status = 0;
try {
Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET)
.header(HttpHeader.AUTHORIZATION, getAuthorizationHeader()).agent(userAgent);
.header(HttpHeader.AUTHORIZATION, authorizationProvider.getAuthorizationHeader()).agent(userAgent);
if (logger.isTraceEnabled()) {
logger.trace("GET request for {}", BASE_URL + path);
}
Expand Down Expand Up @@ -226,7 +185,7 @@ protected RawType getRawRequest(String path) throws IndegoAuthenticationExceptio
int status = 0;
try {
Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET)
.header(HttpHeader.AUTHORIZATION, getAuthorizationHeader()).agent(userAgent);
.header(HttpHeader.AUTHORIZATION, authorizationProvider.getAuthorizationHeader()).agent(userAgent);
if (logger.isTraceEnabled()) {
logger.trace("GET request for {}", BASE_URL + path);
}
Expand Down Expand Up @@ -312,7 +271,7 @@ protected void putPostRequest(HttpMethod method, String path, @Nullable Object r
throws IndegoAuthenticationException, IndegoException {
try {
Request request = httpClient.newRequest(BASE_URL + path).method(method)
.header(HttpHeader.AUTHORIZATION, getAuthorizationHeader())
.header(HttpHeader.AUTHORIZATION, authorizationProvider.getAuthorizationHeader())
.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_HEADER).agent(userAgent);
if (requestDto != null) {
String payload = gson.toJson(requestDto);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidResponseException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.library.types.RawType;

/**
Expand All @@ -61,11 +60,12 @@ public class IndegoDeviceController extends IndegoController {
* Initialize the controller instance.
*
* @param httpClient the HttpClient for communicating with the service
* @param oAuthClientService the OAuthClientService for authorization
* @param authorizationProvider the AuthorizationProvider for authenticating with the service
* @param serialNumber the serial number of the device instance
*/
public IndegoDeviceController(HttpClient httpClient, OAuthClientService oAuthClientService, String serialNumber) {
super(httpClient, oAuthClientService);
public IndegoDeviceController(HttpClient httpClient, AuthorizationProvider authorizationProvider,
String serialNumber) {
super(httpClient, authorizationProvider);
if (serialNumber.isBlank()) {
throw new IllegalArgumentException("Serial number must be provided");
}
Expand Down
Loading

0 comments on commit c844569

Please sign in to comment.