「Easy Auth を使っているのに、1時間後に API が 401 / 403 を返し始める」「/.auth/me を叩いても古いクッキーとトークンしか返らない」――その原因と再発防止の具体策を、Azure App Service と Microsoft Entra ID(旧 Azure AD)の実運用を前提に、手順・設定・コード例・検証方法までまとめて解説します。プロキシ配下(Front Door / Application Gateway / WAF / CDN 等)でも破綻しない設計に落とし込みましょう。
背景:Easy Auth(App Service Authentication)の動作と落とし穴
Easy Auth は Azure App Service に組み込まれた認証ゲートウェイで、アプリ変更なしで Microsoft Entra ID 等の IdP を使ったサインインを実現します。ユーザーがログインすると、ブラウザには AppServiceAuthSession(HttpOnly)などのクッキーが発行され、App Service の Token Store にはアクセストークン/ID トークン/(あれば)リフレッシュトークンが保存されます。アプリコードは X-MS-CLIENT-PRINCIPAL や X-MS-TOKEN-AAD-ACCESS-TOKEN といったヘッダー、もしくは /.auth/me で認証情報を参照できます。
主な内蔵エンドポイント
| エンドポイント | 役割 | 注意点 |
|---|---|---|
/.auth/login/aad | Entra ID にリダイレクトしてログイン | post_login_redirect_uri で復帰先を指定可能 |
/.auth/me | 現在のセッションとトークンの状態確認 | トークンの更新はしない(最新化は /.auth/refresh) |
/.auth/refresh | Token Store 内のトークンを更新し、新しい AppServiceAuthSession を Set-Cookie | GET で呼ぶ。クッキー同伴必須(credentials: 'include') |
/.auth/logout | サインアウト(クッキー破棄) | IdP 側セッションは別管理のため、完全ログアウト構成は別途検討 |
トークンとセッションの寿命(目安)
| 種別 | 既定の有効期限 | 延長可否 | 備考 |
|---|---|---|---|
| アクセストークン | 約 60 分 | 不可(IdP ポリシー) | 期限切れ後はリフレッシュ経由で発行しなおす |
| ID トークン | 約 60 分 | 不可 | クライアント情報の検証用途が主 |
| リフレッシュトークン | 最長 90 日(条件あり) | ポリシー依存 | offline_access スコープが必須 |
| Easy Auth セッション | 最大 8 時間 + 猶予 | 可 | token-refresh-extension-hours で延長(後述) |
ここで重要なのは、/.auth/me はあくまで「照会」APIであり、アクセストークンの更新は行いません。更新は /.auth/refresh だけが担います。
症状:AppServiceAuthSession が期限切れになり、/.auth/me も古いまま
プロキシ配下の Web アプリで「1 時間後に API が 401/403 になり、/.auth/me を見ても失効済みのトークンしか入っていない」という相談が多く寄せられます。これは設計として正しく、更新処理を実装していないことが根本原因です。解決策はシンプルで、60 分ごと(あるいは期限–数分前)に /.auth/refresh をサイレント呼び出し、Token Store とクッキーを進行更新することです。
解決アクション①:/.auth/refresh を定期実行
GET で呼び出すと、バックエンド Token Store のアクセストークンが更新され、レスポンスの Set-Cookie で新しい AppServiceAuthSession が配布されます。その後に /.auth/me を呼び直すと、新しいアクセストークンが取得できます。
// SPA の例(fetch)
async function silentRefresh() {
const res = await fetch('/.auth/refresh', {
method: 'GET',
credentials: 'include', // ← 必須(クッキー同伴)
cache: 'no-store',
});
if (!res.ok) {
// 403/401/500 などはハンドリングしてリトライや再ログインへ
throw new Error('refresh failed: ' + res.status);
}
}
function scheduleRefresh({ intervalMinutes = 55 } = {}) {
// 60分のアクセストークン期限より少し短く
silentRefresh().catch(console.warn);
setInterval(() => silentRefresh().catch(console.warn), intervalMinutes * 60 * 1000);
}
// アプリ起動時にセットアップ
scheduleRefresh({ intervalMinutes: 55 });
// Axios インターセプターで「期限切れ→自動更新→再送」
// (API 401受信時に1回だけリフレッシュを試みてから再送)
import axios from 'axios';
let refreshing = null;
const api = axios.create({ withCredentials: true });
api.interceptors.response.use(undefined, async (error) => {
const original = error.config;
if (error.response && error.response.status === 401 && !original.__retried) {
original.__retried = true;
refreshing = refreshing || fetch('/.auth/refresh', { credentials: 'include' });
try { await refreshing; } finally { refreshing = null; }
return api(original); // 再送
}
return Promise.reject(error);
});
# 手元検証(curl)
curl -i -X GET "https://<app-name>.azurewebsites.net/.auth/refresh" \
--cookie "AppServiceAuthSession=<existing>"
解決アクション②:リフレッシュトークンの前提条件を満たす
- offline_access スコープ:アプリ登録(Microsoft Entra ID)で offline_access を要求しておくと、条件を満たすログインでリフレッシュトークンが交付されます。
- Token Store を有効化:App Service の認証設定で Token Store を On に。これにより Easy Auth がトークンを安全に保管・更新できます。
Auth 設定(v2)例:loginParameters と Token Store
{
"platform": { "enabled": true },
"globalValidation": {
"requireAuthentication": true,
"unauthenticatedClientAction": "RedirectToLoginPage"
},
"identityProviders": {
"azureActiveDirectory": {
"enabled": true,
"registration": {
"clientId": "<APP_CLIENT_ID>",
"openIdIssuer": "https://login.microsoftonline.com/<TENANT_ID>/v2.0"
},
"login": {
"loginParameters": [
"scope=openid profile offline_access"
]
}
}
},
"login": {
"tokenStore": { "enabled": true }
}
}
解決アクション③:セッション猶予を延ばす(運用緩和)
夜間バッチや長時間ワークフローで再サインインを抑制したい場合、次の CLI で Easy Auth のセッション延長を設定できます。
az webapp auth update \
--resource-group <RG> \
--name <APP_NAME> \
--token-refresh-extension-hours 72
ただし、セキュリティ要件とトレードオフです。安易に上げ過ぎず、定期的な /.auth/refresh の実装が基本である点は変わりません。
確認ポイント:/.auth/me の正しい使い方
/.auth/me は照会専用です。更新後に呼ぶことで「新しいアクセストークンが入っているか」を検証できます。更新は必ず /.auth/refresh 経由で行います。
更新のフロー図(簡略)
[Browser] -- GET /.auth/refresh (Cookie 同伴)
| <-- 200 + Set-Cookie: AppServiceAuthSession=NEW
|
(Token Store が新しい access_token / id_token を保持)
|
[Browser] -- GET /.auth/me --> { ... "access_token": "NEW", ... }
症状:/.auth/refresh が 403 Forbidden になる
2025 年前後から、構成の組み合わせによって /.auth/refresh が 403 を返す事例が散見されます。多くは「構成の不整合」か「同意の状態」、もしくは「Cookie の属性改変(プロキシ)」に起因します。以下のチェックリストで一気に切り分けましょう。
A. 設定の不整合(Client ID / Audience / API 権限)
- App Service 側の Client ID:認証ブレードの「Microsoft(Entra ID)」設定で、App Registration の Application (client) ID と一致しているか。
- 許可されたトークン受信者(Allowed Token Audiences):バックエンド API の Application ID URI(例:
api://<clientId>)を正しく登録。 - Expose an API → Authorized client applications:SPA / フロントエンドの Client ID を追加し、バックエンド API にアクセスできるよう明示。
- スコープ定義と付与:フロントから要求するスコープ(例:
api://.../.defaultや個別スコープ)が API 側に存在し、ユーザー/アプリに許可されているか。
B. スコープ/同意の再取得(consent)
ユーザーまたはテナントの同意状態が変わると、リフレッシュトークンの再発行が必要になる場合があります。ログイン URL または /.auth/refresh を呼び出す前に、prompt=consent を一度付与して同意を取り直すと改善する事例があります。
https://<app>.azurewebsites.net/.auth/login/aad?prompt=consent&post_login_redirect_uri=https%3A%2F%2F<app>.azurewebsites.net%2F
C. Cookie の SameSite 属性とプロキシの書き換え
Front Door / Application Gateway / Nginx / CDN 等の配下では、Set-Cookie の属性が意図せず改変・削除されることがあります。AppServiceAuthSession はクロスサイトでの利用ケースがあり、SameSite=None; Secure の維持が必要です。ブラウザの開発者ツールで以下を必ず確認してください。
- レスポンスに
Set-Cookie: AppServiceAuthSession=...; Path=/; HttpOnly; SameSite=None; Secureが残っている - プロキシで「Cookie 書き換え」「ヘッダー正規化」「HTTPS→HTTP ダウングレード」等が無効化されている
- バックエンド到達時も Host ヘッダーが期待どおりで、クッキードメインとの齟齬がない
ワークアラウンドとリセット
- Token Store のオフ→オン:構成変更後、一度 Token Store を Off にしてから On に戻すと、内部キャッシュがクリアされ改善することがあります。
- ブラウザクッキーの削除→再ログイン:ユーザー作業になりますが、403 の間は一時回避策として有効です。
403 切り分け早見表
| 現象 | 疑うポイント | 対処 |
|---|---|---|
| /.auth/refresh 即 403 | Client ID 不一致 / Audience 未設定 | 認証設定の Client ID と App 登録の整合/Allowed Token Audiences を見直し |
| 初回 OK、数日後から 403 | 同意の失効・スコープ変更 | prompt=consent で同意取り直し、スコープ再確認 |
| プロキシ配下のみ 403 | Cookie 属性改変 / HTTPS 統一不備 | SameSite=None; Secure を保持、HTTP を通さない、ヘッダー書き換えを無効化 |
実装レシピ:SPA / モバイル / サーバー間でのベストプラクティス
SPA(React / Vue / Angular 等)
- API コールは
withCredentials(Axios)またはcredentials: 'include'(fetch)を必ず指定。 - アプリ起動時に silent refresh をスケジュール。期限が近づく前(例:55 分)に先回りで更新。
- 401 を受信したら 1 回だけ
/.auth/refresh→ 失敗時はログインページへ。
React のフック例
import { useEffect } from 'react';
export function useEasyAuthRefresh(intervalMinutes = 55) {
useEffect(() => {
let timer = null;
const run = async () => {
try {
await fetch('/.auth/refresh', { credentials: 'include', cache: 'no-store' });
} catch (e) {
console.warn('refresh error', e);
}
};
run(); // 初回
timer = setInterval(run, intervalMinutes * 60 * 1000);
return () => clearInterval(timer);
}, [intervalMinutes]);
}
モバイル(WebView / Capacitor / MAUI Blazor 等)
- WebView 側で
/.auth/refreshをバックグラウンド GET。クッキー共有(SameSite=None; Secure)と Third-Party Cookies の扱いに注意。 - アプリ復帰(フォアグラウンド)時に即時リフレッシュするハンドラを追加。
サーバー間(App Service <-> 下流 API)
Easy Auth が Token Store のアクセストークンを更新していれば、アプリは X-MS-TOKEN-AAD-ACCESS-TOKEN を読み取り、下流 API に転送できます。
// Node.js(Express)で下流 API にプロキシする例
app.get('/api/data', async (req, res) => {
const accessToken = req.header('X-MS-TOKEN-AAD-ACCESS-TOKEN');
if (!accessToken) return res.status(401).send('no token');
const apiRes = await fetch('[https://downstream.example.com/data](https://downstream.example.com/data)', {
headers: { Authorization: `Bearer ${accessToken}` }
});
res.status(apiRes.status).send(await apiRes.text());
});
この方式ではフロントがトークンを保持しないため、XSS リスクを抑えられます(推奨)。
プロキシ配下の設計チェックリスト(Front Door / AppGW / Nginx / CDN)
| 項目 | 良い設定 | 悪い設定/NG | 備考 |
|---|---|---|---|
| プロトコル | 終端~バックエンドまで HTTPS | 途中で HTTP にダウングレード | Secure クッキー無効化や混在コンテンツの原因 |
| Cookie 書き換え | 無効(透過転送) | Name/Path/SameSite を書き換え | SameSite=None; Secure を維持 |
| ヘッダー正規化 | Set-Cookie を削らない | セキュリティ製品が Set-Cookie を除去 | WAF ルール除外を検討 |
| Host ヘッダー | フロントの FQDN を保持 | 内部ホスト名に置換 | クッキー Domain/Path と齟齬が出る |
| CORS | 正しい Origin を許可、資格情報許可 | * + 資格情報 | フロント/バックのドメインが異なる場合必須 |
運用:ログ・監視・検証のすすめ
ブラウザ検証
- Network タブで
/.auth/refreshのレスポンスにSet-Cookie: AppServiceAuthSession=...があるか。 - Cookie タブで
AppServiceAuthSessionの属性(HttpOnly / Secure / SameSite=None)が正しいか。 /.auth/meを更新後に呼び、expires_onなどのクレームが延びているか。
サーバー側ログ
- App Service の診断ログ(Authentication / Authorization)を有効化。
- Application Insights で
/.auth/refreshの 4xx 率、401レスポンスの急増をアラート。
疑似ユーザー監視
1 時間+数分おきに /.auth/refresh → /.auth/me を実行する可用性テストを作成し、Set-Cookie の有無をヘルスチェックするのが実運用では効果的です。
セキュリティとコンプライアンスの注意点
- フロントでアクセストークンをローカルストレージに保存しない(XSS リスク)。トークンは Token Store + HttpOnly クッキーで扱う。
- リフレッシュ間隔は短くしすぎない(過剰なトラフィック)。期限–5 分などのバッファを推奨。
- 長期セッション延長はユーザー行動とポリシーに応じて最小限に。MFA 再認証の要件がある場合は、セッション延長よりもフロー設計を見直す。
- 複数ドメイン(
app.example.comとapi.example.com)を跨ぐ場合は、クッキーのDomain設計と CORS を慎重に。
実践まとめ(要点)
- 更新 API は
/.auth/refresh一択。/.auth/meは状態確認専用。 - offline_access + Token Store を必ず有効にし、60 分ごとにサイレント更新。
- 403 は構成ミス・同意の失効・Cookie 改変が中心。チェックリストで早期是正。
- プロキシ配下では SameSite=None; Secure を壊さない。HTTPS 統一とヘッダー透過転送。
- 運用では「定期リフレッシュ・可用性監視・診断ログ」をセットにし、手動クッキー削除に頼らない設計に。
付録:トラブルシューティングの決定木(文字版)
症状: API が 401/403 / .auth/me が古い
├─ 実装: /.auth/refresh を定期実行しているか?
│ ├─ No → 実装する(55分間隔など)
│ └─ Yes
│ ├─ クッキー同伴(credentials: 'include')しているか?
│ └─ レスポンスに Set-Cookie があるか?
├─ 403 の場合
│ ├─ Client ID / Audience 整合性を確認(App Service & App Reg)
│ ├─ Authorized client applications にフロントの Client ID を追加
│ ├─ 一度 prompt=consent で同意を再取得
│ └─ プロキシで SameSite=None; Secure が維持されているか
└─ 依然不可
├─ Token Store を Off→On でリセット
├─ ブラウザクッキー削除→再ログイン
└─ 診断ログで 4xx の詳細・ルート原因を特定
付録:構成コマンドと設定サンプル集
Token Store とセッション延長(CLI)
# Token Store を有効化
az webapp auth update \
--resource-group <RG> \
--name <APP_NAME> \
--token-store true
# セッション延長(例:72 時間)
az webapp auth update
--resource-group
--name
--token-refresh-extension-hours 72
フロントエンド(ドメインが分離される場合)の CORS 設定
- 許可する Origin にフロントの FQDN を追加。
- 資格情報(クッキー)を使う場合は ワイルドカード(
*)を使わない。
リフレッシュの検証スクリプト(Node.js)
import fetch from 'node-fetch';
async function verify() {
const refreshRes = await fetch('https://.azurewebsites.net/.auth/refresh', {
headers: { cookie: 'AppServiceAuthSession=<...>' }
});
console.log('refresh', refreshRes.status, refreshRes.headers.get('set-cookie'));
const meRes = await fetch('https://.azurewebsites.net/.auth/me', {
headers: { cookie: refreshRes.headers.get('set-cookie') }
});
const body = await meRes.json();
console.log('me', meRes.status, body);
}
verify().catch(console.error);
よくある質問(FAQ)
Q. なぜ /.auth/me が自動更新してくれないの?
A. /.auth/me は「現在の状態を返す」だけのサイドエフェクトなし API です。トークン更新は /.auth/refresh の責務です。
Q. どのくらいの間隔で /.auth/refresh すべき?
A. アクセストークンが約 60 分で失効する前提で、55 分など少し短い間隔を推奨します。スパイクを避けたい場合は API 呼び出し前の遅延リフレッシュ(インターセプター)と併用するのが実戦的です。
Q. 403 がどうしても消えないときは?
A. Client ID と Audience の整合/同意の再取得/Cookie 属性の維持が核心です。変更後は Token Store の Off→On でキャッシュをリセットし、最後の手段としてブラウザクッキー削除→再ログインで復旧させます。
Q. B2C でも同様?
A. 基本の考え方は同じです。プロバイダー名やポリシー(ユーザーフロー)の違いに留意してください。
結論
結論は 3 つです。① トークン更新は /.auth/refresh を使う(/.auth/me は照会専用)。② 403 の多くは構成ミスと同意の失効、またはプロキシによる Cookie 改変。③ プロキシ配下では SameSite=None; Secure の維持と HTTPS 統一を最優先。これらを実践すれば、ユーザーに手動のクッキー削除を求めることなく、シームレスで安全なセッション延長が実現できます。

コメント