app/login/OAuthCallbackComponent.tsx (102 lines of code) (raw):

import React, { useContext, useEffect, useState } from "react"; import { useHistory } from "react-router"; import CircularProgress from "@material-ui/core/CircularProgress"; import { stageTwoExchange, validateAndDecode } from "./OAuthService"; import { makeLoginUrl, UserContext } from "@guardian/pluto-headers"; import { JwtData, OAuthContext } from "@guardian/pluto-headers"; import { Button, Grid, LinearProgress, Link, Typography, } from "@material-ui/core"; import { makeLoginUrl as buildLoginURL } from "@guardian/pluto-headers"; import NotLoggedInPanel from "../panels/NotLoggedInPanel"; import { Replay } from "@material-ui/icons"; /** * this component handles the token redirect from the authentication * once the user has authed successfully with the IdP, the browser is sent a redirect * that lands here. We are given an opaque code by the server in the "code" query parameter. * We take this and try to exchange it for a bearer token; if successful this is stored into * the local storage and we then redirect the user back to what they were doing (via the State parameter) * If not successful, we halt and display an error message. */ const OAuthCallbackComponent: React.FC<{}> = () => { const [inProgress, setInProgress] = useState(false); const [showingLink, setShowingLink] = useState(false); const [lastError, setLastError] = useState<string | undefined>(undefined); const oAuthContext = useContext(OAuthContext); const userContext = useContext(UserContext); const history = useHistory(); const makeLoginURL = () => { if (oAuthContext) { return buildLoginURL(oAuthContext); } //shouldn't show an error message as we always start up without oAuthContext then it gets updated }; useEffect(() => { const timerId = window.setTimeout(() => setShowingLink(true), 3000); return () => window.clearTimeout(timerId); }, [lastError]); useEffect(() => { const loginProcess = async () => { if (oAuthContext) { try { const searchParams = new URLSearchParams(history.location.search); const response = await stageTwoExchange( searchParams, oAuthContext.clientId, oAuthContext.redirectUri, oAuthContext.tokenUri ); if (response.error) { setLastError(response.error); } else { const decodedData = await validateAndDecode(oAuthContext, response); if (decodedData) { const marshalledData = JwtData(decodedData); userContext.updateProfile(marshalledData); //updating this context here seems to trigger a re-mount, or at least a re-running of this hook } else { setLastError("Login was validated but no user data returned"); } const newLocation = searchParams.get("state"); if (newLocation && newLocation != "/") { //disallow any fully-qualified links as they may send us where we don't want to go if (newLocation.startsWith("htt")) { history.push("/"); } else { //if we have a specific location to go to, then assume it's external and do it via window.setLocation window.location.href = newLocation; } setInProgress(false); } else { //if we are going to root, that is one of ours, so do it through react-router history.push("/"); setInProgress(false); } } } catch (err) { setLastError(`Could not log in: ${err}`); setInProgress(false); } } }; if (oAuthContext && !inProgress) { setInProgress(true); //if we don't put a blocker in here this hook is run twice resulting in spurious errors loginProcess(); } }, [oAuthContext]); return lastError ? ( <NotLoggedInPanel bannerText="Could not log you in"> <Grid item> <Typography>There was a problem logging you in: {lastError}</Typography> </Grid> <Grid item> {showingLink ? ( <Button variant="contained" startIcon={<Replay />} onClick={() => window.location.assign(makeLoginURL() ?? "/")} > Try again </Button> ) : ( <CircularProgress /> )} </Grid> </NotLoggedInPanel> ) : ( <NotLoggedInPanel bannerText="Logging you in..."> <Grid item style={{ width: "100%" }}> <LinearProgress color="secondary" style={{ width: "80%", marginLeft: "auto", marginRight: "auto" }} /> </Grid> </NotLoggedInPanel> ); }; export default OAuthCallbackComponent;