谷本 心 in せろ部屋

はてなダイアリーから引っ越してきました

Dapr Advent Calendar 9日目 - DaprでOAuth2を使う

オーッス! Dapr Advent Calendar 9日目デーッス! 今日のテーマは OAuthッス! オーッス!

DaprでOAuth2による認可を行ってみよう

冒頭の元気な挨拶のことは忘れていただき、今回はDaprへのアクセスにOAuth 2.0による認可を行います。

f:id:cero-t:20211209070752p:plain
今回作るアプリケーション

DaprのOAuth対応についてのドキュメントはここにあります。

docs.dapr.io

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設定ファイルで使うので、分かりやすい名前にしておきます。

clientIdclientSecretGitHubで発行します。公式ドキュメントを参考にして、OAuth Appsを作成しました。

docs.github.com

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 は空で構いません。

authURLtokenURLGitHubのドキュメントに書かれているものを使います。

docs.github.com

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.namehandlers.type には、それぞれコンポーネント設定ファイルで記載した metadata.namespec.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のログインフォームが表示されます。

f:id:cero-t:20211209061716p:plain:h640
GitHubのログイン画面

さらにログイン後には認可を求める確認画面が表示されます。

f:id:cero-t:20211209061737p:plain:h480
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 へのアクセスなどが表示されるはずです。

f:id:cero-t:20211209064047p:plain
ネットワークタブ

この 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を使うほうが良いのでしょうね。

その理解が合っているかどうかあまり確信はないので、教えて詳しい人! という感じです。

そんなわけで、また明日!