From 70cb274227d4dbe32308cf2cc259d6c5d583048c Mon Sep 17 00:00:00 2001 From: Archna-1 Date: Wed, 24 Nov 2021 18:16:21 -0600 Subject: [PATCH 01/17] Get auth endpoints working --- backend/app.js | 10 ++- backend/middleware/patientAuthentication.js | 41 +++++++++++ backend/package.json | 4 ++ backend/routes/api/authentication.js | 25 +++++++ backend/yarn.lock | 72 ++++++++++++++++++- .../pages/Patient2FALogin/Patient2FALogin.js | 6 +- frontend/yarn.lock | 11 ++- 7 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 backend/middleware/patientAuthentication.js diff --git a/backend/app.js b/backend/app.js index 5bb71635..7386e313 100644 --- a/backend/app.js +++ b/backend/app.js @@ -3,11 +3,13 @@ require('dotenv').config({ path: `${process.env.NODE_ENV}.env` }); require('./utils/aws/awsSetup'); const path = require('path'); +const passport = require('passport'); const log = require('loglevel'); const express = require('express'); const fileUpload = require('express-fileupload'); const cors = require('cors'); const bodyParser = require('body-parser'); +const session = require('express-session'); const { errorHandler } = require('./utils'); const { requireAuthentication } = require('./middleware/authentication'); @@ -48,7 +50,13 @@ app.get('/*', (req, res, next) => { } }); -app.use(requireAuthentication); +// app.use(requireAuthentication); + +app.use(session({ secret: 'cats' })); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(passport.initialize()); +app.use(passport.session()); + app.use(logRequest); app.use(require('./routes')); diff --git a/backend/middleware/patientAuthentication.js b/backend/middleware/patientAuthentication.js new file mode 100644 index 00000000..2ad5a1fa --- /dev/null +++ b/backend/middleware/patientAuthentication.js @@ -0,0 +1,41 @@ +const passport = require('passport'); +const LocalStrategy = require('passport-local').Strategy; +const twofactor = require('node-2fa'); + +const { models } = require('../models'); +const { TWO_FACTOR_WINDOW_MINS } = require('../utils/constants'); + +const verifyPatientToken = (patient, token) => { + const patientSecret = patient.secret; + + if (patient.secret) { + return twofactor.verifyToken(patientSecret, token, TWO_FACTOR_WINDOW_MINS); + } + + return false; +}; + +passport.use('passport-local', new LocalStrategy( + (_id, token, done) => { + models.Patient.findById(_id, (err, user) => { + if (err) { return done(err); } + if (!user) { + return done(null, false, { message: 'No such user exists.' }); + } + if (!(verifyPatientToken(user, token))) { + return done(null, false, { message: 'Incorrect token.' }); + } + return done(null, user); + }); + }, +)); + +passport.serializeUser((user, done) => { + done(null, user._id); +}); + +passport.deserializeUser((_id, done) => { + models.Patient.findById(_id, (err, user) => { + done(err, user); + }); +}); diff --git a/backend/package.json b/backend/package.json index 2ce31bed..4b0cc9ad 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,6 +23,7 @@ "express": "^4.17.1", "express-async-errors": "^3.1.1", "express-fileupload": "^1.2.0", + "express-session": "^1.17.2", "faker": "^5.5.3", "fs": "^0.0.1-security", "gulp": "^4.0.2", @@ -40,6 +41,9 @@ "node": "^16.9.1", "node-2fa": "^2.0.2", "omit-deep-lodash": "^1.1.5", + "passport": "^0.5.0", + "passport-local": "^1.0.0", + "passport-session": "^1.0.2", "prettier": "^2.4.1", "superagent-defaults": "^0.1.14", "supertest": "^6.1.3", diff --git a/backend/routes/api/authentication.js b/backend/routes/api/authentication.js index a14637c4..d7846a58 100644 --- a/backend/routes/api/authentication.js +++ b/backend/routes/api/authentication.js @@ -1,5 +1,7 @@ const express = require('express'); const twofactor = require('node-2fa'); +const passport = require('passport'); +require('express-session'); const { errorWrap } = require('../../utils'); const { models } = require('../../models'); @@ -8,6 +10,16 @@ const { TWO_FACTOR_WINDOW_MINS } = require('../../utils/constants'); const router = express.Router(); +require('../../middleware/patientAuthentication'); + +router.post('/2fa', passport.authenticate('passport-local'), async (req, res) => { + await sendResponse( + res, + 200, + 'Successfully authenticated patient', + ); +}); + /** * Get secret, generate the token, then return the token * If a patient's secret does not already exist, generate a new secret, then also return the token @@ -39,6 +51,7 @@ router.get( const newToken = twofactor.generateToken(patient.secret); + // take out token & send through messages.js // twilio await sendResponse( res, 200, @@ -89,4 +102,16 @@ router.post( }), ); +// router.set('trust proxy', 1); // trust first proxy +/* router.use(session({ + secret: 'keyboard cat', + resave: false, + saveUninitialized: true, + cookie: { secure: true }, +})); */ + +// router.use(express.session({ secret: 'keyboard cat' })); // TODO + +// router.use(passport.session()); + module.exports = router; diff --git a/backend/yarn.lock b/backend/yarn.lock index 901df4ac..3cf24053 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -3807,7 +3807,7 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== -cookie@^0.4.0: +cookie@0.4.1, cookie@^0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== @@ -4061,6 +4061,11 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" @@ -4668,6 +4673,20 @@ express-fileupload@^1.2.0: dependencies: busboy "^0.3.1" +express-session@^1.17.2: + version "1.17.2" + resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.2.tgz#397020374f9bf7997f891b85ea338767b30d0efd" + integrity sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ== + dependencies: + cookie "0.4.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~2.0.0" + on-headers "~1.0.2" + parseurl "~1.3.3" + safe-buffer "5.2.1" + uid-safe "~2.1.5" + express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -7497,6 +7516,11 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -7689,6 +7713,33 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= +passport-local@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee" + integrity sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4= + dependencies: + passport-strategy "1.x.x" + +passport-session@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/passport-session/-/passport-session-1.0.2.tgz#28abf357b0958e7c704164a3f539bd08cb9851db" + integrity sha1-KKvzV7CVjnxwQWSj9Tm9CMuYUds= + dependencies: + passport-strategy "1.x.x" + +passport-strategy@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= + +passport@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.5.0.tgz#7914aaa55844f9dce8c3aa28f7d6b73647ee0169" + integrity sha512-ln+ue5YaNDS+fes6O5PCzXKSseY5u8MYhX9H5Co4s+HfYI5oqvnHKoOORLYDUPh+8tHvrxugF2GFcUA1Q1Gqfg== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + path-dirname@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" @@ -7771,6 +7822,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" @@ -8014,6 +8070,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +random-bytes@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" + integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs= + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -8399,7 +8460,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -9350,6 +9411,13 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +uid-safe@~2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" + integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== + dependencies: + random-bytes "~1.0.0" + ulid@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/ulid/-/ulid-2.3.0.tgz#93063522771a9774121a84d126ecd3eb9804071f" diff --git a/frontend/src/pages/Patient2FALogin/Patient2FALogin.js b/frontend/src/pages/Patient2FALogin/Patient2FALogin.js index 90e4d76d..5e83f1fd 100644 --- a/frontend/src/pages/Patient2FALogin/Patient2FALogin.js +++ b/frontend/src/pages/Patient2FALogin/Patient2FALogin.js @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import ReactCodeInput from 'react-code-input'; import { useParams } from 'react-router-dom'; +import { authenticatePatient } from '../../api/api'; import Logo from '../../assets/3dp4me_logo.png'; import { useTranslations } from '../../hooks/useTranslations'; @@ -9,6 +10,7 @@ import './Patient2FALogin.scss'; import './TokenInput.scss'; const Patient2FALogin = () => { + const [token, setToken] = useState(); const [isTokenSent, setIsTokenSent] = useState(); const translations = useTranslations()[0]; const params = useParams(); @@ -64,9 +66,9 @@ const Patient2FALogin = () => {
- + setToken(tokenInput)}/> -
Date: Fri, 26 Nov 2021 03:57:05 -0600 Subject: [PATCH 02/17] Work on sending token through Whatsapp/Twilio --- backend/routes/api/authentication.js | 27 +++++++++++++++-- backend/routes/api/metadata.js | 3 +- backend/routes/api/patients.js | 8 +++-- frontend/package.json | 2 +- frontend/src/api/api.js | 29 +++++++++++++++++++ .../pages/Patient2FALogin/Patient2FALogin.js | 10 +++++-- 6 files changed, 70 insertions(+), 9 deletions(-) diff --git a/backend/routes/api/authentication.js b/backend/routes/api/authentication.js index d7846a58..e8e6c052 100644 --- a/backend/routes/api/authentication.js +++ b/backend/routes/api/authentication.js @@ -3,6 +3,15 @@ const twofactor = require('node-2fa'); const passport = require('passport'); require('express-session'); +const accountSid = process.env.ACCOUNT_SID; +const authToken = process.env.TWILIO_AUTH_TOKEN; + +const client = require('twilio')(accountSid, authToken); + +const { + TWILIO_RECEIVING_NUMBER, + TWILIO_SENDING_NUMBER, +} = require('../../utils/constants'); const { errorWrap } = require('../../utils'); const { models } = require('../../models'); const { sendResponse } = require('../../utils/response'); @@ -12,6 +21,7 @@ const router = express.Router(); require('../../middleware/patientAuthentication'); +// TODO: Switch path to be /2fa/authenticated router.post('/2fa', passport.authenticate('passport-local'), async (req, res) => { await sendResponse( res, @@ -52,12 +62,23 @@ router.get( const newToken = twofactor.generateToken(patient.secret); // take out token & send through messages.js // twilio - await sendResponse( + // follow this example, replace w/ patient's phone number, body = token + + client.messages + .create({ + body: newToken, + to: TWILIO_RECEIVING_NUMBER, + from: TWILIO_SENDING_NUMBER, + }) + .then((message) => console.log(message.sid)); + res.send('Authentication token sent.'); + + /* await sendResponse( res, 200, 'New authentication key generated', newToken, - ); + ); */ }), ); @@ -110,7 +131,7 @@ router.post( cookie: { secure: true }, })); */ -// router.use(express.session({ secret: 'keyboard cat' })); // TODO +// router.use(express.session({ secret: 'keyboard cat' })); // TODO // set secret to patient secret // router.use(passport.session()); diff --git a/backend/routes/api/metadata.js b/backend/routes/api/metadata.js index f8963fda..613eaf3d 100644 --- a/backend/routes/api/metadata.js +++ b/backend/routes/api/metadata.js @@ -1,4 +1,5 @@ const express = require('express'); +const passport = require('passport'); const router = express.Router(); const mongoose = require('mongoose'); @@ -19,7 +20,7 @@ const { * If a user isn't allowed to view step, it isn't returned to them. */ router.get( - '/steps', + '/steps', passport.authenticate('passport-local'), errorWrap(async (req, res) => { const metaData = await getReadableSteps(req); diff --git a/backend/routes/api/patients.js b/backend/routes/api/patients.js index 6a780ed3..0fe84883 100644 --- a/backend/routes/api/patients.js +++ b/backend/routes/api/patients.js @@ -1,5 +1,6 @@ const express = require('express'); const mongoose = require('mongoose'); +const passport = require('passport'); const router = express.Router(); const _ = require('lodash'); @@ -52,6 +53,7 @@ router.get( * Returns all of our data on a specific patient. Gets both the basic info * from the Patient collection and the data from each step. * */ +// todo router.get( '/:id', errorWrap(async (req, res) => { @@ -122,6 +124,7 @@ router.post( * Updates the patients information in the Patient collection. * Note: This DOES NOT update the info for individual steps. */ +// todo router.put( '/:id', removeRequestAttributes(PATIENT_IMMUTABLE_ATTRIBUTES), @@ -188,7 +191,7 @@ router.get( * done manually through the AWS website. */ router.delete( - '/:id/files/:stepKey/:fieldKey/:fileName', + '/:id/files/:stepKey/:fieldKey/:fileName', passport.authenticate('passport-local'), errorWrap(async (req, res) => { const { id, stepKey, fieldKey, fileName, @@ -246,8 +249,9 @@ router.delete( * Uploads an individual file to S3 and records it in the DB. * URL format similar to GET file. */ +// todo upload/delete/etc. w/ files; did 2, unsure about the other router.post( - '/:id/files/:stepKey/:fieldKey/:fileName', + '/:id/files/:stepKey/:fieldKey/:fileName', passport.authenticate('passport-local'), errorWrap(async (req, res) => { // TODO during refactoring: We upload file name in form data, is this even needed??? const { diff --git a/frontend/package.json b/frontend/package.json index e27910f1..92549e0a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,7 @@ "@sweetalert/with-react": "^0.1.1", "@types/date-fns": "^2.6.0", "aws-amplify": "^4.2.9", - "axios": "^0.21.1", + "axios": "^0.24.0", "babel-eslint": "^10.1.0", "compose": "^0.1.2", "cra-template": "1.1.2", diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js index 118cf485..1741dfc4 100644 --- a/frontend/src/api/api.js +++ b/frontend/src/api/api.js @@ -1,3 +1,5 @@ +import axios from 'axios' + import instance from './axios-config'; const FileDownload = require('js-file-download'); @@ -223,3 +225,30 @@ export const getSelf = async () => { return res.data; }; + +export const send2FAPatientCode = async (_id) => { + const bodyFormData = new FormData(); + bodyFormData.append('username', _id); + const res = await instance({ + method: "get", + url: "/authentication/:patientId/", + data: bodyFormData, + headers: { "Content-Type": "multipart/form-data" }, + }) + + return res.data; +}; + +export const authenticatePatient = async (_id, token) => { + const bodyFormData = new FormData(); + bodyFormData.append('username', _id); + bodyFormData.append('password', token); + const res = await instance({ + method: "post", + url: "/authentication/2fa/", + data: bodyFormData, + headers: { "Content-Type": "multipart/form-data" }, + }) + + return res.data; +}; diff --git a/frontend/src/pages/Patient2FALogin/Patient2FALogin.js b/frontend/src/pages/Patient2FALogin/Patient2FALogin.js index 5e83f1fd..d19d3529 100644 --- a/frontend/src/pages/Patient2FALogin/Patient2FALogin.js +++ b/frontend/src/pages/Patient2FALogin/Patient2FALogin.js @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import ReactCodeInput from 'react-code-input'; import { useParams } from 'react-router-dom'; -import { authenticatePatient } from '../../api/api'; +import { authenticatePatient, send2FAPatientCode } from '../../api/api'; import Logo from '../../assets/3dp4me_logo.png'; import { useTranslations } from '../../hooks/useTranslations'; @@ -32,6 +32,11 @@ const Patient2FALogin = () => { backgroundColor: '#DEDFFB', }; + const onTokenSend = () => { + send2FAPatientCode(patientId); + setIsTokenSent(true) + }; + const displayAuthPage = () => { if (!isTokenSent) { return ( @@ -44,7 +49,8 @@ const Patient2FALogin = () => { From 0bf780736879bb6baa97383ec7f6fd6e60b171c4 Mon Sep 17 00:00:00 2001 From: Archna-1 Date: Mon, 29 Nov 2021 18:26:20 -0600 Subject: [PATCH 03/17] Connect Twilio to token sending endpoint --- backend/routes/api/authentication.js | 17 ++++------------- frontend/src/AppContent.js | 5 +++++ frontend/src/api/api.js | 16 +++++----------- .../pages/Patient2FALogin/Patient2FALogin.js | 6 ++---- .../src/pages/PatientPortal/PatientPortal.js | 5 +++++ .../src/pages/PatientPortal/PatientPortal.scss | 0 frontend/src/utils/constants.js | 1 + 7 files changed, 22 insertions(+), 28 deletions(-) create mode 100644 frontend/src/pages/PatientPortal/PatientPortal.js create mode 100644 frontend/src/pages/PatientPortal/PatientPortal.scss diff --git a/backend/routes/api/authentication.js b/backend/routes/api/authentication.js index e8e6c052..251fa738 100644 --- a/backend/routes/api/authentication.js +++ b/backend/routes/api/authentication.js @@ -33,6 +33,7 @@ router.post('/2fa', passport.authenticate('passport-local'), async (req, res) => /** * Get secret, generate the token, then return the token * If a patient's secret does not already exist, generate a new secret, then also return the token + * Send the patient token through Twilio to Whatsapp */ router.get( '/:patientId', @@ -61,24 +62,14 @@ router.get( const newToken = twofactor.generateToken(patient.secret); - // take out token & send through messages.js // twilio - // follow this example, replace w/ patient's phone number, body = token - client.messages .create({ - body: newToken, + body: `Your one time token is ${newToken.token}`, to: TWILIO_RECEIVING_NUMBER, from: TWILIO_SENDING_NUMBER, }) - .then((message) => console.log(message.sid)); - res.send('Authentication token sent.'); - - /* await sendResponse( - res, - 200, - 'New authentication key generated', - newToken, - ); */ + .then((message) => console.log(message.sid)) + .catch((err) => console.log(err)); }), ); diff --git a/frontend/src/AppContent.js b/frontend/src/AppContent.js index b7f659cd..e844989a 100644 --- a/frontend/src/AppContent.js +++ b/frontend/src/AppContent.js @@ -15,6 +15,7 @@ import DashboardManagement from './pages/DashboardManagement/DashboardManagement import PatientDetail from './pages/PatientDetail/PatientDetail'; import Patients from './pages/Patients/Patients'; import Patient2FA from './pages/Patient2FALogin/Patient2FALogin'; +import PatientPortal from './pages/PatientPortal/PatientPortal'; import { Context } from './store/Store'; import { COGNITO_ATTRIBUTES, @@ -116,6 +117,10 @@ const AppContent = ({ username, userEmail }) => { > + + +
diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js index dfed47ac..8921d48f 100644 --- a/frontend/src/api/api.js +++ b/frontend/src/api/api.js @@ -1,5 +1,3 @@ -import axios from 'axios' - import instance from './axios-config'; const FileDownload = require('js-file-download'); @@ -251,14 +249,10 @@ export const getSelf = async () => { }; export const send2FAPatientCode = async (_id) => { - const bodyFormData = new FormData(); - bodyFormData.append('username', _id); - const res = await instance({ - method: "get", - url: "/authentication/:patientId/", - data: bodyFormData, - headers: { "Content-Type": "multipart/form-data" }, - }) + const requestString = `/authentication/${_id}`; + const res = await instance.get(requestString); + + if (!res?.data?.success) throw new Error(res?.data?.message); return res.data; }; @@ -275,4 +269,4 @@ export const authenticatePatient = async (_id, token) => { }) return res.data; -}; +}; \ No newline at end of file diff --git a/frontend/src/pages/Patient2FALogin/Patient2FALogin.js b/frontend/src/pages/Patient2FALogin/Patient2FALogin.js index d19d3529..1775e5d0 100644 --- a/frontend/src/pages/Patient2FALogin/Patient2FALogin.js +++ b/frontend/src/pages/Patient2FALogin/Patient2FALogin.js @@ -34,7 +34,7 @@ const Patient2FALogin = () => { const onTokenSend = () => { send2FAPatientCode(patientId); - setIsTokenSent(true) + setIsTokenSent(true); }; const displayAuthPage = () => { @@ -49,8 +49,7 @@ const Patient2FALogin = () => { @@ -73,7 +72,6 @@ const Patient2FALogin = () => {
setToken(tokenInput)}/> - diff --git a/frontend/src/pages/PatientPortal/PatientPortal.js b/frontend/src/pages/PatientPortal/PatientPortal.js new file mode 100644 index 00000000..51683321 --- /dev/null +++ b/frontend/src/pages/PatientPortal/PatientPortal.js @@ -0,0 +1,5 @@ +const PatientPortal = () => { + +} + +export default PatientPortal \ No newline at end of file diff --git a/frontend/src/pages/PatientPortal/PatientPortal.scss b/frontend/src/pages/PatientPortal/PatientPortal.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 6ec96c65..edb766d6 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -47,6 +47,7 @@ export const ROUTES = { DASHBOARD_MANAGEMENT: '/dashboard-management', PATIENT_DETAIL: '/patient-info', PATIENT_2FA: '/patient-2fa', + PATIENT_PORTAL: '/patient-portal' }; /** From d25bd54f846d5558f80ec3e6f774b485f94d39bd Mon Sep 17 00:00:00 2001 From: Archna-1 Date: Mon, 29 Nov 2021 18:45:08 -0600 Subject: [PATCH 04/17] Add support for patient phone numbers from database --- backend/routes/api/authentication.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/routes/api/authentication.js b/backend/routes/api/authentication.js index 251fa738..41622c17 100644 --- a/backend/routes/api/authentication.js +++ b/backend/routes/api/authentication.js @@ -9,7 +9,6 @@ const authToken = process.env.TWILIO_AUTH_TOKEN; const client = require('twilio')(accountSid, authToken); const { - TWILIO_RECEIVING_NUMBER, TWILIO_SENDING_NUMBER, } = require('../../utils/constants'); const { errorWrap } = require('../../utils'); @@ -65,7 +64,7 @@ router.get( client.messages .create({ body: `Your one time token is ${newToken.token}`, - to: TWILIO_RECEIVING_NUMBER, + to: `whatsapp:+${patient.phoneNumber}`, from: TWILIO_SENDING_NUMBER, }) .then((message) => console.log(message.sid)) From 8977d5b65d61ef9b9fe5afbc5b1aaa4cf0e97480 Mon Sep 17 00:00:00 2001 From: Archna-1 Date: Mon, 29 Nov 2021 22:44:56 -0600 Subject: [PATCH 05/17] Connect patient token login to placeholder page --- backend/routes/api/authentication.js | 1 + frontend/src/api/api.js | 3 ++- frontend/src/pages/Patient2FALogin/Patient2FALogin.js | 10 +++++++++- frontend/src/pages/PatientPortal/PatientPortal.js | 10 +++++++++- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/backend/routes/api/authentication.js b/backend/routes/api/authentication.js index 41622c17..360017bc 100644 --- a/backend/routes/api/authentication.js +++ b/backend/routes/api/authentication.js @@ -63,6 +63,7 @@ router.get( client.messages .create({ + // todo: constants body: `Your one time token is ${newToken.token}`, to: `whatsapp:+${patient.phoneNumber}`, from: TWILIO_SENDING_NUMBER, diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js index 8921d48f..d498afd4 100644 --- a/frontend/src/api/api.js +++ b/frontend/src/api/api.js @@ -252,8 +252,9 @@ export const send2FAPatientCode = async (_id) => { const requestString = `/authentication/${_id}`; const res = await instance.get(requestString); + // Need to handle case of invalid patient id if (!res?.data?.success) throw new Error(res?.data?.message); - + return res.data; }; diff --git a/frontend/src/pages/Patient2FALogin/Patient2FALogin.js b/frontend/src/pages/Patient2FALogin/Patient2FALogin.js index 1775e5d0..73907e71 100644 --- a/frontend/src/pages/Patient2FALogin/Patient2FALogin.js +++ b/frontend/src/pages/Patient2FALogin/Patient2FALogin.js @@ -5,6 +5,7 @@ import { useParams } from 'react-router-dom'; import { authenticatePatient, send2FAPatientCode } from '../../api/api'; import Logo from '../../assets/3dp4me_logo.png'; import { useTranslations } from '../../hooks/useTranslations'; +import { ROUTES } from '../../utils/constants'; import './Patient2FALogin.scss'; import './TokenInput.scss'; @@ -37,6 +38,13 @@ const Patient2FALogin = () => { setIsTokenSent(true); }; + const checkIsAuthenticated = async() => { + const res = await authenticatePatient(patientId, token); + if (res.success) { + window.location = `${ window.location.protocol }//${ window.location.hostname }:3000${ ROUTES.PATIENT_PORTAL }/${patientId}`; + } + } + const displayAuthPage = () => { if (!isTokenSent) { return ( @@ -72,7 +80,7 @@ const Patient2FALogin = () => {
setToken(tokenInput)}/> -
{ +import React from 'react' +const PatientPortal = () => { + return ( +
+
+ hi +
+
+ ) } export default PatientPortal \ No newline at end of file From c337f07764d3e98f0c268a75a63c532873345e0e Mon Sep 17 00:00:00 2001 From: Archna-1 Date: Thu, 2 Dec 2021 22:00:49 -0600 Subject: [PATCH 06/17] Add constants and change patient-2fa URL --- backend/routes/api/authentication.js | 25 +++++++++++++++++++++---- backend/routes/api/index.js | 2 +- backend/utils/constants.js | 2 ++ frontend/src/api/api.js | 4 ++-- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/backend/routes/api/authentication.js b/backend/routes/api/authentication.js index 360017bc..65670a8e 100644 --- a/backend/routes/api/authentication.js +++ b/backend/routes/api/authentication.js @@ -10,6 +10,7 @@ const client = require('twilio')(accountSid, authToken); const { TWILIO_SENDING_NUMBER, + TWILIO_WHATSAPP_PREFIX, } = require('../../utils/constants'); const { errorWrap } = require('../../utils'); const { models } = require('../../models'); @@ -20,8 +21,8 @@ const router = express.Router(); require('../../middleware/patientAuthentication'); -// TODO: Switch path to be /2fa/authenticated -router.post('/2fa', passport.authenticate('passport-local'), async (req, res) => { +// Path is now /patient-2fa/authenticated +router.post('/authenticated', passport.authenticate('passport-local'), async (req, res) => { await sendResponse( res, 200, @@ -29,6 +30,22 @@ router.post('/2fa', passport.authenticate('passport-local'), async (req, res) => ); }); +router.get('/isAuth', passport.authenticate('passport-local'), async (req, res) => { + if (req.user) { + await sendResponse( + res, + 200, + 'Successfully authenticated patient', + ); + } else { + await sendResponse( + res, + 500, + 'Successfully authenticated patient', + ); + } +}); + /** * Get secret, generate the token, then return the token * If a patient's secret does not already exist, generate a new secret, then also return the token @@ -63,9 +80,9 @@ router.get( client.messages .create({ - // todo: constants + // TODO: Backend translations body: `Your one time token is ${newToken.token}`, - to: `whatsapp:+${patient.phoneNumber}`, + to: `${TWILIO_WHATSAPP_PREFIX}${patient.phoneNumber}`, from: TWILIO_SENDING_NUMBER, }) .then((message) => console.log(message.sid)) diff --git a/backend/routes/api/index.js b/backend/routes/api/index.js index 2197a81b..065ff700 100644 --- a/backend/routes/api/index.js +++ b/backend/routes/api/index.js @@ -9,6 +9,6 @@ router.use('/metadata', require('./metadata')); router.use('/users', require('./users')); router.use('/roles', require('./roles')); router.use('/messages', require('./messages')); -router.use('/authentication', require('./authentication')); +router.use('/patient-2fa', require('./authentication')); module.exports = router; diff --git a/backend/utils/constants.js b/backend/utils/constants.js index f993351f..0579db1c 100644 --- a/backend/utils/constants.js +++ b/backend/utils/constants.js @@ -64,4 +64,6 @@ module.exports.DEFAULT_PATIENTS_ON_GET_REQUEST = 1; module.exports.TWILIO_SENDING_NUMBER = 'whatsapp:+14155238886'; module.exports.TWILIO_RECEIVING_NUMBER = 'whatsapp:+13098319210'; +module.exports.TWILIO_WHATSAPP_PREFIX = 'whatsapp:+'; + module.exports.TWO_FACTOR_WINDOW_MINS = 5; diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js index d498afd4..6cb96d2a 100644 --- a/frontend/src/api/api.js +++ b/frontend/src/api/api.js @@ -249,7 +249,7 @@ export const getSelf = async () => { }; export const send2FAPatientCode = async (_id) => { - const requestString = `/authentication/${_id}`; + const requestString = `/patient-2fa/${_id}`; const res = await instance.get(requestString); // Need to handle case of invalid patient id @@ -264,7 +264,7 @@ export const authenticatePatient = async (_id, token) => { bodyFormData.append('password', token); const res = await instance({ method: "post", - url: "/authentication/2fa/", + url: "/patient-2fa/authenticated/", data: bodyFormData, headers: { "Content-Type": "multipart/form-data" }, }) From 1d58410cb6e0182a1ae5932456bf431f62c2b5cd Mon Sep 17 00:00:00 2001 From: Archna-1 Date: Thu, 23 Dec 2021 00:27:54 -0600 Subject: [PATCH 07/17] Save progress on authentication --- backend/app.js | 30 +++++++++++++++++-- backend/routes/api/authentication.js | 23 +++++--------- .../pages/Patient2FALogin/Patient2FALogin.js | 1 + .../src/pages/PatientPortal/PatientPortal.js | 21 ++++++++++--- 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/backend/app.js b/backend/app.js index 7386e313..6af2d2c6 100644 --- a/backend/app.js +++ b/backend/app.js @@ -10,6 +10,8 @@ const fileUpload = require('express-fileupload'); const cors = require('cors'); const bodyParser = require('body-parser'); const session = require('express-session'); +const MongoStore = require('connect-mongo'); +const cookieParser = require('cookie-parser'); const { errorHandler } = require('./utils'); const { requireAuthentication } = require('./middleware/authentication'); @@ -26,7 +28,7 @@ const app = express(); app.use(configureHelment()); app.use(setResponseHeaders); app.use(express.static(path.join(__dirname, '../frontend/build'))); -app.use(cors()); +app.use(cors({ credentials: true, origin: 'http://localhost:3000', methods: ['GET', 'POST'] })); app.use( fileUpload({ createParentPath: true, @@ -52,7 +54,31 @@ app.get('/*', (req, res, next) => { // app.use(requireAuthentication); -app.use(session({ secret: 'cats' })); +/** + * This is the secret used to sign the session ID cookie. + * This can be either a string for a single secret, or an array of multiple secrets. + * If an array of secrets is provided, only the first element will be used to sign the session + * ID cookie, while all the elements will be considered when verifying the signature in requests. + * The secret itself should be not easily parsed by a human and would best be a random set of + * characters. + */ +const sess = { + secret: '3DP4ME', + cookie: { + domain: 'localhost', path: '/', httpOnly: true, secure: false, maxAge: 60000, + }, + store: MongoStore.create({ mongoUrl: process.env.DB_URI }), +}; + +if (app.get('env') === 'production') { + app.set('trust proxy', 1); // trust first proxy + sess.cookie.secure = true; // serve secure cookies +} + +app.use(cookieParser()); + +app.use(session(sess)); + app.use(bodyParser.urlencoded({ extended: false })); app.use(passport.initialize()); app.use(passport.session()); diff --git a/backend/routes/api/authentication.js b/backend/routes/api/authentication.js index 65670a8e..465f49ff 100644 --- a/backend/routes/api/authentication.js +++ b/backend/routes/api/authentication.js @@ -1,7 +1,6 @@ const express = require('express'); const twofactor = require('node-2fa'); const passport = require('passport'); -require('express-session'); const accountSid = process.env.ACCOUNT_SID; const authToken = process.env.TWILIO_AUTH_TOKEN; @@ -23,6 +22,9 @@ require('../../middleware/patientAuthentication'); // Path is now /patient-2fa/authenticated router.post('/authenticated', passport.authenticate('passport-local'), async (req, res) => { + req.session.patientId = 'i am sad bc of this pr'; + console.log(req.session); + req.session.save(); await sendResponse( res, 200, @@ -30,8 +32,9 @@ router.post('/authenticated', passport.authenticate('passport-local'), async (re ); }); -router.get('/isAuth', passport.authenticate('passport-local'), async (req, res) => { - if (req.user) { +router.get('/isAuth', async (req, res) => { + console.log(req.session); + if (req.session) { await sendResponse( res, 200, @@ -113,6 +116,8 @@ router.post( isAuthenticated = twofactor.verifyToken(patientSecret, token, TWO_FACTOR_WINDOW_MINS); } + console.log(res); + if (isAuthenticated) { await sendResponse( res, @@ -131,16 +136,4 @@ router.post( }), ); -// router.set('trust proxy', 1); // trust first proxy -/* router.use(session({ - secret: 'keyboard cat', - resave: false, - saveUninitialized: true, - cookie: { secure: true }, -})); */ - -// router.use(express.session({ secret: 'keyboard cat' })); // TODO // set secret to patient secret - -// router.use(passport.session()); - module.exports = router; diff --git a/frontend/src/pages/Patient2FALogin/Patient2FALogin.js b/frontend/src/pages/Patient2FALogin/Patient2FALogin.js index 73907e71..b5150cb5 100644 --- a/frontend/src/pages/Patient2FALogin/Patient2FALogin.js +++ b/frontend/src/pages/Patient2FALogin/Patient2FALogin.js @@ -40,6 +40,7 @@ const Patient2FALogin = () => { const checkIsAuthenticated = async() => { const res = await authenticatePatient(patientId, token); + if (res.success) { window.location = `${ window.location.protocol }//${ window.location.hostname }:3000${ ROUTES.PATIENT_PORTAL }/${patientId}`; } diff --git a/frontend/src/pages/PatientPortal/PatientPortal.js b/frontend/src/pages/PatientPortal/PatientPortal.js index 3bd4480e..dd1461d1 100644 --- a/frontend/src/pages/PatientPortal/PatientPortal.js +++ b/frontend/src/pages/PatientPortal/PatientPortal.js @@ -1,11 +1,24 @@ -import React from 'react' +import React, { useEffect } from 'react' +import axios from 'axios'; + +const BASE_URL = process.env.REACT_APP_BACKEND_BASE_URL; + +const instance = axios.create({ + baseURL: BASE_URL, + validateStatus: () => { + return true; + }, +}); const PatientPortal = () => { + useEffect(() => { + const requestString = `/patient-2fa/isAuth`; + const res = instance.get(requestString, {withCredentials: true}); + }, []); + return (
-
- hi -
+ Hi
) } From c23b05a2b7b64b5a20c9df638629b6a379500e32 Mon Sep 17 00:00:00 2001 From: Archna-1 Date: Thu, 30 Dec 2021 11:34:51 -0600 Subject: [PATCH 08/17] Get sessions working --- backend/app.js | 7 +++ backend/middleware/patientAuthentication.js | 3 ++ backend/package.json | 2 + backend/routes/api/authentication.js | 33 ++++++++------ backend/yarn.lock | 45 ++++++++++++++++++- frontend/src/api/api.js | 3 +- frontend/src/api/axios-patient-auth.js | 44 ++++++++++++++++++ .../pages/Patient2FALogin/Patient2FALogin.js | 2 +- .../src/pages/PatientPortal/PatientPortal.js | 13 +----- 9 files changed, 125 insertions(+), 27 deletions(-) create mode 100644 frontend/src/api/axios-patient-auth.js diff --git a/backend/app.js b/backend/app.js index 6af2d2c6..86e846d5 100644 --- a/backend/app.js +++ b/backend/app.js @@ -67,9 +67,16 @@ const sess = { cookie: { domain: 'localhost', path: '/', httpOnly: true, secure: false, maxAge: 60000, }, + resave: false, + saveUninitialized: false, store: MongoStore.create({ mongoUrl: process.env.DB_URI }), }; +/* app.use(cookieSession({ + maxAge: 60 * 60 * 1000, + keys: [key.cookieSession.key1], +})); */ + if (app.get('env') === 'production') { app.set('trust proxy', 1); // trust first proxy sess.cookie.secure = true; // serve secure cookies diff --git a/backend/middleware/patientAuthentication.js b/backend/middleware/patientAuthentication.js index 2ad5a1fa..76bc1d9b 100644 --- a/backend/middleware/patientAuthentication.js +++ b/backend/middleware/patientAuthentication.js @@ -31,11 +31,14 @@ passport.use('passport-local', new LocalStrategy( )); passport.serializeUser((user, done) => { + console.log('serializing user'); done(null, user._id); }); passport.deserializeUser((_id, done) => { + console.log('deserializing user'); models.Patient.findById(_id, (err, user) => { + console.log('deserializing found user'); done(err, user); }); }); diff --git a/backend/package.json b/backend/package.json index 4b0cc9ad..ff21ac0f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,6 +11,8 @@ "axios": "^0.21.2", "babel-eslint": "^10.1.0", "body-parser": "^1.19.0", + "connect-mongo": "^4.6.0", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "cross-env": "^7.0.3", "del": "^6.0.0", diff --git a/backend/routes/api/authentication.js b/backend/routes/api/authentication.js index 465f49ff..05cfdfd5 100644 --- a/backend/routes/api/authentication.js +++ b/backend/routes/api/authentication.js @@ -20,21 +20,28 @@ const router = express.Router(); require('../../middleware/patientAuthentication'); -// Path is now /patient-2fa/authenticated -router.post('/authenticated', passport.authenticate('passport-local'), async (req, res) => { - req.session.patientId = 'i am sad bc of this pr'; - console.log(req.session); - req.session.save(); - await sendResponse( - res, - 200, - 'Successfully authenticated patient', - ); +router.post('/authenticated/:patientId', passport.authenticate('passport-local'), async (req, res) => { + const { patientId } = req.params; + if (!req.user) { return res.redirect(`/${patientId}`); } + + // req / res held in closure + req.logIn(req.user, (err) => { + console.log(req.user); + if (err) { return err; } + req.session.patientId = patientId; + console.log(req.session); + req.session.save(); + return sendResponse( + res, + 200, + 'Successfully authenticated patient', + ); + }); }); router.get('/isAuth', async (req, res) => { console.log(req.session); - if (req.session) { + if (req?.session?.passport?.user) { await sendResponse( res, 200, @@ -43,8 +50,8 @@ router.get('/isAuth', async (req, res) => { } else { await sendResponse( res, - 500, - 'Successfully authenticated patient', + 401, + 'Unauthorized user', ); } }); diff --git a/backend/yarn.lock b/backend/yarn.lock index 3cf24053..c0ea8132 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -2606,6 +2606,16 @@ asap@^2.0.0: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= +asn1.js@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" + integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" + assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" @@ -3269,6 +3279,11 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" +bn.js@^4.0.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + body-parser@1.19.0, body-parser@^1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -3778,6 +3793,14 @@ confusing-browser-globals@^1.0.10: resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz#30d1e7f3d1b882b25ec4933d1d1adac353d20a59" integrity sha512-gNld/3lySHwuhaVluJUKLePYirM3QNCKzVxqAdhJII9/WXKVX5PURzMVJspS1jTslSqjeuG4KMVTSouit5YPHA== +connect-mongo@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/connect-mongo/-/connect-mongo-4.6.0.tgz#1bf62868efc9f28ecf1459ae9a9d6caaf90ae8a6" + integrity sha512-8new4Z7NLP3CGP65Aw6ls3xDBeKVvHRSh39CXuDZTQsvpeeU9oNMzfFgvqmHqZ6gWpxIl663RyoVEmCAGf1yOg== + dependencies: + debug "^4.3.1" + kruptein "^3.0.0" + content-disposition@0.5.3: version "0.5.3" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" @@ -3797,6 +3820,14 @@ convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.6.0, dependencies: safe-buffer "~5.1.1" +cookie-parser@^1.4.6: + version "1.4.6" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.6.tgz#3ac3a7d35a7a03bbc7e365073a26074824214594" + integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA== + dependencies: + cookie "0.4.1" + cookie-signature "1.0.6" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -6686,6 +6717,13 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +kruptein@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kruptein/-/kruptein-3.0.3.tgz#2073eba5cccbe5ad510b03416950f8a3e2e394c4" + integrity sha512-v5mqSHKS2M1xWUo5V7Q6TMcj1vjTgKWvfspizn6Z939Cmv8NNn5E+Z4LeGBEKDL3yT4pMXaRTjh98oksGTDntA== + dependencies: + asn1.js "^5.4.1" + language-subtag-registry@~0.3.2: version "0.3.21" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a" @@ -7053,6 +7091,11 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -8472,7 +8515,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3": +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js index 6cb96d2a..455d0e75 100644 --- a/frontend/src/api/api.js +++ b/frontend/src/api/api.js @@ -264,9 +264,10 @@ export const authenticatePatient = async (_id, token) => { bodyFormData.append('password', token); const res = await instance({ method: "post", - url: "/patient-2fa/authenticated/", + url: `/patient-2fa/authenticated/${_id}`, data: bodyFormData, headers: { "Content-Type": "multipart/form-data" }, + // credentials: 'include' }) return res.data; diff --git a/frontend/src/api/axios-patient-auth.js b/frontend/src/api/axios-patient-auth.js new file mode 100644 index 00000000..14743a5e --- /dev/null +++ b/frontend/src/api/axios-patient-auth.js @@ -0,0 +1,44 @@ +import axios from 'axios'; + +const BASE_URL = process.env.REACT_APP_BACKEND_BASE_URL; + +// Generalized axios configuration +axios.defaults.headers.common['Content-Type'] = 'application/json'; +axios.defaults.withCredentials = true; + +// The configured axios instance to be exported +const instance = axios.create({ + baseURL: BASE_URL, + validateStatus: () => { + return true; + }, +}); + +export const send2FAPatientCode = async (_id) => { + const requestString = `/patient-2fa/${_id}`; + const res = await instance.get(requestString); + + // Need to handle case of invalid patient id + if (!res?.data?.success) throw new Error(res?.data?.message); + + return res.data; +}; + +export const authenticatePatient = async (_id, token) => { + const bodyFormData = new FormData(); + bodyFormData.append('username', _id); + bodyFormData.append('password', token); + const res = await instance({ + method: "post", + url: `/patient-2fa/authenticated/${_id}`, + data: bodyFormData, + headers: { "Content-Type": "multipart/form-data" }, + }) + + return res.data; +}; + +export const randomMethod = async() => { + const requestString = `/patient-2fa/isAuth`; + const res = instance.get(requestString, {withCredentials: true}); +} \ No newline at end of file diff --git a/frontend/src/pages/Patient2FALogin/Patient2FALogin.js b/frontend/src/pages/Patient2FALogin/Patient2FALogin.js index b5150cb5..5191a3f1 100644 --- a/frontend/src/pages/Patient2FALogin/Patient2FALogin.js +++ b/frontend/src/pages/Patient2FALogin/Patient2FALogin.js @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import ReactCodeInput from 'react-code-input'; import { useParams } from 'react-router-dom'; -import { authenticatePatient, send2FAPatientCode } from '../../api/api'; +import { authenticatePatient, send2FAPatientCode } from '../../api/axios-patient-auth'; import Logo from '../../assets/3dp4me_logo.png'; import { useTranslations } from '../../hooks/useTranslations'; import { ROUTES } from '../../utils/constants'; diff --git a/frontend/src/pages/PatientPortal/PatientPortal.js b/frontend/src/pages/PatientPortal/PatientPortal.js index dd1461d1..990c4bdc 100644 --- a/frontend/src/pages/PatientPortal/PatientPortal.js +++ b/frontend/src/pages/PatientPortal/PatientPortal.js @@ -1,19 +1,10 @@ import React, { useEffect } from 'react' -import axios from 'axios'; -const BASE_URL = process.env.REACT_APP_BACKEND_BASE_URL; - -const instance = axios.create({ - baseURL: BASE_URL, - validateStatus: () => { - return true; - }, -}); +import { randomMethod } from '../../api/axios-patient-auth' const PatientPortal = () => { useEffect(() => { - const requestString = `/patient-2fa/isAuth`; - const res = instance.get(requestString, {withCredentials: true}); + randomMethod(); }, []); return ( From 21539dc3409bbf849b35c2d847e8dbd4b9c0ec87 Mon Sep 17 00:00:00 2001 From: Archna-1 Date: Fri, 31 Dec 2021 00:37:38 -0600 Subject: [PATCH 09/17] Add conditional authentication middleware --- backend/app.js | 10 +------ .../middleware/conditionalAuthentication.js | 23 ++++++++++++++++ backend/middleware/patientAuthentication.js | 3 --- backend/middleware/verifyPatient.js | 26 +++++++++++++++++++ backend/routes/api/authentication.js | 22 ++-------------- backend/routes/api/messages.js | 3 +++ backend/routes/api/metadata.js | 12 +++++---- backend/routes/api/patients.js | 18 +++++++++---- backend/routes/api/roles.js | 3 +++ backend/routes/api/steps.js | 3 +++ backend/routes/api/users.js | 11 +++++--- frontend/src/api/axios-patient-auth.js | 2 +- .../src/pages/PatientPortal/PatientPortal.js | 1 + 13 files changed, 90 insertions(+), 47 deletions(-) create mode 100644 backend/middleware/conditionalAuthentication.js create mode 100644 backend/middleware/verifyPatient.js diff --git a/backend/app.js b/backend/app.js index 86e846d5..b21662e7 100644 --- a/backend/app.js +++ b/backend/app.js @@ -14,7 +14,6 @@ const MongoStore = require('connect-mongo'); const cookieParser = require('cookie-parser'); const { errorHandler } = require('./utils'); -const { requireAuthentication } = require('./middleware/authentication'); const { initDB } = require('./utils/initDb'); const { setResponseHeaders, @@ -52,8 +51,6 @@ app.get('/*', (req, res, next) => { } }); -// app.use(requireAuthentication); - /** * This is the secret used to sign the session ID cookie. * This can be either a string for a single secret, or an array of multiple secrets. @@ -65,18 +62,13 @@ app.get('/*', (req, res, next) => { const sess = { secret: '3DP4ME', cookie: { - domain: 'localhost', path: '/', httpOnly: true, secure: false, maxAge: 60000, + domain: 'localhost', path: '/', httpOnly: true, secure: false, maxAge: 180000, }, resave: false, saveUninitialized: false, store: MongoStore.create({ mongoUrl: process.env.DB_URI }), }; -/* app.use(cookieSession({ - maxAge: 60 * 60 * 1000, - keys: [key.cookieSession.key1], -})); */ - if (app.get('env') === 'production') { app.set('trust proxy', 1); // trust first proxy sess.cookie.secure = true; // serve secure cookies diff --git a/backend/middleware/conditionalAuthentication.js b/backend/middleware/conditionalAuthentication.js new file mode 100644 index 00000000..08090810 --- /dev/null +++ b/backend/middleware/conditionalAuthentication.js @@ -0,0 +1,23 @@ +const log = require('loglevel'); + +const { + ERR_AUTH_FAILED, +} = require('../utils/constants'); +const { sendResponse } = require('../utils/response'); + +const { requireAuthentication } = require('./authentication'); +const { requirePatientAuthentication } = require('./verifyPatient'); + +module.exports.requireConditionalAuthentication = async (req, res, next) => { + try { + const user = req?.session?.passport?.user; + if (!user) { + requireAuthentication(req, res, next); + } else { + requirePatientAuthentication(req, res); + } + } catch (error) { + log.error(error); + sendResponse(res, 401, ERR_AUTH_FAILED); + } +}; diff --git a/backend/middleware/patientAuthentication.js b/backend/middleware/patientAuthentication.js index 76bc1d9b..2ad5a1fa 100644 --- a/backend/middleware/patientAuthentication.js +++ b/backend/middleware/patientAuthentication.js @@ -31,14 +31,11 @@ passport.use('passport-local', new LocalStrategy( )); passport.serializeUser((user, done) => { - console.log('serializing user'); done(null, user._id); }); passport.deserializeUser((_id, done) => { - console.log('deserializing user'); models.Patient.findById(_id, (err, user) => { - console.log('deserializing found user'); done(err, user); }); }); diff --git a/backend/middleware/verifyPatient.js b/backend/middleware/verifyPatient.js new file mode 100644 index 00000000..486de1d5 --- /dev/null +++ b/backend/middleware/verifyPatient.js @@ -0,0 +1,26 @@ +const log = require('loglevel'); + +const { + ERR_AUTH_FAILED, + ERR_NOT_APPROVED, +} = require('../utils/constants'); +const { sendResponse } = require('../utils/response'); + +/** + * Middleware requires the incoming request to be authenticated. If not authenticated, a response + * is sent back to the client, and the middleware chain is stopped. Authentication is done by + * checking the request for a user, which is automatically attached when Passport logs a user in. + */ +module.exports.requirePatientAuthentication = async (req, res) => { + try { + const user = req?.session?.passport?.user; + if (!user) { + sendResponse(res, 401, ERR_AUTH_FAILED); + } else { + sendResponse(res, 403, ERR_NOT_APPROVED); + } + } catch (error) { + log.error(error); + sendResponse(res, 401, ERR_AUTH_FAILED); + } +}; diff --git a/backend/routes/api/authentication.js b/backend/routes/api/authentication.js index 05cfdfd5..40f58b4a 100644 --- a/backend/routes/api/authentication.js +++ b/backend/routes/api/authentication.js @@ -24,12 +24,10 @@ router.post('/authenticated/:patientId', passport.authenticate('passport-local') const { patientId } = req.params; if (!req.user) { return res.redirect(`/${patientId}`); } - // req / res held in closure req.logIn(req.user, (err) => { - console.log(req.user); if (err) { return err; } - req.session.patientId = patientId; console.log(req.session); + // do i need this line? req.session.save(); return sendResponse( res, @@ -39,23 +37,6 @@ router.post('/authenticated/:patientId', passport.authenticate('passport-local') }); }); -router.get('/isAuth', async (req, res) => { - console.log(req.session); - if (req?.session?.passport?.user) { - await sendResponse( - res, - 200, - 'Successfully authenticated patient', - ); - } else { - await sendResponse( - res, - 401, - 'Unauthorized user', - ); - } -}); - /** * Get secret, generate the token, then return the token * If a patient's secret does not already exist, generate a new secret, then also return the token @@ -123,6 +104,7 @@ router.post( isAuthenticated = twofactor.verifyToken(patientSecret, token, TWO_FACTOR_WINDOW_MINS); } + // TODO: remove console logs and booleans in the response console.log(res); if (isAuthenticated) { diff --git a/backend/routes/api/messages.js b/backend/routes/api/messages.js index ed947e1c..318848c1 100644 --- a/backend/routes/api/messages.js +++ b/backend/routes/api/messages.js @@ -12,6 +12,9 @@ const { TWILIO_RECEIVING_NUMBER, TWILIO_SENDING_NUMBER, } = require('../../utils/constants'); +const { requireAuthentication } = require('../../middleware/authentication'); + +router.use(requireAuthentication); router.post('/sms', async (req, res) => { const phone = req?.body?.WaId; diff --git a/backend/routes/api/metadata.js b/backend/routes/api/metadata.js index 613eaf3d..a5689d98 100644 --- a/backend/routes/api/metadata.js +++ b/backend/routes/api/metadata.js @@ -1,5 +1,4 @@ const express = require('express'); -const passport = require('passport'); const router = express.Router(); const mongoose = require('mongoose'); @@ -14,13 +13,16 @@ const { updateStepsInTransaction, getReadableSteps, } = require('../../utils/stepUtils'); +const { requireAuthentication } = require('../../middleware/authentication'); +const { requireConditionalAuthentication } = require('../../middleware/conditionalAuthentication'); /** * Gets the metadata for a step. This describes the fields contained in the steps. * If a user isn't allowed to view step, it isn't returned to them. */ router.get( - '/steps', passport.authenticate('passport-local'), + '/steps', + requireConditionalAuthentication, errorWrap(async (req, res) => { const metaData = await getReadableSteps(req); @@ -38,7 +40,7 @@ router.get( */ router.post( '/steps', - requireAdmin, + requireAuthentication, requireAdmin, errorWrap(async (req, res) => { const step = req.body; const newStep = new models.Step(step); @@ -65,7 +67,7 @@ router.post( */ router.put( '/steps/', - requireAdmin, + requireAuthentication, requireAdmin, errorWrap(async (req, res) => { try { let stepData = []; @@ -99,7 +101,7 @@ router.put( */ router.delete( '/steps/:stepkey', - requireAdmin, + requireAuthentication, requireAdmin, errorWrap(async (req, res) => { const { stepkey } = req.params; const step = await models.Step.deleteOne({ key: stepkey }); diff --git a/backend/routes/api/patients.js b/backend/routes/api/patients.js index 0fe84883..c25a96b9 100644 --- a/backend/routes/api/patients.js +++ b/backend/routes/api/patients.js @@ -25,12 +25,15 @@ const { isFieldWritable, getWritableFields, } = require('../../utils/fieldUtils'); +const { requireAuthentication } = require('../../middleware/authentication'); +const { requireConditionalAuthentication } = require('../../middleware/conditionalAuthentication'); /** * Returns everything in the patients collection (basic patient info) */ router.get( '/', + requireAuthentication, errorWrap(async (req, res) => { const patients = await getDataFromModelWithPagination(req, models.Patient); await sendResponse(res, 200, '', patients); @@ -43,6 +46,7 @@ router.get( router.get( '/count', + requireAuthentication, errorWrap(async (req, res) => { const patientCount = await models.Patient.count(); return sendResponse(res, 200, 'success', patientCount); @@ -53,9 +57,9 @@ router.get( * Returns all of our data on a specific patient. Gets both the basic info * from the Patient collection and the data from each step. * */ -// todo router.get( '/:id', + requireConditionalAuthentication, errorWrap(async (req, res) => { const { id } = req.params; @@ -103,6 +107,7 @@ router.get( */ router.post( '/', + requireAuthentication, removeRequestAttributes(PATIENT_IMMUTABLE_ATTRIBUTES), errorWrap(async (req, res) => { const patient = req.body; @@ -124,9 +129,9 @@ router.post( * Updates the patients information in the Patient collection. * Note: This DOES NOT update the info for individual steps. */ -// todo router.put( '/:id', + requireAuthentication, removeRequestAttributes(PATIENT_IMMUTABLE_ATTRIBUTES), errorWrap(async (req, res) => { const { id } = req.params; @@ -151,6 +156,7 @@ router.put( */ router.get( '/:id/files/:stepKey/:fieldKey/:fileName', + requireConditionalAuthentication, errorWrap(async (req, res) => { const { id, stepKey, fieldKey, fileName, @@ -191,7 +197,8 @@ router.get( * done manually through the AWS website. */ router.delete( - '/:id/files/:stepKey/:fieldKey/:fileName', passport.authenticate('passport-local'), + '/:id/files/:stepKey/:fieldKey/:fileName', + requireConditionalAuthentication, errorWrap(async (req, res) => { const { id, stepKey, fieldKey, fileName, @@ -249,9 +256,9 @@ router.delete( * Uploads an individual file to S3 and records it in the DB. * URL format similar to GET file. */ -// todo upload/delete/etc. w/ files; did 2, unsure about the other router.post( - '/:id/files/:stepKey/:fieldKey/:fileName', passport.authenticate('passport-local'), + '/:id/files/:stepKey/:fieldKey/:fileName', + requireConditionalAuthentication, errorWrap(async (req, res) => { // TODO during refactoring: We upload file name in form data, is this even needed??? const { @@ -329,6 +336,7 @@ router.post( */ router.post( '/:id/:stepKey', + requireConditionalAuthentication, removeRequestAttributes(STEP_IMMUTABLE_ATTRIBUTES), errorWrap(async (req, res) => { const { id, stepKey } = req.params; diff --git a/backend/routes/api/roles.js b/backend/routes/api/roles.js index 350c6c7a..68b6db37 100644 --- a/backend/routes/api/roles.js +++ b/backend/routes/api/roles.js @@ -5,6 +5,9 @@ const { errorWrap } = require('../../utils'); const { models } = require('../../models/index'); const { requireAdmin } = require('../../middleware/authentication'); const { sendResponse } = require('../../utils/response'); +const { requireAuthentication } = require('../../middleware/authentication'); + +router.use(requireAuthentication); /** * Returns all roles in the DB. diff --git a/backend/routes/api/steps.js b/backend/routes/api/steps.js index 60686d78..e9ed3673 100644 --- a/backend/routes/api/steps.js +++ b/backend/routes/api/steps.js @@ -10,6 +10,9 @@ const { sendResponse, getDataFromModelWithPagination, } = require('../../utils/response'); +const { requireAuthentication } = require('../../middleware/authentication'); + +router.use(requireAuthentication); /** * Returns basic information for all patients that are active in diff --git a/backend/routes/api/users.js b/backend/routes/api/users.js index a813a426..e20a7472 100644 --- a/backend/routes/api/users.js +++ b/backend/routes/api/users.js @@ -18,12 +18,15 @@ const { } = require('../../utils/roleUtils'); const { requireAdmin } = require('../../middleware/authentication'); const { ADMIN_ID, DEFAULT_USERS_ON_GET_REQUEST } = require('../../utils/constants'); +const { requireAuthentication } = require('../../middleware/authentication'); +const { requireConditionalAuthentication } = require('../../middleware/conditionalAuthentication'); /** * Gets information about the user making this request. */ router.get( '/self', + requireConditionalAuthentication, errorWrap(async (req, res) => { const isAdmin = req?.user?.roles?.includes(ADMIN_ID) || false; @@ -40,7 +43,7 @@ router.get( */ router.get( '/', - requireAdmin, + requireAuthentication, requireAdmin, errorWrap(async (req, res) => { const { token } = req.query; let nPerPage = req.query.nPerPage ?? DEFAULT_USERS_ON_GET_REQUEST; @@ -70,7 +73,7 @@ router.get( */ router.put( '/:username/roles/:roleId', - requireAdmin, + requireAuthentication, requireAdmin, errorWrap(async (req, res) => { const { username, roleId } = req.params; @@ -101,7 +104,7 @@ router.put( */ router.delete( '/:username/roles/:roleId', - requireAdmin, + requireAuthentication, requireAdmin, errorWrap(async (req, res) => { const { username, roleId } = req.params; @@ -127,7 +130,7 @@ router.delete( */ router.put( '/:username/access/:accessLevel', - requireAdmin, + requireAuthentication, requireAdmin, errorWrap(async (req, res) => { const { username, accessLevel } = req.params; diff --git a/frontend/src/api/axios-patient-auth.js b/frontend/src/api/axios-patient-auth.js index 14743a5e..c3b4ae14 100644 --- a/frontend/src/api/axios-patient-auth.js +++ b/frontend/src/api/axios-patient-auth.js @@ -40,5 +40,5 @@ export const authenticatePatient = async (_id, token) => { export const randomMethod = async() => { const requestString = `/patient-2fa/isAuth`; - const res = instance.get(requestString, {withCredentials: true}); + instance.get(requestString, {withCredentials: true}); } \ No newline at end of file diff --git a/frontend/src/pages/PatientPortal/PatientPortal.js b/frontend/src/pages/PatientPortal/PatientPortal.js index 990c4bdc..cab4e4d8 100644 --- a/frontend/src/pages/PatientPortal/PatientPortal.js +++ b/frontend/src/pages/PatientPortal/PatientPortal.js @@ -2,6 +2,7 @@ import React, { useEffect } from 'react' import { randomMethod } from '../../api/axios-patient-auth' +// TODO: Remove code const PatientPortal = () => { useEffect(() => { randomMethod(); From cfb7c1f23578ba391f4eab6250b2d20edd6b39f1 Mon Sep 17 00:00:00 2001 From: Archna-1 Date: Sat, 1 Jan 2022 22:07:04 -0600 Subject: [PATCH 10/17] Finish auth :) --- frontend/src/App.js | 55 +------ frontend/src/AppContent.js | 99 +----------- frontend/src/Routes/allRoutes.js | 30 ++++ frontend/src/Routes/awsRoutes.js | 145 ++++++++++++++++++ frontend/src/api/axios-patient-auth.js | 7 +- .../src/pages/PatientPortal/PatientPortal.js | 4 - 6 files changed, 188 insertions(+), 152 deletions(-) create mode 100644 frontend/src/Routes/allRoutes.js create mode 100644 frontend/src/Routes/awsRoutes.js diff --git a/frontend/src/App.js b/frontend/src/App.js index b4aa713f..3f95b974 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -24,56 +24,11 @@ registerLocale(LANGUAGES.EN, enUS); registerLocale(LANGUAGES.AR, arSA); function App() { - const [authLevel, setAuthLevel] = useState(UNDEFINED_AUTH); - const [username, setUsername] = useState(''); - const [userEmail, setUserEmail] = useState(''); - - /** - * Attempts to authenticate the user and get their name/email - */ - useEffect(() => { - const getUserInfo = async () => { - const userInfo = await getCurrentUserInfo(); - setUsername(userInfo?.attributes?.name); - setUserEmail(userInfo?.attributes?.email); - }; - - updateAuthLevel(); - getUserInfo(); - }, []); - - /** - * Checks if the current user is authenticated and updates the auth - * level accordingly - */ - const updateAuthLevel = async () => { - try { - await Auth.currentAuthenticatedUser(); - setAuthLevel(AUTHENTICATED); - } catch (error) { - setAuthLevel(UNAUTHENTICATED); - } - }; - - // We get the auth level at startup, then set a listener to get notified when it changes. - setAuthListener((newAuthLevel) => setAuthLevel(newAuthLevel)); - - // If we're not sure of the user's status, say we're authenticating - if (authLevel === UNDEFINED_AUTH) return

Authenticating User

; - - // If the user is unauthenticated, show login screen - if (authLevel === UNAUTHENTICATED) return ; - - // If the user is authenticated, show the app - if (authLevel === AUTHENTICATED) - return ( - - - - ); - - // This should never get executed - return

Something went wrong

; + return ( + + + + ); } export default App; diff --git a/frontend/src/AppContent.js b/frontend/src/AppContent.js index e844989a..f338c993 100644 --- a/frontend/src/AppContent.js +++ b/frontend/src/AppContent.js @@ -1,75 +1,22 @@ -import PropTypes from 'prop-types'; -import React, { useContext, useEffect } from 'react'; +import React, { useContext } from 'react'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import { QueryParamProvider } from 'use-query-params'; -import { getSelf } from './api/api'; -import { getCurrentUserInfo } from './aws/aws-helper'; import ErrorModal from './components/ErrorModal/ErrorModal'; -import Navbar from './components/Navbar/Navbar'; -import { useErrorWrap } from './hooks/useErrorWrap'; import { useTranslations } from './hooks/useTranslations'; -import AccountManagement from './pages/AccountManagement/AccountManagment'; -import Dashboard from './pages/Dashboard/Dashboard'; -import DashboardManagement from './pages/DashboardManagement/DashboardManagement'; -import PatientDetail from './pages/PatientDetail/PatientDetail'; -import Patients from './pages/Patients/Patients'; -import Patient2FA from './pages/Patient2FALogin/Patient2FALogin'; -import PatientPortal from './pages/PatientPortal/PatientPortal'; import { Context } from './store/Store'; import { - COGNITO_ATTRIBUTES, LANGUAGES, REDUCER_ACTIONS, - ROUTES, } from './utils/constants'; +import AllRoutes from './Routes/allRoutes' -const AppContent = ({ username, userEmail }) => { - const errorWrap = useErrorWrap(); +const AppContent = () => { const [state, dispatch] = useContext(Context); const selectedLang = useTranslations()[1]; const contentClassNames = selectedLang === LANGUAGES.AR ? 'flip content' : 'content'; - /** - * Gets the user's preferred language and sets it in the store - * Also checks if the user is an admin and updates store - */ - useEffect(() => { - const setLanguage = async () => { - const userInfo = await getCurrentUserInfo(); - if (!userInfo?.attributes) return; - - const language = userInfo.attributes[COGNITO_ATTRIBUTES.LANGUAGE]; - if (isLanguageValid(language)) { - dispatch({ - type: REDUCER_ACTIONS.SET_LANGUAGE, - language, - }); - } else { - console.error(`Language is invalid: ${language}`); - } - }; - - const setAdminStatus = async () => { - const selfRes = await getSelf(); - dispatch({ - type: REDUCER_ACTIONS.SET_ADMIN_STATUS, - isAdmin: selfRes?.result?.isAdmin, - }); - }; - - setLanguage(); - errorWrap(setAdminStatus); - }, [dispatch, errorWrap]); - - /** - * Returns true if the given string is a valid language identifier - */ - const isLanguageValid = (language) => { - return Object.values(LANGUAGES).includes(language); - }; - /** * Sets store when the global error modal should be closed */ @@ -81,7 +28,7 @@ const AppContent = ({ username, userEmail }) => {
- + {/* Global error popup */} { {/* Routes */}
- - - - - - - - - - - - - - - - - - - - - - - + + +
@@ -129,9 +49,4 @@ const AppContent = ({ username, userEmail }) => { ); }; -AppContent.propTypes = { - username: PropTypes.string.isRequired, - userEmail: PropTypes.string.isRequired, -}; - export default AppContent; diff --git a/frontend/src/Routes/allRoutes.js b/frontend/src/Routes/allRoutes.js new file mode 100644 index 00000000..4ca17f22 --- /dev/null +++ b/frontend/src/Routes/allRoutes.js @@ -0,0 +1,30 @@ +import React from 'react' +import { Route, Switch } from 'react-router-dom'; + +import {ROUTES} from '../utils/constants'; +import Patient2FA from '../pages/Patient2FALogin/Patient2FALogin'; +import PatientPortal from '../pages/PatientPortal/PatientPortal'; + +import AWSRoutes from './awsRoutes'; + +const AllRoutes = () => { + return ( + + + + , + + + , + + + + + ) +} + +export default AllRoutes \ No newline at end of file diff --git a/frontend/src/Routes/awsRoutes.js b/frontend/src/Routes/awsRoutes.js new file mode 100644 index 00000000..f6cfbe05 --- /dev/null +++ b/frontend/src/Routes/awsRoutes.js @@ -0,0 +1,145 @@ +import React, { useContext, useEffect, useState } from 'react' +import { Route, Switch } from 'react-router-dom'; +import { Auth } from 'aws-amplify'; + +import { Context } from '../store/Store'; +import Navbar from '../components/Navbar/Navbar'; +import AccountManagement from '../pages/AccountManagement/AccountManagment'; +import Dashboard from '../pages/Dashboard/Dashboard'; +import DashboardManagement from '../pages/DashboardManagement/DashboardManagement'; +import PatientDetail from '../pages/PatientDetail/PatientDetail'; +import Patients from '../pages/Patients/Patients'; +import { REDUCER_ACTIONS, ROUTES, LANGUAGES , COGNITO_ATTRIBUTES } from '../utils/constants'; +import Login from '../pages/Login/Login'; +import { useErrorWrap } from '../hooks/useErrorWrap'; +import { getCurrentUserInfo } from '../aws/aws-helper'; +import { + UNDEFINED_AUTH, + AUTHENTICATED, + UNAUTHENTICATED, + setAuthListener, +} from '../aws/aws-auth'; +import { useTranslations } from '../hooks/useTranslations'; +import { getSelf } from '../api/api'; + +const AWSRoutes = () => { + const [authLevel, setAuthLevel] = useState(UNDEFINED_AUTH); + const [username, setUsername] = useState(''); + const [userEmail, setUserEmail] = useState(''); + const [state, dispatch] = useContext(Context); + const selectedLang = useTranslations()[1]; + const contentClassNames = + selectedLang === LANGUAGES.AR ? 'flip content' : 'content'; + const errorWrap = useErrorWrap(); + + /** + * Gets the user's preferred language and sets it in the store + * Also checks if the user is an admin and updates store + */ + useEffect(() => { + const setLanguage = async () => { + const userInfo = await getCurrentUserInfo(); + if (!userInfo?.attributes) return; + + const language = userInfo.attributes[COGNITO_ATTRIBUTES.LANGUAGE]; + if (isLanguageValid(language)) { + dispatch({ + type: REDUCER_ACTIONS.SET_LANGUAGE, + language, + }); + } else { + console.error(`Language is invalid: ${language}`); + } + }; + + const setAdminStatus = async () => { + const selfRes = await getSelf(); + dispatch({ + type: REDUCER_ACTIONS.SET_ADMIN_STATUS, + isAdmin: selfRes?.result?.isAdmin, + }); + }; + + setLanguage(); + if (authLevel === AUTHENTICATED) { + errorWrap(setAdminStatus); + } + }, [dispatch, errorWrap]); + + /** + * Returns true if the given string is a valid language identifier + */ + const isLanguageValid = (language) => { + return Object.values(LANGUAGES).includes(language); + }; + + /** + * Attempts to authenticate the user and get their name/email + */ + useEffect(() => { + const getUserInfo = async () => { + const userInfo = await getCurrentUserInfo(); + setUsername(userInfo?.attributes?.name); + setUserEmail(userInfo?.attributes?.email); + }; + + updateAuthLevel(); + getUserInfo(); + }, []); + + /** + * Checks if the current user is authenticated and updates the auth + * level accordingly + */ + const updateAuthLevel = async () => { + try { + await Auth.currentAuthenticatedUser(); + setAuthLevel(AUTHENTICATED); + } catch (error) { + setAuthLevel(UNAUTHENTICATED); + } + }; + + // We get the auth level at startup, then set a listener to get notified when it changes. + setAuthListener((newAuthLevel) => setAuthLevel(newAuthLevel)); + + // If we're not sure of the user's status, say we're authenticating + if (authLevel === UNDEFINED_AUTH) return

Authenticating User

; + + // If the user is unauthenticated, show login screen + if (authLevel === UNAUTHENTICATED) return + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default AWSRoutes \ No newline at end of file diff --git a/frontend/src/api/axios-patient-auth.js b/frontend/src/api/axios-patient-auth.js index c3b4ae14..80b1f0be 100644 --- a/frontend/src/api/axios-patient-auth.js +++ b/frontend/src/api/axios-patient-auth.js @@ -36,9 +36,4 @@ export const authenticatePatient = async (_id, token) => { }) return res.data; -}; - -export const randomMethod = async() => { - const requestString = `/patient-2fa/isAuth`; - instance.get(requestString, {withCredentials: true}); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/frontend/src/pages/PatientPortal/PatientPortal.js b/frontend/src/pages/PatientPortal/PatientPortal.js index cab4e4d8..2f19be21 100644 --- a/frontend/src/pages/PatientPortal/PatientPortal.js +++ b/frontend/src/pages/PatientPortal/PatientPortal.js @@ -1,11 +1,7 @@ import React, { useEffect } from 'react' -import { randomMethod } from '../../api/axios-patient-auth' - -// TODO: Remove code const PatientPortal = () => { useEffect(() => { - randomMethod(); }, []); return ( From 86996515649d7c1f91a15503039290a70bfe4e66 Mon Sep 17 00:00:00 2001 From: Archna-1 Date: Sun, 2 Jan 2022 00:31:38 -0600 Subject: [PATCH 11/17] Add details back to navbars --- frontend/src/Routes/awsRoutes.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/Routes/awsRoutes.js b/frontend/src/Routes/awsRoutes.js index f6cfbe05..27532197 100644 --- a/frontend/src/Routes/awsRoutes.js +++ b/frontend/src/Routes/awsRoutes.js @@ -112,30 +112,30 @@ const AWSRoutes = () => { return ( - + - + - + - + - + - + From 3c96c47781a98f89edee70f0a5775ddb79578131 Mon Sep 17 00:00:00 2001 From: Archna-1 Date: Sun, 2 Jan 2022 15:28:07 -0600 Subject: [PATCH 12/17] Redirect unauthenticated and unrecognized patients --- backend/app.js | 2 +- backend/middleware/verifyPatient.js | 4 +- backend/routes/api/authentication.js | 53 ++++++++++++++++--- backend/routes/api/patients.js | 1 - frontend/src/App.js | 12 +---- frontend/src/AppContent.js | 3 -- frontend/src/Routes/allRoutes.js | 2 +- frontend/src/api/api.js | 25 --------- frontend/src/api/axios-patient-auth.js | 27 ++++++++-- .../src/pages/PatientPortal/PatientPortal.js | 14 ++++- 10 files changed, 86 insertions(+), 57 deletions(-) diff --git a/backend/app.js b/backend/app.js index b21662e7..f0cecc42 100644 --- a/backend/app.js +++ b/backend/app.js @@ -62,7 +62,7 @@ app.get('/*', (req, res, next) => { const sess = { secret: '3DP4ME', cookie: { - domain: 'localhost', path: '/', httpOnly: true, secure: false, maxAge: 180000, + domain: 'localhost', path: '/', httpOnly: true, secure: false, maxAge: 30000, }, resave: false, saveUninitialized: false, diff --git a/backend/middleware/verifyPatient.js b/backend/middleware/verifyPatient.js index 486de1d5..4ab46af9 100644 --- a/backend/middleware/verifyPatient.js +++ b/backend/middleware/verifyPatient.js @@ -15,9 +15,9 @@ module.exports.requirePatientAuthentication = async (req, res) => { try { const user = req?.session?.passport?.user; if (!user) { - sendResponse(res, 401, ERR_AUTH_FAILED); + sendResponse(res, 401, ERR_NOT_APPROVED); } else { - sendResponse(res, 403, ERR_NOT_APPROVED); + sendResponse(res, 200); } } catch (error) { log.error(error); diff --git a/backend/routes/api/authentication.js b/backend/routes/api/authentication.js index 40f58b4a..cb75a0b2 100644 --- a/backend/routes/api/authentication.js +++ b/backend/routes/api/authentication.js @@ -22,8 +22,9 @@ require('../../middleware/patientAuthentication'); router.post('/authenticated/:patientId', passport.authenticate('passport-local'), async (req, res) => { const { patientId } = req.params; + console.log('ya'); if (!req.user) { return res.redirect(`/${patientId}`); } - + console.log('nah'); req.logIn(req.user, (err) => { if (err) { return err; } console.log(req.session); @@ -46,7 +47,6 @@ router.get( '/:patientId', errorWrap(async (req, res) => { const { patientId } = req.params; - const patient = await models.Patient.findById(patientId); if (!patient) { @@ -71,7 +71,6 @@ router.get( client.messages .create({ - // TODO: Backend translations body: `Your one time token is ${newToken.token}`, to: `${TWILIO_WHATSAPP_PREFIX}${patient.phoneNumber}`, from: TWILIO_SENDING_NUMBER, @@ -104,25 +103,63 @@ router.post( isAuthenticated = twofactor.verifyToken(patientSecret, token, TWO_FACTOR_WINDOW_MINS); } - // TODO: remove console logs and booleans in the response - console.log(res); - if (isAuthenticated) { await sendResponse( res, 200, 'Patient verified', - isAuthenticated, ); } else { await sendResponse( res, 404, 'Invalid token entered', - isAuthenticated, ); } }), ); +router.get( + '/patient-portal/:patientId', + errorWrap(async (req, res) => { + const { patientId } = req.params; + let patient; + + try { + patient = await models.Patient.findById(patientId); + } catch { + await sendResponse( + res, + 404, + 'An error occurred while checking for patient authentication', + ); + return; + } + + if (!patient) { + await sendResponse( + res, + 404, + 'Invalid patient ID', + ); + return; + } + + if (!req.user) { + await sendResponse( + res, + 404, + 'Session expired', + ); + return; + } + + await sendResponse( + res, + 200, + 'Patient verified', + ); + }), +); + module.exports = router; diff --git a/backend/routes/api/patients.js b/backend/routes/api/patients.js index 38560917..faad3613 100644 --- a/backend/routes/api/patients.js +++ b/backend/routes/api/patients.js @@ -1,6 +1,5 @@ const express = require('express'); const mongoose = require('mongoose'); -const passport = require('passport'); const router = express.Router(); const _ = require('lodash'); diff --git a/frontend/src/App.js b/frontend/src/App.js index 3f95b974..72b11905 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,20 +1,12 @@ -import React, { useEffect, useState } from 'react'; -import { Amplify, Auth } from 'aws-amplify'; +import React from 'react'; +import { Amplify } from 'aws-amplify'; import { registerLocale } from 'react-datepicker'; import { enUS, arSA } from 'date-fns/locale'; import Store from './store/Store'; import AppContent from './AppContent'; -import Login from './pages/Login/Login'; import { awsconfig } from './aws/aws-exports'; import { LANGUAGES } from './utils/constants'; -import { getCurrentUserInfo } from './aws/aws-helper'; -import { - UNDEFINED_AUTH, - AUTHENTICATED, - UNAUTHENTICATED, - setAuthListener, -} from './aws/aws-auth'; // Configure amplify Amplify.configure(awsconfig); diff --git a/frontend/src/AppContent.js b/frontend/src/AppContent.js index c5198e88..af0e41c0 100644 --- a/frontend/src/AppContent.js +++ b/frontend/src/AppContent.js @@ -1,7 +1,6 @@ import React, { useContext } from 'react'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import { QueryParamProvider } from 'use-query-params'; -import { trackPromise } from 'react-promise-tracker'; import ErrorModal from './components/ErrorModal/ErrorModal'; import { useTranslations } from './hooks/useTranslations'; @@ -32,8 +31,6 @@ const AppContent = () => { - - {/* Global error popup */} { if (!res?.data?.success) throw new Error(res?.data?.message); - return res.data; -}; - -export const send2FAPatientCode = async (_id) => { - const requestString = `/patient-2fa/${_id}`; - const res = await instance.get(requestString); - - // Need to handle case of invalid patient id - if (!res?.data?.success) throw new Error(res?.data?.message); - - return res.data; -}; - -export const authenticatePatient = async (_id, token) => { - const bodyFormData = new FormData(); - bodyFormData.append('username', _id); - bodyFormData.append('password', token); - const res = await instance({ - method: "post", - url: `/patient-2fa/authenticated/${_id}`, - data: bodyFormData, - headers: { "Content-Type": "multipart/form-data" }, - // credentials: 'include' - }) - return res.data; }; \ No newline at end of file diff --git a/frontend/src/api/axios-patient-auth.js b/frontend/src/api/axios-patient-auth.js index 80b1f0be..8b42724a 100644 --- a/frontend/src/api/axios-patient-auth.js +++ b/frontend/src/api/axios-patient-auth.js @@ -16,10 +16,13 @@ const instance = axios.create({ export const send2FAPatientCode = async (_id) => { const requestString = `/patient-2fa/${_id}`; - const res = await instance.get(requestString); - // Need to handle case of invalid patient id - if (!res?.data?.success) throw new Error(res?.data?.message); + const res = await instance.get(requestString); + + // Previously, without the redirect, the site would crash when an invalid patient id was entered + if (!res?.data?.success) { + window.location = `/patient-2fa/${_id}`; + } return res.data; }; @@ -36,4 +39,20 @@ export const authenticatePatient = async (_id, token) => { }) return res.data; -}; \ No newline at end of file +}; + +export const redirectAndAuthenticate = async (_id) => { + const res = await instance({ + method: "get", + url: `/patient-2fa/patient-portal/${_id}`, + data: {_id}, + headers: { "Content-Type": "multipart/form-data" }, + }); + + if (!res?.data?.success) { + window.location = `/patient-2fa/${_id}`; + return false; + } + + return true; +}; diff --git a/frontend/src/pages/PatientPortal/PatientPortal.js b/frontend/src/pages/PatientPortal/PatientPortal.js index 2f19be21..4d8bb524 100644 --- a/frontend/src/pages/PatientPortal/PatientPortal.js +++ b/frontend/src/pages/PatientPortal/PatientPortal.js @@ -1,12 +1,22 @@ -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' +import { useParams } from 'react-router-dom'; + +import { redirectAndAuthenticate } from '../../api/axios-patient-auth'; const PatientPortal = () => { + const params = useParams(); + const { patientId } = params; + const [shouldRender, setShouldRender] = useState(); + useEffect(() => { + setShouldRender(redirectAndAuthenticate(patientId)); }, []); return (
- Hi + { shouldRender &&
+ Hi +
}
) } From bfffd9ebae0ad6377ae0f69d35527e86fa72a6d5 Mon Sep 17 00:00:00 2001 From: Archna-1 Date: Sun, 2 Jan 2022 16:08:28 -0600 Subject: [PATCH 13/17] Check authentication status before rendering --- backend/routes/api/authentication.js | 4 ---- frontend/src/pages/PatientPortal/PatientPortal.js | 14 +++++++++++--- frontend/src/translations.json | 6 ++++++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/backend/routes/api/authentication.js b/backend/routes/api/authentication.js index cb75a0b2..382112a1 100644 --- a/backend/routes/api/authentication.js +++ b/backend/routes/api/authentication.js @@ -22,13 +22,9 @@ require('../../middleware/patientAuthentication'); router.post('/authenticated/:patientId', passport.authenticate('passport-local'), async (req, res) => { const { patientId } = req.params; - console.log('ya'); if (!req.user) { return res.redirect(`/${patientId}`); } - console.log('nah'); req.logIn(req.user, (err) => { if (err) { return err; } - console.log(req.session); - // do i need this line? req.session.save(); return sendResponse( res, diff --git a/frontend/src/pages/PatientPortal/PatientPortal.js b/frontend/src/pages/PatientPortal/PatientPortal.js index 4d8bb524..b8bfdad3 100644 --- a/frontend/src/pages/PatientPortal/PatientPortal.js +++ b/frontend/src/pages/PatientPortal/PatientPortal.js @@ -1,16 +1,24 @@ import React, { useEffect, useState } from 'react' import { useParams } from 'react-router-dom'; +import { useTranslations } from '../../hooks/useTranslations'; import { redirectAndAuthenticate } from '../../api/axios-patient-auth'; const PatientPortal = () => { const params = useParams(); const { patientId } = params; - const [shouldRender, setShouldRender] = useState(); + const [shouldRender, setShouldRender] = useState(false); + const translations = useTranslations()[0]; - useEffect(() => { - setShouldRender(redirectAndAuthenticate(patientId)); + useEffect(async () => { + const isAuth = await redirectAndAuthenticate(patientId); + + setShouldRender(isAuth); }, []); + + if (!shouldRender) { + return
{translations.patientPortal.authenticating}
+ } return (
diff --git a/frontend/src/translations.json b/frontend/src/translations.json index ecc03e95..ff2bd00c 100644 --- a/frontend/src/translations.json +++ b/frontend/src/translations.json @@ -9,6 +9,9 @@ "verify": "Verify", "resendCode": "Send code again" }, + "patientPortal": { + "authenticating": "Checking authentication status" + }, "errors": { "noMetadata": "You aren't allowed to view any steps. Contact an administrator to give you the proper roles." }, @@ -265,6 +268,9 @@ "verify": "التحقق", "resendCode": "ارسل الرمز مجددا" }, + "patientPortal": { + "authenticating": "التحقق من حالة المصادقة" + }, "errors": { "noMetadata": "لا يُسمح لك بمشاهدة أي خطوات. اتصل بالمسؤول لمنحك الأدوار المناسبة." }, From 994d4d31348c918f368f1a88a4a730d7a35d4050 Mon Sep 17 00:00:00 2001 From: Archna-1 Date: Sun, 2 Jan 2022 16:16:19 -0600 Subject: [PATCH 14/17] Fix linter errors --- backend/routes/api/authentication.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/routes/api/authentication.js b/backend/routes/api/authentication.js index 382112a1..ba5d9b8b 100644 --- a/backend/routes/api/authentication.js +++ b/backend/routes/api/authentication.js @@ -1,10 +1,8 @@ +const accountSid = process.env.ACCOUNT_SID; +const authToken = process.env.TWILIO_AUTH_TOKEN; const express = require('express'); const twofactor = require('node-2fa'); const passport = require('passport'); - -const accountSid = process.env.ACCOUNT_SID; -const authToken = process.env.TWILIO_AUTH_TOKEN; - const client = require('twilio')(accountSid, authToken); const { @@ -32,6 +30,8 @@ router.post('/authenticated/:patientId', passport.authenticate('passport-local') 'Successfully authenticated patient', ); }); + + return req.user; }); /** From 1a82aa8c779fb07174f25e184d9388d1f28cce24 Mon Sep 17 00:00:00 2001 From: Archna-1 Date: Sun, 2 Jan 2022 16:37:41 -0600 Subject: [PATCH 15/17] Clean up pathing and change session timeout --- backend/app.js | 5 ++++- frontend/src/pages/Patient2FALogin/Patient2FALogin.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/app.js b/backend/app.js index f0cecc42..06a18ae9 100644 --- a/backend/app.js +++ b/backend/app.js @@ -58,11 +58,14 @@ app.get('/*', (req, res, next) => { * ID cookie, while all the elements will be considered when verifying the signature in requests. * The secret itself should be not easily parsed by a human and would best be a random set of * characters. + * + * Patients will be logged in a session for 10 minutes, unless they refresh to extend this period. + * maxAge can also be set to null, which keeps a user logged in until the BROWSER is closed. */ const sess = { secret: '3DP4ME', cookie: { - domain: 'localhost', path: '/', httpOnly: true, secure: false, maxAge: 30000, + domain: 'localhost', path: '/', httpOnly: true, secure: false, maxAge: 60000 * 10, }, resave: false, saveUninitialized: false, diff --git a/frontend/src/pages/Patient2FALogin/Patient2FALogin.js b/frontend/src/pages/Patient2FALogin/Patient2FALogin.js index 5191a3f1..564e9910 100644 --- a/frontend/src/pages/Patient2FALogin/Patient2FALogin.js +++ b/frontend/src/pages/Patient2FALogin/Patient2FALogin.js @@ -42,7 +42,7 @@ const Patient2FALogin = () => { const res = await authenticatePatient(patientId, token); if (res.success) { - window.location = `${ window.location.protocol }//${ window.location.hostname }:3000${ ROUTES.PATIENT_PORTAL }/${patientId}`; + window.location.href = `${ ROUTES.PATIENT_PORTAL }/${patientId}`; } } From 20aa049a031ce461e90bba886bed5b965bd3aca0 Mon Sep 17 00:00:00 2001 From: Archna-1 Date: Thu, 6 Jan 2022 01:05:44 -0600 Subject: [PATCH 16/17] Run yarn format --- backend/app.js | 4 +-- backend/routes/api/authentication.js | 12 ++++--- frontend/src/AppContent.js | 13 +++----- frontend/src/Routes/allRoutes.js | 26 +++++++--------- frontend/src/Routes/awsRoutes.js | 31 ++++++++++--------- frontend/src/api/api.js | 2 +- frontend/src/api/axios-patient-auth.js | 16 +++++----- .../pages/Patient2FALogin/Patient2FALogin.js | 25 ++++++++++----- .../src/pages/PatientPortal/PatientPortal.js | 22 +++++-------- frontend/src/utils/constants.js | 2 +- 10 files changed, 80 insertions(+), 73 deletions(-) diff --git a/backend/app.js b/backend/app.js index 06a18ae9..6d1d71ca 100644 --- a/backend/app.js +++ b/backend/app.js @@ -59,13 +59,13 @@ app.get('/*', (req, res, next) => { * The secret itself should be not easily parsed by a human and would best be a random set of * characters. * - * Patients will be logged in a session for 10 minutes, unless they refresh to extend this period. + * Patients will be logged in a session for 5 minutes, unless they refresh to extend this period. * maxAge can also be set to null, which keeps a user logged in until the BROWSER is closed. */ const sess = { secret: '3DP4ME', cookie: { - domain: 'localhost', path: '/', httpOnly: true, secure: false, maxAge: 60000 * 10, + domain: 'localhost', path: '/', httpOnly: true, secure: false, maxAge: 150000, }, resave: false, saveUninitialized: false, diff --git a/backend/routes/api/authentication.js b/backend/routes/api/authentication.js index ba5d9b8b..cb75a0b2 100644 --- a/backend/routes/api/authentication.js +++ b/backend/routes/api/authentication.js @@ -1,8 +1,10 @@ -const accountSid = process.env.ACCOUNT_SID; -const authToken = process.env.TWILIO_AUTH_TOKEN; const express = require('express'); const twofactor = require('node-2fa'); const passport = require('passport'); + +const accountSid = process.env.ACCOUNT_SID; +const authToken = process.env.TWILIO_AUTH_TOKEN; + const client = require('twilio')(accountSid, authToken); const { @@ -20,9 +22,13 @@ require('../../middleware/patientAuthentication'); router.post('/authenticated/:patientId', passport.authenticate('passport-local'), async (req, res) => { const { patientId } = req.params; + console.log('ya'); if (!req.user) { return res.redirect(`/${patientId}`); } + console.log('nah'); req.logIn(req.user, (err) => { if (err) { return err; } + console.log(req.session); + // do i need this line? req.session.save(); return sendResponse( res, @@ -30,8 +36,6 @@ router.post('/authenticated/:patientId', passport.authenticate('passport-local') 'Successfully authenticated patient', ); }); - - return req.user; }); /** diff --git a/frontend/src/AppContent.js b/frontend/src/AppContent.js index af0e41c0..5a0552fe 100644 --- a/frontend/src/AppContent.js +++ b/frontend/src/AppContent.js @@ -5,11 +5,8 @@ import { QueryParamProvider } from 'use-query-params'; import ErrorModal from './components/ErrorModal/ErrorModal'; import { useTranslations } from './hooks/useTranslations'; import { Context } from './store/Store'; -import { - LANGUAGES, - REDUCER_ACTIONS, -} from './utils/constants'; -import AllRoutes from './Routes/allRoutes' +import { LANGUAGES, REDUCER_ACTIONS } from './utils/constants'; +import AllRoutes from './Routes/AllRoutes'; import LoadingIndicator from './components/LoadingIndicator/LoadingIndicator'; const AppContent = () => { @@ -40,9 +37,9 @@ const AppContent = () => { {/* Routes */}
- - - + + +
diff --git a/frontend/src/Routes/allRoutes.js b/frontend/src/Routes/allRoutes.js index 54f4703d..fe469f00 100644 --- a/frontend/src/Routes/allRoutes.js +++ b/frontend/src/Routes/allRoutes.js @@ -1,30 +1,28 @@ -import React from 'react' +import React from 'react'; import { Route, Switch } from 'react-router-dom'; import { ROUTES } from '../utils/constants'; import Patient2FA from '../pages/Patient2FALogin/Patient2FALogin'; import PatientPortal from '../pages/PatientPortal/PatientPortal'; -import AWSRoutes from './awsRoutes'; +import AWSRoutes from './AWSRoutes'; const AllRoutes = () => { return ( - + - , - - - , + + , + + + + , - ) -} + ); +}; -export default AllRoutes \ No newline at end of file +export default AllRoutes; diff --git a/frontend/src/Routes/awsRoutes.js b/frontend/src/Routes/awsRoutes.js index 27532197..2c122c18 100644 --- a/frontend/src/Routes/awsRoutes.js +++ b/frontend/src/Routes/awsRoutes.js @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from 'react' +import React, { useContext, useEffect, useState } from 'react'; import { Route, Switch } from 'react-router-dom'; import { Auth } from 'aws-amplify'; @@ -9,7 +9,12 @@ import Dashboard from '../pages/Dashboard/Dashboard'; import DashboardManagement from '../pages/DashboardManagement/DashboardManagement'; import PatientDetail from '../pages/PatientDetail/PatientDetail'; import Patients from '../pages/Patients/Patients'; -import { REDUCER_ACTIONS, ROUTES, LANGUAGES , COGNITO_ATTRIBUTES } from '../utils/constants'; +import { + REDUCER_ACTIONS, + ROUTES, + LANGUAGES, + COGNITO_ATTRIBUTES, +} from '../utils/constants'; import Login from '../pages/Login/Login'; import { useErrorWrap } from '../hooks/useErrorWrap'; import { getCurrentUserInfo } from '../aws/aws-helper'; @@ -19,7 +24,6 @@ import { UNAUTHENTICATED, setAuthListener, } from '../aws/aws-auth'; -import { useTranslations } from '../hooks/useTranslations'; import { getSelf } from '../api/api'; const AWSRoutes = () => { @@ -27,9 +31,6 @@ const AWSRoutes = () => { const [username, setUsername] = useState(''); const [userEmail, setUserEmail] = useState(''); const [state, dispatch] = useContext(Context); - const selectedLang = useTranslations()[1]; - const contentClassNames = - selectedLang === LANGUAGES.AR ? 'flip content' : 'content'; const errorWrap = useErrorWrap(); /** @@ -107,7 +108,12 @@ const AWSRoutes = () => { if (authLevel === UNDEFINED_AUTH) return

Authenticating User

; // If the user is unauthenticated, show login screen - if (authLevel === UNAUTHENTICATED) return + if (authLevel === UNAUTHENTICATED) + return ( + + + + ); return ( @@ -127,10 +133,7 @@ const AWSRoutes = () => { - + @@ -139,7 +142,7 @@ const AWSRoutes = () => { - ) -} + ); +}; -export default AWSRoutes \ No newline at end of file +export default AWSRoutes; diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js index 3256c35f..0660b137 100644 --- a/frontend/src/api/api.js +++ b/frontend/src/api/api.js @@ -268,4 +268,4 @@ export const getSelf = async () => { if (!res?.data?.success) throw new Error(res?.data?.message); return res.data; -}; \ No newline at end of file +}; diff --git a/frontend/src/api/axios-patient-auth.js b/frontend/src/api/axios-patient-auth.js index 8b42724a..9d5df213 100644 --- a/frontend/src/api/axios-patient-auth.js +++ b/frontend/src/api/axios-patient-auth.js @@ -18,12 +18,12 @@ export const send2FAPatientCode = async (_id) => { const requestString = `/patient-2fa/${_id}`; const res = await instance.get(requestString); - + // Previously, without the redirect, the site would crash when an invalid patient id was entered if (!res?.data?.success) { window.location = `/patient-2fa/${_id}`; } - + return res.data; }; @@ -32,21 +32,21 @@ export const authenticatePatient = async (_id, token) => { bodyFormData.append('username', _id); bodyFormData.append('password', token); const res = await instance({ - method: "post", + method: 'post', url: `/patient-2fa/authenticated/${_id}`, data: bodyFormData, - headers: { "Content-Type": "multipart/form-data" }, - }) + headers: { 'Content-Type': 'multipart/form-data' }, + }); return res.data; }; export const redirectAndAuthenticate = async (_id) => { const res = await instance({ - method: "get", + method: 'get', url: `/patient-2fa/patient-portal/${_id}`, - data: {_id}, - headers: { "Content-Type": "multipart/form-data" }, + data: { _id }, + headers: { 'Content-Type': 'multipart/form-data' }, }); if (!res?.data?.success) { diff --git a/frontend/src/pages/Patient2FALogin/Patient2FALogin.js b/frontend/src/pages/Patient2FALogin/Patient2FALogin.js index 564e9910..968e787c 100644 --- a/frontend/src/pages/Patient2FALogin/Patient2FALogin.js +++ b/frontend/src/pages/Patient2FALogin/Patient2FALogin.js @@ -2,7 +2,10 @@ import React, { useState } from 'react'; import ReactCodeInput from 'react-code-input'; import { useParams } from 'react-router-dom'; -import { authenticatePatient, send2FAPatientCode } from '../../api/axios-patient-auth'; +import { + authenticatePatient, + send2FAPatientCode, +} from '../../api/axios-patient-auth'; import Logo from '../../assets/3dp4me_logo.png'; import { useTranslations } from '../../hooks/useTranslations'; import { ROUTES } from '../../utils/constants'; @@ -38,13 +41,13 @@ const Patient2FALogin = () => { setIsTokenSent(true); }; - const checkIsAuthenticated = async() => { + const checkIsAuthenticated = async () => { const res = await authenticatePatient(patientId, token); - + if (res.success) { - window.location.href = `${ ROUTES.PATIENT_PORTAL }/${patientId}`; + window.location.href = `${ROUTES.PATIENT_PORTAL}/${patientId}`; } - } + }; const displayAuthPage = () => { if (!isTokenSent) { @@ -80,8 +83,16 @@ const Patient2FALogin = () => {
- setToken(tokenInput)}/> -
{ const params = useParams(); const { patientId } = params; - const [shouldRender, setShouldRender] = useState(false); + const [shouldRender, setShouldRender] = useState(); const translations = useTranslations()[0]; useEffect(async () => { const isAuth = await redirectAndAuthenticate(patientId); - + setShouldRender(isAuth); }, []); - + if (!shouldRender) { - return
{translations.patientPortal.authenticating}
+ return
{translations.patientPortal.authenticating}
; } - return ( -
- { shouldRender &&
- Hi -
} -
- ) -} + return
{shouldRender &&
Hi
}
; +}; -export default PatientPortal \ No newline at end of file +export default PatientPortal; diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 6253b707..599f5a19 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -47,7 +47,7 @@ export const ROUTES = { DASHBOARD_MANAGEMENT: '/dashboard-management', PATIENT_DETAIL: '/patient-info', PATIENT_2FA: '/patient-2fa', - PATIENT_PORTAL: '/patient-portal' + PATIENT_PORTAL: '/patient-portal', }; /** From 35d33e43ef6c0b8647e7e159734fc75eb4d1ce17 Mon Sep 17 00:00:00 2001 From: Archna-1 Date: Thu, 6 Jan 2022 01:17:33 -0600 Subject: [PATCH 17/17] Rename routes --- frontend/src/Routes/AWSRoutes.js | 148 +++++++++++++++++++++++++++++++ frontend/src/Routes/AllRoutes.js | 28 ++++++ 2 files changed, 176 insertions(+) create mode 100644 frontend/src/Routes/AWSRoutes.js create mode 100644 frontend/src/Routes/AllRoutes.js diff --git a/frontend/src/Routes/AWSRoutes.js b/frontend/src/Routes/AWSRoutes.js new file mode 100644 index 00000000..2c122c18 --- /dev/null +++ b/frontend/src/Routes/AWSRoutes.js @@ -0,0 +1,148 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { Auth } from 'aws-amplify'; + +import { Context } from '../store/Store'; +import Navbar from '../components/Navbar/Navbar'; +import AccountManagement from '../pages/AccountManagement/AccountManagment'; +import Dashboard from '../pages/Dashboard/Dashboard'; +import DashboardManagement from '../pages/DashboardManagement/DashboardManagement'; +import PatientDetail from '../pages/PatientDetail/PatientDetail'; +import Patients from '../pages/Patients/Patients'; +import { + REDUCER_ACTIONS, + ROUTES, + LANGUAGES, + COGNITO_ATTRIBUTES, +} from '../utils/constants'; +import Login from '../pages/Login/Login'; +import { useErrorWrap } from '../hooks/useErrorWrap'; +import { getCurrentUserInfo } from '../aws/aws-helper'; +import { + UNDEFINED_AUTH, + AUTHENTICATED, + UNAUTHENTICATED, + setAuthListener, +} from '../aws/aws-auth'; +import { getSelf } from '../api/api'; + +const AWSRoutes = () => { + const [authLevel, setAuthLevel] = useState(UNDEFINED_AUTH); + const [username, setUsername] = useState(''); + const [userEmail, setUserEmail] = useState(''); + const [state, dispatch] = useContext(Context); + const errorWrap = useErrorWrap(); + + /** + * Gets the user's preferred language and sets it in the store + * Also checks if the user is an admin and updates store + */ + useEffect(() => { + const setLanguage = async () => { + const userInfo = await getCurrentUserInfo(); + if (!userInfo?.attributes) return; + + const language = userInfo.attributes[COGNITO_ATTRIBUTES.LANGUAGE]; + if (isLanguageValid(language)) { + dispatch({ + type: REDUCER_ACTIONS.SET_LANGUAGE, + language, + }); + } else { + console.error(`Language is invalid: ${language}`); + } + }; + + const setAdminStatus = async () => { + const selfRes = await getSelf(); + dispatch({ + type: REDUCER_ACTIONS.SET_ADMIN_STATUS, + isAdmin: selfRes?.result?.isAdmin, + }); + }; + + setLanguage(); + if (authLevel === AUTHENTICATED) { + errorWrap(setAdminStatus); + } + }, [dispatch, errorWrap]); + + /** + * Returns true if the given string is a valid language identifier + */ + const isLanguageValid = (language) => { + return Object.values(LANGUAGES).includes(language); + }; + + /** + * Attempts to authenticate the user and get their name/email + */ + useEffect(() => { + const getUserInfo = async () => { + const userInfo = await getCurrentUserInfo(); + setUsername(userInfo?.attributes?.name); + setUserEmail(userInfo?.attributes?.email); + }; + + updateAuthLevel(); + getUserInfo(); + }, []); + + /** + * Checks if the current user is authenticated and updates the auth + * level accordingly + */ + const updateAuthLevel = async () => { + try { + await Auth.currentAuthenticatedUser(); + setAuthLevel(AUTHENTICATED); + } catch (error) { + setAuthLevel(UNAUTHENTICATED); + } + }; + + // We get the auth level at startup, then set a listener to get notified when it changes. + setAuthListener((newAuthLevel) => setAuthLevel(newAuthLevel)); + + // If we're not sure of the user's status, say we're authenticating + if (authLevel === UNDEFINED_AUTH) return

Authenticating User

; + + // If the user is unauthenticated, show login screen + if (authLevel === UNAUTHENTICATED) + return ( + + + + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default AWSRoutes; diff --git a/frontend/src/Routes/AllRoutes.js b/frontend/src/Routes/AllRoutes.js new file mode 100644 index 00000000..fe469f00 --- /dev/null +++ b/frontend/src/Routes/AllRoutes.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { ROUTES } from '../utils/constants'; +import Patient2FA from '../pages/Patient2FALogin/Patient2FALogin'; +import PatientPortal from '../pages/PatientPortal/PatientPortal'; + +import AWSRoutes from './AWSRoutes'; + +const AllRoutes = () => { + return ( + + + + + , + + + + , + + + + + ); +}; + +export default AllRoutes;