---
url: 'https://www.corbado.com/blog/passkeys-in-app-browsers'
title: 'Passkeys in In-App Browsers: Why Logins break and how to fix it'
description: 'Passkeys often break inside social-app in-app browsers (Instagram, TikTok, Facebook). Learn why it happens, how to detect WebView traffic and how to route users to a method that works.'
lang: 'en'
author: 'Vincent Delitz'
date: '2026-06-22T07:59:33.514Z'
lastModified: '2026-06-22T07:59:33.514Z'
keywords: 'passkeys in-app browser, in-app browser login, webauthn webview'
category: 'Passkeys Strategy'
---

# Passkeys in In-App Browsers: Why Logins break and how to fix it

## 1. Introduction

If a meaningful share of your traffic arrives from social feeds, some of your consumers
never reach your login page in a real browser. They tap a link inside Instagram, TikTok,
Facebook or X and your page opens inside the app's **in-app browser** - an embedded
[WebView](https://www.corbado.com/blog/native-app-passkeys). That environment quietly breaks the login method you
most want them to use: [passkeys](https://www.corbado.com/glossary/passkey).

This hurts most when you have meaningful social ad spend. You are paying for every tap on
that Instagram, TikTok or Facebook ad, and the whole point is that those users land, sign
up and convert. If they drop inside the in-app browser before they can authenticate, you
are burning ad budget on traffic that never had a working path to log in - so the friction
is not just a UX problem, it is wasted acquisition cost.

The failure is almost invisible. The consumer does not file a ticket saying "the passkey
button did nothing inside Instagram." They bounce, and in your funnel it looks like
generic [login friction](https://www.corbado.com/blog/login-friction-kills-conversion). Below: the exact technical
reasons passkeys break in in-app browsers, why standard analytics cannot see it, how to
detect the environment reliably and what to do so those users still get in.

## Key Facts

- In-app browsers are embedded WebViews (**WKWebView** on iOS, **Android System
    WebView**), not the system browser - they run inside the host app and inherit its
    restrictions
    - [WebAuthn](https://www.corbado.com/glossary/webauthn) behaviour inside embedded WebViews is **inconsistent
    and subject to platform-specific bugs**, and [Conditional UI](https://www.corbado.com/glossary/conditional-ui)
    (passkey autofill) is not supported in a WebView - so passkey prompts can silently fail
    to appear
    - On iOS, WKWebView uses an **isolated cookie store** separate from Safari and requires
    Associated Domains plus an AASA file to use passkeys at all - a third-party site's
    passkeys generally will not work inside another app's WebView
    - In-app browsers are **detectable from the User-Agent** (e.g. `FBAN`/`FBAV` for
    Facebook, `Instagram`, `BytedanceWebview`/`musical_ly` for TikTok)
    - These failures happen on the client before any request reaches your backend, so
    **backend logs cannot see them** - this is the same blind spot
    [authentication observability](https://www.corbado.com/blog/authentication-observability) is built to close

## 2. What an In-App Browser actually is

When a user taps a link inside a social app, the app does not hand them off to Safari or
Chrome. It renders the page inside an **embedded WebView** that lives in the host app's
process. On iOS that is `WKWebView`; on Android it is the System WebView component. The
host app, not the user, decides what that WebView is allowed to do.

This matters because there is a second, very different way an app can show web content. The
comparison below contrasts the two containers - and that difference is the whole story:

An **embedded WebView** (`WKWebView`, Android System WebView) runs inside the host app with an
isolated cookie/session store, hides the URL bar and exposes a limited and inconsistent set of
browser capabilities - this is what most social feeds use for their in-app browser. A
**system-browser handoff** (`SFSafariViewController` / `ASWebAuthenticationSession` on iOS,
**Chrome Custom Tabs** on Android) instead uses the real browser engine, shares the system
session, shows the URL with SSL indicators and supports WebAuthn - the trustworthy path for
authentication.

The whole problem comes down to the first category. Passkeys assume real-browser WebAuthn
capabilities and an association with your own domain. An embedded WebView gives you
neither.

## 3. Why Passkeys break inside In-App Browsers

Passkeys depend on [WebAuthn](https://www.corbado.com/glossary/webauthn), and WebAuthn depends on browser and
platform capabilities that embedded WebViews do not reliably provide. The picture is more
nuanced than "WebViews never support WebAuthn": modern in-app browsers often *attempt*
WebAuthn, but the behaviour is inconsistent and breaks in ways that are hard to see.

**WebAuthn support is inconsistent.** An embedded WebView may not expose a fully functional
[WebAuthn](https://www.w3.org/TR/webauthn-2/) API at all, and even where it does, a passkey
ceremony can throw an error or silently do nothing. The host app - not a real browser
engine - controls the WebView, so there is no guarantee it implements the full ceremony the
way Safari or Chrome does. The practical takeaway: a passkey path that works in the system
browser cannot be assumed to work inside a host app's WebView.

**Conditional UI does not work.** [Conditional UI](https://www.corbado.com/glossary/conditional-ui) - the passkey
autofill that surfaces credentials directly in the username field - is not supported inside
a WebView. So even the smoothest passkey entry point you have built is dead there.

**Passkeys are scoped to the host app, not your site.** On iOS, using passkeys inside a
WKWebView requires Associated Domains entitlements and an AASA file - and those belong to
the *host app* (Instagram, TikTok), not to you, the [relying party](https://www.corbado.com/glossary/relying-party).
A consumer arriving at your domain inside someone else's app cannot use your passkeys,
because the WebView is not associated with your domain.

The net effect: to the consumer it looks like a broken button; to your funnel it looks
like an unexplained abandon. The fast, passwordless login you would normally lean on is
precisely the one that breaks here, pushing the consumer toward passwords or
[OTP](https://www.corbado.com/blog/webauthn-errors), the slowest and most abandon-prone routes.

Social login degrades in the same in-app-browser environments too, for separate reasons -
we cover that in detail in [social login conversion rate](https://www.corbado.com/blog/social-login-conversion-rate).

## 4. An emerging exception: app-injected device-bound passkeys

In-app browsers are not always a dead end anymore. At Identiverse, PayPal presented a
workaround built together with Meta: when a Meta app's WebView redirects to `paypal.com`,
the host app conditionally injects a WebAuthn-compliant custom credentials API into the
in-app browser, so `navigator.credentials` becomes available where it normally is not. That
injected API talks directly to the device hardware to create and store the key pair, making
it a real, hardware-backed - but **device-bound** - [passkey](https://www.corbado.com/glossary/passkey).

It works, but the trade-offs are significant:

- **No sync (today).** The credential started out device-bound, exactly as WebAuthn Level 1
  was: it never reaches iCloud Keychain or Google Password Manager and does not follow the
  user to another device. Broader cloud-backed support is a stated direction, not the
  current state.
- **A separate passkey per host app.** Each app registers its own credential, tied to that
  app's identifier - so the passkey created inside Instagram's WebView is not the one inside
  Facebook's WebView, even on the same device. The user ends up with one silo per app.
- **Enrollment first, every time.** Because nothing is synced, the user has to create the
  passkey in each app context, so the core synced-passkey promise ("you already have it
  everywhere") does not apply.
- **Trust shifts to the host app.** Since the host app injects the credentials API, a fake
  implementation (for example a forged `paypal.com` passkey) is conceivable. The protection
  is server-side: the [relying party](https://www.corbado.com/glossary/relying-party) still verifies every
  assertion, so a forged credential fails verification. Note this is not OAuth - PayPal acts
  as its own identity provider here.

For most teams this is not a drop-in fix: it needs host-app cooperation plus a custom,
standards-compliant integration - the same approach PayPal
[demonstrated at Authenticate 2025](https://www.youtube.com/watch?v=fUjavqs-69M). But it is
a useful signal of where the ecosystem is heading - and turns "WebViews break passkeys" into
"WebViews break passkeys unless the host app explicitly bridges them."

## 5. Why your Analytics cannot see it

This is the part that makes in-app-browser friction so dangerous: it is structurally
invisible to the tooling most teams trust.

A passkey ceremony that never starts, or fails silently on the client, produces **no
successful backend call**. Your identity provider logs and server-side dashboards only
record requests that reach them.
The failure happens one layer earlier, on the device, so it leaves no trace in the place
you are looking.

This is the same gap that authentication observability exists to close: the difference
between "was the login successful?" and "how was it
successful, and if it failed, where and why?" Without client-side telemetry, in-app-browser
abandons are silently bucketed into generic drop-off, and you optimize everything except the
actual cause.

## 6. How to detect In-App Browser traffic

The good news: in-app browsers identify themselves. The host app appends recognizable
tokens to the `User-Agent` string, so you can detect the environment on page load, before
you render the login options.

The exact tokens vary by app and platform, but the major social and messaging apps each
leave a recognizable fingerprint. The most useful ones to match:

- **Facebook:** `FBAN`, `FBAV`, `FB_IAB`, `FBIOS`, `FB4A`
- **Facebook Messenger:** `MessengerForiOS`, `Orca-Android`
- **Instagram:** `Instagram`
- **Threads:** `Barcelona` (Threads' internal codename in the UA)
- **TikTok:** `BytedanceWebview`, `musical_ly`, `Trill`, `TikTok`, `aweme`
- **X (Twitter):** `Twitter`, `TwitterAndroid`
- **LinkedIn:** `LinkedInApp`
- **Snapchat:** `Snapchat`
- **Pinterest:** `Pinterest`
- **Reddit:** `Reddit`
- **WhatsApp:** `WhatsApp`
- **WeChat:** `MicroMessenger`
- **LINE:** `Line/`
- **Telegram:** `Telegram`
- **KakaoTalk:** `KAKAOTALK`
- **Weibo:** `Weibo`
- **Naver:** `NAVER`
- **Baidu:** `baiduboxapp`
- **Google app (GSA):** `GSA`

A more complete detection helper, covering the apps above and returning which one was
matched so you can segment by it later:

```js
const IN_APP_BROWSER_RULES = [
  { name: 'instagram', test: /Instagram/i },
  { name: 'messenger', test: /Messenger(ForiOS)?|Orca-Android/i },
  { name: 'threads', test: /Barcelona/i },
  { name: 'facebook', test: /FBAN|FBAV|FB_IAB|FBIOS|FB4A/i },
  { name: 'tiktok', test: /BytedanceWebview|musical_ly|Trill|TikTok|aweme/i },
  { name: 'twitter', test: /Twitter|TwitterAndroid/i },
  { name: 'linkedin', test: /LinkedInApp/i },
  { name: 'snapchat', test: /Snapchat/i },
  { name: 'pinterest', test: /Pinterest/i },
  { name: 'reddit', test: /Reddit/i },
  { name: 'whatsapp', test: /WhatsApp/i },
  { name: 'wechat', test: /MicroMessenger/i },
  { name: 'line', test: /\bLine\//i },
  { name: 'telegram', test: /Telegram/i },
  { name: 'kakaotalk', test: /KAKAOTALK/i },
  { name: 'weibo', test: /Weibo/i },
  { name: 'naver', test: /NAVER/i },
  { name: 'baidu', test: /baiduboxapp/i },
  { name: 'google', test: /\bGSA\b/i },
];

function getInAppBrowser(ua = navigator.userAgent) {
  return IN_APP_BROWSER_RULES.find(rule => rule.test.test(ua))?.name ?? null;
}
```

Order the rules from most to least specific: Instagram, Messenger and Threads are checked
before generic Facebook tokens because Meta apps share parts of the `FBAN`/`FBAV` family,
and you want the more precise label.

User-Agent matching is heuristic, not perfect, so two rules apply. First, **maintain it**:
host apps change their strings over time, so treat the list as something you review, not
set-and-forget. Second, **measure it**: tag every login session with the detected in-app
browser (or `null`) and feed that tag into your analytics so the failure mode finally
becomes a number you can see. Two signals are especially diagnostic when segmented by this
tag:

- **Method switching** - users who start with a passkey and then fall back to password or
  OTP within the same session. A concentration of switching inside in-app sessions is the
  fingerprint of this problem.
- **Drop-off by entry touchpoint** - if sessions referred from social apps convert far
  worse than direct traffic, in-app browsers are a prime suspect.

## 7. What to do about it

You cannot make social apps fix their WebViews, so every durable fix is on your side.

1. **Detect early, before rendering the login UI.** Run the User-Agent check on load so you
   can adapt the screen for in-app sessions instead of showing controls that will fail.
2. **Do not lead with a method that cannot work.** If you detect a true embedded WebView,
   deprioritize the passkey button for that session and lead with a method that works
   reliably there, such as an email magic link or OTP. The consumer should never be left
   staring at a dead button. (Keep a graceful path back to passkeys once they are in a
   capable browser - see [passkey fallback and recovery](https://www.corbado.com/blog/passkey-fallback-recovery).)
3. **Offer "Open in browser."** Most in-app browsers expose an "open in Safari/Chrome"
   affordance. A small, well-placed prompt to open the page in the real browser restores
   full passkey support.
4. **If the traffic is from your own app, use the right container.** When you control the
   app, never wrap authentication in a bare WebView. Use `SFSafariViewController` /
   [`ASWebAuthenticationSession`](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession)
   on iOS or **Chrome Custom Tabs** on Android, which use the real browser engine and
   support WebAuthn. The [native app passkeys guide](https://www.corbado.com/blog/native-app-passkeys) covers this
   in depth.
5. **Measure the before/after.** Track in-app-browser login success as its own metric so you
   can prove a fix moved the number rather than guessing, and so any future regression in this
   environment surfaces as a sudden drop instead of going unnoticed.

## 8. How Corbado helps

Two of the hard parts here are visibility and routing, and both map to what Corbado does.
[Corbado Observe](https://www.corbado.com/blog/authentication-observability) provides the client-side telemetry to
segment login outcomes by browser, OS and entry point - so in-app-browser friction shows up
as a measurable cohort instead of vanishing into generic drop-off. On the routing side,
detecting the environment and presenting the method most likely to succeed for that session
is exactly the kind of data-driven decision passkey-first login should be making.

If you are running passkeys at consumer scale and want to see where logins quietly fail
before they reach your backend, this is the visibility layer that surfaces it.

## 9. Conclusion

In-app browsers are a structural, not occasional, source of login failure for any product
with social traffic, and they specifically break passkeys - the method you would otherwise
rely on as your fast path. Passkeys fail because WebViews lack reliable WebAuthn and
Conditional UI and are not associated with your domain. The failures happen on the client,
so backend logs never see them.

The fix is a sequence: gain visibility by tagging in-app-browser sessions, then handle them
gracefully by detecting the environment and routing users to a method that works, with a
clear path back to a real browser. Teams that do neither keep losing conversions to a
failure mode they cannot even see.

## FAQ

### Do passkeys work in in-app browsers like Instagram or TikTok?

Often not reliably. Social-app in-app browsers are embedded WebViews (`WKWebView` on iOS,
Android System WebView), where WebAuthn behaviour is inconsistent and Conditional UI (passkey
autofill) is not supported. A passkey ceremony may throw an error or silently do nothing, so
the consumer sees a button that appears to do nothing.

### How do I detect if a user is in an in-app browser?

Inspect the `User-Agent` string for host-app tokens: `FBAN`, `FBAV` and `FB_IAB` for
Facebook, `Instagram` for the Instagram app and `BytedanceWebview`, `musical_ly` or `trill`
for TikTok. Tag every login session with whether it originated in an in-app browser so the
failure mode becomes visible in your analytics.

### What is the difference between an in-app browser and Chrome Custom Tabs?

An embedded WebView (`WKWebView`, Android System WebView) runs inside the host app with an
isolated session and limited browser features. Chrome Custom Tabs on Android and
`SFSafariViewController` / `ASWebAuthenticationSession` on iOS use the real browser engine,
share the system session and support WebAuthn, so passkeys work as expected.
