xtr-security-oauth2-server-core 4.0.2__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.
Files changed (90) hide show
  1. xtr_security_oauth2_server_core-4.0.2/LICENSE +22 -0
  2. xtr_security_oauth2_server_core-4.0.2/PKG-INFO +386 -0
  3. xtr_security_oauth2_server_core-4.0.2/README.md +360 -0
  4. xtr_security_oauth2_server_core-4.0.2/pyproject.toml +160 -0
  5. xtr_security_oauth2_server_core-4.0.2/pyproject.toml.orig +140 -0
  6. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/__init__.py +14 -0
  7. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/_repeated_parameter.py +54 -0
  8. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/abstract_handler.py +435 -0
  9. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/authorization_server.py +316 -0
  10. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/authorization_validators/__init__.py +25 -0
  11. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/authorization_validators/authorization_validator_interface.py +38 -0
  12. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/authorization_validators/bearer_token_validator.py +320 -0
  13. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/authorization_validators/bearer_token_validator_interface.py +46 -0
  14. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/bearer_challenge.py +58 -0
  15. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/code_challenge_verifiers/__init__.py +19 -0
  16. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/code_challenge_verifiers/code_challenge_verifier_interface.py +28 -0
  17. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/code_challenge_verifiers/plain_verifier.py +45 -0
  18. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/code_challenge_verifiers/s256_verifier.py +44 -0
  19. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/crypt_trait.py +210 -0
  20. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/__init__.py +36 -0
  21. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/access_token_entity_interface.py +32 -0
  22. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/access_token_signer.py +63 -0
  23. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/auth_code_entity_interface.py +32 -0
  24. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/client_entity_interface.py +51 -0
  25. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/device_code_entity_interface.py +71 -0
  26. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/refresh_token_entity_interface.py +48 -0
  27. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/scope_entity_interface.py +34 -0
  28. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/token_interface.py +77 -0
  29. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/__init__.py +35 -0
  30. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/access_token_trait.py +158 -0
  31. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/auth_code_trait.py +24 -0
  32. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/client_trait.py +56 -0
  33. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/device_code_trait.py +95 -0
  34. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/entity_trait.py +65 -0
  35. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/refresh_token_trait.py +43 -0
  36. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/scope_trait.py +33 -0
  37. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/token_entity_trait.py +85 -0
  38. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/user_entity_interface.py +22 -0
  39. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/exception/__init__.py +30 -0
  40. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/exception/invalid_argument_error.py +28 -0
  41. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/exception/oauth2_error.py +24 -0
  42. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/exception/oauth_server_error.py +336 -0
  43. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/exception/unique_token_identifier_constraint_violation_error.py +29 -0
  44. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/__init__.py +41 -0
  45. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/abstract_authorize_grant.py +186 -0
  46. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/abstract_grant.py +612 -0
  47. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/auth_code_grant.py +356 -0
  48. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/client_credentials_grant.py +79 -0
  49. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/device_code_grant.py +332 -0
  50. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/grant_type_interface.py +191 -0
  51. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/implicit_grant.py +207 -0
  52. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/password_grant.py +142 -0
  53. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/refresh_token_grant.py +192 -0
  54. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/handlers/__init__.py +31 -0
  55. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/handlers/abstract_token_handler.py +154 -0
  56. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/handlers/token_handler_interface.py +90 -0
  57. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/handlers/token_introspection_handler.py +81 -0
  58. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/handlers/token_revocation_handler.py +110 -0
  59. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/handlers/validated_token.py +45 -0
  60. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/oauth2_grants.py +63 -0
  61. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/py.typed +0 -0
  62. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/redirect_uri.py +45 -0
  63. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/redirect_uri_validators/__init__.py +16 -0
  64. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/redirect_uri_validators/redirect_uri_validator.py +102 -0
  65. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/redirect_uri_validators/redirect_uri_validator_interface.py +21 -0
  66. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/__init__.py +38 -0
  67. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/access_token_repository_interface.py +77 -0
  68. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/auth_code_repository_interface.py +51 -0
  69. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/client_repository_interface.py +51 -0
  70. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/device_code_repository_interface.py +61 -0
  71. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/refresh_token_repository_interface.py +67 -0
  72. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/repository_interface.py +23 -0
  73. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/scope_repository_interface.py +70 -0
  74. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/user_repository_interface.py +51 -0
  75. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/request_access_token_event.py +47 -0
  76. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/request_event.py +78 -0
  77. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/request_refresh_token_event.py +46 -0
  78. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/request_types/__init__.py +17 -0
  79. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/request_types/authorization_request.py +186 -0
  80. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/request_types/authorization_request_interface.py +108 -0
  81. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/resource_server.py +69 -0
  82. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/response_types/__init__.py +36 -0
  83. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/response_types/abstract_response_type.py +81 -0
  84. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/response_types/bearer_token_response.py +131 -0
  85. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/response_types/device_code_response.py +124 -0
  86. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/response_types/introspection_response.py +232 -0
  87. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/response_types/introspection_response_type_interface.py +44 -0
  88. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/response_types/redirect_response.py +56 -0
  89. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/response_types/response_type_interface.py +66 -0
  90. xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/token_server.py +209 -0
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xterr
4
+ Copyright (C) Alex Bilbie
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,386 @@
1
+ Metadata-Version: 2.4
2
+ Name: xtr-security-oauth2-server-core
3
+ Version: 4.0.2
4
+ Summary: The OAuth 2.0 authorization-server library: grants, servers, entities, response types and the resource server.
5
+ Keywords: security,oauth2,authorization-server,resource-server,grants,token
6
+ Author: Xterr
7
+ Author-email: Xterr <me@xterr.dev>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: xtr-security-core>=4.0,<5
18
+ Requires-Dist: xtr-security-jwt>=4.0,<5
19
+ Requires-Dist: xtr-clock>=4.0,<5
20
+ Requires-Dist: xtr-event-dispatcher-contracts>=4.0,<5
21
+ Requires-Dist: xtr-http-message-contracts>=4.0,<5
22
+ Requires-Dist: joserfc>=1.7
23
+ Requires-Dist: typing-extensions>=4.12
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+
27
+ <div align="center">
28
+
29
+ # xtr-security-oauth2-server-core
30
+
31
+ **The OAuth 2.0 authorization-server library: grants, servers, entities, response types and the resource server.**
32
+
33
+ <img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
34
+ <img alt="typed" src="https://img.shields.io/badge/typed-ty%20%2B%20basedpyright-1f6feb">
35
+ <img alt="license MIT" src="https://img.shields.io/badge/license-MIT-blue">
36
+
37
+ </div>
38
+
39
+ ---
40
+
41
+ ## Why?
42
+
43
+ This is the framework-neutral half of [xtr-security-oauth2-server](../xtr-security-oauth2-server):
44
+ the authorization server, the token server, the grants, the entities and their interfaces, the
45
+ response types, the proof-key and redirect-URI validators, the resource server and the protocol
46
+ errors. An application builds them by hand, so nothing here reaches for a web framework, a
47
+ container or an ORM — it speaks the HTTP message contracts, and the caller hands it a request and
48
+ a response.
49
+
50
+ What it decides is the protocol; what it leaves to whoever builds it is storage. Every store is a
51
+ `Protocol` in `repositories/` — clients, scopes, access tokens, codes, refresh tokens, device
52
+ codes, users — with async methods, and every entity a store hands back is an interface in
53
+ `entities/`, composed from the traits in `entities/traits/`. Authorization codes and refresh
54
+ tokens are sealed (JWE `dir` + `A256GCM`), so storage only has to remember whether one was spent;
55
+ access tokens are signed RFC 9068 tokens, read back by the resource server without a round trip.
56
+
57
+ The bundle that wires all of this into an application on
58
+ [xtr-dependency-injection](../xtr-dependency-injection) — with stores, persistence, controllers,
59
+ a router, commands and a firewall authenticator — lives in
60
+ [xtr-security-oauth2-server](../xtr-security-oauth2-server), which depends on this package. To use
61
+ the server in an application, follow its
62
+ [Use in an application](../xtr-security-oauth2-server/README.md#use-in-an-application) section.
63
+
64
+ ## Install
65
+
66
+ ```sh
67
+ uv add xtr-security-oauth2-server-core
68
+ ```
69
+
70
+ Most applications install [xtr-security-oauth2-server](../xtr-security-oauth2-server) instead,
71
+ which brings this along. Building the servers by hand also needs an event dispatcher and HTTP
72
+ messages: [xtr-event-dispatcher](../xtr-event-dispatcher) and
73
+ [xtr-http-message](../xtr-http-message) are what the Quick start uses.
74
+
75
+ ## Quick start
76
+
77
+ An `AuthorizationServer` issuing a token to a service by client credentials, and a
78
+ `ResourceServer` reading it back — built from constructors, over stores written here in memory.
79
+
80
+ ```python
81
+ import json
82
+ import secrets
83
+ from collections.abc import Sequence
84
+ from datetime import timedelta
85
+
86
+ import anyio
87
+ from xtr_clock import SystemClock
88
+ from xtr_event_dispatcher import EventDispatcher
89
+ from xtr_http_message import Response, ServerRequest
90
+ from xtr_security_jwt.services.jws_provider import JoserfcJwsProvider
91
+ from xtr_security_jwt.services.key_loader import RawKeyLoader, generate_signing_key, signing_key_id
92
+ from xtr_security_oauth2_server_core.authorization_server import AuthorizationServer
93
+ from xtr_security_oauth2_server_core.authorization_validators.bearer_token_validator import (
94
+ BearerTokenValidator,
95
+ )
96
+ from xtr_security_oauth2_server_core.entities.access_token_entity_interface import (
97
+ AccessTokenEntityInterface,
98
+ )
99
+ from xtr_security_oauth2_server_core.entities.access_token_signer import AccessTokenSigner
100
+ from xtr_security_oauth2_server_core.entities.client_entity_interface import ClientEntityInterface
101
+ from xtr_security_oauth2_server_core.entities.scope_entity_interface import ScopeEntityInterface
102
+ from xtr_security_oauth2_server_core.entities.traits.access_token_trait import AccessTokenTrait
103
+ from xtr_security_oauth2_server_core.entities.traits.client_trait import ClientTrait
104
+ from xtr_security_oauth2_server_core.entities.traits.entity_trait import EntityTrait
105
+ from xtr_security_oauth2_server_core.entities.traits.scope_trait import ScopeTrait
106
+ from xtr_security_oauth2_server_core.entities.traits.token_entity_trait import TokenEntityTrait
107
+ from xtr_security_oauth2_server_core.grant.client_credentials_grant import ClientCredentialsGrant
108
+ from xtr_security_oauth2_server_core.repositories.access_token_repository_interface import (
109
+ AccessTokenRepositoryInterface,
110
+ )
111
+ from xtr_security_oauth2_server_core.repositories.client_repository_interface import (
112
+ ClientRepositoryInterface,
113
+ )
114
+ from xtr_security_oauth2_server_core.repositories.scope_repository_interface import (
115
+ ScopeRepositoryInterface,
116
+ )
117
+ from xtr_security_oauth2_server_core.resource_server import ResourceServer
118
+
119
+ ISSUER = "https://authorization.example.com"
120
+
121
+
122
+ # Entities: the traits hold the state, the interface comes last.
123
+ class Client(EntityTrait, ClientTrait, ClientEntityInterface):
124
+ def __init__(self, identifier: str, name: str) -> None:
125
+ self.set_identifier(identifier)
126
+ self._name = name
127
+ self._redirect_uri = ()
128
+ self._is_confidential = True
129
+
130
+
131
+ class Scope(EntityTrait, ScopeTrait, ScopeEntityInterface):
132
+ def __init__(self, identifier: str) -> None:
133
+ self.set_identifier(identifier)
134
+
135
+
136
+ class AccessToken(EntityTrait, TokenEntityTrait, AccessTokenTrait, AccessTokenEntityInterface):
137
+ pass
138
+
139
+
140
+ # Repositories: where the server reads clients and scopes and keeps tokens.
141
+ class Clients(ClientRepositoryInterface):
142
+ def __init__(self) -> None:
143
+ self._clients = {"billing": (Client("billing", "Billing"), "billing-secret")}
144
+
145
+ async def get_client_entity(self, client_identifier: str) -> ClientEntityInterface | None:
146
+ found = self._clients.get(client_identifier)
147
+ return found[0] if found is not None else None
148
+
149
+ async def validate_client(
150
+ self,
151
+ client_identifier: str,
152
+ client_secret: str | None,
153
+ grant_type: str | None,
154
+ ) -> bool:
155
+ found = self._clients.get(client_identifier)
156
+ # A real store keeps a hash and verifies it; never compare with ==.
157
+ return found is not None and secrets.compare_digest(found[1], client_secret or "")
158
+
159
+
160
+ class Scopes(ScopeRepositoryInterface):
161
+ async def get_scope_entity_by_identifier(self, identifier: str) -> ScopeEntityInterface | None:
162
+ return Scope(identifier) if identifier in {"invoices:read", "invoices:write"} else None
163
+
164
+ async def finalize_scopes(
165
+ self,
166
+ scopes: Sequence[ScopeEntityInterface],
167
+ grant_type: str,
168
+ client_entity: ClientEntityInterface,
169
+ user_identifier: str | None = None,
170
+ auth_code_id: str | None = None,
171
+ ) -> Sequence[ScopeEntityInterface]:
172
+ return scopes
173
+
174
+
175
+ class AccessTokens(AccessTokenRepositoryInterface):
176
+ def __init__(self) -> None:
177
+ self._revoked: set[str] = set()
178
+
179
+ async def get_new_token(
180
+ self,
181
+ client_entity: ClientEntityInterface,
182
+ scopes: Sequence[ScopeEntityInterface],
183
+ user_identifier: str | None = None,
184
+ ) -> AccessTokenEntityInterface:
185
+ token = AccessToken()
186
+ token.set_client(client_entity)
187
+ for scope in scopes:
188
+ token.add_scope(scope)
189
+ if user_identifier is not None:
190
+ token.set_user_identifier(user_identifier)
191
+ return token
192
+
193
+ async def persist_new_access_token(
194
+ self, access_token_entity: AccessTokenEntityInterface
195
+ ) -> None:
196
+ pass # keep it, so it can be revoked
197
+
198
+ async def revoke_access_token(self, token_id: str) -> None:
199
+ self._revoked.add(token_id)
200
+
201
+ async def is_access_token_revoked(self, token_id: str) -> bool:
202
+ return token_id in self._revoked
203
+
204
+
205
+ async def main() -> None:
206
+ clock = SystemClock()
207
+ dispatcher = EventDispatcher()
208
+ key_loader = RawKeyLoader(generate_signing_key("RS256").as_pem(private=True).decode(), None)
209
+ jws_provider = JoserfcJwsProvider(key_loader, "RS256", None, 0, clock)
210
+ signer = AccessTokenSigner(
211
+ jws_provider,
212
+ signing_key_id(key_loader, "RS256"), # the kid the published key set names
213
+ ISSUER,
214
+ clock,
215
+ )
216
+ encryption_key = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8" # base64url of 32 bytes
217
+ clients, scopes, access_tokens = Clients(), Scopes(), AccessTokens()
218
+
219
+ server = AuthorizationServer(
220
+ clients, access_tokens, scopes, signer, encryption_key, "key", dispatcher
221
+ )
222
+ server.enable_grant_type(ClientCredentialsGrant(dispatcher, clock), timedelta(minutes=15))
223
+
224
+ # The token endpoint: the caller hands in the request and the response to answer on.
225
+ request = ServerRequest("POST", f"{ISSUER}/token").with_parsed_body(
226
+ {
227
+ "grant_type": "client_credentials",
228
+ "client_id": "billing",
229
+ "client_secret": "billing-secret",
230
+ "scope": "invoices:read",
231
+ },
232
+ )
233
+ answer = await server.respond_to_access_token_request(request, Response())
234
+ issued = json.loads(await answer.get_body().get_contents())
235
+ print(answer.get_status_code(), issued["token_type"], issued["expires_in"])
236
+
237
+ # The API: the resource server reads the bearer token back.
238
+ resource_server = ResourceServer(
239
+ access_tokens,
240
+ BearerTokenValidator(access_tokens, jws_provider, ISSUER, clock),
241
+ )
242
+ call = ServerRequest(
243
+ "GET",
244
+ "https://api.example.com/invoices",
245
+ {"Authorization": f"Bearer {issued['access_token']}"},
246
+ )
247
+ read = await resource_server.validate_authenticated_request(call)
248
+ print(read.get_attribute("oauth_client_id"), read.get_attribute("oauth_scopes"))
249
+
250
+
251
+ anyio.run(main)
252
+ ```
253
+
254
+ ```console
255
+ 200 Bearer 899
256
+ billing ['invoices:read']
257
+ ```
258
+
259
+ - A refusal is an `OAuthServerError`: catch it around a server call and answer with
260
+ `await error.generate_http_response(response)`. Every answer here writes a body, so every
261
+ `generate_http_response` is awaited.
262
+ - The entities compose traits with the interface **last** among the bases: an interface listed
263
+ first would shadow the traits' accessors with its own empty stubs.
264
+ - `encryption_key_type` is `"key"` (the base64url of 32 random bytes) or `"plain"` (a password of
265
+ at least 32 characters, stretched once with PBKDF2 when the server is built).
266
+
267
+ ## Grants
268
+
269
+ Each grant takes the event dispatcher and the clock, plus the stores only it needs; the server
270
+ hands it the shared ones — client, access-token and scope repositories, the signer, the
271
+ encryption key — when `enable_grant_type(grant, access_token_ttl)` enables it.
272
+
273
+ | Grant | Identifier | Constructor |
274
+ |---|---|---|
275
+ | `ClientCredentialsGrant` | `client_credentials` | `(dispatcher, clock)` |
276
+ | `AuthCodeGrant` | `authorization_code` | `(dispatcher, clock, auth_code_repository, refresh_token_repository, auth_code_ttl)` |
277
+ | `RefreshTokenGrant` | `refresh_token` | `(dispatcher, clock, refresh_token_repository)` |
278
+ | `DeviceCodeGrant` | `urn:ietf:params:oauth:grant-type:device_code` | `(dispatcher, clock, device_code_repository, refresh_token_repository, device_code_ttl, verification_uri, retry_interval=5)` |
279
+ | `PasswordGrant` | `password` | `(dispatcher, clock, user_repository, refresh_token_repository)` |
280
+ | `ImplicitGrant` | `implicit` | `(dispatcher, clock, access_token_ttl, query_delimiter="#")` |
281
+
282
+ `OAuth2Grants` holds the identifiers. The authorization code grant requires a proof key of a
283
+ public client unless `disable_require_code_challenge_for_public_clients()` is called, refuses a
284
+ verifier for a code issued without a challenge, and judges the challenge by the method the code
285
+ carries. A grant of your own subclasses `AbstractGrant` (or `AbstractAuthorizeGrant` for one
286
+ reached through `/authorize`); `AuthCodeGrant` keeps two seams for one that carries something
287
+ through the code — `create_authorization_request()` and `get_auth_code_payload()`.
288
+
289
+ `AuthorizationServer` answers `validate_authorization_request` / `complete_authorization_request`,
290
+ `respond_to_access_token_request`, `respond_to_device_authorization_request` and
291
+ `complete_device_authorization_request(device_code, user_id, user_approved)`. `TokenServer`
292
+ answers `respond_to_token_revocation_request` (RFC 7009) and
293
+ `respond_to_token_introspection_request` (RFC 7662), with the same client authentication.
294
+
295
+ ## Response types
296
+
297
+ | Response type | Answers |
298
+ |---|---|
299
+ | `BearerTokenResponse(clock)` | The token document: `token_type`, `expires_in`, `access_token`, `refresh_token`, `scope`. Override `get_extra_params(access_token)` to add fields |
300
+ | `DeviceCodeResponse(clock)` | `device_code`, `user_code`, `verification_uri`, `expires_in`, and the complete URI and interval when asked |
301
+ | `RedirectResponse()` | The redirect back to the client, carrying a code or, for the implicit grant, a token in the fragment |
302
+ | `IntrospectionResponse()` | `{"active": false}`, or the claims of a live token; `get_extra_params(token_type, token_data)` adds fields |
303
+
304
+ `AuthorizationServer`'s last argument is the `ResponseTypeInterface` it answers token requests
305
+ with — a `BearerTokenResponse` when left out. Each request is answered on a shallow copy, so the
306
+ services a response type holds are shared and per-request state is not.
307
+
308
+ ## Events
309
+
310
+ The grants and servers announce through the dispatcher they were built with, each event under
311
+ its own name:
312
+
313
+ | Name | Event | When |
314
+ |---|---|---|
315
+ | `RequestEvent.ACCESS_TOKEN_ISSUED` | `RequestAccessTokenEvent` | An access token was issued |
316
+ | `RequestEvent.REFRESH_TOKEN_ISSUED` | `RequestRefreshTokenEvent` | A refresh token was issued |
317
+ | `RequestEvent.CLIENT_AUTHENTICATION_FAILED` | `RequestEvent` | A client could not be authenticated |
318
+ | `RequestEvent.USER_AUTHENTICATION_FAILED` | `RequestEvent` | The password grant refused a user's credentials |
319
+ | `RequestEvent.REFRESH_TOKEN_CLIENT_FAILED` | `RequestEvent` | A refresh token was presented by another client |
320
+
321
+ ```python
322
+ from xtr_event_dispatcher import EventDispatcher
323
+ from xtr_security_oauth2_server_core.request_access_token_event import RequestAccessTokenEvent
324
+ from xtr_security_oauth2_server_core.request_event import RequestEvent
325
+
326
+
327
+ async def record(event: RequestAccessTokenEvent) -> None:
328
+ print("issued", event.get_access_token().get_identifier())
329
+
330
+
331
+ dispatcher = EventDispatcher()
332
+ dispatcher.add_listener(RequestEvent.ACCESS_TOKEN_ISSUED, record)
333
+ ```
334
+
335
+ ## Errors
336
+
337
+ Everything the library raises derives from `OAuth2Error`
338
+ (`xtr_security_oauth2_server_core.exception`):
339
+
340
+ | Error | Base | Raised when |
341
+ |---|---|---|
342
+ | `OAuthServerError` | `OAuth2Error` | A request the protocol has a refusal for. Built by named constructors — `invalid_request`, `invalid_client`, `invalid_grant`, `invalid_scope`, `access_denied`, `unsupported_grant_type`, `slow_down`, … — and answered with `await generate_http_response(response, use_fragment=False)` |
343
+ | `UniqueTokenIdentifierConstraintViolationError` | `OAuthServerError` | A store met an identifier already taken; a grant retries ten times before it lets this out as a `500` |
344
+ | `InvalidArgumentError` | `OAuth2Error`, `ValueError` | One of the library's objects used wrongly — a reserved extra claim, an unreadable encryption key, a grant asked to work before it was enabled |
345
+
346
+ A refresh token that is invalid, expired or spent is `invalid_grant` with status `400`, as RFC
347
+ 6749 §5.2 names it.
348
+
349
+ ## Layout
350
+
351
+ ```
352
+ xtr_security_oauth2_server_core/
353
+ ├── authorization_server.py AuthorizationServer
354
+ ├── token_server.py TokenServer: revocation and introspection
355
+ ├── resource_server.py ResourceServer
356
+ ├── abstract_handler.py what grants and token handlers share
357
+ ├── crypt_trait.py sealing codes and refresh tokens
358
+ ├── oauth2_grants.py the grant identifiers
359
+ ├── bearer_challenge.py the WWW-Authenticate challenge a bearer refusal carries
360
+ ├── redirect_uri.py carrying parameters to where a browser is sent
361
+ ├── request_event.py, request_access_token_event.py, request_refresh_token_event.py
362
+ ├── grant/ the grants and their interface
363
+ ├── handlers/ the revocation and introspection handlers
364
+ ├── entities/ the entity interfaces, AccessTokenSigner, traits/
365
+ ├── repositories/ the store interfaces
366
+ ├── response_types/ bearer, device code, redirect and introspection answers
367
+ ├── request_types/ AuthorizationRequest
368
+ ├── authorization_validators/ BearerTokenValidator
369
+ ├── code_challenge_verifiers/ S256 and plain
370
+ ├── redirect_uri_validators/ exact matching, loopback ports aside
371
+ └── exception/ OAuth2Error and the errors under it
372
+ ```
373
+
374
+ ## Development
375
+
376
+ Developed in the [python-xtr](https://github.com/xterr/python-xtr) monorepo, under
377
+ `packages/xtr-security-oauth2-server-core`; run the commands below from there.
378
+
379
+ ```sh
380
+ uv sync --all-packages --all-extras
381
+ uv run ruff check && uv run ruff format --check && uv run basedpyright && uv run ty check && uv run pytest --cov
382
+ ```
383
+
384
+ ## License
385
+
386
+ MIT — see [LICENSE](LICENSE).