Skip to main content

OAuth 2.0 Integration in React: Step-by-Step

OAuth 2.0 is an open standard that lets users log in using a third-party provider (Google, GitHub, Apple) without sharing their password with your app. The provider (e.g., Google) authenticates the user, issues an authorization code, and your app exchanges it for an access token. OAuth eliminates password management on your end, reduces phishing risk (users authenticate directly with the trusted provider), and enables single sign-on (SSO). This article covers the Authorization Code Flow, the most common and secure OAuth pattern for web apps.

OAuth 2.0 Authorization Code Flow

The Flow Steps

  1. User clicks "Log In with Google"
  2. Your React app redirects to Google's authorization endpoint with your client ID
  3. User logs in to Google (or skips if already logged in)
  4. Google displays a consent screen: "This app wants access to your name and email"
  5. User clicks "Allow"; Google redirects back to your app's redirectUri with an authorization code
  6. React exchanges the code for tokens (access token, ID token) by calling your backend
  7. Your backend verifies the tokens with Google, then issues your own JWT or session
  8. React stores the JWT and logs the user in

The key: the user never shares their password with your app. Google handles authentication and your backend validates the response.

Setting Up Google OAuth

Step 1: Create a Google Cloud Project

  1. Go to Google Cloud Console
  2. Create a new project
  3. Enable the Google+ API
  4. Go to Credentials → Create OAuth 2.0 Credentials → Web Application
  5. Set Authorized redirect URIs:
    • http://localhost:3000/api/auth/callback/google (development)
    • https://app.example.com/api/auth/callback/google (production)
  6. Copy the Client ID and Client Secret (keep the secret safe; never commit it)

Step 2: React Component for OAuth Login

// GoogleLoginButton.js
import React from 'react';

function GoogleLoginButton() {
const CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID;
const REDIRECT_URI = `${window.location.origin}/api/auth/callback/google`;

const handleGoogleLogin = () => {
const scope = encodeURIComponent('openid profile email');
const responseType = 'code';
const accessType = 'offline'; // Request refresh token

// Redirect to Google's authorization endpoint
const googleAuthUrl =
`https://accounts.google.com/o/oauth2/v2/auth?` +
`client_id=${CLIENT_ID}&` +
`redirect_uri=${encodeURIComponent(REDIRECT_URI)}&` +
`response_type=${responseType}&` +
`scope=${scope}&` +
`access_type=${accessType}`;

window.location.href = googleAuthUrl;
};

return (
<button onClick={handleGoogleLogin} className="btn-google">
Log In with Google
</button>
);
}

export default GoogleLoginButton;

Step 3: Callback Handler (Backend)

After the user authorizes, Google redirects to /api/auth/callback/google?code=.... Your backend exchanges the code for tokens:

// backend/routes/auth.js (Node.js/Express)
const axios = require('axios');
const jwt = require('jsonwebtoken');

app.get('/api/auth/callback/google', async (req, res) => {
const { code, error } = req.query;

if (error) {
// User denied or error occurred
return res.redirect(`/login?error=${error}`);
}

if (!code) {
return res.status(400).json({ error: 'Authorization code missing' });
}

try {
// ✅ Step 1: Exchange code for tokens
const tokenResponse = await axios.post(
'https://oauth2.googleapis.com/token',
{
code,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: `${process.env.APP_URL}/api/auth/callback/google`,
grant_type: 'authorization_code'
}
);

const { access_token, id_token } = tokenResponse.data;

// ✅ Step 2: Verify ID token (issued by Google)
const payload = jwt.decode(id_token);

// Validate the token signature (in production, use a library like jwt-decode with verification)
// For now, assume Google's tokens are valid if they came from their server

const { sub, email, name, picture } = payload;

// ✅ Step 3: Find or create user in your database
let user = await User.findOne({ googleId: sub });

if (!user) {
// Create new user
user = await User.create({
googleId: sub,
email,
name,
avatar: picture,
password: null // OAuth users have no password
});
} else {
// Update user profile (optional)
user.name = name;
user.avatar = picture;
await user.save();
}

// ✅ Step 4: Issue your own JWT
const accessToken = jwt.sign(
{
sub: user.id,
email: user.email,
provider: 'google'
},
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);

const refreshToken = jwt.sign(
{ sub: user.id },
process.env.REFRESH_SECRET,
{ expiresIn: '7d' }
);

// ✅ Step 5: Set httpOnly cookies and redirect
res.cookie('accessToken', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'Strict',
maxAge: 15 * 60 * 1000
});

res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'Strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});

// Redirect to dashboard or login page with success query param
res.redirect('/dashboard?oauth=success');

} catch (err) {
console.error('OAuth callback error:', err);
res.redirect(`/login?error=oauth_failed`);
}
});

React: Handling the Callback Redirect

Your backend redirects to /dashboard?oauth=success. React detects this and logs the user in:

// Dashboard.js
import React, { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';

function Dashboard() {
const location = useLocation();
const navigate = useNavigate();

useEffect(() => {
const searchParams = new URLSearchParams(location.search);

if (searchParams.get('oauth') === 'success') {
console.log('OAuth login successful');
// The httpOnly cookie is already set; user is logged in
// Remove the query param from URL
window.history.replaceState({}, document.title, '/dashboard');
}

if (searchParams.get('error')) {
const error = searchParams.get('error');
alert(`Login failed: ${error}`);
navigate('/login');
}
}, [location, navigate]);

return <h1>Welcome to your dashboard!</h1>;
}

export default Dashboard;

Using a Library: react-oauth/google

For production, use a well-maintained library. @react-oauth/google simplifies OAuth handling:

npm install @react-oauth/google

Setup

// App.js
import { GoogleOAuthProvider } from '@react-oauth/google';
import LoginPage from './LoginPage';

function App() {
return (
<GoogleOAuthProvider clientId={process.env.REACT_APP_GOOGLE_CLIENT_ID}>
<LoginPage />
</GoogleOAuthProvider>
);
}

export default App;

Using GoogleLogin Component

// LoginPage.js
import { GoogleLogin } from '@react-oauth/google';

function LoginPage() {
const handleGoogleSuccess = async (credentialResponse) => {
const token = credentialResponse.credential; // ID token

// Send token to backend for verification and user creation
const response = await fetch('/api/auth/google', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
credentials: 'include'
});

if (response.ok) {
window.location.href = '/dashboard';
} else {
alert('Login failed');
}
};

return (
<GoogleLogin
onSuccess={handleGoogleSuccess}
onError={() => alert('Login failed')}
/>
);
}

export default LoginPage;

GitHub OAuth Example

GitHub's OAuth is similar. Here is a quick comparison:

ProviderAuth EndpointToken EndpointUser Endpoint
Googleaccounts.google.com/o/oauth2/v2/authoauth2.googleapis.com/tokenID token (JWT)
GitHubgithub.com/login/oauth/authorizegithub.com/login/oauth/access_tokenapi.github.com/user
Appleappleid.apple.com/auth/authorizeappleid.apple.com/auth/tokenID token (JWT)

For GitHub:

// backend/routes/auth.js
app.get('/api/auth/callback/github', async (req, res) => {
const { code } = req.query;

// Exchange code for access token
const tokenResponse = 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
},
{ headers: { Accept: 'application/json' } }
);

const { access_token } = tokenResponse.data;

// Fetch user profile
const userResponse = await axios.get('https://api.github.com/user', {
headers: { Authorization: `token ${access_token}` }
});

const { id, login, avatar_url, email } = userResponse.data;

// Create or update user
let user = await User.findOne({ githubId: id });
if (!user) {
user = await User.create({
githubId: id,
username: login,
avatar: avatar_url,
email
});
}

// Issue JWT and redirect
// (same as Google example above)
});

Scopes and Permissions

OAuth scopes define what data your app can access. Always request the minimum:

// Google scopes
const scope = encodeURIComponent('openid profile email');

// GitHub scopes
const scope = 'user:email'; // Minimal; just email

// Apple scopes (same as OpenID)
const scope = encodeURIComponent('openid email name');

Key Takeaways

  • OAuth 2.0 eliminates password handling; users authenticate directly with the provider (Google, GitHub, Apple)
  • The Authorization Code Flow is the most secure: code is exchanged server-to-server, never exposed to the client
  • Always validate the ID token on the backend; do not trust client-side verification
  • Request only the scopes you need (openid, profile, email); users see permissions and can deny
  • Store the provider ID (googleId, githubId) in your database to link users
  • Issue your own JWT after validating the provider's token; your backend is the source of truth
  • Use libraries like @react-oauth/google or next-auth for production to avoid implementing OAuth from scratch
  • Refresh tokens from OAuth providers may have longer lifetimes; adjust your app's refresh strategy accordingly
  • Support account linking: if a user logs in with Google, then later tries GitHub, check if the email matches and link the accounts

Frequently Asked Questions

What if the user denies permission?

The authorization server redirects back with an error parameter (e.g., access_denied). Catch this and display a message: "Login was canceled. Please try again."

Should I store the provider's access token?

Only if you need it to call the provider's API on behalf of the user (e.g., to fetch their Google Drive files). Otherwise, do not store it; store only your own JWT. The provider's token may expire or be revoked unexpectedly.

What if I want to support multiple providers?

Store each provider's ID separately (googleId, githubId) for the same user, or use a more generic approach: store an array of { provider, providerId } objects. When a user logs in with a new provider, check if the email matches and link the accounts.

Can I revoke OAuth access later?

Yes, via the provider's settings or by calling their revocation endpoint. However, users typically manage this in their provider account settings (e.g., "Connected apps" in Google Account). Your app should also allow users to disconnect an OAuth provider from their account settings.

What about OpenID Connect?

OpenID Connect (OIDC) is an extension of OAuth 2.0 that adds an ID token (JWT). Google and Apple use OIDC; GitHub does not. OIDC simplifies user profile retrieval because the ID token includes user info; GitHub requires a separate API call.

Further Reading