Skip to content

Commit

Permalink
[oauth] Add support for custom deserialization of AccessTokenResponse (
Browse files Browse the repository at this point in the history
…openhab#3537)

* Add possibility to inject custom GsonBuilder

Reverts openhab#1891

Fixes openhab#1888

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
  • Loading branch information
jlaur authored Apr 14, 2023
1 parent 5d46377 commit eb6b6b9
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.JsonDeserializer;
import com.google.gson.GsonBuilder;

/**
* Implementation of OAuthClientService.
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -103,7 +106,7 @@ private OAuthClientServiceImpl(String handle, int tokenExpiresInSeconds, HttpCli
return null;
}
OAuthClientServiceImpl clientService = new OAuthClientServiceImpl(handle, tokenExpiresInSeconds,
httpClientFactory);
httpClientFactory, null);
clientService.storeHandler = storeHandler;
clientService.persistedParams = persistedParamsFromStore;

Expand All @@ -118,13 +121,13 @@ private OAuthClientServiceImpl(String handle, int tokenExpiresInSeconds, HttpCli
* {@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;
Expand Down Expand Up @@ -153,7 +156,9 @@ public String getAuthorizationUrl(@Nullable String redirectURI, @Nullable String
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);
}
Expand Down Expand Up @@ -207,7 +212,9 @@ public AccessTokenResponse getAccessTokenResponseByAuthorizationCode(String auth
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));
Expand Down Expand Up @@ -239,7 +246,9 @@ public AccessTokenResponse getAccessTokenByResourceOwnerPasswordCredentials(Stri
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));
Expand All @@ -264,7 +273,9 @@ public AccessTokenResponse getAccessTokenByClientCredentials(@Nullable String sc
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));
Expand Down Expand Up @@ -298,7 +309,9 @@ public AccessTokenResponse refreshToken() throws OAuthException, IOException, OA
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));
Expand Down Expand Up @@ -400,10 +413,9 @@ private String createNewState() {
}

@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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
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;
Expand Down Expand Up @@ -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();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ class PersistedParams {
String state;
String redirectUri;
int tokenExpiresInSeconds = 60;
@Nullable
String deserializerClassName;

/**
* Default constructor needed for json serialization.
Expand All @@ -59,7 +57,6 @@ public 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,
Expand All @@ -72,7 +69,6 @@ public PersistedParams(String handle, String tokenUrl, String authorizationUrl,
this.scope = scope;
this.supportsBasicAuth = supportsBasicAuth;
this.tokenExpiresInSeconds = tokenExpiresInSeconds;
this.deserializerClassName = deserializerClassName;
}

@Override
Expand All @@ -89,7 +85,6 @@ public int hashCode() {
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;
}

Expand Down Expand Up @@ -168,13 +163,6 @@ public boolean equals(@Nullable Object obj) {
} 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
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:
Expand Down Expand Up @@ -291,10 +291,10 @@ AccessTokenResponse getAccessTokenByImplicit(@Nullable String redirectURI, @Null
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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,36 +26,36 @@
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)
* @param scope the desired scope
* @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()}.
*
Expand All @@ -64,15 +64,15 @@ OAuthClientService createOAuthClientService(String handle, String tokenUrl, @Nul
* 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);

/**
* 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);
}

0 comments on commit eb6b6b9

Please sign in to comment.