In RestTemplate, I have configured two ClientHttpRequestInterceptor (one for BasicAuthorization and another for Token based authentication.
From client side how I ask RestTemplate to use the correct ClientHttpRequestInterceptor to execute the API call.
Some API calls require BasicAuthorization to work. (For Ex: if the URL starts with "/admin" require BasicAuthorization, others require Token based Authentication)
How I can achieve this in Spring 4?
You could use two instances of RestTemplate, one for Basic auth and one for Token auth.
#Bean
#Qualifier("authRestTemplate")
public RestTemplate getAuthTemplate{
// create rest template, add auth interceptor
}
#Bean
#Qualifier("tokenRestTemplate")
public RestTemplate getTokenTemplate{
// create rest template, add token interceptor
}
Then, when autowiring the RestTemplate, use the desired #Qualifier
#Autowired
#Qualifier("authRestTemplate")
private RestTemplate authTemplate;
#Autowired
#Qualifier("tokenRestTemplate")
private RestTemplate tokenTemplate;
Another option would be adding two ClientHttpRequestInterceptor to the RestTemplate
class BasicAuthInterceptor implements ClientHttpRequestInterceptor {
private final AuthService authService;
public BasicAuthHeaderInterceptor(AuthService authService) {
this.authService = authService;
}
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
if(isApplicable(request)){
String token = Base64Utils.encodeToString((authService.getUsername() + ":" + authService.getpassword()).getBytes(Charset.forName("UTF-8")));
request.getHeaders().add("Authorization", "Basic " + token);
}
return execution.execute(request, body);
}
}
class TokenInterceptor implements ClientHttpRequestInterceptor {
private final AuthService authService;
public TokenHeaderInterceptor(AuthService authService) {
this.authService = authService;
}
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
if(isApplicable(request)){
request.getHeaders().add("Authorization", "Bearer " + tokenService.getToken());
}
return execution.execute(request, body);
}
}
Then, add the two interceptors to the RestTemplate
#Bean
public RestTemplate restTemplate(){
RestTemplate template = new RestTemplate();
template.getInterceptors().add(new BasicAuthInterceptor(authService));
template.getInterceptors().add(new TokenInterceptor(authService));
return template;
}
Related
I am trying to get ClientHttpRequestInterceptor working following, Baeldung's Spring RestTemplate Request/Response Logging. The problem is the ClientHttpRequestInterceptor never gets called.
I had a similar issue with a HandlerInterceptor and a HandlerInterceptorAdapter interceptors. The issue was I needed a add a listener, that 99% of the article I found did not mention.
#Configuration
public class ListenerConfig implements WebApplicationInitializer {
#Override
public void onStartup(ServletContext sc) throws ServletException {
sc.addListener(new RequestContextListener());
}
}
I am guessing something about Spring has changed and the listeners are not there by default.
Does anyone know the listener for ClientHttpRequestInterceptor?
The interceptor:
public class LoggingInterceptor implements ClientHttpRequestInterceptor {
static Logger LOGGER = LoggerFactory.getLogger(LoggingInterceptor.class);
#Override
public ClientHttpResponse intercept(
HttpRequest req, byte[] reqBody, ClientHttpRequestExecution ex) throws IOException {
LOGGER.debug("Request body: {}", new String(reqBody, StandardCharsets.UTF_8));
ClientHttpResponse response = ex.execute(req, reqBody);
InputStreamReader isr = new InputStreamReader(
response.getBody(), StandardCharsets.UTF_8);
String body = new BufferedReader(isr).lines()
.collect(Collectors.joining("\n"));
LOGGER.debug("Response body: {}", body);
return response;
}
}
RestClientConfig:
#Configuration
public class RestClientConfig {
#Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> interceptors
= restTemplate.getInterceptors();
if (CollectionUtils.isEmpty(interceptors)) {
interceptors = new ArrayList<>();
}
interceptors.add(new LoggingInterceptor());
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
}
This may be related to ClientHttpRequestInterceptor not called in springboot.
The Spring RestTemplate Interceptor, only works on the client(consumer) side or in test where the tests are acting as the client where you have a RestTemplate. I am trying to log on the server(producer) side. Thus, no need for a RestTemplate.
A Loredana with Bealdung.com was nice enough to email me and point out, I had a misconception.
Somewhere I had picked up Spring used the RestTemplate for the auto decoding and encoding of POJOs in Rest controllers. That is incorrect.
#Tashkhisi was also point to the lack a RestTempale in the commets.
I'm learning about securing microservices with Basic Authentication and OAuth2 JWT Token Authentication. I implemented it using Basic Authentication and now I want to transform it in OAuth2 Authentication.
This is the implementation for securing the communication between these 2 microservices using Basic Auth.
Microservice 1 - REST API
#Configuration
#Getter
public class DemoApiConfiguration {
#Value("${demo.api.credentials.username}")
private String username;
#Value("${demo.api.credentials.password}")
private String password;
}
SecurityConfigurer class:
#Configuration
#RequiredArgsConstructor
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
private final DemoApiConfiguration apiConfig;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and()
.httpBasic();
}
#Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails theUser = User.withUsername(apiConfig.getUsername())
.password(passwordEncoder.encode(apiConfig.getPassword())).roles("USER").build();
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(theUser);
return userDetailsManager;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Controller class:
#RestController
#RequestMapping("/rest/api/v1")
public class HomeController {
#GetMapping("/products")
public String home() {
return "These are products!";
}
}
application.yml:
demo:
api:
credentials:
username: ${demo_api_username:john}
password: ${demo_api_password:test}
Microservice 2 - REST Consumer
#Configuration
#Getter
public class DemoApiConfiguration {
#Value("${demo.api.credentials.username}")
private String username;
#Value("${demo.api.credentials.password}")
private String password;
#Value("${demo.api.credentials.basePath}")
private String basePath;
}
WebConfigurer class:
#Configuration
#RequiredArgsConstructor
public class WebConfigurer {
private final DemoApiConfiguration apiConfig;
#Bean
public ApiClient restTemplate() {
RestTemplate restTemplate = new RestTemplate();
ApiClient apiClient = new ApiClient(restTemplate);
apiClient.setBasePath(apiConfig.getBasePath());
return apiClient;
}
public String getAuthorization() {
return (!StringUtils.isEmpty(apiConfig.getUsername()) &&
!StringUtils.isEmpty(apiConfig.getPassword())) ?
"Basic " + Base64Utils.encodeToString((
apiConfig.getUsername() + ":" + apiConfig.getPassword())
.getBytes()) :
null;
}
}
ApiClient class:
#Getter
#RequiredArgsConstructor
#Slf4j
public class ApiClient {
private static final String AUTHORIZATION_HEADER = "Authorization";
private final RestTemplate restTemplate;
private String basePath;
public ApiClient setBasePath(String basePath) {
this.basePath = basePath;
return this;
}
public String invokeApi(String path, String credentials) {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(basePath).path(path);
RequestEntity.BodyBuilder requestBuilder =
RequestEntity.method(HttpMethod.GET, builder.build().toUri());
requestBuilder.contentType(MediaType.APPLICATION_JSON);
requestBuilder.header(AUTHORIZATION_HEADER, credentials);
RequestEntity<Object> requestEntity = requestBuilder.body(null);
return restTemplate
.exchange(requestEntity, String.class).getBody();
}
}
ConsumeController class:
#RestController
#RequiredArgsConstructor
public class ConsumeController {
private static final String PATH = "/rest/api/v1/products";
private final WebConfigurer webConfigurer;
private final ApiClient apiClient;
#GetMapping(value = "/products-client")
public String getProductList() {
return apiClient.invokeApi(PATH, webConfigurer.getAuthorization());
}
}
application.yml:
server:
port: 8090
demo:
api:
credentials:
username: ${demo_api_username:john}
password: ${demo_api_password:test}
basePath: ${demo_api_path:http://localhost:8080}
So the first microservice is a REST API and the second microservice is a REST consumer and the communication is secured using Basic Auth.
Now I want to implement using OAuth2, and I want to ask you how can I secure the communication using OAuth2? So I want to add another endpoint like "/access-token", and the client first will do a request at this endpoint with username and password and will get a jwt token. After that will do a request for "/products" endpoint with Authorization header using this jwt token. Can you help me to do this kind of implementation? Thank you!
Overview
You will need client credential grant type flow to communicate between apps. Spring has built in support for well known providers like facebook, google and so on. In our case we provide our own authorization server.
Note - Client credential doesn't return a refresh token as per spec - so make sure you ask for new access token when the current access token is expired.
Client
application properties
security.basic.enabled=false
server.port=8082
spring.security.oauth2.client.registration.server.client-id=first-client
spring.security.oauth2.client.registration.server.client-secret=noonewilleverguess
spring.security.oauth2.client.registration.server.client-authentication-method=basic
spring.security.oauth2.client.registration.server.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.server.scope=read
spring.security.oauth2.client.provider.server.token-uri=http://server:8080/oauth/token
main class
#SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
#Bean
RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
}
credential client grant flow configuration
#Configuration
public class OauthClientCredentialConfig {
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository) {
OAuth2AuthorizedClientService service =
new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, service);
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}
pom dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
rest client
#Getter
#RequiredArgsConstructor
#Slf4j
#Component
public class ApiClient {
private static final String AUTHORIZATION_HEADER = "Authorization";
private final RestTemplate restTemplate;
private final OAuth2AuthorizedClientManager authorizedClientManager;
public String invokeApi(String path) {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("http://server:8080").path(path);
RequestEntity.BodyBuilder requestBuilder =
RequestEntity.method(HttpMethod.GET, builder.build().toUri());
requestBuilder.contentType(MediaType.APPLICATION_JSON);
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
OAuth2AuthorizeRequest oAuth2AuthorizeRequest =
OAuth2AuthorizeRequest.withClientRegistrationId("server")
.principal(principal.getName())
.build();
requestBuilder.header(AUTHORIZATION_HEADER, "Bearer " + authorizedClientManager.authorize(oAuth2AuthorizeRequest).getAccessToken().getTokenValue());
RequestEntity<Object> requestEntity = requestBuilder.body(null);
return restTemplate.exchange(requestEntity, String.class).getBody();
}
}
Authorization and Resource Server
Note for authorization and resource server we are using legacy version as there is no support to create authorization server in new spring security oauth2 module.
Configuration
#EnableWebSecurity
public class Security extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/oauth/token")
.and()
.authorizeRequests()
.anyRequest().authenticated();
}
}
#EnableAuthorizationServer
#EnableResourceServer
#SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
Auth Server Config
#Import(AuthorizationServerEndpointsConfiguration.class)
#Configuration
#Order(2)
#RequiredArgsConstructor
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
private final TokenStore tokenStore;
private final AccessTokenConverter accessTokenConverter;
#Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("first-client")
.secret(passwordEncoder().encode("noonewilleverguess"))
.scopes("read")
.authorizedGrantTypes("client_credentials")
.scopes("resource-server-read", "resource-server-write");
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.accessTokenConverter(accessTokenConverter)
.tokenStore(tokenStore);
}
}
Jwt Config
#Configuration
public class JwtTokenConfig {
#Bean
public KeyPair keyPair() throws NoSuchAlgorithmException {
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
gen.initialize(2048);
KeyPair keyPair = gen.generateKeyPair();
return keyPair;
}
#Bean
public TokenStore tokenStore() throws NoSuchAlgorithmException {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() throws NoSuchAlgorithmException {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyPair());
return converter;
}
}
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.4.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.6</version>
</dependency>
I've added a working example at
https://github.com/saagar2000/oauth2_server
https://github.com/saagar2000/oauth2_client
Response with valid access token
More explanation can be found here
It is necessary differentiate between JWT token based authentication, it seems what you are trying to achieve, and OAuth2 authentication, a more complex subject.
For OAuth2 authentication, Spring framework provides support with the Spring Security OAuth project, but my best advice is that, if you actually need OAuth2 in your project, it is better use a third party OAuth2 provider, like Okta or Auth0, or one of the providers offered in the cloud - for instance, GCP OAuth clients, AWS Cognito, Azure AD applications, etcetera, or a product like Keycloak. All these products will provide you a robust OAuth2 implementation and libraries and mechanisms that will help you to integrate with them.
But it seems for the last paragraphs of your question that what you actually need is authenticate your microservices with JWT tokens.
Let's talk about the server side requirements first.
To accomplish this task, the first thing you need is a service that generates and validates JWT tokens. Maybe something like:
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
// ...
#Component
public class JWTService {
// Get itfrom a configuration property, for instance
#Value("${secretKey}")
private String secretKey;
#Value("${tokenValidityInMillis}")
private Long tokenValidityInMillis;
public String createToken(Authentication authentication) {
long now = (new Date()).getTime();
Date validity = new Date(now + this.tokenValidityInMillis);
// Modify it as per your needs, defining claims, etcetera. For instance
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
return Jwts.builder()
.setSubject(authentication.getName())
.claim("authorities", authorities)
// The signature algorithm you consider appropriate
.signWith(SignatureAlgorithm.HS256, secretKey)
.setExpiration(validity)
.compact();
}
public Authentication getAuthentication(String token) {
try {
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
// Get the authorities back
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get("authorities").toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
User principal = new User(claims.getSubject(), "", authorities);
return new PreAuthenticatedAuthenticationToken(principal, token, authorities);
} catch (Exception e) {
// Handle exceptions (expiration, invalid signature, etcetera) as you wish
}
return null;
}
}
You have several libraries for handling the actual JWT token stuff. The example is using jjwt.
Then, define a Controller that swap the provided credentials for an access token:
import org.springframework.security.authentication.AuthenticationManager;
//...
#RestController
public class AuthController {
private final JWTService jwtService;
private final AuthenticationManager authenticationManager;
public AuthRestController(final JWTService jwtService, final AuthenticationManager authenticationManager) {
this.jwtService = jwtService;
this.authenticationManager = authenticationManager;
}
#PostMapping("/access-token")
public ResponseEntity<JWTToken> swapAccessToken(#RequestBody LoginDTO loginDTO) {
// Note we are passing a JSON object with two fields, username and password,
// not actual HTTP parameters. Modify it according to your needs
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword());
Authentication authentication = authenticationManager.authenticate(authenticationToken);
String jwt = jwtService.createToken(authentication);
return new ResponseEntity.ok(new JWTToken(jwt));
}
}
Where LoginDTO is a simple POJO for storing the username and password:
public class LoginDTO {
private String username;
private String password;
// Getters and setters omitted for brevity
}
And JWTToken is just a convenient way to return the generated token as JSON instead of plain text:
public class JWTToken {
private String idToken;
JWTToken(String idToken) {
this.idToken = idToken;
}
#JsonProperty("id_token")
String getIdToken() {
return idToken;
}
}
The next thing you need is some mechanism that will validate the tokens when necessary. I think the best way you can achieve this is implementing a custom filter that performs the user authentication by inspecting the JWT token. For example:
public class JWTFilter extends GenericFilterBean {
private final JWTService jwtService;
public JWTFilter(final JWTService jwtService) {
this.jwtService = jwtService;
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String jwt = getTokenFromHttpRequest(httpServletRequest);
if (jwt != null) {
// We have a token, perform actual authentication
Authentication authentication = this.jwtService.getAuthentication(jwt);
// If success
if (authentication != null) {
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
// Unsuccesful authentication, let the spring security chain continue and fail if necessary
filterChain.doFilter(servletRequest, servletResponse);
}
// Look for token in an Authorization Bearer header
private String getTokenFromHttpRequest(HttpServletRequest request){
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
All this components must be configured for the Spring Security. It probably need to be further adapted, but please, get the idea:
#Configuration
#RequiredArgsConstructor
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
private final DemoApiConfiguration apiConfig;
private final JWTService jwtService;
private UserDetailsService userDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
// Probably you need to handle more stuff like configuring exception
// handling endpoints for access denied, stateless sessions, CORS, think about it...
http
.csrf().disable()
.authorizeRequests()
// Allow to swap the credentials for access token
.antMatchers("/access-token").permitAll()
// Require authentication for the rest of your API
.anyRequest().authenticated();
// Include your filter somewhere the Spring Security filter chain
final JWTFilter jwtFilter = new JWTFilter(jwtService);
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
// This is an important step: as we are providing both username an
// password and preauthenticated credentials, so we need to configure
// AuthenticationManager that actually supports both authentication types
// It will use your userDetailsService for validating
// the original provided credentials
#Bean
#Override
public AuthenticationManager authenticationManager() {
// Username and password validation
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setUserDetailsService(userDetailsService());
PreAuthenticatedAuthenticationProvider preAuthProvider = new PreAuthenticatedAuthenticationProvider();
preAuthProvider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<>(userDetailsService()));
return new ProviderManager(Arrays.<AuthenticationProvider> asList(daoAuthenticationProvider, preAuthProvider));
}
#Bean
public UserDetailsService userDetailsService() {
if (userDetailsService == null) {
userDetailsService = this.initUserDetailsService(passwordEncoder());
}
return userDetailsService;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
private UserDetailsService initUserDetailsService(PasswordEncoder passwordEncoder) {
UserDetails theUser = User.withUsername(apiConfig.getUsername())
.password(passwordEncoder.encode(apiConfig.getPassword())).roles("USER").build();
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(theUser);
return userDetailsManager;
}
}
Your client microservice only need to swap the configured credentials for an access token, and use the returned JWT as the value of a Bearer HTTP Authorization header when you invoke a protected endpoint. It should be straightforward but let me know if you need further help on this.
Microservice Architecture
The ideal way or commonly preferred way is the API Gateway Pattern for the microservices however it may change according to the projects and requirements. Let's consider the following components
Config Server:
Responsible to manage the configurations for the microservices and we may change the configurations dynamically using spring cloud features with a common bus interface with Kafka or RabbitMQ
API Gateway:
This will be the common entry point to manage the REST request for other services. We can manage the requests using a load balancer here. Also, we can serve the UI from the API Gateway.
Authentication Service (UAA):
This should be responsible for managing the user management and related activity. This is where you will add #EnableAuthorizationServer and extend AuthorizationServerConfigurerAdapter
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
int accessTokenValidity = uaaProperties.getWebClientConfiguration().getAccessTokenValidityInSeconds();
accessTokenValidity = Math.max(accessTokenValidity, MIN_ACCESS_TOKEN_VALIDITY_SECS);
int refreshTokenValidity = uaaProperties.getWebClientConfiguration().getRefreshTokenValidityInSecondsForRememberMe();
refreshTokenValidity = Math.max(refreshTokenValidity, accessTokenValidity);
/*
For a better client design, this should be done by a ClientDetailsService (similar to UserDetailsService).
*/
clients.inMemory()
.withClient(uaaProperties.getWebClientConfiguration().getClientId())
.secret(passwordEncoder.encode(uaaProperties.getWebClientConfiguration().getSecret()))
.scopes("openid")
.autoApprove(true)
.authorizedGrantTypes("implicit","refresh_token", "password", "authorization_code")
.accessTokenValiditySeconds(accessTokenValidity)
.refreshTokenValiditySeconds(refreshTokenValidity)
.and()
.withClient(applicationProperties.getSecurity().getClientAuthorization().getClientId())
.secret(passwordEncoder.encode(applicationProperties.getSecurity().getClientAuthorization().getClientSecret()))
.scopes("web-app")
.authorities("ROLE_GA")
.autoApprove(true)
.authorizedGrantTypes("client_credentials")
.accessTokenValiditySeconds((int) jHipsterProperties.getSecurity().getAuthentication().getJwt().getTokenValidityInSeconds())
.refreshTokenValiditySeconds((int) jHipsterProperties.getSecurity().getAuthentication().getJwt().getTokenValidityInSecondsForRememberMe());
}
Service 1, Service 2...
This will be the microservice to manage the business logic and requirements which is commonly known as Resource Server which can be configured with ResourceServerConfigurerAdapter
Diagram
Managing Access and Refresh Tokens
As mentioned API Gateway is the common entry point for the requests. We can manage the login/logout API in the API Gateway. When the user performs the log in and we can manage the authorization grant type using authentication service and OAuth2TokenEndpointClient from org.springframework.security.oauth2.common.OAuth2AccessToken using OAuth2AccessToken sendPasswordGrant(String username, String password); and OAuth2AccessToken sendRefreshGrant(String refreshTokenValue); methods.
The authentication service will provide the OAuth2AccessToken based on the configurations and login users. Inside OAuth2AccessToken you will get access_token, refresh_token, OAuth2, expires_in, scope.
At the time of authentication, two JWTs will be created - access token and refresh token. Refresh token will have longer validity. Both the tokens will be written in cookies so that they are sent in every subsequent request.
On every REST API call, the tokens will be retrieved from the HTTP header. If the access token is not expired, check the privileges of the user and allow access accordingly. If the access token is expired but the refresh token is valid, recreate new access token and refresh token with new expiry dates and sent back through Cookies
/**
* Authenticate the user by username and password.
*
* #param request the request coming from the client.
* #param response the response going back to the server.
* #param loginVM the params holding the username, password and rememberMe.
* #return the {#link OAuth2AccessToken} as a {#link ResponseEntity}. Will return {#code OK (200)}, if successful.
* If the UAA cannot authenticate the user, the status code returned by UAA will be returned.
*/
public ResponseEntity<OAuth2AccessToken> authenticate(HttpServletRequest request, HttpServletResponse response,
LoginVM loginVM) {
try {
String username = loginVM.getUsername();
String password = loginVM.getPassword();
boolean rememberMe = loginVM.isRememberMe();
OAuth2AccessToken accessToken = authorizationClient.sendPasswordGrant(username, password);
OAuth2Cookies cookies = new OAuth2Cookies();
cookieHelper.createCookies(request, accessToken, rememberMe, cookies);
cookies.addCookiesTo(response);
if (log.isDebugEnabled()) {
log.debug("successfully authenticated user {}", username);
}
return ResponseEntity.ok(accessToken);
} catch (HttpStatusCodeException in4xx) {
throw new UAAException(ErrorConstants.BAD_CREDENTIALS);
}
catch (ResourceAccessException in5xx) {
throw new UAAException(ErrorConstants.UAA_APPLICATION_IS_NOT_RESPONDING);
}
}
/**
* Try to refresh the access token using the refresh token provided as cookie.
* Note that browsers typically send multiple requests in parallel which means the access token
* will be expired on multiple threads. We don't want to send multiple requests to UAA though,
* so we need to cache results for a certain duration and synchronize threads to avoid sending
* multiple requests in parallel.
*
* #param request the request potentially holding the refresh token.
* #param response the response setting the new cookies (if refresh was successful).
* #param refreshCookie the refresh token cookie. Must not be null.
* #return the new servlet request containing the updated cookies for relaying downstream.
*/
public HttpServletRequest refreshToken(HttpServletRequest request, HttpServletResponse response, Cookie
refreshCookie) {
//check if non-remember-me session has expired
if (cookieHelper.isSessionExpired(refreshCookie)) {
log.info("session has expired due to inactivity");
logout(request, response); //logout to clear cookies in browser
return stripTokens(request); //don't include cookies downstream
}
OAuth2Cookies cookies = getCachedCookies(refreshCookie.getValue());
synchronized (cookies) {
//check if we have a result from another thread already
if (cookies.getAccessTokenCookie() == null) { //no, we are first!
//send a refresh_token grant to UAA, getting new tokens
String refreshCookieValue = OAuth2CookieHelper.getRefreshTokenValue(refreshCookie);
OAuth2AccessToken accessToken = authorizationClient.sendRefreshGrant(refreshCookieValue);
boolean rememberMe = OAuth2CookieHelper.isRememberMe(refreshCookie);
cookieHelper.createCookies(request, accessToken, rememberMe, cookies);
//add cookies to response to update browser
cookies.addCookiesTo(response);
} else {
log.debug("reusing cached refresh_token grant");
}
//replace cookies in original request with new ones
CookieCollection requestCookies = new CookieCollection(request.getCookies());
requestCookies.add(cookies.getAccessTokenCookie());
requestCookies.add(cookies.getRefreshTokenCookie());
return new CookiesHttpServletRequestWrapper(request, requestCookies.toArray());
}
}
Secured Communication between Microservices
We can communicate between the service using the FeignClient and can secure the communication by customizing the configurations. See Class<?>[] configuration() default OAuth2UserClientFeignConfiguration.class;
Here we have enhanced default #FeignClient with AuthorizedUserFeignClient interface which consists of custom configuration as OAuth2UserClientFeignConfiguration which consists of #Bean for UserFeignClientInterceptor which manage the autehication using the headers
AuthorizedUserFeignClient.java
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
#Documented
#FeignClient
public #interface AuthorizedUserFeignClient {
#AliasFor(annotation = FeignClient.class, attribute = "name")
String name() default "";
/**
* A custom {#code #Configuration} for the feign client.
*
* Can contain override {#code #Bean} definition for the pieces that make up the client, for instance {#link
* feign.codec.Decoder}, {#link feign.codec.Encoder}, {#link feign.Contract}.
*
* #see FeignClientsConfiguration for the defaults.
*/
#AliasFor(annotation = FeignClient.class, attribute = "configuration")
Class<?>[] configuration() default OAuth2UserClientFeignConfiguration.class;
/**
* An absolute URL or resolvable hostname (the protocol is optional).
*/
String url() default "";
/**
* Whether 404s should be decoded instead of throwing FeignExceptions.
*/
boolean decode404() default false;
/**
* Fallback class for the specified Feign client interface. The fallback class must implement the interface
* annotated by this annotation and be a valid Spring bean.
*/
Class<?> fallback() default void.class;
/**
* Path prefix to be used by all method-level mappings. Can be used with or without {#code #RibbonClient}.
*/
String path() default "";
}
UserFeignClientInterceptor.java
public class UserFeignClientInterceptor implements RequestInterceptor{
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_TOKEN_TYPE = "Bearer";
#Override
public void apply(RequestTemplate template) {
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
if (authentication != null && authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
template.header(AUTHORIZATION_HEADER, String.format("%s %s", BEARER_TOKEN_TYPE, details.getTokenValue()));
}
}
}
Might be helpful
Architecture Overview
Managing the authentication service
I try to create a simple authentication schema with Reactive approach.
I've created a project from scratch with dependencies to reactive components and security.
Introduced Configuration file where I configure authentication manager and security context repository.
The problem is that I notice, that Mono injected into controller initiates double requests to "login" endpoint.
Why does it happens and how to prevent it?
Here is the code of configuration:
#Configuration
#EnableWebFluxSecurity
public class WebFluxSecurityConfiguration {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private SecurityContextRepository securityContextRepository;
#Bean
public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http) {
return http
.csrf().disable()
.cors().disable()
.httpBasic().disable()
.logout().disable()
.formLogin().disable()
.authorizeExchange()
.pathMatchers("/login").permitAll()
.anyExchange().authenticated()
.and()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContextRepository)
.build();
}
}
Here is the authentication manager
#Component
public class AuthenticationManager implements ReactiveAuthenticationManager {
private final WebClient webClient;
public AuthenticationManager(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder.baseUrl("http://localhost:8080/login")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
return webClient.post()
.header("Authorization","Bearer bla-bla")
.retrieve()
.bodyToMono(String.class)
.map(r->new AuthenticatedUser());
}
}
And here is a security context repository
#Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
private static final String TOKEN_PREFIX = "Bearer ";
private AuthenticationManager authenticationManager;
List<PathPattern> pathPatternList;
public SecurityContextRepository(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
PathPattern pathPattern1 = new PathPatternParser().parse("/login");
pathPatternList = new ArrayList<>();
pathPatternList.add(pathPattern1);
}
#Override
public Mono load(ServerWebExchange swe) {
ServerHttpRequest request = swe.getRequest();
RequestPath path = request.getPath();
if (pathPatternList.stream().anyMatch(pathPattern -> pathPattern.matches(path.pathWithinApplication()))) {
System.out.println(path.toString() + " path excluded");
return Mono.empty();
}
System.out.println("executing logic for " + path.toString() + " path");
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
String authToken = null;
//test
authHeader = "Bearer bla-bla";
//~test
if (authHeader != null && authHeader.startsWith(TOKEN_PREFIX)) {
authToken = authHeader.replace(TOKEN_PREFIX, "");
}else {
System.out.println("couldn't find bearer string, will ignore the header.");
}
if (authToken != null) {
Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
return this.authenticationManager.authenticate(auth).map((authentication) -> new SecurityContextImpl(authentication));
} else {
return Mono.empty();
}
}
#Override
public Mono<Void> save(ServerWebExchange serverWebExchange, SecurityContext securityContext) {
return null;
}
}
link to repository of full project
A quick solution to this particular case is to implement caching of WebClient result. So inside of post method to authorization server for retrieval of user profile I just introduced a method cache().
return webClient.post()
.header("Authorization","Bearer bla-bla")
.retrieve()
.bodyToMono(String.class)
.cache()
.map(r->new AuthenticatedUser());
That helped to avoid repeating queries to authorization endpoint during the request.
first of all I try to write unit-test for remote service to get weather and using restTemplate then implement ClientHttpRequestInterceptor to get remote request && remote response for logging purpose now i try to mock restTemplate to write unit-test and get the following error
When using matchers, all arguments have to be provided by matchers.
For example:
//correct:
someMethod(anyObject(), eq("String by matcher"));
For more info see javadoc for Matchers class.
and here you here code i wrote for unit-test
#RunWith(MockitoJUnitRunner.class)
public class WeatherRemoteServiceTest {
#Mock CustomResttemplate restTemplate;
#Mock WeatherRemoteUtilies weatherUtilies;
#InjectMocks WeatherRemoteService weatherRemote;
#Test
public void testRetrieveWeather() {
ResponseEntity<String> MockResponse= new ResponseEntity<String>(HttpStatus.OK);
Mockito.when(weatherUtilies.buildRequestParams(Mockito.anyString()))
.thenReturn(Mockito.any(MultiValueMap.class));
ResponseEntity<String> responseEntity = new ResponseEntity<String>("sampleBodyString", HttpStatus.OK);
Mockito.when(restTemplate.buildRestTemplate().exchange(
Matchers.anyString(),
Matchers.any(HttpMethod.class),
Matchers.<HttpEntity<?>> any(),
Matchers.<Class<String>> any()
)
).thenReturn(responseEntity);
assertEquals(weatherRemote.retrieveWeather("ciaro").getStatusCode(), HttpStatus.OK);
}
}
and here is code for business logic itself
#Service
public class WeatherRemoteService {
private final Logger logger= LoggerFactory.getLogger(this.getClass());
#Value("${openweather.url}")
private String url;
#Autowired
private WeatherRemoteUtilies weatherUtilies;
#Autowired
private CustomResttemplate customRestTemplate;
public ResponseEntity<?> retrieveWeather(String city) {
logger.info(Constants.CLASS_NAME+this.getClass().getName()+Constants.METHOD_NAME+new Object() {}.getClass().getEnclosingMethod().getName());
logger.debug(Constants.METHOD_ARGUMENTS+city);
RestTemplate restRequest= customRestTemplate.buildRestTemplate();
HttpHeaders headers= new HttpHeaders();
headers.set("Accept",MediaType.APPLICATION_JSON_UTF8_VALUE);
UriComponentsBuilder uri= UriComponentsBuilder.fromUriString(url).
path("/data/2.5/weather")
.queryParams(weatherUtilies.buildRequestParams(city));
HttpEntity<String>entity= new HttpEntity<>(headers);
ResponseEntity<String>WeatherResponse=restRequest.exchange(uri.toUriString(), HttpMethod.GET, entity, String.class);
logger.info(Constants.END_METHOD);
return WeatherResponse;
}
}
code for RestTemplateInterceptor
public class RestTemplateInterceptor implements ClientHttpRequestInterceptor {
private final Logger logger =LoggerFactory.getLogger(this.getClass());
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
logRequest(request, body);
ClientHttpResponse httpResponse= execution.execute(request, body);
logResponse(httpResponse);
return httpResponse;
}
private void logRequest(HttpRequest request, byte[] body) throws IOException {
logger.info(Constants.START_REMOTE_REQUEST);
logger.info("URI :{}", request.getURI().toString());
logger.info("Method :{}", request.getMethod().toString());
logger.info("Headers :{}", request.getHeaders().toString());
logger.info("Request body :{}", new String(body,"UTF-8").toString());
logger.info(Constants.END_REMOTE_REQUEST);
}
private void logResponse(ClientHttpResponse response) throws IOException {
logger.info(Constants.START_REMOTE_RESPONSE);
logger.info("Status code :{}", response.getStatusCode().toString());
logger.info("Status text :{}", response.getStatusText().toString());
logger.info("Headers :{}", response.getHeaders().toString());
logger.info("Response body :{}", StreamUtils.copyToString(response.getBody(), Charset.defaultCharset()));
logger.info(Constants.END_REMOTE_RESPONSE);
}
}
then setInterceptor on RestTemplate factory using the following code
public class CustomResttemplate {
public RestTemplate buildRestTemplate() {
SimpleClientHttpRequestFactory simpleFactory= new SimpleClientHttpRequestFactory();
simpleFactory.setOutputStreaming(false);
ClientHttpRequestFactory factory = new BufferingClientHttpRequestFactory(simpleFactory);
RestTemplate restTemplate= new RestTemplate(factory);
restTemplate.setInterceptors(Collections.singletonList(new RestTemplateInterceptor()));
return restTemplate;
}
}
Your problem is where you write
.thenReturn(Mockito.any(MultiValueMap.class))
You actually have to tell Mockito what to return. You can't use a matcher here. Matchers are for verifying, and for setting up what conditions to stub. You can't use them to tell Mockito what to return from a stubbed call.
Create a specific MultiValueMap to pass to thenReturn.
I have a REST controller:
#RequestMapping(value = "greeting", method = RequestMethod.GET, produces = "application/json; charset=utf-8")
#Transactional(readOnly = true)
#ResponseBody
public HttpEntity<GreetingResource> greetingResource(#RequestParam(value = "message", required = false, defaultValue = "World") String message) {
GreetingResource greetingResource = new GreetingResource(String.format(TEMPLATE, message));
greetingResource.add(linkTo(methodOn(AdminController.class).greetingResource(message)).withSelfRel());
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add("Content-Type", "application/json; charset=utf-8");
return new ResponseEntity<GreetingResource>(greetingResource, responseHeaders, HttpStatus.OK);
}
As you can see, I'm trying hard to specify the content type returned by the controller.
It is accessed with a REST client:
public String getGreetingMessage() {
String message;
try {
HttpHeaders httpHeaders = Common.createAuthenticationHeaders("stephane" + ":" + "mypassword");
ResponseEntity<GreetingResource> responseEntity = restTemplate.getForEntity("/admin/greeting", GreetingResource.class, httpHeaders);
GreetingResource greetingResource = responseEntity.getBody();
message = greetingResource.getMessage();
} catch (HttpMessageNotReadableException e) {
message = "The GET request FAILED with the message being not readable: " + e.getMessage();
} catch (HttpStatusCodeException e) {
message = "The GET request FAILED with the HttpStatusCode: " + e.getStatusCode() + "|" + e.getStatusText();
} catch (RuntimeException e) {
message = "The GET request FAILED " + ExceptionUtils.getFullStackTrace(e);
}
return message;
}
The http headers are created by a utility:
static public HttpHeaders createAuthenticationHeaders(String usernamePassword) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
byte[] encodedAuthorisation = Base64.encode(usernamePassword.getBytes());
headers.add("Authorization", "Basic " + new String(encodedAuthorisation));
return headers;
}
The web security configuration and code work fine. I make sure of this using a mockMvc based integration test which succeeds.
The only test that fails is the one based on the REST template:
#Test
public void testGreeting() throws Exception {
mockServer.expect(requestTo("/admin/greeting")).andExpect(method(HttpMethod.GET)).andRespond(withStatus(HttpStatus.OK));
String message = adminRestClient.getGreetingMessage();
mockServer.verify();
assertThat(message, allOf(containsString("Hello"), containsString("World")));
}
The exception given in the Maven build console output is:
java.lang.AssertionError:
Expected: (a string containing "Hello" and a string containing "World")
got: "The GET request FAILED org.springframework.web.client.RestClientException : Could not extract response: no suitable HttpMessageConverter found for response type [class com.thalasoft.learnintouch.rest.resource.GreetingR esource] and content type [application/octet-stream]\n\tat org.springframework.web.client.HttpMessageConverte rExtractor.extractData(HttpMessageConverterExtract or.java:107)
I'm using the Spring Framework 3.2.2.RELEASE version and the Spring Security 3.1.4.RELEASE version on the Java 1.6 version.
At first, I had a bare bone REST template:
#Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}
I have now added to it, hoping it would help:
private static final Charset UTF8 = Charset.forName("UTF-8");
#Bean
public RestTemplate restTemplate() {
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("application", "json", UTF8)));
messageConverters.add(mappingJackson2HttpMessageConverter);
Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
jaxb2Marshaller.setClassesToBeBound(new Class[] {
GreetingResource.class
});
MarshallingHttpMessageConverter marshallingHttpMessageConverter = new MarshallingHttpMessageConverter(jaxb2Marshaller, jaxb2Marshaller);
messageConverters.add(marshallingHttpMessageConverter);
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(new FormHttpMessageConverter());
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("text", "plain", UTF8)));
messageConverters.add(stringHttpMessageConverter);
messageConverters.add(new BufferedImageHttpMessageConverter());
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
RestTemplate restTemplate = new RestTemplate();
restTemplate.setMessageConverters(messageConverters);
return restTemplate;
}
But it didn't change anything and the exception remains the same.
My understanding is that, it is not the REST template that needs any specific JSON configuration, but rather, that, for some reason, my controller is spitting out some application/octet-stream content type instead of some application/json content type.
Any clue?
Some additional information...
The admin rest client bean in the web test configuration:
#Configuration
public class WebTestConfiguration {
#Bean
public AdminRestClient adminRestClient() {
return new AdminRestClient();
}
#Bean
public RestTemplate restTemplate() {
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("application", "json", UTF8)));
messageConverters.add(mappingJackson2HttpMessageConverter);
Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
jaxb2Marshaller.setClassesToBeBound(new Class[] {
Greeting.class
});
MarshallingHttpMessageConverter marshallingHttpMessageConverter = new MarshallingHttpMessageConverter(jaxb2Marshaller, jaxb2Marshaller);
messageConverters.add(marshallingHttpMessageConverter);
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(new FormHttpMessageConverter());
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("text", "plain", UTF8)));
messageConverters.add(stringHttpMessageConverter);
messageConverters.add(new BufferedImageHttpMessageConverter());
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
RestTemplate restTemplate = new RestTemplate();
restTemplate.setMessageConverters(messageConverters);
return restTemplate;
}
}
The base test class:
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#ContextConfiguration( classes = { ApplicationConfiguration.class, WebSecurityConfig.class, WebConfiguration.class, WebTestConfiguration.class })
#Transactional
public abstract class AbstractControllerTest {
#Autowired
private WebApplicationContext webApplicationContext;
#Autowired
private FilterChainProxy springSecurityFilterChain;
#Autowired
protected RestTemplate restTemplate;
protected MockRestServiceServer mockServer;
#Before
public void setup() {
this.mockServer = MockRestServiceServer.createServer(restTemplate);
}
}
The web init class:
public class WebInit implements WebApplicationInitializer {
private static Logger logger = LoggerFactory.getLogger(WebInit.class);
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
registerListener(servletContext);
registerDispatcherServlet(servletContext);
registerJspServlet(servletContext);
createSecurityFilter(servletContext);
}
private void registerListener(ServletContext servletContext) {
// Create the root application context
AnnotationConfigWebApplicationContext appContext = createContext(ApplicationConfiguration.class, WebSecurityConfig.class);
// Set the application display name
appContext.setDisplayName("LearnInTouch");
// Create the Spring Container shared by all servlets and filters
servletContext.addListener(new ContextLoaderListener(appContext));
}
private void registerDispatcherServlet(ServletContext servletContext) {
AnnotationConfigWebApplicationContext webApplicationContext = createContext(WebConfiguration.class);
ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(webApplicationContext));
dispatcher.setLoadOnStartup(1);
Set<String> mappingConflicts = dispatcher.addMapping("/");
if (!mappingConflicts.isEmpty()) {
for (String mappingConflict : mappingConflicts) {
logger.error("Mapping conflict: " + mappingConflict);
}
throw new IllegalStateException(
"The servlet cannot be mapped to '/'");
}
}
private void registerJspServlet(ServletContext servletContext) {
}
private AnnotationConfigWebApplicationContext createContext(final Class... modules) {
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(modules);
return appContext;
}
private void createSecurityFilter(ServletContext servletContext) {
FilterRegistration.Dynamic springSecurityFilterChain = servletContext.addFilter("springSecurityFilterChain", DelegatingFilterProxy.class);
springSecurityFilterChain.addMappingForUrlPatterns(null, false, "/*");
}
}
The web configuration:
#Configuration
#EnableWebMvc
#EnableEntityLinks
#ComponentScan(basePackages = "com.thalasoft.learnintouch.rest.controller")
public class WebConfiguration extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
PageableArgumentResolver resolver = new PageableArgumentResolver();
resolver.setFallbackPageable(new PageRequest(1, 10));
resolvers.add(new ServletWebArgumentResolverAdapter(resolver));
super.addArgumentResolvers(resolvers);
}
}
The application configuration is empty for now:
#Configuration
#Import({ ApplicationContext.class })
public class ApplicationConfiguration extends WebMvcConfigurerAdapter {
// Declare "application" scope beans here, that is, beans that are not only used by the web context
}
I had my doubts before, but now that you've posted everything, here's what's up. Assuming the RestTemplate object you use in your getGreetingMessage() method is the same as the one declared in the #Bean method, the problem starts here
this.mockServer = MockRestServiceServer.createServer(restTemplate);
This call overwrites the default ClientHttpRequestFactory object that the RestTemplate object uses internally with a mock. In your getGreetingMessage() method, this call
ResponseEntity<GreetingResource> responseEntity = restTemplate.getForEntity("/admin/greeting", GreetingResource.class, httpHeaders);
doesn't actually go through the network. The RestTemplate uses the mocked ClientHttpRequestFactory to create a fake ClientHttpRequest which produces a fake ClientHttpResponse which doesn't have a Content-Type header. When the RestTemplate looks at the ClientHttpResponse to determine its Content-Type and doesn't find one, it assumes application/octet-stream by default.
So, your controller isn't setting the content type because your controller is never hit. The RestTemplate is using a default content type for your response because it is mocked and doesn't actually contain one.
From your comments:
I wonder if I understand what the mock server is testing. I understand
it is to be used in acceptance testing scenario. Is it supposed to hit
the controller at all ?
The javadoc for MockRestServiceServer states:
Main entry point for client-side REST testing. Used for tests that
involve direct or indirect (through client code)
use of the RestTemplate. Provides a way to set up fine-grained
expectations on the requests that will be performed through the
RestTemplate and a way to define the responses to send back removing
the need for an actual running server.
In other words, it's as if your application server didn't exist. So you could throw any expectations (and actual return values) you wanted and test whatever happens from the client side. So you aren't testing your server, you are testing your client.
Are you sure you aren't looking for MockMvc, which is
Main entry point for server-side Spring MVC test support.
which you can setup to actually use your #Controller beans in an integration environment. You aren't actually sending HTTP request, but the MockMvc is simulating how they would be sent and how your server would respond.
It is bug in MockHttpServletRequest and I will try to describe it.
Issue in tracker https://jira.springsource.org/browse/SPR-11308#comment-97327
Fixed in version 4.0.1
Bug
When DispatcherServlet looking for method to invoke it using some RequestConditions. One of them is ConsumesRequestCondition. The following is a piece of code:
#Override
protected boolean matchMediaType(HttpServletRequest request) throws HttpMediaTypeNotSupportedException {
try {
MediaType contentType = StringUtils.hasLength(request.getContentType()) ?
MediaType.parseMediaType(request.getContentType()) :
MediaType.APPLICATION_OCTET_STREAM;
return getMediaType().includes(contentType);
}
catch (IllegalArgumentException ex) {
throw new HttpMediaTypeNotSupportedException(
"Can't parse Content-Type [" + request.getContentType() + "]: " + ex.getMessage());
}
}
We are interested in piece request.getContentType(). There request is MockHttpServletRequest. Let's look on method getContentType():
public String getContentType() {
return this.contentType;
}
It just return value of this.contentType. It does not return a value from the header! And this.contentType is always NULL. Then contentType in matchMediaType methos will be always MediaType.APPLICATION_OCTET_STREAM.
Solution
I have tried many ways but have found only one that works.
Create package org.springframework.test.web.client in your test directory.
Create copy of org.springframework.test.web.client.MockMvcClientHttpRequestFactory but rename it. For example rename to FixedMockMvcClientHttpRequestFactory.
Find line:
MvcResult mvcResult = MockMvcClientHttpRequestFactory.this.mockMvc.perform(requestBuilder).andReturn();
Replace it with code:
MvcResult mvcResult = FixedMockMvcClientHttpRequestFactory.this.mockMvc.perform(new RequestBuilder() {
#Override
public MockHttpServletRequest buildRequest(ServletContext servletContext) {
MockHttpServletRequest request = requestBuilder.buildRequest(servletContext);
request.setContentType(request.getHeader("Content-Type"));
return request;
}
}).andReturn();
And register your ClientHttpReque
#Bean
public ClientHttpRequestFactory clientHttpRequestFactory(MockMvc mockMvc) {
return new FixedMockMvcClientHttpRequestFactory(mockMvc);
}
I know that it is not beautiful solution but it works fine.