PR TIMESの認証認可を再設計するときに考えたこと

  • URLをコピーしました!

PR TIMESでCTOをやっている金子 (@catatsuy) です。

2023年に、PR TIMESのログインシステムへJWTを導入したことを開発者ブログで紹介しました。

あわせて読みたい
PR TIMESのログインシステムにJWTを導入しました こんにちは、開発本部でインターンをしている田中 湧大です。 今回は認証機能をPR TIMES上で実装し、企業・事業主ユーザーとメディアユーザーでログインするときにJWTを...

この記事では、JWTの仕様や移行の進め方など、実装した仕組みを中心に説明しています。一方で、なぜこの構成を選んだのかについては、あまり触れていませんでした。

本記事では、PR TIMESの認証認可を再設計するときに何を考えていたのか、その設計が現在の構成にどうつながっているのかを振り返ります。

目次

認証サーバーはあったが、認証状態は一つではなかった

私が入社した時点で、PR TIMESには認証サーバーが存在していました。一方、認証後はPR TIMES、STORY、Webクリッピングがそれぞれsessionを作成しており、ユーザーの認証状態がシステム全体で一意に定まらない構成になっていました。

そのため、あるサービスではログインしているが、別のサービスではログインしていないという状態が発生します。session間の状態差に起因する不具合も発生していました。また、フロントエンドは、処理を実行する前にユーザーがどのサービスのsessionを持っているかを判断する必要がありました。

認証サーバーが共通であることと、認証状態が共通であることは同じではありません。当時の構成では、認証後の状態を各サービスが個別に持っていました。

本記事では、この仕組みが作られた経緯には踏み込みません。ここでは、当時存在していた構造上の課題に対して、どのような要件を置き、認証認可を再設計したのかを説明します。

認証認可の再設計で考えたこと

認証認可を作り直すにあたり、重視したことは大きく三つあります。

一つ目は、通常のリクエストを自前のGatewayや認証サーバーへ集中させないことです。

すべてのリクエストを共通のGatewayへ通し、そこで認証する構成や、認証済みかどうかをリクエストのたびに認証サーバーへ問い合わせる構成も考えられます。しかし、そのGatewayや認証サーバーに障害が起きると、PR TIMES、STORY、Webクリッピングのすべてで認証が必要な機能を利用できなくなります。アクセスも集中するため、十分な処理能力を用意し、デプロイや監視、障害対応まで自分たちで担う必要があります。

多くの企業に利用されているPR TIMESにおいて、そのような単一障害点を新たに作ることは、耐障害性の観点から現実的ではありませんでした。そこで、認証サーバーがJWTを発行し、通常のリクエストでは各アプリケーションがJWTを検証する構成にしました。

二つ目は、既存アプリケーションの変更量を最小限にすることです。

認証基盤を作り直すために、PR TIMESの既存のログイン処理やsession、それを前提に動いている機能を大きく変更することは避ける必要がありました。必要だったのは、PR TIMESの既存機能を壊さずに、WebクリッピングやSTORYでも同じ認証状態を利用できる仕組みです。

そこで、まずPR TIMESの既存のログイン処理にJWTの発行を追加し、WebクリッピングとSTORYが独自のsessionではなくJWTを参照する構成へ段階的に移行しました。PR TIMES側の既存処理を維持しながら、サービス間で認証状態を共有できるようにしています。

三つ目は、既存のPHPアプリケーションを中心にしながら、処理特性やセキュリティ要件に応じて別の実行基盤や環境を組み合わせても、同じ認証状態を扱えることです。

認証基盤の再設計と並行して、Next.jsを使ったSSR基盤の開発も進んでいました。この構成では、ブラウザからのリクエストをFastlyでNext.jsへ振り分け、Next.jsはSSRに必要なデータをPHPに新たに用意する内部APIから取得します。Next.jsへの移行と配信構成については、「プレスリリース掲載ページのNext.js移行でやったこと」で詳しく紹介しています。

ブラウザからPHPへ直接リクエストする従来の構成とは異なり、Next.jsから内部APIを呼び出す際には、元のユーザーの認証状態をどう引き継ぐかが問題になります。

PR TIMESの中核機能はPHPで実装されており、既存のコードや運用上の知見は今後も活用すべき重要な資産です。Next.jsの導入もPHPを置き換えるためではなく、PHPを中心とした既存の構成に、異なる役割を持つアプリケーションを組み合わせる取り組みでした。

この認証基盤は、PHPで実装された機能を別の実行基盤へ移すこと自体を目的にしたものではありません。PHPが適している部分はPHPで実装し続けられます。そのうえで、異なる処理特性やセキュリティ要件を持つ機能が必要になったときに、認証が技術選定の制約にならない状態を作ることを目指しました。

この考え方は、後に「PR TIMESがPHPを使い続ける判断軸」で明文化しました。本記事で扱っている認証基盤は、その具体例でもあります。

将来どの技術を使うか、どこまで別のアプリケーションを利用するかは決めていませんでした。しかし、新しいアプリケーションを作るたびに認証基盤やsession連携を作り直す構成にはしたくありませんでした。

JWTで認証状態を持ち運べるようにする

これらの要件を満たすために、PR TIMESがJWTを発行し、各サービスがそのJWTを検証する認証基盤を作りました。

JWTは署名を検証できるため、通常のリクエストごとに認証サーバーへ問い合わせる必要がありません。PR TIMESが秘密鍵を持ち、JWTを利用するアプリケーションが公開鍵を持つことで、認証処理を一つのサーバーへ集中させずに、同じ認証状態を利用できます。

ここでJWTに持たせた情報は、ユーザーを一意に特定するsubと有効期限のexpだけです。JWTの中に各サービスの詳細な権限を持たせるのではなく、誰からのリクエストなのかを伝えるために使います。

JWTが正しいことだけを理由に処理を許可してはいけません。JWTを受け取る各アプリケーションが、対象のリソースと業務ルールに基づいて必ず認可を行うことを、この構成の前提にしています。

sessionを完全になくすことが目的ではありません。サービスごとに別の認証状態を作るのではなく、既存アプリケーションへの変更を抑えながら、同じユーザーの認証コンテキストを持ち運べるようにすることが目的でした。

移行時には、既存の仕組みでログインした際にJWTとRefresh Tokenを発行するところから始めました。Cookieに保存することで、フロントエンドの大きな変更を避けています。具体的な実装や段階的な移行については、以前の記事で紹介しています。

JWTは変換せず、そのまま内部APIまで引き回す

この認証基盤を利用するうえで重要なのは、途中でJWTを別のtokenへ変換しないことです。

例えばNext.jsでSSRを行う場合、Next.jsはブラウザから受け取ったJWTを、PHPに用意した内部APIへ送るリクエストのAuthorizationヘッダーにそのまま設定します。

Authorization: Bearer <ブラウザから受け取ったJWT>

内部APIはJWTを検証し、subからユーザーを特定したうえで、既存の業務ルールに基づいて認可します。PHPの内部APIが提供するリソースの認可はPHP側で行うため、Next.js側へその認可処理を複製する必要はありません。Next.js自身が提供するリソースについては、Next.js側で認可します。

Next.jsは画面とSSRを担当し、PHPの内部APIが提供する業務処理と認可はPHPに残せます。それぞれのアプリケーションが自身のリソースを認可することで、PHPの既存資産を活用しながら、必要な部分に別の実行基盤を組み合わせられる構成です。

認証サーバーで別のtokenへ交換する構成も採用していません。Token Exchangeを行えば、交換処理を担うサーバーが通信経路上の依存先になります。JWTをそのまま引き回し、リソースを所有するアプリケーションが検証と認可を行うことで、その依存を増やさないようにしています。

Fastlyはリクエストを振り分ける

PHPやNext.jsなど、異なる実行基盤を同じサービスで利用するためには、リクエストを適切なバックエンドへ振り分ける必要があります。Fastlyへ移行した背景やVCLによるパスベースルーティングの実装については、「PR TIMESのCDNをCloudFrontからFastlyに移行しました」で紹介しています。

PR TIMESでは、自前のGatewayを新たに構築するのではなく、FastlyのVCLでリクエストの転送先を決めています。FastlyはもともとPR TIMESへの通信経路に存在しており、その後ろに自社運用のGatewayを追加する必要がありません。可用性やスケーリングを含む基盤の運用はFastlyの責任範囲に寄せながら、私たちはVCLで経路を制御します。

一方、Fastlyで業務上の認可は行いません。Fastlyの責務はリクエストを振り分けることであり、JWTを受け取ったアプリケーションが検証と認可を行います。

この分担により、クライアントから見えるURLや認証状態を変えずに、処理特性やセキュリティ要件に応じて転送先を選べます。PHPを中心に据えながらNext.jsを組み合わせられる現在の構成は、この境界の上に成り立っています。今後、既存アプリケーションとは異なる環境が必要になった場合にも、同じ考え方を利用できます。

数年経った今、真価を発揮しはじめている

認証基盤を作った直後に解決したのは、サービスごとに認証状態が分かれていた問題です。WebクリッピングやSTORYは独自のsessionを発行せず、共通のJWTがあればログインしていると判断できるようになりました。既存アプリケーションへの変更を抑えながら、サービス間で同じ認証状態を扱えるようになりました。

Next.jsからPHPに用意した内部APIへJWTを引き回す構成も、認証基盤の導入後すぐに利用されました。Next.jsは画面とSSRを担当し、PHPは既存の業務処理と認可を担当するという分担を、サービスごとのsessionを新たに作らずに実現できました。

この設計が数年間使われる中で、認証状態がPHPのsessionに閉じていないことの価値は、より広い技術選定に表れ始めています。既存アプリケーションとは分離した環境を選ぶことや、SSEのように既存のPHP実行環境では提供に工夫が必要な機能を、認証基盤から作り直さずに検討できます。

この認証基盤を設計した当時、生成AIの会話UIが広く使われることも、そのストリーミング応答のためにSSEが復権することも予想していませんでした。SSE自体は以前からある仕組みで、PHPでも実装できます。一方で、多数の長時間接続を既存アプリケーションと同じ実行環境で処理するには工夫が必要です。重要なのはSSEを採用することではなく、必要になったときに、既存の認証状態を引き継いだまま処理に適した実行基盤を検討できることです。

異なる実行基盤や分離した環境を利用する場合にも、認証のために専用のsessionや自前Gatewayを追加する必要はありません。処理特性やセキュリティ要件に合った環境を、既存のPR TIMESと組み合わせられます。

Next.jsへの対応を含め、PHP以外の実行基盤でも同じ認証状態を扱えることは、認証基盤の設計時から想定していました。一方で、異なる実行基盤や分離した環境をどこまで使うことになるかは、当時は証明できませんでした。数年が経ち、Next.jsでの運用実績が積み上がり、さらに広い技術選定を現実的に検討できるようになったことで、設計の価値を実例とともに説明できるようになりました。

次の改善

現在の仕組みにも改善したい点はあります。

JWTには有効期限を設定し、Refresh Tokenを利用して更新しています。現在は各ページでJavaScriptを読み込み、クライアントから定期的に更新APIを呼び出す構成です。既存アプリケーションへの変更を最小限にするうえでは有効でしたが、tokenを更新する責務がクライアント側に残っています。

この点については、Device Bound Session Credentials(DBSC)を利用し、credentialをブラウザが必要に応じて更新する構成を検討しています。サービスごとに更新処理を実装する必要を減らしながら、現在の認証基盤を発展させられる可能性があると考えています。

まとめ

認証認可を再設計した直接のきっかけは、PR TIMES、STORY、Webクリッピングがそれぞれsessionを作り、ユーザーの認証状態が一つになっていなかったことです。

これを解決する際に、単に共通のsessionサーバーを作ることはしませんでした。認証サーバーや自前Gatewayにリクエストを集中させる構成は、耐障害性の観点から現実的ではありません。また、PR TIMESの既存機能を壊さずに、WebクリッピングやSTORYで同じ認証状態を利用できるようにする必要がありました。

そこで、PR TIMESがJWTを発行し、必要なアプリケーションがそれを検証する構成にしました。JWTは認可結果を表すものではなく、JWTを受け取る各アプリケーションが自身のリソースに対して必ず認可を行います。Next.jsからPHPに用意した内部APIを呼び出す場合は、受け取ったJWTをAuthorizationヘッダーにそのまま設定し、PHP側がそのリクエストを認可します。認証状態を持ち運べるようにしたことで、既存の処理を維持しながら異なる実行基盤を組み合わせられます。

具体的に何が普及するかは分からなくても、PR TIMESが進化を続けるために、PHPだけに技術選定を限定しない状態を作る必要があることは分かっていました。これはPHPから別の言語へ置き換えるという話ではありません。PHPが適している処理はPHPで実装し、例えばSSEによるストリーミングやセキュリティ要件など、異なるアーキテクチャが適している部分については、別の実行基盤や分離した環境も選択肢にできるようにするということです。

認証状態をPHPのsessionだけに閉じず、JWTとして持ち運べるようにしたことは、導入直後からNext.jsとの連携に利用され、現在の技術選定の柔軟性につながりました。さらに数年が経ち、アプリケーションを分離することや、SSEのように既存のPHP実行環境では提供に工夫が必要な機能も現実的な選択肢として検討できるようになり、この認証基盤はより大きな真価を発揮しはじめています。

  • URLをコピーしました!

この記事を書いた人

株式会社PR TIMESでCTOをやっています。がんばります。

目次