ByteScope

401

HTTP 401 Unauthorized の直し方

4xx クライアントエラーIETF 標準RFC 9110

リクエストが使える認証情報を運んでいませんでした。理由句は Unauthorized ですが、実際に表しているのは「未認証」の状態で、しかも何を出せば通るのかを示すヘッダーの添付は必須です。

401 が出ている理由と、やるべきこと

同じ 3 桁でも、リクエストのどちら側にいるかで別々の問題になります。自分に当てはまるブロックを読んでください。

curl で 401 を再現する

失敗した URL に対してそのまま実行してください。エラーページを表示せずにステータスだけを出すので、ブラウザが描画したものではなくサーバーが言ったことが見えます。

curl
curl -sS -o /dev/null -D - -w '\n%{http_code}\n' https://example.com/api/me

`-sS` は進捗表示だけを消してエラーメッセージは残し、`-o /dev/null` はエラーページの本文を捨て、`-D -` は応答ヘッダーを標準出力に落とし、`-w` は最後にステータスを 1 行で出します。目当てのヘッダーは `www-authenticate` です。§15.5.2 が 401 に必須としているもので、最初のトークンがスキーム、Bearer の API なら `error` パラメータが「トークンが無い」「不正」「期限切れ」を分けます。このヘッダーが無ければ、そのサーバーは仕様どおりに答えておらず、以後の情報源はそのサーバー自身のログだけになります。次に、ステータスコードでは分けられない 2 つの場合を切り分けるため、同じコマンドをあと 2 回実行します。1 回は `-H 'authorization: Bearer <token>'` を付けて、もう 1 回は `-u wrong:creds` を付けて。両方 401 なら検証側が何もかも拒んでいるということで、疑うべきはあなたのトークンではなく鍵か issuer か audience です。本物のトークンを送ったときだけ 401 なら、そのトークンが期限切れか失効しています。なお `-u` は Basic の認証情報を先回りして送るので、その実行が 401 になったこと自体は、サーバーが Basic を提示したかどうかについて何も語りません。それを語るのは `www-authenticate` だけです。チャレンジがサイトではなくプロキシを名乗っているなら、見るべきは 407 のほうです。

401 が実際に意味していること

RFC 9110 §15.5.2 は 401 を、対象リソースに対する有効な認証情報を欠いているためリクエストが適用されなかった状態と定義し、そこに強い要求を付けています。401 を生成するサーバーは、少なくとも 1 つのチャレンジを含む `WWW-Authenticate` ヘッダーフィールドを送らなければならない (MUST)、と。このヘッダーこそが 401 と 403 を分けるすべてです。違う認証情報で再試行すれば成功しうるからこそ存在するもので、サーバーは「ではどの認証情報なのか」を機械可読な形でクライアントに返す義務を負います。チャレンジの無い 401 は仕様違反であり、同時にいちばん午後を溶かすタイプの 401 でもあります。サーバーが何を求めていたのかを語る唯一のフィールドが消えているからです。同じ節は、意外に思われがちな場合も扱っています。リクエストが認証情報を含んでいた場合、401 はその認証情報に対して認可が拒否されたことを意味します。つまり 1 つのステータスコードが「何も送らなかった」と「送ったが拒否された」の両方を覆っていて、ステータス行にはそれを分ける情報がありません。だからあなた自身のログで分けるしかないのです。§15.5.2 はさらに、リクエストを繰り返して同じチャレンジが返ってきたユーザーエージェントに対し、応答ボディを利用者に見せるよう求めています。診断上の詳細はたいていそこに載っているからです。見る場所を変える事実があと 2 つあります。§11.3 のチャレンジ文法はスキーム名とパラメータの並びで、そのうち `realm` が保護空間に名前を与えます。そしてブラウザの挙動を決めるのはスキームです。`Basic` チャレンジ (RFC 7617) を返すと、ブラウザはあなたのアプリケーションの上に自前のユーザー名とパスワードのダイアログを描きます。トークン API については、RFC 6750 §3.1 が好みに任せずステータスで場合分けを規定しています。`invalid_request` は 400、`invalid_token` は 401、`insufficient_scope` は 403 です。この読み方に従えば、OAuth で保護された API が返す 401 は「トークンが無い・壊れている・期限切れ・失効した」のいずれかであり、本人のものではあるが権限の範囲が足りないトークンは 403 で返ってくるべきだったことになります。最後に、401 は RFC 9110 §15.1 がヒューリスティックにキャッシュ可能としているステータスコードには入っていないので、プロキシが独断でキャッシュすることはありません。

401 と間違えられやすいコード

紛らわしいコード見分け方
403401 は「あなたが誰か分からない」、403 は「分かったうえで、それでも駄目だ」です。信頼できる判定材料は言葉づかいではなくヘッダーです。RFC 9110 が 401 に `WWW-Authenticate` を要求するのは、違う認証情報で再試行すれば通りうるからで、403 に同種の要求が無いのは通りえないからです。RFC 6750 §3.1 も OAuth で同じ線を引いています。トークンが無い、あるいは不正なら `invalid_token` で 401、有効だが必要なスコープを欠くなら `insufficient_scope` で 403 です。
404存在すると確信しているリソースが、未認証の呼び出し元には正当に 404 を返すことがあります。§15.5.4 は、禁止されたリソースの存在自体を認めたくないサーバーが代わりに 404 を送ることを許していて、非公開リポジトリやオブジェクトストレージはまさにそれをやります。だから 404 をルーティングのバグとして追い始める前に、認証情報を付けて試してください。200 や 403 に変われば、最初から変装したアクセス制御の判断だったということです。
400サーバーがそもそもパースできない `Authorization` ヘッダーは、拒否された身元ではなく壊れたリクエストです。RFC 6750 §3.1 も `invalid_request` には 401 ではなく 400 で答えるべきだと明記しています。実際のフレームワークは足並みが揃っていないので、認証が要るエンドポイントで出た 400 は、トークンそのものが拒否されたと決めつける前に「ヘッダーの形が違う」と読む価値があります——`Bearer ` の接頭辞が無い、余計な改行が混ざっている、`Authorization` が 2 本ある、など。
407407 (Proxy Authentication Required, §15.5.8) は同じ発想を 1 ホップ手前でやったものです。認証情報を求めているのはオリジンではなくプロキシで、チャレンジも `WWW-Authenticate` ではなく `Proxy-Authenticate` で来ます。社内ネットワークで説明のつかない 401 は、よく読んでいない 407 であることがよくあります。見分け方は、いま触っている API だけでなく、試すホスト全部でチャレンジが返ってくることです。
BasicBasic スキーム (RFC 7617) は `user:password` を base64 にしますが、これは符号化であって暗号化ではありません。ヘッダーが見える者は誰でもパスワードを読めるので、安全なのは TLS の上だけです。それに、意図されることの少ない UI 上の副作用もあります。`Basic` チャレンジを受け取ったブラウザは、ページの上に自前の認証ダイアログを描きます。JavaScript から呼ばれる前提の API が `Bearer` でチャレンジすべき理由がこれです。
理由句
Unauthorized
クラス
4xx クライアントエラー
定義
RFC 9110
標準
IETF 標準

このサイトのツール

どれもブラウザ内で完結します。アップロードは発生しません。

関連するステータスコード

401 を調べているとき、ついでに読むことになりがちなコードです。

よくある質問

このステータスコードを出したのがどのサーバーかを知るには?

ページではなく応答ヘッダーを読んでください。curl -sS -o /dev/null -D - https://example.com/path が本文なしでヘッダーだけを出し、serverviacf-ray の 3 つで、答えたのがどの層かが分かります。cf-ray の値があれば応答を扱ったのは Cloudflare で、その値はサポートが聞いてくる ID です。エッジを完全に外して確かめたいなら、--resolve example.com:443:203.0.113.10 を付けて同じリクエストを投げ直します。元のホスト名と SNI を送りつつ、指定したオリジンのアドレスへ接続するので、答えが変われば、エッジとオリジンの言い分が食い違っているということです。

数字のあとの理由句(Not Found など)に意味はありますか?

ありません。分岐条件にしてはいけません。RFC 9112 はクライアントに理由句を無視するよう求めていますし、サーバーは自由に変えられますし、HTTP/2 と HTTP/3 にはそもそも理由句が存在しません。HTTP/1.1 では 404 Not Found として届くものが、HTTP/2 では素の 404 として届きます。このサイトが登録済みの理由句を載せているのは、それが検索される語であり HTTP/1.1 のログに出る文字列だからで、何かのソフトウェアがそれに依存しているからではありません。

このコードで失敗したリクエストは再試行すべきですか?

5xx と 429 は再試行してよく、4xx はしないでください。次に投げても中身は同じだからです。応答に Retry-After が付いていればそれに従ってください。RFC 9110 がまさにこのために定義していて、値は秒数でも日時でも構いません。付いていなければ、ジッター付きの指数バックオフと明確な上限を使い、しかも冪等なメソッドに限ってください。再試行された POST はカードに二重で課金しかねません。すでに失敗しているサーバーへの再試行の嵐は、短い障害を長い障害に変える典型的な経路です。

自分のサーバーが返すステータスコードは変えられますか?

変えられます。守るべき規則は 1 つだけ、そのコードが本当であること。応答を自動で読むもの——検索エンジン、監視、キャッシュ、クライアント側の再試行ロジック——はすべて数字だけで判断し、ページの中身は一切見ません。エラーページを 200 で返せば自分の監視から障害が隠れますし、無いページを 200 で返せばエラーが本文として索引されますし、単に不正なリクエストに 500 を返せば、当番の人をスタックの間違った半分へ送り込むことになります。

401 でまだ詰まっているなら、ステータスコード一覧をすべて見る。あるいは上に戻って、自分の立ち位置に向けて書かれたブロックを読んでください。