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',
|
secret: 'default',
|
||||||
cookie: {
|
cookie: {
|
||||||
secure: false,
|
secure: false,
|
||||||
maxAge: 2592000000, // 30 days
|
maxAge: 31557600000, // 1 year
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
mail: {
|
mail: {
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
|
@ -6,4 +6,9 @@
|
|||||||
database: "swaf_test",
|
database: "swaf_test",
|
||||||
create_database_automatically: true,
|
create_database_automatically: true,
|
||||||
},
|
},
|
||||||
|
session: {
|
||||||
|
cookie: {
|
||||||
|
maxAge: 1000, // 1s
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,7 @@ export default class AuthGuard {
|
|||||||
|
|
||||||
if (proofs.length === 0) {
|
if (proofs.length === 0) {
|
||||||
session.is_authenticated = false;
|
session.is_authenticated = false;
|
||||||
|
session.persistent = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return proofs;
|
return proofs;
|
||||||
@ -107,6 +108,7 @@ export default class AuthGuard {
|
|||||||
public async authenticateOrRegister(
|
public async authenticateOrRegister(
|
||||||
session: Session & Partial<SessionData>,
|
session: Session & Partial<SessionData>,
|
||||||
proof: AuthProof<User>,
|
proof: AuthProof<User>,
|
||||||
|
persistSession: boolean,
|
||||||
onLogin?: (user: User) => Promise<void>,
|
onLogin?: (user: User) => Promise<void>,
|
||||||
beforeRegister?: (connection: Connection, user: User) => Promise<RegisterCallback[]>,
|
beforeRegister?: (connection: Connection, user: User) => Promise<RegisterCallback[]>,
|
||||||
afterRegister?: (connection: Connection, user: User) => Promise<RegisterCallback[]>,
|
afterRegister?: (connection: Connection, user: User) => Promise<RegisterCallback[]>,
|
||||||
@ -151,6 +153,7 @@ export default class AuthGuard {
|
|||||||
|
|
||||||
// Login
|
// Login
|
||||||
session.is_authenticated = true;
|
session.is_authenticated = true;
|
||||||
|
session.persistent = persistSession;
|
||||||
if (onLogin) await onLogin(user);
|
if (onLogin) await onLogin(user);
|
||||||
|
|
||||||
return 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(
|
await MagicLinkController.sendMagicLink(
|
||||||
this.app,
|
this.app,
|
||||||
req.getSession().id,
|
req.getSession().id,
|
||||||
|
@ -62,7 +62,7 @@ export default class MagicLinkController<A extends Application> extends Controll
|
|||||||
// Auth
|
// Auth
|
||||||
try {
|
try {
|
||||||
return await req.as(AuthMiddleware).getAuthGuard().authenticateOrRegister(
|
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);
|
const userNameComponent = user.asOptional(UserNameComponent);
|
||||||
if (userNameComponent) userNameComponent.name = magicLink.as(MagicLinkUserNameComponent).username;
|
if (userNameComponent) userNameComponent.name = magicLink.as(MagicLinkUserNameComponent).username;
|
||||||
return [];
|
return [];
|
||||||
|
@ -58,7 +58,11 @@ export default class PasswordAuthMethod implements AuthMethod<PasswordAuthProof>
|
|||||||
|
|
||||||
await passwordAuthProof.authorize(req.body.password);
|
await passwordAuthProof.authorize(req.body.password);
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
if (e instanceof AuthError) {
|
if (e instanceof AuthError) {
|
||||||
Throttler.throttle('login_failed_attempts_user', 3, 3 * 60 * 1000, // 3min
|
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());
|
const passwordAuthProof = PasswordAuthProof.createAuthorizedProofForRegistration(req.getSession());
|
||||||
try {
|
try {
|
||||||
await this.app.as(AuthComponent).getAuthGuard().authenticateOrRegister(req.getSession(), passwordAuthProof,
|
await this.app.as(AuthComponent).getAuthGuard().authenticateOrRegister(req.getSession(), passwordAuthProof,
|
||||||
undefined, async (connection, user) => {
|
true, undefined, async (connection, user) => {
|
||||||
const callbacks: RegisterCallback[] = [];
|
const callbacks: RegisterCallback[] = [];
|
||||||
|
|
||||||
// Password
|
// Password
|
||||||
|
@ -114,10 +114,10 @@ class RedisStore extends Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get(sid: string, callback: (err?: Error, session?: (session.SessionData | null)) => void): void {
|
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 => {
|
.then(value => {
|
||||||
if (value) {
|
if (value) {
|
||||||
this.redisComponent.persist('-session:' + sid, 2592000000) // 30 days
|
this.redisComponent.persist(`-session:${sid}`, config.get<number>('session.cookie.maxAge'))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
callback(undefined, JSON.parse(value));
|
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 {
|
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(() => {
|
.then(() => {
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
})
|
})
|
||||||
@ -138,7 +138,7 @@ class RedisStore extends Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public destroy(sid: string, callback?: (err?: Error) => void): void {
|
public destroy(sid: string, callback?: (err?: Error) => void): void {
|
||||||
this.redisComponent.forget('-session:' + sid)
|
this.redisComponent.forget(`-session:${sid}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
})
|
})
|
||||||
|
@ -30,7 +30,6 @@ export default class SessionComponent extends ApplicationComponent {
|
|||||||
cookie: {
|
cookie: {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: config.get('session.cookie.secure'),
|
secure: config.get('session.cookie.secure'),
|
||||||
maxAge: config.get('session.cookie.maxAge'),
|
|
||||||
},
|
},
|
||||||
rolling: true,
|
rolling: true,
|
||||||
}));
|
}));
|
||||||
@ -38,6 +37,7 @@ export default class SessionComponent extends ApplicationComponent {
|
|||||||
router.use(flash());
|
router.use(flash());
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
|
// Request session getters
|
||||||
req.getSessionOptional = () => {
|
req.getSessionOptional = () => {
|
||||||
return req.session;
|
return req.session;
|
||||||
};
|
};
|
||||||
@ -47,8 +47,18 @@ export default class SessionComponent extends ApplicationComponent {
|
|||||||
return session;
|
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 = {};
|
const _flash: FlashStorage = {};
|
||||||
res.locals.flash = (key?: string): FlashMessages | unknown[] => {
|
res.locals.flash = (key?: string): FlashMessages | unknown[] => {
|
||||||
if (key !== undefined) {
|
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;
|
previousUrl?: string;
|
||||||
|
|
||||||
|
wantsSessionPersistence?: boolean;
|
||||||
|
|
||||||
|
persistent?: boolean;
|
||||||
|
|
||||||
is_authenticated?: boolean;
|
is_authenticated?: boolean;
|
||||||
|
|
||||||
auth_password_proof?: PasswordAuthProofSessionData;
|
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, '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>
|
<button type="submit">Authenticate</button>
|
||||||
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
|
Loading…
Reference in New Issue
Block a user