🚀 Oferta especial: 60% OFF no CrazyStack - Últimas vagas!Garantir vaga →
Módulo 2 - Aula 4

Autenticação NextAuth

Integre NextAuth.js com tRPC: configuração completa, providers OAuth, sessões seguras e proteção de rotas profissional.

65 min
Avançado
Autenticação

🎯 Por que NextAuth + tRPC é essencial para SaaS?

Segurança Robusta: Sessões seguras, CSRF protection e OAuth providers configurados automaticamente.

DX Excepcional: Tipos de usuário automaticamente disponíveis em todos os procedures tRPC.

🔧 Configuração NextAuth

src/server/auth/config.ts
// 📁 src/server/auth/config.ts
import { NextAuthOptions } from 'next-auth';
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import GoogleProvider from 'next-auth/providers/google';
import GithubProvider from 'next-auth/providers/github';
import CredentialsProvider from 'next-auth/providers/credentials';
import bcrypt from 'bcryptjs';
import { prisma } from '../db/client';

export const authOptions: NextAuthOptions = {
  // 🗄️ Adapter do Prisma para persistir sessões
  adapter: PrismaAdapter(prisma),

  // 🔐 Providers de autenticação
  providers: [
    // 🌐 Google OAuth
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      authorization: {
        params: {
          prompt: 'consent',
          access_type: 'offline',
          response_type: 'code',
        },
      },
    }),

    // 🐙 GitHub OAuth
    GithubProvider({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),

    // 📧 Email/Password
    CredentialsProvider({
      name: 'credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          return null;
        }

        // 🔍 Buscar usuário no banco
        const user = await prisma.user.findUnique({
          where: { email: credentials.email },
        });

        if (!user || !user.password) {
          return null;
        }

        // 🔒 Verificar senha
        const passwordValid = await bcrypt.compare(
          credentials.password,
          user.password
        );

        if (!passwordValid) {
          return null;
        }

        return {
          id: user.id,
          email: user.email,
          name: user.name,
          role: user.role,
        };
      },
    }),
  ],

  // 🎯 Configuração de sessão
  session: {
    strategy: 'jwt',
    maxAge: 30 * 24 * 60 * 60, // 30 dias
  },

  // 🔑 JWT configuration
  jwt: {
    secret: process.env.NEXTAUTH_SECRET,
    maxAge: 30 * 24 * 60 * 60, // 30 dias
  },

  // 📄 Páginas customizadas
  pages: {
    signIn: '/auth/signin',
    signUp: '/auth/signup',
    error: '/auth/error',
  },

  // 🎭 Callbacks para customizar sessão e JWT
  callbacks: {
    // 🔧 Customizar JWT token
    async jwt({ token, user, account }) {
      // 🆕 Primeiro login
      if (user) {
        token.id = user.id;
        token.role = user.role;
      }

      // 🔗 OAuth account linking
      if (account) {
        token.accessToken = account.access_token;
      }

      return token;
    },

    // 🔧 Customizar sessão
    async session({ session, token }) {
      if (token) {
        session.user.id = token.id as string;
        session.user.role = token.role as string;
      }

      return session;
    },

    // 🔐 Callback de redirect
    async redirect({ url, baseUrl }) {
      // ✅ Permite redirects relativos e para o mesmo origin
      if (url.startsWith('/')) return `${baseUrl}${url}`;
      if (new URL(url).origin === baseUrl) return url;
      return baseUrl;
    },
  },

  // 🔒 Security settings
  cookies: {
    sessionToken: {
      name: 'next-auth.session-token',
      options: {
        httpOnly: true,
        sameSite: 'lax',
        path: '/',
        secure: process.env.NODE_ENV === 'production',
      },
    },
  },

  // 📝 Events e debugging
  events: {
    async signIn({ user, account, profile }) {
      console.log(`✅ User signed in: ${user.email}`);
    },
    async signOut({ session }) {
      console.log(`👋 User signed out: ${session?.user?.email}`);
    },
  },

  // 🐛 Debug em desenvolvimento
  debug: process.env.NODE_ENV === 'development',
};

🌐 API Route NextAuth

src/app/api/auth/[...nextauth]/route.ts
// 📁 src/app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import { authOptions } from '@/server/auth/config';

// 🎯 Handler para todas as rotas de auth
const handler = NextAuth(authOptions);

// 📤 Exportar para GET e POST
export { handler as GET, handler as POST };

🏷️ Tipos TypeScript para NextAuth

src/types/next-auth.d.ts
// 📁 src/types/next-auth.d.ts
import { type DefaultSession } from 'next-auth';
import { type Role } from '@prisma/client';

// 🎯 Extend dos tipos padrão do NextAuth
declare module 'next-auth' {
  interface Session {
    user: {
      id: string;
      role: Role;
    } & DefaultSession['user'];
  }

  interface User {
    id: string;
    role: Role;
  }
}

declare module 'next-auth/jwt' {
  interface JWT {
    id: string;
    role: Role;
  }
}

🔌 Provider Component

src/components/providers/auth-provider.tsx
// 📁 src/components/providers/auth-provider.tsx
'use client';

import { SessionProvider } from 'next-auth/react';
import { type ReactNode } from 'react';

interface AuthProviderProps {
  children: ReactNode;
}

export function AuthProvider({ children }: AuthProviderProps) {
  return (
    <SessionProvider
      // 🔄 Refetch session every 5 minutes
      refetchInterval={5 * 60}
      
      // 🔄 Refetch when window gains focus
      refetchOnWindowFocus={true}
    >
      {children}
    </SessionProvider>
  );
}

// 📁 src/app/layout.tsx (atualização)
import { AuthProvider } from '@/components/providers/auth-provider';
import { TrpcProvider } from '@/components/providers/trpc-provider';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="pt-BR">
      <body>
        <AuthProvider>
          <TrpcProvider>
            {children}
          </TrpcProvider>
        </AuthProvider>
      </body>
    </html>
  );
}

🪝 Hooks e Utilitários de Auth

src/hooks/use-auth.ts
// 📁 src/hooks/use-auth.ts
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

// 🔐 Hook para rotas protegidas
export function useRequireAuth(redirectTo = '/auth/signin') {
  const { data: session, status } = useSession();
  const router = useRouter();

  useEffect(() => {
    if (status === 'loading') return; // Ainda carregando

    if (!session) {
      router.push(redirectTo);
    }
  }, [session, status, router, redirectTo]);

  return {
    session,
    isLoading: status === 'loading',
    isAuthenticated: !!session,
  };
}

// 👑 Hook para rotas de admin
export function useRequireAdmin(redirectTo = '/dashboard') {
  const { session, isLoading, isAuthenticated } = useRequireAuth();
  const router = useRouter();

  useEffect(() => {
    if (isLoading) return;
    
    if (isAuthenticated && session?.user?.role !== 'ADMIN') {
      router.push(redirectTo);
    }
  }, [session, isLoading, isAuthenticated, router, redirectTo]);

  return {
    session,
    isLoading,
    isAuthenticated,
    isAdmin: session?.user?.role === 'ADMIN',
  };
}

// 📁 src/lib/auth-utils.ts
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/server/auth/config';
import { type GetServerSidePropsContext } from 'next';

// 🔧 Utility para buscar sessão no servidor
export async function getServerAuthSession(ctx?: {
  req: GetServerSidePropsContext['req'];
  res: GetServerSidePropsContext['res'];
}) {
  return await getServerSession(ctx?.req, ctx?.res, authOptions);
}

// 🛡️ HOC para proteção de páginas
export function withAuth<T extends object>(
  WrappedComponent: React.ComponentType<T>,
  options: {
    requireAdmin?: boolean;
    redirectTo?: string;
  } = {}
) {
  return function AuthenticatedComponent(props: T) {
    const { requireAdmin = false, redirectTo = '/auth/signin' } = options;
    
    if (requireAdmin) {
      const { session, isLoading } = useRequireAdmin(redirectTo);
      
      if (isLoading) {
        return <div>Carregando...</div>;
      }
      
      if (!session) {
        return null;
      }
    } else {
      const { session, isLoading } = useRequireAuth(redirectTo);
      
      if (isLoading) {
        return <div>Carregando...</div>;
      }
      
      if (!session) {
        return null;
      }
    }

    return <WrappedComponent {...props} />;
  };
}

🚀 Router de Autenticação tRPC

src/server/trpc/routers/auth.ts
// 📁 src/server/trpc/routers/auth.ts
import { z } from 'zod';
import bcrypt from 'bcryptjs';
import { TRPCError } from '@trpc/server';
import { router, publicProcedure, protectedProcedure } from '../trpc';

export const authRouter = router({
  // 📝 Registrar novo usuário
  register: publicProcedure
    .input(z.object({
      name: z.string().min(2).max(50),
      email: z.string().email(),
      password: z.string().min(8).max(100),
    }))
    .mutation(async ({ input, ctx }) => {
      const { name, email, password } = input;

      // 🔍 Verificar se usuário já existe
      const existingUser = await ctx.prisma.user.findUnique({
        where: { email },
      });

      if (existingUser) {
        throw new TRPCError({
          code: 'CONFLICT',
          message: 'Usuário com este email já existe',
        });
      }

      // 🔒 Hash da senha
      const hashedPassword = await bcrypt.hash(password, 12);

      // 💾 Criar usuário
      const user = await ctx.prisma.user.create({
        data: {
          name,
          email,
          password: hashedPassword,
        },
        select: {
          id: true,
          name: true,
          email: true,
          role: true,
          createdAt: true,
        },
      });

      return {
        user,
        message: 'Usuário criado com sucesso',
      };
    }),

  // 🔄 Atualizar senha
  changePassword: protectedProcedure
    .input(z.object({
      currentPassword: z.string(),
      newPassword: z.string().min(8).max(100),
    }))
    .mutation(async ({ input, ctx }) => {
      const { currentPassword, newPassword } = input;

      // 🔍 Buscar usuário atual
      const user = await ctx.prisma.user.findUnique({
        where: { id: ctx.session.user.id },
      });

      if (!user || !user.password) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: 'Usuário não encontrado',
        });
      }

      // 🔒 Verificar senha atual
      const isValidPassword = await bcrypt.compare(currentPassword, user.password);

      if (!isValidPassword) {
        throw new TRPCError({
          code: 'UNAUTHORIZED',
          message: 'Senha atual incorreta',
        });
      }

      // 🔒 Hash da nova senha
      const hashedNewPassword = await bcrypt.hash(newPassword, 12);

      // 🔄 Atualizar senha
      await ctx.prisma.user.update({
        where: { id: ctx.session.user.id },
        data: { password: hashedNewPassword },
      });

      return {
        message: 'Senha atualizada com sucesso',
      };
    }),

  // 👤 Buscar sessão atual
  getSession: publicProcedure
    .query(async ({ ctx }) => {
      return ctx.session;
    }),

  // 🗑️ Deletar conta
  deleteAccount: protectedProcedure
    .input(z.object({
      password: z.string(),
      confirmation: z.literal('DELETE_MY_ACCOUNT'),
    }))
    .mutation(async ({ input, ctx }) => {
      const { password, confirmation } = input;

      if (confirmation !== 'DELETE_MY_ACCOUNT') {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Confirmação incorreta',
        });
      }

      // 🔍 Buscar usuário
      const user = await ctx.prisma.user.findUnique({
        where: { id: ctx.session.user.id },
      });

      if (!user || !user.password) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: 'Usuário não encontrado',
        });
      }

      // 🔒 Verificar senha
      const isValidPassword = await bcrypt.compare(password, user.password);

      if (!isValidPassword) {
        throw new TRPCError({
          code: 'UNAUTHORIZED',
          message: 'Senha incorreta',
        });
      }

      // 🗑️ Deletar usuário (cascade delete configurado no Prisma)
      await ctx.prisma.user.delete({
        where: { id: ctx.session.user.id },
      });

      return {
        message: 'Conta deletada com sucesso',
      };
    }),
});

🎨 Componentes de Auth UI

src/components/auth/signin-form.tsx
// 📁 src/components/auth/signin-form.tsx
'use client';

import { useState } from 'react';
import { signIn, getSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';

export function SignInForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState('');
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsLoading(true);
    setError('');

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

      if (result?.error) {
        setError('Email ou senha incorretos');
      } else {
        // ✅ Login bem-sucedido, atualizar sessão
        await getSession();
        router.push('/dashboard');
      }
    } catch (error) {
      setError('Erro interno. Tente novamente.');
    } finally {
      setIsLoading(false);
    }
  };

  const handleOAuthSignIn = async (provider: 'google' | 'github') => {
    setIsLoading(true);
    await signIn(provider, { callbackUrl: '/dashboard' });
  };

  return (
    <div className="max-w-md mx-auto bg-gray-900 p-8 rounded-lg">
      <h2 className="text-2xl font-bold text-white mb-6">Entrar</h2>
      
      {error && (
        <div className="bg-red-900/20 border border-red-400/30 text-red-400 p-3 rounded mb-4">
          {error}
        </div>
      )}

      <form onSubmit={handleSubmit} className="space-y-4">
        <Input
          type="email"
          placeholder="Email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
          disabled={isLoading}
        />
        
        <Input
          type="password"
          placeholder="Senha"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
          disabled={isLoading}
        />

        <Button 
          type="submit" 
          className="w-full"
          disabled={isLoading}
        >
          {isLoading ? 'Entrando...' : 'Entrar'}
        </Button>
      </form>

      <div className="mt-6">
        <div className="text-center text-gray-400 mb-4">ou</div>
        
        <div className="space-y-2">
          <Button
            variant="outline"
            className="w-full"
            onClick={() => handleOAuthSignIn('google')}
            disabled={isLoading}
          >
            Continuar com Google
          </Button>
          
          <Button
            variant="outline"
            className="w-full"
            onClick={() => handleOAuthSignIn('github')}
            disabled={isLoading}
          >
            Continuar com GitHub
          </Button>
        </div>
      </div>
    </div>
  );
}

🔑 Variáveis de Ambiente

.env.local
# 📄 .env.local
# NextAuth
NEXTAUTH_SECRET="seu-secret-super-seguro-aqui"
NEXTAUTH_URL="http://localhost:3000"

# Google OAuth
GOOGLE_CLIENT_ID="seu-google-client-id"
GOOGLE_CLIENT_SECRET="seu-google-client-secret"

# GitHub OAuth
GITHUB_CLIENT_ID="seu-github-client-id" 
GITHUB_CLIENT_SECRET="seu-github-client-secret"

# Database
DATABASE_URL="postgresql://user:password@localhost:5432/saas_trpc"

✅ Vantagens NextAuth + tRPC

Type Safety:Dados do usuário tipados automaticamente em todos os procedures.

Segurança Automática:CSRF protection, secure cookies e session management.

OAuth Simples:Google, GitHub, Discord e outros providers com poucos cliques.

Middleware Integrado:Proteção de rotas automática em procedures protegidos.