django-clerk-users 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl

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 (47) hide show
  1. django_clerk_users/__init__.py +78 -7
  2. django_clerk_users/apps.py +20 -0
  3. django_clerk_users/authentication/__init__.py +24 -0
  4. django_clerk_users/authentication/backends.py +89 -0
  5. django_clerk_users/authentication/drf.py +111 -0
  6. django_clerk_users/authentication/utils.py +171 -0
  7. django_clerk_users/caching.py +161 -0
  8. django_clerk_users/checks.py +127 -0
  9. django_clerk_users/client.py +32 -0
  10. django_clerk_users/decorators.py +181 -0
  11. django_clerk_users/exceptions.py +51 -0
  12. django_clerk_users/management/__init__.py +0 -0
  13. django_clerk_users/management/commands/__init__.py +0 -0
  14. django_clerk_users/management/commands/migrate_users_to_clerk.py +223 -0
  15. django_clerk_users/management/commands/sync_clerk_organizations.py +191 -0
  16. django_clerk_users/management/commands/sync_clerk_users.py +114 -0
  17. django_clerk_users/managers.py +120 -0
  18. django_clerk_users/middleware/__init__.py +9 -0
  19. django_clerk_users/middleware/auth.py +230 -0
  20. django_clerk_users/migrations/0001_initial.py +174 -0
  21. django_clerk_users/migrations/0002_make_clerk_id_nullable.py +24 -0
  22. django_clerk_users/migrations/__init__.py +0 -0
  23. django_clerk_users/models.py +180 -0
  24. django_clerk_users/organizations/__init__.py +8 -0
  25. django_clerk_users/organizations/admin.py +81 -0
  26. django_clerk_users/organizations/apps.py +8 -0
  27. django_clerk_users/organizations/middleware.py +130 -0
  28. django_clerk_users/organizations/migrations/0001_initial.py +349 -0
  29. django_clerk_users/organizations/migrations/__init__.py +0 -0
  30. django_clerk_users/organizations/models.py +314 -0
  31. django_clerk_users/organizations/webhooks.py +417 -0
  32. django_clerk_users/settings.py +37 -0
  33. django_clerk_users/testing.py +381 -0
  34. django_clerk_users/utils.py +210 -0
  35. django_clerk_users/webhooks/__init__.py +26 -0
  36. django_clerk_users/webhooks/handlers.py +346 -0
  37. django_clerk_users/webhooks/security.py +108 -0
  38. django_clerk_users/webhooks/signals.py +42 -0
  39. django_clerk_users/webhooks/views.py +76 -0
  40. django_clerk_users-0.1.0.dist-info/METADATA +311 -0
  41. django_clerk_users-0.1.0.dist-info/RECORD +43 -0
  42. {django_clerk_users-0.0.1.dist-info → django_clerk_users-0.1.0.dist-info}/WHEEL +1 -2
  43. django_clerk_users/main.py +0 -2
  44. django_clerk_users-0.0.1.dist-info/METADATA +0 -24
  45. django_clerk_users-0.0.1.dist-info/RECORD +0 -7
  46. django_clerk_users-0.0.1.dist-info/top_level.txt +0 -1
  47. {django_clerk_users-0.0.1.dist-info → django_clerk_users-0.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,191 @@
1
+ """
2
+ Management command to sync organizations from Clerk to Django.
3
+ """
4
+
5
+ from django.core.management.base import BaseCommand
6
+
7
+ from django_clerk_users.client import get_clerk_client
8
+
9
+
10
+ class Command(BaseCommand):
11
+ help = "Sync organizations from Clerk to Django database"
12
+
13
+ def add_arguments(self, parser):
14
+ parser.add_argument(
15
+ "--limit",
16
+ type=int,
17
+ default=100,
18
+ help="Maximum number of organizations to sync per batch (default: 100)",
19
+ )
20
+ parser.add_argument(
21
+ "--offset",
22
+ type=int,
23
+ default=0,
24
+ help="Offset to start syncing from (default: 0)",
25
+ )
26
+ parser.add_argument(
27
+ "--all",
28
+ action="store_true",
29
+ help="Sync all organizations (paginate through all results)",
30
+ )
31
+ parser.add_argument(
32
+ "--sync-members",
33
+ action="store_true",
34
+ help="Also sync organization members",
35
+ )
36
+ parser.add_argument(
37
+ "--dry-run",
38
+ action="store_true",
39
+ help="Show what would be synced without making changes",
40
+ )
41
+
42
+ def handle(self, *args, **options):
43
+ # Check if organizations app is installed
44
+ try:
45
+ from django_clerk_users.organizations.models import Organization
46
+ from django_clerk_users.organizations.webhooks import (
47
+ update_or_create_organization,
48
+ )
49
+ except ImportError:
50
+ self.stderr.write(
51
+ self.style.ERROR(
52
+ "Organizations app is not installed. "
53
+ "Add 'django_clerk_users.organizations' to INSTALLED_APPS."
54
+ )
55
+ )
56
+ return
57
+
58
+ limit = options["limit"]
59
+ offset = options["offset"]
60
+ sync_all = options["all"]
61
+ sync_members = options["sync_members"]
62
+ dry_run = options["dry_run"]
63
+
64
+ clerk = get_clerk_client()
65
+
66
+ created_count = 0
67
+ updated_count = 0
68
+ error_count = 0
69
+ total_count = 0
70
+
71
+ self.stdout.write("Starting organization sync from Clerk...")
72
+
73
+ if dry_run:
74
+ self.stdout.write(self.style.WARNING("DRY RUN - No changes will be made"))
75
+
76
+ while True:
77
+ self.stdout.write(
78
+ f"Fetching organizations (offset={offset}, limit={limit})..."
79
+ )
80
+
81
+ try:
82
+ response = clerk.organizations.list(limit=limit, offset=offset)
83
+ orgs = response.data if hasattr(response, "data") else response
84
+ except Exception as e:
85
+ self.stderr.write(
86
+ self.style.ERROR(f"Failed to fetch organizations: {e}")
87
+ )
88
+ break
89
+
90
+ if not orgs:
91
+ self.stdout.write("No more organizations to sync.")
92
+ break
93
+
94
+ for clerk_org in orgs:
95
+ total_count += 1
96
+ org_id = getattr(clerk_org, "id", None)
97
+ name = getattr(clerk_org, "name", "Unknown")
98
+
99
+ if not org_id:
100
+ error_count += 1
101
+ continue
102
+
103
+ if dry_run:
104
+ self.stdout.write(f" Would sync: {name} ({org_id})")
105
+ continue
106
+
107
+ try:
108
+ organization, created = update_or_create_organization(org_id)
109
+ if created:
110
+ created_count += 1
111
+ self.stdout.write(
112
+ self.style.SUCCESS(f" Created: {organization.name}")
113
+ )
114
+ else:
115
+ updated_count += 1
116
+ self.stdout.write(f" Updated: {organization.name}")
117
+
118
+ if sync_members and organization:
119
+ self._sync_organization_members(organization, org_id, dry_run)
120
+
121
+ except Exception as e:
122
+ error_count += 1
123
+ self.stderr.write(
124
+ self.style.ERROR(f" Failed to sync {name}: {e}")
125
+ )
126
+
127
+ if not sync_all:
128
+ break
129
+
130
+ offset += limit
131
+
132
+ self.stdout.write("")
133
+ self.stdout.write(self.style.SUCCESS("Sync complete!"))
134
+ self.stdout.write(f" Total processed: {total_count}")
135
+ if not dry_run:
136
+ self.stdout.write(f" Created: {created_count}")
137
+ self.stdout.write(f" Updated: {updated_count}")
138
+ self.stdout.write(f" Errors: {error_count}")
139
+
140
+ def _sync_organization_members(self, organization, org_id, dry_run):
141
+ """Sync members for a specific organization."""
142
+ from django.contrib.auth import get_user_model
143
+
144
+ from django_clerk_users.client import get_clerk_client
145
+ from django_clerk_users.organizations.models import OrganizationMember
146
+ from django_clerk_users.utils import update_or_create_clerk_user
147
+
148
+ User = get_user_model()
149
+ clerk = get_clerk_client()
150
+
151
+ try:
152
+ response = clerk.organizations.get_membership_list(
153
+ organization_id=org_id, limit=100
154
+ )
155
+ memberships = response.data if hasattr(response, "data") else response
156
+ except Exception as e:
157
+ self.stderr.write(
158
+ self.style.WARNING(f" Failed to fetch members: {e}")
159
+ )
160
+ return
161
+
162
+ for membership in memberships or []:
163
+ membership_id = getattr(membership, "id", None)
164
+ user_data = getattr(membership, "public_user_data", None)
165
+ user_id = getattr(user_data, "user_id", None) if user_data else None
166
+
167
+ if not membership_id or not user_id:
168
+ continue
169
+
170
+ if dry_run:
171
+ self.stdout.write(f" Would sync member: {user_id}")
172
+ continue
173
+
174
+ try:
175
+ user = User.objects.filter(clerk_id=user_id).first()
176
+ if not user:
177
+ user, _ = update_or_create_clerk_user(user_id)
178
+
179
+ OrganizationMember.objects.update_or_create(
180
+ clerk_membership_id=membership_id,
181
+ defaults={
182
+ "organization": organization,
183
+ "user": user,
184
+ "role": getattr(membership, "role", "member"),
185
+ },
186
+ )
187
+ self.stdout.write(f" Synced member: {user.email}")
188
+ except Exception as e:
189
+ self.stderr.write(
190
+ self.style.WARNING(f" Failed to sync member {user_id}: {e}")
191
+ )
@@ -0,0 +1,114 @@
1
+ """
2
+ Management command to sync users from Clerk to Django.
3
+ """
4
+
5
+ from django.core.management.base import BaseCommand
6
+
7
+ from django_clerk_users.client import get_clerk_client
8
+ from django_clerk_users.utils import update_or_create_clerk_user
9
+
10
+
11
+ class Command(BaseCommand):
12
+ help = "Sync users from Clerk to Django database"
13
+
14
+ def add_arguments(self, parser):
15
+ parser.add_argument(
16
+ "--limit",
17
+ type=int,
18
+ default=100,
19
+ help="Maximum number of users to sync per batch (default: 100)",
20
+ )
21
+ parser.add_argument(
22
+ "--offset",
23
+ type=int,
24
+ default=0,
25
+ help="Offset to start syncing from (default: 0)",
26
+ )
27
+ parser.add_argument(
28
+ "--all",
29
+ action="store_true",
30
+ help="Sync all users (paginate through all results)",
31
+ )
32
+ parser.add_argument(
33
+ "--dry-run",
34
+ action="store_true",
35
+ help="Show what would be synced without making changes",
36
+ )
37
+
38
+ def handle(self, *args, **options):
39
+ limit = options["limit"]
40
+ offset = options["offset"]
41
+ sync_all = options["all"]
42
+ dry_run = options["dry_run"]
43
+
44
+ clerk = get_clerk_client()
45
+
46
+ created_count = 0
47
+ updated_count = 0
48
+ error_count = 0
49
+ total_count = 0
50
+
51
+ self.stdout.write("Starting user sync from Clerk...")
52
+
53
+ if dry_run:
54
+ self.stdout.write(self.style.WARNING("DRY RUN - No changes will be made"))
55
+
56
+ while True:
57
+ self.stdout.write(f"Fetching users (offset={offset}, limit={limit})...")
58
+
59
+ try:
60
+ response = clerk.users.list(limit=limit, offset=offset)
61
+ users = response.data if hasattr(response, "data") else response
62
+ except Exception as e:
63
+ self.stderr.write(self.style.ERROR(f"Failed to fetch users: {e}"))
64
+ break
65
+
66
+ if not users:
67
+ self.stdout.write("No more users to sync.")
68
+ break
69
+
70
+ for clerk_user in users:
71
+ total_count += 1
72
+ clerk_id = getattr(clerk_user, "id", None)
73
+
74
+ if not clerk_id:
75
+ error_count += 1
76
+ continue
77
+
78
+ email = None
79
+ email_addresses = getattr(clerk_user, "email_addresses", []) or []
80
+ if email_addresses:
81
+ email = getattr(email_addresses[0], "email_address", None)
82
+
83
+ if dry_run:
84
+ self.stdout.write(f" Would sync: {email} ({clerk_id})")
85
+ continue
86
+
87
+ try:
88
+ user, created = update_or_create_clerk_user(clerk_id)
89
+ if created:
90
+ created_count += 1
91
+ self.stdout.write(
92
+ self.style.SUCCESS(f" Created: {user.email}")
93
+ )
94
+ else:
95
+ updated_count += 1
96
+ self.stdout.write(f" Updated: {user.email}")
97
+ except Exception as e:
98
+ error_count += 1
99
+ self.stderr.write(
100
+ self.style.ERROR(f" Failed to sync {clerk_id}: {e}")
101
+ )
102
+
103
+ if not sync_all:
104
+ break
105
+
106
+ offset += limit
107
+
108
+ self.stdout.write("")
109
+ self.stdout.write(self.style.SUCCESS("Sync complete!"))
110
+ self.stdout.write(f" Total processed: {total_count}")
111
+ if not dry_run:
112
+ self.stdout.write(f" Created: {created_count}")
113
+ self.stdout.write(f" Updated: {updated_count}")
114
+ self.stdout.write(f" Errors: {error_count}")
@@ -0,0 +1,120 @@
1
+ """
2
+ Custom managers for django-clerk-users models.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from django.contrib.auth.models import BaseUserManager
10
+
11
+ if TYPE_CHECKING:
12
+ from django_clerk_users.models import AbstractClerkUser
13
+
14
+
15
+ class ClerkUserManager(BaseUserManager["AbstractClerkUser"]):
16
+ """
17
+ Custom manager for ClerkUser model.
18
+
19
+ Handles user creation with clerk_id as the primary identifier.
20
+ """
21
+
22
+ def create_user(
23
+ self,
24
+ email: str,
25
+ clerk_id: str | None = None,
26
+ password: str | None = None,
27
+ **extra_fields: Any,
28
+ ) -> "AbstractClerkUser":
29
+ """
30
+ Create and save a user with the given email and optional clerk_id.
31
+
32
+ Args:
33
+ email: The user's email address (required).
34
+ clerk_id: The Clerk user ID (optional for Django admin users).
35
+ password: Optional password (required for Django admin users).
36
+ **extra_fields: Additional fields for the user model.
37
+
38
+ Returns:
39
+ The created user instance.
40
+
41
+ Raises:
42
+ ValueError: If email is not provided.
43
+ """
44
+ if not email:
45
+ raise ValueError("The email must be set")
46
+
47
+ email = self.normalize_email(email)
48
+ extra_fields.setdefault("is_active", True)
49
+ extra_fields.setdefault("is_staff", False)
50
+ extra_fields.setdefault("is_superuser", False)
51
+
52
+ user = self.model(clerk_id=clerk_id, email=email, **extra_fields)
53
+ if password:
54
+ user.set_password(password)
55
+ else:
56
+ user.set_unusable_password()
57
+ user.save(using=self._db)
58
+ return user
59
+
60
+ def create_superuser(
61
+ self,
62
+ email: str,
63
+ password: str | None = None,
64
+ clerk_id: str | None = None,
65
+ **extra_fields: Any,
66
+ ) -> "AbstractClerkUser":
67
+ """
68
+ Create and save a superuser with the given email.
69
+
70
+ Args:
71
+ email: The user's email address (required).
72
+ password: Password for the superuser (required for Django admin access).
73
+ clerk_id: The Clerk user ID (optional).
74
+ **extra_fields: Additional fields for the user model.
75
+
76
+ Returns:
77
+ The created superuser instance.
78
+ """
79
+ extra_fields.setdefault("is_staff", True)
80
+ extra_fields.setdefault("is_superuser", True)
81
+ extra_fields.setdefault("is_active", True)
82
+
83
+ if extra_fields.get("is_staff") is not True:
84
+ raise ValueError("Superuser must have is_staff=True.")
85
+ if extra_fields.get("is_superuser") is not True:
86
+ raise ValueError("Superuser must have is_superuser=True.")
87
+
88
+ return self.create_user(
89
+ email=email, clerk_id=clerk_id, password=password, **extra_fields
90
+ )
91
+
92
+ def get_by_clerk_id(self, clerk_id: str) -> "AbstractClerkUser | None":
93
+ """
94
+ Get a user by their Clerk ID.
95
+
96
+ Args:
97
+ clerk_id: The Clerk user ID.
98
+
99
+ Returns:
100
+ The user instance or None if not found.
101
+ """
102
+ try:
103
+ return self.get(clerk_id=clerk_id)
104
+ except self.model.DoesNotExist:
105
+ return None
106
+
107
+ def get_by_email(self, email: str) -> "AbstractClerkUser | None":
108
+ """
109
+ Get a user by their email address.
110
+
111
+ Args:
112
+ email: The user's email address.
113
+
114
+ Returns:
115
+ The user instance or None if not found.
116
+ """
117
+ try:
118
+ return self.get(email=self.normalize_email(email))
119
+ except self.model.DoesNotExist:
120
+ return None
@@ -0,0 +1,9 @@
1
+ """
2
+ Middleware for django-clerk-users.
3
+ """
4
+
5
+ from django_clerk_users.middleware.auth import ClerkAuthMiddleware
6
+
7
+ __all__ = [
8
+ "ClerkAuthMiddleware",
9
+ ]
@@ -0,0 +1,230 @@
1
+ """
2
+ Clerk authentication middleware.
3
+
4
+ This middleware validates Clerk JWT tokens and creates Django sessions
5
+ for authenticated users. It uses manual session handling instead of
6
+ Django's login() to avoid triggering signals that may conflict with Clerk.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import time
13
+ from typing import TYPE_CHECKING, Callable
14
+
15
+ from django.conf import settings
16
+ from django.contrib.auth.models import AnonymousUser
17
+
18
+ from django_clerk_users.authentication.utils import (
19
+ get_clerk_payload_from_request,
20
+ get_or_create_user_from_payload,
21
+ )
22
+ from django_clerk_users.exceptions import ClerkAuthenticationError, ClerkTokenError
23
+ from django_clerk_users.settings import CLERK_SESSION_REVALIDATION_SECONDS
24
+
25
+ if TYPE_CHECKING:
26
+ from django.http import HttpRequest, HttpResponse
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Authentication backend path
31
+ CLERK_BACKEND = "django_clerk_users.authentication.ClerkBackend"
32
+
33
+
34
+ class ClerkAuthMiddleware:
35
+ """
36
+ Middleware that authenticates users via Clerk JWT tokens.
37
+
38
+ This middleware implements a session-based optimization strategy:
39
+ 1. On first request: Validates JWT, creates Django session
40
+ 2. On subsequent requests: Uses session (avoids repeated validation)
41
+ 3. Periodically re-validates to detect token expiration
42
+
43
+ After processing, the middleware sets:
44
+ - request.user: The authenticated Django user (or AnonymousUser)
45
+ - request.clerk_user: Same as request.user (for explicit Clerk access)
46
+ - request.clerk_payload: The decoded JWT payload (if authenticated)
47
+ - request.org: The organization ID from the token (if present)
48
+
49
+ Note: This middleware manually sets session data instead of using Django's
50
+ login() function to avoid triggering signals that may conflict with Clerk's
51
+ authentication flow.
52
+ """
53
+
54
+ def __init__(self, get_response: Callable[["HttpRequest"], "HttpResponse"]):
55
+ self.get_response = get_response
56
+ self.debug = getattr(settings, "DEBUG", False)
57
+
58
+ def __call__(self, request: "HttpRequest") -> "HttpResponse":
59
+ # Process authentication before the view
60
+ self.process_request(request)
61
+
62
+ # Call the next middleware/view
63
+ response = self.get_response(request)
64
+
65
+ return response
66
+
67
+ def process_request(self, request: "HttpRequest") -> None:
68
+ """
69
+ Process the request and authenticate the user.
70
+
71
+ Sets request.user, request.clerk_user, request.clerk_payload,
72
+ and request.org based on the authentication result.
73
+
74
+ Supports hybrid authentication:
75
+ - If user is authenticated via Django session (e.g., admin login),
76
+ respects that authentication and skips Clerk JWT validation
77
+ - Otherwise, attempts Clerk JWT authentication
78
+ """
79
+ # Initialize attributes
80
+ request.clerk_user = None # type: ignore
81
+ request.clerk_payload = None # type: ignore
82
+ request.org = None # type: ignore
83
+
84
+ # Check if user is already authenticated via Django's standard auth
85
+ # (e.g., admin login with username/password)
86
+ if hasattr(request, "user") and request.user.is_authenticated:
87
+ # Check if this is a Clerk session or a traditional Django session
88
+ if self._is_clerk_session(request):
89
+ # This is a Clerk session, validate it
90
+ if self._is_session_valid(request):
91
+ # Clerk session is still valid
92
+ request.clerk_user = request.user # type: ignore
93
+ request.org = request.session.get("clerk_org_id") # type: ignore
94
+ return
95
+ # Clerk session expired, clear it and try JWT auth below
96
+ self._clear_session(request)
97
+ else:
98
+ # This is a traditional Django session (e.g., admin login)
99
+ # Don't interfere with it - just skip Clerk authentication
100
+ logger.debug("Using existing Django session (non-Clerk)")
101
+ return
102
+
103
+ # Try to authenticate via JWT token
104
+ try:
105
+ payload = get_clerk_payload_from_request(request)
106
+ except ClerkTokenError as e:
107
+ logger.debug(f"Token validation failed: {e}")
108
+ self._set_anonymous(request)
109
+ return
110
+
111
+ if not payload:
112
+ # No token provided - anonymous user
113
+ self._set_anonymous(request)
114
+ return
115
+
116
+ # Get or create the Django user
117
+ try:
118
+ user, created = get_or_create_user_from_payload(payload)
119
+ except ClerkAuthenticationError as e:
120
+ logger.warning(f"Failed to get/create user: {e}")
121
+ self._set_anonymous(request)
122
+ return
123
+ except Exception as e:
124
+ logger.error(f"ClerkAuthMiddleware error: {e}", exc_info=True)
125
+ if self.debug:
126
+ raise
127
+ self._set_anonymous(request)
128
+ return
129
+
130
+ if not user.is_active:
131
+ logger.debug(f"User {user.clerk_id} is inactive")
132
+ self._set_anonymous(request)
133
+ return
134
+
135
+ # Create Django session (without calling login())
136
+ self._create_session(request, user, payload)
137
+
138
+ # Set request attributes
139
+ user.backend = CLERK_BACKEND
140
+ request.user = user
141
+ request.clerk_user = user # type: ignore
142
+ request.clerk_payload = payload # type: ignore
143
+ request.org = payload.get("org_id") # type: ignore
144
+
145
+ if created:
146
+ logger.info(f"Created new user: {user.email} ({user.clerk_id})")
147
+
148
+ def _is_clerk_session(self, request: "HttpRequest") -> bool:
149
+ """
150
+ Check if the current session is a Clerk-authenticated session.
151
+
152
+ Returns True if this session was created by Clerk authentication,
153
+ False if it's a traditional Django session (e.g., admin login).
154
+ """
155
+ # Clerk sessions have the last_clerk_check timestamp
156
+ return "last_clerk_check" in request.session
157
+
158
+ def _is_session_valid(self, request: "HttpRequest") -> bool:
159
+ """
160
+ Check if the current Clerk session is valid and doesn't need revalidation.
161
+
162
+ Returns True if:
163
+ 1. User is authenticated in session
164
+ 2. The session hasn't expired
165
+ 3. The re-validation interval hasn't passed
166
+
167
+ Note: This method should only be called for Clerk sessions.
168
+ """
169
+ if not hasattr(request, "user") or not request.user.is_authenticated:
170
+ return False
171
+
172
+ # Check if re-validation is needed
173
+ last_check = request.session.get("last_clerk_check", 0)
174
+ now = int(time.time())
175
+
176
+ if now - last_check > CLERK_SESSION_REVALIDATION_SECONDS:
177
+ # Re-validation needed - try to validate the token
178
+ try:
179
+ payload = get_clerk_payload_from_request(request)
180
+ if payload:
181
+ # Token is still valid, update session
182
+ request.session["last_clerk_check"] = now
183
+ request.session["clerk_org_id"] = payload.get("org_id")
184
+ request.clerk_payload = payload # type: ignore
185
+ return True
186
+
187
+ # Missing token or invalid payload - end the session
188
+ self._clear_session(request)
189
+ return False
190
+ except ClerkTokenError:
191
+ # Token is invalid - invalidate session
192
+ self._clear_session(request)
193
+ return False
194
+
195
+ return True
196
+
197
+ def _create_session(self, request: "HttpRequest", user, payload: dict) -> None:
198
+ """
199
+ Create a Django session for the authenticated user.
200
+
201
+ Note: We manually set session data instead of calling login() to avoid
202
+ triggering Django signals (like user_logged_in) that may conflict with
203
+ Clerk's authentication flow.
204
+ """
205
+ # Manually set session auth data (what login() would do internally)
206
+ request.session["_auth_user_id"] = str(user.pk)
207
+ request.session["_auth_user_backend"] = CLERK_BACKEND
208
+ request.session["_auth_user_hash"] = ""
209
+
210
+ # Store Clerk-specific session data
211
+ request.session["last_clerk_check"] = int(time.time())
212
+ request.session["clerk_org_id"] = payload.get("org_id")
213
+
214
+ logger.debug(f"Created session for user {user.email}")
215
+
216
+ def _clear_session(self, request: "HttpRequest") -> None:
217
+ """
218
+ Clear the Django session.
219
+ """
220
+ request.session.flush()
221
+
222
+ def _set_anonymous(self, request: "HttpRequest") -> None:
223
+ """
224
+ Set the request user to anonymous.
225
+ """
226
+ if not hasattr(request, "user") or request.user.is_authenticated:
227
+ request.user = AnonymousUser()
228
+ request.clerk_user = None # type: ignore
229
+ request.clerk_payload = None # type: ignore
230
+ request.org = None # type: ignore