Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ managed-owasp-java-html-sanitizer = "20260313.1"
managed-jjwt = "0.13.0"
managed-ojdbc-extensions = "1.1.0" # https://github.com/oracle/ojdbc-extensions/releases
micronaut = "5.1.6"
micronaut-platform = "5.0.3"
micronaut-platform = "5.0.4"
awaitility = "4.3.0"
geb = "8.0.1"
selenium = "4.41.0"
Expand Down
3 changes: 1 addition & 2 deletions security-jwt/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ dependencies {
annotationProcessor(mn.micronaut.graal)
annotationProcessor(mnSerde.micronaut.serde.processor)
annotationProcessor(mnValidation.micronaut.validation.processor)
api(mnValidation.validation) // // jakarta.validation:jakarta.validation-api
testImplementation(mnValidation.micronaut.validation)
api(mnValidation.micronaut.validation)
api(projects.micronautSecurity)
api(libs.managed.nimbus.jose.jwt)
implementation(mnReactor.micronaut.reactor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import io.micronaut.core.util.ArgumentUtils;
import io.micronaut.security.token.jwt.config.JwtConfigurationProperties;
import jakarta.validation.constraints.NotNull;
import io.micronaut.validation.annotation.URL;

/**
* JSON Web Key Set (JWKS) Signature Configuration properties holder.
Expand Down Expand Up @@ -53,6 +54,7 @@
@NonNull
private Integer cacheExpiration = DEFAULT_CACHE_EXPIRATION;

@URL
Comment thread
sdelamo marked this conversation as resolved.
private String url;

private KeyType keyType = DEFAULT_KEYTYPE;
Expand All @@ -74,7 +76,7 @@
@Override
@NonNull
@Deprecated(forRemoval = true, since = "4.11.0")
public Integer getCacheExpiration() {

Check warning on line 79 in security-jwt/src/main/java/io/micronaut/security/token/jwt/signature/jwks/JwksSignatureConfigurationProperties.java

View workflow job for this annotation

GitHub Actions / build

[removal] getCacheExpiration() in JwksSignatureConfiguration has been deprecated and marked for removal
return cacheExpiration;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ class JwksProxySpec extends Specification {
'micronaut.http.services.gateway.url' : "http://localhost:${authEmbeddedServer.port}",
'micronaut.http.services.gateway.proxy-type' : 'http',
'micronaut.http.services.gateway.proxy-address' : "localhost:${proxyEmbeddedServer.port}",
'micronaut.security.token.jwt.signatures.jwks.gateway.url' : "/keys",
'micronaut.security.token.jwt.signatures.jwks.gateway.url' : "http://localhost:${authEmbeddedServer.port}/keys",
],
null
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.micronaut.security.token.jwt.signature.jwks;

import io.micronaut.context.ApplicationContext;
import io.micronaut.context.exceptions.BeanInstantiationException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

class JwksSignatureConfigurationPropertiesTest {

@Test
void validJwksUrlIsAccepted() {
try (ApplicationContext context = ApplicationContext.run(Map.of(
"micronaut.security.token.jwt.signatures.jwks.test.url", "https://example.com/.well-known/jwks.json"))) {
JwksSignatureConfiguration configuration = context.getBean(JwksSignatureConfiguration.class);

assertEquals("https://example.com/.well-known/jwks.json", configuration.getUrl());
}
}

@ParameterizedTest
@ValueSource(strings = {
"example.com/.well-known/jwks.json",
"/.well-known/jwks.json"
})
void jwksUrlMustBeValid(String url) {
BeanInstantiationException exception = assertThrows(BeanInstantiationException.class, () -> {

Check warning on line 33 in security-jwt/src/test/java/io/micronaut/security/token/jwt/signature/jwks/JwksSignatureConfigurationPropertiesTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor the code of the lambda to have only one invocation possibly throwing a runtime exception.

See more on https://sonarcloud.io/project/issues?id=micronaut-projects_micronaut-security&issues=AZ9gQPzYsTdhG0Y6ib9z&open=AZ9gQPzYsTdhG0Y6ib9z&pullRequest=2252
try (ApplicationContext context = ApplicationContext.run(Map.of(
"micronaut.security.token.jwt.signatures.jwks.test.url", url))) {
context.getBean(JwksSignatureConfiguration.class);
}
});

assertTrue(exception.getMessage().contains("url - must be a valid URL"));
}
}
4 changes: 1 addition & 3 deletions security-oauth2/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ dependencies {
annotationProcessor(mnSerde.micronaut.serde.processor)
annotationProcessor(projects.micronautSecurityProcessor)
annotationProcessor(mnValidation.micronaut.validation.processor)

api(mnValidation.validation) // // jakarta.validation:jakarta.validation-api
testImplementation(mnValidation.micronaut.validation)
api(mnValidation.micronaut.validation)
compileOnly(mn.micronaut.inject.java)
compileOnly(projects.micronautSecurityJwt)
compileOnly(mn.micronaut.http.server)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public interface OauthClientConfiguration extends Toggleable {
*/
default SecureEndpoint getTokenEndpoint() throws ConfigurationException {
return getToken().map(secureEndpointConfiguration -> new DefaultSecureEndpoint(secureEndpointConfiguration, DEFAULT_AUTH_METHOD))
.orElseThrow(() -> new ConfigurationException("Oauth client " + getName() + " requires the token endpoint configuration to be set in configuration"));
.orElseThrow(() -> new ConfigurationException("Oauth client " + getName() + " requires the token endpoint configuration to be set in configuration"));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import io.micronaut.security.oauth2.configuration.endpoints.RevocationEndpointConfiguration;
import io.micronaut.security.oauth2.configuration.endpoints.SecureEndpointConfiguration;
import io.micronaut.security.oauth2.configuration.endpoints.TokenEndpointConfiguration;
import jakarta.validation.Valid;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import io.micronaut.core.convert.format.MapFormat;
Expand All @@ -54,6 +55,8 @@
import java.util.Map;
import java.util.Optional;

import static io.micronaut.security.oauth2.configuration.endpoints.DefaultEndpointConfiguration.HTTP_OR_HTTPS_URL_REGEX;
Comment thread
Copilot marked this conversation as resolved.
Outdated

/**
* Stores configuration of each configured OAuth 2.0 client.
*
Expand Down Expand Up @@ -83,11 +86,11 @@ public class OauthClientConfigurationProperties implements OauthClientConfigurat
private List<String> scopes;
private boolean enabled = DEFAULT_ENABLED;
private GrantType grantType = GrantType.AUTHORIZATION_CODE;
private AuthorizationEndpointConfigurationProperties authorization;
private TokenEndpointConfigurationProperties token;
private IntrospectionEndpointConfigurationProperties introspection;
private RevocationEndpointConfigurationProperties revocation;
private OpenIdClientConfigurationProperties openid;
private @Valid AuthorizationEndpointConfigurationProperties authorization;
private @Valid TokenEndpointConfigurationProperties token;
private @Valid IntrospectionEndpointConfigurationProperties introspection;
private @Valid RevocationEndpointConfigurationProperties revocation;
private @Valid OpenIdClientConfigurationProperties openid;
private ClientCredentialsConfigurationProperties clientCredentials;
private AuthorizationServer authorizationServer;
private boolean proxyWellKnownOauthAuthorizationServer;
Expand Down Expand Up @@ -524,12 +527,14 @@ public static class OpenIdClientConfigurationProperties implements OpenIdClientC
private boolean fetchConfiguration = DEFAULT_FETCH_CONFIGURATION;
private URL issuer;
private String configurationPath = DEFAULT_CONFIG_PATH;

@io.micronaut.validation.annotation.URL(regexp = HTTP_OR_HTTPS_URL_REGEX)
private String jwksUri;
private RegistrationEndpointConfigurationProperties registration;
private UserInfoEndpointConfigurationProperties userInfo;
private AuthorizationEndpointConfigurationProperties authorization;
private TokenEndpointConfigurationProperties token;
private EndSessionConfigurationProperties endSession = new EndSessionConfigurationProperties();
private @Valid RegistrationEndpointConfigurationProperties registration;
private @Valid UserInfoEndpointConfigurationProperties userInfo;
private @Valid AuthorizationEndpointConfigurationProperties authorization;
private @Valid TokenEndpointConfigurationProperties token;
private @Valid EndSessionConfigurationProperties endSession = new EndSessionConfigurationProperties();
Comment on lines +533 to +537

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this cascades validation down, maybe you want to add more tests for different validation use cases

private boolean protectedResourceMetadata = DEFAULT_PROTECTED_RESOURCE_METADATA;

/**
Expand Down Expand Up @@ -603,7 +608,7 @@ public void setConfigurationPath(@NonNull String configurationPath) {
}

@Override
public Optional<String> getJwksUri() {
public Optional<@io.micronaut.validation.annotation.URL(regexp = HTTP_OR_HTTPS_URL_REGEX) String> getJwksUri() {
return Optional.ofNullable(jwksUri);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.micronaut.security.oauth2.configuration.endpoints;

import io.micronaut.validation.annotation.URL;
import java.util.Optional;

/**
Expand All @@ -24,9 +25,10 @@
* @since 1.2.0
*/
public interface EndpointConfiguration {
String HTTP_OR_HTTPS_URL_REGEX = "(?i)https?://.*";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably worth adding javadoc to this public constant


/**
* @return The optional endpoint url
*/
Optional<String> getUrl();
Optional<@URL(regexp = HTTP_OR_HTTPS_URL_REGEX) String> getUrl();
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class PasswordGrantConditionSpec extends Specification {

where:
properties | description
['micronaut.security.oauth2.clients.foo.openid.token.url': 'https:/foo.com/auth/token'] | "PasswordGrantCondition evaluates to true for openId grant-type=password configuration"
['micronaut.security.oauth2.clients.foo.openid.token.url': 'https://foo.com/auth/token'] | "PasswordGrantCondition evaluates to true for openId grant-type=password configuration"
['micronaut.security.oauth2.clients.foo.token.url': 'https://foo.com/auth/token', 'spec.name': 'FooNamedPasswordAuthenticationProviderSpec'] | "PasswordGrantCondition to true for oauth2 grant-type=password configuration"
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
package io.micronaut.security.oauth2.configuration;

import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.Property;
import io.micronaut.context.exceptions.BeanInstantiationException;
import io.micronaut.security.oauth2.configuration.endpoints.SecureEndpointConfiguration;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.Map;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.*;

Expand Down Expand Up @@ -45,4 +54,57 @@
assertEquals("client_secret_post", tokenNewEndpoint.getAuthenticationMethod().get());
}

@Test
void validTokenEndpointUrlIsAccepted() {
assertEquals("https://www.strava.com/oauth/token", stravaNewConfiguration.getTokenEndpoint().getUrl());
}

@ParameterizedTest
@ValueSource(strings = {
"example.com/oauth/token",
"https:/example.com/oauth/token",
"/oauth/token",
"ftp://example.com/oauth/token"
})
void tokenEndpointUrlMustBeAbsoluteHttpUrl(String url) {
assertInvalidTokenEndpointUrl(url);
}

@ParameterizedTest(name = "{0}")
@MethodSource("endpointConfigurationProperties")
void endpointConfigurationUrlMustBeAbsoluteHttpUrl(String configurationType, String property) {
assertInvalidOauthClientProperty(property, "ftp://example.com/oauth/endpoint");
}

@Test
void openIdJwksUriMustBeAnAbsoluteHttpUrl() {
assertInvalidOauthClientProperty("openid.jwks-uri", "ftp://example.com/oauth/jwks");
}

private static Stream<Arguments> endpointConfigurationProperties() {
return Stream.of(
Arguments.of("AuthorizationEndpointConfigurationProperties", "authorization.url"),
Arguments.of("IntrospectionEndpointConfigurationProperties", "introspection.url"),
Arguments.of("RevocationEndpointConfigurationProperties", "revocation.url"),
Arguments.of("RegistrationEndpointConfigurationProperties", "openid.registration.url"),
Arguments.of("UserInfoEndpointConfigurationProperties", "openid.user-info.url"),
Arguments.of("OpenIdClientConfigurationProperties.AuthorizationEndpointConfigurationProperties", "openid.authorization.url"),
Arguments.of("OpenIdClientConfigurationProperties.TokenEndpointConfigurationProperties", "openid.token.url"),
Arguments.of("EndSessionConfigurationProperties", "openid.end-session.url")
);
}

private void assertInvalidTokenEndpointUrl(String url) {
assertInvalidOauthClientProperty("token.url", url);
}

private void assertInvalidOauthClientProperty(String property, String value) {
BeanInstantiationException exception = assertThrows(BeanInstantiationException.class, () -> ApplicationContext.run(Map.of(

Check warning on line 102 in security-oauth2/src/test/java/io/micronaut/security/oauth2/configuration/OauthClientConfigurationTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor the code of the lambda to have only one invocation possibly throwing a runtime exception.

See more on https://sonarcloud.io/project/issues?id=micronaut-projects_micronaut-security&issues=AZ9gQPwbsTdhG0Y6ib9x&open=AZ9gQPwbsTdhG0Y6ib9x&pullRequest=2252
"micronaut.security.oauth2.clients.invalid." + property, value,
"micronaut.security.oauth2.clients.invalid.client-id", "xxx",
"micronaut.security.oauth2.clients.invalid.client-secret", "yyy")));

assertTrue(exception.getMessage().contains("must be a valid URL"));
}
Comment thread
Copilot marked this conversation as resolved.

}
Loading