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
    
    Setting up dashboard? go back here

    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:

    1. Magic Link (Note: Limited by the cost of email service providers)
    2. OAuth (by default, Google and GitHub are implemented but you can add more)
    3. Username and Password
    4. 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";
    

    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

    1. Navigate to /src/lib/auth/index.ts
    2. 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

    1. Navigate to /src/types/auth.ts
    2. Add Github to OAuthLoginProvider enum

    /src/types/auth.ts

    export enum OAuthLoginProvider {
      //...
      github = "github",
    }
    
    1. Navigate to /src/lib/auth/o-auth.ts and import your desired provider from arctic and initialize it in this case we will import GitHub

    /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",
    );
    
    1. Create a function to generate the Authorization state and URL specific to github and make sure the return type matches OAuthAuthorizationData 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 };
    };
    
    1. Create a function to get the user information from GitHub and return the data in the shape of OAuthUser type, GitHub's API endpoint is https://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}`,
      };
    };
    
    1. 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);
        //...
      }
      //...
    };
    
    1. 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!