Spring Cloud Gateway with spring-boot-starter-web - spring-boot

I am creating gateway for Spring Boot microservices using Spring Cloud Gateway. Gateway is also responsible for JWT authorization using Spring Security.
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
...
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String header = request.getHeader(JwtProperties.HEADER_STRING);
if (header == null || !header.startsWith(JwtProperties.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
Authentication authentication = getUsernamePasswordAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
private Authentication getUsernamePasswordAuthentication(HttpServletRequest request) {
String token = request.getHeader(JwtProperties.HEADER_STRING).replace(JwtProperties.TOKEN_PREFIX, "");
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC512(JwtProperties.SECRET.getBytes())).build().verify(token);
String username = decodedJWT.getSubject();
if (username != null) {
UserPrincipal principal = (UserPrincipal) userPrincipalService.loadUserByUsername(username);
Authentication auth = new UsernamePasswordAuthenticationToken(username, null, principal.getAuthorities());
return auth;
}
return null;
}
}
This filter is registred in configure method like this:
#Configuration
#EnableWebSecurity
public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter {
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(new JwtAuthorizationFilter(authenticationManager(), userPrincipalService))
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll()
...
.anyRequest().authenticated();
}
...
}
As you can see, Spring Security is using HttpServletRequest, HttpServletResponse, FilterChain interfaces which belong to spring-boot-starter-web. But that is main problem beacause it's incompatible with spring cloud gateway.
Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway at this time. Please remove spring-boot-starter-web dependency.
Is there any way to avoid this error or any different solution for implementing jwt authorization filter on gateway?
Thanks!

In the Documentation of spring cloud gateway it is explicitely stated that this product runs on top of Netty and requires webflux, hence it's not compatible with spring MVC.
The filter that you use (JwtAuthorizationFilter) is something that belongs to the non-reactive world so you probably should rewrite it with spring security for web flux building blocks.
Disclaimer, I'm not a spring web flux / spring-security expert, but please consider checking This application - it shows how to define JWT secured application with a reactive version of spring security.
So bottom line you should choose whether you want a reactive application or a traditional one and use the relevant technologies but you can't really mix them.

Related

How to add Jwt Token based security In Microservices

In my microservices, I will try to implement Jwt spring-security, But I don't know how to apply it.
In my microservices, I have used the 2020.0.3 spring cloud version.
In user services, I have connected the department service using the Rest template.
I need help with how to add Jwt security in these microservices.
This is 4 microservices
Server = Eureka Server
service-API-gateway = Spring cloud Apigateway
service-department & services-user = These two microservices connect with Rest template
Microservices Project Structure
: https://i.stack.imgur.com/ajTiX.png
So at a higher level, Spring Security is applied on controller level when using jwt as authentication. First you need to add a Security config that will extend WebSecurityConfigurerAdapter (this is common for http based security) and in that class you need to define configure method like:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.csrf().disable() // IF your clients connect without a cookie based, this will be fine
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/register", "/login","/your_open_endpoints_etc").permitAll()
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
Then in the filter class which extends OncePerRequestFilter, you can define the do filter like this, you have to set the UsernamePasswordAuthenticationFilter instance inside the Spring authentication context:
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
logger.info("do filter...");
String token = jwtProvider.getTokenFromRequest((HttpServletRequest) httpServletRequest);
try{
if (token != null && jwtProvider.validateToken(token)) {
String username = jwtProvider.getUsernameFromToken(token);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, jwtProvider.getAuthorities(token));
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
catch (RuntimeException e)
{
// Some general Exception handling that will wrap and send as HTTP Response
}
}
Check on the extending filters further, they might change as per your requirement
finally in rest endpoints you can safe guard like:
#PreAuthorize("hasRole('ROLE_YOURROLE')")
#GetMapping(path = "/your_secured_endpoint", consumes = "application/json",
produces = "application/json")
public ResponseEntity<List<SomePOJOObject>> getAllAppointmentsForPatient()
{
return new ResponseEntity<>(thatSomePOJOObjectListYouWant, HttpStatus.OK);
}

Spring Security OAuth - how to disable login page?

I want to secure my application with Spring Security, using OAuth 2. However, I don't want the server to redirect incoming unauthorized requests, but instead to respond with HTTP 401. Is it possible?
Example: this code redirects requests to a default login page.
application.properties
spring.security.oauth2.client.registration.google.client-id=...
spring.security.oauth2.client.registration.google.client-secret=...
AuthConfig.java
#Configuration
public class AuthConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/secured/**").authenticated()
.anyRequest().permitAll()
.and()
.oauth2Login();
// https://stackoverflow.com/questions/31714585/spring-security-disable-login-page-redirect
// deos not work
// .and()
// .formLogin().successHandler((request, response, authentication) -> {});
}
}
You need to create new authentication entry point and set it in configuration.
#Configuration
public class AuthConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new AuthenticationEntryPoint())
.and()
.authorizeRequests()
.antMatchers("/secured/**").authenticated()
.anyRequest().permitAll()
.and()
.oauth2Login();
}
}
public class AuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
public AuthenticationEntryPoint() {
super("");
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(401, "Unauthorized");
}
}
You need to set oauth2Login.loginPage in your HttpSecurity config and create a controller mapping to return whatever you want. Here's a simple example.
So in your security config
http
.authorizeRequests()
.antMatchers("/noauth").permitAll()
.oauth2Login()
.loginPage("/noauth")
In a controller
#GetMapping("/noauth")
public ResponseEntity<?> noAuth() {
Map<String, String> body = new HashMap<>();
body.put("message", "unauthorized");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(body);
}
You can pass a map or pojo to the body method.
I would like to expand on Petr's answer by explaining that apparently for the time being first of all, the default login page is shown when there are more than one OAuth2 configured providers. I would expect that Spring Boot would have a smart trick to bypass this page easily and choose the right provider automatically, basing e.g. on the existence of the provider's client ID in the original request. I found out the hard way that this is not the case. So the way to do this is.. this not very apparent trick of providing a custom handler for failures - that will REDIRECT the user to the correct OAuth2 endpoint for each provider, based on the original HTTP request URL. I tried this and it works and I spent a whole day trying all manners of other solutions - my original scenario was to pass additional parameters to OAuth2 scheme in order to be able to get them back on successful authentication - they used to do this appending Base64 encoded information to the "state" URL request parameter, but Spring Security does not allow this at the moment. So the only alternative was to call a Spring Security-protected URL with those parameters already there, so when the successful authentication happens, this URL is accessed again automatically with those parameters intact.
Related: Multiple Login endpoints Spring Security OAuth2

Spring Boot - How to disable Keycloak?

I have a Spring Boot project with keycloak integrated. Now I want to disable keycloak for testing purposes.
I tried by adding keycloak.enabled=false to application.properties as mentioned in Keycloak documentation but it didnt work.
So how do I disable it?
Update 2022
Please follow this excellent guide on Baeldung.
For anyone who might have the same trouble, here is what I did.
I didn't disable Keycloak but I made a separate a Keycloak config file for testing purposes.
Here is my config file
#Profile("test")
#Configuration
#EnableWebSecurity
public class SecurityTestConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").permitAll();
http.headers().frameOptions().disable();
http.csrf().disable();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/**");
}
#Bean
#Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public AccessToken accessToken() {
AccessToken accessToken = new AccessToken();
accessToken.setSubject("abc");
accessToken.setName("Tester");
return accessToken;
}
}
Please note it is important to use this only in a test environment and therefore I have annotated the config as #Profile("test"). I have also added an AccessToken bean since some of the auditing features in my application depend on it.
It should work, but based on the last comment on the jira ticket for this, it seems it is not.
As the description state you could exclude the spring boot autoconfiguration for keycloak adding to your application.properties: spring.autoconfigure.exclude=org.keycloak.adapters.springboot.KeycloakSpringBootConfiguration
You need to exclude keycloak auto configuration. In order to do this just add this entry to your related spring configuration file, in your case application.properties.
spring.autoconfigure.exclude = org.keycloak.adapters.springboot.KeycloakAutoConfiguration
My Workaround:
1. Create a Custom-Filter and add it to the (Spring) Security-Chain in early position.
2. Create a flag in the application.yml (securityEnabled)
3. Query the flag in the Custom-Filter. If 'true' simply go on with the next filter by calling chain.doFilter(). If 'false' create a dummy Keycloak-Account set the roles you need and set it to the context.
4. By the way the roles are also outsourced to the application.yml
5. Skip the rest of the filters in the Security-Chain (so no keycloak-stuff is executed and the corresponding Authorization happend)
In Detail:
1. Class of Custom-Filter
public class CustomFilter extends OncePerRequestFilter {
#Value("${securityEnabled}")
private boolean securityEnabled;
#Value("${grantedRoles}")
private String[] grantedRoles;
#Override
public void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
if (!securityEnabled){
// Read roles from application.yml
Set<String> roles = Arrays.stream(grantedRoles)
.collect(Collectors.toCollection(HashSet::new));
// Dummy Keycloak-Account
RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(null, null, null, null, null, null, null);
final KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = new KeycloakPrincipal<>("Dummy_Principal", session);
final KeycloakAccount account = new SimpleKeycloakAccount(principal, roles, principal.getKeycloakSecurityContext());
// Dummy Security Context
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(new KeycloakAuthenticationToken(account, false));
SecurityContextHolder.setContext(context);
// Skip the rest of the filters
req.getRequestDispatcher(req.getServletPath()).forward(req, res);
}
chain.doFilter(req, res);
}
}
2. Insert Custom-Filter in the http-Configuration of Spring-Security
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.cors()
.and()
.csrf()
.disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.sessionAuthenticationStrategy(sessionAuthenticationStrategy())
.and()
.addFilterAfter(CustomFilter(), CsrfFilter.class)
.authorizeRequests()
.anyRequest().permitAll();
}
Have a look at the default Filter-Chain after configuring Keycloak:
Filter-Chain
So it´s obvious to insert the Custom-Filter at position 5 to avoid the whole Keycloak-Magic.
I have used this workaround to defeat the method security and it´s #Secured-Annotation.
Updated answer for spring boot 2.5.6 and keycloak 16.1.0
Set this in your application.properties:
spring.autoconfigure.exclude=org.keycloak.adapters.springboot.KeycloakAutoConfiguration
(The autoconfig class name has changed since earlier answers)
The keycloak adapter dependency brings in the standard spring security autoconfig too, so if you want to disable both use this:
spring.autoconfigure.exclude=org.keycloak.adapters.springboot.KeycloakAutoConfiguration,org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration

Securing a REST application in SpringBoot and accessing it from a Rest Client

I am pretty new to Springboot. I have developed a rest server but I was wondering how to perform Basic authentication from a client and how to configure the spring boot server to authenticate request. The tutorials I saw online didn't include a restful client. Would be great if you can show some code including both the client request and server authentication process with springboot rest.
On the client side since you are using Jersey Client you need to do something like the following:
Client c = Client.create();
c.addFilter(new HTTPBasicAuthFilter(user, password));
One the server side you need to enable Spring Security and set Basic Authentication for it which would look something like the following (this is the simplest possible case).
#Configuration
#EnableWebSecurity
public class RootConfig extends WebSecurityConfigurerAdapter {
#Override
protected void registerAuthentication(AuthenticationManagerBuilder auth) throws Exception
{
auth.inMemoryAuthentication()
.withUser("tester").password("passwd").roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeUrls()
.anyRequest().authenticated()
.and()
.httpBasic();
}
}

Spring Security - Token based API auth & user/password authentication

I am trying to create a webapp that will primarily provide a REST API using Spring, and am trying to configure the security side.
I am trying to implement this kind of pattern: https://developers.google.com/accounts/docs/MobileApps (Google have totally changed that page, so no longer makes sense - see the page I was referring to here: http://web.archive.org/web/20130822184827/https://developers.google.com/accounts/docs/MobileApps)
Here is what I need to accompish:
Web app has simple sign-in/sign-up forms that work with normal spring user/password authentication (have done this type of thing before with dao/authenticationmanager/userdetailsservice etc)
REST api endpoints that are stateless sessions and every request authenticated based ona token provided with the request
(e.g. user logins/signs up using normal forms, webapp provides secure cookie with token that can then be used in following API requests)
I had a normal authentication setup as below:
#Override protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/resources/**").permitAll()
.antMatchers("/mobile/app/sign-up").permitAll()
.antMatchers("/v1/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/")
.loginProcessingUrl("/loginprocess")
.failureUrl("/?loginFailure=true")
.permitAll();
}
I was thinking of adding a pre-auth filter, that checks for the token in the request and then sets the security context (would that mean that the normal following authentication would be skipped?), however, beyond the normal user/password I have not done too much with token based security, but based on some other examples I came up with the following:
Security Config:
#Override protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.addFilter(restAuthenticationFilter())
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.exceptionHandling().authenticationEntryPoint(new Http403ForbiddenEntryPoint()).and()
.antMatcher("/v1/**")
.authorizeRequests()
.antMatchers("/resources/**").permitAll()
.antMatchers("/mobile/app/sign-up").permitAll()
.antMatchers("/v1/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/")
.loginProcessingUrl("/loginprocess")
.failureUrl("/?loginFailure=true")
.permitAll();
}
My custom rest filter:
public class RestAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public RestAuthenticationFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
}
private final String HEADER_SECURITY_TOKEN = "X-Token";
private String token = "";
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
this.token = request.getHeader(HEADER_SECURITY_TOKEN);
//If we have already applied this filter - not sure how that would happen? - then just continue chain
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
//Now mark request as completing this filter
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
//Attempt to authenticate
Authentication authResult;
authResult = attemptAuthentication(request, response);
if (authResult == null) {
unsuccessfulAuthentication(request, response, new LockedException("Forbidden"));
} else {
successfulAuthentication(request, response, chain, authResult);
}
}
/**
* Attempt to authenticate request - basically just pass over to another method to authenticate request headers
*/
#Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
AbstractAuthenticationToken userAuthenticationToken = authUserByToken();
if(userAuthenticationToken == null) throw new AuthenticationServiceException(MessageFormat.format("Error | {0}", "Bad Token"));
return userAuthenticationToken;
}
/**
* authenticate the user based on token, mobile app secret & user agent
* #return
*/
private AbstractAuthenticationToken authUserByToken() {
AbstractAuthenticationToken authToken = null;
try {
// TODO - just return null - always fail auth just to test spring setup ok
return null;
} catch (Exception e) {
logger.error("Authenticate user by token error: ", e);
}
return authToken;
}
The above actually results in an error on app startup saying: authenticationManager must be specified
Can anyone tell me how best to do this - is a pre_auth filter the best way to do this?
EDIT
I wrote up what I found and how I did it with Spring-security (including the code) implementing a standard token implementation (not OAuth)
Overview of the problem and approach/solution
Implementing the solution with Spring-security
Hope it helps some others..
I believe the error that you mention is just because the AbstractAuthenticationProcessingFilter base class that you are using requires an AuthenticationManager. If you aren't going to use it you can set it to a no-op, or just implement Filter directly. If your Filter can authenticate the request and sets up the SecurityContext then usually the downstream processing will be skipped (it depends on the implementation of the downstream filters, but I don't see anything weird in your app, so they probably all behave that way).
If I were you I might consider putting the API endpoints in a completely separate filter chain (another WebSecurityConfigurerAdapter bean). But that only makes things easier to read, not necessarily crucial.
You might find (as suggested in comments) that you end up reinventing the wheel, but no harm in trying, and you will probably learn more about Spring and Security in the process.
ADDITION: the github approach is quite interesting: users just use the token as a password in basic auth, and the server doesn't need a custom filter (BasicAuthenticationFilter is fine).

Resources