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:
+ *
+ * - Two realm roles ("admin", "user")
+ * - Two users:
-
+ *
+ * - user named alice and password alice123+ with "admin" and "user" role
+ * - user named bob and password bob123+ with "user" role
+ *
+ *
+ */
+ 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() {