Adding CSRF token in response spring cloud gateway - spring-boot

I have configured CSRF in spring cloud gateway.
I followed the documentation to add the controllerAdvice for adding the CSRF token in the response header.
But it did not work. Below is the code.
#ControllerAdvice
public class SecurityControllerAdvice {
#ModelAttribute
Mono<CsrfToken> csrfToken(ServerWebExchange exchange) {
Mono<CsrfToken> csrfToken = exchange.getAttribute(CsrfToken.class.getName());
return csrfToken.doOnSuccess(token -> exchange.getAttributes()
.put(CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME, token));
}
}
so after some research online I found the below snippet and added it to my GatewayApplication.class.
public WebFilter addCsrfTokenFilter() {
return (exchange, next) -> Mono.just(exchange)
.flatMap(ex -> ex.<Mono<CsrfToken>>getAttribute(CsrfToken.class.getName()))
.doOnNext(ex -> {
})
.then(next.filter(exchange));
}
After this addition, it is adding the token in the response header.
But I am unable to understand this code. Can someone please help me understand this snippet.
Also will this code mutate the request and remove formData. Because i noticed that POST request to my rest apis does not have the formData, it is lost when the request is forwaded to my rest api from the gateway.

Related

How do I make Spring Security return a 500 instead of a 403 when a InternalAuthenticationServiceException is thrown

I am using Spring Security to handle auth on my RESTful-ish webservice.
The goal is to create a /login endpoint for which the user provides a username/password and which returns a JWT. I'm loosely following this guide: https://auth0.com/blog/implementing-jwt-authentication-on-spring-boot/
I've got the happy-path working; when a user provides a valid user/pass, a valid JWT is returned to them.
The problem is my error case. If my UserService.loadUserByUsername method (which is called by Spring Security in order to validate the user/pass) throws an IOException, I want Spring to return a 500 error. Instead, Spring returns a 403. I stepped through the internal Spring classes a bit, and they clearly differentiate between a AuthenticationException (which is thrown when auth fails) and a InternalAuthenticationServiceException (which is thrown when auth is unable to be completed due to an internal error). I want a 500 error returned when an InternalAuthenticationServiceException is encountered. How to I configure Spring to do this? Will I need to implement my own AuthenticationFailureHandler?
As Thomas Andolf said, it might have been best for me to simply implement an OAuth2 flow instead of a custom authentication solution. However, if you want to implement a custom authentication solution, and if you want to return specific HTTP error codes for specific errors, then you can write a custom AuthenticationFailureHandler to accomplish this. I'm not sure if this is a good solution, but it is a solution. Something like this:
class CustomAuthenticationFilter(authManager: AuthenticationManager) : AbstractAuthenticationProcessingFilter(AntPathRequestMatcher("/login", "POST")) {
init{
this.authenticationManager = authManager;
this.setAuthenticationSuccessHandler(CustomAuthenticationSuccessHandler())
this.setAuthenticationFailureHandler(CustomAuthenticationFailureHandler())
}
#Throws(AuthenticationException::class, PreAuthenticatedCredentialsNotFoundException::class)
override fun attemptAuthentication(req: HttpServletRequest, res: HttpServletResponse): Authentication {
// insert code to parse the request into a username and password
return authenticationManager.authenticate(
UsernamePasswordAuthenticationToken(
username,
password,
ArrayList())
)
}
}
class CustomAuthenticationSuccessHandler: AuthenticationSuccessHandler{
override fun onAuthenticationSuccess(request: HttpServletRequest?, response: HttpServletResponse?, authentication: Authentication?) {
// these next three lines simply verify that none of the inputs are null; this is Kotlin syntax.
request!!
response!!
authentication!!
val username = (authentication.principal as User).getUsername();
val expiration = Date(System.currentTimeMillis() + EXPIRATION_DURATION_MILLIS)
// insert code to create a JWT and write it to the response
// no need to return anything
}
}
class CustomAuthenticationFailureHandler : AuthenticationFailureHandler{
override fun onAuthenticationFailure(request: HttpServletRequest?, response: HttpServletResponse?, exception: AuthenticationException?) {
// these next two lines simply verify that none of the inputs are null; this is Kotlin syntax.
request!!
response!!
when (exception) {
is PreAuthenticatedCredentialsNotFoundException -> {
response.status = 400;
}
is AuthenticationServiceException -> {
response.status = 500;
}
else -> {
response.status = 401;
// consider adding a WWW-Authenticate header as well
}
}
}
}

Request body is empty when no authentication is present for secure APIs

I am trying to log the request body on all requests in a spring boot reactive application secured with spring security. But I am running into an issue where the request body is logged only if the basic auth header is present (even if the username and password are invalid). But if no auth header is present the request body does not get logged. I am unsure what I am missing and would like to find out how I maybe able to get access to the request body for cases where there is no authentication header present.
The request body logging is done using an authentication entry point set on HttpBasicSpec. The security configuration looks as follows:
#Configuration
#EnableWebFluxSecurity
class SecurityConfiguration {
private val logger = LoggerFactory.getLogger(this::class.java)
#Bean
fun securityConfigurationBean(http: ServerHttpSecurity) =
http.csrf().disable()
.cors().disable()
.httpBasic()
.authenticationEntryPoint { exchange, _ ->
exchange.request.body
.subscribe { logger.info(CharsetUtil.UTF_8.decode(it.asByteBuffer()).toString()) }
.let { Mono.error(HttpServerErrorException(HttpStatus.UNAUTHORIZED)) }
}.and().authorizeExchange().anyExchange().authenticated().and().build()
}
There is a test router config that has a one route:
#Configuration
class TestRouterConfig {
#Bean
fun testRoutes() =
router {
POST("/test") {
ServerResponse.ok().bodyValue("This is a test route")
}
}
}
When I make a request to http:localhost:8080/test with a request body of
{"sample": "sample"}
with an invalid username and password in the basic auth header, I see the following in the console:
2019-12-06 11:51:18.175 INFO 11406 --- [ctor-http-nio-2] uration$$EnhancerBySpringCGLIB$$5b5f0067 : {"sample": "sample"}
But when I remove authentication all together I don't see the above logging statement for the same endpoint (I am using a rest client to make these calls).
The versions of tools/frameworks/languages:
Kotlin: 1.3.50
Spring boot: 2.2.1
Java: 12
Gradle: 5.6.4
Spring dependency management: 1.0.8.RELEASE
I would like to be able to log the request body for all requests that result in an authentication failure including the absence of an authentication header and would appreciate any help in this regard. My apologies if this has been discussed/posted elsewhere.
Thank you!

CSRF Using Spring Security for Backend to Backend Calls

I was going to implement protective measures against CSRF attack (using Spring Security) on my already built application. However, I am facing the following issues while designing the approach:
Suppose I have two APIs with following endpoints:
/abc
/xyz
Scenario 1: Front End calls /abc along with csrf token. Server checks the csrf token and passes it if found correct. This is working fine.
Scenario 2: Front End calls /xyz along with csrf token. Server checks the csrf token and passes it if found correct. This again is working fine.
Scenario 3: The API /abc calls the API /xyz internally. However, API /xyz is expecting the CSRF token which only comes from front end and hence /xyz is failing due to no csrf token.
Scenario 4: We also have few third party apps (like payment gateway) that consumes our APIs. How will they pass CSRF token to our APIs?
Basically, I want to protect all our APIs from CSRF attack but I am finding it hard to pass the csrf token from BE to BE and from Payment Gateway to BE. Please help me in finalizing the approach that I should follow so that I can easily cover all these 4 scenarios and protect the application from any CSRF attack.
UPDATING QUESTION WITH CODE SAMPLES
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.requireCsrfProtectionMatcher(new RequestMatcher() {
#Override
public boolean matches(HttpServletRequest request) {
final HashSet<String> allowedMethods = new HashSet<String>(
Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
boolean methodCheck = allowedMethods.contains(request.getMethod());
if(methodCheck) {
return false;
}
return true;
}
});
}
}
APIs
API 1:
#RestController
public class GetVersion {
#RequestMapping(path="/", method=RequestMethod.GET)
public String getVersion() {
return "This is a Get Call";
}
}
API 2:
#RestController
public class PostCall2 {
#RequestMapping(value="/{path}/postcall2",method=RequestMethod.POST)
public String postCall2(#PathVariable("path") String path) {
return "This is path: "+path;
}
}
API 3:
#RestController
public class PostCall1 {
#RequestMapping(path="/{path}/postcall1",method=RequestMethod.POST)
#ResponseBody
public String postCall1(#PathVariable("path") String path) {
System.out.println("Tring to call /postcall2 from /postcall1");
final String url = "http://localhost:8080/thisisxyz/postcall2";
RestTemplate restTemplate = new RestTemplate();
try {
String result = restTemplate.postForObject(url, "", String.class);
System.out.println("Result is: "+result);
System.out.println("Successfully called /postcall2 from /postcall1");
return "This is path: "+path;
}
catch(HTTPException e) {
e.printStackTrace();
return "Failed";
}
catch(Exception e) {
e.printStackTrace();
return "Failed";
}
}
}
API 1 and API 2 are working fine as they are being called directly. However, API 3 is trying to internally call API 2 and it is failing because it cannot provide CSRF Token to API 2. Please help.

Using #PreAuthorize in a method that returns a SseEmitter returns HTTP 406

I'm using jwt for authentication, I'm suspecting that it has something to do with the request header which has the token.
Receiving a server sent event without adding #PreAuthorize works just fine:
#GetMapping("/sse")
public SseEmitter serverSentEvent() throws IOException
{
SseEmitter emitter = new SseEmitter(2592000000L);
emitter.send("OK");
return emitter;
}
How ever when i add #PreAuthorize to check use role, i get a 406 response (can’t establish a connection to the server at http://localhost:8080/sse)
#GetMapping("/sse")
#PreAuthorize("hasRole('ADMIN')")
public SseEmitter serverSentEvent() throws IOException
{
SseEmitter emitter = new SseEmitter(2592000000L);
emitter.send("OK");
return emitter;
}
Apparently, the JWT interceptor wasn't adding the token in authorization header for EventSource requests, so I used ng-event-source which allows for custom HTTP headers in EventSource.

Make Angular client login to Spring backend with JWT

I added JWT to my Spring backend following this guide: https://auth0.com/blog/securing-spring-boot-with-jwts/
When I send a PUT request using a software like Postman everything works fine, but as soon as I'm trying to login with my Angular client the data in the HttpServletRequest is empty.
I check the data in the attemptAuthentication method of the JWTLoginFilter in the following way:
#Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException, IOException, ServletException {
String reqBody = req.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
// this appears to be empty on angular client calls
System.out.println(reqBody);
ObjectMapper objectMapper = new ObjectMapper().configure(Feature.AUTO_CLOSE_SOURCE, true)
.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
AccountCredentials creds = objectMapper.readValue(reqBody, AccountCredentials.class);
return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(creds.getUsername(),
creds.getPassword(), Collections.emptyList()));
}
I'm sending the request out of the client like this:
const user = {
username: "asdf",
password: "asdf"
};
// imported from '#angular/http'
const headers = new Headers({
'Content-Type': 'application/json'
});
const body = JSON.stringify(user);
return this.http
.put("http://localhost:8080/api/login", body, {headers})
.toPromise()
.then(response => response.json().data as User)
.catch(this.handleError);
My suggestion would be that I'm sending the request body in a wrong way, but I can't see, what I'm doing wrong.
I tried:
sending the body as a regular JS object
sending it wrapped in another object
sending it as a JSON string as shown in the example
using POST instead of PUT (although it works with PUT in Postman)
changing the Content-Type Header to other values
None of this made any data appear in the backend.
If you need more information about anything, please ask me.
I got it.
I needed to allow CORS also on my HttpSecurity object as following:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.so()
.on();
}
}
I'm not really sure, why the requests I sent with Postman could get received without CORS enabled in my HttpSecurity, but nevertheless it's working now.
I hope I can help others with this in future.
This might be the reason, Spring OAuth 2 normally expect a POST request with URL encoded username and password. So try something like this.
return this.http.post(Url, "grant_type=password&username=" + username + "&password=" + password)
I'm not 100% sure if this the case as mine was a Spring-MVC, but I expect it to be very similar.

Resources