Skip to content

Commit

Permalink
NET-422: extract AlsoEnergy OAuth token expiration from JWT itself, a…
Browse files Browse the repository at this point in the history
…s no expires_in parameter provided in get token response.
  • Loading branch information
msqr committed Nov 22, 2024
1 parent 69898fa commit e0b3143
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions solarnet/common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>
* 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.
* </p>
*
* @author matt
* @version 1.0
*/
public class JwtOAuth2AccessTokenResponseConverter
implements Converter<Map<String, Object>, OAuth2AccessTokenResponse> {

private final InstantSource clock;
private final Converter<Map<String, Object>, OAuth2AccessTokenResponse> delegate;

/**
* Constructor.
*
* @param clock
* the instant source to use
* @param delegate
* the delegate converter
* @throws IllegalArgumentException
* if any argument is {@code null}
*/
public JwtOAuth2AccessTokenResponseConverter(InstantSource clock,
Converter<Map<String, Object>, OAuth2AccessTokenResponse> delegate) {
super();
this.clock = ObjectUtils.requireNonNullArgument(clock, "clock");
this.delegate = ObjectUtils.requireNonNullArgument(delegate, "delegate");
}

@Override
public OAuth2AccessTokenResponse convert(Map<String, Object> source) {
// check if no "expires_in" parameter provided, and if not see if token is JWT with expiration date
if ( !source.containsKey(OAuth2ParameterNames.EXPIRES_IN)
&& source.containsKey(OAuth2ParameterNames.ACCESS_TOKEN) ) {
try {
JWT jwt = JWTParser.parse(getMapString(OAuth2ParameterNames.ACCESS_TOKEN, source));
JWTClaimsSet claims = jwt.getJWTClaimsSet();
if ( claims != null ) {
Date exp = claims.getExpirationTime();
if ( exp != null ) {
Map<String, Object> newSource = new LinkedHashMap<>(source);
newSource.put(OAuth2ParameterNames.EXPIRES_IN,
ChronoUnit.SECONDS.between(clock.instant(), exp.toInstant()));
source = newSource;
}
}
} catch ( Exception e ) {
// assume not a JWT, so ignore
}
}
return delegate.convert(source);
}

}

0 comments on commit e0b3143

Please sign in to comment.