Compare commits
No commits in common. "8e8f5b8faf697bf47d10a6eafb8bf3671e94824a" and "7a0c0a9007335db3ea0fcf4dd563d05ff44e99af" have entirely different histories.
8e8f5b8faf
...
7a0c0a9007
@ -29,7 +29,6 @@ const (
|
|||||||
authCookieName = "notatest.refresh_token"
|
authCookieName = "notatest.refresh_token"
|
||||||
viewCookieName = "notatest.expires_at"
|
viewCookieName = "notatest.expires_at"
|
||||||
authCookiePath = "/api/auth/cookie"
|
authCookiePath = "/api/auth/cookie"
|
||||||
viewCookiePath = "/"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -595,29 +594,25 @@ func (rs authResource) userFromCtxClaims(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rs authResource) setAuthCookies(w http.ResponseWriter, tokenPair *tokenPair, clearCookies bool) {
|
func (rs authResource) setAuthCookies(w http.ResponseWriter, tokenPair *tokenPair, clearCookies bool) {
|
||||||
|
expirationTime := time.Now().Add(refreshTokenDuration)
|
||||||
|
expirationUnix := strconv.FormatInt(expirationTime.Unix(), 10)
|
||||||
|
|
||||||
log.Debug().Msgf("Setting authentication cookies (clearCookies: %t)", clearCookies)
|
log.Debug().Msgf("Setting authentication cookies (clearCookies: %t)", clearCookies)
|
||||||
|
|
||||||
var maxAge int
|
var maxAge int
|
||||||
var expirationTime time.Time
|
var value string
|
||||||
var rtValue string
|
|
||||||
|
|
||||||
if clearCookies {
|
if clearCookies {
|
||||||
expirationTime = time.Now()
|
|
||||||
maxAge = 0 // Expires immediately
|
maxAge = 0 // Expires immediately
|
||||||
rtValue = ""
|
value = ""
|
||||||
} else {
|
} else {
|
||||||
expirationTime = time.Now().Add(refreshTokenDuration)
|
|
||||||
maxAge = int(refreshTokenDuration.Seconds())
|
maxAge = int(refreshTokenDuration.Seconds())
|
||||||
rtValue = tokenPair.RefreshToken
|
value = tokenPair.RefreshToken
|
||||||
}
|
}
|
||||||
|
|
||||||
expirationValue := strconv.FormatInt(expirationTime.Unix(), 10)
|
|
||||||
log.Debug().Msgf("AC: {path='%s', maxAge='%d'}, VC: {path='%s', maxAge='%d'}", authCookiePath, maxAge, viewCookiePath, maxAge)
|
|
||||||
|
|
||||||
// The actual auth cookie is httpOnly, i.e. not viewable by the client
|
// The actual auth cookie is httpOnly, i.e. not viewable by the client
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: authCookieName,
|
Name: authCookieName,
|
||||||
Value: rtValue,
|
Value: value,
|
||||||
Domain: rs.Config.Domain,
|
Domain: rs.Config.Domain,
|
||||||
Path: authCookiePath,
|
Path: authCookiePath,
|
||||||
MaxAge: maxAge,
|
MaxAge: maxAge,
|
||||||
@ -627,13 +622,12 @@ func (rs authResource) setAuthCookies(w http.ResponseWriter, tokenPair *tokenPai
|
|||||||
})
|
})
|
||||||
|
|
||||||
// The information cookie can be used by the client to check how long it'll take until the
|
// The information cookie can be used by the client to check how long it'll take until the
|
||||||
// actual auth cookie expires (notably `HttpOnly: false` and `Path: "/"` must be set for
|
// actual auth cookie expires (notably `HttpOnly: false` is a must)
|
||||||
// the cookie to be readable from our client-side implementation)
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: viewCookieName,
|
Name: viewCookieName,
|
||||||
Value: expirationValue,
|
Value: expirationUnix,
|
||||||
Domain: rs.Config.Domain,
|
Domain: rs.Config.Domain,
|
||||||
Path: viewCookiePath,
|
Path: authCookiePath,
|
||||||
MaxAge: maxAge,
|
MaxAge: maxAge,
|
||||||
HttpOnly: false,
|
HttpOnly: false,
|
||||||
Secure: rs.Config.IsProd,
|
Secure: rs.Config.IsProd,
|
||||||
|
@ -20,8 +20,15 @@ type SvcConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sc *SvcConfig) allowedOrigins() []string {
|
func (sc *SvcConfig) allowedOrigins() []string {
|
||||||
allowed := []string{sc.FrontendURL}
|
var allowed []string
|
||||||
|
if sc.IsProd {
|
||||||
|
allowed = []string{sc.FrontendURL}
|
||||||
|
} else {
|
||||||
|
allowed = []string{"http://localhost:5173"}
|
||||||
|
}
|
||||||
|
|
||||||
log.Debug().Msgf("CORS allowedOrigins: %v", allowed)
|
log.Debug().Msgf("CORS allowedOrigins: %v", allowed)
|
||||||
|
|
||||||
return allowed
|
return allowed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,5 @@
|
|||||||
import { get, writable, type Writable } from "svelte/store"
|
import { get, writable, type Writable } from "svelte/store"
|
||||||
import {
|
import { API_BASE_ADDR, AT_EXP_MS, CSRF_EXP_MS, REFRESH_BUF, UUID_REGEX } from "./const"
|
||||||
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 { goto } from "$app/navigation"
|
||||||
import { usersPagination } from "./pages"
|
import { usersPagination } from "./pages"
|
||||||
|
|
||||||
@ -94,7 +84,7 @@ interface ApiFullVersionResponse {
|
|||||||
created_at: string
|
created_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some of these could just be local variables as not all of them are being used globally
|
// Some of these could just be local variables as they're not used globally
|
||||||
export const currentUser: Writable<User | null> = writable(null)
|
export const currentUser: Writable<User | null> = writable(null)
|
||||||
export const currentFullNote: Writable<FullNote | null> = writable(null)
|
export const currentFullNote: Writable<FullNote | null> = writable(null)
|
||||||
export const availableNotes: Writable<NoteMetadata[] | null> = writable(null)
|
export const availableNotes: Writable<NoteMetadata[] | null> = writable(null)
|
||||||
@ -134,13 +124,7 @@ class ApiClient {
|
|||||||
// will be automatically included to corresponding requests by browser and CSRF rotation is
|
// will be automatically included to corresponding requests by browser and CSRF rotation is
|
||||||
// handled by the `refreshAccessToken` method
|
// handled by the `refreshAccessToken` method
|
||||||
if (options.useBearerAuth) {
|
if (options.useBearerAuth) {
|
||||||
try {
|
await this.checkAndRefreshAccessToken()
|
||||||
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()
|
return await fn()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -183,10 +167,8 @@ class ApiClient {
|
|||||||
private async checkAndRefreshAccessToken(): Promise<void> {
|
private async checkAndRefreshAccessToken(): Promise<void> {
|
||||||
// Notably we must check whether we even have an authentication cookie present
|
// Notably we must check whether we even have an authentication cookie present
|
||||||
// as that's obviously required for completing the token rotation procedure
|
// as that's obviously required for completing the token rotation procedure
|
||||||
if (this.refreshInProgress) {
|
if (this.refreshInProgress || !this.isAuthCookieValid) {
|
||||||
return
|
return
|
||||||
} else if (!this.isAuthCookieValid()) {
|
|
||||||
throw new Error("Session expired, please authenticate again.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeSinceUpdate = Date.now() - this.lastAtUpdate.getTime()
|
const timeSinceUpdate = Date.now() - this.lastAtUpdate.getTime()
|
||||||
@ -200,8 +182,6 @@ class ApiClient {
|
|||||||
this.refreshInProgress = true
|
this.refreshInProgress = true
|
||||||
await this.refreshAccessToken()
|
await this.refreshAccessToken()
|
||||||
this.refreshInProgress = false
|
this.refreshInProgress = false
|
||||||
} else {
|
|
||||||
console.log("no need to rotate tokens")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,13 +246,9 @@ class ApiClient {
|
|||||||
private async handleLocalLogout(): Promise<void> {
|
private async handleLocalLogout(): Promise<void> {
|
||||||
this.lastAtUpdate = new Date(0)
|
this.lastAtUpdate = new Date(0)
|
||||||
this.lastCsrfUpdate = new Date(0)
|
this.lastCsrfUpdate = new Date(0)
|
||||||
|
|
||||||
currentUser.set(null)
|
currentUser.set(null)
|
||||||
accessToken.set(null)
|
accessToken.set(null)
|
||||||
csrfToken.set(null)
|
csrfToken.set(null)
|
||||||
|
|
||||||
this.deleteViewCookie()
|
|
||||||
|
|
||||||
await goto("/login")
|
await goto("/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,47 +259,23 @@ class ApiClient {
|
|||||||
|
|
||||||
private isAuthCookieValid(): boolean {
|
private isAuthCookieValid(): boolean {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const viewCookie = this.getCookieValue(this.viewCookieName)
|
const cookies = document.cookie.split(";")
|
||||||
|
|
||||||
if (!viewCookie) {
|
for (let cookie of cookies) {
|
||||||
console.log("view cookie not found")
|
const [name, value] = cookie.trim().split("=")
|
||||||
return false
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = viewCookie.split("=")[1]
|
// Fallback when the cookie can't be found
|
||||||
const expirationTimestamp = parseInt(value, 10)
|
return false
|
||||||
|
|
||||||
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 {
|
private deserializeUser(apiResponse: ApiUserResponse): User {
|
||||||
@ -478,9 +430,7 @@ class ApiClient {
|
|||||||
})
|
})
|
||||||
const data = await this.handleResponse<ApiUserResponse>(response, { useBearerAuth: false })
|
const data = await this.handleResponse<ApiUserResponse>(response, { useBearerAuth: false })
|
||||||
const user = this.deserializeUser(data)
|
const user = this.deserializeUser(data)
|
||||||
|
|
||||||
console.log(user)
|
console.log(user)
|
||||||
|
|
||||||
currentUser.set(user)
|
currentUser.set(user)
|
||||||
},
|
},
|
||||||
{ useBearerAuth: true }
|
{ useBearerAuth: true }
|
||||||
|
@ -2,21 +2,14 @@
|
|||||||
// will automatically be proxied to the correct destination
|
// will automatically be proxied to the correct destination
|
||||||
export const API_BASE_ADDR = import.meta.env.PROD ? "/api" : "http://localhost:8080/api"
|
export const API_BASE_ADDR = import.meta.env.PROD ? "/api" : "http://localhost:8080/api"
|
||||||
|
|
||||||
// Lifetimes of *in-memory* authentication tokens
|
// Probably shouldn't be hardcoded, but instead read from .env
|
||||||
export const AT_EXP_MS = 15 * 60 * 1000 // 15 min.
|
export const AT_EXP_MS = 15 * 60 * 1000 // 15 min.
|
||||||
export const CSRF_EXP_MS = 12 * 60 * 60 * 1000 // 12 h.
|
export const CSRF_EXP_MS = 12 * 60 * 60 * 1000 // 12 h.
|
||||||
export const REFRESH_BUF = 30 * 1000 // 30 s.
|
export const REFRESH_BUF = 30 * 1000 // 30 s.
|
||||||
|
|
||||||
// User registration password restrictions
|
|
||||||
export const MIN_PASSWORD_LENGTH = 12
|
export const MIN_PASSWORD_LENGTH = 12
|
||||||
export const MAX_PASSWORD_LENGTH = 72
|
export const MAX_PASSWORD_LENGTH = 72
|
||||||
export const MIN_PASSWORD_ENTROPY = 60.0
|
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
|
export const TITLE_MAX_LENGTH = 150
|
||||||
|
|
||||||
@ -25,8 +18,10 @@ export const UUID_REGEX = new RegExp(
|
|||||||
"i"
|
"i"
|
||||||
)
|
)
|
||||||
|
|
||||||
// View cookie configuration (holds UNIX timestamp value of the actual refresh token cookie's expiration date)
|
// Underestimation compared to the backend to prevent mismatches when registering
|
||||||
export const VIEW_COOKIE_PATH = import.meta.env.VITE_VIEW_COOKIE_PATH || "/"
|
export const ENTROPY_CLASSES: Array<[RegExp, number]> = [
|
||||||
export const VIEW_COOKIE_DOMAIN = import.meta.env.VITE_VIEW_COOKIE_DOMAIN || "localhost"
|
[/[a-z]/, 24], // 26
|
||||||
export const COOKIE_SAME_SITE = import.meta.env.VITE_COOKIE_SAME_SITE || "strict"
|
[/[A-Z]/, 24], // 26
|
||||||
export const COOKIE_SECURE = import.meta.env.PROD ? true : false
|
[/\d/, 10],
|
||||||
|
[/[!@#$%^&*()\-_+=\[\]{}|;:'",.<>\/?`~\\]/, 32] // 40
|
||||||
|
]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user