diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index 7a0112c1d1d66..1948cc4dbeab4 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -570,6 +570,13 @@ public enum ResponseMode { @ConfigItem public Optional> scopes = Optional.empty(); + /** + * Add the 'openid' scope automatically to the list of scopes. This is required for OpenId Connect providers + * but will not work for OAuth2 providers such as Twitter OAuth2 which does not accept that scope and throws an error. + */ + @ConfigItem(defaultValueDocumentation = "true") + public Optional addOpenidScope = Optional.empty(); + /** * Additional properties which will be added as the query parameters to the authentication redirect URI. */ @@ -721,6 +728,14 @@ public void setExtraParams(Map extraParams) { this.extraParams = extraParams; } + public void setAddOpenidScope(boolean addOpenidScope) { + this.addOpenidScope = Optional.of(addOpenidScope); + } + + public Optional isAddOpenidScope() { + return addOpenidScope; + } + public Optional isForceRedirectHttpsScheme() { return forceRedirectHttpsScheme; } @@ -1065,7 +1080,8 @@ public static enum Provider { FACEBOOK, GITHUB, GOOGLE, - MICROSOFT + MICROSOFT, + TWITTER } public Optional getProvider() { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index 9be8d0b93cde6..1df47a5867625 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -350,9 +350,10 @@ && isRedirectFromProvider(context, configContext)) { ? configContext.oidcConfig.getAuthentication().scopes.get() : Collections.emptyList(); List scopes = new ArrayList<>(oidcConfigScopes.size() + 1); - scopes.add("openid"); + if (configContext.oidcConfig.getAuthentication().addOpenidScope.orElse(true)) { + scopes.add("openid"); + } scopes.addAll(oidcConfigScopes); - configContext.oidcConfig.getAuthentication().scopes.ifPresent(scopes::addAll); codeFlowParams.append(AMP).append(OidcConstants.TOKEN_SCOPE).append(EQ) .append(OidcCommonUtils.urlEncode(String.join(" ", scopes))); @@ -387,6 +388,8 @@ && isRedirectFromProvider(context, configContext)) { String authorizationURL = configContext.provider.getMetadata().getAuthorizationUri() + "?" + codeFlowParams.toString(); + LOG.debugf("Code flow redirect to: %s", authorizationURL); + return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.FOUND.code(), HttpHeaders.LOCATION, authorizationURL)); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java index c74b325f5efa1..f509a3613bb24 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java @@ -62,6 +62,7 @@ public Uni getJsonWebKeySet() { } public Uni getUserInfo(String token) { + LOG.debugf("Get UserInfo on: %s auth: %s", metadata.getUserInfoUri(), OidcConstants.BEARER_SCHEME + " " + token); return client.getAbs(metadata.getUserInfoUri()) .putHeader(AUTHORIZATION_HEADER, OidcConstants.BEARER_SCHEME + " " + token) .send().onItem().transform(resp -> getUserInfo(resp)); @@ -126,6 +127,7 @@ private UniOnItem> getHttpResponse(String uri, MultiMap for } else { formBody.add(OidcConstants.CLIENT_ID, oidcConfig.clientId.get()); } + LOG.debugf("Get token on: %s params: %s headers: %s", metadata.getTokenUri(), formBody, request.headers()); // Retry up to three times with a one second delay between the retries if the connection is closed. Uni> response = request.sendBuffer(OidcCommonUtils.encodeForm(formBody)) .onFailure(ConnectException.class) @@ -152,6 +154,7 @@ private TokenIntrospection getTokenIntrospection(HttpResponse resp) { private static JsonObject getJsonObject(HttpResponse resp) { if (resp.statusCode() == 200) { + LOG.debugf("Request succeeded: %s", resp.bodyAsJsonObject()); return resp.bodyAsJsonObject(); } else { throw responseException(resp); @@ -160,6 +163,7 @@ private static JsonObject getJsonObject(HttpResponse resp) { private static String getString(HttpResponse resp) { if (resp.statusCode() == 200) { + LOG.debugf("Request succeeded: %s", resp.bodyAsString()); return resp.bodyAsString(); } else { throw responseException(resp); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index 5200606cbd385..8a28e0bac033e 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -353,9 +353,15 @@ static OidcTenantConfig mergeTenantConfig(OidcTenantConfig tenant, OidcTenantCon if (tenant.authentication.userInfoRequired.isEmpty()) { tenant.authentication.userInfoRequired = provider.authentication.userInfoRequired; } + if (tenant.authentication.pkceRequired.isEmpty()) { + tenant.authentication.pkceRequired = provider.authentication.pkceRequired; + } if (tenant.authentication.scopes.isEmpty()) { tenant.authentication.scopes = provider.authentication.scopes; } + if (tenant.authentication.addOpenidScope.isEmpty()) { + tenant.authentication.addOpenidScope = provider.authentication.addOpenidScope; + } if (tenant.authentication.forceRedirectHttpsScheme.isEmpty()) { tenant.authentication.forceRedirectHttpsScheme = provider.authentication.forceRedirectHttpsScheme; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java index e4aaca08414c8..c02ea11da47b6 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java @@ -20,6 +20,8 @@ public static OidcTenantConfig provider(OidcTenantConfig.Provider provider) { return microsoft(); } else if (OidcTenantConfig.Provider.FACEBOOK == provider) { return facebook(); + } else if (OidcTenantConfig.Provider.TWITTER == provider) { + return twitter(); } return null; } @@ -38,6 +40,22 @@ private static OidcTenantConfig github() { return ret; } + private static OidcTenantConfig twitter() { + OidcTenantConfig ret = new OidcTenantConfig(); + ret.setAuthServerUrl("https://api.twitter.com/2/oauth2"); + ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP); + ret.setDiscoveryEnabled(false); + ret.setAuthorizationPath("https://twitter.com/i/oauth2/authorize"); + ret.setTokenPath("token"); + ret.setUserInfoPath("https://api.twitter.com/2/users/me"); + ret.getAuthentication().setAddOpenidScope(false); + ret.getAuthentication().setScopes(List.of("offline.access", "tweet.read", "users.read")); + ret.getAuthentication().setUserInfoRequired(true); + ret.getAuthentication().setIdTokenRequired(false); + ret.getAuthentication().setPkceRequired(true); + return ret; + } + private static OidcTenantConfig google() { OidcTenantConfig ret = new OidcTenantConfig(); ret.setAuthServerUrl("https://accounts.google.com"); diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java index 272f3b441f5fc..1d877bd773a04 100644 --- a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java @@ -83,6 +83,62 @@ public void testOverrideGitHubProperties() throws Exception { assertEquals(List.of("write"), config.authentication.scopes.get()); } + @Test + public void testAcceptTwitterProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.TWITTER)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); + assertFalse(config.isDiscoveryEnabled().get()); + assertEquals("https://api.twitter.com/2/oauth2", config.getAuthServerUrl().get()); + assertEquals("https://twitter.com/i/oauth2/authorize", config.getAuthorizationPath().get()); + assertEquals("token", config.getTokenPath().get()); + assertEquals("https://api.twitter.com/2/users/me", config.getUserInfoPath().get()); + + assertFalse(config.authentication.idTokenRequired.get()); + assertTrue(config.authentication.userInfoRequired.get()); + assertFalse(config.authentication.addOpenidScope.get()); + assertEquals(List.of("offline.access", "tweet.read", "users.read"), config.authentication.scopes.get()); + assertTrue(config.authentication.pkceRequired.get()); + } + + @Test + public void testOverrideTwitterProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + + tenant.setApplicationType(ApplicationType.HYBRID); + tenant.setDiscoveryEnabled(true); + tenant.setAuthServerUrl("http://localhost/wiremock"); + tenant.setAuthorizationPath("authorization"); + tenant.setTokenPath("tokens"); + tenant.setUserInfoPath("userinfo"); + + tenant.authentication.setIdTokenRequired(true); + tenant.authentication.setUserInfoRequired(false); + tenant.authentication.setAddOpenidScope(true); + tenant.authentication.setPkceRequired(false); + tenant.authentication.setScopes(List.of("write")); + + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.TWITTER)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); + assertTrue(config.isDiscoveryEnabled().get()); + assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); + assertEquals("authorization", config.getAuthorizationPath().get()); + assertEquals("tokens", config.getTokenPath().get()); + assertEquals("userinfo", config.getUserInfoPath().get()); + + assertTrue(config.authentication.idTokenRequired.get()); + assertFalse(config.authentication.userInfoRequired.get()); + assertEquals(List.of("write"), config.authentication.scopes.get()); + assertTrue(config.authentication.addOpenidScope.get()); + assertFalse(config.authentication.pkceRequired.get()); + } + @Test public void testAcceptFacebookProperties() throws Exception { OidcTenantConfig tenant = new OidcTenantConfig(); diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java index 7aeefe828004e..ef2df3a1f0d36 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/CodeFlowTest.java @@ -146,7 +146,7 @@ public void testCodeFlowScopeErrorWithErrorPage() throws IOException { endpointErrorLocation = "http" + endpointErrorLocation.substring(5); HtmlPage page = webClient.getPage(URI.create(endpointErrorLocation).toURL()); - assertEquals("error: invalid_scope, error_description: Invalid scopes: unknown profile email phone", + assertEquals("error: invalid_scope, error_description: Invalid scopes: unknown", page.getBody().asText()); webClient.getCookieManager().clearCookies(); }