Technical
Mar 28, 2024
Aneesh Arora
Cofounder and CTO
Learn the correct way to encrypt passwords and send emails
This is part 2 of our earlier blog post on user authentication and authorization, in which we delved into the problems of relying on external services to manage user identities and learnt how to create JWT tokens and store them in extremely secure cookies.
In this part, we will learn how to handle password encryption and verify email addresses.
If you aren't interested in learning the theory, you can directly skip to the implementation, although I wouldn't recommend it.
Never store user passwords in plain text. If hackers manage to access your database, all users will be compromised not just on your website but on others where they have, unfortunately, reused the same password, through the use of credential stuffing .
When encrypting passwords, we employ one-way encryption, known as hashing, rather than two-way encryption. This means it’s not possible to decrypt the hashed password back into the original plain text, ensuring the original password remains inaccessible.
You might wonder how we verify the password when a user logs in. The process is straightforward: we compute the hash of the entered password in the same way as initially and simply compare the hashes!
The advantage of this method is that it prevents anyone, including attackers or even ourselves, from accessing the user’s plain text password. Thus, it ensures the user's password can never be compromised by us, even in the event of a database breach.
To further enhance security when hashing passwords, we add a salt. Salting involves appending a unique random value to the end of the password. How does this help?
This extra step provides additional protection against rainbow table attacks, where hackers use a pre-computed database of decrypted hash passwords to determine user passwords. With each password having a unique salt, hackers are forced to re-compute hashes for every user, significantly slowing them down.
But don't be intimidated by all the theory; we're not going to handle all of this manually.
In Python, the best library for managing password hashing and salting is Passlib.
For password hashing, we can use bcrypt or Argon2. We will opt for Argon2 because it is newer and more robust. It emerged as the winner of the Password Hashing Competition in 2015.
First, let's install the necessary packages:
pip install passlib
pip install argon2-cffi
Now, let's write some code to hash a password and then verify it:
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
# To hash a password for the first time
hashed_password = pwd_context.hash("user_password")
# Verifying the password
def verify_password(user_password, hashed_password):
return pwd_context.verify(user_password, hashed_password)
Validating a user's email address is crucial to ensure that the user is who they claim to be. A valid email address is necessary for sharing important updates with the user and for resetting passwords if the user forgets theirs.
For these purposes, we can generate a secure, time-limited token and email it to the user. This token is also stored in the database, associated with the user's email. When the user clicks on the link in the email, they will be directed to our website, where we will verify that the token is correct, unexpired, and matches the one in the database to confirm the user's email address.
To generate this token, we can use the JSON token code we created in the earlier blog post .
Once the token is created, it needs to be emailed to the user.
import smtplib, ssl
from email.message import EmailMessage
WEBSITE_URL="yourwebsite.com"
# Server configuration
smtp_server = "smtp.gmail.com"
port = 465 # For SSL
# Email settings
sender_email = "sender_email@gmail.com" # Enter your address
password = "sender_email_password"
# Create the email message
message = EmailMessage()
message["From"] = sender_email
message["Subject"] = "Verify your email address"
def send_email(receiver_email, verify_token):
message["To"] = receiver_email
link = f"{WEBSITE_URL}/verifyemail?token={verify_token}"
email_text = """Please click the link below to verify your email address
{{link}}"""
email_text = email_text.replace("{{link}}", link)
message.set_content(email_text)
# Create a secure SSL context
context = ssl.create_default_context()
# Try to log in to server and send email
try:
server = smtplib.SMTP_SSL(smtp_server, port, context=context)
server.login(sender_email, password)
# Send email here
server.send_message(message)
finally:
server.quit()
This approach will send plain text emails. Sending HTML emails is more complex because all email clients adhere to different specifications, which haven't been updated in 20 years. Therefore, it's recommended to use pre-designed templates. However, that topic is beyond the scope of this blog post.
We will also need to create the route to handle verification of email token once the user clicks on the link.
from fastapi import FastAPI, Response
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Set up CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=[WEBSITE_URL],
allow_credentials=True,
allow_methods=["*"], # Allows all methods
allow_headers=["*"], # Allows all headers
)
TOKEN_EXPIRE_MINUTES = 60
@app.put("/verifyemail")
async def verifyEmail(token: str, response: Response):
if token:
payload = decode_token(token)
if "email" in payload:
#find user and token in database to confirm and
#delete token from database
if Token and user in DB:
print(payload["email"]," email address verified")
response.set_cookie("token",
value=create_jwt({"email":payload["email"]}),
max_age=TOKEN_EXPIRE_MINUTES*60,
httponly=True,
secure=True,
samesite="Strict",
domain="your site domain")
return {"email": payload["email"]}
else:
print("Could not find token in DB")
else:
print("Could not find email in payload")
This covers the basics of password encryption and sending emails. However, this isn't a complete example, as there are additional aspects to consider, such as creating login/signup forms, enforcing password strength, and integrating social signup options. Perhaps a third and final blog post will address these topics.