← Back to DevBytes

Material UI Authentication: JWT, Sessions, and OAuth Integration

Understanding Authentication in Modern React Apps with Material UI

Authentication is the backbone of secure client‑server communication. In React applications built with Material UI, handling user identity through JWT (JSON Web Tokens), traditional sessions, or third‑party OAuth providers isn’t just about placing a login form — it’s about designing a seamless, secure experience that aligns with Material Design principles. This tutorial walks you through practical integrations, showing complete code examples for each strategy, along with the security patterns that keep your users safe.

Why Authentication Matters in Material UI Projects

Material UI provides a polished component library, but authentication logic remains in your hands. A well‑integrated auth layer:

What This Tutorial Covers

JWT (JSON Web Tokens) with Material UI

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

A JSON Web Token is a compact, self‑contained credential that your backend issues after successful login. The token contains user information and an expiry, and is typically sent as an Authorization: Bearer <token> header on every request. On the frontend, you need a login form, a storage mechanism, and an Axios interceptor to attach the token automatically.

Building a Login Form with Material UI

Below is a complete login component using MUI’s TextField, Button, and Alert. It captures email and password, sends them to a hypothetical /api/login endpoint, and stores the returned JWT.

import React, { useState } from 'react';
import {
  TextField,
  Button,
  Box,
  Alert,
  Typography,
  Paper,
} from '@mui/material';
import axios from '../utils/axiosInstance'; // custom instance

const LoginForm = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');
    setLoading(true);
    try {
      const response = await axios.post('/api/login', {
        email,
        password,
      });
      const { accessToken, refreshToken } = response.data;
      // Store tokens securely (prefer httpOnly cookies, but example uses memory/localStorage)
      localStorage.setItem('accessToken', accessToken);
      localStorage.setItem('refreshToken', refreshToken);
      // Optionally decode and set user context
      window.location.href = '/dashboard'; // simple redirect
    } catch (err) {
      setError(
        err.response?.data?.message || 'Login failed. Please try again.'
      );
    } finally {
      setLoading(false);
    }
  };

  return (
    
      
        Sign In
      
      {error && (
        
          {error}
        
      )}
      
         setEmail(e.target.value)}
          margin="normal"
          required
        />
         setPassword(e.target.value)}
          margin="normal"
          required
        />
        
      
    
  );
};

export default LoginForm;

Storing and Sending JWT with Axios Interceptors

Instead of manually attaching the token to every request, create a dedicated Axios instance with request and response interceptors. The request interceptor reads the token from storage and adds the Authorization header. The response interceptor handles 401 errors by attempting a token refresh, or redirecting to login.

import axios from 'axios';

const axiosInstance = axios.create({
  baseURL: process.env.REACT_APP_API_URL || 'http://localhost:5000',
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request interceptor: attach access token
axiosInstance.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('accessToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// Response interceptor: handle 401 and refresh
axiosInstance.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    if (
      error.response?.status === 401 &&
      !originalRequest._retry
    ) {
      originalRequest._retry = true;
      try {
        const refreshToken = localStorage.getItem('refreshToken');
        const response = await axios.post(
          `${process.env.REACT_APP_API_URL}/api/token/refresh`,
          { refreshToken }
        );
        const { accessToken, refreshToken: newRefresh } = response.data;
        localStorage.setItem('accessToken', accessToken);
        localStorage.setItem('refreshToken', newRefresh);
        originalRequest.headers.Authorization = `Bearer ${accessToken}`;
        return axiosInstance(originalRequest);
      } catch (refreshError) {
        // Clear tokens and redirect to login
        localStorage.removeItem('accessToken');
        localStorage.removeItem('refreshToken');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    return Promise.reject(error);
  }
);

export default axiosInstance;

Protecting Routes with JWT

A ProtectedRoute component checks for the existence of a valid token (or a decoded user object) and redirects to login if missing. Use it with React Router.

import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';

const ProtectedRoute = () => {
  const token = localStorage.getItem('accessToken');
  // Also verify token expiry if desired
  if (!token) {
    return ;
  }
  return ;
};

export default ProtectedRoute;

Then wrap your authenticated routes:

<Routes>
  <Route element={<ProtectedRoute />}>
    <Route path="/dashboard" element={<Dashboard />} />
    <Route path="/profile" element={<Profile />} />
  </Route>
  <Route path="/login" element={<Login />} />
</Routes>

Decoding JWT and User State

To show user‑specific UI (avatar, name) inside Material UI’s AppBar, decode the token payload using jwt-decode and store it in React context.

import jwtDecode from 'jwt-decode';

const AuthContext = React.createContext();

export const AuthProvider = ({ children }) => {
  const [user, setUser] = React.useState(null);

  React.useEffect(() => {
    const token = localStorage.getItem('accessToken');
    if (token) {
      try {
        const decoded = jwtDecode(token);
        // Check expiry
        if (decoded.exp * 1000 < Date.now()) {
          localStorage.removeItem('accessToken');
          setUser(null);
        } else {
          setUser({ id: decoded.sub, email: decoded.email, role: decoded.role });
        }
      } catch {
        setUser(null);
      }
    }
  }, []);

  const logout = () => {
    localStorage.removeItem('accessToken');
    localStorage.removeItem('refreshToken');
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, setUser, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => React.useContext(AuthContext);

Session-Based Authentication with Material UI

Session authentication relies on the server maintaining a session identifier, typically stored in an httpOnly cookie. The browser automatically sends the cookie with every request, so no manual token attachment is needed. This approach reduces the risk of token theft via XSS, but requires careful handling of CSRF and cookie attributes.

Login Form and Session Cookie

The login form remains similar to the JWT example, but the Axios instance must include withCredentials: true to send and receive cookies. The backend sets the session cookie (often named connect.sid or a custom name) with httpOnly, Secure, and SameSite flags.

// Axios instance for session auth
import axios from 'axios';

const sessionAxios = axios.create({
  baseURL: 'http://localhost:5000',
  withCredentials: true, // crucial for cookie sending/receiving
  headers: {
    'Content-Type': 'application/json',
  },
});

export default sessionAxios;

The login handler sends credentials and, on success, the server returns a response (often just a status 200 with user data). The cookie is set automatically.

const handleLogin = async (email, password) => {
  try {
    const response = await sessionAxios.post('/api/session/login', {
      email,
      password,
    });
    // The session cookie is now stored by the browser (httpOnly)
    // Use response data to update user context
    setUser(response.data.user);
    navigate('/dashboard');
  } catch (err) {
    setError(err.response?.data?.message || 'Login failed');
  }
};

CSRF Protection

When using session cookies, you must protect against Cross‑Site Request Forgery. The standard pattern is to have the backend issue a CSRF token (often in a non‑httpOnly cookie or a custom header) that the frontend reads and sends back as a custom header (e.g., X‑CSRF‑Token). Material UI forms can include a hidden field or, more commonly, an Axios interceptor reads the CSRF token from a cookie and attaches it to every state‑changing request.

// Add CSRF interceptor to sessionAxios
sessionAxios.interceptors.request.use((config) => {
  const csrfToken = getCookie('csrf_access_token'); // helper to read cookie
  if (csrfToken && ['post', 'put', 'delete', 'patch'].includes(config.method)) {
    config.headers['X-CSRF-Token'] = csrfToken;
  }
  return config;
});

// Simple cookie reader (since CSRF cookie is not httpOnly)
function getCookie(name) {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) return parts.pop().split(';').shift();
}

User Session Context

A React context fetches the current user on app mount by calling a /api/me endpoint that validates the session cookie. The MUI CircularProgress component provides a loading indicator while the check runs.

import { CircularProgress, Box } from '@mui/material';

const SessionAuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        const response = await sessionAxios.get('/api/me');
        setUser(response.data.user);
      } catch {
        setUser(null);
      } finally {
        setLoading(false);
      }
    };
    fetchUser();
  }, []);

  if (loading) {
    return (
      
        
      
    );
  }

  return (
    
      {children}
    
  );
};

OAuth 2.0 Integration with Material UI (Social Login)

OAuth 2.0 allows users to authenticate via third‑party providers like Google, GitHub, or Facebook. The frontend redirects the user to the provider, obtains an authorization code or token, and then exchanges it with your backend to establish a session or issue a JWT. Material UI shines here by offering branded buttons that fit naturally into your login page.

Google OAuth Example with Material UI Button

Using the @react-oauth/google library, you can render a Google sign‑in button that matches MUI’s design. Install it first:

npm install @react-oauth/google

Wrap your app (or the login page) with GoogleOAuthProvider and use the GoogleLogin component, customizing its appearance with MUI’s styling.

import React from 'react';
import { GoogleOAuthProvider, GoogleLogin } from '@react-oauth/google';
import { Button, Box, Typography } from '@mui/material';
import sessionAxios from '../utils/sessionAxios'; // or JWT axios

const LoginPage = () => {
  const handleGoogleSuccess = async (credentialResponse) => {
    // credentialResponse.credential is the ID token
    try {
      const res = await sessionAxios.post('/api/auth/google', {
        credential: credentialResponse.credential,
      });
      // Backend validates the token, creates session or returns JWT
      // Update user context and redirect
      window.location.href = '/dashboard';
    } catch (error) {
      console.error('Google login failed', error);
    }
  };

  return (
    
      
        
          Sign In or Create Account
        

        {/* MUI styled container around the Google button */}
        
           console.log('Login Failed')}
            // Use the "standard" button type, then style via MUI's theme or CSS
            shape="pill"
            text="signin_with"
            size="large"
          />
        

        {/* Alternatively, use a custom MUI Button that triggers the flow */}
        {/* Requires using the useGoogleLogin hook instead */}
      
    
  );
};

export default LoginPage;

Handling OAuth Tokens

After Google returns a credential (ID token), your backend must verify it, extract user information, and either create a new user or log them in. Then it issues a session cookie or a JWT. The frontend treats this exactly like a successful login — updating context and redirecting.

If you prefer a custom MUI button that triggers the OAuth flow programmatically, use the useGoogleLogin hook:

import { useGoogleLogin } from '@react-oauth/google';
import { Button } from '@mui/material';

const CustomGoogleButton = () => {
  const login = useGoogleLogin({
    onSuccess: async (tokenResponse) => {
      // tokenResponse has access_token, can be used with Google APIs
      // Send to your backend
      await axios.post('/api/auth/google/callback', {
        access_token: tokenResponse.access_token,
      });
    },
    flow: 'implicit', // or 'auth-code'
  });

  return (
    
  );
};

Combining OAuth with Existing JWT or Session Flows

Your backend should unify authentication — after a successful OAuth exchange, it can return the same accessToken and refreshToken as your email/password login, or set the same session cookie. This means your protected routes and user context remain unchanged, regardless of how the user logged in. The Material UI frontend just updates the context and redirects.

Best Practices for Material UI Authentication

Conclusion

Integrating authentication into a Material UI application isn’t just about plugging in a login form — it’s about architecting a secure, cohesive experience that leverages MUI’s design system while following robust security patterns. Whether you choose stateless JWT, traditional session cookies, or social OAuth logins, the core principles remain the same: protect tokens, validate every request, separate concerns, and always provide clear user feedback. The code examples above give you a production‑ready starting point that you can adapt to your backend and styling preferences. By combining Material UI’s component library with sound authentication practices, you’ll deliver both a polished interface and a trustworthy application.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles