Can I use Spring Cloud Gateway for role based authorization? - java

I have several microservices in my architecture. I want to implement an API Gateway to route request to services. To achieve that, I implement spring-cloud-gateway and this is my application.yml
server:
port: 9090
spring:
application:
name: "API-GATEWAY"
cloud:
gateway:
routes:
- id: task-service
uri: 'http://localhost:8083'
predicates:
- Path=/task/**
So far everything works as expected. a request localhost:9090/task/123 is to localhost:8083/task/123. Here comes to second part.
I want some users access to only some endpoints. In my JWT token, I have role field.
{
"accountName": "erdem.ontas",
"surname": "Öntaş",
"roles": [
"ADMIN",
"USER"
],
}
I don't want specify authorization in every service separately, is there any way to specify role based access in spring-cloud-gateway? For example I want USER role to be able to access to GET http://localhost:9090/task/ but not to GET http://localhost:9090/dashboard/

If you do not want and need to create full OAuth 2 Server/Client infrastructure and want to keep it simple just create a custom GatewayFilter in which just check if the JWT token extracted from the header has the preconfigured roles.
So start with a simple GatewayFilter
#Component
public class RoleAuthGatewayFilterFactory extends
AbstractGatewayFilterFactory<RoleAuthGatewayFilterFactory.Config> {
public RoleAuthGatewayFilterFactory() {
super(Config.class);
}
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
var request = exchange.getRequest();
// JWTUtil can extract the token from the request, parse it and verify if the given role is available
if(!JWTUtil.hasRole(request, config.getRole())){
// seems we miss the auth token
var response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
return chain.filter(exchange);
};
}
#Data
public static class Config {
private String role;
}
#Override
public List<String> shortcutFieldOrder() {
// we need this to use shortcuts in the application.yml
return Arrays.asList("role");
}
}
Here we just create a simple filter which receives the required role from the config (application.yml) and checks if the request is authorized to continue.
To use the filter just add filters into you route config.
server:
port: 9090
spring:
application:
name: "API-GATEWAY"
cloud:
gateway:
routes:
- id: task-service
uri: 'http://localhost:8083'
filters:
- RoleAuth=ADMIN
predicates:
- Path=/task/**
So this way the RoleAuth filter can be reused over the several routes.

Related

No 'Access-Control-Allow-Origin' header is present (CORS) - Spring Boot (Spring security) Microservices + Vue.js

I'm working on Spring Boot project based on microservices architecture on backend and Vue.js on frontend.
Structure of my project is next:
For avoiding CORS error usually I add #CrossOrigin annotation on to class and it works.
It was all good and has been working well, until I added security part with ability to login users.
What did I did:
1. To API Gateway that built on spring-cloud-gateway I've added AuthFilter that uses as interceptor to create and check JWT:
api-gateway/src/main/java/.../AuthFilter.java
#Component
public class AuthFilter extends AbstractGatewayFilterFactory<AuthFilter.Config> {
private final WebClient.Builder webClientBuilder;
#Autowired
public AuthFilter(WebClient.Builder webClientBuilder) {
super(Config.class);
this.webClientBuilder = webClientBuilder;
}
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
if(!exchange.getRequest().getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
throw new RuntimeException("Missing auth information");
}
String authHeader = exchange.getRequest().getHeaders().get(org.springframework.http.HttpHeaders.AUTHORIZATION).get(0);
String[] parts = authHeader.split(" ");
if(parts.length != 2 || !"Bearer".equals(parts[0])) {
throw new RuntimeException("Incorrect auth structure");
}
return webClientBuilder.build()
.post()
.uri("http://manager-service/api/v1/auth/validateToken?token=" + parts[1])
.retrieve()
.bodyToMono(EmployeeDTO.class) //EmployeeDTO.class is custom DTO that represents User
.map(user -> {
exchange.getRequest()
.mutate()
.header("x-auth-user-id", user.getId());
return exchange;
}).flatMap(chain::filter);
};
}
public static class Config {
//live it empty because we dont need any particular configuration
}
}
2. I've added AuthFilter as filter to each service in application.properties:
api-gateway/src/resource/application.properties
##Workshop service routes
spring.cloud.gateway.routes[0].id=workshop-service
spring.cloud.gateway.routes[0].uri=lb://workshop-service
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/v1/workshop/**
spring.cloud.gateway.routes[0].filters[0]=AuthFilter
##Manage service routes
spring.cloud.gateway.routes[1].id=manager-service
spring.cloud.gateway.routes[1].uri=lb://manager-service
spring.cloud.gateway.routes[1].predicates[0]=Path=/api/v1/manage/**
spring.cloud.gateway.routes[1].filters[0]=AuthFilter
##Manage service for singIn. Here we dont need to add AuthFilter, cause sign in page should be available for all
spring.cloud.gateway.routes[2].id=manager-service-sign-in
spring.cloud.gateway.routes[2].uri=lb://manager-service
spring.cloud.gateway.routes[2].predicates[0]=Path=/api/v1/auth/signIn
...
3. Manager-service microservice used to control base entities for system, such as users, roles, organizations where users working are and so on, so here I added SecurityConfig and WebConfig, because this microservice will be responsible for JWT generating:
manager-service/src/main/java/.../SecurityConfig.java
#EnableWebSecurity
public class SecurityConfig {
#Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.authorizeRequests().anyRequest().permitAll();
return httpSecurity.build();
}
}
manager-service/src/main/java/.../WebConfig.java
#EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
private static final Long MAX_AGE=3600L;
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedHeaders(
HttpHeaders.AUTHORIZATION,
HttpHeaders.CONTENT_TYPE,
HttpHeaders.ACCEPT)
.allowedMethods(
HttpMethod.GET.name(),
HttpMethod.POST.name(),
HttpMethod.PUT.name(),
HttpMethod.DELETE.name())
.maxAge(MAX_AGE)
.allowedOrigins("http://localhost:8100")
.allowCredentials(false);
}
}
4. In controller, that represents auth I also added #CrossOrigin annotation to class:
manager-service/src/main/java/.../AuthController.java
#RestController
#RequestMapping("api/v1/auth")
#CrossOrigin(origins = "http://localhost:8100")
#Slf4j
public class AuthController {
private final AuthService authService;
#Autowired
public AuthController(AuthService authService) {
this.authService = authService;
}
#PostMapping("/signIn")
public ResponseEntity<EmployeeDTO> signIn(#RequestBody CredentialsDTO credentialsDTO) {
log.info("Trying to login {}", credentialsDTO.getLogin());
return ResponseEntity.ok(EmployeeMapper.convertToDTO(authService.signIn(credentialsDTO)));
}
#PostMapping("/validateToken")
public ResponseEntity<EmployeeDTO> validateToken(#RequestParam String token) {
log.info("Trying to validate token {}", token);
Employee validatedTokenUser = authService.validateToken(token);
return ResponseEntity.ok(EmployeeMapper.convertToDTO(validatedTokenUser));
}
}
5. For frontend I use Vue.js. For requests I use axios. Here are post-request to login:
axios.post('http://localhost:8080/api/v1/auth/signIn', this.credentials).then(response => {
console.log('response = ', response)
console.log('token from response', response.data.token)
this.$store.commit('saveToken', response.data.token)
}).catch(error => {
console.log('Error is below')
console.log(error)
})
All what I'm getting is an error: Access to XMLHttpRequest at 'http://localhost:8080/api/v1/auth/signIn' from origin 'http://localhost:8100' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.. Below you'll see headers, that displays Chrome with request:
I've been trying to add another one corsConfiguration, tried to mark with CrossOrigin annotation only method, not class at all but it hadn't take any effects. If I try to make such requests with postman it gives me expected response with generated token.
I'll be grateful for any idea what could I do wrong.
Thanks!
UPDATE: As I understood well - all problems is in api-gateway. If I make requests directly to service - I get right response, but if I make request through gateway - I'm facing an error, logs of api-gateway below:
2022-07-05 00:34:18.128 TRACE 8105 --- [or-http-epoll-5] o.s.c.g.h.p.PathRoutePredicateFactory : Pattern "[/api/v1/workshop/**]" does not match against value "/api/v1/auth/signIn"
2022-07-05 00:34:18.129 TRACE 8105 --- [or-http-epoll-5] o.s.c.g.h.p.PathRoutePredicateFactory : Pattern "[/api/v1/manage/**]" does not match against value "/api/v1/auth/signIn"
2022-07-05 00:34:18.129 TRACE 8105 --- [or-http-epoll-5] o.s.c.g.h.p.PathRoutePredicateFactory : Pattern "/api/v1/auth/signIn" matches against value "/api/v1/auth/signIn"
2022-07-05 00:34:18.129 DEBUG 8105 --- [or-http-epoll-5] o.s.c.g.h.RoutePredicateHandlerMapping : Route matched: manager-service-sign-in
2022-07-05 00:34:18.129 DEBUG 8105 --- [or-http-epoll-5] o.s.c.g.h.RoutePredicateHandlerMapping : Mapping [Exchange: OPTIONS http://localhost:8080/api/v1/auth/signIn] to Route{id='manager-service-sign-in', uri=lb://manager-service, order=0, predicate=Paths: [/api/v1/auth/signIn], match trailing slash: true, gatewayFilters=[], metadata={}}
2022-07-05 00:34:18.129 DEBUG 8105 --- [or-http-epoll-5] o.s.c.g.h.RoutePredicateHandlerMapping : [e5b87280-8] Mapped to org.springframework.cloud.gateway.handler.FilteringWebHandler#78df1cfc
After research I've solved problem. It was all Gateway's fault
As I mentioned before, direct request gives me right response, but only if I go through api-gateway it gives me an errors.
So solution is to add CORS Configuration rules to gateway:
spring:
cloud:
gateway:
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "http://localhost:8100"
allowedHeaders: "*"
allowedMethods: "*"
Please, note that if you don't add section with gateway: default-filters you will be facing similar error with header that contains multiple values.
Thanks to answer by Pablo Aragonés in another question and Spring Cloud Documentation
add this to applications.yml in your gateway, solved issue for me:
spring:
cloud:
gateway:
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-
Allow-Credentials, RETAIN_UNIQUE
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "http://localhost:<your port>"
allowedHeaders: "*"
allowedMethods: "*"
I have faced similar issue & got resolved by defining following bean in my Configuration.
#Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("http://localhost:8100")); // Provide list of origins if you want multiple origins
config.setAllowedHeaders(Arrays.asList("Origin", "Content-Type", "Accept"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "OPTIONS", "DELETE", "PATCH"));
config.setAllowCredentials(true);
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
You can comment out all other details e.g. #CrossOrigin(origins = "http://localhost:8100"), WebConfig.java, SecurityConfig.java as once we define above bean these things are not required.
Your code may not be running because you have defined bean , security config as well as webconfig which might be conflicting while processing your request.

How do you use spring-security OAuth2 behind a proxy, but only use ForwardedHeaderTransformer for the OAuth components

I am trying to merge Spring Cloud Gateway with Discovery Client with Spring Security with OAuth. I got most of it working except that I cannot do both OAuth and Discovery Client.
When I use Discovery Client it correctly resolves to the service say /v1/whoami goes to the whoami service requesting /, when I enable security, I would get a 404 when it tries to request /oauth/authorization/google as it should be /v1/oauth/authorization/google
To fix the above I add this
#Bean
public ForwardedHeaderTransformer forwardedHeaderTransformer() {
return new ForwardedHeaderTransformer();
}
However, when I do that it will look up /v1/whoami as /v1/whoami which does not exist.
I tried creating and registering this class but it does not work either
public class ForwardedHeaderTransformerForOAuthOnly extends ForwardedHeaderTransformer {
#Override
public ServerHttpRequest apply(ServerHttpRequest request) {
System.out.println(">>>> " + request.getPath().value());
if (isOauth(request)) {
System.out.println(">>>> IS OAUTH");
return super.apply(request);
}
return request;
//return super.apply(request);
}
private boolean isOauth(ServerHttpRequest request) {
return request.getPath().value().startsWith("/oauth2/authorization/") || request.getPath().value().startsWith("/login/oauth2/code/");
}
}
I got it working adding the following to eat the prefix before the service ID.
spring:
cloud:
gateway:
discovery:
locator:
predicates:
- Path='/*/'+serviceId+'/**'
filters:
- StripPrefix=2
Combined with adding
#Bean
public ForwardedHeaderTransformer forwardedHeaderTransformer() {
return new ForwardedHeaderTransformer();
}

How to configure FEIGN on the client to authenticate against a REST service using SEEDSTACK framework?

I would like to call a REST web service from my client application using FEIGN and SEEDSTACK. The web service, which is developed with SEEDSATCK too, is configured with the following authentication method: "filters: [ authcBasic ]"
How to configure or program the client to get the authentication right? How to pass the USER and PASSWORD information?
client FEIGNAPI class:
#FeignApi
public interface neosdServer {
#RequestLine("GET /file/getfilesprop")
List<NeosdFile> getfilesprop();
#RequestLine("GET /file/getfiles")
List<String> getfiles();
}
client APPLICATION.YAML
feign:
endpoints:
neosdClient.interfaces.rest.neosdServer:
baseUrl: http://localhost:8080
encoder: feign.jackson.JacksonEncoder
decoder: feign.jackson.JacksonDecoder
server APPLICATION.YAML
web:
urls:
-
pattern: /file/getfiles
filters: [ authcBasic, 'roles[read]' ]
The current SeedStack integration doesn't support configuring interceptors on feign builders for now. Instead, to do authentication you can specify a header in your Feign interface with the #Headers annotation (example for basic authentication):
#FeignApi
#Headers({"Authorization: Basic {credentials}"})
public interface neosdServer {
#RequestLine("GET /file/getfilesprop")
List<NeosdFile> getfilesprop(#Param("credentials") String credentials);
#RequestLine("GET /file/getfiles")
List<String> getfiles(#Param("credentials") String credentials);
}
Note that #Headers can also be used on individual methods.
You will then have to pass the credentials as method parameter. An example implementation, with credentials coming from your application configuration, coudl be:
public class MyClass {
#Configuration("myApp.credentials.user")
private String username;
#Configuration("myApp.credentials.password")
private String password;
#Inject
private NeoSdClient client;
public void myMethod() {
List<String> files = client.getFiles(encodeCredentials());
}
private String encodeCredentials() {
return BaseEncoding
.base64()
.encode((username + ":" + password)
.getBytes(Charsets.UTF_8));
}
}
I created an issue on the Feign add-on repository to track the implementation of interceptor support: https://github.com/seedstack/feign-addon/issues/4.

Auth0 API + Spring: How to verify user identity from successful Auth0 API response

Problem
I'm trying to create an app that uses Auth0 SPA + React on the frontend to auth users without ever having to deal with passwords. Then, I'd like to secure any endpoints I create using an Auth server that I'm required to create using the Spring Framework.
Just to clarify, the flow would be
Frontend ->
Auth through Auth0 ->
Redirect to users dashboard on frontend ->
Make HTTP request to endpoint sending JWT returned from Auth0 ->
Endpoint makes request to my Auth Server sending JWT returned from Auth0 ->
Auth server either either returns 401 or user object based on JWT ->
Endpoint grabs data specific to that user from DB ->
Returns data to frontend
I've managed to get my frontend to work just fine using the Quickstart Guide that Auth0 provides but I'm having a lot of trouble figuring out how to get my Auth Service to verify the user.
I believe I've come to the conclusion that I need to create an "API" on Auth0 and grab an access token and use that to validate the JWT, which in this case is just the access token and not the JWT that my frontend contains. I've also got this part working but there doesn't seem to be a way to know who the user is. When testing this "API", after sending a valid request I am returned
{
"iss": "https://${username}.auth0.com/",
"sub": "${alphanumericCharacters}#clients",
"aud": "${ApiIdentifier}",
"iat": ${issuedAt},
"exp": ${expiresAt},
"azp": "${alphanumericCharacters}",
"gty": "client-credentials"
}
While it's good to know I'm on the right track I can't seem to figure out what to do with this response to find the user.
Expected
I expect to be able to identify a specific user after validating an access_token from my Auth Service
Code
I don't have much code to show but I'll provide what I can from my Auth Service
SecurityConfiguration.java
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Value("${auth0.audience}")
private String audience;
#Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
#Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.mvcMatchers("/api/validate")
.authenticated()
.and()
.oauth2ResourceServer()
.jwt();
}
#Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoderJwkSupport jwtDecoder = (NimbusJwtDecoderJwkSupport)
JwtDecoders.fromOidcIssuerLocation(issuer);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
}
AudienceValidator.java
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
private final String audience;
public AudienceValidator(String audience) {
this.audience = audience;
}
public OAuth2TokenValidatorResult validate(Jwt jwt) {
OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);
if (jwt.getAudience().contains(audience)) {
return OAuth2TokenValidatorResult.success();
}
return OAuth2TokenValidatorResult.failure(error);
}
}
ValidateController.java
#RestController
#RequestMapping("/api/validate")
public class ValidateController {
#GetMapping
public boolean validate() {
return true; // only returns if successfully authed
}
}
After reading through the docs I've found my solution.
It turns out that I don't need to create an "API" on Auth0 but instead need to use my Applications endspoint(s) from Auth0. Auth0 provides many endpoints based on your account that you can take advantage of from any of your applications (CLI, Server, Client, etc.) as long as you can:
Make an HTTP Request
Provide credentials
So the way to get a users information is explained here.
Data flow
Using my projects auth/data flow it's pretty much:
Using #auth0/auth0-spa-js on the frontend, you can grab a users access token after a successful auth by using the getTokenSilently() method.
Send up HTTP request to your Rest Service
Rest Service sends that token to your Auth Service
Auth Service sends GET request to https://myAuth0Username.auth0.com/userinfo with the Authorization: Bearer ${access_token} header. Example
If successfully authed from Auth0
Returns your users information such as "name", "email", etc.
Else
Returns a 403 Forbidden HTTP Status
Auth Service then returns user object to Rest Service
Rest Service then does necessary logic for that endpoint (DB query, another HTTP request, etc.)
Example Auth Service endpoint to validate tokens and return a user
ValidateController.java
package x.SpringTodo_Auth.Controllers;
import x.SpringTodo_Auth.Models.User;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
#RestController
#RequestMapping("/api/validate")
public class ValidateController {
#GetMapping
public Object validate() {
// Create and set the "Authorization" header before sending HTTP request
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + access_token);
HttpEntity<String> entity = new HttpEntity<>("headers", headers);
// Use the "RestTemplate" API provided by Spring to make the HTTP request
RestTemplate restTemplate = new RestTemplate();
Object user = restTemplate.exchange("https://myAuth0Username.auth0.com/userinfo", HttpMethod.POST, entity, User.class);
return user;
}
}
User.java (This is the class passed to the restTemplate.exchange(...) method as the last argument
package x.SpringTodo_Auth.Models;
public class User {
private String sub;
private String given_name;
private String family_name;
private String nickname;
private String name;
private String picture;
private String locale;
private String updated_at;
private String email;
private boolean email_verified;
// Getters/setters (or you can use Lombok)
}

Extra authentication step for OAuth2/OIDC client authorisations from multiple auth servers

I've currently set up a Spring Cloud Gateway Reverse Proxy with the intention of:
a) Handling Authentication of multiple OAuth/OIDC providers, including obtaining a Token
b) Look up the details from the provider locally, ensuring that the OAuth user x Oauth provider combination are authorised
c) If authorised look up the Grants/Permissions, and forward the request to SCG, with a JWT containing details of the authorised principal.
d) If not authorised display a page displaying pertinent details from the OAuth2 Auth, and explain that they are not authorised.
I have achieved most steps, but I am having trouble incorporating step c) into Spring Security Webflux
What I want to do is take the OAuth2AuthenticationToken obtained from the Authentication exchange, perform the lookup in step, and return a
bespoke Prinicipal based on results.
This would then be used via code to either trigger the SCG behaviour, or display the page.
spring-boot.version>2.1.6.RELEASE
spring-cloud.version>Greenwich.SR2
My problem is I don't know the best way of doing this.
Use some hook in the OAuth2 client to perform the extra auth steps. I may need to return an OAuth2Principal in this case
Add an extra security filter into the chain after the authentication.
This would replace the OAuth2Principal with my own prncipal. I'm not sure whether it is legal to replace a Principal after it has been authenticated, possibly removing the authentication status
Writing a custom AuthN Provider that would proxy to the OAuth client, and once competed run it's own logic before signalling that it is authenticated. This seems a complicated approach, and I'm not sure what classes I would use for this.
I've read the Spring Security documentation, and understand the general architecture of Spring Security, but cannot work out the best way of solving this.
This is my spring security filter logic
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
#Profile("oauth")
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return addAuthZ(http)
.oauth2Login()
.and().build();
}
private ServerHttpSecurity addAuthZ(ServerHttpSecurity http) {
return http.authorizeExchange()
.anyExchange().authenticated().and();
}
}
And here is the config, I am using sample OAuth2 providers Google and Facebook and a custom OAuth2 provider provided using CAS
spring:
security:
oauth2:
client:
registration:
google:
client-id: SET_ME
client-secret: SET_ME
facebook:
client-id: SET_ME
client-secret: SET_ME
sgd-authn:
provider: sgd-authn
client-id: SET_ME
client-secret: SET_ME
scope: openid
client-authentication-method: secret
authorization-grant-type: authorization_code
#redirect-uri: "{baseUrl}/oauth2/
redirect-uri-template: "{baseUrl}/{action}/oauth2/code/{registrationId}"
provider:
# These are needed for talking to CAS OIDC
sgd-authn:
authorization-uri: ${cas.url}/oidc/authorize
token-uri: ${cas.url}/oidc/accessToken
jwk-set-uri: ${cas.url}/oidc/jwks
user-info-uri: ${cas.url}/oidc/profile
user-name-attribute: sub
Well I managed to get something working using option 2, adding a filter after authentication.
Write a filter that takes in an Authentication (OAuth), runs it through some logic, and returns a new Authentication object that is instanceof a specific superclass
Arrange for this to return isAuthenticated = false if we could not look up the OAuth2 details.
Write a bespoke handler for NotAuthenticated
The filter is registered using something like this:
#Bean
public SecurityWebFilterChain springSecurityFilterChain(
ServerHttpSecurity http,
WebFilter proxyAuthFilter
) {
return http.addFilterAt(proxyAuthFilter, SecurityWebFiltersOrder.AUTHENTICATION); // Do configuration ...
}
and the filter is something like this:
public class ProxyAuthFilter implements WebFilter {
static private Logger LOGGER = LoggerFactory.getLogger(ProxyAuthFilter.class);
private final AuthZClientReactive authZClient;
public ProxyAuthFilter(AuthZClientReactive authZClient) {
this.authZClient = authZClient;
}
/**
* Process the Web request and (optionally) delegate to the next
* {#code WebFilter} through the given {#link WebFilterChain}.
*
* #param exchange the current server exchange
* #param chain provides a way to delegate to the next filter
* #return {#code Mono<Void>} to indicate when request processing is complete
*/
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return ReactiveSecurityContextHolder.getContext()
.flatMap(this::transform)
.then(chain.filter(exchange));
}
private Mono<Authentication> transform(SecurityContext securityContext) {
Authentication authentication = securityContext.getAuthentication();
if (authentication.isAuthenticated()) {
return authenticate(authentication)
.map(a -> {
securityContext.setAuthentication(a);
return a;
});
} else {
LOGGER.info("ProxyFilter - not authenticated {}", authentication);
return Mono.just(authentication);
}
}
// Runs the chain, then returns a Mono with the exchange object which completes
// when the auth header is added to the request.
private Mono<Authentication> authenticate(Authentication authentication) {
LOGGER.info("ProxyFilter - Checking authorisation for {}", authentication);
Mono<AuthTokenInfo.Builder> authInfo = null;
// Catch if this filter runs twice
if (authentication instanceof ProxyAuthentication) {
LOGGER.info("ProxyAuthentication already found");
return Mono.just(authentication);
} else if (authentication instanceof UsernamePasswordAuthenticationToken) {
// If lookup is successful, returns instance of ProxyAuthentication
return getUsernamePasswordAuth((UsernamePasswordAuthenticationToken) authentication);
} else if (authentication instanceof OAuth2AuthenticationToken) {
// If lookup is successful, returns instance of ProxyAuthentication
return getOAuthAuth((OAuth2AuthenticationToken) authentication);
} else {
LOGGER.info("Unknown principal {}", authentication);
// Signals a failed authentication, can be picked up by error page
// to display bespoke information
return Mono.just(new ProxyAuthenticationNotAuthenticated(
authentication, ProxyAuthenticationNotAuthenticated.Reason.UnknownAuthenticationType)
);
}
}

Categories

Resources