Authentication
GoodData provides a complete authentication system with JWT tokens, social login, and session management. This guide covers everything you need to implement secure user authentication.
Overview
The authentication system includes:
- JWT-based authentication with automatic token refresh
- Google OAuth integration for social login
- Protected routes with automatic redirects
- Session management with React hooks
- Secure token storage in localStorage
- Frontend-only implementation that works with any backend
Authentication Flow
sequenceDiagram
participant U as User
participant F as Frontend
participant B as Backend
U->>F: Click "Sign In"
F->>B: POST /auth/google (OAuth)
B->>F: Redirect with token
F->>F: Store token in localStorage
F->>B: GET /auth/me (verify)
B->>F: Return user data
F->>U: Show authenticated state
Quick Start
Configure Environment
Set your API base URL in .env:
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000/api
NEXT_PUBLIC_APP_URL=http://localhost:3000Set Up Backend Endpoints
Your backend must implement these authentication endpoints:
POST /auth/login # Email/password login
POST /auth/register # User registration
POST /auth/google # Google OAuth
POST /auth/logout # Session logout
POST /auth/refresh # Token refresh
GET /auth/me # Get user profileUse Authentication in Components
import { useSession, signOut } from '@/lib/auth-client';
export function UserProfile() {
const { user, isLoading } = useSession();
if (isLoading) return <div>Loading...</div>;
if (!user) return <div>Not authenticated</div>;
return (
<div>
<p>Welcome, {user.name}!</p>
<button onClick={() => signOut()}>Sign Out</button>
</div>
);
}Backend API Requirements
Authentication Endpoints
Your backend API must implement these endpoints with the specified request/response formats:
Login with Email/Password
POST /auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "password123"
}
Response:
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "user_123",
"email": "user@example.com",
"name": "John Doe",
"avatar": "https://example.com/avatar.jpg"
}
}
Google OAuth
GET /auth/google?redirect=https://yourapp.com/dashboard
This should redirect to Google OAuth and then back to your frontend with a token in the URL or handle the OAuth flow server-side.
Get Current User
GET /auth/me
Authorization: Bearer <jwt-token>
Response:
{
"success": true,
"user": {
"id": "user_123",
"email": "user@example.com",
"name": "John Doe",
"avatar": "https://example.com/avatar.jpg"
}
}
Token Refresh
POST /auth/refresh
Authorization: Bearer <jwt-token>
Response:
{
"success": true,
"token": "new_jwt_token_here"
}
Frontend Implementation
Using the Auth Hook
The useSession hook provides reactive authentication state:
import { useSession } from '@/lib/auth-client';
export function MyComponent() {
const { data: session, user, isPending } = useSession();
if (isPending) {
return <div>Loading...</div>;
}
if (!user) {
return <div>Please sign in</div>;
}
return (
<div>
<h1>Welcome, {user.name}!</h1>
<p>Email: {user.email}</p>
</div>
);
}
Manual Authentication
For custom login forms, use the auth methods directly:
import { signIn, signOut, getSession } from '@/lib/auth-client';
// Email/password login
const handleLogin = async () => {
const result = await signIn({
email: 'user@example.com',
password: 'password123'
});
if (result.success) {
// User is now logged in
console.log('Login successful');
} else {
console.error('Login failed:', result.error);
}
};
// Sign out
const handleLogout = async () => {
await signOut();
// User is now logged out
};
// Get current session
const session = getSession();
if (session) {
console.log('User is logged in:', session.user);
}
Social Login
For Google OAuth, use the social sign-in method:
import { socialSignIn } from '@/lib/auth-client';
const handleGoogleLogin = async () => {
await socialSignIn.social({
provider: 'google',
callbackURL: '/dashboard' // Where to redirect after login
});
// This will redirect to your backend's Google OAuth endpoint
};
Protected Routes
Page-Level Protection
Protect entire pages by checking authentication in the component:
'use client';
import { useSession } from '@/lib/auth-client';
import { redirect } from 'next/navigation';
import { useEffect } from 'react';
export default function DashboardPage() {
const { user, isPending } = useSession();
useEffect(() => {
if (!isPending && !user) {
redirect('/login');
}
}, [user, isPending]);
if (isPending) {
return <div>Loading...</div>;
}
if (!user) {
return null; // Will redirect
}
return (
<div>
<h1>Dashboard</h1>
<p>Welcome to your dashboard, {user.name}!</p>
</div>
);
}
Component-Level Protection
Protect specific components or features:
import { useSession } from '@/lib/auth-client';
export function PremiumFeature() {
const { user } = useSession();
if (!user) {
return (
<div className="p-4 border border-dashed">
<p>Sign in to access this feature</p>
<a href="/login">Sign In</a>
</div>
);
}
return (
<div>
<h3>Premium Feature</h3>
<p>This is only visible to authenticated users!</p>
</div>
);
}
Route Guard Hook
Create a reusable hook for route protection:
import { useSession } from '@/lib/auth-client';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
export function useAuthGuard(redirectTo: string = '/login') {
const { user, isPending } = useSession();
const router = useRouter();
useEffect(() => {
if (!isPending && !user) {
router.push(redirectTo);
}
}, [user, isPending, redirectTo, router]);
return { user, isPending, isAuthenticated: !!user };
}
// Usage
export default function ProtectedPage() {
const { isAuthenticated, isPending } = useAuthGuard();
if (isPending) return <div>Loading...</div>;
if (!isAuthenticated) return null;
return <div>Protected content</div>;
}
Error Handling
Authentication Errors
Handle authentication errors gracefully:
import { api, handleAPIError } from '@/lib/api';
import { useState } from 'react';
export function LoginForm() {
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (data: { email: string; password: string }) => {
setIsLoading(true);
setError('');
try {
const result = await api.login(data);
// Handle success - user is automatically logged in
console.log('Login successful');
} catch (err) {
setError(handleAPIError(err));
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
{error && <div className="text-red-500">{error}</div>}
<button disabled={isLoading} type="submit">
{isLoading ? 'Signing in...' : 'Sign In'}
</button>
</form>
);
}
Token Expiration
The auth client automatically handles token refresh, but you can also handle expiration manually:
import { authClient } from '@/lib/auth-client';
// Check if token needs refresh
const refreshIfNeeded = async () => {
const session = authClient.getSession();
if (!session) return;
// Implement your token expiration logic
const isExpiringSoon = /* check token expiration */;
if (isExpiringSoon) {
const result = await authClient.refreshSession();
if (!result.success) {
// Refresh failed, redirect to login
window.location.href = '/login';
}
}
};
Customization
Custom Login Page
Create a custom login page with your own styling:
'use client';
import { useState } from 'react';
import { signIn, socialSignIn } from '@/lib/auth-client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError('');
const result = await signIn({ email, password });
if (!result.success) {
setError(result.error || 'Login failed');
}
setIsLoading(false);
};
const handleGoogleLogin = () => {
socialSignIn.social({
provider: 'google',
callbackURL: '/dashboard'
});
};
return (
<div className="max-w-md mx-auto mt-8 p-6">
<h1 className="text-2xl font-bold mb-6">Sign In</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<Input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<Input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
{error && <p className="text-red-500 text-sm">{error}</p>}
<Button type="submit" disabled={isLoading} className="w-full">
{isLoading ? 'Signing in...' : 'Sign In'}
</Button>
</form>
<div className="mt-4">
<Button
onClick={handleGoogleLogin}
variant="outline"
className="w-full"
>
Continue with Google
</Button>
</div>
</div>
);
}
User Avatar Component
Create a user avatar component that works with the auth system:
import { useSession } from '@/lib/auth-client';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
interface UserAvatarProps {
className?: string;
size?: 'sm' | 'md' | 'lg';
}
export function UserAvatar({ className, size = 'md' }: UserAvatarProps) {
const { user } = useSession();
if (!user) return null;
const sizeClasses = {
sm: 'h-6 w-6',
md: 'h-8 w-8',
lg: 'h-12 w-12'
};
return (
<Avatar className={`${sizeClasses[size]} ${className}`}>
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback>
{user.name?.charAt(0) || user.email.charAt(0)}
</AvatarFallback>
</Avatar>
);
}
Security Best Practices
Token Storage
Security Note: The current implementation stores JWT tokens in localStorage. For production applications, consider these security measures:
- Use httpOnly cookies for token storage
- Implement proper CSRF protection
- Use short-lived access tokens with refresh tokens
- Validate tokens on every request
API Security
Ensure your backend implements proper security:
// Backend validation example (Node.js/Express)
const jwt = require('jsonwebtoken');
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid token' });
}
req.user = user;
next();
});
};
Environment Security
# Use strong, random secrets in production
JWT_SECRET=your-super-secure-random-secret-here
JWT_EXPIRES_IN=24h
REFRESH_TOKEN_EXPIRES_IN=7d
# Use HTTPS in production
API_BASE_URL=https://api.yourapp.com
Testing Authentication
Testing Components
Test components that use authentication:
import { render, screen } from '@testing-library/react';
import { useSession } from '@/lib/auth-client';
import { UserProfile } from '@/components/user-profile';
// Mock the auth hook
jest.mock('@/lib/auth-client', () => ({
useSession: jest.fn()
}));
describe('UserProfile', () => {
it('shows loading state', () => {
(useSession as jest.Mock).mockReturnValue({
user: null,
isPending: true
});
render(<UserProfile />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('shows user info when authenticated', () => {
(useSession as jest.Mock).mockReturnValue({
user: { name: 'John Doe', email: 'john@example.com' },
isPending: false
});
render(<UserProfile />);
expect(screen.getByText('Welcome, John Doe!')).toBeInTheDocument();
});
});
Manual Testing
Test the authentication flow manually:
Test Google OAuth
- Click "Sign in with Google" on
/login - Complete OAuth flow on Google
- Verify redirect back to your app
- Check that user is logged in
Test Session Persistence
- Log in successfully
- Refresh the page
- Verify user stays logged in
- Check localStorage for token
Test Logout
- While logged in, click "Sign Out"
- Verify user is logged out
- Check that protected pages redirect to login
Troubleshooting
Common Issues
Google OAuth not working:
- Check that your backend Google OAuth is configured correctly
- Verify the redirect URL matches your frontend URL
- Ensure CORS is configured to allow your frontend domain
Token not persisting:
- Check browser localStorage in DevTools
- Verify the auth client is storing tokens correctly
- Make sure token refresh is working
API authentication errors:
- Verify your backend accepts Bearer tokens
- Check that the token format matches your backend expectations
- Ensure the API base URL is correct
Protected routes not working:
- Make sure you're using
useSessioncorrectly - Verify the redirect logic in your components
- Check that the auth state is updating properly
Debug Authentication
Add debugging to see what's happening:
import { authClient } from '@/lib/auth-client';
// Check current auth state
console.log('Current session:', authClient.getSession());
// Monitor auth state changes
authClient.subscribe((session) => {
console.log('Auth state changed:', session);
});
Next Steps
Now that you have authentication set up:
- API Integration - Connect more backend features
- User Management - Handle user profiles and settings
- Protected Routes - Advanced route protection patterns
- Deployment - Deploy your authenticated app
You're all set! Your authentication system is ready. Users can now sign in with Google OAuth or email/password, and you can protect any route or component in your application.