auth-gate 0.2.2__tar.gz → 0.3.0__tar.gz
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.
- {auth_gate-0.2.2/src/auth_gate.egg-info → auth_gate-0.3.0}/PKG-INFO +205 -18
- {auth_gate-0.2.2 → auth_gate-0.3.0}/README.md +203 -16
- {auth_gate-0.2.2 → auth_gate-0.3.0}/pyproject.toml +2 -2
- {auth_gate-0.2.2 → auth_gate-0.3.0}/setup.cfg +1 -1
- auth_gate-0.3.0/src/auth_gate/__init__.py +148 -0
- auth_gate-0.3.0/src/auth_gate/exceptions.py +58 -0
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/auth_gate/fastapi_utils.py +264 -2
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/auth_gate/middleware.py +198 -6
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/auth_gate/schemas.py +56 -1
- auth_gate-0.3.0/src/auth_gate/subscription.py +126 -0
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/auth_gate/user_auth.py +48 -1
- {auth_gate-0.2.2 → auth_gate-0.3.0/src/auth_gate.egg-info}/PKG-INFO +205 -18
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/auth_gate.egg-info/SOURCES.txt +3 -0
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/tests/conftest.py +111 -0
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/tests/test_intergration.py +2 -2
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/tests/test_middleware.py +344 -1
- auth_gate-0.3.0/src/tests/test_subscription.py +237 -0
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/tests/test_user_auth.py +1 -1
- auth_gate-0.2.2/src/auth_gate/__init__.py +0 -67
- {auth_gate-0.2.2 → auth_gate-0.3.0}/LICENSE +0 -0
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/auth_gate/config.py +0 -0
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/auth_gate/s2s_auth.py +0 -0
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/auth_gate.egg-info/dependency_links.txt +0 -0
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/auth_gate.egg-info/requires.txt +0 -0
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/auth_gate.egg-info/top_level.txt +0 -0
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/tests/__init__.py +0 -0
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/tests/test_config.py +0 -0
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/tests/test_fastapi_utils.py +0 -0
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/tests/test_s2s_auth.py +0 -0
- {auth_gate-0.2.2 → auth_gate-0.3.0}/src/tests/test_schema.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: auth-gate
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Enterprise-grade authentication for microservices with Kong and
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Enterprise-grade authentication for microservices with Kong/Keycloak integration and subscription tier support
|
|
5
5
|
Home-page: https://github.com/tradelink-org/auth-gate
|
|
6
6
|
Author: Brian Mburu
|
|
7
7
|
Author-email: Brian Mburu <brian.mburu@students.jkuat.ac.ke>
|
|
@@ -41,12 +41,13 @@ Dynamic: license-file
|
|
|
41
41
|
|
|
42
42
|
# Auth Gate
|
|
43
43
|
|
|
44
|
-
Enterprise-grade authentication for microservices with Kong and Keycloak integration, supporting
|
|
44
|
+
Enterprise-grade authentication for microservices with Kong and Keycloak integration, supporting user authentication, service-to-service authentication, and subscription tier enforcement.
|
|
45
45
|
|
|
46
46
|
## Features
|
|
47
47
|
|
|
48
48
|
- **Dual authentication types**: Support for both user authentication and service-to-service authentication
|
|
49
49
|
- **Unified authentication flow**: Single middleware handles both user and service tokens seamlessly
|
|
50
|
+
- **Subscription tier enforcement**: Built-in support for FREE, BASIC, PROFESSIONAL, and ENTERPRISE tiers
|
|
50
51
|
- **Flexible endpoint protection**: Configure endpoints as user-only, service-only, or accessible by both
|
|
51
52
|
- **Dual-mode authentication**: Support for both Kong header-based auth (production) and direct Keycloak validation (development)
|
|
52
53
|
- **Service-to-service authentication**: Built-in client credentials flow for secure inter-service communication
|
|
@@ -55,6 +56,7 @@ Enterprise-grade authentication for microservices with Kong and Keycloak integra
|
|
|
55
56
|
- **FastAPI integration**: Ready-to-use dependencies for protecting endpoints
|
|
56
57
|
- **Role-based access control**: Fine-grained permission management with role and scope validation for both users and services
|
|
57
58
|
- **Middleware support**: Automatic request authentication with configurable exclusions
|
|
59
|
+
- **Organization context**: Multi-tenant support with organization ID tracking
|
|
58
60
|
|
|
59
61
|
## Installation
|
|
60
62
|
|
|
@@ -136,6 +138,44 @@ async def get_data(auth: AuthContext = Depends(get_current_auth)):
|
|
|
136
138
|
return {"data": "full", "service": auth.service_name}
|
|
137
139
|
```
|
|
138
140
|
|
|
141
|
+
### Subscription Tier Enforcement
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from auth_gate import (
|
|
145
|
+
require_tier,
|
|
146
|
+
require_tier_and_active,
|
|
147
|
+
require_basic,
|
|
148
|
+
require_professional,
|
|
149
|
+
require_enterprise,
|
|
150
|
+
require_paid_subscription,
|
|
151
|
+
SubscriptionTier,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Require minimum tier (PROFESSIONAL or higher)
|
|
155
|
+
@app.get("/api/analytics/advanced")
|
|
156
|
+
async def get_advanced_analytics(
|
|
157
|
+
auth: AuthContext = Depends(require_tier(SubscriptionTier.PROFESSIONAL))
|
|
158
|
+
):
|
|
159
|
+
return {"data": "advanced analytics"}
|
|
160
|
+
|
|
161
|
+
# Convenience dependency for common tiers
|
|
162
|
+
@app.get("/api/reports/enterprise")
|
|
163
|
+
async def get_enterprise_reports(auth: AuthContext = Depends(require_enterprise)):
|
|
164
|
+
return {"reports": [...]}
|
|
165
|
+
|
|
166
|
+
# Require both minimum tier AND active subscription
|
|
167
|
+
@app.get("/api/premium/dashboard")
|
|
168
|
+
async def get_premium_dashboard(
|
|
169
|
+
auth: AuthContext = Depends(require_tier_and_active(SubscriptionTier.BASIC))
|
|
170
|
+
):
|
|
171
|
+
return {"dashboard": "premium data"}
|
|
172
|
+
|
|
173
|
+
# Require any paid subscription (non-free)
|
|
174
|
+
@app.get("/api/paid-feature")
|
|
175
|
+
async def get_paid_feature(auth: AuthContext = Depends(require_paid_subscription)):
|
|
176
|
+
return {"feature": "paid-only data"}
|
|
177
|
+
```
|
|
178
|
+
|
|
139
179
|
### Role-Based Access Control
|
|
140
180
|
|
|
141
181
|
```python
|
|
@@ -214,6 +254,16 @@ VERIFY_HMAC=false
|
|
|
214
254
|
INTERNAL_HMAC_KEY=your-hmac-key
|
|
215
255
|
```
|
|
216
256
|
|
|
257
|
+
### Kong Headers for Subscription
|
|
258
|
+
|
|
259
|
+
When using Kong, configure the Token Introspector plugin to inject these subscription headers:
|
|
260
|
+
|
|
261
|
+
| Header | Description | Example Values |
|
|
262
|
+
| ----------------------- | ------------------------------ | --------------------------------------- |
|
|
263
|
+
| `X-Subscription-Tier` | User's subscription tier | `free`, `basic`, `professional`, `enterprise` |
|
|
264
|
+
| `X-Subscription-Status` | Subscription status | `active`, `suspended`, `cancelled`, `past_due` |
|
|
265
|
+
| `X-Organization-ID` | Organization identifier | `org-12345` |
|
|
266
|
+
|
|
217
267
|
## Service-to-Service Authentication
|
|
218
268
|
|
|
219
269
|
### Making Service Calls
|
|
@@ -290,6 +340,44 @@ app.add_middleware(
|
|
|
290
340
|
)
|
|
291
341
|
```
|
|
292
342
|
|
|
343
|
+
### Parameterized Paths with UUID Matching
|
|
344
|
+
|
|
345
|
+
You can exclude or make paths optional using UUID v4 parameters:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
app.add_middleware(
|
|
349
|
+
AuthMiddleware,
|
|
350
|
+
excluded_paths={
|
|
351
|
+
"/api/v1/categories/{category_id:uuid}": {"GET"}, # Public read
|
|
352
|
+
"/api/v1/products/{product_id:uuid}": {"GET"},
|
|
353
|
+
},
|
|
354
|
+
excluded_prefixes={
|
|
355
|
+
"/api/{version:uuid}": {"GET"}, # Version-specific docs
|
|
356
|
+
},
|
|
357
|
+
optional_auth_paths={
|
|
358
|
+
"/api/v1/recommendations/{user_id:uuid}": {"GET"}, # Personalized if authenticated
|
|
359
|
+
}
|
|
360
|
+
)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**Pattern Syntax:**
|
|
364
|
+
- `{param:uuid}` - Matches valid UUID v4 format (case-insensitive)
|
|
365
|
+
- Works with exact paths, prefixes, and optional auth paths
|
|
366
|
+
- Supports method-specific exclusions
|
|
367
|
+
- Exact matches take precedence over patterns
|
|
368
|
+
|
|
369
|
+
**Example Behavior:**
|
|
370
|
+
```python
|
|
371
|
+
# Matches: /api/v1/categories/7b5bcc8f-2c99-43c0-9c7d-e27c10881bd2
|
|
372
|
+
# Does not match: /api/v1/categories/invalid-id
|
|
373
|
+
# Does not match: /api/v1/categories/all
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**UUID v4 Validation:**
|
|
377
|
+
- Must have version digit "4" in the correct position
|
|
378
|
+
- Must have variant bits (8, 9, a, or b) in the correct position
|
|
379
|
+
- Accepts uppercase, lowercase, or mixed case
|
|
380
|
+
|
|
293
381
|
### Direct Validator Usage
|
|
294
382
|
|
|
295
383
|
```python
|
|
@@ -341,14 +429,17 @@ The S2S auth client includes automatic circuit breaker protection:
|
|
|
341
429
|
|
|
342
430
|
```python
|
|
343
431
|
class UserContext:
|
|
344
|
-
user_id: str
|
|
345
|
-
username: str | None
|
|
346
|
-
email: str | None
|
|
347
|
-
roles: List[str]
|
|
348
|
-
scopes: List[str]
|
|
349
|
-
session_id: str | None
|
|
350
|
-
client_id: str | None
|
|
351
|
-
auth_source: str
|
|
432
|
+
user_id: str # Unique user identifier
|
|
433
|
+
username: str | None # Username
|
|
434
|
+
email: str | None # Email address
|
|
435
|
+
roles: List[str] # User roles
|
|
436
|
+
scopes: List[str] # OAuth scopes
|
|
437
|
+
session_id: str | None # Session identifier
|
|
438
|
+
client_id: str | None # OAuth client ID
|
|
439
|
+
auth_source: str # Authentication source
|
|
440
|
+
organization_id: str | None # Organization identifier
|
|
441
|
+
subscription_tier: SubscriptionTier # Subscription tier (FREE, BASIC, PROFESSIONAL, ENTERPRISE)
|
|
442
|
+
subscription_status: SubscriptionStatus # Status (ACTIVE, SUSPENDED, CANCELLED, PAST_DUE)
|
|
352
443
|
|
|
353
444
|
# Properties
|
|
354
445
|
is_service: bool # Always False for users
|
|
@@ -356,6 +447,9 @@ class UserContext:
|
|
|
356
447
|
is_supplier: bool # True if has supplier role
|
|
357
448
|
is_customer: bool # True if has customer role
|
|
358
449
|
is_moderator: bool # True if has moderator role
|
|
450
|
+
is_buyer: bool # True if has buyer or customer role
|
|
451
|
+
is_subscription_active: bool # True if subscription status is ACTIVE
|
|
452
|
+
is_paid_subscriber: bool # True if tier is not FREE
|
|
359
453
|
|
|
360
454
|
# Methods
|
|
361
455
|
has_role(role: str) -> bool
|
|
@@ -363,18 +457,23 @@ class UserContext:
|
|
|
363
457
|
has_all_roles(roles: List[str]) -> bool
|
|
364
458
|
has_scope(scope: str) -> bool
|
|
365
459
|
has_any_scope(scopes: List[str]) -> bool
|
|
460
|
+
has_minimum_tier(required_tier: SubscriptionTier) -> bool
|
|
461
|
+
can_access_feature(required_tier: SubscriptionTier) -> bool
|
|
366
462
|
```
|
|
367
463
|
|
|
368
464
|
### ServiceContext
|
|
369
465
|
|
|
370
466
|
```python
|
|
371
467
|
class ServiceContext:
|
|
372
|
-
service_name: str
|
|
373
|
-
service_id: str | None
|
|
374
|
-
roles: List[str]
|
|
375
|
-
session_id: str | None
|
|
376
|
-
client_id: str | None
|
|
377
|
-
auth_source: str
|
|
468
|
+
service_name: str # Service identifier (client_id)
|
|
469
|
+
service_id: str | None # Service sub claim
|
|
470
|
+
roles: List[str] # Service roles
|
|
471
|
+
session_id: str | None # Session identifier
|
|
472
|
+
client_id: str | None # OAuth client ID
|
|
473
|
+
auth_source: str # Authentication source
|
|
474
|
+
organization_id: str | None # Organization identifier
|
|
475
|
+
subscription_tier: SubscriptionTier # Defaults to FREE (services bypass tier checks)
|
|
476
|
+
subscription_status: SubscriptionStatus # Defaults to ACTIVE
|
|
378
477
|
|
|
379
478
|
# Properties
|
|
380
479
|
is_service: bool # Always True for services
|
|
@@ -386,6 +485,8 @@ class ServiceContext:
|
|
|
386
485
|
has_all_roles(roles: List[str]) -> bool
|
|
387
486
|
```
|
|
388
487
|
|
|
488
|
+
**Note:** Services bypass subscription tier checks by default when using `require_tier()` and related dependencies.
|
|
489
|
+
|
|
389
490
|
## Available Dependencies
|
|
390
491
|
|
|
391
492
|
### Authentication Dependencies
|
|
@@ -424,6 +525,59 @@ require_service_roles("role1", "role2", ...)
|
|
|
424
525
|
require_scopes("scope1", "scope2", ...)
|
|
425
526
|
```
|
|
426
527
|
|
|
528
|
+
### Subscription Dependencies
|
|
529
|
+
|
|
530
|
+
| Dependency | Description |
|
|
531
|
+
| ----------------------------- | --------------------------------------------------- |
|
|
532
|
+
| `require_tier(tier)` | Factory requiring minimum subscription tier |
|
|
533
|
+
| `require_active_subscription()` | Factory requiring active subscription status |
|
|
534
|
+
| `require_tier_and_active(tier)` | Factory requiring both tier and active status |
|
|
535
|
+
| `require_basic` | Requires BASIC tier or higher |
|
|
536
|
+
| `require_professional` | Requires PROFESSIONAL tier or higher |
|
|
537
|
+
| `require_enterprise` | Requires ENTERPRISE tier |
|
|
538
|
+
| `require_paid_subscription` | Requires any paid tier (non-FREE) |
|
|
539
|
+
| `get_subscription_tier` | Extract tier from header |
|
|
540
|
+
| `get_organization_id` | Extract organization ID from header |
|
|
541
|
+
|
|
542
|
+
### Subscription Types
|
|
543
|
+
|
|
544
|
+
```python
|
|
545
|
+
from auth_gate import SubscriptionTier, SubscriptionStatus
|
|
546
|
+
|
|
547
|
+
# Tier hierarchy (lowest to highest)
|
|
548
|
+
SubscriptionTier.FREE
|
|
549
|
+
SubscriptionTier.BASIC
|
|
550
|
+
SubscriptionTier.PROFESSIONAL
|
|
551
|
+
SubscriptionTier.ENTERPRISE
|
|
552
|
+
|
|
553
|
+
# Subscription statuses
|
|
554
|
+
SubscriptionStatus.ACTIVE
|
|
555
|
+
SubscriptionStatus.SUSPENDED
|
|
556
|
+
SubscriptionStatus.CANCELLED
|
|
557
|
+
SubscriptionStatus.PAST_DUE
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### Subscription Utilities
|
|
561
|
+
|
|
562
|
+
```python
|
|
563
|
+
from auth_gate import (
|
|
564
|
+
meets_minimum_tier,
|
|
565
|
+
compare_tiers,
|
|
566
|
+
is_paid_tier,
|
|
567
|
+
get_tier_level,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
# Check if user tier meets requirement
|
|
571
|
+
meets_minimum_tier(SubscriptionTier.PROFESSIONAL, SubscriptionTier.BASIC) # True
|
|
572
|
+
|
|
573
|
+
# Compare tiers (-1, 0, 1)
|
|
574
|
+
compare_tiers(SubscriptionTier.ENTERPRISE, SubscriptionTier.FREE) # > 0
|
|
575
|
+
|
|
576
|
+
# Check if tier is paid
|
|
577
|
+
is_paid_tier(SubscriptionTier.BASIC) # True
|
|
578
|
+
is_paid_tier(SubscriptionTier.FREE) # False
|
|
579
|
+
```
|
|
580
|
+
|
|
427
581
|
## Migration Guide
|
|
428
582
|
|
|
429
583
|
### Updating Existing Applications
|
|
@@ -491,11 +645,15 @@ from auth_gate import (
|
|
|
491
645
|
AuthContext,
|
|
492
646
|
UserContext,
|
|
493
647
|
ServiceContext,
|
|
648
|
+
SubscriptionTier,
|
|
494
649
|
get_current_auth,
|
|
495
650
|
get_current_user,
|
|
496
651
|
get_current_service,
|
|
497
652
|
require_roles,
|
|
498
653
|
require_user_roles,
|
|
654
|
+
require_tier,
|
|
655
|
+
require_tier_and_active,
|
|
656
|
+
require_professional,
|
|
499
657
|
)
|
|
500
658
|
|
|
501
659
|
app = FastAPI()
|
|
@@ -514,7 +672,11 @@ async def health():
|
|
|
514
672
|
# User-only endpoint
|
|
515
673
|
@app.get("/api/user/profile")
|
|
516
674
|
async def get_profile(user: UserContext = Depends(get_current_user)):
|
|
517
|
-
return {
|
|
675
|
+
return {
|
|
676
|
+
"user": user.user_id,
|
|
677
|
+
"tier": user.subscription_tier.value,
|
|
678
|
+
"organization": user.organization_id
|
|
679
|
+
}
|
|
518
680
|
|
|
519
681
|
# Service-only endpoint
|
|
520
682
|
@app.post("/api/internal/batch")
|
|
@@ -535,6 +697,31 @@ require_user_editor = require_user_roles("editor")
|
|
|
535
697
|
@app.post("/api/articles")
|
|
536
698
|
async def create_article(user: UserContext = Depends(require_user_editor)):
|
|
537
699
|
return {"author": user.user_id}
|
|
700
|
+
|
|
701
|
+
# Tier-protected endpoint (PROFESSIONAL or higher)
|
|
702
|
+
@app.get("/api/analytics")
|
|
703
|
+
async def get_analytics(auth: AuthContext = Depends(require_professional)):
|
|
704
|
+
return {"analytics": "professional data"}
|
|
705
|
+
|
|
706
|
+
# Tier and active subscription required
|
|
707
|
+
@app.get("/api/premium/reports")
|
|
708
|
+
async def get_premium_reports(
|
|
709
|
+
auth: AuthContext = Depends(require_tier_and_active(SubscriptionTier.BASIC))
|
|
710
|
+
):
|
|
711
|
+
return {"reports": "premium data"}
|
|
712
|
+
|
|
713
|
+
# Custom tier check within endpoint
|
|
714
|
+
@app.get("/api/features")
|
|
715
|
+
async def get_features(user: UserContext = Depends(get_current_user)):
|
|
716
|
+
features = ["basic_dashboard"]
|
|
717
|
+
|
|
718
|
+
if user.has_minimum_tier(SubscriptionTier.PROFESSIONAL):
|
|
719
|
+
features.append("advanced_analytics")
|
|
720
|
+
|
|
721
|
+
if user.has_minimum_tier(SubscriptionTier.ENTERPRISE):
|
|
722
|
+
features.append("custom_integrations")
|
|
723
|
+
|
|
724
|
+
return {"features": features, "tier": user.subscription_tier.value}
|
|
538
725
|
```
|
|
539
726
|
|
|
540
727
|
## License
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# Auth Gate
|
|
2
2
|
|
|
3
|
-
Enterprise-grade authentication for microservices with Kong and Keycloak integration, supporting
|
|
3
|
+
Enterprise-grade authentication for microservices with Kong and Keycloak integration, supporting user authentication, service-to-service authentication, and subscription tier enforcement.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Dual authentication types**: Support for both user authentication and service-to-service authentication
|
|
8
8
|
- **Unified authentication flow**: Single middleware handles both user and service tokens seamlessly
|
|
9
|
+
- **Subscription tier enforcement**: Built-in support for FREE, BASIC, PROFESSIONAL, and ENTERPRISE tiers
|
|
9
10
|
- **Flexible endpoint protection**: Configure endpoints as user-only, service-only, or accessible by both
|
|
10
11
|
- **Dual-mode authentication**: Support for both Kong header-based auth (production) and direct Keycloak validation (development)
|
|
11
12
|
- **Service-to-service authentication**: Built-in client credentials flow for secure inter-service communication
|
|
@@ -14,6 +15,7 @@ Enterprise-grade authentication for microservices with Kong and Keycloak integra
|
|
|
14
15
|
- **FastAPI integration**: Ready-to-use dependencies for protecting endpoints
|
|
15
16
|
- **Role-based access control**: Fine-grained permission management with role and scope validation for both users and services
|
|
16
17
|
- **Middleware support**: Automatic request authentication with configurable exclusions
|
|
18
|
+
- **Organization context**: Multi-tenant support with organization ID tracking
|
|
17
19
|
|
|
18
20
|
## Installation
|
|
19
21
|
|
|
@@ -95,6 +97,44 @@ async def get_data(auth: AuthContext = Depends(get_current_auth)):
|
|
|
95
97
|
return {"data": "full", "service": auth.service_name}
|
|
96
98
|
```
|
|
97
99
|
|
|
100
|
+
### Subscription Tier Enforcement
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from auth_gate import (
|
|
104
|
+
require_tier,
|
|
105
|
+
require_tier_and_active,
|
|
106
|
+
require_basic,
|
|
107
|
+
require_professional,
|
|
108
|
+
require_enterprise,
|
|
109
|
+
require_paid_subscription,
|
|
110
|
+
SubscriptionTier,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Require minimum tier (PROFESSIONAL or higher)
|
|
114
|
+
@app.get("/api/analytics/advanced")
|
|
115
|
+
async def get_advanced_analytics(
|
|
116
|
+
auth: AuthContext = Depends(require_tier(SubscriptionTier.PROFESSIONAL))
|
|
117
|
+
):
|
|
118
|
+
return {"data": "advanced analytics"}
|
|
119
|
+
|
|
120
|
+
# Convenience dependency for common tiers
|
|
121
|
+
@app.get("/api/reports/enterprise")
|
|
122
|
+
async def get_enterprise_reports(auth: AuthContext = Depends(require_enterprise)):
|
|
123
|
+
return {"reports": [...]}
|
|
124
|
+
|
|
125
|
+
# Require both minimum tier AND active subscription
|
|
126
|
+
@app.get("/api/premium/dashboard")
|
|
127
|
+
async def get_premium_dashboard(
|
|
128
|
+
auth: AuthContext = Depends(require_tier_and_active(SubscriptionTier.BASIC))
|
|
129
|
+
):
|
|
130
|
+
return {"dashboard": "premium data"}
|
|
131
|
+
|
|
132
|
+
# Require any paid subscription (non-free)
|
|
133
|
+
@app.get("/api/paid-feature")
|
|
134
|
+
async def get_paid_feature(auth: AuthContext = Depends(require_paid_subscription)):
|
|
135
|
+
return {"feature": "paid-only data"}
|
|
136
|
+
```
|
|
137
|
+
|
|
98
138
|
### Role-Based Access Control
|
|
99
139
|
|
|
100
140
|
```python
|
|
@@ -173,6 +213,16 @@ VERIFY_HMAC=false
|
|
|
173
213
|
INTERNAL_HMAC_KEY=your-hmac-key
|
|
174
214
|
```
|
|
175
215
|
|
|
216
|
+
### Kong Headers for Subscription
|
|
217
|
+
|
|
218
|
+
When using Kong, configure the Token Introspector plugin to inject these subscription headers:
|
|
219
|
+
|
|
220
|
+
| Header | Description | Example Values |
|
|
221
|
+
| ----------------------- | ------------------------------ | --------------------------------------- |
|
|
222
|
+
| `X-Subscription-Tier` | User's subscription tier | `free`, `basic`, `professional`, `enterprise` |
|
|
223
|
+
| `X-Subscription-Status` | Subscription status | `active`, `suspended`, `cancelled`, `past_due` |
|
|
224
|
+
| `X-Organization-ID` | Organization identifier | `org-12345` |
|
|
225
|
+
|
|
176
226
|
## Service-to-Service Authentication
|
|
177
227
|
|
|
178
228
|
### Making Service Calls
|
|
@@ -249,6 +299,44 @@ app.add_middleware(
|
|
|
249
299
|
)
|
|
250
300
|
```
|
|
251
301
|
|
|
302
|
+
### Parameterized Paths with UUID Matching
|
|
303
|
+
|
|
304
|
+
You can exclude or make paths optional using UUID v4 parameters:
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
app.add_middleware(
|
|
308
|
+
AuthMiddleware,
|
|
309
|
+
excluded_paths={
|
|
310
|
+
"/api/v1/categories/{category_id:uuid}": {"GET"}, # Public read
|
|
311
|
+
"/api/v1/products/{product_id:uuid}": {"GET"},
|
|
312
|
+
},
|
|
313
|
+
excluded_prefixes={
|
|
314
|
+
"/api/{version:uuid}": {"GET"}, # Version-specific docs
|
|
315
|
+
},
|
|
316
|
+
optional_auth_paths={
|
|
317
|
+
"/api/v1/recommendations/{user_id:uuid}": {"GET"}, # Personalized if authenticated
|
|
318
|
+
}
|
|
319
|
+
)
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Pattern Syntax:**
|
|
323
|
+
- `{param:uuid}` - Matches valid UUID v4 format (case-insensitive)
|
|
324
|
+
- Works with exact paths, prefixes, and optional auth paths
|
|
325
|
+
- Supports method-specific exclusions
|
|
326
|
+
- Exact matches take precedence over patterns
|
|
327
|
+
|
|
328
|
+
**Example Behavior:**
|
|
329
|
+
```python
|
|
330
|
+
# Matches: /api/v1/categories/7b5bcc8f-2c99-43c0-9c7d-e27c10881bd2
|
|
331
|
+
# Does not match: /api/v1/categories/invalid-id
|
|
332
|
+
# Does not match: /api/v1/categories/all
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**UUID v4 Validation:**
|
|
336
|
+
- Must have version digit "4" in the correct position
|
|
337
|
+
- Must have variant bits (8, 9, a, or b) in the correct position
|
|
338
|
+
- Accepts uppercase, lowercase, or mixed case
|
|
339
|
+
|
|
252
340
|
### Direct Validator Usage
|
|
253
341
|
|
|
254
342
|
```python
|
|
@@ -300,14 +388,17 @@ The S2S auth client includes automatic circuit breaker protection:
|
|
|
300
388
|
|
|
301
389
|
```python
|
|
302
390
|
class UserContext:
|
|
303
|
-
user_id: str
|
|
304
|
-
username: str | None
|
|
305
|
-
email: str | None
|
|
306
|
-
roles: List[str]
|
|
307
|
-
scopes: List[str]
|
|
308
|
-
session_id: str | None
|
|
309
|
-
client_id: str | None
|
|
310
|
-
auth_source: str
|
|
391
|
+
user_id: str # Unique user identifier
|
|
392
|
+
username: str | None # Username
|
|
393
|
+
email: str | None # Email address
|
|
394
|
+
roles: List[str] # User roles
|
|
395
|
+
scopes: List[str] # OAuth scopes
|
|
396
|
+
session_id: str | None # Session identifier
|
|
397
|
+
client_id: str | None # OAuth client ID
|
|
398
|
+
auth_source: str # Authentication source
|
|
399
|
+
organization_id: str | None # Organization identifier
|
|
400
|
+
subscription_tier: SubscriptionTier # Subscription tier (FREE, BASIC, PROFESSIONAL, ENTERPRISE)
|
|
401
|
+
subscription_status: SubscriptionStatus # Status (ACTIVE, SUSPENDED, CANCELLED, PAST_DUE)
|
|
311
402
|
|
|
312
403
|
# Properties
|
|
313
404
|
is_service: bool # Always False for users
|
|
@@ -315,6 +406,9 @@ class UserContext:
|
|
|
315
406
|
is_supplier: bool # True if has supplier role
|
|
316
407
|
is_customer: bool # True if has customer role
|
|
317
408
|
is_moderator: bool # True if has moderator role
|
|
409
|
+
is_buyer: bool # True if has buyer or customer role
|
|
410
|
+
is_subscription_active: bool # True if subscription status is ACTIVE
|
|
411
|
+
is_paid_subscriber: bool # True if tier is not FREE
|
|
318
412
|
|
|
319
413
|
# Methods
|
|
320
414
|
has_role(role: str) -> bool
|
|
@@ -322,18 +416,23 @@ class UserContext:
|
|
|
322
416
|
has_all_roles(roles: List[str]) -> bool
|
|
323
417
|
has_scope(scope: str) -> bool
|
|
324
418
|
has_any_scope(scopes: List[str]) -> bool
|
|
419
|
+
has_minimum_tier(required_tier: SubscriptionTier) -> bool
|
|
420
|
+
can_access_feature(required_tier: SubscriptionTier) -> bool
|
|
325
421
|
```
|
|
326
422
|
|
|
327
423
|
### ServiceContext
|
|
328
424
|
|
|
329
425
|
```python
|
|
330
426
|
class ServiceContext:
|
|
331
|
-
service_name: str
|
|
332
|
-
service_id: str | None
|
|
333
|
-
roles: List[str]
|
|
334
|
-
session_id: str | None
|
|
335
|
-
client_id: str | None
|
|
336
|
-
auth_source: str
|
|
427
|
+
service_name: str # Service identifier (client_id)
|
|
428
|
+
service_id: str | None # Service sub claim
|
|
429
|
+
roles: List[str] # Service roles
|
|
430
|
+
session_id: str | None # Session identifier
|
|
431
|
+
client_id: str | None # OAuth client ID
|
|
432
|
+
auth_source: str # Authentication source
|
|
433
|
+
organization_id: str | None # Organization identifier
|
|
434
|
+
subscription_tier: SubscriptionTier # Defaults to FREE (services bypass tier checks)
|
|
435
|
+
subscription_status: SubscriptionStatus # Defaults to ACTIVE
|
|
337
436
|
|
|
338
437
|
# Properties
|
|
339
438
|
is_service: bool # Always True for services
|
|
@@ -345,6 +444,8 @@ class ServiceContext:
|
|
|
345
444
|
has_all_roles(roles: List[str]) -> bool
|
|
346
445
|
```
|
|
347
446
|
|
|
447
|
+
**Note:** Services bypass subscription tier checks by default when using `require_tier()` and related dependencies.
|
|
448
|
+
|
|
348
449
|
## Available Dependencies
|
|
349
450
|
|
|
350
451
|
### Authentication Dependencies
|
|
@@ -383,6 +484,59 @@ require_service_roles("role1", "role2", ...)
|
|
|
383
484
|
require_scopes("scope1", "scope2", ...)
|
|
384
485
|
```
|
|
385
486
|
|
|
487
|
+
### Subscription Dependencies
|
|
488
|
+
|
|
489
|
+
| Dependency | Description |
|
|
490
|
+
| ----------------------------- | --------------------------------------------------- |
|
|
491
|
+
| `require_tier(tier)` | Factory requiring minimum subscription tier |
|
|
492
|
+
| `require_active_subscription()` | Factory requiring active subscription status |
|
|
493
|
+
| `require_tier_and_active(tier)` | Factory requiring both tier and active status |
|
|
494
|
+
| `require_basic` | Requires BASIC tier or higher |
|
|
495
|
+
| `require_professional` | Requires PROFESSIONAL tier or higher |
|
|
496
|
+
| `require_enterprise` | Requires ENTERPRISE tier |
|
|
497
|
+
| `require_paid_subscription` | Requires any paid tier (non-FREE) |
|
|
498
|
+
| `get_subscription_tier` | Extract tier from header |
|
|
499
|
+
| `get_organization_id` | Extract organization ID from header |
|
|
500
|
+
|
|
501
|
+
### Subscription Types
|
|
502
|
+
|
|
503
|
+
```python
|
|
504
|
+
from auth_gate import SubscriptionTier, SubscriptionStatus
|
|
505
|
+
|
|
506
|
+
# Tier hierarchy (lowest to highest)
|
|
507
|
+
SubscriptionTier.FREE
|
|
508
|
+
SubscriptionTier.BASIC
|
|
509
|
+
SubscriptionTier.PROFESSIONAL
|
|
510
|
+
SubscriptionTier.ENTERPRISE
|
|
511
|
+
|
|
512
|
+
# Subscription statuses
|
|
513
|
+
SubscriptionStatus.ACTIVE
|
|
514
|
+
SubscriptionStatus.SUSPENDED
|
|
515
|
+
SubscriptionStatus.CANCELLED
|
|
516
|
+
SubscriptionStatus.PAST_DUE
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Subscription Utilities
|
|
520
|
+
|
|
521
|
+
```python
|
|
522
|
+
from auth_gate import (
|
|
523
|
+
meets_minimum_tier,
|
|
524
|
+
compare_tiers,
|
|
525
|
+
is_paid_tier,
|
|
526
|
+
get_tier_level,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# Check if user tier meets requirement
|
|
530
|
+
meets_minimum_tier(SubscriptionTier.PROFESSIONAL, SubscriptionTier.BASIC) # True
|
|
531
|
+
|
|
532
|
+
# Compare tiers (-1, 0, 1)
|
|
533
|
+
compare_tiers(SubscriptionTier.ENTERPRISE, SubscriptionTier.FREE) # > 0
|
|
534
|
+
|
|
535
|
+
# Check if tier is paid
|
|
536
|
+
is_paid_tier(SubscriptionTier.BASIC) # True
|
|
537
|
+
is_paid_tier(SubscriptionTier.FREE) # False
|
|
538
|
+
```
|
|
539
|
+
|
|
386
540
|
## Migration Guide
|
|
387
541
|
|
|
388
542
|
### Updating Existing Applications
|
|
@@ -450,11 +604,15 @@ from auth_gate import (
|
|
|
450
604
|
AuthContext,
|
|
451
605
|
UserContext,
|
|
452
606
|
ServiceContext,
|
|
607
|
+
SubscriptionTier,
|
|
453
608
|
get_current_auth,
|
|
454
609
|
get_current_user,
|
|
455
610
|
get_current_service,
|
|
456
611
|
require_roles,
|
|
457
612
|
require_user_roles,
|
|
613
|
+
require_tier,
|
|
614
|
+
require_tier_and_active,
|
|
615
|
+
require_professional,
|
|
458
616
|
)
|
|
459
617
|
|
|
460
618
|
app = FastAPI()
|
|
@@ -473,7 +631,11 @@ async def health():
|
|
|
473
631
|
# User-only endpoint
|
|
474
632
|
@app.get("/api/user/profile")
|
|
475
633
|
async def get_profile(user: UserContext = Depends(get_current_user)):
|
|
476
|
-
return {
|
|
634
|
+
return {
|
|
635
|
+
"user": user.user_id,
|
|
636
|
+
"tier": user.subscription_tier.value,
|
|
637
|
+
"organization": user.organization_id
|
|
638
|
+
}
|
|
477
639
|
|
|
478
640
|
# Service-only endpoint
|
|
479
641
|
@app.post("/api/internal/batch")
|
|
@@ -494,6 +656,31 @@ require_user_editor = require_user_roles("editor")
|
|
|
494
656
|
@app.post("/api/articles")
|
|
495
657
|
async def create_article(user: UserContext = Depends(require_user_editor)):
|
|
496
658
|
return {"author": user.user_id}
|
|
659
|
+
|
|
660
|
+
# Tier-protected endpoint (PROFESSIONAL or higher)
|
|
661
|
+
@app.get("/api/analytics")
|
|
662
|
+
async def get_analytics(auth: AuthContext = Depends(require_professional)):
|
|
663
|
+
return {"analytics": "professional data"}
|
|
664
|
+
|
|
665
|
+
# Tier and active subscription required
|
|
666
|
+
@app.get("/api/premium/reports")
|
|
667
|
+
async def get_premium_reports(
|
|
668
|
+
auth: AuthContext = Depends(require_tier_and_active(SubscriptionTier.BASIC))
|
|
669
|
+
):
|
|
670
|
+
return {"reports": "premium data"}
|
|
671
|
+
|
|
672
|
+
# Custom tier check within endpoint
|
|
673
|
+
@app.get("/api/features")
|
|
674
|
+
async def get_features(user: UserContext = Depends(get_current_user)):
|
|
675
|
+
features = ["basic_dashboard"]
|
|
676
|
+
|
|
677
|
+
if user.has_minimum_tier(SubscriptionTier.PROFESSIONAL):
|
|
678
|
+
features.append("advanced_analytics")
|
|
679
|
+
|
|
680
|
+
if user.has_minimum_tier(SubscriptionTier.ENTERPRISE):
|
|
681
|
+
features.append("custom_integrations")
|
|
682
|
+
|
|
683
|
+
return {"features": features, "tier": user.subscription_tier.value}
|
|
497
684
|
```
|
|
498
685
|
|
|
499
686
|
## License
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "auth-gate"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Enterprise-grade authentication for microservices with Kong and
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Enterprise-grade authentication for microservices with Kong/Keycloak integration and subscription tier support"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
11
|
license = {text = "MIT"}
|