Search code examples
springspring-bootauthenticationspring-securitycas

Spring Boot Multiple Authentication Providers from a Single Login Form Entry Point


I have a Spring Boot 2.2 app that authenticates users with the organization's CAS instance. When a user request requires authorization (eg. a Role), the spring sec cas integration (filters) begin authentication by forwarding them to sign in with the CAS server, then redirects them to their original request. Great.

Now we need users external to the organization to be able to sign in (will not have CAS accounts).

I have two problems with my current configuration:

1) It's forwarding all unauthenticated users to the CAS service. Instead I would like to forward them to an in app login form at /login (with POST method login); which provides the option to sign in with CAS. See redacted screenshot of sign in form

2) When a user is at the /login form and they click sign in with CAS, then I would like them to authenticate with CAS and then have them continue with their original request.

Both AuthenticationProviders are working in pieces. The dao auth provider is only accessible if they visit /login. Otherwise problem #1 above happens. I don't know how to configure #2 when they click the button (a GET on /login/cas? but then how would that return them to original request as the GET is seen as a new request, previous being abandoned?).


A scenario to summarize the expected behaviour: An unauthenticated user makes a web request to a protected area. The user is forwarded to /login

a) They sign in with email and password (authenticated with daoAutheticationProvider). Or

b) They click sign in with CAS button (authenticated with casAuthenticationProvider).

Upon authentication they are forwarded back to their original request.


Here is code, its all in java config, we use @PreAuthorize on methods, not antmatchers on HttpSecurity config, but willing to go back to antmatchers if its how the solution will need to work

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(daoAuthenticationProvider());
    auth.authenticationProvider(casAuthenticationProvider());
}

private DaoAuthenticationProvider daoAuthenticationProvider() {
    DaoAuthenticationProvider ap = new DaoAuthenticationProvider();
    ap.setUserDetailsService(daoUserDetailsService); //implements UserDetailsService
    return ap;
}

private CasAuthenticationProvider casAuthenticationProvider() {
    CasAuthenticationProvider ap = new CasAuthenticationProvider();
    ap.setAuthenticationUserDetailsService(casCustomUserDetailsService); //extends AbstractCasAssertionUserDetailsService
    ap.setServiceProperties(serviceProperties());
    ap.setTicketValidator(new Cas30ServiceTicketValidator(casServiceUrl));
    ap.setKey("obviouslyredacted");
    return ap;
}

private CasAuthenticationFilter casAuthenticationFilter() throws Exception {
    CasAuthenticationFilter filter = new CasAuthenticationFilter();
    filter.setFilterProcessesUrl("/j_spring_cas_security_check");
    filter.setAuthenticationManager(authenticationManager());
    return filter;
}

private LogoutFilter requestSingleLogoutFilter() {
    LogoutFilter filter = new LogoutFilter(casServiceUrl + "/logout", new SecurityContextLogoutHandler());
    filter.setFilterProcessesUrl("/j_spring_cas_security_logout");
    return filter;
}

private SingleSignOutFilter singleSignOutFilter() {
    SingleSignOutFilter filter = new SingleSignOutFilter();
    filter.setCasServerUrlPrefix(casServiceUrl);
    filter.setIgnoreInitConfiguration(true);
    return filter;
}

private CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
    CasAuthenticationEntryPoint ep = new CasAuthenticationEntryPoint();
    ep.setLoginUrl(casServiceUrl + "/login");
    ep.setServiceProperties(serviceProperties());
    return ep;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    XFrameOptionsHeaderWriter xframeoptions = new XFrameOptionsHeaderWriter(SAMEORIGIN);
    XXssProtectionHeaderWriter xxssprotection = new XXssProtectionHeaderWriter();
    xxssprotection.setEnabled(true);

    http
            .sessionManagement()
            .sessionCreationPolicy(ALWAYS)

            .and().csrf().disable()
            .authorizeRequests()
            .antMatchers("/login").permitAll()
            .antMatchers("/css/**").permitAll()
            .antMatchers("/favicon_192x192.png").permitAll()
            .anyRequest().authenticated()

            .and().formLogin()
            .loginPage("/login")
            .permitAll()

            .and().logout()
            .logoutRequestMatcher(new AntPathRequestMatcher("/dologout"))
            .invalidateHttpSession(true)
            .deleteCookies("JSESSIONID")
            .permitAll()

            .and().exceptionHandling()
            .accessDeniedPage(NAV_URL_ERROR + "/403")

            .and().headers().addHeaderWriter(xframeoptions)
            .and().headers().addHeaderWriter(xxssprotection);

    //Only enable CAS in Production or SPI
    if (activeProfile.equals(ENV_PRODUCTION) || activeProfile.equals(ENV_SPI))
        http
                .addFilter(casAuthenticationFilter())
                .addFilterBefore(requestSingleLogoutFilter(), LogoutFilter.class)
                .addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class)
                .logout()
                .logoutUrl("/dologout")
                .logoutSuccessUrl(applicationBaseUrl + "/j_spring_cas_security_logout")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .permitAll()

                .and()
                .exceptionHandling()
                .authenticationEntryPoint(casAuthenticationEntryPoint())

                .and()
                .csrf().disable()
                .headers().frameOptions().disable();
}

}

Also feel like my http config method could be cleaned up (this project has been migrated from boot 1.x days)

Update: With Marco's comments, I was able to figure out a solution.

I created a custom class that extends CasAuthenticationPoint, the custom class calls super(...) on all overridden methods, only to expose the final and protected methods for manipulation (I still need CAS to create and encode the service and redirect urls).

private CommonAuthenticationEntryPoint commonAuthenticationEntryPoint() {
        CustomCasAuthenticationEntryPoint ep = new CustomCasAuthenticationEntryPoint();
        ep.setLoginUrl(casServiceUrl + "/login");
        ep.setServiceProperties(serviceProperties());
        return new CommonAuthenticationEntryPoint(ep);
    }

Then the AutheticationEntryPoint:

public class CommonAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {

    private final CustomCasAuthenticationEntryPoint customCasAuthenticationEntryPoint;

    public CommonAuthenticationEntryPoint(CustomCasAuthenticationEntryPoint customCasAuthenticationEntryPoint) {
        this.customCasAuthenticationEntryPoint = customCasAuthenticationEntryPoint;
    }

    @Override
    public void afterPropertiesSet() {
        customCasAuthenticationEntryPoint.afterPropertiesSet();
    }

    @Override
    public void commence(final HttpServletRequest request,
                         final HttpServletResponse response,
                         final AuthenticationException authenticationException) throws IOException {
        if (request.getParameter("cas") == null)
            response.sendRedirect("login");
        else {
            final String urlEncodedService = customCasAuthenticationEntryPoint.createServiceUrl(request, response);
            final String redirectUrl = customCasAuthenticationEntryPoint.createRedirectUrl(urlEncodedService);

            customCasAuthenticationEntryPoint.preCommence(request, response);

            response.sendRedirect(redirectUrl);
        }
    }
}

And the button in thymeleaf does this:

<a class="btn btn-lg btn-default btn-block" th:href="@{${session.get('SPRING_SECURITY_SAVED_REQUEST').redirectUrl}(cas=true)}">
    <img th:src="@{/favicon_192x192.png}" aria-hidden="true" alt="" id="org-sso"/> Organization CAS
</a>

Solution

  • CAS is intended as a central service, so it's a bit strange to have this flow set up. Being CAS able to integrate all the authentication providers you need, it's better to use always the CAS login page and let it verify users credentials against the providers: the users that you call "external" can also make use of CAS if you let it access the configured authentication provider. Have you tried this way?