byoi 0.1.0a1__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.
- byoi/__init__.py +233 -0
- byoi/__main__.py +228 -0
- byoi/cache.py +346 -0
- byoi/config.py +349 -0
- byoi/dependencies.py +144 -0
- byoi/errors.py +360 -0
- byoi/models.py +451 -0
- byoi/pkce.py +144 -0
- byoi/providers.py +434 -0
- byoi/py.typed +0 -0
- byoi/repositories.py +252 -0
- byoi/service.py +723 -0
- byoi/telemetry.py +352 -0
- byoi/tokens.py +340 -0
- byoi/types.py +130 -0
- byoi-0.1.0a1.dist-info/METADATA +504 -0
- byoi-0.1.0a1.dist-info/RECORD +20 -0
- byoi-0.1.0a1.dist-info/WHEEL +4 -0
- byoi-0.1.0a1.dist-info/entry_points.txt +3 -0
- byoi-0.1.0a1.dist-info/licenses/LICENSE +21 -0
byoi/__init__.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""BYOI - Bring Your Own Identity.
|
|
2
|
+
|
|
3
|
+
A server library for FastAPI applications that provides OIDC authentication
|
|
4
|
+
with support for multiple identity providers.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- PKCE (Proof Key for Code Exchange) - Prevent authorization code interception attacks
|
|
8
|
+
- ID Token Validation - Full JWT validation with JWKS fetching and caching
|
|
9
|
+
- Identity Linking - Users can link multiple providers (Google + Microsoft, etc.)
|
|
10
|
+
- Nonce Validation - Prevents replay attacks
|
|
11
|
+
- Proper Error Handling - Clean error responses throughout
|
|
12
|
+
- FastAPI dependencies
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
```python
|
|
16
|
+
from contextlib import asynccontextmanager
|
|
17
|
+
from fastapi import FastAPI
|
|
18
|
+
from byoi import BYOI, BYOIConfig, OIDCProviderConfig
|
|
19
|
+
|
|
20
|
+
# Create your repository implementations
|
|
21
|
+
user_repo = MyUserRepository()
|
|
22
|
+
identity_repo = MyIdentityRepository()
|
|
23
|
+
auth_state_repo = MyAuthStateRepository()
|
|
24
|
+
|
|
25
|
+
# Configure BYOI
|
|
26
|
+
config = BYOIConfig(
|
|
27
|
+
user_repository=user_repo,
|
|
28
|
+
identity_repository=identity_repo,
|
|
29
|
+
auth_state_repository=auth_state_repo,
|
|
30
|
+
providers=[
|
|
31
|
+
OIDCProviderConfig(
|
|
32
|
+
name="google",
|
|
33
|
+
display_name="Google",
|
|
34
|
+
issuer="https://accounts.google.com",
|
|
35
|
+
client_id="your-client-id",
|
|
36
|
+
client_secret="your-client-secret",
|
|
37
|
+
),
|
|
38
|
+
],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
byoi = BYOI(config)
|
|
42
|
+
app = FastAPI(lifespan=byoi.lifespan)
|
|
43
|
+
|
|
44
|
+
@app.post("/auth/login")
|
|
45
|
+
async def login(
|
|
46
|
+
request: TokenExchangeRequest,
|
|
47
|
+
auth_service: AuthServiceDep,
|
|
48
|
+
):
|
|
49
|
+
return await auth_service.authenticate(request)
|
|
50
|
+
```
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from byoi.cache import (
|
|
54
|
+
CacheProtocol,
|
|
55
|
+
InMemoryCache,
|
|
56
|
+
NullCache,
|
|
57
|
+
RedisCache,
|
|
58
|
+
)
|
|
59
|
+
from byoi.config import BYOI, BYOIConfig
|
|
60
|
+
from byoi.dependencies import (
|
|
61
|
+
AuthServiceDep,
|
|
62
|
+
ProviderManagerDep,
|
|
63
|
+
create_auth_service_dependency,
|
|
64
|
+
create_provider_manager_dependency,
|
|
65
|
+
get_auth_service,
|
|
66
|
+
get_provider_manager,
|
|
67
|
+
)
|
|
68
|
+
from byoi.errors import (
|
|
69
|
+
AuthenticationError,
|
|
70
|
+
BYOIError,
|
|
71
|
+
CacheError,
|
|
72
|
+
CannotUnlinkLastIdentityError,
|
|
73
|
+
ConfigurationError,
|
|
74
|
+
IdentityAlreadyLinkedError,
|
|
75
|
+
IdentityError,
|
|
76
|
+
IdentityNotFoundError,
|
|
77
|
+
InvalidAudienceError,
|
|
78
|
+
InvalidCodeError,
|
|
79
|
+
InvalidIssuerError,
|
|
80
|
+
InvalidNonceError,
|
|
81
|
+
InvalidStateError,
|
|
82
|
+
JWKSFetchError,
|
|
83
|
+
ProviderDiscoveryError,
|
|
84
|
+
ProviderError,
|
|
85
|
+
ProviderNotFoundError,
|
|
86
|
+
StateExpiredError,
|
|
87
|
+
TokenExchangeError,
|
|
88
|
+
TokenExpiredError,
|
|
89
|
+
TokenRefreshError,
|
|
90
|
+
TokenValidationError,
|
|
91
|
+
UserNotFoundError,
|
|
92
|
+
)
|
|
93
|
+
from byoi.models import (
|
|
94
|
+
AuthenticatedUser,
|
|
95
|
+
AuthorizationRequest,
|
|
96
|
+
AuthorizationResponse,
|
|
97
|
+
ClientType,
|
|
98
|
+
IdentityInfo,
|
|
99
|
+
LinkedIdentityInfo,
|
|
100
|
+
LinkIdentityRequest,
|
|
101
|
+
OIDCProviderConfig,
|
|
102
|
+
OIDCProviderInfo,
|
|
103
|
+
TokenExchangeRequest,
|
|
104
|
+
TokenExchangeResponse,
|
|
105
|
+
TokenRefreshRequest,
|
|
106
|
+
TokenRefreshResponse,
|
|
107
|
+
UnlinkIdentityRequest,
|
|
108
|
+
UserIdentities,
|
|
109
|
+
)
|
|
110
|
+
from byoi.pkce import (
|
|
111
|
+
PKCEChallenge,
|
|
112
|
+
generate_code_challenge,
|
|
113
|
+
generate_code_verifier,
|
|
114
|
+
generate_nonce,
|
|
115
|
+
generate_pkce_pair,
|
|
116
|
+
generate_state,
|
|
117
|
+
)
|
|
118
|
+
from byoi.providers import OIDCDiscoveryDocument, ProviderManager
|
|
119
|
+
from byoi.repositories import (
|
|
120
|
+
AuthStateRepositoryProtocol,
|
|
121
|
+
LinkedIdentityRepositoryProtocol,
|
|
122
|
+
UserRepositoryProtocol,
|
|
123
|
+
)
|
|
124
|
+
from byoi.service import AuthService
|
|
125
|
+
from byoi.telemetry import (
|
|
126
|
+
configure_tracing,
|
|
127
|
+
get_tracer,
|
|
128
|
+
is_tracing_available,
|
|
129
|
+
span_context,
|
|
130
|
+
traced,
|
|
131
|
+
traced_async,
|
|
132
|
+
)
|
|
133
|
+
from byoi.tokens import TokenValidator, validate_id_token
|
|
134
|
+
from byoi.types import (
|
|
135
|
+
AuthStateProtocol,
|
|
136
|
+
LinkedIdentityProtocol,
|
|
137
|
+
UserProtocol,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
__version__ = "0.1.0a1"
|
|
141
|
+
|
|
142
|
+
__all__ = (
|
|
143
|
+
# Version
|
|
144
|
+
"__version__",
|
|
145
|
+
# Main Configuration
|
|
146
|
+
"BYOI",
|
|
147
|
+
"BYOIConfig",
|
|
148
|
+
# Services
|
|
149
|
+
"AuthService",
|
|
150
|
+
"ProviderManager",
|
|
151
|
+
"TokenValidator",
|
|
152
|
+
# Cache
|
|
153
|
+
"CacheProtocol",
|
|
154
|
+
"InMemoryCache",
|
|
155
|
+
"RedisCache",
|
|
156
|
+
"NullCache",
|
|
157
|
+
# Protocols (for implementing applications)
|
|
158
|
+
"UserProtocol",
|
|
159
|
+
"LinkedIdentityProtocol",
|
|
160
|
+
"AuthStateProtocol",
|
|
161
|
+
# Repository Protocols
|
|
162
|
+
"UserRepositoryProtocol",
|
|
163
|
+
"LinkedIdentityRepositoryProtocol",
|
|
164
|
+
"AuthStateRepositoryProtocol",
|
|
165
|
+
# Pydantic Models - Provider
|
|
166
|
+
"OIDCProviderConfig",
|
|
167
|
+
"OIDCProviderInfo",
|
|
168
|
+
"OIDCDiscoveryDocument",
|
|
169
|
+
# Pydantic Models - Auth Flow
|
|
170
|
+
"AuthorizationRequest",
|
|
171
|
+
"AuthorizationResponse",
|
|
172
|
+
"TokenExchangeRequest",
|
|
173
|
+
"TokenExchangeResponse",
|
|
174
|
+
"TokenRefreshRequest",
|
|
175
|
+
"TokenRefreshResponse",
|
|
176
|
+
# Pydantic Models - Identity
|
|
177
|
+
"IdentityInfo",
|
|
178
|
+
"LinkedIdentityInfo",
|
|
179
|
+
"LinkIdentityRequest",
|
|
180
|
+
"UnlinkIdentityRequest",
|
|
181
|
+
# Pydantic Models - User
|
|
182
|
+
"AuthenticatedUser",
|
|
183
|
+
"UserIdentities",
|
|
184
|
+
# Enums
|
|
185
|
+
"ClientType",
|
|
186
|
+
# PKCE Utilities
|
|
187
|
+
"PKCEChallenge",
|
|
188
|
+
"generate_code_verifier",
|
|
189
|
+
"generate_code_challenge",
|
|
190
|
+
"generate_pkce_pair",
|
|
191
|
+
"generate_state",
|
|
192
|
+
"generate_nonce",
|
|
193
|
+
# Token Validation
|
|
194
|
+
"validate_id_token",
|
|
195
|
+
# Telemetry
|
|
196
|
+
"configure_tracing",
|
|
197
|
+
"get_tracer",
|
|
198
|
+
"is_tracing_available",
|
|
199
|
+
"span_context",
|
|
200
|
+
"traced",
|
|
201
|
+
"traced_async",
|
|
202
|
+
# FastAPI Dependencies
|
|
203
|
+
"get_provider_manager",
|
|
204
|
+
"get_auth_service",
|
|
205
|
+
"ProviderManagerDep",
|
|
206
|
+
"AuthServiceDep",
|
|
207
|
+
"create_provider_manager_dependency",
|
|
208
|
+
"create_auth_service_dependency",
|
|
209
|
+
# Errors
|
|
210
|
+
"BYOIError",
|
|
211
|
+
"ConfigurationError",
|
|
212
|
+
"ProviderError",
|
|
213
|
+
"ProviderNotFoundError",
|
|
214
|
+
"ProviderDiscoveryError",
|
|
215
|
+
"AuthenticationError",
|
|
216
|
+
"InvalidStateError",
|
|
217
|
+
"StateExpiredError",
|
|
218
|
+
"InvalidCodeError",
|
|
219
|
+
"TokenExchangeError",
|
|
220
|
+
"TokenRefreshError",
|
|
221
|
+
"TokenValidationError",
|
|
222
|
+
"InvalidNonceError",
|
|
223
|
+
"InvalidIssuerError",
|
|
224
|
+
"InvalidAudienceError",
|
|
225
|
+
"TokenExpiredError",
|
|
226
|
+
"JWKSFetchError",
|
|
227
|
+
"IdentityError",
|
|
228
|
+
"IdentityAlreadyLinkedError",
|
|
229
|
+
"IdentityNotFoundError",
|
|
230
|
+
"CannotUnlinkLastIdentityError",
|
|
231
|
+
"UserNotFoundError",
|
|
232
|
+
"CacheError",
|
|
233
|
+
)
|
byoi/__main__.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Command-line interface for BYOI utilities.
|
|
2
|
+
|
|
3
|
+
This module provides CLI commands for BYOI operations, including PKCE generation.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python -m byoi pkce [--length LENGTH] [--method METHOD] [--json]
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from byoi.pkce import (
|
|
14
|
+
generate_nonce,
|
|
15
|
+
generate_pkce_pair,
|
|
16
|
+
generate_state,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def pkce_command(args: argparse.Namespace) -> int:
|
|
21
|
+
"""Generate PKCE code verifier and challenge.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
args: Parsed command line arguments.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Exit code (0 for success).
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
pkce = generate_pkce_pair(
|
|
31
|
+
verifier_length=args.length,
|
|
32
|
+
method=args.method,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if args.json:
|
|
36
|
+
output = {
|
|
37
|
+
"code_verifier": pkce.code_verifier,
|
|
38
|
+
"code_challenge": pkce.code_challenge,
|
|
39
|
+
"code_challenge_method": pkce.code_challenge_method,
|
|
40
|
+
}
|
|
41
|
+
if args.state:
|
|
42
|
+
output["state"] = generate_state()
|
|
43
|
+
if args.nonce:
|
|
44
|
+
output["nonce"] = generate_nonce()
|
|
45
|
+
print(json.dumps(output, indent=2))
|
|
46
|
+
else:
|
|
47
|
+
print(f"Code Verifier: {pkce.code_verifier}")
|
|
48
|
+
print(f"Code Challenge: {pkce.code_challenge}")
|
|
49
|
+
print(f"Code Challenge Method: {pkce.code_challenge_method}")
|
|
50
|
+
if args.state:
|
|
51
|
+
print(f"State: {generate_state()}")
|
|
52
|
+
if args.nonce:
|
|
53
|
+
print(f"Nonce: {generate_nonce()}")
|
|
54
|
+
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
except ValueError as e:
|
|
58
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
59
|
+
return 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def state_command(args: argparse.Namespace) -> int:
|
|
63
|
+
"""Generate an OAuth state parameter.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
args: Parsed command line arguments.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Exit code (0 for success).
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
state = generate_state(length=args.length)
|
|
73
|
+
|
|
74
|
+
if args.json:
|
|
75
|
+
print(json.dumps({"state": state}, indent=2))
|
|
76
|
+
else:
|
|
77
|
+
print(f"State: {state}")
|
|
78
|
+
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
except ValueError as e:
|
|
82
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def nonce_command(args: argparse.Namespace) -> int:
|
|
87
|
+
"""Generate a nonce for ID token validation.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
args: Parsed command line arguments.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Exit code (0 for success).
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
nonce = generate_nonce(length=args.length)
|
|
97
|
+
|
|
98
|
+
if args.json:
|
|
99
|
+
print(json.dumps({"nonce": nonce}, indent=2))
|
|
100
|
+
else:
|
|
101
|
+
print(f"Nonce: {nonce}")
|
|
102
|
+
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
except ValueError as e:
|
|
106
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
107
|
+
return 1
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
111
|
+
"""Create the argument parser for the CLI.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
The configured argument parser.
|
|
115
|
+
"""
|
|
116
|
+
parser = argparse.ArgumentParser(
|
|
117
|
+
prog="byoi",
|
|
118
|
+
description="BYOI - Bring Your Own Identity CLI utilities",
|
|
119
|
+
)
|
|
120
|
+
parser.add_argument(
|
|
121
|
+
"--version",
|
|
122
|
+
action="version",
|
|
123
|
+
version="%(prog)s 0.1.0a1",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
subparsers = parser.add_subparsers(
|
|
127
|
+
title="commands",
|
|
128
|
+
description="Available commands",
|
|
129
|
+
dest="command",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# PKCE command
|
|
133
|
+
pkce_parser = subparsers.add_parser(
|
|
134
|
+
"pkce",
|
|
135
|
+
help="Generate PKCE code verifier and challenge",
|
|
136
|
+
description="Generate a PKCE code verifier and challenge pair for OAuth 2.0 PKCE flow.",
|
|
137
|
+
)
|
|
138
|
+
pkce_parser.add_argument(
|
|
139
|
+
"--length",
|
|
140
|
+
type=int,
|
|
141
|
+
default=64,
|
|
142
|
+
help="Length of the code verifier (43-128, default: 64)",
|
|
143
|
+
)
|
|
144
|
+
pkce_parser.add_argument(
|
|
145
|
+
"--method",
|
|
146
|
+
choices=["S256", "plain"],
|
|
147
|
+
default="S256",
|
|
148
|
+
help="Code challenge method (default: S256)",
|
|
149
|
+
)
|
|
150
|
+
pkce_parser.add_argument(
|
|
151
|
+
"--state",
|
|
152
|
+
action="store_true",
|
|
153
|
+
help="Also generate a state parameter",
|
|
154
|
+
)
|
|
155
|
+
pkce_parser.add_argument(
|
|
156
|
+
"--nonce",
|
|
157
|
+
action="store_true",
|
|
158
|
+
help="Also generate a nonce",
|
|
159
|
+
)
|
|
160
|
+
pkce_parser.add_argument(
|
|
161
|
+
"--json",
|
|
162
|
+
action="store_true",
|
|
163
|
+
help="Output in JSON format",
|
|
164
|
+
)
|
|
165
|
+
pkce_parser.set_defaults(func=pkce_command)
|
|
166
|
+
|
|
167
|
+
# State command
|
|
168
|
+
state_parser = subparsers.add_parser(
|
|
169
|
+
"state",
|
|
170
|
+
help="Generate an OAuth state parameter",
|
|
171
|
+
description="Generate a cryptographically random state parameter for OAuth 2.0.",
|
|
172
|
+
)
|
|
173
|
+
state_parser.add_argument(
|
|
174
|
+
"--length",
|
|
175
|
+
type=int,
|
|
176
|
+
default=32,
|
|
177
|
+
help="Length of the state parameter (minimum 16, default: 32)",
|
|
178
|
+
)
|
|
179
|
+
state_parser.add_argument(
|
|
180
|
+
"--json",
|
|
181
|
+
action="store_true",
|
|
182
|
+
help="Output in JSON format",
|
|
183
|
+
)
|
|
184
|
+
state_parser.set_defaults(func=state_command)
|
|
185
|
+
|
|
186
|
+
# Nonce command
|
|
187
|
+
nonce_parser = subparsers.add_parser(
|
|
188
|
+
"nonce",
|
|
189
|
+
help="Generate a nonce for ID token validation",
|
|
190
|
+
description="Generate a cryptographically random nonce for OIDC ID token validation.",
|
|
191
|
+
)
|
|
192
|
+
nonce_parser.add_argument(
|
|
193
|
+
"--length",
|
|
194
|
+
type=int,
|
|
195
|
+
default=32,
|
|
196
|
+
help="Length of the nonce (minimum 16, default: 32)",
|
|
197
|
+
)
|
|
198
|
+
nonce_parser.add_argument(
|
|
199
|
+
"--json",
|
|
200
|
+
action="store_true",
|
|
201
|
+
help="Output in JSON format",
|
|
202
|
+
)
|
|
203
|
+
nonce_parser.set_defaults(func=nonce_command)
|
|
204
|
+
|
|
205
|
+
return parser
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main(argv: list[str] | None = None) -> int:
|
|
209
|
+
"""Main entry point for the CLI.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
argv: Command line arguments (defaults to sys.argv[1:]).
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Exit code.
|
|
216
|
+
"""
|
|
217
|
+
parser = create_parser()
|
|
218
|
+
args = parser.parse_args(argv)
|
|
219
|
+
|
|
220
|
+
if args.command is None:
|
|
221
|
+
parser.print_help()
|
|
222
|
+
return 0
|
|
223
|
+
|
|
224
|
+
return args.func(args)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
if __name__ == "__main__":
|
|
228
|
+
sys.exit(main())
|