Skip to content

Commit

Permalink
Server: Expire sessions after 12 hours
Browse files Browse the repository at this point in the history
  • Loading branch information
laurent22 committed Oct 26, 2021
1 parent 134bc91 commit 0ada1df
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 1 deletion.
46 changes: 46 additions & 0 deletions packages/server/src/models/SessionModel.test.ts
@@ -0,0 +1,46 @@
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models } from '../utils/testing/testUtils';
import { defaultSessionTtl } from './SessionModel';

describe('SessionModel', function() {

beforeAll(async () => {
await beforeAllDb('SessionModel');
});

afterAll(async () => {
await afterAllTests();
});

beforeEach(async () => {
await beforeEachDb();
});

test('should delete expired sessions', async function() {
jest.useFakeTimers('modern');

const t0 = new Date('2020-01-01T00:00:00').getTime();
jest.setSystemTime(t0);

const { user, password } = await createUserAndSession(1);
await models().session().authenticate(user.email, password);

jest.setSystemTime(new Date(t0 + defaultSessionTtl + 10));

const lastSession = await models().session().authenticate(user.email, password);

expect(await models().session().count()).toBe(3);

await models().session().deleteExpiredSessions();

expect(await models().session().count()).toBe(1);
expect((await models().session().all())[0].id).toBe(lastSession.id);

await models().session().authenticate(user.email, password);
await models().session().deleteExpiredSessions();

expect(await models().session().count()).toBe(2);

jest.useRealTimers();
});

});
8 changes: 8 additions & 0 deletions packages/server/src/models/SessionModel.ts
Expand Up @@ -2,6 +2,9 @@ import BaseModel from './BaseModel';
import { User, Session, Uuid } from '../services/database/types';
import uuidgen from '../utils/uuidgen';
import { ErrorForbidden } from '../utils/errors';
import { Hour } from '../utils/time';

export const defaultSessionTtl = 12 * Hour;

export default class SessionModel extends BaseModel<Session> {

Expand Down Expand Up @@ -40,4 +43,9 @@ export default class SessionModel extends BaseModel<Session> {
await query.delete();
}

public async deleteExpiredSessions() {
const cutOffTime = Date.now() - defaultSessionTtl;
await this.db(this.tableName).where('created_time', '<', cutOffTime).delete();
}

}
1 change: 1 addition & 0 deletions packages/server/src/services/TaskService.ts
Expand Up @@ -12,6 +12,7 @@ export enum TaskId {
HandleOversizedAccounts = 3,
HandleBetaUserEmails = 4,
HandleFailedPaymentSubscriptions = 5,
DeleteExpiredSessions = 6,
}

export enum RunType {
Expand Down
12 changes: 11 additions & 1 deletion packages/server/src/utils/routeUtils.ts
Expand Up @@ -5,6 +5,7 @@ import Router from './Router';
import { AppContext, HttpMethod, RouteType } from './types';
import { URL } from 'url';
import { csrfCheck } from './csrf';
import { contextSessionId } from './requestUtils';

const { ltrimSlashes, rtrimSlashes } = require('@joplin/lib/path-utils');

Expand Down Expand Up @@ -200,7 +201,16 @@ export async function execRequest(routes: Routers, ctx: AppContext) {
// This is a generic catch-all for all private end points - if we
// couldn't get a valid session, we exit now. Individual end points
// might have additional permission checks depending on the action.
if (!isPublicRoute && !ctx.joplin.owner) throw new ErrorForbidden();
if (!isPublicRoute && !ctx.joplin.owner) {
if (contextSessionId(ctx, false)) {
// If we have a session but not a user it means the session was
// invalid or has expired, so display a special message, since this
// is also going to be displayed on the website.
throw new ErrorForbidden('Your session has expired. Please login again.');
} else {
throw new ErrorForbidden();
}
}

await csrfCheck(ctx, isPublicRoute);
disabledAccountCheck(match, ctx.joplin.owner);
Expand Down
7 changes: 7 additions & 0 deletions packages/server/src/utils/setupTaskService.ts
Expand Up @@ -29,6 +29,13 @@ export default function(env: Env, models: Models, config: Config): TaskService {
schedule: '0 */2 30 * *',
run: (models: Models) => models.user().handleOversizedAccounts(),
},

{
id: TaskId.DeleteExpiredSessions,
description: 'Delete expired sessions',
schedule: '0 */6 * * *',
run: (models: Models) => models.session().deleteExpiredSessions(),
},
];

if (config.isJoplinCloud) {
Expand Down

0 comments on commit 0ada1df

Please sign in to comment.