Table of Contents#
- Understanding the GitHub OAuth 2.0 Flow
- Why CORS Blocks the Access Token Request?
- Common Pitfalls in React-Redux Apps
- Step-by-Step Solution to Fix CORS and Retrieve the Access Token
- Conclusion
- 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:
- User Initiates Login: The user clicks "Login with GitHub" in your React app.
- Redirect to GitHub: Your app redirects the user to GitHub’s OAuth authorization endpoint (
https://github.com/login/oauth/authorize), including yourclient_idand requested scopes. - User Authenticates: The user logs in to GitHub (if not already) and approves your app’s access request.
- GitHub Redirects Back with Code: GitHub redirects the user back to your app’s preconfigured
redirect_uri, appending an authorizationcodeas a query parameter (e.g.,http://localhost:3000/auth/callback?code=ABC123). - Exchange Code for Access Token: Your app sends this
codeto GitHub’s token endpoint (https://github.com/login/oauth/access_token) to exchange it for anaccess_token(and optionally arefresh_token). - Use Token for Authenticated Requests: Your app uses the
access_tokento 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
codefor a token directly from the React frontend (client-side). As explained, this triggers CORS. - Misconfigured GitHub OAuth App: Incorrect
redirect_uriin 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 aclient_secretfrom the frontend. - Axios Misconfiguration: Not setting
Content-Typeheaders 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:
-
Go to GitHub Developer Settings → OAuth Apps → Select your app (or create a new one).
-
Client ID: Copy this (you’ll need it in both frontend and backend).
-
Client Secret: Keep this secure (only use it in your backend).
-
Redirect URI: Set this to your React app’s callback page (e.g.,
http://localhost:3000/auth/callbackfor local development).Critical: The
redirect_uriin 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
codefrom your React frontend. - Exchange the
codefor anaccess_tokenby making a server-side request to GitHub. - Return the
access_tokento 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#
-
Start the Backend:
cd oauth-backend node index.jsYou should see:
Backend proxy running on http://localhost:5000. -
Start the React App:
cd your-react-app npm start -
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;
-
-
Complete the Flow:
- GitHub will redirect you to
http://localhost:3000/auth/callback?code=ABC123. - The
AuthCallbackcomponent extracts thecode, sends it to the backend, and the backend returns theaccess_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.
- GitHub will redirect you to
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_secretin 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.