coderain blog

How to Fix Axios CORS Issue with GitHub OAuth: Access Token Not Received in React-Redux App

GitHub OAuth integration is a popular way to add "Login with GitHub" functionality to web apps. However, developers working with React-Redux often hit a roadblock: after the user authenticates with GitHub, the frontend fails to retrieve the access token due to CORS (Cross-Origin Resource Sharing) errors. This issue occurs when the React app (running on http://localhost:3000, for example) tries to directly exchange the OAuth authorization code for an access token with GitHub’s API (https://github.com), triggering browser security restrictions.

In this blog, we’ll demystify why this CORS error happens, walk through common pitfalls, and provide a step-by-step solution to fix it. By the end, you’ll have a secure, working OAuth flow where your React-Redux app successfully retrieves and stores the GitHub access token.

2026-01

Table of Contents#

  1. Understanding the GitHub OAuth 2.0 Flow
  2. Why CORS Blocks the Access Token Request?
  3. Common Pitfalls in React-Redux Apps
  4. Step-by-Step Solution to Fix CORS and Retrieve the Access Token
  5. Conclusion
  6. References

1. Understanding the GitHub OAuth 2.0 Flow#

Before diving into the CORS issue, let’s recap the GitHub OAuth 2.0 authorization code flow. This is the standard flow for web apps and involves the following steps:

  1. User Initiates Login: The user clicks "Login with GitHub" in your React app.
  2. Redirect to GitHub: Your app redirects the user to GitHub’s OAuth authorization endpoint (https://github.com/login/oauth/authorize), including your client_id and requested scopes.
  3. User Authenticates: The user logs in to GitHub (if not already) and approves your app’s access request.
  4. GitHub Redirects Back with Code: GitHub redirects the user back to your app’s preconfigured redirect_uri, appending an authorization code as a query parameter (e.g., http://localhost:3000/auth/callback?code=ABC123).
  5. Exchange Code for Access Token: Your app sends this code to GitHub’s token endpoint (https://github.com/login/oauth/access_token) to exchange it for an access_token (and optionally a refresh_token).
  6. Use Token for Authenticated Requests: Your app uses the access_token to make API calls to GitHub on behalf of the user.

The CORS issue typically arises at Step 5: when your React frontend tries to directly send the code to GitHub’s token endpoint. Let’s explore why.

2. Why CORS Blocks the Access Token Request?#

Modern browsers enforce the Same-Origin Policy (SOP), which restricts web pages from making requests to a different origin (domain, protocol, or port) than the one that served the page. CORS (Cross-Origin Resource Sharing) is a mechanism that relaxes SOP by allowing servers to explicitly permit cross-origin requests via HTTP headers.

When your React app (running on http://localhost:3000) sends a request to GitHub’s token endpoint (https://github.com), this is a cross-origin request. For the browser to allow your app to access the response, GitHub’s server must include the Access-Control-Allow-Origin header in its response, explicitly permitting http://localhost:3000 (or your production domain).

However, GitHub’s OAuth token endpoint does not include this header for security reasons. Even if your request to https://github.com/login/oauth/access_token succeeds (i.e., GitHub receives the code and sends back a token), the browser will block your React app from accessing the response. This is why you see errors like:

Access to XMLHttpRequest at 'https://github.com/login/oauth/access_token' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.  

3. Common Pitfalls in React-Redux Apps#

Before we fix the CORS issue, let’s highlight common mistakes that exacerbate the problem:

  • Direct Client-Side Token Exchange: Trying to exchange the code for a token directly from the React frontend (client-side). As explained, this triggers CORS.
  • Misconfigured GitHub OAuth App: Incorrect redirect_uri in your GitHub OAuth app settings (must match the URI your app uses to handle the callback).
  • Exposing Secrets in Frontend: Hardcoding client_secret (a sensitive credential) in your React app. This is a security risk and GitHub will reject requests with a client_secret from the frontend.
  • Axios Misconfiguration: Not setting Content-Type headers or handling JSON responses correctly in Axios.
  • Redux State Mismanagement: Failing to dispatch Redux actions to store the token after retrieval, leading to "token not received" errors.

4. Step-by-Step Solution to Fix CORS and Retrieve the Access Token#

The core solution to bypass CORS is to use a backend proxy server. Since backend servers (e.g., Node.js/Express) are not subject to browser SOP restrictions, they can safely exchange the code for a token with GitHub. The frontend then communicates with your backend, which acts as an intermediary.

4.1 Verify GitHub OAuth App Settings#

First, ensure your GitHub OAuth app is configured correctly:

  1. Go to GitHub Developer SettingsOAuth Apps → Select your app (or create a new one).

  2. Client ID: Copy this (you’ll need it in both frontend and backend).

  3. Client Secret: Keep this secure (only use it in your backend).

  4. Redirect URI: Set this to your React app’s callback page (e.g., http://localhost:3000/auth/callback for local development).

    Critical: The redirect_uri in your app must exactly match the URI GitHub redirects to. Mismatched URIs will cause GitHub to reject the OAuth flow.

4.2 Set Up a Backend Proxy Server#

We’ll use a simple Node.js/Express server as the proxy. This server will:

  • Receive the code from your React frontend.
  • Exchange the code for an access_token by making a server-side request to GitHub.
  • Return the access_token to the frontend.

Step 4.2.1: Initialize the Backend#

Create a new directory for your backend (e.g., oauth-backend) and initialize it:

mkdir oauth-backend && cd oauth-backend  
npm init -y  
npm install express axios dotenv cors  

Step 4.2.2: Configure Environment Variables#

Create a .env file to store sensitive credentials (never commit this to version control!):

# .env  
GITHUB_CLIENT_ID=your_github_client_id  
GITHUB_CLIENT_SECRET=your_github_client_secret  
PORT=5000  

Step 4.2.3: Build the Proxy Endpoint#

Create an index.js file for your Express server:

// index.js  
require('dotenv').config();  
const express = require('express');  
const axios = require('axios');  
const cors = require('cors');  
 
const app = express();  
app.use(cors()); // Enable CORS for all frontend origins (adjust in production!)  
app.use(express.json());  
 
// Proxy endpoint to exchange code for access token  
app.post('/api/exchange-code', async (req, res) => {  
  const { code } = req.body;  
 
  if (!code) {  
    return res.status(400).json({ error: 'Authorization code is required' });  
  }  
 
  try {  
    // Request access token from GitHub  
    const response = await axios.post(  
      'https://github.com/login/oauth/access_token',  
      {  
        client_id: process.env.GITHUB_CLIENT_ID,  
        client_secret: process.env.GITHUB_CLIENT_SECRET,  
        code: code,  
      },  
      {  
        headers: {  
          Accept: 'application/json', // GitHub returns JSON instead of form-encoded  
        },  
      }  
    );  
 
    // Extract access token from response  
    const { access_token, token_type } = response.data;  
 
    if (!access_token) {  
      return res.status(400).json({ error: 'Failed to retrieve access token' });  
    }  
 
    // Send token back to frontend  
    res.json({ accessToken: access_token, tokenType: token_type });  
 
  } catch (error) {  
    console.error('Error exchanging code for token:', error.response?.data || error.message);  
    res.status(500).json({ error: 'Failed to exchange code for token' });  
  }  
});  
 
const PORT = process.env.PORT || 5000;  
app.listen(PORT, () => {  
  console.log(`Backend proxy running on http://localhost:${PORT}`);  
});  

4.3 Update React Frontend to Use the Backend Proxy#

Now, modify your React app to send the authorization code to your backend proxy instead of directly to GitHub.

Step 4.3.1: Handle the OAuth Callback#

Create a callback component (e.g., AuthCallback.js) to capture the code from the URL and send it to the backend:

// src/components/AuthCallback.js  
import { useEffect } from 'react';  
import { useDispatch } from 'react-redux';  
import { useLocation, useNavigate } from 'react-router-dom';  
import axios from 'axios';  
import { setAccessToken } from '../redux/authSlice'; // Redux action to store token  
 
const AuthCallback = () => {  
  const location = useLocation();  
  const navigate = useNavigate();  
  const dispatch = useDispatch();  
 
  useEffect(() => {  
    // Extract the 'code' from the URL query parameters  
    const searchParams = new URLSearchParams(location.search);  
    const code = searchParams.get('code');  
 
    if (code) {  
      // Send code to backend proxy to exchange for token  
      const exchangeCodeForToken = async () => {  
        try {  
          const response = await axios.post(  
            'http://localhost:5000/api/exchange-code', // Backend proxy endpoint  
            { code: code }  
          );  
 
          const { accessToken } = response.data;  
 
          if (accessToken) {  
            // Dispatch Redux action to store the token  
            dispatch(setAccessToken(accessToken));  
            // Redirect user to dashboard/home  
            navigate('/dashboard');  
          } else {  
            console.error('No access token received');  
            navigate('/login');  
          }  
 
        } catch (error) {  
          console.error('Error exchanging code:', error.response?.data || error.message);  
          navigate('/login');  
        }  
      };  
 
      exchangeCodeForToken();  
    } else {  
      console.error('No authorization code found in URL');  
      navigate('/login');  
    }  
  }, [location, navigate, dispatch]);  
 
  return <div>Logging in...</div>;  
};  
 
export default AuthCallback;  

Step 4.3.2: Configure React Router#

Ensure your React Router routes include the callback page. For example, in src/App.js:

// src/App.js  
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';  
import AuthCallback from './components/AuthCallback';  
import Login from './components/Login';  
import Dashboard from './components/Dashboard';  
 
function App() {  
  return (  
    <Router>  
      <Routes>  
        <Route path="/login" element={<Login />} />  
        <Route path="/auth/callback" element={<AuthCallback />} />  
        <Route path="/dashboard" element={<Dashboard />} />  
      </Routes>  
    </Router>  
  );  
}  
 
export default App;  

4.4 Handle the Access Token in Redux#

Store the access token in your Redux store so it’s accessible across components. Use Redux Toolkit for simplicity.

Step 4.4.1: Create an Auth Slice#

// src/redux/authSlice.js  
import { createSlice } from '@reduxjs/toolkit';  
 
const initialState = {  
  accessToken: null,  
  isAuthenticated: false,  
};  
 
const authSlice = createSlice({  
  name: 'auth',  
  initialState,  
  reducers: {  
    setAccessToken: (state, action) => {  
      state.accessToken = action.payload;  
      state.isAuthenticated = !!action.payload;  
    },  
    clearAccessToken: (state) => {  
      state.accessToken = null;  
      state.isAuthenticated = false;  
    },  
  },  
});  
 
export const { setAccessToken, clearAccessToken } = authSlice.actions;  
export default authSlice.reducer;  

Step 4.4.2: Configure Redux Store#

// src/redux/store.js  
import { configureStore } from '@reduxjs/toolkit';  
import authReducer from './authSlice';  
 
export const store = configureStore({  
  reducer: {  
    auth: authReducer,  
  },  
});  

4.5 Test the Flow End-to-End#

  1. Start the Backend:

    cd oauth-backend  
    node index.js  

    You should see: Backend proxy running on http://localhost:5000.

  2. Start the React App:

    cd your-react-app  
    npm start  
  3. Initiate Login:

    • Navigate to http://localhost:3000/login.

    • Click "Login with GitHub" (implement a simple button that redirects to GitHub’s authorization endpoint):

      // src/components/Login.js  
      const Login = () => {  
        const handleGitHubLogin = () => {  
          const clientId = 'your_github_client_id'; // From GitHub OAuth app  
          const redirectUri = 'http://localhost:3000/auth/callback';  
          const scope = 'user repo'; // Requested scopes  
       
          window.location.href = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}`;  
        };  
       
        return <button onClick={handleGitHubLogin}>Login with GitHub</button>;  
      };  
       
      export default Login;  
  4. Complete the Flow:

    • GitHub will redirect you to http://localhost:3000/auth/callback?code=ABC123.
    • The AuthCallback component extracts the code, sends it to the backend, and the backend returns the access_token.
    • Redux dispatches setAccessToken, storing the token in the store.
    • You’re redirected to /dashboard, where you can use the token to make authenticated GitHub API calls.

5. Conclusion#

By using a backend proxy server, you bypass the CORS restrictions that block direct frontend-to-GitHub token exchange. This approach is secure (secrets stay in the backend) and reliable. Key takeaways:

  • CORS is a browser security feature; backend servers avoid this restriction.
  • Never expose client_secret in the frontend – use environment variables in the backend.
  • Redux manages the token state to keep it accessible across your React app.

With this setup, your React-Redux app will successfully retrieve and use GitHub OAuth access tokens.

6. References#