jamlib 0.0.2a0__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.
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Adrian Makridenko
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
13
+ all 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
21
+ THE SOFTWARE.
22
+
23
+
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.3
2
+ Name: jamlib
3
+ Version: 0.0.2a0
4
+ Summary: Simple and univirsal library for authorization
5
+ License: MIT
6
+ Author: Makridenko Adrian
7
+ Author-email: adrianmakridenko@duck.com
8
+ Requires-Python: >=3.13
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Requires-Dist: pydantic (>=2.10.5,<3.0.0)
13
+ Requires-Dist: pydantic-settings (>=2.7.1,<3.0.0)
14
+ Project-URL: Homepage, https://github.com/lyaguxafrog/jam
15
+ Project-URL: Issues, https://github.com/lyaguxafrog/jam/issues
16
+ Project-URL: Repository, https://github.com/lyaguxafrog/jam
17
+ Description-Content-Type: text/markdown
18
+
19
+ # jam
20
+
21
+ ![Static Badge](https://img.shields.io/badge/Python-3.13-blue?logo=python&logoColor=white)
22
+ ![tests](https://github.com/lyaguxafrog/jam/actions/workflows/run-tests.yml/badge.svg) ![License](https://img.shields.io/badge/Licese-MIT-grey?link=https%3A%2F%2Fgithub.com%2Flyaguxafrog%2Fjam%2Fblob%2Frelease%2FLICENSE.md)
23
+
24
+
25
+ > [!CAUTION]
26
+ > In active development! Cannot be used in real projects!
27
+ >
28
+
29
+
30
+ ## Features
31
+ - [ ] JWT Making
32
+ - [ ] Another crypt alghorutms
33
+ - [ ] White/Black lists
34
+ - [ ] Session Maker
35
+ - [ ] Integration with Django, FastAPI, Strawberry
36
+ - [ ] OAuth2
37
+
@@ -0,0 +1,18 @@
1
+ # jam
2
+
3
+ ![Static Badge](https://img.shields.io/badge/Python-3.13-blue?logo=python&logoColor=white)
4
+ ![tests](https://github.com/lyaguxafrog/jam/actions/workflows/run-tests.yml/badge.svg) ![License](https://img.shields.io/badge/Licese-MIT-grey?link=https%3A%2F%2Fgithub.com%2Flyaguxafrog%2Fjam%2Fblob%2Frelease%2FLICENSE.md)
5
+
6
+
7
+ > [!CAUTION]
8
+ > In active development! Cannot be used in real projects!
9
+ >
10
+
11
+
12
+ ## Features
13
+ - [ ] JWT Making
14
+ - [ ] Another crypt alghorutms
15
+ - [ ] White/Black lists
16
+ - [ ] Session Maker
17
+ - [ ] Integration with Django, FastAPI, Strawberry
18
+ - [ ] OAuth2
@@ -0,0 +1,3 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from jam.config import JAMConfig
@@ -0,0 +1,28 @@
1
+ # -*- coding: utf -*-
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic_settings import BaseSettings
6
+
7
+
8
+ class JAMConfig(BaseSettings):
9
+ JWT_ACCESS_SECRET_KEY: str | None
10
+ JWT_REFRESH_SECRET_KEY: str | None
11
+ JWT_ALGORITHM: Literal[
12
+ "HS256",
13
+ "HS384",
14
+ "HS512",
15
+ "RS256",
16
+ "RS384",
17
+ "RS512",
18
+ "ES256",
19
+ "ES384",
20
+ "ES512",
21
+ "PS256",
22
+ "PS384",
23
+ "PS512",
24
+ ] = "HS256"
25
+
26
+ JWT_ACCESS_EXP: int = 3600
27
+ JWT_REFRESH_EXP: int = JWT_ACCESS_EXP
28
+ JWT_HEADERS: dict = {}
@@ -0,0 +1,16 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+
4
+ class JamNullJWTSecret(Exception):
5
+ def __init__(self, message="Secret keys cannot be Null") -> None:
6
+ self.message = message
7
+
8
+
9
+ class JamJWTMakingError(Exception):
10
+ def __init__(self, message) -> None:
11
+ self.message = message
12
+
13
+
14
+ class JamInvalidSignature(Exception):
15
+ def __init__(self, message="Invalid signature") -> None:
16
+ self.message = message
@@ -0,0 +1 @@
1
+ # -*- coding: utf-8 -*-
@@ -0,0 +1,275 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import base64
4
+ import hashlib
5
+ import hmac
6
+ import json
7
+ import logging
8
+ import secrets
9
+ import time
10
+ from typing import Literal
11
+
12
+ from jam.config import JAMConfig
13
+ from jam.jwt.__errors__ import JamInvalidSignature as InvalidSignature
14
+ from jam.jwt.__errors__ import JamJWTMakingError as JWTError
15
+ from jam.jwt.__errors__ import JamNullJWTSecret as NullSecret
16
+ from jam.jwt.types import Tokens
17
+
18
+
19
+ def __check_secrets__(config: JAMConfig) -> bool:
20
+ """
21
+ Private tool for check secrets in confg
22
+
23
+ :param config: Base jam config
24
+ :type config: jam.config.JAMConfig
25
+
26
+ :returns: True if secrets in config
27
+ :rtype: bool
28
+ """
29
+
30
+ if not config.JWT_ACCESS_SECRET_KEY or not config.JWT_REFRESH_SECRET_KEY:
31
+ raise NullSecret
32
+
33
+ else:
34
+ return True
35
+
36
+
37
+ def __gen_access_token__(config: JAMConfig, payload: dict) -> str:
38
+ """
39
+ Private tool for generating access token
40
+
41
+ :param config: Standart jam confg
42
+ :type config: jam.config.JAMConfig
43
+ :param payload: Custom user payload
44
+ :type payload: dict
45
+
46
+ :returns: Returns access token by string
47
+ :rtype: str
48
+ """
49
+
50
+ if not config.JWT_ACCESS_SECRET_KEY:
51
+ raise NullSecret(message="JWT_ACCESS_SECRET_KEY is null")
52
+
53
+ __payload__: dict = {
54
+ "data": payload,
55
+ "exp": int(time.time()) + config.JWT_ACCESS_EXP,
56
+ }
57
+
58
+ encoded_header: str = (
59
+ base64.urlsafe_b64encode(
60
+ json.dumps({"alg": config.JWT_ALGORITHM, "typ": "JWT"}).encode()
61
+ )
62
+ .decode()
63
+ .rstrip("=")
64
+ )
65
+ encoded_payload: str = (
66
+ base64.urlsafe_b64encode(json.dumps(__payload__).encode())
67
+ .decode()
68
+ .rstrip("=")
69
+ )
70
+
71
+ __signature__: bytes = hmac.new(
72
+ config.JWT_ACCESS_SECRET_KEY.encode(),
73
+ f"{encoded_header}.{encoded_payload}".encode(),
74
+ hashlib.sha256,
75
+ ).digest()
76
+ encoded_signature: str = (
77
+ base64.urlsafe_b64encode(__signature__).decode().rstrip("=")
78
+ )
79
+
80
+ access_token: str = (
81
+ f"{encoded_header}.{encoded_payload}.{encoded_signature}"
82
+ )
83
+ return access_token
84
+
85
+
86
+ def __gen_refresh_token__(config: JAMConfig, payload: dict) -> str:
87
+ """
88
+ Private tool for generating refresh token
89
+
90
+ :param config: Standart jam config
91
+ :type config: jam.config.JAMConfig
92
+ :param payload: Custom user payload
93
+ :type payload: dict
94
+
95
+ :returns: Returns refresh roken by string
96
+ :type: str
97
+ """
98
+
99
+ if not config.JWT_REFRESH_SECRET_KEY:
100
+ raise NullSecret(message="JWT_REFRESH_TOKEN is null")
101
+
102
+ __payload__: dict = {
103
+ "data": payload,
104
+ "exp": int(time.time()) + config.JWT_REFRESH_EXP,
105
+ "jti": secrets.token_hex(16),
106
+ }
107
+
108
+ encoded_header: str = (
109
+ base64.urlsafe_b64encode(
110
+ json.dumps({"alg": config.JWT_ALGORITHM, "typ": "JWT"}).encode()
111
+ )
112
+ .decode()
113
+ .rstrip("=")
114
+ )
115
+ encoded_payload: str = (
116
+ base64.urlsafe_b64encode(json.dumps(__payload__).encode())
117
+ .decode()
118
+ .rstrip("=")
119
+ )
120
+
121
+ __signature__: bytes = hmac.new(
122
+ config.JWT_REFRESH_SECRET_KEY.encode(),
123
+ f"{encoded_header}.{encoded_payload}".encode(),
124
+ hashlib.sha256,
125
+ ).digest()
126
+ encoded_signature: str = (
127
+ base64.urlsafe_b64encode(__signature__).decode().rstrip("=")
128
+ )
129
+
130
+ refresh_token: str = (
131
+ f"{encoded_header}.{encoded_payload}.{encoded_signature}"
132
+ )
133
+ return refresh_token
134
+
135
+
136
+ def gen_jwt_tokens(*, config: JAMConfig, payload: dict = {}) -> Tokens:
137
+ """
138
+ Service for generating JWT tokens
139
+
140
+ Example:
141
+ ```
142
+ config = JAMConfig(
143
+ JWT_ACCESS_SECRET_KEY="SOME_SUPER_SECRET_KEY",
144
+ JWT_REFRESH_SECRET_KEY="ANOTHER_SECRET_KEY"
145
+ )
146
+
147
+ payload: dict = {
148
+ "id": 1,
149
+ "username": "lyaguxafrog"
150
+ }
151
+
152
+ tokens = gen_jwt_tokens(config=config, payload=payload)
153
+ ```
154
+
155
+ :param config: Standart jam config
156
+ :type config: jam.config.JAMConfig
157
+ :param payload: Custom user payload
158
+ :type payload: dict
159
+
160
+ :returns: Base model with access and refresh tokens
161
+ :rtype: jam.jwt.types.Tokens
162
+ """
163
+
164
+ try:
165
+ access: str = __gen_access_token__(config, payload)
166
+ refresh: str = __gen_refresh_token__(config, payload)
167
+
168
+ except Exception as e:
169
+ raise JWTError(message=e)
170
+
171
+ return Tokens(access=access, refresh=refresh)
172
+
173
+
174
+ def check_jwt_signature(
175
+ *, config: JAMConfig, token_type: Literal["access", "refresh"], token: str
176
+ ) -> bool:
177
+ """
178
+ Service for checking JWT signature
179
+
180
+ :param config: Base jam config
181
+ :type config: jam.config.JAMConfig
182
+ :param token: JWT token
183
+ :type token: str
184
+ :param key_type: Type of JWT ( access token or refresh token )
185
+ :type key_type: str
186
+
187
+ :returns: Bool with signature status
188
+ :rtype: bool
189
+ """
190
+
191
+ if token_type == "access":
192
+ secret_key: str | None = config.JWT_ACCESS_SECRET_KEY
193
+
194
+ elif token_type == "refresh":
195
+ secret_key: str | None = config.JWT_REFRESH_SECRET_KEY
196
+
197
+ else:
198
+ raise ValueError("Invalid key type. Must be 'access' or 'refresh'.")
199
+
200
+ if not secret_key:
201
+ raise NullSecret("The specified secret key is missing.")
202
+
203
+ try:
204
+ header, payload, signature = token.split(".")
205
+ except ValueError:
206
+ raise ValueError(
207
+ "Invalid token format. Token must have three parts separated by '.'"
208
+ )
209
+
210
+ data_to_sign = f"{header}.{payload}".encode("utf-8")
211
+
212
+ expected_signature = (
213
+ base64.urlsafe_b64encode(
214
+ hmac.new(
215
+ secret_key.encode("utf-8"), data_to_sign, hashlib.sha256
216
+ ).digest()
217
+ )
218
+ .decode("utf-8")
219
+ .rstrip("=")
220
+ )
221
+
222
+ return expected_signature == signature
223
+
224
+
225
+ def decode_token(
226
+ *,
227
+ config: JAMConfig,
228
+ token: str,
229
+ checksum: bool = False,
230
+ checksum_token_type: Literal["access", "refresh"] | None = None,
231
+ ) -> dict:
232
+ """
233
+ Service for decoding JWT token
234
+
235
+ :param config: Base jam config
236
+ :type config: jam.config.JAMConfig
237
+ :param token: Some jwt token
238
+ :type token: str
239
+ :param checksum: Use `check_jwt_signature` in decode?
240
+ :type checksum: bool
241
+ :param checksum_token_type: Type of JWT ( access or refresh )
242
+ :type checksum_token_type: str | None
243
+
244
+ :retutns: Dict with information in token
245
+ :rtype: dict
246
+ """
247
+
248
+ if checksum:
249
+ sum: bool = check_jwt_signature(
250
+ config=config, token_type=checksum_token_type, token=token # type: ignore
251
+ )
252
+ if not sum:
253
+ raise InvalidSignature
254
+ else:
255
+ logging.info("Signature valid")
256
+ pass
257
+ else:
258
+ pass
259
+
260
+ if not config.JWT_ACCESS_SECRET_KEY or not config.JWT_REFRESH_SECRET_KEY:
261
+ raise NullSecret
262
+
263
+ try:
264
+ header, payload, signature = token.split(".")
265
+ except ValueError:
266
+ raise ValueError(
267
+ "Invalid token format. Token must have three parts separated by '.'"
268
+ )
269
+
270
+ try:
271
+ padding: str = "=" * (4 - len(payload) % 4)
272
+ decoded_payload: bytes = base64.urlsafe_b64decode(payload + padding)
273
+ return json.loads(decoded_payload)
274
+ except (ValueError, json.JSONDecodeError) as e:
275
+ raise ValueError("Failed to decode the payload: " + str(e))
@@ -0,0 +1,16 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class Tokens(BaseModel):
8
+ """
9
+ Scop for tokens
10
+
11
+ * access: str
12
+ * refresh: str
13
+ """
14
+
15
+ access: str
16
+ refresh: str
@@ -0,0 +1,79 @@
1
+ [project]
2
+ name = "jamlib"
3
+ version = "0.0.2-alpha"
4
+ description = "Simple and univirsal library for authorization"
5
+ authors = [
6
+ {name = "Makridenko Adrian",email = "adrianmakridenko@duck.com"}
7
+ ]
8
+ license = {text = "MIT"}
9
+ readme = "README.md"
10
+ requires-python = ">=3.13"
11
+ dependencies = [
12
+ "pydantic (>=2.10.5,<3.0.0)",
13
+ "pydantic-settings (>=2.7.1,<3.0.0)"
14
+ ]
15
+
16
+ [project.urls]
17
+ Homepage = "https://github.com/lyaguxafrog/jam"
18
+ Repository = "https://github.com/lyaguxafrog/jam"
19
+ Issues = "https://github.com/lyaguxafrog/jam/issues"
20
+
21
+
22
+ [build-system]
23
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
24
+ build-backend = "poetry.core.masonry.api"
25
+
26
+ [tool.poetry]
27
+ packages = [
28
+ { include = "jam" },
29
+ ]
30
+
31
+ [tool.poetry.group.dev.dependencies]
32
+ pretty-errors = "^1.2.25"
33
+ icecream = "^2.1.4"
34
+ pre-commit = "^4.1.0"
35
+ flake8 = "^7.1.1"
36
+ isort = "^5.13.2"
37
+ black = "^24.10.0"
38
+ mypy = "^1.14.1"
39
+
40
+
41
+ [tool.poetry.group.tests.dependencies]
42
+ pytest = "^8.3.4"
43
+
44
+
45
+ [tool.poetry.group.docs.dependencies]
46
+ mkdocs = "^1.6.1"
47
+ mkdocs-material = "^9.5.50"
48
+ mkdocstrings = "^0.27.0"
49
+ mkdocstrings-python = "^1.13.0"
50
+
51
+
52
+ [tool.flake8]
53
+ ignore = ["D203", "E501"]
54
+ exclude = [".git", "__pycache__", "__init__.py", "docs/"]
55
+ max-complexity = 18
56
+ max-line-length = 80
57
+
58
+ [tool.isort]
59
+ profile = "black"
60
+ default_section = "THIRDPARTY"
61
+ balanced_wrapping = true
62
+ known_first_party = "src"
63
+ line_length = 80
64
+ lines_after_imports = 2
65
+ lines_between_sections = 1
66
+ multi_line_output = 3
67
+ sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
68
+ skip = ["env", ".env", ".env.example"]
69
+
70
+ [tool.black]
71
+ line-length = 80
72
+ skip-string-normalization = false
73
+
74
+ [tool.mypy]
75
+ disable_error_code = ["no-redef", "import-not-found"]
76
+
77
+ [tool.pytest]
78
+ asyncio_default_fixture_loop_scope = "function"
79
+ python_files = ["test*.py"]