xn-auth 0.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.
@@ -0,0 +1 @@
1
+ VENV=venv
@@ -0,0 +1,7 @@
1
+ /.idea
2
+ /venv
3
+ .env
4
+ /*.egg-info
5
+ /dist
6
+ *.pyc
7
+ /build
@@ -0,0 +1,36 @@
1
+ repos:
2
+ - repo: local
3
+ hooks:
4
+ - id: tag
5
+ name: tag
6
+ ### make tag with next ver only if "fix" in commit_msg or starts with "feat"
7
+ entry: bash -c 'grep -e ^feat -e fix .git/COMMIT_EDITMSG && make patch || exit 0'
8
+ language: system
9
+ verbose: true
10
+ pass_filenames: false
11
+ always_run: true
12
+ stages: [post-commit]
13
+
14
+ - id: build
15
+ name: build
16
+ ### build & upload package only for "main" branch push
17
+ entry: bash -c 'echo $PRE_COMMIT_LOCAL_BRANCH | grep /master && make build || echo 0'
18
+ language: system
19
+ pass_filenames: false
20
+ verbose: true
21
+ require_serial: true
22
+ stages: [pre-push]
23
+
24
+ - repo: https://github.com/astral-sh/ruff-pre-commit
25
+ ### Ruff version.
26
+ rev: v0.6.4
27
+ hooks:
28
+ ### Run the linter.
29
+ - id: ruff
30
+ args: [--fix, --unsafe-fixes]
31
+ stages: [pre-commit]
32
+ ### Run the formatter.
33
+ - id: ruff-format
34
+ types_or: [python, pyi]
35
+ verbose: true
36
+ stages: [pre-commit]
xn_auth-0.0.2/PKG-INFO ADDED
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.1
2
+ Name: xn-auth
3
+ Version: 0.0.2
4
+ Summary: Auth adapter for XN-Api framework
5
+ Author-email: Artemiev <mixartemev@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/XyncNet/x-api
8
+ Project-URL: Repository, https://github.com/XyncNet/x-api
9
+ Keywords: starlette,fastapi,auth
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: fastapi
13
+ Requires-Dist: pwdlib[argon2]
14
+ Requires-Dist: python-jose[cryptography]
15
+ Requires-Dist: xn-model
16
+ Provides-Extra: dev
17
+ Requires-Dist: build; extra == "dev"
18
+ Requires-Dist: python-dotenv; extra == "dev"
19
+ Requires-Dist: setuptools_scm; extra == "dev"
20
+ Requires-Dist: twine; extra == "dev"
21
+
22
+ # X-Auth
23
+ ###### Pswd-jwt authentication for x-api
24
+
25
+ #### Requirements
26
+ - Python >= 3.12
27
+
28
+ ### INSTALL
29
+ ```bash
30
+ pip install x-auth
31
+ ```
32
+
33
+ ---
34
+ Made with ❤ on top of the [X-Model](https://github.com/XyncNet/x-model).
@@ -0,0 +1,13 @@
1
+ # X-Auth
2
+ ###### Pswd-jwt authentication for x-api
3
+
4
+ #### Requirements
5
+ - Python >= 3.12
6
+
7
+ ### INSTALL
8
+ ```bash
9
+ pip install x-auth
10
+ ```
11
+
12
+ ---
13
+ Made with ❤ on top of the [X-Model](https://github.com/XyncNet/x-model).
xn_auth-0.0.2/makefile ADDED
@@ -0,0 +1,24 @@
1
+ include .env
2
+ PACKAGE := x_auth
3
+ VPYTHON := $(VENV)/bin/python
4
+
5
+ .PHONY: all install pre-commit clean build twine patch
6
+
7
+ all:
8
+ make install clean build
9
+
10
+ install: $(VENV)
11
+ $(VENV)/bin/pip install .[dev]; make pre-commit
12
+ pre-commit: .pre-commit-config.yaml
13
+ pre-commit install -t pre-commit -t post-commit -t pre-push
14
+
15
+ clean: dist $(PACKAGE).egg-info
16
+ rm -rf dist/* $(PACKAGE).egg-info $(PACKAGE)/__pycache__ dist/__pycache__
17
+
18
+ build: $(VENV)
19
+ $(VPYTHON) -m build; make twine
20
+ twine: $(VENV) dist
21
+ $(VPYTHON) -m twine upload dist/* --skip-existing
22
+
23
+ patch: $(VENV)
24
+ git tag `$(VPYTHON) -m setuptools_scm --strip-dev`; git push --tags --prune -f
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "xn-auth"
3
+ requires-python = ">=3.12"
4
+ authors = [
5
+ {name = "Artemiev", email = "mixartemev@gmail.com"},
6
+ ]
7
+ keywords = ["starlette", "fastapi", "auth"]
8
+ description = "Auth adapter for XN-Api framework"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ dynamic = ["version"]
12
+
13
+ dependencies = [
14
+ "fastapi",
15
+ 'pwdlib[argon2]',
16
+ "python-jose[cryptography]",
17
+ "xn-model",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ dev = [
22
+ "build",
23
+ "python-dotenv",
24
+ "setuptools_scm",
25
+ "twine",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/XyncNet/x-api"
30
+ Repository = "https://github.com/XyncNet/x-api"
31
+
32
+ [build-system]
33
+ requires = ["setuptools>=64", "setuptools-scm[toml]>=8"]
34
+ build-backend = "setuptools.build_meta"
35
+
36
+ [tool.setuptools]
37
+ packages = ["x_auth"]
38
+
39
+ [tool.setuptools_scm]
40
+ version_scheme = "python-simplified-semver" # if "feature" in `branch_name` SEMVER_MINOR++ else SEMVER_PATCH++
41
+ local_scheme = "no-local-version"
42
+
43
+ [tool.ruff]
44
+ line-length = 120
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,211 @@
1
+ from datetime import timedelta, datetime
2
+ from enum import IntEnum
3
+
4
+ # from importlib.metadata import distributions
5
+ from typing import Annotated
6
+ from fastapi import Depends, HTTPException, Security
7
+ from fastapi.security import OAuth2PasswordBearer, SecurityScopes, OAuth2PasswordRequestForm
8
+ from jose import jwt, JWTError
9
+ from pydantic import BaseModel, ValidationError
10
+ from starlette import status
11
+ from starlette.authentication import AuthCredentials, SimpleUser, AuthenticationError, AuthenticationBackend
12
+ from starlette.requests import HTTPConnection
13
+ from starlette.responses import Response
14
+ from x_model.enum import Scope, UserStatus
15
+ from x_model.model import Model, User
16
+ from x_model.pydantic import UserReg, UserAuth
17
+
18
+
19
+ class AuthFailReason(IntEnum):
20
+ username = 1
21
+ password = 2
22
+ signature = 3
23
+ expired = 4
24
+ dep_not_installed = 5
25
+
26
+
27
+ class AuthException(AuthenticationError, HTTPException):
28
+ detail: AuthFailReason
29
+
30
+ def __init__(self, detail: AuthFailReason, clear_cookie: str | None = "access_token") -> None:
31
+ hdrs = (
32
+ {"set-cookie": clear_cookie + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT"} if clear_cookie else None
33
+ ) # path=/;
34
+ super().__init__(status_code=status.HTTP_401_UNAUTHORIZED, detail=detail.name, headers=hdrs)
35
+
36
+
37
+ def on_error(_: HTTPConnection, exc: AuthException) -> Response:
38
+ hdr = (
39
+ {
40
+ "Location": "/login",
41
+ }
42
+ if exc.status_code == 303 and "/login" in (r.path for r in _.app.routes)
43
+ else {}
44
+ )
45
+ resp = Response(str(exc), status_code=exc.status_code, headers=hdr)
46
+ resp.delete_cookie("access_token")
47
+ return resp
48
+
49
+
50
+ class AuthType(IntEnum):
51
+ pwd = 1
52
+ tg = 2
53
+
54
+
55
+ class TokenData(BaseModel):
56
+ id: int
57
+ username: str | None = None
58
+ scopes: list[str] = []
59
+
60
+
61
+ class AuthUser(SimpleUser):
62
+ id: int
63
+
64
+ def __init__(self, uid: int, username: str) -> None:
65
+ super().__init__(username)
66
+ self.id = uid
67
+
68
+
69
+ class OAuth(AuthenticationBackend):
70
+ EXPIRES = timedelta(days=7)
71
+
72
+ class Token(BaseModel):
73
+ access_token: str
74
+ token_type: str
75
+ user: UserAuth
76
+
77
+ def __init__(self, secret: str, db_user_model: type[User] = User, auth_type: AuthType = AuthType.pwd):
78
+ self.secret: str = secret
79
+ self.db_user_model: type[User] = db_user_model
80
+ self.auth_type: AuthType = auth_type
81
+
82
+ self.read = Security(self.check_token, scopes=[Scope.READ.name])
83
+ self.write = Security(self.check_token, scopes=[Scope.WRITE.name])
84
+ self.my = Security(self.check_token, scopes=[Scope.ALL.name])
85
+ self.active = Depends(self.check_token)
86
+
87
+ oauth2_scheme = OAuth2PasswordBearer(
88
+ tokenUrl="token",
89
+ scopes={
90
+ Scope.READ.name: "READ own items",
91
+ Scope.WRITE.name: "Write own items",
92
+ Scope.ALL.name: "Access for not only own items",
93
+ },
94
+ )
95
+
96
+ # async def get_token_for_tg(self, tg_user: WebAppUser) -> Token:
97
+ # user: User
98
+ # user, cr = await user_upsert(tg_user)
99
+ # access_token = self.gen_access_token(
100
+ # data={"sub": tg_user.username or str(tg_user.id), "id": tg_user.id,
101
+ # "scopes": self.role_scopes_map[user.role]},
102
+ # expires_delta=self.EXPIRES,
103
+ # )
104
+ # auth_user: UserAuth = UserAuth.model_validate(user, from_attributes=True)
105
+ # return self.Token.model_validate(
106
+ # {"access_token": access_token, "token_type": "bearer", "user": auth_user},
107
+ # from_attributes=True
108
+ # )
109
+
110
+ def get_data_from_jwt(self, jwtoken: str) -> tuple[AuthCredentials, AuthUser]:
111
+ payload = jwt.decode(jwtoken, self.secret, algorithms=["HS256"])
112
+ uid: int = payload.get("id")
113
+ username: str = payload.get("sub")
114
+ scopes = payload.get("scopes", [])
115
+ return AuthCredentials(scopes), AuthUser(uid, username)
116
+
117
+ async def authenticate(self, conn: HTTPConnection) -> tuple[AuthCredentials, AuthUser] | None:
118
+ if not (auth := conn.headers.get("Authorization")):
119
+ return
120
+ # try:
121
+ scheme, credentials = auth.split()
122
+ if scheme.lower() == "bearer":
123
+ try:
124
+ return self.get_data_from_jwt(credentials)
125
+ except JWTError as e:
126
+ print(e)
127
+ raise AuthException(AuthFailReason.expired, "access_token")
128
+ except ValidationError as e:
129
+ print(e)
130
+ raise AuthException(AuthFailReason.signature, "access_token")
131
+ # except Exception as exc:
132
+ # raise AuthenticationError(exc, 'Invalid auth credentials')
133
+ # elif scheme.lower() == 'tg':
134
+ # if 'xtg-auth' not in [d.name for d in distributions()]:
135
+ # raise AuthException(AuthFailReason.dep_not_installed, None)
136
+ # from aiogram.utils.web_app import safe_parse_webapp_init_data
137
+ # tgData = safe_parse_webapp_init_data(self.secret, credentials)
138
+ # credentials = (await self.get_token_for_tg(tgData.user)).access_token
139
+
140
+ # dependency
141
+ async def check_token(
142
+ self, security_scopes: SecurityScopes, token: Annotated[str | None, Depends(oauth2_scheme)]
143
+ ): # , tg_data: [str, ]
144
+ auth_val = "Bearer"
145
+ if security_scopes.scopes:
146
+ auth_val += f' scope="{security_scopes.scope_str}"'
147
+ cred_exc = HTTPException(
148
+ status_code=status.HTTP_401_UNAUTHORIZED,
149
+ detail="Could not validate credentials",
150
+ headers={"WWW-Authenticate": auth_val},
151
+ )
152
+ try:
153
+ creds, user = self.get_data_from_jwt(token)
154
+ except (JWTError, ValidationError) as e:
155
+ cred_exc.detail += f": {e}"
156
+ raise cred_exc
157
+ if not user.username or not user.id:
158
+ cred_exc.detail += "token"
159
+ raise cred_exc
160
+ # noinspection PyTypeChecker
161
+ user_status: UserStatus | None = await self.db_user_model.get_or_none(username=user.username).values_list(
162
+ "status", flat=True
163
+ )
164
+ if not user_status:
165
+ cred_exc.detail = "User not found"
166
+ raise cred_exc
167
+ elif user_status < UserStatus.TEST:
168
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user")
169
+ for scope in security_scopes.scopes:
170
+ if scope not in creds.scopes:
171
+ cred_exc.detail = f"Not enough permissions. Need `{scope}`"
172
+ raise cred_exc
173
+
174
+ # api reg endpoint
175
+ async def reg_user(self, user_reg_input: UserReg) -> Token:
176
+ data = user_reg_input.model_dump()
177
+ try:
178
+ await self.db_user_model.create(**data)
179
+ except Exception as e:
180
+ raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE, detail=e.__repr__())
181
+ tok = await self.login_for_access_token(
182
+ OAuth2PasswordRequestForm(username=user_reg_input.username, password=user_reg_input.password)
183
+ )
184
+ return tok
185
+
186
+ async def authenticate_user(self, username: str, password: str) -> tuple[TokenData, Model]:
187
+ if user_db := await self.db_user_model.get_or_none(username=username):
188
+ td = TokenData.model_validate(user_db, from_attributes=True)
189
+ td.scopes = self.role_scopes_map[user_db.role]
190
+ if user_db.pwd_vrf(password):
191
+ return td, user_db
192
+ reason = AuthFailReason.password
193
+ else:
194
+ reason = AuthFailReason.username
195
+ raise AuthException(detail=reason)
196
+
197
+ def gen_access_token(self, data: dict, expires_delta: timedelta = EXPIRES) -> str:
198
+ return jwt.encode({"exp": datetime.utcnow() + expires_delta, **data}, self.secret)
199
+
200
+ # api login endpoint
201
+ async def login_for_access_token(self, form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token:
202
+ token, user_db = await self.authenticate_user(form_data.username, form_data.password)
203
+ if isinstance(token, TokenData):
204
+ access_token = self.gen_access_token(
205
+ data={"id": token.id, "sub": token.username, "scopes": token.scopes},
206
+ expires_delta=self.EXPIRES,
207
+ )
208
+ r = self.Token.model_validate(
209
+ {"access_token": access_token, "token_type": "bearer", "user": user_db}, from_attributes=True
210
+ )
211
+ return r
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.1
2
+ Name: xn-auth
3
+ Version: 0.0.2
4
+ Summary: Auth adapter for XN-Api framework
5
+ Author-email: Artemiev <mixartemev@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/XyncNet/x-api
8
+ Project-URL: Repository, https://github.com/XyncNet/x-api
9
+ Keywords: starlette,fastapi,auth
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: fastapi
13
+ Requires-Dist: pwdlib[argon2]
14
+ Requires-Dist: python-jose[cryptography]
15
+ Requires-Dist: xn-model
16
+ Provides-Extra: dev
17
+ Requires-Dist: build; extra == "dev"
18
+ Requires-Dist: python-dotenv; extra == "dev"
19
+ Requires-Dist: setuptools_scm; extra == "dev"
20
+ Requires-Dist: twine; extra == "dev"
21
+
22
+ # X-Auth
23
+ ###### Pswd-jwt authentication for x-api
24
+
25
+ #### Requirements
26
+ - Python >= 3.12
27
+
28
+ ### INSTALL
29
+ ```bash
30
+ pip install x-auth
31
+ ```
32
+
33
+ ---
34
+ Made with ❤ on top of the [X-Model](https://github.com/XyncNet/x-model).
@@ -0,0 +1,12 @@
1
+ .env.dist
2
+ .gitignore
3
+ .pre-commit-config.yaml
4
+ README.md
5
+ makefile
6
+ pyproject.toml
7
+ x_auth/__init__.py
8
+ xn_auth.egg-info/PKG-INFO
9
+ xn_auth.egg-info/SOURCES.txt
10
+ xn_auth.egg-info/dependency_links.txt
11
+ xn_auth.egg-info/requires.txt
12
+ xn_auth.egg-info/top_level.txt
@@ -0,0 +1,10 @@
1
+ fastapi
2
+ pwdlib[argon2]
3
+ python-jose[cryptography]
4
+ xn-model
5
+
6
+ [dev]
7
+ build
8
+ python-dotenv
9
+ setuptools_scm
10
+ twine
@@ -0,0 +1 @@
1
+ x_auth