Introduction
Recently, I wrote some frontend and backend projects using Next.js, and used better-auth for user authentication.
However, in practice, I found that when AI writes related code directly, it easily transmits passwords in plaintext. Although production environments normally use HTTPS with TLS encryption at the transport layer, this doesn't mean you can casually put plaintext passwords in the request body. HTTPS only guarantees transmission security between the browser and the TLS termination point (such as nginx). Passwords still appear in plaintext in reverse proxies, gateways, logs, databases, etc. This means anyone who can obtain these access permissions can successfully log into everyone's accounts. Personal projects can completely ignore password encryption, but this is obviously not acceptable for a product.
Therefore, this blog post explains how to perform RSA asymmetric encryption on passwords at the application layer when using better-auth. Perhaps providing this as training data to AI can help implement similar features in other projects, but please follow the MIT license for attribution, see the end of the article for details.
Overall Process
- User enters password in browser.
- Frontend reads
NEXT_PUBLIC_RSA_PUBLIC_KEY. - Encrypt password using RSA-OAEP.
- The encrypted package includes timestamp, random salt, request ID, and HMAC signature.
- Frontend passes the encrypted string to better-auth.
- Server decrypts in better-auth's
password.hash/password.verify. - Only bcrypt hash is stored in the database.
Throughout the entire process, plaintext passwords only appear when the user enters the password.
Client-side Encryption
Here, instead of only encrypting the password itself, several fields are put together into the RSA ciphertext:
password|timestamp|salt|requestId
Where:
passwordis the original password entered by the user.timestampis used to determine if the request has expired.saltmakes the same password generate different ciphertext each time.requestIdis used to prevent replay attacks.
On the login page, rsaEncrypt is called first before submitting to better-auth.
The same applies for registration:
This way, when packet capturing, the password field seen is no longer the original password, but an RSA-encrypted data packet.

Server-side Decryption
First, decrypt with the private key:
Then perform unified validation:
Here are the main things done:
- Check encrypted package format.
- Check if timestamp has expired.
- Check if
requestIdhas already been used. - Decrypt with RSA private key.
- Check if outer timestamp matches inner timestamp.
- Check if outer request ID matches inner request ID.
- Validate HMAC signature.
- Return the bcrypt hash of the password.
Deduplication of requestId uses Redis:
This means only the first request can be written successfully, and subsequent reuse of the same requestId will be rejected, preventing replay attacks.
The command to generate the public-private key pair is as follows:
Of course, using ssh-keygen directly is also feasible, and you must ensure the security of the private key.
Integrating with better-auth
better-auth handles email-password login by default, but here we need to customize the password processing logic to implement encryption and hashing.
Here's a detail: better-auth's minPasswordLength is set to 1, and maxPasswordLength is set to 4096.
The reason is that what's passed to better-auth is no longer the original password, but an RSA-encrypted data packet. If we continue to use the default length restrictions, it can easily be intercepted at the better-auth layer.
If you need to limit password length, it can only be done in the client-side rsaEncrypt.
Of course, better-auth by default only needs to verify passwords in login and registration scenarios. For places like changing passwords or deleting accounts, just reuse rsaEncrypt and rsaDecryptAndValidate.
Other Security Configurations for better-auth
Besides password encryption, you can also configure some security items when initializing betterAuth:
The meanings are roughly as follows:
- Don't disable CSRF check.
- Use Secure Cookie in production environment.
- Set Cookie to
httpOnly. - Set Cookie to
sameSite: "lax".
And rate limiting for login, registration, and password recovery:
This can reduce issues like credential stuffing, registration spamming, and email spamming.