From 0309638909fe5b107366ff8e288ca25352221677 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sat, 13 Feb 2021 00:26:45 +0300 Subject: Adds Basic Auth Functionality Moves all authorization functionality to a new file, and adds a helper to send discord OAuth code to the backend, and set JWT. Adds a library to read and set cookies. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/auth.ts | 208 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 src/api/auth.ts (limited to 'src/api/auth.ts') diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 0000000..ae20236 --- /dev/null +++ b/src/api/auth.ts @@ -0,0 +1,208 @@ +import Cookies, { CookieSetOptions } from "universal-cookie"; +import { AxiosResponse } from "axios"; + +import APIClient from "./client"; + +const OAUTH2_CLIENT_ID = process.env.REACT_APP_OAUTH2_CLIENT_ID; +const PRODUCTION = process.env.NODE_ENV !== "development"; + +/** + * Authorization result as returned from the backend. + */ +interface AuthResult { + token: string, + expiry: string +} + +interface JWTResponse { + JWT: string, + Expiry: Date +} + +/** + * Name properties for authorization cookies. + */ +enum CookieNames { + Scopes = "DiscordOAuthScopes", + Token = "FormBackendToken" +} + +/** + * [Reference]{@link https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes} + * + * Commented out enums are locked behind whitelists. + */ +export enum OAuthScopes { + Bot = "bot", + Connections = "connections", + Email = "email", + Identify = "identify", + Guilds = "guilds", + GuildsJoin = "guilds.join", + GDMJoin = "gdm.join", + MessagesRead = "messages.read", + // RPC = "rpc", + // RPC_API = "rpc.api", + // RPCNotifRead = "rpc.notifications.read", + WebhookIncoming = "webhook.incoming", + // AppsBuildsUpload = "applications.builds.upload", + AppsBuildsRead = "applications.builds.read", + AppsStoreUpdate = "applications.store.update", + AppsEntitlements = "applications.entitlements", + // RelationshipsRead = "relationships.read", + // ActivitiesRead = "activities.read", + // ActivitiesWrite = "activities.write", + AppsCommands = "applications.commands", + AppsCommandsUpdate = "applications.commands.update" +} + +/** + * Helper method to ensure the minimum required scopes + * for the application to function exist in a list. + */ +function ensureMinimumScopes(scopes: unknown, expected: OAuthScopes | OAuthScopes[]): OAuthScopes[] { + let result: OAuthScopes[] = []; + if (scopes && Array.isArray(scopes)) { + result = scopes; + } + + if (Array.isArray(expected)) { + expected.forEach(scope => { if (!result.includes(scope)) result.push(scope); }); + } else { + if (!result.includes(expected)) result.push(expected); + } + + return result; +} + +/** + * Return true if the program has the requested scopes or higher. + */ +export function checkScopes(scopes?: OAuthScopes[], path = ""): boolean { + const cleanedScopes = ensureMinimumScopes(scopes, OAuthScopes.Identify); + + // Get Active Scopes And Ensure Type + const cookies = new Cookies().get(CookieNames.Scopes + path); + if (!cookies || !Array.isArray(cookies)) { + return false; + } + + // Check For Scope Existence + for (const scope of cleanedScopes) { + if (!cookies.includes(scope)) { + return false; + } + } + + return true; +} + +/*** + * Request authorization code from the discord api with the provided scopes. + * + * If disable function is passed, the component will be disabled while the login is ongoing. + * + * @returns {code, cleanedScopes} The discord authorization code and the scopes the code is granted for. + * @throws {Error} Indicates that an integrity check failed. + */ +export async function getDiscordCode(scopes: OAuthScopes[], disableFunction?: (newState: boolean) => void): Promise<{code: string, cleanedScopes: OAuthScopes[]}> { + const cleanedScopes = ensureMinimumScopes(scopes, OAuthScopes.Identify); + const disable = (newState: boolean) => { if (disableFunction) disableFunction(newState); }; + + disable(true); + + // Generate a new user state + const state = crypto.getRandomValues(new Uint32Array(1))[0]; + + const scopeString = encodeURIComponent(cleanedScopes.join(" ")); + const redirectURI = encodeURIComponent(document.location.protocol + "//" + document.location.host + "/callback"); + + // Open login window + const windowRef = window.open( + `https://discord.com/api/oauth2/authorize?client_id=${OAUTH2_CLIENT_ID}&state=${state}&response_type=code&scope=${scopeString}&redirect_uri=${redirectURI}&prompt=none`, + "Discord_OAuth2", + "height=700,width=500,location=no,menubar=no,resizable=no,status=no,titlebar=no,left=300,top=300" + ); + + // Clean up on login + const interval = setInterval(() => { + if (windowRef?.closed) { + clearInterval(interval); + disable(false); + } + }, 500); + + // Handle response + const code = await new Promise(resolve => { + window.onmessage = (message: MessageEvent) => { + if (message.data.source) { + // React DevTools has a habit of sending messages, ignore them. + return; + } + + if (message.isTrusted) { + windowRef?.close(); + + clearInterval(interval); + disable(false); + + // State integrity check + if (message.data.state !== state.toString()) { + // This indicates a lack of integrity + throw Error("Integrity check failed."); + } + + // Remove handler + window.onmessage = null; + resolve(message.data.code); + } + }; + }); + + return {code: code, cleanedScopes: cleanedScopes}; +} + +/** + * Sends a discord code from a given path to the backend, + * and returns the resultant JWT and expiry date. + */ +export async function requestBackendJWT(code: string): Promise { + const result = await APIClient.post("/auth/authorize", {token: code}) + .catch(reason => { throw reason; }) // TODO: Show some sort of authentication failure here + .then((response: AxiosResponse) => { + const _expiry = new Date(); + _expiry.setTime(Date.parse(response.data.expiry)); + + return {JWT: response.data.token, Expiry: _expiry}; + }); + + if (!result.JWT || !result.Expiry) { + throw Error("Could not fetch OAuth code."); + } + + return result; +} + +/** + * Handle a full authorization flow. Sets a token for the specified path with the JWT and scopes. + * + * @param scopes The scopes that should be authorized for the application. + * @param disableFunction An optional function that can disable a component while processing. + * @param path The site path to save the token under. + */ +export default function authorize(scopes?: OAuthScopes[], disableFunction?: (newState: boolean) => void, path = "/"): void { + if (!checkScopes(scopes, path)) { + const cookies = new Cookies; + cookies.remove(CookieNames.Token + path); + cookies.remove(CookieNames.Scopes + path); + + getDiscordCode(scopes || [], disableFunction).then(discord_response =>{ + requestBackendJWT(discord_response.code).then(backend_response => { + const options: CookieSetOptions = {sameSite: "strict", expires: backend_response.Expiry, secure: PRODUCTION, path: path}; + + cookies.set(CookieNames.Token + path, backend_response.JWT, options); + cookies.set(CookieNames.Scopes + path, discord_response.cleanedScopes, options); + }); + }); + } +} -- cgit v1.2.3 From 2350aa12fae661e37895616e0e7fe3e7ebc62f53 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sat, 13 Feb 2021 01:13:37 +0300 Subject: Makes Authorize Helper Async Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/auth.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'src/api/auth.ts') diff --git a/src/api/auth.ts b/src/api/auth.ts index ae20236..fa3ac85 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -190,19 +190,21 @@ export async function requestBackendJWT(code: string): Promise { * @param disableFunction An optional function that can disable a component while processing. * @param path The site path to save the token under. */ -export default function authorize(scopes?: OAuthScopes[], disableFunction?: (newState: boolean) => void, path = "/"): void { +export default async function authorize(scopes?: OAuthScopes[], disableFunction?: (newState: boolean) => void, path = "/"): Promise { if (!checkScopes(scopes, path)) { const cookies = new Cookies; cookies.remove(CookieNames.Token + path); cookies.remove(CookieNames.Scopes + path); - getDiscordCode(scopes || [], disableFunction).then(discord_response =>{ - requestBackendJWT(discord_response.code).then(backend_response => { + await getDiscordCode(scopes || [], disableFunction).then(async discord_response =>{ + await requestBackendJWT(discord_response.code).then(backend_response => { const options: CookieSetOptions = {sameSite: "strict", expires: backend_response.Expiry, secure: PRODUCTION, path: path}; cookies.set(CookieNames.Token + path, backend_response.JWT, options); cookies.set(CookieNames.Scopes + path, discord_response.cleanedScopes, options); }); }); + + return new Promise(resolve => resolve()); } } -- cgit v1.2.3 From e8bd7798a0154725ba27295eceae7160156e9889 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 16 Feb 2021 23:28:48 +0300 Subject: Adds Error Handling To Auth Helpers Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/auth.ts | 75 +++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 18 deletions(-) (limited to 'src/api/auth.ts') diff --git a/src/api/auth.ts b/src/api/auth.ts index fa3ac85..ad97e67 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -27,6 +27,19 @@ enum CookieNames { Token = "FormBackendToken" } +export interface APIErrors { + Message: APIErrorMessages, + Error: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ +} + +export enum APIErrorMessages { + BackendValidation = "Backend could not authorize with Discord. Please contact the forms team.", + BackendValidationDev = "Backend could not authorize with Discord, possibly due to being on a preview branch. Please contact the forms team.", + BackendUnresponsive = "Unable to reach the backend, please retry, or contact the forms team.", + BadResponse = "The server returned a bad response, please contact the forms team.", + Unknown = "An unknown error occurred, please contact the forms team." +} + /** * [Reference]{@link https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes} * @@ -100,16 +113,11 @@ export function checkScopes(scopes?: OAuthScopes[], path = ""): boolean { /*** * Request authorization code from the discord api with the provided scopes. * - * If disable function is passed, the component will be disabled while the login is ongoing. - * * @returns {code, cleanedScopes} The discord authorization code and the scopes the code is granted for. * @throws {Error} Indicates that an integrity check failed. */ -export async function getDiscordCode(scopes: OAuthScopes[], disableFunction?: (newState: boolean) => void): Promise<{code: string, cleanedScopes: OAuthScopes[]}> { +export async function getDiscordCode(scopes: OAuthScopes[]): Promise<{code: string, cleanedScopes: OAuthScopes[]}> { const cleanedScopes = ensureMinimumScopes(scopes, OAuthScopes.Identify); - const disable = (newState: boolean) => { if (disableFunction) disableFunction(newState); }; - - disable(true); // Generate a new user state const state = crypto.getRandomValues(new Uint32Array(1))[0]; @@ -128,7 +136,6 @@ export async function getDiscordCode(scopes: OAuthScopes[], disableFunction?: (n const interval = setInterval(() => { if (windowRef?.closed) { clearInterval(interval); - disable(false); } }, 500); @@ -144,7 +151,6 @@ export async function getDiscordCode(scopes: OAuthScopes[], disableFunction?: (n windowRef?.close(); clearInterval(interval); - disable(false); // State integrity check if (message.data.state !== state.toString()) { @@ -165,19 +171,47 @@ export async function getDiscordCode(scopes: OAuthScopes[], disableFunction?: (n /** * Sends a discord code from a given path to the backend, * and returns the resultant JWT and expiry date. + * + * @throws { APIErrors } On error, the APIErrors.Message is set, and an APIErrors object is thrown. */ export async function requestBackendJWT(code: string): Promise { - const result = await APIClient.post("/auth/authorize", {token: code}) - .catch(reason => { throw reason; }) // TODO: Show some sort of authentication failure here - .then((response: AxiosResponse) => { - const _expiry = new Date(); - _expiry.setTime(Date.parse(response.data.expiry)); + const reason: APIErrors = { Message: APIErrorMessages.Unknown, Error: null }; + let result; + + try { + result = await APIClient.post("/auth/authorize", {token: code}) + .catch(error => { + reason.Error = error; + + if (error.response) { + // API Responded with a non-2xx Response + if (error.response.status === 400) { + reason.Message = process.env.CONTEXT === "deploy-preview" ? APIErrorMessages.BackendValidationDev : APIErrorMessages.BackendValidation; + } + } else if (error.request) { + // API did not respond + reason.Message = APIErrorMessages.BackendUnresponsive; + } - return {JWT: response.data.token, Expiry: _expiry}; - }); + throw error; + + }).then((response: AxiosResponse) => { + const expiry = new Date(); + expiry.setTime(Date.parse(response.data.expiry)); + + return {JWT: response.data.token, Expiry: expiry}; + }); + } catch (e) { + if (reason.Error === null) { + reason.Error = e; + } + + throw reason; + } if (!result.JWT || !result.Expiry) { - throw Error("Could not fetch OAuth code."); + reason.Message = APIErrorMessages.BadResponse; + throw reason; } return result; @@ -189,20 +223,25 @@ export async function requestBackendJWT(code: string): Promise { * @param scopes The scopes that should be authorized for the application. * @param disableFunction An optional function that can disable a component while processing. * @param path The site path to save the token under. + * + * @throws { APIErrors } See documentation on { requestBackendJWT }. */ -export default async function authorize(scopes?: OAuthScopes[], disableFunction?: (newState: boolean) => void, path = "/"): Promise { +export default async function authorize(scopes: OAuthScopes[] = [], disableFunction?: (newState: boolean) => void, path = "/"): Promise { if (!checkScopes(scopes, path)) { const cookies = new Cookies; cookies.remove(CookieNames.Token + path); cookies.remove(CookieNames.Scopes + path); - await getDiscordCode(scopes || [], disableFunction).then(async discord_response =>{ + if (disableFunction) { disableFunction(true); } + await getDiscordCode(scopes).then(async discord_response =>{ await requestBackendJWT(discord_response.code).then(backend_response => { const options: CookieSetOptions = {sameSite: "strict", expires: backend_response.Expiry, secure: PRODUCTION, path: path}; cookies.set(CookieNames.Token + path, backend_response.JWT, options); cookies.set(CookieNames.Scopes + path, discord_response.cleanedScopes, options); }); + }).finally(() => { + if (disableFunction) { disableFunction(false); } }); return new Promise(resolve => resolve()); -- cgit v1.2.3 From fba316f235a3871743427f37b3bbd07bea6d77bd Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Wed, 17 Feb 2021 09:30:47 +0300 Subject: Removes Path From Auth Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/auth.ts | 23 +++++++++++------------ src/components/OAuth2Button.tsx | 5 ++--- src/pages/FormPage.tsx | 6 ++---- 3 files changed, 15 insertions(+), 19 deletions(-) (limited to 'src/api/auth.ts') diff --git a/src/api/auth.ts b/src/api/auth.ts index ad97e67..cfaa563 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -91,11 +91,11 @@ function ensureMinimumScopes(scopes: unknown, expected: OAuthScopes | OAuthScope /** * Return true if the program has the requested scopes or higher. */ -export function checkScopes(scopes?: OAuthScopes[], path = ""): boolean { +export function checkScopes(scopes?: OAuthScopes[]): boolean { const cleanedScopes = ensureMinimumScopes(scopes, OAuthScopes.Identify); // Get Active Scopes And Ensure Type - const cookies = new Cookies().get(CookieNames.Scopes + path); + const cookies = new Cookies().get(CookieNames.Scopes); if (!cookies || !Array.isArray(cookies)) { return false; } @@ -169,7 +169,7 @@ export async function getDiscordCode(scopes: OAuthScopes[]): Promise<{code: stri } /** - * Sends a discord code from a given path to the backend, + * Sends a discord code to the backend, * and returns the resultant JWT and expiry date. * * @throws { APIErrors } On error, the APIErrors.Message is set, and an APIErrors object is thrown. @@ -218,27 +218,26 @@ export async function requestBackendJWT(code: string): Promise { } /** - * Handle a full authorization flow. Sets a token for the specified path with the JWT and scopes. + * Handle a full authorization flow. Sets a cookie with the JWT and scopes. * * @param scopes The scopes that should be authorized for the application. * @param disableFunction An optional function that can disable a component while processing. - * @param path The site path to save the token under. * * @throws { APIErrors } See documentation on { requestBackendJWT }. */ -export default async function authorize(scopes: OAuthScopes[] = [], disableFunction?: (newState: boolean) => void, path = "/"): Promise { - if (!checkScopes(scopes, path)) { +export default async function authorize(scopes: OAuthScopes[] = [], disableFunction?: (newState: boolean) => void): Promise { + if (!checkScopes(scopes)) { const cookies = new Cookies; - cookies.remove(CookieNames.Token + path); - cookies.remove(CookieNames.Scopes + path); + cookies.remove(CookieNames.Token); + cookies.remove(CookieNames.Scopes); if (disableFunction) { disableFunction(true); } await getDiscordCode(scopes).then(async discord_response =>{ await requestBackendJWT(discord_response.code).then(backend_response => { - const options: CookieSetOptions = {sameSite: "strict", expires: backend_response.Expiry, secure: PRODUCTION, path: path}; + const options: CookieSetOptions = {sameSite: "strict", expires: backend_response.Expiry, secure: PRODUCTION}; - cookies.set(CookieNames.Token + path, backend_response.JWT, options); - cookies.set(CookieNames.Scopes + path, discord_response.cleanedScopes, options); + cookies.set(CookieNames.Token, backend_response.JWT, options); + cookies.set(CookieNames.Scopes, discord_response.cleanedScopes, options); }); }).finally(() => { if (disableFunction) { disableFunction(false); } diff --git a/src/components/OAuth2Button.tsx b/src/components/OAuth2Button.tsx index 1ee456c..25c5871 100644 --- a/src/components/OAuth2Button.tsx +++ b/src/components/OAuth2Button.tsx @@ -11,7 +11,6 @@ import { selectable } from "../commonStyles"; interface OAuth2ButtonProps { scopes?: OAuthScopes[], - path?: string, rerender: () => void } @@ -47,7 +46,7 @@ const errorStyles = css` `; async function login(props: OAuth2ButtonProps, errorDialog: React.RefObject, setDisabled: (newState: boolean) => void) { - await authenticate(props.scopes, setDisabled, props.path).catch((reason: APIErrors) => { + await authenticate(props.scopes, setDisabled).catch((reason: APIErrors) => { // Display Error Message if (errorDialog.current) { errorDialog.current.style.visibility = "visible"; @@ -60,7 +59,7 @@ async function login(props: OAuth2ButtonProps, errorDialog: React.RefObject { - PAGE_PATH = "/form" - containerStyles = css` margin: auto; width: 50%; @@ -97,9 +95,9 @@ class Navigation extends React.Component { let submit = null; if (this.props.form_state) { - if (this.props.scopes.includes(OAuthScopes.Identify) && !checkScopes(this.props.scopes, this.PAGE_PATH)) { + if (this.props.scopes.includes(OAuthScopes.Identify) && !checkScopes(this.props.scopes)) { // Render OAuth button if login is required, and the scopes needed are not available - submit = this.setState({"logged_in": true})}/>; + submit = this.setState({"logged_in": true})}/>; } else { submit = ; } -- cgit v1.2.3 From 604620f5f98a76461741212f9a78ea2468bce814 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sat, 20 Feb 2021 03:55:27 +0300 Subject: Adds Token Refresh Adds automatic token refresh, and removes manual setting of JWT. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/auth.ts | 84 +++++++++++++++++++++++++++++++++++-------------------- src/api/client.ts | 3 +- 2 files changed, 55 insertions(+), 32 deletions(-) (limited to 'src/api/auth.ts') diff --git a/src/api/auth.ts b/src/api/auth.ts index cfaa563..1aba307 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -10,21 +10,16 @@ const PRODUCTION = process.env.NODE_ENV !== "development"; * Authorization result as returned from the backend. */ interface AuthResult { - token: string, + username: string, expiry: string } -interface JWTResponse { - JWT: string, - Expiry: Date -} - /** * Name properties for authorization cookies. */ enum CookieNames { Scopes = "DiscordOAuthScopes", - Token = "FormBackendToken" + Username = "DiscordUsername" } export interface APIErrors { @@ -169,12 +164,12 @@ export async function getDiscordCode(scopes: OAuthScopes[]): Promise<{code: stri } /** - * Sends a discord code to the backend, - * and returns the resultant JWT and expiry date. + * Sends a discord code to the backend, which sets an authentication JWT + * and returns the Discord username. * * @throws { APIErrors } On error, the APIErrors.Message is set, and an APIErrors object is thrown. */ -export async function requestBackendJWT(code: string): Promise { +export async function requestBackendJWT(code: string): Promise<{username: string, maxAge: number}> { const reason: APIErrors = { Message: APIErrorMessages.Unknown, Error: null }; let result; @@ -196,10 +191,8 @@ export async function requestBackendJWT(code: string): Promise { throw error; }).then((response: AxiosResponse) => { - const expiry = new Date(); - expiry.setTime(Date.parse(response.data.expiry)); - - return {JWT: response.data.token, Expiry: expiry}; + const expiry = Date.parse(response.data.expiry); + return {username: response.data.username, maxAge: (expiry - Date.now()) / 1000}; }); } catch (e) { if (reason.Error === null) { @@ -209,7 +202,7 @@ export async function requestBackendJWT(code: string): Promise { throw reason; } - if (!result.JWT || !result.Expiry) { + if (!result || !result.username || !result.maxAge) { reason.Message = APIErrorMessages.BadResponse; throw reason; } @@ -217,32 +210,61 @@ export async function requestBackendJWT(code: string): Promise { return result; } +/** + * Refresh the backend authentication JWT. Returns the success of the operation, and silently handles denied requests. + */ +export async function refreshBackendJWT(): Promise { + const cookies = new Cookies(); + + let pass = true; + APIClient.post("/auth/refresh").then((response: AxiosResponse) => { + cookies.set(CookieNames.Username, response.data.username, {sameSite: "strict", secure: PRODUCTION}); + + const expiry = Date.parse(response.data.expiry); + setTimeout(refreshBackendJWT, (expiry * 0.9)); + }).catch(() => { + pass = false; + cookies.remove(CookieNames.Scopes); + }); + + return new Promise(resolve => resolve(pass)); +} + /** * Handle a full authorization flow. Sets a cookie with the JWT and scopes. * * @param scopes The scopes that should be authorized for the application. * @param disableFunction An optional function that can disable a component while processing. + * @param refresh If true, the token refresh will be scehduled automatically * * @throws { APIErrors } See documentation on { requestBackendJWT }. */ -export default async function authorize(scopes: OAuthScopes[] = [], disableFunction?: (newState: boolean) => void): Promise { - if (!checkScopes(scopes)) { - const cookies = new Cookies; - cookies.remove(CookieNames.Token); - cookies.remove(CookieNames.Scopes); +export default async function authorize(scopes: OAuthScopes[] = [], disableFunction?: (newState: boolean) => void, refresh = true): Promise { + if (checkScopes(scopes)) { + return; + } - if (disableFunction) { disableFunction(true); } - await getDiscordCode(scopes).then(async discord_response =>{ - await requestBackendJWT(discord_response.code).then(backend_response => { - const options: CookieSetOptions = {sameSite: "strict", expires: backend_response.Expiry, secure: PRODUCTION}; + const cookies = new Cookies; + cookies.remove(CookieNames.Scopes); - cookies.set(CookieNames.Token, backend_response.JWT, options); - cookies.set(CookieNames.Scopes, discord_response.cleanedScopes, options); - }); - }).finally(() => { - if (disableFunction) { disableFunction(false); } + if (disableFunction) { disableFunction(true); } + await getDiscordCode(scopes).then(async discord_response =>{ + await requestBackendJWT(discord_response.code).then(backend_response => { + const options: CookieSetOptions = {sameSite: "strict", secure: PRODUCTION}; + cookies.set(CookieNames.Username, backend_response.username, options); + + options.maxAge = backend_response.maxAge; + cookies.set(CookieNames.Scopes, discord_response.cleanedScopes, options); + + if (refresh) { + // Schedule refresh after 90% of it's age + setTimeout(refreshBackendJWT, (backend_response.maxAge * 0.9) * 1000); + } }); + }).finally(() => { + if (disableFunction) { disableFunction(false); } + }); - return new Promise(resolve => resolve()); - } + + return new Promise(resolve => resolve()); } diff --git a/src/api/client.ts b/src/api/client.ts index b534938..a9499cc 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -2,5 +2,6 @@ import axios from "axios"; export default axios.create({ - baseURL: process.env.BACKEND_URL + baseURL: process.env.BACKEND_URL, + withCredentials: true }); -- cgit v1.2.3 From 54fd22c22ef1213ab5a8096d15f6f4cc79ac0998 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sun, 21 Feb 2021 01:10:34 +0300 Subject: Saves Username & Scopes On `/` Path Explicitly sets the path attribute of username and scope cookies, to ensure they work correctly across page transitions, and to match the actual authorization cookie. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/api/auth.ts') diff --git a/src/api/auth.ts b/src/api/auth.ts index 1aba307..7bdf2bb 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -218,7 +218,7 @@ export async function refreshBackendJWT(): Promise { let pass = true; APIClient.post("/auth/refresh").then((response: AxiosResponse) => { - cookies.set(CookieNames.Username, response.data.username, {sameSite: "strict", secure: PRODUCTION}); + cookies.set(CookieNames.Username, response.data.username, {sameSite: "strict", secure: PRODUCTION, path: "/"}); const expiry = Date.parse(response.data.expiry); setTimeout(refreshBackendJWT, (expiry * 0.9)); @@ -250,7 +250,7 @@ export default async function authorize(scopes: OAuthScopes[] = [], disableFunct if (disableFunction) { disableFunction(true); } await getDiscordCode(scopes).then(async discord_response =>{ await requestBackendJWT(discord_response.code).then(backend_response => { - const options: CookieSetOptions = {sameSite: "strict", secure: PRODUCTION}; + const options: CookieSetOptions = {sameSite: "strict", secure: PRODUCTION, path: "/"}; cookies.set(CookieNames.Username, backend_response.username, options); options.maxAge = backend_response.maxAge; -- cgit v1.2.3 From b915ce2ba225d1c1209f7499618a67524450739f Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sat, 6 Mar 2021 23:17:54 +0300 Subject: Set Username Expiry To Permanent Extends username expiry to a very far date, to prevent it from expiring on session. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/api/auth.ts') diff --git a/src/api/auth.ts b/src/api/auth.ts index 7bdf2bb..f118108 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -218,7 +218,7 @@ export async function refreshBackendJWT(): Promise { let pass = true; APIClient.post("/auth/refresh").then((response: AxiosResponse) => { - cookies.set(CookieNames.Username, response.data.username, {sameSite: "strict", secure: PRODUCTION, path: "/"}); + cookies.set(CookieNames.Username, response.data.username, {sameSite: "strict", secure: PRODUCTION, path: "/", expires: new Date(3000, 1)}); const expiry = Date.parse(response.data.expiry); setTimeout(refreshBackendJWT, (expiry * 0.9)); @@ -250,7 +250,7 @@ export default async function authorize(scopes: OAuthScopes[] = [], disableFunct if (disableFunction) { disableFunction(true); } await getDiscordCode(scopes).then(async discord_response =>{ await requestBackendJWT(discord_response.code).then(backend_response => { - const options: CookieSetOptions = {sameSite: "strict", secure: PRODUCTION, path: "/"}; + const options: CookieSetOptions = {sameSite: "strict", secure: PRODUCTION, path: "/", expires: new Date(3000, 1)}; cookies.set(CookieNames.Username, backend_response.username, options); options.maxAge = backend_response.maxAge; -- cgit v1.2.3 From 3b83545b6ef294e1234f0b4426bd083708552f22 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sun, 7 Mar 2021 17:21:07 +0300 Subject: Remove Unused OAuth Scopes Co-authored-by: Joe Banks --- src/api/auth.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) (limited to 'src/api/auth.ts') diff --git a/src/api/auth.ts b/src/api/auth.ts index f118108..50f9a69 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -41,27 +41,10 @@ export enum APIErrorMessages { * Commented out enums are locked behind whitelists. */ export enum OAuthScopes { - Bot = "bot", Connections = "connections", Email = "email", Identify = "identify", - Guilds = "guilds", - GuildsJoin = "guilds.join", - GDMJoin = "gdm.join", - MessagesRead = "messages.read", - // RPC = "rpc", - // RPC_API = "rpc.api", - // RPCNotifRead = "rpc.notifications.read", - WebhookIncoming = "webhook.incoming", - // AppsBuildsUpload = "applications.builds.upload", - AppsBuildsRead = "applications.builds.read", - AppsStoreUpdate = "applications.store.update", - AppsEntitlements = "applications.entitlements", - // RelationshipsRead = "relationships.read", - // ActivitiesRead = "activities.read", - // ActivitiesWrite = "activities.write", - AppsCommands = "applications.commands", - AppsCommandsUpdate = "applications.commands.update" + Guilds = "guilds" } /** -- cgit v1.2.3 From 7e0d4a9fffa1590353068ffd054b09c359fdadd9 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sun, 7 Mar 2021 17:39:47 +0300 Subject: Removes Unused OAuth Cleaning Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/auth.ts | 9 --------- 1 file changed, 9 deletions(-) (limited to 'src/api/auth.ts') diff --git a/src/api/auth.ts b/src/api/auth.ts index 50f9a69..160c832 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -110,13 +110,6 @@ export async function getDiscordCode(scopes: OAuthScopes[]): Promise<{code: stri "height=700,width=500,location=no,menubar=no,resizable=no,status=no,titlebar=no,left=300,top=300" ); - // Clean up on login - const interval = setInterval(() => { - if (windowRef?.closed) { - clearInterval(interval); - } - }, 500); - // Handle response const code = await new Promise(resolve => { window.onmessage = (message: MessageEvent) => { @@ -128,8 +121,6 @@ export async function getDiscordCode(scopes: OAuthScopes[]): Promise<{code: stri if (message.isTrusted) { windowRef?.close(); - clearInterval(interval); - // State integrity check if (message.data.state !== state.toString()) { // This indicates a lack of integrity -- cgit v1.2.3