7 API Authentication Methods Compared (API Key, JWT, OAuth, Basic)
Compare 7 API authentication methods — API keys, JWT, OAuth 2.0, Basic Auth, HMAC, mTLS, and session tokens — with code examples and a security tradeoff table.
Get more content like this on Telegram!
Daily AI tips, notes & resources — free
Security is one of those areas where getting it wrong has consequences that range from mildly embarrassing to catastrophic. I've reviewed APIs that stored credentials in plain text, returned user tokens in URLs, and used home-rolled crypto. This guide won't make you a security expert, but it'll help you choose the right authentication method and implement it correctly.
There are seven main approaches to API authentication. Each has legitimate use cases, tradeoffs around complexity and security, and situations where it's clearly the wrong choice. Let's go through all of them.
Method 1: API Keys
The simplest approach. You generate a long random string and require clients to include it in every request. No expiry, no user context — just a shared secret between your server and the client application.
// Client usage — typically as a header
const response = await fetch('https://api.example.com/data', {
headers: {
'X-API-Key': 'sk_live_abc123def456ghi789'
}
});
// Or as a query parameter (less secure — keys appear in logs)
// GET /data?api_key=sk_live_abc123def456ghi789
// Server-side validation (Express)
const validateApiKey = async (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: 'API key required' });
}
// Look up the key in your database
const keyRecord = await ApiKey.findOne({
key: hashKey(apiKey), // Always store hashed, never plain text
active: true
});
if (!keyRecord) {
return res.status(401).json({ error: 'Invalid API key' });
}
// Attach the associated app/user to the request
req.apiKey = keyRecord;
next();
};
Use when: Public APIs consumed by developer applications, server-to-server communication, simple integrations where user identity doesn't matter.
Avoid when: You need to authenticate individual users (API keys don't carry user identity), or when you need granular permissions beyond "has access / doesn't have access."
Method 2: HTTP Basic Authentication
The oldest approach. Username and password are base64-encoded and sent as a header on every request. Note that base64 is encoding, not encryption — it provides zero security without HTTPS.
// Client
const credentials = Buffer.from('alice:mysecretpassword').toString('base64');
const res = await fetch('https://api.example.com/profile', {
headers: {
'Authorization': `Basic ${credentials}`
}
});
// Server
const basicAuth = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Basic ')) {
return res.status(401).json({ error: 'Authentication required' });
}
const decoded = Buffer.from(authHeader.slice(6), 'base64').toString('utf8');
const [username, password] = decoded.split(':');
// Validate against database with bcrypt comparison
const user = await User.findOne({ username });
const valid = user && await bcrypt.compare(password, user.passwordHash);
if (!valid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
req.user = user;
next();
};
Use when: Internal tools, simple admin interfaces, quick prototypes, or as a fallback for clients that can't handle more complex schemes.
Avoid when: Building any public-facing API. Credentials on every request means a single intercepted request exposes the user's password.
Method 3: JWT (JSON Web Tokens)
The most common choice for modern APIs. A JWT is a signed, self-contained token that carries claims about the authenticated user. The server doesn't need to look anything up — it just verifies the signature.
// Install: npm install jsonwebtoken bcryptjs
const jwt = require('jsonwebtoken');
// Login endpoint — issues a token
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const accessToken = jwt.sign(
{ userId: user._id, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user._id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
// Store refresh token in database (to enable revocation)
await RefreshToken.create({ token: hashToken(refreshToken), userId: user._id });
res.json({ accessToken, refreshToken });
});
// Middleware to validate JWT
const verifyToken = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authorization token required' });
}
const token = authHeader.slice(7);
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = payload;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
}
return res.status(401).json({ error: 'Invalid token' });
}
};
// Refresh token endpoint
app.post('/api/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
try {
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
// Verify it's in our database
const stored = await RefreshToken.findOne({
token: hashToken(refreshToken),
userId: payload.userId
});
if (!stored) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
const newAccessToken = jwt.sign(
{ userId: payload.userId },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
Use when: SPAs, mobile apps, APIs where you want stateless authentication, microservices where you need to pass user context between services.
For more on building APIs with JWT authentication integrated, the Node.js Express MongoDB tutorial shows a complete project structure.
Method 4: OAuth 2.0
OAuth 2.0 is an authorization framework (not just authentication) that allows applications to act on behalf of users without the user sharing their password. It's what powers "Login with Google/GitHub/Facebook."
// OAuth 2.0 Authorization Code Flow (simplified)
// Using passport.js with GitHub strategy
const passport = require('passport');
const GitHubStrategy = require('passport-github2').Strategy;
passport.use(new GitHubStrategy({
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: 'https://yourapp.com/auth/github/callback',
scope: ['user:email', 'read:org'] // specific permissions requested
}, async (accessToken, refreshToken, profile, done) => {
// accessToken is GitHub's token — use it to call GitHub's API
const user = await User.findOrCreate({
githubId: profile.id,
email: profile.emails[0].value,
name: profile.displayName
});
return done(null, user);
}));
// Routes
app.get('/auth/github', passport.authenticate('github'));
app.get('/auth/github/callback',
passport.authenticate('github', { failureRedirect: '/login' }),
(req, res) => {
// Issue your own JWT to the authenticated user
const token = jwt.sign({ userId: req.user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.redirect(`/app?token=${token}`);
}
);
Use when: Social login, integrations with third-party services (GitHub, Google, Stripe), any situation where your app needs to access data that belongs to the user on another platform.
Avoid when: You're authenticating your own users against your own user database. OAuth is for third-party authorization, not first-party authentication (though OpenID Connect, built on top of OAuth, does handle authentication).
Method 5: HMAC Signatures
HMAC (Hash-based Message Authentication Code) signs the entire request with a secret key. Unlike Bearer tokens, which authenticate the caller, HMAC authenticates the specific request — preventing replay attacks and request tampering.
const crypto = require('crypto');
// Client: sign and send a request
function signRequest(method, path, body, secretKey) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const bodyHash = body
? crypto.createHash('sha256').update(JSON.stringify(body)).digest('hex')
: '';
const stringToSign = `${method}\n${path}\n${timestamp}\n${bodyHash}`;
const signature = crypto
.createHmac('sha256', secretKey)
.update(stringToSign)
.digest('hex');
return { signature, timestamp };
}
// Server: verify the signature
function verifyHmac(req, res, next) {
const signature = req.headers['x-signature'];
const timestamp = req.headers['x-timestamp'];
// Reject requests older than 5 minutes (prevents replay attacks)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 300) {
return res.status(401).json({ error: 'Request expired' });
}
const bodyHash = req.body
? crypto.createHash('sha256').update(JSON.stringify(req.body)).digest('hex')
: '';
const stringToSign = `${req.method}\n${req.path}\n${timestamp}\n${bodyHash}`;
const expectedSig = crypto
.createHmac('sha256', process.env.HMAC_SECRET)
.update(stringToSign)
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
const sigBuffer = Buffer.from(signature, 'hex');
const expectedBuffer = Buffer.from(expectedSig, 'hex');
if (sigBuffer.length !== expectedBuffer.length ||
!crypto.timingSafeEqual(sigBuffer, expectedBuffer)) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
}
Use when: Webhook verification (Stripe, GitHub use HMAC for webhooks), financial APIs, any scenario where you need tamper-proof requests.
Method 6: Session Tokens
Traditional web authentication. The server creates a session record, stores it (usually in Redis), and sends the client a session ID cookie. Every request includes the cookie; the server looks up the session.
// npm install express-session connect-redis ioredis
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
app.use(session({
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true, // Prevents XSS access to cookie
sameSite: 'strict', // Prevents CSRF
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// Login
app.post('/login', async (req, res) => {
const user = await authenticateUser(req.body.email, req.body.password);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
req.session.userId = user._id;
req.session.role = user.role;
res.json({ message: 'Logged in' });
});
// Middleware
const requireAuth = (req, res, next) => {
if (!req.session.userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
next();
};
// Logout — invalidates immediately
app.post('/logout', (req, res) => {
req.session.destroy(() => {
res.clearCookie('connect.sid');
res.json({ message: 'Logged out' });
});
});
Use when: Traditional server-rendered web apps, admin panels, any scenario where immediate session revocation matters.
Method 7: Mutual TLS (mTLS)
Both client and server present TLS certificates. The server verifies the client certificate against a CA (Certificate Authority). This is the most secure option and the most complex to set up.
const https = require('https');
const fs = require('fs');
const serverOptions = {
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.crt'),
ca: fs.readFileSync('ca.crt'), // CA that signed client certs
requestCert: true, // Require client certificate
rejectUnauthorized: true // Reject if cert doesn't match CA
};
const server = https.createServer(serverOptions, app);
// Middleware to check certificate subject
const verifyClientCert = (req, res, next) => {
const cert = req.socket.getPeerCertificate();
if (!req.client.authorized) {
return res.status(401).json({ error: 'Invalid client certificate' });
}
req.clientCN = cert.subject.CN;
next();
};
Use when: Internal microservices communication, financial systems, healthcare APIs, any B2B API where client applications are known and controlled.
Comparison Table
| Method | Security Level | Stateless | Complexity | User Identity | Best Use Case |
|---|---|---|---|---|---|
| API Key | Moderate | Yes | Low | No | Developer APIs, server-to-server |
| Basic Auth | Low (needs HTTPS) | Yes | Very Low | Yes | Internal tools only |
| JWT | High | Yes | Medium | Yes | SPAs, mobile apps, microservices |
| OAuth 2.0 | High | Yes | High | Yes | Third-party integrations |
| HMAC | High | Yes | Medium | No | Webhooks, financial APIs |
| Session Token | High | No | Medium | Yes | Traditional web apps |
| mTLS | Very High | Yes | Very High | No | Internal services, B2B |
For comparing how different databases handle user data for auth systems, the PostgreSQL vs MySQL comparison covers the tradeoffs. For pure query performance when scaling auth tables, check out the SQL query optimization guide.
Choosing the Right Method
The decision tree I actually use:
- Are you building an API for other developers? → API Key
- Do you need social login or third-party data access? → OAuth 2.0
- Are you building a SPA or mobile app with your own user accounts? → JWT
- Is this a traditional server-rendered web app? → Session tokens
- Are you verifying webhooks from external services? → HMAC
- Is this internal microservice-to-microservice communication? → mTLS or JWT with service accounts
Most real applications use more than one. A SaaS product might use OAuth for social login, JWT for the API, API keys for developer access, and HMAC for incoming webhooks. That's completely normal.
The REST API design best practices guide covers how to structure your API endpoints around these auth patterns. If you're comparing full frameworks and want to see authentication in a Python context, the FastAPI tutorial has examples using similar JWT patterns.
You can also reference Auth0's documentation for production-ready implementations of OAuth and JWT flows.
Conclusion
Authentication isn't a binary "secure vs insecure" choice — it's about matching the method to your threat model and use case. API keys are perfectly fine for developer APIs. JWT is the right choice for most modern frontends. mTLS is the gold standard for high-security internal services.
The one universal rule: always use HTTPS, always hash stored credentials with bcrypt, and never build your own cryptography. Use battle-tested libraries (jsonwebtoken, bcryptjs, passport.js) rather than rolling your own implementations.
Pick the method that fits your actual requirements — not the most complex one that sounds most secure. Complexity introduces bugs, and bugs in auth code have real consequences.
FAQ
What's the difference between authentication and authorization?
Authentication answers 'who are you?' — it verifies identity. Authorization answers 'what are you allowed to do?' — it checks permissions. In API context: authentication is checking the Bearer token is valid and belongs to a real user. Authorization is checking that user has permission to perform the requested operation (read, write, delete, admin). You need both, and they're separate concerns.
Is JWT better than session-based authentication?
Depends on your use case. JWT is stateless — the server doesn't store anything, which makes it easy to scale horizontally. Session tokens require server-side storage (usually Redis) to validate, adding infrastructure complexity. The tradeoff: JWTs can't be truly revoked before expiry (you need a blocklist to work around this), while sessions can be invalidated instantly. For most APIs, JWT with short expiry and refresh tokens is the right call.
When should I use OAuth 2.0 instead of a simple API key?
Use OAuth when your API acts on behalf of a user — when your app needs to access a user's Google Drive, GitHub repos, or social profile. API keys authenticate the application, not the individual user. OAuth authenticates the user and grants the application specific permissions (scopes) the user approves. If you're building a public API consumed by other developers, API keys are simpler. If you're building integrations with user data, OAuth is the right choice.
Frequently Asked Questions
AiTechWorlds Team
✓ Verified WriterThe AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.
Related Articles
7 Common API Security Vulnerabilities (and How to Fix Them)
Real API security vulnerabilities from the OWASP API Top 10 — with working code fixes, risk levels, and testing tools so you can protect your APIs today.
Build a Secure JWT Authentication System in Node.js (2026)
Step-by-step guide to building JWT authentication in Node.js with access tokens, refresh tokens, httpOnly cookies, and security best practices for 2026.
AWS vs Azure vs GCP for Startups: Pricing and Free Tier Guide 2026
AWS, Azure, or GCP for your startup in 2026? Real free tier limits, monthly cost estimates, and honest recommendations based on your actual use case.
How to Use Docker Compose for Local Dev (Node.js + PostgreSQL)
Set up a full local dev environment with Docker Compose, Node.js, PostgreSQL, and pgAdmin. Includes .env config, named volumes, healthchecks, and common error fixes.