diff --git a/web/src/lib/client.ts b/web/src/lib/client.ts index 5de8379..92be515 100644 --- a/web/src/lib/client.ts +++ b/web/src/lib/client.ts @@ -56,6 +56,7 @@ export const isPending: Writable = writable(false) export const cError: Writable = writable(null) class ApiClient { + private viewCookieName: string private baseUrl: string private lastAtUpdate = new Date(0) private lastCsrfUpdate = new Date(0) @@ -63,6 +64,7 @@ class ApiClient { constructor(baseUrl: string) { this.baseUrl = baseUrl + this.viewCookieName = "notatest.expires_at" } private async handleRequest( @@ -101,7 +103,7 @@ class ApiClient { // but it's still good to have as a fallback try { console.log("unexpected 401 caught, attempting refresh") - await this.refreshAccessToken() + await this.checkAndRefreshAccessToken() } catch (err) { console.log("refresh attempt not successful") await this.handleLocalLogout() @@ -120,7 +122,11 @@ class ApiClient { } private async checkAndRefreshAccessToken(): Promise { - if (this.refreshInProgress) return + // Notably we must check whether we even have an authentication cookie present + // as that's obviously required for completing the token rotation procedure + if (this.refreshInProgress || !this.isAuthCookieValid) { + return + } const timeSinceUpdate = Date.now() - this.lastAtUpdate.getTime() const needsRefresh = timeSinceUpdate > AT_EXP_MS - REFRESH_BUF @@ -141,10 +147,10 @@ class ApiClient { async () => { const response = await fetch(`${this.baseUrl}/auth/cookie/refresh`, { method: "POST", - credentials: "include", headers: { ...(await this.getCsrfHeader()) - } + }, + credentials: "include" }) if (!response.ok) { @@ -166,11 +172,12 @@ class ApiClient { const needsRefresh = timeSinceUpdate > CSRF_EXP_MS - REFRESH_BUF if (!token || needsRefresh) { + console.log("refreshing csrf token") await this.refreshCsrfToken() token = get(csrfToken) } - return { "X-CSRF-Token": token || "" } + return { "X-Csrf-Token": token || "" } } private async refreshCsrfToken(): Promise { @@ -185,7 +192,7 @@ class ApiClient { throw new Error("Fetching CSRF token failed.") } - const newToken = response.headers.get("X-CSRF-Token") + const newToken = response.headers.get("X-Csrf-Token") csrfToken.set(newToken) this.lastCsrfUpdate = new Date() }, @@ -207,6 +214,27 @@ class ApiClient { return { Authorization: `Bearer ${token}` } } + private isAuthCookieValid(): boolean { + const now = new Date() + const cookies = document.cookie.split(";") + + for (let cookie of cookies) { + const [name, value] = cookie.trim().split("=") + + if (name === this.viewCookieName) { + const expirationTimestamp = parseInt(value, 10) + if (!isNaN(expirationTimestamp)) { + const expirationDate = new Date(expirationTimestamp * 1000) + console.log(`auth cookie expirationDate: ${expirationDate}`) + return now < expirationDate + } + } + } + + // Fallback when the cookie can't be found + return false + } + public async register(username: string, password: string): Promise { return this.handleRequest( async () => { @@ -237,7 +265,8 @@ class ApiClient { const response = await fetch(`${this.baseUrl}/auth/login?${params}`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ username, password }) + body: JSON.stringify({ username, password }), + credentials: "include" }) const { access_token: token, user } = await this.handleResponse<{ @@ -263,7 +292,8 @@ class ApiClient { method: "POST", headers: { ...this.getAuthHeader() - } + }, + credentials: "include" }) if (response.status === 204) { @@ -328,7 +358,8 @@ class ApiClient { const response = await fetch(`${this.baseUrl}/auth/owner`, { method: "DELETE", headers: { - ...this.getAuthHeader() + ...this.getAuthHeader(), + credentials: "include" }, body: JSON.stringify({ password }) }) @@ -414,6 +445,8 @@ class ApiClient { } }) + // TODO: handle case where no notes have yet been created, i.e. `notes` from response is `null` + const notes = await this.handleResponse(response, { useBearerAuth: false })