Skip to content

Commit

Permalink
Merge pull request #40 from italiangrid/fix-jwk-caching
Browse files Browse the repository at this point in the history
Fixes:

- Fix caching of remote JWK sets
- move spring-boot version from 2.7.17 to 2.7.18
- added classes and dependencies necessary to remove jetty-utils
- voms-api-java now is a direct dependency

Fix: https://issues.infn.it/jira/browse/STOR-1603
  • Loading branch information
enricovianello authored Mar 13, 2024
2 parents b9cd6df + 937db49 commit 68b0ee0
Show file tree
Hide file tree
Showing 19 changed files with 1,575 additions and 56 deletions.
75 changes: 42 additions & 33 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!-- Keep this aligned with the spring.boot-version property below! -->
<version>2.7.17</version>
<version>2.7.18</version>
<relativePath />
</parent>

Expand All @@ -32,14 +32,13 @@
<java.version>17</java.version>

<!-- Keep this aligned with the parent project version! -->
<spring-boot.version>2.7.17</spring-boot.version>
<spring-boot.version>2.7.18</spring-boot.version>

<!-- Sonarcloud.io properties -->
<sonar.projectKey>italiangrid_storm-webdav</sonar.projectKey>
<sonar.organization>italiangrid</sonar.organization>
<sonar.host.url>https://sonarcloud.io</sonar.host.url>

<jetty-utils.version>0.4.6.v20220506</jetty-utils.version>
<milton.version>2.7.1.7</milton.version>

<commons-lang.version>2.3</commons-lang.version>
Expand All @@ -55,9 +54,9 @@
<owner-config.version>1.0.5.1</owner-config.version>

<spring-security-oauth2.version>2.3.3.RELEASE</spring-security-oauth2.version>
<nimbus-jose-jwt.version>6.0.2</nimbus-jose-jwt.version>
<mock-server.version>5.5.1</mock-server.version>
<bouncycastle.version>1.76</bouncycastle.version>
<voms-api-java.version>3.3.2</voms-api-java.version>

<start-class>org.italiangrid.storm.webdav.WebdavService</start-class>

Expand Down Expand Up @@ -348,34 +347,6 @@
<artifactId>metrics-servlets</artifactId>
</dependency>

<dependency>
<groupId>org.italiangrid</groupId>
<artifactId>jetty-utils</artifactId>
<version>${jetty-utils.version}</version>
<exclusions>
<exclusion>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
</exclusion>
<exclusion>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</exclusion>
<exclusion>
<groupId>org.eclipse.jetty.aggregate</groupId>
<artifactId>jetty-all</artifactId>
</exclusion>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</exclusion>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
Expand All @@ -388,7 +359,6 @@
<version>${bouncycastle.version}</version>
</dependency>


<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
Expand All @@ -404,6 +374,11 @@
<artifactId>logback-classic</artifactId>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
</dependency>

<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
Expand All @@ -419,6 +394,34 @@
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-rewrite</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-common</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-conscrypt-server</artifactId>
</dependency>

<dependency>
<groupId>commons-lang</groupId>
Expand Down Expand Up @@ -463,6 +466,12 @@
<version>${milton.version}</version>
</dependency>

<dependency>
<groupId>org.italiangrid</groupId>
<artifactId>voms-api-java</artifactId>
<version>${voms-api-java.version}</version>
</dependency>

<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ public boolean isEnforceAudienceChecks() {
@Min(value = 1, message = "The refresh period must be a positive integer")
int refreshPeriodMinutes = 60;

@Min(value = 1, message = "The refresh timeout must be a positive integer")
int refreshTimeoutSeconds = 30;

public List<AuthorizationServer> getIssuers() {
return issuers;
}
Expand All @@ -112,6 +115,14 @@ public void setRefreshPeriodMinutes(int refreshPeriodMinutes) {
this.refreshPeriodMinutes = refreshPeriodMinutes;
}

public int getRefreshTimeoutSeconds() {
return refreshTimeoutSeconds;
}

public void setRefreshTimeoutSeconds(int refreshTimeoutSeconds) {
this.refreshTimeoutSeconds = refreshTimeoutSeconds;
}

public void setEnableOidc(boolean enableOidc) {
this.enableOidc = enableOidc;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,45 @@
import static java.lang.String.format;

import java.net.URI;
import java.time.Duration;
import java.util.Arrays;
import java.util.Map;

import org.italiangrid.storm.webdav.config.OAuthProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import com.nimbusds.jose.RemoteKeySourceException;

@Service
public class DefaultOidcConfigurationFetcher implements OidcConfigurationFetcher {

public static final String WELL_KNOWN_FRAGMENT = "/.well-known/openid-configuration";
public static final String ISSUER_MISMATCH_ERROR_TEMPLATE =
"Issuer in metadata '%s' does not match with requested issuer '%s'";
public static final String NO_JWKS_URI_ERROR_TEMPLATE =
public static final String NO_JWKS_URI_ERROR_TEMPLATE =
"No jwks_uri found in metadata for issuer '%s'";

private static final MediaType APPLICATION_JWK_SET_JSON =
new MediaType("application", "jwk-set+json");

public static final Logger LOG = LoggerFactory.getLogger(DefaultOidcConfigurationFetcher.class);

final RestTemplateBuilder restBuilder;
final RestTemplate restTemplate;

@Autowired
public DefaultOidcConfigurationFetcher(RestTemplateBuilder restBuilder) {
this.restBuilder = restBuilder;
public DefaultOidcConfigurationFetcher(RestTemplateBuilder restBuilder,
OAuthProperties oAuthProperties) {
final Duration TIMEOUT = Duration.ofSeconds(oAuthProperties.getRefreshTimeoutSeconds());
this.restTemplate = restBuilder.setConnectTimeout(TIMEOUT).setReadTimeout(TIMEOUT).build();
}

private void metadataChecks(String issuer, Map<String, Object> oidcConfiguration) {
Expand All @@ -59,29 +70,27 @@ private void metadataChecks(String issuer, Map<String, Object> oidcConfiguration
throw new OidcConfigurationResolutionError(
format(ISSUER_MISMATCH_ERROR_TEMPLATE, metadataIssuer, issuer));
}

if (!oidcConfiguration.containsKey("jwks_uri")) {
throw new OidcConfigurationResolutionError(format(NO_JWKS_URI_ERROR_TEMPLATE,issuer));
throw new OidcConfigurationResolutionError(format(NO_JWKS_URI_ERROR_TEMPLATE, issuer));
}
}

@Override
public Map<String, Object> loadConfigurationForIssuer(String issuer) {
LOG.debug("Fetching OpenID configuration for {}", issuer);

ParameterizedTypeReference<Map<String, Object>> typeReference =
new ParameterizedTypeReference<Map<String, Object>>() {};

RestTemplate rest = restBuilder.build();

URI uri = UriComponentsBuilder.fromUriString(issuer + WELL_KNOWN_FRAGMENT).build().toUri();

try {

RequestEntity<Void> request = RequestEntity.get(uri).build();
Map<String, Object> conf = rest.exchange(request, typeReference).getBody();
Map<String, Object> conf = restTemplate.exchange(request, typeReference).getBody();
metadataChecks(issuer, conf);
return conf;
return conf;
} catch (RuntimeException e) {
final String errorMsg =
format("Unable to resolve OpenID configuration for issuer '%s' from '%s': %s", issuer,
Expand All @@ -95,4 +104,30 @@ public Map<String, Object> loadConfigurationForIssuer(String issuer) {
}
}

@Override
public String loadJWKSourceForURL(URI uri) throws RemoteKeySourceException {

LOG.debug("Fetching JWK from {}", uri);

HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON));
ResponseEntity<String> response = null;
try {
RequestEntity<Void> request = RequestEntity.get(uri).headers(headers).build();
response = restTemplate.exchange(request, String.class);
if (response.getStatusCodeValue() != 200) {
throw new RuntimeException(
format("Received status code: %s", response.getStatusCodeValue()));
}
} catch (RuntimeException e) {
final String errorMsg = format("Unable to get JWK from '%s'", uri);
if (LOG.isDebugEnabled()) {
LOG.error("{}: {}", errorMsg, e.getMessage());
}
throw new RemoteKeySourceException(errorMsg, e);
}

return response.getBody();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.italiangrid.storm.webdav.oauth.utils;

import java.util.concurrent.Callable;

import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.lang.Nullable;

public class NoExpirationStringCache extends AbstractValueAdaptingCache {

private static final String NAME = "NoExpirationCache";
private final String value;

public NoExpirationStringCache(String value) {
super(false);
this.value = value;
}

@Override
public String getName() {
return NAME;
}

@Override
public Object getNativeCache() {
return this;
}

@Override
@Nullable
protected Object lookup(Object key) {
return value;
}

@Override
public void put(Object key, Object value) {
return;
}

@Override
public void evict(Object key) {
return;
}

@Override
public void clear() {
return;
}

@SuppressWarnings("unchecked")
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
return (T) fromStoreValue(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@
*/
package org.italiangrid.storm.webdav.oauth.utils;

import java.net.URI;
import java.util.Map;

import com.nimbusds.jose.RemoteKeySourceException;

public interface OidcConfigurationFetcher {

Map<String, Object> loadConfigurationForIssuer(String issuer);

String loadJWKSourceForURL(URI uri) throws RemoteKeySourceException;

}
Loading

0 comments on commit 68b0ee0

Please sign in to comment.