JWT Authorization Kotlin Coroutines Spring Security and Spring WebFlux - spring

I'm trying to implement Authentication & Authorization with Spring Boot (2.3.1.RELEASE) WebFlux + Kotlin + Coroutines, I think that I reach a point where I cannot find any useful information about Authorization even in the Spring Security github source code.
If I understand correctly, for the Authentication flow I did the following:
#Bean
fun authenticationWebFilter(reactiveAuthenticationManager: ReactiveAuthenticationManager,
jwtConverter: JWTConverter,
serverAuthenticationSuccessHandler: ServerAuthenticationSuccessHandler): AuthenticationWebFilter {
val authenticationWebFilter = AuthenticationWebFilter(reactiveAuthenticationManager)
authenticationWebFilter.setRequiresAuthenticationMatcher { ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/login").matches(it) }
authenticationWebFilter.setServerAuthenticationConverter(jwtConverter)
authenticationWebFilter.setAuthenticationSuccessHandler(serverAuthenticationSuccessHandler)
return authenticationWebFilter
}
The flow is:
ServerWebExchangeMatcher is executed and check if the client is requesting the login endpoint with the correct HTTP verb.
If everything is ok, the ServerAuthenticationConverter gets the username and password from the body and creates a unauthenticated UsernamePasswordAuthenticationToken.
ReactiveAuthenticationManager is called and performs the authentication (looks in db and check passwords).
If everything goes well the ServerAuthenticationSuccessHandler is called.
My ServerAuthenticationConverter:
import com.kemenu.dark.admin.application.HttpExceptionFactory.badRequest
import com.kemenu.dark.admin.application.login.LoginRequest
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactor.mono
import org.springframework.core.ResolvableType
import org.springframework.http.MediaType
import org.springframework.http.codec.json.AbstractJackson2Decoder
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
import javax.validation.Validator
#Component
class JWTConverter(private val jacksonDecoder: AbstractJackson2Decoder,
private val validator: Validator) : ServerAuthenticationConverter {
override fun convert(exchange: ServerWebExchange?): Mono<Authentication> = mono {
val loginRequest = getUsernameAndPassword(exchange!!) ?: throw badRequest()
if (validator.validate(loginRequest).isNotEmpty()) {
throw badRequest()
}
return#mono UsernamePasswordAuthenticationToken(loginRequest.username, loginRequest.password)
}
private suspend fun getUsernameAndPassword(exchange: ServerWebExchange): LoginRequest? {
val dataBuffer = exchange.request.body
val type = ResolvableType.forClass(LoginRequest::class.java)
return jacksonDecoder
.decodeToMono(dataBuffer, type, MediaType.APPLICATION_JSON, mapOf())
.onErrorResume { Mono.empty<LoginRequest>() }
.cast(LoginRequest::class.java)
.awaitFirstOrNull()
}
}
My ReactiveAuthenticationManager:
#Bean
fun reactiveAuthenticationManager(reactiveUserDetailsService: AdminReactiveUserDetailsService,
passwordEncoder: PasswordEncoder): ReactiveAuthenticationManager {
val manager = UserDetailsRepositoryReactiveAuthenticationManager(reactiveUserDetailsService)
manager.setPasswordEncoder(passwordEncoder)
return manager
}
My ServerAuthenticationSuccessHandler:
import com.kemenu.dark.admin.application.HttpExceptionFactory.unauthorized
import com.kemenu.dark.admin.application.security.JWTService
import kotlinx.coroutines.reactor.mono
import org.springframework.security.core.Authentication
import org.springframework.security.core.userdetails.User
import org.springframework.security.web.server.WebFilterExchange
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
#Component
class JWTServerAuthenticationSuccessHandler(private val jwtService: JWTService) : ServerAuthenticationSuccessHandler {
private val FIFTEEN_MIN = 1000 * 60 * 15
private val FOUR_HOURS = 1000 * 60 * 60 * 4
override fun onAuthenticationSuccess(webFilterExchange: WebFilterExchange?, authentication: Authentication?): Mono<Void> = mono {
val principal = authentication?.principal ?: throw unauthorized()
when(principal) {
is User -> {
val accessToken = jwtService.accessToken(principal.username, FIFTEEN_MIN)
val refreshToken = jwtService.refreshToken(principal.username, FOUR_HOURS)
webFilterExchange?.exchange?.response?.headers?.set("Authorization", accessToken)
webFilterExchange?.exchange?.response?.headers?.set("JWT-Refresh-Token", refreshToken)
}
}
return#mono null
}
}
My Security config:
import com.kemenu.dark.admin.application.security.authentication.AdminReactiveUserDetailsService
import com.kemenu.dark.admin.application.security.authentication.JWTConverter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.ClassPathResource
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.http.codec.json.AbstractJackson2Decoder
import org.springframework.http.codec.json.Jackson2JsonDecoder
import org.springframework.security.authentication.ReactiveAuthenticationManager
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager
import org.springframework.security.authorization.ReactiveAuthorizationManager
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.SecurityWebFiltersOrder
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.web.server.authentication.AuthenticationWebFilter
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler
import org.springframework.security.web.server.authorization.AuthorizationWebFilter
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers
import org.springframework.web.reactive.config.EnableWebFlux
import org.springframework.web.reactive.config.WebFluxConfigurer
import org.springframework.web.reactive.function.server.router
import org.springframework.web.server.ServerWebExchange
import java.net.URI
#Configuration
#EnableWebFlux
#EnableWebFluxSecurity
class WebConfig : WebFluxConfigurer {
#Bean
fun configureSecurity(http: ServerHttpSecurity,
jwtAuthenticationFilter: AuthenticationWebFilter,
jwtAuthorizationWebFilter: AuthorizationWebFilter): SecurityWebFilterChain {
return http
.cors().disable()
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.authorizeExchange()
.pathMatchers("/login").permitAll()
.anyExchange().authenticated()
.and()
.addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.addFilterAt(jwtAuthorizationWebFilter, SecurityWebFiltersOrder.AUTHORIZATION)
.build()
}
#Bean
fun mainRouter() = router {
accept(MediaType.TEXT_HTML).nest {
GET("/") { temporaryRedirect(URI("/index.html")).build() }
}
resources("/**", ClassPathResource("public/"))
}
#Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
#Bean
fun authenticationWebFilter(reactiveAuthenticationManager: ReactiveAuthenticationManager,
jwtConverter: JWTConverter,
serverAuthenticationSuccessHandler: ServerAuthenticationSuccessHandler): AuthenticationWebFilter {
val authenticationWebFilter = AuthenticationWebFilter(reactiveAuthenticationManager)
authenticationWebFilter.setRequiresAuthenticationMatcher { ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/login").matches(it) }
authenticationWebFilter.setServerAuthenticationConverter(jwtConverter)
authenticationWebFilter.setAuthenticationSuccessHandler(serverAuthenticationSuccessHandler)
return authenticationWebFilter
}
#Bean
fun authorizationWebFilter(jwtReactiveAuthorizationManager: ReactiveAuthorizationManager<ServerWebExchange>): AuthorizationWebFilter = AuthorizationWebFilter(jwtReactiveAuthorizationManager)
#Bean
fun jacksonDecoder(): AbstractJackson2Decoder = Jackson2JsonDecoder()
#Bean
fun reactiveAuthenticationManager(reactiveUserDetailsService: AdminReactiveUserDetailsService,
passwordEncoder: PasswordEncoder): ReactiveAuthenticationManager {
val manager = UserDetailsRepositoryReactiveAuthenticationManager(reactiveUserDetailsService)
manager.setPasswordEncoder(passwordEncoder)
return manager
}
}
Now for the Authorization I created a ReactiveAuthorizationManager:
import com.kemenu.dark.admin.application.security.JWTService
import kotlinx.coroutines.reactor.mono
import org.springframework.http.HttpHeaders
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.authorization.AuthorizationDecision
import org.springframework.security.authorization.ReactiveAuthorizationManager
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
#Component
class JWTReactiveAuthorizationManager(private val jwtService: JWTService) : ReactiveAuthorizationManager<ServerWebExchange> {
override fun check(authentication: Mono<Authentication>?, exchange: ServerWebExchange?): Mono<AuthorizationDecision> = mono {
val authHeader = exchange?.request?.headers?.getFirst(HttpHeaders.AUTHORIZATION) ?: return#mono AuthorizationDecision(false)
if (!authHeader.startsWith("Bearer ")) {
return#mono AuthorizationDecision(false)
}
val decodedJWT = jwtService.decodeAccessToken(authHeader)
if (decodedJWT.subject.isNullOrBlank()) {
return#mono AuthorizationDecision(false)
}
SecurityContextHolder.getContext().authentication = UsernamePasswordAuthenticationToken(decodedJWT.subject, null, listOf())
return#mono AuthorizationDecision(true)
}
}
And I added it to the configuration as a WebFilter for Authorization.
So far so good, my problem comes when I run this test:
#Test
fun `Given an admin when tries to fetch data from customers API with AUTHORIZATION then receives the data`() {
val customer = CustomerHelper.random()
runBlocking {
customerRepository.save(customer)
}
webTestClient
.get().uri("/v1/customers")
.header(HttpHeaders.AUTHORIZATION, accessToken())
.exchange()
.expectStatus().isOk
.expectBodyList<Customer>()
.contains(customer)
}
Then I receive an unauthorized 401 http error, why?

Related

Integration test for rest controller with #WebMvcTest or #SpringBootTest results in 404. (Spring boot)

I'm developing Spring boot and security web application with security that is WebSecurityConfigurerAdapter based. This application has two WebSecurityConfigurerAdapters for two authorization types - login form and bearer with JWT.
There are some simple rest controllers with JWT protected endpoints. WebSecurityConfigurerAdapter implementation for the bearer with JWT is as follows:
#Configuration
#Order(2)
public class SecurityConfigRest extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.requestMatcher(new AntPathRequestMatcher("/rest/**")).authorizeRequests()
.regexMatchers(HttpMethod.POST,"/rest/products/add/?").hasRole("ADMIN")
.antMatchers(HttpMethod.GET,"/rest/products/store/**").hasRole("ADMIN")
.regexMatchers(HttpMethod.POST,"/rest/store/add/?").hasRole("ADMIN")
.regexMatchers(HttpMethod.POST,"/rest/store/\\d/brands/?").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.csrf().disable()
.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter()); // - simple custom converter
}
}
I'm creating a unit test for those JWT protected endpoints:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import java.math.BigDecimal;
import java.net.URI;
import java.util.Collections;
import java.util.Optional;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
#RunWith(SpringRunner.class)
#WebMvcTest(ProductControllerRest.class )
#ContextConfiguration(classes = WebMwcTestConfig.class)
#ActiveProfiles("test")
public class ProductControllerRestIntegrationTest {
#Autowired
private MockMvc mockMvc;
#Test
public void testProductAdd() throws Exception {
String scheme = env.getProperty(SERVER_SSL_ENABLED, Boolean.class, Boolean.TRUE) ? "https" : "http";
String port = Optional.ofNullable(env.getProperty(SERVER_PORT))
.map(":"::concat).orElse("");
StringBuilder uriBuilder = new StringBuilder(scheme).append("://").append("localhost").append(port)
.append("/rest/products/add/");
URI uri = URI.create(uriBuilder.toString());
MvcResult mvcResult = mockMvc.perform(post(uri)
.secure(true)
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer test-token")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(getStringJson(productDto))).andReturn();
verify(productRepository).save(productCaptor.capture());
......
}
}
JavaConfig contains aplication context moks for WebSecurityConfigurerAdapter and JWT converter/decoder for the spring security filter chain:
import local.authorization.resource.server.controller.rest.ProductControllerRest;
import local.authorization.resource.server.security.SecurityConfigRest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import java.util.Arrays;
import java.util.Collections;
#Configuration
public class WebMwcTestConfig {
#Bean
public SecurityConfigRest securityConfigRest() {
return new SecurityConfigRest();
}
#Bean // - add JwtDecoder mock to application context to be used in JwtAuthenticationProvider
public JwtDecoder jwtDecoderAdmin() {
return (token) -> createJwtToken("testAdmin", "ROLE_ADMIN");
}
#Bean
public UserDetailsService userDetailsService() {
User basicUserTest = new User("testUser","testUserPass", Arrays.asList(
new SimpleGrantedAuthority("USER")
));
User managerActiveUser = new User("testAdmin","testAdminPass", Arrays.asList(
new SimpleGrantedAuthority("ADMIN")
));
return new InMemoryUserDetailsManager(Arrays.asList(
basicUserTest, managerActiveUser
));
}
private Jwt createJwtToken(String userName, String role) {
String userId = "AZURE-ID-OF-USER";
String applicationId = "AZURE-APP-ID";
return Jwt.withTokenValue("test-token")
.header("typ", "JWT")
.header("alg", "none")
.claim("oid", userId)
.claim("user_name", userName)
.claim("azp", applicationId)
.claim("ver", "1.0")
.claim("authorities", Collections.singletonList(role))
.subject(userId)
.build();
}
}
But after MockHttpServletRequest is passing the spring security filter chain and reaches rest controller, MockMvc returns MockHttpServletResponse with 404 status:
MockHttpServletRequest:
HTTP Method = POST
Request URI = /rest/products/add/
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8", Accept:"application/json", Authorization:"Bearer test-token", Content-Length:"125"]
Body = {
"name" : "test_product_1_name",
"description" : "test_product_1_description",
"price" : 0.1,
"storeId" : 100
}
Session Attrs = {}
Handler:
Type = org.springframework.web.servlet.resource.ResourceHttpRequestHandler
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 404
Error message = null
Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", Strict-Transport-Security:"max-age=31536000 ; includeSubDomains", X-Frame-Options:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
It seems that the reason is that ProductControllerRest hasn't been mocked out and added to the application context by #WebMvcTest(controllers = ProductControllerRest.class). Here is indicated that #WebMvcTest is the right way for controllers mocking.
Another oprinon:
#SpringBootTest(webEnvironment = MOCK)
#AutoConfigureMockMvc
is resulting in the same - being able to pass security chain, MockHttpServletResponse status is still 404
I was trying also :
#Autowired
private WebApplicationContext webAppContext;
mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build(); // - init mockMvc with webAppContext Autowired
In this case security filter chain isn't being passed.
Are there other configurations and options how to mock a rest controller out with #WebMvcTest or #SpringBootTest?

spring boot 3 - digest authentication not working

I am not able to create a simple rest API with digest authentication with spring boot 3. It was working with version 2, but with a different configuration, because WebSecurityConfigurerAdapter is not present anymore.
I create simple GitHub repository with the sample:
https://github.com/martinspudich/spring-boot-3-digest
I will appreciate any help you can provide. Thank you in advance.
I create simple HelloController:
package com.example.springboot3digest.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class HelloController {
#GetMapping("/")
public String hello() {
return "Hello World!";
}
}
And WebSecurityConfig:
package com.example.springboot3digest.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
#Configuration
#EnableWebSecurity
public class WebSecurityConfig {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http, UserDetailsService userDetailsService) throws Exception {
http
.authorizeHttpRequests(request -> {
request.anyRequest().authenticated();
})
.exceptionHandling(e -> e.authenticationEntryPoint(entryPoint()))
.addFilterBefore(digestAuthenticationFilter(userDetailsService), BasicAuthenticationFilter.class);
return http.build();
}
#Bean
UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("user")
.password("user")
.roles("USER").build();
return new InMemoryUserDetailsManager(user);
}
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
DigestAuthenticationEntryPoint entryPoint() {
DigestAuthenticationEntryPoint result = new DigestAuthenticationEntryPoint();
result.setRealmName("My App Realm");
result.setKey("3028472b-da34-4501-bfd8-a355c42bdf92");
return result;
}
DigestAuthenticationFilter digestAuthenticationFilter(UserDetailsService userDetailsService) {
DigestAuthenticationFilter result = new DigestAuthenticationFilter();
result.setUserDetailsService(userDetailsService);
result.setAuthenticationEntryPoint(entryPoint());
return result;
}
}
I was following spring.io documentation:
https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/digest.html
But authentication is not working. When I try to authenticate it returns the error 403 Forbidden.
The DigestAuthenticationFilter does not generate "authenticated" tokens unless you explicitly tell it to do so as described here.
Add the line:
result.setCreateAuthenticatedToken(true);
to your DigestAuthenticationFilter instantiation:
DigestAuthenticationFilter digestAuthenticationFilter(UserDetailsService userDetailsService) {
DigestAuthenticationFilter result = new DigestAuthenticationFilter();
result.setUserDetailsService(userDetailsService);
result.setCreateAuthenticatedToken(true);
result.setAuthenticationEntryPoint(entryPoint());
return result;
}

Opaque Token Implementation in spring security

im trying to create a secured spring rest api for the security i want to use opaque token stored in the database so that if the client query on the api with a bearer token . the server will check on the database if the token exist if the token is valid and get the user and the privilege and check if the user have the authority to do the request. i've done some research on the net but didn't found result that can be understood by a beginner. how can i implement this.
method 1 found method 2 i have found this two methods but i dont know where too implements the database verification and validation
after a lot of research i've found this and it is working
first i've created a Authorization filter like this :
package com.example.bda_test_11.security;
import com.example.bda_test_11.model.BdaUser;
import com.example.bda_test_11.model.Token;
import com.example.bda_test_11.repository.TokenRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
#Slf4j
public class AuthorizationFilter extends BasicAuthenticationFilter {
private final TokenRepository tokenRepository;
public AuthorizationFilter(AuthenticationManager authenticationManager,TokenRepository tokenRepository) {
super(authenticationManager);
this.tokenRepository = tokenRepository;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String tokenCode = request.getHeader(HttpHeaders.AUTHORIZATION);
log.info(tokenCode);
if(tokenCode == null ) {
filterChain.doFilter(request,response);
return;
}
Token token = tokenRepository.findByCode(tokenCode).orElse(null);
if (token == null) {
filterChain.doFilter(request,response);
return;
}
BdaUser user = token.getUser();
UsernamePasswordAuthenticationToken userToken = new UsernamePasswordAuthenticationToken(user.getLogin(),null,user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(userToken);
filterChain.doFilter(request,response);
}
}
and a UsernamePasswordAuthenticationFilter like this
package com.example.bda_test_11.security;
import com.example.bda_test_11.security.domain.LoginCredentials;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
#Slf4j
public class JsonObjectAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final ObjectMapper objectMapper = new ObjectMapper();
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response){
try{
BufferedReader reader = request.getReader();
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line=reader.readLine())!=null){
stringBuilder.append(line);
}
LoginCredentials authRequest = objectMapper.readValue(stringBuilder.toString(),LoginCredentials.class);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
authRequest.getLogin(),
authRequest.getPassword()
);
setDetails(request,token);
log.info(token.toString());
return this.getAuthenticationManager().authenticate(token);
} catch (IOException e){
throw new RuntimeException(e);
}
}
}
if the connection is successful we generate a token like :
package com.example.bda_test_11.security;
import com.example.bda_test_11.model.BdaUser;
import com.example.bda_test_11.model.Token;
import com.example.bda_test_11.repository.TokenRepository;
import com.example.bda_test_11.service.BdaUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
#Slf4j #Component
public class AuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final TokenRepository tokenRepository;
private final BdaUserService userService;
#Autowired
public AuthSuccessHandler(TokenRepository tokenRepository, BdaUserService userService) {
this.tokenRepository = tokenRepository;
this.userService = userService;
}
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
UserDetails principal = (UserDetails) authentication.getPrincipal();
BdaUser user = userService.findByLogin(principal.getUsername());
Token token = new Token(user);
tokenRepository.save(token);
log.info(token.getCode());
response.addHeader("Authorization",token.getCode());
response.addHeader("Content-Type","application/json");
response.getWriter().write("{\"token\":"+token.getCode()+",\"login\":"+user.getLogin());
}
}
and then ive configured the filterChain bean like this
package com.example.bda_test_11.security;
import com.example.bda_test_11.repository.TokenRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
#Configuration
public class BdaSecurity {
private final AuthenticationManager authenticationManager;
private final AuthSuccessHandler authSuccessHandler;
private final TokenRepository tokenRepository;
#Autowired
public BdaSecurity(AuthenticationManager authenticationManager, AuthSuccessHandler authSuccessHandler, TokenRepository tokenRepository) {
this.authenticationManager = authenticationManager;
this.authSuccessHandler = authSuccessHandler;
this.tokenRepository = tokenRepository;
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.authorizeHttpRequests((auth)->{
try {
auth
.antMatchers("/api/admin").hasAuthority("ADMIN")
.antMatchers("/api/user").hasAuthority("USER")
.anyRequest().permitAll()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(authenticationFilter())
.addFilter(new AuthorizationFilter(authenticationManager,tokenRepository))
.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.httpBasic(Customizer.withDefaults());
return http.build();
}
#Bean
public JsonObjectAuthenticationFilter authenticationFilter() {
JsonObjectAuthenticationFilter filter = new JsonObjectAuthenticationFilter();
filter.setAuthenticationSuccessHandler(authSuccessHandler);
filter.setAuthenticationManager(authenticationManager);
return filter;
}
}

Spring webclient testing with okhttp3 Mockwebserver

I am unable to moock webclient
WebClientConfig.java
public #Bean("oauthWebClient") WebClient oauthWebClient() {
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create().option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000)
.doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10)))))
.filter(logOAuthResponse()).baseUrl(oauthTokenUrl)
.defaultHeaders(headers -> {
headers.setBasicAuth(secretHeaderValue);
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
})
.defaultUriVariables(oauthUriVariables)
.build();
}
serviceUti.java
package com.test.stats.volts.busops.utils;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientRequestException;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.test.stats.volts.busops.exception.VoltsDataAccessException;
import com.test.stats.volts.busops.exception.MtnDataAccessNotRespondingException;
import com.test.stats.volts.busops.exception.VoltsDataAcessException;
import com.test.stats.volts.busops.exception.UnableToGetOAuthTokenException;
#Component("voltsDataAccessServiceUtil")
public class ServiceUtil {
private static final Logger logger = LoggerFactory.getLogger(ServiceUtil.class);
#Autowired
#Qualifier("voltsDataAccessWebClient")
WebClient voltsDataAccessClient;
#Autowired
#Qualifier("oauthWebClient")
WebClient oauthClient;
#Value("${volts.dataaccess.end.point.save-meeting}")
private String saveMeetingEndPoint;
#Value("${volts.dataaccess.oauth.header-key.name}")
private String oauthHeaderKeyName;
#Value("${volts.dataccess.oauth.response.token.key-name}")
private String oauthResponseTokenKeyName;
#Value("#{${volts.dataaccess.api.endpoints.default.uri-variables}}")
private Map<String, String> voltsDataAccessUriVariables;
#Autowired
ObjectMapper mapper;
#Value("#{${volts.dataaccess.oauth.uri-variables}}")
private Map<String, String> oauthUriVariables;
#Value("${volts.dataaccess.oauth.scope}")
private String oauthScope;
#Value("${volts.dataaccess.oauth.grant_type}")
private String grantType;
#Value("${volts.dataaccess.oauth.secret.header-name}")
private String secretHeaderName;
#Value("${volts.dataaccess.oauth.secret.header-value}")
private String secretHeaderValue;
#Value("${volts.dataaccess.end.point.fetch-meeting}")
private String fetchMeetingEndPoint;
#Value("${volts.dataaccess.query.param.api-key-value}")
private String apiKey;
#Retryable(maxAttempts = 2)
public String postOAuthToken() throws VoltsDataAccessException {
try {
String respEntity = oauthClient.post()
.uri(uriBuilder -> uriBuilder.queryParam("scope", oauthScope).queryParam("grant_type", grantType)
.build())
.headers(headers -> headers.setBasicAuth(secretHeaderValue))
.retrieve()
.bodyToMono(String.class)
.block();
if (respEntity == null)
throw new UnableToGetOAuthTokenException(
"Exception while fetching OAUthToken as response from OAUth service is null resposne body");
try {
return mapper.readValue(respEntity, ObjectNode.class).get(oauthResponseTokenKeyName).asText();
} catch (JsonProcessingException e) {
logger.error("Exception while pasring token from Oauth ", e);
throw new UnableToGetOAuthTokenException(
"Exception while fetching OAUthToken as response from OAUth service is null resposne body");
}
} catch (WebClientRequestException e) {
logger.error("Request Exception to OAUth: message {}", e.getMessage(), e);
logger.error("", e);
throw new VoltsDataAccessException(e.getMessage(), HttpStatus.REQUEST_TIMEOUT.value());
}
catch (WebClientResponseException e) {
throw new VoltsDataAccessException(e.getMessage(), e.getRawStatusCode());
}
}
#Retryable(value = VoltsDataAcessException.class)
public ResponseEntity<VoltsResp> postSaveMeetingDetails(SaveVoltsReq voltsReq)
throws VoltsDataAcessException {
try {
ResponseEntity<VoltsResp> resp = voltsDataAccessClient.post()
.uri(saveMeetingEndPoint, uri -> uri.queryParam("apikey", apiKey).build())
.headers(headers -> headers.setBearerAuth(postOAuthToken()))
.contentType(MediaType.APPLICATION_JSON).bodyValue(voltsReq).retrieve()
.toEntity(VoltsResp.class).block();
return resp;
} catch (WebClientRequestException e) {
throw new VoltsDataAcessException(e.getMessage(), HttpStatus.REQUEST_TIMEOUT.value());
} catch (WebClientResponseException e) {
throw new VoltsDataAcessException(e.getMessage(), e.getRawStatusCode());
}
}
}
My test classs
WebClienrTests.java
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.function.client.WebClient;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.test.stats.volts.busops.config.WebClientConfig;
import com.test.stats.volts.busops.utils.ServiceUtil;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
#SpringBootTest(classes = { ServiceUtil.class, WebClientConfig.class,
ObjectMapper.class })
#RunWith(SpringRunner.class)
class ServiceUtilTests {
public static MockWebServer mockServer = new MockWebServer();
#MockBean
WebClientConfig webClientCondfig;
#Autowired
ServiceUtil serviceUtil;
#MockBean(name = "oauthWebClient")
WebClient oauthClient;
#MockBean(name = "voltsDataAccessWebClient")
WebClient voltsDataAccessWebClient;
#BeforeEach
void startMockServer() throws Exception {
mockServer.start(9090);
oauthClient = WebClient.create(mockServer.url("/").url().toString());
}
#AfterEach
void stopMockServer() throws Exception {
mockServer.shutdown();
}
#Test
void testPostOAuth() throws Exception {
mockServer.enqueue(new MockResponse().setResponseCode(200).setBody("OAuthToken"));
String expected = serviceUtil.postOAuthToken();
assertThat(expected).isNotNull().isEqualTo("OAuthToken");
}
}
When I run above test I get this error
java.lang.NullPointerException:
At postAuthToken() while setting headers .headers(headers -> headers.setBasicAuth(secretHeaderValue))
Please help on how to mock whole thing

UsernamePasswordAuthenticationFilter in spring Security doesn't get invoke

I wanted to pass in JSON instead of using params while logging in. So what I do is I create a filter, however, the strange thing is that the filter itself doesn't get invoke at all (Or basically when I try logging in, the request by pass it, completely ignore my filter). The request go straight to my AuthenticationHandler. I have gone through many posts and I still have no clue of why would this happen, especially when I replicate the same structure of code in Java but it works perfectly as intended...
Did I miss something obvious? Here's the UsernamePasswordAuthenticationFilter and my security config. My Java version works fine, but my Kotlin version completely ignores the filter.
It doesn't return 404 as well, it returns my AuthenticationFailureHandler.
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.ObjectMapper
import lombok.Getter
import org.apache.commons.io.IOUtils
import org.springframework.http.HttpMethod
import org.springframework.security.authentication.AuthenticationServiceException
import org.springframework.security.authentication.InternalAuthenticationServiceException
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import java.io.IOException
import java.nio.charset.Charset
class JsonLoginFilter : UsernamePasswordAuthenticationFilter() {
#Throws(AuthenticationException::class)
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse?): Authentication {
if (!HttpMethod.POST.matches(request.method)) {
throw AuthenticationServiceException("Authentication method not supported: " + request.method)
}
val payload: String
try {
payload = IOUtils.toString(request.inputStream, Charset.defaultCharset())
val auth = ObjectMapper().readValue(payload, JsonAuthenticationParser::class.java)
// println(auth.username)
// println(auth.password)
val authRequest = UsernamePasswordAuthenticationToken(auth.username, auth.password)
return this.authenticationManager.authenticate(authRequest)
} catch (e: IOException) {
throw InternalAuthenticationServiceException("Could not parse authentication payload")
}
}
#Getter
data class JsonAuthenticationParser #JsonCreator
constructor(#param:JsonProperty("username")
val username: String,
#param:JsonProperty("password")
val password: String)
}
My Security config in Kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler
#EnableWebSecurity
class WebSecurity: WebSecurityConfigurerAdapter() {
#Autowired
private lateinit var entryConfig: EntryConfig
#Autowired
private lateinit var failAuth: FailAuthentication
#Autowired
private lateinit var successAuthentication: SuccessAuthentication
#Autowired
private lateinit var authenticationHandler: AuthenticationHandler
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.antMatchers("/api/v1/traveller/add","/api/v1/symptoms","/api/v1/flights","/api/v1/user/login","/api/v1/user/logout").permitAll()
.antMatchers("/api/v1/user/**","/api/v1/traveller/**").hasRole("ADMIN")
.antMatchers("/**").authenticated()
.and()
.addFilterAt(authenFilter(), UsernamePasswordAuthenticationFilter::class.java)
.formLogin().loginProcessingUrl("/api/v1/user/login")
.successHandler(successAuthentication).failureHandler(failAuth)
.and()
.exceptionHandling().authenticationEntryPoint(entryConfig)
.and()
.cors()
.and()
.logout().logoutUrl("/api/v1/user/logout")
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.logoutSuccessHandler(HttpStatusReturningLogoutSuccessHandler())
.permitAll()
//
http
.csrf()
.disable()
}
#Throws(Exception::class)
override fun configure(auth: AuthenticationManagerBuilder) {
auth.authenticationProvider(authenticationHandler)
}
#Bean
#Throws(Exception::class)
fun authenFilter(): JsonLoginFilter {
var filter : JsonLoginFilter = JsonLoginFilter()
filter.setAuthenticationManager(authenticationManagerBean())
filter.setAuthenticationSuccessHandler(successAuthentication)
filter.setAuthenticationFailureHandler(failAuth)
return filter
}
#Bean
fun passwordEncoder(): BCryptPasswordEncoder {
return BCryptPasswordEncoder()
}
}
My Java version, slightly differ but I believe it should have the same structure
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import org.apache.commons.io.IOUtils;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.Charset;
public class JsonAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("hello");
if (! HttpMethod.POST.matches(request.getMethod())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String payload;
try {
payload = IOUtils.toString(request.getInputStream(), Charset.defaultCharset());
JsonAuthenticationParser auth = new ObjectMapper().readValue(payload, JsonAuthenticationParser.class);
System.out.println(auth.username);
System.out.println(auth.password);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(auth.username, auth.password);
return this.getAuthenticationManager().authenticate(authRequest);
} catch (IOException e) {
throw new InternalAuthenticationServiceException("Could not parse authentication payload");
}
}
#Getter
static class JsonAuthenticationParser {
private final String username;
private final String password;
#JsonCreator
public JsonAuthenticationParser(#JsonProperty("username") String username, #JsonProperty("password") String password) {
this.username = username;
this.password = password;
}
}
}
Security config in Java
import hard.string.security.AuthenticationHandler;
import hard.string.security.EntryConfig;
import hard.string.security.FailAuthhentication;
import hard.string.security.SuccessAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
#Autowired
private EntryConfig entryConfig;
#Autowired
private FailAuthhentication failAuth;
#Autowired
private SuccessAuthentication successAuthentication;
#Autowired
private AuthenticationHandler authenticationHandler;
#Bean
public JsonAuthenticationFilter authenticationFilter() throws Exception {
JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());
// filter.setContinueChainBeforeSuccessfulAuthentication(true);
filter.setAuthenticationSuccessHandler(successAuthentication);
filter.setAuthenticationFailureHandler(failAuth);
return filter;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// http://stackoverflow.com/questions/19500332/spring-security-and-json-authentication
http
.authorizeRequests()
.antMatchers("/login", "/logout", "/register",
"/debug/**").permitAll()
.antMatchers("/**").authenticated()
.and()
.addFilterAt(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.formLogin().loginProcessingUrl("/login")
.successHandler(successAuthentication).failureHandler(failAuth)
.and()
.exceptionHandling().authenticationEntryPoint(entryConfig)
.and()
.cors()
.and()
.logout().logoutUrl("/logout")
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
.permitAll();
//
http
.csrf()
.disable();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationHandler);
}
#Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
Thanks for the help
Ok, After spending days to find the bug. I found out that the filter doesn't automatically link with loginProcessingUrl. You need to specify what url you want to do the filter on or else it will just apply the filter only to localhost:xxxx/login
I just going to leave this question up here just in case someone run into this stupid problem like myself.
fun authenFilter(): JsonLoginFilter {
var filter : JsonLoginFilter = JsonLoginFilter()
filter.setAuthenticationManager(authenticationManagerBean())
filter.setAuthenticationSuccessHandler(successAuthentication)
filter.setAuthenticationFailureHandler(failAuth)
filter.setFilterProcessesUrl("/api/v1/user/login") //HERE
return filter
}

Resources