app/login/OAuthService.ts (130 lines of code) (raw):

import { loadInSigningKey, OAuthContextData, verifyJwt, } from "@guardian/pluto-headers"; interface OAuthResponse { token?: string; refreshToken?: string; error?: string; } /** * performs the second-stage exchange, i.e. it sends the received code back to the server and requests a bearer * token in response * @param searchParams the URLSearchParams received from the server in stage one, including the "code" or "error" parameter * @param clientId oauth client ID from context * @param redirectUri oauth redirect uri from context * @param tokenUri oauth token uri from context */ async function stageTwoExchange( searchParams: URLSearchParams, clientId: string, redirectUri: string, tokenUri: string ): Promise<OAuthResponse> { const authCode = searchParams.get("code"); const errorInUrl = searchParams.get("error"); const codeChallenge = sessionStorage.getItem("cx") as string | null; //this is set in OAuthContext.tsx, in @guardian/pluto-headers, via makeLoginUrl() sessionStorage.removeItem("cx"); if (errorInUrl) { return { token: undefined, refreshToken: undefined, error: errorInUrl, }; } if (!authCode) { return { error: "There was no code provided to exchange", }; } else { const postdata: Record<string, string> = { grant_type: "authorization_code", client_id: clientId, redirect_uri: redirectUri, code: authCode, }; console.log("passed client_id ", clientId); if (!!codeChallenge && codeChallenge != "") { console.log(`have code_verifier '${codeChallenge}' from step one`); postdata["code_verifier"] = codeChallenge; } const content_elements = Object.keys(postdata).map( (k) => k + "=" + encodeURIComponent(postdata[k]) ); const body_content = content_elements.join("&"); const response = await fetch(tokenUri, { method: "POST", body: body_content, headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded", }, }); switch (response.status) { case 200: const content = await response.json(); return { token: content.id_token ?? content.access_token, refreshToken: content.hasOwnProperty("refresh_token") ? content.refresh_token : undefined, error: undefined, }; default: const errorContent = await response.text(); console.log( "token endpoint returned ", response.status, ": ", errorContent ); return { error: "Could not get token from server", }; } } } function delayedRequest(url: string, timeoutDelay: number, token: string) { return new Promise<void>((resolve, reject) => { const timerId = window.setTimeout(() => { console.error("Request timed out, could not contact UserBeacon"); resolve(); }, timeoutDelay); fetch(url, { method: "PUT", headers: { Authorization: `Bearer ${token}`, body: "" }, }) .then((response) => { try { window.clearTimeout(timerId); } catch (err) { console.error("Could not clear the time out: ", err); } if (response.status === 200) { console.log("UserBeacon contacted successfully"); } else { console.log("UserBeacon returned an error: ", response.status); } resolve(); }) .catch((err) => { try { window.clearTimeout(timerId); } catch (error) { console.error("Could not clear the time out: ", error); } console.error("Could not contact UserBeacon: ", err); reject(err); }); }); } /** * * perform the validation of the token via jsonwebtoken library. * if validation fails then the returned promise is rejected * if validation succeeds, then the promise only completes once the decoded content has been set into the state. */ async function validateAndDecode( oAuthContext: OAuthContextData, response: OAuthResponse ) { if (response.token) { const decoded = await verifyJwt( oAuthContext, response.token, response.refreshToken ); /* Make a request to pluto-user-beacon (if available) to ensure that the user exists in Vidispine. * The function should wait for up to five seconds for a response from pluto-user-beacon. */ await delayedRequest("/userbeacon/register-login", 5000, response.token); return decoded; } else { throw "Could not validate aas no token was provided"; } } export { stageTwoExchange, delayedRequest, validateAndDecode };