Keycloak OIDCでの認証時に発生したCORSエラー。犯人はトレイリングスラッシュでした。

"Keycloak - Identity and Access Management for Modern Applications - Second Edition"を読んでいます。

邦題『実践Keycloak』の2nd Edition版ですね。

この本のChapter 2のワークを進める上で一瞬ハマりかけました。

エラーログを読んで冷静になったらすぐに解決できたのですが、理解のおさらいも兼ねて思考過程をメモしておきます。

Keycloakはもちろん、OAuth、OIDC、CORS等には入門したばかりなので、違うこと言ってる可能性がかなりあると思いますが、これも勉強のうちということで…。

利用したサンプルアプリケーションについて

本書で用意されていたサンプルアプリケーションについて前提整理のためにざっくり書いておきます。

概要

  • Node.jsで実装されたSPA
  • KeycloakをIdPとしたOIDC Authorization Code Flowを利用したユーザーログイン機能を実装
  • ログイン処理後、Keycloakより返されたID TokenやAccess Tokenを画面に表示できる
  • KeycloakにてClient登録済み
  • 登録したURL等は以下

想定される挙動(一部Keycloakのライブラリが内部的に実行していると思われる)

  1. サンプルアプリケーションにてログインボタンを押す
  2. KeycloakへAuthorization Code Requestが送られる
  3. Keycloakのログイン画面が表示される
  4. Keycloakでユーザーがログインする
  5. サンプルアプリケーションのValid redirect URIsとして登録した http://localhost:8000/にリダイレクト。パラメータにAuthorization codeを含む
  6. サンプルアプリケーションがAuthorization code含めたリクエストをKeycloakのToken Endpointに送信する
  7. KeycloakからID TokenとAccess Tokenがサンプルアプリケーションに返される
  8. サンプルアプリケーションはID TokenとAccess Tokenを画面に表示する

CORS policyによるアクセス拒否

サンプルアプリケーションの仕様ではログイン後に取得したID TokenやAccess Tokenが画面に表示されるはずが何も表示されませんでした。

ChromeのDeveloper Toolを開いてConsoleのログを見るとエラー文が。

Access to XMLHttpRequest at 'http://localhost:8080/realms/myrealm/protocol/openid-connect/token' from origin 'http://localhost:8000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

CORS policyでサンプルアプリケーションのオリジン(http://localhost:8000)からのリクエストが許可されてないとのこと。

Web originsは設定しているはずだが…

前述した想定される挙動の「5. サンプルアプリケーションがAuthorization code含めたリクエストをKeycloakのToken Endpointに送信する」にてエラーが発生していると思われます。

サンプルアプリケーションはフロントエンドのJavascriptによってリクエストを送信しており、リクエスト先であるKeycloakのCORS policyによってオリジンが許可されている場合のみリクエストが成功します。

そしてKeycloakではClientのWeb originsにて設定された値をAccess-Control-Allow-Originに追加する模様。

事前の設定で追加しているはずなんだが…

おや…?

おやおや…?

Web originsの設定値にトレイリングスラッシュが入っていた

http://localhost:8000/Web originsに登録していたが、よくよくエラー文を見てみるとorigin 'http://localhost:8000' has been blocked by CORS policy と書かれています。

そう。犯人は末尾の/

本書を読み返してみると、確かに文中ではWeb originsの値のみ末尾の/が記載されていませんでした。読み間違いやらかしてたー。

1つだけ弁明させてもらうと、本書に添付されていた設定画面のスクショでは/がしっかり入っている値が表示されているのです。少し罠だと思いました。

そしてWeb originsの値をhttp://localhost:8000へと修正したら見事解決。

ちなみに末尾の/は「トレイリングスラッシュ」というらしいです。

gmotech.jp

オリジンにトレイリングスラッシュは含まない

となるとオリジンの定義が気になるところなので調べてみました。

developer.mozilla.org

定義としては

Web content's origin is defined by the scheme (protocol), hostname (domain), and port of the URL used to access it. Two objects have the same origin only when the scheme, hostname, and port all match.

とのこと。これだけではトレイリングスラッシュを含むかどうか判断しにくいなと思いましたが、オリジンの例を見てみるとどれも末尾に/がなく、オリジンというものはトレイリングスラッシュを含まないみたいですね。

These are not same origin because they use different hostnames:

よくよく考えたら HTTP requestは以下のような形式なので、オリジンにトレイリングスラッシュが入ったらおかしなことになりそうではありますね。

GET / HTTP/1.1
Host: example.com

Valid redirect URIsはトレイリングスラッシュが必要

ちなみに、Valid redirect URIs の方は逆にトレイリングスラッシュ必須なの?というのが気になったので検証してみると、想定挙動の「2. Keycloakのログイン画面が表示される」にてエラーが発生したため必須ぽいです。

このときのKeycloakへのリクエストURLを見てみると以下のようになっていて、redirect_uriの値には末尾に/(エンコードされて%2Fになっている)が設定されていることがわかります。

http://localhost:8080/realms/myrealm/protocol/openid-connect/auth?client_id=myclient&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2F&state=以下略

コード実装上では以下のようになっており、redirect_uriはKeycloakライブラリが設定している模様。

var kc = new Keycloak({ realm: 'myrealm', clientId: 'myclient' });
~~略~~
 <button onclick="window.kc.login()">Login</button>

ここでのredirect_uriはおそらくアクセス元から生成していて、トレイリングスラッシュをつけるようになっているのではなかろうか。そしてKeycloakのClientに設定した Valid redirect URIsに一致するものがあるかチェックしてる気がします。

(ライブラリのソースも少し読んでみたがすぐにはわからなそうで断念したので憶測です)

そもそもブラウザは基本、トレイリングスラッシュがないURLの入力には自動でトレイリングスラッシュをつけるらしく、Keycloakライブラリが設定するredirect_uriにトレイリングスラッシュがついてるのもそういう流れなのかもしれないですねえ。

webmasters.stackexchange.com

参考