はじめに
SinatraでDex(dexidp/dex, OpenID Connect Provider)を利用した認証を利用するための検証用コードを作成してみました。
初稿では参考にした docs.openathens.net の記事と重複しないようdiff出力の差分で解説していましたが、読み替えしてみて自分でも補足がないと読むのが難しかった点などがあったので少し見直しました。
またopenapi-generatorが生成するsinatraのスケルトンコードも変更する可能性がありますので、差分だけではpatchが当たらない可能性があります。
そのため、コードはできるだけ全体を見通せる分量を掲載するようにしています。
blog.s21g.comの記事では、Sinatraで利用可能な認証モジュールのリストがありますが、現在はメンテナンスされていないライブラリも含めて掲載されています。
最終的に、docs.openathens.netの記事のクラスを利用しました。
OpenAPIの本来の目的で利用する場合には、API-Keyを個別に発行してAuthorization: Bearerの利用をクライアントに要求するのが作法だと思います。今回はopenapi-generatorをWebアプリケーションのスケルトンコード生成器として使用していますので、対話的なクライアントアクセスのためにOIDCを利用しています。
References
- http://blog.s21g.com/articles/1635
- https://docs.openathens.net/display/public/OAAccess/Ruby+OpenID+Connect+example
環境
記事を見直すタイミングで環境を更新しました。
- OpenAPI Generator: 5.2.1
- OpenID Connect Provider: Dex (https://github.com/dexidp/dex)
- OS: Ubuntu 20.04 LTS
- ruby: 2.7.0 (Ubuntu付属のパッケージ) and 3.0.0 (Dockerコンテナ)
- ruby-bundler: 2.1.4 (Ubuntu付属のパッケージ)
DexはTLSによる接続を受け付けるように構成しています。デフォルトの構成ではOpenID Connect ProviderへのアクセスにTLS接続を仮定するので、OIDC Serverへのアクセスに http:// を指定するとエラーになります。
openapi-generator-cliを利用して、定義ファイル(openapi.yaml)からcode/ディレクトリにスケルトンコードを生成します。
openapi: 3.0.1
info:
contact:
email: yasu@yasundial.org
name: Yasuhiro ABE
url: https://www.yasundial.org/
description: This is the test site
title: The test client of the OpenID Connect
version: 0.1.0
servers:
- url: https://localhost:8080/
paths:
/:
get:
description: The landing page.
responses:
200:
description: If authenticated, it shows user information.
/protected:
get:
description: Protected contents only authenticated user can view.
responses:
200:
description: If authenticated, it shows user information.
303:
description: If not-authenticated, then redirecting to the /login.
/login:
get:
description: The OIDC login entrypoint.
responses:
200:
description: Show the login form.
post:
description: OIDC login page
responses:
303:
description: Redirect to the dex oids provider
/callback:
get:
description: |
The callback endpoint from an OIDC server.
e.g. /callback?code=xxxx&state=xxxx
parameters:
- explode: false
in: path
name: code
required: true
schema:
type: string
style: simple
- explode: false
in: path
name: state
required: true
schema:
type: string
style: simple
responses:
303:
description: Redirect to the top landing page or somewhere.
/logout:
get:
description: The OIDC logout endpoint to clear the session object.
responses:
200:
description: stay on this page.
303:
description: redirect to /.
components:
schemas: {}
$ openapi-generator-cli generate -g ruby-sinatra -o code -i openapi.yaml
code/以下の各ファイルの差分は次のようになっています。実際の作業では実装を追加したコードはcode.impl/ディレクトリに作成しています。
最終的な成果物は、GitHubとDockerHubで公開しています。
docs.openathens.net を参考にする上での注意点
Module名は、MyOIDCProvider に変更しています。
この他、以前の記事を参考にしたところ、そのままでは動かない点がいくつかありました。
OIDC Scopeの指定
将来的に groups を利用したいので、MyOIDCProviderモジュールの中での scope変数に :groups を追加しています。
def auth_uri(nonce)
authz_url = init_client.authorization_uri(
scope: [:profile, :email, :groups],
state: nonce,
nonce: nonce
)
end
Session管理
sessionオブジェクト(session[:origin])にredirect元のURLを記録していますが、ここではRedisを利用しています。
Gemfileで redis-rack を指定しています。
また、あらかじめredisをdockerから起動しています。
sudo docker run -it --rm -d \
-p 6379:6379 \
--name redis redis:latest
JSON::JWK::Set::KidNotFound エラーへの対応
openathens.netのブログで紹介されているコードを利用した時に、openid-configurationの結果を@disco
オブジェクトにキャッシュしている部分でエラーの原因となることがあります。
アプリケーションがOIDC Providerから渡された認証情報を検証しようとする時にjwks_uriをキャッシュした@disco
の情報を利用することがあります。
これは時間の経過を考慮していないので、Provider側が公開鍵情報を更新したタイミングで利用できなくなります。そのタイミングはProvider側の実装次第なので発生しないか、比較的短期間に発生するか予測することはできません。
myoidcprovider.rbのコードはこういった状況を考慮していないので、このままでは正常に動作しない可能性がありました。
ユーザーのログインが短時間に集中することが見込まれれば@disco
によるキャッシュはネットワークトラフィックとProvider側の負荷軽減に役立ちますが、そうでもなければ検証時の例外をトラップして更新処理と再検証のロジックを埋め込む必要があります。
公開しているサンプルコードでは単純に@disco
オブジェクトの利用を止めることにしました。
diff --git a/_docker/myoidcprovider.rb b/_docker/myoidcprovider.rb
index a523072..7a374e9 100644
--- a/_docker/myoidcprovider.rb
+++ b/_docker/myoidcprovider.rb
@@ -63,7 +63,8 @@ module MyOIDCProvider
end
def discover
- @disco ||= OpenIDConnect::Discovery::Provider::Config.discover! @provider_uri
+ OpenIDConnect::Discovery::Provider::Config.discover! @provider_uri
+ ## Removed "@disco ||=" by YasuhiroABE <yasu@yasundial.org>
end
end
end