ASP.NET Core Minimal APIでPOSTだけCORSブロック!IIS Express×Windows認証の原因と解決策完全ガイド

ASP.NET CoreのMinimal APIをIIS Express上で動かしていると、GETは成功しているのにPOSTだけCORSエラーで落ちる――開発現場でとても起きやすい落とし穴です。本記事は「なぜそうなるのか」をHTTPレベルから分解し、Windows認証×IIS Express特有のハマりどころまで含めて、最短で直すための手順と再発防止策を体系的にまとめました。貼り付けて動く最小コードも用意しています。

目次

ASP.NET Core Minimal APIで「POSTだけCORSでブロック」になるときの原因と対処

症状

  • Visual Studio 2022 + IIS Express で開発。
  • Windows 認証(Negotiate)が 有効。
  • GET は成功、POST は失敗。コンソールには次のエラー:
    Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present…
  • AddCors と UseCors は設定済みのつもり。

結論(最短の対処フロー)

まずは下表の順に確認・修正してください。ほとんどのケースはこれだけで復旧します。

手順内容補足
1具体的なオリジンでCORSポリシーを定義
builder.Services.AddCors(o => o.AddPolicy("FrontEnd", p => p.WithOrigins("https://frontend.example.com", "https://localhost:5173") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials()));
AllowAnyOrigin() と AllowCredentials() は併用不可。必ず具体的なOriginを列挙。
2ミドルウェアの順序を修正
app.UseCors("FrontEnd"); // ① CORS(認証より前) app.UseAuthentication(); // ② 認証 app.UseAuthorization(); // ③ 認可
プリフライト(OPTIONS)は認証前に許可しないと401/403で落ちる。
3OPTIONSに余計なハンドラを置かない
Minimal APIのCORSは AllowAnyHeader/Method があればプリフライトに自動応答。手動でMapMethods(..., "OPTIONS", ...)しているなら削除かCORSの後段へ。
独自ハンドラが先に動くとCORSヘッダーが付かない。
4資格情報を送る(フロントエンド)
fetch(url, { method: "POST", credentials: "include", // ★必須 headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) });
Windows認証やCookieベース認証では、credentials がないと実質401→CORSエラーに見える。
5IIS Express依存を切り分け
dotnet run でKestrelを直接起動し、POSTが通るか確認。通るならIIS Express固有の問題。開発中はKestrelを使うか、IIS側の認証/プリフライト許可を調整。
実際にKestrelでは解決したという報告が多い。
6最小構成サンプル(本記事後半に全文あり)GET/POSTともに正常動作する雛形を提示。

なぜ「GETは成功、POSTだけCORSエラー」になるのか

キモはプリフライトです。クロスオリジンで POST(しかも Content-Type: application/json などの非シンプルヘッダー)を送ると、ブラウザは最初に OPTIONS を投げて「このメソッドとヘッダー、資格情報を送っても良いか?」をサーバーに問い合わせます。ここでサーバーが正しく応答できないと、本番の POST は送信すらされません。

Windows認証が有効なIIS Expressでは、OPTIONS 自体に認証を要求してしまい401応答を返すケースがよくあります。401応答には通常CORSヘッダーが付かないため、ブラウザ視点では「Access-Control-Allow-Origin が無い=CORS違反」と判断され、上記のエラーが表示されます。GETは「シンプルリクエスト」でプリフライト不要のため通っている、という理屈です。

HTTPで見る理想のプリフライト応答

OPTIONS /todoitems HTTP/1.1
Origin: https://localhost:5173
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: [https://localhost:5173](https://localhost:5173)
Vary: Origin
Access-Control-Allow-Methods: GET,POST,OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true

ポイントは Allow-Origin が実際のオリジン値になっていること(ワイルドカード不可)と、Allow-Credentials: true が返っていることです。前者は WithOrigins(...).AllowCredentials() の組み合わせで解決します。

正しいCORS設定とミドルウェア順序

Program.cs(.NET 8/9 Minimal API)の基本形

using Microsoft.AspNetCore.Authentication.Negotiate;

const string CORS = "FrontEnd";

var builder = WebApplication.CreateBuilder(args);

// 認証・認可
builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
.AddNegotiate();
builder.Services.AddAuthorization();

// CORS
builder.Services.AddCors(o => o.AddPolicy(CORS, p =>
p.WithOrigins(
"[https://frontend.example.com](https://frontend.example.com)",
"[https://localhost:5173](https://localhost:5173)",       // Vite等
"[https://localhost:3000](https://localhost:3000)")       // React等
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
));

// アプリ
var app = builder.Build();

app.UseHttpsRedirection();

// ★順序が最重要:CORS → 認証 → 認可
app.UseCors(CORS);
app.UseAuthentication();
app.UseAuthorization();

// サンプルAPI
app.MapGet("/ping", () => Results.Ok(new { ok = true }))
.RequireCors(CORS);

app.MapPost("/todoitems", (TodoItem item) => Results.Created($"/todoitems/{item.Id}", item))
.RequireAuthorization()  // Windows認証が必要なら付与
.RequireCors(CORS);

app.Run();

record TodoItem(int Id, string Title);

よくある誤りは、UseAuthentication()/UseAuthorization() を UseCors() より前に置いてしまうことです。これだけでプリフライトが401になり、POSTが一切届かなくなります。

エンドポイント単位でCORSを指定する(必要な場合)

APIが大規模になり、エンドポイントごとに許可するオリジンを変えたい場合はグループ化が便利です。

var v1 = app.MapGroup("/api/v1").RequireCors("FrontEnd");
v1.MapPost("/orders", ...).RequireAuthorization();

フロントエンド側の必須設定

fetchの例

await fetch("https://api.local.example/todoitems", {
  method: "POST",
  credentials: "include",               // ★重要
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ title: "Write article" })
});

Axiosの例

axios.post("https://api.local.example/todoitems",
  { title: "Write article" },
  {
    withCredentials: true,              // ★重要
    headers: { "Content-Type": "application/json" }
  });
  • Authorizationヘッダーを送る場合、プリフライトの Access-Control-Request-Headers に authorization が含まれます。サーバー側は .AllowAnyHeader()(または .WithHeaders("Authorization", ...))で許可しておきます。
  • Cookieベース認証を併用するなら、クロスサイトで使うCookieは SameSite=None; Secure が必要です(Windows認証のみなら通常不要)。
  • フロントとAPIのスキーム(http/https)は揃えるか、双方をhttpsに。混在はブラウザのセキュリティで弾かれがちです。

IIS Express固有の落とし穴と対処

匿名認証(Anonymous)を有効にする

Windows認証だけを有効にし、匿名認証を無効にしていると、IIS Expressが OPTIONS も含め全てのリクエストに認証を要求し、プリフライトが401で失敗しがちです。開発中は次のいずれかで回避します。

  • Visual Studioのプロジェクト設定(プロパティ → デバッグ → プロファイル「IIS Express」)で
    Windows Authentication: 有効、Anonymous Authentication: 有効 にする。
  • applicationhost.config で該当サイトの認証を調整する。

applicationhost.configの一例

Visual Studioのソリューション隣にある .vs\config\applicationhost.config で、アプリのスコープに以下のような設定が入っているか確認します(例:一部抜粋)。

<system.webServer>
  <security>
    <authentication>
      <anonymousAuthentication enabled="true" />
      <windowsAuthentication enabled="true" />
    </authentication>
  </security>
  <!-- ★IIS CORS Module導入済み環境のみ(任意)。ASP.NET CoreのCORSがあれば必須ではない -->
  <cors enabled="true">
    <add origin="https://localhost:5173">
      <allowHeaders allowAllRequestedHeaders="true" />
      <allowMethods>
        <add method="GET" />
        <add method="POST" />
        <add method="OPTIONS" />
      </allowMethods>
      <allowCredentials>true</allowCredentials>
    </add>
  </cors>
</system.webServer>

※ <cors> 要素はIIS CORS Moduleが入っていない環境では使えません。導入していなくても、ASP.NET Core側のCORSだけで十分対応可能です。

Kestrelでの切り分け

dotnet run(Kestrel直起動)に切り替えてPOSTが通るなら、原因はIIS Express側にあります。開発中はKestrelをデフォルトにし、IIS/IIS Expressでの動作確認は最終段のみに絞ると生産的です。

最小構成サンプル:これでGET/POSTともに動く

Program.cs(フル)

using Microsoft.AspNetCore.Authentication.Negotiate;

const string CORS = "FrontEnd";

var builder = WebApplication.CreateBuilder(args);

// 認証・認可
builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
.AddNegotiate();
builder.Services.AddAuthorization();

// CORSポリシー
builder.Services.AddCors(o => o.AddPolicy(CORS, p =>
p.WithOrigins("[https://localhost:5173](https://localhost:5173)", "[https://frontend.example.com](https://frontend.example.com)")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
));

var app = builder.Build();

app.UseHttpsRedirection();

// ★重要:順序
app.UseCors(CORS);
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/ping", () => Results.Ok(new { ok = true }))
.RequireCors(CORS);

app.MapPost("/todoitems", (TodoItem item) => Results.Created($"/todoitems/{item.Id}", item))
.RequireAuthorization()
.RequireCors(CORS);

app.Run();

public record TodoItem(int Id, string Title);

launchSettings.json(IIS Expressプロファイルの例)

{
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "windowsAuthentication": true,
      "anonymousAuthentication": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "MinimalApi": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "applicationUrl": "https://localhost:7194;http://localhost:5194"
    }
  }
}

フロントエンド(開発中Vite/React等)

const res = await fetch("https://localhost:7194/todoitems", {
  method: "POST",
  credentials: "include",                // これが無いとWindows認証はほぼ失敗
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ id: 1, title: "Task" })
});
console.log(await res.json());

デバッグ手順:3分で原因特定

  1. DevToolsのNetworkタブを開き、失敗したPOSTの直前にある OPTIONS を選択。
  2. Request Headersに Origin、Access-Control-Request-Method、Access-Control-Request-Headers が付いているか確認。
  3. Response Headersに Access-Control-Allow-Origin と Access-Control-Allow-Credentials が付いているか確認。無ければCORSヘッダー未付与=ミドルウェア順序かIIS側認証が原因。
  4. ステータスコードが 401/403/404/405 のどれかなら、CORSの前に落ちています。UseCors() の順序、IISの匿名認証、WebDAV/ハンドラの干渉を疑う。
  5. フロントのリクエストオプションに credentials: "include"(または withCredentials: true)があるか確認。

よくある落とし穴と対処

  • AllowAnyOrigin + AllowCredentials を併用
    セキュリティ仕様で禁止。必ず WithOrigins(...) を使う。
  • Originのポート/スキーム違い
    https://localhost:5173 と http://localhost:5173 は別オリジン。両方必要なら両方列挙。
  • Authorizationヘッダーを許可していない
    プリフライトが authorization を要求し、サーバーが許可していないと失敗。.AllowAnyHeader() で回避。
  • OPTIONSを独自に処理
    手動レスポンスはCORSヘッダーの付け忘れが起こりやすい。基本はフレームワークに任せる。
  • CookieのSameSite設定
    OpenID Connect等のCookie認証をクロスサイトで使うなら SameSite=None; Secure を付ける。付いていないとブラウザがCookieを送らず401に。
  • WebDAVやハンドラがOPTIONSを奪う
    フルIISではWebDAVモジュールが405を返すことあり。不要なら無効化。IIS Expressでは通常入っていないが、ハンドラの競合には注意。

原因別・解決クックブック

現象原因の典型対処
OPTIONSが401UseCorsの順序が遅い/IISが匿名を拒否UseCorsを認証より前に。IIS Expressは匿名認証を有効に。
OPTIONSが405WebDAV/ハンドラがOPTIONSを禁止モジュールを無効化、またはIIS CORS Module/ASP.NET Core CORSで許可。
OPTIONSが200だが本番POSTが401資格情報が送られていないフロントで credentials: "include" / withCredentials: true。
Allow-Originが*AllowAnyOriginとAllowCredentialsの誤用WithOrigins(...).AllowCredentials() に変更。
開発では成功、本番IISで失敗本番IISの認証/モジュール設定が異なるIIS側にもCORS/認証の設計を反映。匿名認証やCORSモジュールの可否を確認。

運用ベストプラクティス

  • 本番はオリジンを固定(ワイルドカード禁止)。
  • HTTPSを強制し、HSTS/リダイレクトを徹底。
  • 面倒なときはリバースプロキシ(Nginx/Traefik/Azure Front Door等)側でCORS終端。アプリの責務を減らせます。
  • APIのバージョン毎に MapGroup+RequireCors で明示的に制御。
  • 監視にCORSヘッダーの有無を含め、デプロイ差分で早期検知。

再発防止のための最終チェックリスト

  • Program.cs:UseCors → UseAuthentication → UseAuthorization の順序になっている。
  • CORSポリシー:WithOrigins で実際のフロントURLを完全一致で列挙(スキーム・ポート含む)。
  • AllowCredentials() を付けつつ、AllowAnyOrigin() は使っていない。
  • AllowAnyHeader()/AllowAnyMethod() を設定済み(AuthorizationやContent-Typeが通る)。
  • フロント:credentials: “include” / withCredentials: true を付与。
  • IIS Express:匿名認証 ON、Windows認証 ON。OPTIONS が401/405になっていない。
  • DevToolsでプリフライトに Access-Control-Allow-* が返っている。
  • (Cookie利用時)SameSite=None; Secure を設定。

まとめ

「GETは通るのにPOSTだけCORSエラー」は、ほぼ確実にプリフライトが認証やミドルウェアの順序で阻害されているサインです。ASP.NET Core側では WithOrigins + AllowCredentials、UseCors → UseAuthentication → UseAuthorization の順序を守り、フロントでは credentials: "include" を付ける。IIS Expressを使う場合は匿名認証を有効化して OPTIONS を素通しさせる――この3点を押さえれば、開発環境でも本番でも安定して動作します。詰まったらまずプリフライトの応答ヘッダーを確認し、本文のチェックリストに沿って一つずつ潰していってください。


付録:トラブル再現と解消のための比較コード

(NG例)CORSの順序が認証より後ろ

// ❌ これだとOPTIONSが401になりやすい
app.UseAuthentication();
app.UseAuthorization();
app.UseCors("FrontEnd");

(NG例)AllowAnyOriginとAllowCredentialsの併用

// ❌ 実行時例外。セキュリティ仕様で禁止
builder.Services.AddCors(o => o.AddPolicy("FrontEnd", p =>
    p.AllowAnyOrigin().AllowCredentials()
));

(OK例)エンドポイント単位のCORS+認可

app.MapGroup("/api").RequireCors("FrontEnd")
   .MapPost("/secure", () => Results.Ok())
   .RequireAuthorization();

(参考)Networkタブでの見え方

OPTIONS のレスポンスヘッダーに以下が並んでいれば成功のサインです。

Access-Control-Allow-Origin: https://localhost:5173
Access-Control-Allow-Methods: GET,POST,OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Vary: Origin

付録:IIS/IIS Expressのよくある設定差分

項目開発(IIS Express)本番(IIS)注意点
匿名認証ONにするプリフライトのみ許可か、CORSモジュールで処理プリフライトは認証なしで通すのが基本。
Windows認証ONON(サイト/アプリケーションレベル)Negotiate/NTLMの優先度に応じてヘッダーが変わる。
WebDAV通常なし環境により有効有効だとOPTIONSが405になることがある。
CORS処理ASP.NET Coreミドルウェアミドルウェア or IIS CORS Module二重設定は避ける。どちらか一方に統一。

付録:一発判定スクリプト(開発用)

ターミナルからプリフライトが通るかだけを即判定したいときのワンライナーです(PowerShell)。

$origin = "https://localhost:5173"
$api = "https://localhost:7194/todoitems"
$headers = @{
  "Origin" = $origin
  "Access-Control-Request-Method" = "POST"
  "Access-Control-Request-Headers" = "content-type, authorization"
}
try {
  $r = Invoke-WebRequest -Method Options -Uri $api -Headers $headers -SkipCertificateCheck
  ($r.Headers["Access-Control-Allow-Origin"] -eq $origin) -and
  ($r.Headers["Access-Control-Allow-Credentials"] -eq "true")
} catch {
  "NG: $($_.Exception.Message)"
}

最後に

「POSTだけCORSでブロック」は、(1)オリジンの固定、(2)ミドルウェア順序、(3)資格情報の送信という3条件を満たせば解けます。IIS Express×Windows認証は特にプリフライトが詰まりやすいので、匿名認証の扱いとKestrelでの切り分けを覚えておくと、次からは数分で解消できます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次