Skip to content

Commit

Permalink
Add switch accounts interstitial to login flow (take 2) (#10440)
Browse files Browse the repository at this point in the history
* Reapply "Add switch accounts interstitial to login flow (#10431)" (#10437)

This reverts commit 2a470e0.

* fix handling auth requests coming from the browser

* add comments

* fix connection parsing; hide switch accounts on replay browser
  • Loading branch information
ryanjduffy authored Mar 15, 2024
1 parent 0d75dd5 commit 418275f
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 6 deletions.
70 changes: 67 additions & 3 deletions src/ui/components/shared/Login/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { gql } from "@apollo/client";
import { ExclamationIcon } from "@heroicons/react/outline";
import Link from "next/link";
import { useRouter } from "next/router";
import { ReactNode, useEffect, useState } from "react";

import { Button } from "replay-next/components/Button";
import { query } from "shared/graphql/apolloClient";
import { GetConnection, GetConnectionVariables } from "shared/graphql/generated/GetConnection";
import { getReadOnlyParamsFromURL } from "shared/utils/environment";
import { isMacOS } from "shared/utils/os";
import { UserInfo, useGetUserInfo } from "ui/hooks/users";
import { getAuthClientId, getAuthHost } from "ui/utils/auth";
import { requestBrowserLogin, setUserInBrowserPrefs } from "ui/utils/browser";
import { isTeamMemberInvite } from "ui/utils/onboarding";
import { sendTelemetryEvent } from "ui/utils/telemetry";
import useAuth0 from "ui/utils/useAuth0";
import useToken from "ui/utils/useToken";

import { OnboardingContentWrapper, OnboardingModalContainer } from "../Onboarding";

Expand Down Expand Up @@ -126,6 +129,39 @@ function LoginMessaging() {
);
}

function SwitchAccountMessage({
user,
label,
onSwitch,
onCancel,
}: {
user: UserInfo;
label: string;
onCancel: () => void;
onSwitch: () => void;
}) {
return (
<div className="space-y-6">
<p className="text-center text-base">
You are already logged in as <strong>{user.email}</strong>.
</p>
<Button className="w-full justify-center" onClick={onCancel} size="large">
{label}
</Button>
{global.__IS_RECORD_REPLAY_RUNTIME__ ? null : (
<Button
className="w-full justify-center text-sm font-bold text-primaryAccent underline"
onClick={onSwitch}
size="large"
variant="outline"
>
Switch Accounts
</Button>
)}
</div>
);
}

function SocialLogin({
onShowSSOLogin,
onLogin,
Expand Down Expand Up @@ -242,8 +278,16 @@ export default function Login({
challenge?: string;
state?: string;
}) {
const { loginWithRedirect, error } = useAuth0();
const router = useRouter();
const { loginWithRedirect, error, connection } = useAuth0();
const [sso, setSSO] = useState(false);
const [continueToLogin, setContinueToLogin] = useState(false);
const token = useToken();
const userInfo = useGetUserInfo();

// `true` when we're in the process of completing the auth flow from the
// Replay browser
const isCompletingBrowserAuth = Boolean(userInfo && challenge && state);

const url = new URL(returnToPath, window.location.origin);
if (url.pathname === "/login" || (url.pathname === "/" && url.searchParams.has("state"))) {
Expand All @@ -255,7 +299,12 @@ export default function Login({
if (challenge && state) {
const authHost = getAuthHost();
const clientId = getAuthClientId();
window.location.href = `https://${authHost}/authorize?response_type=code&code_challenge_method=S256&code_challenge=${challenge}&client_id=${clientId}&redirect_uri=${returnToPath}&scope=openid profile offline_access&state=${state}&audience=https://api.replay.io&connection=${connection}`;
// when continueToLogin was selected, the user was previously logged in
// and wanted to select a different account so force the login prompt by
// passing prompt=login to auth0
window.location.href = `https://${authHost}/authorize?response_type=code&code_challenge_method=S256&code_challenge=${challenge}&client_id=${clientId}&redirect_uri=${returnToPath}&scope=openid profile offline_access&state=${state}&audience=https://api.replay.io&connection=${connection}&prompt=${
continueToLogin ? "login" : ""
}`;

return;
}
Expand All @@ -266,14 +315,29 @@ export default function Login({
});
};

const handleUseCurrentAuth = async () => {
if (isCompletingBrowserAuth && connection) {
await onLogin(connection);
} else {
router.push("/");
}
};

useEffect(() => {
setUserInBrowserPrefs(null);
}, []);

return (
<OnboardingModalContainer theme="light">
<OnboardingContentWrapper overlay>
{global.__IS_RECORD_REPLAY_RUNTIME__ && isOSX ? (
{token.token && userInfo.email && !continueToLogin ? (
<SwitchAccountMessage
label={isCompletingBrowserAuth ? "Continue with this account" : "Continue to Library"}
user={userInfo}
onSwitch={() => setContinueToLogin(true)}
onCancel={() => handleUseCurrentAuth()}
/>
) : global.__IS_RECORD_REPLAY_RUNTIME__ && isOSX ? (
<ReplayBrowserLogin />
) : sso ? (
<SSOLogin onLogin={onLogin} />
Expand Down
17 changes: 14 additions & 3 deletions src/ui/utils/useAuth0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import { useGetUserInfo } from "ui/hooks/users";
import { setAccessTokenInBrowserPrefs } from "./browser";
import useToken from "./useToken";

export type AuthContext = Auth0ContextInterface | typeof TEST_AUTH;

const TEST_AUTH = {
error: undefined,
isLoading: false,
isAuthenticated: true,
connection: "TEST",
user: {
sub: "auth0|60351bdaa6afe80068af126e",
name: "e2e-testing@replay.io",
Expand All @@ -24,8 +27,6 @@ const TEST_AUTH = {
getAccessTokenSilently: () => Promise.resolve(),
};

export type AuthContext = Auth0ContextInterface | typeof TEST_AUTH;

// TODO [hbenl, ryanjduffy] This function should `useMemo` to memoize the "user" object it returns.
// As it is, this hooks prevents components like CommentTool from limiting how often their effects run.
export default function useAuth0() {
Expand All @@ -48,6 +49,7 @@ export default function useAuth0() {
error: undefined,
isLoading: loading,
isAuthenticated: true,
connection: "EXTERNAL",
user: loading
? undefined
: {
Expand All @@ -70,5 +72,14 @@ export default function useAuth0() {
return TEST_AUTH;
}

return { ...auth, loginAndReturn };
// for social logins, the connection (e.g. google-oauth2) is the prefix. For
// SAML logins, the connection is the client-specific code after the samlp
// prefix (samlp|client-name|user-id).
let connection: string | undefined;
if (auth.user) {
const parts = auth.user.sub.split("|") ?? [];
connection = parts[0] === "samlp" ? parts[1] : parts[0];
}

return { ...auth, connection, loginAndReturn };
}

0 comments on commit 418275f

Please sign in to comment.