# HTTP Authentication Methods and Best Practices

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

Source: https://howhttpworks.com/guides/authentication
Last reviewed: 2026-10-04

> **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:**

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

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

**Implementation:**

```javascript
// 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:**

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

**JWT (JSON Web Token) Implementation:**

```javascript
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:**

```javascript
// 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:**

```http
# 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:**

```javascript
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

### 4. Session Cookie Authentication

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

**Flow:**

```text
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:**

```javascript
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):**

```text
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):**

```javascript
// 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')
})
```

## 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

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

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  limit: 5, // 5 attempts (express-rate-limit v7+; `max` in older versions)
  message: 'Too many login attempts, please try again later'
})

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

### 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)

```javascript
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
})
```

### 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:**

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

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

## Troubleshooting Common Issues

**Issue: 401 Unauthorized on valid credentials**

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

**Issue: CORS errors with credentials**

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

**Issue: Tokens not being sent**

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

## Related Resources

- [Authorization Header](https://howhttpworks.com/headers/authorization)
- [WWW-Authenticate Header](https://howhttpworks.com/headers/www-authenticate)
- [Cookie Security](https://howhttpworks.com/guides/cookie-security)
- [401 Unauthorized](https://howhttpworks.com/status-codes/401)
- [403 Forbidden](https://howhttpworks.com/status-codes/403)
