fauth 0.2.0__tar.gz → 0.2.1__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.
- {fauth-0.2.0 → fauth-0.2.1}/CHANGELOG.md +16 -0
- {fauth-0.2.0 → fauth-0.2.1}/PKG-INFO +20 -7
- {fauth-0.2.0 → fauth-0.2.1}/README.md +19 -6
- {fauth-0.2.0 → fauth-0.2.1}/fauth/providers/protocols.py +6 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/providers/provider.py +43 -3
- {fauth-0.2.0 → fauth-0.2.1}/fauth/testing/__init__.py +2 -1
- {fauth-0.2.0 → fauth-0.2.1}/fauth/testing/fakes.py +11 -0
- {fauth-0.2.0 → fauth-0.2.1}/pyproject.toml +1 -1
- {fauth-0.2.0 → fauth-0.2.1}/tests/providers/conftest.py +35 -4
- {fauth-0.2.0 → fauth-0.2.1}/tests/providers/test_provider.py +83 -0
- {fauth-0.2.0 → fauth-0.2.1}/uv.lock +1 -1
- {fauth-0.2.0 → fauth-0.2.1}/.github/dependabot.yml +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/.github/workflows/cicd.yml +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/.github/workflows/pre-commit-autoupdate.yml +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/.gitignore +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/.pre-commit-config.yaml +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/LICENSE +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/api/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/api/router.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/core/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/core/config.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/core/exceptions.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/core/schemas.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/crypto/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/crypto/jwt.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/crypto/password.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/providers/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/testing/config.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/testing/provider.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/transports/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/transports/base.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/transports/bearer.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/utils/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/fauth/utils/logging.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/pytest.ini +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/requirements.txt +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/api/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/api/conftest.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/api/test_openapi.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/api/test_router.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/conftest.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/core/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/core/conftest.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/core/test_config.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/core/test_exceptions.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/crypto/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/crypto/conftest.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/crypto/test_jwt.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/crypto/test_password.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/providers/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/testing/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/testing/test_testing.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/utils/conftest.py +0 -0
- {fauth-0.2.0 → fauth-0.2.1}/tests/utils/test_logging.py +0 -0
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.2.1 (2026-04-03)
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- Implement AuthProvider for FastAPI authentication and add testing utilities
|
|
9
|
+
([`f19f7ad`](https://github.com/justmatias/fauth/commit/f19f7ad32a8a79368d50862ffa3f4446dcc2cc70))
|
|
10
|
+
|
|
11
|
+
### Chores
|
|
12
|
+
|
|
13
|
+
- **config**: Merge with main
|
|
14
|
+
([`d8caae2`](https://github.com/justmatias/fauth/commit/d8caae2b3e357fcc2b3bc5f78c2929c1d0b12427))
|
|
15
|
+
|
|
16
|
+
|
|
4
17
|
## v0.2.0 (2026-04-02)
|
|
5
18
|
|
|
6
19
|
### Bug Fixes
|
|
@@ -32,6 +45,9 @@
|
|
|
32
45
|
|
|
33
46
|
### Bug Fixes
|
|
34
47
|
|
|
48
|
+
- **config**: Add authenticate method to AuthProvider with IdentityLoader support
|
|
49
|
+
([`f905766`](https://github.com/justmatias/fauth/commit/f905766afa02394a28f97b319e52134576c4c614))
|
|
50
|
+
|
|
35
51
|
- **config**: Add openapi security scheme support to AuthProvider and router dependencies
|
|
36
52
|
([`72d7754`](https://github.com/justmatias/fauth/commit/72d7754fb0519be979b6b00b5f253b245770698c))
|
|
37
53
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fauth
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Ergonomic, lightweight JWT authentication for FastAPI. Secure your routes instantly with plug-and-play dependency injection
|
|
5
5
|
Project-URL: Homepage, https://github.com/justmatias/fauth
|
|
6
6
|
Project-URL: Repository, https://github.com/justmatias/fauth
|
|
@@ -132,7 +132,8 @@ That's it. The `/me` endpoint is now protected. Requests without a valid `Bearer
|
|
|
132
132
|
## Full Example
|
|
133
133
|
|
|
134
134
|
```python
|
|
135
|
-
from fastapi import FastAPI, Depends
|
|
135
|
+
from fastapi import FastAPI, Depends, HTTPException
|
|
136
|
+
from fastapi.security import OAuth2PasswordRequestForm
|
|
136
137
|
from pydantic import BaseModel
|
|
137
138
|
from fauth import AuthConfig, AuthProvider, TokenPayload, SecureAPIRouter
|
|
138
139
|
|
|
@@ -155,9 +156,21 @@ DB: dict[str, User] = {
|
|
|
155
156
|
async def load_user(payload: TokenPayload) -> User | None:
|
|
156
157
|
return DB.get(payload.sub)
|
|
157
158
|
|
|
158
|
-
# 3.
|
|
159
|
+
# 3. Define how to fetch a user by username/email for login
|
|
160
|
+
async def load_user_by_identity(identifier: str) -> User | None:
|
|
161
|
+
# In a real app, this would query your database
|
|
162
|
+
for user in DB.values():
|
|
163
|
+
if user.username == identifier:
|
|
164
|
+
return user
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
# 4. Instantiate the auth component
|
|
159
168
|
config = AuthConfig(secret_key="my-super-secret-key", algorithm="HS256")
|
|
160
|
-
auth: AuthProvider[User] = AuthProvider(
|
|
169
|
+
auth: AuthProvider[User] = AuthProvider(
|
|
170
|
+
config=config,
|
|
171
|
+
user_loader=load_user,
|
|
172
|
+
identity_loader=load_user_by_identity
|
|
173
|
+
)
|
|
161
174
|
|
|
162
175
|
# --- Routes ---
|
|
163
176
|
|
|
@@ -168,17 +181,17 @@ async def login():
|
|
|
168
181
|
|
|
169
182
|
@app.get("/me")
|
|
170
183
|
async def get_me(user: User = Depends(auth.require_user)):
|
|
171
|
-
#
|
|
184
|
+
# 6. `auth.require_user` secures the endpoint automatically
|
|
172
185
|
return {"message": f"Hello {user.username}"}
|
|
173
186
|
|
|
174
187
|
@app.get("/admin")
|
|
175
188
|
async def get_admin_data(user: User = Depends(auth.require_roles(["admin"]))):
|
|
176
|
-
#
|
|
189
|
+
# 7. `auth.require_roles` enforces RBAC with list of roles
|
|
177
190
|
return {"secret_data": "Top secret admin info"}
|
|
178
191
|
|
|
179
192
|
# --- Securing Multiple Routes ---
|
|
180
193
|
|
|
181
|
-
#
|
|
194
|
+
# 8. Use `SecureAPIRouter` to protect an entire group of routes.
|
|
182
195
|
# Any route added to this router will require an active user automatically.
|
|
183
196
|
# This also enables the "Authorize" button in Swagger UI!
|
|
184
197
|
secure_router = SecureAPIRouter(auth_provider=auth, prefix="/internal", tags=["Protected"])
|
|
@@ -99,7 +99,8 @@ That's it. The `/me` endpoint is now protected. Requests without a valid `Bearer
|
|
|
99
99
|
## Full Example
|
|
100
100
|
|
|
101
101
|
```python
|
|
102
|
-
from fastapi import FastAPI, Depends
|
|
102
|
+
from fastapi import FastAPI, Depends, HTTPException
|
|
103
|
+
from fastapi.security import OAuth2PasswordRequestForm
|
|
103
104
|
from pydantic import BaseModel
|
|
104
105
|
from fauth import AuthConfig, AuthProvider, TokenPayload, SecureAPIRouter
|
|
105
106
|
|
|
@@ -122,9 +123,21 @@ DB: dict[str, User] = {
|
|
|
122
123
|
async def load_user(payload: TokenPayload) -> User | None:
|
|
123
124
|
return DB.get(payload.sub)
|
|
124
125
|
|
|
125
|
-
# 3.
|
|
126
|
+
# 3. Define how to fetch a user by username/email for login
|
|
127
|
+
async def load_user_by_identity(identifier: str) -> User | None:
|
|
128
|
+
# In a real app, this would query your database
|
|
129
|
+
for user in DB.values():
|
|
130
|
+
if user.username == identifier:
|
|
131
|
+
return user
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
# 4. Instantiate the auth component
|
|
126
135
|
config = AuthConfig(secret_key="my-super-secret-key", algorithm="HS256")
|
|
127
|
-
auth: AuthProvider[User] = AuthProvider(
|
|
136
|
+
auth: AuthProvider[User] = AuthProvider(
|
|
137
|
+
config=config,
|
|
138
|
+
user_loader=load_user,
|
|
139
|
+
identity_loader=load_user_by_identity
|
|
140
|
+
)
|
|
128
141
|
|
|
129
142
|
# --- Routes ---
|
|
130
143
|
|
|
@@ -135,17 +148,17 @@ async def login():
|
|
|
135
148
|
|
|
136
149
|
@app.get("/me")
|
|
137
150
|
async def get_me(user: User = Depends(auth.require_user)):
|
|
138
|
-
#
|
|
151
|
+
# 6. `auth.require_user` secures the endpoint automatically
|
|
139
152
|
return {"message": f"Hello {user.username}"}
|
|
140
153
|
|
|
141
154
|
@app.get("/admin")
|
|
142
155
|
async def get_admin_data(user: User = Depends(auth.require_roles(["admin"]))):
|
|
143
|
-
#
|
|
156
|
+
# 7. `auth.require_roles` enforces RBAC with list of roles
|
|
144
157
|
return {"secret_data": "Top secret admin info"}
|
|
145
158
|
|
|
146
159
|
# --- Securing Multiple Routes ---
|
|
147
160
|
|
|
148
|
-
#
|
|
161
|
+
# 8. Use `SecureAPIRouter` to protect an entire group of routes.
|
|
149
162
|
# Any route added to this router will require an active user automatically.
|
|
150
163
|
# This also enables the "Authorize" button in Swagger UI!
|
|
151
164
|
secure_router = SecureAPIRouter(auth_provider=auth, prefix="/internal", tags=["Protected"])
|
|
@@ -9,3 +9,9 @@ class UserLoader(Protocol[T]):
|
|
|
9
9
|
"""Consumer implements this to look up a user from JWT payload."""
|
|
10
10
|
|
|
11
11
|
async def __call__(self, token_payload: TokenPayload) -> T | None: ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class IdentityLoader(Protocol[T]):
|
|
15
|
+
"""Look up a user from an identifier (username, email, etc.)."""
|
|
16
|
+
|
|
17
|
+
async def __call__(self, identifier: str) -> T | None: ...
|
|
@@ -11,11 +11,16 @@ from fauth.core import (
|
|
|
11
11
|
TokenPayload,
|
|
12
12
|
TokenResponse,
|
|
13
13
|
)
|
|
14
|
-
from fauth.crypto import
|
|
14
|
+
from fauth.crypto import (
|
|
15
|
+
create_access_token,
|
|
16
|
+
create_refresh_token,
|
|
17
|
+
decode_token,
|
|
18
|
+
verify_password,
|
|
19
|
+
)
|
|
15
20
|
from fauth.transports import BearerTransport, Transport
|
|
16
21
|
from fauth.utils import logger
|
|
17
22
|
|
|
18
|
-
from .protocols import UserLoader
|
|
23
|
+
from .protocols import IdentityLoader, UserLoader
|
|
19
24
|
|
|
20
25
|
T = TypeVar("T")
|
|
21
26
|
|
|
@@ -26,16 +31,20 @@ class AuthProvider(Generic[T]):
|
|
|
26
31
|
and returns dependencies for routes.
|
|
27
32
|
"""
|
|
28
33
|
|
|
29
|
-
def __init__(
|
|
34
|
+
def __init__( # pylint: disable=too-many-arguments, too-many-positional-arguments
|
|
30
35
|
self,
|
|
31
36
|
config: AuthConfig,
|
|
32
37
|
user_loader: UserLoader[T],
|
|
38
|
+
identity_loader: IdentityLoader[T] | None = None,
|
|
33
39
|
transport: Transport | None = None,
|
|
34
40
|
token_payload_schema: type[TokenPayload] = TokenPayload,
|
|
41
|
+
password_field_name: str = "hashed_password",
|
|
35
42
|
):
|
|
36
43
|
self.config = config
|
|
37
44
|
self.user_loader = user_loader
|
|
45
|
+
self.identity_loader = identity_loader
|
|
38
46
|
self.token_payload_schema = token_payload_schema
|
|
47
|
+
self.password_field_name = password_field_name
|
|
39
48
|
|
|
40
49
|
if not transport:
|
|
41
50
|
transport = BearerTransport()
|
|
@@ -137,6 +146,37 @@ class AuthProvider(Generic[T]):
|
|
|
137
146
|
|
|
138
147
|
return permission_checker
|
|
139
148
|
|
|
149
|
+
async def authenticate(self, identifier: str, password: str) -> T:
|
|
150
|
+
"""
|
|
151
|
+
Authenticates a user by their identifier and password.
|
|
152
|
+
Returns the user if valid and active, otherwise raises HTTPException.
|
|
153
|
+
"""
|
|
154
|
+
if not self.identity_loader:
|
|
155
|
+
raise RuntimeError("IdentityLoader must be provided to use authenticate()")
|
|
156
|
+
|
|
157
|
+
user = await self.identity_loader(identifier)
|
|
158
|
+
if not user:
|
|
159
|
+
raise HTTPException(
|
|
160
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
161
|
+
detail="Invalid credentials",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
hashed = getattr(user, self.password_field_name, None)
|
|
165
|
+
if not hashed or not verify_password(password, hashed):
|
|
166
|
+
raise HTTPException(
|
|
167
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
168
|
+
detail="Invalid credentials",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Ensure user is active if it has is_active attribute
|
|
172
|
+
if hasattr(user, "is_active") and not user.is_active:
|
|
173
|
+
raise HTTPException(
|
|
174
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
175
|
+
detail="Inactive user",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return user
|
|
179
|
+
|
|
140
180
|
async def login(
|
|
141
181
|
self,
|
|
142
182
|
sub: str,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from .config import fake_auth_config
|
|
2
|
-
from .fakes import FakeUserLoader
|
|
2
|
+
from .fakes import FakeIdentityLoader, FakeUserLoader
|
|
3
3
|
from .provider import build_fake_auth_provider
|
|
4
4
|
|
|
5
5
|
__all__ = [
|
|
6
|
+
"FakeIdentityLoader",
|
|
6
7
|
"FakeUserLoader",
|
|
7
8
|
"build_fake_auth_provider",
|
|
8
9
|
"fake_auth_config",
|
|
@@ -16,3 +16,14 @@ class FakeUserLoader(Generic[T]):
|
|
|
16
16
|
|
|
17
17
|
async def __call__(self, token_payload: TokenPayload) -> T | None:
|
|
18
18
|
return self._users.get(token_payload.sub)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FakeIdentityLoader(Generic[T]):
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self._users: dict[str, T] = {}
|
|
24
|
+
|
|
25
|
+
def add_user(self, user_id: str, user: T) -> None:
|
|
26
|
+
self._users[user_id] = user
|
|
27
|
+
|
|
28
|
+
async def __call__(self, identifier: str) -> T | None:
|
|
29
|
+
return self._users.get(identifier)
|
|
@@ -8,13 +8,14 @@ from fastapi.testclient import TestClient
|
|
|
8
8
|
from pydantic import BaseModel, Field
|
|
9
9
|
|
|
10
10
|
from fauth.core import AuthConfig
|
|
11
|
-
from fauth.crypto import create_access_token
|
|
11
|
+
from fauth.crypto import create_access_token, hash_password
|
|
12
12
|
from fauth.providers import AuthProvider
|
|
13
|
-
from fauth.testing import FakeUserLoader
|
|
13
|
+
from fauth.testing import FakeIdentityLoader, FakeUserLoader
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class DummyUser(BaseModel):
|
|
17
17
|
id_: UUID = Field(default_factory=uuid4)
|
|
18
|
+
hashed_password: str = Field(default=hash_password("secret_password"))
|
|
18
19
|
is_active: bool = Field(default=True)
|
|
19
20
|
roles: list[str] = Field(default_factory=list)
|
|
20
21
|
permissions: list[str] = Field(default_factory=list)
|
|
@@ -35,11 +36,22 @@ def user_loader() -> FakeUserLoader[DummyUser]:
|
|
|
35
36
|
return FakeUserLoader()
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def identity_loader() -> FakeIdentityLoader[DummyUser]:
|
|
41
|
+
return FakeIdentityLoader()
|
|
42
|
+
|
|
43
|
+
|
|
38
44
|
@pytest.fixture
|
|
39
45
|
def provider(
|
|
40
|
-
auth_config: AuthConfig,
|
|
46
|
+
auth_config: AuthConfig,
|
|
47
|
+
user_loader: FakeUserLoader[DummyUser],
|
|
48
|
+
identity_loader: FakeIdentityLoader[DummyUser],
|
|
41
49
|
) -> AuthProvider[DummyUser]:
|
|
42
|
-
return AuthProvider(
|
|
50
|
+
return AuthProvider(
|
|
51
|
+
config=auth_config,
|
|
52
|
+
user_loader=user_loader,
|
|
53
|
+
identity_loader=identity_loader,
|
|
54
|
+
)
|
|
43
55
|
|
|
44
56
|
|
|
45
57
|
@pytest.fixture
|
|
@@ -145,3 +157,22 @@ def _populate_no_perms_user(
|
|
|
145
157
|
@pytest.fixture
|
|
146
158
|
def no_perms_user_token(auth_config: AuthConfig, no_perms_user: DummyUser) -> str:
|
|
147
159
|
return create_access_token(sub=str(no_perms_user.id_), config=auth_config)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.fixture
|
|
163
|
+
def _populate_identity_user(
|
|
164
|
+
identity_loader: FakeIdentityLoader[DummyUser], user: DummyUser
|
|
165
|
+
) -> None:
|
|
166
|
+
identity_loader.add_user("alice", user)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@pytest.fixture
|
|
170
|
+
def _populate_identity_inactive_user(
|
|
171
|
+
identity_loader: FakeIdentityLoader[DummyUser], inactive_user: DummyUser
|
|
172
|
+
) -> None:
|
|
173
|
+
identity_loader.add_user("alice", inactive_user)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@pytest.fixture
|
|
177
|
+
def password() -> str:
|
|
178
|
+
return "secret_password"
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import pytest
|
|
2
|
+
from fastapi import HTTPException, status
|
|
2
3
|
from fastapi.testclient import TestClient
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
3
5
|
|
|
6
|
+
from fauth.core import AuthConfig
|
|
7
|
+
from fauth.crypto import hash_password
|
|
4
8
|
from fauth.providers import AuthProvider
|
|
9
|
+
from fauth.testing import FakeIdentityLoader
|
|
10
|
+
|
|
11
|
+
from .conftest import DummyUser, FakeUserLoader
|
|
5
12
|
|
|
6
13
|
|
|
7
14
|
def test_require_user_expired_token(client: TestClient, expired_token: str) -> None:
|
|
@@ -85,3 +92,79 @@ async def test_login_returns_token_response(provider: AuthProvider) -> None:
|
|
|
85
92
|
assert result.access_token
|
|
86
93
|
assert result.refresh_token
|
|
87
94
|
assert result.token_type == "bearer"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.mark.asyncio
|
|
98
|
+
@pytest.mark.usefixtures("_populate_identity_user")
|
|
99
|
+
async def test_authenticate_success(
|
|
100
|
+
provider: AuthProvider[DummyUser],
|
|
101
|
+
user: DummyUser,
|
|
102
|
+
password: str,
|
|
103
|
+
) -> None:
|
|
104
|
+
authenticated_user = await provider.authenticate("alice", password)
|
|
105
|
+
|
|
106
|
+
assert authenticated_user.id_ == user.id_
|
|
107
|
+
assert authenticated_user.hashed_password == user.hashed_password
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@pytest.mark.asyncio
|
|
111
|
+
@pytest.mark.usefixtures("_populate_identity_user")
|
|
112
|
+
async def test_authenticate_invalid_password(provider: AuthProvider[DummyUser]) -> None:
|
|
113
|
+
with pytest.raises(HTTPException) as excinfo:
|
|
114
|
+
await provider.authenticate("alice", "wrong_password")
|
|
115
|
+
|
|
116
|
+
assert excinfo.value.status_code == status.HTTP_401_UNAUTHORIZED
|
|
117
|
+
assert excinfo.value.detail == "Invalid credentials"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.asyncio
|
|
121
|
+
async def test_authenticate_unknown_user(provider: AuthProvider[DummyUser]) -> None:
|
|
122
|
+
with pytest.raises(HTTPException) as excinfo:
|
|
123
|
+
await provider.authenticate("unknown", "password")
|
|
124
|
+
|
|
125
|
+
assert excinfo.value.status_code == status.HTTP_401_UNAUTHORIZED
|
|
126
|
+
assert excinfo.value.detail == "Invalid credentials"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@pytest.mark.asyncio
|
|
130
|
+
@pytest.mark.usefixtures("_populate_identity_inactive_user")
|
|
131
|
+
async def test_authenticate_inactive_user(
|
|
132
|
+
provider: AuthProvider[DummyUser],
|
|
133
|
+
password: str,
|
|
134
|
+
) -> None:
|
|
135
|
+
with pytest.raises(HTTPException) as excinfo:
|
|
136
|
+
await provider.authenticate("alice", password)
|
|
137
|
+
|
|
138
|
+
assert excinfo.value.status_code == status.HTTP_401_UNAUTHORIZED
|
|
139
|
+
assert excinfo.value.detail == "Inactive user"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@pytest.mark.asyncio
|
|
143
|
+
async def test_authenticate_custom_password_field(auth_config: AuthConfig) -> None:
|
|
144
|
+
class CustomUser(BaseModel):
|
|
145
|
+
id: str
|
|
146
|
+
pw: str = Field(default=hash_password("secret_password"))
|
|
147
|
+
|
|
148
|
+
identity_loader = FakeIdentityLoader[CustomUser]()
|
|
149
|
+
user_loader = FakeUserLoader[CustomUser]()
|
|
150
|
+
|
|
151
|
+
provider = AuthProvider(
|
|
152
|
+
config=auth_config,
|
|
153
|
+
user_loader=user_loader,
|
|
154
|
+
identity_loader=identity_loader,
|
|
155
|
+
password_field_name="pw",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
identity_loader.add_user("alice", CustomUser(id="user-1"))
|
|
159
|
+
|
|
160
|
+
authenticated_user = await provider.authenticate("alice", "secret_password")
|
|
161
|
+
assert authenticated_user.id == "user-1"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@pytest.mark.asyncio
|
|
165
|
+
async def test_authenticate_no_loader_raises_error(
|
|
166
|
+
auth_config: AuthConfig, user_loader: FakeUserLoader[DummyUser]
|
|
167
|
+
) -> None:
|
|
168
|
+
provider = AuthProvider(config=auth_config, user_loader=user_loader)
|
|
169
|
+
with pytest.raises(RuntimeError, match="IdentityLoader must be provided"):
|
|
170
|
+
await provider.authenticate("alice", "password")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|