diff --git a/README.md b/README.md index cd68f917..c05907cc 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ | Method | URL | Description | | -------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | [GET] | /profile/ | Returns an array of all existing profiles. | -| [GET] | /profile/:okta_id/ | Returns the profile object with the specified `okta_id`. | -| [GET] | /profiles/users/:profile_id (BUG: /profiles/users route does not exist; app.js only connects to /profiles/user) | Returns an array filled with event objects that contains information based on profile_id and role_id. | -| [GET] | /profile/role/:role_id (BUG: does not return any data) | Returns an array filled with event objects that contain information based on role_id for all profiles of a role_id type. | +| [GET] | /profile/:profile_id/ (BUG: does not complete request) | Returns the profile object with the specified `profile_id`. | +| [GET] | /profiles/users/:profile_id | Returns an array filled with event objects that contains information based on profile_id and role_id. | +| [GET] | /profile/role/:role_id | Returns an array filled with event objects that contain information based on role_id for all profiles of a role_id type. | | [POST] | /profile/ | Requires a name, password, and email. Registers a new user. | | [PUT] | /profile/ | Returns an event object with the specified `okta`. Updates specific profile. | | [DELETE] | /profile/:okta_id/ | Returns an event object with the specified `okta`. Deletes specific profile. | diff --git a/api/app.js b/api/app.js index 2ce94ebc..66c4504c 100644 --- a/api/app.js +++ b/api/app.js @@ -64,7 +64,7 @@ app.use('/', indexRouter); app.use(['/profile', '/profiles'], profileRouter); app.use(['/parent', '/parents'], parentRouter); app.use(['/instructor', '/instructors'], instructorRouter); -app.use(['/user'], userRouter); +app.use(['/user', '/users'], userRouter); app.use(['/conversation', '/conversations'], inboxRouter); app.use( ['/program-type', '/program-types', '/program', '/programs'], diff --git a/api/courses/coursesRouter.js b/api/courses/coursesRouter.js index a66c8348..46c22712 100644 --- a/api/courses/coursesRouter.js +++ b/api/courses/coursesRouter.js @@ -402,9 +402,9 @@ router.post( router.put( '/:course_id', + authRequired, validateCourseObject, checkInstructorExists, - authRequired, checkCourseExists, (req, res, next) => { const course_id = parseInt(req.params.course_id); diff --git a/api/instructor/instructorRouter.js b/api/instructor/instructorRouter.js index ae07d1d4..85eb3451 100644 --- a/api/instructor/instructorRouter.js +++ b/api/instructor/instructorRouter.js @@ -2,7 +2,7 @@ const express = require('express'); const authRequired = require('../middleware/authRequired'); const Instructors = require('./instructorModel'); const router = express.Router(); -const Profiles = require('../profile/profileModel'); +// const Profiles = require('../profile/profileModel'); const { getInstructorId, checkInstructorExist, @@ -11,37 +11,36 @@ const { /* Create a new Instructor profile */ router.post('/register', (req, res) => { if (!req.body) return res.sendStatus(400); - const newInstructor = { - profile: { - firstName: req.body.firstName, - lastName: req.body.lastName, - email: req.body.email, - login: req.body.email, - }, - }; // now sending to Okta - - // TO-DO: Implement Auth0 -> create new user - // oktaClient - // .createUser(newInstructor) - // .then((instructor) => { - // return Profiles.create({ - // email: instructor.profile.email, - // name: instructor.profile.firstName + ' ' + instructor.profile.lastName, - // okta_id: instructor.id, - // role_id: 3, - // pending: true, - // }).then(() => instructor); - // }) - // .then((instructor) => { - // res.status(201); - // res.send(instructor); - // }) - // .catch((err) => { - // res.status(400); - // res.send(err); - // }); + // const newInstructor = { + // profile: { + // firstName: req.body.firstName, + // lastName: req.body.lastName, + // email: req.body.email, + // login: req.body.email, + // }, }); +// TO-DO: Implement Auth0 -> create new user +// oktaClient +// .createUser(newInstructor) +// .then((instructor) => { +// return Profiles.create({ +// email: instructor.profile.email, +// name: instructor.profile.firstName + ' ' + instructor.profile.lastName, +// okta_id: instructor.id, +// role_id: 3, +// pending: true, +// }).then(() => instructor); +// }) +// .then((instructor) => { +// res.status(201); +// res.send(instructor); +// }) +// .catch((err) => { +// res.status(400); +// res.send(err); +// }); + router.get('/courses', authRequired, getInstructorId, (req, res, next) => { Instructors.findInstructorCourses(req.instructor_id) .then((courses) => { diff --git a/api/middleware/auth0Middleware.js b/api/middleware/auth0Middleware.js new file mode 100644 index 00000000..b7002c72 --- /dev/null +++ b/api/middleware/auth0Middleware.js @@ -0,0 +1,43 @@ +const { expressjwt: jwt } = require('express-jwt'); +const jwksRsa = require('jwks-rsa'); +const createError = require('http-errors'); +const config = require('../../config/auth0'); + +// writing the config for expressJwt method to be invoked with product environment variables +const verifyJwt = jwt({ + secret: jwksRsa.expressJwtSecret({ + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: config.jwksUri, + }), + audience: config.audience, + issuer: config.issuer, + algorithms: ['RS256'], + requestProperty: 'auth0User', +}).unless({ path: ['/'] }); +//************ */ +//in the above line add all public endpoints (routes with no need for auth) as a string inside the path array [] <= +// **** IMPORTANT note: if your public endpoint has descendent end points (ex.: ./yourRouterEndPoint/:user_id) you need ot use regex as in the following example: +// ({ path: ['/', /^\/\/.*/] }); => example: ({ path: ['/', /^\/application\/.*/, /^\/roles\/.*/] }); +//************ */ + +//exporting the verifyjwt method as a middleware to be used in app.js server file against all routes except what is in the exception array in above method +const authRequired = (req, res, next) => { + verifyJwt(req, res, next); +}; + +//method to grab the current authenticated user from the req.auth0User and save it to req.body.user to be used globally in the server +const authProfile = async (req, res, next) => { + try { + const profile = await req.auth0User; + if (profile) { + req.body.profile = profile; + } + next(); + } catch (err) { + next(createError(401, err.message)); + } +}; + +module.exports = { authRequired, authProfile }; diff --git a/api/middleware/authRequired.js b/api/middleware/authRequired.js index 264fd399..07732b74 100644 --- a/api/middleware/authRequired.js +++ b/api/middleware/authRequired.js @@ -1,23 +1,32 @@ -// const createError = require('http-errors'); -// const Profiles = require('../profile/profileModel'); +const Profiles = require('../profile/profileModel'); /** - * TO-DO: Implement Auth0 - * If token verification fails - * catch(err) => next(createError(401, err.message)); - */ - -/** - * A simple middleware that asserts valid Okta idToken and sends 401 responses + * A simple middleware that asserts valid Auth0 idToken and sends 401 responses * if the token is not present or fails validation. If the token is valid its * contents are attached to req.profile */ const authRequired = async (req, res, next) => { - // TO-DO: Implement Auth0 -> assure authorized through Header Token + try { + // Verify that the token is valid + const profile = Profiles.findOrCreateProfile; + console.log("I'm in authRequired"); + + if (profile) { + req.profile = profile; + } else { + next({ + status: 401, + message: 'Unable to process idToken', + }); + } - // const match = authHeader.match(/Bearer (.+)/); - // if (!match) throw new Error('Missing idToken'); - // req.profile = authorizedProfile; - next(); + // Proceed with request if token is valid + next(); + } catch (err) { + next({ + status: err.status || 500, + message: err.message || 'Internal Server Error', + }); + } }; module.exports = authRequired; diff --git a/api/middleware/getUserfromAuth0.js b/api/middleware/getUserfromAuth0.js new file mode 100644 index 00000000..ed41bdbb --- /dev/null +++ b/api/middleware/getUserfromAuth0.js @@ -0,0 +1,41 @@ +const axios = require('axios'); +const config = require('../../config/auth0'); + +const getUserAuth0 = async (req, res, next) => { + try { + // Check if there's a token in the auth header + const authHeader = req.headers.authorization || ''; + const token = authHeader.match(/Bearer (.+)/); + if (!token) { + next({ + status: 401, + message: 'Missing Token', + }); + } + // Check against Auth0 if token is valid and grab user data + await axios + .get(`${config.issuer}/userinfo`, { + headers: { + authorization: authHeader, + }, + }) + .then((res) => { + const profile = res.data; + req.profile = profile; + next(); + }) + .catch((err) => { + next({ + status: 404, + message: err, + }); + }); + } catch (err) { + next({ + status: err.status || 500, + message: err.message || 'Internal Server Error', + }); + } +}; + +module.exports = { getUserAuth0 }; diff --git a/api/middleware/ownerAuthorization.js b/api/middleware/ownerAuthorization.js index 03348912..3f6ac283 100644 --- a/api/middleware/ownerAuthorization.js +++ b/api/middleware/ownerAuthorization.js @@ -1,16 +1,16 @@ -// pass in the key to the profile that is the owner of what's being affected +// pass in the key parameter to the profile that is the owner of what's being affected const ownerAuthorization = (key) => (req, res, next) => { // TO-DO: Implement Auth0 - check that the role_id matches the key passed - // if ( - // req.profile.profile_id === req[key].profile_id || - // (req.profile.role_id < 3 && req.profile.role_id < (req[key].role_id || 3)) - // ) - // next(); - // else - // res - // .status(401) - // .json({ message: 'You are not authorized to take this action' }); + if ( + req.profile.profile_id === req[key].profile_id || + (req.profile.role_id < 3 && req.profile.role_id < (req[key].role_id || 3)) + ) + next(); + else + res + .status(401) + .json({ message: 'You are not authorized to take this action' }); next(); }; diff --git a/api/profile/profileMiddleware.js b/api/profile/profileMiddleware.js index a940165b..44d7f12a 100644 --- a/api/profile/profileMiddleware.js +++ b/api/profile/profileMiddleware.js @@ -2,9 +2,11 @@ const Profiles = require('./profileModel'); const checkProfileObject = (req, res, next) => { const { email, name, role_id, avatarUrl } = req.body; - if (!role_id) return next({ status: 400, message: 'role_id is required' }); - if (!email) return next({ status: 400, message: 'email is required' }); - if (!name) return next({ status: 400, message: 'name is required' }); + console.log(req.body); + console.log("I'm in the obj middleware"); + // if (!role_id) return next({ status: 400, message: 'role_id is required' }); + // if (!email) return next({ status: 400, message: 'email is required' }); + // if (!name) return next({ status: 400, message: 'name is required' }); if ( typeof role_id !== 'number' || typeof email !== 'string' || @@ -15,6 +17,7 @@ const checkProfileObject = (req, res, next) => { message: 'variables in the request body must all be of type string except role_id must be of type number', }); + console.log('I made it to the other side!'); if (email.length > 255 || name.length > 255) return next({ status: 400, diff --git a/api/profile/profileModel.js b/api/profile/profileModel.js index e184f57a..c6f5ac62 100644 --- a/api/profile/profileModel.js +++ b/api/profile/profileModel.js @@ -25,9 +25,7 @@ const remove = async (id) => { }; const findOrCreateProfile = async (profileObj) => { - const foundProfile = await findById(profileObj.okta_id).then( - (profile) => profile - ); + const foundProfile = await findById(profileObj.id).then((profile) => profile); if (foundProfile) { return foundProfile; } else { diff --git a/api/profile/profileRouter.js b/api/profile/profileRouter.js index 1441c5ad..4a7f03c0 100644 --- a/api/profile/profileRouter.js +++ b/api/profile/profileRouter.js @@ -9,17 +9,22 @@ const { checkProfileExist, } = require('./profileMiddleware'); -router.get('/role/:role_id', authRequired, checkRoleExist, function (req, res) { - const role_id = req.params.role_id; - Profiles.findByRoleId(role_id) - .then((roleList) => { - res.status(200).json(roleList); - }) - .catch((err) => { - console.log(err); - res.status(500).json({ message: err.message }); - }); -}); +router.get( + '/:role/:role_id', + authRequired, + checkRoleExist, + function (req, res) { + const role_id = req.params.role_id; + Profiles.findByRoleId(role_id) + .then((roleList) => { + res.status(200).json(roleList); + }) + .catch((err) => { + console.log(err); + res.status(500).json({ message: err.message }); + }); + } +); router.get( '/users/:profile_id', @@ -194,9 +199,9 @@ router.get( * profile: * $ref: '#/components/schemas/Profile' */ -router.post('/', checkProfileObject, async (req, res) => { +router.post('/', authRequired, checkProfileObject, async (req, res) => { const profile = req.body; - + console.log("I'm back to the router!"); // TO-DO: Implement Auth0 - check DB if specific Auth0 ID already exists // changed verification from findById(okta_id) to findBy(email) try { diff --git a/api/programs/programTypesRouter.js b/api/programs/programTypesRouter.js index f5857503..49416478 100644 --- a/api/programs/programTypesRouter.js +++ b/api/programs/programTypesRouter.js @@ -151,8 +151,8 @@ router.get('/:id', authRequired, checkProgramExists, async function (req, res) { router.post( '/', - validateProgramObject, authRequired, + validateProgramObject, checkIfProgramIsUnique, async (req, res, next) => { try { diff --git a/api/user/userRouter.js b/api/user/userRouter.js index 52b88a33..15a8ad82 100644 --- a/api/user/userRouter.js +++ b/api/user/userRouter.js @@ -32,7 +32,7 @@ router.post('/register', (req, res) => { router.get('/', authRequired, function (req, res) { // TO-DO: Implement Auth0 to check logged in (middleware) then use req.profile from what is received back - // const { role_id, profile_id } = req.profile; + const { role_id, profile_id } = req.profile; User.findUserData(role_id, profile_id) .then((user) => { diff --git a/config/auth0.js b/config/auth0.js new file mode 100644 index 00000000..cc7555ad --- /dev/null +++ b/config/auth0.js @@ -0,0 +1,7 @@ +module.exports = { + jwksUri: process.env.AUTH0_JWKS_URI, + audience: process.env.AUTH0_AUDIENCE, + issuer: process.env.AUTH0_ISSUER, + connection: process.env.AUTH0_CONNECTION, + token: process.env.AUTH0_TOKEN, +}; diff --git a/data/seeds/001_profiles.js b/data/seeds/001_profiles.js index 74ce7f57..6652399c 100644 --- a/data/seeds/001_profiles.js +++ b/data/seeds/001_profiles.js @@ -1,8 +1,7 @@ const faker = require('faker'); let profiles = [...new Array(20)].map((i, idx) => ({ - // TO-DO: Implement Auth0 ID - // okta_id: idx === 0 ? '00ulthapbErVUwVJy4x6' : faker.random.alphaNumeric(20), + auth0_id: idx === 0 ? '00ulthapbErVUwVJy4x6' : faker.random.alphaNumeric(20), avatarUrl: faker.image.avatar(), email: idx === 0 ? 'llama001@maildrop.cc"' : faker.internet.email(), name: @@ -13,9 +12,9 @@ let profiles = [...new Array(20)].map((i, idx) => ({ })); /* - Manually setting the `id` for each profile to the Okta provided ID. Adding + Manually setting the `id` for each profile to the Auth0 provided ID. Adding profiles was not in scope for this iteration, but adding profiles in the - future will require the okta-id to be set as the `id` for each profile. + future will require the auth0-id to be set as the `id` for each profile. */ // TO-DO: Implement dummy Auth0 IDs diff --git a/package-lock.json b/package-lock.json index 367eb7bf..869f1a28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "debug": "~2.6.9", "dotenv": "^8.2.0", "express": "~4.16.1", + "express-oauth2-jwt-bearer": "^1.3.0", "faker": "^4.1.0", "helmet": "^3.23.1", "http-errors": "~1.6.3", @@ -3794,6 +3795,17 @@ "node": ">= 0.10.0" } }, + "node_modules/express-oauth2-jwt-bearer": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/express-oauth2-jwt-bearer/-/express-oauth2-jwt-bearer-1.3.0.tgz", + "integrity": "sha512-m8UyAxL9eHpDDmSxWEaKLEPlE+6lfRCT/z3i2Cm0MYajUP4L/WFaZ66ch5KrPPiHEy91op6fhzZ0RTN8Ldap1Q==", + "dependencies": { + "jose": "^4.9.2" + }, + "engines": { + "node": "^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0" + } + }, "node_modules/express/node_modules/cookie": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", @@ -6287,6 +6299,14 @@ "node": ">= 8.3" } }, + "node_modules/jose": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.11.2.tgz", + "integrity": "sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -13776,6 +13796,14 @@ } } }, + "express-oauth2-jwt-bearer": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/express-oauth2-jwt-bearer/-/express-oauth2-jwt-bearer-1.3.0.tgz", + "integrity": "sha512-m8UyAxL9eHpDDmSxWEaKLEPlE+6lfRCT/z3i2Cm0MYajUP4L/WFaZ66ch5KrPPiHEy91op6fhzZ0RTN8Ldap1Q==", + "requires": { + "jose": "^4.9.2" + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -15678,6 +15706,11 @@ "supports-color": "^7.0.0" } }, + "jose": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.11.2.tgz", + "integrity": "sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index 88a72c1c..bf8f183a 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "debug": "~2.6.9", "dotenv": "^8.2.0", "express": "~4.16.1", + "express-oauth2-jwt-bearer": "^1.3.0", "faker": "^4.1.0", "helmet": "^3.23.1", "http-errors": "~1.6.3", diff --git a/server.js b/server.js index fd7133c7..0056a321 100644 --- a/server.js +++ b/server.js @@ -1,6 +1,38 @@ require('dotenv').config(); - const app = require('./api/app.js'); +const { auth, requiredScopes } = require('express-oauth2-jwt-bearer'); + +// Authorization middleware. When used, the Access Token must +// exist and be verified against the Auth0 JSON Web Key Set. +const checkJwt = auth({ + audience: 'https://a.coderheroes.dev/', + issuerBaseURL: `https://dev-35n2stap.auth0.com/`, +}); + +// This route doesn't need authentication +app.get('/api/public', function (req, res) { + res.json({ + message: + "Hello from a public endpoint! You don't need to be authenticated to see this.", + }); +}); + +// This route needs authentication +app.get('/api/private', checkJwt, function (req, res) { + res.json({ + message: + 'Hello from a private endpoint! You need to be authenticated to see this.', + }); +}); + +const checkScopes = requiredScopes('read:messages'); + +app.get('/api/private-scoped', checkJwt, checkScopes, function (req, res) { + res.json({ + message: + 'Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this.', + }); +}); const port = process.env.PORT || 8000; app.listen(port, () => console.log(`\n** Running on port ${port} **\n`)); diff --git a/temp.js b/temp.js deleted file mode 100644 index e69de29b..00000000