# Sign-in in installed PWAs

> Configure OAuth redirects, conditional passkey autofill, and manifest scope for sign-in pages opened in installed PWAs.

OAuth callbacks can stay within the manifest scope, and a password form can provide
a fallback when conditional passkey mediation is unavailable.

## Browser and platform support

RFC 10017 defines OAuth requirements for browser-based applications. MDN marks
`PublicKeyCredential.isConditionalMediationAvailable()` as available only in secure contexts and
as widely available across browsers since October 2023. The manifest `scope` member defines which
URLs belong to the installed experience; MDN notes that browsers allow out-of-scope navigation but
may show the URL and browser controls.

## Configure the callback path

RFC 10017 requires browser-based public clients to use the Authorization Code grant with PKCE. It
also requires clients to register redirect URIs and use exact registered redirect URIs. For an app
under `/app/`, place its callback under that path when it should remain in the manifest scope:

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

Register `https://example.com/app/oauth/callback` with the authorization server. A callback at
`https://example.com/oauth/callback` is outside this example's manifest scope, although the browser
does not block that navigation.

:::security
RFC 10017 requires browser-based applications to protect the redirect URI against cross-site
request forgery. Its listed defenses include enforced PKCE, a unique and verified OAuth `state`
value, or a verified OpenID Connect `nonce` when OpenID Connect is in use.
:::

## Examples

The OAuth example delegates construction of the authorization request and storage of the PKCE
verifier to the application server. The passkey example preserves the form as its fallback.

### Start an OAuth redirect

The button requests an authorization URL from the application server, then navigates the current
top-level window. The error branch restores the button and leaves the password form in place.

```html
<button id="oauth" type="button">Continue with identity provider</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 = 'Use email and password to sign in.';
      button.disabled = false;
    }
  });
</script>
```

After the redirect returns to `/app/oauth/callback`, process the authorization response using the
same transaction that created the PKCE verifier.

### Detect conditional passkey support

The web.dev passkey autofill guide adds the `webauthn` token to the username field and calls
`navigator.credentials.get()` with `mediation: 'conditional'`. Run MDN's feature
checks first and keep the ordinary form enabled if support is missing or a call fails.

```html
<form action="/app/session" method="post">
  <label for="username">Email</label>
  <input id="username" name="username" autocomplete="username webauthn" required>
  <label for="password">Password</label>
  <input id="password" name="password" type="password" autocomplete="current-password">
  <button>Sign in</button>
</form>

<script type="module">
  async function offerPasskey() {
    if (!('PublicKeyCredential' in window) ||
        !('credentials' in navigator) ||
        !PublicKeyCredential.isConditionalMediationAvailable ||
        !PublicKeyCredential.parseRequestOptionsFromJSON ||
        !PublicKeyCredential.prototype.toJSON) {
      return; // Keep the password form as the fallback.
    }

    if (!(await PublicKeyCredential.isConditionalMediationAvailable())) {
      return; // Keep the password form as the fallback.
    }

    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(() => {
    // Keep the password form enabled if the conditional request rejects.
  });
</script>
```

The explicit capability test and each early return keep the password form as the runtime fallback.
The server supplies the WebAuthn request options and verifies the returned credential. MDN documents
the JSON conversion methods used above and says the availability check may reject with
`SecurityError` when the relying-party domain is invalid.

:::caution
`isConditionalMediationAvailable()` can reject with `SecurityError` rather than return `false` when
the relying-party domain is invalid, as documented by [MDN](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/isConditionalMediationAvailable_static#exceptions).
Keep the rejection handler shown above so the password form remains usable on that path.
:::

## Failure modes

- A redirect URI that differs from the registered value violates RFC 10017's exact-match rule.
- A callback outside manifest `scope` remains navigable, but MDN says browsers may add URL or
  navigation controls for the out-of-scope page.
- A missing or false conditional-mediation check means this example uses the password form.
- A successful availability check does not select a credential; it only reports whether conditional
  mediation is available, as defined by MDN.

## See also

- [Make a PWA installable](/guides/installable/)
- [WebAuthn and passkeys browser support](/compatibility/webauthn/)
- [scope manifest member](/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)