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)
|
export const cError: Writable<string | null> = writable(null)
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
|
private viewCookieName: string
|
||||||
private baseUrl: string
|
private baseUrl: string
|
||||||
private lastAtUpdate = new Date(0)
|
private lastAtUpdate = new Date(0)
|
||||||
private lastCsrfUpdate = new Date(0)
|
private lastCsrfUpdate = new Date(0)
|
||||||
@ -63,6 +64,7 @@ class ApiClient {
|
|||||||
|
|
||||||
constructor(baseUrl: string) {
|
constructor(baseUrl: string) {
|
||||||
this.baseUrl = baseUrl
|
this.baseUrl = baseUrl
|
||||||
|
this.viewCookieName = "notatest.expires_at"
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleRequest<T>(
|
private async handleRequest<T>(
|
||||||
@ -101,7 +103,7 @@ class ApiClient {
|
|||||||
// but it's still good to have as a fallback
|
// but it's still good to have as a fallback
|
||||||
try {
|
try {
|
||||||
console.log("unexpected 401 caught, attempting refresh")
|
console.log("unexpected 401 caught, attempting refresh")
|
||||||
await this.refreshAccessToken()
|
await this.checkAndRefreshAccessToken()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("refresh attempt not successful")
|
console.log("refresh attempt not successful")
|
||||||
await this.handleLocalLogout()
|
await this.handleLocalLogout()
|
||||||
@ -120,7 +122,11 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async checkAndRefreshAccessToken(): Promise<void> {
|
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 timeSinceUpdate = Date.now() - this.lastAtUpdate.getTime()
|
||||||
const needsRefresh = timeSinceUpdate > AT_EXP_MS - REFRESH_BUF
|
const needsRefresh = timeSinceUpdate > AT_EXP_MS - REFRESH_BUF
|
||||||
@ -141,10 +147,10 @@ class ApiClient {
|
|||||||
async () => {
|
async () => {
|
||||||
const response = await fetch(`${this.baseUrl}/auth/cookie/refresh`, {
|
const response = await fetch(`${this.baseUrl}/auth/cookie/refresh`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
|
||||||
headers: {
|
headers: {
|
||||||
...(await this.getCsrfHeader())
|
...(await this.getCsrfHeader())
|
||||||
}
|
},
|
||||||
|
credentials: "include"
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -166,11 +172,12 @@ class ApiClient {
|
|||||||
const needsRefresh = timeSinceUpdate > CSRF_EXP_MS - REFRESH_BUF
|
const needsRefresh = timeSinceUpdate > CSRF_EXP_MS - REFRESH_BUF
|
||||||
|
|
||||||
if (!token || needsRefresh) {
|
if (!token || needsRefresh) {
|
||||||
|
console.log("refreshing csrf token")
|
||||||
await this.refreshCsrfToken()
|
await this.refreshCsrfToken()
|
||||||
token = get(csrfToken)
|
token = get(csrfToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { "X-CSRF-Token": token || "" }
|
return { "X-Csrf-Token": token || "" }
|
||||||
}
|
}
|
||||||
|
|
||||||
private async refreshCsrfToken(): Promise<void> {
|
private async refreshCsrfToken(): Promise<void> {
|
||||||
@ -185,7 +192,7 @@ class ApiClient {
|
|||||||
throw new Error("Fetching CSRF token failed.")
|
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)
|
csrfToken.set(newToken)
|
||||||
this.lastCsrfUpdate = new Date()
|
this.lastCsrfUpdate = new Date()
|
||||||
},
|
},
|
||||||
@ -207,6 +214,27 @@ class ApiClient {
|
|||||||
return { Authorization: `Bearer ${token}` }
|
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> {
|
public async register(username: string, password: string): Promise<void> {
|
||||||
return this.handleRequest(
|
return this.handleRequest(
|
||||||
async () => {
|
async () => {
|
||||||
@ -237,7 +265,8 @@ class ApiClient {
|
|||||||
const response = await fetch(`${this.baseUrl}/auth/login?${params}`, {
|
const response = await fetch(`${this.baseUrl}/auth/login?${params}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
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<{
|
const { access_token: token, user } = await this.handleResponse<{
|
||||||
@ -263,7 +292,8 @@ class ApiClient {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
...this.getAuthHeader()
|
...this.getAuthHeader()
|
||||||
}
|
},
|
||||||
|
credentials: "include"
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.status === 204) {
|
if (response.status === 204) {
|
||||||
@ -328,7 +358,8 @@ class ApiClient {
|
|||||||
const response = await fetch(`${this.baseUrl}/auth/owner`, {
|
const response = await fetch(`${this.baseUrl}/auth/owner`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
...this.getAuthHeader()
|
...this.getAuthHeader(),
|
||||||
|
credentials: "include"
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ password })
|
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, {
|
const notes = await this.handleResponse<NoteMetadataResponse[]>(response, {
|
||||||
useBearerAuth: false
|
useBearerAuth: false
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user