Skip to content

Commit

Permalink
Merge pull request #248 from Skyllarr/WLYWEB-237
Browse files Browse the repository at this point in the history
[ELYWEB-237] [ELYWEB-222] Upgrade Elytron to 2.2.4.Final and add a test for single sign on across two apps with logout
  • Loading branch information
Skyllarr authored Mar 15, 2024
2 parents 9d6d11c + b126ca2 commit d122e71
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 8 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@

<properties>
<version.io.undertow>2.3.7.Final</version.io.undertow>
<version.org.wildfly.security.elytron>2.2.1.Final</version.org.wildfly.security.elytron>
<version.org.wildfly.security.elytron>2.2.4.Final</version.org.wildfly.security.elytron>
<version.org.wildfly.common>1.5.4.Final</version.org.wildfly.common>
<version.org.wildfly.security.jakarta.elytron-ee>3.0.1.Final</version.org.wildfly.security.jakarta.elytron-ee>
<version.org.apache.httpcomponents.httpclient>4.5.13</version.org.apache.httpcomponents.httpclient>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ protected void assertSuccessfulUnconstraintResponse(HttpResponse result, String
}
}

protected void assertLoginPage(HttpResponse response) throws Exception {
protected static void assertLoginPage(HttpResponse response) throws Exception {
assertTrue(EntityUtils.toString(response.getEntity()).contains("Login Page"));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,7 @@ public void forceShutdown() {
after();
}

public String getContextRoot() {
return contextRoot;
}
}
12 changes: 11 additions & 1 deletion undertow-servlet/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,17 @@
<artifactId>xnio-nio</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wildfly.security</groupId>
<artifactId>wildfly-elytron-http-sso</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

<repositories>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
/*
* Copyright 2024 Red Hat, Inc.
*
* 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.elytron.web.undertow.server.servlet;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.wildfly.security.password.interfaces.ClearPassword.ALGORITHM_CLEAR;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.message.BasicNameValuePair;
import org.infinispan.Cache;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
import org.junit.Rule;
import org.junit.Test;
import org.wildfly.elytron.web.undertow.common.AbstractHttpServerMechanismTest;
import org.wildfly.elytron.web.undertow.common.UndertowServer;
import org.wildfly.elytron.web.undertow.server.servlet.util.UndertowServletServer;
import org.wildfly.security.auth.SupportLevel;
import org.wildfly.security.auth.permission.LoginPermission;
import org.wildfly.security.auth.realm.SimpleMapBackedSecurityRealm;
import org.wildfly.security.auth.realm.SimpleRealmEntry;
import org.wildfly.security.auth.server.RealmIdentity;
import org.wildfly.security.auth.server.RealmUnavailableException;
import org.wildfly.security.auth.server.SecurityDomain;
import org.wildfly.security.auth.server.SecurityRealm;
import org.wildfly.security.credential.Credential;
import org.wildfly.security.credential.PasswordCredential;
import org.wildfly.security.evidence.Evidence;
import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory;
import org.wildfly.security.http.util.sso.DefaultSingleSignOnManager;
import org.wildfly.security.http.util.sso.DefaultSingleSignOnSessionFactory;
import org.wildfly.security.http.util.sso.DefaultSingleSignOnSessionIdentifierFactory;
import org.wildfly.security.http.util.sso.SingleSignOnEntry;
import org.wildfly.security.http.util.sso.SingleSignOnManager;
import org.wildfly.security.http.util.sso.SingleSignOnServerMechanismFactory;
import org.wildfly.security.http.util.sso.SingleSignOnSessionFactory;
import org.wildfly.security.password.PasswordFactory;
import org.wildfly.security.password.spec.ClearPasswordSpec;
import org.wildfly.security.permission.PermissionVerifier;

/**
* Test case to test HTTP FORM authentication where authentication is backed by Elytron and session replication is enabled.
*
* @author <a href="mailto:fjuma@redhat.com">Farah Juma</a>
*/
public class FormServletAuthenticationWithClusteredSSOTest extends AbstractHttpServerMechanismTest {

private Supplier<KeyPair> keyPairSupplier;
private AtomicInteger realmIdentityInvocationCount = new AtomicInteger(0);

@Rule
public final UndertowServer serverA = createUndertowServer(7776);

@Rule
public final UndertowServer serverB = createUndertowServer(7777);

public FormServletAuthenticationWithClusteredSSOTest() throws Exception {
}

@Override
protected String getMechanismName() {
return "FORM";
}

@Override
protected SecurityDomain doCreateSecurityDomain() throws Exception {
PasswordFactory passwordFactory = PasswordFactory.getInstance(ALGORITHM_CLEAR);
Map<String, SimpleRealmEntry> passwordMap = new HashMap<>();

passwordMap.put("ladybird",
new SimpleRealmEntry(Collections.singletonList(new PasswordCredential(passwordFactory.generatePassword(new ClearPasswordSpec("Coleoptera".toCharArray()))))));

SimpleMapBackedSecurityRealm delegate = new SimpleMapBackedSecurityRealm();

delegate.setPasswordMap(passwordMap);

SecurityRealm securityRealm = new SecurityRealm() {

@Override
public RealmIdentity getRealmIdentity(Principal principal) throws RealmUnavailableException {
realmIdentityInvocationCount.incrementAndGet();
return delegate.getRealmIdentity(principal);
}

@Override
public SupportLevel getCredentialAcquireSupport(Class<? extends Credential> credentialType, String algorithmName,
AlgorithmParameterSpec algorithmParameterSpec) throws RealmUnavailableException {
return delegate.getCredentialAcquireSupport(credentialType, algorithmName, algorithmParameterSpec);
}

@Override
public SupportLevel getEvidenceVerifySupport(Class<? extends Evidence> evidenceType,
String algorithmName) throws RealmUnavailableException {
return delegate.getEvidenceVerifySupport(evidenceType, algorithmName);
}
};

SecurityDomain.Builder builder = SecurityDomain.builder()
.setDefaultRealmName("TestRealm");

builder.addRealm("TestRealm", securityRealm).build();
builder.setPermissionMapper((principal, roles) -> PermissionVerifier.from(new LoginPermission()));

return builder.build();
}

private UndertowServer createUndertowServer(int port) throws Exception {
return UndertowServletServer.builder()
.setAuthenticationMechanism(getMechanismName())
.setSecurityDomain(getSecurityDomain())
.setPort(port)
.setContextRoot("/" + port)
.setDeploymentName(String.valueOf(port))
.setHttpServerAuthenticationMechanismFactory(getHttpServerAuthenticationMechanismFactory(Collections.emptyMap()))
.build();
}

@Override
protected HttpServerAuthenticationMechanismFactory getHttpServerAuthenticationMechanismFactory(Map<String, ?> properties) {
HttpServerAuthenticationMechanismFactory delegate = super.getHttpServerAuthenticationMechanismFactory(properties);

String cacheManagerName = UUID.randomUUID().toString();
EmbeddedCacheManager cacheManager = new DefaultCacheManager(
GlobalConfigurationBuilder.defaultClusteredBuilder()
.globalJmxStatistics().cacheManagerName(cacheManagerName)
.transport().nodeName(cacheManagerName).addProperty(JGroupsTransport.CONFIGURATION_FILE, "fast.xml")
.build(),
new ConfigurationBuilder()
.clustering()
.cacheMode(CacheMode.REPL_SYNC)
.build()
);

Cache<String, SingleSignOnEntry> cache = cacheManager.getCache();
SingleSignOnManager manager = new DefaultSingleSignOnManager(cache, new DefaultSingleSignOnSessionIdentifierFactory(), (id, entry) -> cache.put(id, entry));
SingleSignOnServerMechanismFactory.SingleSignOnConfiguration signOnConfiguration =
new SingleSignOnServerMechanismFactory.SingleSignOnConfiguration("JSESSIONSSOID", null,
"/", false, false);

if (keyPairSupplier == null) {
keyPairSupplier = new KeyPairSupplier();
}
SingleSignOnSessionFactory singleSignOnSessionFactory = new DefaultSingleSignOnSessionFactory(manager, keyPairSupplier.get());

return new SingleSignOnServerMechanismFactory(delegate, singleSignOnSessionFactory, signOnConfiguration);
}

@Test
public void testSingleSignOnAcrossTwoAppsWithLogout() throws Exception {
BasicCookieStore cookieStore = new BasicCookieStore();
HttpClient httpClient = HttpClientBuilder.create()
.setDefaultCookieStore(cookieStore)
.setRedirectStrategy(new LaxRedirectStrategy())
.build();

assertLoginPage(httpClient.execute(new HttpGet(serverA.createUri())));

assertFalse(cookieStore.getCookies().stream().filter(cookie -> cookie.getName().equals("JSESSIONSSOID")).findAny().isPresent());

// log into APP_A
HttpResponse execute = loginToApp(httpClient, serverA, "ladybird", "Coleoptera");
assertTrue(cookieStore.getCookies().stream().filter(cookie -> cookie.getName().equals("JSESSIONSSOID")).findAny().isPresent());
assertSuccessfulResponse(execute, "ladybird");
String appOneSessionId = getSessionIdForApp(cookieStore, serverA);

// can now access APP_B without logging in again
assertSuccessfulResponse(httpClient.execute(new HttpGet(serverB.createUri())), "ladybird");
String appTwoSessionId = getSessionIdForApp(cookieStore, serverB);

// log out of APP_A
httpClient.execute(new HttpGet(serverA.createUri("/logout")));

// log into APP_A again
execute = loginToApp(httpClient, serverA, "ladybird", "Coleoptera");
assertTrue(cookieStore.getCookies().stream().filter(cookie -> cookie.getName().equals("JSESSIONSSOID")).findAny().isPresent());
assertSuccessfulResponse(execute, "ladybird");
String appOneNewSessionId = getSessionIdForApp(cookieStore, serverA);

// the session ID for APP_A should now be different from the initial session ID
assertTrue(appOneSessionId != null && appOneNewSessionId != null && ! appOneSessionId.equals(appOneNewSessionId));

// access APP_B without logging in again
assertSuccessfulResponse(httpClient.execute(new HttpGet(serverB.createUri())), "ladybird");
String appTwoNewSessionId = getSessionIdForApp(cookieStore, serverB);

// the session ID for APP_B should now be different from the initial session ID
assertTrue(appTwoSessionId != null && appTwoNewSessionId != null && ! appTwoSessionId.equals(appTwoNewSessionId));
}

private static HttpResponse loginToApp(HttpClient httpClient, UndertowServer server, String username, String password) throws Exception {
assertLoginPage(httpClient.execute(new HttpGet(server.createUri())));
HttpPost httpAuthenticate = new HttpPost(server.createUri("/j_security_check"));
List<NameValuePair> parameters = new ArrayList<>(2);
parameters.add(new BasicNameValuePair("j_username", "ladybird"));
parameters.add(new BasicNameValuePair("j_password", "Coleoptera"));
httpAuthenticate.setEntity(new UrlEncodedFormEntity(parameters));
return httpClient.execute(httpAuthenticate);
}

private static String getSessionIdForApp(BasicCookieStore cookieStore, UndertowServer server) {
return cookieStore.getCookies().stream().filter(cookie -> cookie.getName().equals("JSESSIONID")
&& cookie.getPath().equals(server.getContextRoot())).findAny().get().getValue();
}

class KeyPairSupplier implements Supplier<KeyPair> {

private final KeyPair keyPair;

KeyPairSupplier() {
try {
this.keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException();
}
}

@Override
public KeyPair get() {
return this.keyPair;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -52,23 +52,25 @@ public class UndertowServletServer extends UndertowServer {
private final SecurityDomain securityDomain;
private final HttpServerAuthenticationMechanismFactory httpServerAuthenticationMechanismFactory;
private final String authenticationMechanism;
private String deploymentName;

private Undertow undertowServer;

protected UndertowServletServer(Supplier<SSLContext> serverSslContext, int port, String contextRoot, final String authenticationMechanism,
final SecurityDomain securityDomain, final HttpServerAuthenticationMechanismFactory httpServerAuthenticationMechanismFactory) {
final SecurityDomain securityDomain, final HttpServerAuthenticationMechanismFactory httpServerAuthenticationMechanismFactory, final String deploymentName) {
super(serverSslContext, port, contextRoot, SERVLET);
this.authenticationMechanism = authenticationMechanism;
this.securityDomain = securityDomain;
this.httpServerAuthenticationMechanismFactory = httpServerAuthenticationMechanismFactory;
this.deploymentName = deploymentName;
}

@Override
protected void before() throws Throwable {
DeploymentInfo deploymentInfo = Servlets.deployment()
.setClassLoader(TestServlet.class.getClassLoader())
.setContextPath(contextRoot)
.setDeploymentName("helloworld.war")
.setDeploymentName(deploymentName)
.setLoginConfig(new LoginConfig(authenticationMechanism, "Elytron Realm", "/login", "/error"))
.addSecurityConstraint(new SecurityConstraint()
.addWebResourceCollection(new WebResourceCollection()
Expand Down Expand Up @@ -102,8 +104,8 @@ protected void before() throws Throwable {
DeploymentManager deployManager = Servlets.defaultContainer().addDeployment(deploymentInfo);
deployManager.deploy();

PathHandler path = Handlers.path(Handlers.redirect(CONTEXT_ROOT))
.addPrefixPath(CONTEXT_ROOT, deployManager.start());
PathHandler path = Handlers.path(Handlers.redirect(contextRoot))
.addPrefixPath(contextRoot, deployManager.start());

Undertow.Builder undertowBuilder = Undertow.builder()
.setHandler(path);
Expand Down Expand Up @@ -137,6 +139,7 @@ public static class Builder {
private int port = 7776;
private Supplier<SSLContext> serverSslContext;
private HttpServerAuthenticationMechanismFactory httpServerAuthenticationMechanismFactory;
String deploymentName = "helloworld.war";

public Builder setAuthenticationMechanism(final String authenticationMechanism) {
this.authenticationMechanism = authenticationMechanism;
Expand Down Expand Up @@ -180,8 +183,13 @@ public Builder setHttpServerAuthenticationMechanismFactory(final HttpServerAuthe
return this;
}

public Builder setDeploymentName(final String deploymentName) {
this.deploymentName = deploymentName;
return this;
}

public UndertowServer build() throws Exception {
return new UndertowServletServer(serverSslContext, port, contextRoot, authenticationMechanism, securityDomain, httpServerAuthenticationMechanismFactory);
return new UndertowServletServer(serverSslContext, port, contextRoot, authenticationMechanism, securityDomain, httpServerAuthenticationMechanismFactory, deploymentName);
}


Expand Down

0 comments on commit d122e71

Please sign in to comment.