Authentication
Complete authentication system powered by NextAuth.js with support for OAuth providers and credentials.
Overview
ShadPanel includes a pre-configured authentication system with:
- ✅ NextAuth.js v5 - Industry-standard authentication
- ✅ Multiple Providers - Google, GitHub, credentials (email/password)
- ✅ Session Management - Automatic session handling
- ✅ Protected Routes - Easy route protection
- ✅ Login/Signup Pages - Pre-built authentication UI
- ✅ Type Safety - Full TypeScript support
- ✅ Customizable - Extend with your own providers
Quick Start
Enable Authentication
When initializing a project, include authentication:
# Full panel with auth (default)
shadpanel init my-app
# Auth + components only
shadpanel init my-app --auth-components
# Choose specific providers
shadpanel init my-app --google --github --credentialsConfigure Environment Variables
Edit .env file:
# Required for NextAuth
NEXTAUTH_SECRET=your-secret-here
NEXTAUTH_URL=http://localhost:3000
# Google OAuth (optional)
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# GitHub OAuth (optional)
GITHUB_ID=your-github-id
GITHUB_SECRET=your-github-secretAuthentication Providers
Credentials (Email/Password)
Default provider for email/password authentication.
Configuration:
// app/api/auth/[...nextauth]/route.ts
import CredentialsProvider from "next-auth/providers/credentials"
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
// Implement your authentication logic
const user = await authenticateUser(credentials.email, credentials.password)
if (user) {
return user
}
return null
}
})
]Login Page:
Pre-built at /admin/login
Signup Page:
Pre-built at /admin/signup
Google OAuth
Sign in with Google accounts.
Setup:
-
Create OAuth credentials:
- Go to Google Cloud Console
- Create a new project or select existing
- Navigate to “APIs & Services” → “Credentials”
- Create “OAuth 2.0 Client ID”
- Add authorized redirect URI:
http://localhost:3000/api/auth/callback/google
-
Add to
.env:GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=your-client-secret -
Configuration:
import GoogleProvider from "next-auth/providers/google" providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }) ]
GitHub OAuth
Sign in with GitHub accounts.
Setup:
-
Create OAuth App:
- Go to GitHub Settings
- Navigate to “OAuth Apps” → “New OAuth App”
- Set Homepage URL:
http://localhost:3000 - Set Authorization callback URL:
http://localhost:3000/api/auth/callback/github
-
Add to
.env:GITHUB_ID=your-github-client-id GITHUB_SECRET=your-github-client-secret -
Configuration:
import GitHubProvider from "next-auth/providers/github" providers: [ GitHubProvider({ clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET!, }) ]
Using Authentication
Check Session
'use client'
import { useSession } from 'next-auth/react'
export default function ProfilePage() {
const { data: session, status } = useSession()
if (status === 'loading') {
return <div>Loading...</div>
}
if (status === 'unauthenticated') {
return <div>Access Denied</div>
}
return (
<div>
<h1>Welcome, {session?.user?.name}</h1>
<p>Email: {session?.user?.email}</p>
</div>
)
}Sign In/Out
'use client'
import { signIn, signOut, useSession } from 'next-auth/react'
import { Button } from '@/components/ui/button'
export default function AuthButton() {
const { data: session } = useSession()
if (session) {
return (
<div className="flex items-center gap-4">
<span>Signed in as {session.user?.email}</span>
<Button onClick={() => signOut()}>Sign Out</Button>
</div>
)
}
return (
<div className="flex gap-2">
<Button onClick={() => signIn('google')}>
Sign in with Google
</Button>
<Button onClick={() => signIn('github')}>
Sign in with GitHub
</Button>
<Button onClick={() => signIn('credentials')}>
Sign in with Email
</Button>
</div>
)
}Protected Pages
Client Component
'use client'
import { useSession } from 'next-auth/react'
import { redirect } from 'next/navigation'
export default function ProtectedPage() {
const { status } = useSession({
required: true,
onUnauthenticated() {
redirect('/admin/login')
},
})
if (status === 'loading') {
return <div>Loading...</div>
}
return <div>Protected Content</div>
}Server Component
import { getServerSession } from 'next-auth'
import { redirect } from 'next/navigation'
import { authOptions } from '@/app/api/auth/[...nextauth]/route'
export default async function ProtectedPage() {
const session = await getServerSession(authOptions)
if (!session) {
redirect('/admin/login')
}
return <div>Protected Content</div>
}Middleware Protection
// middleware.ts
import { withAuth } from 'next-auth/middleware'
export default withAuth({
callbacks: {
authorized: ({ token }) => !!token,
},
})
export const config = {
matcher: ['/admin/dashboard/:path*'],
}Pre-built Auth Pages
Login Page
Located at /admin/login:
// app/admin/login/page.tsx
import { LoginForm } from '@/components/login-form'
export default function LoginPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<LoginForm />
</div>
)
}Features:
- Email/password input
- OAuth provider buttons
- “Remember me” checkbox
- “Forgot password” link
- Link to signup page
- Form validation
- Error handling
Signup Page
Located at /admin/signup:
// app/admin/signup/page.tsx
import { SignupForm } from '@/components/signup-form'
export default function SignupPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<SignupForm />
</div>
)
}Features:
- Name, email, password inputs
- Password confirmation
- OAuth provider buttons
- Form validation
- Link to login page
- Error handling
Session Management
Access User Data
const { data: session } = useSession()
// User info
session?.user?.name
session?.user?.email
session?.user?.image
// Custom fields (if added)
session?.user?.id
session?.user?.roleUpdate Session
import { useSession } from 'next-auth/react'
const { data: session, update } = useSession()
// Update session data
await update({
...session,
user: {
...session?.user,
name: 'New Name',
},
})Session Callbacks
Customize session data:
// app/api/auth/[...nextauth]/route.ts
callbacks: {
async session({ session, token }) {
// Add custom fields to session
if (session.user) {
session.user.id = token.sub
session.user.role = token.role
}
return session
},
async jwt({ token, user }) {
// Add custom fields to token
if (user) {
token.role = user.role
}
return token
},
}Customization
Custom Login Page
Replace the default login form:
'use client'
import { signIn } from 'next-auth/react'
import { Form, FormInput } from '@/components/ui'
import { Button } from '@/components/ui/button'
export default function CustomLoginPage() {
return (
<div className="max-w-md mx-auto">
<h1 className="text-2xl font-bold mb-4">Sign In</h1>
<Form
initialValues={{ email: '', password: '' }}
onSubmit={async (values) => {
const result = await signIn('credentials', {
email: values.email,
password: values.password,
redirect: false,
})
if (result?.error) {
console.error(result.error)
} else {
window.location.href = '/admin/dashboard'
}
}}
>
<FormInput
accessor="email"
label="Email"
type="email"
required
/>
<FormInput
accessor="password"
label="Password"
type="password"
required
/>
<Button type="submit" className="w-full">
Sign In
</Button>
</Form>
<div className="mt-4 space-y-2">
<Button
variant="outline"
className="w-full"
onClick={() => signIn('google')}
>
Sign in with Google
</Button>
<Button
variant="outline"
className="w-full"
onClick={() => signIn('github')}
>
Sign in with GitHub
</Button>
</div>
</div>
)
}Add Custom Provider
import type { Provider } from 'next-auth/providers'
import TwitterProvider from 'next-auth/providers/twitter'
const providers: Provider[] = [
TwitterProvider({
clientId: process.env.TWITTER_CLIENT_ID!,
clientSecret: process.env.TWITTER_CLIENT_SECRET!,
}),
// ... other providers
]Custom Authentication Logic
CredentialsProvider({
async authorize(credentials) {
try {
// Your custom logic
const response = await fetch('https://your-api.com/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: credentials.email,
password: credentials.password,
}),
})
const user = await response.json()
if (response.ok && user) {
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
}
}
return null
} catch (error) {
return null
}
}
})Database Integration
With Prisma
Store users and sessions in database:
// prisma/schema.prisma
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
password String? // For credentials
role String @default("user")
accounts Account[]
sessions Session[]
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}Configure adapter:
import { PrismaAdapter } from "@auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
export const authOptions = {
adapter: PrismaAdapter(prisma),
// ... rest of config
}Security Best Practices
1. Strong Secrets
Generate secure NEXTAUTH_SECRET:
openssl rand -base64 322. HTTPS in Production
# .env.production
NEXTAUTH_URL=https://yourdomain.com3. Secure Cookies
cookies: {
sessionToken: {
name: `__Secure-next-auth.session-token`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: true
}
}
}4. Rate Limiting
Implement rate limiting on auth routes:
// middleware.ts
import { Ratelimit } from "@upstash/ratelimit"
const ratelimit = new Ratelimit({
// ... config
})
export async function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/auth')) {
const { success } = await ratelimit.limit(request.ip)
if (!success) {
return new Response('Too Many Requests', { status: 429 })
}
}
}Troubleshooting
”NEXTAUTH_SECRET is not set”
Add to .env:
NEXTAUTH_SECRET=$(openssl rand -base64 32)OAuth Redirect URI Mismatch
Ensure redirect URIs match in provider settings:
- Development:
http://localhost:3000/api/auth/callback/[provider] - Production:
https://yourdomain.com/api/auth/callback/[provider]
Session Not Persisting
Check that SessionProvider wraps your app:
// app/layout.tsx
import { SessionProvider } from 'next-auth/react'
export default function RootLayout({ children }) {
return (
<html>
<body>
<SessionProvider>{children}</SessionProvider>
</body>
</html>
)
}Next Steps
- Database Integration - Store users in database
- Protected Routes - Secure your pages
- NextAuth.js Docs - Official documentation