From 9b6042e44de8f4464247cce36bbb5e8b04238bdd Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra <1884376+raisedadead@users.noreply.github.com> Date: Thu, 9 Feb 2023 14:01:13 +0530 Subject: [PATCH] feat: enable mobile auth endpoints (#49298 Reverts #49212 --- api-server/package.json | 3 + api-server/src/common/models/user.js | 17 +++++ api-server/src/server/boot/authentication.js | 62 +++++++++++++++++- api-server/src/server/middleware.json | 5 +- .../src/server/middlewares/rate-limit.js | 19 ++++++ .../middlewares/request-authorization.js | 4 +- api-server/src/server/utils/middleware.js | 14 ++++ package-lock.json | 65 +++++++++++++++++++ 8 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 api-server/src/server/middlewares/rate-limit.js diff --git a/api-server/package.json b/api-server/package.json index 7be433231a7..8b2b9214fea 100644 --- a/api-server/package.json +++ b/api-server/package.json @@ -45,6 +45,7 @@ "dedent": "0.7.0", "dotenv": "6.2.0", "express-flash": "0.0.2", + "express-rate-limit": "^6.7.0", "express-session": "1.17.3", "express-validator": "6.14.1", "helmet": "3.23.3", @@ -60,12 +61,14 @@ "mongodb": "3.6.9", "morgan": "1.10.0", "nanoid": "3.3.4", + "node-fetch": "^2.6.7", "nodemailer-ses-transport": "1.5.1", "passport": "0.4.1", "passport-auth0": "1.4.2", "passport-local": "1.0.0", "passport-mock-strategy": "2.0.0", "query-string": "6.14.0", + "rate-limit-mongo": "^2.3.2", "rx": "4.1.0", "stripe": "8.205.0", "uuid": "3.4.0", diff --git a/api-server/src/common/models/user.js b/api-server/src/common/models/user.js index 31efb6a04d5..028ffcc7dc9 100644 --- a/api-server/src/common/models/user.js +++ b/api-server/src/common/models/user.js @@ -162,6 +162,8 @@ export default function initializeUser(User) { User.definition.properties.rand.default = getRandomNumber; // increase user accessToken ttl to 900 days User.settings.ttl = 900 * 24 * 60 * 60 * 1000; + // Sets ttl to 900 days for mobile login created access tokens + User.settings.maxTTL = 900 * 24 * 60 * 60 * 1000; // username should not be in blocklist User.validatesExclusionOf('username', { @@ -341,6 +343,21 @@ export default function initializeUser(User) { ); }; + User.prototype.mobileLoginByRequest = function mobileLoginByRequest( + req, + res + ) { + return new Promise((resolve, reject) => + this.createAccessToken({}, (err, accessToken) => { + if (err) { + return reject(err); + } + setAccessTokenToResponse({ accessToken }, req, res); + return resolve(accessToken); + }) + ); + }; + User.afterRemote('logout', function ({ req, res }, result, next) { removeCookies(req, res); next(); diff --git a/api-server/src/server/boot/authentication.js b/api-server/src/server/boot/authentication.js index b55a90cdc49..c7589da53ce 100644 --- a/api-server/src/server/boot/authentication.js +++ b/api-server/src/server/boot/authentication.js @@ -2,10 +2,9 @@ import dedent from 'dedent'; import { check } from 'express-validator'; import jwt from 'jsonwebtoken'; import passport from 'passport'; +import fetch from 'node-fetch'; import { isEmail } from 'validator'; - import { jwtSecret } from '../../../../config/secrets'; - import { decodeEmail } from '../../common/utils'; import { createPassportCallbackAuthenticator, @@ -14,7 +13,11 @@ import { } from '../component-passport'; import { wrapHandledError } from '../utils/create-handled-error.js'; import { removeCookies } from '../utils/getSetAccessToken'; -import { ifUserRedirectTo, ifNoUserRedirectHome } from '../utils/middleware'; +import { + ifUserRedirectTo, + ifNoUserRedirectHome, + ifNotMobileRedirect +} from '../utils/middleware'; import { getRedirectParams } from '../utils/redirection'; import { createDeleteUserToken } from '../middlewares/user-token'; @@ -34,6 +37,7 @@ module.exports = function enableAuthentication(app) { // enable loopback access control authentication. see: // loopback.io/doc/en/lb2/Authentication-authorization-and-permissions.html app.enableAuth(); + const ifNotMobile = ifNotMobileRedirect(); const ifUserRedirect = ifUserRedirectTo(); const ifNoUserRedirect = ifNoUserRedirectHome(); const devSaveAuthCookies = devSaveResponseAuthCookies(); @@ -87,6 +91,8 @@ module.exports = function enableAuthentication(app) { createGetPasswordlessAuth(app) ); + api.get('/mobile-login', ifNotMobile, ifUserRedirect, mobileLogin(app)); + app.use(api); }; @@ -188,3 +194,53 @@ function createGetPasswordlessAuth(app) { ); }; } + +function mobileLogin(app) { + const { + models: { User } + } = app; + return async function getPasswordlessAuth(req, res, next) { + try { + const auth0Res = await fetch( + `https://${process.env.AUTH0_DOMAIN}/userinfo`, + { + headers: { Authorization: req.headers.authorization } + } + ); + + if (!auth0Res.ok) { + return next( + wrapHandledError(new Error('Invalid Auth0 token'), { + type: 'danger', + message: 'We could not log you in, please try again in a moment.', + status: auth0Res.status + }) + ); + } + + const { email } = await auth0Res.json(); + + if (!isEmail(email)) { + return next( + wrapHandledError(new TypeError('decoded email is invalid'), { + type: 'danger', + message: 'The email is incorrectly formatted', + status: 400 + }) + ); + } + + User.findOne$({ where: { email } }) + .do(async user => { + if (!user) { + user = await User.create({ email }); + } + await user.mobileLoginByRequest(req, res); + res.end(); + }) + .subscribe(() => {}, next); + } catch (err) { + next(err); + } + }; +} diff --git a/api-server/src/server/middleware.json b/api-server/src/server/middleware.json index 69a47f44021..df8d4ae9ff8 100644 --- a/api-server/src/server/middleware.json +++ b/api-server/src/server/middleware.json @@ -39,7 +39,10 @@ "./middlewares/constant-headers": {}, "./middlewares/csp": {}, "./middlewares/flash-cheaters": {}, - "./middlewares/passport-login": {} + "./middlewares/passport-login": {}, + "./middlewares/rate-limit": { + "paths": ["/mobile-login"] + } }, "files": {}, "final:after": { diff --git a/api-server/src/server/middlewares/rate-limit.js b/api-server/src/server/middlewares/rate-limit.js new file mode 100644 index 00000000000..08f7b8e0d29 --- /dev/null +++ b/api-server/src/server/middlewares/rate-limit.js @@ -0,0 +1,19 @@ +import rateLimit from 'express-rate-limit'; +import MongoStore from 'rate-limit-mongo'; + +const url = process.env.MONGODB || process.env.MONGOHQ_URL; + +// Rate limit for mobile login +// 10 requests per 15 minute windows +export default function rateLimitMiddleware() { + return rateLimit({ + windowMs: 15 * 60 * 1000, + max: 10, + standardHeaders: true, + legacyHeaders: false, + store: new MongoStore({ + uri: url, + expireTimeMs: 15 * 60 * 1000 + }) + }); +} diff --git a/api-server/src/server/middlewares/request-authorization.js b/api-server/src/server/middlewares/request-authorization.js index b12858222bf..60aedcdb936 100644 --- a/api-server/src/server/middlewares/request-authorization.js +++ b/api-server/src/server/middlewares/request-authorization.js @@ -26,6 +26,7 @@ const updateHooksRE = /^\/hooks\/update-paypal$/; // note: this would be replaced by webhooks later const donateRE = /^\/donate\/charge-stripe$/; const submitCoderoadChallengeRE = /^\/coderoad-challenge-completed$/; +const mobileLoginRE = /^\/mobile-login\/?$/; const _pathsAllowedREs = [ authRE, @@ -41,7 +42,8 @@ const _pathsAllowedREs = [ unsubscribeRE, updateHooksRE, donateRE, - submitCoderoadChallengeRE + submitCoderoadChallengeRE, + mobileLoginRE ]; export function isAllowedPath(path, pathsAllowedREs = _pathsAllowedREs) { diff --git a/api-server/src/server/utils/middleware.js b/api-server/src/server/utils/middleware.js index 52f5551fc85..61144fae64b 100644 --- a/api-server/src/server/utils/middleware.js +++ b/api-server/src/server/utils/middleware.js @@ -77,6 +77,20 @@ export function ifUserRedirectTo(status) { }; } +export function ifNotMobileRedirect() { + return (req, res, next) => { + // + // Todo: Use the below check once we have done more research on usage + // + // const isMobile = /(iPhone|iPad|Android)/.test(req.headers['user-agent']); + // if (!isMobile) { + // res.json({ error: 'not from mobile' }); + // } else { + // next(); + // } + next(); + }; +} // for use with express-validator error formatter export const createValidatorErrorHandler = (...args) => diff --git a/package-lock.json b/package-lock.json index 6f0f4f68bac..a675d5ace56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -131,6 +131,7 @@ "dedent": "0.7.0", "dotenv": "6.2.0", "express-flash": "0.0.2", + "express-rate-limit": "^6.7.0", "express-session": "1.17.3", "express-validator": "6.14.1", "helmet": "3.23.3", @@ -146,12 +147,14 @@ "mongodb": "3.6.9", "morgan": "1.10.0", "nanoid": "3.3.4", + "node-fetch": "^2.6.7", "nodemailer-ses-transport": "1.5.1", "passport": "0.4.1", "passport-auth0": "1.4.2", "passport-local": "1.0.0", "passport-mock-strategy": "2.0.0", "query-string": "6.14.0", + "rate-limit-mongo": "^2.3.2", "rx": "4.1.0", "stripe": "8.205.0", "uuid": "3.4.0", @@ -24594,6 +24597,17 @@ "version": "1.2.0", "license": "ISC" }, + "node_modules/express-rate-limit": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", + "integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==", + "engines": { + "node": ">= 12.9.0" + }, + "peerDependencies": { + "express": "^4 || ^5" + } + }, "node_modules/express-session": { "version": "1.17.3", "license": "MIT", @@ -42419,6 +42433,21 @@ "node": ">= 0.6" } }, + "node_modules/rate-limit-mongo": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/rate-limit-mongo/-/rate-limit-mongo-2.3.2.tgz", + "integrity": "sha512-dLck0j5N/AX9ycVHn5lX9Ti2Wrrwi1LfbXitu/mMBZOo2nC26RgYKJVbcb2mYgb9VMaPI2IwJVzIa2hAQrMaDA==", + "dependencies": { + "mongodb": "^3.6.7", + "twostep": "0.4.2", + "underscore": "1.12.1" + } + }, + "node_modules/rate-limit-mongo/node_modules/underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" + }, "node_modules/raw-body": { "version": "2.5.1", "license": "MIT", @@ -50191,6 +50220,11 @@ "dev": true, "license": "MIT" }, + "node_modules/twostep": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/twostep/-/twostep-0.4.2.tgz", + "integrity": "sha512-O/wdPYk9ey04qcCiw8AQN74DbvLFZLAgnryrNTpV7T/sxB4lcGkCMHynx5xCcA6fCh739ZAqp3HcGhy770X1qA==" + }, "node_modules/type": { "version": "1.2.0", "license": "ISC" @@ -55889,6 +55923,7 @@ "dedent": "0.7.0", "dotenv": "6.2.0", "express-flash": "0.0.2", + "express-rate-limit": "^6.7.0", "express-session": "1.17.3", "express-validator": "6.14.1", "helmet": "3.23.3", @@ -55905,6 +55940,7 @@ "mongodb": "3.6.9", "morgan": "1.10.0", "nanoid": "3.3.4", + "node-fetch": "^2.6.7", "nodemailer-ses-transport": "1.5.1", "nodemon": "2.0.16", "passport": "0.4.1", @@ -55912,6 +55948,7 @@ "passport-local": "1.0.0", "passport-mock-strategy": "2.0.0", "query-string": "6.14.0", + "rate-limit-mongo": "^2.3.2", "rx": "4.1.0", "smee-client": "1.2.3", "stripe": "8.205.0", @@ -71687,6 +71724,12 @@ } } }, + "express-rate-limit": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", + "integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==", + "requires": {} + }, "express-session": { "version": "1.17.3", "requires": { @@ -83153,6 +83196,23 @@ "range-parser": { "version": "1.2.1" }, + "rate-limit-mongo": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/rate-limit-mongo/-/rate-limit-mongo-2.3.2.tgz", + "integrity": "sha512-dLck0j5N/AX9ycVHn5lX9Ti2Wrrwi1LfbXitu/mMBZOo2nC26RgYKJVbcb2mYgb9VMaPI2IwJVzIa2hAQrMaDA==", + "requires": { + "mongodb": "^3.6.7", + "twostep": "0.4.2", + "underscore": "1.12.1" + }, + "dependencies": { + "underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" + } + } + }, "raw-body": { "version": "2.5.1", "requires": { @@ -88146,6 +88206,11 @@ "version": "1.5.0", "dev": true }, + "twostep": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/twostep/-/twostep-0.4.2.tgz", + "integrity": "sha512-O/wdPYk9ey04qcCiw8AQN74DbvLFZLAgnryrNTpV7T/sxB4lcGkCMHynx5xCcA6fCh739ZAqp3HcGhy770X1qA==" + }, "type": { "version": "1.2.0" },