Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring webflux custom authentication for API

Tags:

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

like image 247
Jan Wytze Avatar asked Nov 17 '17 15:11

Jan Wytze


2 Answers

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.

like image 79
Jan Wytze Avatar answered Sep 21 '22 03:09

Jan Wytze


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

like image 36
user2669657 Avatar answered Sep 20 '22 03:09

user2669657