diff --git a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/AlsoEnergyConfig.java b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/AlsoEnergyConfig.java index 70b3cbb57..30cfb73c9 100644 --- a/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/AlsoEnergyConfig.java +++ b/solarnet/cloud-integrations/src/main/java/net/solarnetwork/central/c2c/config/AlsoEnergyConfig.java @@ -24,6 +24,7 @@ import static net.solarnetwork.central.c2c.config.SolarNetCloudIntegrationsConfiguration.CLOUD_INTEGRATIONS; import static net.solarnetwork.central.common.config.SolarNetCommonConfiguration.OAUTH_CLIENT_REGISTRATION; +import java.time.Clock; import java.util.Arrays; import java.util.Collection; import javax.cache.Cache; @@ -50,6 +51,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.endpoint.DefaultMapOAuth2AccessTokenResponseConverter; import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; import org.springframework.web.client.RestOperations; import net.solarnetwork.central.biz.UserEventAppenderBiz; @@ -69,6 +71,7 @@ import net.solarnetwork.central.c2c.http.OAuth2Utils; import net.solarnetwork.central.security.jdbc.JdbcOAuth2AuthorizedClientService; import net.solarnetwork.central.security.service.CachingOAuth2ClientRegistrationRepository; +import net.solarnetwork.central.security.service.JwtOAuth2AccessTokenResponseConverter; import net.solarnetwork.central.security.service.RetryingOAuth2AuthorizedClientManager; /** @@ -136,12 +139,17 @@ public OAuth2AuthorizedClientManager alsoEnergyOauthAuthorizedClientManager( var clientService = new JdbcOAuth2AuthorizedClientService(bytesEncryptor, jdbcOperations, repo); + // AlsoEnergy not providing expires_in in token response, so extract from JWT exp claim + var tokenResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); + tokenResponseConverter.setAccessTokenResponseConverter(new JwtOAuth2AccessTokenResponseConverter( + Clock.systemUTC(), new DefaultMapOAuth2AccessTokenResponseConverter())); + // @formatter:off var authRestOps = new RestTemplateBuilder() .requestFactory(t -> reqFactory) .messageConverters(Arrays.asList( new FormHttpMessageConverter(), - new OAuth2AccessTokenResponseHttpMessageConverter())) + tokenResponseConverter)) .errorHandler(new OAuth2ErrorResponseErrorHandler()) .build(); // @formatter:on diff --git a/solarnet/common/build.gradle b/solarnet/common/build.gradle index e3d7b0dcf..733a180a9 100644 --- a/solarnet/common/build.gradle +++ b/solarnet/common/build.gradle @@ -74,6 +74,7 @@ dependencies { implementation "software.amazon.awssdk:secretsmanager:${awsSdk2Version}" implementation 'com.cronutils:cron-utils:9.1.5' implementation 'com.fasterxml.uuid:java-uuid-generator:4.0.1' + implementation 'com.nimbusds:nimbus-jose-jwt' implementation 'commons-codec:commons-codec' implementation "commons-io:commons-io:${commonsIoVersion}" implementation 'jakarta.validation:jakarta.validation-api' diff --git a/solarnet/common/src/main/java/net/solarnetwork/central/security/service/JwtOAuth2AccessTokenResponseConverter.java b/solarnet/common/src/main/java/net/solarnetwork/central/security/service/JwtOAuth2AccessTokenResponseConverter.java new file mode 100644 index 000000000..920d3b072 --- /dev/null +++ b/solarnet/common/src/main/java/net/solarnetwork/central/security/service/JwtOAuth2AccessTokenResponseConverter.java @@ -0,0 +1,100 @@ +/* ================================================================== + * JwtOAuth2AccessTokenResponseConverter.java - 23/11/2024 6:27:24 am + * + * Copyright 2024 SolarNetwork.net Dev Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + * ================================================================== + */ + +package net.solarnetwork.central.security.service; + +import static net.solarnetwork.util.CollectionUtils.getMapString; +import java.time.InstantSource; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import net.solarnetwork.util.ObjectUtils; + +/** + * Decode {@code OAuth2AccessTokenResponse} with support for extracting + * attributes from a JWT token. + * + *
+ * Some OAuth providers do not provide a {@code expires_in} value in their token + * response. If such a provider returns the token as a JWT, this class will + * attempt to extract the {@code exp} token claim date and generate a + * {@code expires_in} value from that. + *
+ * + * @author matt + * @version 1.0 + */ +public class JwtOAuth2AccessTokenResponseConverter + implements Converter