Spring Cloud Zuul API gateway doesn't forward JWT token for stateless sessions - spring-boot

I am trying to implement Microservices architecture backend using Spring Boot 1.5.6.RELEASE and Spring Cloud Dalston.SR3 that would be consumed by mobile/web endpoints.
API Gateway application
#SpringBootApplicatio
#EnableEurekaClient
#EnableZuulProxy
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
API security
#Configuration
#EnableWebSecurity
#Order(ManagementServerProperties.ACCESS_OVERRIDE_ORDER)
#EnableOAuth2Sso
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/sign-up", "/login")
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf()
.ignoringAntMatchers("/sign-up", "/login")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// #formatter:on
}
}
Gradle security related dependencies
// Spring OAuth2 security
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.security.oauth:spring-security-oauth2")
compile("org.springframework.cloud:spring-cloud-starter-oauth2")
compile("org.springframework.security:spring-security-jwt")
Zuul routes
zuul:
ignoredServices: '*'
routes:
user-service:
path: /user-service/**
stripPrefix: false
serviceId: user-webservice
sensitiveHeaders:
task-service:
path: /task-service/**
stripPrefix: false
serviceId: task-webservice
sensitiveHeaders:
user:
path: /userauth/**
stripPrefix: false
serviceId: auth-server
sensitiveHeaders:
I am able to get the access token from the authorization server(stateless sessions - no JSESSIONID cookie)
curl -D - --request POST -u acme:acmesecret
"http://localhost:8899/userauth/oauth/token?grant_type=password&username=<...>&password=<...>"
{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDQ3ODg4NzgsInVzZXJfbmFtZSI6IjcyMTk2MTk2NDEiLCJhdXRob3JpdGllcyI6WyJST0xFX1BBVElFTlQiXSwianRpIjoiZThhMzBjNmQtZjA2MS00MWEzLWEyZGItYTZiN2ZjYTI5ODk1IiwiY2xpZW50X2lkIjoiYWNtZSIsInNjb3BlIjpbIm9wZW5pZCJdfQ.AhF_kqfsRYM1t1HVT........
I can use the access token to request data from the authorization server or another resource
curl -D - --request GET -H "Authorization: Bearer
eyJhbGciOiJSUzI1...." http://localhost:8899/userauth/me
{"authorities":[{"authority":"ROLE_P.........}
curl -D - --request GET -H "Authorization: Bearer
eyJhbGciOiJSUzI1NiIsInR5......." http://localhost:8081/user-service/
[{"firstName":"Anil".....}]
However for the same requests routed through the API gateway, it fails at the Gateway itself and is filtered as AnonymousAuthenticationToken.
curl -D - --request GET -H "Authorization: Bearer
eyJhbGciOiJSUzI1...." http://localhost:8765/user-service/
HTTP/1.1 302 Set-Cookie:
XSRF-TOKEN=b5a1c34e-e83c-47ea-86a6-13a237c027d4; Path=/ Location:
http://localhost:8765/login
I was assuming that with #EnableZuulProxy and #EnableOAuth2Sso, Zuul would take care to forward the bearer token to the downstream services but that is not happening. I already have a working sample that uses HTTP session and browser redirection to get the API gateway to pass tokens - https://github.com/anilallewar/microservices-basics-spring-boot
But I am struggling to get it to work with Stateless sessions, any pointers what might be missing on the Zuul API gateway side?

Zuul considers Authorization header as a sensitive header by default and does not pass it to downstream requests. To override this, you can modify sensitiveHeaders in Zuul configuration either globally (for all routes):
zuul:
# exclude Authorization from sensitive headers
sensitiveHeaders: Cookie,Set-Cookie
ignoredServices: '*'
Or for a specific route:
zuul:
ignoredServices: '*'
routes:
user-service:
path: /user-service/**
stripPrefix: false
serviceId: user-webservice
# exclude Authorization from sensitive headers
sensitiveHeaders: Cookie,Set-Cookie
To find more about the problem, check this question:
Authorization header not passed by ZuulProxy starting with Brixton.RC1

I was assuming that with #EnableZuulProxy and #EnableOAuth2Sso, Zuul would take care to forward the bearer token to the downstream services but that is not happening.
I assumed the same thing, but in my (painful) experience, #EnableOAuth2Sso secures all endpoints with SSO and blocks even the requests with a Bearer token from getting to downstream services. I had to change my gateway to disable authentication on the routes that lead to my resources, so that the request with a Bearer token could get through.
Try adding /user-service/** and /task-service/** to your permitAll() matcher:
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/sign-up", "/login", "/task-service/**", "/user-service/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf()
.ignoringAntMatchers("/sign-up", "/login")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// #formatter:on
}

Related

Allow only acces by Zuul

I have several microservices, that I only want to access them by zuul resources server and avoid direct access.
Zuul server implements jwt authentication.
I have these redirections in application.yaml file
zuul:
routes:
servicio-carga-electores:
path: /cargaElectores/**
service-id: servicio-carga-electores
url: ${server-uri}/cargaElectoresWS
servicio-oauth-server:
service-id: servicio-oauth-server
path: /oauth/**
url: ${server-uri}/celec-oauth-server
sensitive-headers:
- Cookie
- Set-Cookie
ribbon:
eureka:
enabled: false
Trying with postman, after getting jwt token, all redirections causes 403 forbidden.
http://localhost:8080/celec-zuul-server/cargaElectores/recupera_ficheros
{
"timestamp": "2022-08-24T08:00:12.806+0000",
"status": 403,
"error": "Forbidden",
"message": "Forbidden",
"path": "/cargaElectoresWS/recupera_ficheros"
}
I've seen in other places using hasIpAdress, but isn't working.
#Configuration
#EnableWebSecurity
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/estado.jsp").permitAll()
.and()
.authorizeRequests()
.antMatchers("/**").hasIpAddress("::1");
}
}
If I don't use oauth2, the redirection goes right, but I can access directly to the microservices.
Thanks for advance.
José Pascual

Spring Cloud Gateway + Keycloak CORS not working

I developed backend microservice application using Spring Boot and put API Gateway in front of microservices. To authenticate users I am using Keycloak.
Right now I am developing frontend application using Svelte, I configured my application.yml in gateway application like this:
spring:
cloud:
gateway:
default-filters:
- TokenRelay
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: "*"
allowedHeaders: "*"
add-to-simple-url-handler-mapping: true
However, when I am trying to send AJAX request I get the CORS error.
Also I Have spring security (through org.springframework.boot:spring-boot-starter-oauth2-client and org.springframework.boot:spring-boot-starter-oauth2-resource-server dependencies). I defined SecurityWebFilterChain as:
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.cors().and()
.authorizeExchange()
.pathMatchers("/actuator/**")
.permitAll()
.and()
.authorizeExchange()
.anyExchange()
.authenticated()
.and()
.oauth2Login(); // to redirect to oauth2 login page.
return http.build();
}
When putting build of frontend in static folder there is no CORS error, but for development I need developer node.js server on localhost on different port.
So, how to fix this cors issue?
You can create a class to define the Cors mapping like this:
#Configuration
#EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
}
The example above enables CORS requests from any origin to any endpoint in the application.
To lock this down a bit more, the registry.addMapping method returns a CorsRegistration object, which we can use for additional configuration. There’s also an allowedOrigins method that lets us specify an array of allowed origins. This can be useful if we need to load this array from an external source at runtime.
Additionally, there are also allowedMethods, allowedHeaders, exposedHeaders, maxAge and allowCredentials that we can use to set the response headers and customization options.
CORS With Spring Security:
If you use Spring Security in youproject, you must take an extra step to make sure it plays well with CORS. That's because CORS needs to be processed first. Otherwise, Spring Security will reject the request before it reaches Spring MVC.
Luckily, Spring Security provides an out-of-the-box solution:
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()...
}
}

Spring Security with OAuth2 losing session

We have a Spring Boot-based Gateway using Spring Security, OAuth2 login, and Zuul routing. It is also using Spring Session to store sessions in Redis. This Gateway stores an OAuth2 token in the session and forwards the OAuth2 Bearer token to backend services.
We have an issue where users are being signed out quite often. It appears this happens roughly hourly. We are not even quite sure what is causing this with all the different tools in place.
Our session cookie in the browser expires in a longer period of time. So I suspect it is either Spring invalidating the session, or the OAuth2 token expiring.
From a quick inspection of the code, it appears that OAuth2TokenRelayFilter supports refreshing the token. Is this correct?
How can track down the cause of this and fix it?
For reference, we are using these versions:
Spring Boot 2.1.12
Spring Cloud Greenwich.SR4
Here are some relevant snippets.
Our web security config for the web pages.
#Configuration
#EnableWebSecurity
#EnableOAuth2Sso
#Order(SecurityProperties.BASIC_AUTH_ORDER - 2)
#Profile("!security-disabled")
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.authorizeRequests()
.antMatchers("/login", "/login/**", "/favicon.ico").permitAll()
.antMatchers("/signout").authenticated()
.anyRequest().hasAnyRole("ADMIN", "MEMBER")
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.httpBasic()
.disable()
.formLogin()
.disable()
.logout()
.logoutUrl("/signout")
.deleteCookies("SESSION")
.and()
// #formatter:on
}
Security configuration for API paths.
#Configuration
#Order(SecurityProperties.BASIC_AUTH_ORDER - 2 - 10)
#Profile("!security-disabled")
public class ApiSecurityConfig extends WebSecurityConfigurerAdapter
{
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.requestMatchers()
.antMatchers("/api/**")
.and()
.authorizeRequests()
.antMatchers("/**").hasAnyRole("ADMIN", "MEMBER")
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.headers()
.frameOptions().sameOrigin()
.and()
.httpBasic()
.disable()
.formLogin()
.disable()
.logout()
.disable()
.exceptionHandling().authenticationEntryPoint(new Http403ForbiddenEntryPoint());
// #formatter:on
}
}
Update
We have done some debugging of the Spring internals. First, we found that we were missing an OAuth2RestTemplate. Per the OAuth2 Boot documentation we found how to add it with:
#Bean
public OAuth2RestTemplate oauth2RestTemplate(
OAuth2ClientContext oauth2ClientContext,
OAuth2ProtectedResourceDetails details)
{
return new OAuth2RestTemplate(details, oauth2ClientContext);
}
This is now throwing an exception when OAuth2TokenRelayFilter calls restTemplate.getAccessToken().getValue();.
A redirect is required to get the users approval
This exception is thrown from AuthorizationCodeAccessTokenProvider.
OAuth2TokenRelayFilter
OAuth2TokenRelayFilter is a pre type filter which set the contexts with ACCESS_TOKEN and TOKEN_TYPE which will be used for the further authentication. It validates the tokens using getAccessToken() method and responds with "Cannot obtain valid access token" with 401 status.
You may check the validity of tokens and refresh token is correctly configured with grant_type as refresh_token as The Refresh Token grant type is used by clients to exchange a refresh token for an access token when the access token has expired which allows clients to continue to have a valid access token without further interaction with the user.
In case if you want to disable OAuth2TokenRelayFilter, you may use the following
zuul.OAuth2TokenRelayFilter.pre.disable=true

Keycloak with Spring boot - How to apply resource scopes

I'm using Keycloak 7.0.1 with Spring Boot 1.5.16.RELEASE securing endpoint by specifying resources, role based polices and permissions - and that's works as expected.
The tricky things is to secure only POST and allow GET requests to one some particular URIs. What have I done in application.yml:
policy-enforcer-config:
enforcement-mode: ENFORCING
paths[0]:
name: all
path: /*
paths[1]:
name: test post
path: /my/url
methods[0]:
method: GET
scopes[0]: view
methods[1]:
method: POST
scopes[0]: edit
In keyclaok I've created edit and view scopes, /my/url resource, policy with role and negative decision (if user has that role - deny access), permission contains resource, scope and policy. Evaluation works as expected, but my spring application always receive 403 error.
Could you provide me with an example of resource scope usage or advice what else shout be done to make that working?
Could be multiple problems, but first of all, check your SecurityConfig.
This is what we have in place:
#Configuration
#EnableWebSecurity
#KeycloakConfiguration
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableConfigurationProperties(KeycloakSpringBootProperties.class)
#RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
bla-bla..
#Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers(HttpMethod.POST, "/auth/**")
.antMatchers(HttpMethod.OPTIONS,"/**")
// allow anonymous resource requests
.and()
.ignoring()
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/actuator/**"
)
;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// we don't need CSRF because our token is invulnerable
.csrf().disable()
.exceptionHandling()
.defaultAuthenticationEntryPointFor(
getRestAuthenticationEntryPoint(),
new AntPathRequestMatcher("/**")
)
.authenticationEntryPoint(unauthorizedHandler).and()
// don't create session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// system state endpoint
.antMatchers("/ping").permitAll()
.antMatchers(HttpMethod.GET, "/whatever/you/need/to/open/to/public/one", "/whatever/you/need/to/open/to/public/two").permitAll()
// User authentication actions
.antMatchers("/auth/**").permitAll()
.antMatchers("/**/*.css").permitAll()
.anyRequest().authenticated()
;
http
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
;
// disable page caching
http
.headers()
.frameOptions().sameOrigin()
.cacheControl();
}
If you want to restrict REST API endpoints with role, add #PreAuthorize("hasRole('your.role.from.keycloak')") for your controller method

How to config Spring #EnableOAuth2Sso App with self-signed SSL OAuth2 Server?

My app works well with normal non-SSL OAuth2 Servers. But I dont know how to config client to work with the SSL equivalents. I always get 401 when oauth2 server redirects back to http://localhost:8051/app1/login after success authentication and granting permissions. My App code likes below:
application.yml:
server:
port: 8051
servlet:
contextPath: /app1
security:
oauth2:
client:
clientId: sampleclient
clientSecret: samplepw
accessTokenUri: https://localhost:8668/sso-server/oauth/token
userAuthorizationUri: https://localhost:8668/sso-server/oauth/authorize
tokenName: oauth_token
resource:
userInfoUri: https://localhost:8688/api/me
Application.java
#SpringBootApplication
#EnableOAuth2Sso
public class App1 extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login", "/error**", "/webjars/**").permitAll()
.anyRequest().authenticated()
.and().logout()
.logoutUrl("/logout")
.logoutSuccessUrl("https://localhost:8668/sso-server/logout")
.invalidateHttpSession(true)
.deleteCookies("client-session", "JSESSIONID")
.permitAll()
.and().csrf().disable()
;
}
public static void main(String[] args) {
SpringApplication.run(App1.class, args);
}
}
OAuth2 Servers work as expected and I can call all oauth APIs that clients need with https schema on Postman, so I think we have nothing to do with them and don't post the code here.

Resources