mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-04 01:18:20 +02:00
fix: deletes all sessions for user on logout (#2071)
* fix: deletes all sessions for user on logout
This commit is contained in:
parent
a3585343e1
commit
667fb9a8cf
@ -18,7 +18,7 @@ class IndexRouter extends Controller {
|
|||||||
|
|
||||||
this.use('/health', new HealthCheckController(config, services).router);
|
this.use('/health', new HealthCheckController(config, services).router);
|
||||||
this.use('/internal-backstage', new BackstageController(config).router);
|
this.use('/internal-backstage', new BackstageController(config).router);
|
||||||
this.use('/logout', new LogoutController(config).router);
|
this.use('/logout', new LogoutController(config, services).router);
|
||||||
this.use(
|
this.use(
|
||||||
'/auth/simple',
|
'/auth/simple',
|
||||||
new SimplePasswordProvider(config, services).router,
|
new SimplePasswordProvider(config, services).router,
|
||||||
|
@ -4,12 +4,26 @@ import { createTestConfig } from '../../test/config/test-config';
|
|||||||
|
|
||||||
import LogoutController from './logout';
|
import LogoutController from './logout';
|
||||||
import { IAuthRequest } from './unleash-types';
|
import { IAuthRequest } from './unleash-types';
|
||||||
|
import SessionService from '../services/session-service';
|
||||||
|
import FakeSessionStore from '../../test/fixtures/fake-session-store';
|
||||||
|
import noLogger from '../../test/fixtures/no-logger';
|
||||||
|
import { addDays } from 'date-fns';
|
||||||
|
|
||||||
test('should redirect to "/" after logout', async () => {
|
test('should redirect to "/" after logout', async () => {
|
||||||
const baseUriPath = '';
|
const baseUriPath = '';
|
||||||
const app = express();
|
const app = express();
|
||||||
const config = createTestConfig({ server: { baseUriPath } });
|
const config = createTestConfig({ server: { baseUriPath } });
|
||||||
app.use('/logout', new LogoutController(config).router);
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService(
|
||||||
|
{ sessionStore },
|
||||||
|
{ getLogger: noLogger },
|
||||||
|
);
|
||||||
|
app.use(
|
||||||
|
'/logout',
|
||||||
|
new LogoutController(config, {
|
||||||
|
sessionService,
|
||||||
|
}).router,
|
||||||
|
);
|
||||||
const request = supertest(app);
|
const request = supertest(app);
|
||||||
expect.assertions(0);
|
expect.assertions(0);
|
||||||
await request
|
await request
|
||||||
@ -22,7 +36,12 @@ test('should redirect to "/basePath" after logout when baseUriPath is set', asyn
|
|||||||
const baseUriPath = '/basePath';
|
const baseUriPath = '/basePath';
|
||||||
const app = express();
|
const app = express();
|
||||||
const config = createTestConfig({ server: { baseUriPath } });
|
const config = createTestConfig({ server: { baseUriPath } });
|
||||||
app.use('/logout', new LogoutController(config).router);
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService(
|
||||||
|
{ sessionStore },
|
||||||
|
{ getLogger: noLogger },
|
||||||
|
);
|
||||||
|
app.use('/logout', new LogoutController(config, { sessionService }).router);
|
||||||
const request = supertest(app);
|
const request = supertest(app);
|
||||||
expect.assertions(0);
|
expect.assertions(0);
|
||||||
await request
|
await request
|
||||||
@ -35,7 +54,13 @@ test('should set "Clear-Site-Data" header', async () => {
|
|||||||
const baseUriPath = '';
|
const baseUriPath = '';
|
||||||
const app = express();
|
const app = express();
|
||||||
const config = createTestConfig({ server: { baseUriPath } });
|
const config = createTestConfig({ server: { baseUriPath } });
|
||||||
app.use('/logout', new LogoutController(config).router);
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService(
|
||||||
|
{ sessionStore },
|
||||||
|
{ getLogger: noLogger },
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use('/logout', new LogoutController(config, { sessionService }).router);
|
||||||
const request = supertest(app);
|
const request = supertest(app);
|
||||||
expect.assertions(0);
|
expect.assertions(0);
|
||||||
await request
|
await request
|
||||||
@ -51,7 +76,13 @@ test('should not set "Clear-Site-Data" header', async () => {
|
|||||||
server: { baseUriPath },
|
server: { baseUriPath },
|
||||||
session: { clearSiteDataOnLogout: false },
|
session: { clearSiteDataOnLogout: false },
|
||||||
});
|
});
|
||||||
app.use('/logout', new LogoutController(config).router);
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService(
|
||||||
|
{ sessionStore },
|
||||||
|
{ getLogger: noLogger },
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use('/logout', new LogoutController(config, { sessionService }).router);
|
||||||
const request = supertest(app);
|
const request = supertest(app);
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
await request
|
await request
|
||||||
@ -66,7 +97,14 @@ test('should clear "unleash-session" cookies', async () => {
|
|||||||
const baseUriPath = '';
|
const baseUriPath = '';
|
||||||
const app = express();
|
const app = express();
|
||||||
const config = createTestConfig({ server: { baseUriPath } });
|
const config = createTestConfig({ server: { baseUriPath } });
|
||||||
app.use('/logout', new LogoutController(config).router);
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService(
|
||||||
|
{ sessionStore },
|
||||||
|
{ getLogger: noLogger },
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use('/logout', new LogoutController(config, { sessionService }).router);
|
||||||
|
|
||||||
const request = supertest(app);
|
const request = supertest(app);
|
||||||
expect.assertions(0);
|
expect.assertions(0);
|
||||||
await request
|
await request
|
||||||
@ -85,7 +123,14 @@ test('should clear "unleash-session" cookie even when disabled clear site data',
|
|||||||
server: { baseUriPath },
|
server: { baseUriPath },
|
||||||
session: { clearSiteDataOnLogout: false },
|
session: { clearSiteDataOnLogout: false },
|
||||||
});
|
});
|
||||||
app.use('/logout', new LogoutController(config).router);
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService(
|
||||||
|
{ sessionStore },
|
||||||
|
{ getLogger: noLogger },
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use('/logout', new LogoutController(config, { sessionService }).router);
|
||||||
|
|
||||||
const request = supertest(app);
|
const request = supertest(app);
|
||||||
expect.assertions(0);
|
expect.assertions(0);
|
||||||
await request
|
await request
|
||||||
@ -108,7 +153,14 @@ test('should call destroy on session', async () => {
|
|||||||
req.session = fakeSession;
|
req.session = fakeSession;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
app.use('/logout', new LogoutController(config).router);
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService(
|
||||||
|
{ sessionStore },
|
||||||
|
{ getLogger: noLogger },
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use('/logout', new LogoutController(config, { sessionService }).router);
|
||||||
|
|
||||||
const request = supertest(app);
|
const request = supertest(app);
|
||||||
await request.get(`${baseUriPath}/logout`);
|
await request.get(`${baseUriPath}/logout`);
|
||||||
|
|
||||||
@ -125,7 +177,14 @@ test('should handle req.logout with callback function', async () => {
|
|||||||
req.logout = logoutFunction;
|
req.logout = logoutFunction;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
app.use('/logout', new LogoutController(config).router);
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService(
|
||||||
|
{ sessionStore },
|
||||||
|
{ getLogger: noLogger },
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use('/logout', new LogoutController(config, { sessionService }).router);
|
||||||
|
|
||||||
const request = supertest(app);
|
const request = supertest(app);
|
||||||
await request.get(`${baseUriPath}/logout`);
|
await request.get(`${baseUriPath}/logout`);
|
||||||
|
|
||||||
@ -143,7 +202,14 @@ test('should handle req.logout without callback function', async () => {
|
|||||||
req.logout = logoutFunction;
|
req.logout = logoutFunction;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
app.use('/logout', new LogoutController(config).router);
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService(
|
||||||
|
{ sessionStore },
|
||||||
|
{ getLogger: noLogger },
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use('/logout', new LogoutController(config, { sessionService }).router);
|
||||||
|
|
||||||
const request = supertest(app);
|
const request = supertest(app);
|
||||||
await request.get(`${baseUriPath}/logout`);
|
await request.get(`${baseUriPath}/logout`);
|
||||||
|
|
||||||
@ -162,10 +228,61 @@ test('should redirect to alternative logoutUrl', async () => {
|
|||||||
req.session = fakeSession;
|
req.session = fakeSession;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
app.use('/logout', new LogoutController(config).router);
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService(
|
||||||
|
{ sessionStore },
|
||||||
|
{ getLogger: noLogger },
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use('/logout', new LogoutController(config, { sessionService }).router);
|
||||||
|
|
||||||
const request = supertest(app);
|
const request = supertest(app);
|
||||||
await request
|
await request
|
||||||
.get('/logout')
|
.get('/logout')
|
||||||
.expect(302)
|
.expect(302)
|
||||||
.expect('Location', '/some-other-path');
|
.expect('Location', '/some-other-path');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Should destroy sessions for user', async () => {
|
||||||
|
const app = express();
|
||||||
|
const config = createTestConfig();
|
||||||
|
const fakeSession = {
|
||||||
|
destroy: jest.fn(),
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
app.use((req: IAuthRequest, res, next) => {
|
||||||
|
req.session = fakeSession;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
const sessionStore = new FakeSessionStore();
|
||||||
|
const sessionService = new SessionService(
|
||||||
|
{ sessionStore },
|
||||||
|
{ getLogger: noLogger },
|
||||||
|
);
|
||||||
|
await sessionStore.insertSession({
|
||||||
|
sid: '1',
|
||||||
|
sess: {
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expired: addDays(new Date(), 2),
|
||||||
|
});
|
||||||
|
await sessionStore.insertSession({
|
||||||
|
sid: '2',
|
||||||
|
sess: {
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expired: addDays(new Date(), 2),
|
||||||
|
});
|
||||||
|
let activeSessionsBeforeLogout = await sessionStore.getSessionsForUser(1);
|
||||||
|
expect(activeSessionsBeforeLogout).toHaveLength(2);
|
||||||
|
app.use('/logout', new LogoutController(config, { sessionService }).router);
|
||||||
|
await supertest(app).get('/logout').expect(302);
|
||||||
|
let activeSessions = await sessionStore.getSessionsForUser(1);
|
||||||
|
expect(activeSessions).toHaveLength(0);
|
||||||
|
});
|
||||||
|
@ -3,6 +3,8 @@ import { promisify } from 'util';
|
|||||||
import { IUnleashConfig } from '../types/option';
|
import { IUnleashConfig } from '../types/option';
|
||||||
import Controller from './controller';
|
import Controller from './controller';
|
||||||
import { IAuthRequest } from './unleash-types';
|
import { IAuthRequest } from './unleash-types';
|
||||||
|
import { IUnleashServices } from '../types';
|
||||||
|
import SessionService from '../services/session-service';
|
||||||
|
|
||||||
class LogoutController extends Controller {
|
class LogoutController extends Controller {
|
||||||
private clearSiteDataOnLogout: boolean;
|
private clearSiteDataOnLogout: boolean;
|
||||||
@ -11,8 +13,14 @@ class LogoutController extends Controller {
|
|||||||
|
|
||||||
private baseUri: string;
|
private baseUri: string;
|
||||||
|
|
||||||
constructor(config: IUnleashConfig) {
|
private sessionService: SessionService;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
config: IUnleashConfig,
|
||||||
|
{ sessionService }: Pick<IUnleashServices, 'sessionService'>,
|
||||||
|
) {
|
||||||
super(config);
|
super(config);
|
||||||
|
this.sessionService = sessionService;
|
||||||
this.baseUri = config.server.baseUriPath;
|
this.baseUri = config.server.baseUriPath;
|
||||||
this.clearSiteDataOnLogout = config.session.clearSiteDataOnLogout;
|
this.clearSiteDataOnLogout = config.session.clearSiteDataOnLogout;
|
||||||
this.cookieName = config.session.cookieName;
|
this.cookieName = config.session.cookieName;
|
||||||
@ -41,6 +49,11 @@ class LogoutController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (req.session) {
|
if (req.session) {
|
||||||
|
if (req.session.user?.id) {
|
||||||
|
await this.sessionService.deleteSessionsForUser(
|
||||||
|
req.session.user.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
req.session.destroy();
|
req.session.destroy();
|
||||||
}
|
}
|
||||||
res.clearCookie(this.cookieName);
|
res.clearCookie(this.cookieName);
|
||||||
@ -48,7 +61,9 @@ class LogoutController extends Controller {
|
|||||||
if (this.clearSiteDataOnLogout) {
|
if (this.clearSiteDataOnLogout) {
|
||||||
res.set('Clear-Site-Data', '"cookies", "storage"');
|
res.set('Clear-Site-Data', '"cookies", "storage"');
|
||||||
}
|
}
|
||||||
|
if (req.user?.id) {
|
||||||
|
await this.sessionService.deleteSessionsForUser(req.user.id);
|
||||||
|
}
|
||||||
res.redirect(`${this.baseUri}/`);
|
res.redirect(`${this.baseUri}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,14 +340,15 @@ class UserService {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.store.successfullyLogin(user);
|
await this.store.successfullyLogin(user);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async changePassword(userId: number, password: string): Promise<void> {
|
async changePassword(userId: number, password: string): Promise<void> {
|
||||||
this.validatePassword(password);
|
this.validatePassword(password);
|
||||||
const passwordHash = await bcrypt.hash(password, saltRounds);
|
const passwordHash = await bcrypt.hash(password, saltRounds);
|
||||||
return this.store.setPasswordHash(userId, passwordHash);
|
await this.store.setPasswordHash(userId, passwordHash);
|
||||||
|
await this.sessionService.deleteSessionsForUser(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserForToken(token: string): Promise<TokenUserSchema> {
|
async getUserForToken(token: string): Promise<TokenUserSchema> {
|
||||||
|
@ -13,6 +13,7 @@ import { RoleName } from '../../../../lib/types/model';
|
|||||||
import { IRoleStore } from 'lib/types/stores/role-store';
|
import { IRoleStore } from 'lib/types/stores/role-store';
|
||||||
import { randomId } from '../../../../lib/util/random-id';
|
import { randomId } from '../../../../lib/util/random-id';
|
||||||
import { omitKeys } from '../../../../lib/util/omit-keys';
|
import { omitKeys } from '../../../../lib/util/omit-keys';
|
||||||
|
import { ISessionStore } from '../../../../lib/types/stores/session-store';
|
||||||
|
|
||||||
let stores;
|
let stores;
|
||||||
let db;
|
let db;
|
||||||
@ -21,6 +22,7 @@ let app;
|
|||||||
let userStore: IUserStore;
|
let userStore: IUserStore;
|
||||||
let eventStore: IEventStore;
|
let eventStore: IEventStore;
|
||||||
let roleStore: IRoleStore;
|
let roleStore: IRoleStore;
|
||||||
|
let sessionStore: ISessionStore;
|
||||||
let editorRole: IRole;
|
let editorRole: IRole;
|
||||||
let adminRole: IRole;
|
let adminRole: IRole;
|
||||||
|
|
||||||
@ -32,6 +34,7 @@ beforeAll(async () => {
|
|||||||
userStore = stores.userStore;
|
userStore = stores.userStore;
|
||||||
eventStore = stores.eventStore;
|
eventStore = stores.eventStore;
|
||||||
roleStore = stores.roleStore;
|
roleStore = stores.roleStore;
|
||||||
|
sessionStore = stores.sessionStore;
|
||||||
const roles = await roleStore.getRootRoles();
|
const roles = await roleStore.getRootRoles();
|
||||||
editorRole = roles.find((r) => r.name === RoleName.EDITOR);
|
editorRole = roles.find((r) => r.name === RoleName.EDITOR);
|
||||||
adminRole = roles.find((r) => r.name === RoleName.ADMIN);
|
adminRole = roles.find((r) => r.name === RoleName.ADMIN);
|
||||||
@ -231,11 +234,12 @@ test('validator should accept strong password', async () => {
|
|||||||
|
|
||||||
test('should change password', async () => {
|
test('should change password', async () => {
|
||||||
const user = await userStore.insert({ email: 'some@mail.com' });
|
const user = await userStore.insert({ email: 'some@mail.com' });
|
||||||
|
const spy = jest.spyOn(sessionStore, 'deleteSessionsForUser');
|
||||||
return app.request
|
await app.request
|
||||||
.post(`/api/admin/user-admin/${user.id}/change-password`)
|
.post(`/api/admin/user-admin/${user.id}/change-password`)
|
||||||
.send({ password: 'simple123-_ASsad' })
|
.send({ password: 'simple123-_ASsad' })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should search for users', async () => {
|
test('should search for users', async () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user