localhostでhttpsを使いcookieを発行する方法

バックエンド

はじめに

Jamstack構成でアプリ開発をする際に、認証をどう実装するかいつも悩むので現時点での俺的解決策を残しておきます。

railsを使った実装例ですが、それ以外でも使える内容です。
また、解決案としてdockerの利用が必須となっています。
( docker利用しなくても同じ原理で実現は可能です。)

セキュリティ的にやばいことがあっても責任は取れませんので悪しからず。
もしやばい部分あったら、そっと教えていただけると嬉しいです。

やりたいこと

  • ローカル環境と本番環境であまり差異が生じない
    ( ローカル開発のためにhostsとかをいじって環境を汚したくない )
  • 異なるタブでアプリを開いても認証が持続するようにcookieを使いたい。
  • cookieにトークンを保存するのはバックエンドで行いたい。
    ( フロントエンドで保存するとXSSとか怖い )

困っていること

Jamstack構成にしている場合、ローカル環境ではバックエンドとフロントエンドは基本的にはcross siteになっているかと思います。
そのためsame_site属性はnoneで発行する必要があります。

ただ、same_site属性をnoneにした場合、secure属性はtrueにする必要があります。
もしsecure属性をfalseにした場合、chromeだとこんな感じでcookieをブロックしといたよというアラートが表示され、cookieを受け取ることができません

secure false

少し前のchromeのバージョンだったら、secure属性はfalseでもブロックしないみたいなことができたらしいですが、ざっと調べた感じ2022年ではどう頑張っても無理そうでした。

なので、secure属性をtrueにする必要があるのですが、SSL通信に対応しないといけないので、ローカル開発では結構面倒です。

環境

dockerを用いて開発を行います。APIサーバーはrailsで実装しています。
また、APIサーバー、フロントエンド、webサーバーはdocker-composeを用いて連携させています。
rails とNginxはソケット通信で連携しています。

コンテナ名 コンテナポート
api API ( rails )
next フロント ( Next.js )3000
nginx web ( Nginx )80

手順

バックエンドでcookieを発行するのに必要な箇所のみ記載します。
Nginxの設定は今回関係してこないのでお好みで。

要点としてはsteveltn/https-portalというdockerコンテナを使ってAPIをSSL対応させます。

rails

以下のgemをインストールします。

gem 'rails_same_site_cookie'

config/application.rbに以下を追記します。

config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore, domain: Rails.env.production? ? ENV["HOST"] : "localhost", tld_length: 2, same_site: :none, key: 'key_name', secure: true
config.action_dispatch.cookies_same_site_protection = :none

あとは以下のようにして値を保存してあげればOKです。

# 格納
session[key] = "value"

# 取り出し
value = session[key]

## e.g.)
if login_success?
  session['access-token'] = access_token
end

steveltn/https-portal

steveltn/https-portalイメージを使ってAPIのSSL対応をします。
まずは、docker-composeにsteveltn/https-portalのコンテナを追加します。

version: '3'

services:
  https-portal:
    image: steveltn/https-portal:1
    ports:
      - '4443:443'
    environment:
      DOMAINS: 'localhost -> http://nginx'
      STAGE: local
    depends_on:
      - nginx

ポイントは9行目と12行目です。
https-portalコンテナからのプロキシ先を記述します。
今回はnginxのwebサーバーを使っているのでnginxコンテナに飛ばします。
もし、webサーバーなしで開発している場合はAPIのコンテナに飛ばしてください。

また、https-portalコンテナのDOMAINS変数はプロキシ先コンテナのポートを指定してください。
プロキシ先でports123:456と設定されてたら456に向けてください。

フロントエンド

APIの問い合わせ先をhttps://localhost:4443に変更します。
また、受けとったcookieをAPIサーバーに返却するための設定を適宜行いましょう。
axiosだったらwithCredentials、apolloだったらcreadentials: "include"とかだったかな?

これでローカル開発でもcookieを用いた認証を行うことが可能になりました!
本番環境との条件分岐なしなのが個人的に気に入ってます。

Tips

APIサーバーで認証に使うライブラリでは、アクセストークンはヘッダーに格納することを求めるものが多い気がします。
今回はcookieにアクセストークンを格納しています。
以下の実装をすることでヘッダーとcookie間でのトークンのやり取りを自動化できます。

module SetToken
  extend ActiveSupport::Concern

  included do
    prepend_before_action :cp_token_session_to_header
    prepend_after_action :cp_token_header_to_session
  end

  private

  def cp_token_session_to_header
    token_keys.each do |key|
      request.headers[key] = session[key]
    end
  end

  def cp_token_header_to_session
    token_keys.each do |key|
      session[key] = response.headers[key]
    end
  end

  # devise_token_authの場合これらが認証に必要
  def token_keys
    [
      DeviseTokenAuth.headers_names[:'uid'],
      DeviseTokenAuth.headers_names[:'access-token'],
      DeviseTokenAuth.headers_names[:'client'],
      DeviseTokenAuth.headers_names[:'tokey-type']
    ]
  end
end
class ApplicationController < ActionController::API
  include SetToken
end

これでフロントエンドでcookieからトークンを取り出し、リクエスト時にヘッダーに格納するということが不要になります。
つまりhttpOnly属性をつけることができるようになります!

タイトルとURLをコピーしました