Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[boschindego] Refactor OAuth2 implementation #14950

Merged
merged 4 commits into from
May 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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