Dapr Advent Calendar 9日目 - DaprでOAuth2を使う
オーッス! Dapr Advent Calendar 9日目デーッス! 今日のテーマは OAuthッス! オーッス!
DaprでOAuth2による認可を行ってみよう
冒頭の元気な挨拶のことは忘れていただき、今回はDaprへのアクセスにOAuth 2.0による認可を行います。
DaprのOAuth対応についてのドキュメントはここにあります。
DaprではOAuthサーバとしてTwitterやSlack、Azure AD、Google APIsなどを利用することができます。今回はGitHubをOAuthサーバとして利用します。
今回作成するアプリケーションのソースコードはgithubに置いてあります。
https://github.com/cero-t/dapr-advent-2021/tree/main/oauth
僕はセキュリティにあまり詳しくないので、OAuthを認証代わりに使うのはおかしいとか、説明が間違ってるだとか色々あるかも知れませんので、何かあればTwitterなどでお知らせください。
OAuthを使うアプリケーションの作成
コントローラークラスの作成
まずはWebアプリケーションの作成です。別にアプリケーション自体は特に何でも構わないので、Hello World程度にしておきます。
OAuthController.java
@RestController public class OAuthController { @GetMapping("/hello") public Map<String, String> hello() { return Map.of("message", "Hello, world!"); } @GetMapping("/token") public Map<String, ?> token(@RequestHeader("x-oauth-token") String oauthToken) { return Map.of("x-oauth-token", oauthToken); } }
Hello Worldとは別に、リクエストの x-oauth-token
ヘッダをそのまま返すAPIも追加しました。このヘッダについてはDaprの設定ファイルのところで説明します。
ポート番号をアプリケーション設定ファイルで指定
続いて、設定ファイルでアプリケーションの起動ポートを指定します。
application.properties
server.port=8088
このアプリケーションは8088番ポートを利用します。
GitHubのOAuthを利用するためのDapr設定ファイルの作成
GitHubのOAuthを利用するためのDaprのコンポーネント設定ファイルを作成します。
components/oauth.yaml
apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: oauth2 spec: type: middleware.http.oauth2 metadata: - name: clientId value: "***" - name: clientSecret value: "***" - name: scopes value: "" - name: authURL value: "https://github.com/login/oauth/authorize" - name: tokenURL value: "https://github.com/login/oauth/access_token" - name: redirectURL value: "http://localhost:18088" - name: authHeaderName value: "x-oauth-token"
metadata.name
は、ソースコードでは使いませんが、また別のDapr設定ファイルで使うので、分かりやすい名前にしておきます。
clientId
と clientSecret
はGitHubで発行します。公式ドキュメントを参考にして、OAuth Appsを作成しました。
OAuth Appを作成する際の「Homepage URL」の「Authorization callback URL」は、いずれも http://localhost にしました。コールバックURLには実際にはポート番号が入るのですが、GitHub側のOAuthではポート番号は省略しても大丈夫なようです。
OAuth Appを作成すると「Client ID」が発行されるので、これを clientId
に指定します。また「Client secrets」にある「Generate a new client secret」ボタンを押すと「Client secret」が発行されるので、これを clientSecret
に指定します。
scopes
は空で構いません。
authURL
と tokenURL
はGitHubのドキュメントに書かれているものを使います。
redirectURL
は、GitHub側での認可が終わったときに戻ってくるアプリケーション側のURLです。認可後にはDaprに戻ってくる必要があるため、Daprのポート番号として利用予定の18088番ポートを指定したURLにしました。
authHeaderName
は、GitHubで発行されたトークンをDaprからアプリケーション側に渡す際のHTTPヘッダ名です。x-oauth-token
を指定しています。上で作ったコントローラークラスは、この値をそのまま返しているわけです。
public Map<String, ?> token(@RequestHeader("x-oauth-token") String oauthToken) { return Map.of("x-oauth-token", oauthToken); }
少し長くなりましたが、これでDaprのコンポーネント設定ファイルができました。
OAuthを行うためのパイプライン設定ファイルの作成
次に、OAuthを有効にするためにDaprのパイプライン設定ファイルを作成します。このファイルは前回の分散トレーシングを設定する際にも利用したもので、Dapr内部でフィルタのように動作する「Middleware」の設定を行うためのファイルです。
次のような設定を行います。
config.yaml
apiVersion: dapr.io/v1alpha1 kind: Configuration metadata: name: appconfig spec: httpPipeline: handlers: - name: oauth2 type: middleware.http.oauth2
metadata.name
はこの設定の名称です。任意の名前で構いません。
spec.httpPipeline
はDaprを通過するHTTPリクエストに対して行う処理のパイプラインを記載するもので、handlers
には複数のMiddlewareコンポーネントを列挙することができます。今回はOauth2の設定のみを記述しました。
handlers.name
と handlers.type
には、それぞれコンポーネント設定ファイルで記載した metadata.name
と spec.type
の値を指定します。nameだけ列挙すれば良いような気もするのですが、typeもあわせて列挙する仕様となっています。
これでパイプライン設定ファイルの作成は完了です。
Daprを使ったアプリケーションの起動
それではDaprを使ってアプリケーションを起動します。
dapr run --app-id oauth-app --app-port 8088 --dapr-http-port 18088 --components-path ./components --config config.yaml ../mvnw spring-boot:run
Daprのコマンド引数に --config config.yaml
を追加して、新規に作成したパイプライン設定ファイルが有効になるようにしています。
ブラウザからアクセスして確認
アプリケーションが起動したら、ブラウザで次のURLにアクセスします。
http://localhost:18088/v1.0/invoke/oauth-app/method/hello
GitHubに未ログインの場合は、GitHubのログインフォームが表示されます。
さらにログイン後には認可を求める確認画面が表示されます。
ここで「Authorize」を選択すると、作成したアプリケーションに戻り、次のJSONが表示されるはずです。
{ "message": "Hello, world!" }
期待通り、Hello, worldのメッセージを取得できました。
ブラウザからアクセスしてトークンを確認
もう一つのエンドポイントにもアクセスしてみましょう。ブラウザで次のURLにアクセスします。
http://localhost:18088/v1.0/invoke/oauth-app/method/token
次のようなJSONが表示されます。
{ "x-oauth-token": "Bearer gh*_********" }
これはGitHubにアクセスする際に必要となるトークンです。これをどう使うかは後で説明するとして、ひとまずGitHubのOAuthが成功してトークンを取得できたことが分かりました。
curlコマンドでアクセス
続いて、curlコマンドでもアクセスしてみましょう。
curl -i localhost:18088/v1.0/invoke/oauth-app/method/hello
次のようなレスポンスが返ってきます。
HTTP/1.1 302 Found Server: fasthttp Date: Wed, 09 Dec 2021 00:00:00 GMT Content-Length: 0 Location: https://github.com/login/oauth/authorize?access_type=offline&client_id=********&redirect_uri=http%3A%2F%2Flocalhost%3A18088&response_type=code&scope=&state=******** Traceparent: 00-0147c54031ce7f17b09f38840c8ec3be-7a79984201eba8b1-00 Set-Cookie: gosessionsid=********; expires=Wed, 09 Dec 2021 02:00:00 GMT; path=/; HttpOnly
GitHubのログイン画面に対するリダイレクトのレスポンスです。このリダイレクトに従って、GitHubのログイン画面に遷移しているわけです。
ちなみに、クッキーに gosessionsid
というセッションIDを保存しているようですね。
また、Daprではなく、アプリケーションに直接アクセスした場合はどうなるかも確認しましょう。次のコマンドでアクセスしてください。
curl localhost:8088/hello
次のメッセージが取得できたはずです。
{"message":"Hello, world!"}
OAuthも何もなくアクセスできてしまいました。
OAuthはあくまでもDaprの設定で行っているものであり、Webアプリケーション側に直接アクセスした場合は何の確認もなくアクセスできるのです。その辺りをどう設計すべきか、どうやってアクセスするルートを絞るかなどは別のテーマとなるため、ここでは説明を割愛したいと思います。
トークンを使ってGitHubにアクセスする
ここまでで、アクセス時にGitHub側での認証と認可を行い、アプリケーション側でGitHubのトークンを手に入れることができました。ただ、アクセスしたユーザが誰かという情報はありません。もしOAuthを認証代わりに利用するなら、アクセスしてきたのが誰かということを知る必要があります。その実装をしてみましょう。
※そもそもそれならOpenID Connectを使う方が良いのでしょうけど、Dapr単体でOpenID Connectを使って外部のサーバとID連携するのが難しかったので、OAuthを使いました。
コントローラークラスにGitHubにアクセスするメソッドを追加
GitHubで発行されたトークンは、リクエストヘッダの x-oauth-token
でアプリケーションに渡されます。このトークンを使えばGitHubにアクセスすることができるので、GitHubにアクセスするメソッドをコントローラークラスに追加しましょう。
OAuthController.java(抜粋)
@Value("https://api.github.com/user") private String userApi; @GetMapping("/user") public Map<?, ?> user(@RequestHeader("x-oauth-token") String oauthToken) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.set("Authorization", oauthToken); HttpEntity<?> request = new HttpEntity<>(httpHeaders); return restTemplate.exchange(userApi, HttpMethod.GET, request, Map.class).getBody(); }
GitHubのUser APIにアクセスしてユーザ情報を取得する処理を追加しました。Authorization
ヘッダに x-oauth-token
のトークンをそのまま渡しています。
アプリケーションの再起動
アプリケーションをいったん止めて、再起動します。
dapr run --app-id oauth-app --app-port 8088 --dapr-http-port 18088 --components-path ./components --config config.yaml ../mvnw spring-boot:run
アプリケーションが起動したら、次のURLにブラウザでアクセスします。
http://localhost:18088/v1.0/invoke/oauth-app/method/user
次のようなJSONが表示されるはずです。
{ "login": "cero-t", "id": 1438519, "node_id": "****", "avatar_url": "https://avatars.githubusercontent.com/u/1438519?v=4", "gravatar_id": "", "url": "https://api.github.com/users/cero-t", "html_url": "https://github.com/cero-t", "followers_url": "https://api.github.com/users/cero-t/followers", "following_url": "https://api.github.com/users/cero-t/following{/other_user}", "gists_url": "https://api.github.com/users/cero-t/gists{/gist_id}", "starred_url": "https://api.github.com/users/cero-t/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/cero-t/subscriptions", "organizations_url": "https://api.github.com/users/cero-t/orgs", "repos_url": "https://api.github.com/users/cero-t/repos", "events_url": "https://api.github.com/users/cero-t/events{/privacy}", "received_events_url": "https://api.github.com/users/cero-t/received_events", "type": "User", "site_admin": false, "name": "Shin Tanimoto", "company": null, "blog": "https://cero-t.hatenadiary.jp/", "location": null, "email": null, "hireable": null, "bio": null, "twitter_username": "cero_t", "public_repos": 44, "public_gists": 4, "followers": 54, "following": 0, "created_at": "2012-02-15T02:14:10Z", "updated_at": "2021-09-22T04:04:10Z" }
GitHubのユーザ情報が取得できました。これでアプリケーション側で、アクセスしてきたユーザが誰なのかを把握することができますね。
ブラウザはDaprに何を送っているのか?
ところで、Daprにアクセスする際に、ブラウザはどういう情報をDaprに送っているのでしょうか。GitHubのトークンを毎回送っているはずもないので、最後にそれを確認しておきましょう。
まずブラウザの開発者ツールを表示します。たいていキーボードのF12とか、Alt + Command + Iとかで開きます。
開発者ツールを開いたら、次のURLにアクセスします。
http://localhost:18088/v1.0/invoke/oauth-app/method/hello
開発者ツールのネットワークタブに hello
へのアクセスなどが表示されるはずです。
この hello
の詳細を開き、下の方にある Request Headers
を見てください。
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate, br Accept-Language: ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7 Cache-Control: max-age=0 Connection: keep-alive Cookie: gosessionsid=******** Host: localhost:18088 sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS" Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: none Sec-Fetch-User: ?1 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36
ここで Cookie: gosessionsid=
にある通り、クッキーに入ったセッションIDをDaprに送っていることが分かります。Daprがログイン状態をセッションに保存しているのだろうと推測できます。
むろんブラウザがDaprにアクセスする際に x-oauth-token
に入っていたようなGitHubのアクセストークンを毎回送るというのも変な話ですから、このようなセッションIDを使うのも当然ですよね。もしセッションではなく、トークンなどを使ってステートレスに認証認可を行いたいのであれば、OpenID ConnectのJWTトークンを使うべきなのでしょうね。
まとめ
- Daprを使ってOAuthの認可をすることができます
- 主要なクラウドサービスのOAuthサーバを利用することができます
- コンポーネント設定ファイルに利用するOAuthサーバのClientIDなどを記述します
- パイプライン設定ファイルにそのOAuthコンポーネント設定を利用することを記述します
- OAuthサーバにアクセスするためのトークンを、アプリケーションのHTTPヘッダで受け取ることができます
- アプリケーションとDapr間でセッションを使って認証認可の状態を維持しているようです
正直、OAuthとOpenID Connectの違いもよく分かってなかったのですが、Daprでいろいろ試してみて、OAuthはあくまでもOAuthサーバ(今回であればGitHub)のAPIを利用するために使うものなんだなと理解しました。一方、OpenID Connectであれば認証/認可後にJWTトークンを用いるため、認証状態をステートレスに維持できますし、このJWTトークンからユーザ情報も取得できるため、目的がアプリケーションの認証なのであればOpenID Connectを使うほうが良いのでしょうね。
その理解が合っているかどうかあまり確信はないので、教えて詳しい人! という感じです。
そんなわけで、また明日!