はじめに
今回OpenID Connectの認証サーバーを自作してみたのでその記録を残しておきます。
システムの概要
OAuthの認可機能と、OpenID Connectによるシングル・サイン・オン(以下、SSO)機能を提供しています。
認証・認可のフローは下図のとおりです。
OpenID Connectの仕様に完全準拠させるのは大変なので、OpenID Connectの中核となるフローを実装しました。他の仕様については徐々に対応させていきたいと思っています。
アプリ画面
以下のリンクから、開発した認証サーバーを用いたSSOのデモを行うことができます。
https://auth-example.piny940.com
ボタンをクリックすると、認証ページに飛びます。アカウントを持っていない場合は、アカウントを作成することができます。
ログインすると、認可チェックのページにリダイレクトされます。(この画面のUIは追々改善したいなーと思ってます。)
Approveすると、最初のデモサイトにリダイレクトされます。デモサイトはサーバーサイドで認証サーバーにトークンリクエストを送信しており、アクセストークンとIDトークンを取得しています。
開発
今回の開発ではあえてOAuth向けのライブラリやフレームワークを使わずに1から実装をしてみました(JWTの生成などはライブラリを使用しています)。
フレームワークを使ったほうが楽だしセキュリティ的にも確実ですが、今回の開発は「OpenID Connectについての理解を深める」ということを大きな目的としていたため、あえてフレームワークを使わないという選択をしました。
設計
まずは、OpenID Connectの仕様に基づいて、認証サーバーのAPI・データベースの設計を行いました。
API設計
API設計はOpenAPIの形式で記述し、以下のサイトで公開しています。
OpenAPIの仕様書をYamlで書くのは面倒なので、OpenAPIの仕様書をTypeScriptのような記法で書けるTypeSpecを活用しました。
データベース設計
データベースは以下のように設計しました。
実装で悩んだ点
実装する際に悩んだのは、認可リクエストをどのようにバックエンドに送るか、ということです。
通常、Backendサーバーのページにユーザーが直接アクセスするのは稀で、JSでBackendサーバーのエンドポイントを叩くということが多いです。しかし、認可リクエストの場合は、リダイレクトレスポンスを返すようRFCに定められているため、JSでリクエストを送るという方法とは相性が悪いです。
これに対する方法として以下の2つの方法を考えました。
- 通常通りJSでリクエストを送り、レスポンスが302であれば
next/router
を用いてリダイレクトする - Backendのエンドポイント(
/authorize
など)に直接アクセスする
1つ目の方法は、「通常通りBackendのエンドポイントはJSから叩く」という意味で一貫性があって分かりやすいです。一方で、「リダイレクト」というOAuthのコアな仕様に関する処理がFrontendのコードにまで入ってしまうため、セキュリティ的にはあまりよくありません。
2つ目の方法は、Backendのエンドポイントにユーザーが直接アクセスする、という点で若干の気持ち悪さがありますが、コアロジックをGoサーバーに集約することができるため、セキュリティ的に安心です。
以上を考えた結果、認可リクエストに関しては直接Backendのエンドポイントにアクセスする2つ目の方法を採用しました。
工夫した点
domainロジックの分離
Backendのコードは、今回レイヤード・アーキテクチャを採用し、domain層・usecase層・infrastructure層・api層に分割しました。
ドメインロジックをdomain層に集約することで、バグが起こりづらくし、また、テストもしやすくなりました。
コードの自動生成
上述の通り、Backendではレイヤード・アーキテクチャを採用しました。レイヤード・アーキテクチャでは、domain層、infrastructure層、api層でそれぞれの役割が異なるため、それぞれの層でモデルを定義する必要があります。また、各層の間の依存関係の注入(以下、DI)も大変です。これらをすべて手動で記述するのは大変ですし、ミスも起こりやすいです。
そこで、今回のプロジェクトでは以下の自動生成ツールを導入しました。
- infrastracture層:go-gorm/gormのGen
- api層:oapi-codegen/oapi-codegen
- DI:google/wire
これにより、Backendのコードの自動生成を行うことができ、コードの品質を保つとともに、実装コストを大幅に削減することができました。
Frontendでもopenapi-ts/openapi-typescriptを用いて自動生成を行うことで、APIの型安全性を保つことができました。
今後について
現状、課題をいくつか抱えています。
-
ID Tokenの有効性をサーバーに問い合わせることができない
現状、1度発行したトークンは有効期限が切れるまで失効させることができないため、ID Tokenが漏洩した場合などでも、有効期限が切れるまでそのトークンを使い続けることができてしまいます。これに対応するために、トークンを失効させる仕組みを導入する必要があります。 -
リフレッシュトークンが発行されていない
現状、リフレッシュトークンを発行していないため、アクセストークンの有効期限が切れると再度認可フローを行う必要があります。これでは利便性が落ちてしまうため、リフレッシュトークンを導入したいと考えています。
こういった点の改善を徐々に行っていきたいと考えています。
おわりに
今回はOAuth2.0・OpenID Connectの認証サーバーを自作しました。開発を通して、OAuth2.0・OpenID Connect・JWTの仕様について理解を深めることができました。今後も、セキュリティに関する知識を深めていきたいと考えています。