The Battle with OIDC
Overview
最近の記事では Zig によるシステムプログラムばかり書いていますが、開発現場ではバックエンドにアサインして頂いているという事もあり、たまには API の記事など書こうと思います。今回は OIDC プロバイダを Go (Gin) で実装しています
数年前、鉄道会社向けに OIDC を認証基盤ごと仕様準拠で実装した経験があり、そのシステムは現在でも問題なく稼働しています。今回はその知見を踏まえつつ、最小構成で再設計した OSS 版を公開しています。私はこれを Asteroid と名付けました
仕様を理解し、期待する動作を確認した上で公開していますが、本実装は特定の企業や契約に基づくものではなく、個人開発した OSS 版です。実運用での利用可否や最終的な安全性については利用者自身の判断と検証に委ねられます。ライセンスの範囲でご自由にどうぞ
Specification
Asteroid は OAuth 2.0 (RFC 6749) で定義される最小限 (MUST) と、強く推奨 (事実上の MUST) されている認可フローを実装し、上位仕様である OIDC のコア機能を提供します
OIDC は OAuth 2.0 に「認証 (Authentication)」を追加した上位仕様ですが、Asteroid は純粋に「認可 (Authorization)」のみを責務とし、認証やユーザー情報管理はスコープに含めません
これによって Asteroid は 4 つのエンドポイントにだけ集中し、OIDC を成立させる最小限を、わずか 6000 行で実装し、インターフェイス定義のみを残すことで、どのような認証基盤とも接続できる拡張余地を意図的に確保しています
When you need more, you add more.
Yesterday’s complexity should not pollute today’s simplicity.
Asteroid の仕様は以下です
Asteroid specification
OpenID Connect Core 1.0 (OIDC) の仕様は以下です
Architecture
Asteroid は OIDC を以下の様に整理しています
また Go の特性を活かし、HTTP サーバーを単一バイナリとして内包しており、どこでも動作しますが、実運用では nginx から unix socket で直接リバースプロキシするか、ALB 直後に配置する構成を想定しています
TLS 終端やアクセス制御を上位で完結させることで、Asteroid をシンプルに保てるためです
詳細は以下に定義しています
Asteroid architecture
http.*
HTTP I/O 層
- Gin を用いたリクエスト/レスポンス処理のみを担当
- ルーティングと I/O のみに責務を限定
- Gin の *gin.Context を扱う唯一の層
- OIDC のビジネスロジックは一切持たない
- 依存(サービス、ストアなど)は外部から明示的に注入する
この層は「外の世界」との境界線であり、プロトコル変換だけを担当します。Gin への依存はここで完全に閉じる事が可能で、アプリケーションは Gin を知りません
oidc.*
OIDC のビジネスロジック層
- OIDC のプロトコル仕様(認可、クライアント検証、スコープ検証など)だけを扱う
- HTTP(Gin)には全く依存しない
- JSON など「I/O」には依存しない
- ストアへは interface 経由でアクセスし、実装には依存しない
フレームワークや I/O を持ち込まない、純粋な OIDC ライブラリとしての性質を持ち、ドメインモデルとして動作する層です
store.*
データアクセス層
- 認可コード / トークン などの永続化インターフェース
- 扱うのは純粋な構造体だけ
- OIDC 層は interface のみ依存し、具体的な永続化方法を知らない
- 実装は Memory / Redis をコンパイル時に差し替え可能
- /userinfo で外部接続が必要であれば、簡単に拡張する事が可能
どのバックエンドでも動作する、極めてゆるい結合のデータ層です。本実装のデフォルトはオンメモリですが、クラウド環境においては Redis を強く推奨しています
Gin Web Framework
フレームワークがアーキテクチャを強制した瞬間、ソフトウェアは自由を失います。本来フレームワークが提供すべきものは、薄い HTTP の糖衣と、ロギングなど最小限の補助だけです
アプリケーションがどのように構造化されるべきかは、フレームワークではなく、プログラマ自身の責任で決めるものです
Gin はコードを見れば明らかなように、標準ライブラリに薄い糖衣をかぶせたフレームワークです
プログラマが書きたい形で書けるよう設計されており、余計な抽象や継承構造に縛られることのない明示的な参照はエレガントで、透き通るほどに透明です
Infrastructure
Asteroid は NixOS (EC2) へデプロイしています
NixOS の純粋な参照透過性と、AWS CDK による宣言的なプロビジョニングに加え Go の持つ API Frozen という特性により、いつ何度でも環境は同じ状態で構築され、コンテナによる無駄なオーバーヘッドはなく、本当に必要なコードだけでコンパイルした Nginx がその上を走ります
Nginx を狭いコンテナに閉じ込めず、不要な実装を全て取り除いた状態で L7 最前線に直接配置するこの構成において、Linux カーネルの network stack を前提に設計された、低いレイヤーを高速に走る本来の姿を見せるのです
当然ながら Asteroid は systemd の管理下でデーモンとして動作します
起動順序、再起動、権限、書き込み可能な状態のディレクトリまでを宣言的に定義し、プロセスを状態として管理します。Nginx と Asteroid は明確な責務境界を持ち unit / socket を軸に Linux Native で連携します
- Nginx custom compiled
- AWS CDK (NixOS EC2 / etc.)
- Asteroid (nix-linked Go static binary)
インスタンスは無であり、ただ Nix によって定義されるのみ
Vim だけで Nix を書き Linux を構築する感覚は、現代のサーバーレスやコンテナといった高度に抽象化された環境では、もはや感じる事の出来なくなったあの頃の記憶が確かに蘇ります
Lower is Truth
If the runtime never provides access to memory, abstraction turns into decoration. It’s only real when you can descend a layer and shape it with your own hands.
The ground is already defined by HTTP and POSIX.
Why Fast?
Asteroid は起動時に秘密鍵やクライアント情報など、レスポンス生成に必要なデータをすべてメモリに展開し、永続化が必要な部分だけを Redis に委ねます
さらに Token のライフサイクル管理も Redis の TTL に任せられるため、リクエスト処理のほぼ全てがネイティブバイナリの純粋なメモリアクセスだけで完結します
鍵ローテーションにおいては堅牢なメモリ管理の上に goroutine で実行され、最新鍵の生成・公開鍵セット (JWKS) の再構築もメインパスを一切阻害せず、理論的に極めて高速です
Asteroid がどれほど高速に動作するかは、実測値をまとめた、Benchmark ページがあるのでそちらを参照してください。全てのエンドポイントは μs で応答します
Public-Key Cryptography
Asteroid は認可に必要な署名を秘密鍵によって行いますが、公開鍵暗号には「2つのまったく逆の用途」が存在します
Encryption/Decryption
公開鍵で暗号化 → 秘密鍵で復号
一般に TLS のサーバ証明書 (HTTPS) をイメージすれば簡単で、クライアントがリクエストを公開鍵によって暗号化し、サーバーは秘密鍵で復号する用途です
Signing
秘密鍵で署名 → 公開鍵で検証
ブロックチェーンのトランザクション署名と同じで、秘密鍵で署名し、改竄されていない事を公開鍵によって検証する用途です。今回はこちらが実装になります
How a Public Key Becomes JSON
JWT の世界で最も美しい部分は 「数学的に定義された公開鍵が、JSON として流通できる形式に変換される」 という点です
これはただの実装詳細ではなく、暗号技術が抽象化され、一般的な API として扱えるレベルにまで引き上げられた瞬間です
OIDC の /jwks.json に現れる公開鍵は次のいずれかとなり、Asteroid では両方を実装しています
- RSA 公開鍵 → 1つの巨大な整数 n と、それに対応する指数 e
- ECDSA 公開鍵 → 楕円曲線上の点 (x, y)
この「巨大整数」あるいは「曲線上の点」が、JWK ではただの JSON フィールドとして並びます (アルゴリズムの詳細はどちらも本当に美しいので初見なら是非調べる事をおすすめします) 分解された公開鍵はシリアライズされ、ネットワークを越えます
{
"kty": "EC",
"use": "sig",
"alg": "ES256",
"kid": "4a506dd2-c5e4-407c-b374-a2f1cf963381",
"crv": "P-256",
"x": "sZp54h19PBmib3OKNVm5XdZI_5X-gUHY26Tpy4V8iNQ",
"y": "hki24xDbYP7kRyIzUEfN26-US66h0SVdPsmF5h33c28"
}
そしてあなたの元に届き、数学は静かに真実を語り始めるのです
Asteroid follows the UNIX philosophy
Asteroid は UNIX の精神に基づき設計されています
小さく、明確で、必要最小限の責務だけを持つコンポーネントがそれぞれ独立して動作し、お互いに結合しない("separate, but cooperate")構造を徹底しています
余計な抽象を積み上げず、薄い層を境界として機能を分離することで、ロジックは常に見通しよく、変更は局所化され、どの層も単体で理解できます
- Small is Beautiful
- Do One Thing and Do It Well
- Loosely Coupled is Powerful
UNIX の哲学は、私たちの抱える設計上の悩みの多くが何年も前に解かれていることを思い起こさせます。困難にぶつかったときは、この原点に戻るだけで道が自然と見えてきます
Special Thanks
Thanks to the Go community and the Gin team for a framework that is truly crystal clear. And thanks to my colleagues at XMart Inc. for the continuous technical discussions that make our work better.
Visibility is freedom. Stay transparent.
Discussion