Authentication
The WispHub API implements two independent authentication layers :
JWT Bearer Token — protects all endpoints of this API. Clients (bots, frontends, etc.) must obtain a token before consuming any resource.
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
Property Value Algorithm HS256 Default expiry 60 minutes (configurable) Claim sub = usernameHeader scheme Bearer
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:
# 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:
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
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:
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:
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).
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
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" )
Restrict File Permissions
Ensure the .env file has restricted permissions: This makes it readable only by the file owner.
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
Rotate Keys Regularly
Establish a key rotation policy:
Generate new API keys quarterly
Invalidate old keys after migration
Maintain audit log of key usage
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)
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:
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 :
Verify the API key in your .env file
Check that the key hasn’t been revoked in WispHub Net
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: