diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfigurationBuilder.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfigurationBuilder.java index 99f9b185a5d..f2d757e493c 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfigurationBuilder.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfigurationBuilder.java @@ -100,6 +100,9 @@ protected OidcClientConfiguration internalBuild(final OidcJsonConfiguration oidc if (oidcJsonConfiguration.getTokenCookiePath() != null) { oidcClientConfiguration.setOidcStateCookiePath(oidcJsonConfiguration.getTokenCookiePath()); } + if (oidcJsonConfiguration.getScope() != null) { + oidcClientConfiguration.setScope(oidcJsonConfiguration.getScope()); + } if (oidcJsonConfiguration.getPrincipalAttribute() != null) oidcClientConfiguration.setPrincipalAttribute(oidcJsonConfiguration.getPrincipalAttribute()); oidcClientConfiguration.setResourceCredentials(oidcJsonConfiguration.getCredentials()); diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcJsonConfiguration.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcJsonConfiguration.java index 5e65d60fe06..724d61885cf 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcJsonConfiguration.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcJsonConfiguration.java @@ -46,7 +46,7 @@ "register-node-at-startup", "register-node-period", "token-store", "adapter-state-cookie-path", "principal-attribute", "proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live", "min-time-between-jwks-requests", "public-key-cache-ttl", - "ignore-oauth-query-parameter", "verify-token-audience", "token-signature-algorithm" + "ignore-oauth-query-parameter", "verify-token-audience", "token-signature-algorithm", "scope" }) public class OidcJsonConfiguration { @@ -140,6 +140,9 @@ public class OidcJsonConfiguration { @JsonProperty("token-signature-algorithm") protected String tokenSignatureAlgorithm = DEFAULT_TOKEN_SIGNATURE_ALGORITHM; + @JsonProperty("scope") + protected String scope; + /** * The Proxy url to use for requests to the auth-server, configurable via the adapter config property {@code proxy-url}. */ @@ -511,5 +514,13 @@ public void setTokenSignatureAlgorithm(String tokenSignatureAlgorithm) { this.tokenSignatureAlgorithm = tokenSignatureAlgorithm; } + public String getScope(){ + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + } diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcRequestAuthenticator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcRequestAuthenticator.java index 6b51d980d97..bdfdd98c24c 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcRequestAuthenticator.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcRequestAuthenticator.java @@ -169,7 +169,7 @@ protected String getRedirectUri(String state) { for (String paramName : forwardableQueryParams) { String paramValue = getQueryParamValue(facade, paramName); if (SCOPE.equals(paramName)) { - paramValue = addOidcScopeIfNeeded(paramValue); + paramValue = addOidcScopeIfNeeded(deployment.getScope()); } if (paramValue != null && !paramValue.isEmpty()) { forwardedQueryParams.add(new BasicNameValuePair(paramName, paramValue)); @@ -180,6 +180,7 @@ protected String getRedirectUri(String state) { if (deployment.getAuthUrl() == null) { return null; } + URIBuilder redirectUriBuilder = new URIBuilder(deployment.getAuthUrl()) .addParameter(RESPONSE_TYPE, CODE) .addParameter(CLIENT_ID, deployment.getResourceName()) diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java index b1fb8ea2d2e..a76975f165a 100644 --- a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java @@ -181,6 +181,7 @@ protected HtmlInput loginToKeycloak(String username, String password, URI reques webClient.addCookie(getCookieString(cookie), requestUri.toURL(), null); } } + HtmlPage keycloakLoginPage = webClient.getPage(location); HtmlForm loginForm = keycloakLoginPage.getForms().get(0); loginForm.getInputByName(KEYCLOAK_USERNAME).setValueAttribute(username); diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java index 84132472d1c..47d559f22c6 100644 --- a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java @@ -22,18 +22,23 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; import static org.wildfly.security.http.oidc.Oidc.OIDC_NAME; +import static org.wildfly.security.http.oidc.Oidc.OIDC_SCOPE; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.net.URI; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.HashMap; +import java.util.List; import java.util.Map; +import com.gargoylesoftware.htmlunit.WebClient; import org.apache.http.HttpStatus; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import org.wildfly.security.http.HttpAuthenticationException; import org.wildfly.security.http.HttpServerAuthenticationMechanism; import com.gargoylesoftware.htmlunit.TextPage; @@ -42,6 +47,7 @@ import io.restassured.RestAssured; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.QueueDispatcher; +import org.wildfly.security.http.HttpServerCookie; /** * Tests for the OpenID Connect authentication mechanism. @@ -161,6 +167,32 @@ public void testTokenSignatureAlgorithm() throws Exception { performAuthentication(getOidcConfigurationInputStreamWithTokenSignatureAlgorithm(), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); } + @Test + public void testInvalidScope() throws Exception { + checkInvalidScopeError(getoidcConfigurationInputStreamWithScope(CLIENT_SECRET, "INVALID_SCOPE"), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), "error=invalid_scope", OIDC_SCOPE + "+INVALID_SCOPE"); + } + + @Test + public void testEmptyScope() throws Exception { + performAuthentication(getoidcConfigurationInputStreamWithScope(CLIENT_SECRET, ""), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + checkScopeValuesInHTTPServerResponse(OIDC_SCOPE); + } + + @Test + public void testSingleScopeValue() throws Exception { + performAuthentication(getoidcConfigurationInputStreamWithScope(CLIENT_SECRET, "profile"), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + checkScopeValuesInHTTPServerResponse(OIDC_SCOPE + "+profile"); + } + + @Test + public void testMultipleScopeValue() throws Exception { + performAuthentication(getoidcConfigurationInputStreamWithScope(CLIENT_SECRET, "profile email phone"), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + checkScopeValuesInHTTPServerResponse(OIDC_SCOPE + "+profile+email+phone"); + } private void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, int expectedDispatcherStatusCode, String expectedLocation, String clientPageText) throws Exception { @@ -199,6 +231,52 @@ private InputStream getOidcConfigurationInputStream(String clientSecret) { return getOidcConfigurationInputStream(clientSecret, KEYCLOAK_CONTAINER.getAuthServerUrl()); } + private void checkScopeValuesInHTTPServerResponse(String expectedScope) throws HttpAuthenticationException, URISyntaxException { + Map props = new HashMap<>(); + HttpServerAuthenticationMechanism mechanism = oidcFactory.createAuthenticationMechanism(OIDC_NAME, props, getCallbackHandler()); + URI requestUri = new URI(getClientUrl()); + TestingHttpServerRequest request = new TestingHttpServerRequest(null, requestUri); + mechanism.evaluateRequest(request); + TestingHttpServerResponse response = request.getResponse(); + assert(response.getFirstResponseHeaderValue("Location").contains("scope=" + expectedScope)); + } + + private void checkInvalidScopeError(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, + int expectedDispatcherStatusCode, String expectedLocation, String clientPageText, String expectedScope) throws Exception { + try { + Map props = new HashMap<>(); + OidcClientConfiguration oidcClientConfiguration = OidcClientConfigurationBuilder.build(oidcConfig); + assertEquals(OidcClientConfiguration.RelativeUrlsUsed.NEVER, oidcClientConfiguration.getRelativeUrls()); + + OidcClientContext oidcClientContext = new OidcClientContext(oidcClientConfiguration); + oidcFactory = new OidcMechanismFactory(oidcClientContext); + HttpServerAuthenticationMechanism mechanism = oidcFactory.createAuthenticationMechanism(OIDC_NAME, props, getCallbackHandler()); + + URI requestUri = new URI(getClientUrl()); + TestingHttpServerRequest request = new TestingHttpServerRequest(null, requestUri); + mechanism.evaluateRequest(request); + TestingHttpServerResponse response = request.getResponse(); + assert(response.getFirstResponseHeaderValue("Location").contains("scope=" + expectedScope)); + assertEquals(loginToKeycloak ? HttpStatus.SC_MOVED_TEMPORARILY : HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + assertEquals(Status.NO_AUTH, request.getResult()); + + if (loginToKeycloak) { + client.setDispatcher(createAppResponse(mechanism, expectedDispatcherStatusCode, expectedLocation, clientPageText)); + WebClient webClient = getWebClient(); + List cookies = response.getCookies(); + if (cookies != null) { + for (HttpServerCookie cookie : cookies) { + webClient.addCookie(getCookieString(cookie), requestUri.toURL(), null); + } + } + TextPage keycloakLoginPage = webClient.getPage(response.getLocation()); + assertTrue(keycloakLoginPage.getWebResponse().getWebRequest().toString().contains("error_description=Invalid+scopes")); + } + } finally { + client.setDispatcher(new QueueDispatcher()); + } + } + private InputStream getOidcConfigurationInputStream(String clientSecret, String authServerUrl) { String oidcConfig = "{\n" + " \"realm\" : \"" + TEST_REALM + "\",\n" + @@ -290,4 +368,19 @@ private InputStream getOidcConfigurationInputStreamWithTokenSignatureAlgorithm() "}"; return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); } + + private InputStream getoidcConfigurationInputStreamWithScope(String clientSecret, String scopeValue){ + String oidcConfig = "{\n" + + " \"realm\" : \"" + TEST_REALM + "\",\n" + + " \"resource\" : \"" + CLIENT_ID + "\",\n" + + " \"public-client\" : \"false\",\n" + + " \"provider-url\" : \"" + KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM + "/" + "\",\n" + + " \"scope\" : \"" + scopeValue + "\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"credentials\" : {\n" + + " \"secret\" : \"" + clientSecret + "\"\n" + + " }\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } }