-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Patient Authentication Backend #547
base: master
Are you sure you want to change the base?
Changes from all commits
70cb274
b405c26
b413198
0bf7807
d25bd54
8977d5b
c337f07
1d58410
c23b05a
21539dc
cfb7c1f
8699651
94eefa9
3c96c47
bfffd9e
994d4d3
1a82aa8
20aa049
35d33e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,14 +3,17 @@ 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 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, | ||
|
@@ -24,7 +27,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, | ||
|
@@ -48,7 +51,40 @@ 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. | ||
* 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. | ||
* | ||
* 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: 150000, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same issue with |
||
}, | ||
resave: false, | ||
saveUninitialized: false, | ||
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()); | ||
|
||
app.use(logRequest); | ||
app.use(require('./routes')); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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_NOT_APPROVED); | ||
} else { | ||
sendResponse(res, 200); | ||
} | ||
} catch (error) { | ||
log.error(error); | ||
sendResponse(res, 401, ERR_AUTH_FAILED); | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,52 @@ | ||
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 { | ||
TWILIO_SENDING_NUMBER, | ||
TWILIO_WHATSAPP_PREFIX, | ||
} = require('../../utils/constants'); | ||
const { errorWrap } = require('../../utils'); | ||
const { models } = require('../../models'); | ||
const { sendResponse } = require('../../utils/response'); | ||
const { TWO_FACTOR_WINDOW_MINS } = require('../../utils/constants'); | ||
|
||
const router = express.Router(); | ||
|
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Many console.logs that could be removed |
||
// do i need this line? | ||
req.session.save(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably don't need this line anymore. I'd try without it |
||
return 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 | ||
* Send the patient token through Twilio to Whatsapp | ||
*/ | ||
router.get( | ||
'/:patientId', | ||
errorWrap(async (req, res) => { | ||
const { patientId } = req.params; | ||
|
||
const patient = await models.Patient.findById(patientId); | ||
|
||
if (!patient) { | ||
|
@@ -39,12 +69,14 @@ router.get( | |
|
||
const newToken = twofactor.generateToken(patient.secret); | ||
|
||
await sendResponse( | ||
res, | ||
200, | ||
'New authentication key generated', | ||
newToken, | ||
); | ||
client.messages | ||
.create({ | ||
body: `Your one time token is ${newToken.token}`, | ||
to: `${TWILIO_WHATSAPP_PREFIX}${patient.phoneNumber}`, | ||
from: TWILIO_SENDING_NUMBER, | ||
}) | ||
.then((message) => console.log(message.sid)) | ||
.catch((err) => console.log(err)); | ||
}), | ||
); | ||
|
||
|
@@ -76,17 +108,58 @@ router.post( | |
res, | ||
200, | ||
'Patient verified', | ||
isAuthenticated, | ||
); | ||
} else { | ||
await sendResponse( | ||
res, | ||
404, | ||
'Invalid token entered', | ||
isAuthenticated, | ||
); | ||
} | ||
}), | ||
); | ||
|
||
router.get( | ||
'/patient-portal/:patientId', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already have a route for getting a patient by id. It seems like this route would be redundant then. The only difference would be the authentication... I.e. If a volunteer is authenticated, they can view any patient. If a patient is authenticated, they can only view their own data. So I would just reuse the existing patient endpoint but change the auth middleware on that endpoint so that it allows either type of user to access it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But now that I'm looking at this more, it seems like you were just using this endpoint for testing right? |
||
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,9 @@ const { | |
TWILIO_RECEIVING_NUMBER, | ||
TWILIO_SENDING_NUMBER, | ||
} = require('../../utils/constants'); | ||
const { requireAuthentication } = require('../../middleware/authentication'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be honest, we can delete this file. It doesn't contain any routes that we're using. |
||
|
||
router.use(requireAuthentication); | ||
|
||
router.post('/sms', async (req, res) => { | ||
const phone = req?.body?.WaId; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need the
origin
property for this to work? If so, then we need to make origin part of the env vars so that the production environment doesn't have localhost as origin