diff --git a/.github/workflows/pr-ci.yaml b/.github/workflows/pr-ci.yaml index 2f50a0d5e56..6dae10efb3e 100644 --- a/.github/workflows/pr-ci.yaml +++ b/.github/workflows/pr-ci.yaml @@ -18,6 +18,13 @@ jobs: uses: actions/setup-java@v1 with: java-version: 11 + - if: matrix.os == 'macos-latest' + name: Install docker + run: | + brew install docker docker-machine docker-compose + brew services start docker-machine + docker-machine create --driver virtualbox default + docker --version - name: Cache Maven packages uses: actions/cache@v2 with: diff --git a/http/oidc/pom.xml b/http/oidc/pom.xml index 5ecba23b7f4..9a3557079f1 100644 --- a/http/oidc/pom.xml +++ b/http/oidc/pom.xml @@ -97,6 +97,58 @@ jose4j + + org.wildfly.security + wildfly-elytron-tests + test-jar + test + + + junit + junit + test + + + io.rest-assured + rest-assured + test + + + org.testcontainers + testcontainers + test + + + org.keycloak + keycloak-admin-client + test + + + org.jboss.logmanager + jboss-logmanager + test + + + org.jboss.slf4j + slf4j-jboss-logmanager + test + + + net.sourceforge.htmlunit + htmlunit + test + + + org.apache.commons + commons-lang3 + test + + + com.squareup.okhttp3 + mockwebserver + test + + diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/KeycloakConfiguration.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/KeycloakConfiguration.java new file mode 100644 index 00000000000..0e80a70cf59 --- /dev/null +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/KeycloakConfiguration.java @@ -0,0 +1,131 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2021 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 + * + * http://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 org.wildfly.security.http.oidc; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.RolesRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +import io.restassured.RestAssured; + +/** + * Keycloak configuration for testing. + * + * @author Farah Juma + */ +public class KeycloakConfiguration { + + private static final String USER_ROLE = "user"; + private static final String ADMIN_ROLE = "admin"; + public static final String ALICE = "alice"; + public static final String ALICE_PASSWORD = "alice123+"; + private static final String BOB = "bob"; + private static final String BOB_PASSWORD = "bob123+"; + + /** + * Configure RealmRepresentation as follows: + * + */ + public static RealmRepresentation getRealmRepresentation(final String realmName, String clientId, String clientSecret, + String clientHostName, int clientPort, String clientApp) { + return createRealm(realmName, clientId, clientSecret, clientHostName, clientPort, clientApp); + } + + public static String getAdminAccessToken(String authServerUrl) { + return RestAssured + .given() + .param("grant_type", "password") + .param("username", KeycloakContainer.KEYCLOAK_ADMIN_USER) + .param("password", KeycloakContainer.KEYCLOAK_ADMIN_PASSWORD) + .param("client_id", "admin-cli") + .when() + .post(authServerUrl + "/realms/master/protocol/openid-connect/token") + .as(AccessTokenResponse.class).getToken(); + } + + private static RealmRepresentation createRealm(String name, String clientId, String clientSecret, + String clientHostName, int clientPort, String clientApp) { + RealmRepresentation realm = new RealmRepresentation(); + + realm.setRealm(name); + realm.setEnabled(true); + realm.setUsers(new ArrayList<>()); + realm.setClients(new ArrayList<>()); + realm.setAccessTokenLifespan(3); + realm.setSsoSessionMaxLifespan(3); + + RolesRepresentation roles = new RolesRepresentation(); + List realmRoles = new ArrayList<>(); + + roles.setRealm(realmRoles); + realm.setRoles(roles); + + realm.getRoles().getRealm().add(new RoleRepresentation("user", null, false)); + realm.getRoles().getRealm().add(new RoleRepresentation("admin", null, false)); + + realm.getClients().add(createWebAppClient(clientId, clientSecret, clientHostName, clientPort, clientApp)); + + realm.getUsers().add(createUser(ALICE, ALICE_PASSWORD, Arrays.asList(USER_ROLE, ADMIN_ROLE))); + realm.getUsers().add(createUser(BOB, BOB_PASSWORD, Arrays.asList(USER_ROLE))); + return realm; + } + + private static ClientRepresentation createWebAppClient(String clientId, String clientSecret, String clientHostName, int clientPort, String clientApp) { + ClientRepresentation client = new ClientRepresentation(); + client.setClientId(clientId); + client.setPublicClient(false); + client.setSecret(clientSecret); + //client.setRedirectUris(Arrays.asList("*")); + client.setRedirectUris(Arrays.asList("http://" + clientHostName + ":" + clientPort + "/" + clientApp)); + client.setEnabled(true); + return client; + } + + private static UserRepresentation createUser(String username, String password, List realmRoles) { + UserRepresentation user = new UserRepresentation(); + user.setUsername(username); + user.setEnabled(true); + user.setCredentials(new ArrayList<>()); + user.setRealmRoles(realmRoles); + user.setEmail(username + "@gmail.com"); + + CredentialRepresentation credential = new CredentialRepresentation(); + credential.setType(CredentialRepresentation.PASSWORD); + credential.setValue(password); + credential.setTemporary(false); + user.getCredentials().add(credential); + return user; + } + +} \ No newline at end of file diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/KeycloakContainer.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/KeycloakContainer.java new file mode 100644 index 00000000000..b59db032f8c --- /dev/null +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/KeycloakContainer.java @@ -0,0 +1,61 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2021 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 + * + * http://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 org.wildfly.security.http.oidc; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +/** + * KeycloakContainer for testing. + * + * @author Farah Juma + */ +public class KeycloakContainer extends GenericContainer { + public static final String KEYCLOAK_ADMIN_USER = "admin"; + public static final String KEYCLOAK_ADMIN_PASSWORD = "admin"; + private static final String KEYCLOAK_AUTH_PATH = "/auth"; + + private static final String KEYCLOAK_IMAGE = "quay.io/keycloak/keycloak:latest"; + private static final int KEYCLOAK_PORT_HTTP = 8080; + private static final int KEYCLOAK_PORT_HTTPS = 8443; + + private boolean useHttps; + + public KeycloakContainer() { + this(false); + } + + public KeycloakContainer(final boolean useHttps) { + super(KEYCLOAK_IMAGE); + this.useHttps = useHttps; + + } + + @Override + protected void configure() { + withExposedPorts(KEYCLOAK_PORT_HTTP, KEYCLOAK_PORT_HTTPS); + waitingFor(Wait.forHttp("/auth").forPort(8080)); + withEnv("KEYCLOAK_USER", KEYCLOAK_ADMIN_USER); + withEnv("KEYCLOAK_PASSWORD", KEYCLOAK_ADMIN_PASSWORD); + } + + public String getAuthServerUrl() { + return String.format("http://%s:%s%s", getContainerIpAddress(), useHttps ? getMappedPort(KEYCLOAK_PORT_HTTPS) : getMappedPort(KEYCLOAK_PORT_HTTP), KEYCLOAK_AUTH_PATH); + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..446ddf44fda --- /dev/null +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java @@ -0,0 +1,362 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2021 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 + * + * http://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 org.wildfly.security.http.oidc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; +import static org.wildfly.security.http.oidc.Oidc.OIDC_NAME; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.AuthorizeCallback; + +import org.apache.http.HttpStatus; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.keycloak.representations.idm.RealmRepresentation; +import org.testcontainers.DockerClientFactory; +import org.wildfly.security.auth.callback.AuthenticationCompleteCallback; +import org.wildfly.security.auth.callback.EvidenceVerifyCallback; +import org.wildfly.security.auth.callback.IdentityCredentialCallback; +import org.wildfly.security.auth.callback.SecurityIdentityCallback; +import org.wildfly.security.auth.server.SecurityDomain; +import org.wildfly.security.evidence.Evidence; +import org.wildfly.security.http.HttpServerAuthenticationMechanism; +import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; +import org.wildfly.security.http.HttpServerCookie; +import org.wildfly.security.http.impl.AbstractBaseHttpTest; +import org.wildfly.security.json.util.JsonSerialization; + +import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; +import com.gargoylesoftware.htmlunit.TextPage; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlInput; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.javascript.SilentJavaScriptErrorListener; + +import io.restassured.RestAssured; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.QueueDispatcher; +import okhttp3.mockwebserver.RecordedRequest; + +/** + * Tests for the OpenID Connect authentication mechanism. + * + * @author Farah Juma + */ +public class OidcTest extends AbstractBaseHttpTest { + + public static final String CLIENT_ID = "test-webapp"; + public static final String CLIENT_SECRET = "secret"; + private static KeycloakContainer KEYCLOAK_CONTAINER; + private static final String TEST_REALM = "WildFly"; + private static final String KEYCLOAK_USERNAME = "username"; + private static final String KEYCLOAK_PASSWORD = "password"; + private static final String KEYCLOAK_LOGIN = "login"; + private static final int CLIENT_PORT = 5002; + private static final String CLIENT_APP = "clientApp"; + private static final String CLIENT_PAGE_TEXT = "Welcome page!"; + private static final String CLIENT_HOST_NAME = "localhost"; + private static MockWebServer client; // to simulate the application being secured + + protected HttpServerAuthenticationMechanismFactory oidcFactory; + + @BeforeClass + public static void startTestContainers() throws Exception { + assumeTrue("Docker isn't available, OIDC tests will be skipped", isDockerAvailable()); + KEYCLOAK_CONTAINER = new KeycloakContainer(); + KEYCLOAK_CONTAINER.start(); + sendRealmCreationRequest(KeycloakConfiguration.getRealmRepresentation(TEST_REALM, CLIENT_ID, CLIENT_SECRET, CLIENT_HOST_NAME, CLIENT_PORT, CLIENT_APP)); + client = new MockWebServer(); + client.start(CLIENT_PORT); + } + + private static Dispatcher createAppResponse(HttpServerAuthenticationMechanism mechanism, int expectedStatusCode, String expectedLocation, String clientPageText) { + return new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest recordedRequest) throws InterruptedException { + String path = recordedRequest.getPath(); + if (path.contains("/" + CLIENT_APP) && path.contains("&code=")) { + try { + TestingHttpServerRequest request = new TestingHttpServerRequest(null, + new URI(recordedRequest.getRequestUrl().toString()), recordedRequest.getHeader("Cookie")); + mechanism.evaluateRequest(request); + TestingHttpServerResponse response = request.getResponse(); + assertEquals(expectedStatusCode, response.getStatusCode()); + assertEquals(expectedLocation, response.getLocation()); + return new MockResponse().setBody(clientPageText); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return new MockResponse() + .setBody(""); + } + }; + } + + @AfterClass + public static void generalCleanup() throws Exception { + if (KEYCLOAK_CONTAINER != null) { + RestAssured + .given() + .auth().oauth2(KeycloakConfiguration.getAdminAccessToken(KEYCLOAK_CONTAINER.getAuthServerUrl())) + .when() + .delete(KEYCLOAK_CONTAINER.getAuthServerUrl() + "/admin/realms/" + TEST_REALM).then().statusCode(204); + KEYCLOAK_CONTAINER.stop(); + } + if (client != null) { + client.shutdown(); + } + } + + private static void sendRealmCreationRequest(RealmRepresentation realm) { + try { + RestAssured + .given() + .auth().oauth2(KeycloakConfiguration.getAdminAccessToken(KEYCLOAK_CONTAINER.getAuthServerUrl())) + .contentType("application/json") + .body(JsonSerialization.writeValueAsBytes(realm)) + .when() + .post(KEYCLOAK_CONTAINER.getAuthServerUrl() + "/admin/realms").then() + .statusCode(201); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test + public void testWrongPassword() throws Exception { + Map props = new HashMap<>(); + OidcClientConfiguration oidcClientConfiguration = OidcClientConfigurationBuilder.build(getOidcConfigurationInputStream()); + 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(); + assertEquals(HttpStatus.SC_MOVED_TEMPORARILY, response.getStatusCode()); + assertEquals(Status.NO_AUTH, request.getResult()); + + HtmlPage page = loginToKeycloak(KeycloakConfiguration.ALICE, "WRONG_PASSWORD", requestUri, response.getLocation(), response.getCookies()).click(); + assertTrue(page.getBody().asText().contains("Invalid username or password")); + } + + @Test + public void testWrongAuthServerUrl() throws Exception { + performAuthentication(getOidcConfigurationInputStream(CLIENT_SECRET, "http://fakeauthserver/auth"), KeycloakConfiguration.ALICE, + KeycloakConfiguration.ALICE_PASSWORD, false, -1, null, null); + } + + @Test + public void testWrongClientSecret() throws Exception { + performAuthentication(getOidcConfigurationInputStream("WRONG_CLIENT_SECRET"), KeycloakConfiguration.ALICE, + KeycloakConfiguration.ALICE_PASSWORD, true, HttpStatus.SC_FORBIDDEN, null,"Forbidden"); + } + + @Test(expected = RuntimeException.class) + public void testMissingRequiredConfigurationOption() { + OidcClientConfigurationBuilder.build(getOidcConfigurationMissingRequiredOption()); + } + + @Test + public void testSucessfulAuthenticationWithAuthServerUrl() throws Exception { + performAuthentication(getOidcConfigurationInputStream(), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + @Test + public void testSucessfulAuthenticationWithIssuerUrl() throws Exception { + performAuthentication(getOidcConfigurationInputStreamWithIssuerUrl(), KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, + true, HttpStatus.SC_MOVED_TEMPORARILY, getClientUrl(), CLIENT_PAGE_TEXT); + } + + private void performAuthentication(InputStream oidcConfig, String username, String password, boolean loginToKeycloak, + int expectedDispatcherStatusCode, String expectedLocation, String clientPageText) throws Exception { + try { + Map props = new HashMap<>(); + OidcClientConfiguration oidcClientConfiguration = OidcClientConfigurationBuilder.build(oidcConfig); + 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(); + 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)); + TextPage page = loginToKeycloak(username, password, requestUri, response.getLocation(), + response.getCookies()).click(); + assertTrue(page.getContent().contains(clientPageText)); + } + } finally { + client.setDispatcher(new QueueDispatcher()); + } + } + + private WebClient getWebClient() { + WebClient webClient = new WebClient(); + webClient.setCssErrorHandler(new SilentCssErrorHandler()); + webClient.setJavaScriptErrorListener(new SilentJavaScriptErrorListener()); + return webClient; + } + + private HtmlInput loginToKeycloak(String username, String password, URI requestUri, String location, List cookies) throws IOException { + WebClient webClient = getWebClient(); + if (cookies != null) { + for (HttpServerCookie cookie : cookies) { + webClient.addCookie(getCookieString(cookie), requestUri.toURL(), null); + } + } + HtmlPage keycloakLoginPage = webClient.getPage(location); + HtmlForm loginForm = keycloakLoginPage.getForms().get(0); + loginForm.getInputByName(KEYCLOAK_USERNAME).setValueAttribute(username); + loginForm.getInputByName(KEYCLOAK_PASSWORD).setValueAttribute(password); + return loginForm.getInputByName(KEYCLOAK_LOGIN); + } + + private InputStream getOidcConfigurationInputStream() { + return getOidcConfigurationInputStream(CLIENT_SECRET); + } + + private InputStream getOidcConfigurationInputStream(String clientSecret) { + return getOidcConfigurationInputStream(clientSecret, KEYCLOAK_CONTAINER.getAuthServerUrl()); + } + + private InputStream getOidcConfigurationInputStream(String clientSecret, String authServerUrl) { + String oidcConfig = "{\n" + + " \"realm\" : \"" + TEST_REALM + "\",\n" + + " \"resource\" : \"" + CLIENT_ID + "\",\n" + + " \"public-client\" : \"false\",\n" + + " \"auth-server-url\" : \"" + authServerUrl + "\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"credentials\" : {\n" + + " \"secret\" : \"" + clientSecret + "\"\n" + + " }\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } + + private InputStream getOidcConfigurationInputStreamWithIssuerUrl() { + String oidcConfig = "{\n" + + " \"resource\" : \"" + CLIENT_ID + "\",\n" + + " \"public-client\" : \"false\",\n" + + " \"issuer-url\" : \"" + KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM + "\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"credentials\" : {\n" + + " \"secret\" : \"" + CLIENT_SECRET + "\"\n" + + " }\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } + + private InputStream getOidcConfigurationMissingRequiredOption() { + String oidcConfig = "{\n" + + " \"public-client\" : \"false\",\n" + + " \"issuer-url\" : \"" + KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM + "\",\n" + + " \"ssl-required\" : \"EXTERNAL\",\n" + + " \"credentials\" : {\n" + + " \"secret\" : \"" + CLIENT_SECRET + "\"\n" + + " }\n" + + "}"; + return new ByteArrayInputStream(oidcConfig.getBytes(StandardCharsets.UTF_8)); + } + + private CallbackHandler getCallbackHandler() { + return callbacks -> { + for(Callback callback : callbacks) { + if (callback instanceof EvidenceVerifyCallback) { + Evidence evidence = ((EvidenceVerifyCallback) callback).getEvidence(); + ((EvidenceVerifyCallback) callback).setVerified(evidence.getDecodedPrincipal() != null); + } else if (callback instanceof AuthenticationCompleteCallback) { + // NO-OP + } else if (callback instanceof IdentityCredentialCallback) { + // NO-OP + } else if (callback instanceof AuthorizeCallback) { + ((AuthorizeCallback) callback).setAuthorized(true); + } else if (callback instanceof SecurityIdentityCallback) { + ((SecurityIdentityCallback) callback).setSecurityIdentity(SecurityDomain.builder().build().getCurrentSecurityIdentity()); + } else { + throw new UnsupportedCallbackException(callback); + } + } + }; + } + + private static boolean isDockerAvailable() { + try { + DockerClientFactory.instance().client(); + return true; + } catch (Throwable ex) { + return false; + } + } + + private String getCookieString(HttpServerCookie cookie) { + final StringBuilder header = new StringBuilder(cookie.getName()); + header.append("="); + if(cookie.getValue() != null) { + header.append(cookie.getValue()); + } + if (cookie.getPath() != null) { + header.append("; Path="); + header.append(cookie.getPath()); + } + if (cookie.getDomain() != null) { + header.append("; Domain="); + header.append(cookie.getDomain()); + } + if (cookie.isSecure()) { + header.append("; Secure"); + } + if (cookie.isHttpOnly()) { + header.append("; HttpOnly"); + } + if (cookie.getMaxAge() >= 0) { + header.append("; Max-Age="); + header.append(cookie.getMaxAge()); + } + return header.toString(); + } + + private static String getClientUrl() { + return "http://" + CLIENT_HOST_NAME + ":" + CLIENT_PORT + "/" + CLIENT_APP; + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 61b4c8b742a..b033aa042df 100644 --- a/pom.xml +++ b/pom.xml @@ -98,6 +98,10 @@ 5.4.1 3.0.0 0.7.5 + 1.15.3 + 15.0.2 + 4.3.3 + 2.40.0 INFO @@ -804,6 +808,12 @@ wildfly-elytron-tool ${project.version} + + org.wildfly.security + wildfly-elytron-tests + ${project.version} + test-jar + org.wildfly.security wildfly-elytron-tests-common @@ -1202,6 +1212,46 @@ ${version.org.mock-server.mockserver-netty} test + + io.rest-assured + rest-assured + ${version.io.rest-assured} + test + + + org.testcontainers + testcontainers + ${version.org.testcontainers.testcontainers} + test + + + org.keycloak + keycloak-admin-client + ${version.org.keycloak} + test + + + org.keycloak + keycloak-core + ${version.org.keycloak} + test + + + net.sourceforge.htmlunit + htmlunit + ${version.net.sourceforge.htmlunit.htmlunit} + test + + + org.apache.httpcomponents + httpmime + + + commons-logging + commons-logging + + + diff --git a/tests/base/pom.xml b/tests/base/pom.xml index c844b01b766..45f6e98aac4 100644 --- a/tests/base/pom.xml +++ b/tests/base/pom.xml @@ -236,6 +236,26 @@ + + + + test-jar + + + + + + + org.apache.maven.plugins + maven-source-plugin + ${version.source.plugin} + + + + test-jar + + + maven-surefire-plugin diff --git a/tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java b/tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java index bfc1f994ef8..68c37a81d21 100644 --- a/tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java +++ b/tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java @@ -20,6 +20,7 @@ import static org.wildfly.security.http.HttpConstants.AUTHENTICATION_INFO; import static org.wildfly.security.http.HttpConstants.AUTHORIZATION; +import static org.wildfly.security.http.HttpConstants.LOCATION; import static org.wildfly.security.http.HttpConstants.WWW_AUTHENTICATE; import java.io.InputStream; @@ -29,6 +30,7 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -107,18 +109,79 @@ protected enum Status { FAILED; } - protected class TestingHttpServerRequest implements HttpServerRequest { + protected static class TestingHttpServerRequest implements HttpServerRequest { private String[] authorization; private Status result; private HttpServerMechanismsResponder responder; private String remoteUser; + private URI requestURI; + private List cookies; public TestingHttpServerRequest(String[] authorization) { this.authorization = authorization; this.remoteUser = null; } + public TestingHttpServerRequest(String[] authorization, URI requestURI) { + this.authorization = authorization; + this.remoteUser = null; + this.requestURI = requestURI; + } + + public TestingHttpServerRequest(String[] authorization, URI requestURI, String cookie) { + this.authorization = authorization; + this.remoteUser = null; + this.requestURI = requestURI; + if (cookie != null) { + final String cookieName = cookie.substring(0, cookie.indexOf('=')); + final String cookieValue = cookie.substring(cookie.indexOf('=') + 1); + cookies = new ArrayList<>(); + cookies.add(new HttpServerCookie() { + @Override + public String getName() { + return cookieName; + } + + @Override + public String getValue() { + return cookieValue; + } + + @Override + public String getDomain() { + return null; + } + + @Override + public int getMaxAge() { + return -1; + } + + @Override + public String getPath() { + return "/"; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public boolean isHttpOnly() { + return true; + } + }); + } + } + + public Status getResult() { return result; } @@ -164,7 +227,8 @@ public void authenticationComplete(HttpServerMechanismsResponder responder) { } public void authenticationComplete(HttpServerMechanismsResponder responder, Runnable logoutHandler) { - throw new IllegalStateException(); + result = Status.COMPLETE; + this.responder = responder; } public void authenticationFailed(String message, HttpServerMechanismsResponder responder) { @@ -181,7 +245,7 @@ public String getRequestMethod() { } public URI getRequestURI() { - throw new IllegalStateException(); + return requestURI; } public String getRequestPath() { @@ -205,7 +269,7 @@ public String getFirstParameterValue(String name) { } public List getCookies() { - throw new IllegalStateException(); + return cookies; } public InputStream getInputStream() { @@ -213,11 +277,11 @@ public InputStream getInputStream() { } public InetSocketAddress getSourceAddress() { - throw new IllegalStateException(); + return null; } public boolean suspendRequest() { - throw new IllegalStateException(); + return true; } public boolean resumeRequest() { @@ -225,7 +289,39 @@ public boolean resumeRequest() { } public HttpScope getScope(Scope scope) { - throw new IllegalStateException(); + return new HttpScope() { + + @Override + public boolean exists() { + return true; + } + + @Override + public boolean create() { + return false; + } + + @Override + public boolean supportsAttachments() { + return true; + } + + @Override + public boolean supportsInvalidation() { + return false; + } + + @Override + public void setAttachment(String key, Object value) { + // no-op + } + + @Override + public Object getAttachment(String key) { + return null; + } + + }; } public Collection getScopeIds(Scope scope) { @@ -246,10 +342,12 @@ public String getRemoteUser() { } } - protected class TestingHttpServerResponse implements HttpServerResponse { + protected static class TestingHttpServerResponse implements HttpServerResponse { private int statusCode; private String authenticate; + private String location; + private List cookies; public void setStatusCode(int statusCode) { this.statusCode = statusCode; @@ -262,8 +360,8 @@ public int getStatusCode() { public void addResponseHeader(String headerName, String headerValue) { if (WWW_AUTHENTICATE.equals(headerName)) { authenticate = headerValue; - } else { - throw new IllegalStateException(); + } else if (LOCATION.equals(headerName)) { + location = headerValue; } } @@ -271,8 +369,19 @@ public String getAuthenticateHeader() { return authenticate; } + public String getLocation() { + return location; + } + + public List getCookies() { + return cookies; + } + public void setResponseCookie(HttpServerCookie cookie) { - throw new IllegalStateException(); + if (cookies == null) { + cookies = new ArrayList<>(); + } + cookies.add(cookie); } public OutputStream getOutputStream() {