skip to Main Content

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.

2

Answers


  1. Chosen as BEST ANSWER

    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.

    Onelogin

    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.

    Apache and shibboleth

    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>
    

    Spring-boot

    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.


  2. 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!!

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search