mirror of
https://github.com/danieldemus/openhab-core.git
synced 2026-07-29 12:34:22 +02:00
Add extra fields support to oAuth AccessTokenResponse (#5355)
* add extra fields support to oAuth AccessTokenResponse Signed-off-by: Laurent ARNAL <laurent@clae.net>
This commit is contained in:
+6
-3
@@ -458,10 +458,13 @@ public class OAuthClientServiceImpl implements OAuthClientService {
|
||||
*/
|
||||
@Override
|
||||
public void addExtraAuthField(String key, String value) {
|
||||
if (extraAuthFields == null) {
|
||||
extraAuthFields = new Fields();
|
||||
Fields lcExtraAuthFields = extraAuthFields;
|
||||
if (lcExtraAuthFields == null) {
|
||||
lcExtraAuthFields = new Fields();
|
||||
extraAuthFields = lcExtraAuthFields;
|
||||
}
|
||||
extraAuthFields.add(key, value);
|
||||
|
||||
lcExtraAuthFields.add(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+9
-4
@@ -37,6 +37,7 @@ import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.util.Fields;
|
||||
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
|
||||
import org.openhab.core.auth.client.oauth2.AccessTokenResponseExtraFieldsAdapterFactory;
|
||||
import org.openhab.core.auth.client.oauth2.OAuthException;
|
||||
import org.openhab.core.auth.client.oauth2.OAuthResponseException;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
@@ -89,7 +90,11 @@ public class OAuthConnector {
|
||||
public OAuthConnector(HttpClientFactory httpClientFactory, @Nullable Fields extraFields, GsonBuilder gsonBuilder) {
|
||||
this.httpClientFactory = httpClientFactory;
|
||||
this.extraFields = extraFields;
|
||||
gson = gsonBuilder.setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT)
|
||||
this.gson = getGson(gsonBuilder);
|
||||
}
|
||||
|
||||
static Gson getGson(GsonBuilder gsonBuilder) {
|
||||
return gsonBuilder.setDateFormat(DateTimeType.DATE_PATTERN_JSON_COMPAT)
|
||||
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
|
||||
.registerTypeAdapter(OAuthResponseException.class,
|
||||
(JsonDeserializer<OAuthResponseException>) (json, typeOfT, context) -> {
|
||||
@@ -120,7 +125,7 @@ public class OAuthConnector {
|
||||
} catch (DateTimeParseException e) {
|
||||
return LocalDateTime.parse(json.getAsString()).atZone(ZoneId.systemDefault()).toInstant();
|
||||
}
|
||||
}).create();
|
||||
}).registerTypeAdapterFactory(new AccessTokenResponseExtraFieldsAdapterFactory()).create();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,8 +219,8 @@ public class OAuthConnector {
|
||||
* @throws OAuthResponseException Error codes given by authorization provider, as in RFC 6749 section 5.2 Error
|
||||
* Response
|
||||
*/
|
||||
public AccessTokenResponse grantTypeRefreshToken(String tokenUrl, String refreshToken, @Nullable String clientId,
|
||||
@Nullable String clientSecret, @Nullable String scope, boolean supportsBasicAuth)
|
||||
public AccessTokenResponse grantTypeRefreshToken(String tokenUrl, @Nullable String refreshToken,
|
||||
@Nullable String clientId, @Nullable String clientSecret, @Nullable String scope, boolean supportsBasicAuth)
|
||||
throws OAuthResponseException, OAuthException, IOException {
|
||||
HttpClient httpClient = null;
|
||||
try {
|
||||
|
||||
+4
-1
@@ -252,7 +252,10 @@ public class OAuthStoreHandlerImpl implements OAuthStoreHandler {
|
||||
return dcrDecrypted;
|
||||
}
|
||||
|
||||
private @Nullable String encrypt(String token) throws GeneralSecurityException {
|
||||
private @Nullable String encrypt(@Nullable String token) throws GeneralSecurityException {
|
||||
if (token == null) {
|
||||
return null;
|
||||
}
|
||||
if (storageCipher.isEmpty()) {
|
||||
return token; // do nothing if no cipher
|
||||
} else {
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 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.core.auth.oauth2client.internal;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
/**
|
||||
* JUnit tests for {@link AccessTokenResponse}
|
||||
*
|
||||
* @author Laurent Arnal - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class AccessTokenResponseExtraFieldTest {
|
||||
|
||||
@Test
|
||||
public void testExtraFieldDeserialization() {
|
||||
Gson gson = OAuthConnector.getGson(new GsonBuilder());
|
||||
|
||||
// \"created_on\":\"2026-02-26T15:10:49.965249200Z\"
|
||||
String json = "{\"access_token\":\"AccessToken\",\"expires_in\":60,\"refresh_token\":\"RefreshToken\",\"app_client_id\":\"ApplicationClientId\"}";
|
||||
AccessTokenResponse atr = gson.fromJson(json, AccessTokenResponse.class);
|
||||
|
||||
if (atr != null) {
|
||||
assertEquals("AccessToken", atr.getAccessToken());
|
||||
assertEquals(60, atr.getExpiresIn());
|
||||
assertEquals("RefreshToken", atr.getRefreshToken());
|
||||
|
||||
Map<String, String> extraFields = atr.getExtraFields();
|
||||
|
||||
assertEquals(1, extraFields.size());
|
||||
assertTrue(extraFields.containsKey("app_client_id"));
|
||||
assertEquals("ApplicationClientId", extraFields.get("app_client_id"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
-18
@@ -15,8 +15,13 @@ package org.openhab.core.auth.client.oauth2;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This is the Access Token Response, a simple value-object that holds the result of the
|
||||
* from an Access Token Request, as listed in RFC 6749:
|
||||
@@ -28,6 +33,7 @@ import java.util.Objects;
|
||||
* @author Michael Bock - Initial contribution
|
||||
* @author Gary Tse - Adaptation for Eclipse SmartHome
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class AccessTokenResponse implements Serializable, Cloneable {
|
||||
|
||||
/**
|
||||
@@ -46,14 +52,14 @@ public final class AccessTokenResponse implements Serializable, Cloneable {
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6749#section-1.4">rfc6749 section-1.4</a>
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6749#section-10.3">rfc6749 section-10.3</a>
|
||||
*/
|
||||
private String accessToken;
|
||||
private @Nullable String accessToken;
|
||||
|
||||
/**
|
||||
* Token type. e.g. Bearer, MAC
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6749#section-7.1">rfc6749 section-7.1</a>
|
||||
*/
|
||||
private String tokenType;
|
||||
private @Nullable String tokenType;
|
||||
|
||||
/**
|
||||
* Number of seconds that this OAuthToken is valid for since the time it was created.
|
||||
@@ -74,7 +80,7 @@ public final class AccessTokenResponse implements Serializable, Cloneable {
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6749#section-1.5">rfc6749 section-1.5</a>
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6749#section-10.4">rfc6749 section-10.4</a>
|
||||
*/
|
||||
private String refreshToken;
|
||||
private @Nullable String refreshToken;
|
||||
|
||||
/**
|
||||
* A space-delimited case-sensitive un-ordered string. This may be used
|
||||
@@ -83,7 +89,7 @@ public final class AccessTokenResponse implements Serializable, Cloneable {
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc6749#section-3.3">rfc6749 section-3.3</a>
|
||||
*/
|
||||
private String scope;
|
||||
private @Nullable String scope;
|
||||
|
||||
/**
|
||||
* If the {@code state} parameter was present in the access token request.
|
||||
@@ -91,7 +97,7 @@ public final class AccessTokenResponse implements Serializable, Cloneable {
|
||||
*
|
||||
* <a href="https://tools.ietf.org/html/rfc6749#section-4.2.2">rfc6749 section-4.2.2</a>
|
||||
*/
|
||||
private String state;
|
||||
private @Nullable String state;
|
||||
|
||||
/**
|
||||
* Created datetime of this access token. This is generated locally
|
||||
@@ -100,7 +106,37 @@ public final class AccessTokenResponse implements Serializable, Cloneable {
|
||||
* This should be slightly later than the actual time the access token
|
||||
* is produced at the server.
|
||||
*/
|
||||
private Instant createdOn;
|
||||
private @Nullable Instant createdOn;
|
||||
|
||||
/**
|
||||
* Extra elements that may be passed in the token response by a specific OAuth implementation.
|
||||
* <p>
|
||||
* These fields are provider-specific and are not restricted to standard OAuth fields. As such,
|
||||
* they may contain sensitive information (for example, identifiers, metadata or other values
|
||||
* that should not be logged or exposed).
|
||||
* <p>
|
||||
* Consumers of this value MUST treat all entries as potentially sensitive and avoid logging or
|
||||
* otherwise exposing them unless they have explicitly verified that the data is safe to do so.
|
||||
*/
|
||||
|
||||
private Map<String, String> extraFields = Collections.emptyMap();
|
||||
|
||||
/**
|
||||
* Returns the additional provider-specific fields from the token response.
|
||||
* <p>
|
||||
* Note: the returned map may contain sensitive information in non-standard fields. Callers
|
||||
* MUST take care not to log, persist, or expose these values without first ensuring that
|
||||
* doing so is appropriate in their security context.
|
||||
*
|
||||
* @return a map of additional fields as provided by the authorization server
|
||||
*/
|
||||
public Map<String, String> getExtraFields() {
|
||||
return Collections.unmodifiableMap(extraFields);
|
||||
}
|
||||
|
||||
public void setExtraFields(Map<String, String> extraFields) {
|
||||
this.extraFields = (extraFields.isEmpty()) ? Collections.emptyMap() : Map.copyOf(extraFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate if the token is expired against the given time.
|
||||
@@ -117,15 +153,15 @@ public final class AccessTokenResponse implements Serializable, Cloneable {
|
||||
|| createdOn.plusSeconds(expiresIn).minusSeconds(tokenExpiresInBuffer).isBefore(givenTime);
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
public @Nullable String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public void setAccessToken(String accessToken) {
|
||||
public void setAccessToken(@Nullable String accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
public String getTokenType() {
|
||||
public @Nullable String getTokenType() {
|
||||
return tokenType;
|
||||
}
|
||||
|
||||
@@ -141,15 +177,15 @@ public final class AccessTokenResponse implements Serializable, Cloneable {
|
||||
this.expiresIn = expiresIn;
|
||||
}
|
||||
|
||||
public String getRefreshToken() {
|
||||
public @Nullable String getRefreshToken() {
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
public void setRefreshToken(String refreshToken) {
|
||||
public void setRefreshToken(@Nullable String refreshToken) {
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
public String getScope() {
|
||||
public @Nullable String getScope() {
|
||||
return scope;
|
||||
}
|
||||
|
||||
@@ -157,7 +193,7 @@ public final class AccessTokenResponse implements Serializable, Cloneable {
|
||||
this.scope = scope;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
public @Nullable String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -165,7 +201,7 @@ public final class AccessTokenResponse implements Serializable, Cloneable {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public Instant getCreatedOn() {
|
||||
public @Nullable Instant getCreatedOn() {
|
||||
return createdOn;
|
||||
}
|
||||
|
||||
@@ -184,11 +220,11 @@ public final class AccessTokenResponse implements Serializable, Cloneable {
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(accessToken, tokenType, expiresIn, refreshToken, scope, state, createdOn);
|
||||
return Objects.hash(accessToken, tokenType, expiresIn, refreshToken, scope, state, createdOn, extraFields);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object thatAuthTokenObj) {
|
||||
public boolean equals(@Nullable Object thatAuthTokenObj) {
|
||||
if (this == thatAuthTokenObj) {
|
||||
return true;
|
||||
}
|
||||
@@ -203,13 +239,18 @@ public final class AccessTokenResponse implements Serializable, Cloneable {
|
||||
return Objects.equals(this.accessToken, that.accessToken) && Objects.equals(this.tokenType, that.tokenType)
|
||||
&& Objects.equals(this.expiresIn, that.expiresIn)
|
||||
&& Objects.equals(this.refreshToken, that.refreshToken) && Objects.equals(this.scope, that.scope)
|
||||
&& Objects.equals(this.state, that.state) && Objects.equals(this.createdOn, that.createdOn);
|
||||
&& Objects.equals(this.state, that.state) && Objects.equals(this.createdOn, that.createdOn)
|
||||
&& Objects.equals(this.extraFields, that.extraFields);
|
||||
}
|
||||
|
||||
@Override
|
||||
/*
|
||||
* Warning: the toString() function may returns sensitive information that should not go into the log.
|
||||
*
|
||||
*/
|
||||
public String toString() {
|
||||
return "AccessTokenResponse [accessToken=" + accessToken + ", tokenType=" + tokenType + ", expiresIn="
|
||||
+ expiresIn + ", refreshToken=" + refreshToken + ", scope=" + scope + ", state=" + state
|
||||
+ ", createdOn=" + createdOn + "]";
|
||||
+ ", createdOn=" + createdOn + ", extraFields= " + extraFields + "]";
|
||||
}
|
||||
}
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 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.core.auth.client.oauth2;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
|
||||
/**
|
||||
* A {@link TypeAdapterFactory} that decorates the default {@link AccessTokenResponse} adapter in order to capture
|
||||
* additional fields returned by an OAuth 2.0 authorization server that are not part of the standard RFC 6749
|
||||
* specification. All unknown JSON properties are collected into a map and exposed via {@code extraFields} on the
|
||||
* {@link AccessTokenResponse}.
|
||||
*
|
||||
* @author Laurent Arnal - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class AccessTokenResponseExtraFieldsAdapterFactory implements TypeAdapterFactory {
|
||||
|
||||
private static final Set<String> KNOWN_FIELDS = Set.of("access_token", "token_type", "expires_in", "refresh_token",
|
||||
"scope", "state", "created_on", "extra_fields");
|
||||
|
||||
@Override
|
||||
public <T> @Nullable TypeAdapter<T> create(@Nullable Gson gson, @Nullable TypeToken<T> type) {
|
||||
if (type == null || !AccessTokenResponse.class.isAssignableFrom(type.getRawType())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (gson == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
|
||||
final TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);
|
||||
|
||||
return new TypeAdapter<>() {
|
||||
|
||||
@Override
|
||||
public void write(JsonWriter out, T value) throws IOException {
|
||||
delegate.write(out, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T read(JsonReader in) throws IOException {
|
||||
JsonElement tree = elementAdapter.read(in);
|
||||
if (tree == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!tree.isJsonObject()) {
|
||||
return delegate.fromJsonTree(tree);
|
||||
}
|
||||
|
||||
JsonObject obj = tree.getAsJsonObject();
|
||||
|
||||
T parsed = delegate.fromJsonTree(tree);
|
||||
AccessTokenResponse response = (AccessTokenResponse) parsed;
|
||||
|
||||
Map<String, String> extras = new HashMap<>();
|
||||
for (Map.Entry<String, JsonElement> entry : obj.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if (KNOWN_FIELDS.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
extras.put(key, toStringValue(gson, entry.getValue()));
|
||||
}
|
||||
|
||||
if (response != null) {
|
||||
response.setExtraFields(extras);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static String toStringValue(Gson gson, JsonElement el) {
|
||||
if (el.isJsonNull()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (el.isJsonPrimitive()) {
|
||||
JsonPrimitive p = el.getAsJsonPrimitive();
|
||||
return p.getAsString();
|
||||
}
|
||||
|
||||
return gson.toJson(el);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user