29 // journal

Why Google sign-in fails inside a WebView, and what the fix actually is

engineering · 2026-11 · 6 min read · engineering · auth · oauth · security

journal/oauth-webviews
Why Google sign-in fails inside a WebView, and what the fix actually is

fig.01 · embedded vs external user-agent, per rfc 8252's own definitions

Your Google sign-in worked in the simulator for months, then started returning an error screen inside the login sheet. The error is disallowed_useragent, and it is not a bug: Google's authorization endpoint detected that your app is showing the consent page inside a WebView it controls, and refused. The rule behind it is not "use a browser instead of a webview." It is subtler and more useful than that, and getting the distinction right is the difference between fighting the APIs and using them.

The constraint

OAuth's threat model assumes the user is typing credentials into a surface the relying app cannot see. A native app embedding WKWebView or android.webkit.WebView breaks that assumption completely: the webview lives inside the app's process, and the app can inspect every pixel, inject scripts, and read every cookie. Google's 2021 policy notice states it plainly: "an application embedding a webview can modify or intercept network requests, insert custom scripts that can potentially record every keystroke entered in a login form, access session cookies, or alter the content of the webpage."

The options

Embedded WebView. The path of least resistance and the one the RFC and Google's endpoint banned. Google's timeline: announced August 2016, new OAuth clients blocked from October 20, 2016, all clients blocked from April 20, 2017, and a 2021 secure-browser policy made the block total with stragglers cleaned up through early 2023. Rejected, permanently.

Kick out to the system browser. RFC-compliant and the worst UX of the three: app switches to Safari, user logs in, redirect bounces back, and the user wonders whether they left your app. It survives as a fallback, not a default, since iOS has offered an in-app equivalent since 2015.

In-app browser tabs. The sanctioned path: ASWebAuthenticationSession on iOS and Chrome Custom Tabs on Android, wrapped by AppAuth (iOS, macOS, Android) or your platform's auth SDK. These look embedded but are not, and that difference is the entire post.

The mechanism

RFC 8252 draws the line by security domain, not by pixels. An external user-agent is "a separate entity or security domain to the native app making the request, such that the app cannot access the cookie storage, nor inspect or modify page content." An embedded user-agent "forms a part of the app or shares the same security domain such that the app can access the cookie storage and/or inspect or modify page content."

From those definitions fall the normative rules: "native apps MUST use an external user-agent to perform OAuth authorization requests" and "MUST NOT use embedded user-agents," while authorization endpoints "MAY take steps to detect and block authorization requests in embedded user-agents." Google did exactly that. The RFC's reasoning matches Google's: the host app "can record every keystroke entered in the login form to capture usernames and passwords, automatically submit forms to bypass user consent, and copy session cookies and use them to perform authenticated actions as the user."

This is why ASWebAuthenticationSession is compliant while WKWebView is not, even though both render a web page inside your app's frame. Apple's docs describe it as "a secure, embedded web view" on iOS, but the embedding is visual only: the sheet runs in the system's security domain, shows the user which domain the app is authenticating with, and "ensures that only the calling app's session receives the authentication callback." Your app cannot read its cookies or inject a line of script. Same for Custom Tabs: Chrome renders the page, your app gets the redirect. SFSafariViewController shares the trait; WKWebView never will.

The other edge the RFC carves out: prefersEphemeralWebBrowserSession on ASWebAuthenticationSession asks the browser not to share cookies with the user's normal session, which exists for the cases where you deliberately want zero SSO carryover.

Where it bites

"It worked on my test account" is the standard story. The block is per-client and rolled out in phases, so a legacy OAuth client could limp along for years while a freshly created one was refused on day one. If your bug report says "Google sign-in is broken," check whether the app renders the consent page inside a WebView before touching anything else; disallowed_useragent is Google's own label for exactly this.

The block is policy, not detection-proof. The endpoint blocks what it can detect, and the RFC grants it that permission explicitly. Do not design around the detector; design into the compliant API, because the policy text covers "software libraries that allow a developer to insert arbitrary scripts, alter the default routing of a request to the Google OAuth server, or access session cookies," and workarounds age badly.

Apple has no equivalent store rule. Nothing in the App Review Guidelines bans embedded-webview OAuth; the enforcement you feel is Google's endpoint and the RFC. What Apple contributes is the compliant primitive: ASWebAuthenticationSession exists precisely to keep the flow inside the app without handing the app the keys.

Not every browserless device is exempt. TVs and input-limited hardware are not a webview loophole; they get the OAuth 2.0 Device Authorization Grant (RFC 8628), a different flow entirely, which AppAuth implements for tvOS.

The numbers

  • August 22, 2016: Google announces the webview ban, citing security and broken SSO.
  • October 20, 2016 / April 20, 2017: new clients blocked, then all clients, per the same announcement.
  • September 30, 2021: the secure-browser policy closes the remaining gap.
  • February 6, 2023: disallowed_useragent blocking reaches the grandfathered stragglers, per notification emails Google sent to affected developers.
  • October 2017: RFC 8252 publishes, co-authored by the same Google PM who wrote the 2016 post, turning one vendor's policy into the industry best practice.

Our position

If the sign-in surface is a webview your app controls, it is wrong no matter how polished it looks. Ship ASWebAuthenticationSession or Custom Tabs (via AppAuth or the platform SDK, which is what google_sign_in and flutter_appauth wrap anyway) and treat any embedded consent page as a credential-harvesting surface by definition, because RFC 8252 does. The UX argument for webviews evaporated the day in-app browser tabs existed; what remains is only the code smell.

The takeaway

TEST
consent page inside a wkwebview/webview you control?
then it is already wrong, working or not
RULE
rfc 8252: external user-agent required, embedded
banned; endpoint may detect and refuse
ERROR
disallowed_useragent means the endpoint saw an
embedded agent; not a transient failure
FIX
aswebauthenticationsession on ios, custom tabs on
android; appauth wraps both
TRAIT
compliant = outside your security domain: no cookie
access, no script injection, no content inspection
BONUS
ephemeral session flag kills sso carryover when you
actually want zero shared state
EDGE
tvs and browserless devices use the device
authorization grant, not a webview exception
STORE
apple writes no rule banning it; the endpoint does
the banning

Auth surface decisions like this are the kind we make in the first week of a custom software build, before the login screen is designed. If your app's OAuth flow predates the rule, send us the client.

// next step

We work this way on client projects too.

Tell us what you are making. We reply with a plan, a timeline, and a price.

← the journal