From f5da03e73992486ddb4cfb5d473180f0cc028513 Mon Sep 17 00:00:00 2001 From: Johannes Leupold Date: Thu, 4 Sep 2025 14:15:17 +0200 Subject: [PATCH 1/4] OpenID Connect: Honor supported grant types unless the server decides otherwise --- .../vertx/ext/auth/oauth2/providers/OpenIDConnectAuth.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/OpenIDConnectAuth.java b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/OpenIDConnectAuth.java index f50474386..fd69cafda 100644 --- a/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/OpenIDConnectAuth.java +++ b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/OpenIDConnectAuth.java @@ -135,10 +135,11 @@ static Future discover(final Vertx vertx, final OAuth2Options config } - // reset config - config.setSupportedGrantTypes(null); if (json.containsKey("grant_types_supported")) { + // reset config + config.setSupportedGrantTypes(null); + // optional config JsonArray flows = json.getJsonArray("grant_types_supported"); flows.forEach(el -> config.addSupportedGrantType((String) el)); From 17abd35856cc28f0bbf18947d5aaefaed2906552 Mon Sep 17 00:00:00 2001 From: Johannes Leupold Date: Thu, 4 Sep 2025 14:31:43 +0200 Subject: [PATCH 2/4] empty commit to retrigger ECA validation From 65617abecf4eced629c357178021bf6ed456c14f Mon Sep 17 00:00:00 2001 From: Johannes Leupold Date: Fri, 5 Sep 2025 12:09:39 +0200 Subject: [PATCH 3/4] Tests for changed grant type handling --- vertx-auth-oauth2/pom.xml | 23 +- .../io/vertx/tests/OpenIDCDiscoveryTest.java | 252 +++++++++++++++++- 2 files changed, 271 insertions(+), 4 deletions(-) diff --git a/vertx-auth-oauth2/pom.xml b/vertx-auth-oauth2/pom.xml index e1bef5402..a1177756a 100644 --- a/vertx-auth-oauth2/pom.xml +++ b/vertx-auth-oauth2/pom.xml @@ -32,6 +32,18 @@ false + + + + org.testcontainers + testcontainers-bom + 1.21.3 + pom + import + + + + io.vertx @@ -50,9 +62,18 @@ org.testcontainers testcontainers - 1.18.0 test + + org.testcontainers + mockserver + test + + + org.mock-server + mockserver-client-java + 5.15.0 + diff --git a/vertx-auth-oauth2/src/test/java/io/vertx/tests/OpenIDCDiscoveryTest.java b/vertx-auth-oauth2/src/test/java/io/vertx/tests/OpenIDCDiscoveryTest.java index 88f1c458e..af354b354 100644 --- a/vertx-auth-oauth2/src/test/java/io/vertx/tests/OpenIDCDiscoveryTest.java +++ b/vertx-auth-oauth2/src/test/java/io/vertx/tests/OpenIDCDiscoveryTest.java @@ -3,6 +3,7 @@ import io.vertx.core.buffer.Buffer; import io.vertx.ext.auth.JWTOptions; import io.vertx.ext.auth.PubSecKeyOptions; +import io.vertx.ext.auth.oauth2.OAuth2FlowType; import io.vertx.ext.auth.oauth2.OAuth2Options; import io.vertx.ext.auth.oauth2.impl.OAuth2AuthProviderImpl; import io.vertx.ext.auth.oauth2.providers.*; @@ -10,17 +11,54 @@ import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.RunTestOnContext; import io.vertx.ext.unit.junit.VertxUnitRunner; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; import org.junit.runner.RunWith; +import org.mockserver.client.MockServerClient; +import org.mockserver.matchers.Times; +import org.mockserver.model.HttpTemplate; +import org.mockserver.model.MediaType; +import org.testcontainers.containers.MockServerContainer; +import org.testcontainers.utility.DockerImageName; + +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; @RunWith(VertxUnitRunner.class) public class OpenIDCDiscoveryTest { + private static final DockerImageName MOCKSERVER_IMAGE = DockerImageName + .parse("mockserver/mockserver") + .withTag("mockserver-" + MockServerClient.class.getPackage().getImplementationVersion()); + + @ClassRule + public static final MockServerContainer mockServer = new MockServerContainer(MOCKSERVER_IMAGE); + public static MockServerClient mockServerClient; @Rule public final RunTestOnContext rule = new RunTestOnContext(); + @BeforeClass + public static void setup() { + mockServerClient = new MockServerClient(mockServer.getHost(), mockServer.getServerPort()); + } + + @AfterClass + public static void teardown() { + // This will also stop MockServer + mockServerClient.stop(); + } + + @Before + public void resetMockServer() { + mockServerClient.reset(); + } + @Test public void testGoogle(TestContext should) { final Async test = should.async(); @@ -140,4 +178,212 @@ public void testApple(TestContext should) { .onFailure(should::fail); } + @Test + public void testConfiguredFlowTypes(TestContext should) { + final Async test = should.async(3); + + // Setup expectations for mockserver + mockServerClient + .when( + request() + .withPath("/fake-auth-server/{tenant}/.well-known/openid-configuration") + .withPathParameter("tenant", "[a-z][a-zA-Z0-9]*") + .withMethod("GET") + ) + .respond(fakeAuthServerConfigurationTemplate()); + + mockServerClient + .when( + request() + .withPath("/fake-auth-server/{tenant}/jwks") + .withPathParameter("tenant", "[a-z][a-zA-Z0-9]*") + .withMethod("GET") + ) + .respond( + response() + .withContentType(MediaType.APPLICATION_JSON.withCharset(StandardCharsets.UTF_8)) + .withBody("{\"keys\": []}") + ); + + // Configured grant types should be retained, as the server doesn't send any + OpenIDConnectAuth.discover( + rule.vertx(), + new OAuth2Options() + .setSite(mockServer.getEndpoint() + "/fake-auth-server/{tenant}") + .setTenant("test") + .setClientId("test-client") + .setSupportedGrantTypes(List.of(OAuth2FlowType.AUTH_CODE.getGrantType()))) + .onSuccess(result -> { + var options = ((OAuth2AuthProviderImpl) result).getConfig(); + + should.assertEquals( + new HashSet<>(options.getSupportedGrantTypes()), + Set.of(OAuth2FlowType.AUTH_CODE.getGrantType()) + ); + + test.countDown(); + }) + .onFailure(should::fail); + + OpenIDConnectAuth.discover( + rule.vertx(), + new OAuth2Options() + .setSite(mockServer.getEndpoint() + "/fake-auth-server/{tenant}") + .setTenant("test") + // This one should work without a client ID, as it is not required when only the implicit flow is supported + //.setClientId("test-client") + .setSupportedGrantTypes(List.of(OAuth2FlowType.IMPLICIT.getGrantType()))) + .onSuccess(result -> { + var options = ((OAuth2AuthProviderImpl) result).getConfig(); + + should.assertEquals( + new HashSet<>(options.getSupportedGrantTypes()), + Set.of(OAuth2FlowType.IMPLICIT.getGrantType()) + ); + + test.countDown(); + }) + .onFailure(should::fail); + + OpenIDConnectAuth.discover( + rule.vertx(), + new OAuth2Options() + .setSite(mockServer.getEndpoint() + "/fake-auth-server/{tenant}") + .setTenant("test") + .setClientId("test-client") + .addSupportedGrantType(OAuth2FlowType.AUTH_JWT.getGrantType()) + .addSupportedGrantType(OAuth2FlowType.AUTH_CODE.getGrantType()) + .addSupportedGrantType(OAuth2FlowType.IMPLICIT.getGrantType()) + ) + .onSuccess(result -> { + var options = ((OAuth2AuthProviderImpl) result).getConfig(); + + should.assertEquals( + new HashSet<>(options.getSupportedGrantTypes()), + Set.of( + OAuth2FlowType.IMPLICIT.getGrantType(), + OAuth2FlowType.AUTH_JWT.getGrantType(), + OAuth2FlowType.AUTH_CODE.getGrantType() + ) + ); + + test.countDown(); + }) + .onFailure(should::fail); + } + + @Test + public void testServerOverridesFlowTypes(TestContext should) { + final Async test = should.async(2); + + // Setup expectations for mockserver + mockServerClient + .when( + request() + .withPath("/fake-auth-server/{tenant}/.well-known/openid-configuration") + .withPathParameter("tenant", "[a-z][a-zA-Z0-9]*") + .withMethod("GET"), + Times.exactly(1) + ) + .respond(fakeAuthServerConfigurationTemplate(OAuth2FlowType.AUTH_CODE)); + + mockServerClient + .when( + request() + .withPath("/fake-auth-server/{tenant}/.well-known/openid-configuration") + .withPathParameter("tenant", "[a-z][a-zA-Z0-9]*") + .withMethod("GET"), + Times.exactly(1) + ) + .respond(fakeAuthServerConfigurationTemplate(OAuth2FlowType.IMPLICIT, OAuth2FlowType.PASSWORD)); + + mockServerClient + .when( + request() + .withPath("/fake-auth-server/{tenant}/jwks") + .withPathParameter("tenant", "[a-z][a-zA-Z0-9]*") + .withMethod("GET") + ) + .respond( + response() + .withContentType(MediaType.APPLICATION_JSON.withCharset(StandardCharsets.UTF_8)) + .withBody("{\"keys\": []}") + ); + + // Configured grant types should be overridden, as the server sends some + OpenIDConnectAuth.discover( + rule.vertx(), + new OAuth2Options() + .setSite(mockServer.getEndpoint() + "/fake-auth-server/{tenant}") + .setTenant("test") + .setClientId("test-client") + .setSupportedGrantTypes(List.of(OAuth2FlowType.IMPLICIT.getGrantType()))) + .onSuccess(result -> { + var options = ((OAuth2AuthProviderImpl) result).getConfig(); + + should.assertEquals( + new HashSet<>(options.getSupportedGrantTypes()), + Set.of(OAuth2FlowType.AUTH_CODE.getGrantType()) + ); + + test.countDown(); + + // Need to serialize requests this time to make the assertions reproducible + OpenIDConnectAuth.discover( + rule.vertx(), + new OAuth2Options() + .setSite(mockServer.getEndpoint() + "/fake-auth-server/{tenant}") + .setTenant("test") + .setClientId("test-client") + .setSupportedGrantTypes(List.of(OAuth2FlowType.IMPLICIT.getGrantType()))) + .onSuccess(result2 -> { + var options2 = ((OAuth2AuthProviderImpl) result2).getConfig(); + + should.assertEquals( + new HashSet<>(options2.getSupportedGrantTypes()), + Set.of( + OAuth2FlowType.IMPLICIT.getGrantType(), + OAuth2FlowType.PASSWORD.getGrantType() + ) + ); + + test.countDown(); + }) + .onFailure(should::fail); + }) + .onFailure(should::fail); + } + + private static HttpTemplate fakeAuthServerConfigurationTemplate(OAuth2FlowType... supportedGrantTypes) { + var base = mockServer.getEndpoint() + "/fake-auth-server/{{request.pathParameters.tenant.0}}"; + var body = "{" + + "\\\"issuer\\\": \\\"" + base + "\\\"," + + "\\\"authorization_endpoint\\\": \\\"" + base + "/auth\\\"," + + "\\\"token_endpoint\\\": \\\"" + base + "/token\\\"," + + "\\\"end_session_endpoint\\\": \\\"" + base + "/logout\\\"," + + "\\\"revocation_endpoint\\\": \\\"" + base + "/revoke\\\"," + + "\\\"userinfo_endpoint\\\": \\\"" + base + "/userinfo\\\"," + + "\\\"introspection_endpoint\\\": \\\"" + base + "/introspect\\\","; + + if (supportedGrantTypes.length > 0) { + body += "\\\"grant_types_supported\\\": " + + Stream.of(supportedGrantTypes) + .map(OAuth2FlowType::getGrantType) + .map(grantType -> "\\\"" + grantType + "\\\"") + .collect(Collectors.joining(", ", "[", "]")) + + ","; + } + + body += "\\\"jwks_uri\\\": \\\"" + base + "/jwks\\\""; + body += "}"; + + var template = + "{\n" + + " \"statusCode\": 200,\n" + + " \"headers\": {\"Content-Type\": \"application/json; charset=utf-8\"},\n" + + " \"body\": \"" + body + "\"\n" + + "}"; + + return HttpTemplate.template(HttpTemplate.TemplateType.MUSTACHE, template); + } } From ed4814109ce30e74816d0b465d9abb5955c63afb Mon Sep 17 00:00:00 2001 From: Johannes Leupold Date: Fri, 5 Sep 2025 12:48:04 +0200 Subject: [PATCH 4/4] Alternative solution: Intersection of supported grant types --- .../oauth2/providers/OpenIDConnectAuth.java | 41 ++- .../io/vertx/tests/OpenIDCDiscoveryTest.java | 239 +++++++++++++++++- 2 files changed, 264 insertions(+), 16 deletions(-) diff --git a/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/OpenIDConnectAuth.java b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/OpenIDConnectAuth.java index fd69cafda..3cf7b72a2 100644 --- a/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/OpenIDConnectAuth.java +++ b/vertx-auth-oauth2/src/main/java/io/vertx/ext/auth/oauth2/providers/OpenIDConnectAuth.java @@ -19,13 +19,18 @@ import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.http.HttpMethod; -import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.ext.auth.JWTOptions; import io.vertx.ext.auth.impl.http.SimpleHttpClient; import io.vertx.ext.auth.oauth2.OAuth2Auth; import io.vertx.ext.auth.oauth2.OAuth2Options; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + /** * Simplified factory to create an {@link io.vertx.ext.auth.oauth2.OAuth2Auth} for OpenID Connect. * @@ -134,15 +139,39 @@ static Future discover(final Vertx vertx, final OAuth2Options config jwtOptions.setIssuer(json.getString("issuer")); } - - if (json.containsKey("grant_types_supported")) { + // optional config + List configuredGrantTypes = config.getSupportedGrantTypes(); + final Set configured = configuredGrantTypes == null ? null : new HashSet<>(configuredGrantTypes); + // reset config config.setSupportedGrantTypes(null); - // optional config - JsonArray flows = json.getJsonArray("grant_types_supported"); - flows.forEach(el -> config.addSupportedGrantType((String) el)); + Stream supportedGrantTypes = json.getJsonArray("grant_types_supported") + .stream() + .map(el -> (String) el); + + // If the caller configured supported grant types, use the intersection with the server-supported grant types. + // Otherwise, use all grant types that the server supports. + if (configured != null) { + supportedGrantTypes = supportedGrantTypes.filter(configured::contains); + } + + supportedGrantTypes + .forEach(config::addSupportedGrantType); + + // If the supported grant types are still null here, either the server sent an empty list of supported grant + // types or the intersection with the configured grant types was empty. Both cases are errors. + if (config.getSupportedGrantTypes() == null) { + return Future.failedFuture( + "No supported grant types with this authorization provider. Supported: " + + json.getJsonArray("grant_types_supported").stream() + .map(el -> (String) el) + .collect(Collectors.joining(", ", "[", "]")) + + ". Configured: " + + (configuredGrantTypes == null ? "" : configuredGrantTypes.stream().collect(Collectors.joining(", ", "[", "]"))) + ); + } } try { diff --git a/vertx-auth-oauth2/src/test/java/io/vertx/tests/OpenIDCDiscoveryTest.java b/vertx-auth-oauth2/src/test/java/io/vertx/tests/OpenIDCDiscoveryTest.java index af354b354..bdc1fb77b 100644 --- a/vertx-auth-oauth2/src/test/java/io/vertx/tests/OpenIDCDiscoveryTest.java +++ b/vertx-auth-oauth2/src/test/java/io/vertx/tests/OpenIDCDiscoveryTest.java @@ -22,7 +22,6 @@ import java.nio.charset.StandardCharsets; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -212,7 +211,7 @@ public void testConfiguredFlowTypes(TestContext should) { .setSite(mockServer.getEndpoint() + "/fake-auth-server/{tenant}") .setTenant("test") .setClientId("test-client") - .setSupportedGrantTypes(List.of(OAuth2FlowType.AUTH_CODE.getGrantType()))) + .addSupportedGrantType(OAuth2FlowType.AUTH_CODE.getGrantType())) .onSuccess(result -> { var options = ((OAuth2AuthProviderImpl) result).getConfig(); @@ -232,7 +231,7 @@ public void testConfiguredFlowTypes(TestContext should) { .setTenant("test") // This one should work without a client ID, as it is not required when only the implicit flow is supported //.setClientId("test-client") - .setSupportedGrantTypes(List.of(OAuth2FlowType.IMPLICIT.getGrantType()))) + .addSupportedGrantType(OAuth2FlowType.IMPLICIT.getGrantType())) .onSuccess(result -> { var options = ((OAuth2AuthProviderImpl) result).getConfig(); @@ -273,7 +272,7 @@ public void testConfiguredFlowTypes(TestContext should) { } @Test - public void testServerOverridesFlowTypes(TestContext should) { + public void testIntersectFlowTypes(TestContext should) { final Async test = should.async(2); // Setup expectations for mockserver @@ -285,7 +284,7 @@ public void testServerOverridesFlowTypes(TestContext should) { .withMethod("GET"), Times.exactly(1) ) - .respond(fakeAuthServerConfigurationTemplate(OAuth2FlowType.AUTH_CODE)); + .respond(fakeAuthServerConfigurationTemplate(OAuth2FlowType.AUTH_CODE, OAuth2FlowType.IMPLICIT)); mockServerClient .when( @@ -295,7 +294,93 @@ public void testServerOverridesFlowTypes(TestContext should) { .withMethod("GET"), Times.exactly(1) ) - .respond(fakeAuthServerConfigurationTemplate(OAuth2FlowType.IMPLICIT, OAuth2FlowType.PASSWORD)); + .respond(fakeAuthServerConfigurationTemplate(OAuth2FlowType.IMPLICIT, OAuth2FlowType.PASSWORD, OAuth2FlowType.AUTH_CODE)); + + mockServerClient + .when( + request() + .withPath("/fake-auth-server/{tenant}/jwks") + .withPathParameter("tenant", "[a-z][a-zA-Z0-9]*") + .withMethod("GET") + ) + .respond( + response() + .withContentType(MediaType.APPLICATION_JSON.withCharset(StandardCharsets.UTF_8)) + .withBody("{\"keys\": []}") + ); + + // Configured grant types should be overridden, as the server sends some + OpenIDConnectAuth.discover( + rule.vertx(), + new OAuth2Options() + .setSite(mockServer.getEndpoint() + "/fake-auth-server/{tenant}") + .setTenant("test") + // This one should work without a client ID, as it is not required when only the implicit flow is supported + //.setClientId("test-client") + .addSupportedGrantType(OAuth2FlowType.IMPLICIT.getGrantType())) + .onSuccess(result -> { + var options = ((OAuth2AuthProviderImpl) result).getConfig(); + + // Server sends authorization_code, implicit, so the intersection is only implicit + should.assertEquals( + new HashSet<>(options.getSupportedGrantTypes()), + Set.of(OAuth2FlowType.IMPLICIT.getGrantType()) + ); + + test.countDown(); + + // Need to serialize requests this time to make the assertions reproducible + OpenIDConnectAuth.discover( + rule.vertx(), + new OAuth2Options() + .setSite(mockServer.getEndpoint() + "/fake-auth-server/{tenant}") + .setTenant("test") + .setClientId("test-client") + .addSupportedGrantType(OAuth2FlowType.AUTH_CODE.getGrantType()) + .addSupportedGrantType(OAuth2FlowType.IMPLICIT.getGrantType())) + .onSuccess(result2 -> { + var options2 = ((OAuth2AuthProviderImpl) result2).getConfig(); + + // Server sends authorization_code, implicit, password, so the intersection is implicit, authorization_code + should.assertEquals( + new HashSet<>(options2.getSupportedGrantTypes()), + Set.of( + OAuth2FlowType.IMPLICIT.getGrantType(), + OAuth2FlowType.AUTH_CODE.getGrantType() + ) + ); + + test.countDown(); + }) + .onFailure(should::fail); + }) + .onFailure(should::fail); + } + + @Test + public void testServerSupportedFlowTypes(TestContext should) { + final Async test = should.async(2); + + // Setup expectations for mockserver + mockServerClient + .when( + request() + .withPath("/fake-auth-server/{tenant}/.well-known/openid-configuration") + .withPathParameter("tenant", "[a-z][a-zA-Z0-9]*") + .withMethod("GET"), + Times.exactly(1) + ) + .respond(fakeAuthServerConfigurationTemplate(OAuth2FlowType.AUTH_CODE, OAuth2FlowType.IMPLICIT)); + + mockServerClient + .when( + request() + .withPath("/fake-auth-server/{tenant}/.well-known/openid-configuration") + .withPathParameter("tenant", "[a-z][a-zA-Z0-9]*") + .withMethod("GET"), + Times.exactly(1) + ) + .respond(fakeAuthServerConfigurationTemplate(OAuth2FlowType.IMPLICIT, OAuth2FlowType.PASSWORD, OAuth2FlowType.AUTH_CODE)); mockServerClient .when( @@ -317,13 +402,14 @@ public void testServerOverridesFlowTypes(TestContext should) { .setSite(mockServer.getEndpoint() + "/fake-auth-server/{tenant}") .setTenant("test") .setClientId("test-client") - .setSupportedGrantTypes(List.of(OAuth2FlowType.IMPLICIT.getGrantType()))) + ) .onSuccess(result -> { var options = ((OAuth2AuthProviderImpl) result).getConfig(); + // Server sends authorization_code, implicit should.assertEquals( new HashSet<>(options.getSupportedGrantTypes()), - Set.of(OAuth2FlowType.AUTH_CODE.getGrantType()) + Set.of(OAuth2FlowType.AUTH_CODE.getGrantType(), OAuth2FlowType.IMPLICIT.getGrantType()) ); test.countDown(); @@ -335,15 +421,17 @@ public void testServerOverridesFlowTypes(TestContext should) { .setSite(mockServer.getEndpoint() + "/fake-auth-server/{tenant}") .setTenant("test") .setClientId("test-client") - .setSupportedGrantTypes(List.of(OAuth2FlowType.IMPLICIT.getGrantType()))) + ) .onSuccess(result2 -> { var options2 = ((OAuth2AuthProviderImpl) result2).getConfig(); + // Server sends authorization_code, implicit, password should.assertEquals( new HashSet<>(options2.getSupportedGrantTypes()), Set.of( OAuth2FlowType.IMPLICIT.getGrantType(), - OAuth2FlowType.PASSWORD.getGrantType() + OAuth2FlowType.PASSWORD.getGrantType(), + OAuth2FlowType.AUTH_CODE.getGrantType() ) ); @@ -354,6 +442,137 @@ public void testServerOverridesFlowTypes(TestContext should) { .onFailure(should::fail); } + @Test + public void testDefaultFlowTypes(TestContext should) { + final Async test = should.async(2); + + // Setup expectations for mockserver + mockServerClient + .when( + request() + .withPath("/fake-auth-server/{tenant}/.well-known/openid-configuration") + .withPathParameter("tenant", "[a-z][a-zA-Z0-9]*") + .withMethod("GET") + ) + .respond(fakeAuthServerConfigurationTemplate()); + + mockServerClient + .when( + request() + .withPath("/fake-auth-server/{tenant}/jwks") + .withPathParameter("tenant", "[a-z][a-zA-Z0-9]*") + .withMethod("GET") + ) + .respond( + response() + .withContentType(MediaType.APPLICATION_JSON.withCharset(StandardCharsets.UTF_8)) + .withBody("{\"keys\": []}") + ); + + // Configured grant types should be overridden, as the server sends some + OpenIDConnectAuth.discover( + rule.vertx(), + new OAuth2Options() + .setSite(mockServer.getEndpoint() + "/fake-auth-server/{tenant}") + .setTenant("test") + .setClientId("test-client") + ) + .onSuccess(result -> { + var options = ((OAuth2AuthProviderImpl) result).getConfig(); + + // Server sends nothing, nothing is configured -> fall back to default + should.assertNull(options.getSupportedGrantTypes()); + + test.countDown(); + }) + .onFailure(should::fail); + + OpenIDConnectAuth.discover( + rule.vertx(), + new OAuth2Options() + .setSite(mockServer.getEndpoint() + "/fake-auth-server/{tenant}") + .setTenant("test") + .setClientId("test-client") + ) + .onSuccess(result2 -> { + var options2 = ((OAuth2AuthProviderImpl) result2).getConfig(); + + // Server sends nothing, nothing is configured -> fall back to default + should.assertNull(options2.getSupportedGrantTypes()); + + test.countDown(); + }) + .onFailure(should::fail); + } + + @Test + public void testNoSupportedFlowTypes(TestContext should) { + final Async test = should.async(2); + + // Setup expectations for mockserver + mockServerClient + .when( + request() + .withPath("/fake-auth-server/{tenant}/.well-known/openid-configuration") + .withPathParameter("tenant", "[a-z][a-zA-Z0-9]*") + .withMethod("GET") + ) + .respond(fakeAuthServerConfigurationTemplate(OAuth2FlowType.AUTH_CODE, OAuth2FlowType.IMPLICIT)); + + mockServerClient + .when( + request() + .withPath("/fake-auth-server/{tenant}/jwks") + .withPathParameter("tenant", "[a-z][a-zA-Z0-9]*") + .withMethod("GET") + ) + .respond( + response() + .withContentType(MediaType.APPLICATION_JSON.withCharset(StandardCharsets.UTF_8)) + .withBody("{\"keys\": []}") + ); + + // Configured grant types should be overridden, as the server sends some + OpenIDConnectAuth.discover( + rule.vertx(), + new OAuth2Options() + .setSite(mockServer.getEndpoint() + "/fake-auth-server/{tenant}") + .setTenant("test") + .setClientId("test-client") + .addSupportedGrantType(OAuth2FlowType.PASSWORD.getGrantType()) + ) + .onSuccess(result -> should.fail("Discovery should fail")) + .onFailure(err -> { + should.assertEquals( + "No supported grant types with this authorization provider. Supported: [authorization_code, implicit]. Configured: [password]", + err.getMessage() + ); + + test.countDown(); + + // Need to serialize requests this time to make the assertions reproducible + OpenIDConnectAuth.discover( + rule.vertx(), + new OAuth2Options() + .setSite(mockServer.getEndpoint() + "/fake-auth-server/{tenant}") + .setTenant("test") + .setClientId("test-client") + .addSupportedGrantType(OAuth2FlowType.CLIENT.getGrantType()) + .addSupportedGrantType(OAuth2FlowType.PASSWORD.getGrantType()) + .addSupportedGrantType(OAuth2FlowType.AUTH_JWT.getGrantType()) + ) + .onSuccess(result -> should.fail("Discovery should fail")) + .onFailure(err2 -> { + should.assertEquals( + "No supported grant types with this authorization provider. Supported: [authorization_code, implicit]. Configured: [client_credentials, password, urn:ietf:params:oauth:grant-type:jwt-bearer]", + err2.getMessage() + ); + + test.countDown(); + }); + }); + } + private static HttpTemplate fakeAuthServerConfigurationTemplate(OAuth2FlowType... supportedGrantTypes) { var base = mockServer.getEndpoint() + "/fake-auth-server/{{request.pathParameters.tenant.0}}"; var body = "{" +