Authentication Flows

Comprehensive guide for implementing authentication flows in ShipKit

Authentication Flows

This guide covers the implementation of various authentication flows in ShipKit using NextAuth v5.

Social Login Setup

Google Authentication

// src/auth.ts
import Google from "next-auth/providers/google";

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      authorization: {
        params: {
          prompt: "consent",
          access_type: "offline",
          response_type: "code",
          scope: "openid email profile",
        },
      },
    }),
  ],
  callbacks: {
    async session({ session, user }) {
      if (session.user) {
        session.user.id = user.id;
      }
      return session;
    },
    async jwt({ token, user, account }) {
      if (user) {
        token.id = user.id;
      }
      if (account) {
        token.accessToken = account.access_token;
      }
      return token;
    },
  },
});

GitHub Authentication

import GitHub from "next-auth/providers/github";

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    GitHub({
      clientId: process.env.GITHUB_CLIENT_ID,
      clientSecret: process.env.GITHUB_CLIENT_SECRET,
      authorization: {
        params: {
          scope: "read:user user:email",
        },
      },
    }),
  ],
  // ... other configuration
});

Custom Authentication

Email/Password Authentication

// src/auth.ts
import Credentials from "next-auth/providers/credentials";
import { z } from "zod";
import { verifyPassword } from "@/lib/auth/password";
import { db } from "@/lib/db";
import { users } from "@/lib/db/schema";
import { eq } from "drizzle-orm";

const credentialsSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    Credentials({
      async authorize(credentials) {
        const parsedCredentials = credentialsSchema.safeParse(credentials);

        if (!parsedCredentials.success) {
          return null;
        }

        const { email, password } = parsedCredentials.data;
        const user = await db.query.users.findFirst({
          where: eq(users.email, email),
        });

        if (!user) {
          return null;
        }

        const isValid = await verifyPassword(password, user.password);

        if (!isValid) {
          return null;
        }

        return {
          id: user.id,
          email: user.email,
          name: user.name,
        };
      },
    }),
  ],
  pages: {
    signIn: "/login",
    error: "/login",
  },
});

Magic Link Authentication

// src/auth.ts
import Email from "next-auth/providers/email";
import { createTransport } from "nodemailer";
import { resend } from "@/lib/email";

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    Email({
      server: {
        host: process.env.SMTP_HOST,
        port: Number(process.env.SMTP_PORT),
        auth: {
          user: process.env.SMTP_USER,
          pass: process.env.SMTP_PASSWORD,
        },
      },
      from: "[email protected]",
      async sendVerificationRequest({
        identifier: email,
        url,
        provider: { server, from },
      }) {
        await resend.emails.send({
          from,
          to: email,
          subject: "Sign in to Your App",
          react: (
            <EmailTemplate
              url={url}
              email={email}
            />
          ),
        });
      },
    }),
  ],
  pages: {
    verifyRequest: "/auth/verify-request",
    error: "/auth/error",
  },
});

Session Management

Custom Session Handling

// src/middleware.ts
import { auth } from "@/auth";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
  const session = await auth();

  // Protected routes
  if (
    request.nextUrl.pathname.startsWith("/dashboard") &&
    !session
  ) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  // Role-based access
  if (
    request.nextUrl.pathname.startsWith("/admin") &&
    session?.user?.role !== "admin"
  ) {
    return NextResponse.redirect(new URL("/dashboard", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*", "/admin/:path*"],
};

Session Utilities

// src/lib/auth/session.ts
import { auth } from "@/auth";
import { redirect } from "next/navigation";

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

  if (!session?.user) {
    redirect("/login");
  }

  return session.user;
}

export async function checkRole(allowedRoles: string[]) {
  const session = await auth();

  if (!session?.user?.role || !allowedRoles.includes(session.user.role)) {
    redirect("/dashboard");
  }
}

Client-Side Authentication

React Hooks

// src/hooks/use-auth.ts
"use client";

import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";

export function useAuth(options: { required?: boolean } = {}) {
  const { data: session, status } = useSession();
  const router = useRouter();

  const isLoading = status === "loading";
  const isAuthenticated = !!session;

  if (options.required && !isLoading && !isAuthenticated) {
    router.push("/login");
  }

  return {
    session,
    isLoading,
    isAuthenticated,
  };
}

Protected Components

// src/components/auth/protected.tsx
"use client";

import { useAuth } from "@/hooks/use-auth";
import { Spinner } from "@/components/ui/spinner";

interface ProtectedProps {
  children: React.ReactNode;
  roles?: string[];
}

export function Protected({ children, roles }: ProtectedProps) {
  const { session, isLoading } = useAuth({ required: true });

  if (isLoading) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <Spinner />
      </div>
    );
  }

  if (roles && !roles.includes(session?.user?.role)) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <p>You don't have permission to view this content.</p>
      </div>
    );
  }

  return children;
}

Error Handling

Authentication Errors

// src/lib/auth/errors.ts
export class AuthError extends Error {
  constructor(
    message: string,
    public code: string,
    public status: number = 401
  ) {
    super(message);
    this.name = "AuthError";
  }
}

export const authErrors = {
  invalidCredentials: new AuthError(
    "Invalid email or password",
    "INVALID_CREDENTIALS"
  ),
  emailNotVerified: new AuthError(
    "Please verify your email address",
    "EMAIL_NOT_VERIFIED"
  ),
  accountDisabled: new AuthError(
    "Your account has been disabled",
    "ACCOUNT_DISABLED"
  ),
  sessionExpired: new AuthError(
    "Your session has expired",
    "SESSION_EXPIRED"
  ),
};

Error Components

// src/components/auth/error-message.tsx
"use client";

import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { XCircle } from "lucide-react";

interface AuthErrorMessageProps {
  error?: string;
  className?: string;
}

export function AuthErrorMessage({
  error,
  className,
}: AuthErrorMessageProps) {
  if (!error) return null;

  return (
    <Alert variant="destructive" className={className}>
      <XCircle className="h-4 w-4" />
      <AlertTitle>Authentication Error</AlertTitle>
      <AlertDescription>{error}</AlertDescription>
    </Alert>
  );
}

Best Practices

Security

  1. Password Security

    • Use strong password hashing (Argon2 or bcrypt)
    • Implement password complexity requirements
    • Add rate limiting for login attempts
  2. Session Security

    • Use secure, HTTP-only cookies
    • Implement proper CSRF protection
    • Regular session rotation
  3. OAuth Security

    • Validate OAuth state parameters
    • Use proper scopes
    • Secure client secrets

User Experience

  1. Loading States

    • Show loading indicators during authentication
    • Disable forms while submitting
    • Provide clear feedback
  2. Error Handling

    • Display user-friendly error messages
    • Guide users to resolve issues
    • Log errors for debugging
  3. Form Validation

    • Client-side validation
    • Clear validation messages
    • Proper form focus management

Examples

Complete Login Form

// src/components/auth/login-form.tsx
"use client";

import { useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { AuthErrorMessage } from "@/components/auth/error-message";

export function LoginForm() {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string>();
  const router = useRouter();

  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setIsLoading(true);
    setError(undefined);

    const formData = new FormData(event.currentTarget);
    const email = formData.get("email") as string;
    const password = formData.get("password") as string;

    try {
      const result = await signIn("credentials", {
        email,
        password,
        redirect: false,
      });

      if (result?.error) {
        setError(result.error);
        return;
      }

      router.push("/dashboard");
    } catch (error) {
      setError("An unexpected error occurred");
    } finally {
      setIsLoading(false);
    }
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <AuthErrorMessage error={error} className="mb-4" />

      <div className="space-y-2">
        <Input
          name="email"
          type="email"
          placeholder="Email"
          required
          disabled={isLoading}
        />
      </div>

      <div className="space-y-2">
        <Input
          name="password"
          type="password"
          placeholder="Password"
          required
          disabled={isLoading}
        />
      </div>

      <Button
        type="submit"
        className="w-full"
        disabled={isLoading}
      >
        {isLoading ? "Signing in..." : "Sign in"}
      </Button>
    </form>
  );
}

Related Resources