OAuth 2.0 is the de‑facto standard for delegating authentication and authorization in modern web applications. Whether you’re building a single‑page app, a server‑side rendered site, or a hybrid, getting OAuth right is critical for both user experience and security. This guide walks you through a practical, hands‑on implementation of OAuth 2.0 using the Authorization Code flow with PKCE, the recommended approach for public clients. We’ll cover everything from setting up your provider to handling token refreshes, sprinkled with real commands and common pitfalls to keep you on track.
What You’ll Need
- A cloud‑hosted OAuth provider (Google, GitHub, Auth0, Okta, etc.)
- A web application skeleton (Node.js/Express, Django, Flask, or any language you prefer)
- Basic knowledge of HTTP, JSON, and command‑line tools
- Postman or a similar API testing tool (optional but handy)
- An HTTPS‑enabled domain (OAuth providers require secure callbacks)
- Git for version control (recommended)
Step 1: Create Your OAuth 2.0 Credentials
First, log into your provider’s developer console. For illustration, we’ll use GitHub, but the steps are almost identical for Google, Auth0, or Okta.
Navigate to Settings > Developer Settings > OAuth Apps and click New OAuth App. Fill in the form:
- Application name: YourApp OAuth Demo
- Homepage URL: https://yourapp.example.com
- Authorization callback URL: https://yourapp.example.com/auth/callback
After saving, you’ll receive a Client ID and Client Secret. Keep the secret safe—never commit it to public repositories. In a production environment, store it in environment variables or a secrets manager.
Step 2: Set Up Redirect URIs and Scopes
OAuth requires a redirect URI that the provider can return the user to after authentication. Make sure the URI you registered matches the route your app will expose. In GitHub’s case, you might also want to request specific scopes like read:user or repo depending on your needs.
In your provider’s console, locate the Scopes section and add the permissions you need. For a minimal demo, read:user is enough to fetch basic profile data.
Step 3: Install Dependencies
Below is a Node.js/Express example, but the concepts translate to any stack.
npm init -y
npm install express express-session passport passport-github2 dotenv
We’ll use passport for OAuth strategy handling and express-session to maintain user sessions. Create a .env file to store your credentials:
GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secret
SESSION_SECRET=supersecretstring
BASE_URL=https://yourapp.example.com
Step 4: Configure the Auth Server
In app.js, set up Express and Passport:
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const GitHubStrategy = require('passport-github2').Strategy;
const app = express();
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
}));
app.use(passport.initialize());
app.use(passport.session());
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((obj, done) => done(null, obj));
passport.use(new GitHubStrategy({
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: `${process.env.BASE_URL}/auth/github/callback`
},
function(accessToken, refreshToken, profile, done) {
// Store tokens and profile in session or DB
profile.accessToken = accessToken;
profile.refreshToken = refreshToken;
return done(null, profile);
}
));
Notice we’re attaching the accessToken and refreshToken to the user profile. In a real app, you’d persist these securely.
Step 5: Implement the Authorization Code Flow
Define routes for login, callback, and logout:
// Login route
app.get('/auth/github', passport.authenticate('github', { scope: ['user:email'] }));
// Callback route
app.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/' }),
(req, res) => {
// Successful authentication
res.redirect('/dashboard');
}
);
// Logout route
app.get('/logout', (req, res) => {
req.logout();
res.redirect('/');
});
When a user hits /auth/github, Passport redirects them to GitHub’s consent page. After granting access, GitHub redirects back to /auth/github/callback, where Passport exchanges the code for tokens.
Step 6: Handle Token Refresh
GitHub’s OAuth tokens don’t expire, but many providers do. To illustrate, we’ll add a manual refresh endpoint that uses the refresh_token to obtain a new access token. In your provider’s docs, locate the token endpoint (e.g., https://github.com/login/oauth/access_token for GitHub).
const axios = require('axios');
app.get('/refresh-token', async (req, res) => {
if (!req.isAuthenticated()) return res.redirect('/');
const { refreshToken } = req.user;
try {
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,
refresh_token: refreshToken,
grant_type: 'refresh_token'
}, {
headers: { Accept: 'application/json' }
});
req.user.accessToken = response.data.access_token;
res.send('Token refreshed!');
} catch (err) {
res.status(500).send('Refresh failed');
}
});
Replace the endpoint URL and payload with your provider’s specifics. Always validate the response and handle errors gracefully.
Step 7: Secure the Tokens
Storing tokens in memory is fine for development, but in production you should:
- Persist tokens in an encrypted database column.
- Rotate
client_secretperiodically. - Use HTTPS everywhere—OAuth providers reject non‑secure callbacks.
- Set
SameSite=LaxorStricton session cookies. - Implement CSRF protection on the callback route.
Additionally, consider using PKCE for public clients (mobile or SPA). It adds a code verifier that mitigates interception attacks.
Common Mistakes to Avoid
1. Hard‑coding secrets in source control—use environment variables or a secrets vault instead.
2. Using HTTP instead of HTTPS—most providers reject non‑secure callback URLs.
3. Mismatched redirect URIs—even a trailing slash can cause a failure.
4. Ignoring scopes—requesting more than you need can scare users and expose unnecessary data.
5. Not handling token expiration—without refresh logic, users will be logged out unexpectedly.
Tips and Tricks
- Use
dotenv-clito load environment variables during development:dotenv -e .env.local -- node app.js. - Leverage Passport’s
passport.authenticatemiddleware to add CSRF tokens automatically. - Cache user profile data in your DB to reduce API calls to the provider.
- For SPA front‑ends, consider the Implicit flow or the newer Authorization Code flow with PKCE.
- Audit your OAuth app’s permissions regularly—remove unused scopes.
Frequently Asked Questions
What’s the difference between Authorization Code and Implicit flows?
The Authorization Code flow is designed for server‑side applications where the client secret can be kept confidential. It exchanges a short‑lived code for an access token. The Implicit flow skips the code exchange and returns the token directly to the client, which is less secure because the token is exposed in the URL. Modern best practice is to use Authorization Code with PKCE even for SPAs.
How do I handle multiple providers (Google, GitHub, etc.)?
Passport supports multiple strategies. Instantiate each with its own clientID and clientSecret, and define separate routes (/auth/google, /auth/github). Store provider identifiers in your user model to differentiate accounts.
Can I use OAuth 2.0 for internal microservices?
Yes. For service‑to‑service authentication, use the Client Credentials flow. Obtain a token by sending your client_id and client_secret to the token endpoint, then attach the token to the Authorization header of internal requests.
Conclusion
Implementing OAuth 2.0 is no longer a luxury—it’s a necessity for secure, user‑friendly authentication. By following these steps, you’ll have a robust foundation that scales from simple demos to production‑grade applications. Remember to keep your secrets safe, enforce HTTPS, and stay mindful of token lifecycle. Happy coding, and may your tokens stay refreshed!





