[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.LoggerFactory;
import com.google.gson.JsonDeserializer;
import com.google.gson.GsonBuilder;
/**
* Implementation of OAuthClientService.
@@ -68,16 +68,19 @@ public class OAuthClientServiceImpl implements OAuthClientService {
private final String handle;
private final int tokenExpiresInSeconds;
private final HttpClientFactory httpClientFactory;
private final @Nullable GsonBuilder gsonBuilder;
private final List<AccessTokenRefreshListener> accessTokenRefreshListeners = new ArrayList<>();
private PersistedParams persistedParams = new PersistedParams();
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.tokenExpiresInSeconds = tokenExpiresInSeconds;
this.httpClientFactory = httpClientFactory;
this.gsonBuilder = gsonBuilder;
}
/**
@@ -103,7 +106,7 @@ public class OAuthClientServiceImpl implements OAuthClientService {
return null;
}
OAuthClientServiceImpl clientService = new OAuthClientServiceImpl(handle, tokenExpiresInSeconds,
httpClientFactory);
httpClientFactory, null);
clientService.storeHandler = storeHandler;
clientService.persistedParams = persistedParamsFromStore;
@@ -118,13 +121,13 @@ public class OAuthClientServiceImpl implements OAuthClientService {
* {@link org.openhab.core.auth.client.oauth2.OAuthFactory#createOAuthClientService}*
* @param storeHandler Storage handler
* @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
*/
static OAuthClientServiceImpl createInstance(String handle, OAuthStoreHandler storeHandler,
HttpClientFactory httpClientFactory, PersistedParams params) {
OAuthClientServiceImpl clientService = new OAuthClientServiceImpl(handle, params.tokenExpiresInSeconds,
httpClientFactory);
httpClientFactory, null);
clientService.storeHandler = storeHandler;
clientService.persistedParams = params;
@@ -153,7 +156,9 @@ public class OAuthClientServiceImpl implements OAuthClientService {
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,
scopeToUse);
}
@@ -207,7 +212,9 @@ public class OAuthClientServiceImpl implements OAuthClientService {
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,
clientId, persistedParams.clientSecret, redirectURI,
Boolean.TRUE.equals(persistedParams.supportsBasicAuth));
@@ -239,7 +246,9 @@ public class OAuthClientServiceImpl implements OAuthClientService {
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,
persistedParams.clientId, persistedParams.clientSecret, scope,
Boolean.TRUE.equals(persistedParams.supportsBasicAuth));
@@ -264,7 +273,9 @@ public class OAuthClientServiceImpl implements OAuthClientService {
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
AccessTokenResponse accessTokenResponse = connector.grantTypeClientCredentials(tokenUrl, clientId,
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");
}
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,
lastAccessToken.getRefreshToken(), persistedParams.clientId, persistedParams.clientSecret,
persistedParams.scope, Boolean.TRUE.equals(persistedParams.supportsBasicAuth));
@@ -400,10 +413,9 @@ public class OAuthClientServiceImpl implements OAuthClientService {
}
@Override
public <T extends JsonDeserializer<?>> OAuthClientService withDeserializer(Class<T> deserializerClass) {
public OAuthClientService withGsonBuilder(GsonBuilder gsonBuilder) {
OAuthClientServiceImpl clientService = new OAuthClientServiceImpl(handle, persistedParams.tokenExpiresInSeconds,
httpClientFactory);
persistedParams.deserializerClassName = deserializerClass.getName();
httpClientFactory, gsonBuilder);
clientService.persistedParams = persistedParams;
clientService.storeHandler = storeHandler;
@@ -15,7 +15,6 @@ package org.openhab.core.auth.oauth2client.internal;
import static org.openhab.core.auth.oauth2client.internal.Keyword.*;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
@@ -67,29 +66,20 @@ public class OAuthConnector {
private final Logger logger = LoggerFactory.getLogger(OAuthConnector.class);
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;
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) -> {
try {
return Instant.parse(json.getAsString());
} catch (DateTimeParseException e) {
return LocalDateTime.parse(json.getAsString()).atZone(ZoneId.systemDefault()).toInstant();
}
});
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();
}).create();
}
/**
@@ -33,8 +33,6 @@ class PersistedParams {
String state;
String redirectUri;
int tokenExpiresInSeconds = 60;
@Nullable
String deserializerClassName;
/**
* 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
* official stated expiry time; thus prevents the caller obtaining a valid token at the time of invoke,
* 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,
String clientSecret, String scope, Boolean supportsBasicAuth, int tokenExpiresInSeconds,
@@ -72,7 +69,6 @@ class PersistedParams {
this.scope = scope;
this.supportsBasicAuth = supportsBasicAuth;
this.tokenExpiresInSeconds = tokenExpiresInSeconds;
this.deserializerClassName = deserializerClassName;
}
@Override
@@ -89,7 +85,6 @@ class PersistedParams {
result = prime * result + ((supportsBasicAuth == null) ? 0 : supportsBasicAuth.hashCode());
result = prime * result + tokenExpiresInSeconds;
result = prime * result + ((tokenUrl == null) ? 0 : tokenUrl.hashCode());
result = prime * result + ((deserializerClassName != null) ? deserializerClassName.hashCode() : 0);
return result;
}
@@ -168,13 +163,6 @@ class PersistedParams {
} else if (!tokenUrl.equals(other.tokenUrl)) {
return false;
}
if (deserializerClassName == null) {
if (other.deserializerClassName != null) {
return false;
}
} else if (deserializerClassName != null && !deserializerClassName.equals(other.deserializerClassName)) {
return false;
}
return true;
}
@@ -17,11 +17,11 @@ import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
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 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>
* The basic uses of this OAuthClient are as follows:
@@ -291,10 +291,10 @@ public interface OAuthClientService extends AutoCloseable {
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
* @return the oauth service
* @param gsonBuilder the custom GsonBuilder instance
* @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 {
/**
* 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.
*
* @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 authorizationUrl the authorization url of the oauth provider. This is used purely for generating
* @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 authorizationUrl the authorization url of the OAuth provider. This is used purely for generating
* authorization code/ url.
* @param clientId the client id
* @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
* 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)
* @return the oauth service
* @return the OAuth service
*/
OAuthClientService createOAuthClientService(String handle, String tokenUrl, @Nullable String authorizationUrl,
String clientId, @Nullable String clientSecret, @Nullable String scope,
@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
* @return the oauth service or null if it doesn't exist
* @param handle the handle to the OAuth service
* @return the OAuth service or null if it doesn't exist
*/
@Nullable
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
* 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)},
* 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);
@@ -72,7 +72,7 @@ public interface OAuthFactory {
* This method is for unget/unregister the service,
* 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);
}