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 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())