HTTP

Guide

HTTP Authentication Methods and Best Practices

A comprehensive guide to HTTP authentication methods including Basic Auth, Bearer tokens, API keys, and OAuth 2.0.

7 min read intermediate Try in Playground

TL;DR: Pick the auth mechanism that matches your trust boundary and operational needs. Basic Auth is for narrow controlled cases, API keys are for service access, Bearer tokens are common for APIs, and session cookies are often the simplest fit for traditional web apps. Use HTTPS in every case.

HTTP authentication is the process of proving who a client is before the server decides what that client may do. In practice, most teams are not choosing an abstract security concept. They are choosing how browsers, mobile apps, CLIs, workers, and services present credentials on every request.

Introduction

Every time you log into a site, call an internal API, or send a request with an API key, you are in authentication territory. Authentication answers “who are you?” Authorization answers “what are you allowed to do?” Mixing those two is one of the fastest ways to make an auth system confusing.

Why authentication matters:

  • Protects sensitive data and operations
  • Enables personalized user experiences
  • Prevents unauthorized access to resources
  • Provides audit trails for security compliance
  • Enables rate limiting and usage tracking

Common Authentication Methods

1. Basic Authentication

Basic Auth sends credentials as a Base64-encoded string in the Authorization header.

How it works:

GET /api/users HTTP/1.1
Host: api.example.com
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=

The string dXNlcm5hbWU6cGFzc3dvcmQ= is Base64 encoding of username:password.

Implementation:

// Server (Node.js/Express)
app.use((req, res, next) => {
  const auth = req.headers.authorization

  if (!auth || !auth.startsWith('Basic ')) {
    return res.status(401).set('WWW-Authenticate', 'Basic').send('Authentication required')
  }

  const credentials = Buffer.from(auth.slice(6), 'base64').toString()
  const [username, password] = credentials.split(':')

  if (username === 'admin' && password === 'secret') {
    next()
  } else {
    res.status(401).send('Invalid credentials')
  }
})

// Client
fetch('https://api.example.com/users', {
  headers: {
    Authorization: 'Basic ' + btoa('username:password')
  }
})

Pros:

  • Simple to implement
  • Widely supported
  • No additional infrastructure needed

Cons:

  • Credentials sent with every request
  • Vulnerable if not used over HTTPS
  • No built-in expiration
  • Not suitable for most browser-based product UX

2. Bearer Token Authentication

Bearer tokens (like JWTs) are passed in the Authorization header and represent proof of authentication.

How it works:

GET /api/protected HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

JWT (JSON Web Token) Implementation:

const jwt = require('jsonwebtoken')

// Generate token on login
app.post('/login', (req, res) => {
  const { username, password } = req.body

  // Verify credentials...
  if (validCredentials(username, password)) {
    const token = jwt.sign({ userId: 123, username }, process.env.JWT_SECRET, { expiresIn: '24h' })

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

// Verify token on protected routes
const authMiddleware = (req, res, next) => {
  const auth = req.headers.authorization

  if (!auth || !auth.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' })
  }

  const token = auth.slice(7)

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET)
    req.user = decoded
    next()
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' })
  }
}

app.get('/api/protected', authMiddleware, (req, res) => {
  res.json({ message: `Hello ${req.user.username}` })
})

Client usage:

// Store token from login
const token = await login('username', 'password')
localStorage.setItem('token', token)

// Use token for API requests
fetch('https://api.example.com/protected', {
  headers: {
    Authorization: `Bearer ${localStorage.getItem('token')}`
  }
})

The tradeoff here is operational, not just syntactic: bearer tokens are convenient because any holder of the token can use it. That simplicity is why they fit APIs well and why token storage decisions matter so much.

Pros:

  • Stateless (no server-side session storage)
  • Can include user data and permissions
  • Supports expiration
  • Works well with SPAs and mobile apps

Cons:

  • Larger than session IDs
  • Cannot be invalidated without additional infrastructure
  • Vulnerable to XSS if stored in localStorage
  • Requires careful secret management

3. API Key Authentication

API keys are long-lived credentials used for server-to-server or application authentication.

Common patterns:

# Header-based
GET /api/data HTTP/1.1
X-API-Key: sk_live_abc123xyz

# Query parameter (less secure)
GET /api/data?api_key=sk_live_abc123xyz HTTP/1.1

# Custom authentication scheme
Authorization: ApiKey sk_live_abc123xyz

Implementation:

app.use('/api', (req, res, next) => {
  const apiKey = req.headers['x-api-key']

  if (!apiKey) {
    return res.status(401).json({ error: 'API key required' })
  }

  // Validate against database
  const isValid = await validateApiKey(apiKey)

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid API key' })
  }

  next()
})

Best practices:

  • Use different keys for development/production
  • Implement rate limiting per key
  • Allow key rotation
  • Prefix keys to identify type (e.g., pk_ for publishable, sk_ for secret)
  • Never commit keys to version control

Server stores session data and sends a session ID to the client as a cookie.

Flow:

1. User logs in with credentials
2. Server creates session and stores in database/memory
3. Server sends session ID as HttpOnly cookie
4. Browser automatically includes cookie in subsequent requests
5. Server validates session ID and retrieves user data

Implementation:

const session = require('express-session')

app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
      secure: true, // HTTPS only
      httpOnly: true, // No JavaScript access
      sameSite: 'strict', // CSRF protection
      maxAge: 86400000 // 24 hours
    }
  })
)

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

  if (validCredentials(username, password)) {
    req.session.userId = getUserId(username)
    req.session.username = username
    res.json({ success: true })
  } else {
    res.status(401).json({ error: 'Invalid credentials' })
  }
})

app.get('/api/profile', (req, res) => {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Not authenticated' })
  }

  res.json({ userId: req.session.userId, username: req.session.username })
})

app.post('/logout', (req, res) => {
  req.session.destroy()
  res.json({ success: true })
})

Pros:

  • Secure (HttpOnly cookies prevent XSS)
  • Server can invalidate sessions
  • Automatic browser handling
  • Natural CSRF protection with SameSite

Cons:

  • Requires server-side storage
  • Doesn’t work well with distributed systems (without shared session store)
  • CORS complications for cross-domain requests

5. OAuth 2.0

OAuth is a delegation protocol that allows third-party applications to access user data without sharing passwords.

Common flows:

Authorization Code Flow (for web apps):

1. User clicks "Login with Google"
2. Redirect to Google with client_id and redirect_uri
3. User logs in and grants permission
4. Google redirects back with authorization code
5. Exchange code for access token (server-side)
6. Use access token to access user data

Implementation example (simplified):

// Step 1: Redirect to OAuth provider
app.get('/auth/google', (req, res) => {
  const authUrl =
    `https://accounts.google.com/o/oauth2/v2/auth?` +
    `client_id=${process.env.GOOGLE_CLIENT_ID}&` +
    `redirect_uri=${encodeURIComponent('http://localhost:3000/auth/callback')}&` +
    `response_type=code&` +
    `scope=profile email`

  res.redirect(authUrl)
})

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

  // Exchange code for 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: 'http://localhost:3000/auth/callback',
      grant_type: 'authorization_code'
    })
  })

  const { access_token } = await tokenResponse.json()

  // Use access token to get user info
  const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
    headers: { Authorization: `Bearer ${access_token}` }
  })

  const user = await userResponse.json()

  // Create session or JWT for user
  req.session.user = user
  res.redirect('/dashboard')
})
```text

## Security Best Practices

### 1. Always Use HTTPS

```javascript
// Enforce HTTPS
app.use((req, res, next) => {
  if (!req.secure && process.env.NODE_ENV === 'production') {
    return res.redirect(`https://${req.headers.host}${req.url}`)
  }
  next()
})

2. Implement Rate Limiting

const rateLimit = require('express-rate-limit')

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 attempts
  message: 'Too many login attempts, please try again later'
})

app.post('/login', authLimiter, loginHandler)
```javascript

### 3. Use Strong Passwords

```javascript
const bcrypt = require('bcrypt')

// Hash password on registration
const hashedPassword = await bcrypt.hash(password, 10)

// Verify on login
const isValid = await bcrypt.compare(password, hashedPassword)

4. Implement Multi-Factor Authentication (MFA)

const speakeasy = require('speakeasy')

// Generate secret
const secret = speakeasy.generateSecret({ name: 'MyApp' })

// Verify TOTP code
const verified = speakeasy.totp.verify({
  secret: secret.base32,
  encoding: 'base32',
  token: userProvidedCode
})
```javascript

### 5. Protect Against Common Attacks

**CSRF Protection:**

```javascript
const csrf = require('csurf')
app.use(csrf({ cookie: true }))

// Include CSRF token in forms
<input type="hidden" name="_csrf" value="<%= csrfToken %>">

XSS Protection:

// Sanitize user input
const validator = require('validator')
const clean = validator.escape(userInput)

// Use HttpOnly cookies
res.cookie('session', sessionId, { httpOnly: true })
```text

## Troubleshooting Common Issues

**Issue: 401 Unauthorized on valid credentials**

```text
Check:
- Password hashing comparison
- Token expiration
- Clock skew (for JWT)
- Case sensitivity in credentials
```text

**Issue: CORS errors with credentials**

```javascript
app.use(
  cors({
    origin: 'https://yourfrontend.com',
    credentials: true
  })
)

Issue: Tokens not being sent

// Ensure credentials: 'include' for cookies
fetch('/api/protected', {
  credentials: 'include'
})

Frequently Asked Questions

What is HTTP authentication?

HTTP authentication is a mechanism for servers to challenge clients for credentials. The server sends 401 with WWW-Authenticate, client responds with Authorization header.

What is the difference between Basic and Bearer auth?

Basic sends username:password base64-encoded. Bearer sends a token (like JWT). Bearer is more secure and flexible, supporting token expiration and scopes.

Is Basic authentication secure?

Only over HTTPS. Basic auth credentials are base64-encoded (not encrypted) and easily decoded. Always use HTTPS and consider Bearer tokens for APIs.

What is the Authorization header format?

Authorization: scheme credentials. For Basic: Authorization: Basic base64(user:pass). For Bearer: Authorization: Bearer token123.

Keep Learning