From 8ccf073139131446103d41e4764bbceda7ca079a Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Wed, 3 Jun 2020 13:44:47 +0200 Subject: [PATCH] Add MagicLinkAuthController helper class --- package.json | 2 + .../magic_link/MagicLinkAuthController.ts | 156 ++++++++++++++++++ yarn.lock | 85 +++++++++- 3 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 src/auth/magic_link/MagicLinkAuthController.ts diff --git a/package.json b/package.json index 8eade23..43bc0bb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/auth/magic_link/MagicLinkAuthController.ts b/src/auth/magic_link/MagicLinkAuthController.ts new file mode 100644 index 0000000..0d7d655 --- /dev/null +++ b/src/auth/magic_link/MagicLinkAuthController.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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.`); + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 78f1c65..bc378e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"