feat: client-side view cookie reading/deletion
This commit is contained in:
parent
d7900e8078
commit
8e8f5b8faf
@ -1,5 +1,15 @@
|
||||
import { get, writable, type Writable } from "svelte/store"
|
||||
import { API_BASE_ADDR, AT_EXP_MS, CSRF_EXP_MS, REFRESH_BUF, UUID_REGEX } from "./const"
|
||||
import {
|
||||
API_BASE_ADDR,
|
||||
AT_EXP_MS,
|
||||
COOKIE_SAME_SITE,
|
||||
COOKIE_SECURE,
|
||||
CSRF_EXP_MS,
|
||||
REFRESH_BUF,
|
||||
UUID_REGEX,
|
||||
VIEW_COOKIE_DOMAIN,
|
||||
VIEW_COOKIE_PATH
|
||||
} from "./const"
|
||||
import { goto } from "$app/navigation"
|
||||
import { usersPagination } from "./pages"
|
||||
|
||||
@ -84,7 +94,7 @@ interface ApiFullVersionResponse {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// Some of these could just be local variables as they're not used globally
|
||||
// Some of these could just be local variables as not all of them are being used globally
|
||||
export const currentUser: Writable<User | null> = writable(null)
|
||||
export const currentFullNote: Writable<FullNote | null> = writable(null)
|
||||
export const availableNotes: Writable<NoteMetadata[] | null> = writable(null)
|
||||
@ -124,7 +134,13 @@ class ApiClient {
|
||||
// will be automatically included to corresponding requests by browser and CSRF rotation is
|
||||
// handled by the `refreshAccessToken` method
|
||||
if (options.useBearerAuth) {
|
||||
await this.checkAndRefreshAccessToken()
|
||||
try {
|
||||
await this.checkAndRefreshAccessToken()
|
||||
} catch (err) {
|
||||
console.log("refresh attempt not successful")
|
||||
await this.handleLocalLogout()
|
||||
throw new Error("Session expired, please authenticate again.")
|
||||
}
|
||||
}
|
||||
return await fn()
|
||||
} catch (err) {
|
||||
@ -167,8 +183,10 @@ class ApiClient {
|
||||
private async checkAndRefreshAccessToken(): Promise<void> {
|
||||
// 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) {
|
||||
if (this.refreshInProgress) {
|
||||
return
|
||||
} else if (!this.isAuthCookieValid()) {
|
||||
throw new Error("Session expired, please authenticate again.")
|
||||
}
|
||||
|
||||
const timeSinceUpdate = Date.now() - this.lastAtUpdate.getTime()
|
||||
@ -182,6 +200,8 @@ class ApiClient {
|
||||
this.refreshInProgress = true
|
||||
await this.refreshAccessToken()
|
||||
this.refreshInProgress = false
|
||||
} else {
|
||||
console.log("no need to rotate tokens")
|
||||
}
|
||||
}
|
||||
|
||||
@ -246,9 +266,13 @@ class ApiClient {
|
||||
private async handleLocalLogout(): Promise<void> {
|
||||
this.lastAtUpdate = new Date(0)
|
||||
this.lastCsrfUpdate = new Date(0)
|
||||
|
||||
currentUser.set(null)
|
||||
accessToken.set(null)
|
||||
csrfToken.set(null)
|
||||
|
||||
this.deleteViewCookie()
|
||||
|
||||
await goto("/login")
|
||||
}
|
||||
|
||||
@ -259,23 +283,47 @@ class ApiClient {
|
||||
|
||||
private isAuthCookieValid(): boolean {
|
||||
const now = new Date()
|
||||
const cookies = document.cookie.split(";")
|
||||
const viewCookie = this.getCookieValue(this.viewCookieName)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
if (!viewCookie) {
|
||||
console.log("view cookie not found")
|
||||
return false
|
||||
}
|
||||
|
||||
// Fallback when the cookie can't be found
|
||||
return false
|
||||
const value = viewCookie.split("=")[1]
|
||||
const expirationTimestamp = parseInt(value, 10)
|
||||
|
||||
if (isNaN(expirationTimestamp)) {
|
||||
console.log(`invalid expiration timestamp: ${value}`)
|
||||
return false
|
||||
}
|
||||
|
||||
const expirationDate = new Date(expirationTimestamp * 1000)
|
||||
console.log(`auth cookie expiration: ${expirationDate}`)
|
||||
|
||||
return now < expirationDate
|
||||
}
|
||||
|
||||
private deleteViewCookie() {
|
||||
const viewCookie = this.getCookieValue(this.viewCookieName)
|
||||
if (!viewCookie) {
|
||||
return
|
||||
}
|
||||
|
||||
// Overwrite the view cookie with details that match with the real cookie
|
||||
document.cookie = `${this.viewCookieName}=;path=${VIEW_COOKIE_PATH};domain=${VIEW_COOKIE_DOMAIN};expires=Thu, 01 Jan 1970 00:00:00 GMT;${COOKIE_SECURE ? "secure;" : ""}sameSite=${COOKIE_SAME_SITE}`
|
||||
}
|
||||
|
||||
private getCookieValue(cookieName: string): string | null {
|
||||
const cookieString = document.cookie
|
||||
const cookies = cookieString.split(";").map((cookie) => cookie.trim())
|
||||
const cookieValue = cookies.find((cookie) => cookie.startsWith(`${cookieName}=`))
|
||||
|
||||
if (!cookieValue) {
|
||||
return null
|
||||
}
|
||||
|
||||
return cookieValue
|
||||
}
|
||||
|
||||
private deserializeUser(apiResponse: ApiUserResponse): User {
|
||||
@ -430,7 +478,9 @@ class ApiClient {
|
||||
})
|
||||
const data = await this.handleResponse<ApiUserResponse>(response, { useBearerAuth: false })
|
||||
const user = this.deserializeUser(data)
|
||||
|
||||
console.log(user)
|
||||
|
||||
currentUser.set(user)
|
||||
},
|
||||
{ useBearerAuth: true }
|
||||
|
@ -2,14 +2,21 @@
|
||||
// will automatically be proxied to the correct destination
|
||||
export const API_BASE_ADDR = import.meta.env.PROD ? "/api" : "http://localhost:8080/api"
|
||||
|
||||
// Probably shouldn't be hardcoded, but instead read from .env
|
||||
// Lifetimes of *in-memory* authentication tokens
|
||||
export const AT_EXP_MS = 15 * 60 * 1000 // 15 min.
|
||||
export const CSRF_EXP_MS = 12 * 60 * 60 * 1000 // 12 h.
|
||||
export const REFRESH_BUF = 30 * 1000 // 30 s.
|
||||
|
||||
// User registration password restrictions
|
||||
export const MIN_PASSWORD_LENGTH = 12
|
||||
export const MAX_PASSWORD_LENGTH = 72
|
||||
export const MIN_PASSWORD_ENTROPY = 60.0
|
||||
export const ENTROPY_CLASSES: Array<[RegExp, number]> = [
|
||||
[/[a-z]/, 24], // 26
|
||||
[/[A-Z]/, 24], // 26
|
||||
[/\d/, 10],
|
||||
[/[!@#$%^&*()\-_+=\[\]{}|;:'",.<>\/?`~\\]/, 32] // 40
|
||||
]
|
||||
|
||||
export const TITLE_MAX_LENGTH = 150
|
||||
|
||||
@ -18,10 +25,8 @@ export const UUID_REGEX = new RegExp(
|
||||
"i"
|
||||
)
|
||||
|
||||
// Underestimation compared to the backend to prevent mismatches when registering
|
||||
export const ENTROPY_CLASSES: Array<[RegExp, number]> = [
|
||||
[/[a-z]/, 24], // 26
|
||||
[/[A-Z]/, 24], // 26
|
||||
[/\d/, 10],
|
||||
[/[!@#$%^&*()\-_+=\[\]{}|;:'",.<>\/?`~\\]/, 32] // 40
|
||||
]
|
||||
// View cookie configuration (holds UNIX timestamp value of the actual refresh token cookie's expiration date)
|
||||
export const VIEW_COOKIE_PATH = import.meta.env.VITE_VIEW_COOKIE_PATH || "/"
|
||||
export const VIEW_COOKIE_DOMAIN = import.meta.env.VITE_VIEW_COOKIE_DOMAIN || "localhost"
|
||||
export const COOKIE_SAME_SITE = import.meta.env.VITE_COOKIE_SAME_SITE || "strict"
|
||||
export const COOKIE_SECURE = import.meta.env.PROD ? true : false
|
||||
|
Loading…
x
Reference in New Issue
Block a user