ulisesbocchio / spring-boot-security-saml

spring-security-saml integration with Spring Boot
MIT License
158 stars 73 forks source link

I had to use tricks to setup my own entry point #75

Closed igormukhin closed 5 years ago

igormukhin commented 5 years ago

I had to use tricks to setup my own entry point:

public class SAMLServiceProviderConfig extends ServiceProviderConfigurerAdapter {

    private static final String LOGIN_PAGE = "/login.jsf";

    @Inject
    private Environment env;

    @Override
    public void configure(HttpSecurity http) {
        http
            .formLogin()
                .loginPage(LOGIN_PAGE)
                .permitAll()
                .failureUrl("/login.jsf?error=true")
                .defaultSuccessUrl("/index.jsf");

        // override entry point to form on developer environments
        if (env.getProperty("security.default-form-entry-point", Boolean.class, false)) {
            // Remove the existing configurer that was already setup by SAML
            // otherwise it will run before our configurer can modify it
            http.removeConfigurers(ExceptionHandlingConfigurer.class);

            // register our configurer that will modify exception handling later
            http.apply(loginFormEntryPointConfigurer());
        }
    }

    @Bean
    @Profile(AppProfiles.DEV)
    public LoginFormEntryPointConfigurer loginFormEntryPointConfigurer() {
        return new LoginFormEntryPointConfigurer();
    }

    public static class LoginFormEntryPointConfigurer
            extends AbstractHttpConfigurer<LoginFormEntryPointConfigurer, HttpSecurity> {

        @Override
        public void init(HttpSecurity http) {
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint(LOGIN_PAGE));
        }

    }
}

I just wanted to setup my own entry point, but this library sets up SAMLEntryPoint in such a phase that you can't easily override it.

You configure it like this in SAMLConfigurerBean.java:

    @Override
    public void init(HttpSecurity http) throws Exception {
...
        http
            .exceptionHandling()
            .authenticationEntryPoint(sAMLEntryPoint);
...
    }

May be you can configure it at another phase, so it could be easily overridable.

igormukhin commented 5 years ago

Exerpt from AbstractAuthenticationFilterConfigurer:

private void registerDefaultAuthenticationEntryPoint(B http) {

        // ...

        RequestMatcher notXRequestedWith = new NegatedRequestMatcher(
                new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));

        RequestMatcher preferredMatcher = new AndRequestMatcher(Arrays.asList(notXRequestedWith, mediaMatcher));

        // !!! registering an entry point with a matcher makes it overridable
        exceptionHandling.defaultAuthenticationEntryPointFor(
                postProcess(authenticationEntryPoint), preferredMatcher);
    }
ulisesbocchio commented 5 years ago

you can probably use this method:

@Configuration
public static class MyServiceProviderConfig extends WebSecurityConfigurerAdapter {

    @Bean
    SAMLConfigurerBean saml() {
        return new SAMLConfigurerBean();
    }

    @Bean
    BasicConfigurerBean basic() {
        return new BasicConfigurerBean(saml().serviceProvider());
    }

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http.httpBasic()
            .disable()
            .csrf()
            .disable()
            .anonymous()
        .and()
            .apply(saml())
            .serviceProvider()
                // ... sp config
        .http()
            .authorizeRequests()
            .requestMatchers(saml().endpointsMatcher())
            .permitAll()
        .and()
            .authorizeRequests()
            .anyRequest()
            .authenticated()
         .and()
             .apply(basic()); // NOTICE HERE APPLYING SECOND CONFIGURER
        // @formatter:on
    }
}

public class BasicConfigurerBean extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    private ServiceProviderBuilder serviceProvider;

    public BasicConfigurerBean(ServiceProviderBuilder serviceProvider) {
        this.serviceProvider = serviceProvider;
    }

    @Override
    public void init(HttpSecurity http) throws Exception {
        SAMLEntryPoint sAMLEntryPoint = serviceProvider.getSharedObject(SAMLEntryPoint.class);

        LoginUrlAuthenticationEntryPoint basicEntryPoint = new LoginUrlAuthenticationEntryPoint(LOGIN_PAGE);

        LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints = new LinkedHashMap();

        RequestMatcher notXRequestedWith = new NegatedRequestMatcher(new 
        RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
        RequestMatcher preferredMatcher = new AndRequestMatcher(Arrays.asList(notXRequestedWith, mediaMatcher));

        entryPoints.put(preferredMatcher, basicEntryPoint);
        entryPoints.put(new AnyRequestMatcher(), sAMLEntryPoint);

        http
            .exceptionHandling()
            .authenticationEntryPoint(new DelegatingEntryPoint(entryPoints));
    }
}

You can basically define a composite entry point. In this case the only trick is that you're catching the saml one, putting in inside a delegating one, and setting it back. For the library though, one option could be simply set te samlEntryPoint as default entry point and then you'd be able to setup any other entry point in whatever stage. Or externalize a method on the serviceProvider DSL to provide a custom entryPoint (there's one but only accepts SAMLEntryPoint type). There's a chance I don't understand what you're doing there, but if you need something to switch the login page but they don't have to co-exist together at the same time then I'd suggest loading a different security config using ConditionalOn... type classes to load one security config or the other.

skozlov commented 4 years ago

I have a similar issue, but I don't want SAMLEntryPoint to be registered as AuthenticationEntryPoint at all. I want it to act only as a filter at /saml/login.

@ulisesbocchio thank you for this project and the workaround above!

Workaround for my case (maybe it can help someone):

@Bean
SAMLConfigurerBean saml() {
    return new SAMLConfigurerBean(){
        @Override
        public void init(HttpSecurity http) throws Exception {
            super.init(http);
            http.exceptionHandling().authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
        }
    };
}