はじめに
Jamstack構成でアプリ開発をする際に、認証をどう実装するかいつも悩むので現時点での俺的解決策を残しておきます。
railsを使った実装例ですが、それ以外でも使える内容です。
また、解決案としてdockerの利用が必須となっています。
( docker利用しなくても同じ原理で実現は可能です。)
セキュリティ的にやばいことがあっても責任は取れませんので悪しからず。
もしやばい部分あったら、そっと教えていただけると嬉しいです。
やりたいこと
- ローカル環境と本番環境であまり差異が生じない
( ローカル開発のためにhostsとかをいじって環境を汚したくない ) - 異なるタブでアプリを開いても認証が持続するようにcookieを使いたい。
- cookieにトークンを保存するのはバックエンドで行いたい。
( フロントエンドで保存するとXSSとか怖い )
困っていること
Jamstack構成にしている場合、ローカル環境ではバックエンドとフロントエンドは基本的にはcross site
になっているかと思います。
そのためsame_site
属性はnone
で発行する必要があります。
ただ、same_site
属性をnone
にした場合、secure
属性はtrue
にする必要があります。
もしsecure
属性をfalse
にした場合、chromeだとこんな感じでcookieをブロックしといたよというアラートが表示され、cookieを受け取ることができません。
少し前の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
変数はプロキシ先コンテナのポートを指定してください。
プロキシ先でports
が123: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
属性をつけることができるようになります!