OCIサービスを利用したWebサイトの作成 その4~Identity Cloud Serviceでサイトの一部を保護 (2021/12/30)
OCIサービスを利用したWebサイトの作成 その4~Identity Cloud Serviceでサイトの一部を保護 (2021/12/30)
https://blogs.oracle.com/cloudsecurity/post/using-oci-services-part-4
今回は、OCI Native Servicesを使って完全にサーバーレスでWebサイトをホスティングするシリーズの第4弾です。
- パート1では、OCI WAF + API Gateway + Functionを使用して、OCI Object Store Bucketから静的コンテンツを提供する基本的な方法を紹介しました。
- パート2では、バケットを(公開ではなく)非公開にし、Resource Prinicpalを使用して安全にアクセスする方法を紹介しました。
- パート3では、ハードコードされた設定を削除し、代わりにそれらの設定をアプリケーションに保存する方法を紹介しました。
パート4では、サイトの一部をログインの後ろに置いてみようと思っています。
この記事は、ちょっとした「フクロウの描き方」のようなものになりそうです。でも、コードを見ればきっと理解できるはずです!
IDCSでアプリケーションを作成
注:OCI Identity Domains は、OCI の新規顧客に対して IDCS の機能を直ちに提供します。現在 IDCS を使用している顧客については、既存の IDCS ストライプは今後数ヶ月の間に OCI Identity Domains に移行される予定です。詳細については、OCI Identity Domainのドキュメントを参照してください。この変更は、このブログで説明したセキュリティ・パターンに影響を与えるものではありません。このブログで IDCS に起因するセキュリティ機能は、OCI Identity Domainsでも提供されます。
まず最初に、サイトを表現するために IDCS で実際にアプリケーションを作成する必要があります。
IDCSコンソールを開き、「アプリケーション」に移動し、「作成」をクリックします。Functionアプリケーションはクライアントシークレットを保持することができるため、Confidential Applicationを選択します。アプリケーションに名前を付けて、次のステップに進みます。どのグラントタイプが必要かを選択する必要があるので、「Authorization Code」を選択します。そのGrant Typeは、IDCSがユーザをどこに送り返せばよいかを知るために、コールバックURLを必要とします。
コールバック URL は、私たちの Web サイト上の特別な URL で、上で述べたように、ユーザが認証された後に IDCS がユーザを送り返す場所です。しかし、私たちはそこに実際のコンテンツをホストしているわけではありません。その代わり、これは私たちのコードが目を光らせている魔法のURLで、もしこれが見つかったら、IDCSからリダイレクトされるように処理します。この値には https://<the site url>/callback/ を入力します。
より詳細な情報、またはスクリーンショットによるステップバイステップは、前回の投稿をご覧ください。
Client IDとSecretを保存し、ウィンドウを閉じる前にActivateをクリックするのを忘れないでください。
Pythonコードに必要なOAuthライブラリを追加
OAuthとOpenID Connectはとても簡単ですが、それでも良いライブラリがあれば、独自に開発するよりもそちらを使うべきでしょう。そこで、怠惰に身を任せ、以下のライブラリを requirements.txt に追加してください。
- oauthlib
- requests
- requests_oauthlib
- PyJWT
requirements.txtはFunctionのビルド時に "pip "ユーティリティによって使用され、Python Functionのコードが必要とするライブラリを指定するために使用されます。
oauthlibライブラリはOAuthの複雑な部分を隠し、requestsライブラリはHTTPリクエストを可能にし、requests_oauthlibはピーナッツバターとチョコレートのように2つを組み合わせ、PyJWTはIDCSが返すようなJWTの内部を簡単に見ることができるようにしています。
Functionアプリケーションで設定を作成
次に、Functionアプリケーションにいくつかの設定を追加する必要があります。BucketNameの設定はすでにありますが、あと4つ必要です。
コンフィギュレーション設定はこのようになっているはずです。
また、コマンドラインから設定を確認することもできます。
アプリケーションの設定にクライアントシークレットを置くことは、私が「ベストプラクティス」と呼ぶものではないことに注意する必要があります。本当に秘密の場所に置くべきでしょう。OCIにシークレットを保存する場所があればいいのですが!
この点については、後日改めて紹介する予定です。しかし、今のところ、アプリケーションの設定に置くことは、ソースコードに焼き付けるよりもまだましです。
Functionコードの更新
「フクロウの続きを描け」という投稿の部分です。
私は元のコードをリファクタリングして、構成設定のパージングをConfigurationという新しいクラスに移動させました。そして、オブジェクトストアのアクセスコードをすべてObjectStoreという別のクラスに移動させました。
それから、OpenID Connectを実際に実装するための小さなコードの塊を書きました(上記のライブラリを使って)。
このコードはかなり荒削りで、もっとうまくやるべきだったところを指摘するコメントがたくさん付いています。コードの中で最も恥ずかしいのは、セッションクッキーを生成するところです。
また、ゼロ権限、クッキーの検証、その他たくさんのとてもとても悪いことがそこにあります。
しかし、これは動作します。そして、最小限のものを実際に動作させることが、この投稿シリーズの全目標です。
ここでは、より興味深い部分のいくつかを紹介します。
Authorization と Token のエンドポイントを見つける
上記で、IDCSのURLのホスト名以降を無視すると書きました。私はこのコードの塊でそれを行いました。
Pythonのurlparse()関数は、URLをバラバラにして、それが適切に構築されているかどうかを確認します。私は、URLが(おそらく)OKであることを確認するためにこれを使用しています。
urljoin()関数も同じように、既存のパスとクエリ文字列を削除し、第2パラメータでパスを追加します。この関数を使用して、Authorize URLとToken URLを構築しています。
このコードの将来のバージョンでは、おそらくこのすべてを削除して、OpenID Connectライブラリの使用に切り替え、正しいURLを取得するためにOpenID Connect Discoveryを使用させることになるでしょう。しかし、それほど重要ではないいくつかの理由から、ここではシンプルなOAuth構造を使用したいと思いました。
Authorizationエンドポイントへのリダイレクト
セッションクッキーを取得し、それを「復号化」し、ユーザ名を取得し、クッキーを設定するコードのブロックがあります。そのコードの「else」ケースは、次のようになります。
このコードは、実際にユーザーを認証するためにIDCSに送り出すところです。WebApplicationClientはOAuthライブラリの一部であり、prepare_request_url()メソッドは実際にログイン(またはSSO)プロセスを開始するためのURLを構築します。ここでは、いくつかの興味深いビットを見ることができます。
- redirect_urlにはコールバックURLが設定されています。IDCSのApplicationで設定したのと同じURLです。
- スコープを "openid "に設定することで、IDCSにOpenID Connectを行うよう指示します。
- 最後にstateがFunctionコンテキストのRequestURLに設定されます。基本的には、ユーザーがアクセスしようとしたWebサイト上のパスです。また、クエリ文字列のパラメータがある場合は、それらも含まれます。
このコードの素晴らしいところは、URLエンコーディング、フィールドの正しい命名、値が正しいかどうかのチェックなど、複雑な処理をすべて行ってくれることです。ライブラリ関数に適切なパラメータを渡すだけで、すぐに実行できます。
ここでもうひとつ興味深いのは、元のRequest URLをクッキーなどではなく、stateフィールドに保存していることです。ステートに保存することで、ユーザーが複数のウィンドウやタブを開いている場合、ユーザーが認証された後、それぞれ正しい場所に戻ってくることになります。これは技術的にIDCSから「見える」ことになり、IDCSやカスタムログイン画面がその情報をどう扱うかによって、良いことも悪いこともあります。また、ブラウザの履歴にも残ります。ですから、この情報を暗号化するか、難読化するのがよい方法でしょう。繰り返しますが、将来的にやるべきことのリストに追加すべきことがもう一つあります。
注:OCI Identity Domains は、OCI の新規顧客に対して IDCS の機能を直ちに提供する。現在 IDCS を使用している顧客については、既存の IDCS ストライプは今後数ヶ月の間に OCI Identity Domains に移行される予定である。詳細については、OCI Identity Domainのドキュメントを参照してください。この変更は、このブログで説明したセキュリティ・パターンに影響を与えるものではありません。このブログで IDCS に起因するセキュリティ機能は、OCI Identity Domains でも提供されています。
認証コードの処理
IDCSに認証された後、または既に認証されている場合、ブラウザはコールバックURLに送信されます。IDCSはOAuthとOpenID Connectの仕様に従っているので、URLにいくつかの追加パラメータが追加されます。
- code - これはOAuthの認証コードです。
- state - これは、IDCSの認証URLへのリダイレクトを構築する際に追加した状態です。
それを処理するコードです。
この中には、いくつか興味深い点があります。
- 例えば、リダイレクトURLは/に設定されていますが、何か問題が発生した場合、ユーザーは/に戻ることになります。
- stateには、ユーザーを特定した後にリダイレクトするパスを指定します。私は、攻撃者がユーザーを他のウェブサーバーにリダイレクトするように私を騙すことができないように、stateが/で始まることを確認するサニティ・チェックを設けています。つまり、Open Redirectを防ぐためです。
- prepare_request_bodyでは、POSTのペイロードにクライアントIDとsecretを入れます。OAuthとOpenID Connectは、OAuthクライアント(つまりこのコード)がOAuthサーバに対して自分自身を認証するための方法をいくつか提供しています。IDCS の OpenID Connect Discovery Endpoint を見ると、client_secret_basic と client_secret_post の両方をサポートしていることがわかります。今回使用するライブラリは、デフォルトでPOSTペイロードにClient IDとSecretを送信するようになっており、IDCSがこれをサポートしているので、このライブラリを使用するのは非常に簡単です。
ユーザー名の取得とCookieへの保存
Token EndpointからのJSONレスポンスには、3つのフィールドが含まれます。
- access_token: JWT形式のアクセストークン
- id_token: 同じくJWTであるIdentity Token
- expires_in: 上記トークンの有効期限を示すフィールド
私のコードはとても、とても、とても、悪いです。ATとexpires_inフィールドを無視し、Identity Tokenだけを取り出しているのです。ID TokenをJWTライブラリに渡し(署名をわざわざチェックしないように指示 - これも非常にまずい)、ユーザ名を取り出します。署名を無視するのはベストプラクティスではないですが、私は認証コードを提示してHTTPS上でATを取得したので、これは比較的安全であると言えます。少なくとも、私がこれからやろうとしていることよりは安全です。
ここで起こること
ここでの大きな改善点は、以下の通りです。
- id_tokenからユーザ名を抽出するだけでなく、IDCSのToken Introspectionエンドポイントにアクセストークンを提示すること
- ユーザー名以上のものを保存すること(トークンからの「サブ」クレーム)。例えば、セッションの有効期限や最終アクセス時刻、IDCSからのユーザー「id」、その他必要と思われる属性を保存する必要があります。
- 実際にCookieを暗号化する必要があります。OCIのネイティブ暗号化サービスを使うか、秘密鍵(そしてその鍵をOCI Vaultに保存することができます)を使って暗号化することができます。
コードはこちら
コードはGitHubの https://github.com/therealcmj/demosite で見ることができます。
今後
この関数コードは動作します。しかし、上で述べたように、さらに良くするための変更がたくさんあります。それらを上記で述べた順番にリストアップしてみます。
- Client Secret は OCI Vault Secrets に格納
- トークンイントロスペクションエンドポイントを呼び出して、ユーザーのIDを取得する必要があります。
- IDCSのアプリケーションロールに基づいて)認可を行うべきです
- 少なくともユーザー名以上の情報をクッキーに保存しておく必要があります。
- ユーザID
- 最終アクセス時刻
- セッションの有効期限
- クッキーをrot13以上の何かで暗号化する必要があります。
- アイドルタイムアウトに対応すべき
- POST保存に対応できるかもしれない
変更を加え、プルリクエストを生成することは歓迎されます。
でも、他の面白いプロジェクトが一段落したら、自分で変更しに行くかもしれません。
だから、期待していてください
コメント
コメントを投稿