PR TIMESのログインシステムにJWTを導入しました

  • URLをコピーしました!

こんにちは、開発本部でインターンをしている田中 湧大です。

今回は認証機能をPR TIMES上で実装し、企業・事業主ユーザーとメディアユーザーでログインするときにJWTを発行するようにしたのでその紹介をします。

目次

JWT(JSON Web Token)とは

JSON Web Tokenは以下のようなドットで結合された文字列です。${HEADER}.${PAYLOAD}.${SIGNATURE}で構成されており、それぞれbase64エンコードされています。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRhbmFrYSIsInVybCI6InBydGltZXMuanAifQ.
NVG3MT6djcCjv1Q39Q-QfdZafDA45YYzDiVeknO4KjM

JWTはログインシステム、認証、認可などの様々な用途で用いられます。

Auth0 | JWT ハンドブック

なぜJWTを発行する必要があったか

理由についてはいくつかありますが、まず従来のシステムでは認証プロバイダーという別ドメイン・別システムが認証サーバーとして提供されており、PR TIMESとSTORY、Webクリッピングは認証プロバイダーから認可を受けた後それぞれでセッションを発行しています。その影響でログイン状態が共有されずバグなどが発生している状況でした。

またセッションが共有されていないため、例えばPR TIMESのフロントエンドからWebクリッピングのAPIを直接呼び出すことができませんでした(Webクリッピングのセッションが発行されている保証がない)。

そこでPR TIMES上で認証機能を実装した上で、ログイン時にJWTとRefresh Tokenを発行・Cookie上に保存し、それ以外のサービスがCookie上のJWTの信頼性を検証することでこれらの課題の解決を図りました。

Cookie上に保存した理由としては以下の2点です。

  • セキュリティ面
  • フロントエンドの実装を変更する必要がない

セキュリティ面についてですが、localStorageではXSSの脆弱性があったときJWTを盗むことができてしまいます。

CookieであればHttpOnly属性をつけることで直接奪えなくなります。Refresh Tokenはこれに加え、SameSite属性にStrictとPath属性をつけてできる限りの対策をしています。

従ってJWT導入後の構成は以下のようになります。PR TIMESはJWT発行のための秘密鍵を持っており、他のサービスは対応する公開鍵を所持しています。

JWTを導入するための準備として行ったこと

認証プロバイダーのパスワードカラムをPR TIMESのDBにマイグレートする

企業ユーザーのパスワードはPR TIMESのPostgreSQLではなく認証プロバイダー側のMySQLに保存されていました。これをPR TIMESのPostgreSQLにマイグレートする作業を行いました。

詳しくは以下の記事をご覧ください。

あわせて読みたい
認証プロバイダーのパスワードカラムマイグレーション: MySQLからPostgreSQLへの移行の記録 こんにちは、開発本部でインターンをしている田中です。 今回は企業ユーザーの認証を担っている認証プロバイダーのMySQLから、PR TIMESのPostgreSQLにパスワードカラム...

JWTの仕様を定める

JWTを発行するにあたって署名に用いるアルゴリズム、ペイロードの形式を定義しました。

アルゴリズムはEdDSAを採用しました。

詳しくは以下の記事をご覧ください。

あわせて読みたい
JWTに用いる署名アルゴリズムは何が適切かPHPで検証してみた こんにちは、PR TIMESで学生インターンをしている田中湧大です。 今回は、PHPでパフォーマンスの観点から署名アルゴリズムについて検証してみたのでその紹介をしたいと...

今回はJWTを所有しているユーザーが一意に定まれば良く、他の情報については不要なため以下のようなシンプルなペイロードとしました。

{
  "sub": "${ユーザー種別}::${ID}",
  "exp": 99999999
}

PR TIMESにJWTを発行する処理を実装する

ここではJWTを発行し、セッションとして参照するまでを紹介します。

既存システムでログインされたときにJWTを発行する

いきなりJWTでのセッション管理に切り替えると全クライアントが強制ログアウトしてしまうため、これは避けたいです。そのためまず既存システムでログインされたときにJWTを発行するように変更します。

具体的には認証プロバイダーでログイン後、prtimes.jp/auth/callbackにリダイレクトされたときにJWTと更新に利用するRefresh Tokenを発行します。

Refresh TokenからJWTを更新するAPIを実装する

JWTの有効期限は短い時間を設定することが推奨されています。

しかしセッションに用いるという側面から数分ごと再認証が発生してしまうとUXの観点からよくありません。かといって有効期限を長くすることはセキュリティ的に問題があります。

そこでJWTの有効期限はある程度短くしつつ、Refresh Tokenを用いて定期的にJWTを更新するAPIをクライアントから定期的に叩くようにしました。

このAPIではRefresh Token自体の更新も有効期限に応じて行っています。

上記の章でも書いた通り、Refresh TokenはCookieのPath属性を利用して必要なリクエストパス以外には極力送信しないようにしています。

またRefresh Tokenはセッションに保存しているのでCookie上のRefresh Tokenと比較することでJWTの更新可否を判断しています。

APIはリクエストを受けて、Cookie上のRefresh Tokenがセッションに保存されたRefresh Tokenと一致するかとセッション上のRefresh Tokenが有効期限内かどうかを見てJWTを再発行するか削除するかを行います。

クライアントからAPIを叩くJavaScriptの実装は非常にシンプルで主な処理は以下だけです。

const update_token = () => {
  fetch('/auth/token/refresh', {method: 'POST'}).then();
}

// requestIdleCallbackはSafari非対応のため
if (typeof requestIdleCallback === 'undefined') {
  setTimeout(update_token, 1000);
} else {
  requestIdleCallback(update_token, {
    timeout: 1000
  });
}

// time interval: 10 minute
setInterval(update_token, 600000);

この処理によって極力メインの処理や描画をブロックしたくなかったため requestIdleCallback を使ってブラウザがアイドル状態の時に実行されるようにしました。

ただしこの関数は実験的な機能のためSafariなど一部のブラウザに互換性がありません。

対応していないブラウザだった場合は setTimeout を使うようにしています。

requestIdleCallback – Web API | MDN

上記のJavaScriptを各ページで読み込みます。HTMLパースの中断を防ぐためにdefer属性を付与しています。

<script type="text/javascript" src="update_prtimes_jwt.js?v=xxx" defer></script>

今後、ページが追加された場合でも上記タグを一行埋め込むだけで良いので非常にシンプルです。

Webクリッピング、STORYがJWTを参照するように変更する

各サービスは独自にセッションを発行せず、JWTがあればログインしていると見なします。

この変更によりUX上大きく変わったことは各サービスのページに遷移したとき再認証の必要がなくなったことだと思います。

従来のシステムではトップページから企業ユーザーにログインし、Webクリッピングのページに飛んだときログインページにリダイレクトされ再認証する必要がありました。

現在ではその必要もなくなりスムーズにページ遷移できています。

ログインページのURLを変更する

auth.prtimes.jp/loginからprtimes.jp/auth/loginにログインページを変更します。

auth.prtimes.jp/loginはLaravelのBladeでレンダリングされており、それをPR TIMES側で使用しているテンプレートエンジンのSmartyに移行します。

単一ページで機能としてもシンプルなものしかないのでそこまで苦労せずに移行することができました。

リリースにはフィーチャートグルを使ってデプロイすることでリリース前の本番環境でも確認できるようにしました。

これによって実装者が気が付かなかった問題があった場合でも他の社内メンバーが発見したり、リリース前に気がつける可能性が高くなります。

またWebクリッピング、STORYの変更前にリリースし事前に検証できたこともGoodポイントでした。

フィーチャートグルについて

あわせて読みたい
本番環境で新機能・旧機能を自由に切り替えたい こんにちは、開発本部でバックエンドエンジニアをしています。江間です。 IPアドレスとCookieを使って、機能の切り替えが出来る仕組みを実装したので、それについてお話...

認証プロバイダーで発行されていた独自IDをPR TIMES側で発行するようにする

企業ユーザーには company_user_id の他に認証プロバイダーでユーザー作成時に発行され、管理されている auth_user_id という2つのIDが存在します。auth_user_id はWebクリッピングなどの他のサービスでも使われていることもあり、認証プロバイダーが廃止されても切り離せない要素でした。

そこでPR TIMES側で発行されるように切り替えます。具体的にはPostgreSQLで auth_user_id に対してシーケンスオブジェクトを作成しました。初期値には余裕を持った値を設定します。

SELECT setval('m_company_user_auth_user_id_seq', (SELECT max(auth_user_id) FROM m_company_user) + XXXX);
ALTER TABLE m_company_user ALTER auth_user_id SET DEFAULT nextval('m_company_user_auth_user_id_seq');
ALTER SEQUENCE m_company_user_auth_user_id_seq OWNED BY m_company_user.auth_user_id;

シーケンスオブジェクトを作成したあとは認証プロバイダーから返ってきた auth_user_id をインサートせず、PostgreSQLに発行を任せます。

これにより認証プロバイダーに依存しているデータは完全になくなり、廃止することができます。

まとめ

このタスクではJWTの設計から実装、移行まで幅広く携わらせて頂きました。

自分にとってこのタスクは非常に挑戦的なタスクでした。先輩エンジニアのサポートもありつつ今回のタスクをやり切ることができ、エンジニアとして成長できたのではないかと感じています。

これからも様々な課題に挑戦しつつ、PR TIMESを前に進めるような開発をしていきたいと思いました。

  • URLをコピーしました!

この記事を書いた人

PR TIMESの開発本部でバックエンドインターンをしています。

目次