mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
This commit is contained in:
@@ -310,32 +310,55 @@ fn popup_should_stay_in_app(provider: &str, url: &Url) -> bool {
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
"linkedin" => {
|
||||
// LinkedIn's "Sign in with Google" button is rendered as a Google
|
||||
// Identity Services (GSI) iframe loaded from
|
||||
// `accounts.google.com/gsi/button`. When the user clicks it, GSI
|
||||
// calls `window.open("https://accounts.google.com/gsi/select?...",
|
||||
// "gsig", "width=500,height=600,...")` to show the account chooser.
|
||||
//
|
||||
// This popup MUST stay as a real in-app child window — NOT routed
|
||||
// to the system browser (blank screen) and NOT a parent navigation
|
||||
// (the parent page would be replaced, so the postMessage credential
|
||||
// callback from the popup can never reach LinkedIn's JS handler).
|
||||
//
|
||||
// After the user selects an account the popup postMessages the
|
||||
// signed credential back to the opener; LinkedIn's GSI callback
|
||||
// receives it and completes sign-in (#1021).
|
||||
match url.host_str() {
|
||||
Some(host) => {
|
||||
is_google_sso_host(host) && url.path().to_ascii_lowercase().contains("gsi")
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
// LinkedIn (#1021) and X/Twitter (#5009) render "Sign in with Google"
|
||||
// as a Google Identity Services (GSI) popup that the GIS SDK drives via
|
||||
// window.open() and completes by postMessage-ing the credential back to
|
||||
// window.opener (the provider page). Every leg of that popup — the
|
||||
// account chooser and the /o/oauth2 auth hop — MUST stay as a real
|
||||
// in-app child window: routing it to the system browser leaves the
|
||||
// embedded webview on a dead page, and navigating the parent to it
|
||||
// destroys the opener so the credential can never post back. See
|
||||
// `is_google_gsi_popup`.
|
||||
"linkedin" | "twitter" => is_google_gsi_popup(url),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// `true` for a Google Identity Services (GSI) **popup-mode** "Sign in with
|
||||
/// Google" window — the opener-dependent flow the GIS SDK drives via
|
||||
/// `window.open(...)` and completes by `postMessage`-ing the credential back to
|
||||
/// `window.opener` (the provider page). Covers both the account-chooser
|
||||
/// (`accounts.google.com/gsi/select`, #1021) and the `/o/oauth2/v2/auth` leg the
|
||||
/// same popup navigates through (#5009).
|
||||
///
|
||||
/// Matched by Google SSO host (`is_google_sso_host`) AND either a `gsi` path
|
||||
/// segment or one of the GIS-SDK popup-mode query markers. Deliberately narrow
|
||||
/// — a redirect-mode Google sign-in (a real https `redirect_uri`, no GSI
|
||||
/// markers) does NOT match, so it still replaces the parent. NOT a blanket
|
||||
/// popup allow.
|
||||
///
|
||||
/// Keeping these popups in-app is what lets the postMessage handshake complete
|
||||
/// inside the account's isolated CEF profile instead of leaking to the system
|
||||
/// browser (#5009) or being dropped when the parent (the opener) is navigated
|
||||
/// away (#5009 / #1021).
|
||||
fn is_google_gsi_popup(url: &Url) -> bool {
|
||||
if !url.host_str().is_some_and(is_google_sso_host) {
|
||||
return false;
|
||||
}
|
||||
if url.path().to_ascii_lowercase().contains("gsi") {
|
||||
return true;
|
||||
}
|
||||
// GIS-SDK popup-mode markers. These appear on the popup's auth URLs (incl.
|
||||
// `/o/oauth2/v2/auth`) and never on a plain redirect-mode sign-in.
|
||||
url.query_pairs().any(|(key, value)| {
|
||||
let k = key.to_ascii_lowercase();
|
||||
let v = value.to_ascii_lowercase();
|
||||
((k == "ux_mode" || k == "display") && v == "popup")
|
||||
|| k == "gsiwebsdk"
|
||||
|| (k == "redirect_uri" && v == "gis_transform")
|
||||
})
|
||||
}
|
||||
|
||||
/// `true` if `scheme` is a known provider native-desktop-app deep-link
|
||||
/// scheme. We suppress these instead of routing them to the system
|
||||
/// browser because macOS hands them to the native provider app
|
||||
@@ -397,6 +420,17 @@ fn popup_should_navigate_parent(provider: &str, url: &Url) -> Option<Url> {
|
||||
if url.scheme() == "about" {
|
||||
return None;
|
||||
}
|
||||
// #5009: X/Twitter (and LinkedIn) sign in with Google via the GSI popup —
|
||||
// an opener-dependent flow where the popup postMessages the credential back
|
||||
// to `window.opener` (the provider page). Navigating the parent to any leg
|
||||
// of that popup (chooser or the `/o/oauth2` hop) destroys the opener, so the
|
||||
// sign-in never completes and the pane paints blank. Keep it in-app instead
|
||||
// (`popup_should_stay_in_app` catches it for these providers). Redirect-mode
|
||||
// Google sign-in (a real https `redirect_uri`, no GSI markers) is NOT a GSI
|
||||
// popup, so it still falls through to the parent-navigation below.
|
||||
if matches!(provider, "linkedin" | "twitter") && is_google_gsi_popup(url) {
|
||||
return None;
|
||||
}
|
||||
if is_google_auth_popup(url) {
|
||||
return Some(url.clone());
|
||||
}
|
||||
|
||||
@@ -341,6 +341,103 @@ fn linkedin_google_oauth2_popup_navigates_parent() {
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linkedin_google_account_chooser_popup_stays_in_app() {
|
||||
// Regression guard for the refactor that shares the GSI account-chooser
|
||||
// arm between linkedin + twitter: LinkedIn's gsi/select popup must still
|
||||
// stay in-app (#1021).
|
||||
assert!(popup_should_stay_in_app(
|
||||
"linkedin",
|
||||
&url("https://accounts.google.com/gsi/select?client_id=x&ux_mode=popup"),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn twitter_supports_google_sso() {
|
||||
// X/Twitter offers "Continue with Google"; without twitter in the SSO set
|
||||
// the /o/oauth2 leg would not navigate the parent in-app (#5009).
|
||||
assert!(provider_supports_google_sso("twitter"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn twitter_google_account_chooser_popup_stays_in_app() {
|
||||
// #5009: X's "Continue with Google" opens the GSI account chooser via
|
||||
// window.open(accounts.google.com/gsi/select). Before the fix twitter had
|
||||
// no popup_should_stay_in_app arm, so this popup fell through to the system
|
||||
// browser — the user picked their account OUTSIDE the app and the embedded
|
||||
// webview was revealed on a dead page (blank screen). It must stay in-app.
|
||||
assert!(popup_should_stay_in_app(
|
||||
"twitter",
|
||||
&url("https://accounts.google.com/gsi/select?client_id=x&ux_mode=popup&origin=https%3A%2F%2Fx.com"),
|
||||
));
|
||||
// The GSI button/status endpoints share the same in-app requirement.
|
||||
assert!(popup_should_stay_in_app(
|
||||
"twitter",
|
||||
&url("https://accounts.google.com/gsi/button?client_id=x"),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn twitter_google_account_chooser_popup_is_not_navigate_parent() {
|
||||
// The account-chooser popup must be handled by popup_should_stay_in_app,
|
||||
// NOT popup_should_navigate_parent — navigating the parent to gsi/select
|
||||
// would destroy the opener the popup postMessages the credential back to.
|
||||
// (gsi/select is not an is_google_auth_popup match, so this holds.)
|
||||
assert!(popup_should_navigate_parent(
|
||||
"twitter",
|
||||
&url("https://accounts.google.com/gsi/select?client_id=x&ux_mode=popup"),
|
||||
)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn twitter_google_oauth2_popup_mode_stays_in_app_not_navigate_parent() {
|
||||
// #5009 core fix: X's "Continue with Google" is a GSI *popup* (ux_mode=popup)
|
||||
// — the popup postMessages the id_token back to the x.com opener. The
|
||||
// /o/oauth2 leg must stay in-app and must NOT navigate the parent; doing so
|
||||
// destroyed the opener and painted the pane white.
|
||||
let u = url(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth?client_id=x&ux_mode=popup\
|
||||
&gsiwebsdk=gis_attributes&redirect_uri=gis_transform&response_type=id_token\
|
||||
&origin=https%3A%2F%2Fx.com",
|
||||
);
|
||||
assert!(
|
||||
popup_should_navigate_parent("twitter", &u).is_none(),
|
||||
"popup-mode /o/oauth2 must not replace the x.com opener"
|
||||
);
|
||||
assert!(
|
||||
popup_should_stay_in_app("twitter", &u),
|
||||
"popup-mode /o/oauth2 must stay as an in-app child window"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linkedin_google_oauth2_redirect_mode_still_navigates_parent() {
|
||||
// Regression: LinkedIn's Google sign-in is a *redirect* flow (a real https
|
||||
// redirect_uri, no GSI popup markers). That is not a GSI popup, so it must
|
||||
// still replace the parent in-app (#1021) — the #5009 guard must not touch
|
||||
// it.
|
||||
let u = url("https://accounts.google.com/o/oauth2/v2/auth?client_id=x\
|
||||
&redirect_uri=https://www.linkedin.com/oauth/callback");
|
||||
assert!(popup_should_navigate_parent("linkedin", &u).is_some());
|
||||
assert!(!popup_should_stay_in_app("linkedin", &u));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn twitter_non_google_popup_does_not_stay_in_app() {
|
||||
// Scope guard: only the Google account-chooser popup is kept in-app. An
|
||||
// ordinary target="_blank"/window.open link still routes to the system
|
||||
// browser, so this is not a blanket popup allow.
|
||||
assert!(!popup_should_stay_in_app(
|
||||
"twitter",
|
||||
&url("https://example.com/some/article"),
|
||||
));
|
||||
assert!(!popup_should_stay_in_app(
|
||||
"twitter",
|
||||
&url("https://x.com/i/status/123")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linkedin_google_sso_navigation_is_internal() {
|
||||
// Direct (non-popup) navigation to accounts.google.com during the
|
||||
|
||||
Reference in New Issue
Block a user