4

I'm using Postman to test a simple OAuth2 API I'm creating in Spring Boot 2.2.6 with Spring Security. I successfully receive a JWT when requesting new user credentials, but all of my endpoints return a 403 Forbidden error when I attempt to access them with this token in my headers.

My classes are as follows:

My server security configuration:

@Configuration
@EnableWebSecurity
@Order(1)
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
class ServerSecurityConfiguration(
        @Qualifier("userService")
        private val userDetailsService: UserDetailsService
) : WebSecurityConfigurerAdapter() {
    private val logger: Logger = LoggerFactory.getLogger(ServerSecurityConfiguration::class.java)

    @Bean
    fun authenticationProvider(): DaoAuthenticationProvider {
        val provider = DaoAuthenticationProvider()
        provider.setPasswordEncoder(passwordEncoder())
        provider.setUserDetailsService(userDetailsService)

        return provider
    }

    @Bean
    fun passwordEncoder(): PasswordEncoder {
        return BCryptPasswordEncoder()
    }

    @Bean
    @Throws(Exception::class)
    override fun authenticationManagerBean(): AuthenticationManager {
        return super.authenticationManagerBean()
    }

    @Throws(Exception::class)
    override fun configure(auth: AuthenticationManagerBuilder) {
        auth
            .parentAuthenticationManager(authenticationManagerBean())
            .authenticationProvider(authenticationProvider())
            .userDetailsService(userDetailsService)
            .and()
    }

    @Throws(Exception::class)
    override fun configure(http: HttpSecurity) {
        http
            .cors().and().csrf().disable() // remove for production
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            .and()
                .authorizeRequests()
                    .antMatchers(
                            "/",
                            "/index.html",
                            "/**/*.js",
                            "/**/*.html",
                            "/**/*.css",
                            "/**/*.woff",
                            "/**/*.woff2",
                            "/**/*.svg",
                            "/**/*.ttf",
                            "/**/*.ico",
                            "/**/*.eot",
                            "/**/assets/*",
                            "/api/login/**",
                            "/oauth/token",
                            "/oauth/authorize"
                    )
                        .permitAll()
                    .antMatchers(HttpMethod.POST, "/api/submissions")
                        .authenticated()
                    .antMatchers(HttpMethod.POST, "/api/users")
                        .hasAuthority(Role.ADMIN.name)
                    .antMatchers(HttpMethod.POST,"/api/**")
                        .hasAuthority(Role.ADMIN.name)
                    .antMatchers(HttpMethod.DELETE, "/api/**")
                        .hasAuthority(Role.ADMIN.name)
                    .antMatchers(HttpMethod.PUT, "/api/**")
                        .hasAnyAuthority(Role.ADMIN.name)
                    .antMatchers(HttpMethod.GET, "/api/**")
                        .authenticated()
                    .anyRequest()
                        .authenticated()
    }
}

My OAuth2 configuration:

@Configuration
@EnableAuthorizationServer
class OAuth2Configuration(
        @Qualifier("authenticationManagerBean") private val authenticationManager: AuthenticationManager,
        private val passwordEncoder: PasswordEncoder,
        private val userService: UserService,
        private val jwt: JwtProperties
) : AuthorizationServerConfigurerAdapter() {
    private val logger = LoggerFactory.getLogger("OAuth2Configuration")

    @Throws(Exception::class)
    override fun configure(clients: ClientDetailsServiceConfigurer?) {
        clients
            ?.inMemory()
            ?.withClient(jwt.clientId)
            ?.secret(passwordEncoder.encode(jwt.clientSecret))
            ?.accessTokenValiditySeconds(jwt.accessTokenValiditySeconds)
            ?.refreshTokenValiditySeconds(jwt.refreshTokenValiditySeconds)
            ?.authorizedGrantTypes(*jwt.authorizedGrantTypes)
            ?.scopes("read", "write")
            ?.resourceIds("api")
    }

    override fun configure(endpoints: AuthorizationServerEndpointsConfigurer?) {
        endpoints
            ?.tokenStore(tokenStore())
            ?.accessTokenConverter(accessTokenConverter())
            ?.userDetailsService(userService)
            ?.authenticationManager(authenticationManager)
    }

    @Bean
    fun accessTokenConverter(): JwtAccessTokenConverter {
        val converter = JwtAccessTokenConverter()
        converter.setSigningKey(jwt.signingKey)

        return converter
    }

    @Bean
    @Primary
    fun tokenServices(): DefaultTokenServices {
        val services = DefaultTokenServices()
        services.setTokenStore(tokenStore())

        return services
    }

    @Bean
    fun tokenStore(): JwtTokenStore {
        return JwtTokenStore(accessTokenConverter())
    }
}

My resource server configuration:

@Configuration
@EnableResourceServer
class ResourceServerConfiguration : ResourceServerConfigurerAdapter() {
    override fun configure(resources: ResourceServerSecurityConfigurer?) {
        resources?.resourceId("api")
    }
}

My user details service:

@Service
class UserService(private val repository: UserRepository) : UserDetailsService {
    private val logger: Logger = LoggerFactory.getLogger(UserService::class.java)

    override fun loadUserByUsername(username: String?): UserDetails {
        val user = repository.findByUsername(username)
                ?: throw UserNotFoundException("User with username $username not found.")

        return org.springframework.security.core.userdetails.User
            .withUsername(user.name)
            .password(user.passwordHash)
            .authorities(user.role.name)
            .build()
    }
}

Any help would be appreciated, I'm at a loss here.


Debug logs are as follows:

2020-04-21 08:05:42.583 DEBUG 14388 --- [nio-8080-exec-3] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/api/submissions'; against '/api/**'
2020-04-21 08:05:42.583 DEBUG 14388 --- [nio-8080-exec-3] o.s.s.w.a.i.FilterSecurityInterceptor    : Secure object: FilterInvocation: URL: /api/submissions; Attributes: [authenticated]
2020-04-21 08:05:42.584 DEBUG 14388 --- [nio-8080-exec-3] o.s.s.w.a.i.FilterSecurityInterceptor    : Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@ac165fba: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
2020-04-21 08:05:42.584 DEBUG 14388 --- [nio-8080-exec-3] o.s.s.access.vote.AffirmativeBased       : Voter: org.springframework.security.web.access.expression.WebExpressionVoter@1097cbf1, returned: -1
2020-04-21 08:05:42.601 DEBUG 14388 --- [nio-8080-exec-3] o.s.s.w.a.ExceptionTranslationFilter     : Access is denied (user is anonymous); redirecting to authentication entry point

org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233) ~[spring-security-core-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:123) ~[spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:90) ~[spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:118) ~[spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:158) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:92) [spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:92) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:77) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178) [spring-security-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:358) [spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:271) [spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) [spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) [spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) [spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) [spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1594) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_121]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_121]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.33.jar:9.0.33]
    at java.lang.Thread.run(Thread.java:745) [na:1.8.0_121]

It looks like my user object isn't getting the ADMIN role.


Update: I added a filter and printed out the bearer token. It does exist.

the spectre
  • 350
  • 4
  • 11
  • 1
    You'll have to go through the gruelling process of turning on Spring Security debug logging and trying to decipher what it says. In preparation, I recommend you do something that makes you happy. Eat junkfood or something. https://stackoverflow.com/questions/30855252/how-do-i-enable-logging-for-spring-security – Gimby Apr 21 '20 at 14:45
  • What I can guess from what you've provided is that your ResourceServerConfigurerAdapter is not sufficient. You have currently put all the rules in the WebSecurityConfigurerAdapter class, but how it works is that as soon as a JWT token is part of the request, the ResourceServerConfigurerAdapter class is the one that "takes over" and defines the security rules. Likely if you copy over rules from your web security to your resource server security class, things will start to work. – Gimby Apr 21 '20 at 14:49
  • Also a side note: using EnableResourceServer and EnableAuthorizationServer are considered obsolete and support for it will be dropped eventually, probably next year. https://spring.io/blog/2019/11/14/spring-security-oauth-2-0-roadmap-update – Gimby Apr 21 '20 at 14:51
  • Okay, it sounds like I need to use @EnableOAuth2Client instead? Does it annotate both classes? I can't seem to interpret the docs on this. – the spectre Apr 21 '20 at 15:00
  • You either want to fix what you have or you want to migrate - two completely different questions. And migrating definitely takes more than 10 minutes of research effort. Especially when you realise that EnableAuthorizationServer support has been completely dropped instead of replaced with a new API. – Gimby Apr 21 '20 at 15:06
  • Maybe I should just move this to a different REST framework then. This seems overly complicated for the job I'm trying to do here. – the spectre Apr 21 '20 at 15:07
  • Moving my rules to WebSecurityConfigurerAdapter doesn't seem to help. I'll edit and post DEBUG logs. – the spectre Apr 21 '20 at 15:09
  • From logs it looks like you don't have any user associated with request, so it even doesn't get to check roles, because there is no user object. – Pavlus Apr 21 '20 at 16:14
  • This is what I suspect too. I'm not sure why it's the case though, given that I'm sending the JWT in the header. Do I need to implement an authorization filter? – the spectre Apr 21 '20 at 16:28

2 Answers2

2

A little bit late, but a simpler solution could be creating a custom Jwt Authentication Converter:

class CustomJwtAuthenticationConverter : Converter<Jwt, AbstractAuthenticationToken> {

  private val jwtGrantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()

  override fun convert(source: Jwt): AbstractAuthenticationToken {
    val scopes = jwtGrantedAuthoritiesConverter.convert(source)
    val authorities = source.getClaimAsStringList("authorities")?.map { SimpleGrantedAuthority(it) }
    return JwtAuthenticationToken(source, scopes.orEmpty() + authorities.orEmpty())
  }
}

and then supply the converter to your override fun configure(http: HttpSecurity) implementation, like:

.jwtAuthenticationConverter(CustomJwtAuthenticationConverter())

lcs_godoy
  • 88
  • 1
  • 11
0

Adding the following filter seems to resolve the issue.

class JwtAuthorizationFilter(
        authenticationManager: AuthenticationManager,
        @Qualifier("userService")
        private val userDetailsService: UserService,
        private val jwt: JwtProperties,
        private val passwordEncoder: PasswordEncoder

) : BasicAuthenticationFilter(authenticationManager) {
    private val log: Logger = LoggerFactory.getLogger(JwtAuthorizationFilter::class.java)

    @Throws(ServletException::class, IOException::class)
    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
        val header = request
            .getHeader(jwt.authorizationHeaderString)
            ?.takeIf { s -> s.startsWith(jwt.tokenBearerPrefix) }

        if (header == null) {
            filterChain.doFilter(request, response)
            return
        }

        val auth = authenticate(request)

        if (auth != null) {
            log.info("Authentication valid for ${auth.principal}.")
            SecurityContextHolder.getContext().authentication = auth
        }

        log.info("Bearer token processed. Continue.")
        filterChain.doFilter(request, response)
    }

    private fun authenticate(request: HttpServletRequest): UsernamePasswordAuthenticationToken? {
        val token = request.getHeader(jwt.authorizationHeaderString)

        if (token != null) {
            val claims = JWT
                .require(Algorithm.HMAC256(jwt.signingKey))
                .build()
                .verify(token.replace("${jwt.tokenBearerPrefix} ", ""))
                .claims

            val username = claims["user_name"]?.asString()
            val authorities = claims["authorities"]
                    ?.asArray(String::class.java)
                    ?.map { s -> SimpleGrantedAuthority(s) }
                    ?: return null

            if (username != null) {
                return UsernamePasswordAuthenticationToken(username, null, authorities)
            }
        }

        return null
    }
}

You will need to append

            .and()
                .addFilter(
                        JwtAuthorizationFilter(authenticationManager(), userDetailsService as UserService, jwt, passwordEncoder())
                )

to your server security HTTP configuration.

the spectre
  • 350
  • 4
  • 11