# 已安装 PWA 的登录

> 配置已安装 PWA 登录页的 OAuth 重定向、条件式通行密钥自动填充与 manifest scope 回调路径。

OAuth 回调可以留在 manifest scope 内；条件式通行密钥 mediation 不可用时，保留密码表单供用户登录。

## 浏览器与平台支持

RFC 10017 定义浏览器应用的 OAuth 要求。MDN 将
`PublicKeyCredential.isConditionalMediationAvailable()` 标为仅限安全上下文，并记录其自
2023 年 10 月起跨浏览器广泛可用。manifest 的 `scope` 成员定义已安装体验包含的 URL；
MDN 说明浏览器允许导航到 scope 外，但可能显示 URL 和浏览器控件。

## 配置回调路径

RFC 10017 要求浏览器中的公共客户端使用带 PKCE 的授权码模式，还要求客户端注册重定向 URI，
并只使用与注册值准确匹配的重定向 URI。应用位于 `/app/` 下，且回调需要留在 manifest scope 内时，把回调也放在这个路径下：

```json
{
  "name": "Field Notes",
  "start_url": "/app/",
  "scope": "/app/",
  "display": "standalone"
}
```

向授权服务器注册 `https://example.com/app/oauth/callback`。在此示例中，
`https://example.com/oauth/callback` 位于 manifest scope 外，不过浏览器不会阻止该导航。

:::security
RFC 10017 要求浏览器应用保护重定向 URI，防止跨站请求伪造。它列出的防护包括强制 PKCE、
唯一且经过验证的 OAuth `state` 值，以及在使用 OpenID Connect 时验证 `nonce`。
:::

## 示例

OAuth 示例把授权请求的构造和 PKCE verifier 的保存交给应用服务器。通行密钥不可用时，用户仍可使用表单。

### 启动 OAuth 重定向

按钮从应用服务器请求授权 URL，再导航当前顶层窗口。错误分支重新启用按钮，并保留密码表单。

```html
<button id="oauth" type="button">使用身份提供方继续</button>
<p id="oauth-status" role="status"></p>

<script type="module">
  const button = document.querySelector('#oauth');
  const status = document.querySelector('#oauth-status');

  button.addEventListener('click', async () => {
    button.disabled = true;
    try {
      const response = await fetch('/app/api/oauth/start', { method: 'POST' });
      if (!response.ok) throw new Error(`OAuth start failed: ${response.status}`);
      const { authorizationUrl } = await response.json();
      location.assign(authorizationUrl);
    } catch (error) {
      status.textContent = '请使用邮箱和密码登录。';
      button.disabled = false;
    }
  });
</script>
```

重定向返回 `/app/oauth/callback` 后，用创建 PKCE verifier 的同一事务处理授权响应。

### 检测条件式通行密钥支持

web.dev 的通行密钥自动填充指南要求在用户名字段加入 `webauthn` token，并以
`mediation: 'conditional'` 调用 `navigator.credentials.get()`。先按 MDN 检测特性；
不支持或失败的路径都会保留普通表单。

```html
<form action="/app/session" method="post">
  <label for="username">邮箱</label>
  <input id="username" name="username" autocomplete="username webauthn" required>
  <label for="password">密码</label>
  <input id="password" name="password" type="password" autocomplete="current-password">
  <button>登录</button>
</form>

<script type="module">
  async function offerPasskey() {
    if (!('PublicKeyCredential' in window) ||
        !('credentials' in navigator) ||
        !PublicKeyCredential.isConditionalMediationAvailable ||
        !PublicKeyCredential.parseRequestOptionsFromJSON ||
        !PublicKeyCredential.prototype.toJSON) {
      return; // 保留密码表单作为回退路径。
    }

    if (!(await PublicKeyCredential.isConditionalMediationAvailable())) {
      return; // 保留密码表单作为回退路径。
    }

    const response = await fetch('/app/api/passkey/options');
    if (!response.ok) return;
    const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(
      await response.json(),
    );
    const credential = await navigator.credentials.get({
      publicKey,
      mediation: 'conditional',
    });
    if (!credential) return;

    const verification = await fetch('/app/api/passkey/verify', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(credential.toJSON()),
    });
    if (verification.ok) location.assign('/app/');
  }

  offerPasskey().catch(() => {
    // 条件式请求被拒绝时仍启用密码表单。
  });
</script>
```

显式能力检测和每个提前返回分支都会在运行时保留密码表单作为回退。服务器提供 WebAuthn 请求
选项并验证返回的凭据。MDN 记录了上面使用的 JSON 转换方法，并说明 relying-party 域无效时，
可用性检测可能以 `SecurityError` 拒绝。

:::caution
当 relying-party 域无效时，`isConditionalMediationAvailable()` 可能以 `SecurityError` 拒绝，
而不是返回 `false`；详见 [MDN](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/isConditionalMediationAvailable_static#exceptions)。
保留拒绝处理程序，使密码表单在这条路径上仍然可用。
:::

## 失败模式

- 与注册值不同的重定向 URI 违反 RFC 10017 的准确匹配规则。
- manifest `scope` 外的回调仍可导航，但 MDN 说明浏览器可能为 scope 外页面增加 URL 或导航控件。
- 条件式 mediation 检测不存在或返回 `false` 时，本示例使用密码表单。
- 检测成功不会选择凭据；按照 MDN 的定义，它只报告条件式 mediation 是否可用。

## 另请参阅

- [让 PWA 可安装](/zh/guides/installable/)
- [WebAuthn 与通行密钥浏览器支持](/zh/compatibility/webauthn/)
- [scope manifest 成员](/zh/reference/manifest/scope/)
- [OAuth 2.0 for Browser-Based Applications](https://www.rfc-editor.org/rfc/rfc10017.html)（RFC Editor）
- [PublicKeyCredential: isConditionalMediationAvailable()](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/isConditionalMediationAvailable_static)（MDN）
- [Sign in with a passkey through form autofill](https://web.dev/articles/passkey-form-autofill)（web.dev）