Add MagicLinkAuthController helper class

This commit is contained in:
Alice Gaudon 2020-06-03 13:44:47 +02:00
parent 384bd2fc17
commit 8ccf073139
3 changed files with 240 additions and 3 deletions

View File

@ -22,6 +22,7 @@
"@types/connect-redis": "^0.0.13",
"@types/cookie": "^0.3.3",
"@types/cookie-parser": "^1.4.2",
"@types/geoip-lite": "^1.1.31",
"@types/jest": "^25.2.1",
"@types/mjml": "^4.0.4",
"@types/on-finished": "^2.3.1",
@ -46,6 +47,7 @@
"cookie-parser": "^1.4.5",
"express": "^4.17.1",
"express-session": "^1.17.1",
"geoip-lite": "^1.4.2",
"mjml": "^4.6.2",
"mysql": "^2.18.1",
"nodemailer": "^6.4.6",

View File

@ -0,0 +1,156 @@
import {Request, Response} from "express";
import Controller from "../../Controller";
import MagicLink from "../models/MagicLink";
import {REQUIRE_AUTH_MIDDLEWARE, REQUIRE_GUEST_MIDDLEWARE} from "../AuthComponent";
import {BadRequestError} from "../../HttpError";
import UserEmail from "../models/UserEmail";
import MagicLinkController from "./MagicLinkController";
import {MailTemplate} from "../../Mail";
import {AuthError} from "../AuthGuard";
import geoip from "geoip-lite";
export default abstract class AuthController extends Controller {
public static async checkAndAuth(req: Request, magicLink: MagicLink): Promise<void> {
if (magicLink.getSessionID() !== req.sessionID!) throw new BadOwnerMagicLink();
if (!await magicLink.isAuthorized()) throw new UnauthorizedMagicLink();
if (!await magicLink.isValid()) throw new InvalidMagicLink();
// Auth
await req.authGuard.authenticateOrRegister(req.session!, magicLink);
}
protected readonly loginMagicLinkActionType: string = 'Login';
protected readonly registerMagicLinkActionType: string = 'Register';
private readonly magicLinkMailTemplate: MailTemplate;
protected constructor(magicLinkMailTemplate: MailTemplate) {
super();
this.magicLinkMailTemplate = magicLinkMailTemplate;
}
public getRoutesPrefix(): string {
return '/auth';
}
routes(): void {
this.get('/', this.getAuth, 'auth', REQUIRE_GUEST_MIDDLEWARE);
this.post('/', this.postAuth, 'auth', REQUIRE_GUEST_MIDDLEWARE);
this.get('/check', this.getCheckAuth, 'check_auth');
this.get('/logout', this.getLogout, 'logout', REQUIRE_AUTH_MIDDLEWARE);
}
protected async getAuth(request: Request, response: Response): Promise<void> {
const registerEmail = request.flash('register_confirm_email');
const link = await MagicLink.bySessionID(request.sessionID!, [this.loginMagicLinkActionType, this.registerMagicLinkActionType]);
if (link && await link.isValid()) {
response.redirect(Controller.route('magic_link_lobby'));
return;
}
response.render('auth', {
register_confirm_email: registerEmail.length > 0 ? registerEmail[0] : null,
});
}
protected async postAuth(req: Request, res: Response): Promise<void> {
const email = req.body.email;
if (!email) throw new BadRequestError('Email not specified.', 'Please try again.', req.originalUrl);
let userEmail = await UserEmail.fromEmail(email);
let isRegistration = false;
if (!userEmail) {
isRegistration = true;
userEmail = new UserEmail({
email: email,
main: true,
});
await userEmail.validate(true);
}
if (!isRegistration || req.body.confirm_register === 'confirm') {
// Register (email link)
const geo = geoip.lookup(req.ip);
await MagicLinkController.sendMagicLink(
req.sessionID!,
isRegistration ? this.registerMagicLinkActionType : this.loginMagicLinkActionType,
Controller.route('auth'),
email,
this.magicLinkMailTemplate,
{
type: isRegistration ? 'register' : 'login',
ip: req.ip,
geo: geo ? `${geo.city}, ${geo.country}` : 'Unknown location',
},
req,
res
);
} else {
// Confirm registration req
req.flash('register_confirm_email', email);
res.redirect(Controller.route('auth'));
}
}
/**
* Check whether a magic link is authorized, and authenticate if yes
*/
protected async getCheckAuth(req: Request, res: Response): Promise<void> {
const magicLink = await MagicLink.bySessionID(req.sessionID!, [this.loginMagicLinkActionType, this.registerMagicLinkActionType]);
if (!magicLink) {
res.format({
json: () => {
throw new BadRequestError('No magic link were found linked with that session.', 'Please retry once you have requested a magic link.', req.originalUrl);
},
default: () => {
req.flash('warning', 'No magic link found. Please try again.');
res.redirect(Controller.route('auth'));
},
});
return;
}
await AuthController.checkAndAuth(req, magicLink);
// Auth success
const username = req.models.user?.name;
res.format({
json: () => {
res.json({'status': 'success', 'message': `Welcome, ${username}!`});
},
default: () => {
req.flash('success', `Authentication success. Welcome, ${username}!`);
res.redirect('/');
},
});
return;
}
protected async getLogout(req: Request, res: Response): Promise<void> {
await req.authGuard.logout(req.session!);
req.flash('success', 'Successfully logged out.');
res.redirectBack('/');
}
}
export class BadOwnerMagicLink extends AuthError {
constructor() {
super(`This magic link doesn't belong to this session.`);
}
}
export class UnauthorizedMagicLink extends AuthError {
constructor() {
super(`This magic link is unauthorized.`);
}
}
export class InvalidMagicLink extends AuthError {
constructor() {
super(`This magic link is invalid.`);
}
}

View File

@ -575,6 +575,11 @@
"@types/qs" "*"
"@types/serve-static" "*"
"@types/geoip-lite@^1.1.31":
version "1.1.31"
resolved "https://registry.toot.party/@types%2fgeoip-lite/-/geoip-lite-1.1.31.tgz#0063e47916ea982fa913a8c825f429e66b59ad10"
integrity sha512-fUfJw0jak7dvicCe+Dg5fOdXxrD89yZn8ir8TgMSOiLDx2DxwlrRZaHqB9lH617B5OR5ikI08LCQrNHHkACJvw==
"@types/graceful-fs@^4.1.2":
version "4.1.3"
resolved "https://registry.toot.party/@types%2fgraceful-fs/-/graceful-fs-4.1.3.tgz#039af35fe26bec35003e8d86d2ee9c586354348f"
@ -927,6 +932,13 @@ astral-regex@^1.0.0:
resolved "https://registry.toot.party/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
async@^2.1.1:
version "2.6.3"
resolved "https://registry.toot.party/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
dependencies:
lodash "^4.17.14"
async@^3.1.0:
version "3.2.0"
resolved "https://registry.toot.party/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
@ -1131,6 +1143,11 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.toot.party/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
buffer-from@1.x, buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.toot.party/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@ -1332,6 +1349,11 @@ color-name@~1.1.4:
resolved "https://registry.toot.party/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
colors@^1.1.2:
version "1.4.0"
resolved "https://registry.toot.party/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
combined-stream@^1.0.6, combined-stream@~1.0.6:
version "1.0.8"
resolved "https://registry.toot.party/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@ -2006,6 +2028,13 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.toot.party/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
dependencies:
pend "~1.2.0"
fill-range@^4.0.0:
version "4.0.0"
resolved "https://registry.toot.party/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
@ -2123,6 +2152,19 @@ gensync@^1.0.0-beta.1:
resolved "https://registry.toot.party/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==
geoip-lite@^1.4.2:
version "1.4.2"
resolved "https://registry.toot.party/geoip-lite/-/geoip-lite-1.4.2.tgz#f41dc50086cce3bc31a6d2d578cad1c37f9f17b3"
integrity sha512-1rUNqar68+ldSSlSMdpLZPAM+NRokIDzB2lpQFRHSOaDVqtmy25jTAWe0lM2GqWFeaA35RiLhF8GF0vvL+qOKA==
dependencies:
async "^2.1.1"
colors "^1.1.2"
iconv-lite "^0.4.13"
ip-address "^5.8.9"
lazy "^1.0.11"
rimraf "^2.5.2"
yauzl "^2.9.2"
get-caller-file@^2.0.1:
version "2.0.5"
resolved "https://registry.toot.party/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
@ -2340,7 +2382,7 @@ human-signals@^1.1.1:
resolved "https://registry.toot.party/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
iconv-lite@0.4.24, iconv-lite@^0.4.4:
iconv-lite@0.4.24, iconv-lite@^0.4.13, iconv-lite@^0.4.4:
version "0.4.24"
resolved "https://registry.toot.party/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@ -2395,6 +2437,15 @@ ini@^1.3.4, ini@~1.3.0:
resolved "https://registry.toot.party/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
ip-address@^5.8.9:
version "5.9.4"
resolved "https://registry.toot.party/ip-address/-/ip-address-5.9.4.tgz#4660ac261ad61bd397a860a007f7e98e4eaee386"
integrity sha512-dHkI3/YNJq4b/qQaz+c8LuarD3pY24JqZWfjB8aZx1gtpc2MDILu9L9jpZe1sHpzo/yWFweQVn+U//FhazUxmw==
dependencies:
jsbn "1.1.0"
lodash "^4.17.15"
sprintf-js "1.1.2"
ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.toot.party/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
@ -3031,6 +3082,11 @@ js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
jsbn@1.1.0:
version "1.1.0"
resolved "https://registry.toot.party/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
integrity sha1-sBMHyym2GKHtJux56RH4A8TaAEA=
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.toot.party/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@ -3152,6 +3208,11 @@ kleur@^3.0.3:
resolved "https://registry.toot.party/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
lazy@^1.0.11:
version "1.0.11"
resolved "https://registry.toot.party/lazy/-/lazy-1.0.11.tgz#daa068206282542c088288e975c297c1ae77b690"
integrity sha1-2qBoIGKCVCwIgojpdcKXwa53tpA=
leven@^3.1.0:
version "3.1.0"
resolved "https://registry.toot.party/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
@ -3260,7 +3321,7 @@ lodash.unescape@^4.0.1:
resolved "https://registry.toot.party/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c"
integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=
lodash@^4.17.13, lodash@^4.17.15:
lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15:
version "4.17.15"
resolved "https://registry.toot.party/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@ -4214,6 +4275,11 @@ path-to-regexp@0.1.7:
resolved "https://registry.toot.party/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.toot.party/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.toot.party/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@ -4556,7 +4622,7 @@ ret@~0.1.10:
resolved "https://registry.toot.party/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
rimraf@^2.6.1:
rimraf@^2.5.2, rimraf@^2.6.1:
version "2.7.1"
resolved "https://registry.toot.party/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
@ -4839,6 +4905,11 @@ split-string@^3.0.1, split-string@^3.0.2:
dependencies:
extend-shallow "^3.0.0"
sprintf-js@1.1.2:
version "1.1.2"
resolved "https://registry.toot.party/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.toot.party/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
@ -5547,6 +5618,14 @@ yargs@^15.3.1:
y18n "^4.0.0"
yargs-parser "^18.1.1"
yauzl@^2.9.2:
version "2.10.0"
resolved "https://registry.toot.party/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"
yn@3.1.1:
version "3.1.1"
resolved "https://registry.toot.party/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"