Add persist session checkbox on login
Makes session not persistent by default Closes #11
This commit is contained in:
parent
5897b6bf36
commit
1b8ff1428f
@ -31,8 +31,8 @@
|
||||
secret: 'default',
|
||||
cookie: {
|
||||
secure: false,
|
||||
maxAge: 2592000000, // 30 days
|
||||
}
|
||||
maxAge: 31557600000, // 1 year
|
||||
},
|
||||
},
|
||||
mail: {
|
||||
host: "127.0.0.1",
|
||||
|
@ -6,4 +6,9 @@
|
||||
database: "swaf_test",
|
||||
create_database_automatically: true,
|
||||
},
|
||||
session: {
|
||||
cookie: {
|
||||
maxAge: 1000, // 1s
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -81,6 +81,7 @@ export default class AuthGuard {
|
||||
|
||||
if (proofs.length === 0) {
|
||||
session.is_authenticated = false;
|
||||
session.persistent = false;
|
||||
}
|
||||
|
||||
return proofs;
|
||||
@ -107,6 +108,7 @@ export default class AuthGuard {
|
||||
public async authenticateOrRegister(
|
||||
session: Session & Partial<SessionData>,
|
||||
proof: AuthProof<User>,
|
||||
persistSession: boolean,
|
||||
onLogin?: (user: User) => Promise<void>,
|
||||
beforeRegister?: (connection: Connection, user: User) => Promise<RegisterCallback[]>,
|
||||
afterRegister?: (connection: Connection, user: User) => Promise<RegisterCallback[]>,
|
||||
@ -151,6 +153,7 @@ export default class AuthGuard {
|
||||
|
||||
// Login
|
||||
session.is_authenticated = true;
|
||||
session.persistent = persistSession;
|
||||
if (onLogin) await onLogin(user);
|
||||
|
||||
return user;
|
||||
|
@ -98,6 +98,8 @@ export default class MagicLinkAuthMethod implements AuthMethod<MagicLink> {
|
||||
});
|
||||
}
|
||||
|
||||
req.getSession().wantsSessionPersistence = !!req.body.persist_session || isRegistration;
|
||||
|
||||
await MagicLinkController.sendMagicLink(
|
||||
this.app,
|
||||
req.getSession().id,
|
||||
|
@ -62,7 +62,7 @@ export default class MagicLinkController<A extends Application> extends Controll
|
||||
// Auth
|
||||
try {
|
||||
return await req.as(AuthMiddleware).getAuthGuard().authenticateOrRegister(
|
||||
session, magicLink, undefined, async (connection, user) => {
|
||||
session, magicLink, !!session.wantsSessionPersistence, undefined, async (connection, user) => {
|
||||
const userNameComponent = user.asOptional(UserNameComponent);
|
||||
if (userNameComponent) userNameComponent.name = magicLink.as(MagicLinkUserNameComponent).username;
|
||||
return [];
|
||||
|
@ -58,7 +58,11 @@ export default class PasswordAuthMethod implements AuthMethod<PasswordAuthProof>
|
||||
|
||||
await passwordAuthProof.authorize(req.body.password);
|
||||
try {
|
||||
await this.app.as(AuthComponent).getAuthGuard().authenticateOrRegister(req.getSession(), passwordAuthProof);
|
||||
await this.app.as(AuthComponent).getAuthGuard().authenticateOrRegister(
|
||||
req.getSession(),
|
||||
passwordAuthProof,
|
||||
!!req.body.persist_session,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) {
|
||||
Throttler.throttle('login_failed_attempts_user', 3, 3 * 60 * 1000, // 3min
|
||||
@ -102,7 +106,7 @@ export default class PasswordAuthMethod implements AuthMethod<PasswordAuthProof>
|
||||
const passwordAuthProof = PasswordAuthProof.createAuthorizedProofForRegistration(req.getSession());
|
||||
try {
|
||||
await this.app.as(AuthComponent).getAuthGuard().authenticateOrRegister(req.getSession(), passwordAuthProof,
|
||||
undefined, async (connection, user) => {
|
||||
true, undefined, async (connection, user) => {
|
||||
const callbacks: RegisterCallback[] = [];
|
||||
|
||||
// Password
|
||||
|
@ -114,10 +114,10 @@ class RedisStore extends Store {
|
||||
}
|
||||
|
||||
public get(sid: string, callback: (err?: Error, session?: (session.SessionData | null)) => void): void {
|
||||
this.redisComponent.get('-session:' + sid)
|
||||
this.redisComponent.get(`-session:${sid}`)
|
||||
.then(value => {
|
||||
if (value) {
|
||||
this.redisComponent.persist('-session:' + sid, 2592000000) // 30 days
|
||||
this.redisComponent.persist(`-session:${sid}`, config.get<number>('session.cookie.maxAge'))
|
||||
.then(() => {
|
||||
callback(undefined, JSON.parse(value));
|
||||
})
|
||||
@ -130,7 +130,7 @@ class RedisStore extends Store {
|
||||
}
|
||||
|
||||
public set(sid: string, session: session.SessionData, callback?: (err?: Error) => void): void {
|
||||
this.redisComponent.remember('-session:' + sid, JSON.stringify(session), 2592000000) // 30 days
|
||||
this.redisComponent.remember(`-session:${sid}`, JSON.stringify(session), config.get<number>('session.cookie.maxAge'))
|
||||
.then(() => {
|
||||
if (callback) callback();
|
||||
})
|
||||
@ -138,7 +138,7 @@ class RedisStore extends Store {
|
||||
}
|
||||
|
||||
public destroy(sid: string, callback?: (err?: Error) => void): void {
|
||||
this.redisComponent.forget('-session:' + sid)
|
||||
this.redisComponent.forget(`-session:${sid}`)
|
||||
.then(() => {
|
||||
if (callback) callback();
|
||||
})
|
||||
|
@ -30,7 +30,6 @@ export default class SessionComponent extends ApplicationComponent {
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
secure: config.get('session.cookie.secure'),
|
||||
maxAge: config.get('session.cookie.maxAge'),
|
||||
},
|
||||
rolling: true,
|
||||
}));
|
||||
@ -38,6 +37,7 @@ export default class SessionComponent extends ApplicationComponent {
|
||||
router.use(flash());
|
||||
|
||||
router.use((req, res, next) => {
|
||||
// Request session getters
|
||||
req.getSessionOptional = () => {
|
||||
return req.session;
|
||||
};
|
||||
@ -47,8 +47,18 @@ export default class SessionComponent extends ApplicationComponent {
|
||||
return session;
|
||||
};
|
||||
|
||||
res.locals.session = req.getSession();
|
||||
// Session persistence
|
||||
const session = req.getSession();
|
||||
if (session.persistent) {
|
||||
session.cookie.maxAge = config.get('session.cookie.maxAge');
|
||||
} else {
|
||||
session.cookie.maxAge = session.cookie.expires = undefined;
|
||||
}
|
||||
|
||||
// Views session local
|
||||
res.locals.session = session;
|
||||
|
||||
// Views flash function
|
||||
const _flash: FlashStorage = {};
|
||||
res.locals.flash = (key?: string): FlashMessages | unknown[] => {
|
||||
if (key !== undefined) {
|
||||
|
4
src/types/Express.d.ts
vendored
4
src/types/Express.d.ts
vendored
@ -40,6 +40,10 @@ declare module 'express-session' {
|
||||
|
||||
previousUrl?: string;
|
||||
|
||||
wantsSessionPersistence?: boolean;
|
||||
|
||||
persistent?: boolean;
|
||||
|
||||
is_authenticated?: boolean;
|
||||
|
||||
auth_password_proof?: PasswordAuthProofSessionData;
|
||||
|
@ -1028,3 +1028,94 @@ describe('Manage email addresses', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session persistence', () => {
|
||||
let cookies: string[], csrf: string;
|
||||
|
||||
test('Not persistent at registration', async () => {
|
||||
let res = await agent.get('/csrf').expect(200);
|
||||
cookies = res.get('Set-Cookie');
|
||||
csrf = res.text;
|
||||
|
||||
await agent.post('/auth/register')
|
||||
.set('Cookie', cookies)
|
||||
.send({
|
||||
csrf: csrf,
|
||||
auth_method: 'magic_link',
|
||||
identifier: 'zuko@example.org',
|
||||
name: 'zuko',
|
||||
})
|
||||
.expect(302)
|
||||
.expect('Location', '/magic/lobby?redirect_uri=%2Fcsrf');
|
||||
|
||||
await followMagicLinkFromMail(agent, cookies);
|
||||
|
||||
expect(cookies[0]).toMatch(/^connect\.sid=.+; Path=\/; HttpOnly$/);
|
||||
|
||||
res = await agent.get('/csrf')
|
||||
.set('Cookie', cookies)
|
||||
.expect(200);
|
||||
expect(res.get('Set-Cookie')[0]).toMatch(/^connect\.sid=.+; Path=\/; Expires=.+; HttpOnly$/);
|
||||
|
||||
// Logout
|
||||
await agent.post('/auth/logout')
|
||||
.set('Cookie', cookies)
|
||||
.send({csrf})
|
||||
.expect(302)
|
||||
.expect('Location', '/');
|
||||
});
|
||||
|
||||
test('Login with persistence', async () => {
|
||||
await agent.post('/auth/login')
|
||||
.set('Cookie', cookies)
|
||||
.send({
|
||||
csrf: csrf,
|
||||
auth_method: 'magic_link',
|
||||
identifier: 'zuko@example.org',
|
||||
persist_session: 'on',
|
||||
})
|
||||
.expect(302)
|
||||
.expect('Location', '/magic/lobby?redirect_uri=%2Fcsrf');
|
||||
|
||||
await followMagicLinkFromMail(agent, cookies);
|
||||
|
||||
const res = await agent.get('/csrf')
|
||||
.set('Cookie', cookies)
|
||||
.expect(200);
|
||||
expect(res.get('Set-Cookie')[0]).toMatch(/^connect\.sid=.+; Path=\/; Expires=.+; HttpOnly$/);
|
||||
|
||||
// Logout
|
||||
await agent.post('/auth/logout')
|
||||
.set('Cookie', cookies)
|
||||
.send({csrf})
|
||||
.expect(302)
|
||||
.expect('Location', '/');
|
||||
});
|
||||
|
||||
test('Login without persistence', async () => {
|
||||
await agent.post('/auth/login')
|
||||
.set('Cookie', cookies)
|
||||
.send({
|
||||
csrf: csrf,
|
||||
auth_method: 'magic_link',
|
||||
identifier: 'zuko@example.org',
|
||||
persist_session: undefined,
|
||||
})
|
||||
.expect(302)
|
||||
.expect('Location', '/magic/lobby?redirect_uri=%2Fcsrf');
|
||||
|
||||
await followMagicLinkFromMail(agent, cookies);
|
||||
|
||||
const res = await agent.get('/csrf')
|
||||
.set('Cookie', cookies)
|
||||
.expect(200);
|
||||
expect(res.get('Set-Cookie')[0]).toMatch(/^connect\.sid=.+; Path=\/; HttpOnly$/);
|
||||
|
||||
// Logout
|
||||
await agent.post('/auth/logout')
|
||||
.set('Cookie', cookies)
|
||||
.send({csrf})
|
||||
.expect(302)
|
||||
.expect('Location', '/');
|
||||
});
|
||||
});
|
||||
|
@ -20,6 +20,8 @@
|
||||
|
||||
{{ macros.field(_locals, 'password', 'password', null, 'Your password', 'Do not fill to log in via magic link.') }}
|
||||
|
||||
{{ macros.field(_locals, 'checkbox', 'persist_session', null, 'Stay logged in on this computer.') }}
|
||||
|
||||
<button type="submit">Authenticate</button>
|
||||
|
||||
{{ macros.csrf(getCsrfToken) }}
|
||||
|
Loading…
Reference in New Issue
Block a user