Authentication
Why Not NextAuth?
Many developers choose NextAuth
for authentication because they perceive
setting up custom authentication as intimidating or overly complex. However,
the reality is that while implementing your own solution may seem daunting at
first, it is often straightforward but tedious.
Also NextAuth
has several drawbacks some of them:
- Restricts customization and flexibility.
- Adds unnecessary dependencies.
- Enforces rigid, opinionated patterns.
- Harder to debug due to abstraction.
- Authentication is a core part of any application, and relying on a tool that could be deprecated leaves your project vulnerable.
And that's why we did it ourselves.
Setup
Start by adding these keys to your .env.local
# (Required)
ADMIN_EMAIL=username@your-email-domain.example # Add your admin email here
AUTH_SECRET= # Add your auth secret here by using a random 43 characters string or generate one here https://jwtsecret.com/generate
# Google OAuth
AUTH_GOOGLE_ID=747633799062-6db8jdcfso7fe7ulajca0abjo8f7hasp.apps.googleusercontent.com
AUTH_GOOGLE_SECRET=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
# Github OAuth
AUTH_GITHUB_ID=f1e12e93338a421d7003
AUTH_GITHUB_SECRET=8457225c9df26a4e1f95ad175e42b2dfc4fbabe1
Authentication Forms & Pages
- All forms/components related to are located under
/src/components/authentication
- All pages are located under
/src/app/auth
Authentication Methods
Our built-in authentication feature supports:
- Magic Link
(Note: Limited by the cost of email service providers)
- OAuth
(by default, Google and GitHub are implemented but you can add more)
- Username and Password
- More in the future.
Authentication API Calls
You can use authentication api calls anywhere not limited to the their
pages by importing them from lib/api
/src/lib/auth/o-auth.ts
import {
loginWithMagicLink,
loginWithPassword,
loginWithProvider,
registerWithPassword,
sendPasswordReset,
resetPassword,
logout,
} from "lib/api";
Note
Even though users can login using the login dialog in the home page, sometimes
you might need to redirect them to authentication page and they are located
under under /src/app/auth
Add more properties on the User
If you want to add more properties on User
database model, and you want these
properties to be accessible do these steps
- Navigate to
/src/lib/auth/index.ts
- Update
validateSessionToken
by adding the properties you want
/src/lib/auth/index.ts
export const validateSessionToken = async (token: string) => {
// ...
const result = await db.session.findUnique({
where: { id: sessionId },
include: {
user: {
select: {
id: true,
name: true,
role: true,
email: true,
avatarUrl: true,
emailVerified: true,
stripeCustomerId: true,
stripeCustomerUrl: true,
phone: true,
subscribed: true,
// add more properties here
my_example_property: true,
},
},
},
});
// ...
};
Adding an oauth provider walkthrough
Let's rebuild the Github login together so you understand the process better
Make sure to create a GitHub OAuth app here
and add your client id
and secret key
to your .env.local
file
- Navigate to
/src/types/auth.ts
- Add Github to
OAuthLoginProvider
enum
/src/types/auth.ts
export enum OAuthLoginProvider {
//...
github = "github",
}
- Navigate to
/src/lib/auth/o-auth.ts
and import your desired provider fromarctic
and initialize it in this case we will importGitHub
/src/lib/auth/o-auth.ts
import { GitHub } from "arctic";
export const github = new GitHub(
"your_github_auth_id",
"your_github_auth_secret",
process.env.NEXT_PUBLIC_BACKEND_URL + "/auth/login/github/callback",
);
- Create a function to generate the Authorization
state
andURL
specific togithub
and make sure the return type matchesOAuthAuthorizationData
type
/src/lib/auth/o-auth.ts
import { generateState } from "arctic";
export const signInWithGithub = async (): Promise<OAuthAuthorizationData> => {
const state = generateState();
const url = github.createAuthorizationURL(state, ["user:email"]);
return { url: url.href, state };
};
Some OAuth providers require an extra codeVerifier
to be generated in that
case refer to signInWithGoogle
.
/src/lib/auth/o-auth.ts
import { generateCodeVerifier, generateState } from "arctic";
export const signInWithGoogle = async (): Promise<OAuthAuthorizationData> => {
const state = generateState();
const codeVerifier = generateCodeVerifier();
const url = google.createAuthorizationURL(state, codeVerifier, [
"profile",
"email",
]);
return { url: url.href, state, codeVerifier };
};
- Create a function to get the user information from GitHub and
return the data in the shape of
OAuthUser
type, GitHub's API endpoint ishttps://api.github.com
if you are adding a different provider always refer to their docs. It is usually straightforward to find.
/src/lib/auth/o-auth.ts
import axios from "axios";
import { GitHubUser, GitHubUserEmail, OAuthUser } from "types/auth";
const getGithubUser = async (access_token: string): Promise<OAuthUser> => {
const headers = { Authorization: `Bearer ${access_token}` };
const [userData, userEmails] = await Promise.all([
axios
.get<GitHubUser>("https://api.github.com/user", { headers })
.then((res) => res.data),
axios
.get<GitHubUserEmail[]>("https://api.github.com/user/emails", { headers })
.then((res) => res.data),
]);
const { id, avatar_url } = userData;
const { email, verified } = userEmails?.find((e) => e.primary === true) ?? {};
if (typeof email === "undefined" || typeof verified === "undefined") {
throw new Error("Unable to fetch github email.");
}
return {
email,
avatarUrl: avatar_url,
emailVerified: verified,
providerUserId: `${id}`,
};
};
Note
Github requires multiple calls to their API to get all user information we need
- Add the methods you have created to the below function
/src/lib/auth/o-auth.ts
export const generateOAuthAuthorizationData = async (
provider: OAuthLoginProvider,
): Promise<OAuthAuthorizationData> => {
switch (provider) {
//...
case OAuthLoginProvider.github:
return await signInWithGithub();
//...
}
};
export const getOAuthUser = (
provider: OAuthLoginProvider,
access_token: string,
): Promise<OAuthUser> => {
switch (provider) {
//...
case "github":
return getGithubUser(access_token);
//...
}
};
export const validateOAuthAuthorizationCode = async (
provider: OAuthLoginProvider,
url: URL,
) => {
//...
switch (provider) {
//...
case "github":
return await github.validateAuthorizationCode(code);
//...
}
//...
};
- All you need to do now is to head to
/src/components/authentication/auth-provider.tsx
and add the new provider to the list of providers
/src/components/authentication/auth-provider.tsx
const providers = [
//...
{
id: OAuthLoginProvider.github,
title: "Github",
Icon: Github,
},
];
🎉 Your new provider is ready!