feat: auth cookie expiration check
This commit is contained in:
parent
2e188c26f3
commit
90ef589197
@ -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
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user