auth0-api-python 1.0.0b1__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.
- auth0_api_python-1.0.0b1/LICENSE +21 -0
- auth0_api_python-1.0.0b1/PKG-INFO +134 -0
- auth0_api_python-1.0.0b1/README.md +115 -0
- auth0_api_python-1.0.0b1/pyproject.toml +31 -0
- auth0_api_python-1.0.0b1/src/__init__.py +14 -0
- auth0_api_python-1.0.0b1/src/api_client.py +128 -0
- auth0_api_python-1.0.0b1/src/config.py +24 -0
- auth0_api_python-1.0.0b1/src/errors.py +21 -0
- auth0_api_python-1.0.0b1/src/token_utils.py +84 -0
- auth0_api_python-1.0.0b1/src/utils.py +88 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Auth0, Inc. <support@auth0.com> (http://auth0.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: auth0-api-python
|
|
3
|
+
Version: 1.0.0b1
|
|
4
|
+
Summary: SDK for verifying access tokens and securing APIs with Auth0, using Authlib.
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Snehil Kishore snehil.kishore@okta.com
|
|
7
|
+
Requires-Python: >=3.9,<4.0
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Dist: authlib (>=1.0,<2.0)
|
|
16
|
+
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
17
|
+
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
The `auth0-api-python` library allows you to secure APIs running on Python, particularly for verifying Auth0-issued access tokens.
|
|
21
|
+
|
|
22
|
+
It’s intended as a foundation for building more framework-specific integrations (e.g., with FastAPI, Django, etc.), but you can also use it directly in any Python server-side environment.
|
|
23
|
+
|
|
24
|
+
  [](https://opensource.org/licenses/MIT)
|
|
25
|
+
|
|
26
|
+
📚 [Documentation](#documentation) - 🚀 [Getting Started](#getting-started) - 💬 [Feedback](#feedback)
|
|
27
|
+
|
|
28
|
+
## Documentation
|
|
29
|
+
|
|
30
|
+
- [Docs Site](https://auth0.com/docs) - explore our docs site and learn more about Auth0.
|
|
31
|
+
|
|
32
|
+
## Getting Started
|
|
33
|
+
|
|
34
|
+
### 1. Install the SDK
|
|
35
|
+
|
|
36
|
+
_This library requires Python 3.9+._
|
|
37
|
+
|
|
38
|
+
```shell
|
|
39
|
+
pip install auth0-api-python
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If you’re using Poetry:
|
|
43
|
+
|
|
44
|
+
```shell
|
|
45
|
+
poetry install auth0-api-python
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Create the Auth0 SDK client
|
|
49
|
+
|
|
50
|
+
Create an instance of the `ApiClient`. This instance will be imported and used anywhere we need access to the methods.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from auth0_api_python import ApiClient, ApiClientOptions
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
api_client = ApiClient(ApiClientOptions(
|
|
57
|
+
domain="<AUTH0_DOMAIN>",
|
|
58
|
+
audience="<AUTH0_AUDIENCE>"
|
|
59
|
+
))
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- The `AUTH0_DOMAIN` can be obtained from the [Auth0 Dashboard](https://manage.auth0.com) once you've created an application.
|
|
63
|
+
- The `AUTH0_AUDIENCE` is the identifier of the API. You can find this in the [APIs section of the Auth0 Dashboard](https://manage.auth0.com/#/apis/).
|
|
64
|
+
|
|
65
|
+
### 3. Verify the Access Token
|
|
66
|
+
|
|
67
|
+
Use the `verify_access_token` method to validate access tokens. The method automatically checks critical claims like `iss`, `aud`, `exp`, `nbf`.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import asyncio
|
|
71
|
+
|
|
72
|
+
from auth0_api_python import ApiClient, ApiClientOptions
|
|
73
|
+
|
|
74
|
+
async def main():
|
|
75
|
+
api_client = ApiClient(ApiClientOptions(
|
|
76
|
+
domain="<AUTH0_DOMAIN>",
|
|
77
|
+
audience="<AUTH0_AUDIENCE>"
|
|
78
|
+
))
|
|
79
|
+
access_token = "..."
|
|
80
|
+
|
|
81
|
+
decoded_and_verified_token = await api_client.verify_access_token(access_token=access_token)
|
|
82
|
+
print(decoded_and_verified_token)
|
|
83
|
+
|
|
84
|
+
asyncio.run(main())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
In this example, the returned dictionary contains the decoded claims (like `sub`, `scope`, etc.) from the verified token.
|
|
88
|
+
|
|
89
|
+
#### Requiring Additional Claims
|
|
90
|
+
|
|
91
|
+
If your application demands extra claims, specify them with `required_claims`:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
decoded_and_verified_token = await api_client.verify_access_token(
|
|
95
|
+
access_token=access_token,
|
|
96
|
+
required_claims=["my_custom_claim"]
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
If the token lacks `my_custom_claim` or fails any standard check (issuer mismatch, expired token, invalid signature), the method raises a `VerifyAccessTokenError`.
|
|
101
|
+
|
|
102
|
+
## Feedback
|
|
103
|
+
|
|
104
|
+
### Contributing
|
|
105
|
+
|
|
106
|
+
We appreciate feedback and contribution to this repo! Before you get started, please read the following:
|
|
107
|
+
|
|
108
|
+
- [Auth0's general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
|
|
109
|
+
- [Auth0's code of conduct guidelines](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
|
|
110
|
+
- [This repo's contribution guide](./../../CONTRIBUTING.md)
|
|
111
|
+
|
|
112
|
+
### Raise an issue
|
|
113
|
+
|
|
114
|
+
To provide feedback or report a bug, please [raise an issue on our issue tracker](https://github.com/auth0/auth0-server-python/issues).
|
|
115
|
+
|
|
116
|
+
## Vulnerability Reporting
|
|
117
|
+
|
|
118
|
+
Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues.
|
|
119
|
+
|
|
120
|
+
## What is Auth0?
|
|
121
|
+
|
|
122
|
+
<p align="center">
|
|
123
|
+
<picture>
|
|
124
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.auth0.com/website/sdks/logos/auth0_dark_mode.png" width="150">
|
|
125
|
+
<source media="(prefers-color-scheme: light)" srcset="https://cdn.auth0.com/website/sdks/logos/auth0_light_mode.png" width="150">
|
|
126
|
+
<img alt="Auth0 Logo" src="https://cdn.auth0.com/website/sdks/logos/auth0_light_mode.png" width="150">
|
|
127
|
+
</picture>
|
|
128
|
+
</p>
|
|
129
|
+
<p align="center">
|
|
130
|
+
Auth0 is an easy to implement, adaptable authentication and authorization platform. To learn more checkout <a href="https://auth0.com/why-auth0">Why Auth0?</a>
|
|
131
|
+
</p>
|
|
132
|
+
<p align="center">
|
|
133
|
+
This project is licensed under the MIT license. See the <a href="https://github.com/auth0/auth0-server-python/blob/main/packages/auth0_api_python/LICENSE"> LICENSE</a> file for more info.
|
|
134
|
+
</p>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
The `auth0-api-python` library allows you to secure APIs running on Python, particularly for verifying Auth0-issued access tokens.
|
|
2
|
+
|
|
3
|
+
It’s intended as a foundation for building more framework-specific integrations (e.g., with FastAPI, Django, etc.), but you can also use it directly in any Python server-side environment.
|
|
4
|
+
|
|
5
|
+
  [](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
📚 [Documentation](#documentation) - 🚀 [Getting Started](#getting-started) - 💬 [Feedback](#feedback)
|
|
8
|
+
|
|
9
|
+
## Documentation
|
|
10
|
+
|
|
11
|
+
- [Docs Site](https://auth0.com/docs) - explore our docs site and learn more about Auth0.
|
|
12
|
+
|
|
13
|
+
## Getting Started
|
|
14
|
+
|
|
15
|
+
### 1. Install the SDK
|
|
16
|
+
|
|
17
|
+
_This library requires Python 3.9+._
|
|
18
|
+
|
|
19
|
+
```shell
|
|
20
|
+
pip install auth0-api-python
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
If you’re using Poetry:
|
|
24
|
+
|
|
25
|
+
```shell
|
|
26
|
+
poetry install auth0-api-python
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. Create the Auth0 SDK client
|
|
30
|
+
|
|
31
|
+
Create an instance of the `ApiClient`. This instance will be imported and used anywhere we need access to the methods.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from auth0_api_python import ApiClient, ApiClientOptions
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
api_client = ApiClient(ApiClientOptions(
|
|
38
|
+
domain="<AUTH0_DOMAIN>",
|
|
39
|
+
audience="<AUTH0_AUDIENCE>"
|
|
40
|
+
))
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- The `AUTH0_DOMAIN` can be obtained from the [Auth0 Dashboard](https://manage.auth0.com) once you've created an application.
|
|
44
|
+
- The `AUTH0_AUDIENCE` is the identifier of the API. You can find this in the [APIs section of the Auth0 Dashboard](https://manage.auth0.com/#/apis/).
|
|
45
|
+
|
|
46
|
+
### 3. Verify the Access Token
|
|
47
|
+
|
|
48
|
+
Use the `verify_access_token` method to validate access tokens. The method automatically checks critical claims like `iss`, `aud`, `exp`, `nbf`.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import asyncio
|
|
52
|
+
|
|
53
|
+
from auth0_api_python import ApiClient, ApiClientOptions
|
|
54
|
+
|
|
55
|
+
async def main():
|
|
56
|
+
api_client = ApiClient(ApiClientOptions(
|
|
57
|
+
domain="<AUTH0_DOMAIN>",
|
|
58
|
+
audience="<AUTH0_AUDIENCE>"
|
|
59
|
+
))
|
|
60
|
+
access_token = "..."
|
|
61
|
+
|
|
62
|
+
decoded_and_verified_token = await api_client.verify_access_token(access_token=access_token)
|
|
63
|
+
print(decoded_and_verified_token)
|
|
64
|
+
|
|
65
|
+
asyncio.run(main())
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
In this example, the returned dictionary contains the decoded claims (like `sub`, `scope`, etc.) from the verified token.
|
|
69
|
+
|
|
70
|
+
#### Requiring Additional Claims
|
|
71
|
+
|
|
72
|
+
If your application demands extra claims, specify them with `required_claims`:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
decoded_and_verified_token = await api_client.verify_access_token(
|
|
76
|
+
access_token=access_token,
|
|
77
|
+
required_claims=["my_custom_claim"]
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
If the token lacks `my_custom_claim` or fails any standard check (issuer mismatch, expired token, invalid signature), the method raises a `VerifyAccessTokenError`.
|
|
82
|
+
|
|
83
|
+
## Feedback
|
|
84
|
+
|
|
85
|
+
### Contributing
|
|
86
|
+
|
|
87
|
+
We appreciate feedback and contribution to this repo! Before you get started, please read the following:
|
|
88
|
+
|
|
89
|
+
- [Auth0's general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
|
|
90
|
+
- [Auth0's code of conduct guidelines](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
|
|
91
|
+
- [This repo's contribution guide](./../../CONTRIBUTING.md)
|
|
92
|
+
|
|
93
|
+
### Raise an issue
|
|
94
|
+
|
|
95
|
+
To provide feedback or report a bug, please [raise an issue on our issue tracker](https://github.com/auth0/auth0-server-python/issues).
|
|
96
|
+
|
|
97
|
+
## Vulnerability Reporting
|
|
98
|
+
|
|
99
|
+
Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues.
|
|
100
|
+
|
|
101
|
+
## What is Auth0?
|
|
102
|
+
|
|
103
|
+
<p align="center">
|
|
104
|
+
<picture>
|
|
105
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.auth0.com/website/sdks/logos/auth0_dark_mode.png" width="150">
|
|
106
|
+
<source media="(prefers-color-scheme: light)" srcset="https://cdn.auth0.com/website/sdks/logos/auth0_light_mode.png" width="150">
|
|
107
|
+
<img alt="Auth0 Logo" src="https://cdn.auth0.com/website/sdks/logos/auth0_light_mode.png" width="150">
|
|
108
|
+
</picture>
|
|
109
|
+
</p>
|
|
110
|
+
<p align="center">
|
|
111
|
+
Auth0 is an easy to implement, adaptable authentication and authorization platform. To learn more checkout <a href="https://auth0.com/why-auth0">Why Auth0?</a>
|
|
112
|
+
</p>
|
|
113
|
+
<p align="center">
|
|
114
|
+
This project is licensed under the MIT license. See the <a href="https://github.com/auth0/auth0-server-python/blob/main/packages/auth0_api_python/LICENSE"> LICENSE</a> file for more info.
|
|
115
|
+
</p>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "auth0-api-python"
|
|
3
|
+
version = "1.0.0.b1"
|
|
4
|
+
description = "SDK for verifying access tokens and securing APIs with Auth0, using Authlib."
|
|
5
|
+
authors = ["Snehil Kishore snehil.kishore@okta.com"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
|
|
9
|
+
packages = [
|
|
10
|
+
{ include = "src" }
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[tool.poetry.dependencies]
|
|
14
|
+
python = "^3.9"
|
|
15
|
+
authlib = "^1.0" # For JWT/OIDC features
|
|
16
|
+
requests = "^2.31.0" # If you use requests for HTTP calls (e.g., discovery)
|
|
17
|
+
httpx = "^0.28.1"
|
|
18
|
+
|
|
19
|
+
[tool.poetry.group.dev.dependencies]
|
|
20
|
+
pytest = "^8.0"
|
|
21
|
+
pytest-cov = "^4.0"
|
|
22
|
+
pytest-asyncio = "^0.20.3"
|
|
23
|
+
pytest-mock = "^3.14.0"
|
|
24
|
+
pytest-httpx = "^0.35.0"
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
addopts = "--cov=src --cov-report=term-missing:skip-covered --cov-report=xml"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["poetry-core>=1.0.0"]
|
|
31
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
auth0-api-python
|
|
3
|
+
|
|
4
|
+
A lightweight Python SDK for verifying Auth0-issued access tokens
|
|
5
|
+
in server-side APIs, using Authlib for OIDC discovery and JWKS fetching.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .api_client import ApiClient
|
|
9
|
+
from .config import ApiClientOptions
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ApiClient",
|
|
13
|
+
"ApiClientOptions"
|
|
14
|
+
]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Optional, List, Dict, Any
|
|
3
|
+
|
|
4
|
+
from authlib.jose import JsonWebToken, JsonWebKey
|
|
5
|
+
|
|
6
|
+
from .config import ApiClientOptions
|
|
7
|
+
from .errors import MissingRequiredArgumentError, VerifyAccessTokenError
|
|
8
|
+
from .utils import fetch_oidc_metadata, fetch_jwks, get_unverified_header
|
|
9
|
+
|
|
10
|
+
class ApiClient:
|
|
11
|
+
"""
|
|
12
|
+
The main class for discovering OIDC metadata (issuer, jwks_uri) and verifying
|
|
13
|
+
Auth0-issued JWT access tokens in an async environment.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, options: ApiClientOptions):
|
|
17
|
+
|
|
18
|
+
if not options.domain:
|
|
19
|
+
raise MissingRequiredArgumentError("domain")
|
|
20
|
+
if not options.audience:
|
|
21
|
+
raise MissingRequiredArgumentError("audience")
|
|
22
|
+
|
|
23
|
+
self.options = options
|
|
24
|
+
self._metadata: Optional[Dict[str, Any]] = None
|
|
25
|
+
self._jwks_data: Optional[Dict[str, Any]] = None
|
|
26
|
+
|
|
27
|
+
self._jwt = JsonWebToken(["RS256"])
|
|
28
|
+
|
|
29
|
+
async def _discover(self) -> Dict[str, Any]:
|
|
30
|
+
"""Lazy-load OIDC discovery metadata."""
|
|
31
|
+
if self._metadata is None:
|
|
32
|
+
self._metadata = await fetch_oidc_metadata(
|
|
33
|
+
domain=self.options.domain,
|
|
34
|
+
custom_fetch=self.options.custom_fetch
|
|
35
|
+
)
|
|
36
|
+
return self._metadata
|
|
37
|
+
|
|
38
|
+
async def _load_jwks(self) -> Dict[str, Any]:
|
|
39
|
+
"""Fetches and caches JWKS data from the OIDC metadata."""
|
|
40
|
+
if self._jwks_data is None:
|
|
41
|
+
metadata = await self._discover()
|
|
42
|
+
jwks_uri = metadata["jwks_uri"]
|
|
43
|
+
self._jwks_data = await fetch_jwks(
|
|
44
|
+
jwks_uri=jwks_uri,
|
|
45
|
+
custom_fetch=self.options.custom_fetch
|
|
46
|
+
)
|
|
47
|
+
return self._jwks_data
|
|
48
|
+
|
|
49
|
+
async def verify_access_token(
|
|
50
|
+
self,
|
|
51
|
+
access_token: str,
|
|
52
|
+
required_claims: Optional[List[str]] = None
|
|
53
|
+
) -> Dict[str, Any]:
|
|
54
|
+
"""
|
|
55
|
+
Asynchronously verifies the provided JWT access token.
|
|
56
|
+
|
|
57
|
+
- Fetches OIDC metadata and JWKS if not already cached.
|
|
58
|
+
- Decodes and validates signature (RS256) with the correct key.
|
|
59
|
+
- Checks standard claims: 'iss', 'aud', 'exp', 'iat'
|
|
60
|
+
- Checks extra required claims if 'required_claims' is provided.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The decoded token claims if valid.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
MissingRequiredArgumentError: If no token is provided.
|
|
67
|
+
VerifyAccessTokenError: If verification fails (signature, claims mismatch, etc.).
|
|
68
|
+
"""
|
|
69
|
+
if not access_token:
|
|
70
|
+
raise MissingRequiredArgumentError("access_token")
|
|
71
|
+
|
|
72
|
+
required_claims = required_claims or []
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
header = await get_unverified_header(access_token)
|
|
77
|
+
kid = header["kid"]
|
|
78
|
+
except Exception as e:
|
|
79
|
+
raise VerifyAccessTokenError(f"Failed to parse token header: {str(e)}") from e
|
|
80
|
+
|
|
81
|
+
jwks_data = await self._load_jwks()
|
|
82
|
+
matching_key_dict = None
|
|
83
|
+
for key_dict in jwks_data["keys"]:
|
|
84
|
+
if key_dict.get("kid") == kid:
|
|
85
|
+
matching_key_dict = key_dict
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
if not matching_key_dict:
|
|
89
|
+
raise VerifyAccessTokenError(f"No matching key found for kid: {kid}")
|
|
90
|
+
|
|
91
|
+
public_key = JsonWebKey.import_key(matching_key_dict)
|
|
92
|
+
|
|
93
|
+
if isinstance(access_token, str) and access_token.startswith("b'"):
|
|
94
|
+
access_token = access_token[2:-1]
|
|
95
|
+
try:
|
|
96
|
+
claims = self._jwt.decode(access_token, public_key)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
raise VerifyAccessTokenError(f"Signature verification failed: {str(e)}") from e
|
|
99
|
+
|
|
100
|
+
metadata = await self._discover()
|
|
101
|
+
issuer = metadata["issuer"]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if claims.get("iss") != issuer:
|
|
105
|
+
raise VerifyAccessTokenError("Issuer mismatch")
|
|
106
|
+
|
|
107
|
+
expected_aud = self.options.audience
|
|
108
|
+
actual_aud = claims.get("aud")
|
|
109
|
+
|
|
110
|
+
if isinstance(actual_aud, list):
|
|
111
|
+
if expected_aud not in actual_aud:
|
|
112
|
+
raise VerifyAccessTokenError("Audience mismatch (not in token's aud array)")
|
|
113
|
+
else:
|
|
114
|
+
if actual_aud != expected_aud:
|
|
115
|
+
raise VerifyAccessTokenError("Audience mismatch (single aud)")
|
|
116
|
+
|
|
117
|
+
now = int(time.time())
|
|
118
|
+
if "exp" not in claims or now >= claims["exp"]:
|
|
119
|
+
raise VerifyAccessTokenError("Token is expired")
|
|
120
|
+
if "iat" not in claims:
|
|
121
|
+
raise VerifyAccessTokenError("Missing 'iat' claim in token")
|
|
122
|
+
|
|
123
|
+
#Additional required_claims
|
|
124
|
+
for rc in required_claims:
|
|
125
|
+
if rc not in claims:
|
|
126
|
+
raise VerifyAccessTokenError(f"Missing required claim: {rc}")
|
|
127
|
+
|
|
128
|
+
return claims
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration classes and utilities for auth0-api-python.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Callable
|
|
6
|
+
|
|
7
|
+
class ApiClientOptions:
|
|
8
|
+
"""
|
|
9
|
+
Configuration for the ApiClient.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
domain: The Auth0 domain, e.g., "my-tenant.us.auth0.com".
|
|
13
|
+
audience: The expected 'aud' claim in the token.
|
|
14
|
+
custom_fetch: Optional callable that can replace the default HTTP fetch logic.
|
|
15
|
+
"""
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
domain: str,
|
|
19
|
+
audience: str,
|
|
20
|
+
custom_fetch: Optional[Callable[..., object]] = None
|
|
21
|
+
):
|
|
22
|
+
self.domain = domain
|
|
23
|
+
self.audience = audience
|
|
24
|
+
self.custom_fetch = custom_fetch
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for auth0-api-python SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
class MissingRequiredArgumentError(Exception):
|
|
6
|
+
"""Error raised when a required argument is missing."""
|
|
7
|
+
code = "missing_required_argument_error"
|
|
8
|
+
|
|
9
|
+
def __init__(self, argument: str):
|
|
10
|
+
super().__init__(f"The argument '{argument}' is required but was not provided.")
|
|
11
|
+
self.argument = argument
|
|
12
|
+
self.name = self.__class__.__name__
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class VerifyAccessTokenError(Exception):
|
|
16
|
+
"""Error raised when verifying the access token fails."""
|
|
17
|
+
code = "verify_access_token_error"
|
|
18
|
+
|
|
19
|
+
def __init__(self, message: str):
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.name = self.__class__.__name__
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Optional, Dict, Any, Union
|
|
3
|
+
from authlib.jose import JsonWebKey, jwt
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# A private RSA JWK for test usage.
|
|
7
|
+
|
|
8
|
+
PRIVATE_JWK = {
|
|
9
|
+
"kty": "RSA",
|
|
10
|
+
"alg": "RS256",
|
|
11
|
+
"use": "sig",
|
|
12
|
+
"kid": "TEST_KEY",
|
|
13
|
+
"n": "whYOFK2Ocbbpb_zVypi9SeKiNUqKQH0zTKN1-6fpCTu6ZalGI82s7XK3tan4dJt90ptUPKD2zvxqTzFNfx4HHHsrYCf2-FMLn1VTJfQazA2BvJqAwcpW1bqRUEty8tS_Yv4hRvWfQPcc2Gc3-_fQOOW57zVy-rNoJc744kb30NjQxdGp03J2S3GLQu7oKtSDDPooQHD38PEMNnITf0pj-KgDPjymkMGoJlO3aKppsjfbt_AH6GGdRghYRLOUwQU-h-ofWHR3lbYiKtXPn5dN24kiHy61e3VAQ9_YAZlwXC_99GGtw_NpghFAuM4P1JDn0DppJldy3PGFC0GfBCZASw",
|
|
14
|
+
"e": "AQAB",
|
|
15
|
+
"d": "VuVE_KEP6323WjpbBdAIv7HGahGrgGANvbxZsIhm34lsVOPK0XDegZkhAybMZHjRhp-gwVxX5ChC-J3cUpOBH5FNxElgW6HizD2Jcq6t6LoLYgPSrfEHm71iHg8JsgrqfUnGYFzMJmv88C6WdCtpgG_qJV1K00_Ly1G1QKoBffEs-v4fAMJrCbUdCz1qWto-PU-HLMEo-krfEpGgcmtZeRlDADh8cETMQlgQfQX2VWq_aAP4a1SXmo-j0cvRU4W5Fj0RVwNesIpetX2ZFz4p_JmB5sWFEj_fC7h5z2lq-6Bme2T3BHtXkIxoBW0_pYVnASC8P2puO5FnVxDmWuHDYQ",
|
|
16
|
+
"p": "07rgXd_tLUhVRF_g1OaqRZh5uZ8hiLWUSU0vu9coOaQcatSqjQlIwLW8UdKv_38GrmpIfgcEVQjzq6rFBowUm9zWBO9Eq6enpasYJBOeD8EMeDK-nsST57HjPVOCvoVC5ZX-cozPXna3iRNZ1TVYBY3smn0IaxysIK-zxESf4pM",
|
|
17
|
+
"q": "6qrE9TPhCS5iNR7QrKThunLu6t4H_8CkYRPLbvOIt2MgZyPLiZCsvdkTVSOX76QQEXt7Y0nTNua69q3K3Jhf-YOkPSJsWTxgrfOnjoDvRKzbW3OExIMm7D99fVBODuNWinjYgUwGSqGAsb_3TKhtI-Gr5ls3fn6B6oEjVL0dpmk",
|
|
18
|
+
"dp": "mHqjrFdgelT2OyiFRS3dAAPf3cLxJoAGC4gP0UoQyPocEP-Y17sQ7t-ygIanguubBy65iDFLeGXa_g0cmSt2iAzRAHrDzI8P1-pQl2KdWSEg9ssspjBRh_F_AiJLLSPRWn_b3-jySkhawtfxwO8Kte1QsK1My765Y0zFvJnjPws",
|
|
19
|
+
"dq": "KmjaV4YcsVAUp4z-IXVa5htHWmLuByaFjpXJOjABEUN0467wZdgjn9vPRp-8Ia8AyGgMkJES_uUL_PDDrMJM9gb4c6P4-NeUkVtreLGMjFjA-_IQmIMrUZ7XywHsWXx0c2oLlrJqoKo3W-hZhR0bPFTYgDUT_mRWjk7wV6wl46E",
|
|
20
|
+
"qi": "iYltkV_4PmQDfZfGFpzn2UtYEKyhy-9t3Vy8Mw2VHLAADKGwJvVK5ficQAr2atIF1-agXY2bd6KV-w52zR8rmZfTr0gobzYIyqHczOm13t7uXJv2WygY7QEC2OGjdxa2Fr9RnvS99ozMa5nomZBqTqT7z5QV33czjPRCjvg6FcE",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def generate_token(
|
|
25
|
+
domain: str,
|
|
26
|
+
user_id: str,
|
|
27
|
+
audience: Optional[str] = None,
|
|
28
|
+
issuer: Union[str, bool, None] = None,
|
|
29
|
+
iat: bool = True,
|
|
30
|
+
exp: bool = True,
|
|
31
|
+
claims: Optional[Dict[str, Any]] = None,
|
|
32
|
+
expiration_time: int = 3600,
|
|
33
|
+
) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Generates a real RS256-signed JWT using the private key above.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
domain: The Auth0 domain (used if issuer is not False).
|
|
39
|
+
user_id: The 'sub' claim in the token.
|
|
40
|
+
audience: The 'aud' claim in the token. If omitted, 'aud' won't be included.
|
|
41
|
+
issuer:
|
|
42
|
+
- If a string, it's placed in 'iss' claim.
|
|
43
|
+
- If None, default is f"https://{domain}/".
|
|
44
|
+
- If False, skip 'iss' claim entirely.
|
|
45
|
+
iat: Whether to set the 'iat' (issued at) claim. If False, skip it.
|
|
46
|
+
exp: Whether to set the 'exp' claim. If False, skip it.
|
|
47
|
+
claims: Additional custom claims to merge into the token.
|
|
48
|
+
expiration_time: If exp is True, how many seconds from now until expiration.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
A RS256-signed JWT string.
|
|
52
|
+
|
|
53
|
+
Example usage:
|
|
54
|
+
token = generate_token(
|
|
55
|
+
domain="example.us.auth0.com",
|
|
56
|
+
user_id="user123",
|
|
57
|
+
audience="my-api",
|
|
58
|
+
issuer=False,
|
|
59
|
+
iat=False,
|
|
60
|
+
exp=False,
|
|
61
|
+
claims={"scope": "read:stuff"}
|
|
62
|
+
)
|
|
63
|
+
"""
|
|
64
|
+
token_claims = dict(claims or {})
|
|
65
|
+
token_claims.setdefault("sub", user_id)
|
|
66
|
+
|
|
67
|
+
if iat:
|
|
68
|
+
token_claims["iat"] = int(time.time())
|
|
69
|
+
|
|
70
|
+
if exp:
|
|
71
|
+
token_claims["exp"] = int(time.time()) + expiration_time
|
|
72
|
+
|
|
73
|
+
if issuer is not False:
|
|
74
|
+
token_claims["iss"] = issuer if isinstance(issuer, str) else f"https://{domain}/"
|
|
75
|
+
|
|
76
|
+
if audience:
|
|
77
|
+
token_claims["aud"] = audience
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
key = JsonWebKey.import_key(PRIVATE_JWK)
|
|
81
|
+
|
|
82
|
+
header = {"alg": "RS256", "kid": PRIVATE_JWK["kid"]}
|
|
83
|
+
token = jwt.encode(header, token_claims, key)
|
|
84
|
+
return token
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for OIDC discovery and JWKS fetching (asynchronously)
|
|
3
|
+
using httpx or a custom fetch approach.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import base64
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, Dict, Optional, Callable, Union
|
|
10
|
+
|
|
11
|
+
async def fetch_oidc_metadata(
|
|
12
|
+
domain: str,
|
|
13
|
+
custom_fetch: Optional[Callable[..., Any]] = None
|
|
14
|
+
) -> Dict[str, Any]:
|
|
15
|
+
"""
|
|
16
|
+
Asynchronously fetch the OIDC config from https://{domain}/.well-known/openid-configuration.
|
|
17
|
+
Returns a dict with keys like issuer, jwks_uri, authorization_endpoint, etc.
|
|
18
|
+
If custom_fetch is provided, we call it instead of httpx.
|
|
19
|
+
"""
|
|
20
|
+
url = f"https://{domain}/.well-known/openid-configuration"
|
|
21
|
+
if custom_fetch:
|
|
22
|
+
response = await custom_fetch(url)
|
|
23
|
+
return response.json() if hasattr(response, "json") else response
|
|
24
|
+
else:
|
|
25
|
+
async with httpx.AsyncClient() as client:
|
|
26
|
+
resp = await client.get(url)
|
|
27
|
+
resp.raise_for_status()
|
|
28
|
+
return resp.json()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def fetch_jwks(
|
|
32
|
+
jwks_uri: str,
|
|
33
|
+
custom_fetch: Optional[Callable[..., Any]] = None
|
|
34
|
+
) -> Dict[str, Any]:
|
|
35
|
+
"""
|
|
36
|
+
Asynchronously fetch the JSON Web Key Set from jwks_uri.
|
|
37
|
+
Returns the raw JWKS JSON, e.g. {'keys': [...]}
|
|
38
|
+
|
|
39
|
+
If custom_fetch is provided, it must be an async callable
|
|
40
|
+
that fetches data from the jwks_uri.
|
|
41
|
+
"""
|
|
42
|
+
if custom_fetch:
|
|
43
|
+
response = await custom_fetch(jwks_uri)
|
|
44
|
+
return response.json() if hasattr(response, "json") else response
|
|
45
|
+
else:
|
|
46
|
+
async with httpx.AsyncClient() as client:
|
|
47
|
+
resp = await client.get(jwks_uri)
|
|
48
|
+
resp.raise_for_status()
|
|
49
|
+
return resp.json()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def get_unverified_header(token: Union[str, bytes]) -> dict:
|
|
53
|
+
"""
|
|
54
|
+
Parse the first segment (header) of a JWT without verifying signature.
|
|
55
|
+
Ensures correct Base64 padding before decode to avoid garbage bytes.
|
|
56
|
+
"""
|
|
57
|
+
if isinstance(token, bytes):
|
|
58
|
+
token = token.decode("utf-8")
|
|
59
|
+
try:
|
|
60
|
+
header_b64, _, _ = token.split(".", 2)
|
|
61
|
+
except ValueError:
|
|
62
|
+
raise ValueError("Not enough segments in token")
|
|
63
|
+
|
|
64
|
+
header_b64 = remove_bytes_prefix(header_b64)
|
|
65
|
+
|
|
66
|
+
header_b64 = fix_base64_padding(header_b64)
|
|
67
|
+
|
|
68
|
+
header_data = base64.urlsafe_b64decode(header_b64)
|
|
69
|
+
return json.loads(header_data)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def fix_base64_padding(segment: str) -> str:
|
|
74
|
+
"""
|
|
75
|
+
If `segment`'s length is not a multiple of 4, add '=' padding
|
|
76
|
+
so that base64.urlsafe_b64decode won't produce nonsense bytes.
|
|
77
|
+
No extra '=' added if length is already a multiple of 4.
|
|
78
|
+
"""
|
|
79
|
+
remainder = len(segment) % 4
|
|
80
|
+
if remainder == 0:
|
|
81
|
+
return segment # No additional padding needed
|
|
82
|
+
return segment + ("=" * (4 - remainder))
|
|
83
|
+
|
|
84
|
+
def remove_bytes_prefix(s: str) -> str:
|
|
85
|
+
"""If the string looks like b'eyJh...', remove the leading b' and trailing '."""
|
|
86
|
+
if s.startswith("b'"):
|
|
87
|
+
return s[2:] # cut off the leading b'
|
|
88
|
+
return s
|