go で雑に作った API に認証を付けるまでの道のりを残す

前提
-
gorilla/mux
で作った Web API 的なものがすでにある - auth0 使いたい(AWSのIAMとかあるじゃん的な話はしない)
経緯
気持ちとしては、単純に API に認証を付けたいなら AWS に乗っかってしまえばたぶん IAM グループでうんぬんかんぬんみたいなのが出来るんだろうなーと思っている。(前職ではそうしているっぽかった、という雰囲気の理解でしかないが)
ただ、今回はとにかく API デプロイして遊びたいというより、ベーシックなバックエンドの勉強みたいなものをしたいという漠然とした気持ちがあるので、 Docker コンテナと場合によっては k8s マニフェストを書いて手元で動かしたりマネージド k8s にデプロイしたりくらいにして、クラウドサービスを意識しないようなコードを書きたい気持ちがある。
なので認証周りは auth0 にして(これもまあサービスロックインではあるが)デプロイ先はあまり選ばないような構成にしたいと考えている。
今日やったこと
このあたり見ながらとりあえず auth0 いじったりしてた
どうやら app 作成時に Regular Web Application
を選択すると quick start が
Configure OAuth2 and OpenID Connect packages
みたいな方式で認証するようなコードが案内されて、 Machine to Machine
を選択すると Bearer
token を付与したHTTPリクエストをしてトークン認証するような認証をするようなコードが案内される模様。
個人的には API トークン認証(この単語選びが適切かどうかも良くわかっていない)でいいかなあと思ってコード書いてたけど、いまいち上手くいかないし、そもそもどういうことを自分がやろうとしているのかも理解していないので、よろしくないと感じている。
TODO
- auth0公式が公開してたJWTハンドブック を読む
-
アクセストークン認証について(と
Bearer
認証(?))について調べる

認証について
JWT ハンドブックの2.1で
いわゆるステートレス セッションは、実際にはクライアント側のデータにすぎません。
とかいい出すので調べてみると、以下の記事とかに行きついた。
で、そのあたりから掘っていくとどうやら、従来の方式として
なんらかの方法で認証を行ったあと、その認証を行ったセッションにたいしてセッションID(?)を払い出して、それをCookieで管理(?)する
というのがよくある認証と、その状態管理だったっぽい。
補足
ググってると「ステートフル認証」とか「Cookie認証」という単語が見受けられるけど、たぶんだけどそういう単語はどこかのコミュニティが定義している単語ではないっぽい(私の調査不足の可能性はある)
認証方式の種類について
MDNのHTTP認証についてのページが信頼できそう
「OAuth認証」というものはないと言われがちなことについて
上記のMDNのページからリンクされているIANAのドキュメントAuthentication Scheme Name
に OAuth
って書いてあるじゃんって思った。
それについて調べると、どうやら OAuth 1.0 と OAuth 2.0 はあんまり連続性が無いっぽい。
RFC 6749 (OAuth 2.0) の Abstract には下記の引用にあるように、「この仕様は OAuth 1.0 を置き換え、廃止するものである」と書いてあります。
んで、 OAuth 1.0
とかでググると、以下の Twitter API のページが出てくるんだけど
ここで以下のような区別をしてる
ベアラー(Bearer)は上記のMDNのHTTP認証のページにもある通り
OAuth 2.0 で保護されたリソースにアクセスするベアラトークンです。
なんだけど、それと並列に OAuth 1.0a
と書いてあるので、たぶんだけど OAuth 1.0 (もしくは OAuth 1.0a ?)では認証も含んだ仕様だったぽい。
んで、たぶん OAuth1.0 が出始めたときにそこを混同した人がいて、それに対して OAuth 2.0 を理解している人が「OAuth (2.0)は認可について定義をしているから OAuth 2.0 なんてものはないが???」となってるのが現在のインターネットなのかなって所感。
一応言っておくけど、もちろん OAuth 2.0 は認可についての仕様だし、 OAuth 2.0 で保護されたリソースにアクセスするための認証は Bearer 認証をもちいるもんっぽいことは理解しています。
現在の理解まとめ
たぶん OAuth が出てくる前は Basic 認証なりなんなりを使って認証したあと、その状態をいろんな方法を使って保持してたっぽい。たぶん代表的なプラクティスは Cookie の利用。
でもセキュリティ上いろいろつらみがあったっぽくて、そのあたりイイカンジにしようよってなったのが OAuth っぽい。
んで OAuth 2.0 として標準化されたときは
- 認証は Bearer 認証で行う
- 認可は OAuth 2.0 の仕様に沿って行う
って概念になるっぽい。

OAuth 2.0 について
以下の記事あたりを読んだ
なんか上記の図だと OAuth 2.0 が認証についても定義しそうに見えるけど、たぶんそれはしていない。(いろんなひとが「 OAuth 2.0 は認可フローであって認証ではない」ってめちゃくちゃ言ってるからそうなんだろうなって感じで、ソースはない)
たぶんだけど世の中の認証基盤まわりのサービスの大概が認証についても面倒を見てしまっているから上記のような説明になってて、たぶん認可エンドポイントにアクセスしたあとに「認証については別のところで面倒見てね」ってことに仕様ではなってるんだと思う。たぶん。
で、まあ現状理解としては「安全にリソースにアクセスできるようにする(認可する)にはどうしたらいいかについてのフローを標準化しました」ってのがOAuth2.0という理解をした。
ちなみに、OAuth 2.0 の仕様では、上記フローでやりとりされるアクセストークンの実装方法は認可サーバーの実装依存らしい。
OAuth のアクセストークン※1の実装方法は認可サーバーの実装依存です。
Bearer 認証完全理解した(大嘘)
OAuth 2.0 の RFC は RFC6749 なんだけど
Bearer Token Usage の RFC は RFC6750 で、
OAuth 2.0の保護リソースへアクセスするために, 署名無しトークンをHTTPリクエスト中でどのように利用するか記述したものである.
って書いてある。だから Baerer トークン(での認証)はトークンをHTTPリクエストでどう扱うかを標準化したものっぽい。でもまあざっくり理解としては Authorization
ヘッダに Bearer <token>
って含めるってことっしょ?って感じ。
日本語訳の引用で恐縮だけど、以下の説明がめちゃくちゃわかりやすかった。
トークンを所有する任意のパーティ (持参人 = bearer) は, 「トークンを所有している」という条件を満たしさえすればそのトークンを利用することができる. 署名無しトークンを利用する際, 持参人は, 暗号鍵の所持を証明 (proof-of-posession) するよう要求されない.
JWT 完全理解した(大嘘)
上記 RFC6750 で
署名無しトークンを
って言ってたけど、じゃあ署名するにはどうしたらいいかってことで登場するのが JWT っぽい。
JWTは署名とか暗号化とかしたデータをイイカンジに扱うためのデータの標準仕様。JSON形式。
JWTの仕様としては別に署名しないといけないわけではないんだけど、 Auth0 が配布してた JWT ハンドブックに
しかし実際には、セキュリティ保護のない JWT はほとんど使⽤されません。
って書いてあるので、たぶん利用シーンはあんまりない。
ざっくりまとめ
たぶん昨今の認証・認可フローとしては基本のひな型として
JWT の仕様で暗号化されたアクセストークンを Baerer トークンの仕様でHTTPでやりとりして認証し OAuth 2.0 の仕様通りに認可する
ってのがある。ここまでが OAuth 2.0 のRFCとその関連で標準化されている範囲。
そこに必要だったらID・PW認証なり多要素認証を乗っけて下さいね~って感じなんだけど Bearer 認証は
トークンを所有する任意のパーティ (持参人 = bearer) は, 「トークンを所有している」という条件を満たしさえすればそのトークンを利用することができる. 署名無しトークンを利用する際, 持参人は, 暗号鍵の所持を証明 (proof-of-posession) するよう要求されない.
って感じだからユルめだし、別途ID・PW認証とか多要素認証も実装しないといけない場合がおおい。すごく大変。だから IDaaS とか言われてるアイデンティティ管理サービスがあるんだろうなーって感じた。

もう一度 Auth0 のドキュメントを読む
を見ると Client Credentials Flow の項に
With machine-to-machine (M2M) applications, such as CLIs, daemons, or services running on your back-end, the system authenticates and authorizes the app rather than a user.
とあるので、単純にAPIを使うマシンを認証する場合は Client Credentials Flow で良さそうな気がした。
Client Credentials Flow の詳細を見ると
にいろいろ詳細が書いてあって、ざっくりだけど
- アプリからAuth0 テナントの
/oauth/token
エンドポイントにリクエストをする。このリクエストにクライアントIDとクライアントシークレットを付与することで、 Auth0 認証サーバーはこのアプリを認証する。
Your app authenticates with the Auth0 Authorization Server using its Client ID and Client Secret (/oauth/token endpoint).
- Auth0 認可サーバーがクライアントIDとクライアントシークレットをバリデーションする
Your Auth0 Authorization Server validates the Client ID and Client Secret.
- Auth0 認可サーバーがアクセストークンを含むレスポンスをアプリに返す
Your Auth0 Authorization Server responds with an Access Token.
- OAuth 2.0 の仕様に沿って保護されたAPIに、これまでに取得したアクセストークンを用いてリクエストをする
Your application can use the Access Token to call an API on behalf of itself.
- API はそのアクセストークンを解析し問題が無ければリクエストされたデータを返す
The API responds with requested data.
という感じのフローだという理解をした。
API でのアクセストークンの解析はどうするの?
以下のページへのリンクがあったので踏んだ
とりあえず Go のコードを読む
Create a middleware to validate Access Tokens の段落を読む
正直詳しいことはよくわからないが、独自バリデート関数とか定義したり、たぶん Auth0 に指定した公開鍵とかも取得してきていて署名の検証とか復号(?)とかしてるっぽい。
そのあたりは jwtmiddleware
にいいかんじに隠ぺいされている雰囲気なので、とりあえず変数部分を置き換えたらいいように作られてそうだなーという所感。

auth0 をもちいた API 側でのアクセストークンの解析のコードを書こうとした
を信用してコード書いてみたけど
github.com/auth0/go-jwt-middleware/validate/jwt-go
がたぶん今は存在しなくて、そんなものは go get
できんって怒られて大変つらくなる。
仕方がないので、 auth0.com
の内容は無視して github の README を信用することとした。

色々探してたらgo-jwt-middleware/v2
を使った例があった。
まずは上記リポジトリを clone してみたらイイカンジに動いたので、手元にも実装をまねしてみたら動いた。
もろもろ動作確認もできたのでPR作った。

アクセストークンなしで curl したとき
$ curl --request GET \
--url http:/localhost:10000/users/1
{"message":"Failed to validate JWT."}%
アクセストークンありで curl したとき
$ curl --request GET \
> --url http:/localhost:10000/users/1 \
> --header 'authorization: Bearer ACCESS_TOKEN'
{"Value":{"ID":1,"CreatedAt":"2022-01-07T02:23:52+09:00","UpdatedAt":"2022-01-07T02:23:52+09:00","DeletedAt":null,"Name":"Create test name","Email":"Create test email"},"Error":null,"RowsAffected":1}%
という感じに動作確認できたので、スクラップを close する。