API authentication is how you prove your identity when making API requests. This beginner-friendly guide explains different authentication methods, when to use each, and how to implement them.
Table of Contents
- What is API Authentication?
- Why Authentication Matters
- API Keys
- Bearer Tokens
- JWT (JSON Web Tokens)
- OAuth 2.0
- Basic Authentication
- Comparison and Best Practices
What is API Authentication?
Authentication is the process of verifying who you are. When you use an API, authentication proves that you're allowed to access it.
Simple Analogy: Authentication is like showing your ID at a club entrance. The bouncer (API) checks your ID (credentials) to verify you're allowed in. Different clubs have different ID requirements - some want a driver's license (API key), some want a membership card (token), some let friends vouch for you (OAuth).
Authentication vs Authorization
Authentication: "Who are you?"
Authorization: "What are you allowed to do?"
AUTHENTICATION (Identity):
"I am John Doe" (prove it with password/token)
✅ Login successful
AUTHORIZATION (Permission):
"Can John Doe delete this file?"
✅ Yes, he's an admin
❌ No, he's only a viewer
How Authentication Works
1. CLIENT: "I want to access your API"
2. SERVER: "Show me your credentials"
3. CLIENT: Sends credentials (API key, token, etc.)
4. SERVER: Verifies credentials
├─ Valid → "Welcome! Here's your data"
└─ Invalid → "Access denied" (401 Unauthorized)
5. CLIENT: Uses data
Why Authentication Matters
Without Authentication (Public API)
// Anyone can access - no security
fetch('https://api.example.com/weather')
.then(res => res.json())
.then(data => console.log(data));
// Problems:
// ❌ No usage limits (abuse possible)
// ❌ Can't track who's using it
// ❌ No personalized data
// ❌ Can't charge for usage
// ❌ Anyone can spam your server
With Authentication
// Must prove identity
fetch('https://api.example.com/user/profile', {
headers: {
'Authorization': 'Bearer your-token-here'
}
})
.then(res => res.json())
.then(data => console.log(data));
// Benefits:
// ✅ Track usage per user
// ✅ Enforce rate limits
// ✅ Provide personalized data
// ✅ Charge based on usage
// ✅ Block abusive users
// ✅ Protect sensitive data
API Keys
API Keys are the simplest form of authentication. They're like passwords for your application.
How API Keys Work
1. Sign up for the service
2. Service gives you an API key: "abc123xyz789"
3. Include key in every API request
4. Server checks if key is valid
5. If valid → Access granted ✅
If invalid → Access denied ❌
Using API Keys
Method 1: Query Parameter (Simple but less secure)
// In URL (visible in logs and browser history)
fetch('https://api.example.com/data?api_key=abc123xyz789')
.then(res => res.json())
.then(data => console.log(data));
// Example: Weather API
fetch('https://api.openweathermap.org/data/2.5/weather?q=London&appid=YOUR_API_KEY')
.then(res => res.json())
.then(data => console.log(data));
Method 2: Header (More secure)
// In header (not visible in URL)
fetch('https://api.example.com/data', {
headers: {
'X-API-Key': 'abc123xyz789'
}
})
.then(res => res.json())
.then(data => console.log(data));
// or sometimes:
fetch('https://api.example.com/data', {
headers: {
'Authorization': 'ApiKey abc123xyz789'
}
})
.then(res => res.json())
.then(data => console.log(data));
Real-World API Key Examples
// Google Maps API
const googleMapsKey = 'AIzaSyB...';
const script = document.createElement('script');
script.src = \`https://maps.googleapis.com/maps/api/js?key=\${googleMapsKey}\`;
document.head.appendChild(script);
// OpenWeatherMap API
const weatherKey = 'abc123xyz789';
fetch(\`https://api.openweathermap.org/data/2.5/weather?q=London&appid=\${weatherKey}\`)
.then(res => res.json())
.then(data => console.log(data));
// SendGrid Email API
fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': \`Bearer \${sendgridApiKey}\`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
personalizations: [{ to: [{ email: 'user@example.com' }] }],
from: { email: 'noreply@example.com' },
subject: 'Hello',
content: [{ type: 'text/plain', value: 'Hello, World!' }]
})
});
API Key Best Practices
// ✅ GOOD - Store in environment variables
const API_KEY = process.env.API_KEY;
fetch(\`https://api.example.com/data?api_key=\${API_KEY}\`);
// ❌ BAD - Hardcoded in code
const API_KEY = 'abc123xyz789'; // Visible in Git!
// ✅ GOOD - Use .env file
// .env
API_KEY=abc123xyz789
// ❌ BAD - Expose in frontend (visible in browser)
// Client-side JavaScript - anyone can see your key!
fetch(\`https://api.example.com/data?api_key=abc123xyz789\`);
// ✅ GOOD - Use proxy server
// Frontend calls your server, server calls API with key
fetch('/api/weather?city=London') // Your server
.then(res => res.json());
// Your server (backend):
app.get('/api/weather', async (req, res) => {
const response = await fetch(
\`https://api.openweathermap.org/data/2.5/weather?q=\${req.query.city}&appid=\${process.env.API_KEY}\`
);
const data = await response.json();
res.json(data);
});
Security Warning: Never commit API keys to Git! Use environment variables and add .env to .gitignore. If you accidentally commit a key, regenerate it immediately.
When to Use API Keys
✅ Good for:
- Server-to-server communication
- Backend applications
- Development/testing
- Simple authentication needs
- Service-to-service authentication
❌ Avoid for:
- Frontend applications (keys exposed)
- User-specific authentication
- Complex permission systems
- When you need expiration/refresh
Bearer Tokens
Bearer Tokens are strings that grant access to resources. "Bearer" means "whoever holds this token has access."
How Bearer Tokens Work
1. Login with username/password
POST /api/login
Body: { "email": "user@example.com", "password": "secret" }
2. Server verifies credentials
✅ Valid → Generate token
3. Server returns token
Response: { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }
4. Client stores token (localStorage, cookie, etc.)
5. Client sends token with every request
GET /api/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
6. Server validates token
✅ Valid → Return data
❌ Invalid/Expired → 401 Unauthorized
Using Bearer Tokens
// Step 1: Login and get token
async function login(email, password) {
const response = await fetch('https://api.example.com/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, password })
});
const data = await response.json();
// Store token
localStorage.setItem('token', data.token);
return data.token;
}
// Step 2: Use token for authenticated requests
async function getUserProfile() {
const token = localStorage.getItem('token');
const response = await fetch('https://api.example.com/user/profile', {
headers: {
'Authorization': \`Bearer \${token}\`
}
});
if (response.status === 401) {
// Token expired or invalid
// Redirect to login
window.location.href = '/login';
return;
}
const profile = await response.json();
return profile;
}
// Step 3: Logout (remove token)
function logout() {
localStorage.removeItem('token');
window.location.href = '/login';
}
Complete Authentication Flow
class AuthService {
constructor() {
this.token = localStorage.getItem('token');
}
async login(email, password) {
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) {
throw new Error('Login failed');
}
const data = await response.json();
this.token = data.token;
localStorage.setItem('token', data.token);
return data;
} catch (error) {
console.error('Login error:', error);
throw error;
}
}
async fetchWithAuth(url, options = {}) {
// Add token to every request
const headers = {
...options.headers,
'Authorization': \`Bearer \${this.token}\`
};
const response = await fetch(url, { ...options, headers });
// Handle expired token
if (response.status === 401) {
this.logout();
window.location.href = '/login';
throw new Error('Authentication required');
}
return response;
}
logout() {
this.token = null;
localStorage.removeItem('token');
}
isAuthenticated() {
return !!this.token;
}
}
// Usage
const auth = new AuthService();
// Login
await auth.login('user@example.com', 'password123');
// Make authenticated requests
const response = await auth.fetchWithAuth('/api/profile');
const profile = await response.json();
// Logout
auth.logout();
JWT (JSON Web Tokens)
JWT is a specific type of bearer token that contains encoded information about the user.
What is JWT?
A JWT consists of three parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
└────────────────┬────────────────┘ └──────────────────────┬──────────────────────┘ └──────────────┬──────────────┘
HEADER PAYLOAD SIGNATURE
Header: Token type and algorithm
{
"alg": "HS256",
"typ": "JWT"
}
Payload: User data and claims
{
"sub": "1234567890",
"name": "John Doe",
"email": "john@example.com",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
Signature: Ensures token hasn't been tampered with
JWT Advantages
✅ Self-contained: All info is in the token (no database lookup needed)
✅ Stateless: Server doesn't need to store sessions
✅ Scalable: Works across multiple servers
✅ Secure: Cryptographically signed
✅ Expiration: Built-in expiry time
Creating and Using JWT (Node.js)
const jwt = require('jsonwebtoken');
const SECRET_KEY = process.env.JWT_SECRET;
// ===================================
// SERVER: Generate JWT on login
// ===================================
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
// Verify credentials (check database)
const user = await User.findOne({ email });
if (!user || !await user.comparePassword(password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate JWT
const token = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
SECRET_KEY,
{
expiresIn: '24h' // Token expires in 24 hours
}
);
res.json({ token, user: { id: user.id, email: user.email } });
});
// ===================================
// SERVER: Verify JWT middleware
// ===================================
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"
if (!token) {
return res.status(401).json({ error: 'Token required' });
}
try {
const decoded = jwt.verify(token, SECRET_KEY);
req.user = decoded; // Add user info to request
next();
} catch (error) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
}
// ===================================
// SERVER: Protected routes
// ===================================
app.get('/api/profile', authenticateToken, (req, res) => {
// req.user contains decoded JWT data
res.json({
userId: req.user.userId,
email: req.user.email,
role: req.user.role
});
});
app.get('/api/admin', authenticateToken, (req, res) => {
// Check role from JWT
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
res.json({ message: 'Admin data' });
});
JWT Client-Side Usage
// Login and store JWT
async function login(email, password) {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
// Store JWT
localStorage.setItem('jwt', data.token);
return data;
}
// Decode JWT to read user info (without verifying)
function parseJwt(token) {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
return JSON.parse(jsonPayload);
}
// Check if token is expired
function isTokenExpired(token) {
try {
const decoded = parseJwt(token);
const currentTime = Date.now() / 1000;
return decoded.exp < currentTime;
} catch {
return true;
}
}
// Use JWT for requests
async function fetchWithJWT(url, options = {}) {
const token = localStorage.getItem('jwt');
if (!token || isTokenExpired(token)) {
// Redirect to login
window.location.href = '/login';
return;
}
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': \`Bearer \${token}\`
}
});
}
JWT Refresh Tokens
// ===================================
// SERVER: Generate access + refresh tokens
// ===================================
app.post('/api/login', async (req, res) => {
const user = await authenticateUser(req.body);
// Short-lived access token (15 minutes)
const accessToken = jwt.sign(
{ userId: user.id },
ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
// Long-lived refresh token (7 days)
const refreshToken = jwt.sign(
{ userId: user.id },
REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);
// Store refresh token in database
await RefreshToken.create({ userId: user.id, token: refreshToken });
res.json({ accessToken, refreshToken });
});
// ===================================
// SERVER: Refresh access token
// ===================================
app.post('/api/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
// Verify refresh token
try {
const decoded = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET);
// Check if refresh token exists in database
const storedToken = await RefreshToken.findOne({
userId: decoded.userId,
token: refreshToken
});
if (!storedToken) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
// Generate new access token
const newAccessToken = jwt.sign(
{ userId: decoded.userId },
ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch (error) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
});
// ===================================
// CLIENT: Auto-refresh access token
// ===================================
class AuthService {
async fetchWithAuth(url, options = {}) {
let accessToken = localStorage.getItem('accessToken');
// Try request with current token
let response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': \`Bearer \${accessToken}\`
}
});
// If 401, try to refresh
if (response.status === 401) {
const refreshToken = localStorage.getItem('refreshToken');
// Get new access token
const refreshResponse = await fetch('/api/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
if (refreshResponse.ok) {
const data = await refreshResponse.json();
localStorage.setItem('accessToken', data.accessToken);
// Retry original request with new token
response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': \`Bearer \${data.accessToken}\`
}
});
} else {
// Refresh failed, logout
this.logout();
window.location.href = '/login';
}
}
return response;
}
}
OAuth 2.0
OAuth is an authorization framework that lets you grant access to your data on one site to another site without sharing your password.
How OAuth Works (Simplified)
Example: "Login with Google" on a website
1. YOU: Click "Login with Google" on MyApp.com
2. MyApp.com: Redirects you to Google
3. GOOGLE: Shows permission screen
"MyApp wants to access your email and profile"
[Allow] [Deny]
4. YOU: Click "Allow"
5. GOOGLE: Redirects back to MyApp.com with authorization code
6. MyApp.com: Exchanges code for access token (server-to-server)
7. MyApp.com: Uses token to get your data from Google
8. YOU: Logged in to MyApp.com (without giving MyApp your Google password!)
OAuth Flow Diagram
USER YOUR APP GOOGLE (OAuth Provider)
│ │ │
│ Click "Login" │ │
├─────────────────>│ │
│ │ Redirect to Google │
│ ├───────────────────────>│
│ │ │
│ │ Show permission page │
│<──────────────────────────────────────────┤
│ │ │
│ User approves │ │
├──────────────────────────────────────────>│
│ │ │
│ │ Redirect with code │
│<─────────────────┤ │
│ │ │
│ │ Exchange code for token
│ ├───────────────────────>│
│ │ │
│ │ Return access token │
│ │<───────────────────────┤
│ │ │
│ │ Get user data │
│ ├───────────────────────>│
│ │ │
│ │ User data │
│ │<───────────────────────┤
│ │ │
│ Logged in! ✅ │ │
│<─────────────────┤ │
OAuth Example (Google Login)
// ===================================
// CLIENT: Initiate OAuth flow
// ===================================
const GOOGLE_CLIENT_ID = 'your-google-client-id';
const REDIRECT_URI = 'http://localhost:3000/auth/callback';
function loginWithGoogle() {
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth?' + new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'email profile',
access_type: 'offline'
});
// Redirect to Google
window.location.href = authUrl;
}
// ===================================
// SERVER: Handle callback
// ===================================
app.get('/auth/callback', async (req, res) => {
const { code } = req.query;
try {
// Exchange authorization code for access token
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code'
})
});
const tokens = await tokenResponse.json();
// tokens.access_token, tokens.refresh_token, tokens.id_token
// Get user info from Google
const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: {
'Authorization': \`Bearer \${tokens.access_token}\`
}
});
const googleUser = await userResponse.json();
// { id, email, name, picture }
// Create or update user in your database
let user = await User.findOne({ googleId: googleUser.id });
if (!user) {
user = await User.create({
googleId: googleUser.id,
email: googleUser.email,
name: googleUser.name,
picture: googleUser.picture
});
}
// Create your own JWT for the user
const jwt = createJWT(user);
// Redirect to frontend with JWT
res.redirect(\`http://localhost:3000?token=\${jwt}\`);
} catch (error) {
console.error('OAuth error:', error);
res.redirect('http://localhost:3000/login?error=oauth_failed');
}
});
Popular OAuth Providers
// Google OAuth
https://accounts.google.com/o/oauth2/v2/auth
// GitHub OAuth
https://github.com/login/oauth/authorize
// Facebook OAuth
https://www.facebook.com/v12.0/dialog/oauth
// Twitter OAuth
https://twitter.com/i/oauth2/authorize
// Microsoft OAuth
https://login.microsoftonline.com/common/oauth2/v2.0/authorize
// Each has similar flow but different parameters
When to Use OAuth
✅ Good for:
- "Login with Google/Facebook/GitHub"
- Accessing user's data from other services
- Third-party integrations
- When you don't want to store passwords
❌ Overkill for:
- Simple API authentication
- Server-to-server communication
- Internal applications
- When OAuth provider isn't needed
Basic Authentication
Basic Auth sends username and password with every request. Simple but less secure.
How Basic Auth Works
# Credentials: username "john" password "secret123"
# 1. Combine with colon
john:secret123
# 2. Encode in Base64
am9objpzZWNyZXQxMjM=
# 3. Send in header
GET /api/users HTTP/1.1
Authorization: Basic am9objpzZWNyZXQxMjM=
Using Basic Auth
// JavaScript
const username = 'john';
const password = 'secret123';
// Encode credentials
const credentials = btoa(\`\${username}:\${password}\`);
fetch('https://api.example.com/data', {
headers: {
'Authorization': \`Basic \${credentials}\`
}
})
.then(res => res.json())
.then(data => console.log(data));
// Or use fetch with credentials directly
fetch('https://api.example.com/data', {
headers: {
'Authorization': 'Basic ' + btoa('john:secret123')
}
});
Security Warning: Basic Auth sends credentials with EVERY request and is easy to decode (Base64 is encoding, not encryption). Only use with HTTPS! Prefer Bearer tokens for modern applications.
When to Use Basic Auth
✅ Acceptable for:
- Internal tools
- Development/testing
- Simple admin panels
- Over HTTPS only
❌ Avoid for:
- Production user authentication
- Public APIs
- When security is critical
- Mobile apps
Comparison and Best Practices
Authentication Methods Comparison
| method | security | complexity | best For | expiration |
|---|---|---|---|---|
| API Keys | 🟡 Medium | ✅ Simple | Service-to-service | Manual |
| Bearer Tokens | ✅ Good | 🟡 Medium | User authentication | Configurable |
| JWT | ✅ Good | 🟡 Medium | Stateless auth | Built-in |
| OAuth 2.0 | ✅ Excellent | ❌ Complex | Third-party login | Yes |
| Basic Auth | ❌ Poor | ✅ Very simple | Internal tools only | No |
Best Practices
1. Always Use HTTPS
✅ https://api.example.com (Encrypted)
❌ http://api.example.com (Plaintext - credentials visible!)
All authentication requires HTTPS to prevent credentials from being intercepted.
2. Store Tokens Securely
// ✅ GOOD - HttpOnly cookie (can't be accessed by JavaScript)
res.cookie('token', jwt, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
});
// 🟡 OKAY - localStorage (accessible by JavaScript)
localStorage.setItem('token', jwt);
// ❌ BAD - Regular cookie (accessible by JavaScript, vulnerable to XSS)
document.cookie = \`token=\${jwt}\`;
3. Implement Token Expiration
// ✅ GOOD - Tokens expire
const token = jwt.sign(
{ userId: user.id },
SECRET,
{ expiresIn: '15m' } // Short-lived
);
// ❌ BAD - Tokens never expire
const token = jwt.sign({ userId: user.id }, SECRET);
// If stolen, attacker has access forever!
4. Use Refresh Tokens
✅ GOOD Pattern:
- Access token: Short-lived (15 minutes)
- Refresh token: Long-lived (7 days)
- Store refresh token securely (HttpOnly cookie or database)
- Automatically refresh access token when expired
❌ BAD Pattern:
- Long-lived access token (24 hours+)
- No refresh mechanism
- If stolen, attacker has extended access
5. Validate All Tokens
// ✅ GOOD - Verify on every request
function authenticateToken(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Token required' });
}
try {
const decoded = jwt.verify(token, SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(403).json({ error: 'Invalid token' });
}
}
// ❌ BAD - Trust without verification
function authenticateToken(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
req.user = jwt.decode(token); // Decoded but not verified!
next();
}
6. Rate Limit Authentication Attempts
const rateLimit = require('express-rate-limit');
// Limit login attempts
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts, please try again later'
});
app.post('/api/login', loginLimiter, async (req, res) => {
// Login logic
});
7. Never Log Tokens
// ❌ BAD - Tokens in logs
console.log('User logged in:', token);
logger.info(\`Token: \${token}\`);
// ✅ GOOD - Log user ID only
console.log('User logged in:', userId);
logger.info(\`User \${userId} authenticated\`);
Summary
API authentication is essential for securing your applications and APIs. Choose the right method based on your needs, always use HTTPS, and follow security best practices.
Key Takeaways:
✅ API Keys - Simple, for server-to-server
✅ Bearer Tokens - User authentication, expires
✅ JWT - Self-contained, stateless, popular
✅ OAuth - Third-party login ("Login with Google")
✅ Basic Auth - Simple but insecure (avoid in production)
✅ Always use HTTPS
✅ Store tokens securely (HttpOnly cookies preferred)
✅ Implement token expiration and refresh
✅ Validate tokens on every request
✅ Rate limit authentication attempts
Quick Decision Guide:
- Building a public API? → API Keys
- User login system? → JWT with refresh tokens
- "Login with Google"? → OAuth 2.0
- Simple internal tool? → Basic Auth (with HTTPS)