Last week, we added OAuth with Google for client-side Authentication followed by Kotlin backend token verification. This worked very nicely, but depending on how many tests you have performed with this afterwards, you may have discovered some issues with it. We will cover those here.
In this article, we will first cover the pesky CORS errors that every FullStack developer knows all too well. Next, we will make the Security vary depending on how you’re currently testing the application. There was one issue in particular that took me quite a while to figure out!
CORS
CORS - What is it?
CORS stands for Cross-origin resource sharing. In essence, it means that the client and the server do not need to be on the same host.
Now, this is a protection of the client and the server. The server protects itself by saying who is allowed to access it. It is then the browser’s responsibility that requests are only sent from those allowed domains.
Another important protection CORS provides is to protect against Cross-site request forgery. It prevents a site from making some types of requests to another site. These requests would be made using any previously created tokens, such as session tokens.
CORS - How to handle it?
So if we want to allow CORS requests, we need to tell the browser that it’s okay to send requests from the host the user is
accessing. This can be done in multiple ways. One way is to add @CrossOrigin
on the RestController
entirely,
or on a single endpoint. Another is to create a filter that will do the same work. We just need to ensure that the
server adds the Access-Control-Allow-Origin
header in the response.
I like the approach of the Filter. This will allow me to forget the @CrossOrigin
on the other places. :)
Here is how such a Filter could look:
package com.mauquoi.moneymanagement.moneymanagement.security
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Component
class CorsFilter: OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
response.setHeader("Access-Control-Allow-Origin", "*")
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
response.setHeader("Access-Control-Max-Age", "3600")
response.setHeader("Access-Control-Allow-Headers", "authorization, content-type, xsrf-token")
response.setHeader("Access-Control-Expose-Headers", "xsrf-token")
if ("OPTIONS".equals(request.method, true)) {
response.status = 200
} else {
filterChain.doFilter(request, response)
}
}
}
Next, we need to add this filter in the FilterChain. For brevity, I have posted the entire class:
class AppWebSecurityConfigurerAdapter @Inject constructor(
private val oAuthGoogleSecurityFilter: OAuthGoogleSecurityFilter,
private val corsFilter: CorsFilter
) :
WebSecurityConfigurerAdapter() {
override fun configure(web: WebSecurity?) {
web?.ignoring()?.antMatchers("/actuator/*", "/users", "/users/**", "/error/**")
}
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilterBefore(oAuthGoogleSecurityFilter, UsernamePasswordAuthenticationFilter::class.java)
.addFilterBefore(corsFilter, SessionManagementFilter::class.java)
.httpBasic()
}
}
Once these changes are in, the browser knows that it can pass requests from the defined hosts. In the case above, the wildcard allows for requests from any site.
Postman issues
Now, even after all those changes, I kept getting 403 whenever I was testing POST requests from Postman. I just didn’t understand, as I was passing the Authentication token correctly. After disabling the web security altogether, it worked. This is just not a viable solution, however.
After quite some research, I found out that it was just the application protecting me from CRSF, or Cross-Site Request Forgery. In fact, Postman does not provide the token that would let the request go through. Spring allows GET requests, but the POST and PUT methods won’t get through.
Disabling csrf altogether is, again, not an acceptable solution though. This is not a feature we want in Production!
Not being able to test with Postman anymore is also not great. So, instead, we create a second extension of the
WebSecurityConfigurerAdapter
, which only runs when we activate it, based on a profile. You could also do it based on some
property using @ConditionalOnProperty
- this is really up to your preferences. I just went with the profile on this occasion.
Now, for this to work, we first need to decide on the profile. I use the postman
profile, as it very clearly defines what it
is used for. So, first, we add the new Configuration:
package com.mauquoi.moneymanagement.moneymanagement.security
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.builders.WebSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.session.SessionManagementFilter
import org.springframework.web.servlet.config.annotation.EnableWebMvc
import javax.inject.Inject
@Configuration
@EnableWebSecurity
@EnableWebMvc
@Profile("postman")
class AppWebSecurityConfigurerAdapterLocal @Inject constructor(
private val oAuthGoogleSecurityFilter: OAuthGoogleSecurityFilter,
private val corsFilter: CorsFilter
) :
WebSecurityConfigurerAdapter() {
override fun configure(web: WebSecurity?) {
web?.ignoring()?.antMatchers("/actuator/*", "/users", "/users/**", "/error/**")
}
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilterBefore(oAuthGoogleSecurityFilter, UsernamePasswordAuthenticationFilter::class.java)
.addFilterBefore(corsFilter, SessionManagementFilter::class.java)
.csrf().disable()
}
}
Note the last line, where we disable csrf. If you run the application again and call some POST endpoint, it will now go through as expected.
Finally, we also need to add the annotation to NOT create the regular WebSecurityConfigurerAdapter
on that profile. Fortunately,
the @Profile
annotation works also for negative cases, so simply add @Profile("!postman")
on the
AppWebSecurityConfigurerAdapter
. Now, if you run the application without the profile, those Postman requests will
fail with status 403 again. If you make the same request from some other client, be it a frontend or a different backend,
they will pass!
There you have it. I hope this article will save you some time in your development, and that you don’t need to look for the solution to these issues as long as I had to! :)