GoodData
Integration Guides

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:3000

Set 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 profile

Use 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

  1. Click "Sign in with Google" on /login
  2. Complete OAuth flow on Google
  3. Verify redirect back to your app
  4. Check that user is logged in

Test Session Persistence

  1. Log in successfully
  2. Refresh the page
  3. Verify user stays logged in
  4. Check localStorage for token

Test Logout

  1. While logged in, click "Sign Out"
  2. Verify user is logged out
  3. 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 useSession correctly
  • 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:

  1. API Integration - Connect more backend features
  2. User Management - Handle user profiles and settings
  3. Protected Routes - Advanced route protection patterns
  4. 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.