07 - Authentication
Overview
Wavic WebApp uses NextAuth.js v4 for authentication, supporting:
- Email/Password credentials
- Google OAuth
- JWT-based sessions
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ AUTHENTICATION FLOW │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ Sign In │─────►│ NextAuth │─────►│ API │ │
│ │ Page │ │ Handler │ │ /user/ │ │
│ └─────────────┘ └──────┬──────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ JWT Token │ │
│ │ + Session │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Protected Routes │ │
│ │ (middleware.ts enforces authentication) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘Configuration Files
Auth Options
typescript
// src/app/api/auth/[...nextauth]/auth-options.ts
import { type NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import GoogleProvider from 'next-auth/providers/google';
export const authOptions: NextAuthOptions = {
pages: {
signIn: '/signin',
error: '/signin',
},
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, // 30 days
},
callbacks: {
async signIn({ user, account }) {
if (account?.provider === 'google') {
// Validate with backend
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/user/auth/google/callback?token=${account?.id_token}`
);
const dbUser = await res.json();
return !!dbUser?.user;
}
return true;
},
async jwt({ token, user, account, trigger, session }) {
if (account?.provider === 'credentials') {
if (user) return { ...token, ...user };
}
if (account?.provider === 'google') {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/user/auth/google/callback?token=${account?.id_token}`
);
const dbUser = await res.json();
if (dbUser) return { token: dbUser.token, user: dbUser.user };
}
if (trigger === 'update') {
return { ...token, ...session };
}
return token;
},
async session({ session, token }) {
if (token.user) session.user = token.user;
if (token.token) session.token = token.token;
if (token.iat) session.iat = token.iat;
if (token.exp) session.exp = token.exp;
return session;
},
},
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/user/signin`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
}
);
const data = await res.json();
if (data.token && data.user) {
return { token: data.token, user: data.user };
}
return null;
},
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
};Pages Options
typescript
// src/app/api/auth/[...nextauth]/pages-options.ts
export const pagesOptions = {
signIn: '/signin',
error: '/signin',
};Auth Provider
typescript
// src/app/api/auth/[...nextauth]/auth-provider.tsx
'use client';
import { SessionProvider } from 'next-auth/react';
export default function AuthProvider({
children,
session
}: {
children: React.ReactNode;
session: any;
}) {
return (
<SessionProvider session={session}>
{children}
</SessionProvider>
);
}Middleware (Route Protection)
typescript
// src/middleware.ts
import { pagesOptions } from '@/app/api/auth/[...nextauth]/pages-options';
import withAuth from 'next-auth/middleware';
export default withAuth({
pages: {
...pagesOptions,
},
});
export const config = {
// Protected routes
matcher: [
'/',
'/artist/:path*',
'/project/:path*',
'/wave/:path*',
'/profile',
'/blank',
'/transfers',
'/library',
'/storage',
'/user/:path*',
'/trash',
'/onboarding',
],
};Session Type Extensions
typescript
// types/next-auth.d.ts
import 'next-auth';
import { IUser } from './user';
declare module 'next-auth' {
interface Session {
user: IUser;
token: string;
iat: number;
exp: number;
jti: string;
}
}
declare module 'next-auth/jwt' {
interface JWT {
user: IUser;
token: string;
}
}Using Authentication
In Server Components
typescript
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options';
export default async function Page() {
const session = await getServerSession(authOptions);
if (!session) {
redirect('/signin');
}
return <Dashboard user={session.user} />;
}In Client Components
typescript
'use client';
import { useSession, signIn, signOut } from 'next-auth/react';
function UserMenu() {
const { data: session, status } = useSession();
if (status === 'loading') {
return <Loader />;
}
if (status === 'unauthenticated') {
return <Button onClick={() => signIn()}>Sign In</Button>;
}
return (
<div>
<span>{session.user.fullName}</span>
<Button onClick={() => signOut()}>Sign Out</Button>
</div>
);
}Getting Token for API Calls
typescript
'use client';
import { useSession } from 'next-auth/react';
function MyComponent() {
const { data: session } = useSession();
const fetchData = async () => {
const response = await fetch('/api/data', {
headers: {
Authorization: `Bearer ${session?.token}`,
},
});
return response.json();
};
// ...
}Sign In Page
typescript
// src/app/signin/page.tsx
'use client';
import { signIn } from 'next-auth/react';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function SignInPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const result = await signIn('credentials', {
email,
password,
redirect: false,
});
if (result?.error) {
setError('Invalid credentials');
} else {
router.push('/');
}
};
const handleGoogleSignIn = () => {
signIn('google', { callbackUrl: '/' });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{error && <p className="text-red-500">{error}</p>}
<button type="submit">Sign In</button>
<button type="button" onClick={handleGoogleSignIn}>
Sign in with Google
</button>
</form>
);
}Session Updates
To update session data (e.g., after profile changes):
typescript
import { useSession } from 'next-auth/react';
function ProfileForm() {
const { update } = useSession();
const handleProfileUpdate = async (newData) => {
// Update on server
await updateProfile(newData);
// Update session
await update({
user: {
...session.user,
...newData,
},
});
};
}Environment Variables
bash
# Required for NextAuth
NEXTAUTH_SECRET=your-secret-key-min-32-chars
NEXTAUTH_URL=http://localhost:3000
# Google OAuth (optional)
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# API
NEXT_PUBLIC_API_BASE_URL=http://localhost:8080Authentication Flow
Email/Password Sign In
- User enters credentials on
/signin signIn('credentials', {...})called- NextAuth calls
authorize()callback - Backend validates at
/user/signin - Backend returns JWT + user data
- NextAuth creates session
- User redirected to protected route
Google OAuth Sign In
- User clicks "Sign in with Google"
signIn('google')called- User redirected to Google consent screen
- Google redirects back with token
- NextAuth
signIncallback validates with backend - Backend at
/user/auth/google/callbackcreates/finds user - Session created, user redirected
Security Considerations
- JWT Secret: Use strong, random secret for
NEXTAUTH_SECRET - HTTPS: Always use HTTPS in production
- Token Expiry: Sessions expire after 30 days
- Protected Routes: Middleware enforces authentication
- API Authorization: All API calls include
Bearertoken