Skip to main content

Chapter 6: Authentication System

In the previous chapter, we explored how ShipNowKit's Email System lets your application communicate with users through their inboxes. Now, let's discover how the Authentication System works to identify your users and secure your application.

Introduction

Imagine you're running an exclusive club. At the entrance, a bouncer checks IDs, maintains a guest list, and ensures only authorized people can enter certain areas. The bouncer remembers regulars and might even let VIPs skip the line.

Your web application's Authentication System works very similarly. It's responsible for:

  1. Verifying user identities (Who are you?)
  2. Managing user sessions (Yes, I remember you from earlier today)
  3. Controlling access to protected areas (Sorry, only premium members can enter this section)

ShipNowKit's Authentication System makes these complex tasks simple by using NextAuth.js, a powerful authentication library for Next.js applications.

The Problem: Secure and User-Friendly Authentication

Building authentication from scratch creates several challenges:

  • Securely handling passwords and user credentials
  • Supporting multiple login methods (email, social login)
  • Managing user sessions across page loads
  • Protecting routes that require authentication
  • Providing a smooth user experience

ShipNowKit's Authentication System Solution

ShipNowKit solves these problems with an integrated Authentication System that:

  1. Offers multiple login methods (Google, GitHub, magic links, etc.)
  2. Securely manages user sessions
  3. Provides simple components to protect routes
  4. Works seamlessly with the Email System for passwordless login

Let's see how to use it!

Key Components of the Authentication System

1. Authentication Setup

The authentication system is set up in auth.ts, which configures NextAuth.js:

// auth.ts (simplified)
import { PrismaAdapter } from "@auth/prisma-adapter";
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/github";
import ResendProvider from "next-auth/providers/resend";
import { prisma } from "@/db/client";

export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
pages: {
signIn: "/signin"
},
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
ResendProvider({
sendVerificationRequest: resendVerificationRequest
}),
// Other providers...
],
// Other configuration...
});

This code configures NextAuth with:

  • A database adapter (using Prisma)
  • Custom sign-in page location
  • Multiple authentication providers (Google, email magic links, etc.)

2. Authentication Provider

To make authentication available throughout your application, ShipNowKit includes an AuthProvider:

// providers/AuthProvider.tsx
'use client'

import { SessionProvider } from 'next-auth/react'

export default function AuthProvider({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>
}

This component wraps your application and provides authentication context to all components.

3. Sign-In and Sign-Out Components

ShipNowKit includes ready-to-use authentication components:

// components/auth/SignInButton.tsx (simplified)
"use client"
import { Button } from "@/components/ui/button"
import { signInAction } from "@/lib/actions/auth"

export function SignInButton({
provider,
callbackUrl,
children = "Sign In",
...props
}) {
return (
<Button
{...props}
onClick={async () => {
const redirectUrl = await signInAction(provider, callbackUrl)
if (redirectUrl) {
window.location.href = redirectUrl;
}
}}
>
{children}
</Button>
)
}

This button triggers the sign-in process with an optional provider (like "google" or "github").

// components/auth/SignOutButton.tsx
"use client"
import { Button } from "@/components/ui/button"
import { signOutAction } from "@/lib/actions/auth"

export function SignOutButton({ children = "Sign Out", ...props }) {
return (
<Button {...props}
onClick={async () => {
await signOutAction()
}}
>
{children}
</Button>
)
}

This button triggers the sign-out process when clicked.

4. Protected Routes Component

ShipNowKit makes it easy to protect routes that should only be accessible to authenticated users:

// components/auth/SignedIn.tsx
'use client'
import { authConfig } from "@/config"
import { useSession } from "next-auth/react"
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'

export function SignedIn({ children }: { children: React.ReactNode }) {
const { data: session } = useSession()
const router = useRouter()

useEffect(() => {
if (!session || !session.user) {
router.push(authConfig.signInPage)
}
}, [session, router])

if (!session) {
return null
}

return children
}

This component checks if the user is signed in. If not, it redirects to the sign-in page.

Using the Authentication System

Let's see how to use these components in your application:

1. Adding Authentication to Your App

First, add the AuthProvider to your application layout:

// app/layout.tsx
import AuthProvider from "@/providers/AuthProvider";

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
);
}

This makes authentication available throughout your application.

2. Creating a Sign-In Page

Let's create a simple sign-in page with email and social login:

// app/signin/page.tsx
import { SignInButton } from "@/components/auth/SignInButton";

export default function SignInPage() {
return (
<div className="container mx-auto p-8 max-w-md">
<h1 className="text-2xl font-bold mb-6">Sign In</h1>

<div className="space-y-4">
{/* Social login buttons */}
<SignInButton
provider="google"
className="w-full"
>
Continue with Google
</SignInButton>

<SignInButton
provider="github"
className="w-full"
>
Continue with GitHub
</SignInButton>

{/* Divider */}
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-background text-muted-foreground">
Or continue with email
</span>
</div>
</div>

{/* Email form */}
<EmailSignInForm />
</div>
</div>
);
}

And here's the email sign-in form component:

// components/EmailSignInForm.tsx
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { sendMagicLinkAction } from "@/lib/actions/auth";
import { useState } from "react";

export function EmailSignInForm() {
const [status, setStatus] = useState({ success: "", error: "" });

async function handleSubmit(formData: FormData) {
const result = await sendMagicLinkAction(formData, "resend");
setStatus(result);
}

return (
<form action={handleSubmit} className="space-y-4">
<div>
<Input
type="email"
name="email"
placeholder="Email address"
required
/>
</div>

{status.error && (
<p className="text-red-600 text-sm">{status.error}</p>
)}

{status.success && (
<p className="text-green-600 text-sm">{status.success}</p>
)}

<Button type="submit" className="w-full">
Send Magic Link
</Button>
</form>
);
}

This creates a sign-in page with:

  • Social login buttons (Google and GitHub)
  • An email form that sends a magic link

3. Creating Protected Routes

To create a route that only authenticated users can access:

// app/dashboard/page.tsx
import { SignedIn } from "@/components/auth/SignedIn";
import { SignOutButton } from "@/components/auth/SignOutButton";

export default function DashboardPage() {
return (
<SignedIn>
<div className="container mx-auto p-8">
<div className="flex justify-between items-center mb-8">
<h1 className="text-2xl font-bold">Dashboard</h1>
<SignOutButton />
</div>

<div className="bg-card p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">Welcome to your dashboard!</h2>
<p>This page is only visible to authenticated users.</p>
</div>
</div>
</SignedIn>
);
}

The SignedIn component ensures this page is only accessible to authenticated users. If a visitor isn't signed in, they'll be redirected to the sign-in page.

4. Accessing User Information

You can access information about the current user in client components:

// components/UserProfile.tsx
"use client";
import { useSession } from "next-auth/react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";

export function UserProfile() {
const { data: session } = useSession();

if (!session?.user) {
return null;
}

return (
<div className="flex items-center gap-3">
<Avatar>
<AvatarImage src={session.user.image || ""} />
<AvatarFallback>
{session.user.name?.charAt(0) || "U"}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">{session.user.name}</p>
<p className="text-sm text-muted-foreground">{session.user.email}</p>
</div>
</div>
);
}

This component displays the user's avatar, name, and email if they're signed in.

How the Authentication System Works Under the Hood

Let's visualize the authentication flow:

When a user logs in:

  1. They click a sign-in button in your application
  2. The authentication system redirects them to the provider (like Google)
  3. After they authenticate with the provider, they return to your app
  4. The authentication system verifies their identity and creates a session
  5. The user is redirected to the appropriate page in your application

Session Management

NextAuth.js handles session management using JSON Web Tokens (JWTs) or database sessions. ShipNowKit uses the JWT strategy for better performance:

// From auth.ts
session: { strategy: "jwt" },
callbacks: {
async jwt({ token, user }) {
if (user) {
token.userId = user.id;
}
return token;
},

async session({ session, token }) {
return {
...session,
user: {
...session.user,
id: token.userId
},
};
},
}

This code:

  1. Sets the session strategy to "jwt"
  2. Adds the user ID to the JWT token
  3. Adds the user ID to the session object for easy access

When a user navigates your site:

  1. The JWT is sent with each request (via a cookie)
  2. NextAuth.js verifies the token's validity
  3. If valid, the user remains authenticated
  4. If invalid, the user is treated as unauthenticated

Advanced Authentication Features

1. Checking Authentication Status in Server Components

You can check if a user is authenticated in server components:

// app/profile/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";

export default async function ProfilePage() {
const session = await auth();

if (!session) {
redirect("/signin");
}

return (
<div>
<h1>Welcome, {session.user.name}!</h1>
<p>Your email: {session.user.email}</p>
</div>
);
}

This checks authentication status on the server before rendering the page.

2. Custom Redirect After Authentication

You can customize where users are redirected after authentication:

// components/SignInButtonWithRedirect.tsx
import { SignInButton } from "@/components/auth/SignInButton";

export function SignInButtonWithRedirect({ redirectTo = "/dashboard" }) {
return (
<SignInButton
provider="google"
callbackUrl={redirectTo}
>
Sign In with Google
</SignInButton>
);
}

This button will redirect users to the specified URL after they sign in.

Conclusion

ShipNowKit's Authentication System provides a comprehensive solution for identifying and managing users in your application. By using NextAuth.js and integrating with the Email System and Database Services, it offers a secure, flexible, and user-friendly authentication experience.

With just a few components, you can implement various authentication methods (social login, magic links) and protect routes that require authentication. The system handles the complex security aspects, so you can focus on building your application's features.

In the next chapter, we'll explore the Payment Integration System, which allows you to accept payments and manage subscriptions in your application.