Skip to main content

πŸ” What is OAuth 2.0?

πŸ“– Definition​

OAuth 2.0 is an open standard protocol that allows users to grant access to their information on other websites or applications without sharing their passwords. "Sign in with Google" and "Sign in with GitHub" are typical examples of social login. It focuses on Authorization rather than Authentication, providing a secure and convenient user experience.

🎯 Understanding Through Analogies​

Hotel Key Card​

Traditional Method = Hotel Master Key
Hotel Owner: "Here's the master key"
You: "This opens all rooms! 😱"
"It's like giving away my password"

OAuth 2.0 = Hotel Key Card
Hotel Owner: "This key card..."
βœ… Only opens your room
βœ… Expires automatically at checkout
βœ… Master key stays safe even if lost
βœ… Can be revoked anytime

You: "Perfect!"

Power of Attorney​

Scenario: Delegating bank tasks to a representative

❌ Password Sharing Method
You: "My bank password is 1234"
Representative: "I can do everything at the bank!"
Risk: All permissions including withdrawals, account deletion

βœ… OAuth Method
You: "I'll create a power of attorney at the bank"
Bank: "What permissions do you want to grant?"
You: "Only balance inquiry"
Bank: "For how long?"
You: "Just one week"

Representative can:
βœ… Only check balance
βœ… Expires automatically after a week
βœ… Doesn't know your password
βœ… Can be revoked anytime

βš™οΈ How It Works​

1. OAuth 2.0 Key Concepts​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ OAuth 2.0 Four Roles β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

1. Resource Owner
└─ User (you)

2. Client
└─ Application that wants to use the service
└─ Example: Your website

3. Authorization Server
└─ Server that grants permissions
└─ Example: Google, GitHub, Naver

4. Resource Server
└─ Server that holds user information
└─ Example: Google API, GitHub API

2. OAuth 2.0 Authentication Flow (Authorization Code)​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ User β”‚ β”‚ Client β”‚
β”‚ (You) β”‚ β”‚ (Web App)β”‚
β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
β”‚ β”‚
β”‚ 1. Click "Sign in with Google" β”‚
│──────────────────────────────────────────>β”‚
β”‚ β”‚
β”‚ 2. Redirect to Google login page β”‚
β”‚<──────────────────────────────────────────│
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Google β”‚ β”‚
β”‚ 3. Loginβ”‚ (Auth Server) β”‚ β”‚
│────────>β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ 4. Permission request β”‚
β”‚ "This app wants to view your β”‚
β”‚ email and profile" β”‚
β”‚<────────│ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ 5. Approveβ”‚ β”‚ β”‚
│────────>β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 6. Issue Authorization Code β”‚
β”‚ β”‚ (one-time code) β”‚
β”‚<────────│ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ 7. Pass Authorization Code β”‚
│──────────────────────────────────────────>β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Google β”‚ β”‚
β”‚ β”‚ (Auth Server) β”‚ 8. Exchange β”‚
β”‚ β”‚ β”‚<─── Code + β”‚
β”‚ β”‚ β”‚ Client β”‚
β”‚ β”‚ β”‚ Secret β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 9. Issue Access β”‚ ────────────>β”‚
β”‚ β”‚ Token β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Google API β”‚ β”‚
β”‚ β”‚ (Resource β”‚ 10. Request β”‚
β”‚ β”‚ Server) β”‚<──── user β”‚
β”‚ β”‚ β”‚ info β”‚
β”‚ β”‚ β”‚ with β”‚
β”‚ β”‚ β”‚ Token β”‚
β”‚ β”‚ 11. User info β”‚ ────────────>β”‚
β”‚ β”‚ response β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ 12. Login complete! β”‚
β”‚<──────────────────────────────────────────│
β”‚ β”‚

Key Points:
- User password is only entered at Google
- Web app never knows the password
- Access token only allows access to authorized information

3. OAuth 2.0 Grant Types​

========== 1. Authorization Code (Most Secure) ==========
Use: Server-side web applications
Pros: Most secure (uses Client Secret)
Example: "Sign in with Google"

Flow:
1. User β†’ Google login
2. Google β†’ Issue Authorization Code
3. Server β†’ Exchange Code + Secret for Access Token
4. Server β†’ Call API with Token

========== 2. Implicit (Simple but Less Secure) ==========
Use: Browser-only apps (SPA)
Pros: Simple (no server needed)
Cons: Security vulnerability (Token exposed in URL)
Example: Legacy SPA
⚠️ Not recommended anymore

========== 3. Resource Owner Password (Legacy) ==========
Use: Only trusted apps
Pros: Simple
Cons: App knows user password
Example: First-party mobile apps
⚠️ Against OAuth philosophy, not recommended

========== 4. Client Credentials ==========
Use: Server-to-server communication (M2M)
Pros: No user authentication needed
Example: Backend service communication

Flow:
1. Server β†’ Send Client ID + Secret
2. Authorization Server β†’ Issue Access Token
3. Server β†’ Call API with Token

========== 5. PKCE (Proof Key for Code Exchange) ==========
Use: Mobile apps, SPA (currently recommended)
Pros: Authorization Code + additional security
Example: Modern mobile/SPA apps

Flow:
1. App β†’ Generate code_verifier (random string)
2. App β†’ Generate code_challenge (verifier hash)
3. App β†’ Request Authorization Code with challenge
4. Google β†’ Issue Code
5. App β†’ Exchange Code + verifier for Token
6. Google β†’ Verify verifier and issue Token

πŸ’‘ Real Examples​

Google OAuth 2.0 Login (Node.js)​

// ========== 1. Google Cloud Console Setup ==========
/*
1. Go to https://console.cloud.google.com
2. Create a project
3. "APIs & Services" β†’ "Credentials"
4. Create "OAuth 2.0 Client ID"
5. Add authorized redirect URI:
http://localhost:3000/auth/google/callback
6. Get Client ID and Client Secret
*/

// ========== 2. Server Implementation ==========
const express = require('express');
const axios = require('axios');
const session = require('express-session');

const app = express();

// Session setup
app.use(session({
secret: 'your-session-secret',
resave: false,
saveUninitialized: true
}));

// Google OAuth configuration
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
const REDIRECT_URI = 'http://localhost:3000/auth/google/callback';

// ========== Step 1: Start Login ==========
app.get('/auth/google', (req, res) => {
// Create Google auth URL
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';

const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code', // Request Authorization Code
scope: 'openid email profile', // Requested permissions
access_type: 'offline', // Get Refresh Token
prompt: 'consent' // Always show consent screen
});

// Redirect to Google login page
res.redirect(`${authUrl}?${params}`);
});

// ========== Step 2: Handle Callback ==========
app.get('/auth/google/callback', async (req, res) => {
const { code, error } = req.query;

// User denied
if (error) {
return res.status(400).send(`Authentication failed: ${error}`);
}

try {
// Exchange Authorization Code for Access Token
const tokenResponse = await axios.post(
'https://oauth2.googleapis.com/token',
{
code,
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code'
}
);

const {
access_token,
refresh_token,
expires_in,
token_type
} = tokenResponse.data;

console.log('Access Token:', access_token);
console.log('Expires in:', expires_in, 'seconds');

// Get user info with Access Token
const userResponse = await axios.get(
'https://www.googleapis.com/oauth2/v2/userinfo',
{
headers: {
Authorization: `Bearer ${access_token}`
}
}
);

const user = userResponse.data;
/*
{
id: "123456789",
email: "user@gmail.com",
verified_email: true,
name: "John Doe",
picture: "https://lh3.googleusercontent.com/...",
locale: "en"
}
*/

// Save user info to session
req.session.user = {
id: user.id,
email: user.email,
name: user.name,
picture: user.picture,
accessToken: access_token,
refreshToken: refresh_token
};

// Save user to database
await saveOrUpdateUser(user);

// Redirect to home page
res.redirect('/dashboard');
} catch (error) {
console.error('OAuth error:', error.response?.data || error.message);
res.status(500).send('Error during authentication');
}
});

// ========== Check User Info ==========
app.get('/dashboard', (req, res) => {
if (!req.session.user) {
return res.redirect('/');
}

res.send(`
<h1>Hello, ${req.session.user.name}!</h1>
<img src="${req.session.user.picture}" alt="Profile" />
<p>Email: ${req.session.user.email}</p>
<a href="/logout">Logout</a>
`);
});

// ========== Logout ==========
app.get('/logout', async (req, res) => {
// Revoke Google Access Token (optional)
const accessToken = req.session.user?.accessToken;
if (accessToken) {
try {
await axios.post(
`https://oauth2.googleapis.com/revoke?token=${accessToken}`
);
} catch (error) {
console.error('Token revocation failed:', error.message);
}
}

// Destroy session
req.session.destroy();
res.redirect('/');
});

// ========== Home Page ==========
app.get('/', (req, res) => {
res.send(`
<h1>OAuth 2.0 Demo</h1>
<a href="/auth/google">
<button>Sign in with Google</button>
</a>
`);
});

app.listen(3000, () => {
console.log('Server running: http://localhost:3000');
});

Easy Implementation with Passport.js​

// ========== Using Passport.js ==========
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

// Passport configuration
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: 'http://localhost:3000/auth/google/callback'
},
async (accessToken, refreshToken, profile, done) => {
// Handle user info
try {
// Find or create user in database
let user = await User.findOne({ googleId: profile.id });

if (!user) {
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
picture: profile.photos[0].value
});
}

done(null, user);
} catch (error) {
done(error, null);
}
}
));

// Session serialization
passport.serializeUser((user, done) => {
done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
const user = await User.findById(id);
done(null, user);
});

// Express configuration
app.use(passport.initialize());
app.use(passport.session());

// ========== Routes ==========

// Start Google login
app.get('/auth/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);

// Google callback
app.get('/auth/google/callback',
passport.authenticate('google', {
failureRedirect: '/login',
successRedirect: '/dashboard'
})
);

// Logout
app.get('/logout', (req, res) => {
req.logout((err) => {
if (err) console.error(err);
res.redirect('/');
});
});

// Authentication middleware
function isAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/login');
}

// Protected route
app.get('/dashboard', isAuthenticated, (req, res) => {
res.send(`Hello, ${req.user.name}!`);
});

GitHub OAuth 2.0​

// ========== GitHub OAuth Setup ==========
/*
1. Go to https://github.com/settings/developers
2. Click "New OAuth App"
3. Fill in information:
- Application name: My App
- Homepage URL: http://localhost:3000
- Authorization callback URL: http://localhost:3000/auth/github/callback
4. Get Client ID and Client Secret
*/

const express = require('express');
const axios = require('axios');
const app = express();

const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID;
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET;
const REDIRECT_URI = 'http://localhost:3000/auth/github/callback';

// ========== GitHub Login ==========
app.get('/auth/github', (req, res) => {
const authUrl = 'https://github.com/login/oauth/authorize';

const params = new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'user:email', // Requested permissions
state: Math.random().toString(36) // CSRF prevention
});

res.redirect(`${authUrl}?${params}`);
});

// ========== Handle Callback ==========
app.get('/auth/github/callback', async (req, res) => {
const { code, state } = req.query;

try {
// 1. Get Access Token
const tokenResponse = await axios.post(
'https://github.com/login/oauth/access_token',
{
client_id: GITHUB_CLIENT_ID,
client_secret: GITHUB_CLIENT_SECRET,
code,
redirect_uri: REDIRECT_URI
},
{
headers: { Accept: 'application/json' }
}
);

const { access_token } = tokenResponse.data;

// 2. Get user info
const userResponse = await axios.get('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${access_token}`
}
});

const user = userResponse.data;
/*
{
login: "username",
id: 12345,
avatar_url: "https://avatars.githubusercontent.com/...",
name: "John Doe",
email: "user@example.com",
bio: "Developer",
public_repos: 50
}
*/

// 3. Get email (separate request)
const emailResponse = await axios.get('https://api.github.com/user/emails', {
headers: {
Authorization: `Bearer ${access_token}`
}
});

const primaryEmail = emailResponse.data.find(email => email.primary);

res.json({
user,
email: primaryEmail.email
});
} catch (error) {
console.error('GitHub OAuth error:', error.message);
res.status(500).send('Authentication failed');
}
});

app.listen(3000);

Using OAuth in React​

// ========== React Client ==========

import React, { useEffect, useState } from 'react';
import axios from 'axios';

function App() {
const [user, setUser] = useState(null);

// Check user info on page load
useEffect(() => {
checkAuth();
}, []);

async function checkAuth() {
try {
const response = await axios.get('/api/me', {
withCredentials: true // Include cookies
});
setUser(response.data);
} catch (error) {
console.log('Not logged in');
}
}

function handleGoogleLogin() {
// Navigate to server's OAuth endpoint
window.location.href = 'http://localhost:3000/auth/google';
}

function handleGitHubLogin() {
window.location.href = 'http://localhost:3000/auth/github';
}

function handleLogout() {
window.location.href = 'http://localhost:3000/logout';
}

if (user) {
return (
<div>
<h1>Hello, {user.name}!</h1>
<img src={user.picture} alt="Profile" />
<p>{user.email}</p>
<button onClick={handleLogout}>Logout</button>
</div>
);
}

return (
<div>
<h1>Login</h1>
<button onClick={handleGoogleLogin}>
πŸ”΅ Sign in with Google
</button>
<button onClick={handleGitHubLogin}>
⚫ Sign in with GitHub
</button>
</div>
);
}

export default App;

Enhanced Security with PKCE (SPA/Mobile)​

// ========== PKCE (Proof Key for Code Exchange) ==========
// Used in mobile apps or SPAs

const crypto = require('crypto');

// ========== 1. Generate Code Verifier ==========
function generateCodeVerifier() {
return base64URLEncode(crypto.randomBytes(32));
}

// ========== 2. Generate Code Challenge ==========
function generateCodeChallenge(verifier) {
const hash = crypto.createHash('sha256')
.update(verifier)
.digest();
return base64URLEncode(hash);
}

function base64URLEncode(buffer) {
return buffer.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}

// ========== 3. Start Login (Client) ==========
async function startPKCELogin() {
// Generate and store verifier
const codeVerifier = generateCodeVerifier();
sessionStorage.setItem('code_verifier', codeVerifier);

// Generate challenge
const codeChallenge = generateCodeChallenge(codeVerifier);

// Google auth URL
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'openid email profile',
code_challenge: codeChallenge, // PKCE
code_challenge_method: 'S256' // SHA-256
});

window.location.href = `${authUrl}?${params}`;
}

// ========== 4. Handle Callback ==========
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');

// Get stored verifier
const codeVerifier = sessionStorage.getItem('code_verifier');

// Exchange token
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
code_verifier: codeVerifier // PKCE Verifier
})
});

const { access_token } = await response.json();

// Delete verifier
sessionStorage.removeItem('code_verifier');

return access_token;
}

πŸ€” Frequently Asked Questions​

Q1. OAuth vs JWT vs Session?​

A:

// ========== 1. Session ==========
// Traditional method

// Login
app.post('/login', (req, res) => {
const { username, password } = req.body;

// Verify authentication
if (isValidUser(username, password)) {
// Store in session
req.session.userId = user.id;
res.json({ message: 'Login successful' });
}
});

// Check authentication
app.get('/profile', (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: 'Login required' });
}

const user = getUserById(req.session.userId);
res.json(user);
});

Pros: Simple, full server control
Cons: Requires server memory/storage, low scalability

// ========== 2. JWT (JSON Web Token) ==========
// Stateless authentication

// Login
app.post('/login', (req, res) => {
const { username, password } = req.body;

if (isValidUser(username, password)) {
// Create JWT
const token = jwt.sign(
{ userId: user.id, username: user.username },
SECRET_KEY,
{ expiresIn: '1h' }
);

res.json({ token });
}
});

// Check authentication
app.get('/profile', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];

try {
const decoded = jwt.verify(token, SECRET_KEY);
const user = getUserById(decoded.userId);
res.json(user);
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
});

Pros: Stateless, good scalability
Cons: Difficult to revoke, large size

// ========== 3. OAuth 2.0 ==========
// Third-party authentication and authorization

// User logs in with Google
// β†’ Google issues Access Token
// β†’ Our app calls Google API with Token

Pros:
- No password sharing
- Fine-grained permission control
- Standard protocol
- Enables social login

Cons:
- Complex
- Dependent on external services

// ========== Comparison ==========
Feature | Session | JWT | OAuth
----------------|---------|----------|-------
Storage | Server | Client | Server
Scalability | Low | High | High
Security | High | Medium | High
Complexity | Low | Medium | High
Third-party Auth| No | No | Yes

// ========== Combined Use ==========
// Issue JWT after OAuth login

app.get('/auth/google/callback', async (req, res) => {
// 1. Authenticate user with Google OAuth
const googleUser = await getGoogleUser(req.query.code);

// 2. Save user to our database
const user = await saveOrUpdateUser(googleUser);

// 3. Create and return JWT
const token = jwt.sign(
{ userId: user.id, email: user.email },
SECRET_KEY,
{ expiresIn: '7d' }
);

res.json({ token });
});

// Subsequent requests use JWT
app.get('/api/profile', authenticateJWT, (req, res) => {
res.json(req.user);
});

Q2. What's the difference between Access Token and Refresh Token?​

A:

// ========== Access Token ==========
// Short lifespan (15 minutes~1 hour)
// Used for API requests

// ========== Refresh Token ==========
// Long lifespan (7 days~30 days)
// Used to refresh Access Token

// ========== Implementation Example ==========

// Issue both on login
app.post('/login', async (req, res) => {
const user = await authenticateUser(req.body);

// Access Token (15 minutes)
const accessToken = jwt.sign(
{ userId: user.id, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);

// Refresh Token (7 days)
const refreshToken = jwt.sign(
{ userId: user.id, type: 'refresh' },
REFRESH_SECRET,
{ expiresIn: '7d' }
);

// Save Refresh Token to DB (for revocation)
await saveRefreshToken(user.id, refreshToken);

res.json({ accessToken, refreshToken });
});

// ========== API Request (Access Token) ==========
app.get('/api/data', authenticateAccess, (req, res) => {
res.json({ data: 'Important data' });
});

function authenticateAccess(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];

try {
const decoded = jwt.verify(token, ACCESS_SECRET);

if (decoded.type !== 'access') {
throw new Error('Invalid token type');
}

req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'access_token_expired',
message: 'Please refresh with Refresh Token'
});
}

res.status(401).json({ error: 'Invalid token' });
}
}

// ========== Refresh Access Token ==========
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;

if (!refreshToken) {
return res.status(401).json({ error: 'Refresh Token required' });
}

try {
// 1. Verify Refresh Token
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);

if (decoded.type !== 'refresh') {
throw new Error('Invalid token type');
}

// 2. Check in DB (if revoked)
const isValid = await isRefreshTokenValid(decoded.userId, refreshToken);
if (!isValid) {
throw new Error('Revoked Refresh Token');
}

// 3. Issue new Access Token
const newAccessToken = jwt.sign(
{ userId: decoded.userId, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);

res.json({ accessToken: newAccessToken });
} catch (error) {
res.status(401).json({ error: 'Invalid Refresh Token' });
}
});

// ========== Frontend Auto-refresh ==========
import axios from 'axios';

let accessToken = localStorage.getItem('accessToken');
let refreshToken = localStorage.getItem('refreshToken');

// Axios interceptor
axios.interceptors.request.use(
config => {
config.headers.Authorization = `Bearer ${accessToken}`;
return config;
}
);

axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;

// Access Token expired
if (error.response?.data?.error === 'access_token_expired' &&
!originalRequest._retry) {
originalRequest._retry = true;

try {
// Refresh with Refresh Token
const response = await axios.post('/auth/refresh', {
refreshToken
});

accessToken = response.data.accessToken;
localStorage.setItem('accessToken', accessToken);

// Retry original request
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
} catch (refreshError) {
// Refresh Token also expired β†’ Logout
localStorage.clear();
window.location.href = '/login';
}
}

return Promise.reject(error);
}
);

// ========== Security Tips ==========

// βœ… Good practices
1. Access Token: Short lifespan (15 minutes)
2. Refresh Token: Store in DB for revocation
3. Refresh Token in HttpOnly cookies
4. HTTPS required
5. Refresh Token Rotation (issue new token on refresh)

// ❌ Bad practices
1. Access Token lifespan too long
2. Refresh Token in localStorage
3. Refresh Token cannot be revoked
4. Using HTTP

Q3. What is Scope?​

A:

// ========== Scope Concept ==========
// Range of permissions requested by the application

// ========== Google OAuth Scopes ==========
const scopes = [
'openid', // Basic info
'email', // Email
'profile', // Profile (name, picture)
'https://www.googleapis.com/auth/drive.readonly', // Drive read
'https://www.googleapis.com/auth/gmail.send' // Gmail send
];

// Request permissions on login
app.get('/auth/google', (req, res) => {
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: scopes.join(' ') // Separated by space
});

res.redirect(`${authUrl}?${params}`);
});

// Screen shown to user:
/*
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
My App requests:

βœ“ View your email address
βœ“ View your basic profile info
βœ“ View your Google Drive files
βœ“ Send emails via Gmail

[Allow] [Deny]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
*/

// ========== GitHub OAuth Scopes ==========
const githubScopes = [
'user', // User info
'user:email', // Email (including private)
'repo', // Full repository access
'public_repo', // Public repositories only
'read:org', // Read organization info
'write:org' // Write organization info
];

app.get('/auth/github', (req, res) => {
const authUrl = 'https://github.com/login/oauth/authorize';
const params = new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'user:email public_repo' // Only necessary permissions
});

res.redirect(`${authUrl}?${params}`);
});

// ========== Principle of Least Privilege ==========

// ❌ Bad: Requesting more permissions than needed
scope: 'user repo admin:org' // Too many permissions!

// βœ… Good: Only necessary permissions
scope: 'user:email' // Only email needed

// ========== Dynamic Scope Request ==========

// Initial login: Basic info only
app.get('/auth/google', (req, res) => {
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'email profile' // Basic info only
});

res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});

// Later when additional permissions needed
app.get('/auth/google/drive', (req, res) => {
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'https://www.googleapis.com/auth/drive.readonly',
prompt: 'consent' // Request re-consent
});

res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});

// ========== Incremental Authorization ==========
// Progressively add permissions as needed

// Step 1: Login (basic info)
scope: 'email profile'

// Step 2: Google Drive integration (when user wants)
scope: 'https://www.googleapis.com/auth/drive'

// Step 3: Gmail integration (when user wants)
scope: 'https://www.googleapis.com/auth/gmail.send'

// ========== Scope Validation ==========

// Check scope included in Access Token
async function checkScope(accessToken, requiredScope) {
const response = await axios.get(
'https://www.googleapis.com/oauth2/v1/tokeninfo',
{ params: { access_token: accessToken } }
);

const grantedScopes = response.data.scope.split(' ');

if (!grantedScopes.includes(requiredScope)) {
throw new Error('Insufficient permissions');
}
}

// Usage example
app.get('/api/drive/files', async (req, res) => {
const accessToken = req.headers.authorization?.split(' ')[1];

try {
// Check Drive permission
await checkScope(accessToken, 'https://www.googleapis.com/auth/drive.readonly');

// Call Drive API
const files = await listDriveFiles(accessToken);
res.json(files);
} catch (error) {
res.status(403).json({
error: 'Insufficient permissions',
message: 'Google Drive access permission required'
});
}
});

Q4. What are OAuth security best practices?​

A:

// ========== 1. HTTPS Required ==========
// βœ… Always use HTTPS
const REDIRECT_URI = 'https://myapp.com/callback';

// ❌ HTTP absolutely forbidden
const REDIRECT_URI = 'http://myapp.com/callback'; // Token theft risk!

// ========== 2. State Parameter (CSRF Prevention) ==========

// Start login
app.get('/auth/google', (req, res) => {
// Generate and store random state
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;

const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'email profile',
state // CSRF prevention
});

res.redirect(`${authUrl}?${params}`);
});

// Verify in callback
app.get('/auth/google/callback', (req, res) => {
const { state, code } = req.query;

// Check state
if (state !== req.session.oauthState) {
return res.status(403).send('CSRF attack detected');
}

// Delete state
delete req.session.oauthState;

// ... continue processing
});

// ========== 3. Protect Client Secret ==========

// βœ… Use environment variables
const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;

// βœ… Only use on server
// Never expose to frontend!

// ❌ No hardcoding in code
const CLIENT_SECRET = 'abc123xyz'; // Absolutely forbidden!

// ========== 4. Redirect URI Validation ==========

// Only URIs registered in Google Cloud Console allowed
// βœ… Exact match URIs
https://myapp.com/auth/callback

// ❌ No wildcards
https://myapp.com/*

// ❌ Open redirector vulnerability
https://myapp.com/callback?redirect=evil.com

// ========== 5. Token Storage ==========

// βœ… Server: Environment variables or encrypted DB
await saveToken(userId, encryptToken(accessToken));

// βœ… Client: HttpOnly cookies (XSS prevention)
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict',
maxAge: 3600000 // 1 hour
});

// ❌ localStorage (XSS vulnerable)
localStorage.setItem('token', accessToken); // Dangerous!

// ========== 6. Token Expiration ==========

// βœ… Short lifespan
const accessToken = jwt.sign(payload, secret, {
expiresIn: '15m' // 15 minutes
});

// βœ… Use Refresh Token
const refreshToken = jwt.sign(payload, secret, {
expiresIn: '7d' // 7 days
});

// ❌ No expiration
const token = jwt.sign(payload, secret); // Dangerous!

// ========== 7. Minimize Scope ==========

// βœ… Only necessary permissions
scope: 'email profile'

// ❌ Request all permissions
scope: 'https://www.googleapis.com/auth/drive' // Unnecessary

// ========== 8. Implement Token Revocation ==========

// Revoke token on logout
app.get('/logout', async (req, res) => {
const accessToken = req.cookies.access_token;

// Revoke Google token
await axios.post(
`https://oauth2.googleapis.com/revoke?token=${accessToken}`
);

// Delete Refresh Token from DB
await deleteRefreshToken(req.user.id);

// Clear cookie
res.clearCookie('access_token');

res.redirect('/');
});

// ========== 9. Use PKCE (Mobile/SPA) ==========

// Enhanced security without Client Secret
const codeVerifier = generateRandomString(128);
const codeChallenge = base64URLEncode(sha256(codeVerifier));

// Send challenge on auth request
const params = {
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
code_challenge: codeChallenge,
code_challenge_method: 'S256'
};

// Send verifier on token exchange
const tokenParams = {
code,
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
code_verifier: codeVerifier // Instead of Secret
};

// ========== 10. Regular Token Refresh ==========

// Auto-refresh in background
setInterval(async () => {
try {
const newAccessToken = await refreshAccessToken(refreshToken);
updateToken(newAccessToken);
} catch (error) {
console.error('Token refresh failed:', error);
redirectToLogin();
}
}, 14 * 60 * 1000); // Every 14 minutes (before 15 min expiration)

Q5. Authentication vs Authorization?​

A:

// ========== Authentication ==========
// "Who are you?" - Identity verification

// Example: Login
const user = await authenticateUser(username, password);
if (user) {
console.log('You are John Doe βœ“');
}

// ========== Authorization ==========
// "What can you do?" - Permission verification

// Example: Admin only access
if (user.role === 'admin') {
console.log('Can use admin features βœ“');
} else {
console.log('No permission βœ—');
}

// ========== OAuth 2.0's Role ==========

// OAuth is primarily Authorization
// - User grants permissions to app
// - "Allow this app to view my Google Drive"

// But also used for Authentication
// - OpenID Connect (OAuth 2.0 + Authentication)
// - "Sign in with Google"

// ========== Real Example ==========

// Step 1: Authentication
app.post('/login', async (req, res) => {
const { username, password } = req.body;

// Verify user identity
const user = await User.findOne({ username });

if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: 'Authentication failed' });
}

// Authentication successful β†’ Issue token
const token = jwt.sign(
{
userId: user.id,
role: user.role // Include permission info
},
SECRET_KEY
);

res.json({ token });
});

// Step 2: Authorization
app.get('/admin/users', authenticateToken, authorizeAdmin, (req, res) => {
// Admin only access
const users = await User.findAll();
res.json(users);
});

function authenticateToken(req, res, next) {
// Authentication: Verify token
const token = req.headers.authorization?.split(' ')[1];

try {
const decoded = jwt.verify(token, SECRET_KEY);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: 'Authentication required' });
}
}

function authorizeAdmin(req, res, next) {
// Authorization: Check admin
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'No permission' });
}
next();
}

// ========== Authentication + Authorization with OAuth ==========

// 1. Authentication (OpenID Connect)
app.get('/auth/google/callback', async (req, res) => {
// Verify user with Google OAuth
const googleUser = await getGoogleUser(req.query.code);

// Authentication complete β†’ User identity verified
console.log('User:', googleUser.email);

// Login to our system
const user = await findOrCreateUser(googleUser);
req.session.userId = user.id;
});

// 2. Authorization (Scope)
app.get('/api/drive/files', async (req, res) => {
// Check Google Drive permission
const hasPermission = await checkGoogleScope(
req.user.accessToken,
'https://www.googleapis.com/auth/drive.readonly'
);

if (!hasPermission) {
return res.status(403).json({
error: 'Permission required',
message: 'Please grant Google Drive access permission'
});
}

// Has permission β†’ Call API
const files = await listDriveFiles(req.user.accessToken);
res.json(files);
});

// ========== Comparison Summary ==========

Authentication
- Question: "Who are you?"
- Purpose: Identity verification
- Examples:
* Password entry
* Fingerprint recognition
* Sign in with Google
- HTTP status code: 401 Unauthorized

Authorization
- Question: "What can you do?"
- Purpose: Access permission verification
- Examples:
* Admin permissions
* Google Drive read permission
* OAuth Scope
- HTTP status code: 403 Forbidden

// Both needed!
// 1. First authentication (login)
// 2. Then authorization check (allow access)

πŸŽ“ Next Steps​

After understanding OAuth 2.0, learn about:

  1. JWT Tokens (document pending) - Token-based authentication
  2. What is HTTPS? (document pending) - Secure communication
  3. What is CORS? (document pending) - API security

Practice​

# ========== 1. Google OAuth Practice ==========

# Create project
mkdir oauth-demo
cd oauth-demo
npm init -y

# Install packages
npm install express axios express-session dotenv

# Create .env file
cat > .env << EOF
GOOGLE_CLIENT_ID=your_client_id
GOOGLE_CLIENT_SECRET=your_client_secret
SESSION_SECRET=your_session_secret
EOF

# Write server.js then run
node server.js

# Visit http://localhost:3000

# ========== 2. Passport.js Practice ==========

npm install passport passport-google-oauth20

# ========== 3. Implement Multiple Social Logins ==========

npm install passport-github2 passport-facebook

# Support Google, GitHub, Facebook all together

🎬 Conclusion​

OAuth 2.0 is the standard authentication protocol of the modern web:

  • Security: Safe authentication without password sharing
  • Convenience: Quick registration with social login
  • Permission Control: Grant only necessary permissions
  • Standard: Supported by all major services

Build a secure and convenient authentication system! πŸ”