From 500504af5e415bc2340bdfa5d1d650fd57e4e551 Mon Sep 17 00:00:00 2001 From: Ian Krieger <48930920+IanKrieger@users.noreply.github.com> Date: Tue, 6 Aug 2024 09:00:53 -0400 Subject: [PATCH] feat(login): obtain user consent when verifying magic link (#1302) Introduce better Anti-CSRF protections by prompting the user to make sure that every login request is an intentional request. --- https://github.com/user-attachments/assets/e45b9f94-7246-4601-b63a-bc8a859f85f0 --- src/auth/hooks/queries/useAuthorize.ts | 20 ++-- src/auth/lib/index.ts | 31 +++--- src/auth/views/AuthVerify.tsx | 106 +++++++++++--------- src/auth/views/MagicLink.tsx | 10 +- src/auth/views/components/AuthContainer.tsx | 2 +- 5 files changed, 88 insertions(+), 81 deletions(-) diff --git a/src/auth/hooks/queries/useAuthorize.ts b/src/auth/hooks/queries/useAuthorize.ts index 5739b414..87d20b12 100644 --- a/src/auth/hooks/queries/useAuthorize.ts +++ b/src/auth/hooks/queries/useAuthorize.ts @@ -1,28 +1,22 @@ -import { useEffect, useState } from "react"; +import { useCallback, useState } from "react"; import { useAuthContext } from "@/auth/context/auth.hook"; -import { authorize, ResponseUser } from "@/auth/lib"; +import { authorize } from "@/auth/lib"; interface Options { - variables: { - code: string; - id: string; - }; onCompleted?: () => void; onError?: () => void; } -export function useAuthorize({ variables, onCompleted, onError }: Options) { +export function useAuthorize({ onCompleted, onError }: Options) { const { setSessionUser } = useAuthContext(); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); const [error, setError] = useState(); - const [data, setData] = useState(); - useEffect(() => { - authorize(variables) + const verify = useCallback((code: string, id: string) => { + authorize({ code, id }) .then((data) => { if (data) { setSessionUser(data); - setData(data); } if (onCompleted) { onCompleted(); @@ -40,5 +34,5 @@ export function useAuthorize({ variables, onCompleted, onError }: Options) { }); }, []); - return { data, loading, error }; + return { loading, error, verify }; } diff --git a/src/auth/lib/index.ts b/src/auth/lib/index.ts index 761ce985..c96040b3 100644 --- a/src/auth/lib/index.ts +++ b/src/auth/lib/index.ts @@ -120,29 +120,30 @@ export const clearCredentials = async (): Promise => { }; export const getLink = async (user: { email: string }): Promise => { - const encodedEmail = encodeURIComponent(user.email.trim()); - await fetch( - buildAdServerV2Endpoint(`/auth/magic-link?email=${encodedEmail}`), - { - method: "GET", - mode: "cors", - credentials: "include", + await fetch(buildAdServerV2Endpoint(`/auth/magic-link`), { + method: "POST", + mode: "cors", + credentials: "include", + headers: { + "Content-Type": "application/json", }, - ); + body: JSON.stringify({ email: user.email.trim() }), + }); }; export const authorize = async (req: { code: string; id: string; }): Promise => { - const res = await fetch( - buildAdServerV2Endpoint(`/auth/authorize?code=${req.code}&id=${req.id}`), - { - method: "GET", - mode: "cors", - credentials: "include", + const res = await fetch(buildAdServerV2Endpoint(`/auth/authorize`), { + method: "POST", + mode: "cors", + credentials: "include", + headers: { + "Content-Type": "application/json", }, - ); + body: JSON.stringify({ code: req.code, id: req.id }), + }); if (!res.ok) { throw new Error(t`Invalid Token`); diff --git a/src/auth/views/AuthVerify.tsx b/src/auth/views/AuthVerify.tsx index a0bac75f..7047ef15 100644 --- a/src/auth/views/AuthVerify.tsx +++ b/src/auth/views/AuthVerify.tsx @@ -1,11 +1,11 @@ import { AuthContainer } from "@/auth/views/components/AuthContainer"; import { useAuthorize } from "@/auth/hooks/queries/useAuthorize"; -import { Link as RouterLink, useHistory } from "react-router-dom"; -import { CircularProgress, Link, Stack, Typography } from "@mui/material"; -import VerifiedIcon from "@mui/icons-material/Verified"; -import CancelOutlinedIcon from "@mui/icons-material/CancelOutlined"; +import { Link as RouterLink, Switch, useHistory } from "react-router-dom"; +import { Alert, AlertTitle, Button, Typography } from "@mui/material"; import { useTrackWithMatomo } from "@/hooks/useTrackWithMatomo"; import { Trans } from "@lingui/macro"; +import { LoadingButton } from "@mui/lab"; +import logo from "@/assets/images/brave-icon-release-color.svg"; export function AuthVerify() { const { trackMatomoEvent } = useTrackWithMatomo({ @@ -14,11 +14,7 @@ export function AuthVerify() { const history = useHistory(); const params = new URLSearchParams(history.location.search); - const { loading, error } = useAuthorize({ - variables: { - code: params.get("code") ?? "", - id: params.get("id") ?? "", - }, + const { loading, error, verify } = useAuthorize({ onCompleted() { history.push("/user/main"); trackMatomoEvent("magic-link", "verified"); @@ -28,49 +24,61 @@ export function AuthVerify() { }, }); + const id = params.get("id"); + const code = params.get("code"); + + if (!id || !code) { + return ( + + + + ); + } + return ( - {loading && ( - - - Logging in - - - - - )} - {!loading && !error && ( - - - Successfully logged in! - - - - - Not automatically redirected? Click this link to go to the - dashboard. - - - - )} + + + You are logging into the Brave Ads Manager Dashboard. + + + + Click the continue button below to complete the login process. + + + verify(code, id)} + variant="contained" + sx={{ mt: 2, pl: 5, pr: 5, borderRadius: "12px", mb: 2 }} + size="large" + > + Continue + {!loading && error && ( - - - - Unable to login, link has expired or has already been used. - - - - - Request another link. - - + + Return + + } + > + + Unable to login. + + + The magic link you have requested has either expired or has already + been used. Please return to the login page and try again. + + )} ); diff --git a/src/auth/views/MagicLink.tsx b/src/auth/views/MagicLink.tsx index bf7346f1..449e518b 100644 --- a/src/auth/views/MagicLink.tsx +++ b/src/auth/views/MagicLink.tsx @@ -40,8 +40,11 @@ export function MagicLink() { + Don’t see the email? + + - Don’t see the email? Check your spam folder or{" "} + Check your spam folder or{" "} - try again. - + return to the login page + {" "} + to try again. diff --git a/src/auth/views/components/AuthContainer.tsx b/src/auth/views/components/AuthContainer.tsx index be9d8008..43541a11 100644 --- a/src/auth/views/components/AuthContainer.tsx +++ b/src/auth/views/components/AuthContainer.tsx @@ -17,7 +17,7 @@ export function AuthContainer({ children, belowCard, aboveCard }: Props) {