feat: auth cookie expiration check

This commit is contained in:
ae 2025-04-17 16:06:10 +03:00
parent 2e188c26f3
commit 90ef589197
Signed by: ae
GPG Key ID: 995EFD5C1B532B3E

View File

@ -56,6 +56,7 @@ export const isPending: Writable<boolean> = writable(false)
export const cError: Writable<string | null> = 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<T>(
@ -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<void> {
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<void> {
@ -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<void> {
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<NoteMetadataResponse[]>(response, {
useBearerAuth: false
})