Let's improve our authentication and store JWT token in a more secure place than Local Storage.
First of all, I strongly recommend reading this article - Please Stop Using Local Storage.
Now we are going to store the JWT token in
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter::class.java)
And now we can remove local CORS annotations in the controller classes (
IMPORTANT:
You will see all non-httpOnly cookies related to this website. They are accessible from JavaScript as you see.
And now let's authorize (this action guarantees that cookie was received) in our application and do the same:
First of all, I strongly recommend reading this article - Please Stop Using Local Storage.
Now we are going to store the JWT token in
httpOnly
cookies. That means that JWT token will not be accessible from JavaScript anymore. Since we intend to completely abandon the use of Local Storage...#Step 1
Remove all the logic related to using Local Storage in the
frontend
subproject. Please, be careful with store/index.js
- if your login and logout operations don't run clearly, you will get annoying bugs. We have to use a new way for identifying if user is authenticated - for example, if role stored in Local Storage is defined (that's not sensitive data, so we can still store it in Local Storage) or any another way.
#Step 2
Return JWT as cookie from your backend, not in the body of response:
IMPORTANT: Please, pay attention, that I placed parameters
And use the new response entity which doesn't include a special field for JWT.
IMPORTANT: Please, pay attention, that I placed parameters
authCookieName
and isCookieSecure
to application.properties - sending secure
cookie is impossible via http (not https), so it can't be debugged on localhost
. BUT it's highly recommended in production.
And use the new response entity which doesn't include a special field for JWT.
#Step 3
Update JwtAuthTokenFilter: previously we extracted token from header, but now we take it from cookie:
@Value("\${ksvg.app.authCookieName}")
lateinit var authCookieName: String
...
private fun getJwt(request: HttpServletRequest): String? {
for (cookie in request.cookies) {
if (cookie.name == authCookieName) {
return cookie.value
}
}
return null
}
return null
}
#Step 4
Actually, this step is optional. However, it's strange that we protect token storing but still didn't enable CORS policy. Let's fix it by implementing global CORS policy. Update
WebSecurityConfig.kt
:
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val configuration = CorsConfiguration()
configuration.allowedOrigins = Arrays.asList("http://localhost:8080", "http://localhost:8081", "https://kotlin-spring-vue-gradle-demo.herokuapp.com")
configuration.allowedHeaders = Arrays.asList("*")
configuration.allowedMethods = Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")
configuration.allowCredentials = true
configuration.maxAge = 3600
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", configuration)
return source
}
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.cors().and()
configuration.allowedMethods = Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")
configuration.allowCredentials = true
configuration.maxAge = 3600
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", configuration)
return source
}
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.cors().and()
.csrf().disable().authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter::class.java)
http.headers().cacheControl().disable()
}
}
And now we can remove local CORS annotations in the controller classes (
@CrossOrigin(origins =...
).
IMPORTANT:
AllowCredentials
parameter is required for sending cookie value in requests from frontend. Read more about that here.
#Step 5
Actualize frontend headers in
http-commons.js
:
export const AXIOS = axios.create({
baseURL: `/api`,
headers: {
'Access-Control-Allow-Origin': ['http://localhost:8080', 'http://localhost:8081', 'https://kotlin-spring-vue-gradle-demo.herokuapp.com'],
'Access-Control-Allow-Methods': 'GET,POST,DELETE,PUT,OPTIONS',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Credentials': true
}
})
})
Check
Now let's try to access our application from non-allowed host. Run backend on port
Sign in request is blocked by CORS policy.
And now let's look at how
8080
, frontend - on port 8082
and try to sign in:
Sign in request is blocked by CORS policy.
And now let's look at how
httpOnly
cookies actually work. Go to https://kotlinlang.org/, for example, open development console and execute short JavaScript code:document.cookie
You will see all non-httpOnly cookies related to this website. They are accessible from JavaScript as you see.
And now let's authorize (this action guarantees that cookie was received) in our application and do the same:
Ways to Improve
- Use Double Submit Cookies:
httpOnly
cookies are still not protected against advanced XSS attack. Moreover, that is more applicable solution for application scaling. - Make JWT cookie
SameSite=strict
using any other library or implement it manually. However not all browsers support this feature nowadays.
Comments
Post a Comment