apikey-gateway 1.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ """API Key Gateway - Professional API key validation library"""
2
+
3
+ # Public API exports
4
+ from .gateway import APIKeyGateway, gateway
5
+ from .decorators import apikey_login
6
+ from .helpers import (
7
+ add_apikey_gateway_middleware,
8
+ retry_on_exception,
9
+ fetch_valid_public_keys,
10
+ compute_public_key,
11
+ validate_api_key,
12
+ CACHE_TTL_SECONDS,
13
+ API_KEY_CACHE,
14
+ PH
15
+ )
16
+
17
+ # Middleware-related exports
18
+ try:
19
+ from .middleware import APIKeyGatewayMiddleware, FASTAPI_AVAILABLE
20
+ __all__ = [
21
+ 'APIKeyGateway',
22
+ 'gateway',
23
+ 'apikey_login',
24
+ 'add_apikey_gateway_middleware',
25
+ 'APIKeyGatewayMiddleware',
26
+ 'FASTAPI_AVAILABLE',
27
+ 'retry_on_exception',
28
+ 'fetch_valid_public_keys',
29
+ 'compute_public_key',
30
+ 'validate_api_key',
31
+ 'CACHE_TTL_SECONDS',
32
+ 'API_KEY_CACHE',
33
+ 'PH'
34
+ ]
35
+ except ImportError:
36
+ __all__ = [
37
+ 'APIKeyGateway',
38
+ 'gateway',
39
+ 'apikey_login',
40
+ 'add_apikey_gateway_middleware',
41
+ 'retry_on_exception',
42
+ 'fetch_valid_public_keys',
43
+ 'compute_public_key',
44
+ 'validate_api_key',
45
+ 'CACHE_TTL_SECONDS',
46
+ 'API_KEY_CACHE',
47
+ 'PH'
48
+ ]
@@ -0,0 +1,65 @@
1
+ """Decorators for API key validation"""
2
+
3
+ import argparse
4
+ import sys
5
+ import os
6
+
7
+
8
+ def apikey_login(func=None, *, public_keys_url=None, service=None, verbose=True):
9
+ """
10
+ Decorator that adds API key validation to a function.
11
+
12
+ :param func: The function to decorate (required if using as decorator without params)
13
+ :param public_keys_url: Optional URL to fetch valid public keys from (defaults to the official URL)
14
+ :param service: The service name to validate the API key against (required)
15
+ :param verbose: Whether to print verbose messages (default: True)
16
+ """
17
+ # Check if service parameter is provided
18
+ if func is None:
19
+ # If we're in the parameterized decorator mode, we'll check service later
20
+ pass
21
+ elif service is None:
22
+ raise ValueError("service parameter is required for apikey_login decorator")
23
+
24
+ # Handle both decorator with and without parameters
25
+ if func is None:
26
+ # Return a new decorator with the provided parameters
27
+ return lambda f: apikey_login(f, public_keys_url=public_keys_url, service=service, verbose=verbose)
28
+
29
+ def wrapper(*args, **kwargs):
30
+ # Create argument parser
31
+ parser = argparse.ArgumentParser(add_help=False) # Don't add help to avoid conflict
32
+ parser.add_argument(
33
+ '-a', '--apikey',
34
+ required=False,
35
+ help='Your API secret key (fallbacks to AKGATEWAY_API_KEY env var)'
36
+ )
37
+
38
+ # Save original argv to restore later
39
+ original_argv = sys.argv.copy()
40
+
41
+ # Parse only API key arguments
42
+ args_parsed, remaining_args = parser.parse_known_args()
43
+
44
+ # Get API key from command line or environment variable
45
+ secret_key = args_parsed.apikey or os.environ.get('AKGATEWAY_API_KEY')
46
+
47
+ # If no API key provided, raise an error
48
+ if not secret_key:
49
+ parser.error('API key is required. Use -a/--apikey or set AKGATEWAY_API_KEY environment variable')
50
+
51
+ # Replace sys.argv with remaining args (excluding API key) for the wrapped function
52
+ sys.argv = [sys.argv[0]] + remaining_args
53
+
54
+ try:
55
+ # Use the gateway singleton to validate the API key
56
+ from .gateway import gateway
57
+ gateway.validate_api_key(secret_key, service, public_keys_url=public_keys_url, verbose=verbose)
58
+
59
+ # If validation passes, call the decorated function
60
+ return func(*args, **kwargs)
61
+ finally:
62
+ # Always restore original argv
63
+ sys.argv = original_argv
64
+
65
+ return wrapper
@@ -0,0 +1,245 @@
1
+ """Core API key gateway logic for validation and caching"""
2
+
3
+ import argparse
4
+ import urllib.request
5
+ import urllib.error
6
+ import json
7
+ import os
8
+ import time
9
+ from functools import wraps
10
+ from argon2 import PasswordHasher
11
+ from argon2.exceptions import HashingError
12
+
13
+
14
+ class APIKeyGateway:
15
+ """Core API key gateway logic for validation and caching"""
16
+
17
+ def __init__(self):
18
+ self.PH = PasswordHasher(
19
+ memory_cost=65536,
20
+ time_cost=3,
21
+ parallelism=4,
22
+ hash_len=32,
23
+ salt_len=16
24
+ )
25
+
26
+ # Cache to store valid API key-service pairs
27
+ # Format: {(secret_key, service): expiration_time}
28
+ self.API_KEY_CACHE = {}
29
+
30
+ # Cache TTL: 1 day (86400 seconds)
31
+ self.CACHE_TTL_SECONDS = 86400
32
+
33
+ # Default URL using JSONBin.io
34
+ self.DEFAULT_URL = "https://api.jsonbin.io/v3/b/691ec6a543b1c97be9b8ea6d"
35
+
36
+ @staticmethod
37
+ def retry_on_exception(exceptions, max_retries=3, delay=0.5, backoff=1):
38
+ """
39
+ Decorator that retries a function on specific exceptions.
40
+ """
41
+ def decorator(func):
42
+ @wraps(func)
43
+ def wrapper(*args, **kwargs):
44
+ verbose = kwargs.get('verbose', True)
45
+ func_kwargs = {k: v for k, v in kwargs.items() if k != 'verbose'}
46
+ retries = 0
47
+ while retries <= max_retries:
48
+ try:
49
+ return func(*args, **func_kwargs)
50
+ except exceptions as e:
51
+ retries += 1
52
+ if retries > max_retries:
53
+ if verbose:
54
+ print(f" All {max_retries} retry attempts failed. Exception: {e}")
55
+ raise
56
+ wait_time = delay * (backoff ** (retries - 1))
57
+ if verbose:
58
+ print(f" Retry {retries}/{max_retries} in {wait_time:.2f} seconds due to: {type(e).__name__}: {e}")
59
+ time.sleep(wait_time)
60
+ return None # This line should never be reached
61
+ return wrapper
62
+ return decorator
63
+
64
+ @retry_on_exception(
65
+ urllib.error.URLError,
66
+ max_retries=3,
67
+ delay=0.5,
68
+ backoff=1 # constant delay, not exponential
69
+ )
70
+ def fetch_valid_public_keys(self, url, verbose=True, jsonbin_api_key=None):
71
+ """Fetch the list of valid public keys from the specified URL."""
72
+ try:
73
+ # Set proper headers for JSONBin.io API
74
+ headers = {
75
+ 'accept': 'application/json',
76
+ }
77
+
78
+ # Add JSONBin API key if available (parameter takes precedence over env var)
79
+ jsonbin_api_key = jsonbin_api_key or os.environ.get('JSONBIN_API_KEY')
80
+ if jsonbin_api_key:
81
+ headers['X-Access-Key'] = jsonbin_api_key
82
+
83
+ # Create a request object with the headers
84
+ req = urllib.request.Request(url, headers=headers)
85
+
86
+ # Set timeout to avoid hanging (10 seconds)
87
+ with urllib.request.urlopen(req, timeout=10) as response:
88
+ data = json.loads(response.read().decode('utf-8'))
89
+
90
+ # JSONBin API wraps the data in a 'record' field
91
+ if isinstance(data, dict) and 'record' in data:
92
+ data = data['record']
93
+ all_keys = []
94
+
95
+ # Expected structure: {service: {key_id: public_key, ...}}
96
+ for service_name, service_data in data.items():
97
+ if isinstance(service_data, dict):
98
+ # Service has multiple API keys
99
+ for key_id, public_key in service_data.items():
100
+ all_keys.append({
101
+ "service": service_name,
102
+ "public_key": public_key
103
+ })
104
+ else:
105
+ # Service has a single API key
106
+ all_keys.append({
107
+ "service": service_name,
108
+ "public_key": service_data
109
+ })
110
+ return all_keys
111
+ raise RuntimeError(f"Unexpected response structure from JSONBin API: {data}")
112
+ except urllib.error.HTTPError as e:
113
+ if e.code == 401:
114
+ # 401 Unauthorized typically means missing or invalid API key
115
+ jsonbin_api_key = os.environ.get('JSONBIN_API_KEY')
116
+ if not jsonbin_api_key:
117
+ raise RuntimeError("Failed to fetch valid public keys from jsonbin.io: JSONBIN_API_KEY environment variable is missing")
118
+ else:
119
+ raise RuntimeError(f"Failed to fetch valid public keys from jsonbin.io: Invalid API key provided (HTTP 401). Check your JSONBIN_API_KEY environment variable.")
120
+ raise RuntimeError(f"Failed to fetch valid public keys from jsonbin.io: HTTP {e.code} error - {e.reason}")
121
+ except urllib.error.URLError:
122
+ # Re-raise URLError so the retry decorator can handle it
123
+ raise
124
+ except Exception as e:
125
+ # Wrap all other exceptions in RuntimeError
126
+ raise RuntimeError(f"Failed to fetch valid public keys: {e}")
127
+
128
+ def compute_public_key(self, secret_key):
129
+ """Compute the public key from the secret key using argon2id."""
130
+ try:
131
+ return self.PH.hash(secret_key)
132
+ except HashingError as e:
133
+ raise RuntimeError(f"Failed to compute public key: {e}")
134
+
135
+ def validate_api_key(self, secret_key, service, public_keys_url=None, verbose=True, jsonbin_api_key=None):
136
+ """Validate an API key for a specific service"""
137
+ # Fast fail validations
138
+ if not secret_key:
139
+ raise PermissionError("API key validation failed: API key cannot be empty")
140
+
141
+ # Check if secret key has correct length
142
+ expected_length = 43 # Based on test secret key: LVYYllZk6xLIRoGVfGK9A78Dl1ehJbZOpRo6JoF2LAM
143
+ if len(secret_key) != expected_length:
144
+ raise PermissionError(f"API key validation failed: API key must be {expected_length} characters long")
145
+
146
+ # Check cache first
147
+ cache_key = (secret_key, service)
148
+ current_time = time.time()
149
+
150
+ if cache_key in self.API_KEY_CACHE:
151
+ expiration_time = self.API_KEY_CACHE[cache_key]
152
+ if current_time < expiration_time:
153
+ # Cache hit: key-service pair is still valid
154
+ if verbose:
155
+ print(f"✓ API key validation passed (cached until {time.ctime(expiration_time)})")
156
+ return True
157
+
158
+ # Cache miss or expired: proceed with full validation
159
+ if verbose:
160
+ print(f"⟳ Checking API key validity for service '{service}'...")
161
+
162
+ # Fetch valid public keys
163
+ url = public_keys_url or self.DEFAULT_URL
164
+ if verbose:
165
+ print(" Checking public keys from remote server...")
166
+
167
+ all_valid_keys = self.fetch_valid_public_keys(url, verbose=verbose, jsonbin_api_key=jsonbin_api_key)
168
+
169
+ # Filter keys by the specified service
170
+ valid_public_keys = [key['public_key'] for key in all_valid_keys if key['service'] == service]
171
+
172
+ # Check if service exists
173
+ if not valid_public_keys:
174
+ # Check if the service name exists in the fetched keys
175
+ available_services = set(key['service'] for key in all_valid_keys)
176
+ if service not in available_services:
177
+ # Find similar service names to suggest
178
+ def levenshtein_distance(s1, s2):
179
+ """Compute the Levenshtein distance between two strings"""
180
+ if len(s1) < len(s2):
181
+ return levenshtein_distance(s2, s1)
182
+ if len(s2) == 0:
183
+ return len(s1)
184
+
185
+ previous_row = range(len(s2) + 1)
186
+ for i, c1 in enumerate(s1):
187
+ current_row = [i + 1]
188
+ for j, c2 in enumerate(s2):
189
+ insertions = previous_row[j + 1] + 1
190
+ deletions = current_row[j] + 1
191
+ substitutions = previous_row[j] + (0 if c1 == c2 else 1)
192
+ current_row.append(min(insertions, deletions, substitutions))
193
+ previous_row = current_row
194
+ return previous_row[-1]
195
+
196
+ # Suggest services with Levenshtein distance <= 2
197
+ similar_services = sorted(
198
+ [s for s in available_services if levenshtein_distance(service.lower(), s.lower()) <= 2],
199
+ key=lambda s: levenshtein_distance(service.lower(), s.lower())
200
+ )
201
+
202
+ if similar_services:
203
+ raise PermissionError(
204
+ f"API key validation failed: Service '{service}' does not exist. Did you mean: {', '.join(similar_services)}?"
205
+ )
206
+ else:
207
+ raise PermissionError(
208
+ f"API key validation failed: Service '{service}' does not exist."
209
+ )
210
+
211
+ # Compute public key from secret key
212
+ try:
213
+ computed_public_key = self.compute_public_key(secret_key)
214
+ except RuntimeError as e:
215
+ raise RuntimeError(f"API key validation failed: {e}")
216
+
217
+ # Check if any of the valid public keys match the secret key
218
+ valid = False
219
+ for public_key in valid_public_keys:
220
+ try:
221
+ if self.PH.verify(public_key, secret_key):
222
+ valid = True
223
+ break
224
+ except Exception:
225
+ # If verification fails for any reason, try next one
226
+ continue
227
+
228
+ if not valid:
229
+ raise PermissionError(
230
+ "API key validation failed: API key is not correct for the specified service. Please apply for a valid API key from yanru@cyanru.com"
231
+ )
232
+
233
+ # Update cache with valid key-service pair
234
+ self.API_KEY_CACHE[cache_key] = current_time + self.CACHE_TTL_SECONDS
235
+ expiration_time = self.API_KEY_CACHE[cache_key]
236
+
237
+ if verbose:
238
+ print(f"✓ API key validation passed for service '{service}'")
239
+ print(f" Caching result until {time.ctime(expiration_time)}")
240
+
241
+ return True
242
+
243
+
244
+ # Create singleton instance
245
+ gateway = APIKeyGateway()
@@ -0,0 +1,71 @@
1
+ """Helper functions for the API key gateway"""
2
+
3
+ from .gateway import gateway
4
+
5
+
6
+ # Expose some constants and helper functions for backward compatibility
7
+ CACHE_TTL_SECONDS = gateway.CACHE_TTL_SECONDS
8
+ API_KEY_CACHE = gateway.API_KEY_CACHE
9
+ PH = gateway.PH
10
+
11
+
12
+ def retry_on_exception(exceptions, max_retries=3, delay=0.5, backoff=1):
13
+ """
14
+ Decorator that retries a function on specific exceptions.
15
+
16
+ :param exceptions: Tuple of exceptions to retry on
17
+ :param max_retries: Maximum number of retries (default: 3)
18
+ :param delay: Initial delay between retries in seconds (default: 0.5)
19
+ :param backoff: Factor by which delay increases each retry (default: 1 for constant delay)
20
+ """
21
+ from .gateway import APIKeyGateway
22
+ return APIKeyGateway.retry_on_exception(exceptions, max_retries, delay, backoff)
23
+
24
+
25
+ def fetch_valid_public_keys(url, verbose=True):
26
+ """Fetch the list of valid public keys from the specified URL."""
27
+ return gateway.fetch_valid_public_keys(url, verbose=verbose)
28
+
29
+
30
+ def compute_public_key(secret_key):
31
+ """Compute the public key from the secret key using argon2id."""
32
+ return gateway.compute_public_key(secret_key)
33
+
34
+
35
+ def validate_api_key(secret_key, service, public_keys_url=None, verbose=True, jsonbin_api_key=None):
36
+ """Validate an API key for a specific service"""
37
+ return gateway.validate_api_key(secret_key, service, public_keys_url=public_keys_url, verbose=verbose, jsonbin_api_key=jsonbin_api_key)
38
+
39
+
40
+ def add_apikey_gateway_middleware(
41
+ app,
42
+ service: str,
43
+ public_keys_url: str | None = None,
44
+ api_key_headers: list[str] | None = None,
45
+ jsonbin_api_key_header: str | None = None,
46
+ verbose: bool = True
47
+ ):
48
+ """
49
+ Convenience function to add APIKeyGatewayMiddleware to a FastAPI app.
50
+
51
+ :param app: FastAPI app instance
52
+ :param service: Service name to validate against
53
+ :param public_keys_url: Custom URL for public keys
54
+ :param api_key_headers: List of headers to check for API key
55
+ :param jsonbin_api_key_header: Header name to check for JSONBin API key (default: X-JSONBIN-API-KEY)
56
+ :param verbose: Verbose logging
57
+ """
58
+ from .middleware import APIKeyGatewayMiddleware, FASTAPI_AVAILABLE
59
+
60
+ if not FASTAPI_AVAILABLE:
61
+ raise RuntimeError("FastAPI is not installed. Please install it with 'pip install fastapi'.")
62
+
63
+ # Add middleware directly to the app
64
+ app.add_middleware(
65
+ APIKeyGatewayMiddleware,
66
+ service=service,
67
+ public_keys_url=public_keys_url,
68
+ api_key_headers=api_key_headers,
69
+ jsonbin_api_key_header=jsonbin_api_key_header,
70
+ verbose=verbose
71
+ )
@@ -0,0 +1,124 @@
1
+ """FastAPI middleware for API key gateway validation"""
2
+
3
+ # Try to import FastAPI types for middleware (optional dependency)
4
+ try:
5
+ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
6
+ from starlette.requests import Request
7
+ from starlette.responses import Response, JSONResponse
8
+ from fastapi import FastAPI
9
+ FASTAPI_AVAILABLE = True
10
+ except ImportError:
11
+ # If FastAPI is not installed, middleware-related functionality will be disabled
12
+ FASTAPI_AVAILABLE = False
13
+ # Create mock classes for type hinting
14
+ class Request:
15
+ pass
16
+
17
+ class Response:
18
+ pass
19
+
20
+ class JSONResponse:
21
+ pass
22
+
23
+ class BaseHTTPMiddleware:
24
+ pass
25
+
26
+ class RequestResponseEndpoint:
27
+ pass
28
+
29
+ class FastAPI:
30
+ pass
31
+
32
+
33
+ class APIKeyGatewayMiddleware(BaseHTTPMiddleware):
34
+ """
35
+ FastAPI middleware for API key gateway validation.
36
+
37
+ Best practice:
38
+ - Validates API keys for all incoming requests to protected routes
39
+ - Extracts API key from configurable headers (default: X-API-Key, Authorization Bearer)
40
+ - Supports service-specific validation
41
+ - Handles proper error responses with appropriate status codes
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ app: FastAPI,
47
+ service: str,
48
+ public_keys_url: str | None = None,
49
+ api_key_headers: list[str] | None = None,
50
+ jsonbin_api_key_header: str | None = None,
51
+ verbose: bool = True
52
+ ):
53
+ """
54
+ Initialize the API key gateway middleware.
55
+
56
+ :param app: FastAPI app instance
57
+ :param service: Service name to validate against (required)
58
+ :param public_keys_url: Custom URL for public keys (defaults to gateway's DEFAULT_URL)
59
+ :param api_key_headers: List of headers to check for API key (default: X-AKGATEWAY-API-KEY, Authorization Bearer)
60
+ :param jsonbin_api_key_header: Header name to check for JSONBin API key (default: X-JSONBIN-API-KEY)
61
+ :param verbose: Verbose logging
62
+ """
63
+ super().__init__(app)
64
+ from .gateway import gateway
65
+ self.service = service
66
+ self.public_keys_url = public_keys_url
67
+ self.api_key_headers = api_key_headers or [
68
+ "x-akgateway-api-key",
69
+ "authorization" # for Bearer token format
70
+ ]
71
+ self.jsonbin_api_key_header = jsonbin_api_key_header or "x-jsonbin-api-key"
72
+ self.verbose = verbose
73
+ self.gateway_instance = gateway
74
+
75
+
76
+ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
77
+ """
78
+ Middleware dispatch method that handles API key validation.
79
+ """
80
+ # Extract API key from configured headers
81
+ headers = request.headers
82
+
83
+ # Extract secret API key
84
+ secret_key = None
85
+ for header_name in self.api_key_headers:
86
+ header_value = headers.get(header_name)
87
+ if header_value:
88
+ if header_name.lower() == "authorization":
89
+ # Handle Bearer token format
90
+ if header_value.lower().startswith("bearer "):
91
+ secret_key = header_value[7:].strip()
92
+ break
93
+ else:
94
+ # Handle direct header value
95
+ secret_key = header_value.strip()
96
+ break
97
+
98
+ # Extract JSONBin API key from header if present
99
+ jsonbin_api_key = headers.get(self.jsonbin_api_key_header)
100
+
101
+ try:
102
+ # Validate the API key using the gateway instance
103
+ if secret_key:
104
+ self.gateway_instance.validate_api_key(
105
+ secret_key=secret_key,
106
+ service=self.service,
107
+ public_keys_url=self.public_keys_url,
108
+ verbose=self.verbose,
109
+ jsonbin_api_key=jsonbin_api_key
110
+ )
111
+ else:
112
+ # No API key found in headers
113
+ raise PermissionError("API key validation failed: No API key found in request headers")
114
+
115
+ # If validation passes, call the next endpoint
116
+ response = await call_next(request)
117
+ return response
118
+
119
+ except (PermissionError, RuntimeError) as e:
120
+ # Return appropriate error response
121
+ return JSONResponse(
122
+ status_code=401 if isinstance(e, PermissionError) else 500,
123
+ content={"error": str(e)}
124
+ )
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: apikey-gateway
3
+ Version: 1.1.1
4
+ Summary: A Python library for API key validation
5
+ Project-URL: Homepage, https://github.com/yanrucheng/apikey-gateway
6
+ Project-URL: Bug Tracker, https://github.com/yanrucheng/apikey-gateway/issues
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: argon2-cffi>=25.1.0
13
+ Requires-Dist: pytest>=8.3.5
14
+
15
+ # API Key Gateway
16
+
17
+ A Python library that provides an `@apikey_login` decorator to validate API keys with service-aware authentication.
18
+
19
+ ## Features
20
+
21
+ - **CLI Decorator**: Automatically adds `--apikey/-a` CLI parameter to decorated functions
22
+ - **FastAPI Middleware**: Built-in support for FastAPI applications
23
+ - **Strong Security**: Validates API keys using argon2id hashing algorithm
24
+ - **Service-Aware**: Keys are scoped to specific services
25
+ - **Remote Key Management**: Fetches valid public keys from a remote JSON endpoint
26
+ - **Caching**: Caches valid API key-service pairs for improved performance
27
+ - **Retry Mechanism**: Automatically retries fetching keys on network failures
28
+ - **Flexible Configuration**: Supports custom API key URLs and authentication headers
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ uv install apikey-gateway
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ The library supports two usage modes: **CLI Decorator** and **FastAPI Middleware**.
39
+
40
+ ### CLI Decorator
41
+ The `apikey_login` decorator automatically adds `--apikey/-a` CLI parameter to decorated functions.
42
+
43
+ ```python
44
+ from apikey_gateway import apikey_login
45
+
46
+ @apikey_login(service="media-match")
47
+ def media_app():
48
+ print("API key validated for media-match service!")
49
+ # Your media application logic here
50
+
51
+ @apikey_login(service="analytics")
52
+ def analytics_app():
53
+ print("API key validated for analytics service!")
54
+ # Your analytics application logic here
55
+
56
+ if __name__ == "__main__":
57
+ media_app() # or analytics_app()
58
+ ```
59
+
60
+ Run with:
61
+ ```bash
62
+ python app.py --apikey your-secret-key
63
+ ```
64
+
65
+ ### FastAPI Middleware
66
+ The library provides built-in FastAPI middleware for validating API keys on all requests.
67
+
68
+ ```python
69
+ from fastapi import FastAPI
70
+ from apikey_gateway import add_apikey_gateway_middleware
71
+
72
+ # Create your FastAPI app
73
+ app = FastAPI(title="My API")
74
+
75
+ # Add API key validation middleware
76
+ add_apikey_gateway_middleware(
77
+ app=app,
78
+ service="my-service",
79
+ verbose=True
80
+ )
81
+
82
+ # All endpoints below will require API key validation
83
+ @app.get("/protected/resource")
84
+ def protected_resource():
85
+ return {"message": "Access granted to protected resource"}
86
+
87
+ # To run: uvicorn app:app --reload
88
+ ```
89
+
90
+ Test the FastAPI endpoint:
91
+ ```bash
92
+ # With X-AKGATEWAY-API-KEY header (default)
93
+ curl -H "X-AKGATEWAY-API-KEY: your-secret-key" http://localhost:8000/protected/resource
94
+
95
+ # With Authorization Bearer header
96
+ curl -H "Authorization: Bearer your-secret-key" http://localhost:8000/protected/resource
97
+ ```
98
+
99
+ Middleware Features:
100
+ - Automatically checks `X-AKGATEWAY-API-KEY` and `Authorization Bearer` headers
101
+ - Supports custom API key headers
102
+ - Can be configured with a custom public keys URL
103
+ - Supports `X-JSONBIN-API-KEY` header for JSONBin API key authentication
104
+ - Allows customization of both API key header and JSONBin API key header names
105
+
106
+ ## How It Works
107
+
108
+ ### For CLI Applications
109
+ 1. The application specifies the `service` name when using the `@apikey_login` decorator
110
+ 2. The user provides a secret API key via the `--apikey/-a` CLI parameter
111
+ 3. Followed by the same validation steps as FastAPI applications...
112
+
113
+ ### For FastAPI Applications
114
+ 1. The application adds the middleware with a specific `service` name
115
+ 2. The client provides a secret API key via `X-AKGATEWAY-API-KEY` or `Authorization Bearer` header
116
+ 3. Followed by the same validation steps as CLI applications...
117
+
118
+ ### Common Validation Steps
119
+ 3. The library computes an argon2id hash (public key) from the secret key
120
+ 4. It fetches the list of valid public keys from `https://api.jsonbin.io/v3/b/691ec6a543b1c97be9b8ea6d`
121
+ 5. Valid keys are filtered to only those belonging to the specified service
122
+ 6. If the computed public key matches any valid service-specific public key, access is granted
123
+
124
+ ## JSON Format
125
+
126
+ The remote JSON follows a service-aware structure where keys are organized by service name:
127
+
128
+ ### Service-Aware Structure
129
+ ```json
130
+ {
131
+ "service1": {
132
+ "key_id_1": "argon2id_hash_here",
133
+ "key_id_2": "another_hash_here"
134
+ },
135
+ "service2": "single_key_hash_here"
136
+ }
137
+ ```
138
+
139
+ - Top-level keys are service names
140
+ - Each service can have either multiple keys (as a dictionary) or a single key (as a string)
141
+ - The library automatically handles both formats when fetching keys
142
+
143
+ ## License
144
+
145
+ MIT
@@ -0,0 +1,9 @@
1
+ apikey_gateway/__init__.py,sha256=wik-nyj1clngVzzmK244hzvZqzAyx8oTmB932byH8aU,1207
2
+ apikey_gateway/decorators.py,sha256=7K8khH0t6NOAWNvaVVtOlCWSJBXe8ygK4p7pd8bSYvs,2555
3
+ apikey_gateway/gateway.py,sha256=lNz5Wua3_Jn-kNqBWiMvNxR3Dkta1XPrgSPMzFmRbwg,10533
4
+ apikey_gateway/helpers.py,sha256=91Snz_LZsCOOgpPcQdhZDd9-ZJanYXtKPDtgoME-jdc,2616
5
+ apikey_gateway/middleware.py,sha256=ym-zNMukNkx9kNulYroMCe7F9Dna6BzpxL7gKu5wZt8,4456
6
+ apikey_gateway-1.1.1.dist-info/METADATA,sha256=uuU93HScsmFy0IyGPOw8nzNEnQhZ_STZMVhf49F_f8Q,4709
7
+ apikey_gateway-1.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ apikey_gateway-1.1.1.dist-info/top_level.txt,sha256=nwreq0PaHh4WpaMOluQ3V5iA1LP8y9GliflzObT7G1I,15
9
+ apikey_gateway-1.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ apikey_gateway