Building a Secure Login/Signup System

1. Introduction (Vibha)

2. Connecting Front End + Backend Components (Anusha)

Quick Demo of Login/Register/CRUD Demonstration

Login/Register/CRUD Demonstration

Input Validation

Anatomy of JWT w/ CRUD Operations

id = db.Column(db.Integer, primary_key=True)
note = db.Column(db.Text, unique=False, nullable=False)
image = db.Column(db.String, unique=False)
# Define a relationship in Notes Schema to userID who originates the note, many-to-one (many notes to one user)
userID = db.Column(db.Integer, db.ForeignKey('users.id'))
id = db.Column(db.Integer, primary_key=True)
_name = db.Column(db.String(255), unique=False, nullable=False)
_uid = db.Column(db.String(255), unique=True, nullable=False)
_password = db.Column(db.String(255), unique=False, nullable=False)
_dob = db.Column(db.Date)

3. Justin (CRUD)

To create and register a user, you can using the existing code in user.py in the api folder and update it, like this: We used Teacher’s code as a reference here:

We first had a class for creating a user/fetching data from all the users. This would just be our normal localhost link. Since we are using users.py in the api folder, our link is just /users here. class _CRUD(Resource): # User API operation for Create, Read. THe Update, Delete methods need to be implemented def post(self): # Create method ‘’’ Read data for json body ‘’’ body = request.get_json()

        ''' Avoid garbage in, error checking '''
        # validate name
        name = body.get('name')
        if name is None or len(name) < 2:
            return {'message': f'Name is missing, or is less than 2 characters'}, 400
        # validate uid
        uid = body.get('uid')
        if uid is None or len(uid) < 2:
            return {'message': f'User ID is missing, or is less than 2 characters'}, 400
        # look for password and dob
        password = body.get('password')
        dob = body.get('dob')
        coins = 0
        
        
        tracking = body.get('tracking') #validate tracking
        #
        exercise = body.get('exercise') #validate exercise

        ''' #1: Key code block, setup USER OBJECT '''
        uo = User(name=name, #user name
                  uid=uid, tracking=tracking, exercise=exercise, dob=dob, coins=coins)
        
        ''' Additional garbage error checking '''
        # set password if provided
        if password is not None:
            uo.set_password(password)
        # convert to date type
        # if dob is not None:
        #     try:
        #         uo.dob = datetime.strptime(dob, '%Y-%m-%d').date()
        #     except:
        #         return {'message': f'Date of birth format error {dob}, must be mm-dd-yyyy'}, 400
        if tracking is not None:
            uo.tracking = tracking
        
        if exercise is not None:
            uo.exercise = exercise
            
        ''' #2: Key Code block to add user to database '''
        # create user in database
        user = uo.create()
        # success returns json of user
        if user:
            #return jsonify(user.read())
            return user.read()
        # failure returns error
        return {'message': f'Processed {name}, either a format error or User ID {uid} is duplicate'}, 400

    @token_required
    def get(self, current_user): # Read Method
        users = User.query.all()    # read/extract all users from database
        json_ready = [user.read() for user in users]  # prepare output in json
        return jsonify(json_ready)  # jsonify creates Flask response object, more specific to APIs than json.dumps
    
    def put(self, user_id):
        '''Update a user'''
        user = User.query.get(user_id)
        if not user:
            return {'message': 'User not found'}, 404
        body = request.get_json()
        user.name = body.get('name', user.name)
        user.uid = body.get('uid', user.uid)
        db.session.commit()
        return user.read(), 200

    def delete(self, user_id):
        '''Delete a user'''
        user = User.query.get(user_id)
        if not user:
            return {'message': 'User not found'}, 404
        db.session.delete(user)
        db.session.commit()
        return {'message': 'User deleted'}, 200 To update and delete a user, you need their specific information (id). So, we have a seperate class with a seperate link. To update information and fetch old information, we have another get  method here as well just to get data from a specific user rather than all users. 
<style>
	.modal-backdrop {
		display: none;
		position: fixed;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		background-color: rgba(0, 0, 0, 0.7);
		z-index: 1;
	}

	.modal-content {
		position: absolute;
		top: 50%;
		left: 50%;
		transform: translate(-50%, -50%);
		background: #272726;
		padding: 40px;
		z-index: 2;
		color: #ffffff
	}

	.close-modal {
		position: absolute;
		top: 10px;
		right: 10px;
		cursor: pointer;
		background: none;
		border: none;
		font-size: 24px;
		color: white;
	}

	.wrapper,
	section {
		max-width: 900px;
	}
</style>

<hr style="margin-top: 10px" />

<h2>Current Records</h2>
<table id="userTable">
	<tr>
		<th>Name</th>
		<th>Username</th>
		<th>Password</th>
		<th>Edit</th>
		<th>Delete</th>
	</tr>
</table>

<div id="editModalBackdrop" class="modal-backdrop">
	<div id="editModal" onsubmit="submitEdit(event)" class="modal-content">
		<button id="closeModal" class="close-modal">X</button>
		<form id="editForm">
			<input type="hidden" id="editId" name="editId" />

			<label for="editFullName">Name:</label>
			<input type="text" id="editFullName" name="editFullName" /><br /><br />

			<label for="editGithubUsername">Username:</label>
			<input type="text" id="editGithubUsername" name="editGithubUsername" /><br /><br />

			<input type="submit" value="Update" />
		</form>
	</div>
</div>

<script>
	const apiUrl = "http://127.0.0.1:8240/api/users/";
	// const apiUrl = "http://127.0.0.1:8240/api/users/";
    // const apiUrl = "https://devops.nighthawkcodingsociety.com/api/users/";
	let users = [];

	function fetchUsers() {
		fetch(apiUrl)
			.then((response) => response.json())
			.then((response) => {
				users = response;

				const table = document.getElementById("userTable");
				users.forEach((user, idx) => {
					const row = table.insertRow();

					row.setAttribute("data-id", user.id);
					["name", "uid", "password"].forEach(
						(field) => {
							const cell = row.insertCell();
							if (user[field] === "none") {
								users[idx][field] = "";
							}
							cell.innerText = users[idx][field];
						}
					);

					const editCell = row.insertCell();
					const editButton = document.createElement("button");
					editButton.innerHTML = "Edit";
					editButton.addEventListener("click", editUser);
					editCell.appendChild(editButton);

					const deleteCell = row.insertCell();
					const deleteButton = document.createElement("button");
					deleteButton.innerText = "Delete";
					deleteButton.addEventListener("click", () => deleteUser(user.id, row));
					deleteCell.appendChild(deleteButton);
				});
			});
	}

	function submitForm(event) {
		event.preventDefault();
		const formData = new FormData(event.target);
		const name = formData.get("fullName");
		const uid = formData.get("githubUsername");
		const password = formData.get("password");

		const payload = {
			name,
			uid,
			password,
		};

		fetch(apiUrl, {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
			},
			body: JSON.stringify(payload),
		})
			.then((response) => {
				if (response.ok) {
					return response.json();
				} else {
					alert("server error");
					throw new Error("server");
				}
			})
			.then((data) => {
				const table = document.getElementById("userTable");
				const row = table.insertRow();
				row.setAttribute("data-id", data.id);
				[
					data.name,
					data.uid,
					data.password,
				].forEach((value) => {
					const cell = row.insertCell();
					cell.innerText = value;
				});

				const editCell = row.insertCell();
				const editButton = document.createElement("button");
				editButton.innerHTML = "Edit";
				editButton.addEventListener("click", editUser);
				editCell.appendChild(editButton);

				const deleteCell = row.insertCell();
				const deleteButton = document.createElement("button");
				deleteButton.innerText = "Delete";
				deleteButton.addEventListener("click", () => deleteUser(user.id, row));
				deleteCell.appendChild(deleteButton);

				users.push(data);
				alert("Created sucessfully!");
			})
			.catch((error) => console.error("Error:", error));
	}

	function editUser(event) {
		const id = event.currentTarget.parentElement.parentElement.getAttribute("data-id");
		document.getElementById("editId").value = id;

		const form = document.getElementById("editForm");
		const user = users.find((u) => u.id == id);

		form.querySelector("#editGithubUsername").value = user.uid;
		form.querySelector("#editFullName").value = user.name;

		document.getElementById("editModalBackdrop").style.display = "block";
	}

	// Fetch users and ensure close modal interaction
	document.addEventListener("DOMContentLoaded", function () {
		fetchUsers();
		document.getElementById("closeModal").addEventListener("click", function () {
			document.getElementById("editModalBackdrop").style.display = "none";
		});
	});

	function submitEdit(event) {
		event.preventDefault();
		const formData = new FormData(event.target);
		const id = formData.get("editId");
		const name = formData.get("editFullName");
		const uid = formData.get("editGithubUsername");

		const payload = {
			id,
			name,
			uid,
		};

		fetch(`${apiUrl}${id}`, {
			method: "PUT",
			headers: {
				"Content-Type": "application/json",
			},
			body: JSON.stringify(payload),
		}).then((response) => {
			if (response.ok) {
				// Update the corresponding row in the table
				const row = document.querySelector(`tr[data-id='${id}']`);
				row.cells[0].innerText = name;
				row.cells[1].innerText = uid;

				// Show an alert indicating success
				alert("User information updated successfully.");
			}
		});

		document.getElementById("editModalBackdrop").style.display = "none";
	}

	function deleteUser(id, row) {
		const confirmation = prompt('Type "DELETE" to confirm.');
		if (confirmation === "DELETE") {
			fetch(`${apiUrl}${id}`, {
				method: "DELETE",
			})
				.then(() => {
					row.remove();
					alert("User deleted successfully");
				})
				.catch((error) => {
					console.error("Error:", error);
				});
		}
	}
</script>

4. Login Process/Signup Isabel

The login was a bit tricky to implement, because I had to modify things from the old flask-portfolio I was already working on. As our teacher has mentioned already, we can either fork the new cpt repository to start our Login or we can make changes to an existing repository. Since I started thinking about CRUD ahead, I decided to use the repositroy I already had and made some changes. Here is the link to the teachers changes if you started out like me and like the old format better: Flask Portfolio With JWT

class _Security(Resource): def post(self): try: body = request.get_json()

            if not body:
                return jsonify({
                    "message": "Please provide user details",
                    "data": None,
                    "error": "Bad request"
                }), 400

            uid = body.get('uid')
            password = body.get('password')

            if uid is None or password is None:
                return jsonify({'message': 'User ID or password is missing'}), 400

            user = User.query.filter_by(_uid=uid).first()

            if not user or not user.is_password(password):
                return jsonify({'message': "Invalid user ID or password"}), 400

            token = self.generate_token(user)

            # Additional response data
            
            print("User Object:", user)
            response_data = {
                "message": f"Authentication for {user._uid} successful",
                "data": {    # I needed to send this data to the frontend so that I can implement crud. 
                    "jwt": token,
                    "user": {
                'name': user.name,
                'id': user.id
            }
                }
            }

            resp = jsonify(response_data)
            resp.set_cookie("jwt", token,
                            max_age=3600,
                            secure=True,
                            httponly=True,
                            path='/'
                            )

            return resp

        except Exception as e:
            return jsonify({
                "message": "Something went wrong!",
                "error": str(e),
                "data": None
            }), 500

    def generate_token(self, user): # Notice how I put the generate token within the security function. Teacher did not do that. He called the function with a decorator and created a middle ware py file. I didn't do that and I put in directly instead
        try:
            token = jwt.encode(
                {"_uid": user._uid},
                current_app.config["SECRET_KEY"],
                algorithm="HS256"
            )
            return token
        except Exception as e:
            return jsonify({
                "error": "Something went wrong during token generation",
                "message": str(e)
            }), 500 Here is the video for reference using postman!

Video Postman Login Test

6. Best Practices and Additional Features Vibja

Token-Based Authentication: Implement token-based authentication, such as JSON Web Tokens (JWT) or OAuth, to securely manage user sessions. Tokens should be generated securely, have a limited lifespan, and be securely stored on the client side.

Secure Password Storage: Hash and salt passwords before storing them in the database. Use strong hashing algorithms (e.g., bcrypt) to protect user passwords from being exposed in the event of a data breach.

Authentication Rate Limiting: Implement rate limiting to prevent brute-force attacks on login endpoints. This can involve limiting the number of login attempts within a specified time period to mitigate the risk of unauthorized access.

Secure User Registration: Implement validation and sanitization checks on user registration inputs to prevent injection attacks. Verify the authenticity of email addresses and usernames during the registration process.

Multi-Factor Authentication (MFA): Encourage or require users to enable MFA to add an additional layer of security. This can involve using one-time codes sent via SMS, email, or authenticator apps.

Session Management: Implement secure session management practices. Ensure that session tokens are securely stored and transmitted, and consider implementing session timeout and re-authentication mechanisms.

Cross-Site Request Forgery (CSRF) Protection: Implement measures to protect against CSRF attacks. Use anti-CSRF tokens and ensure that requests from legitimate users originate from trusted sources.

Input Validation and Sanitization: Validate and sanitize all user inputs to prevent injection attacks, such as SQL injection or Cross-Site Scripting (XSS). Use parameterized queries for database interactions.

Logging and Monitoring: Implement comprehensive logging for login/signup activities. Monitor and log failed login attempts, unusual patterns, and potential security events to detect and respond to security incidents.

API Key Security: If applicable, secure API keys used for authentication and authorization. Ensure that keys are kept confidential, rotated regularly, and that access is restricted to only necessary entities.

Regular Security Audits and Updates: Conduct regular security audits of your codebase and dependencies. Stay updated on security best practices and promptly apply patches and updates to address any vulnerabilities. –>