Spring Cloud Gateway YML routing - is there a way to check permissions? - java

I have some microservices, and a Gateway using Spring Cloud. I'm trying to set up the routing in the Gateway. Ideally, I would like to set up the routing in the YML file, or with a RouteLocator bean.
But currently, in my Gateway, I have REST endpoints for every route, which is just sending the request onwards using a WebClient.
(Prior to this, I have a ReactiveAuthenticationManager filter which validates a JWT token. It returns a UsernamePasswordAuthenticationToken which includes some user authorities it gets from the token).
Here's an example where I have set up a route to a microservice called the tracking service:
Controller:
#RestController
#RequestMapping("/tracking-service/tracking")
public class TrackingController {
#Autowired
private TrackingService trackingService;
#GetMapping
public Flux getAllTracking() {
return trackingService.getAllTracking();
}
}
Service:
#Service
public class TrackingService {
private WebClient webClient;
#PreAuthorize("hasAuthority('MANAGER')")
public Flux getAllTracking() {
//Make HTTP call to the tracking service
}
}
The reason I have done it this way is because of the #PreAuthorize annotation. If the client's JWT token does not include the 'MANAGER' token, then this will return a 403 forbidden status. Not all of the endpoints require the MANAGER authority and some of the endpoints require other authorities.
My question - is it possible to do this when routing with the YML? I was hoping to see something that looks like this, but I'm not sure it is possible? I have read the Spring docs and looked at all the available filters, and there was nothing that does this job that I could see.
spring:
cloud:
gateway:
routes:
- id: tracking
uri: http://tracking-service
predicates:
- Path=/tracking-service/**
filters:
- StripPrefix=1
- PreAuthorize=hasAuthority('MANAGER')
Thanks.

API Gateway and authorization are two separation of concern. Ideally authorization should not be mixed in gateway application. For permission you can directly map it to service like this.
#RestController
public class ResourceREST {
#RequestMapping(value = "/resource/", method = RequestMethod.GET)
#PreAuthorize("#customPermissionEvaluator.hasPrivilege(authentication,'somepermission')")
public Mono<ResponseEntity<?>> user() {
return Mono.just(ResponseEntity.ok(new Message("Access allowed")));
}
}
And define custom permission evaluator
#Service
public class CustomPermissionEvaluator {
public boolean hasPrivilege(Authentication auth, String permission) {
String requiredPermissionCheck = permission.toLowerCase();
for (GrantedAuthority grantedAuth : auth.getAuthorities()) {
if (grantedAuth.getAuthority().contains(requiredPermissionCheck)) {
return true;
}
}
return false;
}
}
As such gateway does not provide any authorization in the routing which is ideal.

Related

Spring Cloud Gateway Requests Get Turned into the Cookie Value instead of GET

I am new to Spring Cloud Gateway and I am struggling to debug a 404. The current gateway flow has our web app sending an HTTP request to our Gateway with a SESSION Cookie value. It takes this cookie value and sends it to micro-service A and micro-service b to have them both authenticate the request. Then once the request is authenticated it makes a request again to micro-service A to obtain the data that requested. This request fails instantly at micro service A with the following error org.springframework.web.serverlet.PageNotFound: No mapping for SES ION=<session cookie value>. It seems that somewhere in the flow the HTTP Request from Spring Cloud Gateway to micro-service A changes from a GET request to the cookie value. Has anyone ran into this before or know how I could debug this?
The Filter:
#Component
public class Filter {
private final AuthFilter authFilter;
public Filter(final AuthFilter authFilter) {
this.authFilter = authFilter;
filters =
f ->
f.filter(authFilter)
.rewritePath(
"/api/v1/(?<segment>.*)",
"/${segment}"
)
}
}
#Component
#RequiredArgsConstructor
public class AuthFilter implements GatewayFilter {
#Override
public Mono<Void> filter(final ServerWebExchange exchange, final GatewayFilterChain chain) {
// user validation request
// just a rest template get and checks for a 200
// permission check
// just a rest template post and checks for a 200 and the response is not null
// requests pass both auth checks
return chain.filter(exchange);
}
I am unsure what other code I can provide as in the other micro-service it does not reach any of our code. Thanks for any help in advance

Setting up CSRF for spring websocket

I am building an application where authentication is done by spring security for HTTP handlers, for HTTP I've disabled csrf protection, and now I want to disable csrf for spring web socket, but I can't figure out how to accomplish this, I've already tried many different approaches but no one seems to be working. If it is impossible to disable csrf for WebSocket how to get a csrf token? (I tried setting up the csrf endpoint to obtain a token but it is not work, and all tutorials I've found are outdated)
Thanks in advance!
web socket security config:
#Configuration
#EnableWebSocketSecurity
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Bean
AuthorizationManager<Message<?>> messageAuthorizationManager(
MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages.anyMessage().permitAll();
return messages.build();
}
#Override
protected boolean sameOriginDisabled() {
return true;
}
}
security config:
#Configuration
#EnableWebSecurity(debug = true)
public class SecurityConfig {
#Autowired
private JwtFilter jwtFilter;
#Bean
SecurityFilterChain securityFilterChain(HttpSecurity HTTP) throws Exception {
return http.addFilterBefore(jwtFilter, BasicAuthenticationFilter.class)
.cors(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/authenticate").permitAll()
.requestMatchers("/createchatroom").authenticated()
.requestMatchers("/public/*").permitAll()
.requestMatchers("/private/*").permitAll()
.requestMatchers("/ws/**").authenticated()
.requestMatchers("/register").permitAll()
.requestMatchers("/csrf").authenticated()
.requestMatchers("/addEmployeeToFavorites").hasAnyAuthority(EMPLOYEE.name(),
ADMIN.name())
.requestMatchers("/addChatRoomToFavorites")
.hasAnyAuthority(EMPLOYEE.name(), ADMIN.name())
.requestMatchers("/home").hasAnyAuthority(EMPLOYEE.name(), ADMIN.name()))
.build();
}
}
By default, Spring Security requires the CSRF token in any CONNECT message type. This ensures that only a site that has access to the CSRF token can connect. Since only the same origin can access the CSRF token, external domains are not allowed to make a connection.
Spring Security 4.0 has introduced authorization support for WebSockets through the Spring Messaging abstraction.
In Spring Security 5.8, this support has been refreshed to use the AuthorizationManager API.
To configure authorization using Java Configuration, simply include the #EnableWebSocketSecurity annotation and publish an AuthorizationManager<Message<?>> bean or in XML use the use-authorization-manager attribute. One way to do this is by using the AuthorizationManagerMessageMatcherRegistry to specify endpoint patterns like so:
#Configuration
#EnableWebSocketSecurity
public class WebSocketSecurityConfig {
#Bean
AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
.simpDestMatchers("/user/**").authenticated()
return messages.build();
}
}
Any inbound CONNECT message requires a valid CSRF token to enforce the Same Origin Policy.
The SecurityContextHolder is populated with the user within the simpUser header attribute for any inbound request.
Our messages require the proper authorization. Specifically, any inbound message that starts with "/user/" will require ROLE_USER. Additional details on authorization can be found in [websocket-authorization]
At this point, CSRF is not configurable when using #EnableWebSocketSecurity, though this will likely be added in a future release.
To disable CSRF, instead of using #EnableWebSocketSecurity, you can use XML support or add the Spring Security components yourself, like so:
Java
#Configuration
public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
AuthorizationManager<Message<?>> myAuthorizationRules = AuthenticatedAuthorizationManager.authenticated();
AuthorizationChannelInterceptor authz = new AuthorizationChannelInterceptor(myAuthorizationRules);
AuthorizationEventPublisher publisher = new SpringAuthorizationEventPublisher(this.context);
authz.setAuthorizationEventPublisher(publisher);
registration.interceptors(new SecurityContextChannelInterceptor(), authz);
}
}
web.xml
<websocket-message-broker use-authorization-manager="true" same-origin-disabled="true">
<intercept-message pattern="/**" access="authenticated"/>
</websocket-message-broker>
On the other hand, if you are using the legacy-websocket-configuration and you want to allow other domains to access your site, you can disable Spring Security’s protection. For example, in Java Configuration you can use the following:
#Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
...
#Override
protected boolean sameOriginDisabled() {
return true;
}
}
References
WebSocket Security - Spring Security

Spring Boot + Keycloak: optional auth endpoint

I'm trying to configure a Spring Boot application with Keycloak to have an endpoint that is both accessible for authenticated and unauthenticated users. For authenticated users, I want to return some extra information. Here is a simple example of what I'm trying to achieve:
#RestController
public class HelloController {
#GetMapping("/")
public String index(Principal principal) {
KeycloakPrincipal keycloakPrincipal = (KeycloakPrincipal) principal;
if (keycloakPrincipal != null) {
return "Hello " + keycloakPrincipal.getKeycloakSecurityContext().getToken().getPreferredUsername();
} else {
return "Hello";
}
}
}
application.properties:
keycloak.securityConstraints[0].authRoles[0] = *
keycloak.securityConstraints[0].securityCollections[0].name = Hello
keycloak.securityConstraints[0].securityCollections[0].patterns[0] = /*
So far, I only got it to work for one of both cases. If I protect the endpoint using the security constraint above, the endpoint is only accessible to authenticated users. If I remove the security constraint, the endpoint is accessible for everyone, but then the principal will always be null.
Is it possible to achieve the intended behavior?
Have you tried something like Principal principal = SecurityContextHolder.getContext().getAuthentication();?
I believe the Principal as method parameter is only populated on secured endpoints but am unsure if it would exist in the SecurityContext. If not, you need to add a Filter to add it yourself.
I was able to solve the problem by calling the authenticate() method on the HttpServletRequest object. This will trigger the authentication process and will populate the user principal whenever possible. From the docs:
Triggers the same authentication process as would be triggered if the
request is for a resource that is protected by a security constraint.
To avoid triggering an authentication challenge, I pass in a dummy response object to the authenticate() call.

Spring Cloud Gateway as a gateway as well as web application

I have a spring cloud gateway application and what I want is like if there are two routes then on one route it should redirect to some external application but for other route it should forward the request to same app with a particular url.
-id: my_local_route
predicates:
- Path="/services/local"
uri: "/mylocal/services/local" //can we do something like that
Please note I want to create my rest services in same app as in spring cloud gateway. I understand it is not correct approach but for my knowledge I wanted to know whether it is possible or not.
If you have some rest APIs within your spring-cloud-gateway project, you don't need to explicitly put the routes for it.
So suppose you have following rest api in gateway project
#RestController
#RequestMapping("test")
class Controller{
#GetMapping("hello")
public String hello(){
return "hello";
}
}
and for external-url, you want to send some traffic to let's say https://httpbin.org. So in gateway application.yml could look something like this:
spring:
cloud:
gateway:
routes:
- id: httpbin-route
uri: https://httpbin.org
predicates:
- Path=/status/**
With this request like
http://localhost:8080/test/hello will be resolved by your rest controller
http://localhost:8080/status/200 will be redirected to httpbin site
If for some reason you have the same root path for both cases, the controller will have precedence.
If you have the same endpoint in gateway predicates and controller, by default controller will take precedence over predicates, if you want predicates to take precedence over controller, just create a BeanPostProcessor to adjust the order:
#Component
public class RequestMappingHandlerMappingBeanPostProcessor implements BeanPostProcessor {
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof RequestMappingHandlerMapping) {
((RequestMappingHandlerMapping) bean).setOrder(2); // After RoutePredicateHandlerMapping
}
return bean;
}
}

Authorization header not passed by ZuulProxy starting with Brixton.RC1

In switching from Spring Cloud Brixton.M5 to Brixton.RC1 my ZuulProxy no longer passes Authorization headers downstream to my proxied services.
There's various actors in play in my setup, but most all of them are fairly simple:
- AuthorizationServer: runs separately; hands out JWTs to clients
- Clients: get JWTs from OAuth server; each with access to a subset of resources.
- ResourceServers: consume JWTs for access decisions
- MyZuulProxy: proxies various resource servers; should relay JWTs.
It should be noted that MyZuulProxy has no security dependencies whatsoever; It passed the Authorization: Bearer {JWT} header it receives to the ResourceServers, pre-RC1. MyZuulProxy is explicitly not a Client itself, and does not use #EnableOAuth2SSO or similar at the moment.
What could I do to get MyZuulProxy to relay the JWTs to the ResourceServers again when using Spring Cloud Brixton.RC1?
There's very little code to post: It's just #EnableZuulProxy, #EnableAuthorizationServer and #EnableResourceServer in three different jars. My Clients are not Spring applications.
Update: Fixed in https://github.com/spring-cloud/spring-cloud-netflix/pull/963/files
Sensitive headers can also be set globally setting zuul.sensitiveHeaders. If sensitiveHeaders is set on a route, this will override the global sensitiveHeaders setting.
So use:
# Pass Authorization header downstream
zuul:
sensitiveHeaders: Cookie,Set-Cookie
So pending a fix for https://github.com/spring-cloud/spring-cloud-netflix/issues/944, jebeaudet was kind enough to provide a workaround:
#Component
public class RelayTokenFilter extends ZuulFilter {
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
// Alter ignored headers as per: https://gitter.im/spring-cloud/spring-cloud?at=56fea31f11ea211749c3ed22
Set<String> headers = (Set<String>) ctx.get("ignoredHeaders");
// We need our JWT tokens relayed to resource servers
headers.remove("authorization");
return null;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public String filterType() {
return "pre";
}
#Override
public int filterOrder() {
return 10000;
}
}
Set the sensitiveHeaders globally helped me solve the issue
zuul:
sensitiveHeaders: Cookie,Set-Cookie
Please note that the property name is sensitiveHeaders not sensitive-headers
[I use spring-cloud-starter-zuul version:1.3.1.RELEASE ]

Categories

Resources