[oauth] Add support for custom deserialization of AccessTokenResponse (#3537)

* Add possibility to inject custom GsonBuilder

Reverts #1891

Fixes #1888

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
Jacob Laursen
2023-04-14 21:19:08 +02:00
committed by GitHub
parent 5d46377d22
commit eb6b6b9677
5 changed files with 49 additions and 59 deletions
@@ -35,7 +35,7 @@ import org.openhab.core.io.net.http.HttpClientFactory;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.gson.JsonDeserializer; import com.google.gson.GsonBuilder;
/** /**
* Implementation of OAuthClientService. * Implementation of OAuthClientService.
@@ -68,16 +68,19 @@ public class OAuthClientServiceImpl implements OAuthClientService {
private final String handle; private final String handle;
private final int tokenExpiresInSeconds; private final int tokenExpiresInSeconds;
private final HttpClientFactory httpClientFactory; private final HttpClientFactory httpClientFactory;
private final @Nullable GsonBuilder gsonBuilder;
private final List<AccessTokenRefreshListener> accessTokenRefreshListeners = new ArrayList<>(); private final List<AccessTokenRefreshListener> accessTokenRefreshListeners = new ArrayList<>();
private PersistedParams persistedParams = new PersistedParams(); private PersistedParams persistedParams = new PersistedParams();
private volatile boolean closed = false; private volatile boolean closed = false;
private OAuthClientServiceImpl(String handle, int tokenExpiresInSeconds, HttpClientFactory httpClientFactory) { private OAuthClientServiceImpl(String handle, int tokenExpiresInSeconds, HttpClientFactory httpClientFactory,
@Nullable GsonBuilder gsonBuilder) {
this.handle = handle; this.handle = handle;
this.tokenExpiresInSeconds = tokenExpiresInSeconds; this.tokenExpiresInSeconds = tokenExpiresInSeconds;
this.httpClientFactory = httpClientFactory; this.httpClientFactory = httpClientFactory;
this.gsonBuilder = gsonBuilder;
} }
/** /**
@@ -103,7 +106,7 @@ public class OAuthClientServiceImpl implements OAuthClientService {
return null; return null;
} }
OAuthClientServiceImpl clientService = new OAuthClientServiceImpl(handle, tokenExpiresInSeconds, OAuthClientServiceImpl clientService = new OAuthClientServiceImpl(handle, tokenExpiresInSeconds,
httpClientFactory); httpClientFactory, null);
clientService.storeHandler = storeHandler; clientService.storeHandler = storeHandler;
clientService.persistedParams = persistedParamsFromStore; clientService.persistedParams = persistedParamsFromStore;
@@ -118,13 +121,13 @@ public class OAuthClientServiceImpl implements OAuthClientService {
* {@link org.openhab.core.auth.client.oauth2.OAuthFactory#createOAuthClientService}* * {@link org.openhab.core.auth.client.oauth2.OAuthFactory#createOAuthClientService}*
* @param storeHandler Storage handler * @param storeHandler Storage handler
* @param httpClientFactory Http client factory * @param httpClientFactory Http client factory
* @param persistedParams These parameters are static with respect to the oauth provider and thus can be persisted. * @param persistedParams These parameters are static with respect to the OAuth provider and thus can be persisted.
* @return OAuthClientServiceImpl an instance * @return OAuthClientServiceImpl an instance
*/ */
static OAuthClientServiceImpl createInstance(String handle, OAuthStoreHandler storeHandler, static OAuthClientServiceImpl createInstance(String handle, OAuthStoreHandler storeHandler,
HttpClientFactory httpClientFactory, PersistedParams params) { HttpClientFactory httpClientFactory, PersistedParams params) {
OAuthClientServiceImpl clientService = new OAuthClientServiceImpl(handle, params.tokenExpiresInSeconds, OAuthClientServiceImpl clientService = new OAuthClientServiceImpl(handle, params.tokenExpiresInSeconds,
httpClientFactory); httpClientFactory, null);
clientService.storeHandler = storeHandler; clientService.storeHandler = storeHandler;
clientService.persistedParams = params; clientService.persistedParams = params;
@@ -153,7 +156,9 @@ public class OAuthClientServiceImpl implements OAuthClientService {
throw new OAuthException("Missing client ID"); throw new OAuthException("Missing client ID");
} }
OAuthConnector connector = new OAuthConnector(httpClientFactory, persistedParams.deserializerClassName); GsonBuilder gsonBuilder = this.gsonBuilder;
OAuthConnector connector = gsonBuilder == null ? new OAuthConnector(httpClientFactory)
: new OAuthConnector(httpClientFactory, gsonBuilder);
return connector.getAuthorizationUrl(authorizationUrl, clientId, redirectURI, persistedParams.state, return connector.getAuthorizationUrl(authorizationUrl, clientId, redirectURI, persistedParams.state,
scopeToUse); scopeToUse);
} }
@@ -207,7 +212,9 @@ public class OAuthClientServiceImpl implements OAuthClientService {
throw new OAuthException("Missing client ID"); throw new OAuthException("Missing client ID");
} }
OAuthConnector connector = new OAuthConnector(httpClientFactory, persistedParams.deserializerClassName); GsonBuilder gsonBuilder = this.gsonBuilder;
OAuthConnector connector = gsonBuilder == null ? new OAuthConnector(httpClientFactory)
: new OAuthConnector(httpClientFactory, gsonBuilder);
AccessTokenResponse accessTokenResponse = connector.grantTypeAuthorizationCode(tokenUrl, authorizationCode, AccessTokenResponse accessTokenResponse = connector.grantTypeAuthorizationCode(tokenUrl, authorizationCode,
clientId, persistedParams.clientSecret, redirectURI, clientId, persistedParams.clientSecret, redirectURI,
Boolean.TRUE.equals(persistedParams.supportsBasicAuth)); Boolean.TRUE.equals(persistedParams.supportsBasicAuth));
@@ -239,7 +246,9 @@ public class OAuthClientServiceImpl implements OAuthClientService {
throw new OAuthException("Missing token url"); throw new OAuthException("Missing token url");
} }
OAuthConnector connector = new OAuthConnector(httpClientFactory, persistedParams.deserializerClassName); GsonBuilder gsonBuilder = this.gsonBuilder;
OAuthConnector connector = gsonBuilder == null ? new OAuthConnector(httpClientFactory)
: new OAuthConnector(httpClientFactory, gsonBuilder);
AccessTokenResponse accessTokenResponse = connector.grantTypePassword(tokenUrl, username, password, AccessTokenResponse accessTokenResponse = connector.grantTypePassword(tokenUrl, username, password,
persistedParams.clientId, persistedParams.clientSecret, scope, persistedParams.clientId, persistedParams.clientSecret, scope,
Boolean.TRUE.equals(persistedParams.supportsBasicAuth)); Boolean.TRUE.equals(persistedParams.supportsBasicAuth));
@@ -264,7 +273,9 @@ public class OAuthClientServiceImpl implements OAuthClientService {
throw new OAuthException("Missing client ID"); throw new OAuthException("Missing client ID");
} }
OAuthConnector connector = new OAuthConnector(httpClientFactory, persistedParams.deserializerClassName); GsonBuilder gsonBuilder = this.gsonBuilder;
OAuthConnector connector = gsonBuilder == null ? new OAuthConnector(httpClientFactory)
: new OAuthConnector(httpClientFactory, gsonBuilder);
// depending on usage, cannot guarantee every parameter is not null at the beginning // depending on usage, cannot guarantee every parameter is not null at the beginning
AccessTokenResponse accessTokenResponse = connector.grantTypeClientCredentials(tokenUrl, clientId, AccessTokenResponse accessTokenResponse = connector.grantTypeClientCredentials(tokenUrl, clientId,
persistedParams.clientSecret, scope, Boolean.TRUE.equals(persistedParams.supportsBasicAuth)); persistedParams.clientSecret, scope, Boolean.TRUE.equals(persistedParams.supportsBasicAuth));
@@ -298,7 +309,9 @@ public class OAuthClientServiceImpl implements OAuthClientService {
throw new OAuthException("tokenUrl is required but null"); throw new OAuthException("tokenUrl is required but null");
} }
OAuthConnector connector = new OAuthConnector(httpClientFactory, persistedParams.deserializerClassName); GsonBuilder gsonBuilder = this.gsonBuilder;
OAuthConnector connector = gsonBuilder == null ? new OAuthConnector(httpClientFactory)
: new OAuthConnector(httpClientFactory, gsonBuilder);
AccessTokenResponse accessTokenResponse = connector.grantTypeRefreshToken(tokenUrl, AccessTokenResponse accessTokenResponse = connector.grantTypeRefreshToken(tokenUrl,
lastAccessToken.getRefreshToken(), persistedParams.clientId, persistedParams.clientSecret, lastAccessToken.getRefreshToken(), persistedParams.clientId, persistedParams.clientSecret,
persistedParams.scope, Boolean.TRUE.equals(persistedParams.supportsBasicAuth)); persistedParams.scope, Boolean.TRUE.equals(persistedParams.supportsBasicAuth));
@@ -400,10 +413,9 @@ public class OAuthClientServiceImpl implements OAuthClientService {
} }
@Override @Override
public <T extends JsonDeserializer<?>> OAuthClientService withDeserializer(Class<T> deserializerClass) { public OAuthClientService withGsonBuilder(GsonBuilder gsonBuilder) {
OAuthClientServiceImpl clientService = new OAuthClientServiceImpl(handle, persistedParams.tokenExpiresInSeconds, OAuthClientServiceImpl clientService = new OAuthClientServiceImpl(handle, persistedParams.tokenExpiresInSeconds,
httpClientFactory); httpClientFactory, gsonBuilder);
persistedParams.deserializerClassName = deserializerClass.getName();
clientService.persistedParams = persistedParams; clientService.persistedParams = persistedParams;
clientService.storeHandler = storeHandler; clientService.storeHandler = storeHandler;
@@ -15,7 +15,6 @@ package org.openhab.core.auth.oauth2client.internal;
import static org.openhab.core.auth.oauth2client.internal.Keyword.*; import static org.openhab.core.auth.oauth2client.internal.Keyword.*;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Instant; import java.time.Instant;
@@ -67,29 +66,20 @@ public class OAuthConnector {
private final Logger logger = LoggerFactory.getLogger(OAuthConnector.class); private final Logger logger = LoggerFactory.getLogger(OAuthConnector.class);
private final Gson gson; private final Gson gson;
public OAuthConnector(HttpClientFactory httpClientFactory, @Nullable String deserializerClassName) { public OAuthConnector(HttpClientFactory httpClientFactory) {
this(httpClientFactory, new GsonBuilder());
}
public OAuthConnector(HttpClientFactory httpClientFactory, GsonBuilder gsonBuilder) {
this.httpClientFactory = httpClientFactory; this.httpClientFactory = httpClientFactory;
GsonBuilder gsonBuilder = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) gson = gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(Instant.class, (JsonDeserializer<Instant>) (json, typeOfT, context) -> { .registerTypeAdapter(Instant.class, (JsonDeserializer<Instant>) (json, typeOfT, context) -> {
try { try {
return Instant.parse(json.getAsString()); return Instant.parse(json.getAsString());
} catch (DateTimeParseException e) { } catch (DateTimeParseException e) {
return LocalDateTime.parse(json.getAsString()).atZone(ZoneId.systemDefault()).toInstant(); return LocalDateTime.parse(json.getAsString()).atZone(ZoneId.systemDefault()).toInstant();
} }
}); }).create();
if (deserializerClassName != null) {
try {
Class<?> deserializerClass = Class.forName(deserializerClassName);
gsonBuilder = gsonBuilder.registerTypeAdapter(AccessTokenResponse.class,
deserializerClass.getConstructor().newInstance());
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException
| ClassNotFoundException e) {
logger.error("Unable to construct custom deserializer '{}'", deserializerClassName, e);
}
}
gson = gsonBuilder.create();
} }
/** /**
@@ -33,8 +33,6 @@ class PersistedParams {
String state; String state;
String redirectUri; String redirectUri;
int tokenExpiresInSeconds = 60; int tokenExpiresInSeconds = 60;
@Nullable
String deserializerClassName;
/** /**
* Default constructor needed for json serialization. * Default constructor needed for json serialization.
@@ -59,7 +57,6 @@ class PersistedParams {
* of the access tokens. This allows the access token to expire earlier than the * of the access tokens. This allows the access token to expire earlier than the
* official stated expiry time; thus prevents the caller obtaining a valid token at the time of invoke, * official stated expiry time; thus prevents the caller obtaining a valid token at the time of invoke,
* only to find the token immediately expired. * only to find the token immediately expired.
* @param deserializerClass (optional) if a specific deserializer is needed
*/ */
public PersistedParams(String handle, String tokenUrl, String authorizationUrl, String clientId, public PersistedParams(String handle, String tokenUrl, String authorizationUrl, String clientId,
String clientSecret, String scope, Boolean supportsBasicAuth, int tokenExpiresInSeconds, String clientSecret, String scope, Boolean supportsBasicAuth, int tokenExpiresInSeconds,
@@ -72,7 +69,6 @@ class PersistedParams {
this.scope = scope; this.scope = scope;
this.supportsBasicAuth = supportsBasicAuth; this.supportsBasicAuth = supportsBasicAuth;
this.tokenExpiresInSeconds = tokenExpiresInSeconds; this.tokenExpiresInSeconds = tokenExpiresInSeconds;
this.deserializerClassName = deserializerClassName;
} }
@Override @Override
@@ -89,7 +85,6 @@ class PersistedParams {
result = prime * result + ((supportsBasicAuth == null) ? 0 : supportsBasicAuth.hashCode()); result = prime * result + ((supportsBasicAuth == null) ? 0 : supportsBasicAuth.hashCode());
result = prime * result + tokenExpiresInSeconds; result = prime * result + tokenExpiresInSeconds;
result = prime * result + ((tokenUrl == null) ? 0 : tokenUrl.hashCode()); result = prime * result + ((tokenUrl == null) ? 0 : tokenUrl.hashCode());
result = prime * result + ((deserializerClassName != null) ? deserializerClassName.hashCode() : 0);
return result; return result;
} }
@@ -168,13 +163,6 @@ class PersistedParams {
} else if (!tokenUrl.equals(other.tokenUrl)) { } else if (!tokenUrl.equals(other.tokenUrl)) {
return false; return false;
} }
if (deserializerClassName == null) {
if (other.deserializerClassName != null) {
return false;
}
} else if (deserializerClassName != null && !deserializerClassName.equals(other.deserializerClassName)) {
return false;
}
return true; return true;
} }
@@ -17,11 +17,11 @@ import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.JsonDeserializer; import com.google.gson.GsonBuilder;
/** /**
* This is the service factory to produce an OAuth2 service client that authenticates using OAUTH2. * This is the service factory to produce an OAuth2 service client that authenticates using OAUTH2.
* This is a service factory pattern; the OAuthe2 service client is not shared between bundles. * This is a service factory pattern; the OAuth2 service client is not shared between bundles.
* *
* <p> * <p>
* The basic uses of this OAuthClient are as follows: * The basic uses of this OAuthClient are as follows:
@@ -291,10 +291,10 @@ public interface OAuthClientService extends AutoCloseable {
boolean removeAccessTokenRefreshListener(AccessTokenRefreshListener listener); boolean removeAccessTokenRefreshListener(AccessTokenRefreshListener listener);
/** /**
* Adds a personalized deserializer to a given oauth service. * Adds a custom GsonBuilder to be used with the OAuth service instance.
* *
* @param deserializeClass the deserializer class that should be used to deserialize AccessTokenResponse * @param gsonBuilder the custom GsonBuilder instance
* @return the oauth service * @return the OAuth service
*/ */
<T extends JsonDeserializer<?>> OAuthClientService withDeserializer(Class<T> deserializerClass); OAuthClientService withGsonBuilder(GsonBuilder gsonBuilder);
} }
@@ -26,12 +26,12 @@ import org.eclipse.jdt.annotation.Nullable;
public interface OAuthFactory { public interface OAuthFactory {
/** /**
* Creates a new oauth service. Use this method only once to obtain a handle and store * Creates a new OAuth service. Use this method only once to obtain a handle and store
* this handle for further in a persistent storage container. * this handle for further in a persistent storage container.
* *
* @param handle the handle to the oauth service * @param handle the handle to the OAuth service
* @param tokenUrl the token url of the oauth provider. This is used for getting access token. * @param tokenUrl the token url of the OAuth provider. This is used for getting access token.
* @param authorizationUrl the authorization url of the oauth provider. This is used purely for generating * @param authorizationUrl the authorization url of the OAuth provider. This is used purely for generating
* authorization code/ url. * authorization code/ url.
* @param clientId the client id * @param clientId the client id
* @param clientSecret the client secret (optional) * @param clientSecret the client secret (optional)
@@ -39,23 +39,23 @@ public interface OAuthFactory {
* @param supportsBasicAuth whether the OAuth provider supports basic authorization or the client id and client * @param supportsBasicAuth whether the OAuth provider supports basic authorization or the client id and client
* secret should be passed as form params. true - use http basic authentication, false - do not use http * secret should be passed as form params. true - use http basic authentication, false - do not use http
* basic authentication, null - unknown (default to do not use) * basic authentication, null - unknown (default to do not use)
* @return the oauth service * @return the OAuth service
*/ */
OAuthClientService createOAuthClientService(String handle, String tokenUrl, @Nullable String authorizationUrl, OAuthClientService createOAuthClientService(String handle, String tokenUrl, @Nullable String authorizationUrl,
String clientId, @Nullable String clientSecret, @Nullable String scope, String clientId, @Nullable String clientSecret, @Nullable String scope,
@Nullable Boolean supportsBasicAuth); @Nullable Boolean supportsBasicAuth);
/** /**
* Gets the oauth service for a given handle * Gets the OAuth service for a given handle
* *
* @param handle the handle to the oauth service * @param handle the handle to the OAuth service
* @return the oauth service or null if it doesn't exist * @return the OAuth service or null if it doesn't exist
*/ */
@Nullable @Nullable
OAuthClientService getOAuthClientService(String handle); OAuthClientService getOAuthClientService(String handle);
/** /**
* Unget an oauth service, this unget/unregister the service, and frees the resources. * Unget an OAuth service, this unget/unregister the service, and frees the resources.
* The existing tokens/ configurations (persisted parameters) are still saved * The existing tokens/ configurations (persisted parameters) are still saved
* in the store. It will internally call {@code OAuthClientService#close()}. * in the store. It will internally call {@code OAuthClientService#close()}.
* *
@@ -64,7 +64,7 @@ public interface OAuthFactory {
* If OAuth service is closed directly, without using {@code #ungetOAuthService(String)}, * If OAuth service is closed directly, without using {@code #ungetOAuthService(String)},
* then a small residual footprint is left in the cache. * then a small residual footprint is left in the cache.
* *
* @param handle the handle to the oauth service * @param handle the handle to the OAuth service
*/ */
void ungetOAuthService(String handle); void ungetOAuthService(String handle);
@@ -72,7 +72,7 @@ public interface OAuthFactory {
* This method is for unget/unregister the service, * This method is for unget/unregister the service,
* then <strong>DELETE</strong> access token, configuration data from the store * then <strong>DELETE</strong> access token, configuration data from the store
* *
* @param handle * @param handle the handle to the OAuth service
*/ */
void deleteServiceAndAccessToken(String handle); void deleteServiceAndAccessToken(String handle);
} }