Skip to main content

Authentication

The WispHub API implements two independent authentication layers:
  1. JWT Bearer Token — protects all endpoints of this API. Clients (bots, frontends, etc.) must obtain a token before consuming any resource.
  2. WispHub Net API Key — used internally by the API to authenticate against the WispHub Net backend. Clients of this API do not need to manage this key.

Part 1: JWT Authentication (API Clients)

Authentication Flow

┌──────────────┐
│ Client App   │
│ (Bot/Web)    │
└──────┬───────┘

       │ POST /api/v1/auth/token
       │ {username, password}

┌──────────────────┐
│  WispHub API     │  Validates credentials → issues JWT
│  (This API)      │
└──────┬───────────┘

       │ Authorization: Bearer <jwt_token>
       │ (all subsequent requests)

┌──────────────────┐
│  Protected       │  get_current_user dependency
│  Endpoints       │  validates and decodes the JWT
└──────┬───────────┘
       │ (if JWT is valid)
       │ HTTP Request
       │ Header: Authorization: Api-Key {KEY}

┌──────────────────┐
│  WispHub Net     │  API Key validation
│  (External)      │
└──────────────────┘

Obtaining a Token

Call the login endpoint with your credentials as form-data:
curl -X POST http://localhost:8000/api/v1/auth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "username=admin&password=your_password"
Response:
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer"
}
This endpoint follows the OAuth2 application/x-www-form-urlencoded standard, not JSON. The fields are username and password.

Using the Token

Include the token in the Authorization header of all requests to protected endpoints:
Authorization: Bearer <access_token>
Example:
curl http://localhost:8000/api/v1/clients/ \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Token Properties

PropertyValue
AlgorithmHS256
Default expiry60 minutes (configurable)
Claimsub = username
Header schemeBearer

Token Expiry

When a token expires, protected endpoints return:
{
  "detail": "Could not validate credentials."
}
With the WWW-Authenticate: Bearer header. Simply request a new token.

JWT Configuration

The following environment variables control JWT behavior:
.env
# Secret key used to sign tokens — use a long random string
JWT_SECRET_KEY=your_very_long_random_secret_key_here

# Signing algorithm (default HS256)
JWT_ALGORITHM=HS256

# Token validity in minutes (default 60)
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=60

# API access credentials
API_USERNAME=admin
API_PASSWORD_HASH=$2b$12$...  # bcrypt hash of your password
JWT_SECRET_KEY must be a long, cryptographically random string. Never use predictable values like "secret" or "1234" in production. Generate one with: openssl rand -hex 32
To generate the bcrypt hash of your password, use Python:
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
print(pwd_context.hash("your_password"))

How JWT is Implemented

Token verification on protected endpoints is handled via a FastAPI dependency:
app/api/dependencies.py
from fastapi import Depends
from fastapi.security import OAuth2PasswordBearer
from app.core.security import decode_access_token

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token")

async def get_current_user(token: str = Depends(oauth2_scheme)) -> str:
    return decode_access_token(token)
All protected endpoints include _: str = Depends(get_current_user) in their signature, causing FastAPI to automatically reject requests without a valid token with HTTP 401.

Testing JWT Authentication

tests/conftest.py
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app

@pytest.fixture
async def auth_token(async_client: AsyncClient):
    response = await async_client.post(
        "/api/v1/auth/token",
        data={"username": "admin", "password": "test_password"},
    )
    return response.json()["access_token"]

@pytest.fixture
def auth_headers(auth_token: str):
    return {"Authorization": f"Bearer {auth_token}"}
tests/api/test_clients.py
@pytest.mark.asyncio
async def test_list_clients_requires_auth(async_client):
    # Without token → 401
    response = await async_client.get("/api/v1/clients/")
    assert response.status_code == 401

@pytest.mark.asyncio
async def test_list_clients_with_token(async_client, auth_headers):
    # With valid token → 200
    response = await async_client.get("/api/v1/clients/", headers=auth_headers)
    assert response.status_code == 200

Part 2: WispHub Net API Key (Outbound)

Authentication Flow

┌──────────────┐
│ Client App   │
│ (Bot/Web)    │
└──────┬───────┘

       │ HTTP Request
       │ Header: Authorization: Bearer {JWT}

┌──────────────────┐
│  WispHub API     │  JWT validation
│  (This API)      │  (get_current_user)
└──────┬───────────┘

       │ HTTP Request
       │ Header: Authorization: Api-Key {KEY}

┌──────────────────┐
│  WispHub Net     │  API Key validation
│  (External)      │
└──────────────────┘
Clients authenticate with this API using JWT (see Part 1). The API acts as a trusted proxy to WispHub Net using its own Api-Key, without exposing it to clients.

Configuration

Environment Variables

Authentication credentials are configured via environment variables:
.env
WISPHUB_NET_KEY=your_api_key_here
WISPHUB_NET_HOST=https://api.wisphub.net
Never commit the .env file to version control. Add it to .gitignore to prevent credential leakage.

Settings Management

Credentials are loaded using Pydantic Settings:
app/core/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", case_sensitive=True)

    # WispHub Net authentication
    WISPHUB_NET_KEY: str
    WISPHUB_NET_HOST: str
    
    # ... other settings

settings = Settings()
The case_sensitive=True setting ensures environment variable names must match exactly (e.g., WISPHUB_NET_KEY, not wisphub_net_key).

Authentication Headers

All requests to WispHub Net include the API key in the Authorization header:
app/services/clients_service.py
HEADERS = {
    "Authorization": f"Api-Key {settings.WISPHUB_NET_KEY}"
}

async def fetch_client(params: Dict[str, str]) -> Optional[ClientResponse]:
    async with httpx.AsyncClient(timeout=10) as client:
        response = await client.get(
            settings.CLIENTS_URL,
            headers=HEADERS,
            params=params
        )
The format is:
Authorization: Api-Key <your_api_key>

Security Best Practices

1

Use Environment Variables

Never hardcode API keys in source code. Always use environment variables:
# ❌ NEVER DO THIS
API_KEY = "abc123def456"

# ✅ CORRECT
API_KEY = os.getenv("WISPHUB_NET_KEY")
2

Restrict File Permissions

Ensure the .env file has restricted permissions:
chmod 600 .env
This makes it readable only by the file owner.
3

Use Secrets Management in Production

For production deployments, use proper secrets management:
  • Docker: Use Docker secrets or environment variables
  • Kubernetes: Use Kubernetes secrets
  • Cloud: Use AWS Secrets Manager, Azure Key Vault, or Google Secret Manager
4

Rotate Keys Regularly

Establish a key rotation policy:
  • Generate new API keys quarterly
  • Invalidate old keys after migration
  • Maintain audit log of key usage
5

Monitor for Unauthorized Access

Watch WispHub Net logs for:
  • Unexpected API usage patterns
  • Failed authentication attempts
  • Requests from unknown IP addresses

Docker Deployment

When deploying with Docker, pass environment variables securely:

Using —env-file (Development)

docker run -d \
  --name wisphub_api_server \
  -p 8000:8000 \
  --env-file .env \
  wisphubapi:latest

Using Docker Secrets (Production)

docker-compose.yml
version: '3.8'

services:
  api:
    image: wisphubapi:latest
    ports:
      - "8000:8000"
    secrets:
      - wisphub_api_key
    environment:
      WISPHUB_NET_KEY_FILE: /run/secrets/wisphub_api_key
      WISPHUB_NET_HOST: https://api.wisphub.net

secrets:
  wisphub_api_key:
    external: true
Then create the secret:
echo "your_api_key" | docker secret create wisphub_api_key -

Error Handling

Invalid API Key

If the API key is invalid, WispHub Net returns a 401 or 403 error:
if response.status_code in [401, 403]:
    # Authentication failed
    return None
The middleware API handles this gracefully:
{
  "ok": false,
  "type": "error",
  "action": "CLIENT_NOT_FOUND",
  "data": null,
  "message": "Unable to fetch client data"
}
Authentication failures are logged but not exposed to end users to prevent information disclosure.

Missing API Key

If the WISPHUB_NET_KEY environment variable is not set, the application will fail to start:
from pydantic import ValidationError

try:
    settings = Settings()
except ValidationError as e:
    print(f"Configuration error: {e}")
    sys.exit(1)
Error output:
Configuration error: 1 validation error for Settings
WISPHUB_NET_KEY
  field required (type=value_error.missing)

Testing Authentication

For testing purposes, you can mock the authentication:
tests/conftest.py
import pytest
from unittest.mock import patch

@pytest.fixture
def mock_wisphub_auth():
    with patch('app.core.config.settings.WISPHUB_NET_KEY', 'test_key_123'):
        yield
Using respx to mock WispHub Net responses:
tests/api/test_clients.py
import respx
import httpx

@pytest.mark.asyncio
@respx.mock
async def test_client_fetch_with_auth():
    respx.get("https://api.wisphub.net/api/clientes/").mock(
        return_value=httpx.Response(
            status_code=200,
            json={"results": [{"id_servicio": 1, "nombre": "Test"}]}
        )
    )
    
    # Test your endpoint
    response = await async_client.get("/api/v1/clients/")
    assert response.status_code == 200

API Key Permissions

The WispHub API key must have the following permissions in WispHub Net:
  • Read Clients (api/clientes/): View client information
  • Update Clients (api/clientes/{id}/perfil/): Update client profiles
  • Read Plans (api/plan-internet/): View internet plans
  • Read/Write Tickets (api/tickets/): Create and view support tickets
  • Read Tasks (api/tasks/): View task information
Use the principle of least privilege. Only grant permissions that the API actually needs.

Rate Limiting

WispHub Net may impose rate limits on API key usage:
  • Typical limit: 1000 requests per hour
  • Burst limit: 100 requests per minute
The caching system helps avoid rate limits:
  • Without cache: ~6,000 requests/hour for 100 users
  • With cache: ~12 requests/hour for 100 users
The 5-minute cache TTL for clients and 15-minute TTL for plans effectively keeps the API well under typical rate limits.

Troubleshooting

Issue: “401 Unauthorized” errors

Cause: Invalid or expired API key Solution:
  1. Verify the API key in your .env file
  2. Check that the key hasn’t been revoked in WispHub Net
  3. Ensure no extra whitespace in the environment variable
# Check the loaded value
docker exec wisphub_api_server env | grep WISPHUB_NET_KEY

Issue: “403 Forbidden” errors

Cause: API key lacks required permissions Solution: Contact your WispHub administrator to grant the necessary permissions to your API key.

Issue: Application won’t start

Cause: Missing WISPHUB_NET_KEY environment variable Solution: Ensure the .env file exists and contains the required variable:
cat .env | grep WISPHUB_NET_KEY
For complete configuration details, see: