For the past year, I've been working on a React Native mobile app for schools in my governorate to use. And this app was going to be responsible for things including students' attendance grades. So it could mean a lot if such data was forged or changed illegitimately...
For the majority of the time I spent writing this app, its API, and its website, I had not a single thought about whether or not this system was any secure. And this was because of one crucial mistake that you should never make when creating apps or systems:
Don't suppose that your API will be used correctly!
That's right, the first thing you should do when making your projects secure is think of how your API (or anything that communicates with your app's backend or database) could be used out of proper context, misused maliciously, or even just accidentally used improperly!
One common example in which I see friends making this mistake is when they make an API endpoint that changes data in the database, and their front-end uses that endpoint whenever the user makes a change. But they never think about how that same API endpoint could be called outside of the application and misused maliciously without the legitimate user's permission.
So, considering my "API" was made on Cloudflare Workers, I needed a simple way to make it secure that would work on serverless functions like Workers. So my first step was to move from the web editor to Wrangler CLI to be able to install compatible NPM packages to my project. Then I wrote this workflow of account authorization:
Registration
- User enters their username, email and password in the registration form
- User client generates a new Public/Private RSA 4096-bit Key Pair and keeps them aside
- User client encrypts the password with the server's public key as well as a nonce (number used only once)
User client sends the username, email, encrypted password, the nonce, and the newly-generated public key to the server
Server decrypts the
password + nonce
with its own private key, then strips the decrypted value of thenonce
provided by the userThe nonce is then stored so that if the same nonce is used ever again, it can reject the request. This is to prevent Replay Attacks
Server makes sure the account doesn't already exist
Server generates a new account ID for the user (this is just for users to identify each other and is not related to the security of the system)
Server generates a Salt and appends it to the password. The server then hashes the password using the new salt. It stores both the hash and the salt on the database
The server then generates a new AES encryption/decryption key, generates a new JWT, encrypts that JWT with the new AES Key, and keeps the encrypted value aside
The server encrypts that same original JWT with its own public key and stores it in the database
The server finally saves the new user with their name, ID, hashed password, and salt, then adds a new session to that user that contains their public key and an encrypted version of the newly-generated AES Key
Server responds back to the client with their new session token (encrypted), AES Key (encrypted), and the IV
Client decrypts their AES Key, their token using the provided IV. And the client finally saves this data and the registration process is successful!
PHEW! This was a lot, I understand. Especially for people who are new to this stuff, this could require re-reading. But if you have any questions let me know in the comments!
Now for the login proc— Nope I'm just kidding, I'm not writing all that again 😅
However, I'm gonna subtly go through the other process real quick for those who want to read more:
Login
- Same password and nonce relationship as before
- Non-nonced password is hashed using the same salt stored on the database
- If the hashes match, the user is authorized
- Generate a new AES Key, and JWT token and do the same response process as with the registration
Load Data
This is for the client to make sure it's still authorized and show data about the user (e.g. profile picture, bio):
Client sends encrypts its saved JWT token with its saved AES Key. It sends its public key, user ID and the IV as well to the server.
The server checks if an account with this user ID and session with such a public key exists
The server decrypts the JWT using the provided IV and the saved AES Key
The server validates the JWT token using various checks to ensure that the token is valid (These checks, besides the signature check, differ from app to app)
If all goes well, respond with the User Data
Logout
- The client sends its user ID, public key, session token (encrypted with the AES Key), and the used IV to the server
- Server checks if an account with this user ID and session with such a public key exists.
- The server decrypts the JWT and verifies it as before
- The server deletes the found session from the database and the client logs itself out
For any other processes that require Authorization, do the same validation steps as the Load Data process (in addition to any process-specific validations) to verify the user!
I understand this could feel a little overwhelming, but no worries, a couple of YouTube videos can get you well started in the realm of Cybersecurity! I myself am still learning Cybersecurity at the time of writing this, I'm not an expert and I don't claim to be. This was just the security model that I found resonated most with my needs, you can change it however you like, or come up with an entirely different structure!
However, if you want to get started in this field for absolutely free, I recommend watching the CS50's Introduction to Cybersecurity course on YouTube. CS50 is a group of teachers at Harvard that introduces absolute beginners to the world of computer science. They have been most helpful to me, I don't think I would've continued in the field of computer science without their courses. So check them out!
Put any suggestions you want down in the comments, and happy hacking!