diff options
author | 2021-02-20 03:55:27 +0300 | |
---|---|---|
committer | 2021-02-20 03:58:43 +0300 | |
commit | 604620f5f98a76461741212f9a78ea2468bce814 (patch) | |
tree | 3bd5921c958aa99825056267f041e977fec76c7a /src/api | |
parent | Merge branch 'main' into discord-oauth (diff) |
Adds Token Refresh
Adds automatic token refresh, and removes manual setting of JWT.
Signed-off-by: Hassan Abouelela <[email protected]>
Diffstat (limited to 'src/api')
-rw-r--r-- | src/api/auth.ts | 84 | ||||
-rw-r--r-- | src/api/client.ts | 3 |
2 files changed, 55 insertions, 32 deletions
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<JWTResponse> { +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<JWTResponse> { throw error; }).then((response: AxiosResponse<AuthResult>) => { - 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<JWTResponse> { throw reason; } - if (!result.JWT || !result.Expiry) { + if (!result || !result.username || !result.maxAge) { reason.Message = APIErrorMessages.BadResponse; throw reason; } @@ -218,31 +211,60 @@ export async function requestBackendJWT(code: string): Promise<JWTResponse> { } /** + * Refresh the backend authentication JWT. Returns the success of the operation, and silently handles denied requests. + */ +export async function refreshBackendJWT(): Promise<boolean> { + const cookies = new Cookies(); + + let pass = true; + APIClient.post("/auth/refresh").then((response: AxiosResponse<AuthResult>) => { + 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<void> { - 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<void> { + 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<void>(resolve => resolve()); - } + + return new Promise<void>(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 }); |