0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

共有レンタルサーバーで Authorization ヘッダだけが消える — Bearer 認証 401 の根因切り分けと三段構えの対策

0
Last updated at Posted at 2026-07-10

はじめに

自己ホスト型の請求書管理 OSS NeNe Invoice(PHP + SPA)を共有レンタルサーバー(HETEML)にデプロイしたところ、奇妙な壊れ方をしました。

  • ログイン(cookie ベースの silent refresh)は 成功する
  • なのに直後の GET /admin/memissing_token401
  • SPA は「セッションが切れました」表示に落ちる

ローカルの Docker では一度も再現しない。本番だけで起きる。原因は、前段のプロキシが Authorization ヘッダだけを握りつぶしていたことでした。

この記事は、その切り分け手順と、.htaccess の定番対策では救えなかった環境で最終的に採った「カスタムヘッダへのミラー送信」の実装をまとめたものです。同じ症状(cookie 認証は通るのに Bearer だけ 401)でハマっている人の時間を節約できれば幸いです。

前提: 認証の構成

NeNe Invoice の管理 SPA は、よくある二段構えです。

  • refresh tokenHttpOnly cookie。POST /auth/refresh にだけ送られる
  • access token — メモリ上に保持し、API リクエストの Authorization: Bearer ... で送る

「ログインは通るのに API が 401」という症状は、この構成だと綺麗に説明がつきます。cookie は届いているが、Authorization ヘッダが届いていない。輸送経路が違うので、片方だけ死ねます。

切り分け: 消しているのは誰か

Authorization が PHP に届かない原因は、手前から順に3層あります。

どの層で消えているかは、ヘッダをエコーする使い捨てエンドポイントを置くと一発で分かります。

<?php // probe.php — 調査が終わったら必ず消す
header('Content-Type: application/json');
echo json_encode([
    'HTTP_AUTHORIZATION'   => $_SERVER['HTTP_AUTHORIZATION']   ?? '(なし)',
    'HTTP_X_CUSTOM_PROBE'  => $_SERVER['HTTP_X_CUSTOM_PROBE']  ?? '(なし)',
]);
$ curl -s https://example.jp/probe.php \
    -H 'Authorization: Bearer test123' \
    -H 'X-Custom-Probe: hello'
{"HTTP_AUTHORIZATION":"(なし)","HTTP_X_CUSTOM_PROBE":"hello"}

HETEML での実測がこれでした。カスタムヘッダは素通しなのに、Authorization だけが消える。 これが重要な手がかりで、Apache の CGI 連携が原因(レイヤ2)なら後述の .htaccess で直りますが、カスタムヘッダが届いているのに標準ヘッダだけ消えるのは、より手前の設備(レイヤ1)が意図的に落としている signature です。

対策は三段構え

第一段: .htaccess の定番(レイヤ2向け)

CGI/FastCGI 構成の Apache は、仕様上 Authorization を環境変数に渡しません。これが原因の環境では定番の一行で直ります。

RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

Apache 2.4.13+ で設定を触れるなら CGIPassAuth On でも同じことができます。まずこれを試すべきで、多くの共有ホストはここで解決します。NeNe Invoice もこの行を .htaccess に入れています — 効く環境のために。

第二段: それでも消える環境がある(レイヤ1)

HETEML では第一段が効きませんでした。%{HTTP:Authorization} の時点で空 — つまり Apache に届く前に消えている。ホスティング事業者の前段プロキシが落としているので、ユーザー側の設定では取り返せません。

推測ですが、共有ホストの前段が Authorization を落とすのは、事業者側の Basic 認証設備との干渉を避けるためと思われます。いずれにせよ、こちらから制御できない層です。

第三段: カスタムヘッダへのミラー送信

残る手は「消されないヘッダで運ぶ」です。SPA 側は Bearer トークンを標準ヘッダと X-Authorization の両方に載せます。

function attachBearer(headers: Record<string, string>): void {
  if (authToken === null) return
  headers['Authorization'] = `Bearer ${authToken}`
  headers['X-Authorization'] = `Bearer ${authToken}`
}

バックエンドは PSR-7 request の生成直後に、Authorization が不在のときだけミラーを採用します。

final class AuthorizationHeaderFallback
{
    public const string FALLBACK_HEADER = 'X-Authorization';

    public static function apply(ServerRequestInterface $request): ServerRequestInterface
    {
        if ($request->getHeaderLine('Authorization') !== '') {
            return $request;   // 標準ヘッダが届く環境には何もしない
        }

        $fallback = $request->getHeaderLine(self::FALLBACK_HEADER);
        if ($fallback === '') {
            return $request;
        }

        return $request->withHeader('Authorization', $fallback);
    }
}

この設計で意識した点が3つあります。

1. 標準ヘッダが常に優先。 Authorization が届く環境では fallback は一切動きません。挙動が変わるのは「壊れていた環境」だけです。

2. 配線は入口の1箇所だけ。 index.php の request 生成直後に apply() を挟むので、認証ミドルウェアもハンドラも Authorization を読むコードのままです。「X-Authorization も見る」という知識がコードベースに漏れません。

$request = $creator->fromGlobals();
$request = AuthorizationHeaderFallback::apply($request);

3. トークンの意味は変えない。 同じ Bearer トークンを別名で運んでいるだけで、有効期限も検証ロジックも同一です。

注意点: キャッシュとの相性

ひとつ罠があります。HTTP のキャッシュ仕様では「Authorization つきリクエストへのレスポンスは共有キャッシュに保存しない」という保護が効きますが、X-Authorization は誰もそんな約束をしてくれません。経路上に共有キャッシュ(CDN・リバースプロキシ)がいる構成では、認証付きレスポンスが Cache-Control: private / no-store を明示していることを確認してください。API が最初から no-store を返しているなら(NeNe Invoice はそうしています)、この心配はありません。

また、クロスオリジンで使う場合はカスタムヘッダなので CORS の preflight 対象になります。same-origin の SPA なら関係ありません。

まとめ

症状 原因の層 対策
$_SERVERHTTP_AUTHORIZATION がない(CGI/FastCGI) Apache .htaccessE=HTTP_AUTHORIZATION / CGIPassAuth On
カスタムヘッダは届くのに Authorization だけ消える 前段プロキシ ユーザー側では救えない → ミラーヘッダ fallback
cookie 認証は通るのに Bearer だけ 401 上のどちらか まず probe で層を特定する

「ログインは通るのに直後の API が 401」は、認証ロジックのバグに見えて、実はヘッダの輸送事故であることがあります。コードを疑う前に、curl とエコーエンドポイントで「何がどこまで届いているか」を測るのが最短でした。

NeNe Invoice は MIT ライセンスの OSS です。この記事の実装は GitHubsrc/Http/AuthorizationHeaderFallback.php(テスト込み)にあります。


── 森 秀之(彩音インターナショナル) 🔗 ayane.co.jp

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?