Authentication

Comprehensive guide to authentication in ShipKit using NextAuth v5

Authentication

ShipKit uses NextAuth.js v5 for secure, flexible authentication with support for multiple providers, session management, and activity logging.

Overview

The authentication system provides:

  • Multiple OAuth providers (GitHub, Discord, Google)
  • JWT-based session management
  • Activity logging with IP tracking
  • Type-safe configuration
  • Secure cookie handling
  • Route protection with middleware
  • Enhanced auth function with redirect capabilities

Core Files

The authentication system is organized into several key files:

  1. src/server/auth.ts - Main authentication setup and enhanced auth function
  2. src/server/auth.config.ts - Core configuration and callbacks
  3. src/server/auth.providers.ts - Provider configuration
  4. src/middleware.ts - Auth middleware for route protection
  5. src/server/services/activity-logger.ts - Activity logging service

Configuration

Core Setup

// src/server/auth.config.ts
import type { NextAuthConfig } from "next-auth";
import { providers } from "./auth.providers";

export const authConfig: NextAuthConfig = {
  trustHost: true,
  providers,
  pages: {
    signIn: "/login",
    signOut: "/logout",
    error: "/error",
    verifyRequest: "/verify",
  },
  session: { strategy: "jwt" },
  cookies: {
    sessionToken: {
      name: process.env.NODE_ENV === "production"
        ? "__Secure-next-auth.session-token"
        : "next-auth.session-token",
      options: {
        httpOnly: true,
        sameSite: "lax",
        path: "/",
        secure: process.env.NODE_ENV === "production",
      },
    },
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isProtected = nextUrl.pathname.startsWith("/dashboard");

      if (isProtected) {
        if (isLoggedIn) return true;
        return false; // Redirect to login
      }
      return true;
    },
  },
};

OAuth Providers

// src/server/auth.providers.ts
import Discord from "next-auth/providers/discord";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";

export const providers = [
  GitHub({
    clientId: env.AUTH_GITHUB_ID,
    clientSecret: env.AUTH_GITHUB_SECRET,
    authorization: {
      params: {
        scope: "read:user user:email",
      },
    },
    profile(profile) {
      return {
        id: profile.id.toString(),
        name: profile.name ?? profile.login,
        email: profile.email,
        image: profile.avatar_url,
        username: profile.login,
      };
    },
  }),
  Google({
    clientId: env.AUTH_GOOGLE_ID,
    clientSecret: env.AUTH_GOOGLE_SECRET,
    authorization: {
      params: {
        scope: "openid email profile",
        prompt: "consent",
      },
    },
  }),
  Discord({
    clientId: env.AUTH_DISCORD_ID,
    clientSecret: env.AUTH_DISCORD_SECRET,
    authorization: {
      params: {
        scope: "identify email",
      },
    },
  }),
].filter(Boolean);

Session Management

Extended Session Type

// src/types/next-auth.d.ts
import type { DefaultSession } from "next-auth";

declare module "next-auth" {
  interface Session {
    user: {
      id: string;
      username?: string | null;
      role?: "user" | "admin";
      teamId?: string | null;
      subscription?: {
        isActive: boolean;
        plan: string;
      } | null;
    } & DefaultSession["user"];
  }

  interface User {
    username?: string | null;
    role?: "user" | "admin";
    teamId?: string | null;
    subscription?: {
      isActive: boolean;
      plan: string;
    } | null;
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    id: string;
    username?: string | null;
    role?: "user" | "admin";
    teamId?: string | null;
    subscription?: {
      isActive: boolean;
      plan: string;
    } | null;
  }
}

Session Callbacks

// src/server/auth.config.ts
callbacks: {
  async signIn({ user, account, profile }) {
    // Log sign-in activity with IP tracking
    await logActivity({
      action: "sign_in",
      category: "auth",
      userId: user.id,
      details: `User signed in via ${account?.provider}`,
      metadata: {
        provider: account?.provider,
        email: user.email,
      },
    });
    return true;
  },

  async jwt({ token, user, trigger, session }) {
    if (user) {
      token.id = user.id;
      token.username = user.username;
      token.role = user.role;
      token.teamId = user.teamId;
      token.subscription = user.subscription;
    }

    // Handle session updates
    if (trigger === "update" && session) {
      return { ...token, ...session };
    }

    return token;
  },

  async session({ session, token }) {
    return {
      ...session,
      user: {
        ...session.user,
        id: token.id,
        username: token.username,
        role: token.role,
        teamId: token.teamId,
        subscription: token.subscription,
      },
    };
  },
}

Route Protection

Middleware

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

export async function middleware(request: NextRequest) {
  const session = await auth();
  const isProtected = request.nextUrl.pathname.startsWith("/dashboard");
  const isAuthPage = request.nextUrl.pathname.startsWith("/login");

  if (isProtected && !session) {
    const redirectUrl = new URL("/login", request.url);
    redirectUrl.searchParams.set("callbackUrl", request.url);
    return NextResponse.redirect(redirectUrl);
  }

  if (isAuthPage && session) {
    return NextResponse.redirect(new URL("/dashboard", request.url));
  }

  return NextResponse.next();
}

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

Usage Examples

Protected API Route

// src/app/api/protected/route.ts
import { auth } from "@/server/auth";
import { NextResponse } from "next/server";

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

  if (!session) {
    return new NextResponse(null, { status: 401 });
  }

  return NextResponse.json({
    user: session.user,
    message: "This is a protected API route",
  });
}

Protected Server Component

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

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

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

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

Client Component with Session

// src/components/user-menu.tsx
"use client";

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

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

  if (!session) {
    return <SignInButton />;
  }

  return (
    <div>
      <img src={session.user.image} alt={session.user.name} />
      <span>{session.user.name}</span>
      <SignOutButton />
    </div>
  );
}

Activity Logging

The authentication system integrates with the activity logger to track auth events:

// Example auth activity logging
await logActivity({
  action: "sign_in",
  category: "auth",
  userId: user.id,
  details: `User signed in via ${provider}`,
  metadata: {
    provider,
    email: user.email,
  },
});

Security Best Practices

  1. Environment Variables

    • Store all secrets securely
    • Use different OAuth apps for development/production
    • Validate environment variables at startup
  2. Session Security

    • Use secure, HTTP-only cookies
    • Implement proper CSRF protection
    • Set appropriate cookie expiration
  3. Route Protection

    • Use middleware for consistent auth checks
    • Implement role-based access control
    • Handle unauthorized access gracefully
  4. Activity Monitoring

    • Log all authentication events
    • Track IP addresses and user agents
    • Monitor for suspicious activity

Notes

  • All routes use the new App Router format
  • Session strategy is JWT-based for better performance
  • Activity logging is non-blocking for better UX
  • Route protection is handled by middleware
  • Environment variables are strictly typed