Added token refresh for session tokens
This commit is contained in:
parent
1476289722
commit
8f76173bc5
|
|
@ -105,6 +105,22 @@ export let handle: Handle = async function ({ event, resolve }) {
|
|||
}
|
||||
}
|
||||
|
||||
if (SessionStore.get_remaining_lifetime(token) < 5 * 60 * 1000) {
|
||||
|
||||
const new_expiry_date = new Date(Date.now() + 15*60*1000)
|
||||
const new_token = SessionStore.reissue_access_token(token, new_expiry_date)
|
||||
|
||||
if (new_token) {
|
||||
event.cookies.set("session_id", new_token, {
|
||||
expires: new_expiry_date,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'strict',
|
||||
path: '/'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
event.locals.user = user;
|
||||
|
||||
return await resolve(event);
|
||||
|
|
|
|||
|
|
@ -1,53 +1,53 @@
|
|||
import Crypto from "node:crypto";
|
||||
import Crypto from "node:crypto"
|
||||
|
||||
import type { User } from "$lib/server/database";
|
||||
import type { User } from "$lib/server/database"
|
||||
import Logs from "$lib/server/log"
|
||||
|
||||
interface UserSession {
|
||||
user: User;
|
||||
last_active: Date;
|
||||
user: User
|
||||
last_active: Date
|
||||
}
|
||||
|
||||
export interface TokenInfo {
|
||||
user_id: number;
|
||||
creation_time: Date;
|
||||
expiry_time: Date;
|
||||
user_id: number
|
||||
creation_time: Date
|
||||
expiry_time: Date
|
||||
}
|
||||
|
||||
class LoginError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
class UserLoginError extends LoginError {
|
||||
user_id: number;
|
||||
user_id: number
|
||||
|
||||
constructor(user_id: number, message: string) {
|
||||
super(message);
|
||||
this.user_id = user_id;
|
||||
super(message)
|
||||
this.user_id = user_id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const TOKEN_LENGTH = 256;
|
||||
const TOKEN_LENGTH = 256
|
||||
|
||||
const active_users = new Map<number, UserSession>();
|
||||
const active_session_tokens = new Map<string, TokenInfo>();
|
||||
const active_users = new Map<number, UserSession>()
|
||||
const active_session_tokens = new Map<string, TokenInfo>()
|
||||
|
||||
|
||||
function issue_access_token_for_user(user: User, expiry_date: Date): string {
|
||||
|
||||
Logs.user.info(`Issuing access token for user id ${user.id}. Expiry date: ${expiry_date.toISOString()}`)
|
||||
|
||||
let user_session = active_users.get(user.id);
|
||||
let user_session = active_users.get(user.id)
|
||||
if (user_session === undefined) {
|
||||
user_session = {
|
||||
user: user,
|
||||
last_active: new Date()
|
||||
} as UserSession;
|
||||
} as UserSession
|
||||
|
||||
active_users.set(user.id, user_session);
|
||||
active_users.set(user.id, user_session)
|
||||
}
|
||||
|
||||
/* | Token (256) | Expiry date as getTime | */
|
||||
|
|
@ -59,49 +59,92 @@ function issue_access_token_for_user(user: User, expiry_date: Date): string {
|
|||
const session_token = Buffer.concat(
|
||||
[ Crypto.randomBytes(TOKEN_LENGTH / 4 * 3),
|
||||
Buffer.from(token_info.creation_time.getTime().toFixed(0)) ]
|
||||
).toString("base64");
|
||||
).toString("base64")
|
||||
|
||||
if (active_session_tokens.has(session_token)) {
|
||||
throw new UserLoginError(user.id, "Tried issuing a duplicate session token.");
|
||||
throw new UserLoginError(user.id, "Tried issuing a duplicate session token.")
|
||||
}
|
||||
|
||||
active_session_tokens.set(session_token, token_info);
|
||||
active_session_tokens.set(session_token, token_info)
|
||||
|
||||
return session_token;
|
||||
return session_token
|
||||
}
|
||||
|
||||
function reissue_access_token(token: string, expiry_date: Date): string | null {
|
||||
|
||||
const token_info = active_session_tokens.get(token)
|
||||
|
||||
if (!token_info || token_info.expiry_time.getTime() < Date.now()) {
|
||||
Logs.user.warn("Tried to reissue an access token for an invalid access token!")
|
||||
return null
|
||||
}
|
||||
|
||||
Logs.user.info(`Reissuing access token for user id ${token_info.user_id}. Expiry date: ${expiry_date.toISOString()}`)
|
||||
|
||||
/* | Token (256) | Expiry date as getTime | */
|
||||
const new_token_info: TokenInfo = {
|
||||
user_id: token_info.user_id,
|
||||
creation_time: new Date(),
|
||||
expiry_time: expiry_date
|
||||
}
|
||||
const new_session_token = Buffer.concat(
|
||||
[ Crypto.randomBytes(TOKEN_LENGTH / 4 * 3),
|
||||
Buffer.from(token_info.creation_time.getTime().toFixed(0)) ]
|
||||
).toString("base64")
|
||||
|
||||
if (active_session_tokens.has(new_session_token)) {
|
||||
throw new UserLoginError(token_info.user_id, "Tried issuing a duplicate session token.")
|
||||
}
|
||||
|
||||
active_session_tokens.set(new_session_token, new_token_info)
|
||||
active_session_tokens.delete(token)
|
||||
|
||||
return new_session_token
|
||||
}
|
||||
|
||||
function get_user_by_access_token(token: string): User | null {
|
||||
|
||||
const token_info = active_session_tokens.get(token);
|
||||
const token_info = active_session_tokens.get(token)
|
||||
|
||||
if (token_info === undefined || token_info.expiry_time.getTime() < Date.now()) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const user = active_users.get(token_info.user_id);
|
||||
const user = active_users.get(token_info.user_id)
|
||||
|
||||
if (user === undefined) {
|
||||
return null;
|
||||
Logs.user.error(`User ${token_info.user_id} has an active session token but is not loaded when retrieving user`)
|
||||
return null
|
||||
}
|
||||
|
||||
user.last_active = new Date();
|
||||
user.last_active = new Date()
|
||||
|
||||
return user.user;
|
||||
return user.user
|
||||
}
|
||||
|
||||
function logout_user_session(token: string): boolean {
|
||||
const token_info = active_session_tokens.get(token);
|
||||
const token_info = active_session_tokens.get(token)
|
||||
|
||||
|
||||
if (!token_info) {
|
||||
Logs.user.warn(`Failed to logout user by token, because token does not exist`);
|
||||
return false;
|
||||
Logs.user.warn(`Failed to logout user by token, because token does not exist`)
|
||||
return false
|
||||
}
|
||||
|
||||
Logs.user.info(`Logging out user ${token_info?.user_id}`)
|
||||
token_info.expiry_time = new Date(0);
|
||||
token_info.expiry_time = new Date(0)
|
||||
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
function get_remaining_lifetime(token: string): number {
|
||||
const token_info = active_session_tokens.get(token)
|
||||
|
||||
if (!token_info) {
|
||||
return NaN
|
||||
}
|
||||
|
||||
return token_info.expiry_time.getTime() - Date.now()
|
||||
}
|
||||
|
||||
function reload_user_data(user: User) {
|
||||
|
|
@ -116,33 +159,35 @@ async function __clean_session_store() {
|
|||
let cleaned_user_sessions = 0
|
||||
let cleaned_active_users = 0
|
||||
|
||||
const active_users_session = new Set<number>();
|
||||
const active_users_session = new Set<number>()
|
||||
active_session_tokens.forEach((token_info, token, token_map) => {
|
||||
if (token_info.expiry_time.getTime() < Date.now()) {
|
||||
token_map.delete(token);
|
||||
token_map.delete(token)
|
||||
cleaned_user_sessions += 1
|
||||
} else {
|
||||
active_users_session.add(token_info.user_id);
|
||||
active_users_session.add(token_info.user_id)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
active_users.forEach((user_info, user_id, user_map) => {
|
||||
if (user_info.last_active.getTime() + 15*60*1000 < Date.now() && !active_users.has(user_id)) {
|
||||
user_map.delete(user_id)
|
||||
cleaned_active_users += 1
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if (cleaned_active_users > 0 || cleaned_active_users > 0) {
|
||||
Logs.user.info(`Cleaned ${cleaned_user_sessions} inactive session tokens and ${cleaned_active_users} inactive users`)
|
||||
}
|
||||
}
|
||||
|
||||
export default class SessionStore {
|
||||
static issue_access_token_for_user = issue_access_token_for_user;
|
||||
static get_user_by_access_token = get_user_by_access_token;
|
||||
static logout_user_session = logout_user_session;
|
||||
static reload_user_data = reload_user_data;
|
||||
export default {
|
||||
issue_access_token_for_user,
|
||||
reissue_access_token,
|
||||
get_user_by_access_token,
|
||||
get_remaining_lifetime,
|
||||
logout_user_session,
|
||||
reload_user_data
|
||||
}
|
||||
|
||||
setInterval(__clean_session_store, 15*60*1000);
|
||||
setInterval(__clean_session_store, 15*60*1000)
|
||||
|
|
|
|||
Loading…
Reference in New Issue