Home > Enterprise >  Custom JWT Auth with Spring Webflux
Custom JWT Auth with Spring Webflux

Time:07-23

I'm trying to setup authentication with JWT with Spring Security inside a Spring WebFlux application. I'm using a custom authorization scheme based on custom JWT claims. The problem I'm having is that when I try to invoke a secured endpoint authentication fails with Authentication failed: An Authentication object was not found in the SecurityContext.

Here is the SecurityWebFilterChain I'm using:

@Configuration
@EnableWebFluxSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class WebSecurityConfiguration {
    @Bean
    fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {

        return http {

            authorizeExchange {
                authorize(anyExchange, authenticated)
            }

            oauth2ResourceServer {
                jwt {
                    jwtAuthenticationConverter = grantedAuthoritiesExtractor()
                }
            }
        }
    }

    fun grantedAuthoritiesExtractor(): Converter<Jwt, Mono<AbstractAuthenticationToken>> {
        val jwtAuthenticationConverter = JwtAuthenticationConverter()
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(MappingJwtAuthoritiesConverter())
        //               custom JWT -> Collection<GrantedAuthority>  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

        return ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter)
    }

    @Bean
    fun jwtDecoder(): ReactiveJwtDecoder {
        val secretKey: SecretKey = SecretKeySpec("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".toByteArray(), "HMACSHA256")

        return NimbusReactiveJwtDecoder.withSecretKey(secretKey).macAlgorithm(MacAlgorithm.HS256).build()
    }

    @Bean
    fun jwtValidator(): OAuth2TokenValidator<Jwt> {
        return OAuth2TokenValidator<Jwt> { OAuth2TokenValidatorResult.success() }
    }

    fun jwtAuthenticationManager(jwtDecoder: ReactiveJwtDecoder): JwtReactiveAuthenticationManager {
        return JwtReactiveAuthenticationManager(jwtDecoder).apply {
            this.setJwtAuthenticationConverter(grantedAuthoritiesExtractor())
        }
    }
}

Here is MappingJwtAuthoritiesConverter:

class MappingJwtAuthoritiesConverter : Converter<Jwt, Collection<GrantedAuthority>> {
    companion object {
        private val WELL_KNOWN_CLAIMS: Set<String> = setOf("myCustomClaim")
    }

    override fun convert(jwt: Jwt): Collection<GrantedAuthority> {
        val authorities = jwt.claims.entries
            .filter { (key, _) -> key in WELL_KNOWN_CLAIMS }
            .map { (key, value) ->
                return@map SimpleGrantedAuthority("$key:$value")
            }

        return authorities
    }
}

I searched online but many JWT/Spring Webflux implementation hand-roll JWT validation and handling, and I'd rather use what's already offered by Spring under OAuth integration. Right now the only custom piece I'm using is the converter from JWT to GrantedAuthority, but I still can't get authentication to work.

With the following JWT:

header:
{
  "typ": "JWT",
  "alg": "HS256"
}

payload:
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1658474926,
  "exp": 1668478526,
  "myCustomClaim": "READ"
}

Encoded: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNjU4NDc0OTI2LCJleHAiOjE2Njg0Nzg1MjYsIm15Q3VzdG9tQ2xhaW0iOiJSRUFEIn0.lh66pHMd_xvXAF2itblHeHbZReJQA5xkGLKqXZV6MjI

The endpoint I'm trying to secure is defined as:

@RestController
class FooController {
    @PreAuthorize("hasAuthority('myCustomClaim:READ')")
    @RequestMapping(
            method = [RequestMethod.GET],
            value = ["/foo"],
    )
    override suspend fun getFoo(): ResponseEntity<String> {
        return ResponseEntity.ok("Got foo")
    }
}

Spring logs:

2022-07-22 09:43:35.138 DEBUG 508191 --- [ctor-http-nio-2] o.s.w.s.adapter.HttpWebHandlerAdapter    : [b7fe0c9e-1] HTTP GET "/foo"
2022-07-22 09:43:35.313 DEBUG 508191 --- [     parallel-2] o.s.w.s.s.DefaultWebSessionManager       : Created new WebSession.
2022-07-22 09:43:35.319 DEBUG 508191 --- [     parallel-2] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : Trying to match using org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers$1@3602a026
2022-07-22 09:43:35.319 DEBUG 508191 --- [     parallel-2] o.s.s.w.s.u.m.OrServerWebExchangeMatcher : matched
2022-07-22 09:43:35.319 DEBUG 508191 --- [     parallel-2] a.DelegatingReactiveAuthorizationManager : Checking authorization on '/foo' using org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager@2668fdab
2022-07-22 09:43:35.320 DEBUG 508191 --- [     parallel-2] o.s.s.w.s.a.AuthorizationWebFilter       : Authorization successful
2022-07-22 09:43:35.325 DEBUG 508191 --- [     parallel-2] s.w.r.r.m.a.RequestMappingHandlerMapping : [b7fe0c9e-1] Mapped to it.project.backend.controllers.FooController#getFoo(Continuation)
2022-07-22 09:43:35.665 DEBUG 508191 --- [     parallel-2] o.s.s.w.s.a.AuthenticationWebFilter      : Authentication failed: An Authentication object was not found in the SecurityContext
2022-07-22 09:43:35.694 DEBUG 508191 --- [     parallel-2] o.s.w.s.adapter.HttpWebHandlerAdapter    : [b7fe0c9e-1] Completed 401 UNAUTHORIZED

Any ideas?

CodePudding user response:

Looks like the secret key you constructed your jwtDecoder with does not correspond to the signature of the token you posted and thus the token validation fails.

You can use https://jwt.io/ to check that.

CodePudding user response:

The problem was using @EnableGlobalMethodSecurity instead of @EnableReactiveMethodSecurity. After fixing that everything started to work.

  • Related