I am creating an API for an Angular 5 application. I would like to use JWT for authentication.
I would like to use the features that are provided by spring security so I can easily work with roles.
I managed to disable basic authentication. But when using http.authorizeExchange().anyExchange().authenticated();
I still get a login prompt.
I would like to just give a 403 instead of the prompt. So overriding the login prompt by a "thing"(Is it a filter?) that checks the Authorization
header for the token.
The login I just want to do in a controller that will return a JWT token. But what spring security bean I should use for checking user credentials? I can build my own services and repositories, but I would like to use the features provided by spring security as much as possible.
The short version of this question is just:
How can I customize spring security's authentication?
What beans do I have to create?
Where do I have to put the configuration? (I now have a bean of SecurityWebFilterChain
)
The only documentation I could find about authentication in webflux with spring security is this: https://docs.spring.io/spring-security/site/docs/5.0.0.BUILD-SNAPSHOT/reference/htmlsingle/#jc-webflux
After a lot of searching and trying I think I have found the solution:
You need a bean of SecurityWebFilterChain
that contains all configuration.
This is mine:
@Configuration public class SecurityConfiguration { @Autowired private AuthenticationManager authenticationManager; @Autowired private SecurityContextRepository securityContextRepository; @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { // Disable default security. http.httpBasic().disable(); http.formLogin().disable(); http.csrf().disable(); http.logout().disable(); // Add custom security. http.authenticationManager(this.authenticationManager); http.securityContextRepository(this.securityContextRepository); // Disable authentication for `/auth/**` routes. http.authorizeExchange().pathMatchers("/auth/**").permitAll(); http.authorizeExchange().anyExchange().authenticated(); return http.build(); } }
I've disabled httpBasic, formLogin, csrf and logout so I could make my custom authentication.
By setting the AuthenticationManager
and SecurityContextRepository
I overridden the default spring security configuration for checking if a user is authenticated/authorized for a request.
The authentication manager:
@Component public class AuthenticationManager implements ReactiveAuthenticationManager { @Override public Mono<Authentication> authenticate(Authentication authentication) { // JwtAuthenticationToken is my custom token. if (authentication instanceof JwtAuthenticationToken) { authentication.setAuthenticated(true); } return Mono.just(authentication); } }
I am not fully sure where the authentication manager is for, but I think for doing the final authentication, so setting authentication.setAuthenticated(true);
when everything is right.
SecurityContextRepository:
@Component public class SecurityContextRepository implements ServerSecurityContextRepository { @Override public Mono<Void> save(ServerWebExchange serverWebExchange, SecurityContext securityContext) { // Don't know yet where this is for. return null; } @Override public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) { // JwtAuthenticationToken and GuestAuthenticationToken are custom Authentication tokens. Authentication authentication = (/* check if authenticated based on headers in serverWebExchange */) ? new JwtAuthenticationToken(...) : new GuestAuthenticationToken(); return new SecurityContextImpl(authentication); } }
In the load I will check based on the headers in the serverWebExchange
if the user is authenticated. I use https://github.com/jwtk/jjwt. I return a different kind of authentication token if the user is authenticated or not.
For those that have same issue(Webflux + Custom Authentication + JWT
) I solved using AuthenticationWebFilter
, custom ServerAuthenticationConverter
and ReactiveAuthenticationManager
, following the code hope could help someone in the future. Tested with latest version(spring-boot 2.2.4.RELEASE
).
@EnableWebFluxSecurity @EnableReactiveMethodSecurity public class SpringSecurityConfiguration { @Bean public SecurityWebFilterChain configure(ServerHttpSecurity http) { return http .csrf() .disable() .headers() .frameOptions().disable() .cache().disable() .and() .authorizeExchange() .pathMatchers(AUTH_WHITELIST).permitAll() .anyExchange().authenticated() .and() .addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION) .httpBasic().disable() .formLogin().disable() .logout().disable() .build(); }
@Autowired private lateinit var userDetailsService: ReactiveUserDetailsService
class CustomReactiveAuthenticationManager(userDetailsService: ReactiveUserDetailsService?) : UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService) { override fun authenticate(authentication: Authentication): Mono<Authentication> { return if (authentication.isAuthenticated) { Mono.just<Authentication>(authentication) } else super.authenticate(authentication) } } private fun responseError() : ServerAuthenticationFailureHandler{ return ServerAuthenticationFailureHandler{ webFilterExchange: WebFilterExchange, _: AuthenticationException -> webFilterExchange.exchange.response.statusCode = HttpStatus.UNAUTHORIZED webFilterExchange.exchange.response.headers.addIfAbsent(HttpHeaders.LOCATION,"/") webFilterExchange.exchange.response.setComplete(); } } private AuthenticationWebFilter authenticationWebFilter() { AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(reactiveAuthenticationManager()); authenticationWebFilter.setServerAuthenticationConverter(new JwtAuthenticationConverter(tokenProvider)); NegatedServerWebExchangeMatcher negateWhiteList = new NegatedServerWebExchangeMatcher(ServerWebExchangeMatchers.pathMatchers(AUTH_WHITELIST)); authenticationWebFilter.setRequiresAuthenticationMatcher(negateWhiteList); authenticationWebFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository()); authenticationWebFilter.setAuthenticationFailureHandler(responseError()); return authenticationWebFilter; } } public class JwtAuthenticationConverter implements ServerAuthenticationConverter { private final TokenProvider tokenProvider; public JwtAuthenticationConverter(TokenProvider tokenProvider) { this.tokenProvider = tokenProvider; } private Mono<String> resolveToken(ServerWebExchange exchange) { log.debug("servletPath: {}", exchange.getRequest().getPath()); return Mono.justOrEmpty(exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION)) .filter(t -> t.startsWith("Bearer ")) .map(t -> t.substring(7)); } @Override public Mono<Authentication> convert(ServerWebExchange exchange) { return resolveToken(exchange) .filter(tokenProvider::validateToken) .map(tokenProvider::getAuthentication); } } public class CustomReactiveAuthenticationManager extends UserDetailsRepositoryReactiveAuthenticationManager { public CustomReactiveAuthenticationManager(ReactiveUserDetailsService userDetailsService) { super(userDetailsService); } @Override public Mono<Authentication> authenticate(Authentication authentication) { if (authentication.isAuthenticated()) { return Mono.just(authentication); } return super.authenticate(authentication); } }
PS: The TokenProvider class you find at https://github.com/jhipster/jhipster-registry/blob/master/src/main/java/io/github/jhipster/registry/security/jwt/TokenProvider.java
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With