How to set HostnameVerifier in Feign-Client from spring-cloud-netflix-feign - java

I'm trying to setup my Spring Cloud Feign Client to use custom HostnameVerifier. I need the custom HostnameVerifier to ignore certificate problems. How can I do that?
here is my current config:
#FeignClient(name = "AccountSettingsClient", url = "${account.settings.service.url}", decode404 = true,
configuration = AccountSettingsClientConfig.class, fallbackFactory = AccountSettingsClientFallbackFactory.class)
public interface AccountSettingsClient {
#RequestMapping(method = RequestMethod.GET, value = "/settings/{uuid}")
AccountSettings accountSettings(#PathVariable("uuid") String uuid);
}
#Component
#Slf4j
class AccountSettingsClientFallbackFactory implements FallbackFactory<AccountSettingsClient> {
#Override
public AccountSettingsClient create(Throwable cause) {
return uuid -> {
log.warn("Falling back to null.", cause);
return null;
};
}
}
#Configuration
#RequiredArgsConstructor
#EnableConfigurationProperties(SomeProperties.class)
#EnableFeignClients
public class AccountSettingsClientConfig {
private final SomeProperties someProperties;
#Bean
RequestInterceptor oauth2FeignRequestInterceptor() {
return new OAuth2FeignRequestInterceptor(new
DefaultOAuth2ClientContext(), resource());
}
}

By default there gets created a LoadBalancerFeignClient with a HttpURLConnection on board and can't override settings of HostnameVerifier of it. In order to override it choose another client like OkHttp or Apache Http Client, add correspondent maven dependency and then you can override the client with all settings.
I chose the OkHttpClient and added to my AccountSettingsConfig follow bean:
#Bean
public okhttp3.OkHttpClient okHttpClient() {
return new OkHttpClient.Builder().hostnameVerifier((s, sslSession) -> true)
.build();
}

Related

Spring Boot & WebClient - how to add additional headers using #ConditionalOnProperty to a specific WebClient instance?

I have a service that is holding an WebClient instance:
#Service
#Getter
public class SomeApiWebClient {
private final WebClient webClient;
private final String someApiUrl;
public SomeApiWebClient(#Value("${some.api.url}") String someApiUrl) {
this.someApiUrl= someApiUrl;
this.webClient = WebClient.builder()
.baseUrl(someApiUrl)
.defaultHeaders(getDefaultHttpHeaders())
.build();
}
public Consumer<HttpHeaders> getDefaultHttpHeaders() {
return headers -> {
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setContentType(MediaType.APPLICATION_JSON);
};
}
}
To call this API from UAT/PROD environments - this is just fine. But to call it from our local machines, we need to use Client-ID and Secret HTTP headers:
public Consumer<HttpHeaders> getDefaultHttpHeaders() {
return headers -> {
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add("Client-Id", "someId");
headers.add("Secret", "someSecret");
};
}
I have put the someId and someSecret values into application-dev.properties:
some.api.client-id=someId
some.api.token=someSecret
Now I would like to use #Configuration combined with #ConditionalOnProperty({"some.api.client-id", "some.api.token"}) to intercept and add a filter to MDSWebClient that will add those headers when the some.api.client-id and some.api.token are present.
I have tried doing something like this:
#Configuration
#ConditionalOnProperty({"some.api.client-id", "some.api.token"})
public class MDSWebClientInterceptor implements WebFilter {
#Value("some.api.client-id")
private String clientId;
#Value("some.api.token")
private String token;
private static final String CLIENT_ID_HEADER = "Client-Id";
private static final String CLIENT_TOKEN_HEADER = "Secret";
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(
exchange.mutate().request(
exchange.getRequest().mutate()
.header(CLIENT_ID_HEADER, clientId)
.header(CLIENT_TOKEN_HEADER, token)
.build())
.build());
}
}
But this doesn't work at all, and I have a hunch that if it would work, it would affect ALL WebClient instances, not just the one in SomeApiWebClient.
Is something like this even possible?

How to set urls with port of StubRunner from spring-cloud-contract in spring-cloud-gateway contract tests

I have a spring cloud gateway application which routes requests to another service. Another service defines contracts which are imported as stubs by spring cloud gateway application in tests.
Now I would like to have contract tests in my gateway that will consume the stubs of another service. The problem is that I do not know how to inject the StubRunnerPort as property/environment so it can be picked by my configuration class and configure the routes accordingly :
Api gateway routes configuration
#Configuration
class GatewayConfig {
#Value("${subscriptions.url}")
private String subscriptionsUrl;
#Autowired
private TokenRelayGatewayFilterFactory tokenFilterFactory;
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.csrf(ServerHttpSecurity.CsrfSpec::disable);
return http.build();
}
#Bean
RouteLocator routeLocator(final RouteLocatorBuilder routeLocatorBuilder) {
return routeLocatorBuilder.routes()
.route("subscriptions", subscriptionsRoute())
.build();
}
private Function<PredicateSpec, Buildable<Route>> subscriptionsRoute() {
return spec -> spec
.path("/subscriptions/**")
.filters(s -> s.filter(tokenFilterFactory.apply()).prefixPath("/v1"))
.uri(subscriptionsUrl);
}
}
And the test class :
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {PnApiGatewayApp.class})
#AutoConfigureStubRunner(ids = "io.mkrzywanski:subscription-app:+:stubs", stubsMode = StubRunnerProperties.StubsMode.CLASSPATH)
#ActiveProfiles("test")
class SubscriptionSpec {
private WebTestClient webClient;
#LocalServerPort
private int port;
#StubRunnerPort("io.mkrzywanski:subscription-app")
private int stubRunnerPort;
#Autowired
ConfigurableEnvironment environment;
#BeforeEach
void setup() {
String baseUri = "http://localhost:" + port;
this.webClient = WebTestClient.bindToServer()
.responseTimeout(Duration.ofSeconds(10))
.baseUrl(baseUri).build();
}
#Test
void test() {
String body = "{\"userId\":\"22e90bbd-7399-468a-9b76-cf050ff16c63\",\"itemSet\":[{\"value\":\" Rainbow Six\"}]}";
var response = webClient.post()
.uri("/subscriptions")
.header("Authorization", "Bearer xxx")
.header("Content-type", MediaType.APPLICATION_JSON_VALUE)
.bodyValue(body)
.exchange()
.expectStatus().isCreated()
.expectBody(String.class)
.value(Matchers.equalTo("{\"subscriptionId : \"6d692849-58fd-439b-bb2c-50a5d3669fa9\"\"}"));
}
Ideally I would like to have subscriptions.url property set after stub runner is configured but before my gateway configuration is picked by Spring so that url redirects will work.
I have already tried to use ApplicationContextInitializer but it seems that StubRunnerPort is not configured yet, when instance of initializer is launched.
So the question is - how to get stub runner port and use it to inject it into other services url, so that gateway would route the requests to the stub runner in tests?
Solution 1
This can be achieved by using application-test.yml file with defined url property that makes use of substitution. application-test.yml file. The approach is described here :
subscriptions:
url: http://localhost:${stubrunner.runningstubs.io.mkrzywanski.subscription-app.port}
Here stubrunner.runningstubs.io.mkrzywanski.subscription-app.port will be available as Stub port so it can be substituted. No configuration changes are reuqired.
Solution 2 (requires more code)
I made it work by creating a test configuration which extends configuration that contains url properties and RouteLocator configuration and has a dependency on batchStubRunner bean :
#DependsOn("batchStubRunner")
#EnableAutoConfiguration
#Import(LoggingFilter.class)
class GatewayTestConfig extends GatewayConfig implements InitializingBean {
#Autowired
ConfigurableEnvironment environment;
#Override
public void afterPropertiesSet() {
this.subscriptionsUrl = "http://localhost:" + environment.getProperty("stubrunner.runningstubs.io.mkrzywanski.subscription-app.port");
}
}
The key points here are :
the configuration is run only after batchStubRunner bean is available so the port of StrubRunner can be found in the environment
The configuration implements InitializingBean so I am able to override the subscriptionsUrl which is now protected in parent configuration
After subscriptionsUrl is overriden - it can be used to configure RouteLocator bean from parent configuration.
The test looks like this now :
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {GatewayTestConfig.class})
#AutoConfigureStubRunner(ids = "io.mkrzywanski:subscription-app:+:stubs", stubsMode = StubRunnerProperties.StubsMode.CLASSPATH)
#ActiveProfiles("test")
class SubscriptionSpec {
private WebTestClient webClient;
#LocalServerPort
private int port;
#BeforeEach
void setup() {
String baseUri = "http://localhost:" + port;
this.webClient = WebTestClient.bindToServer()
.responseTimeout(Duration.ofSeconds(10))
.baseUrl(baseUri).build();
}
#Test
void shouldRouteToSubscriptions() {
String body = "{\"userId\":\"22e90bbd-7399-468a-9b76-cf050ff16c63\",\"itemSet\":[{\"value\":\"Rainbow Six\"}]}";
webClient.post()
.uri("/subscriptions")
.header("Accept", MediaType.APPLICATION_JSON_VALUE)
.header("Authorization", "Bearer xxx")
.header("Content-type", MediaType.APPLICATION_JSON_VALUE)
.bodyValue(body)
.exchange()
.expectStatus().isCreated()
.expectBody()
.jsonPath("$.subscriptionId").exists()
.jsonPath("$.subscriptionId").value(IsUUID.UUID());
}
}

Get request url in feign client interceptor

I am using spring feign client for making http requests.
Fiegn configuration class
DefaultConfig.class
public class DefaultConfig {
#Bean
public OkHttpClient client() {
return new OkHttpClient();
}
#Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
#Bean
public Decoder feignDecoder() {
return new JacksonDecoder();
}
#Bean
public Encoder feignEncoder() {
return new JacksonEncoder();
}
#Bean
public RequestInterceptor requestInterceptor() {
return template -> {
template.header(Authorization, apiKey);
};
}
}
My client interface:
#FeignClient(name = "default", url = "${base-url}",
configuration = DefaultConfig.class)
public interface {
#PostMapping(value = "/users/")
Response createUser(#RequestBody Map<String, ?> requestBody);
#GetMapping(value = "/users/{id}")
Response getUserDetails(#PathVariable String id);
}
The thing is now I need to use different authorization key for GET and POST request. In the configuration class I need to differentiate the request by url called. How can we get the url the requester interceptor or any other way we can achieve this. We can create separate interceptor for this but I try to use the same interceptor for both cases.
Everything you need seems to be in the template object.
#Bean
public RequestInterceptor requestInterceptor() {
return template -> {
if ("GET".equals(template.method()) {
template.header(Authorization, apiKey);
} else {
template.header(Authorization, differentApiKey);
}
};
}
Everything you need seems to be in the code below.
#Bean.
public RequestInterceptor requestInterceptor() {
return template -> {
template.request().url() // this is request url which is from feign!
template.requsst().httpMethod() // this is request method!
};
}

Spring Boot 1.5 sessionRegistry().getAllPrincipals() always returns empty list

I inherited a Spring Boot 1.5 project which cannot be migrated up at the moment and need to work on session registry to manage users (ex: list logged users, email users of production updates etc.)
I tried all existing SO solutions I could find, but all of them provide a null result for sessionRegistry().getAllPrincipals(). My security config looks like:
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.headers().frameOptions().sameOrigin()
.and()
.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry());
}
#Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
#Bean
public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
return new ServletListenerRegistrationBean<>(new HttpSessionEventPublisher());
}
}
My Application config looks like:
#EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600000)
#EnableDiscoveryClient
#EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
#EnableZuulProxy
#EnableFeignClients
#EnableSwagger2
#SpringBootApplication
#RibbonClients({
#RibbonClient(name = "employeeService", configuration = StickySessionEditorRibbonConfiguration.class)
})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public PreFilter preFilter() {
return new PreFilter();
}
#Bean
public PostFilter postFilter() {
return new PostFilter();
}
#Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
#Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
serializer.setCookieMaxAge(3600000);
return serializer;
}
}
Relevant code to access the session registry looks like this:
public class TestController extends BaseController {
#Autowired
private SessionRegistry sessionRegistry;
...
public List<User> findAllLoggedInUsers() {
final List<Object> allPrincipals = sessionRegistry.getAllPrincipals();
}
}
Using the actuator/bean endpoint I can see that the SessionRegistry Bean is active.
I am logging in successfully from a couple of browsers but allPrincipals is always size 0 before and after the logins.
Am at a loss why, and any help here is much appreciated.
Based on #M.Deinum 's comment regarding disabled login I want to add that the project uses Zuul Filters (preFilter and PostFilter) as indicated in the application config. We have an account-manager service completely different from this api-gateway service, which authenticates users based on simple login/password.
The logic in preFilter looks like this:
public class PreFilter extends BaseFilter {
#Autowired
SessionRegistry sessionRegistry;
#Autowired
private SessionRepository sessionRepository;
#Override
public String filterType() {
return "pre";
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest req = ctx.getRequest();
HttpSession session = req.getSession();
try {
String uri = req.getRequestURI();
// user assumed is not authenticated
String authToken = null;
//Login code
if (uri.contains("/api/public/authorization/login") && req.getMethod().equals("POST")) {
session.removeAttribute(AUTH_TOKEN_HEADER);
LoginRequest request = createLoginRequest(req);
/* LoginRequest basically contains "userName" and "password" entered by user */
ResponseEntity<MessageWrapper<String>> response = accountManagerFeignClient.authenticate(loginRequest);
authToken = response.getBody().getData();
if (authToken != null) {
session.setAttribute(AUTH_TOKEN_HEADER, authToken);
ctx.setResponseStatusCode(HttpStatus.OK.value());
ctx.setSendZuulResponse(false);
return null;
}
// authToken == null implies the user was not authenticated by accountManager
} else if ("internal or public apis are called, they won't need authentication") {
// user remains unauthenticated, which is fine for public or internal apis
return null;
} else {
// Assume this is a protected API and provide authToken if one exists
authToken = (String) session.getAttribute(AUTH_TOKEN_HEADER);
}
if (authToken == null)
throw new Exception(UNAUTHORIZED + ". Log String: " + logString);
// Validated user will go through here
ctx.addZuulRequestHeader(AUTH_TOKEN_HEADER, authToken);
} catch (Exception ex) {
ctx.setResponseBody(UNAUTHORIZED);
ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
ctx.setSendZuulResponse(false);
}
return null;
}
}
The only relevant logic in postFilter (similar to preFilter) disables sessions during logout in this manner:
if (authToken != null) {
session.removeAttribute(AUTH_TOKEN_HEADER);
session.removeAttribute(StickySessionEditorRule.STICKY_ID);
session.removeAttribute(StickySessionWSGRule.STICKY_ID);
ctx.setResponseBody(LOGGED_OUT);
ctx.setResponseStatusCode(HttpStatus.OK.value());
ctx.setSendZuulResponse(false);
}
session.invalidate();
My other time-consuming option would be to use the HTTPSessionBindingListener as shown here . I have not tried that yet.
Finally, if none of the above work, how could I work directly with redis and do a findAll() ? It looks like there is a SessionRepository, but I could not find a documented way of using it.
Thank You.

How to mock JWT authentication in a Spring Boot Unit Test?

I have added JWT Authentication using Auth0 to my Spring Boot REST API following this example.
Now, as expected, my previously working Controller unit tests give a response code of401 Unauthorized rather than 200 OK as I am not passing any JWT in the tests.
How can I mock the JWT/Authentication part of my REST Controller tests?
Unit test class
#AutoConfigureMockMvc
public class UserRoundsControllerTest extends AbstractUnitTests {
private static String STUB_USER_ID = "user3";
private static String STUB_ROUND_ID = "7e3b270222252b2dadd547fb";
#Autowired
private MockMvc mockMvc;
private Round round;
private ObjectId objectId;
#BeforeEach
public void setUp() {
initMocks(this);
round = Mocks.roundOne();
objectId = Mocks.objectId();
}
#Test
public void shouldGetAllRoundsByUserId() throws Exception {
// setup
given(userRoundService.getAllRoundsByUserId(STUB_USER_ID)).willReturn(
Collections.singletonList(round));
// mock the rounds/userId request
RequestBuilder requestBuilder = Requests.getAllRoundsByUserId(STUB_USER_ID);
// perform the requests
MockHttpServletResponse response = mockMvc.perform(requestBuilder)
.andReturn()
.getResponse();
// asserts
assertNotNull(response);
assertEquals(HttpStatus.OK.value(), response.getStatus());
}
//other tests
}
Requests class (used above)
public class Requests {
private Requests() {}
public static RequestBuilder getAllRoundsByUserId(String userId) {
return MockMvcRequestBuilders
.get("/users/" + userId + "/rounds/")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON);
}
}
Spring Security Config
/**
* Configures our application with Spring Security to restrict access to our API endpoints.
*/
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${auth0.audience}")
private String audience;
#Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
#Override
public void configure(HttpSecurity http) throws Exception {
/*
This is where we configure the security required for our endpoints and setup our app to serve as
an OAuth2 Resource Server, using JWT validation.
*/
http.cors().and().csrf().disable().sessionManagement().
sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
.mvcMatchers(HttpMethod.GET, "/users/**").authenticated()
.mvcMatchers(HttpMethod.POST, "/users/**").authenticated()
.mvcMatchers(HttpMethod.DELETE, "/users/**").authenticated()
.mvcMatchers(HttpMethod.PUT, "/users/**").authenticated()
.and()
.oauth2ResourceServer().jwt();
}
#Bean
JwtDecoder jwtDecoder() {
/*
By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is
indeed intended for our app. Adding our own validator is easy to do:
*/
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
JwtDecoders.fromOidcIssuerLocation(issuer);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer,
audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Abstract Unit test class
#ExtendWith(SpringExtension.class)
#SpringBootTest(
classes = PokerStatApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
public abstract class AbstractUnitTests {
// mock objects etc
}
If I understand correctly your case there is one of the solutions.
In most cases, JwtDecoder bean performs token parsing and validation if the token exists in the request headers.
Example from your configuration:
#Bean
JwtDecoder jwtDecoder() {
/*
By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is
indeed intended for our app. Adding our own validator is easy to do:
*/
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
JwtDecoders.fromOidcIssuerLocation(issuer);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
So for the tests, you need to add stub of this bean and also for replacing this bean in spring context, you need the test configuration with it.
It can be some things like this:
#TestConfiguration
public class TestSecurityConfig {
static final String AUTH0_TOKEN = "token";
static final String SUB = "sub";
static final String AUTH0ID = "sms|12345678";
#Bean
public JwtDecoder jwtDecoder() {
// This anonymous class needs for the possibility of using SpyBean in test methods
// Lambda cannot be a spy with spring #SpyBean annotation
return new JwtDecoder() {
#Override
public Jwt decode(String token) {
return jwt();
}
};
}
public Jwt jwt() {
// This is a place to add general and maybe custom claims which should be available after parsing token in the live system
Map<String, Object> claims = Map.of(
SUB, USER_AUTH0ID
);
//This is an object that represents contents of jwt token after parsing
return new Jwt(
AUTH0_TOKEN,
Instant.now(),
Instant.now().plusSeconds(30),
Map.of("alg", "none"),
claims
);
}
}
For using this configuration in tests just pick up this test security config:
#SpringBootTest(classes = TestSecurityConfig.class)
Also in the test request should be authorization header with a token like Bearer .. something.
Here is an example regarding your configuration:
public static RequestBuilder getAllRoundsByUserId(String userId) {
return MockMvcRequestBuilders
.get("/users/" + userId + "/rounds/")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer token"))
.contentType(MediaType.APPLICATION_JSON);
}
For me, I made it pretty simple.
I don't want to actually check for the JWT token, this can also be mocked.
Have a look at this security config.
#Override
public void configure(HttpSecurity http) throws Exception {
//#formatter:off
http
.cors()
.and()
.authorizeRequests()
.antMatchers("/api/v1/orders/**")
.authenticated()
.and()
.authorizeRequests()
.anyRequest()
.denyAll()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.oauth2ResourceServer()
.jwt();
Then in my test, I make use of two thing
Provide a mock bean for the jwtDecoder
Use the SecurityMockMvcRequestPostProcessors to mock the JWT in the request. This is available in the following dependency
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
And Here is how it's done.
#SpringBootTest
#AutoConfigureMockMvc
public class OrderApiControllerIT {
#Autowired
protected MockMvc mockMvc;
#MockBean
private JwtDecoder jwtDecoder;
#Test
void testEndpoint() {
MvcResult mvcResult = mockMvc.perform(post("/api/v1/orders")
.with(SecurityMockMvcRequestPostProcessors.jwt())
.content(jsonString)
.contentType(MediaType.APPLICATION_JSON)
)
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andReturn();
}
That's it and it should work.
For others like me, who after gathering information from what seems like a gazillion StackOverlow answers on how to do this, here is the summary of what ultimately worked for me (using Kotlin syntax, but it is applicable to Java as well):
Step 1 - Define a custom JWT decoder to be used in tests
Notice the JwtClaimNames.SUB entry - this is the user name which will ultimately be accessible via authentication.getName() field.
val jwtDecoder = JwtDecoder {
Jwt(
"token",
Instant.now(),
Instant.MAX,
mapOf(
"alg" to "none"
),
mapOf(
JwtClaimNames.SUB to "testUser"
)
)
}
Step 2 - Define a TestConfiguration
This class goes in your test folder. We do this to replace real implementation with a stub one which always treats the user as authenticated.
Note that we are not done yet, check Step 3 as well.
#TestConfiguration
class TestAppConfiguration {
#Bean // important
fun jwtDecoder() {
// Initialize JWT decoder as described in step 1
// ...
return jwtDecoder
}
}
Step 3 - Update your primary configuration to avoid bean conflict
Without this change your test and production beans would clash, resulting in a conflict. Adding this line delays the resolution of the bean and lets Spring prioritise test bean over production one.
There is a caveat, however, as this change effectively removes bean conflict protection in production builds for JwtDecoder instances.
#Configuration
class AppConfiguration {
#Bean
#ConditionalOnMissingBean // important
fun jwtDecoder() {
// Provide decoder as you would usually do
}
}
Step 4 - Import TestAppConfiguration in your test
This makes sure that your test actually takes TestConfiguration into account.
#SpringBootTest
#Import(TestAppConfiguration::class)
class MyTest {
// Your tests
}
Step 5 - Add #WithMockUser annotation to your test
You do not really need to provide any arguments to the annotation.
#Test
#WithMockUser
fun myTest() {
// Test body
}
Step 6 - Provide Authentication header during the test
mockMvc
.perform(
post("/endpointUnderTest")
.header(HttpHeaders.AUTHORIZATION, "Bearer token") // important
)
.andExpect(status().isOk)
SecurityConfig bean can be loaded conditionally as,
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Bean
#Profile("!test")
public WebSecurityConfigurerAdapter securityEnabled() {
return new WebSecurityConfigurerAdapter() {
#Override
protected void configure(HttpSecurity http) throws Exception {
// your code goes here
}
};
}
#Bean
#Profile("test")
public WebSecurityConfigurerAdapter securityDisabled() {
return new WebSecurityConfigurerAdapter() {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll();
}
};
}
}
So this bean won't be initialized in case of test profile. It means now security is disabled and all endpoints are accessible without any authorization header.
Now "test" profile needs to be active in case of running the tests, this can be done as,
#RunWith(SpringRunner.class)
#ActiveProfiles("test")
#WebMvcTest(UserRoundsController.class)
public class UserRoundsControllerTest extends AbstractUnitTests {
// your code goes here
}
Now this test is going to run with profile "test".
Further if you want to have any properties related to this test, that can be put under src/test/resources/application-test.properties.
Hope this helps! please let me know otherwise.
Update:
Basic idea is to disable security for test profile. In previous code, even after having profile specific bean, default security was getting enabled.
You can get the Bearer token and pass it on as a HTTP Header. Below is a sample snippet of the Test Method for your reference,
#Test
public void existentUserCanGetTokenAndAuthentication() throws Exception {
String username = "existentuser";
String password = "password";
String body = "{\"username\":\"" + username + "\", \"password\":\"
+ password + "\"}";
MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/token")
.content(body))
.andExpect(status().isOk()).andReturn();
String response = result.getResponse().getContentAsString();
response = response.replace("{\"access_token\": \"", "");
String token = response.replace("\"}", "");
mvc.perform(MockMvcRequestBuilders.get("/users/" + userId + "/rounds")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}
I am using the JwtAuthenticationToken from the Security Context. The #WithMockUser annotation is creating a Username-based Authentication Token.
I wrote my own implementation of #WithMockJwt:
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Inherited
#Documented
#WithSecurityContext(factory = WithMockJwtSecurityContextFactory.class)
public #interface WithMockJwt {
long value() default 1L;
String[] roles() default {};
String email() default "ex#example.org";
}
And the related factory:
public class WithMockJwtSecurityContextFactory implements WithSecurityContextFactory<WithMockJwt> {
#Override
public SecurityContext createSecurityContext(WithMockJwt annotation) {
val jwt = Jwt.withTokenValue("token")
.header("alg", "none")
.claim("sub", annotation.value())
.claim("user", Map.of("email", annotation.email()))
.build();
val authorities = AuthorityUtils.createAuthorityList(annotation.roles());
val token = new JwtAuthenticationToken(jwt, authorities);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(token);
return context;
}
}
And now I can annotate test with:
#Test
#WithMockJwt
void test() {
...omissis...
create application.properties in test/resources (it will override main but for test stage only)
turnoff security by specifyig:
security.ignored=/**
security.basic.enable= false
spring.autoconfigure.exclude= org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration

Categories