Compare commits

...

1 Commits

Author SHA1 Message Date
bymyself
e898a6e72f feat: hide Google SSO button in embedded webviews
Google blocks OAuth in embedded webviews with a 403
disallowed_useragent error. Add isEmbeddedWebView() utility
that detects Android WebView, iOS WKWebView, and social app
in-app browsers via UA pattern matching and JS bridge
detection.

Hide the Google SSO button in all 3 auth views when in a
webview context. GitHub SSO and email/password auth remain
unaffected.

- Fixes Comfy-Org/ComfyUI_frontend#7017
2026-03-28 23:28:42 -07:00
5 changed files with 190 additions and 2 deletions

View File

@@ -49,6 +49,7 @@
<div class="flex flex-col gap-6">
<template v-if="ssoAllowed">
<Button
v-if="!isWebView"
type="button"
class="h-10"
variant="secondary"
@@ -157,6 +158,7 @@ import type { SignInData, SignUpData } from '@/schemas/signInSchema'
import { isCloud } from '@/platform/distribution/types'
import { isHostWhitelisted, normalizeHost } from '@/utils/hostWhitelist'
import { isInChina } from '@/utils/networkUtil'
import { isEmbeddedWebView } from '@/utils/webviewDetection'
import ApiKeyForm from './signin/ApiKeyForm.vue'
import SignInForm from './signin/SignInForm.vue'
@@ -172,6 +174,7 @@ const isSecureContext = window.isSecureContext
const isSignIn = ref(true)
const showApiKeyForm = ref(false)
const ssoAllowed = isHostWhitelisted(normalizeHost(window.location.hostname))
const isWebView = isEmbeddedWebView()
const comfyPlatformBaseUrl = computed(() =>
configValueOrDefault(
remoteConfig.value,

View File

@@ -39,7 +39,12 @@
<template v-if="!showEmailForm">
<!-- OAuth Buttons (primary) -->
<div class="flex flex-col gap-4">
<Button type="button" class="h-10 w-full" @click="signInWithGoogle">
<Button
v-if="!isWebView"
type="button"
class="h-10 w-full"
@click="signInWithGoogle"
>
<i class="pi pi-google mr-2"></i>
{{ t('auth.login.loginWithGoogle') }}
</Button>
@@ -116,6 +121,7 @@ import { useFreeTierOnboarding } from '@/platform/cloud/onboarding/composables/u
import { getSafePreviousFullPath } from '@/platform/cloud/onboarding/utils/previousFullPath'
import { useToastStore } from '@/platform/updates/common/toastStore'
import type { SignInData } from '@/schemas/signInSchema'
import { isEmbeddedWebView } from '@/utils/webviewDetection'
const { t } = useI18n()
const router = useRouter()
@@ -126,6 +132,7 @@ const authError = ref('')
const toastStore = useToastStore()
const showEmailForm = ref(false)
const { isFreeTierEnabled, freeTierCredits } = useFreeTierOnboarding()
const isWebView = isEmbeddedWebView()
function switchToEmailForm() {
showEmailForm.value = true

View File

@@ -35,7 +35,7 @@
<!-- OAuth Buttons (primary) -->
<div class="flex flex-col gap-4">
<div class="relative">
<div v-if="!isWebView" class="relative">
<Button type="button" class="h-10 w-full" @click="signInWithGoogle">
<i class="pi pi-google mr-2"></i>
{{ t('auth.signup.signUpWithGoogle') }}
@@ -141,6 +141,7 @@ import { useTelemetry } from '@/platform/telemetry'
import { useToastStore } from '@/platform/updates/common/toastStore'
import type { SignUpData } from '@/schemas/signInSchema'
import { isInChina } from '@/utils/networkUtil'
import { isEmbeddedWebView } from '@/utils/webviewDetection'
const { t } = useI18n()
const router = useRouter()
@@ -158,6 +159,7 @@ const {
switchToEmailForm,
switchToSocialLogin
} = useFreeTierOnboarding()
const isWebView = isEmbeddedWebView()
const navigateToLogin = async () => {
await router.push({ name: 'cloud-login', query: route.query })

View File

@@ -0,0 +1,119 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { isEmbeddedWebView } from '@/utils/webviewDetection'
describe('isEmbeddedWebView', () => {
afterEach(() => {
vi.unstubAllGlobals()
})
describe('Android WebView', () => {
it('detects Android WebView with wv token', () => {
const ua =
'Mozilla/5.0 (Linux; Android 13; SM-G991B; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/120.0.0.0 Mobile Safari/537.36'
expect(isEmbeddedWebView(ua)).toBe(true)
})
it('does not flag regular Chrome on Android', () => {
const ua =
'Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36'
expect(isEmbeddedWebView(ua)).toBe(false)
})
})
describe('iOS WKWebView', () => {
it('detects iOS WKWebView (AppleWebKit without Safari/)', () => {
const ua =
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148'
expect(isEmbeddedWebView(ua)).toBe(true)
})
it('does not flag regular Safari on iOS', () => {
const ua =
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1'
expect(isEmbeddedWebView(ua)).toBe(false)
})
it('does not flag Chrome on iOS (CriOS)', () => {
const ua =
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0.0.0 Mobile/15E148'
expect(isEmbeddedWebView(ua)).toBe(false)
})
it('does not flag Firefox on iOS (FxiOS)', () => {
const ua =
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/120.0 Mobile/15E148'
expect(isEmbeddedWebView(ua)).toBe(false)
})
})
describe('social app in-app browsers', () => {
it('detects Facebook (FBAN)', () => {
const ua =
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 [FBAN/FBIOS;FBAV/400.0]'
expect(isEmbeddedWebView(ua)).toBe(true)
})
it('detects Instagram', () => {
const ua =
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Instagram 300.0'
expect(isEmbeddedWebView(ua)).toBe(true)
})
it('detects TikTok', () => {
const ua =
'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36 TikTok/30.0'
expect(isEmbeddedWebView(ua)).toBe(true)
})
it('detects Line', () => {
const ua =
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Line/13.0'
expect(isEmbeddedWebView(ua)).toBe(true)
})
it('detects Snapchat', () => {
const ua =
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Snapchat/12.0'
expect(isEmbeddedWebView(ua)).toBe(true)
})
})
describe('regular desktop browsers', () => {
it('does not flag Chrome desktop', () => {
const ua =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
expect(isEmbeddedWebView(ua)).toBe(false)
})
it('does not flag Firefox desktop', () => {
const ua =
'Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0'
expect(isEmbeddedWebView(ua)).toBe(false)
})
it('does not flag Safari desktop', () => {
const ua =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15'
expect(isEmbeddedWebView(ua)).toBe(false)
})
})
describe('edge cases', () => {
it('handles empty string', () => {
expect(isEmbeddedWebView('')).toBe(false)
})
})
describe('JS bridge detection', () => {
it('detects webkit.messageHandlers bridge', () => {
vi.stubGlobal('webkit', { messageHandlers: {} })
expect(isEmbeddedWebView('')).toBe(true)
})
it('detects ReactNativeWebView bridge', () => {
vi.stubGlobal('ReactNativeWebView', { postMessage: vi.fn() })
expect(isEmbeddedWebView('')).toBe(true)
})
})
})

View File

@@ -0,0 +1,57 @@
/**
* Detects whether the app is running inside an embedded webview.
*
* Google blocks OAuth via `signInWithPopup` in embedded webviews,
* returning a 403 `disallowed_useragent` error (policy since 2021).
* This utility is used to hide the Google SSO button in those contexts.
*
* Detection covers:
* • Android WebView (`wv` token in UA)
* • iOS WKWebView (has `AppleWebKit` but lacks `Safari/`)
* • Social app in-app browsers (Facebook, Instagram, TikTok, etc.)
* • JS bridge objects (`window.webkit.messageHandlers`, `ReactNativeWebView`)
*/
const SOCIAL_APP_PATTERNS =
/FBAN|FBAV|Instagram|Line\/|Snapchat|TikTok|musical_ly/i
function isAndroidWebView(ua: string): boolean {
return /\bwv\b/.test(ua) && /Android/.test(ua)
}
function isIOSWebView(ua: string): boolean {
if (!/AppleWebKit/i.test(ua)) return false
if (/Safari\//i.test(ua)) return false
if (/CriOS|FxiOS|OPiOS|EdgiOS/i.test(ua)) return false
return true
}
function isSocialAppBrowser(ua: string): boolean {
return SOCIAL_APP_PATTERNS.test(ua)
}
function hasWebViewBridge(): boolean {
try {
const win = globalThis as Record<string, unknown>
if (
typeof win.webkit === 'object' &&
win.webkit !== null &&
typeof (win.webkit as Record<string, unknown>).messageHandlers ===
'object'
) {
return true
}
if (win.ReactNativeWebView != null) return true
} catch {
// Access to bridge objects may throw in sandboxed contexts
}
return false
}
export function isEmbeddedWebView(ua: string = navigator.userAgent): boolean {
if (isSocialAppBrowser(ua)) return true
if (isAndroidWebView(ua)) return true
if (isIOSWebView(ua)) return true
if (hasWebViewBridge()) return true
return false
}