# ERR-20260520-001: OIDC callback 在 reverse proxy 后面拼出 http:// callbackUrl，触发 Entra AADSTS500112 reply address mismatch

## 症状

SSO 登录走完 Microsoft 端 → callback 回我们后端 → 后端 token 兑换被 Entra 拒：

```
AADSTS500112: The reply address 'http://slot-1.chentao.test.jiachentao.com/api/v1/auth/sso/callback'
does not match the reply address 'https://slot-1.chentao.test.jiachentao.com/api/v1/auth/sso/callback'
provided when requesting Authorization code.
```

注意 scheme 不一致：authorize 阶段是 `https://`，token 兑换阶段后端拼成了 `http://`。

## 根因

`backend/src/modules/organization/auth/auth.controller.ts` ssoCallback 构造 callbackUrl 时用了
`${req.protocol}://${req.get('host')}`。但在 Caddy / Nginx 等 reverse proxy 后面，
**Express 默认 `req.protocol = 'http'`**（拿的是 socket 直连协议），不读 `X-Forwarded-Proto` 头。

调用链：
- 浏览器 → https://slot-1.chentao.test.jiachentao.com/...
- Caddy 反代 → http://host.docker.internal:3301/... （Caddy 加 `X-Forwarded-Proto: https` 头）
- Express → `req.protocol = 'http'`（默认不信 proxy 头）
- callbackUrl 拼成 `http://slot-1.chentao.test.jiachentao.com/...`
- token 兑换 POST 给 Entra 的 redirect_uri 参数 = http://
- Entra 比对：authorize 阶段提交的 `https://` ≠ token 阶段提交的 `http://` → 500112

## 修法（已采用 A）

### A. 用 env 的 redirect_uri origin 拼 callbackUrl（不依赖 req.protocol）

```typescript
// before
const callbackUrl = new URL(
  req.originalUrl || req.url,
  `${req.protocol}://${req.get('host')}`,
);

// after
const expectedRedirect = new URL(this.ssoConfig.redirectUri);
const callbackUrl = new URL(
  req.originalUrl || req.url,
  `${expectedRedirect.protocol}//${expectedRedirect.host}`,
);
```

`AZURE_REDIRECT_URI` env 是 authorize 阶段就提交给 Entra 的 redirect_uri，token 阶段强制用同一个
origin 拼可保证 100% 字符串一致。path + query 仍从 `req.originalUrl` 取（含 `code` / `state` 参数）。

### B. 备选：`app.set('trust proxy', true)`

在 main.ts 加 `app.set('trust proxy', true)`，让 Express 信任 X-Forwarded-Proto / X-Forwarded-For。
- 优点：全局生效，任何依赖 req.protocol / req.ip 的代码都受益
- 缺点：影响面广（IP rate limiting / audit log IP 等），改动需要全局回归测试
- 适用：本项目长期应该走 B，但本期 SSO PR 不该 sneak in 全局改动

## 触发条件

任何 OIDC / OAuth2 流程 + 后端在 reverse proxy 后面（Caddy / Nginx / Cloudflare / AWS ALB）+
用 `req.protocol` 拼 callback URL → 必中。

## 防御

实施 OIDC 时**永远用 env 的 redirect_uri 作为事实源**，不要靠 `req.protocol` / `req.get('host')`
推断。两个原因：
1. reverse proxy 经常隐藏真实 scheme（如本案）
2. 同一后端可能挂多个 vhost（同 cluster），`req.get('host')` 拿到哪个不可控

## 关联

- 工单：#334 Entra ID SSO 接入
- Microsoft docs: https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/app-integration/error-code-aadsts500112-aadsts50011-reply-address-mismatch
- 项目级 reverse proxy: `/home/chentao/caddy-config/Caddyfile` (用户 memory `reference_dev_machine_caddy_domain`)
