Lucia

Inactivity timeout

This page builds upon the Basic session implementation page.

Setting an expiration for sessions is recommended, but it'd be annoying if active users were constantly signed-out. Instead of just removing the expiration all together, we recommend implementing an inactivity timeout as a replacement for it. This ensures active users remain signed in while inactive users are signed out after a set period.

First add a lastVerifiedAt attribute to your sessions. This would include when the session token was last verified.

interface Session {
	id: string;
	secretHash: Uint8Array;
	lastVerifiedAt: Date;
	createdAt: Date;
}
CREATE TABLE session (
    id TEXT NOT NULL PRIMARY KEY,
    secret_hash BLOB NOT NULL,
    last_verified_at INTEGER NOT NULL, -- unix (seconds)
    created_at INTEGER NOT NULL,
) STRICT;

While we can update the attribute after every verification, that would increase our database load dramatically. Instead, we can update the lastVerifiedAt attribute after a set period, e.g. 1 hour. It is important to only update the attribute after the token has been verified.

Finally, invalidate sessions that haven't been used recently. Anywhere from 1 day to 30 days would work depending on your application and type of session.

const inactivityTimeoutSeconds = 60 * 60 * 24 * 10; // 10 days
const activityCheckIntervalSeconds = 60 * 60; // 1 hour

async function validateSessionToken(dbPool: DBPool, token: string): Promise<Session | null> {
	const now = new Date();

	const tokenParts = token.split(".");
	if (tokenParts.length != 2) {
		return null;
	}
	const sessionId = tokenParts[0];
	const sessionSecret = tokensParts[1];

	const session = await getSession(dbPool, sessionId);

	const tokenSecretHash = await hashSecret(sessionSecret);
	const validSecret = constantTimeEqual(tokenSecretHash, session.secretHash);
	if (!validSecret) {
		return null;
	}

	if (now.getTime() - session.lastVerifiedAt.getTime() >= activityCheckIntervalSeconds * 1000) {
		session.lastVerifiedAt = now;
		await executeQuery(dbPool, "UPDATE session SET last_verified_at = ? WHERE id = ?", [
			Math.floor(session.lastVerifiedAt.getTime() / 1000),
			sessionId
		]);
	}

	return session;
}

async function getSession(dbPool: DBPool, sessionId: string): Promise<Session | null> {
	const now = new Date();

	const result = await executeQuery(
		dbPool,
		"SELECT id, secret_hash, last_verified_at, created_at FROM session WHERE id = ?",
		[sessionId]
	);
	if (result.rows.length !== 1) {
		return null;
	}
	const row = result.rows[0];
	const session: Session = {
		id: row[0],
		secretHash: row[1],
		lastVerifiedAt: new Date(row[2] * 1000),
		createdAt: new Date(row[3] * 1000)
	};

	// Inactivity timeout
	if (now.getTime() - session.lastVerifiedAt.getTime() >= inactivityTimeoutSeconds * 1000) {
		await deleteSession(sessionId);
		return null;
	}

	return session;
}