Currently, we've implemented a centralized authorization server using the oauth2.0 protocols and a password flow. I will post the user, pwd, secret and clientID to the auth server endpoint, get a JWT back and be on my way.
What I currently have in this existing "client" application is some spring security boilerplate that configures traffic based on roles.
What I need to do is (I think) - When I log into the application, I will receive a token. That token is stored in the session and whenever I access a page on that application, I will either confirm the token is still good or refresh the token so that I can continue accessing secured content.
I am assuming I need to do the following:
Add a resource server to my existing application (#EnableResourceServer), so that traffic flows through the oauth2.0 process.
Configure said resource server (similar to my WebSecurityConfigurerAdapter configure class), so that the token is checked
Keep everything else (roles, file structure, etc) the same
How do I do this with Spring? I have looked at tutorials and most everything is either same home resource and auth server solutions or has minimal explanation for how something can be configured.
Property file:
security.oauth2.client.client-id= clientid
security.oauth2.client.client-secret= clientsecret
security.oauth2.client.access-token-uri= http://localhost:1234/oauth/token
security.oauth2.client.user-authorization-uri= http://localhost:1234/oauth/authorize
security.oauth2.resource.service-id=res_id
security.oauth2.resource.user-info-uri= http://localhost:1234/me
security.oauth2.resource.token-info-uri= http://localhost:1234/oauth/check_token
security.oauth2.resource.token-type= Bearer
Current WebSecurityConfigurerAdapter:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#ComponentScan("com.testproj")
public class CustomWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
#Autowired
private CustomAuthenticationProvider authProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
// Authentication control
http
.authorizeRequests()
.antMatchers("/login.xhtml**").permitAll() // All everyone to see login page
.antMatchers("/login").permitAll() // All everyone to see login page
.antMatchers("/views/**").hasAnyAuthority("USER", "ADMIN")
.antMatchers("/javax.faces.resource/**").permitAll() // All everyone to see resources
.antMatchers("/resources/**").permitAll() // All everyone to see resources
.anyRequest().authenticated(); // Ensure any request to application is authenticated
// Login control
http
.formLogin()
.loginPage("/login.xhtml")
.loginProcessingUrl("/login")
.successForwardUrl("/views/home.xhtml")
.failureUrl("/login.xhtml?error=true");
// not needed as JSF 2.2 is implicitly protected against CSRF
http
.csrf().disable();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
}
Custom authenticator:
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
private AppUserDAO appUserDAO;
private String accessTokenUri = "http://localhost:1234/oauth/token";
private String clientId = "clientid";
private String clientSecret = "clientsecret";
public AccessTokenProvider userAccessTokenProvider() {
ResourceOwnerPasswordAccessTokenProvider accessTokenProvider = new ResourceOwnerPasswordAccessTokenProvider();
return accessTokenProvider;
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
final String username = authentication.getName();
final String password = authentication.getCredentials().toString();
List<String> scopes = new ArrayList<String>();
scopes.add("read");
final ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
resource.setUsername(username);
resource.setPassword(password);
resource.setAccessTokenUri(accessTokenUri);
resource.setClientId(clientId);
resource.setClientSecret(clientSecret);
resource.setGrantType("password");
resource.setScope(scopes);
// Generate an access token
final OAuth2RestTemplate template = new OAuth2RestTemplate(resource, new DefaultOAuth2ClientContext(new DefaultAccessTokenRequest()));
template.setAccessTokenProvider(userAccessTokenProvider());
OAuth2AccessToken accessToken = null;
try {
accessToken = template.getAccessToken();
System.out.println("Grabbed access token from " + accessTokenUri);
}
catch (OAuth2AccessDeniedException e) {
if (e.getCause() instanceof ResourceAccessException) {
final String errorMessage = String.format(
"While authenticating user '%s': " + "Unable to access accessTokenUri '%s'.", username,
accessTokenUri);
throw new AuthenticationServiceException(errorMessage, e);
}
throw new BadCredentialsException(String.format("Access denied for user '%s'.", username), e);
}
catch (OAuth2Exception e) {
throw new AuthenticationServiceException(
String.format("Unable to perform OAuth authentication for user '%s'.", username), e);
}
// Determine roles for user
List<GrantedAuthority> grantList = ...
// Create custom user for the principal
User user = .....
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, null /*dont store password*/, grantList);
return token;
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
Resource server:
#Configuration
#EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
????
}
Edit:
So I moved my httpsecurity configuration into my resource server configuration and I get "Access Denied" when I attempt to get to the /views/home.xhtml page.
The log indicates the principal is anonymous (it wasn't like that in securityconfig)
Secure object: FilterInvocation: URL: /views/home.xhtml; Attributes: [#oauth2.throwOnError(hasAuthority('USER'))]
Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken#fc4a062: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true;
Edit 2
This might be a question about what is possible with oauth2, because I'm kind of going insane.
There are 4 pieces according to the oauth specs - Auth server, resource server, client and resource owner
I guess my question is - can I actually combine the resource server and client? The resource owner would sign into my app (client), get authorization from the auth server to see their resources on the resource server/client.
I really wish I could find a good example of this - it seems so definitive and common.
Related
I have a question in relation with the combination of SwitchUser filter and authentication.
What I'm trying to achieve is to impersonate an existing user with the help of another user with elevated rights.
I was happy to find out that we can make use of the SwitchUserFilter provided by Spring Security, but when I tried to adapt it to my project and workflow it did not work as expected.
I have the following setup:
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Bean
public TokenAuthenticationFilter tokenAuthenticationFilter() {
return new TokenAuthenticationFilter();
}
#Bean
public SwitchUserFilter switchUserFilter() {
var filter = new SwitchUserFilter();
filter.setUserDetailsService(customUserDetailsService);
filter.setSwitchUserUrl("/impersonate");
filter.setSwitchFailureUrl("/switchUser");
filter.setTargetUrl("/user"); // this is already implemented in my app (GET /user)
return filter;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// I've stripped this to the bare minimum which reproduces my flow
// usually I also configure authenticated access to endpoints & other security related configurations
http.csrf().disable();
http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(switchUserFilter(), FilterSecurityInterceptor.class);
}
where
public class TokenAuthenticationFilter extends OncePerRequestFilter {
#Autowired
private TokenProvider tokenProvider;
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromToken(jwt);
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
log.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
}
In the token I save the user id, and the user details service performs a findById in user's database.
The flow I perform is as follows:
login into application (call endpoint, use authentication manager to authenticate based on username/pass, generate token and return it)
call /impersonate?username=anotherUser with the Bearer token from previous step
the application reaches TokenAuthenticationFilter, decodes the token, finds the user in the database and updates the security context
after that it reaches the SwitchUserFilter where it performs the switch (finds user in database, creates user details and finally updates security context)
--- until this point everything works fine ---
the SwitchUser filter is configured to redirect to GET /user
in debug mode I see that at this point, we reach again step 3 (decode token, find user in db, update security context), but it does not perform the switch
application reaches GET /user with the updated user from previous step (original user, not the switched one)
My question is - how is this combo of authenticating users and switch user filter is supposed to work?
I feel that the flow is kinda natural and normal, the redirect and subsequent requests that will be made from outside need to be authenticated based on the bearer token.
How is this supposed to work? What am I missing/doing wrong?
Thanks!
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 am developing a microservice ecosystem using spring-boot. The microservices which are in place at the moment :
Spring Cloud Gateway - Zuul (responsible also for authorization requests downstream for microservices - extracting tokens from requests and validates whether the user has the right role to perform requests),
SSO using spring security LDAP ( responsible for authenticate user and generate JWT tokens) , SSO has also just a login page using thymeleaf
Web interface using Thymeleaf without login page ( not sure if I should use here spring security, at the moment)
Another microservice which provides data to web ui based on request from the browser
Discovery services using Eureka
The idea is filtering all the requests on the gateway for validating and forward the requests. If the user is not authenticated or token is experied then forward the user to SSO for login.
The firewall will expose only the port on Gateway side then others one will be theirs ports blocked using firewall rules.
Now i am blocked without knowing where to go or if I should move the SSO together with the gateway ( conceptually wrong but it might be a workaround if i do not find any solution)
Following the issue : The user hits the gateway (ex. http://localhost:7070/web) then the gateway forward the user to (ex. http://localhost:8080/sso/login), after the credentials have been validated , the SSO creates the JWT tokens and add it to the Header of response.
Afterwards the SSO redirect the request back to the gateway (ex. http://localhost:7070/web).
Until here, everything works fine but when the request reaches the gateway there is no 'Authorization' header on request which means NO JWT token.
So the gateway should extract the token, check the credentials and forward the request to the Web interface (ex. http://localhost:9090)
I am aware that using Handler on SSO to redirect request won't work at all due to 'Redirect' from spring will remove the token from the header before redirect.
But I do not know whether there is another way to set again the JWT on the header after Spring has removed it from the request or not.
Is there any conceptually issue on the architecture side? How can I forward the JWT to the gateway for being checked?
SSO
#EnableWebSecurity
public class SecurityCredentialsConfig extends WebSecurityConfigurerAdapter {
#Value("${ldap.url}")
private String ldapUrl;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
// Stateless session; session won't be used to store user's state.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin()
.loginPage("/login")
// Add a handler to add token in the response header and forward the response
.successHandler(jwtAuthenticationSuccessHandler())
.failureUrl("/login?error")
.permitAll()
.and()
// handle an authorized attempts
.exceptionHandling()
.accessDeniedPage("/login?error")
.and()
.authorizeRequests()
.antMatchers( "/dist/**", "/plugins/**").permitAll()
.anyRequest().authenticated();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userDnPatterns("uid={0},ou=people")
.groupSearchBase("ou=groups")
.userSearchFilter("uid={0}")
.groupSearchBase("ou=groups")
.groupSearchFilter("uniqueMember={0}")
.contextSource()
.url(ldapUrl);
}
#Bean
public AuthenticationSuccessHandler jwtAuthenticationSuccessHandler() {
return new JwtAuthenticationSuccessHandler();
}
}
public class JwtAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
#Autowired
private JwtConfig jwtConfig;
#Autowired
private JwtTokenService jwtTokenService;
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException, ServletException {
String token = jwtTokenService.expiring(ImmutableMap.of(
"email", auth.getName(),
"authorities", auth.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.map(Object::toString)
.collect(Collectors.joining(","))));
response.addHeader(jwtConfig.getHeader(), jwtConfig.getPrefix() + token);
DefaultSavedRequest defaultSavedRequest = (DefaultSavedRequest) request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST");
if(defaultSavedRequest != null){
getRedirectStrategy().sendRedirect(request, response, defaultSavedRequest.getRedirectUrl());
}else{
getRedirectStrategy().sendRedirect(request, response, "http://localhost:7070/web");
}
}
}
Gateway
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtConfig jwtConfig;
#Value("${accessDeniedPage.url}")
private String accessDeniedUrl;
#Override
protected void configure(final HttpSecurity http) throws Exception {
http
.csrf().disable() // Disable CSRF (cross site request forgery)
// we use stateless session; session won't be used to store user's state.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin()
.loginPage("/sso/login")
.permitAll()
.and()
// handle an authorized attempts
// If a user try to access a resource without having enough permissions
.exceptionHandling()
.accessDeniedPage(accessDeniedUrl)
//.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
// Add a filter to validate the tokens with every request
.addFilterBefore(new JwtTokenAuthenticationFilter(jwtConfig), UsernamePasswordAuthenticationFilter.class)
// authorization requests config
.authorizeRequests()
.antMatchers("/web/**").hasAuthority("ADMIN")
// Any other request must be authenticated
.anyRequest().authenticated();
}
}
#RequiredArgsConstructor
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
private final JwtConfig jwtConfig;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 1. get the authentication header. Tokens are supposed to be passed in the authentication header
String header = request.getHeader(jwtConfig.getHeader());
// 2. validate the header and check the prefix
if(header == null || !header.startsWith(jwtConfig.getPrefix())) {
chain.doFilter(request, response); // If not valid, go to the next filter.
return;
}
// If there is no token provided and hence the user won't be authenticated.
// It's Ok. Maybe the user accessing a public path or asking for a token.
// All secured paths that needs a token are already defined and secured in config class.
// And If user tried to access without access token, then he/she won't be authenticated and an exception will be thrown.
// 3. Get the token
String token = header.replace(jwtConfig.getPrefix(), "");
try { // exceptions might be thrown in creating the claims if for example the token is expired
// 4. Validate the token
Claims claims = Jwts.parser()
.setSigningKey(jwtConfig.getSecret().getBytes())
.parseClaimsJws(token)
.getBody();
String email = claims.get("email").toString();
if(email != null) {
String[] authorities = ((String) claims.get("authorities")).split(",");
final List<String> listAuthorities = Arrays.stream(authorities).collect(Collectors.toList());
// 5. Create auth object
// UsernamePasswordAuthenticationToken: A built-in object, used by spring to represent the current authenticated / being authenticated user.
// It needs a list of authorities, which has type of GrantedAuthority interface, where SimpleGrantedAuthority is an implementation of that interface
final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
email, null, listAuthorities
.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()));
// 6. Authenticate the user
// Now, user is authenticated
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (Exception e) {
// In case of failure. Make sure it's clear; so guarantee user won't be authenticated
SecurityContextHolder.clearContext();
}
// go to the next filter in the filter chain
chain.doFilter(request, response);
}
}
#Component
public class AuthenticatedFilter extends ZuulFilter {
#Override
public String filterType() {
return PRE_TYPE;
}
#Override
public int filterOrder() {
return 0;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() throws ZuulException {
final Object object = SecurityContextHolder.getContext().getAuthentication();
if (object == null || !(object instanceof UsernamePasswordAuthenticationToken)) {
return null;
}
final UsernamePasswordAuthenticationToken user = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
final RequestContext requestContext = RequestContext.getCurrentContext();
/*
final AuthenticationDto authenticationDto = new AuthenticationDto();
authenticationDto.setEmail(user.getPrincipal().toString());
authenticationDto.setAuthenticated(true);
authenticationDto.setRoles(user.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList())); */
try {
//requestContext.addZuulRequestHeader(HttpHeaders.AUTHORIZATION, (new ObjectMapper()).writeValueAsString(authenticationDto));
requestContext.addZuulRequestHeader(HttpHeaders.AUTHORIZATION, (new ObjectMapper()).writeValueAsString("authenticationDto"));
} catch (JsonProcessingException e) {
throw new ZuulException("Error on JSON processing", 500, "Parsing JSON");
}
return null;
}
}
There is an issue about JWT. It is called "Logout Problem". First you need to understand what it is.
Then, check TokenRelay filter (TokenRelayGatewayFilterFactory) which is responsible for passing authorization header to downstream.
If you look at that filter, you will see that JWTs are stored in ConcurrentHashMap (InMemoryReactiveOAuth2AuthorizedClientService). The key is session, the value is JWT. So, session-id is returned instead of JWT header as the response provided.
Until here, everything works fine but when the request reaches the
gateway there is no 'Authorization' header on request which means NO
JWT token.
Yes. When the request comes to gateway, TokenRelay filter takes session-id from request and find JWT from ConcurrentHashMap, then it passes to Authorization header during downstream.
Probably, this flow is designed by spring security team to address JWT logout problem.
I'm using spring basic authentication with a custom authentication provider:
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomAuthenticationProvider authProvider;
#Override
protected void configure(
AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.httpBasic();
}
And
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
if (customauth()) { // use the credentials
// and authenticate against the third-party system
{
return new UsernamePasswordAuthenticationToken(
name, password, new ArrayList<>());
}
} else {
return null;
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(
UsernamePasswordAuthenticationToken.class
);
}
To test this I'm using postman with the following tests:
invalid credentials -> 401 unauthorized
correct credentials -> 200 OK
invalid credentials -> 200 OK
My problem is that the last request should return 401 unauthorized and every following request after a successful login is 200 OK even with a wrong token and without token.
Thanks in advance.
When you logged in successfully, Spring Security will create an Authentication object and will put it in SecurityContext in your HTTP session. As far as you have a valid session with a valid Authentication object at the server, Spring Security won't authenticate your request again and will use the Authentication object saved in your session.
This is a Spring Security feature, see SEC-53:
Check the SecurityContextHolder for an authenticated Authentication and reuse it in that case, do not call the authentication manager again.
If you like to reauthenticate, you could
use no session at all
logout before reauthenticate
In both cases Spring Security will not find an authenticated user saved in the session and will use the new username and password for authentication.
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)};
}
}
}