Implementing OAuth 2.0

Overview

Implementing OAuth 2.0 (with Authorization Code Flow + PKCE for security) in a React app to obtain a bearer token (typically a JWT for stateless auth) is a common way to secure your API. This stateless approach means the backend doesn’t store sessions; instead, it validates the JWT on each request using a shared secret or public key.

I’ll assume:

  • Backend: Node.js with Express.js (adaptable to other stacks like Spring Boot or Django).
  • OAuth Provider: A service like Auth0, Google, or a custom OAuth server (e.g., using Node.js Passport). For simplicity, I’ll use Auth0 as an example—it’s free for basics and handles token issuance.
  • Frontend: React with libraries like react-oauth2-code-pkce for the flow.
  • Stateless Security: Use JWT as the bearer token. Backend verifies it without database lookups.

Key Flow:

  1. User logs in via OAuth provider.
  2. Frontend gets authorization code, exchanges for access token (JWT).
  3. Frontend attaches Authorization: Bearer <token> to API calls.
  4. Backend validates JWT signature and claims (e.g., exp, iss) on each request.

Prerequisites:

  • Sign up for an OAuth provider (e.g., Auth0 dashboard: create an app, note Client ID, Domain, and Callback URL).
  • Install dependencies (detailed below).

Backend Implementation (Node.js/Express)

The backend exposes API endpoints and validates the JWT bearer token. Use jsonwebtoken for verification and express-jwt for middleware.

Step 1: Set Up Project and Dependencies

mkdir backend && cd backend
npm init -y
npm install express jsonwebtoken express-jwt cors helmet
npm install -D nodemon
  • express-jwt: Middleware for JWT validation.
  • jsonwebtoken: For manual verification if needed.
  • cors: Allow React frontend origin.
  • helmet: Basic security headers.

Step 2: Configure Environment Variables

Create .env:

JWT_SECRET=your-super-secret-key (use a strong random string, e.g., from openssl rand -hex 32)
AUTH0_DOMAIN=your-auth0-domain.auth0.com
AUTH0_AUDIENCE=your-api-identifier (from Auth0 dashboard)
  • In Auth0: Go to APIs > Create API, set Identifier (Audience), and enable RBAC if needed.

Step 3: Create Server and Protected Route

In server.js:

const express = require('express');
const jwt = require('express-jwt');
const cors = require('cors');
const helmet = require('helmet');
require('dotenv').config();

const app = express();
const PORT = 5000;

// Middleware
app.use(helmet());
app.use(cors({ origin: 'http://localhost:3000' })); // React dev server
app.use(express.json());

// JWT Middleware (stateless validation)
const authMiddleware = jwt({
  secret: process.env.JWT_SECRET, // For custom JWT; for Auth0, use jwks-rsa below
  audience: process.env.AUTH0_AUDIENCE,
  issuer: `https://${process.env.AUTH0_DOMAIN}/`,
  algorithms: ['RS256'] // Auth0 uses RS256
});

// For Auth0, fetch public keys dynamically (recommended for stateless)
const { expressjwt: jwt } = require('express-jwt');
const jwksRsa = require('jwks-rsa');
const authMiddleware = jwt({
  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`
  }),
  audience: process.env.AUTH0_AUDIENCE,
  issuer: `https://${process.env.AUTH0_DOMAIN}/`,
  algorithms: ['RS256']
});

// Public route (no auth)
app.get('/api/public', (req, res) => {
  res.json({ message: 'Public endpoint' });
});

// Protected route (requires valid bearer token)
app.get('/api/protected', authMiddleware, (req, res) => {
  // req.auth contains decoded JWT claims (e.g., user ID, roles)
  res.json({ message: 'Protected data', user: req.auth.sub });
});

// Error handler for invalid tokens
app.use((err, req, res, next) => {
  if (err.name === 'UnauthorizedError') {
    res.status(401).json({ error: 'Invalid token' });
  }
});

app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
  • Run: nodemon server.js.

Step 4: Handle Token Exchange (Optional: If Custom OAuth)

If not using Auth0, implement /auth/token endpoint to exchange code for JWT:

app.post('/auth/token', async (req, res) => {
  const { code, code_verifier } = req.body; // From frontend PKCE
  // Validate code with OAuth provider, get user info
  // Then sign JWT
  const token = jwt.sign({ sub: user.id, roles: user.roles }, process.env.JWT_SECRET, { expiresIn: '1h' });
  res.json({ access_token: token });
});
  • For Auth0, the frontend handles exchange directly.

Step 5: Test Backend

  • curl http://localhost:5000/api/public → Works.
  • Without token: curl http://localhost:5000/api/protected → 401.
  • With valid token: Use Postman with Authorization: Bearer <jwt>.

Frontend Implementation (React)

Use react-oauth2-code-pkce for secure OAuth flow (handles PKCE to prevent code interception). Store token in memory or localStorage (use httpOnly cookies for prod security).

Step 1: Set Up Project and Dependencies

npx create-react-app frontend
cd frontend
npm install react-oauth2-code-pkce axios
  • react-oauth2-code-pkce: Manages OAuth flow.
  • axios: For API calls with token.

Step 2: Configure Environment Variables

In .env (React loads these):

REACT_APP_AUTH0_DOMAIN=your-auth0-domain.auth0.com
REACT_APP_AUTH0_CLIENT_ID=your-client-id
REACT_APP_AUTH0_REDIRECT_URI=http://localhost:3000/callback
REACT_APP_API_URL=http://localhost:5000/api

Step 3: Create Auth Provider and Components

Wrap app with AuthProvider in src/index.js:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { AuthProvider } from 'react-oauth2-code-pkce';

const authConfig = {
  clientId: process.env.REACT_APP_AUTH0_CLIENT_ID,
  authorizationEndpoint: `https://${process.env.REACT_APP_AUTH0_DOMAIN}/authorize`,
  tokenEndpoint: `https://${process.env.REACT_APP_AUTH0_DOMAIN}/oauth/token`,
  redirectUri: process.env.REACT_APP_AUTH0_REDIRECT_URI,
  scope: 'openid profile email', // Adjust scopes
  extraQueryParams: { audience: 'your-api-identifier' } // For API access
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <AuthProvider authConfig={authConfig}>
    <App />
  </AuthProvider>
);

Create src/AuthButton.js for login/logout:

import React from 'react';
import { useAuth } from 'react-oauth2-code-pkce';

export default function AuthButton() {
  const { login, logOut, isAuthenticated, error, authContextLoading } = useAuth();

  if (authContextLoading) return <p>Loading...</p>;
  if (error) return <p>Auth Error: {error}</p>;

  return isAuthenticated ? (
    <>
      <p>Logged in!</p>
      <button onClick={() => logOut()}>Logout</button>
    </>
  ) : (
    <button onClick={() => login()}>Login with OAuth</button>
  );
}

Step 4: Make Secured API Calls

In a component like src/App.js:

import React from 'react';
import AuthButton from './AuthButton';
import { useAuth } from 'react-oauth2-code-pkce';
import axios from 'axios';

export default function App() {
  const { token, isAuthenticated } = useAuth();

  const fetchProtectedData = async () => {
    if (!isAuthenticated) return;
    try {
      const response = await axios.get(`${process.env.REACT_APP_API_URL}/protected`, {
        headers: { Authorization: `Bearer ${token}` }
      });
      console.log(response.data);
    } catch (error) {
      console.error('API Error:', error.response?.data);
    }
  };

  return (
    <div>
      <AuthButton />
      {isAuthenticated && <button onClick={fetchProtectedData}>Fetch Protected Data</button>}
    </div>
  );
}
  • The library handles redirect to /callback automatically.

Step 5: Handle Callback Route

The library manages the callback, but ensure your package.json has "homepage": "." for dev.

Step 6: Test Frontend

  • Run: npm start.
  • Click Login → Redirect to Auth0 → Back to app with token.
  • API call succeeds only if authenticated.

Additional Best Practices

  • Security:
  • Use HTTPS in prod.
  • Short token expiry (e.g., 15-60min) + refresh tokens.
  • Validate scopes/roles in backend (e.g., if (!req.auth.permissions.includes('read:protected')) return 403;).
  • Avoid storing tokens in localStorage; use in-memory or secure cookies.
  • Error Handling: Add try-catch for token expiry (redirect to login).
  • Custom OAuth: If building your own server, use passport-oauth2 on backend.
  • Deployment: Update origins/URIs in Auth0 for prod (e.g., Vercel/Netlify).
  • Debugging: Check browser console for token, network tab for headers.

If your stack differs (e.g., Next.js backend), provide more details for tailored steps!

Tags: No tags

Add a Comment

Your email address will not be published. Required fields are marked *