Help us understand the problem. What is going on with this article?

[SPA]脆弱性を潰して認証情報を扱う[アクセストークン]

最近、新規事業で完全0-1のプロダクト開発をした際に
Rails/Vue.jsのSPAでいかにセキュアに認証情報を扱うかについて調べた結果を書いていきます
「それおかしくない?」とか「脆弱性つぶせてなくない?」とかあればコメントで指摘していただけるととても助かります

どう実装するか

実装の意図や詳細については下の方で解説します

Rails側

  • 認証情報をsecure属性とhttponly属性をつけたcookieでフロントエンドに返す
    • domain属性はAPIのドメインのみにする(特に設定変えなければデフォルトでそうなってるはず)
  • 独自ヘッダの有無のチェックを認証処理に組みこむ
  • gemrack-corsを導入する
    • originsをSPAのURLのみに制限する
    • credentialstrueにしておく

Vue.js側

  • withCredentialsオプションをtrueに設定する(axiosやxhr)
    • fetchならcredentials: 'include'とかかな
  • リクエストに独自ヘッダをつける
    • なんでもいいし中身も空文字でよくて、ヘッダがあることが重要

なぜ上記のような実装をするのか

以下の3つがポイントになります

  • httponly属性とsecure属性が有効でdomain属性にはAPIのドメインのみを設定したcookieでアクセストークンを持たせる
  • 独自ヘッダが必要であること
  • corsでoriginsを制限されていること

cookieについて

なぜhttponlyにするのか

  • XSSや悪意あるパッケージなどのjsによりcookieにあるトークンを抜かれないようにするため
    • jsから抜かれる危険性はlocalstorageを使わない理由でもある
  • 代わりに、cookieにあるトークンを非同期通信でもリクエストするためにwithCredentialsオプションを有効にする

なぜsecureにするのか

  • 非SSLのリクエストを盗聴されないように、とかっていうよくある理由

なぜdomainをAPIのドメインのみに制限するのか

  • サブドメをワイルドカードにしたりとか他のドメインを許容する必要がないため
    • 少なくとも今回のケースでは複数ドメインをまたいでcookieの共有は必要なかった
    • withCredentialを有効にしてcookieとして飛ばすからフロントエンドのjsからcookieを操作できる必要はない
      • フロントのurlからはcookie見えないけどAPIのドメインへのcookieとしてちゃんと存在しているのでwithCredential有効にしておけばちゃんとリクエストされる
  • 恥ずかしながら自分は最初「フロントのドメインでも扱えなきゃいけないよなぁ」と思い込んでいました。。。

独自ヘッダについて

  • htmlのformによるCSRF対策
    • htmlのform(同期通信)では独自ヘッダを付与することはできないため

CORSについて

  • 独自ヘッダと併用することでjsによるCSRF対策となる
    • originsでちゃんと制限かけることで外からの不正なリクエストを弾く
      • ただし、それだけでは不十分で、ブラウザ上ではエラーになるがリクエスト自体は飛んで処理されてしまう
      • 不正なGETには有効だがPOSTは処理されてしまうので困る
    • POSTは条件次第(formDataを使うこと)で単純リクエストになり、プリフライトリクエストが走らない場合があるため独自ヘッダによりプリフライトを強制させる
      • これによりプリフライトリクエストで止める事ができるので不正なリクエストを飛ばせなくすることができる
  • 独自ヘッダは2度美味しい
  • ちなみに、rack-corsの設定でcredentials: trueにしないとフロントでwithCredential: trueのリクエストを弾いてしまう

実装のサンプル

一部抜粋して書いていきます
参考記事などは下の方にまとめてます

rails側

rack-corsなどの設定

config/application.rb
# ...割愛

    # apiモードのrailsでcookiesを使えるようにするため
    # コントローラ側に `include ActionController::Cookies` も必要
    config.middleware.use ActionDispatch::Cookies

    config.middleware.insert_before 0, Rack::Cors do
      allow do
        # 複数のorigins制限するためドメインの配列
        # 環境変数はjsonにすると配列が簡単に扱えるやんという最近見つけたtips
        origins JSON.parse(ENV.fetch('CORS_DOMAINS_JSON') { '[]' })
        resource "*",
          # axiosなどで `withCredential: true` にした上で、そのcookieを受け取るため
          credentials: true,

          headers: :any,
          methods: [:get, :post, :patch, :delete]
      end
    end

# ...割愛

認証情報をcookieで返す

  • アクセストークンの期限はDBにあるのでpermanent
  • 開発環境はsecure外したいので環境変数COOKIE_SECUREで操作
# ...割愛

cookies.permanent[:access_token] = {
  value: access_token,
  httponly: true,
  secure: ENV['COOKIE_SECURE'].present?,
}
cookies.permanent[:refresh_token] = {
  value: refresh_token,
  httponly: true,
  secure: ENV['COOKIE_SECURE'].present?,
}

# ...割愛

認証処理

access_token = cookies[:access_token]

# ...割愛(access_tokenチェックの処理など)

raise 適当な例外クラス if request.headers[:'X-REQUESTED-BY-MY-APP'].nil?

Vue.js側

withCredentialsを有効にする

src/main.js
// ...割愛

Vue.prototype.$http = axios.create(
  { baseURL: process.env.VUE_APP_API_URI, withCredentials: true },
);

// ...割愛

独自ヘッダをつける

  • Vue.prototype.$httpにインスタンス入れることでどこからでもthis.$httpで使える
  • interceptorsを使うことですべてのリクエストでヘッダ設定の処理を走らせる
src/main.js
// ...割愛

Vue.prototype.$http.interceptors.request.use((request) => ({
  ...request,
  headers: {
    ...request.headers,
    ...{ 'X-REQUESTED-BY-MY-APP': '' },
  },
}));


// ...割愛

疑問

XSS脆弱性があっても認証情報を抜かれないための対策とか、外からのCSRFの対策はしたけど
XSSでXHRのコードをインジェクトされて外部ではなく本来のバックエンドに勝手にリクエストを飛ばされないようにするための対策ってどうすればいいんだろう。。。
根本的にXSSを塞ぐしかないってことになるのかな

参考記事

rails-apiでcookieを使う
RailsでAPIにCORSを設定する
rack-corsでCORS設定をする
さよならCSRF(?) 2017
CORS: OPTIONSリクエスト(preflight request)を避ける

rh_taro
timee
日本の労働力の減少を若者の働き方改革で解決します。好きな時に好きなだけ働けるプラットフォームタイミーを作り、人々が好きなことをできる世界を実現します。
https://timee.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away