API Authentication and Authorization Explained

By Oleksandr Andrushchenko — Published on — Modified on

API Authentication and Authorization Explained
API Authentication and Authorization Explained

API authentication verifies who or what is making a request. API authorization determines what that authenticated identity is allowed to do. They are related security layers, but they solve different problems and should be designed independently.

Production API security depends on more than accepting a bearer token. Secure systems validate credentials, verify token signatures and claims, enforce resource-level permissions, isolate tenants, rotate credentials, audit sensitive actions, and apply least privilege to both users and services.

Table of Contents

Authentication vs Authorization

Authentication and authorization are often grouped into one security layer, but they answer different questions.

Concern Question Example
Authentication Who is making the request? The access token represents user usr_123.
Authorization What may that identity do? User usr_123 may read order ord_456.
Incoming request
  -> Validate credentials
  -> Build trusted identity context
  -> Identify requested action
  -> Load target resource
  -> Evaluate authorization policy
  -> Allow or deny

A request can be successfully authenticated and still be unauthorized. A valid customer token does not imply access to another customer’s records.

Key Point: authentication establishes identity; authorization evaluates permissions for an action and resource.

Identity Model

API security starts with a clear identity model. Callers may be users, backend services, workers, scripts, devices, CI/CD pipelines, or third-party integrations.

Different identities need different credential lifetimes, permissions, audit records, and revocation behavior.

Human Identities

Human identities represent people using browser applications, mobile applications, administrative tools, or command-line clients.

Property Example
Subject usr_123
Organization org_456
Roles admin, billing_manager
Authentication strength Password, MFA, passkey, enterprise SSO.

Machine Identities

Machine identities represent services and automated workloads. They should not reuse human credentials.

orders-api
  identity: svc_orders_api
  permissions:
    - payments.read
    - payments.create

report-worker
  identity: svc_report_worker
  permissions:
    - reports.read
    - storage.write

Production Note: each service should have its own identity. Shared credentials make auditing, rotation, revocation, and least privilege difficult.

Authentication Context

Authentication should produce a structured identity context rather than a simple true/false result.

{
  "subject": "usr_123",
  "organization_id": "org_456",
  "roles": ["billing_manager"],
  "scopes": ["orders:read", "orders:write"],
  "authentication_method": "mfa",
  "session_id": "ses_8b371"
}

Roles, tenant IDs, and permissions must come from trusted credentials or server-side data. They should never be accepted directly from an ordinary request body.

Session Authentication

Session authentication stores authentication state on the server. The client receives an opaque session identifier, usually through a secure cookie.

Advantages

  • Immediate revocation: deleting the session invalidates future requests.
  • Small client credential: only an opaque identifier is sent.
  • Central control: session state can be updated without reissuing credentials.
  • Strong browser fit: secure, HTTP-only cookies reduce direct token exposure to JavaScript.

Disadvantages

  • Shared storage: distributed applications need a shared session store.
  • Lookup cost: requests usually require loading session state.
  • CSRF risk: cookie-authenticated state-changing requests require CSRF protection.
  • Cross-service complexity: session propagation across many APIs can become awkward.

When to Use / Real-World Use Cases

  • First-party web applications using secure cookies.
  • Administrative systems needing immediate logout or forced session termination.
  • Applications with changing permissions that should take effect immediately.
  • Systems where central session auditing is more important than stateless validation.

Example

User signs in
  -> Server validates credentials
  -> Server creates session in Redis
  -> Browser receives HTTP-only secure cookie
  -> Request includes cookie
  -> API loads session and identity context

JWT Bearer Authentication

JWT bearer authentication uses signed tokens containing claims. APIs can verify those tokens using a trusted signing key.

Advantages

  • Distributed validation: APIs can validate tokens without a central session lookup.
  • Standardized claims: issuer, audience, subject, scopes, and expiration have common meanings.
  • Strong API interoperability: widely supported by API gateways, identity providers, and frameworks.
  • Good mobile and service fit: bearer tokens work naturally outside browser-cookie flows.

Disadvantages

  • Harder revocation: valid tokens normally remain usable until expiration.
  • Larger credentials: every request carries signed claims.
  • Validation complexity: issuer, audience, algorithm, expiration, and scopes must all be verified.
  • Stale claims: role or permission changes may not apply until a new token is issued.

When to Use / Real-World Use Cases

  • Mobile applications calling backend APIs.
  • Public REST APIs using OAuth 2.0 access tokens.
  • Microservices validating tokens issued by a shared authorization server.
  • API gateways performing token validation before requests reach services.

Example

Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

API validation:
  - verify signature
  - validate issuer
  - validate audience
  - validate expiration
  - validate scopes
  - build identity context

API Key Authentication

API keys are long-lived credentials used to identify applications, customers, integrations, or projects.

Advantages

  • Simple integration: easy for external systems and scripts.
  • Easy usage attribution: requests can be associated with a customer or application.
  • Useful for rate limiting: limits can be applied per key.
  • Low protocol complexity: no full OAuth flow is required.

Disadvantages

  • Often long-lived: leaked keys may remain valid for months.
  • Limited identity context: keys often represent an application, not a specific user.
  • Manual rotation: external integrations may not update credentials quickly.
  • Unsafe in public clients: browser or mobile binaries cannot reliably protect static keys.

When to Use / Real-World Use Cases

  • Server-to-server public API integrations.
  • Developer APIs where customers manage integration credentials.
  • Usage tracking and quota enforcement.
  • Low-complexity internal tools with narrowly scoped access.

Example

X-API-Key: sk_live_7f91a2...

Production Note: store key hashes rather than plaintext values when possible, show secrets only once, use recognizable prefixes, and support overlap during rotation.

Mutual TLS Authentication

Mutual TLS authenticates both client and server using certificates. The client verifies the server certificate, and the server verifies the client certificate.

Advantages

  • Strong machine identity: authentication happens during TLS connection establishment.
  • Encrypted communication: identity and transport security are combined.
  • Reduced bearer-token theft risk: possession of a token alone is not enough.
  • Good regulatory fit: commonly used in high-trust environments.

Disadvantages

  • Certificate lifecycle complexity: issuance, renewal, revocation, and trust chains must be managed.
  • Operational overhead: debugging failed certificate negotiation can be difficult.
  • Client compatibility: browser and third-party integration support may vary.
  • Infrastructure dependency: proxies and gateways must preserve verified client identity safely.

When to Use / Real-World Use Cases

  • Banking and financial integrations.
  • Internal service meshes.
  • Partner APIs requiring certificate-bound access.
  • Regulated systems needing strong workload identity.

Example

Client
  -> presents client certificate
Server
  -> verifies trusted certificate authority
TLS connection
  -> established only after both sides authenticate

Authentication Method Comparison

Authentication methods should be selected from client type, revocation requirements, credential lifetime, and operational maturity.

Method Best Fit Main Advantage Main Trade-Off
Session First-party web applications Immediate revocation Shared state and CSRF protection
JWT Mobile, public APIs, distributed services Distributed validation Revocation and claim staleness
API Key Third-party server integrations Simplicity Long-lived credential risk
Mutual TLS High-trust machine communication Strong certificate identity Certificate operations

OAuth 2.0 and OpenID Connect

OAuth 2.0 is an authorization framework. OpenID Connect extends OAuth with authentication and standardized identity information.

What OAuth 2.0 Solves

OAuth allows a client to access an API on behalf of a user or itself without receiving the user’s password.

User
  -> Authorization Server
  -> Grants consent
  -> Client receives access token
  -> Client calls API
  -> API validates audience and scope

Authorization Code Flow

The authorization code flow is the standard interactive OAuth flow. Public clients should use PKCE.

1. Client generates PKCE verifier and challenge
2. User is redirected to authorization server
3. User authenticates and grants consent
4. Authorization server returns short-lived code
5. Client exchanges code and verifier for tokens
6. Client calls API with access token

OpenID Connect

OpenID Connect adds identity through an ID token and standardized claims.

Token Consumer Purpose
Access token API Authorize protected operations.
ID token Client application Describe the authenticated login.
Refresh token Client application Obtain new access tokens.

Important: APIs should receive access tokens. ID tokens should not be used as general API credentials.

JWT Validation

Decoding a JWT only reads its payload. Validation proves that the token was issued by a trusted authority and is valid for the current API.

JWT Structure

header.payload.signature
Section Purpose
Header Algorithm and key identifier.
Payload Claims such as issuer, subject, audience, and expiration.
Signature Protects the header and payload from modification.

JWT claims are encoded, not encrypted. Sensitive secrets should not be placed in ordinary signed JWTs.

Signature Validation

Signature validation should use a trusted public key and an explicitly allowed algorithm.

JWT header contains key ID
  -> API loads matching key from trusted JWKS
  -> API verifies signature using allowed algorithm
  -> Invalid tokens are rejected

Important: never accept unsigned tokens or trust the token-provided algorithm without server-side restrictions.

Claim Validation

Claim Required Validation
iss Must match the trusted issuer.
aud Must include the current API.
exp Must be in the future.
nbf Token must already be active.
sub Must contain a valid caller identifier.
scope Must include the operation’s required permission.

JWKS Caching and Key Rotation

APIs using asymmetric JWT signing usually fetch public keys from a JWKS endpoint.

  • Cache keys to avoid a network request on every API call.
  • Respect cache headers when the identity provider supplies them.
  • Refresh on unknown key ID to support signing-key rotation.
  • Fail safely when the key endpoint is unavailable and no valid cached key exists.

Production Note: key rotation should be tested before the first emergency rotation is required.

Role-Based Access Control

RBAC assigns permissions to roles, then assigns roles to users or services.

Advantages

  • Easy to understand: roles map naturally to organizational responsibilities.
  • Simple administration: permissions are managed centrally.
  • Good auditability: role assignments are straightforward to review.
  • Strong enterprise fit: roles such as admin, support, and billing manager are common.

Disadvantages

  • Role explosion: many combinations produce too many roles.
  • Weak contextual rules: roles alone cannot express ownership or location constraints well.
  • Coarse permissions: users may receive more access than needed.
  • Difficult exceptions: one-off business rules often bypass the role model.

When to Use / Real-World Use Cases

  • Administrative dashboards.
  • SaaS applications with organization-level roles.
  • Internal enterprise systems.
  • Applications with stable permission groups.

Example

Role: billing_manager

Permissions:
  invoices.read
  invoices.create
  payments.refund

Attribute-Based Access Control

ABAC evaluates user, resource, environment, and request attributes.

Advantages

  • Fine-grained: policies can include department, clearance, region, or risk level.
  • Dynamic: access can change from current attributes rather than static role assignments.
  • Reduces role explosion: policies replace many specialized roles.
  • Strong policy expressiveness: complex business rules can be modeled directly.

Disadvantages

  • Harder to reason about: many attributes interact.
  • More difficult auditing: effective permissions may not be obvious.
  • Policy testing required: subtle combinations can create unexpected access.
  • Data dependency: inaccurate attributes lead to incorrect authorization.

When to Use / Real-World Use Cases

  • Healthcare systems with patient, provider, and location rules.
  • Financial platforms with risk-based access.
  • Enterprise document systems using department and classification.
  • Multi-region systems with geographic restrictions.

Example

Allow access if:

user.department == document.department

AND

user.clearance_level >= document.classification_level

AND

request.region IN user.allowed_regions

Relationship-Based Access Control

Relationship-Based Access Control evaluates how identities relate to resources and other identities.

Advantages

  • Natural sharing model: permissions follow ownership and collaboration.
  • Good hierarchy support: access can flow through organizations, projects, and teams.
  • Strong resource context: policies reflect real relationships.
  • Good large-product fit: collaborative systems often need relationship graphs.

Disadvantages

  • Graph complexity: authorization checks may require relationship traversal.
  • Caching difficulty: relationship changes can invalidate many decisions.
  • Operational complexity: policy engines and relationship stores may be required.
  • Harder debugging: access can be inherited through several levels.

When to Use / Real-World Use Cases

  • Shared documents and folders.
  • Source-code repositories.
  • Project-management tools.
  • Social and collaboration platforms.

Example

User
  -> member of Team
  -> Team owns Project
  -> Project contains Document

Result:
  User may read Document

Authorization Model Comparison

Authorization models can be combined. RBAC may establish broad responsibilities, while ABAC or relationship checks enforce resource-level rules.

Model Best Fit Main Advantage Main Trade-Off
RBAC Stable organizational roles Simplicity Role explosion
ABAC Contextual enterprise policies Fine-grained rules Policy complexity
Relationship-based Collaborative resource sharing Natural ownership model Relationship graph complexity

Resource-Level Authorization

Authentication and broad scopes are not enough. APIs should verify access to every protected resource.

Ownership Checks

# Load the order using both resource ID and authenticated owner.
# This prevents accidentally returning another user's order.
order = repository.get_order(
    order_id=order_id,
    customer_id=current_user.id,
)

if order is None:
    # Return 404 to avoid revealing whether another user's order exists.
    raise HTTPException(status_code=404, detail="Order not found")

Tenant Isolation

Multi-tenant queries should always include the trusted tenant identifier.

-- tenant_id comes from the validated identity context,
-- never from an untrusted request body.
SELECT *
FROM orders
WHERE tenant_id = :tenant_id
  AND order_id = :order_id;

Important: missing tenant filters are a common cause of cross-customer data exposure.

Preventing IDOR

IDOR occurs when an API accepts a resource ID and returns the resource without checking whether the caller may access it.

GET /orders/ord_123

Required checks:
  - token is valid
  - caller has orders:read
  - order belongs to caller's tenant
  - caller owns order or has elevated role

Token Lifecycle

Token security depends on issuance, storage, expiration, refresh, revocation, and rotation.

Access Token Expiration

Access tokens should usually be short-lived. Five to thirty minutes is common, depending on risk and client behavior.

Short lifetimes reduce exposure if a token is stolen.

Refresh Tokens

Refresh tokens obtain new access tokens without another interactive login.

Login
  -> Access token: 15 minutes
  -> Refresh token: longer lifetime

Access token expires
  -> Client presents refresh token
  -> Authorization server rotates refresh token
  -> Client receives new token pair

Revocation and Rotation

  • Rotate refresh tokens after successful use.
  • Revoke token families when reuse is detected.
  • Rotate signing keys without breaking active validation.
  • Disable compromised API keys immediately.
  • Audit credential issuance and revocation.

Secure Token Storage

Client Type Preferred Storage
Browser web app Secure HTTP-only cookie or carefully designed in-memory token model.
Mobile app Platform secure storage such as Keychain or Keystore.
Backend service Workload identity or secrets manager.
CLI tool OS-protected credential storage.

Production Note: local storage exposes tokens to JavaScript and increases the impact of cross-site scripting vulnerabilities.

Service-to-Service Authentication

Backend services need independent identities and narrowly scoped permissions.

Client Credentials

OAuth client credentials are useful when one backend service calls another through an authorization server.

orders-api
  -> Authorization Server
  -> Access token with payments:create
  -> payments-api

Workload Identities

Cloud workload identities eliminate static credentials.

Platform Mechanism
AWS IAM roles for ECS tasks, Lambda, and EC2.
Kubernetes Service accounts and workload identity integration.
Google Cloud Workload Identity.
Azure Managed Identities.

Signed Requests

Signed-request protocols authenticate the caller and protect selected request components from tampering.

HTTP method
+ request path
+ selected headers
+ payload hash
+ timestamp
  -> cryptographic signature

AWS Signature Version 4 is a common example.

Ready-to-Use Example

The following examples show a practical JWT-protected Orders API with scopes, ownership checks, consistent errors, JWKS caching, and an API Gateway JWT authorizer.

OpenAPI Security Example

openapi: 3.0.3

info:
  title: Orders API
  version: "1.0"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: OAuth 2.0 access token issued for the Orders API.

  schemas:
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
            - request_id
          properties:
            code:
              type: string
            message:
              type: string
            request_id:
              type: string

paths:
  /v1/orders/{order_id}:
    get:
      summary: Get an order
      security:
        - bearerAuth: []

      parameters:
        - name: order_id
          in: path
          required: true
          schema:
            type: string

      responses:
        "200":
          description: Order returned.

        "401":
          description: Missing or invalid access token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

        "403":
          description: Token is valid but required scope is missing.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

        "404":
          description: Order does not exist or is not visible to the caller.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

FastAPI JWT Example

from __future__ import annotations

import time
from dataclasses import dataclass
from typing import Any

import httpx
from fastapi import Depends, FastAPI, Header, HTTPException, Request
from fastapi.responses import JSONResponse
from jose import JWTError, jwt

app = FastAPI(title="Orders API")

ISSUER = "https://auth.example.com"
AUDIENCE = "orders-api"
JWKS_URL = f"{ISSUER}/.well-known/jwks.json"
ALLOWED_ALGORITHMS = ["RS256"]

# Cache public signing keys so every API request does not call the identity provider.
_jwks_cache: dict[str, Any] | None = None
_jwks_cache_expires_at = 0.0


@dataclass(frozen=True)
class Identity:
    subject: str
    tenant_id: str
    scopes: frozenset[str]
    roles: frozenset[str]


ORDERS = {
    "ord_123": {
        "id": "ord_123",
        "tenant_id": "org_456",
        "customer_id": "usr_123",
        "status": "paid",
    }
}


class ApiError(Exception):
    def __init__(self, status_code: int, code: str, message: str):
        self.status_code = status_code
        self.code = code
        self.message = message


@app.middleware("http")
async def add_request_id(request: Request, call_next):
    # Preserve caller request IDs when available so distributed traces stay connected.
    request.state.request_id = request.headers.get("x-request-id", "req_generated")

    response = await call_next(request)
    response.headers["x-request-id"] = request.state.request_id
    return response


@app.exception_handler(ApiError)
async def handle_api_error(request: Request, exc: ApiError):
    # Return one stable error shape across authentication and authorization failures.
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "error": {
                "code": exc.code,
                "message": exc.message,
                "request_id": request.state.request_id,
            }
        },
    )


async def get_jwks(force_refresh: bool = False) -> dict[str, Any]:
    global _jwks_cache
    global _jwks_cache_expires_at

    now = time.time()

    # Refresh when cache expires or an unknown key ID appears during rotation.
    if force_refresh or _jwks_cache is None or now >= _jwks_cache_expires_at:
        async with httpx.AsyncClient(timeout=3.0) as client:
            response = await client.get(JWKS_URL)
            response.raise_for_status()
            _jwks_cache = response.json()

        # A short cache keeps validation available while still supporting key rotation.
        _jwks_cache_expires_at = now + 300

    return _jwks_cache


async def find_signing_key(token: str) -> dict[str, Any]:
    try:
        header = jwt.get_unverified_header(token)
    except JWTError as exc:
        raise ApiError(401, "invalid_token", "Access token is malformed.") from exc

    key_id = header.get("kid")
    algorithm = header.get("alg")

    # Never allow the token to choose an unexpected algorithm.
    if algorithm not in ALLOWED_ALGORITHMS:
        raise ApiError(401, "invalid_token", "Token algorithm is not allowed.")

    for force_refresh in (False, True):
        jwks = await get_jwks(force_refresh=force_refresh)

        for key in jwks.get("keys", []):
            if key.get("kid") == key_id:
                return key

    raise ApiError(401, "invalid_token", "Signing key was not found.")


async def decode_access_token(token: str) -> dict[str, Any]:
    signing_key = await find_signing_key(token)

    try:
        # Verify signature plus issuer, audience, and expiration.
        return jwt.decode(
            token,
            signing_key,
            algorithms=ALLOWED_ALGORITHMS,
            issuer=ISSUER,
            audience=AUDIENCE,
            options={
                "verify_signature": True,
                "verify_exp": True,
                "verify_aud": True,
                "verify_iss": True,
            },
        )
    except JWTError as exc:
        raise ApiError(401, "invalid_token", "Access token is invalid or expired.") from exc


async def get_identity(
    authorization: str | None = Header(default=None),
) -> Identity:
    if not authorization or not authorization.startswith("Bearer "):
        raise ApiError(401, "unauthenticated", "Bearer token is required.")

    token = authorization.removeprefix("Bearer ").strip()
    claims = await decode_access_token(token)

    subject = claims.get("sub")
    tenant_id = claims.get("tenant_id")

    if not subject or not tenant_id:
        raise ApiError(401, "invalid_token", "Required identity claims are missing.")

    scopes = frozenset(str(claims.get("scope", "")).split())
    roles = frozenset(claims.get("roles", []))

    return Identity(
        subject=subject,
        tenant_id=tenant_id,
        scopes=scopes,
        roles=roles,
    )


def require_scope(required_scope: str):
    async def dependency(identity: Identity = Depends(get_identity)) -> Identity:
        if required_scope not in identity.scopes:
            raise ApiError(
                403,
                "insufficient_scope",
                f"Required scope is missing: {required_scope}.",
            )

        return identity

    return dependency


@app.get("/v1/orders/{order_id}")
async def get_order(
    order_id: str,
    identity: Identity = Depends(require_scope("orders:read")),
):
    order = ORDERS.get(order_id)

    # Apply tenant isolation before returning any resource data.
    if not order or order["tenant_id"] != identity.tenant_id:
        raise ApiError(404, "order_not_found", "Order was not found.")

    # Resource ownership is still required after tenant and scope validation.
    is_owner = order["customer_id"] == identity.subject
    is_admin = "admin" in identity.roles

    if not is_owner and not is_admin:
        # 404 avoids revealing that another user's resource exists.
        raise ApiError(404, "order_not_found", "Order was not found.")

    return {
        "id": order["id"],
        "status": order["status"],
    }

Python Client Example

import requests
from uuid import uuid4

API_BASE_URL = "https://api.example.com"
ACCESS_TOKEN = "replace-with-short-lived-access-token"

request_id = f"req_{uuid4().hex[:8]}"

response = requests.get(
    f"{API_BASE_URL}/v1/orders/ord_123",
    headers={
        # Access tokens belong in the Authorization header.
        "Authorization": f"Bearer {ACCESS_TOKEN}",

        # Request IDs connect client errors with server logs.
        "X-Request-ID": request_id,
    },

    # Every production HTTP call needs an explicit timeout.
    timeout=3,
)

if response.status_code == 401:
    print("Token is invalid or expired.")

elif response.status_code == 403:
    print("Token is valid but does not have enough permission.")

elif response.status_code == 404:
    print("Order does not exist or is not visible to this caller.")

else:
    response.raise_for_status()
    print(response.json())

CloudFormation JWT Authorizer

AWSTemplateFormatVersion: "2010-09-09"
Description: HTTP API with JWT authorization

Parameters:
  JwtIssuer:
    Type: String
    Description: Trusted OpenID Connect issuer URL.

  JwtAudience:
    Type: String
    Description: Audience claim expected by this API.

Resources:
  OrdersApi:
    Type: AWS::ApiGatewayV2::Api
    Properties:
      Name: orders-api
      ProtocolType: HTTP

  OrdersJwtAuthorizer:
    Type: AWS::ApiGatewayV2::Authorizer
    Properties:
      ApiId: !Ref OrdersApi

      # JWT authorizers validate signed access tokens before Lambda or ECS receives traffic.
      AuthorizerType: JWT

      # Read the bearer token from the standard Authorization header.
      IdentitySource:
        - "$request.header.Authorization"

      Name: orders-jwt-authorizer

      JwtConfiguration:
        # API Gateway validates that the access token was issued for this API.
        Audience:
          - !Ref JwtAudience

        # API Gateway retrieves signing keys from the issuer's discovery metadata.
        Issuer: !Ref JwtIssuer

  GetOrderRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId: !Ref OrdersApi
      RouteKey: "GET /v1/orders/{order_id}"

      # Associate the route with the JWT authorizer.
      AuthorizationType: JWT
      AuthorizerId: !Ref OrdersJwtAuthorizer

      # Require an explicit scope before traffic reaches the backend.
      AuthorizationScopes:
        - orders:read

      # Replace with a real HTTP or Lambda integration target.
      Target: "integrations/replace-with-integration-id"

Production Note: gateway-level validation reduces invalid traffic reaching the application, but resource ownership and tenant authorization still belong in the backend.

Common Mistakes

API security mistakes usually come from validating only part of the request or treating authentication as complete authorization.

  • Decoding JWTs without verifying signatures.
  • Skipping issuer or audience validation.
  • Using ID tokens as API access tokens.
  • Issuing long-lived access tokens.
  • Storing tokens insecurely in browser local storage.
  • Sharing API keys between services or customers.
  • Checking scopes without checking resource ownership.
  • Accepting tenant IDs from request payloads.
  • Embedding authorization logic inconsistently across controllers.
  • Failing to test signing-key rotation.
  • Logging access tokens, refresh tokens, or API keys.
  • Returning detailed security errors that reveal protected resources.

Production Checklist

A production API security model should validate credentials, enforce least privilege, protect token lifecycle, and authorize each resource access.

  • Separate authentication and authorization.
  • Use HTTPS everywhere.
  • Validate JWT signature, issuer, audience, expiration, and algorithm.
  • Cache JWKS keys and support rotation.
  • Issue short-lived access tokens.
  • Rotate refresh tokens after use.
  • Store browser credentials securely.
  • Hash API keys where possible.
  • Give every service its own identity.
  • Prefer workload identities over static cloud credentials.
  • Validate required scopes or permissions.
  • Apply resource ownership checks.
  • Filter every multi-tenant query by trusted tenant context.
  • Audit privileged and security-sensitive actions.
  • Never log secrets or bearer tokens.
  • Test credential revocation and signing-key rotation.

Conclusion

API authentication establishes trusted identity, while API authorization determines what that identity may do. Secure systems keep these concerns separate and enforce both consistently.

Session authentication, JWTs, API keys, and mutual TLS each solve different client and infrastructure problems. RBAC, ABAC, and relationship-based authorization provide different ways to model permissions. The correct design depends on client type, risk, scale, revocation needs, and resource relationships.

Key Takeaway: validate every credential, restrict every identity, and authorize every protected resource. A valid token proves identity; it does not prove access.

Comments (0)