Closed7

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

mirko-sanmirko-san

前提

  • gorilla/mux で作った Web API 的なものがすでにある
  • auth0 使いたい(AWSのIAMとかあるじゃん的な話はしない)

経緯

気持ちとしては、単純に API に認証を付けたいなら AWS に乗っかってしまえばたぶん IAM グループでうんぬんかんぬんみたいなのが出来るんだろうなーと思っている。(前職ではそうしているっぽかった、という雰囲気の理解でしかないが)

ただ、今回はとにかく API デプロイして遊びたいというより、ベーシックなバックエンドの勉強みたいなものをしたいという漠然とした気持ちがあるので、 Docker コンテナと場合によっては k8s マニフェストを書いて手元で動かしたりマネージド k8s にデプロイしたりくらいにして、クラウドサービスを意識しないようなコードを書きたい気持ちがある。

なので認証周りは auth0 にして(これもまあサービスロックインではあるが)デプロイ先はあまり選ばないような構成にしたいと考えている。

今日やったこと

このあたり見ながらとりあえず auth0 いじったりしてた
https://dev.classmethod.jp/articles/api_authorization_using_m2m_application/

どうやら app 作成時に Regular Web Application を選択すると quick start が

Configure OAuth2 and OpenID Connect packages

みたいな方式で認証するようなコードが案内されて、 Machine to Machine を選択すると Bearer token を付与したHTTPリクエストをしてトークン認証するような認証をするようなコードが案内される模様。

個人的には API トークン認証(この単語選びが適切かどうかも良くわかっていない)でいいかなあと思ってコード書いてたけど、いまいち上手くいかないし、そもそもどういうことを自分がやろうとしているのかも理解していないので、よろしくないと感じている。

TODO

mirko-sanmirko-san

認証について

JWT ハンドブックの2.1で

いわゆるステートレス セッションは、実際にはクライアント側のデータにすぎません。

とかいい出すので調べてみると、以下の記事とかに行きついた。
https://zenn.dev/kaoru6strings/articles/7496bdf018300f
https://qiita.com/mogulla3/items/189c99c87a0fc827520e

で、そのあたりから掘っていくとどうやら、従来の方式として
なんらかの方法で認証を行ったあと、その認証を行ったセッションにたいしてセッションID(?)を払い出して、それをCookieで管理(?)する
というのがよくある認証と、その状態管理だったっぽい。

補足

ググってると「ステートフル認証」とか「Cookie認証」という単語が見受けられるけど、たぶんだけどそういう単語はどこかのコミュニティが定義している単語ではないっぽい(私の調査不足の可能性はある)

認証方式の種類について

MDNのHTTP認証についてのページが信頼できそう
https://developer.mozilla.org/ja/docs/Web/HTTP/Authentication#authentication_schemes

「OAuth認証」というものはないと言われがちなことについて

上記のMDNのページからリンクされているIANAのドキュメント
https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml
Authentication Scheme NameOAuth って書いてあるじゃんって思った。

それについて調べると、どうやら OAuth 1.0 と OAuth 2.0 はあんまり連続性が無いっぽい。

https://qiita.com/TakahikoKawasaki/items/3600b28af7b63671b968#3-oauth-の未来

RFC 6749 (OAuth 2.0) の Abstract には下記の引用にあるように、「この仕様は OAuth 1.0 を置き換え、廃止するものである」と書いてあります。

んで、 OAuth 1.0 とかでググると、以下の Twitter API のページが出てくるんだけど
https://developer.twitter.com/ja/docs/authentication/oauth-1-0a

ここで以下のような区別をしてる

ベアラー(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 の仕様に沿って行う

って概念になるっぽい。

mirko-sanmirko-san

OAuth 2.0 について

以下の記事あたりを読んだ
https://zenn.dev/zaki_yama/articles/oauth2-authorization-code-grant-and-pkce
https://qiita.com/TakahikoKawasaki/items/200951e5b5929f840a1f

なんか上記の図だと OAuth 2.0 が認証についても定義しそうに見えるけど、たぶんそれはしていない。(いろんなひとが「 OAuth 2.0 は認可フローであって認証ではない」ってめちゃくちゃ言ってるからそうなんだろうなって感じで、ソースはない)

たぶんだけど世の中の認証基盤まわりのサービスの大概が認証についても面倒を見てしまっているから上記のような説明になってて、たぶん認可エンドポイントにアクセスしたあとに「認証については別のところで面倒見てね」ってことに仕様ではなってるんだと思う。たぶん。

で、まあ現状理解としては「安全にリソースにアクセスできるようにする(認可する)にはどうしたらいいかについてのフローを標準化しました」ってのがOAuth2.0という理解をした。

ちなみに、OAuth 2.0 の仕様では、上記フローでやりとりされるアクセストークンの実装方法は認可サーバーの実装依存らしい。
https://qiita.com/TakahikoKawasaki/items/970548727761f9e02bcd

OAuth のアクセストークン※1の実装方法は認可サーバーの実装依存です。

Bearer 認証完全理解した(大嘘)

OAuth 2.0 の RFC は RFC6749 なんだけど
https://openid-foundation-japan.github.io/rfc6749.ja.html

Bearer Token Usage の RFC は RFC6750 で、
http://openid-foundation-japan.github.io/rfc6750.ja.html

OAuth 2.0の保護リソースへアクセスするために, 署名無しトークンをHTTPリクエスト中でどのように利用するか記述したものである.

って書いてある。だから Baerer トークン(での認証)はトークンをHTTPリクエストでどう扱うかを標準化したものっぽい。でもまあざっくり理解としては Authorization ヘッダに Bearer <token> って含めるってことっしょ?って感じ。

日本語訳の引用で恐縮だけど、以下の説明がめちゃくちゃわかりやすかった。

トークンを所有する任意のパーティ (持参人 = bearer) は, 「トークンを所有している」という条件を満たしさえすればそのトークンを利用することができる. 署名無しトークンを利用する際, 持参人は, 暗号鍵の所持を証明 (proof-of-posession) するよう要求されない.

JWT 完全理解した(大嘘)

上記 RFC6750 で

署名無しトークンを

って言ってたけど、じゃあ署名するにはどうしたらいいかってことで登場するのが JWT っぽい。
JWTは署名とか暗号化とかしたデータをイイカンジに扱うためのデータの標準仕様。JSON形式。

JWTの仕様としては別に署名しないといけないわけではないんだけど、 Auth0 が配布してた JWT ハンドブックに

しかし実際には、セキュリティ保護のない JWT はほとんど使⽤されません。

って書いてあるので、たぶん利用シーンはあんまりない。

https://auth0.com/resources/ebooks/jp-jwt-handbook

ざっくりまとめ

たぶん昨今の認証・認可フローとしては基本のひな型として
JWT の仕様で暗号化されたアクセストークンを Baerer トークンの仕様でHTTPでやりとりして認証し OAuth 2.0 の仕様通りに認可する
ってのがある。ここまでが OAuth 2.0 のRFCとその関連で標準化されている範囲。
そこに必要だったらID・PW認証なり多要素認証を乗っけて下さいね~って感じなんだけど Bearer 認証は

トークンを所有する任意のパーティ (持参人 = bearer) は, 「トークンを所有している」という条件を満たしさえすればそのトークンを利用することができる. 署名無しトークンを利用する際, 持参人は, 暗号鍵の所持を証明 (proof-of-posession) するよう要求されない.

って感じだからユルめだし、別途ID・PW認証とか多要素認証も実装しないといけない場合がおおい。すごく大変。だから IDaaS とか言われてるアイデンティティ管理サービスがあるんだろうなーって感じた。

mirko-sanmirko-san

もう一度 Auth0 のドキュメントを読む

https://auth0.com/docs/authorization/flows
を見ると 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 の詳細を見ると
https://auth0.com/docs/authorization/flows/client-credentials-flow

にいろいろ詳細が書いてあって、ざっくりだけど

  1. アプリからAuth0 テナントの /oauth/token エンドポイントにリクエストをする。このリクエストにクライアントIDとクライアントシークレットを付与することで、 Auth0 認証サーバーはこのアプリを認証する。

Your app authenticates with the Auth0 Authorization Server using its Client ID and Client Secret (/oauth/token endpoint).

  1. Auth0 認可サーバーがクライアントIDとクライアントシークレットをバリデーションする

Your Auth0 Authorization Server validates the Client ID and Client Secret.

  1. Auth0 認可サーバーがアクセストークンを含むレスポンスをアプリに返す

Your Auth0 Authorization Server responds with an Access Token.

  1. OAuth 2.0 の仕様に沿って保護されたAPIに、これまでに取得したアクセストークンを用いてリクエストをする

Your application can use the Access Token to call an API on behalf of itself.

  1. API はそのアクセストークンを解析し問題が無ければリクエストされたデータを返す

The API responds with requested data.

という感じのフローだという理解をした。

API でのアクセストークンの解析はどうするの?

以下のページへのリンクがあったので踏んだ
https://auth0.com/docs/quickstart/backend

とりあえず Go のコードを読む

https://auth0.com/docs/quickstart/backend/golang

Create a middleware to validate Access Tokens の段落を読む
https://auth0.com/docs/quickstart/backend/golang#create-a-middleware-to-validate-access-tokens

正直詳しいことはよくわからないが、独自バリデート関数とか定義したり、たぶん Auth0 に指定した公開鍵とかも取得してきていて署名の検証とか復号(?)とかしてるっぽい。
そのあたりは jwtmiddleware にいいかんじに隠ぺいされている雰囲気なので、とりあえず変数部分を置き換えたらいいように作られてそうだなーという所感。

mirko-sanmirko-san

auth0 をもちいた API 側でのアクセストークンの解析のコードを書こうとした

https://auth0.com/docs/quickstart/backend/golang#configure-auth0-apis
を信用してコード書いてみたけど

github.com/auth0/go-jwt-middleware/validate/jwt-go

がたぶん今は存在しなくて、そんなものは go get できんって怒られて大変つらくなる。

仕方がないので、 auth0.com の内容は無視して github の README を信用することとした。

mirko-sanmirko-san

アクセストークンなしで 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 する。

このスクラップは2022/01/07にクローズされました