Authentication
Last updated
Last updated
TheNextStartup has a preconfigured authentication system using lucia-auth https://lucia-auth.com/. So why lucia-auth and not some other tools like Next-auth, Clerk or etc?
Here are the reasons why:
Session based authentication is much simpler than token based authentication and lucia-auth promotes it. I don't want to handle token revocation, rotation and blacklisting, session-based is much more simpler because the state of your authentication is checked within your database. Token based authentication is much more suitable for scaled up systems, in our template we don't have that.
Cost. Of course, it's free (well money-free) and if you're starting out, especially if you're bootstrapped, coding the authentication yourself is the way to go. If you have the cash to spend on 3rd-party authentication services like Clerk or even Firebase then you can replace this, because I'm all-in on the idea of not building out a non core business function.
Documentation is clear and concise, if you want to implement magic links and 2FA the documentation is clear as the sky.
As I have stated earlier, I've built a simple todo "SaaS" app to showcase the benefits of using this template and now I'll guide you on how the authentication codebase works. Both the admin and product app shares the same authentication logic, so I'll explain it in one go.
Bob inputs his email and password on the Login form. The email and password is validated using zod and react-hook-forms. The UI form is from shadcn/ui. The login mutation function is made using react-query.
The server's API logic for login just checks if the customer's account status is active
and validates the password if it matches to the found customer row. If password is valid then lucia-auth creates a session row for that specific user and sets the session id into a cookie.
For the admin app, it's just a simple checking if admin row exists
The template's configuration for the session expiration is set to false, so it doesn't expire. You can set the expiration in this file. For more information check out https://lucia-auth.com/basics/sessions
Server just notifies the client if login is successful or not.
Adding OAuth aka "social login" to apps has became the defacto standard in the app industry, one of the reasons why is that it removes the friction between the customer and your application. No more remembering of email and password combinations. I wont explain OAuth here in full details, but you can check it out here if you're interestedhttps://www.youtube.com/watch?app=desktop&v=ZV5yTm4pT8g
One of the prerequisites for using Google login is the creation of GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET. Watch this video to know how to generate that variables https://www.youtube.com/watch?v=tgO_ADSvY1I. Also don't forget to set the HOST_NAME of your application.
Kindly excuse the informal sequence diagram and let me explain how the flow works.
Bob clicks the "Sign in with Google" button and by clicking that button a GET request shall happen at the server side using this API route api/auth/login/google
Our API shall communicate with Google's OAuth server to generate an authorization URL
After the authorization URL has been generated, the server shall redirect Bob to the generated authorization URL.
A Google login page shall be displayed on Bob's screen and he can pick any Google account that he owns to be used for the application.
After choosing a Google account, the GET API endpoint api/auth/login/google/callback
shall be called to facilitate the account creation, login if account already exists, or converting the email and password account into a google type of customer account if Bob already used an email and password login before using his chosen Google email address.
If Google customer account already exists, we will just redirect the user to the /app page.
If the Google email address is already inside our Turso database we will convert it to a Google type of account or create a new customer account if it does not exists alongside with his Stripe or Lemonsqueezy customer account.
We will just redirect Bob to the /app page if the Login using Google account was successful.
For the signup flow, I broken it down into two parts. First part is the actual creation of database records and the second one is the verification of the email using a verification code. Now let's focus on the first part.
I wont make a diagram here since it's just a repeating pattern moving forward and it will be obvious later on when you make multiple forms. The pattern is
Create form using react-hook-forms, zod, and shadcn form to have a client side validated form.
Create an API endpoint to process the form
Make a mutation using react-query and voila
The sign up flow is just
Validate email if it exists inside the customerProfiles table, if yes and it's an active account then throw an error. If yes and it's an not active account, we need to resend the verification code to the user's email address.
If it does not exists at all then, we're just gonna create a payment provider account for the customer, customer row, and a verification code.
After all that steps we create a session cookie and return a successful response to the customer.
Take note that even if I generated a session cookie for the customer, he will not be able to access the app entirely because his account is tagged as inactive
. This is just exclusively beneficial for the second part of the sign up, which is the verification.
Honestly, I was contemplating on whether or not to include this and just let you guys implement it on your own. But for the sake of demonstration purposes, I just did it lol. This verification flow is an 1-1 implementation based on lucia-auth's documentation https://lucia-auth.com/guides/email-and-password/email-verification-codes . So just check the documentation, or if you have additional questions regarding on the implementation, don't hesitate to ping me up.
The 8-digit verification code is just valid for 5 minutes
The code will also be send via Resend and the actual email template was made via react-email library. You can check the Resend documentation here https://resend.com/docs/send-with-nextjs to setup your account, they also provide free tier so it's a good option to jumpstart this feature. Don't forget to paste your RESEND_API_KEY on the .env file
The client side shall redirect the user to the app page once the verification process was a success.
If you have already checked the source code, you might notice the usage of pipe function. This function protects the API endpoints that needs authentication by piping the authentication function and the actual route handler together.
This function is like a middleware function from ExpressJS
and it's a concept from functional programming https://www.freecodecamp.org/news/pipe-and-compose-in-javascript-5b04004ac937/ . Read it if you want.
I thought of writing my own middleware function since I want to sort of emulate ExpressJS routing system inside this template, but thanks to https://github.com/KolbySisk I saved time and effort 😛. Follow him on his socials!
We want to block visitors on our app from accessing certain pages, especially if they are not logged in as a customer.
This just loads the current customer by reading his cookies, validating his session inside the database, and checking if his account status is active. The result of that function is the customer row along with his email, which is then cached using the built-in cache function of React for server-side components. This will help us to offload frequent database hits for session validation. Visit this video tutorial if you want to know more about it https://www.youtube.com/watch?v=MxjCLqdk4G4
The customer object will be stored in a session context so that every pages under that shall have access to the customer object
Tools like next-auth and Clerk implements this for you, but with lucia-auth you have to build it from scratch.