Reputation: 269
The actual Spring Security configuration is like this:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/uri1/**").hasAuthority(Permission.AUTHORITY1.toString())
.antMatchers("/uri2/**").hasAuthority(Permission.AUTHORITY2.toString())
.anyRequest().hasAuthority(Permission.AUTHORITY3.toString())
.and().httpBasic()
.realmName("App").and().csrf().disable();
http.authorizeRequests();
http.headers().frameOptions().sameOrigin().cacheControl().disable();
}
@Bean
public Filter shallowEtagHeaderFilter() {
return new ShallowEtagHeaderFilter();
}
}
And the web MVC configuration is like this:
@Configuration
public class DefaultView extends WebMvcConfigurerAdapter{
@Override
public void addViewControllers( ViewControllerRegistry registry ) {
registry.addViewController( "/" ).setViewName( "forward:myPage.html" );
registry.setOrder( Ordered.HIGHEST_PRECEDENCE);
super.addViewControllers( registry );
}
}
I have to replace the httpBasic authentification done in Spring Security by an authentification using onelogin (so with SAML if I understood what I found on the Internet).
By doing research, I found that a possibility was to use Shibboleth on the Apache server and an other was to use a plugin in Spring Security to manage SAML.
For the first solution (shibboleth), the aim is to manage onelogin authentification directly on Apache server (if not connected, the user is redirected on onelogin authentification page, if connected, the ressource is accessible) and to have needed informations returned in SAML response (like username and other need data) in the header of the request (to be abble to have them in Spring app).
With this solution, is it possible to keep httpBasic authentification in Spring security and to have "Basic XXXX" in the header of each request set by Shibboleth? Or, have I to remove the httpBasic authentification from Spring Security?
For the second solution (plugin to manage SAML in Spring Security), is it the same result as the first solution and how it must be implemented?
Thank you in advance for your reply.
Upvotes: 2
Views: 1985
Reputation: 269
This is the entire solution I used to connect to my application using Onelogin, shibboleth (Apache) and Spring Security. I used http but you have to adapt if you want to use https.
Configure a "SAML Test Connector (SP Shibboleth)" with the following configuration:
Login URL : http://myserver:<port>/my-app
ACS (Consumer) URL : http://myserver:<port>/Shibboleth.sso/SAML2/POST
SAML Recipient : http://myserver:<port>/Shibboleth.sso/SAML2/POST
SAML Single Logout URL : http://myserver:<port>/Shibboleth.sso/Logout
ACS (Consumer) URL Validator : ^http://myserver:<port>/Shibboleth.sso/SAML2/POST$
Audience : http://myserver:<port>/my-app
An parameter "username" has been added and a value is defined for this parameter for each user.
See: https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPJavaInstall
I installed shibboleth.
I activated AJP (module mod_proxy_ajp). It is recommended to use AJP instead of HTTP request headers.
I updated my apache conf file:
<VirtualHost *:[port]>
...
ProxyIOBufferSize 65536
<location /my-app >
ProxyPass "ajp://myappserver:<portAJPApp>"
AuthType shibboleth
ShibRequestSetting requireSession 1
Require valid-user
ProxyPassReverse /
ProxyHTMLEnable On
ProxyHTMLURLMap http://myappserver:<portHttpApp>/ /my-app/
ProxyHTMLURLMap / /my-app/
</location>
<Location /Shibboleth.sso>
SetHandler shib
</Location>
...
</VirtualHost>
In shibboleth2.xml:
<SPConfig xmlns="urn:mace:shibboleth:2.0:native:sp:config"
xmlns:conf="urn:mace:shibboleth:2.0:native:sp:config"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
clockSkew="180">
...
<ApplicationDefaults id="default" policyId="default"
entityID="http://myserver:<port>/my-app"
REMOTE_USER="eppn persistent-id targeted-id"
signing="false" encryption="false"
attributePrefix="AJP_">
<!-- entityId in IdP metadata file -->
<SSO entityID="https://app.onelogin.com/saml/metadata/XXXX">
SAML2
</SSO>
<MetadataProvider type="XML"
uri="https://app.onelogin.com/saml/metadata/XXX"
backingFilePath="onelogin_metadata.xml" reloadInterval="7200">
</MetadataProvider>
</ApplicationDefaults>
...
</SPConfig>
In attribute-map.xml:
<Attributes xmlns="urn:mace:shibboleth:2.0:attribute-map" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
...
<!-- OneLogin attributes: "name" corresponds to the attribute name defined in Onelogin and received in SAML response. "id" is the name of the attribute in shibboleth session accissible by http://myserver:<port>/Shibboleth.sso/Session -->
<Attribute name="username" nameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic" id="username">
<AttributeDecoder xsi:type="StringAttributeDecoder"/>
</Attribute>
...
</Attributes>
Tomcat configuration to add an AJP connector (attributes loaded from yml with a "server" property):
@Configuration
@ConfigurationProperties(prefix = "server")
public class TomcatConfiguration {
private int ajpPort;
private boolean ajpAllowTrace;
private boolean ajpSecure;
private String ajpScheme;
private boolean ajpEnabled;
@Bean
public EmbeddedServletContainerCustomizer customizer() {
return container -> {
if (container instanceof TomcatEmbeddedServletContainerFactory) {
TomcatEmbeddedServletContainerFactory tomcatServletFactory = ((TomcatEmbeddedServletContainerFactory) container);
...
// New connector for AJP
// Doc: http://tomcat.apache.org/tomcat-7.0-doc/config/ajp.html
if (isAjpEnabled()) {
Connector ajpConnector = new Connector("AJP/1.3");
ajpConnector.setPort(getAjpPort());
ajpConnector.setSecure(isAjpSecure());
ajpConnector.setAllowTrace(isAjpAllowTrace());
ajpConnector.setScheme(getAjpScheme());
ajpConnector.setAttribute("packetSize", 65536);
tomcatServletFactory.addAdditionalTomcatConnectors(ajpConnector);
}
}
};
}
// Getters and setters
}
Spring security configuration (the shibboleth filter can be activated through yml with a "shibboleth-filter" property defined in an "authentication" property):
@Configuration
@ConfigurationProperties(prefix = "authentication")
@EnableWebSecurity
@Import(ShibbolethFilterRegistrar.class)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private boolean shibbolethFilter;
@Autowired
private ShibbolethAuthFilter shibbolethAuthFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
if(isShibbolethFilter()) {
http.addFilterBefore(shibbolethAuthFilter, AbstractPreAuthenticatedProcessingFilter.class)
.authorizeRequests()
.antMatchers("/uri1/**").hasAuthority(Permission.AUTHORITY1.toString())
.antMatchers("/uri2/**").hasAuthority(Permission.AUTHORITY2.toString())
.anyRequest().hasAuthority(Permission.AUTHORITY3.toString())
.and().csrf().disable();
http.authorizeRequests();
http.headers().frameOptions().sameOrigin().cacheControl().disable();
}
else {
http
.authorizeRequests()
.antMatchers("/uri1/**").hasAuthority(Permission.AUTHORITY1.toString())
.antMatchers("/uri2/**").hasAuthority(Permission.AUTHORITY2.toString())
.anyRequest().hasAuthority(Permission.AUTHORITY3.toString())
.and().httpBasic()
.realmName("MyApp")
.and().csrf().disable();
http.authorizeRequests();
http.headers().frameOptions().sameOrigin().cacheControl().disable();
}
}
// Getter and setter for shibbolethFilter loaded from yml
}
ShibbolethFilterRegistrar:
@Configuration
public class ShibbolethFilterRegistrar {
@Bean
public ShibbolethAuthenticationManager shibbolethAuthenticationManager() {
return new ShibbolethAuthenticationManager();
}
@Bean
public FilterRegistrationBean shibbolethFilterRegistration(ShibbolethAuthFilter shibbolethAuthFilter) {
FilterRegistrationBean registration = new FilterRegistrationBean(shibbolethAuthFilter);
registration.setEnabled(false);
return registration;
}
@Bean
public ShibbolethAuthFilter shibbolethAuthFilter() {
return new ShibbolethAuthFilter();
}
}
ShibbolethAuthFilter:
public class ShibbolethAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
private static final String USERNAME_ATTRIBUTE_NAME = "username";
private static final String VALID_SHIBBOLETH_ATTR = "_valid_shibboleth_attribute";
@Autowired
private ShibbolethAuthenticationManager shibbolethAuthenticationManager;
@Override
public void afterPropertiesSet() {
setAuthenticationManager(shibbolethAuthenticationManager);
super.afterPropertiesSet();
}
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
// Attribute received in AJP request
Object username = request.getAttribute(USERNAME_ATTRIBUTE_NAME);
if(username == null) {
return null;
}
request.setAttribute(VALID_SHIBBOLETH_ATTR, Boolean.TRUE);
ShibbolethAuthToken authToken = new ShibbolethAuthToken(username.toString());
return authToken;
}
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
if (Boolean.TRUE.equals(request.getAttribute(VALID_SHIBBOLETH_ATTR))) {
return System.currentTimeMillis(); // just returning non null value to satisfy spring security contract
}
logger.trace("Returning null Credentials for non authenticated request");
return null;
}
}
ShibbolethAuthenticationManager:
public class ShibbolethAuthenticationManager implements AuthenticationManager {
@Autowired
private MyAuthenticationProvider myAuthenticationProvider;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
ShibbolethAuthToken principal = (ShibbolethAuthToken) authentication.getPrincipal();
Object credentials = authentication.getCredentials();
UserDetails userDetails = myAuthenticationProvider.loadUserByUsername(principal.getName());
if(userDetails == null || userDetails.getAuthorities() == null || userDetails.getAuthorities().isEmpty()) {
throw new BadCredentialsException("User rights cannot be retrieved for user " + principal.getName());
}
return new PreAuthenticatedAuthenticationToken(principal, credentials, userDetails.getAuthorities());
}
}
ShibbolethAuthToken implements Principal.
Thank you for your help.
Upvotes: 2
Reputation: 1321
welcome to stackoverflow.
... and to have needed informations returned in SAML response (like username and other need data) in the header of the request (to be abble to have them in Spring app)
If I understood correctly, you are already using spring security. This means your application is already using spring security populated context for authentication and authorization in your controller/service layers. If you use said approach, where apache is populating the authenticate user information in headers, than this is NOT going to populate the spring security context all by itself UNLESS you add a preAuthFilter in your chain to extract this information and populate your spring context appropriately.
With this solution, is it possible to keep httpBasic authentification in Spring security and to have "Basic XXXX" in the header of each request set by Shibboleth? Or, have I to remove the httpBasic authentification from Spring Security?
If you are able to do it then what I said above would be a bit relaxed. Having said that, to best of my knowledge, there is no option where you can deduce a Basic authentication header using shibboleth apache module. In addition, I'll also advice to be careful with this approach since, with this approach, you'll still have to authenticate the user in your app with a dummy password (since you are NOT going to get user's correct password via SAML in this header) and this opens up your application for security exploits. I'll strongly advise against this approach. Shibboleth already has some Spoof Checking covered in their documentation.
[EDIT] Based on the additional information, following is what you can do to achieve all handling by apache and still use spring security effectively
First provide implementation of PreAuthenticatedAuthenticationToken in your application, you can use AbstractPreAuthenticatedProcessingFilter for this purpose. A skeleton for the implementation is provided below, this is excerpt from one of my past work and very much stripped down keeping only the essential elements which are relevant for your scenario. Also take a close look at AuthenticationManager and Authentication docs and make sure you fully understand what to use and for what purpose. Please read javadocs for all these 4 classes carefully to understand the contract as it can be confusing to get it right in spring security otherwise. I have added necessary details as TODO and comments in skeleton blow that you'll have to fill in yourself in your implementation.
public class ShibbolethAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
private final String containsValidPrincipalHeader = "_valid_shibboleth_header_present";
private final String shibbolethHeader = "_shibboleth_header";
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* This authentication manager's authenticate method MUST return a fully populated
* org.springframework.security.core.Authentication object. You may very well use
* either PreAuthenticatedAuthenticationToken OR UsernamePasswordAuthenticationToken
* with any credentials set, most important is to correctly populate the Authorities
* in the returned object so that hasAuthority checks works as expected.
*
* Another point, you can use authentication.getPrincipal() in the implementation
* of authenticate method to access the same principal object as returned by
* getPreAuthenticatedPrincipal method of this bean. So basically you pass the
* using Principal object from this bean to AuthenticationManager's authenticate
* method which in turn return a fully populated spring's Authentication object
* with fully populated Authorities.
*/
@Autowired
private ShibbolethAuthenticationManager authenticationManager;
@Override
public void afterPropertiesSet() {
setAuthenticationManager(authenticationManager);
super.afterPropertiesSet();
}
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
String authHeader = request.getHeader(shibbolethHeader);
if (authHeader == null) {
logger.trace("No {} header found, skipping Shibboleth Authentication", shibbolethHeader);
return null;
}
// TODO - validate if all header and it's contents are what they should be
ShibbolethAuthToken authToken = /* TODO - provide your own impl to supply java.security.Principal object here */;
request.setAttribute(containsValidPrincipalHeader, Boolean.TRUE);
return authToken;
}
/**
* No password required thus Credentials will return null
*/
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
if (Boolean.TRUE.equals(request.getAttribute(containsValidPrincipalHeader)))
return System.currentTimeMillis(); // just returning non null value to satisfy spring security contract
logger.trace("Returning null Credentials for non authenticated request");
return null;
}
}
Register this as servlet filter in your app using following registrar
@Configuration
public class ShibbolethFilterRegistrar {
/*
* We don't want to register Shibboleth Filter in spring global chain thus
* added this explicit registration bean to disable just that.
*/
@Bean
public FilterRegistrationBean shibbolethFilterRegistrar(Shibboleth shibbolethAuthFilter) {
FilterRegistrationBean registration = new FilterRegistrationBean(shibbolethAuthFilter);
registration.setEnabled(false);
return registration;
}
@Bean
public ShibbolethAuthFilter shibbolethAuthFilter() {
return new ShibbolethAuthFilter();
}
}
Followed by this, change your WebSecurityConfig to following
@Override
protected void configure(HttpSecurity http) throws Exception {
/* autowire shibbolethAuthFilter bean as well */
http
.addFilterBefore(shibbolethAuthFilter, AbstractPreAuthenticatedProcessingFilter.class);
.authorizeRequests()
.antMatchers("/uri1/**").hasAuthority(Permission.AUTHORITY1.toString())
.antMatchers("/uri2/**").hasAuthority(Permission.AUTHORITY2.toString())
.anyRequest().hasAuthority(Permission.AUTHORITY3.toString())
.and()
.realmName("App").and().csrf().disable();
http.authorizeRequests();
http.headers().frameOptions().sameOrigin().cacheControl().disable();
}
Hope these pointers helps you to integrate external auth successfully.
IMHO, following is still valid - as much as I have understood your scenario, if I had to do it, I'll personally prefer to use spring security inbuilt SAML auth for this purpose since that provides very smooth integration with spring security in every possible context within the framework. In addition, it also simplifies my deployment scenario where I'll also have to take care of provisioning apache which'll typically fall under additional workload for DevOps team. For simplicity and scalability, spring security inbuilt SAML SSO support would be my first choice unless there's a constraint which is forcing me to do otherwise (which I am not able to see in current discussion context based on the explanation provided). There are ample tutorials and examples available on net to get it done. I know this is not what you asked for but I thought to share with you what I have done myself in past for similar SSO solutions in spring distributed apps and learning that I had. Hope it helps!!
Upvotes: 3