Skip to content

Commit

Permalink
Refactoring and testing.
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremyg484 committed Dec 1, 2023
1 parent df61330 commit aa6f6ba
Show file tree
Hide file tree
Showing 15 changed files with 1,024 additions and 61 deletions.
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ groovy = "4.0.13"

geb = "7.0"
selenium = "4.15.0"
system-stubs-core = "2.1.4"
testcontainers = "1.19.2"
unboundid-ldapsdk = "6.0.10"
bouncycastle = "1.70"
Expand Down Expand Up @@ -50,6 +51,8 @@ bcpkix-jdk15on = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bo
bcprov-jdk15on = { module = "org.bouncycastle:bcprov-jdk15on", version.ref = "bouncycastle" }
kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
bcpkix = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bcpkix" }
system-stubs-core = { module = "uk.org.webcompere:system-stubs-core", version.ref = "system-stubs-core" }


testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version.ref = "testcontainers" }
testcontainers = { module = "org.testcontainers:testcontainers" }
Expand Down
4 changes: 3 additions & 1 deletion security-jwt/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ dependencies {
annotationProcessor(mnValidation.micronaut.validation.processor)
api(mnValidation.micronaut.validation)
api(projects.micronautSecurity)
implementation(mn.micronaut.http.client.core)
api(libs.managed.nimbus.jose.jwt)
implementation(mnReactor.micronaut.reactor)
testImplementation(libs.bcpkix.jdk15on)
testImplementation(libs.bcprov.jdk15on)

compileOnly(mn.micronaut.http.client.core)
compileOnly(mn.micronaut.http.server)
compileOnly(mn.micronaut.json.core)

Expand All @@ -35,6 +35,8 @@ dependencies {

testImplementation(platform(libs.testcontainers.bom))
testImplementation(libs.testcontainers)

testImplementation(libs.system.stubs.core)
}

test {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,18 @@
package io.micronaut.security.token.jwt.signature.jwks;

import com.nimbusds.jose.jwk.JWKSet;
import io.micronaut.context.BeanContext;
import io.micronaut.core.annotation.Blocking;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.optim.StaticOptimizations;
import io.micronaut.core.util.SupplierUtil;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.HttpClientConfiguration;
import io.micronaut.http.client.LoadBalancer;
import io.micronaut.http.client.exceptions.HttpClientException;
import io.micronaut.inject.qualifiers.Qualifiers;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.net.URL;
import java.text.ParseException;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

/**
Expand All @@ -53,14 +41,10 @@ public class DefaultJwkSetFetcher implements JwkSetFetcher<JWKSet> {

private static final Logger LOG = LoggerFactory.getLogger(DefaultJwkSetFetcher.class);

private final BeanContext beanContext;
private final Supplier<HttpClient> defaultJwkSetClient;
private final ConcurrentHashMap<String, HttpClient> jwkSetClients = new ConcurrentHashMap<>();
private final JwksClient jwksClient;

public DefaultJwkSetFetcher(BeanContext beanContext,
HttpClientConfiguration defaultClientConfiguration) {
this.beanContext = beanContext;
this.defaultJwkSetClient = SupplierUtil.memoized(() -> beanContext.createBean(HttpClient.class, LoadBalancer.empty(), defaultClientConfiguration));
public DefaultJwkSetFetcher(JwksClient jwksClient) {
this.jwksClient = jwksClient;
}

@Override
Expand All @@ -72,13 +56,13 @@ public Optional<JWKSet> fetch(@Nullable String url) {
}
return OPTIMIZATIONS.findJwkSet(url)
.map(s -> Optional.of(s.get()))
.orElseGet(() -> Optional.ofNullable(load(url)));
.orElseGet(() -> Optional.ofNullable(load(null, url)));
}

@Override
@NonNull
@Blocking
public Optional<JWKSet> fetch(@NonNull String providerName, @Nullable String url) {
public Optional<JWKSet> fetch(@Nullable String providerName, @Nullable String url) {
if (url == null) {
return Optional.empty();
}
Expand All @@ -93,44 +77,18 @@ public void clearCache(@NonNull String url) {
}

@Nullable
private JWKSet load(@NonNull String url) {
private JWKSet load(@Nullable String providerName, @NonNull String url) {
try {
return JWKSet.load(new URL(url));
} catch (IOException | ParseException e) {
String jwkSetContent = jwksClient.load(providerName, url);
return jwkSetContent != null ? JWKSet.parse(jwkSetContent) : null;
} catch (ParseException e) {
if (LOG.isErrorEnabled()) {
LOG.error("Exception loading JWK from " + url, e);
LOG.error("Exception parsing JWK Set response from " + url, e);
}
}
return null;
}

@Nullable
private JWKSet load(@NonNull String providerName, @NonNull String url) {
try {
String jwkSetContent = Mono.from(getClient(providerName).retrieve(url)).block();
Objects.requireNonNull(jwkSetContent, "JWK Set must not be null.");
return JWKSet.parse(jwkSetContent);
} catch (HttpClientException | ParseException e) {
if (LOG.isErrorEnabled()) {
LOG.error("Exception loading JWK from " + url, e);
}
}
return null;
}

/**
* Retrieves a client for the given provider.
*
* @param providerName The provider name
* @return An HTTP client to use to send the request
*/
protected HttpClient getClient(String providerName) {
return jwkSetClients.computeIfAbsent(providerName, provider -> {
Optional<io.micronaut.http.client.HttpClient> client = beanContext.findBean(io.micronaut.http.client.HttpClient.class, Qualifiers.byName(provider));
return client.orElseGet(defaultJwkSetClient);
});
}

/**
* AOT Optimizations.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2017-2023 original authors
*
* 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
*
* https://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 io.micronaut.security.token.jwt.signature.jwks;

import io.micronaut.context.BeanContext;
import io.micronaut.context.annotation.Primary;
import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.util.SupplierUtil;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.HttpClientConfiguration;
import io.micronaut.http.client.HttpClientRegistry;
import io.micronaut.http.client.HttpVersionSelection;
import io.micronaut.http.client.LoadBalancer;
import io.micronaut.http.client.ServiceHttpClientConfiguration;
import io.micronaut.http.client.exceptions.HttpClientException;
import io.micronaut.inject.qualifiers.Qualifiers;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;

import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

/**
* Implementation of {@link JwksClient} that uses the Micronaut {@link HttpClient}.
*
* <p>
* If a named service-specific client is configured (i.e. with "micronaut.http.services.foo.*") with a
* name that matches the name used for security configuration (i.e. "micronaut.security.token.jwt.signatures.jwks.foo.*")
* then that client will be used for the request. Otherwise, a default client will be used.
*
* @author Jeremy Grelle
* @since 4.5.0
*/
@Singleton
@Primary
@Requires(classes = HttpClient.class)
public class HttpClientJwksClient implements JwksClient {

private static final Logger LOG = LoggerFactory.getLogger(HttpClientJwksClient.class);

private final BeanContext beanContext;
private final HttpClientRegistry<HttpClient> clientRegistry;
private final Supplier<HttpClient> defaultJwkSetClient;
private final ConcurrentHashMap<String, HttpClient> jwkSetClients = new ConcurrentHashMap<>();

public HttpClientJwksClient(BeanContext beanContext, HttpClientRegistry<HttpClient> clientRegistry, HttpClientConfiguration defaultClientConfiguration) {
this.beanContext = beanContext;
this.clientRegistry = clientRegistry;
this.defaultJwkSetClient = SupplierUtil.memoized(() -> beanContext.createBean(HttpClient.class, LoadBalancer.empty(), defaultClientConfiguration));
}

@Override
public String load(@Nullable String providerName, @NonNull String url) throws HttpClientException {
try {
return Mono.from(getClient(providerName).retrieve(url)).block();
} catch (HttpClientException e) {
if (LOG.isErrorEnabled()) {
LOG.error("Exception loading JWK from " + url, e);
}
}
return null;
}

/**
* Retrieves an HTTP client for the given provider.
*
* @param providerName The provider name
* @return An HTTP client to use to send the JWKS request
*/
protected HttpClient getClient(@Nullable String providerName) {
if (providerName == null) {
return defaultJwkSetClient.get();
}
return jwkSetClients.computeIfAbsent(providerName, provider ->
beanContext.findBean(ServiceHttpClientConfiguration.class, Qualifiers.byName(provider))
.map(serviceConfig -> this.clientRegistry.getClient(HttpVersionSelection.forClientConfiguration(serviceConfig), provider, "/"))
.orElseGet(defaultJwkSetClient));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ public interface JwkSetFetcher<T> {
*
* @param url The Jwks uri
* @return The Json Web Key Set representation or an empty optional if it could not be loaded
* @deprecated Use {@link #fetch(String, String)} instead.
*/
@NonNull
@Blocking
@Deprecated(since = "4.5.0", forRemoval = true)
Optional<T> fetch(@Nullable String url);

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2017-2023 original authors
*
* 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
*
* https://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 io.micronaut.security.token.jwt.signature.jwks;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;

/**
* Client for loading Json Web Key Set content over http.
*
* @author Jeremy Grelle
* @since 4.5.0
*/
public interface JwksClient {

/**
* Loads remote Json Web Key Set content over http.
*
* @param providerName The jwks provider name
* @param url The URL for loading the remote JWK Set
* @return The JWK Set response body content
*/
@Nullable
String load(@Nullable String providerName, @NonNull String url);
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,27 @@ public boolean verify(SignedJWT jwt) throws JOSEException {
return JwksSignatureUtils.verify(jwt, computeJWKSet().orElse(null), jwkValidator);
}

/**
* Instantiates a JWKSet for a given url.
* @param url JSON Web Key Set Url.
* @return a JWKSet or null if there was an error.
* @deprecated Use {@link #loadJwkSet(String, String)} instead.
*/
@Nullable
@Deprecated(since = "4.5.0", forRemoval = true)
protected JWKSet loadJwkSet(String url) {
return jwkSetFetcher.fetch(null, url)
.orElse(null);
}

/**
* Instantiates a JWKSet for a given url.
* @param providerName The name of the JWKS configuration.
* @param url JSON Web Key Set Url.
* @return a JWKSet or null if there was an error.
*/
@Nullable
protected JWKSet loadJwkSet(String providerName, String url) {
protected JWKSet loadJwkSet(@Nullable String providerName, String url) {
return jwkSetFetcher.fetch(providerName, url)
.orElse(null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,15 @@
import com.nimbusds.jose.jwk.KeyType;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.naming.Named;

/**
* JSON Web Key Set Configuration.
*
* @author Sergio del Amo
* @since 1.1.0
*/
public interface JwksSignatureConfiguration {

@NonNull
String getName();
public interface JwksSignatureConfiguration extends Named {

/**
* Json Web Key Set endpoint url.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class JwksSignatureConfigurationProperties implements JwksSignatureConfig
@SuppressWarnings("WeakerAccess")
public static final int DEFAULT_CACHE_EXPIRATION = 60;

@Nullable
private final String name;

@NonNull
Expand Down
Loading

0 comments on commit aa6f6ba

Please sign in to comment.