implemented automated user session login and session refresh
This commit is contained in:
parent
976cd3edb9
commit
16b5aba457
|
|
@ -0,0 +1,77 @@
|
||||||
|
import type { Handle } from "@sveltejs/kit"
|
||||||
|
|
||||||
|
import { error, redirect, json, fail } from "@sveltejs/kit"
|
||||||
|
|
||||||
|
import { Error401Cause } from "$lib/errors"
|
||||||
|
|
||||||
|
import Config from "$lib/server/config"
|
||||||
|
import UserMgmt from "$lib/server/usermgmt"
|
||||||
|
|
||||||
|
function action_fail<T = undefined>(status: number, data: T) {
|
||||||
|
return json(fail(status, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
|
|
||||||
|
const token = event.cookies.get("session")
|
||||||
|
let session = await UserMgmt.session_login(token ?? "")
|
||||||
|
|
||||||
|
if (session && event.route.id !== "/api/refresh") {
|
||||||
|
if (session.expires.getTime() < Date.now() + Config.session_refresh_grace) {
|
||||||
|
|
||||||
|
const response = await event.fetch("/api/refresh", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: token
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const new_session = await response.json()
|
||||||
|
|
||||||
|
if (new_session && new_session.token !== null && new_session.expires !== null) {
|
||||||
|
event.cookies.set("session", new_session.token, {
|
||||||
|
expires: new Date(new_session.expires),
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: "strict",
|
||||||
|
path: "/"
|
||||||
|
})
|
||||||
|
// For mobile
|
||||||
|
/*event.setHeaders({
|
||||||
|
"X-Authorization-Refresh": new_session.token
|
||||||
|
})*/
|
||||||
|
|
||||||
|
session = await UserMgmt.session_login(new_session.token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.locals.user = session?.user
|
||||||
|
|
||||||
|
if (!event.route.id) {
|
||||||
|
return error(404, { message: "Diese Seite existiert nicht" })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.route.id.startsWith("/api")) {
|
||||||
|
if (event.route.id.startsWith("/api/users") && !event.locals.user) {
|
||||||
|
return error(401, { cause: Error401Cause.NotLoggedIn, message: "Please log in" })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!event.locals.user && !event.route.id.startsWith("/login")) {
|
||||||
|
if (event.request.method === "POST") {
|
||||||
|
if (event.request.headers.get("x-sveltekit-action")) {
|
||||||
|
return action_fail(401, { cause: Error401Cause.NotLoggedIn, message: "Bitte melden Sie sich an." })
|
||||||
|
}
|
||||||
|
return error(401, { cause: Error401Cause.NotLoggedIn, message: "Bitte melden Sie sich an." })
|
||||||
|
}
|
||||||
|
return redirect(307, "/login")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await resolve(event)
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,10 @@ export class DuplicateError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const enum Error401Cause {
|
||||||
|
NotLoggedIn
|
||||||
|
}
|
||||||
|
|
||||||
export const enum RegisterResponseCause {
|
export const enum RegisterResponseCause {
|
||||||
Server = 1,
|
Server = 1,
|
||||||
MalformedRequest,
|
MalformedRequest,
|
||||||
|
|
@ -32,3 +36,10 @@ export const enum LoginResponseCause {
|
||||||
NotFound,
|
NotFound,
|
||||||
Timeout
|
Timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const enum RefreshResponseCause {
|
||||||
|
Server = 1,
|
||||||
|
MalformedRequest,
|
||||||
|
InvalidToken,
|
||||||
|
InvalidSession
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ class Config {
|
||||||
readonly is_production: boolean = process.env.NODE_ENV == "production"
|
readonly is_production: boolean = process.env.NODE_ENV == "production"
|
||||||
|
|
||||||
private _session_timeout: number = 15 * 60 * 1000
|
private _session_timeout: number = 15 * 60 * 1000
|
||||||
|
private _session_refresh_grace: number = 5 * 60 * 1000 // time until expiration
|
||||||
|
|
||||||
get log_dir(): string {
|
get log_dir(): string {
|
||||||
return this._log_dir
|
return this._log_dir
|
||||||
|
|
@ -47,6 +48,7 @@ class Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
get session_timeout(): number { return this._session_timeout }
|
get session_timeout(): number { return this._session_timeout }
|
||||||
|
get session_refresh_grace(): number { return this._session_refresh_grace }
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._log_dir = resolve_env_to_path(process.env.APP_LOG_DIR, "./data/logs")
|
this._log_dir = resolve_env_to_path(process.env.APP_LOG_DIR, "./data/logs")
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,10 @@ class Cache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invalidate_session(session: SessionData) {
|
||||||
|
this._session_map.delete(session.token)
|
||||||
|
}
|
||||||
|
|
||||||
get_session(token: string): SessionData|null {
|
get_session(token: string): SessionData|null {
|
||||||
const session_info = this._session_map.get(token)
|
const session_info = this._session_map.get(token)
|
||||||
if (!session_info) return null
|
if (!session_info) return null
|
||||||
|
|
@ -118,14 +122,39 @@ class UserMgmt {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = Crypto.randomBytes(32).toBase64()
|
const session_info = this._generate_session_for_user(user)
|
||||||
const session_info = this._cache.add(user, token, new Date(), new Date(Date.now() + Config.session_timeout))
|
|
||||||
|
|
||||||
return session_info
|
return session_info
|
||||||
}
|
}
|
||||||
|
|
||||||
async session_login(token: string): Promise<SessionData|null> {
|
async session_login(token: string): Promise<SessionData|null> {
|
||||||
return this._cache.get_session(token)
|
const session = this._cache.get_session(token)
|
||||||
|
if (!session) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (session?.expires.getTime() < Date.now()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh_session(token: string): Promise<SessionData|null> {
|
||||||
|
const session = await this.session_login(token)
|
||||||
|
if (!session) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const new_session = this._generate_session_for_user(session.user)
|
||||||
|
this._cache.invalidate_session(session)
|
||||||
|
|
||||||
|
return new_session
|
||||||
|
}
|
||||||
|
|
||||||
|
private _generate_session_for_user(user: User): SessionData {
|
||||||
|
const token = Crypto.randomBytes(32).toBase64()
|
||||||
|
const session_info = this._cache.add(user, token, new Date(), new Date(Date.now() + Config.session_timeout))
|
||||||
|
|
||||||
|
return session_info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import type { RequestHandler } from "./$types"
|
||||||
|
|
||||||
|
import { error, json } from "@sveltejs/kit"
|
||||||
|
|
||||||
|
import UserMgmt from "$lib/server/usermgmt"
|
||||||
|
import { RefreshResponseCause } from "$lib/errors"
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
|
||||||
|
const data = await request.json()
|
||||||
|
|
||||||
|
const token = data["token"]
|
||||||
|
if (!token || typeof token !== "string") {
|
||||||
|
return error(400, { cause: RefreshResponseCause.MalformedRequest, message: "token must be provided as string." })
|
||||||
|
}
|
||||||
|
|
||||||
|
const new_session = await UserMgmt.refresh_session(token)
|
||||||
|
|
||||||
|
if (!new_session) {
|
||||||
|
return error(401, { cause: RefreshResponseCause.InvalidSession, message: "No session for token" })
|
||||||
|
}
|
||||||
|
|
||||||
|
return json({
|
||||||
|
token: new_session.token,
|
||||||
|
expires: new_session.expires
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue