← Back to Main Page

Module 4: Build Sprint 3

Your Third Ticket

In this final build sprint, you'll complete the most challenging ticket yet, bringing your project to completion.

Your Third Ticket

Auth0 Authentication Integration

Your third ticket focuses on implementing authentication using Auth0. This popular identity management platform will enable you to add secure login functionality to your application.

Ticket Objectives

  • Integrate Auth0 microservice into the project
  • Create a Profile Page that only appears when logged in
  • Display user information on the Profile Page
  • Implement Login/Logout functionality in the navigation

Setting Up Auth0

The first step is to create an Auth0 account and configure your application:

  1. Create an Auth0 account and set up a Single Page Application
    // Key details you'll need from Auth0 dashboard:
    - Domain
    - Client ID
    - Callback URLs
    - Logout URLs
    - Web Origins
  2. Install the Auth0 SDK in your project
    npm install @auth0/auth0-react
    # or
    yarn add @auth0/auth0-react
  3. Configure the Auth0Provider component to wrap your application
    import { Auth0Provider } from '@auth0/auth0-react';
    import { useNavigate } from 'react-router-dom';
    
    export const Auth0ProviderWithConfig = ({ children }) => {
      const navigate = useNavigate();
      const domain = import.meta.env.VITE_AUTH0_DOMAIN;
      const clientId = import.meta.env.VITE_AUTH0_CLIENT_ID;
    
      const onRedirectCallback = (appState) => {
        navigate(appState?.returnTo || window.location.pathname);
      };
    
      return (
        <Auth0Provider
          domain={domain}
          clientId={clientId}
          authorizationParams={{
            redirect_uri: window.location.origin
          }}
          onRedirectCallback={onRedirectCallback}
        >
          {children}
        </Auth0Provider>
      );
    };
  4. Set up environment variables for your Auth0 configuration
    # .env file
    VITE_AUTH0_DOMAIN=your-domain.auth0.com
    VITE_AUTH0_CLIENT_ID=your-client-id

    Note: When using Vite, environment variables must start with VITE_ to be exposed to the client-side code.

Creating Login and Logout Components

import React from 'react';
import { useAuth0 } from '@auth0/auth0-react';

export const LoginButton = () => {
  const { loginWithRedirect, isAuthenticated } = useAuth0();

  return !isAuthenticated && (
    <button 
      onClick={() => loginWithRedirect()}
      className="px-4 py-2 text-white bg-blue-600 rounded hover:bg-blue-700 transition-colors"
    >
      Log In
    </button>
  );
};

export const LogoutButton = () => {
  const { logout, isAuthenticated } = useAuth0();

  return isAuthenticated && (
    <button 
      onClick={() => logout({ returnTo: window.location.origin })}
      className="px-4 py-2 text-white bg-red-600 rounded hover:bg-red-700 transition-colors"
    >
      Log Out
    </button>
  );
};

Styling with Tailwind CSS

Since your project is using Tailwind CSS, you can leverage its utility classes to style your Auth0 components. Here's how you might style the Profile page:

import React from 'react';
import { useAuth0 } from '@auth0/auth0-react';

const Profile = () => {
  const { user, isAuthenticated, isLoading } = useAuth0();

  if (isLoading) {
    return <div className="flex justify-center items-center p-8">
      <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
    </div>;
  }

  return (
    isAuthenticated && (
      <div className="max-w-2xl mx-auto bg-white shadow-lg rounded-lg overflow-hidden my-4">
        <div className="flex items-center p-6 bg-gray-100">
          <img 
            src={user.picture} 
            alt={user.name} 
            className="h-20 w-20 rounded-full object-cover mr-4 shadow"
          />
          <h2 className="text-2xl font-semibold text-gray-800">{user.name}</h2>
        </div>
        <div className="p-6 border-t border-gray-200">
          <p className="text-gray-700 mb-2">
            <span className="font-bold">Email:</span> {user.email}
          </p>
          {user.nickname && (
            <p className="text-gray-700 mb-2">
              <span className="font-bold">Nickname:</span> {user.nickname}
            </p>
          )}
          {user.sub && (
            <p className="text-gray-700 mb-2">
              <span className="font-bold">Auth0 ID:</span> 
              <span className="text-sm font-mono bg-gray-100 p-1 rounded">{user.sub}</span>
            </p>
          )}
          {user.updated_at && (
            <p className="text-gray-700">
              <span className="font-bold">Last Updated:</span> {new Date(user.updated_at).toLocaleDateString()}
            </p>
          )}
        </div>
      </div>
    )
  );
};

For the navigation, you can style it with Tailwind like this:

import { useAuth0 } from '@auth0/auth0-react';
import { Link } from 'react-router-dom';
import { LoginButton, LogoutButton } from './auth-buttons';

const Navigation = () => {
  const { isAuthenticated } = useAuth0();

  return (
    <nav className="bg-gray-800 text-white p-4">
      <div className="container mx-auto flex justify-between items-center">
        <div className="text-xl font-bold">Your App</div>
        <ul className="flex space-x-6">
          <li><Link to="/" className="hover:text-blue-300 transition-colors">Home</Link></li>
          <li><Link to="/about" className="hover:text-blue-300 transition-colors">About</Link></li>
          {isAuthenticated && (
            <li><Link to="/profile" className="hover:text-blue-300 transition-colors">Profile</Link></li>
          )}
          <li>
            {isAuthenticated ? <LogoutButton /> : <LoginButton />}
          </li>
        </ul>
      </div>
    </nav>
  );
};

Building the Profile Page

import React from 'react';
import { useAuth0 } from '@auth0/auth0-react';

const Profile = () => {
  const { user, isAuthenticated, isLoading } = useAuth0();

  if (isLoading) {
    return <div>Loading...</div>;
  }

  return (
    isAuthenticated && (
      <div className="profile-container">
        <div className="profile-header">
          <img src={user.picture} alt={user.name} />
          <h2>{user.name}</h2>
        </div>
        <div className="profile-details">
          <p><strong>Email:</strong> {user.email}</p>
          {user.nickname && (
            <p><strong>Nickname:</strong> {user.nickname}</p>
          )}
          {user.sub && (
            <p><strong>Auth0 ID:</strong> {user.sub}</p>
          )}
          {user.updated_at && (
            <p><strong>Last Updated:</strong> {new Date(user.updated_at).toLocaleDateString()}</p>
          )}
        </div>
      </div>
    )
  );
};

Protecting Routes

Create a wrapper component that only allows authenticated users to access certain routes:

import { useAuth0 } from '@auth0/auth0-react';
import { Navigate } from 'react-router-dom';

export const ProtectedRoute = ({ children }) => {
  const { isAuthenticated, isLoading } = useAuth0();

  if (isLoading) {
    return (
      <div className="flex justify-center items-center h-screen">
        <div className="animate-spin rounded-full h-16 w-16 border-t-4 border-blue-500"></div>
      </div>
    );
  }

  return isAuthenticated ? children : <Navigate to="/" replace />;
};

Use it in your route configuration:

<Routes>
  <Route path="/" element={<Home />} />
  <Route 
    path="/profile" 
    element={
      <ProtectedRoute>
        <Profile />
      </ProtectedRoute>
    } 
  />
</Routes>

Conditional Navigation Menu

Update your navigation menu to conditionally show options based on authentication status:

import { useAuth0 } from '@auth0/auth0-react';
import { LoginButton, LogoutButton } from './auth-buttons';

const Navigation = () => {
  const { isAuthenticated } = useAuth0();

  return (
    <nav>
      <ul>
        <li><Link to="/">Home</Link></li>
        <li><Link to="/about">About</Link></li>
        {isAuthenticated && (
          <li><Link to="/profile">Profile</Link></li>
        )}
        <li>
          {isAuthenticated ? <LogoutButton /> : <LoginButton />}
        </li>
      </ul>
    </nav>
  );
};

Security Best Practices

When implementing Auth0, remember these important security practices:

  • Never store sensitive tokens in localStorage (Auth0 SDK handles this safely)
  • Always validate tokens on your backend before granting access to protected resources
  • Set appropriate token expiration times
  • Use HTTPS for all communication between your app and Auth0
  • Implement proper CORS configuration on your backend

Advanced React Patterns

As you reach the advanced stages of your project, learning about sophisticated React patterns can help you create more maintainable code.

// Example of Compound Component Pattern
import React, { createContext, useContext, useState } from 'react';

// Create a context for the accordion
const AccordionContext = createContext();

// Main Accordion component
export function Accordion({ children, defaultIndex = 0 }) {
  const [activeIndex, setActiveIndex] = useState(defaultIndex);
  
  const toggleItem = (index) => {
    setActiveIndex(index === activeIndex ? null : index);
  };
  
  return (
    <AccordionContext.Provider value={{ activeIndex, toggleItem }}>
      <div className="accordion">
        {children}
      </div>
    </AccordionContext.Provider>
  );
}

// AccordionItem component
export function AccordionItem({ children, index }) {
  const { activeIndex, toggleItem } = useContext(AccordionContext);
  const isActive = activeIndex === index;
  
  return (
    <div className={`accordion-item ${isActive ? 'active' : ''}`}>
      {React.Children.map(children, child => {
        return React.cloneElement(child, { isActive, index, toggleItem });
      })}
    </div>
  );
}

// AccordionHeader component
export function AccordionHeader({ children, isActive, index, toggleItem }) {
  return (
    <div 
      className="accordion-header"
      onClick={() => toggleItem(index)}
    >
      {children}
      <span className="accordion-icon">{isActive ? '▲' : '▼'}</span>
    </div>
  );
}

// AccordionPanel component
export function AccordionPanel({ children, isActive }) {
  return (
    <div className={`accordion-panel ${isActive ? 'show' : 'hide'}`}>
      {children}
    </div>
  );
}

// Usage:
// <Accordion>
//   <AccordionItem index={0}>
//     <AccordionHeader>Section 1</AccordionHeader>
//     <AccordionPanel>Content for section 1</AccordionPanel>
//   </AccordionItem>
//   <AccordionItem index={1}>
//     <AccordionHeader>Section 2</AccordionHeader>
//     <AccordionPanel>Content for section 2</AccordionPanel>
//   </AccordionItem>
// </Accordion>

Deployment Preparation

As you approach the end of the project, it's important to prepare your application for deployment.

  1. Optimize your build for production
    # For Create React App
    npm run build
    
    # For Next.js
    next build && next export
  2. Add environment variables for different deployment environments
    # .env.development
    VITE_API_URL=http://localhost:5000/api
    VITE_AUTH0_DOMAIN=your-domain.auth0.com
    VITE_AUTH0_CLIENT_ID=your-development-client-id
    
    # .env.production
    VITE_API_URL=https://your-production-api.com/api
    VITE_AUTH0_DOMAIN=your-domain.auth0.com
    VITE_AUTH0_CLIENT_ID=your-production-client-id
  3. Set up proper error boundaries
    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }
    
      static getDerivedStateFromError(error) {
        return { hasError: true };
      }
    
      componentDidCatch(error, errorInfo) {
        console.error("Error caught by boundary:", error, errorInfo);
        // You can also log to an error reporting service here
      }
    
      render() {
        if (this.state.hasError) {
          return this.props.fallback || <h1>Something went wrong.</h1>;
        }
    
        return this.props.children;
      }
    }
  4. Set up continuous integration and deployment
    # Example GitHub Actions workflow
    name: Deploy
    
    on:
      push:
        branches: [ main ]
    
    jobs:
      build-and-deploy:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v2
          
          - name: Install Dependencies
            run: npm install
            
          - name: Build
            run: npm run build
            
          - name: Deploy to Netlify
            uses: netlify/actions/cli@master
            with:
              args: deploy --dir=build --prod
            env:
              NETLIFY_AUTH_TOKEN: $\{\{ secrets.NETLIFY_AUTH_TOKEN \}\}
              NETLIFY_SITE_ID: $\{\{ secrets.NETLIFY_SITE_ID \}\}

Deployment Checklist

Before deploying your application, ensure you've:

  • Removed any console.log statements
  • Added proper error handling
  • Tested all main user flows
  • Ensured environment variables are correctly set
  • Updated Auth0 application settings with production URLs (Callback, Logout, Web Origins)
  • Tested the complete authentication flow in a production-like environment
  • Verified token handling and protected routes work correctly
  • Checked for performance issues
  • Performed cross-browser testing
  • Ensured responsive design for mobile devices
  • Added basic SEO metadata if applicable

Final Project Polish

Add final touches to make your project stand out and ensure it's ready for presentation.

Final Polish Checklist

  • Create a comprehensive README with setup instructions
  • Add documentation for API endpoints
  • Include Auth0 setup steps in your documentation
  • Add proper error handling for authentication failures
  • Implement user-friendly login/logout flows with clear feedback
  • Implement final UI/UX improvements
  • Add loading states and error messages
  • Ensure accessibility compliance
  • Add progressive enhancement where possible
  • Create a demo script/video showcasing key features including authentication