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
Related
We have a Spring Boot microservice that does the SOAP call to the external system using org.springframework.ws.client.core.WebServiceTemplate.
Now the system would be protected with Keycloak, so all the request need to beak the auth token.
If it was a REST API, I would just replace the pre-existed RestTemplate with OAuth2RestTemplate. But how to instrument the calls initially done by the org.springframework.ws.client.core.WebServiceTemplate ?
So, I understand, I should put the authentication header manually with value 'Bearer ....token there...'. How I can retrieve that part manually to put it into the request?
you can get current request token using RequestContextHolder class and add into soap request header.
String token = ((ServletRequestAttributes)(RequestContextHolder.getRequestAttributes())).getRequest().getHeader("Authorization");
Also I would suggest use Webservice interecptor instead of adding header in each web service request call.
The problem was caused by
Existing library code, based on org.springframework.ws.client.core.WebServiceTemplate, so large and huge for rewriting it using WebClient, compatible with OAuth2 SpringSecurity or use depricated OAuth2RestTemplate
The webservice we previously communicated with, turns into protected with Gravitee and accepts queries with JWT tokens only. So, the only change here is to add the Authentication header with 'Bearer ....token there...'
We initiate the call from the scheduled jo in the microservice. So, it should be getting token from the Keycloak before the request and be able to update it with time. No one does the explicit authorization like in the frontend, so the OAuth2 client should use client-id and client-secret to connect with no human involved
The Solution
At the beginning, we define the Interceptor to the SOAP calls, that will pass the token as a header, via a Supplier function taking it wherever it can be taken:
public class JwtClientInterceptor implements ClientInterceptor {
private final Supplier<String> jwtToken;
public JwtClientInterceptor(Supplier<String> jwtToken) {
this.jwtToken = jwtToken;
}
#Override
public boolean handleRequest(MessageContext messageContext) {
SoapMessage soapMessage = (SoapMessage) messageContext. getRequest();
SoapHeader soapHeader = soapMessage.getSoapHeader();
soapHeader.addHeaderElement(new QName("authorization"))
.setText(String. format("Bearer %s", jwtToken.get()));
return true;
}
#Override
public boolean handleResponse(MessageContext messageContext) throws WebServiceClientException {
return true;
}
#Override
public boolean handleFault(MessageContext messageContext) throws WebServiceClientException {
return true;
}
#Override
public void afterCompletion(MessageContext messageContext, Exception ex) throws WebServiceClientException {
}
}
Then pass it to the template in addition to other pre-existed interceptor to be called in config class:
protected WebServiceTemplate buildWebServiceTemplate(Jaxb2Marshaller marshaller,
HttpComponentsMessageSender messageSender, String uri, Supplier<String> jwtToken) {
WebServiceTemplate template = new WebServiceTemplate();
template.setMarshaller(marshaller);
template.setUnmarshaller(marshaller);
template.setMessageSender(messageSender);
template.setDefaultUri(uri);
ClientInterceptor[] clientInterceptors = ArrayUtils.addAll(template.getInterceptors(), new Logger(), new JwtClientInterceptor(jwtToken));
template.setInterceptors(clientInterceptors);
return template;
}
Then add the Spring Security Oath2 Client library
compile 'org.springframework.security:spring-security-oauth2-client:5.2.1.RELEASE'
We create OAuth2AuthorizedClientService bean, that uses a standard ClientRegistrationRepository (the repository is initiated through usage of #EnableWebSecurity annotation on the #Configuration class, but please double check about that)
#Bean
public OAuth2AuthorizedClientService oAuth2AuthorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {
return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
}
Then create a OAuth2AuthorizedClientManager
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
Authentication authentication = new Authentication() {
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
GrantedAuthority grantedAuthority = new GrantedAuthority() {
#Override
public String getAuthority() {
return "take_a_needed_value_from_property";
}
};
return Arrays.asList(grantedAuthority);
}
#Override
public Object getCredentials() {
return null;
}
#Override
public Object getDetails() {
return null;
}
#Override
public Object getPrincipal() {
return new Principal() {
#Override
public String getName() {
return "our_client_id_from_properties";
}
};
}
#Override
public boolean isAuthenticated() {
return true;
}
#Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
}
#Override
public String getName() {
return "take_a_needed_name_from_properties";
}
};
//we need to emulate Principal there, as other classes relies on it. In fact, Principal isn't needed for the app which is a client and just do the call, as nothing is authorized in the app against this Principal itself
OAuth2AuthorizationContext oAuth2AuthorizationContext = OAuth2AuthorizationContext.withClientRegistration(clientRegistrationRepository.findByRegistrationId("keycloak")).
principal(authentication).
build();
oAuth2AuthorizationContext.getPrincipal().setAuthenticated(true);
oAuth2AuthorizationContext.getAuthorizedClient();
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder().
//refreshToken().
clientCredentials(). //- we use this one according to our set up
//authorizationCode().
build();
OAuth2AuthorizedClientService oAuth2AuthorizedClientService = oAuth2AuthorizedClientService(clientRegistrationRepository); //use the bean from before step here
AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, oAuth2AuthorizedClientService);
OAuth2AuthorizedClient oAuth2AuthorizedClient = authorizedClientProvider.authorize(oAuth2AuthorizationContext);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
oAuth2AuthorizedClientService.saveAuthorizedClient(oAuth2AuthorizedClient,
oAuth2AuthorizationContext.getPrincipal());
//this step is needed, as without explicit authorize call, the
//oAuth2AuthorizedClient isn't initialized in the service
return authorizedClientManager;
}
Provide a method for supplied function that can be called each time to retrieve the JWT token from the security stuff (repository and manager). Here it should be auto-updated, so we just call for retrieving it
public Supplier<String> getJwtToken() {
return () -> {
OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient("keycloak", "we_havePout_realm_there_from_the_properties");
return authorizedClient.getAccessToken().getTokenValue();
};
}
Pass this Consumer to the #Bean, defining the WebServiceTemplate's
#Bean
public Client client(#Qualifier("Sender1") HttpComponentsMessageSender bnfoMessageSender,
#Qualifier("Sender2") HttpComponentsMessageSender uhMessageSender) {
WebServiceTemplate sender1= buildWebServiceTemplate(buildSender1Marshaller(), sender1MessageSender, properties.getUriSender1(),getJwtToken());
WebServiceTemplate sender2 = buildWebServiceTemplate(buildSender2Marshaller(), sender2MessageSender, properties.getUriSender2(),getJwtToken());
return buildClient(buildRetryTemplate(), sender1, sender2);
}
We add Spring Security Client values to application.yaml in order to configure it.
spring:
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: https://host/keycloak/auth/realms/ourrealm
registration:
keycloak:
client-id: client_id
client-secret: client-secret-here
authorization-grant-type: client_credentials //need to add explicitly, otherwise would try other grant-type by default and never get the token!
client-authentication-method: post //need to have this explicitly, otherwise use basic that doesn't fit best the keycloak set up
scope: openid //if your don't have it, it checks all available scopes on url like https://host/keycloak/auth/realms/ourrealm/ .well-known/openid-configuration keycloak and then sends them as value of parameter named 'scope' in the query for retrieving the token; that works wrong on our keycloak, so to replace this auto-picked value, we place the explicit scopes list here
I am currently working on a Spring Boot REST application with Spring Security. My workplace use Auth0 (external third-party service providing user management) for their authentication and have requested me to implement it in this application. Authentication occurs in the front end application written in React. The frontend application shows a login form and sends the username and password to Auth0, Auth0 verifies the credentials and returns a JWT token when the user is validated.
After this, the frontend application will call the REST services from my application passing a JWT token in the Authorize header. Using an Auth0 plugin, Spring Security verifies this token and the request is allowed to execute. I have tested this much to be working as expected. The code is as follows:
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import com.auth0.spring.security.api.JwtWebSecurityConfigurer;
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
#Value(value = "${auth0.apiAudience}")
private String apiAudience;
#Value(value = "${auth0.issuer}")
private String issuer;
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
configuration.setAllowCredentials(true);
configuration.addAllowedHeader("Authorization");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
JwtWebSecurityConfigurer //Auth0 provided class performs per-authentication using JWT token
.forRS256(apiAudience, issuer)
.configure(http)
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/Test/public").permitAll()
.antMatchers(HttpMethod.GET, "/Test/authenticated").authenticated();
}
}
Now, once this authentication is done, I have observed that the principal in the security context gets updated with user id from Auth0. I have verified this by this code snippet:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String name = authentication.getName(); // Returns the Auth0 user id.
The next step I expect to do is to use this user id to match the user with roles and permissions in my existing database schema. Therefore, I need to implement a custom authorization mechanism that plugs into Spring Security as well. In other words the user's roles must be loaded into the security context shortly after the (pre)authentication is done. How do I implement this? Is there some class that I need to extend or implement some interface?
I think what you are looking for is the AuthenticationProvider Interface. Here are two examples how I handle Authentication:
DaoAuthentication
#Component
public class DaoAdminAuthenticationProvider extends DaoAuthenticationProvider {
private static final Logger LOG =
LoggerFactory.getLogger(DaoAdminAuthenticationProvider.class);
private final AdminUserRepository adminUserRepository;
public DaoAdminAuthenticationProvider(AdminUserRepository adminUserRepository, DaoAdminUserDetailsService daoAdminUserDetailsService) {
this.adminUserRepository = adminUserRepository;
setPasswordEncoder(new BCryptPasswordEncoder(11));
this.setUserDetailsService(daoAdminUserDetailsService);
}
#Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
AdminUser adminUser = adminUserRepository.findByEmail(auth.getName());
if (adminUser == null) {
LOG.info("Invalid username or password");
throw new BadCredentialsException("Invalid username or password");
}
Authentication result = super.authenticate(auth);
return new UsernamePasswordAuthenticationToken(adminUser, result.getCredentials(), result.getAuthorities());
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
JwtAuthenticationProvider
#Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
private static final Logger LOG =
LoggerFactory.getLogger(JwtAuthenticationProvider.class);
private static final String EX_TOKEN_INVALID = "jwt.token.invalid";
private final JwtTokenService jwtTokenService;
#SuppressWarnings("unused")
public JwtAuthenticationProvider() {
this(null);
}
#Autowired
public JwtAuthenticationProvider(JwtTokenService jwtTokenService) {
this.jwtTokenService = jwtTokenService;
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
try {
String token = (String) authentication.getCredentials();
String username = jwtTokenService.getUsernameFromToken(token);
return jwtTokenService.validateToken(token)
.map(aBoolean -> new JwtAuthenticatedProfile(username))
.orElseThrow(() -> new TokenException(EX_TOKEN_INVALID));
} catch (JwtException ex) {
LOG.error("Invalid JWT Token");
throw new TokenException(EX_TOKEN_INVALID);
}
}
#Override
public boolean supports(Class<?> authentication) {
return JwtAuthentication.class.equals(authentication);
}
}
The other classes like JwtTokenService etc. I implemented as well. But regarding to your question I think the answer is to use the AuthenticationProvider Interface.
Ok, I found a solution though I think it's a bit dirty. Going by the weird way that the official Auth0 classes are structured, what I've done could possibly be described as a hack. Anyway, here goes:
First of all, I a custom user details service by implementing the AuthenticationUserDetailsService interface:
#Service
public class VUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationJsonWebToken> {
#Autowired
UserRepository userRepository;
Logger logger = LoggerFactory.getLogger(VUserDetailsService.class);
#Override
#Transactional(readOnly = true)
public UserDetails loadUserDetails(PreAuthenticatedAuthenticationJsonWebToken token) throws UsernameNotFoundException {
logger.debug("User id: "+token.getName());
// Verify whether there is an entry for this id in the database.
User user = userRepository.findByAuxillaryId(token.getName());
if(user == null)
throw new UsernameNotFoundException("The user with id "+token.getName()+" not found in database.");
logger.debug("Obtained user details from db: "+user.toString());
List<GrantedAuthority> authoritiesList = new ArrayList<>();
// Get user roles
List<UserRole> userRoles = user.getUserRoles();
if(userRoles != null) logger.debug("Number of user roles:"+userRoles.size());
for(UserRole userRole : userRoles) {
logger.debug(userRole.getCompositeKey().getRole());
authoritiesList.add(new SimpleGrantedAuthority(userRole.getCompositeKey().getRole()));
}
return new org.springframework.security.core.userdetails.User(token.getName(), "TEMP", authoritiesList);
}
}
Here auxillary id is the user id assigned when a user is created in Auth0. Note that PreAuthenticatedAuthenticationJsonWebToken is a class provided by Auth0 as well.
After this, I created a custom authentication provider extending the Auth0 provided JwtAuthenticationProvider:
public class VAuthenticationProvider extends JwtAuthenticationProvider {
public VAuthenticationProvider(JwkProvider jwkProvider, String issuer, String audience) {
super(jwkProvider, issuer, audience);
}
#Autowired
VUserDetailsService vUserDetailsService;
Logger logger = LoggerFactory.getLogger(VAuthenticationProvider.class);
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
logger.debug("*** Processing authentication for token: "+authentication.getName());
logger.debug("*** Current granted authorities: "+authentication.getAuthorities());
UserDetails userDetails = vUserDetailsService.loadUserDetails((PreAuthenticatedAuthenticationJsonWebToken) authentication);
authentication = new PreAuthenticatedAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
return authentication;
}
#Override
public boolean supports(Class<?> authentication) {
//com.auth0.spring.security.api.authentication.PreAuthenticatedAuthenticationJsonWebToken
return authentication.equals(PreAuthenticatedAuthenticationJsonWebToken.class);
}
}
Then I used this authentication provider in my security configuration class:
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Value(value = "${auth0.apiAudience}")
private String apiAudience;
#Value(value = "${auth0.issuer}")
private String issuer;
#Autowired
VUserDetailsService vUserDetailsService;
Logger log = LoggerFactory.getLogger(SecurityConfiguration.class);
#Bean
public VAuthenticationProvider authProvider() {
JwkProvider jwkProvider = new JwkProviderBuilder(issuer).build(); //Auth0 provided class
VAuthenticationProvider vAuthProvider = new VAuthenticationProvider(jwkProvider, issuer, apiAudience);
return vAuthProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
JwtWebSecurityConfigurer.forRS256(apiAudience, issuer, authProvider())
.configure(http)
.authorizeRequests().antMatchers(HttpMethod.GET, "/Test/public").permitAll()
.antMatchers(HttpMethod.GET, "/Test/authenticated").authenticated()
.antMatchers(HttpMethod.GET, "/admin/*").hasRole("ADMIN") //Not Auth0 role, defined in my DB.
.antMatchers(HttpMethod.GET, "/Test/root").hasRole("ROOT"); //Not Auth0 role, defined in my DB.
}
/* Code ommitted */
Now, all my requests are getting filtered based on the roles in my database. Thus, Auth0 is only being used for authentication and authorization is based on roles in my database.
If anyone thinks this solution could be improved, please let me know.
I've been struggling a lot to properly implement Stomp (websocket) Authentication and Authorization with Spring-Security. For posterity i'll answer my own question to provide a guide.
The Problem
Spring WebSocket documentation (for Authentication) looks unclear ATM (IMHO). And i couldn't understand how to properly handle Authentication and Authorization.
What i want
Authenticate users with login/password.
Prevent anonymous users to CONNECT though WebSocket.
Add authorization layer (user, admin, ...).
Having Principal available in controllers.
What i don't want
Authenticate on HTTP negotiation endpoints (since most of JavaScript libraries don't sends authentication headers along with the HTTP negotiation call).
As stated above the documentation looks unclear (IMHO), until Spring provide some clear documentation, here is a boilerplate to save you from spending two days trying to understand what the security chain is doing.
A really nice attempt was made by Rob-Leggett but, he was forking some Springs class and I don't feel comfortable doing so.
Things to know before you start:
Security chain and Security config for http and WebSocket are completely independent.
Spring AuthenticationProvider take not part at all in Websocket authentication.
The authentication won't happen on HTTP negotiation endpoint in our case, because none of the JavaScripts STOMP (websocket) libraries I know sends the necessary authentication headers along with the HTTP request.
Once set on CONNECT request, the user (simpUser) will be stored in the websocket session and no more authentication will be required on further messages.
Maven deps
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-messaging</artifactId>
</dependency>
WebSocket configuration
The below config register a simple message broker (a simple endpoint that we will later protect).
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(final MessageBrokerRegistry config) {
// These are endpoints the client can subscribes to.
config.enableSimpleBroker("/queue/topic");
// Message received with one of those below destinationPrefixes will be automatically router to controllers #MessageMapping
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
// Handshake endpoint
registry.addEndpoint("stomp"); // If you want to you can chain setAllowedOrigins("*")
}
}
Spring security config
Since the Stomp protocol rely on a first HTTP Request, we'll need to authorize HTTP call to our stomp handshake endpoint.
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(final HttpSecurity http) throws Exception {
// This is not for websocket authorization, and this should most likely not be altered.
http
.httpBasic().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests().antMatchers("/stomp").permitAll()
.anyRequest().denyAll();
}
}
Then we'll create a service responsible for authenticating users.
#Component
public class WebSocketAuthenticatorService {
// This method MUST return a UsernamePasswordAuthenticationToken instance, the spring security chain is testing it with 'instanceof' later on. So don't use a subclass of it or any other class
public UsernamePasswordAuthenticationToken getAuthenticatedOrFail(final String username, final String password) throws AuthenticationException {
if (username == null || username.trim().isEmpty()) {
throw new AuthenticationCredentialsNotFoundException("Username was null or empty.");
}
if (password == null || password.trim().isEmpty()) {
throw new AuthenticationCredentialsNotFoundException("Password was null or empty.");
}
// Add your own logic for retrieving user in fetchUserFromDb()
if (fetchUserFromDb(username, password) == null) {
throw new BadCredentialsException("Bad credentials for user " + username);
}
// null credentials, we do not pass the password along
return new UsernamePasswordAuthenticationToken(
username,
null,
Collections.singleton((GrantedAuthority) () -> "USER") // MUST provide at least one role
);
}
}
Note that: UsernamePasswordAuthenticationToken MUST have at least one GrantedAuthority, if you use another constructor, Spring will auto-set isAuthenticated = false.
Almost there, now we need to create an Interceptor that will set the `simpUser` header or throw `AuthenticationException` on CONNECT messages.
#Component
public class AuthChannelInterceptorAdapter extends ChannelInterceptor {
private static final String USERNAME_HEADER = "login";
private static final String PASSWORD_HEADER = "passcode";
private final WebSocketAuthenticatorService webSocketAuthenticatorService;
#Inject
public AuthChannelInterceptorAdapter(final WebSocketAuthenticatorService webSocketAuthenticatorService) {
this.webSocketAuthenticatorService = webSocketAuthenticatorService;
}
#Override
public Message<?> preSend(final Message<?> message, final MessageChannel channel) throws AuthenticationException {
final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT == accessor.getCommand()) {
final String username = accessor.getFirstNativeHeader(USERNAME_HEADER);
final String password = accessor.getFirstNativeHeader(PASSWORD_HEADER);
final UsernamePasswordAuthenticationToken user = webSocketAuthenticatorService.getAuthenticatedOrFail(username, password);
accessor.setUser(user);
}
return message;
}
}
Note that: preSend() MUST return a UsernamePasswordAuthenticationToken, another element in the spring security chain test this.
Note that: If your UsernamePasswordAuthenticationToken was built without passing GrantedAuthority, the authentication will fail, because the constructor without granted authorities auto set authenticated = false THIS IS AN IMPORTANT DETAIL which is not documented in spring-security.
Finally create two more class to handle respectively Authorization and Authentication.
#Configuration
#Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketAuthenticationSecurityConfig extends WebSocketMessageBrokerConfigurer {
#Inject
private AuthChannelInterceptorAdapter authChannelInterceptorAdapter;
#Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
// Endpoints are already registered on WebSocketConfig, no need to add more.
}
#Override
public void configureClientInboundChannel(final ChannelRegistration registration) {
registration.setInterceptors(authChannelInterceptorAdapter);
}
}
Note that: The #Order is CRUCIAL don't forget it, it allows our interceptor to be registered first in the security chain.
#Configuration
public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) {
// You can customize your authorization mapping here.
messages.anyMessage().authenticated();
}
// TODO: For test purpose (and simplicity) i disabled CSRF, but you should re-enable this and provide a CRSF endpoint.
#Override
protected boolean sameOriginDisabled() {
return true;
}
}
for java client side use this tested example:
StompHeaders connectHeaders = new StompHeaders();
connectHeaders.add("login", "test1");
connectHeaders.add("passcode", "test");
stompClient.connect(WS_HOST_PORT, new WebSocketHttpHeaders(), connectHeaders, new MySessionHandler());
Going with spring authentication is a pain. You can do it in a simple way. Create a web Filter and read the Authorization token by yourself, then perform the authentication.
#Component
public class CustomAuthenticationFilter implements Filter {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
if (servletRequest instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String authorization = request.getHeader("Authorization");
if (/*Your condition here*/) {
// logged
filterChain.doFilter(servletRequest, servletResponse);
} else {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write("{\"message\": "\Bad login\"}");
}
}
}
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void destroy() {
}
}
Then in your configuration define the filter using the spring mechanism:
#Configuration
public class SomeConfig {
#Bean
public FilterRegistrationBean<CustomAuthenticationFilter> securityFilter(
CustomAuthenticationFilter customAuthenticationFilter){
FilterRegistrationBean<CustomAuthenticationFilter> registrationBean
= new FilterRegistrationBean<>();
registrationBean.setFilter(customAuthenticationFilter);
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
I have a Spring Boot app using CAS WebSecurity to make sure that all incoming non authenticated requests are redirected to a common login page.
#Configuration
#EnableWebSecurity
public class CASWebSecurityConfig extends WebSecurityConfigurerAdapter {
I want to expose health endpoints through actuator, and added the relevant dependency. I want to bypass the CAS check for these /health URL which are going to be used by monitoring tools, so in the configure method, I have added :
http.authorizeRequests().antMatchers("/health/**").permitAll();
This works, but now I want to tweak it further :
detailed health status (ie "full content" as per the docs) should be accessible only to some specific monitoring user, for which credentials are provided in property file.
if no authentication is provided, then "status only" should be returned.
Following http://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-monitoring.html#production-ready-health-access-restrictions, I've configured the properties as below, so that it should work :
management.security.enabled: true
endpoints.health.sensitive: false
But I have a problem with how I configure the credentials... following http://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-monitoring.html#production-ready-sensitive-endpoints , I added in my config file :
security.user.name: admin
security.user.password: secret
But it's not working - and when I don't put the properties, I don't see the password generated in logs.
So I'm trying to put some custom properties like
healthcheck.username: healthCheckMonitoring
healthcheck.password: healthPassword
and inject these into my Security config so that configureGlobal method becomes :
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth,
CasAuthenticationProvider authenticationProvider) throws Exception {
auth.inMemoryAuthentication().withUser(healthcheckUsername).password(healthcheckPassword).roles("ADMIN");
auth.authenticationProvider(authenticationProvider);
}
and in the configure method, I change the config for the URL pattern to :
http.authorizeRequests()
.antMatchers("/health/**").hasAnyRole("ADMIN")
.and().httpBasic()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().csrf().disable();
With that config, I get full content when authenticated, but logically, I don't get any status (UP or DOWN) when I'm not authenticated, because the request doesn't even reach the endpoint : it is intercepted and rejected by the security config.
How can I tweak my Spring Security config so that this works properly ? I have the feeling I should somehow chain the configs, with the CAS config first allowing the request to go through purely based on the URL, so that the request then hits a second config that will do basic http authentication if credentials are provided, or let the request hit the endpoint unauthenticated otherwise, so that I get the "status only" result.. But at the same time, I'm thinking Spring Boot can manage this correctly if I configure it properly..
Thanks !
Solution is not great, but so far, that's what works for me :
in my config (only the relevant code):
#Configuration
#EnableWebSecurity
public class CASWebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
//disable HTTP Session management
http
.securityContext()
.securityContextRepository(new NullSecurityContextRepository())
.and()
.sessionManagement().disable();
http.requestCache().requestCache(new NullRequestCache());
//no security checks for health checks
http.authorizeRequests().antMatchers("/health/**").permitAll();
http.csrf().disable();
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint());
http // login configuration
.addFilter(authenticationFilter())
.authorizeRequests().anyRequest().authenticated();
}
}
Then I added a specific filter :
#Component
public class HealthcheckSimpleStatusFilter extends GenericFilterBean {
private final String AUTHORIZATION_HEADER_NAME="Authorization";
private final String URL_PATH = "/health";
#Value("${healthcheck.username}")
private String username;
#Value("${healthcheck.password}")
private String password;
private String healthcheckRole="ADMIN";
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = this.getAsHttpRequest(request);
//doing it only for /health endpoint.
if(URL_PATH.equals(httpRequest.getServletPath())) {
String authHeader = httpRequest.getHeader(AUTHORIZATION_HEADER_NAME);
if (authHeader != null && authHeader.startsWith("Basic ")) {
String[] tokens = extractAndDecodeHeader(authHeader);
if (tokens != null && tokens.length == 2 && username.equals(tokens[0]) && password.equals(tokens[1])) {
createUserContext(username, password, healthcheckRole, httpRequest);
} else {
throw new BadCredentialsException("Invalid credentials");
}
}
}
chain.doFilter(request, response);
}
/**
* setting the authenticated user in Spring context so that {#link HealthMvcEndpoint} knows later on that this is an authorized user
* #param username
* #param password
* #param role
* #param httpRequest
*/
private void createUserContext(String username, String password, String role,HttpServletRequest httpRequest) {
List<GrantedAuthority> authoritiesForAnonymous = new ArrayList<>();
authoritiesForAnonymous.add(new SimpleGrantedAuthority("ROLE_" + role));
UserDetails userDetails = new User(username, password, authoritiesForAnonymous);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
private HttpServletRequest getAsHttpRequest(ServletRequest request) throws ServletException {
if (!(request instanceof HttpServletRequest)) {
throw new ServletException("Expecting an HTTP request");
}
return (HttpServletRequest) request;
}
private String[] extractAndDecodeHeader(String header) throws IOException {
byte[] base64Token = header.substring(6).getBytes("UTF-8");
byte[] decoded;
try {
decoded = Base64.decode(base64Token);
} catch (IllegalArgumentException var7) {
throw new BadCredentialsException("Failed to decode basic authentication token",var7);
}
String token = new String(decoded, "UTF-8");
int delim = token.indexOf(":");
if(delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
} else {
return new String[]{token.substring(0, delim), token.substring(delim + 1)};
}
}
}
at the moment I am trying to integrate Spring-Security-Oauth2, Zuul, OpenAM as OAuth2 authorization Server and a WCF REST API as resource Server. The final Setup should look something like the following:
I read the tutorial, which explains how to setup a SSO Environment with spring and AngularJS (sso with spring and angularJS), however in my case I would like to use OpenAM and password grant flow to authenticate useres.
So in the Spring Boot application my current config Looks as follows:
#SpringBootApplication
#EnableZuulProxy
#EnableOAuth2Client
public class ApolloUIProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ApolloUIProxyApplication.class, args);
}
#Configuration
protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.logout().and().antMatcher("/**").authorizeRequests()
.antMatchers("/index.html", "/home.html", "/", "/login").permitAll()
.anyRequest().authenticated().and().csrf()
.csrfTokenRepository(csrfTokenRepository()).and()
.addFilterAfter(csrfHeaderFilter(), CsrfFilter.class)
.addFilterAfter(authenticationProcessingFilter(), CsrfFilter.class);
}
#Bean
public ZuulFilter tokenRelayFilter(){
JwtTokenRelayFilter filter = new JwtTokenRelayFilter();
filter.setRestTemplate(restTemplate());
return new JwtTokenRelayFilter();
}
#Bean
public ZuulFilter customTokenFilter(){
return new CustomZuulFilter();
}
#Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
return new JwtAccessTokenConverter();
}
private Filter csrfHeaderFilter() {
return new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class
.getName());
if (csrf != null) {
Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
String token = csrf.getToken();
if (cookie == null || token != null
&& !token.equals(cookie.getValue())) {
cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
};
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
private OAuth2ClientAuthenticationProcessingFilter authenticationProcessingFilter(){
OAuth2ClientAuthenticationProcessingFilter processingFilter = new OAuth2ClientAuthenticationProcessingFilter("/login");
processingFilter.setRestTemplate(restTemplate());
processingFilter.setTokenServices(resourceServerTokenServices());
return processingFilter;
}
#Bean
public ResourceServerTokenServices resourceServerTokenServices(){
OpenAMRemoteTokenService remoteTokenServices = new OpenAMRemoteTokenService();
remoteTokenServices.setRestTemplate(restTemplate());
remoteTokenServices.setAccessTokenConverter(accessTokenConverter());
remoteTokenServices.setClientId("...");
remoteTokenServices.setClientSecret("...");
remoteTokenServices.setCheckTokenEndpointUrl("http://...");
return remoteTokenServices;
}
#Bean
public OAuth2RestTemplate restTemplate(){
OAuth2RestTemplate template = new OAuth2RestTemplate(resourceDetails(), clientContext());
return template;
}
#Bean
public AccessTokenProvider accessTokenProvider(){
ResourceOwnerPasswordAccessTokenProvider provider = new ResourceOwnerPasswordAccessTokenProvider();
return provider;
}
#Bean
public OAuth2ClientContext clientContext(){
return new OpenAMClientContext();
}
#Bean
public OAuth2ProtectedResourceDetails resourceDetails(){
ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails();
details.setGrantType("password");
details.setAccessTokenUri("http://...");
details.setScope(Arrays.asList("openid");
details.setClientId("...");
details.setClientSecret("...");
return details;
}
#Bean
public AccessTokenConverter accessTokenConverter(){
DefaultAccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
tokenConverter.setUserTokenConverter(userAuthenticationConverter());
return tokenConverter;
}
#Bean
public UserAuthenticationConverter userAuthenticationConverter(){
return new OpenAMUserAuthenticationConverter();
}
}
}
I wrote a custom RemoteTokenService, because otherwise Spring could not access the tokeninfo endpoint of OpenAM, which requires a GET-request and not a Post. This Connection works fine now, so that i get a valid access-token from OpenAM and can also query the tokeninfo.endpoint for token/user-Infos. The Authentication object gets created and is stored in the security-context of Spring. I can also access the Authentication Object in the ZuulFilter.
My Problem now is, that i had to tweek the "OAuth2ClientContext" to grab the users credentials from the servletRequest and put it on the "AccessTokenRequest". Otherwise I would have to hard-code them in the ResourceDetails, which is not appropriate in my case.
The result is, that the ClientContext (and also AccessTokenRequest I guess) is shared between all users of the System. What i want is a session scoped Client Context, so that I can have multiple useres logged in and can access the right SecurityContext for each user on every request.
So my question are,
1) how can I make the ClientContext and AccessTokenRequest session scoped?
2) Do I need to use Spring Session module?
3) Do I need to set the sessionStrategy
Thank you!