Skip to content

Commit

Permalink
Implement OAuth2 authorization
Browse files Browse the repository at this point in the history
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
  • Loading branch information
jlaur committed Apr 4, 2023
1 parent b8c4198 commit 709671a
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 404 deletions.
13 changes: 11 additions & 2 deletions bundles/org.openhab.binding.boschindego/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,21 @@ Currently the binding supports _**indego**_ mowers as a thing type with these

| Parameter | Description | Default |
|--------------------|-------------------------------------------------------------------|---------|
| username | Username for the Bosch Indego account | |
| password | Password for the Bosch Indego account | |
| refresh | The number of seconds between refreshing device state when idle | 180 |
| stateActiveRefresh | The number of seconds between refreshing device state when active | 30 |
| cuttingTimeRefresh | The number of minutes between refreshing last/next cutting time | 60 |

### Authorization

To authorize, please follow these steps:

- In your browser, go to the [Bosch SingleKey ID login page](https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/authorize?redirect_uri=com.bosch.indegoconnect://login&client_id=65bb8c9d-1070-4fb4-aa95-853618acc876&response_type=code&scope=openid%20offline_access%20https://prodindego.onmicrosoft.com/indego-mobile-api/Indego.Mower.User).
- Select "Bosch ID", enter your e-mail address and password and clock "Log-in".
- In your browser, open Developer Tools.
- With developer tools showing in the right, go to [Bosch SingleKey ID login page](https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/authorize?redirect_uri=com.bosch.indegoconnect://login&client_id=65bb8c9d-1070-4fb4-aa95-853618acc876&response_type=code&scope=openid%20offline_access%20https://prodindego.onmicrosoft.com/indego-mobile-api/Indego.Mower.User) again.
- "Please wait..." should now be displayed.
-

## Channels

| Channel | Item Type | Description | Writeable |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.boschindego.internal.handler.BoschIndegoHandler;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.i18n.TranslationProvider;
Expand All @@ -37,21 +38,25 @@
* handlers.
*
* @author Jonas Fleck - Initial contribution
* @author Jacob Laursen - Replaced authorization by OAuth2
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.boschindego")
public class BoschIndegoHandlerFactory extends BaseThingHandlerFactory {

private final HttpClient httpClient;
private final OAuthFactory oAuthFactory;
private final BoschIndegoTranslationProvider translationProvider;
private final TimeZoneProvider timeZoneProvider;

@Activate
public BoschIndegoHandlerFactory(@Reference HttpClientFactory httpClientFactory,
final @Reference TranslationProvider i18nProvider, final @Reference LocaleProvider localeProvider,
final @Reference TimeZoneProvider timeZoneProvider, ComponentContext componentContext) {
final @Reference OAuthFactory oAuthFactory, final @Reference TranslationProvider i18nProvider,
final @Reference LocaleProvider localeProvider, final @Reference TimeZoneProvider timeZoneProvider,
ComponentContext componentContext) {
super.activate(componentContext);
this.httpClient = httpClientFactory.getCommonHttpClient();
this.oAuthFactory = oAuthFactory;
this.translationProvider = new BoschIndegoTranslationProvider(i18nProvider, localeProvider);
this.timeZoneProvider = timeZoneProvider;
}
Expand All @@ -66,7 +71,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();

if (THING_TYPE_INDEGO.equals(thingTypeUID)) {
return new BoschIndegoHandler(thing, httpClient, translationProvider, timeZoneProvider);
return new BoschIndegoHandler(thing, httpClient, oAuthFactory, translationProvider, timeZoneProvider);
}

return null;
Expand Down

Large diffs are not rendered by default.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
package org.openhab.binding.boschindego.internal.config;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;

/**
* Configuration for the Bosch Indego thing.
Expand All @@ -22,8 +21,6 @@
*/
@NonNullByDefault
public class BoschIndegoConfiguration {
public @Nullable String username;
public @Nullable String password;
public long refresh = 180;
public long stateActiveRefresh = 30;
public long cuttingTimeRefresh = 60;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* 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.console;

import java.util.Arrays;
import java.util.List;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants;
import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
import org.openhab.binding.boschindego.internal.handler.BoschIndegoHandler;
import org.openhab.core.io.console.Console;
import org.openhab.core.io.console.ConsoleCommandCompleter;
import org.openhab.core.io.console.StringsCompleter;
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.binding.ThingHandler;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

/**
* The {@link BoschIndegoCommandExtension} is responsible for handling console commands
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
@Component(service = ConsoleCommandExtension.class)
public class BoschIndegoCommandExtension extends AbstractConsoleCommandExtension implements ConsoleCommandCompleter {

private static final String AUTHORIZE = "authorize";
private static final StringsCompleter SUBCMD_COMPLETER = new StringsCompleter(List.of(AUTHORIZE), false);

private final ThingRegistry thingRegistry;

@Activate
public BoschIndegoCommandExtension(final @Reference ThingRegistry thingRegistry) {
super(BoschIndegoBindingConstants.BINDING_ID, "Interact with the Bosch Indego binding.");
this.thingRegistry = thingRegistry;
}

@Override
public void execute(String[] args, Console console) {
if (args.length != 2 || !AUTHORIZE.equals(args[0])) {
printUsage(console);
return;
}

for (Thing thing : thingRegistry.getAll()) {
ThingHandler thingHandler = thing.getHandler();
if (thingHandler instanceof BoschIndegoHandler indegoHandler) {
try {
indegoHandler.authorize(args[1]);
} catch (IndegoAuthenticationException e) {
console.println("Authorization error: " + e.getMessage());
}
}
}
}

@Override
public List<String> getUsages() {
return Arrays.asList(buildCommandUsage(AUTHORIZE, "authorize by authorization code"));
}

@Override
public @Nullable ConsoleCommandCompleter getCompleter() {
return this;
}

@Override
public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List<String> candidates) {
if (cursorArgumentIndex <= 0) {
return SUBCMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates);
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,15 @@
import com.google.gson.annotations.SerializedName;

/**
* Response from authenticating with server.
* Mower serial number and status.
*
* @author Jacob Laursen - Initial contribution
*/
public class AuthenticationResponse {

public String contextId;

public String userId;
public class Mower {

@SerializedName("alm_sn")
public String serialNumber;

@SerializedName("alm_status")
public int status;
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
Expand Down Expand Up @@ -81,6 +82,7 @@ public class BoschIndegoHandler extends BaseThingHandler {

private final Logger logger = LoggerFactory.getLogger(BoschIndegoHandler.class);
private final HttpClient httpClient;
private final OAuthFactory oAuthFactory;
private final BoschIndegoTranslationProvider translationProvider;
private final TimeZoneProvider timeZoneProvider;

Expand All @@ -99,10 +101,11 @@ public class BoschIndegoHandler extends BaseThingHandler {
private int stateActiveRefreshIntervalSeconds;
private int currentRefreshIntervalSeconds;

public BoschIndegoHandler(Thing thing, HttpClient httpClient, BoschIndegoTranslationProvider translationProvider,
TimeZoneProvider timeZoneProvider) {
public BoschIndegoHandler(Thing thing, HttpClient httpClient, OAuthFactory oAuthFactory,
BoschIndegoTranslationProvider translationProvider, TimeZoneProvider timeZoneProvider) {
super(thing);
this.httpClient = httpClient;
this.oAuthFactory = oAuthFactory;
this.translationProvider = translationProvider;
this.timeZoneProvider = timeZoneProvider;
}
Expand All @@ -113,21 +116,8 @@ public void initialize() {
BoschIndegoConfiguration config = getConfigAs(BoschIndegoConfiguration.class);
stateInactiveRefreshIntervalSeconds = (int) config.refresh;
stateActiveRefreshIntervalSeconds = (int) config.stateActiveRefresh;
String username = config.username;
String password = config.password;

if (username == null || username.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.conf-error.missing-username");
return;
}
if (password == null || password.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.conf-error.missing-password");
return;
}

controller = new IndegoController(httpClient, username, password);
controller = new IndegoController(httpClient, oAuthFactory);

updateStatus(ThingStatus.UNKNOWN);
previousStateCode = Optional.empty();
Expand All @@ -136,6 +126,13 @@ public void initialize() {
config.cuttingTimeRefresh, TimeUnit.MINUTES);
}

public void authorize(String authCode) throws IndegoAuthenticationException {
logger.info("Attempting to authorize using code");
controller.authorizeByAuthorizationCode(authCode);
logger.info("Authorization completed successfully");
rescheduleStatePoll(0, stateInactiveRefreshIntervalSeconds);
}

private boolean rescheduleStatePoll(int delaySeconds, int refreshIntervalSeconds) {
ScheduledFuture<?> statePollFuture = this.statePollFuture;
if (statePollFuture != null) {
Expand Down Expand Up @@ -172,14 +169,6 @@ public void dispose() {
pollFuture.cancel(true);
}
this.cuttingTimeFuture = null;

scheduler.execute(() -> {
try {
controller.deauthenticate();
} catch (IndegoException e) {
logger.debug("Deauthentication failed", e);
}
});
}

@Override
Expand Down Expand Up @@ -280,6 +269,7 @@ private void refreshStateWithExceptionHandling() {
try {
refreshState();
} catch (IndegoAuthenticationException e) {
logger.warn("Failed to authenticate: {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.comm-error.authentication-failure");
} catch (IndegoTimeoutException e) {
Expand Down
Loading

0 comments on commit 709671a

Please sign in to comment.