Spring Boot with Security OAuth2 - how to use resource server with web login form?

18,635

Solution 1

Here is the solution to the problem. Take a look at this exemplary Groovy class:

@Configuration
@EnableResourceServer
class ResourceServer extends ResourceServerConfigurerAdapter {

    @Value('${oauth.resourceId}')
    private String resourceId

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
        http.httpBasic().disable()

        http.requestMatchers().antMatchers('/admin/**', '/uaa/**')
                .and().authorizeRequests()
                    .antMatchers('/uaa/authenticated/**').authenticated()
                    .antMatchers('/uaa/register/**').permitAll()
                    .antMatchers('/uaa/activate/**').permitAll()
                    .antMatchers('/uaa/password/**').permitAll()
                    .antMatchers('/uaa/auth/**').permitAll()
                    .antMatchers('/uaa/account/**').hasAuthority('ADMIN')
                    .antMatchers('/admin/**').hasAuthority('ADMIN')
                    .anyRequest().authenticated()

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(resourceId);
    }
}

Basically, to run OAuth2.0 authentication parallel with web form authentication, you have to put

http.requestMatchers().antMatchers('/path/1/**', '/path/2/**')

to configuration class. My previous configuration missed this important part so only OAuth2.0 took a part in authentication process.

Solution 2

I don't think you should be trying to set up form login or http basic in your ResourceServerConfigurerAdapter, and certainly not if you already have them in your other WebSecurityConfigurerAdapter (you do because they are on by default). It might work, but the authentication and access decisions are so different for an OAuth2 protected resource and a UI that I recommend you keep them separate (as they are in all the samples in github). If you go with the recommendation and continue with the components you already defined, the key to getting this right is to know that the filter chains are tried sequentially and the first one to match wins, so only one of them is going to act on any given request. You have to put request matchers in both chains (or at least the one with the lowest order), and make sure they don't overlap.

Solution 3

what if you use different endpoints configured with different security?

For the above example, everything with /uaa/** secured with WebSecurityConfigurerAdapter, and /api-docs/** with ResourceServerConfigurerAdapter.

In that case, will filter chains still conflict?

Share:
18,635

Related videos on Youtube

Szymon Stepniak
Author by

Szymon Stepniak

πŸ”‘ 30BA 1AAD 71C2 08A2 4C74 5902 FBF5 76F4 BFA5 573B πŸ–³ Freelancer β˜• ToruΕ„ Java User Group founder πŸ“½οΈ YouTube content creator ## πŸ† A few comments about my contributions: "This is a very good answer. (...) It is actually too good and extensive for the little effort the OP put into his question. Anyway, big compliment to Szymon." - link "Awesome analysis, thank you!" - link "Perfect, helps and explains a few more related items." - link "This is awesome! It works :)" - link "you are a genius :)" - link "god dont you just have to love stack overflow - absolutely brilliant. Works a treat i can take it from here." - link ## Is there anything I can help you with? πŸ“§ szymon[dot]stepniak[at]gmail[dot]com

Updated on September 15, 2022

Comments

  • Szymon Stepniak
    Szymon Stepniak over 1 year

    I have Spring Boot (1.2.1.RELEASE) application that serves OAuth2 (2.0.6.RELEASE) authorization and resource server in one application instance. It uses custom UserDetailsService implementation that makes use of MongoTemplate to search users in MongoDB. Authentication with grant_type=password on /oauth/token works like a charm, as well as authorization with Authorization: Bearer {token} header while calling for specific resources.

    Now I want to add simple OAuth confirm dialog to the server, so I can authenticate and authorize e.g. Swagger UI calls in api-docs for protected resources. Here is what I did so far:

    @Configuration
    @SessionAttributes("authorizationRequest")
    class OAuth2ServerConfig extends WebMvcConfigurerAdapter {
    
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/login").setViewName("login");
            registry.addViewController("/oauth/confirm_access").setViewName("authorize");
        }
    
        @Configuration
        @Order(2)
        protected static class LoginConfig extends WebSecurityConfigurerAdapter implements ApplicationEventPublisherAware {
    
            @Autowired
            UserDetailsService userDetailsService
    
            @Autowired
            PasswordEncoder passwordEncoder
    
            ApplicationEventPublisher applicationEventPublisher
    
    
            @Bean
            DaoAuthenticationProvider daoAuthenticationProvider() {
                DaoAuthenticationProvider provider = new DaoAuthenticationProvider()
                provider.passwordEncoder = passwordEncoder
                provider.userDetailsService = userDetailsService
                return provider
            }
    
            @Override
            protected void configure(AuthenticationManagerBuilder auth) throws Exception {
                auth.parentAuthenticationManager(authenticationManagerBean())
                        .userDetailsService(userDetailsService)
                        .passwordEncoder(passwordEncoder())
            }
    
            @Bean
            @Override
            public AuthenticationManager authenticationManagerBean() throws Exception {
                //return super.authenticationManagerBean()
                ProviderManager providerManager = new ProviderManager([daoAuthenticationProvider()], super.authenticationManagerBean())
                providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher))
                return providerManager
            }
    
            @Bean
            public PasswordEncoder passwordEncoder() {
                new BCryptPasswordEncoder(5)
            }
        }
    
    
        @Configuration
        @EnableResourceServer
        protected static class ResourceServer extends ResourceServerConfigurerAdapter {
    
            @Value('${oauth.resourceId}')
            private String resourceId
    
            @Autowired
            @Qualifier('authenticationManagerBean')
            private AuthenticationManager authenticationManager
    
            @Override
            public void configure(HttpSecurity http) throws Exception {
                http.setSharedObject(AuthenticationManager.class, authenticationManager)
    
                http.csrf().disable()
                http.httpBasic().disable()
    
                http.formLogin().loginPage("/login").permitAll()
    
                //http.authenticationProvider(daoAuthenticationProvider())
    
                http.anonymous().and()
                        .authorizeRequests()
                        .antMatchers('/login/**').permitAll()
                        .antMatchers('/uaa/register/**').permitAll()
                        .antMatchers('/uaa/activate/**').permitAll()
                        .antMatchers('/uaa/password/**').permitAll()
                        .antMatchers('/uaa/account/**').hasAuthority('ADMIN')
                        .antMatchers('/api-docs/**').permitAll()
                        .antMatchers('/admin/**').hasAuthority('SUPERADMIN')
                        .anyRequest().authenticated()
    
                //http.sessionManagement().sessionCreationPolicy(STATELESS)
            }
    
            @Override
            public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
                resources.resourceId(resourceId)
                resources.authenticationManager(authenticationManager)
            }
        }
    
        @Configuration
        @EnableAuthorizationServer
        protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {
    
            @Value('${oauth.clientId}')
            private String clientId
    
            @Value('${oauth.secret:}')
            private String secret
    
            @Value('${oauth.resourceId}')
            private String resourceId
    
            @Autowired
            @Qualifier('authenticationManagerBean')
            private AuthenticationManager authenticationManager
    
            @Bean
            public JwtAccessTokenConverter accessTokenConverter() {
                return new JwtAccessTokenConverter();
            }
    
            @Override
            public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
                oauthServer.checkTokenAccess("permitAll()")
                oauthServer.allowFormAuthenticationForClients()
            }
    
            @Override
            public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
                endpoints.authenticationManager(authenticationManager)
                        .accessTokenConverter(accessTokenConverter())
            }
    
            @Override
            public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
                clients.inMemory()
                        .withClient(clientId)
                        .secret(secret)
                        .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
                        .authorities("USER", "ADMIN")
                        .scopes("read", "write", "trust")
                        .resourceIds(resourceId)
            }
        }
    }
    

    Main problem is that I cannot make both (web login form and OAuth2 authorization token in header) running. If ResourceServer gets higher priority, then OAuth2 token authorization works, but I can't login using web form. On the other hand if I set the higher priority to LoginConfig class, then OAuth2 token authorization stops working.

    Case study: Login form works, OAuth2 token authorization does not

    I figured out that in that case the problem is caused by non-registered OAuth2AuthenticationProcessingFilter. I tried to registered it manually in ResourceServer.configure(HttpSecurity http) method, but it didn't work - I could see the filter on FilterChain list, but it didn't get triggered. It wasn't good way to fix it, because there is a lot of other magic done during the ResourceServer initialization so I moved to the second case.

    Case study: Login form does not work, OAuth2 token authorization works

    In that case the main problem is that by default UsernamePasswordAuthenticationFilter cannot find a properly configured AuthenticationProvider instance (in ProviderManager). When I tried to add it manually by:

    http.authenticationProvide(daoAuthenticationProvider())
    

    it gets one, but in this case there is no AuthenticationEventPublisher defined and successful authentication cannot be published to other components. And in fact in the next iteration it gets replaced by AnonymousAuthenticationToken. That's why I tried to define manually AuthenticationManager instance with DaoAuthenticationProvider inside:

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        //return super.authenticationManagerBean()
        ProviderManager providerManager = new ProviderManager([daoAuthenticationProvider()], super.authenticationManagerBean())
        providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher))
        return providerManager
    }
    

    I thought it will work, but there is a different problem with providing AuthenticationManager instance to registered filters. It turns out that each filter has authenticationManager injected manually using sharedObjects component:

    authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
    

    The problem here is that you are not guaranteed to have a proper instance set, because there is a simple HashMap (check it on GitHub) used to store specific shared object and it can be change any time. I tried to set it in:

    http.setSharedObject(AuthenticationManager.class, authenticationManager)
    

    but before I get to the place where it is being read, it's already replaced by default implementation. I checked it with the debugger and it looks like that for each new filter there is a new instance of authentication manager.

    My question is: am I doing it correctly? How can I set up authorization server with the resources server integrated in one application with login form (OAuth2 dialog) working? Maybe it can be done in a different and much easier way. I would be thankful for any help.

  • Szymon Stepniak
    Szymon Stepniak about 9 years
    Hey Dave, thanks for coming with an idea. I already found what was wrong with my resource server configuration, I will post it here shortly. Now the form based login and oauth token authentication works just fine.
  • Maksim
    Maksim over 8 years
    hi @SzymonStepniak. Would you please share your solution.
  • Szymon Stepniak
    Szymon Stepniak over 8 years
    Hey @Maksim, please find an answer below. Sorry you have to wait for it, I'm in the middle of analogue holiday season :)
  • 89n3ur0n
    89n3ur0n over 8 years
    Hi, but basic auth is not disabled. Popup is coming everytime! What should be done? Please help
  • nucatus
    nucatus about 8 years
    Hello Stephniak, In the case of the LoginForm security configuration, did you reuse the one from the original post? I'm also interested in combining OAuth with other type of authentication/authorization, in my case, SpringSession clustering. Thanks.
  • user1791139
    user1791139 over 6 years
    @SzymonStepniak Can you please provide your solution? I would also like to replace the Basic Auth popup of the ResourceServer by a login form.