diff --git a/src/ui/components/shared/Login/Login.tsx b/src/ui/components/shared/Login/Login.tsx
index f1751485b44..3d0baec9be9 100644
--- a/src/ui/components/shared/Login/Login.tsx
+++ b/src/ui/components/shared/Login/Login.tsx
@@ -1,6 +1,7 @@
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";
@@ -8,11 +9,13 @@ 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";
@@ -126,6 +129,39 @@ function LoginMessaging() {
);
}
+function SwitchAccountMessage({
+ user,
+ label,
+ onSwitch,
+ onCancel,
+}: {
+ user: UserInfo;
+ label: string;
+ onCancel: () => void;
+ onSwitch: () => void;
+}) {
+ return (
+
+
+ You are already logged in as {user.email}.
+
+
+ {global.__IS_RECORD_REPLAY_RUNTIME__ ? null : (
+
+ )}
+
+ );
+}
+
function SocialLogin({
onShowSSOLogin,
onLogin,
@@ -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"))) {
@@ -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;
}
@@ -266,6 +315,14 @@ export default function Login({
});
};
+ const handleUseCurrentAuth = async () => {
+ if (isCompletingBrowserAuth && connection) {
+ await onLogin(connection);
+ } else {
+ router.push("/");
+ }
+ };
+
useEffect(() => {
setUserInBrowserPrefs(null);
}, []);
@@ -273,7 +330,14 @@ export default function Login({
return (
- {global.__IS_RECORD_REPLAY_RUNTIME__ && isOSX ? (
+ {token.token && userInfo.email && !continueToLogin ? (
+ setContinueToLogin(true)}
+ onCancel={() => handleUseCurrentAuth()}
+ />
+ ) : global.__IS_RECORD_REPLAY_RUNTIME__ && isOSX ? (
) : sso ? (
diff --git a/src/ui/utils/useAuth0.ts b/src/ui/utils/useAuth0.ts
index ae1c46e1bee..45af6be8b3a 100644
--- a/src/ui/utils/useAuth0.ts
+++ b/src/ui/utils/useAuth0.ts
@@ -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",
@@ -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() {
@@ -48,6 +49,7 @@ export default function useAuth0() {
error: undefined,
isLoading: loading,
isAuthenticated: true,
+ connection: "EXTERNAL",
user: loading
? undefined
: {
@@ -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 };
}