REST API Authentication and Authorization

When you build an API, two questions matter most: Who are you? and What are you allowed to do? These two questions define Authentication and Authorization. They sound similar, but they do completely different jobs.

Authentication checks your identity. It confirms that you are who you claim to be — like showing your ID card at a building entrance.

Authorization checks your permissions. It decides what you can access after your identity is confirmed — like being allowed into only certain floors of that building, not all of them.

Without both working together, an API is either wide open to everyone or frustrating to use. This page covers how each works, how to implement them, and which approach fits which situation.

A Simple Real-World Diagram

  [Client App]
       |
       | --- sends credentials (API key / token / password) --->
       |
  [API Gateway]
       |
       |--- Step 1: AUTHENTICATION --- "Is this a real user?" ---
       |         YES --> Continue
       |         NO  --> 401 Unauthorized
       |
       |--- Step 2: AUTHORIZATION --- "Can this user do THIS?" ---
                 YES --> 200 OK + Data
                 NO  --> 403 Forbidden

Notice the two separate gates. Both must open for a request to succeed. Many developers confuse a 401 Unauthorized (authentication failed) with a 403 Forbidden (authorization failed). They are different problems with different solutions.

Method 1: API Keys

An API key is a long, random string that acts like a password for your application. The server gives each registered app its own unique key. The app sends this key with every request, usually in a header.

How API Keys Work — Step by Step

  Developer registers app
         |
         v
  Server generates API Key: "sk_live_aB3xQ9..."
         |
         v
  Developer stores key securely
         |
         v
  Every API Request:
  GET /products
  Headers:
    X-API-Key: sk_live_aB3xQ9...
         |
         v
  Server checks key in database
         |
    +---------+----------+
    |                    |
  Valid key           Invalid key
    |                    |
  200 OK            401 Unauthorized

Where to Send the API Key

Always send API keys in the request header, not in the URL. A URL appears in server logs, browser history, and referrer headers. A key in the URL is a security disaster waiting to happen.

Wrong:

GET https://api.store.com/orders?api_key=sk_live_aB3xQ9

Right:

GET https://api.store.com/orders
Headers:
  X-API-Key: sk_live_aB3xQ9

When to Use API Keys

  • Server-to-server communication where a human user is not involved
  • Public APIs where tracking usage per application matters
  • Simple integrations like weather APIs or payment gateways

Limitations of API Keys

  • A stolen key gives full access until you revoke it manually
  • Keys carry no information about roles or permissions on their own
  • Hard to rotate without breaking existing integrations

Method 2: HTTP Basic Authentication

Basic Auth sends a username and password with every request. The credentials are encoded in Base64 and placed in the Authorization header. This is the oldest form of HTTP authentication.

What Basic Auth Looks Like

  Username: alice
  Password: mypassword

  Step 1: Combine  →  alice:mypassword
  Step 2: Base64   →  YWxpY2U6bXlwYXNzd29yZA==
  Step 3: Add to header:
    Authorization: Basic YWxpY2U6bXlwYXNzd29yZA==

Important: Base64 is not encryption. Anyone who intercepts that string can decode it instantly. Basic Auth is only safe when the connection uses HTTPS. Without HTTPS, your username and password travel in readable form across the network.

When Basic Auth Makes Sense

  • Internal tools used only inside a private network
  • Quick prototypes where simplicity matters more than sophistication
  • Scenarios where the server cannot store session state

Why Basic Auth Is Rarely Used in Modern APIs

  • Credentials are sent with every single request — more exposure, more risk
  • No built-in expiry — a leaked password works forever until changed
  • No support for roles, scopes, or fine-grained permissions

Method 3: Token-Based Authentication with JWT

JSON Web Tokens (JWT) are the most widely used authentication method in modern REST APIs. Instead of sending credentials with every request, the client logs in once and receives a token. That token is sent with future requests. The server validates the token without looking anything up in a database.

JWT Flow — Visualized

  CLIENT                            SERVER
    |                                  |
    |--- POST /login                   |
    |    { email, password }  -------> |
    |                                  |--- Verify credentials
    |                                  |--- Create JWT
    | <------ 200 OK                   |
    |    { token: "eyJ..." }           |
    |                                  |
    |--- GET /orders                   |
    |    Authorization: Bearer eyJ... -|->
    |                                  |--- Decode JWT header + payload
    |                                  |--- Verify signature
    |                                  |--- Check expiry
    | <------ 200 OK + orders data     |

The Three Parts of a JWT

A JWT token looks like this:

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEyM30.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

It has three parts separated by dots:

  HEADER          PAYLOAD             SIGNATURE
  eyJhbGci...  .  eyJ1c2VySWQi...  .  SflKxwRJ...

  Header:
  {
    "alg": "HS256",   <-- signing algorithm
    "typ": "JWT"
  }

  Payload:
  {
    "userId": 123,
    "role": "admin",
    "exp": 1720000000   <-- expiry timestamp
  }

  Signature:
  HMAC-SHA256(base64(header) + "." + base64(payload), SECRET_KEY)

The signature ties the header and payload together. If anyone tampers with the payload (e.g., changes role from user to admin), the signature no longer matches and the server rejects the token.

Access Tokens vs Refresh Tokens

  LOGIN
    |
    +--> Access Token  (expires in 15 minutes)
    |    Used for API requests
    |
    +--> Refresh Token (expires in 7–30 days)
         Used ONLY to get a new access token

  FLOW:
  [Client] --- GET /products (with access token) ---> [Server]
  [Server] --- 401 Token Expired ----> [Client]
  [Client] --- POST /refresh (with refresh token) ---> [Server]
  [Server] --- 200 OK + new access token ---> [Client]
  [Client] --- GET /products (with NEW access token) ---> [Server]

Short-lived access tokens limit the damage if a token is stolen. The refresh token sits safely in secure storage and only appears when needed.

Method 4: OAuth 2.0

OAuth 2.0 solves a specific problem: letting a third-party app access your data on another service without giving that app your password. You have used OAuth when you clicked "Login with Google" or "Connect with GitHub."

The OAuth 2.0 Cast of Characters

  RESOURCE OWNER  = You (the user)
  CLIENT          = The third-party app (e.g., a photo editing app)
  AUTHORIZATION SERVER = The service that verifies you (e.g., Google)
  RESOURCE SERVER = The API holding your data (e.g., Google Drive)

OAuth 2.0 Authorization Code Flow

  [User] clicks "Login with Google" on PhotoApp
     |
     v
  [PhotoApp] redirects user to Google's auth page
     |
     v
  [Google] shows consent screen:
  "PhotoApp wants to read your Google Drive files. Allow?"
     |
     +--- User clicks ALLOW
     |
     v
  [Google] sends authorization CODE back to PhotoApp
     |
     v
  [PhotoApp] exchanges CODE for ACCESS TOKEN (server-to-server)
     |
     v
  [PhotoApp] uses ACCESS TOKEN to call Google Drive API
     |
     v
  [Google Drive API] returns user's files to PhotoApp

The user never shares their Google password with PhotoApp. The access token can be limited to specific scopes (e.g., read-only access to Drive, not Gmail). Google can revoke the token anytime.

OAuth 2.0 Scopes

Scopes define exactly what the token is allowed to do. They make authorization granular.

  Scope Examples from Google:
  "https://www.googleapis.com/auth/drive.readonly"  → Read files only
  "https://www.googleapis.com/auth/drive"           → Full Drive access
  "https://www.googleapis.com/auth/gmail.send"      → Send emails only

  A token with scope "drive.readonly" CANNOT delete files,
  even if the user owns them.

Role-Based Access Control (RBAC)

Authentication tells you who the user is. Authorization tells you what they can do. RBAC is the most common way to organize permissions. You assign each user a role, and each role has specific permissions.

RBAC in an E-Commerce API

  ROLES AND PERMISSIONS:

  Role: GUEST
  ├── GET /products          ✓ (view products)
  ├── POST /orders           ✗ (must be logged in)
  └── DELETE /products/:id   ✗

  Role: CUSTOMER
  ├── GET /products          ✓
  ├── POST /orders           ✓ (place orders)
  ├── GET /orders/mine       ✓ (own orders only)
  └── DELETE /products/:id   ✗

  Role: ADMIN
  ├── GET /products          ✓
  ├── POST /products         ✓ (add new products)
  ├── PUT /products/:id      ✓ (edit products)
  ├── DELETE /products/:id   ✓
  └── GET /orders            ✓ (all orders, all users)

How RBAC Works in JWT

The user's role is stored inside the JWT payload. The server reads the role on every request and checks it against the required permission for that route.

  JWT Payload for a customer:
  {
    "userId": 456,
    "email": "bob@example.com",
    "role": "customer",
    "exp": 1720000000
  }

  Server middleware for DELETE /products/:id:
  ┌─────────────────────────────────────────┐
  │ 1. Extract JWT from Authorization header│
  │ 2. Verify signature                     │
  │ 3. Read role from payload               │
  │ 4. Is role === "admin"?                 │
  │    YES → proceed to delete              │
  │    NO  → return 403 Forbidden           │
  └─────────────────────────────────────────┘

Choosing the Right Method

  SCENARIO                            BEST METHOD
  ─────────────────────────────────────────────────────
  Machine-to-machine (no user)       → API Key
  Simple internal tool               → Basic Auth (HTTPS only)
  User login for your own app        → JWT
  Third-party app accessing user data→ OAuth 2.0
  Microservices inside one system    → JWT or mTLS
  Public API with many consumers     → API Key + OAuth 2.0

Common Authentication Mistakes and How to Avoid Them

Mistake 1: Storing Tokens in localStorage

  WRONG:
  localStorage.setItem("token", jwt);

  Problem: Any JavaScript on the page can read localStorage.
           XSS attacks steal tokens from localStorage easily.

  RIGHT:
  Store access tokens in memory (JavaScript variable).
  Store refresh tokens in HttpOnly cookies.

  HttpOnly cookie: Cannot be read by JavaScript.
                   Only the browser sends it with HTTP requests.

Mistake 2: No Token Expiry

  WRONG:
  {
    "userId": 123,
    "role": "admin"
    // no "exp" field
  }

  Problem: If this token is stolen, the attacker has
           permanent admin access forever.

  RIGHT:
  {
    "userId": 123,
    "role": "admin",
    "exp": 1720900000,   ← expires in 15 minutes
    "iat": 1720899000    ← issued at (for auditing)
  }

Mistake 3: Trusting the Client for Authorization

  WRONG (client-side only check):
  if (user.role === "admin") {
    showDeleteButton();   // UI hides button for non-admins
  }

  Problem: Anyone can call DELETE /products/5 directly
           through Postman or curl. The button hiding means nothing.

  RIGHT (server-side check, always):
  // Server middleware
  if (decodedToken.role !== "admin") {
    return res.status(403).json({ error: "Forbidden" });
  }
  // Only then execute the delete

Mistake 4: Putting Secrets in the JWT Payload

  WRONG:
  {
    "userId": 123,
    "password": "supersecret",   ← NEVER do this
    "creditCard": "4111..."      ← NEVER do this
  }

  Problem: The JWT payload is Base64 encoded, not encrypted.
           Anyone with the token can decode and read the payload.

  RIGHT:
  Put only non-sensitive identifiers in the payload:
  {
    "userId": 123,
    "role": "customer",
    "exp": 1720900000
  }

Implementing Token Refresh Securely

  SECURE REFRESH TOKEN STRATEGY:

  1. Issue short-lived access token (15 min) → send in response body
  2. Issue long-lived refresh token (7 days) → set as HttpOnly cookie
  3. Client stores access token in memory only (not localStorage)
  4. When access token expires:
     a. Client silently sends POST /auth/refresh
     b. Browser auto-includes HttpOnly cookie
     c. Server validates refresh token
     d. Server issues new access token
     e. Server rotates refresh token (old one becomes invalid)
  5. On logout:
     a. Clear access token from memory
     b. Call POST /auth/logout
     c. Server invalidates refresh token in database

HTTP Status Codes for Auth Errors

  401 Unauthorized
  → The request has no valid credentials
  → "I don't know who you are"
  → Fix: Log in and get a valid token

  403 Forbidden
  → The credentials are valid but permission is denied
  → "I know who you are, but you can't do this"
  → Fix: Use an account with the required role/scope

  407 Proxy Authentication Required
  → A proxy server requires credentials

  Common mistake: Returning 401 when you mean 403.
  They have different meanings and cause different client behavior.

Key Points

  • Authentication = identity verification; Authorization = permission check. Both are mandatory, never skip either.
  • API Keys suit machine-to-machine communication. JWT suits user sessions. OAuth 2.0 suits third-party access delegation.
  • Always send credentials in headers over HTTPS — never in URLs.
  • JWT payloads are readable by anyone. Never store passwords, card numbers, or secrets in a JWT.
  • Use short-lived access tokens with refresh token rotation to reduce the blast radius of token theft.
  • All authorization checks must happen on the server. Client-side checks are UI sugar, not security.
  • Return 401 when credentials are missing or invalid. Return 403 when credentials are valid but permission is denied.

Leave a Comment