Secure Record Storage
Scenario
You have been given a pre-existing “Notes” API. It has full CRUD (Create, Read, Update, Delete) functionality and is protected by authentication middleware, meaning only logged-in users can access the endpoints. However, there’s a significant flaw: any authenticated user can view, update, or delete any note, regardless of who created it.
Your task is to implement authorization logic to ensure that users can only access and manage the notes they personally own.
Instructions
For this lab, you will be provided with a starter codebase. The starter code contains a simple Express API with user authentication and a /api/notes
endpoint.
Task 1: Associate Notes with Users
-
Update the
Note
Model: Add a new field to theNote
schema (models/Note.js
). This field should be nameduser
(orowner
) and should store theObjectId
of the user who created the note. It should be a required reference to theUser
model.// Example snippet for the Note schema user: { type: Schema.Types.ObjectId, ref: 'User', required: true, }
-
Modify the “Create Note” Route: In your notes route file (
routes/api/notes.js
), find thePOST /
route. When a new note is created, you must associate it with the currently logged-in user. The authenticated user’s data should be available onreq.user
from the authentication middleware. Save the user’s_id
to the new note’suser
field.
Task 2: Implement Ownership-Based Authorization
-
Filter “Get All Notes”: Modify the
GET /
route. Instead of returning all notes in the database, it should now only return the notes where theuser
field matches the_id
of the currently authenticated user (req.user._id
). -
Secure “Update Note”: Modify the
PUT /:id
route. Before updating a note, you must first find the note by its ID. Then, check if theuser
field on that note matches the authenticated user’s_id
.- If they match, proceed with the update.
- If they do not match, return a
403 Forbidden
status with an error message like"User is not authorized to update this note."
-
Secure “Delete Note”: Modify the
DELETE /:id
route. Similar to the update route, you must check for ownership before deleting a note.- Find the note by its ID.
- If the user is the owner, delete the note.
- If the user is not the owner, return a
403 Forbidden
status with an appropriate error message.
-
(Optional) Secure “Get Single Note”: If you have a
GET /:id
route, apply the same ownership check there as well.
Acceptance Criteria
- The
Note
model includes a requireduser
field referencing theUser
model. - The
POST /api/notes
route correctly assigns the logged-in user’s ID to the new note. - The
GET /api/notes
route only returns notes created by the currently logged-in user. - The
PUT /api/notes/:id
andDELETE /api/notes/:id
routes prevent users from modifying or deleting notes they do not own, returning a403
status code. - The API functions correctly for all valid requests.
Submission
- Submit a link to your completed GitHub repository.
- Your submission should be based on the provided starter code.
Grading
This lab is worth 50 points. Your submission will be evaluated based on the following criteria:
Criteria | Excellent | Satisfactory | Needs Improvement | Points |
---|---|---|---|---|
Note Model Configuration | (9-10 pts) The Note schema correctly includes a user field with type: Schema.Types.ObjectId , ref: 'User' , and required: true . | (7-8 pts) The user field is added but is missing or has an incorrect configuration for one of the required attributes (e.g., ref , required ). | (0-6 pts) The user field is missing from the schema or is configured incorrectly in multiple ways. | 10 |
Create Endpoint (POST /api/notes ) | (9-10 pts) The endpoint correctly creates a new note and successfully assigns the _id from the authenticated user (req.user ) to the note’s user field. | (7-8 pts) The endpoint creates a note, but fails to associate it with the logged-in user, or the association is incorrect. | (0-6 pts) The create endpoint is non-functional, does not create a note, or has significant errors. | 10 |
Read Endpoint (GET /api/notes ) | (9-10 pts) The endpoint correctly and consistently returns only the notes owned by the authenticated user making the request. | (7-8 pts) The endpoint’s filtering logic is partially incorrect (e.g., it still returns some notes not owned by the user) or fails in certain cases. | (0-6 pts) The endpoint returns all notes from the database with no attempt at authorization, or the endpoint is non-functional. | 10 |
Update/Delete Endpoint (PUT & DELETE /api/notes/:id ) | (18-20 pts) Both routes robustly check for note ownership. They allow access for the owner and consistently return a 403 Forbidden status for non-owners on all attempts. | (14-17 pts) Authorization checks are implemented but are flawed (e.g., one route works, one doesn’t) or the correct 403 status is not always returned. | (0-13 pts) Authorization checks are missing entirely, are non-functional, or are implemented incorrectly on both the update and delete routes. | 20 |
Total | 50 |
Starter Code
You will be provided with a starter codebase for this lab. Below are the contents of the files you will need. Create these files in your local project directory.
node_modules
.env
# Replace with your MongoDB Atlas connection string
MONGO_URI=mongodb://127.0.0.1:27017/notesdb
# Choose a long, random string for your JWT secret
JWT_SECRET=yoursupersecretjwttoken
const express = require('express');
const path = require('path');
const db = require('./config/connection');
const routes = require('./routes');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3001;
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
// if we're in production, serve client/build as static assets
if (process.env.NODE_ENV === 'production') {
app.use(express.static(path.join(__dirname, '../client/build')));
}
app.use(routes);
db.once('open', () => {
app.listen(PORT, () => console.log(`🌍 Now listening on localhost:${PORT}`));
});
const mongoose = require('mongoose');
mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
module.exports = mongoose.connection;
const { Schema, model } = require('mongoose');
const bcrypt = require('bcrypt');
const userSchema = new Schema({
username: {
type: String,
required: true,
unique: true,
trim: true,
},
email: {
type: String,
required: true,
unique: true,
match: [/.+@.+\..+/, 'Must use a valid email address'],
},
password: {
type: String,
required: true,
minlength: 5,
},
});
// hash user password
userSchema.pre('save', async function (next) {
if (this.isNew || this.isModified('password')) {
const saltRounds = 10;
this.password = await bcrypt.hash(this.password, saltRounds);
}
next();
});
// custom method to compare and validate password for logging in
userSchema.methods.isCorrectPassword = async function (password) {
return bcrypt.compare(password, this.password);
};
const User = model('User', userSchema);
module.exports = User;
const { Schema, model } = require('mongoose');
// This is the model you will be modifying
const noteSchema = new Schema({
title: {
type: String,
required: true,
trim: true,
},
content: {
type: String,
required: true,
},
createdAt: {
type: Date,
default: Date.now,
},
});
const Note = model('Note', noteSchema);
module.exports = Note;
const jwt = require('jsonwebtoken');
const secret = process.env.JWT_SECRET;
const expiration = '2h';
module.exports = {
authMiddleware: function (req, res, next) {
let token = req.body.token || req.query.token || req.headers.authorization;
if (req.headers.authorization) {
token = token.split(' ').pop().trim();
}
if (!token) {
return res.status(401).json({ message: 'You must be logged in to do that.' });
}
try {
const { data } = jwt.verify(token, secret, { maxAge: expiration });
req.user = data;
} catch {
console.log('Invalid token');
return res.status(401).json({ message: 'Invalid token.' });
}
next();
},
signToken: function ({ username, email, _id }) {
const payload = { username, email, _id };
return jwt.sign({ data: payload }, secret, { expiresIn: expiration });
},
};
const router = require('express').Router();
const apiRoutes = require('./api');
router.use('/api', apiRoutes);
router.use((req, res) => {
res.status(404).send('<h1>😝 404 Error!</h1>');
});
module.exports = router;
const router = require('express').Router();
const userRoutes = require('./userRoutes');
const noteRoutes = require('./noteRoutes');
router.use('/users', userRoutes);
router.use('/notes', noteRoutes);
module.exports = router;
const router = require('express').Router();
const { User } = require('../../models');
const { signToken } = require('../../utils/auth');
// POST /api/users/register - Create a new user
router.post('/register', async (req, res) => {
try {
const user = await User.create(req.body);
const token = signToken(user);
res.status(201).json({ token, user });
} catch (err) {
res.status(400).json(err);
}
});
// POST /api/users/login - Authenticate a user and return a token
router.post('/login', async (req, res) => {
const user = await User.findOne({ email: req.body.email });
if (!user) {
return res.status(400).json({ message: "Can't find this user" });
}
const correctPw = await user.isCorrectPassword(req.body.password);
if (!correctPw) {
return res.status(400).json({ message: 'Wrong password!' });
}
const token = signToken(user);
res.json({ token, user });
});
module.exports = router;
const router = require('express').Router();
const { Note } = require('../../models');
const { authMiddleware } = require('../../utils/auth');
// Apply authMiddleware to all routes in this file
router.use(authMiddleware);
// GET /api/notes - Get all notes for the logged-in user
// THIS IS THE ROUTE THAT CURRENTLY HAS THE FLAW
router.get('/', async (req, res) => {
// This currently finds all notes in the database.
// It should only find notes owned by the logged in user.
try {
const notes = await Note.find({});
res.json(notes);
} catch (err) {
res.status(500).json(err);
}
});
// POST /api/notes - Create a new note
router.post('/', async (req, res) => {
try {
const note = await Note.create({
...req.body,
// The user ID needs to be added here
});
res.status(201).json(note);
} catch (err) {
res.status(400).json(err);
}
});
// PUT /api/notes/:id - Update a note
router.put('/:id', async (req, res) => {
try {
// This needs an authorization check
const note = await Note.findByIdAndUpdate(req.params.id, req.body, { new: true });
if (!note) {
return res.status(404).json({ message: 'No note found with this id!' });
}
res.json(note);
} catch (err) {
res.status(500).json(err);
}
});
// DELETE /api/notes/:id - Delete a note
router.delete('/:id', async (req, res) => {
try {
// This needs an authorization check
const note = await Note.findByIdAndDelete(req.params.id);
if (!note) {
return res.status(404).json({ message: 'No note found with this id!' });
}
res.json({ message: 'Note deleted!' });
} catch (err) {
res.status(500).json(err);
}
});
module.exports = router;