stapel-auth 0.3.3__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.
- stapel_auth-0.3.3/LICENSE +21 -0
- stapel_auth-0.3.3/PKG-INFO +63 -0
- stapel_auth-0.3.3/README.md +34 -0
- stapel_auth-0.3.3/__init__.py +46 -0
- stapel_auth-0.3.3/admin/__init__.py +0 -0
- stapel_auth-0.3.3/admin/dto.py +46 -0
- stapel_auth-0.3.3/admin/serializers.py +60 -0
- stapel_auth-0.3.3/admin/views.py +129 -0
- stapel_auth-0.3.3/admin.py +73 -0
- stapel_auth-0.3.3/apps.py +52 -0
- stapel_auth-0.3.3/conf.py +196 -0
- stapel_auth-0.3.3/conftest.py +166 -0
- stapel_auth-0.3.3/dto.py +28 -0
- stapel_auth-0.3.3/errors.py +178 -0
- stapel_auth-0.3.3/events.py +35 -0
- stapel_auth-0.3.3/flows.py +167 -0
- stapel_auth-0.3.3/functions.py +53 -0
- stapel_auth-0.3.3/gdpr.py +160 -0
- stapel_auth-0.3.3/magic_link/__init__.py +0 -0
- stapel_auth-0.3.3/magic_link/dto.py +18 -0
- stapel_auth-0.3.3/magic_link/serializers.py +45 -0
- stapel_auth-0.3.3/magic_link/services.py +81 -0
- stapel_auth-0.3.3/magic_link/views.py +131 -0
- stapel_auth-0.3.3/mfa/__init__.py +0 -0
- stapel_auth-0.3.3/mfa/dto.py +124 -0
- stapel_auth-0.3.3/mfa/serializers.py +87 -0
- stapel_auth-0.3.3/mfa/services.py +394 -0
- stapel_auth-0.3.3/mfa/views.py +497 -0
- stapel_auth-0.3.3/migrations/0001_initial.py +102 -0
- stapel_auth-0.3.3/migrations/0002_initial.py +23 -0
- stapel_auth-0.3.3/migrations/0003_alter_emailverification_code_and_more.py +44 -0
- stapel_auth-0.3.3/migrations/0004_authenticatorchangerequest.py +45 -0
- stapel_auth-0.3.3/migrations/0005_user_sessions_totp_device.py +60 -0
- stapel_auth-0.3.3/migrations/0006_rename_usersession_user_revoked_idx_user_sessio_user_id_4d7d26_idx_and_more.py +29 -0
- stapel_auth-0.3.3/migrations/0007_add_audit_log_passkeys_suspicious_session.py +60 -0
- stapel_auth-0.3.3/migrations/0008_add_device_type_details_to_session.py +23 -0
- stapel_auth-0.3.3/migrations/0009_add_access_jti_to_session.py +17 -0
- stapel_auth-0.3.3/migrations/0010_sso_models.py +62 -0
- stapel_auth-0.3.3/migrations/0011_verification_preference.py +28 -0
- stapel_auth-0.3.3/migrations/0012_alter_authauditlog_event_type_alter_orgmembership_id_and_more.py +28 -0
- stapel_auth-0.3.3/migrations/__init__.py +0 -0
- stapel_auth-0.3.3/models.py +476 -0
- stapel_auth-0.3.3/monitoring_proxy.py +36 -0
- stapel_auth-0.3.3/oauth/__init__.py +0 -0
- stapel_auth-0.3.3/oauth/dto.py +71 -0
- stapel_auth-0.3.3/oauth/providers.py +255 -0
- stapel_auth-0.3.3/oauth/serializers.py +42 -0
- stapel_auth-0.3.3/oauth/services.py +64 -0
- stapel_auth-0.3.3/oauth_providers.py +260 -0
- stapel_auth-0.3.3/openid/__init__.py +0 -0
- stapel_auth-0.3.3/openid/views.py +177 -0
- stapel_auth-0.3.3/otp/__init__.py +0 -0
- stapel_auth-0.3.3/otp/dto.py +114 -0
- stapel_auth-0.3.3/otp/serializers.py +230 -0
- stapel_auth-0.3.3/otp/services.py +687 -0
- stapel_auth-0.3.3/otp/utils.py +2 -0
- stapel_auth-0.3.3/otp/views.py +1853 -0
- stapel_auth-0.3.3/password/__init__.py +0 -0
- stapel_auth-0.3.3/password/dto.py +62 -0
- stapel_auth-0.3.3/password/serializers.py +167 -0
- stapel_auth-0.3.3/password/services.py +265 -0
- stapel_auth-0.3.3/password/views.py +496 -0
- stapel_auth-0.3.3/permissions.py +69 -0
- stapel_auth-0.3.3/py.typed +0 -0
- stapel_auth-0.3.3/pyproject.toml +87 -0
- stapel_auth-0.3.3/qr/__init__.py +0 -0
- stapel_auth-0.3.3/qr/dto.py +64 -0
- stapel_auth-0.3.3/qr/serializers.py +64 -0
- stapel_auth-0.3.3/qr/services.py +90 -0
- stapel_auth-0.3.3/qr/views.py +366 -0
- stapel_auth-0.3.3/schemas/emits/user.registered.json +13 -0
- stapel_auth-0.3.3/schemas/emits/user.session_created.json +15 -0
- stapel_auth-0.3.3/schemas/emits/user.session_revoked.json +12 -0
- stapel_auth-0.3.3/schemas/functions/auth.verification.policy.json +14 -0
- stapel_auth-0.3.3/security/__init__.py +0 -0
- stapel_auth-0.3.3/security/dto.py +140 -0
- stapel_auth-0.3.3/security/serializers.py +86 -0
- stapel_auth-0.3.3/security/services.py +120 -0
- stapel_auth-0.3.3/security/views.py +301 -0
- stapel_auth-0.3.3/security_views.py +518 -0
- stapel_auth-0.3.3/serializers.py +63 -0
- stapel_auth-0.3.3/services.py +12 -0
- stapel_auth-0.3.3/sessions/__init__.py +0 -0
- stapel_auth-0.3.3/sessions/dto.py +101 -0
- stapel_auth-0.3.3/sessions/serializers.py +95 -0
- stapel_auth-0.3.3/sessions/services.py +379 -0
- stapel_auth-0.3.3/sessions/views.py +446 -0
- stapel_auth-0.3.3/setup.cfg +4 -0
- stapel_auth-0.3.3/sso_service.py +433 -0
- stapel_auth-0.3.3/sso_views.py +418 -0
- stapel_auth-0.3.3/stapel_auth.egg-info/PKG-INFO +63 -0
- stapel_auth-0.3.3/stapel_auth.egg-info/SOURCES.txt +216 -0
- stapel_auth-0.3.3/stapel_auth.egg-info/dependency_links.txt +1 -0
- stapel_auth-0.3.3/stapel_auth.egg-info/requires.txt +18 -0
- stapel_auth-0.3.3/stapel_auth.egg-info/top_level.txt +1 -0
- stapel_auth-0.3.3/tasks.py +233 -0
- stapel_auth-0.3.3/tests/__init__.py +0 -0
- stapel_auth-0.3.3/tests/conftest.py +145 -0
- stapel_auth-0.3.3/tests/test_auth.py +4558 -0
- stapel_auth-0.3.3/tests/test_extra.py +1380 -0
- stapel_auth-0.3.3/tests/test_public_api.py +42 -0
- stapel_auth-0.3.3/tests/test_serializer_seams.py +54 -0
- stapel_auth-0.3.3/tests/test_services.py +625 -0
- stapel_auth-0.3.3/tests/test_sso.py +665 -0
- stapel_auth-0.3.3/tests/test_upgrade.py +746 -0
- stapel_auth-0.3.3/tests/test_verification.py +1089 -0
- stapel_auth-0.3.3/urls.py +293 -0
- stapel_auth-0.3.3/utils.py +73 -0
- stapel_auth-0.3.3/verification/__init__.py +8 -0
- stapel_auth-0.3.3/verification/dto.py +69 -0
- stapel_auth-0.3.3/verification/serializers.py +89 -0
- stapel_auth-0.3.3/verification/views.py +337 -0
- stapel_auth-0.3.3/verification_factors.py +185 -0
- stapel_auth-0.3.3/views.py +2 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 usestapel
|
|
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,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stapel-auth
|
|
3
|
+
Version: 0.3.3
|
|
4
|
+
Summary: Full-featured authentication Django app for the Stapel framework
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: django,stapel,authentication,jwt,passkey,totp,sso
|
|
7
|
+
Classifier: Framework :: Django
|
|
8
|
+
Classifier: Framework :: Django :: 5.2
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: stapel-core<0.4,>=0.3.0
|
|
15
|
+
Requires-Dist: pyotp>=2.10
|
|
16
|
+
Requires-Dist: webauthn>=2.8
|
|
17
|
+
Requires-Dist: phonenumbers>=8.13
|
|
18
|
+
Provides-Extra: phone
|
|
19
|
+
Requires-Dist: twilio>=9.0; extra == "phone"
|
|
20
|
+
Provides-Extra: oauth
|
|
21
|
+
Requires-Dist: social-auth-app-django>=5.4; extra == "oauth"
|
|
22
|
+
Requires-Dist: python3-openid>=3.2; extra == "oauth"
|
|
23
|
+
Provides-Extra: saml
|
|
24
|
+
Requires-Dist: lxml>=5.0; extra == "saml"
|
|
25
|
+
Requires-Dist: signxml>=3.2; extra == "saml"
|
|
26
|
+
Provides-Extra: all
|
|
27
|
+
Requires-Dist: stapel-auth[oauth,phone,saml]; extra == "all"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# stapel-auth
|
|
31
|
+
|
|
32
|
+
[](https://github.com/usestapel/stapel-auth/actions/workflows/ci.yml)
|
|
33
|
+
[](https://codecov.io/gh/usestapel/stapel-auth)
|
|
34
|
+
|
|
35
|
+
> Full-featured authentication — JWT, passkeys (WebAuthn), TOTP, QR login, OAuth2, SSO (SAML/OIDC), magic link, phone OTP
|
|
36
|
+
|
|
37
|
+
Part of the [Stapel framework](https://github.com/usestapel) — composable Django apps for building production-grade platforms.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install stapel-auth
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
# settings.py
|
|
49
|
+
INSTALLED_APPS = [
|
|
50
|
+
...
|
|
51
|
+
'stapel_auth',
|
|
52
|
+
]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Bus events
|
|
56
|
+
|
|
57
|
+
### Emits
|
|
58
|
+
| `user.session_created` | [schema](schemas/emits/user.session_created.json) | User successfully authenticated and a new session was created. |
|
|
59
|
+
| `user.session_revoked` | [schema](schemas/emits/user.session_revoked.json) | A user session was revoked (logout or admin action). |
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT — see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# stapel-auth
|
|
2
|
+
|
|
3
|
+
[](https://github.com/usestapel/stapel-auth/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/usestapel/stapel-auth)
|
|
5
|
+
|
|
6
|
+
> Full-featured authentication — JWT, passkeys (WebAuthn), TOTP, QR login, OAuth2, SSO (SAML/OIDC), magic link, phone OTP
|
|
7
|
+
|
|
8
|
+
Part of the [Stapel framework](https://github.com/usestapel) — composable Django apps for building production-grade platforms.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install stapel-auth
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
# settings.py
|
|
20
|
+
INSTALLED_APPS = [
|
|
21
|
+
...
|
|
22
|
+
'stapel_auth',
|
|
23
|
+
]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Bus events
|
|
27
|
+
|
|
28
|
+
### Emits
|
|
29
|
+
| `user.session_created` | [schema](schemas/emits/user.session_created.json) | User successfully authenticated and a new session was created. |
|
|
30
|
+
| `user.session_revoked` | [schema](schemas/emits/user.session_revoked.json) | A user session was revoked (logout or admin action). |
|
|
31
|
+
|
|
32
|
+
## License
|
|
33
|
+
|
|
34
|
+
MIT — see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""stapel-auth — pluggable authentication for Django.
|
|
2
|
+
|
|
3
|
+
Public API of the ``stapel_auth`` package, exported lazily (PEP 562) so that
|
|
4
|
+
importing the package itself stays side-effect free: the runtime settings
|
|
5
|
+
object (``auth_settings``), the per-feature URL-pattern factories from
|
|
6
|
+
``stapel_auth.urls`` and the OAuth provider registry (``PROVIDER_REGISTRY``).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from importlib import import_module
|
|
10
|
+
|
|
11
|
+
# name -> (relative module, attribute) — resolved on first attribute access.
|
|
12
|
+
_LAZY_EXPORTS = {
|
|
13
|
+
"auth_settings": (".conf", "auth_settings"),
|
|
14
|
+
"PROVIDER_REGISTRY": (".oauth_providers", "PROVIDER_REGISTRY"),
|
|
15
|
+
"get_admin_api_urls": (".urls", "get_admin_api_urls"),
|
|
16
|
+
"get_magic_link_urls": (".urls", "get_magic_link_urls"),
|
|
17
|
+
"get_mfa_urls": (".urls", "get_mfa_urls"),
|
|
18
|
+
"get_oauth_urls": (".urls", "get_oauth_urls"),
|
|
19
|
+
"get_openid_urls": (".urls", "get_openid_urls"),
|
|
20
|
+
"get_otp_urls": (".urls", "get_otp_urls"),
|
|
21
|
+
"get_password_urls": (".urls", "get_password_urls"),
|
|
22
|
+
"get_qr_urls": (".urls", "get_qr_urls"),
|
|
23
|
+
"get_security_urls": (".urls", "get_security_urls"),
|
|
24
|
+
"get_sessions_urls": (".urls", "get_sessions_urls"),
|
|
25
|
+
"get_sso_urls": (".urls", "get_sso_urls"),
|
|
26
|
+
"get_verification_urls": (".urls", "get_verification_urls"),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
__all__ = sorted(_LAZY_EXPORTS)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def __getattr__(name):
|
|
33
|
+
"""PEP 562 lazy exports: resolve, cache into globals(), return."""
|
|
34
|
+
try:
|
|
35
|
+
module_path, attr = _LAZY_EXPORTS[name]
|
|
36
|
+
except KeyError:
|
|
37
|
+
raise AttributeError(
|
|
38
|
+
f"module {__name__!r} has no attribute {name!r}"
|
|
39
|
+
) from None
|
|
40
|
+
value = getattr(import_module(module_path, __name__), attr)
|
|
41
|
+
globals()[name] = value # cache — __getattr__ is skipped next time
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def __dir__():
|
|
46
|
+
return sorted(set(globals()) | set(__all__))
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Data Transfer Objects for the admin sub-package."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# =============================================================================
|
|
7
|
+
# Admin Broker DTOs
|
|
8
|
+
# =============================================================================
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class AdminUserCreateRequest:
|
|
13
|
+
"""Create a user via admin broker, bypassing OTP verification.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
email: User email. Example: user@example.com
|
|
17
|
+
phone: Phone in E.164 format. Example: +79001234567
|
|
18
|
+
username: Username. Example: alice
|
|
19
|
+
display_name: Display name. Example: Alice
|
|
20
|
+
password: Initial password (optional). Example: secure123
|
|
21
|
+
send_welcome: Send welcome notification via notification service. Example: false
|
|
22
|
+
mark_verified: Mark email/phone as verified immediately. Example: true
|
|
23
|
+
"""
|
|
24
|
+
email: Optional[str] = None
|
|
25
|
+
phone: Optional[str] = None
|
|
26
|
+
username: Optional[str] = None
|
|
27
|
+
display_name: Optional[str] = None
|
|
28
|
+
password: Optional[str] = None
|
|
29
|
+
send_welcome: bool = False
|
|
30
|
+
mark_verified: bool = True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class AdminUserCreateResponse:
|
|
35
|
+
"""Created user summary.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
user_id: Created user UUID. Example: 550e8400-e29b-41d4-a716-446655440000
|
|
39
|
+
email: User email. Example: user@example.com
|
|
40
|
+
phone: User phone. Example: +79001234567
|
|
41
|
+
username: Username. Example: alice
|
|
42
|
+
"""
|
|
43
|
+
user_id: str
|
|
44
|
+
email: Optional[str]
|
|
45
|
+
phone: Optional[str]
|
|
46
|
+
username: Optional[str]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Serializers for the admin sub-package."""
|
|
2
|
+
from rest_framework import serializers
|
|
3
|
+
from stapel_core.django.api.serializers import StapelDataclassSerializer
|
|
4
|
+
from stapel_core.django.api.errors import StapelValidationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from stapel_auth.models import ServiceAPIKey
|
|
8
|
+
from stapel_auth.errors import ERR_400_EMAIL_OR_PHONE_REQUIRED
|
|
9
|
+
from stapel_auth.admin.dto import AdminUserCreateResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _normalize_phone(value):
|
|
13
|
+
"""Parse and normalize phone number to E.164 format (+79991234567)."""
|
|
14
|
+
import phonenumbers
|
|
15
|
+
from stapel_auth.errors import (
|
|
16
|
+
ERR_400_INVALID_PHONE_FORMAT,
|
|
17
|
+
ERR_400_INVALID_PHONE,
|
|
18
|
+
ERR_400_PHONE_TOO_LONG,
|
|
19
|
+
)
|
|
20
|
+
try:
|
|
21
|
+
phone_number = phonenumbers.parse(value, None)
|
|
22
|
+
except phonenumbers.NumberParseException:
|
|
23
|
+
raise StapelValidationError(ERR_400_INVALID_PHONE_FORMAT)
|
|
24
|
+
if not phonenumbers.is_valid_number(phone_number):
|
|
25
|
+
raise StapelValidationError(ERR_400_INVALID_PHONE)
|
|
26
|
+
e164 = phonenumbers.format_number(phone_number, phonenumbers.PhoneNumberFormat.E164)
|
|
27
|
+
if len(e164) > 16: # E.164: '+' + up to 15 digits
|
|
28
|
+
raise StapelValidationError(ERR_400_PHONE_TOO_LONG)
|
|
29
|
+
return e164
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ServiceAPIKeySerializer(serializers.ModelSerializer):
|
|
33
|
+
"""Serializer for Service API Keys"""
|
|
34
|
+
|
|
35
|
+
class Meta:
|
|
36
|
+
model = ServiceAPIKey
|
|
37
|
+
fields = ['id', 'name', 'key', 'description', 'is_active', 'created_at', 'last_used_at', 'allowed_endpoints']
|
|
38
|
+
read_only_fields = ['id', 'key', 'created_at', 'last_used_at']
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AdminUserCreateRequestSerializer(serializers.Serializer):
|
|
42
|
+
email = serializers.EmailField(required=False, allow_null=True, default=None)
|
|
43
|
+
phone = serializers.CharField(required=False, allow_null=True, default=None)
|
|
44
|
+
username = serializers.CharField(required=False, allow_null=True, default=None)
|
|
45
|
+
display_name = serializers.CharField(required=False, allow_null=True, default=None)
|
|
46
|
+
password = serializers.CharField(required=False, allow_null=True, default=None, min_length=8)
|
|
47
|
+
send_welcome = serializers.BooleanField(default=False)
|
|
48
|
+
mark_verified = serializers.BooleanField(default=True)
|
|
49
|
+
|
|
50
|
+
def validate(self, attrs):
|
|
51
|
+
if not any([attrs.get("email"), attrs.get("phone"), attrs.get("username")]):
|
|
52
|
+
raise StapelValidationError(ERR_400_EMAIL_OR_PHONE_REQUIRED)
|
|
53
|
+
if attrs.get("phone"):
|
|
54
|
+
attrs["phone"] = _normalize_phone(attrs["phone"])
|
|
55
|
+
return attrs
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AdminUserCreateResponseSerializer(StapelDataclassSerializer):
|
|
59
|
+
class Meta:
|
|
60
|
+
dataclass = AdminUserCreateResponse
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Views for the admin sub-package: ServiceAPIKeyViewSet, AdminUserViewSet, CapabilitiesView."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from drf_spectacular.utils import extend_schema
|
|
6
|
+
from rest_framework import permissions, viewsets
|
|
7
|
+
from rest_framework.decorators import action
|
|
8
|
+
from rest_framework.views import APIView
|
|
9
|
+
from stapel_core.django.api.errors import (
|
|
10
|
+
StapelResponse,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from stapel_auth.admin.dto import AdminUserCreateResponse
|
|
14
|
+
from stapel_auth.admin.serializers import (
|
|
15
|
+
AdminUserCreateRequestSerializer,
|
|
16
|
+
AdminUserCreateResponseSerializer,
|
|
17
|
+
ServiceAPIKeySerializer,
|
|
18
|
+
)
|
|
19
|
+
from stapel_auth.models import ServiceAPIKey
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@extend_schema(tags=["API Keys"])
|
|
25
|
+
class ServiceAPIKeyViewSet(viewsets.ModelViewSet):
|
|
26
|
+
"""
|
|
27
|
+
ViewSet for managing service API keys
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
queryset = ServiceAPIKey.objects.all()
|
|
31
|
+
serializer_class = ServiceAPIKeySerializer
|
|
32
|
+
permission_classes = [permissions.IsAdminUser]
|
|
33
|
+
|
|
34
|
+
def perform_create(self, serializer):
|
|
35
|
+
"""Generate API key on creation"""
|
|
36
|
+
serializer.save(key=ServiceAPIKey.generate_key())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CapabilitiesView(APIView):
|
|
40
|
+
"""Public endpoint returning enabled auth methods for this deployment."""
|
|
41
|
+
|
|
42
|
+
permission_classes = [permissions.AllowAny]
|
|
43
|
+
authentication_classes = []
|
|
44
|
+
|
|
45
|
+
@extend_schema(
|
|
46
|
+
tags=["Auth"],
|
|
47
|
+
description="Return available authentication and registration methods for this deployment.",
|
|
48
|
+
responses={200: None},
|
|
49
|
+
)
|
|
50
|
+
def get(self, request):
|
|
51
|
+
from stapel_auth.oauth.services import AuthCapabilitiesService
|
|
52
|
+
from stapel_auth.serializers import AuthCapabilitiesSerializer
|
|
53
|
+
|
|
54
|
+
caps = AuthCapabilitiesService.get_capabilities()
|
|
55
|
+
return StapelResponse(AuthCapabilitiesSerializer(caps))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ── Admin User Broker ─────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AdminUserViewSet(viewsets.GenericViewSet):
|
|
62
|
+
"""Admin broker for creating users without OTP verification."""
|
|
63
|
+
|
|
64
|
+
permission_classes = [permissions.AllowAny]
|
|
65
|
+
|
|
66
|
+
@extend_schema(
|
|
67
|
+
tags=["Admin"],
|
|
68
|
+
description="Create a user account without OTP, bypassing normal registration flow. Requires service API key or admin (staff) credentials.",
|
|
69
|
+
request=AdminUserCreateRequestSerializer,
|
|
70
|
+
responses={201: AdminUserCreateResponseSerializer, 400: None, 403: None},
|
|
71
|
+
)
|
|
72
|
+
@action(detail=False, methods=["post"])
|
|
73
|
+
def create_user(self, request):
|
|
74
|
+
from django.contrib.auth import get_user_model
|
|
75
|
+
from stapel_core.django.api.errors import error_403_forbidden
|
|
76
|
+
|
|
77
|
+
from stapel_auth.permissions import IsServiceAPIKey
|
|
78
|
+
|
|
79
|
+
User = get_user_model()
|
|
80
|
+
|
|
81
|
+
# Allow either staff user or service API key
|
|
82
|
+
is_svc_key = IsServiceAPIKey().has_permission(request, self)
|
|
83
|
+
if not is_svc_key and not (
|
|
84
|
+
request.user.is_authenticated and request.user.is_staff
|
|
85
|
+
):
|
|
86
|
+
return error_403_forbidden()
|
|
87
|
+
|
|
88
|
+
serializer = AdminUserCreateRequestSerializer(data=request.data)
|
|
89
|
+
serializer.is_valid(raise_exception=True)
|
|
90
|
+
data = serializer.validated_data
|
|
91
|
+
|
|
92
|
+
email = data.get("email")
|
|
93
|
+
phone = data.get("phone")
|
|
94
|
+
username = data.get("username")
|
|
95
|
+
display_name = data.get("display_name")
|
|
96
|
+
password = data.get("password")
|
|
97
|
+
mark_verified = data.get("mark_verified", True)
|
|
98
|
+
send_welcome = data.get("send_welcome", False)
|
|
99
|
+
|
|
100
|
+
user = User.objects.create(
|
|
101
|
+
email=email,
|
|
102
|
+
phone=phone,
|
|
103
|
+
username=username or (email.split("@")[0] if email else phone),
|
|
104
|
+
is_email_verified=mark_verified and bool(email),
|
|
105
|
+
is_phone_verified=mark_verified and bool(phone),
|
|
106
|
+
)
|
|
107
|
+
if display_name:
|
|
108
|
+
user.first_name = display_name
|
|
109
|
+
if password:
|
|
110
|
+
user.set_password(password)
|
|
111
|
+
user.save()
|
|
112
|
+
|
|
113
|
+
if send_welcome:
|
|
114
|
+
try:
|
|
115
|
+
from stapel_core.notifications import request_notification
|
|
116
|
+
|
|
117
|
+
request_notification(notification_type="welcome", user_id=str(user.id))
|
|
118
|
+
except Exception:
|
|
119
|
+
logger.exception(
|
|
120
|
+
"Failed to send welcome notification for user %s", user.id
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
dto = AdminUserCreateResponse(
|
|
124
|
+
user_id=str(user.id),
|
|
125
|
+
email=user.email,
|
|
126
|
+
phone=user.phone,
|
|
127
|
+
username=user.username,
|
|
128
|
+
)
|
|
129
|
+
return StapelResponse(AdminUserCreateResponseSerializer(dto), status=201)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from .models import PhoneVerification, EmailVerification, ServiceAPIKey, RefreshTokenTracker, LoginAttempt, AuthenticatorChangeRequest
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@admin.register(PhoneVerification)
|
|
6
|
+
class PhoneVerificationAdmin(admin.ModelAdmin):
|
|
7
|
+
"""Phone Verification admin"""
|
|
8
|
+
|
|
9
|
+
list_display = ['phone', 'code', 'is_verified', 'created_at', 'expires_at', 'attempts']
|
|
10
|
+
list_filter = ['is_verified', 'created_at']
|
|
11
|
+
search_fields = ['phone']
|
|
12
|
+
ordering = ['-created_at']
|
|
13
|
+
readonly_fields = ['created_at']
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@admin.register(EmailVerification)
|
|
17
|
+
class EmailVerificationAdmin(admin.ModelAdmin):
|
|
18
|
+
"""Email Verification admin"""
|
|
19
|
+
|
|
20
|
+
list_display = ['email', 'code', 'is_verified', 'created_at', 'expires_at', 'attempts']
|
|
21
|
+
list_filter = ['is_verified', 'created_at']
|
|
22
|
+
search_fields = ['email']
|
|
23
|
+
ordering = ['-created_at']
|
|
24
|
+
readonly_fields = ['created_at']
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@admin.register(ServiceAPIKey)
|
|
28
|
+
class ServiceAPIKeyAdmin(admin.ModelAdmin):
|
|
29
|
+
"""Service API Key admin"""
|
|
30
|
+
|
|
31
|
+
list_display = ['name', 'key', 'is_active', 'created_at', 'last_used_at']
|
|
32
|
+
list_filter = ['is_active', 'created_at']
|
|
33
|
+
search_fields = ['name', 'key']
|
|
34
|
+
ordering = ['-created_at']
|
|
35
|
+
readonly_fields = ['key', 'created_at', 'last_used_at']
|
|
36
|
+
|
|
37
|
+
def save_model(self, request, obj, form, change):
|
|
38
|
+
if not change: # Only set key on creation
|
|
39
|
+
obj.key = ServiceAPIKey.generate_key()
|
|
40
|
+
super().save_model(request, obj, form, change)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@admin.register(RefreshTokenTracker)
|
|
44
|
+
class RefreshTokenTrackerAdmin(admin.ModelAdmin):
|
|
45
|
+
"""Refresh Token Tracker admin"""
|
|
46
|
+
|
|
47
|
+
list_display = ['user', 'created_at', 'expires_at', 'is_revoked', 'device_info']
|
|
48
|
+
list_filter = ['is_revoked', 'created_at']
|
|
49
|
+
search_fields = ['user__username', 'user__email', 'device_info']
|
|
50
|
+
ordering = ['-created_at']
|
|
51
|
+
readonly_fields = ['created_at']
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@admin.register(AuthenticatorChangeRequest)
|
|
55
|
+
class AuthenticatorChangeRequestAdmin(admin.ModelAdmin):
|
|
56
|
+
"""Authenticator Change Request admin"""
|
|
57
|
+
|
|
58
|
+
list_display = ['user', 'change_type', 'status', 'old_value', 'new_value', 'created_at', 'scheduled_at']
|
|
59
|
+
list_filter = ['status', 'change_type']
|
|
60
|
+
search_fields = ['old_value', 'new_value']
|
|
61
|
+
ordering = ['-created_at']
|
|
62
|
+
readonly_fields = ['id', 'change_token', 'created_at', 'completed_at', 'cancelled_at']
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@admin.register(LoginAttempt)
|
|
66
|
+
class LoginAttemptAdmin(admin.ModelAdmin):
|
|
67
|
+
"""Login Attempt admin"""
|
|
68
|
+
|
|
69
|
+
list_display = ['identifier', 'attempt_type', 'ip_address', 'created_at']
|
|
70
|
+
list_filter = ['attempt_type', 'created_at']
|
|
71
|
+
search_fields = ['identifier', 'ip_address']
|
|
72
|
+
ordering = ['-created_at']
|
|
73
|
+
readonly_fields = ['created_at']
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class StapelAuthConfig(AppConfig):
|
|
5
|
+
default_auto_field = 'django.db.models.BigAutoField'
|
|
6
|
+
name = 'stapel_auth'
|
|
7
|
+
label = 'authentication' # keeps existing migration history / DB tables intact
|
|
8
|
+
verbose_name = 'Stapel Auth'
|
|
9
|
+
|
|
10
|
+
def ready(self):
|
|
11
|
+
import warnings
|
|
12
|
+
from django.conf import settings
|
|
13
|
+
from django.utils.module_loading import import_string
|
|
14
|
+
from stapel_core.oauth import register_provider
|
|
15
|
+
from .conf import auth_settings
|
|
16
|
+
|
|
17
|
+
if not auth_settings.FRONTEND_URL:
|
|
18
|
+
warnings.warn(
|
|
19
|
+
"stapel-auth: FRONTEND_URL is not set. "
|
|
20
|
+
"Set STAPEL_AUTH = {'FRONTEND_URL': '...'} or FRONTEND_URL env var. "
|
|
21
|
+
"Redirects after SSO/magic link/QR login will not work correctly.",
|
|
22
|
+
stacklevel=2,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
classes = list(auth_settings.OAUTH_PROVIDER_CLASSES)
|
|
26
|
+
if getattr(settings, 'DEBUG', False):
|
|
27
|
+
classes.append('stapel_auth.oauth_providers.TestProvider')
|
|
28
|
+
|
|
29
|
+
for cls_path in classes:
|
|
30
|
+
register_provider(import_string(cls_path)())
|
|
31
|
+
|
|
32
|
+
# Step-up verification factors: the mechanism (challenge/grant
|
|
33
|
+
# stores, @requires_verification) lives in stapel-core; the concrete
|
|
34
|
+
# factor implementations on top of the auth services are registered
|
|
35
|
+
# here. register_factor is idempotent per factor id.
|
|
36
|
+
from stapel_core.verification import register_factor
|
|
37
|
+
from .verification_factors import DEFAULT_FACTOR_CLASSES
|
|
38
|
+
for factor_cls in DEFAULT_FACTOR_CLASSES:
|
|
39
|
+
register_factor(factor_cls())
|
|
40
|
+
|
|
41
|
+
# comm Function providers (auth.verification.policy). Importing the
|
|
42
|
+
# module runs the @function decorators; re-imports are no-ops and
|
|
43
|
+
# re-registering the same handler object is idempotent.
|
|
44
|
+
from . import functions # noqa: F401
|
|
45
|
+
|
|
46
|
+
# In monolith mode (no GDPR_COLLECTING_SERVICES), register the GDPR provider
|
|
47
|
+
# in-process so the orchestrator can call it directly.
|
|
48
|
+
# In microservices mode the bus consumer (management/commands/consume_gdpr.py) handles it.
|
|
49
|
+
if not getattr(settings, 'GDPR_COLLECTING_SERVICES', None):
|
|
50
|
+
from stapel_core.gdpr import gdpr_registry
|
|
51
|
+
from .gdpr import AuthGDPRProvider
|
|
52
|
+
gdpr_registry.register(AuthGDPRProvider())
|