Basic認証とBearer認証を作ってみる
トークンベースの認証も作ってみる。
公開鍵認証とか、そこら辺もまとめる
Basic認証をまずは実装してみる。
Basic認証やDigest認証、トークンベース認証は、HTTPに元々用意されている認証機能である。
これらの認証(HTTP認証)は、HTTPがステートレスなプロトコルなので、ステートレスな認証方式であると言える。
Basic認証は、認証が必要なページにリクエストがあると、一旦401 Unauthorized(認証が必要なのにされていない)というステータスを返す。これを受けるとブラウザはIDとパスワードの入力画面を自動で表示する。入力されたIDとパスワードを追加したリクエストを改めてサーバーに送る。
401が返されるときに、 WWW-Authenticate ヘッダーも送られて、このヘッダーのvalueに認証方法が指定されている。
Basic認証では、ユーザ名とパスワードの組みをコロン ":" でつなぎ、Base64でエンコードして送信する。このため、盗聴や改竄が簡単であるという欠点を持つ
なるほど。
basic認証って既にサーバー側で登録されているユーザーにしか対応できないやつだね。
WWW-Authenticate: <auth-scheme>
WWW-Authenticate: <auth-scheme> realm=<realm>
auth-schemeは認証方式を指定する。realmは、通常、クライアントに対してどの領域(リソースやサーバー)に対する認証が必要かを示すもの。
realm(レルム)は領域や範囲という意味。
とりあえずできた。
一度Basic認証に成功すると、そのパス以下のリクエストにはブラウザが自動的にAuthorizationヘッダを付与してくれるそうです。私が今回実装した認証機能の場合、Basic認証成功後に全てのページにAuthororizationヘッダを付与するようになってしまったので、実装を見直す必要があるかもです。
Authorizaionヘッダをブラウザ自動で付与してくれるということは、リクエストごとにIDとパスワードが送信されていて、認証状態はどこにも保存されていないことを表す(フォーム認証の場合、認証状態がredisなどに保存されている)。つまり、Basic認証はステートレスである。Basic認証はステートレスなので、ログアウトという概念も存在しない。
一度ベーシック認証で認証に成功してしまえば、ユーザー名とパスワードはブラウザに記憶されるそうです。
ここまでで参考にした記事
Bearer認証後でやるか。
こいつを読む
Bearerトークン
Bearerトークンは、セキュリティトークンのうちその利用可否が「トークンの所有」のみで決定されるものである[1]。持参人トークン・署名なしトークンとも呼ばれる。
概要
セキュリティトークンとはある対象へのアクセス制御(利用可否など)を担うトークン・許可証であり、Bearerトークンはその一形態である。Bearerとは「持参人」すなわち「トークンを持ってきた存在」を意味する。BearerトークンはBearer(それを持ってきた存在)にアクセス権限を与える特性を持つ。
なるほど、Bearerトークンって、署名なしトークンとも呼ばれるのか。Bearerトークンでは、そのトークンを持ってきた存在(Bearer)にアクセス権限を与える特性を持っている。
Bearerトークンはしばしば切符に例えられる。切符は乗り物への乗車=アクセスを制御するトークンである。切符の利用権利は単純に「切符を持ってきた人=Bearer」に付与される。誰が切符を購入し管理していたかは関係がない。極端な例では拾った切符であっても(切符の権利者でなくても)持ってきた人=Bearerに乗車権利が付与される。このように切符はBearerトークンと同じ性質を持っている。
Bearerトークンの対比としてproof-of-possessionトークン(PoPトークン、「所有の証明」トークン、記名式トークン)が挙げられる。PoPトークンはトークンの所有に追加してトークン権利を所有することの証明を必要とする[1]。Bearerトークンは切符に例えられるが、PoPトークンは国際線飛行機チケットに例えられる。国際線飛行機はチケットを提示するだけではなく、チケットに記された氏名の確認、すなわち権利所有者であることの証明が必要である(パスポートを利用する)。Bearerトークンは単純にトークンの所有のみが求められる点でPoPトークンと異なる。
Bearerトークンは電車の切符に例えられる。切符は電車へのアクセスを制限するトークンであるとも言える。切符の利用権利は「切符を持ってきた人(Bearer)」に付与される。ゆえにBearerトークンは電車のきっぷに例えられる。拾った切符であっても、持ってきた人(Bearer)に乗車券りが付与される。
Bearerトークンと対極の存在として、PoPトークンでは、そのトークンを所有している人であることを証明しなければならない。PoPトークンは国際線飛行機のチケットに例えられる。
Bearerトークンを用いたWeb認証はBearer認証あるいはトークン認証と呼ばれる
こいつを読む
OAuthは, クライアントがアクセストークンを取得することで, 保護リソースへのアクセスを可能にする.
トークンはリソース所有者の承認を伴い, 認可サーバによってクライアントに対して発行される. クライアントはアクセストークンを, リソースサーバが持つ保護リソースにアクセスするために利用する.
OAuthって、クラアイント(ブラウザ)がアクセストークンを取得するから、保護リソースへアクセスできるのか。なるほど。
OAuthはリソースオーナーの代わりに保護リソースにアクセスする方法をクライアントに提供する. 一般的なケースにおいて, クライアントが保護リソースにアクセスする前に, クライアントは初めにリソースオーナーの認可を取得し, アクセス認可とアクセストークンを交換しなければならない. アクセストークンは, 与えられた権限の範囲, 期間とその他の属性を示している. クライアントはリソースサーバにアクセストークンを渡すことにより, 保護リソースにアクセスする. いくつかのケースにおいては, クライアントは, 最初にリソースオーナーから認可を得ずに認可サーバからアクセストークンを得るために, 自らのクレデンシャルを認可サーバに直接渡すこともある.
アクセストークンはほかの認証方法 (例: ユーザ名とパスワード, アサーション) をリソースサーバが理解しうる一つのトークンに置き換えるような抽象化を提供する. この抽象レイヤーは有効期間の短いアクセストークンを発行することを可能にするとともに, リソースサーバーが様々な認証スキームを理解しなくともよいようにする.
アクセストークンは他の認証方法をリソースサーバーが理解しうる1つのトークンに置き換えるような抽象化を提供するってすごく分かりやすい。
この文章が気になる。Githubログインとかあれどうなっているんやろうか。
いくつかのケースにおいては, クライアントは, 最初にリソースオーナーから認可を得ずに認可サーバからアクセストークンを得るために, 自らのクレデンシャルを認可サーバに直接渡すこともある.
Authorizationリクエストヘッダー
HTTP の Authorization リクエストヘッダーは、ユーザーエージェントがサーバーから認証を受けるための証明書を保持するヘッダーである。ふつうは、必ずではないが、サーバーが 401 Unauthorized ステータスと WWW-Authenticate ヘッダーを返した後に使われる。
Authenticated Requests(認証されたリクエスト)
リソースの要求において署名無しトークンをリソースサーバに送信する3つの方法を定義する。クライアントは一度のリクエストにおいて, トークンを送信する方法を複数同時に用いてはならない (MUST NOT).
Authorizationリクエストヘッダフィールド
- Authorizationリクエストヘッダフィールド中でアクセストークンを送信する場合, クライアントはBearer認証スキームを用いる。クライアントが署名無しトークンを伴う認証されたリクエストを送信する際には, Bearer HTTP認可スキームを用いたAuthorizationリクエストヘッダフィールドを使用すべきである (SHOULD)。リソースサーバはこの方法をサポートしなければならない (MUST)。
- トークンは以下の文法に従う。
b64token = 1*( ALPHA / DIGIT / "-" / "." / "_" / "‾" / "+" / "/" ) *"=" credentials = "Bearer" 1*SP b64token
Formエンコードされたボディパラメータ
- HTTPリクエストのエンティティボディ中でアクセストークンを送信する際, クライアントはaccess_tokenパラメータを用いてアクセストークンをリクエストボディに付加する. クライアントは以下の条件のすべてを満たさない限りこの方法を用いてはならない (MUST NOT):
- このapplication/x-www-form-urlencodedを用いる方法は, 関与しているブラウザがAuthorizationリクエストヘッダにアクセスできない場合を除いて使用すべきではない (SHOULD NOT). リソースサーバはこの方法をサポートしてもよい (MAY).
URIクエリパラメータ
HTTPリクエストURIの中でアクセストークンを送信する際, クライアントはaccess_tokenパラメータを用いてアクセストークンを"Uniform Resource Identifier (URI): Generic Syntax"[RFC3986]で定義されているURIクエリコンポーネントに追加する.
GET /resource?access_token=mF_9.B5f-4.1JqM HTTP/1.1
Host: server.example.com
URL中のアクセストークン値がログに記録される可能性が高いことなど, URIを利用した場合はセキュリティレベルが低下 (Section 5参照) するため, アクセストークンをAuthorizationリクエストヘッダかHTTPリクエストエンティティボディ中で送信することが不可能でない限りは, この方式を利用すべきではない (SHOULD NOT). リソースサーバはこの方式をサポートしても良い (MAY).
アクセストークン
アクセストークンは "The OAuth 2.0 Authorization Framework" [RFC6749] 中で「クライアントに対するアクセス認可の文字列表現」と定義されており, リソース所有者のクレデンシャルを直接利用することではない.
なるほど、アクセストークンって、クラアイントに対するアクセス認可の文字列表現ってことか。
署名なしトークンをリソースサーバに送るなら、Authorizationヘッダーを使った方が安全かも。
フォームはそもそも条件を満たすのがめんどいし、URIクエリパラメータに関しては、履歴にトークン情報が残っちゃうし。
フレームワークやライブラリに振り回されたくないな。自分がこうしたいからこうするって開発スタイルの方が楽しいはず。
WWW-Authenticate レスポンスヘッダフィールド
保護リソースへのリクエストが, 認証クレデンシャルを含んでいない, または保護リソースへアクセスすることができるアクセストークンを含んでいない場合, リソースサーバはHTTP WWW-Authenticate レスポンスヘッダフィールドを含めなければならない
保護リソースへのリクエストにアクセストークンが含まれているが認証に失敗した場合, リソースサーバはアクセス要求を拒否した理由をクライアントに対して示すために, error 属性を含むべきである (SHOULD). パラメータの値は Section 3.1 に記載されている. さらに, リソースサーバは, 開発者にとって可読性のある説明を提供するために, error_description 属性を含んでもよい (MAY). なおこの情報はエンドユーザに表示することを想定しない.
↓ 認証されていない場合の保護リソースへのリクエストに対するレスポンスのサンプル
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example"
↓有効期限の切れたアクセストークンを用いて認証を試みた場合の保護リソースへのリクエストに対するレスポンスのサンプル
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example",
error="invalid_token",
error_description="The access token expired"
アクセストークンレスポンスの例
典型的には, 署名無しトークンは OAuth 2.0 [RFC6749] のアクセストークンレスポンスの一部としてクライアントに返される.
↓ レスポンスの例
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"mF_9.B5f-4.1JqM",
"token_type":"Bearer",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA"
}
エラーコード
リクエストが失敗した場合, リソースサーバは適切なHTTPステータスコード (典型的には 400, 401, もしくは 403) を用いて応答し, 下記のうち1つのエラーコードをレスポンスに含める:
-
invalid_request
リクエストに必要なパラメータが不足している, 対応していないパラメータもしくはパラメータの値が含まれている, 同じパラメータが複数回現れている, 1つのアクセストークンを含むために複数の方法を用いている, もしくはリクエストがその他不正な形式になっている. リソースサーバはHTTPステータスコード400 (Bad Request) の応答を返すべきである (SHOULD). -
invalid_token
提供されたアクセストークンが期限切れである, 取り消されている, 不正な値である, もしくはその他の理由により無効である. リソースサーバはHTTPステータスコード401 (Unauthorized) の応答を返すべきである (SHOULD). クライアントは新しいアクセストークンを要求して保護されたリソースへのアクセスを再試行してもよい (MAY). -
insufficient_scope
リクエストにはアクセストークンにより提供されるよりも高い権限が必要である. リソースサーバはHTTPステータスコード403 (Forbidden) の応答を返すべき (SHOULD) であり, 保護されたリソースへのアクセスに必要となるスコープを示した scope 属性を含めてもよい (MAY).
リクエストが一切の認証情報を含まない場合 (つまり, 認証が必要であることをクライアントが認識していなかった場合か, 対応していない認証方法をクライアントが試行した場合), リソースサーバはエラーコードやその他のエラー情報を応答に含めるべきではない (SHOULD NOT).
↓ 例
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example"
アクセストークンは盗まれたら使用期限がすぎるまで防ぎようがないから、アクセストークンを認可サーバーから付与する際や、アクセストークンを実際に使ってリクエストする際には必ずhttps出ないとダメ。
推奨事項のまとめ
- 署名なしトークンを保護する:
クライアントの実装は, 保護されたリソースへのアクセスを可能とするために署名無しトークンを使う際には, それが意図されていない相手に漏洩していないことを確実にしなければならない (MUST). これは署名無しトークンを用いる際に最も重要なセキュリティに関する考慮事項であり, 後述の詳細な推奨事項すべての基礎となる. - TLS証明書チェインを検証する:
クライアントが保護リソースのリクエストを送信する際には, TLS証明書のチェインを検証しなければならない (MUST). これに失敗した場合, さまざまな攻撃に対してトークンが晒されることとなり, 攻撃者に対して意図しないアクセスを許すことになる. - Always use TLS (https):
クライアントが署名無しトークンを用いたリクエストを送信する際には, 常に TLS [RFC5246] (https) もしくは同等なトランスポートセキュリティを用いなければならない (MUST). これに失敗した場合, 攻撃者に意図しないアクセスを可能とするさまざまな攻撃にトークンが晒されてしまう. - クッキーに署名無しトークンを保存しない:
平文で送信されうるクッキーに署名無しトークンを保存する実装を行ってはならない (MUST NOT). (平文での送信はクッキーのデフォルト送信モードである) 署名無しトークンをクッキーに保存する実装では, クロスサイトリクエストフォージェリへの対処をしなければならない (MUST). - 有効期間の短い署名無しトークンを発行する:
トークンサーバは, 特にブラウザやその他の情報漏洩の起こりやすい環境の中で実行されるクライアントに対してトークンを発行する際には, 有効期間の短い (1時間ないしそれ以下) 署名無しトークンを発行すべきである (SHOULD). 有効期間の短い署名無しトークンを用いることにより, 漏洩した際の影響を減らすことができる. - スコープの設定された署名無しトークンを発行する:
トークンサーバは, 1つまたは複数の意図されたリライングパーティによる使用に限定されるよう, オーディエンスの制限を含んだ署名無しトークンを発行すべきである (SHOULD). - 署名無しトークンをページURL中で渡さない:
署名無しトークンはページURL中で渡されるべきではない (SHOULD NOT). (たとえばクエリ文字列のパラメータとして) 代わりに, 機密性対策の講じられたHTTPメッセージヘッダもしくはメッセージボディ中で渡されるべきである (SHOULD). ブラウザやウェブサーバその他のソフトウェアは, ブラウザの履歴やウェブサーバのログその他のデータ構造において, 適切にURLを保護していないかもしれない. 署名無しトークンがページURLとして渡された場合, 攻撃者は履歴情報やログ, その他の保護されていない場所から盗み取ることができるかもしれない.
RFCとか公式ドキュメントを見て自分で思考するのではなくて、人の記事を見てなんとなくわかった気になる、その人の意見をまんま採用するのはできるだけ避けた方が良い。一次情報の方が圧倒的に正確な情報だから。答えに辿り着くまでのスピードも早くなる。何を参照した?って聞かれてもちゃんと言える。
後でみる。
使っていけないものがなんであるんだって話
クッキーに署名無しトークンを保存する実装を行ってはならない 。
あくまで署名なしトークンの話か。
よし、一旦spaでステートフルなbearer認証作るか。
このやり方だと、クッキー使わねーな。
サーバーサイド要件
- ユーザーの情報が正しいなら、トークンを生成して、サーバーサイドに返す。そのトークンの情報はdbに保存しておく。トークンをjsonのbodyで返す。この際に、ユーザー情報も返しちゃう。spaではないアプリケーションの場合、ここでリダイレクトを発生させる。それの理由としては、POSTリクエストのurlでページが表示されるのが気持ち悪いから。ただ、今回はJSでリクエストしているから、それは関係ない。
- ログインの場合、認証情報を見て、もしOKならユーザー情報とトークンを返す。
- ログアウトを実施したら、トークンを消す。この際にトークンを持っていないとダメ。
- 既に有効なトークンがあった状態で、/usersや/sessionsにリクエストをした場合、ユーザーのトークンを消して新しいトークンを発行すれば良いか。
- リロードしたら、またマイページ表示しないといけないから、GET /sessionsでユーザー情報を取得する必要があるな。
POST /users
POST /sessions
GET /sessions
DELETE /sessions
フロントエンド(Next.js)
- サインインフォームを実装(ページは/sing_in、メソッドとエンドポイントは POST /users)
- サインインが成功したらトークンが送られてくる(失敗したら、エラーメッセージが送られてくる)
- もしトークンとユーザー情報が送られてきたら、そのユーザー情報をセットして、マイページにルーティングする。トークンはローカルストレージに保存する
- 既にログインしている場合、リロードしてもトークンを送信しているので、マイページは表示される
- ログインフォームも作る。(ページは/login、メソッドとエンドポイントは POST /sessions)
- ログアウトしたい場合、セッションを消すエンドポイントにリクエストを出す。
トークンが盗めても、トークンが使えなかったら意味ない。
ただ、このトークンベースの認証って結局Bearerで(持ってたらOK)本人確認しないから、そこが心配やなあ。てか、セッションベースも今考えたら、そうか。だったら別にいいか。あとはローカルストレージとクッキーでどれだけ、セキュリティ要件が変わるかの話か。
bearer認証のうまみは、接続するクライアントが増えても、認証によるdbへの負荷が上がらないこと。かなあ。セッションストレージを用意する必要もないし。ステートフルトークンを用いた認証にしちゃうと、だったらクッキーセッションで良いやんってなる気もするなあ。spaだからステートフルトークンのがセキュリティが高くなるのかねえ。
金はどうでも良いから(どうでも良くはないけど)、セキュリティは最高にしたい。
セキュリティ要件を満たすように、方針を採用するのがやっぱ頭良いやり方な気がするな。既存のやり方を思考停止で選ぶのではなくて。
クッキーにセットする処理を書かないのは地味に楽やな。その代わりトークンをjsonで返す必要があるけど。
sign_upのエンドポイントは完成
残りはsessionsの3つのエンドポイントをどうにかする
残り3つのエンドポイントもいけたー
あとはNext.jsでフロント作るか。
next devは開発モードでアプリケーションを起動します。ホット・リロードをサポートしている。
next buildはproduction buildを取ります。
next startは本番サーバーを起動します。このコマンドでは、本番ビルドを使用します。
next lintは、Next.js組み込みのESLint設定を実行します。
Next.jsを開発モードで実行すると、次のようなメリットもあります:
自動コンパイルとバンドル
React高速リフレッシュ
静的生成と/pagesのサーバーサイドレンダリング
public経由での静的ファイル提供
ファイル更新時のホットリロード
ホットリロードがマジで便利。
*(アスタリスク):
一つのディレクトリまたはファイル名を表します。
例: src/*.js は src/ ディレクトリ内のすべてのJavaScriptファイルを指します。
**(ダブルアスタリスク):
0個以上のディレクトリを再帰的にマッチします。
例: src/**/* は src/ ディレクトリ内のすべてのサブディレクトリとファイルを再帰的に指します。
Appコンポーネントを使ってページを初期化。その
↓
Documentコンポーネントを使って、ページのレンダリングを制御できる
import type { AppProps } from "next/app";
// Next.jsはAppコンポーネントを使ってページを初期化します。
// このコンポーネントをオーバーライドして、ページの初期化を制御することができます:
// ↓ メリット
// 1. Create a shared layout between page changes
// 2. Inject additional data into pages
// 3. 全ページで必要な挙動をかけたりもする
// pagePropsはデータ取得メソッドの1つによってあなたのページにプリロードされた
// 初期プロップを持つオブジェクトで、そうでなければ空のオブジェクトです。
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
import { Html, Head, Main, NextScript } from "next/document";
// カスタム・ドキュメントは、ページのレンダリングに使われる<html>タグと<body>タグを
// 更新することができます。
// デフォルトのドキュメントを上書きするには、以下のようにpages/_documentファイルを作成します:
// documentはサーバー上でのみレンダリングされるので、onClickのようなイベント・ハンドラはこのファイルでは使用できません。
// <Html>, <Head />, <Main />, <NextScript />は、ページが正しくレンダリングされるために必要です。
// documentで使われている<head />コンポーネントは、next/headと同じではありません。ここで使われている<head />コンポーネントは、
// すべてのページに共通する<head>コードにのみ使うべきです。
export default function Document() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
↓ index.tsx
import React from "react";
export default function Page() {
return (
<div>
<h1>Hello, Next.js</h1>
<img src="./mogura.png" />
</div>
);
}
Next.jsはtsconfig.jsonを検知すると、TSのトランスパイルも勝手にやってくれる。
npm run build
> frontend@1.0.0 build
> next build
Linting and checking validity of types ..
We detected TypeScript in your project and reconfigured your tsconfig.json file for you. Strict-mode is set to false by default.
The following suggested values were added to your tsconfig.json. These values can be changed to fit your project's needs:
- skipLibCheck was set to true
- noEmit was set to true
- incremental was set to true
The following mandatory changes were made to your tsconfig.json:
- esModuleInterop was set to true (requirement for SWC / babel)
- resolveJsonModule was set to true (to match webpack resolution)
- isolatedModules was set to true (requirement for SWC / Babel)
✓ Linting and checking validity of types
✓ Creating an optimized production build
✓ Compiled successfully
✓ Collecting page data
✓ Generating static pages (3/3)
✓ Collecting build traces
✓ Finalizing page optimization
Route (pages) Size First Load JS
┌ ○ / 304 B 79.4 kB
├ /_app 0 B 79.1 kB
└ ○ /404 181 B 79.3 kB
+ First Load JS shared by all 79.1 kB
├ chunks/framework-142bc663a62a6fa3.js 45.3 kB
├ chunks/main-75623049b75f64cc.js 32.8 kB
├ chunks/pages/_app-2f2dab6770fa9c24.js 298 B
└ chunks/webpack-7d45971437c161ca.js 728 B
○ (Static) automatically rendered as static HTML (uses no initial props)
lib
使いたいtargetには使いたい機能がない、でも使いたい。そのような時はlibオプションを指定することで使うことができるようになります。
このような最新バージョンにはある、または現時点では実装には至っていないが提案中(proposal)である機能を取り入れて使えるようにする物を通称ポリフィルと言います。ポリフィルについてさらに詳しく知りたい方は、What is a polyfill? (この単語の創案者である Remy Sharp による記事)をご覧ください。
libは必ず指定する必要はありません。targetを指定すればそのtargetで使われているライブラリは自動的に追加されます。指定したtargetでは実装されていないライブラリや、必要がないライブラリを除外したいときに使います。
このlibは例えばトランスパイル後のjsがes2015(target)の場合に、libにライブラリを指定すれば、そのes2015のtargetでもes2020の構文と同等のコードを生成してくれる
libを指定する上での注意
libを指定すると、明示的にどのtargetのlibを使うかも明記しなければいけません。
そりゃそうか。
response.json()はボディテキストをjsonに変換したものを返すのか。
jwtで直接URL叩かれたらどうするねんって思ってたけど、おそらくローディングを出しているな。てか世の中のSPA大体そうな気がしてきた。クッキーも結局APIサーバー側から付与されるから、Versel側のサーバーにリクエスト出すときに送られない。てことはもうこれに関してはしょうがないってことか。
データのキャッシュとかしているからクソ早いんだと思う。前作ってたサービスでも、sessionsでトークン使って別リクエストを出していた。じゃないとハイドレーションエラー(サーバー側でフロント専用のメソッド使うんじゃねえ的なエラー)が発生するもん