自作OAuth2.0

はじめに

今回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の形式で記述し、以下のサイトで公開しています。

https://auth-doc.piny940.com

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)も大変です。これらをすべて手動で記述するのは大変ですし、ミスも起こりやすいです。

そこで、今回のプロジェクトでは以下の自動生成ツールを導入しました。

これにより、Backendのコードの自動生成を行うことができ、コードの品質を保つとともに、実装コストを大幅に削減することができました。

Frontendでもopenapi-ts/openapi-typescriptを用いて自動生成を行うことで、APIの型安全性を保つことができました。

今後について

現状、課題をいくつか抱えています。

  1. ID Tokenの有効性をサーバーに問い合わせることができない
    現状、1度発行したトークンは有効期限が切れるまで失効させることができないため、ID Tokenが漏洩した場合などでも、有効期限が切れるまでそのトークンを使い続けることができてしまいます。これに対応するために、トークンを失効させる仕組みを導入する必要があります。

  2. リフレッシュトークンが発行されていない
    現状、リフレッシュトークンを発行していないため、アクセストークンの有効期限が切れると再度認可フローを行う必要があります。これでは利便性が落ちてしまうため、リフレッシュトークンを導入したいと考えています。

こういった点の改善を徐々に行っていきたいと考えています。

おわりに

今回はOAuth2.0・OpenID Connectの認証サーバーを自作しました。開発を通して、OAuth2.0・OpenID Connect・JWTの仕様について理解を深めることができました。今後も、セキュリティに関する知識を深めていきたいと考えています。