- Published on
SSOへのリプレイスを実装して学んだこと
- Authors

- Name
- Ippei Shimizu
- @ippei_111
はじめに
実務で、既存の認証システムをAWS Cognitoを使用したSSOにリプレイスするプロジェクトを担当したので、その過程で学んだことをまとめてみたいと思います。
SSO(Single Sign-On)とは
一般的な認証システムは、複数のアプリケーションが独自の認証を持っているため、複数のアプリケーションが存在している場合に、ユーザーは各システムごとにログインする必要があります。
SSOは「一度の認証で複数のシステムにアクセスできる」仕組みです。認証の責任を一箇所に集中させ、各アプリケーションはその認証結果を信頼するという設計思想です。
マイクロサービス
モノリシックアプリケーションでは、すべての機能が一つの大きなコードベースに含まれ、一つのプロセスとしてデプロイされます。Railsアプリケーションでいえば、ユーザー管理機能・カリキュラム管理・課題提出機能・採点機能が全て同じアプリケーション内に実装されているイメージです。
コードベースが大きくなると、変更の影響範囲が読みにくくなり、デプロイに時間がかかり、一部の機能の障害が全体に波及するリスクが高まります。また、チームが大きくなると、同じコードベースを複数のチームが触ることになり、調整コストが増大します。技術スタックも統一せざる得ず、適材適所の技術選択が難しくなります。
マイクロサービスは、これらの課題に対する一つの解決策であり、アプリケーション全体をビジネス機能ごとに独立した小さなサービスに分割します。それぞれのサービスは独自のデータベースを持ち、APIを通じて他のサービスと通信します。
SSOを導入することで、マイクロサービス化された各サービスが独自の認証を持つ必要がなくなり、ユーザーは一度のログインで複数のサービスにアクセスできるようになります。
AWS Cognitoについて
AWS Cognitoは、AWSが提供する認証・認可のサービスで、SSOのIdPとして機能します。
自前で認証を実装する場合に何が必要か
has_secure_passwordを使用した実装では、パスワードのハッシュ化・セッション管理・ログイン・ログアウトの処理などを自分で実装する必要がありました。
ユーザープールとIDプール
Cognitoは、自前で認証を実装する際に必要だった機能をマネージドサービスとして提供してくれます。具体的には、ユーザープールとIDプールの二つの主要なコンポーネントがあります。
ユーザープールは、ユーザーディレクトリとして機能を持ち、サインアップ・サインイン・アカウント管理を担当します。
IDプールは、認証されたユーザーに対してAWSリソースへのアクセス権限を付与する機能です。
IdPとは
IdP(Identity Provider)の略で、認証プロバイダーと呼ばれます。ユーザーの身元(アイデンティティ)を証明し、それを他のシステムに伝える役割を担う存在です。
例えば、飛行機に乗るとき、空港のカウンターで身分証明書を提示すると思いますが、この身分証明書を発行した政府機関がIdPに相当します。政府機関は、この人が確かに本人であることを証明する信頼できる第三者として機能します。空港は、政府機関が発行した証明書を信頼することで、あなたの身元を確認します。こうすることで、空港は身元確認のプロセスを持たずに、政府機関の判断を信頼できるのです。
アプリケーションでも同じ構造になっており、ユーザーがあるアプリケーションにアクセスしようとするとき、そのアプリケーション自身が「本当にこの人は誰なのか」を判断するのではなく、信頼できる第三者、つまりIdPに判断を委ねます。IdPは認証の専門家として、ユーザーの身元を確認し、その結果を証明書(トークンなど)の形で発行します。
IdPの責任
- ユーザーの認証情報(ユーザー名、パスワード、生体情報)を安全に保管する
- ユーザーがログインしようとしたときに、提供された認証情報を検証する。
- 認証が成功したら、そのことを証明するトークンを発行します。誰が・いつ・どのような権限で認証されたかという情報が含まれています。
IdPとアプリケーション間の信頼
IdPとアプリケーション間での信頼関係は、技術的には暗号技術によって保証されています。 IdPはトークンに電子署名を施し、アプリケーションはその署名を検証することで、トークンが本当にそのIdPから発行されたものであることを確認します。これにより、悪意のある第三者が偽のトークンを作成して不正アクセスをすることを防いでいます。
OAuth 2.0
OAuth 2.0が解決しようとした問題は、2000年代後半にWebサービスやスマホアプリが急速に増え始めた時に、問題が発生しました。例えば、写真印刷サービスを使用したい時に、そのサービスがあなたのFacebookの写真にアクセスする必要があったとします。当時の一般的な方法は、印刷サービスにFacebookのユーザー名とパスワードを入力することでした。これはかなり危険で、別サービスにメインのパスワードを渡すことになり、不正アクセスのリスクが高まります。
OAuth 2.0は、パスワードを渡さずに権限を委譲するという課題を解決するために設計されました。
OAuth 2.0は認証のプロトコルではなく、認可のプロトコルということです。認証は「あなたは誰ですか」という問いに答えることですが、認可は「この人に何をさせたいですか」という問いに答えることです。
OAuth 2.0は認可に特化しています。
具体的な動きの例をみていくと、ユーザーが写真印刷サービスを使おうとすると、サービスは「Facebookの写真にアクセスしたい」とユーザーに伝えます。ユーザーが同意すると、Facebookのログインページにリダイレクトされます。
ここでユーザーはFacebookに直接ログインし、「このサービスに写真へのアクセスを許可しますか」という確認画面が表示されます。ユーザーが許可すると、Facebook側は印刷サービスにアクセストークンを発行します。このトークンは「写真を読む権限」だけを持ち、パスワード変更やメッセージ送信などの権限は含みません。
この仕組みの優れた点は、パスワードが第三者に渡らないことと、権限が限定的であることです。アクセストークンには有効期限があり、ユーザーはいつでも権限を取り消すことができます。
また、トークンは特定の操作に限定されているため、サービスは勝手に他のことをすることはできません。
OpenID Connect(OIDC)
OAuth 2.0は認可のプロトコルであり、認証のプロトコルではなかったため、OAuth 2.0を使って認証を実装しようとしたが、本来の用途ではなかったため、いくつかの問題が発生しました。アクセストークンは「何ができるか」を示すものであり、「誰なのか」を示すものではなかったのです。
OIDCはOAuth 2.0の上に薄い認証レイヤーを追加したもので、OAuth 2.0のフローをそのまま使いながら、認証のための情報提供します。OIDCでは、アクセストークンに加えてIDトークンも発行されます。
IDトークンはJWT形式で発行され、ユーザーに関する情報を含んでいます。トークンの中身を見ると、ユーザーID(subject)、発行者(issuer)、発行時刻、有効期限などの標準的なクレームが含まれています。さらに、メールアドレス、名前、プロフィール画像URLなどの追加情報も含めることができます。
OIDCの動作の流れは、OAuth 2.0とほぼ同じですが、大きな違いは認可コードを交換する時に、アクセストークンと一緒にIDトークンも返ってくることです。アプリケーションは、このIDトークンを検証することで「このユーザーは確かに認証された」と判断できます。IDトークンには署名が含まれているため、アプリケーションはIdPの公開鍵を使って署名を検証し、トークンが改ざんされていないことを確認します。
- アクセストークン : リソースサーバー(APIなど)に対して権限を証明するためのものであり、中身を読む必要はない。リソースサーバーはこのトークンをIdPに確認して、有効性と権限を検証します。
- IDトークン : アプリケーションに対して、ユーザーの身元を証明するためのもので、中身を読んで検証する必要がある。
CognitoでOIDCを使い場合、ユーザーがログインに成功すると、CognitoはIDトークンとアクセストークン、リフレッシュトークンを返します。アプリケーションは、IDトークンを検証してユーザーを識別し、セッションを確立します。
アクセストークンは、Cognitoで保護されたAPIエンドポイントを呼び出す時に使用します。リフレッシュトークンは、アクセストークンの有効期限が切れた時に、新しいアクセストークンを取得するために使用します。
SAML
SAMLは、OAuth 2.0やOIDCよりもずっと前から使用されている技術で、Security Assertion Markup Languageの略です。XMLベースのプロトコルで、主に企業向けのシングルサインオン(SSO)で広く使われています。
Access TokenとRefresh Tokenの送信方式
- Access Token : HTTPヘッダーのAuthorizationフィールドにBearerスキームで送信します。
- Refresh Token : JSONリクエストボディ内のフィールドとして送信します。
Access TokenをAuthorizationヘッダーで送る理由
- HTTP標準の設計思想に従っているから
- GETやDELETEリクエストでも送信できるから
Authorizationヘッダーとは
まずは、HTTP通信全体の構造からみていきます。
WebブラウザやアプリケーションがサーバーにHTTPリクエストを送る時、そのリクエストは大きく3つの部分から構成されています。
郵便物に例えると、封筒の表面には宛先や差出人の情報が書かれています。これがHTTPリクエストラインとヘッダーです。封筒の中身がリクエストボディになります。
- リクエストライン : 何をしたいのかを端的に示します。GET /articles/123 HTTP/1.1のように、HTTPメソッド、リクエストURI、HTTPバージョンが含まれます。
- ヘッダー : リクエストに関する様々なメタデータを格納します。Hostヘッダーでどのサーバーに送るのか、Content-Typeヘッダーでどんな形式のデータを送っているのか、User-Agentヘッダーでどんなブラウザやアプリから送っているのかなどが含まれます。
- リクエストボディ : 実際に送りたいデータ本体です。POSTリクエストで新しいユーザーを作成するなら、そのユーザーの名前やメールアドレスといったデータがここに入ります。GETリクエストのように、ボディを持たないリクエストもあります。
Authorizationヘッダーは、ヘッダー群の中の一つで、「このリクエストを送る権限があるかどうか」を証明するための特別な役割をになっています。
Refresh Tokenをリクエストボディで送る理由
- 特定のAPIエンドポイントでのみ使用されるから
- データの送信という性質を持っているから
- セキュリティ上の理由から、ボディに含めることで情報漏洩を防ぐことができるから
JWT
Redisセッションを使用していた認証では、サーバー側がステートを保持する必要があり、Redisというデータストアを常に稼働させ、すべてのリクエストをRedisにアクセスする必要がありました。マイクロサービス化においては、スケールも難しくなります。
JWTは、「サーバー側で状態を保持する」という前提を覆す技術です。JWT自体に、認証の必要なすべての情報が含まれています。そのため、誰なのか・いつ発行されたのか・どんな権限があるのかがすべてトークンの中に記載されています。
この設計により、サーバーはセッションストアを持つ必要がなくなります。リクエストが来たら、JWTを受け取り、署名を検証し、有効期限をチェックするだけです。
JWTの構造
- ヘッダー : トークンに関するメタデータが含まれています。例えば、使用されている署名アルゴリズム(HS256やRS256など)やトークンのタイプ(通常はJWT)が記載されています。
- ペイロード : トークンの本体部分で、ユーザーに関する情報やクレームが含まれています。クレームには、標準クレーム(iss、sub、aud、expなど)とカスタムクレーム(role、permissionsなど)があります。
- シグネチャ : トークンの改ざんを防ぐための署名部分です。ヘッダーとペイロードを組み合わせて、秘密鍵や公開鍵を使って生成されます。
JWTのメリット
- スケーラビリティ : 各サーバーが独立していてもトークンを検証できる。
- マイクロサービスの容易さ : 各サービスが独自に認証を処理できる。
- クロスドメイン認証 : 異なるドメイン間での認証が容易になる。
JWTのデメリット
- トークンの無効化が困難 : JWTはステートレスであるため、一度発行されたトークンをサーバー側で無効化するのが難しい。そのため、トークンの有効期限を短く設定し、リフレッシュトークンを使用して新しいトークンを取得する方法が一般的です。
- トークンサイズ : JWTはペイロードに情報を含むため、トークンのサイズが大きくなることがあります。これにより、HTTPヘッダーのサイズ制限に引っかかる可能性があります。
- ペイロードは暗号化されていない : JWTのペイロードはBase64Urlエンコードされているだけであり、暗号化されていません。そのため、トークンを傍受した攻撃者がペイロードの内容を読むことが可能です。機密情報を含める場合は注意が必要です。