π 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:
- JWT Tokens (document pending) - Token-based authentication
- What is HTTPS? (document pending) - Secure communication
- 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! π