Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract out environment verification logic and add optional Spring Security component #8

Merged
merged 21 commits into from
Jan 4, 2018
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>4.2.3.RELEASE</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
Expand Down
97 changes: 19 additions & 78 deletions src/main/java/org/esbtools/auth/jboss/CertLdapLoginModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,28 @@
*/
package org.esbtools.auth.jboss;

import java.security.Principal;
import java.security.acl.Group;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;

import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginException;

import org.apache.commons.lang.StringUtils;
import org.esbtools.auth.ldap.LdapConfiguration;
import org.esbtools.auth.ldap.LdapRolesProvider;
import org.esbtools.auth.util.CachedRolesProvider;
import org.esbtools.auth.util.Environment;
import org.esbtools.auth.util.RolesCache;
import org.esbtools.auth.util.RolesProvider;
import org.apache.commons.lang.StringUtils;
import org.jboss.security.SimpleGroup;
import org.jboss.security.auth.spi.BaseCertLoginModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.naming.NamingException;
import javax.naming.directory.NoSuchAttributeException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginException;
import java.security.Principal;
import java.security.acl.Group;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;

public class CertLdapLoginModule extends BaseCertLoginModule {

private final Logger LOGGER = LoggerFactory.getLogger(CertLdapLoginModule.class);
Expand Down Expand Up @@ -75,14 +72,8 @@ public class CertLdapLoginModule extends BaseCertLoginModule {

public static final String UID = "uid";
public static final String CN = "cn";
public static final String LOCATION = "l";
public static final String OU = "ou";

public static final String ENVIRONMENT_SEPARATOR= ",";

private static String environment;
private static String allAccessOu;

private static volatile Environment envUtils;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably want to rename variable too to remove the Utils from the name

private static volatile RolesProvider rolesProvider = null;

@Override
Expand All @@ -97,8 +88,9 @@ public void initializeRolesProvider() throws Exception {
if (rolesProvider == null) {
synchronized(LdapRolesProvider.class) {
if (rolesProvider == null) {
environment = (String) options.get(ENVIRONMENT);
allAccessOu = (String) options.get(ALL_ACCESS_OU);
String environment = (String) options.get(ENVIRONMENT);
String allAccessOu = (String) options.get(ALL_ACCESS_OU);
envUtils = new Environment(environment, allAccessOu);

LdapConfiguration ldapConf = new LdapConfiguration();
ldapConf.server((String) options.get(SERVER));
Expand Down Expand Up @@ -160,13 +152,13 @@ protected Group[] getRoleSets() throws LoginException {
LOGGER.debug("Certificate principal:" + certPrincipal);

//first try getting search name from uid in certificate principle (new certificates)
String searchName = getLDAPAttribute(certPrincipal, UID);
String searchName = envUtils.getLDAPAttribute(certPrincipal, UID);
if(StringUtils.isNotBlank(searchName)) {
//only try to validate environment if it is a certificate that contains uid
validateEnvironment(certPrincipal);
envUtils.validate(certPrincipal);
} else {
// fallback to getting search name from cn in certificate principle (legacy certificates)
searchName = getLDAPAttribute(certPrincipal, CN);
searchName = envUtils.getLDAPAttribute(certPrincipal, CN);
}

Collection<String> groupNames = rolesProvider.getUserRoles(searchName);
Expand All @@ -193,55 +185,4 @@ protected Group[] getRoleSets() throws LoginException {
return roleSets;
}

private void validateEnvironment(String certificatePrincipal) throws NamingException {

String ou = getLDAPAttribute(certificatePrincipal, OU);
LOGGER.debug("OU from certificate: ", ou);
String location = getLDAPAttribute(certificatePrincipal, LOCATION);
LOGGER.debug("Location from certificate: ", location);

if(StringUtils.isBlank(ou)) {
throw new NoSuchAttributeException("No ou in dn, you may need to update your certificate: " + certificatePrincipal);
} else {
if(allAccessOu.equalsIgnoreCase(StringUtils.replace(ou, " ", ""))){
LOGGER.debug("Skipping environment validation, user ou matches {} ", allAccessOu);
} else {
//if dn not from allAccessOu, verify the location (l) field
//in the cert matches the configured environment
if(StringUtils.isBlank(location)) {
throw new NoSuchAttributeException("No location in dn, you may need to update your certificate: " + certificatePrincipal);
} else if(!locationMatchesEnvironment(location)){
throw new NoSuchAttributeException("Invalid location from dn, expected " + environment + " but found l=" + location);
}
}
}
}

private String getLDAPAttribute(String certificatePrincipal, String searchAttribute) throws NamingException {
String searchName = new String();
LdapName name = new LdapName(certificatePrincipal);
for (Rdn rdn : name.getRdns()) {
if (rdn.getType().equalsIgnoreCase(searchAttribute)) {
searchName = (String) rdn.getValue();
break;
}
}
return searchName;
}

private boolean locationMatchesEnvironment(String location) {
List<String> environments;
if(environment.contains(ENVIRONMENT_SEPARATOR)) {
environments = Arrays.asList(environment.split(ENVIRONMENT_SEPARATOR));

} else {
environments = Arrays.asList(new String[] {environment});
}
for(String environment : environments) {
if(environment.equalsIgnoreCase(location)) {
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.esbtools.auth.spring;

import org.springframework.security.core.AuthenticationException;

public class CertEnvironmentAuthenticationException extends AuthenticationException {

private static final long serialVersionUID = -1102864286332227011L;

public CertEnvironmentAuthenticationException(String msg) {
super(msg);
}

public CertEnvironmentAuthenticationException(String msg, Throwable t) {
super(msg, t);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.esbtools.auth.spring;

import java.io.IOException;
import java.security.cert.X509Certificate;

import javax.naming.NamingException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.esbtools.auth.util.Environment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.WebAttributes;
import org.springframework.web.filter.OncePerRequestFilter;

public class CertEnvironmentVerificationFilter extends OncePerRequestFilter {

private static final Logger LOGGER = LoggerFactory.getLogger(CertEnvironmentVerificationFilter.class);

private final Environment envUtils;

public CertEnvironmentVerificationFilter(String environment) {
envUtils = (null == environment) ? null : new Environment(environment);

LOGGER.info("Cert Environment: " + ((environment == null) ? "Not Set" : environment));
}

@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if (null != envUtils) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also want to check if what is configured is not an empty string (i.e. ""). In the past an empty environment signified you could skip environment validation. I think that the way this is currently written, an empty environment config would still be validated (because it is not null).

We could also choose to interpret null for the environment configuration as "doesn't need to be validated", we just need to be consistent and allow for that in CertLoginLdapLoginModule (currently requires environment to be present in config) and whatever else uses this.

We could instead a method to the Environment class called requiresValidation() that returns false when the value is null or empty string, and true otherwise (I think I like this option the best). We could also check the allAccessOu value in that method too since that is another factor in determining whether or not to do validation logic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also want to check if what is configured is not an empty string (i.e. "")

Do we need to handle blank spaces also? eg. " "

We could also choose to interpret null for the environment configuration as "doesn't need to be validated"

That is exactly what CertEnvironmentVerificationFilter does, it just handles it at that layer instead of in Environment. I did it at that level because I didn't have enough context to make changes to the logic flow of CertLdapLoginModule.

If we want this to be default behaviour, then it can definitely be moved to the Environment / requiresValidation.

LOGGER.debug("Attempting Environment Cert verification");
X509Certificate certChain[] = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");

if ((null != certChain) && (certChain.length > 0)) {
LOGGER.debug("Verifying environment on cert");
String dn = certChain[0].getSubjectDN().getName();
try {
envUtils.validate(dn);
}
catch (NamingException e) {
unsuccessfulAuthentication(request, response,
new CertEnvironmentAuthenticationException(e.getMessage()));
return; //end the chain
}
}
else {
LOGGER.debug("Cert not found. Skipping Environment Cert verification.");
}
}
else {
LOGGER.debug("No environment configured. Skipping Environment Cert verification.");
}

chain.doFilter(request, response);
}

/**
* Ensures the authentication object in the secure context is set to null when
* authentication fails.
* <p>
* Caches the failure exception as a request attribute
*/
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();

if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Cleared security context due to exception", failed);
}
request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, failed);

response.sendError(HttpServletResponse.SC_UNAUTHORIZED, failed.getMessage());
}

}
67 changes: 67 additions & 0 deletions src/main/java/org/esbtools/auth/spring/LdapUserDetailsService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.esbtools.auth.spring;

import java.util.stream.Collectors;

import org.esbtools.auth.ldap.LdapConfiguration;
import org.esbtools.auth.ldap.LdapRolesProvider;
import org.esbtools.auth.util.CachedRolesProvider;
import org.esbtools.auth.util.RolesCache;
import org.esbtools.auth.util.RolesProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;

public class LdapUserDetailsService implements UserDetailsService, AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {

private static final Logger LOGGER = LoggerFactory.getLogger(LdapUserDetailsService.class);

private final RolesProvider rolesProvider;

public LdapUserDetailsService(String searchBase, LdapConfiguration ldapConfiguration, int rolesCacheExpiryMS) throws Exception {
this(new LdapRolesProvider(searchBase, ldapConfiguration), rolesCacheExpiryMS);
}

public LdapUserDetailsService(String searchBase, LdapConfiguration ldapConfiguration) throws Exception {
this(new LdapRolesProvider(searchBase, ldapConfiguration));
}

public LdapUserDetailsService(LdapRolesProvider rolesProvider, int rolesCacheExpiryMS) {
this(new CachedRolesProvider(rolesProvider, new RolesCache(rolesCacheExpiryMS)));
}

public LdapUserDetailsService(RolesProvider rolesProvider) {
this.rolesProvider = rolesProvider;
}

@Override
public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) throws UsernameNotFoundException {
return loadUserByUsername(token.getName());
}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LOGGER.debug("Using LdapUserDetailsService with principal: " + username);

try {
return new User(
username,
"no-password",
rolesProvider.getUserRoles(username).stream()
.map(n -> new SimpleGrantedAuthority(n))
.collect(Collectors.toList())
);
}
catch (Exception e) {
LOGGER.error("Unable to check ldap for unknown reason.", e);

throw new UsernameNotFoundException(username + " could not be authorized");
}
}

}
Loading