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.
- xtr_security_oauth2_server_core-4.0.2/LICENSE +22 -0
- xtr_security_oauth2_server_core-4.0.2/PKG-INFO +386 -0
- xtr_security_oauth2_server_core-4.0.2/README.md +360 -0
- xtr_security_oauth2_server_core-4.0.2/pyproject.toml +160 -0
- xtr_security_oauth2_server_core-4.0.2/pyproject.toml.orig +140 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/__init__.py +14 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/_repeated_parameter.py +54 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/abstract_handler.py +435 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/authorization_server.py +316 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/authorization_validators/__init__.py +25 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/authorization_validators/authorization_validator_interface.py +38 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/authorization_validators/bearer_token_validator.py +320 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/authorization_validators/bearer_token_validator_interface.py +46 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/bearer_challenge.py +58 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/code_challenge_verifiers/__init__.py +19 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/code_challenge_verifiers/code_challenge_verifier_interface.py +28 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/code_challenge_verifiers/plain_verifier.py +45 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/code_challenge_verifiers/s256_verifier.py +44 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/crypt_trait.py +210 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/__init__.py +36 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/access_token_entity_interface.py +32 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/access_token_signer.py +63 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/auth_code_entity_interface.py +32 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/client_entity_interface.py +51 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/device_code_entity_interface.py +71 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/refresh_token_entity_interface.py +48 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/scope_entity_interface.py +34 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/token_interface.py +77 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/__init__.py +35 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/access_token_trait.py +158 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/auth_code_trait.py +24 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/client_trait.py +56 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/device_code_trait.py +95 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/entity_trait.py +65 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/refresh_token_trait.py +43 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/scope_trait.py +33 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/traits/token_entity_trait.py +85 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/entities/user_entity_interface.py +22 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/exception/__init__.py +30 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/exception/invalid_argument_error.py +28 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/exception/oauth2_error.py +24 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/exception/oauth_server_error.py +336 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/exception/unique_token_identifier_constraint_violation_error.py +29 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/__init__.py +41 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/abstract_authorize_grant.py +186 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/abstract_grant.py +612 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/auth_code_grant.py +356 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/client_credentials_grant.py +79 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/device_code_grant.py +332 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/grant_type_interface.py +191 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/implicit_grant.py +207 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/password_grant.py +142 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/grant/refresh_token_grant.py +192 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/handlers/__init__.py +31 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/handlers/abstract_token_handler.py +154 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/handlers/token_handler_interface.py +90 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/handlers/token_introspection_handler.py +81 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/handlers/token_revocation_handler.py +110 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/handlers/validated_token.py +45 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/oauth2_grants.py +63 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/py.typed +0 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/redirect_uri.py +45 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/redirect_uri_validators/__init__.py +16 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/redirect_uri_validators/redirect_uri_validator.py +102 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/redirect_uri_validators/redirect_uri_validator_interface.py +21 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/__init__.py +38 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/access_token_repository_interface.py +77 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/auth_code_repository_interface.py +51 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/client_repository_interface.py +51 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/device_code_repository_interface.py +61 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/refresh_token_repository_interface.py +67 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/repository_interface.py +23 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/scope_repository_interface.py +70 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/repositories/user_repository_interface.py +51 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/request_access_token_event.py +47 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/request_event.py +78 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/request_refresh_token_event.py +46 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/request_types/__init__.py +17 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/request_types/authorization_request.py +186 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/request_types/authorization_request_interface.py +108 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/resource_server.py +69 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/response_types/__init__.py +36 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/response_types/abstract_response_type.py +81 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/response_types/bearer_token_response.py +131 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/response_types/device_code_response.py +124 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/response_types/introspection_response.py +232 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/response_types/introspection_response_type_interface.py +44 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/response_types/redirect_response.py +56 -0
- xtr_security_oauth2_server_core-4.0.2/src/xtr_security_oauth2_server_core/response_types/response_type_interface.py +66 -0
- 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).
|