Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix caching of remote JWK sets #40

Merged
merged 3 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading