REST APIとSPAの環境構築をした際、CSRFでハマった。
CSRF用のCookieのやりとりは上手くいってたが、X-XSRF-TOKENヘッダーが送付されておらずエラーになっていた。
Spring Securityやnuxt/axiosが色々やってくれるので、背景知識が足りず深くハマってしまった。
状況
SPA
- Nuxt.js 2.14.5
- APIとの通信はnuxt/axiosを使用
- S3 + CloudFrontで環境構築
- ドメインは https://foo.com
REST API
- Kotlin 1.4.0
- Spring Boot 2.3.2
- Spring Security
- ECSで環境構築
- ドメインは https://bar.com
Spring SecurityのCSRF設定
@EnableWebSecurity
class SecurityConfig(): WebSecurityConfigurerAdapter() {
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
...
.and()
.csrf()
.ignoringAntMatchers("/home")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringAntMatchers() で /homeへのCSRFを無効にしています。
.ignoringAntMatchers("/home")
CookieCsrfTokenRepositoryを使用する際は.withHttpOnlyFalse()の記述をすると、JavaScriptからCookieを扱う事ができます。
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
何が起きたか
local環境ではうまくいっていたが、ECS + S3で環境を構築をしデプロイするとCSRFではじかれた。
最初のアクセス /home(csrf無効)後に、次のアクセス /login(csrf有効)するとエラー。
Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'.
CSRF Tokenがnullと言われる。
Google Chrome Developer ToolsでNetworkを確認する。
最初の/home(csrf無効)のResponseのHeadersにset-cookie: XSRF-TOKEN=xxx; が返ってきて、
次の/login(csrf有効)のRequestのCookiesに、XSRF-TOKEN xxxx が入っている。
ただそのHeadersに、X-XSRF-TOKENの記載がない。
nuxt/axiosの仕様では
axios: {
credentials: true
}
と記載しておけば、Cookieを自動で付けてくれる。
CookieにXSRF-TOKENがいる場合は、X-XSRF-TOKENというヘッダーを生成してくれる。
なぜX-XSRF-TOKENのHeaderがないのか
JavaScriptは自身と異なるドメインのcookieを操作する事ができない。という事がわかった。
いわゆる3rd party cookieというやつの操作ができない。
今回の場合だと
フロント: https://foo.com
サーバ: https://bar.com
なので、foo.comのjsはbar.comで発行されたCookieを操作する事ができない事になる。
よって、axiosはCookieのXSRF-TOKENを、X-XSRF-TOKENというヘッダーに付け替える事ができず、nullになってしまった。
解決策
CSRF Token取得用のControllerを作成。
@RestController
class CsrfController {
@GetMapping("/csrf")
fun issueCsrfToken(csrfToken: CsrfToken): CsrfToken {
return csrfToken
}
}
返却するcsrfTokenの中身はこんな感じ
{
parameterName: "_csrf",
token: "xxx-xxxx-xxxx",
headerName: "X-XSRF-TOKEN"
}
JavaScriptで /csrf を叩いてcsrfTokenを取得しcookieに保存。
const csrf = await this.$axios.$get('https://bar.com/csrf')
document.cookie = `XSRF-TOKEN=${csrf.token}`
このようすればJavaScriptでcookieを扱う事ができるので、
次の通信の際に、axiosがX-XSRF-TOKENヘッダーを作成してくれる。