Enforce trading authorization and suitability checks
This commit is contained in:
@@ -4,7 +4,9 @@ const SESSION_KEY = 'portalAuthContext';
|
||||
const IDENTITY_COOKIE = 'portal_auth_user';
|
||||
const CONTEXT_COOKIE = 'portal_auth_context';
|
||||
const AUTH_CHANNEL_NAME = 'portal-auth-session';
|
||||
const AUTH_EVENT_TYPES = new Set(['signed-in', 'signed-out', 'account-switched']);
|
||||
const AUTH_EVENT_TYPES = new Set(['signed-in', 'signed-out', 'account-switched', 'session-expired']);
|
||||
const SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
||||
const SESSION_CHECK_INTERVAL_MS = 15 * 1000;
|
||||
let authChannel = null;
|
||||
let authSyncStarted = false;
|
||||
|
||||
@@ -38,13 +40,59 @@ function writeSharedContext(context, maxAge) {
|
||||
}
|
||||
|
||||
function hasMatchingIdentity(token, context) {
|
||||
const expiresAt = tokenExpiry(token);
|
||||
return Boolean(
|
||||
token
|
||||
&& context
|
||||
&& readCookie(IDENTITY_COOKIE) === String(context.userId),
|
||||
&& readCookie(IDENTITY_COOKIE) === String(context.userId)
|
||||
&& (!expiresAt || Date.now() < expiresAt),
|
||||
);
|
||||
}
|
||||
|
||||
function tokenExpiry(token) {
|
||||
try {
|
||||
const payload = token.split('.')[1];
|
||||
if (!payload) return 0;
|
||||
const normalized = payload.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(payload.length / 4) * 4, '=');
|
||||
const value = JSON.parse(decodeURIComponent(atob(normalized).split('').map((char) => `%${`00${char.charCodeAt(0).toString(16)}`.slice(-2)}`).join('')));
|
||||
return Number(value.exp || 0) * 1000;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function sessionExpired() {
|
||||
clearAuthSession({ eventType: 'session-expired' });
|
||||
if (isProtectedPortalPage()) {
|
||||
window.location.replace(`${loginPathForCurrentPortal()}?reason=session-expired`);
|
||||
}
|
||||
}
|
||||
|
||||
function checkSessionLifetime() {
|
||||
const token = readCookie('auth_token');
|
||||
const context = readSharedContext() || readSessionContext();
|
||||
if (!token || !context || !hasMatchingIdentity(token, context)) return;
|
||||
const expiresAt = Number(context.expiresAt || tokenExpiry(token));
|
||||
const lastActivityAt = Number(context.lastActivityAt || Date.now());
|
||||
if ((expiresAt && Date.now() >= expiresAt) || Date.now() - lastActivityAt >= SESSION_IDLE_TIMEOUT_MS) {
|
||||
sessionExpired();
|
||||
}
|
||||
}
|
||||
|
||||
function touchActivity() {
|
||||
const token = readCookie('auth_token');
|
||||
const context = readSharedContext() || readSessionContext();
|
||||
if (!token || !context || !hasMatchingIdentity(token, context)) return;
|
||||
const now = Date.now();
|
||||
const expiresAt = Number(context.expiresAt || tokenExpiry(token));
|
||||
if (expiresAt && now >= expiresAt) { sessionExpired(); return; }
|
||||
if (now - Number(context.lastActivityAt || 0) < 60_000) return;
|
||||
const next = { ...context, lastActivityAt: now, expiresAt };
|
||||
const remaining = expiresAt ? Math.max(0, Math.ceil((expiresAt - now) / 1000)) : 1800;
|
||||
sessionStorage.setItem(SESSION_KEY, JSON.stringify(next));
|
||||
writeSharedContext(next, remaining);
|
||||
}
|
||||
|
||||
function openAuthChannel() {
|
||||
if (!('BroadcastChannel' in window)) return null;
|
||||
if (authChannel) return authChannel;
|
||||
@@ -80,7 +128,8 @@ function redirectForExternalAuthChange(type) {
|
||||
window.location.replace('/portal/guest/home/?reason=signed-out');
|
||||
return;
|
||||
}
|
||||
window.location.replace(`${loginPathForCurrentPortal()}?reason=account-switched`);
|
||||
const reason = type === 'session-expired' ? 'session-expired' : 'account-switched';
|
||||
window.location.replace(`${loginPathForCurrentPortal()}?reason=${reason}`);
|
||||
}
|
||||
|
||||
function handleExternalAuthChange(type) {
|
||||
@@ -108,6 +157,11 @@ export function startAuthSync() {
|
||||
});
|
||||
window.addEventListener('focus', reconcileSharedSession);
|
||||
window.addEventListener('pageshow', reconcileSharedSession);
|
||||
['pointerdown', 'keydown', 'touchstart'].forEach((eventName) => {
|
||||
window.addEventListener(eventName, touchActivity, { passive: true });
|
||||
});
|
||||
window.setInterval(checkSessionLifetime, SESSION_CHECK_INTERVAL_MS);
|
||||
checkSessionLifetime();
|
||||
}
|
||||
|
||||
export function getAccessToken() {
|
||||
@@ -130,6 +184,8 @@ export function setAuthSession(loginData, username) {
|
||||
roles: Array.isArray(loginData.roles) ? loginData.roles : [],
|
||||
dataScope: loginData.data_scope || 'self',
|
||||
permissions: Array.isArray(loginData.permissions) ? loginData.permissions : [],
|
||||
expiresAt: Date.now() + maxAge * 1000,
|
||||
lastActivityAt: Date.now(),
|
||||
};
|
||||
document.cookie = `${IDENTITY_COOKIE}=${encodeURIComponent(context.userId)}; path=/; max-age=${maxAge}; SameSite=Strict`;
|
||||
writeSharedContext(context, maxAge);
|
||||
@@ -190,8 +246,12 @@ export function updateAuthPermissions(permissions, dataScope) {
|
||||
permissions: Array.isArray(permissions) ? [...permissions] : [],
|
||||
dataScope: dataScope || context.dataScope,
|
||||
};
|
||||
const expiresAt = Number(next.expiresAt || tokenExpiry(readCookie('auth_token')));
|
||||
const remaining = expiresAt ? Math.max(0, Math.ceil((expiresAt - Date.now()) / 1000)) : 1800;
|
||||
next.expiresAt = expiresAt;
|
||||
next.lastActivityAt = Date.now();
|
||||
sessionStorage.setItem(SESSION_KEY, JSON.stringify(next));
|
||||
writeSharedContext(next, 1800);
|
||||
writeSharedContext(next, remaining);
|
||||
return next;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user