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.
Files changed (114) hide show
  1. stapel_auth-0.3.3/LICENSE +21 -0
  2. stapel_auth-0.3.3/PKG-INFO +63 -0
  3. stapel_auth-0.3.3/README.md +34 -0
  4. stapel_auth-0.3.3/__init__.py +46 -0
  5. stapel_auth-0.3.3/admin/__init__.py +0 -0
  6. stapel_auth-0.3.3/admin/dto.py +46 -0
  7. stapel_auth-0.3.3/admin/serializers.py +60 -0
  8. stapel_auth-0.3.3/admin/views.py +129 -0
  9. stapel_auth-0.3.3/admin.py +73 -0
  10. stapel_auth-0.3.3/apps.py +52 -0
  11. stapel_auth-0.3.3/conf.py +196 -0
  12. stapel_auth-0.3.3/conftest.py +166 -0
  13. stapel_auth-0.3.3/dto.py +28 -0
  14. stapel_auth-0.3.3/errors.py +178 -0
  15. stapel_auth-0.3.3/events.py +35 -0
  16. stapel_auth-0.3.3/flows.py +167 -0
  17. stapel_auth-0.3.3/functions.py +53 -0
  18. stapel_auth-0.3.3/gdpr.py +160 -0
  19. stapel_auth-0.3.3/magic_link/__init__.py +0 -0
  20. stapel_auth-0.3.3/magic_link/dto.py +18 -0
  21. stapel_auth-0.3.3/magic_link/serializers.py +45 -0
  22. stapel_auth-0.3.3/magic_link/services.py +81 -0
  23. stapel_auth-0.3.3/magic_link/views.py +131 -0
  24. stapel_auth-0.3.3/mfa/__init__.py +0 -0
  25. stapel_auth-0.3.3/mfa/dto.py +124 -0
  26. stapel_auth-0.3.3/mfa/serializers.py +87 -0
  27. stapel_auth-0.3.3/mfa/services.py +394 -0
  28. stapel_auth-0.3.3/mfa/views.py +497 -0
  29. stapel_auth-0.3.3/migrations/0001_initial.py +102 -0
  30. stapel_auth-0.3.3/migrations/0002_initial.py +23 -0
  31. stapel_auth-0.3.3/migrations/0003_alter_emailverification_code_and_more.py +44 -0
  32. stapel_auth-0.3.3/migrations/0004_authenticatorchangerequest.py +45 -0
  33. stapel_auth-0.3.3/migrations/0005_user_sessions_totp_device.py +60 -0
  34. stapel_auth-0.3.3/migrations/0006_rename_usersession_user_revoked_idx_user_sessio_user_id_4d7d26_idx_and_more.py +29 -0
  35. stapel_auth-0.3.3/migrations/0007_add_audit_log_passkeys_suspicious_session.py +60 -0
  36. stapel_auth-0.3.3/migrations/0008_add_device_type_details_to_session.py +23 -0
  37. stapel_auth-0.3.3/migrations/0009_add_access_jti_to_session.py +17 -0
  38. stapel_auth-0.3.3/migrations/0010_sso_models.py +62 -0
  39. stapel_auth-0.3.3/migrations/0011_verification_preference.py +28 -0
  40. stapel_auth-0.3.3/migrations/0012_alter_authauditlog_event_type_alter_orgmembership_id_and_more.py +28 -0
  41. stapel_auth-0.3.3/migrations/__init__.py +0 -0
  42. stapel_auth-0.3.3/models.py +476 -0
  43. stapel_auth-0.3.3/monitoring_proxy.py +36 -0
  44. stapel_auth-0.3.3/oauth/__init__.py +0 -0
  45. stapel_auth-0.3.3/oauth/dto.py +71 -0
  46. stapel_auth-0.3.3/oauth/providers.py +255 -0
  47. stapel_auth-0.3.3/oauth/serializers.py +42 -0
  48. stapel_auth-0.3.3/oauth/services.py +64 -0
  49. stapel_auth-0.3.3/oauth_providers.py +260 -0
  50. stapel_auth-0.3.3/openid/__init__.py +0 -0
  51. stapel_auth-0.3.3/openid/views.py +177 -0
  52. stapel_auth-0.3.3/otp/__init__.py +0 -0
  53. stapel_auth-0.3.3/otp/dto.py +114 -0
  54. stapel_auth-0.3.3/otp/serializers.py +230 -0
  55. stapel_auth-0.3.3/otp/services.py +687 -0
  56. stapel_auth-0.3.3/otp/utils.py +2 -0
  57. stapel_auth-0.3.3/otp/views.py +1853 -0
  58. stapel_auth-0.3.3/password/__init__.py +0 -0
  59. stapel_auth-0.3.3/password/dto.py +62 -0
  60. stapel_auth-0.3.3/password/serializers.py +167 -0
  61. stapel_auth-0.3.3/password/services.py +265 -0
  62. stapel_auth-0.3.3/password/views.py +496 -0
  63. stapel_auth-0.3.3/permissions.py +69 -0
  64. stapel_auth-0.3.3/py.typed +0 -0
  65. stapel_auth-0.3.3/pyproject.toml +87 -0
  66. stapel_auth-0.3.3/qr/__init__.py +0 -0
  67. stapel_auth-0.3.3/qr/dto.py +64 -0
  68. stapel_auth-0.3.3/qr/serializers.py +64 -0
  69. stapel_auth-0.3.3/qr/services.py +90 -0
  70. stapel_auth-0.3.3/qr/views.py +366 -0
  71. stapel_auth-0.3.3/schemas/emits/user.registered.json +13 -0
  72. stapel_auth-0.3.3/schemas/emits/user.session_created.json +15 -0
  73. stapel_auth-0.3.3/schemas/emits/user.session_revoked.json +12 -0
  74. stapel_auth-0.3.3/schemas/functions/auth.verification.policy.json +14 -0
  75. stapel_auth-0.3.3/security/__init__.py +0 -0
  76. stapel_auth-0.3.3/security/dto.py +140 -0
  77. stapel_auth-0.3.3/security/serializers.py +86 -0
  78. stapel_auth-0.3.3/security/services.py +120 -0
  79. stapel_auth-0.3.3/security/views.py +301 -0
  80. stapel_auth-0.3.3/security_views.py +518 -0
  81. stapel_auth-0.3.3/serializers.py +63 -0
  82. stapel_auth-0.3.3/services.py +12 -0
  83. stapel_auth-0.3.3/sessions/__init__.py +0 -0
  84. stapel_auth-0.3.3/sessions/dto.py +101 -0
  85. stapel_auth-0.3.3/sessions/serializers.py +95 -0
  86. stapel_auth-0.3.3/sessions/services.py +379 -0
  87. stapel_auth-0.3.3/sessions/views.py +446 -0
  88. stapel_auth-0.3.3/setup.cfg +4 -0
  89. stapel_auth-0.3.3/sso_service.py +433 -0
  90. stapel_auth-0.3.3/sso_views.py +418 -0
  91. stapel_auth-0.3.3/stapel_auth.egg-info/PKG-INFO +63 -0
  92. stapel_auth-0.3.3/stapel_auth.egg-info/SOURCES.txt +216 -0
  93. stapel_auth-0.3.3/stapel_auth.egg-info/dependency_links.txt +1 -0
  94. stapel_auth-0.3.3/stapel_auth.egg-info/requires.txt +18 -0
  95. stapel_auth-0.3.3/stapel_auth.egg-info/top_level.txt +1 -0
  96. stapel_auth-0.3.3/tasks.py +233 -0
  97. stapel_auth-0.3.3/tests/__init__.py +0 -0
  98. stapel_auth-0.3.3/tests/conftest.py +145 -0
  99. stapel_auth-0.3.3/tests/test_auth.py +4558 -0
  100. stapel_auth-0.3.3/tests/test_extra.py +1380 -0
  101. stapel_auth-0.3.3/tests/test_public_api.py +42 -0
  102. stapel_auth-0.3.3/tests/test_serializer_seams.py +54 -0
  103. stapel_auth-0.3.3/tests/test_services.py +625 -0
  104. stapel_auth-0.3.3/tests/test_sso.py +665 -0
  105. stapel_auth-0.3.3/tests/test_upgrade.py +746 -0
  106. stapel_auth-0.3.3/tests/test_verification.py +1089 -0
  107. stapel_auth-0.3.3/urls.py +293 -0
  108. stapel_auth-0.3.3/utils.py +73 -0
  109. stapel_auth-0.3.3/verification/__init__.py +8 -0
  110. stapel_auth-0.3.3/verification/dto.py +69 -0
  111. stapel_auth-0.3.3/verification/serializers.py +89 -0
  112. stapel_auth-0.3.3/verification/views.py +337 -0
  113. stapel_auth-0.3.3/verification_factors.py +185 -0
  114. 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
+ [![CI](https://github.com/usestapel/stapel-auth/actions/workflows/ci.yml/badge.svg)](https://github.com/usestapel/stapel-auth/actions/workflows/ci.yml)
33
+ [![codecov](https://codecov.io/gh/usestapel/stapel-auth/graph/badge.svg)](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
+ [![CI](https://github.com/usestapel/stapel-auth/actions/workflows/ci.yml/badge.svg)](https://github.com/usestapel/stapel-auth/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/usestapel/stapel-auth/graph/badge.svg)](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())