0
# Storage Models
1
2
Abstract base classes and mixins for implementing user accounts, social account associations, and authentication data storage with any database or ORM system. The storage models provide a framework-agnostic interface that can be adapted to Django, SQLAlchemy, MongoDB, or any other data persistence layer.
3
4
## Capabilities
5
6
### User Social Auth Mixin
7
8
The main mixin for implementing social authentication user associations that link local user accounts to social provider accounts.
9
10
```python { .api }
11
class UserMixin:
12
"""
13
Mixin for user social auth models.
14
15
This mixin provides the core functionality for managing social authentication
16
data associated with user accounts, including access tokens, refresh tokens,
17
and provider-specific extra data.
18
"""
19
20
# Class constants
21
ACCESS_TOKEN_EXPIRED_THRESHOLD: int = 5 # Seconds before expiry to consider expired
22
23
# Required fields (must be implemented by concrete model)
24
provider: str = "" # Provider name (e.g., 'google-oauth2', 'facebook')
25
uid: str | None = None # Provider-specific user ID
26
extra_data: dict | None = None # Additional provider data (tokens, profile info)
27
28
def save(self):
29
"""
30
Save the model instance.
31
32
Abstract method that must be implemented by the concrete model
33
to persist changes to the database.
34
35
Raises:
36
NotImplementedError: Must be implemented by subclasses
37
"""
38
39
def get_backend(self, strategy):
40
"""
41
Get backend class for this social account.
42
43
Parameters:
44
- strategy: Strategy instance for backend access
45
46
Returns:
47
Backend class for this provider
48
49
Raises:
50
MissingBackend: If backend not found for provider
51
"""
52
53
def get_backend_instance(self, strategy):
54
"""
55
Get backend instance for this social account.
56
57
Parameters:
58
- strategy: Strategy instance for backend instantiation
59
60
Returns:
61
Backend instance configured for this provider, or None if backend missing
62
"""
63
64
@property
65
def access_token(self):
66
"""
67
Get access token from extra_data.
68
69
Returns:
70
Access token string or None if not available
71
"""
72
73
def refresh_token(self, strategy, *args, **kwargs):
74
"""
75
Refresh access token using refresh token.
76
77
Attempts to refresh the access token using the stored refresh token
78
if the backend supports token refresh functionality.
79
80
Parameters:
81
- strategy: Strategy instance
82
- Additional arguments passed to backend refresh method
83
84
Returns:
85
None (updates extra_data in place)
86
"""
87
88
def set_extra_data(self, extra_data):
89
"""
90
Set extra data for this social account.
91
92
Updates the extra_data field with new information from provider,
93
merging with existing data and handling special token fields.
94
95
Parameters:
96
- extra_data: Dictionary of additional provider data
97
"""
98
99
def expiration_datetime(self):
100
"""
101
Get access token expiration datetime.
102
103
Returns:
104
datetime object for token expiration or None if no expiry data
105
"""
106
107
def access_token_expired(self):
108
"""
109
Check if access token has expired.
110
111
Uses ACCESS_TOKEN_EXPIRED_THRESHOLD to consider tokens expired
112
slightly before their actual expiration time.
113
114
Returns:
115
Boolean indicating if token is expired or expires soon
116
"""
117
118
def get_access_token(self, strategy):
119
"""
120
Get access token for API requests.
121
122
Parameters:
123
- strategy: Strategy instance
124
125
Returns:
126
Valid access token string
127
"""
128
129
def expiration_timedelta(self):
130
"""
131
Get access token expiration as timedelta.
132
133
Returns:
134
timedelta object for token expiration or None
135
"""
136
137
# Class methods - must be implemented by concrete storage classes
138
@classmethod
139
def clean_username(cls, value):
140
"""
141
Clean and validate username value.
142
143
Parameters:
144
- value: Raw username string
145
146
Returns:
147
Cleaned username string
148
"""
149
150
@classmethod
151
def changed(cls, user):
152
"""
153
Handle user change notification.
154
155
Called when user data changes to trigger any necessary
156
cleanup or update operations.
157
158
Parameters:
159
- user: User instance that changed
160
161
Raises:
162
NotImplementedError: Must be implemented by subclasses
163
"""
164
165
@classmethod
166
def get_username(cls, user):
167
"""
168
Get username from user instance.
169
170
Parameters:
171
- user: User instance
172
173
Returns:
174
Username string
175
176
Raises:
177
NotImplementedError: Must be implemented by subclasses
178
"""
179
180
@classmethod
181
def user_model(cls):
182
"""
183
Get the user model class.
184
185
Returns:
186
User model class for this storage implementation
187
188
Raises:
189
NotImplementedError: Must be implemented by subclasses
190
"""
191
192
@classmethod
193
def username_max_length(cls):
194
"""
195
Get maximum allowed username length.
196
197
Returns:
198
Integer maximum username length
199
200
Raises:
201
NotImplementedError: Must be implemented by subclasses
202
"""
203
204
@classmethod
205
def allowed_to_disconnect(cls, user, backend_name, association_id=None):
206
"""
207
Check if user is allowed to disconnect this social account.
208
209
Parameters:
210
- user: User instance
211
- backend_name: Provider backend name
212
- association_id: Optional association ID
213
214
Returns:
215
Boolean indicating if disconnection is allowed
216
217
Raises:
218
NotImplementedError: Must be implemented by subclasses
219
"""
220
221
@classmethod
222
def disconnect(cls, entry):
223
"""
224
Disconnect (delete) social account association.
225
226
Parameters:
227
- entry: Social account association to remove
228
229
Raises:
230
NotImplementedError: Must be implemented by subclasses
231
"""
232
233
@classmethod
234
def user_exists(cls, *args, **kwargs):
235
"""
236
Check if user exists.
237
238
Returns:
239
Boolean indicating if user exists
240
241
Raises:
242
NotImplementedError: Must be implemented by subclasses
243
"""
244
245
@classmethod
246
def create_user(cls, *args, **kwargs):
247
"""
248
Create new user account.
249
250
Returns:
251
New user instance
252
253
Raises:
254
NotImplementedError: Must be implemented by subclasses
255
"""
256
257
@classmethod
258
def get_user(cls, pk):
259
"""
260
Get user by primary key.
261
262
Parameters:
263
- pk: Primary key value
264
265
Returns:
266
User instance or None
267
268
Raises:
269
NotImplementedError: Must be implemented by subclasses
270
"""
271
272
@classmethod
273
def get_users_by_email(cls, email):
274
"""
275
Get users by email address.
276
277
Parameters:
278
- email: Email address string
279
280
Returns:
281
List of user instances with matching email
282
283
Raises:
284
NotImplementedError: Must be implemented by subclasses
285
"""
286
287
@classmethod
288
def get_social_auth(cls, provider, uid):
289
"""
290
Get social auth record by provider and UID.
291
292
Parameters:
293
- provider: Provider name string
294
- uid: Provider user ID
295
296
Returns:
297
Social auth instance or None
298
299
Raises:
300
NotImplementedError: Must be implemented by subclasses
301
"""
302
303
@classmethod
304
def get_social_auth_for_user(cls, user, provider=None, id=None):
305
"""
306
Get social auth records for user.
307
308
Parameters:
309
- user: User instance
310
- provider: Optional provider name filter
311
- id: Optional association ID filter
312
313
Returns:
314
List of social auth instances
315
316
Raises:
317
NotImplementedError: Must be implemented by subclasses
318
"""
319
320
@classmethod
321
def create_social_auth(cls, user, uid, provider):
322
"""
323
Create social auth association.
324
325
Parameters:
326
- user: User instance
327
- uid: Provider user ID
328
- provider: Provider name
329
330
Returns:
331
New social auth instance
332
333
Raises:
334
NotImplementedError: Must be implemented by subclasses
335
"""
336
```
337
338
### Nonce Mixin
339
340
Mixin for storing OpenID nonces to prevent replay attacks.
341
342
```python { .api }
343
class NonceMixin:
344
"""
345
Mixin for OpenID nonce models.
346
347
Prevents replay attacks by tracking used nonces and their timestamps
348
to ensure each nonce is only used once within its valid timeframe.
349
"""
350
351
# Required fields
352
server_url: str # OpenID provider server URL
353
timestamp: int # Nonce timestamp
354
salt: str # Nonce salt value
355
356
@classmethod
357
def use(cls, server_url, timestamp, salt):
358
"""
359
Use (create or verify) a nonce.
360
361
Parameters:
362
- server_url: Provider server URL
363
- timestamp: Nonce timestamp
364
- salt: Nonce salt
365
366
Raises:
367
NotImplementedError: Must be implemented by subclasses
368
"""
369
370
@classmethod
371
def get(cls, server_url, salt):
372
"""
373
Get nonce by server URL and salt.
374
375
Parameters:
376
- server_url: Provider server URL
377
- salt: Nonce salt
378
379
Returns:
380
Nonce instance or None
381
382
Raises:
383
NotImplementedError: Must be implemented by subclasses
384
"""
385
386
@classmethod
387
def delete(cls, nonce):
388
"""
389
Delete nonce record.
390
391
Parameters:
392
- nonce: Nonce instance to delete
393
394
Raises:
395
NotImplementedError: Must be implemented by subclasses
396
"""
397
```
398
399
### Association Mixin
400
401
Base mixin for association models that store OpenID associations.
402
403
```python { .api }
404
class AssociationMixin:
405
"""
406
Mixin for association models used in OpenID authentication.
407
408
Stores association data for OpenID providers including handles,
409
secrets, and expiration information.
410
"""
411
412
# Required fields
413
server_url: str # OpenID provider server URL
414
handle: str # Association handle
415
secret: str # Association secret (base64 encoded)
416
issued: int # Issue timestamp
417
lifetime: int # Association lifetime in seconds
418
assoc_type: str # Association type
419
420
@classmethod
421
def oids(cls, server_url, handle=None):
422
"""
423
Get OpenID associations for server.
424
425
Parameters:
426
- server_url: Provider server URL
427
- handle: Optional association handle filter
428
429
Returns:
430
List of association instances
431
"""
432
433
@classmethod
434
def openid_association(cls, assoc):
435
"""
436
Convert to OpenID association object.
437
438
Parameters:
439
- assoc: Association instance
440
441
Returns:
442
OpenID Association object
443
"""
444
445
@classmethod
446
def store(cls, server_url, association):
447
"""
448
Store OpenID association.
449
450
Parameters:
451
- server_url: Provider server URL
452
- association: OpenID Association object
453
454
Raises:
455
NotImplementedError: Must be implemented by subclasses
456
"""
457
458
@classmethod
459
def get(cls, server_url=None, handle=None):
460
"""
461
Get association by server URL and/or handle.
462
463
Parameters:
464
- server_url: Provider server URL (optional)
465
- handle: Association handle (optional)
466
467
Returns:
468
Association instance or None
469
470
Raises:
471
NotImplementedError: Must be implemented by subclasses
472
"""
473
474
@classmethod
475
def remove(cls, ids_to_delete):
476
"""
477
Remove associations by IDs.
478
479
Parameters:
480
- ids_to_delete: List of association IDs to delete
481
482
Raises:
483
NotImplementedError: Must be implemented by subclasses
484
"""
485
```
486
487
### Code Mixin
488
489
Mixin for email validation code storage.
490
491
```python { .api }
492
class CodeMixin:
493
"""
494
Mixin for email validation code models.
495
496
Stores validation codes sent to user email addresses
497
for email verification workflows.
498
"""
499
500
# Required fields
501
email: str # Email address
502
code: str # Validation code
503
verified: bool # Whether code has been verified
504
505
def save(self):
506
"""
507
Save the code record.
508
509
Raises:
510
NotImplementedError: Must be implemented by subclasses
511
"""
512
513
def verify(self):
514
"""
515
Mark code as verified.
516
517
Sets verified=True and saves the record.
518
"""
519
520
@classmethod
521
def generate_code(cls):
522
"""
523
Generate random validation code.
524
525
Returns:
526
Random code string
527
"""
528
529
@classmethod
530
def make_code(cls, email):
531
"""
532
Create new validation code for email.
533
534
Parameters:
535
- email: Email address
536
537
Returns:
538
New code instance
539
"""
540
541
@classmethod
542
def get_code(cls, code):
543
"""
544
Get validation code by code string.
545
546
Parameters:
547
- code: Code string to find
548
549
Returns:
550
Code instance or None
551
552
Raises:
553
NotImplementedError: Must be implemented by subclasses
554
"""
555
```
556
557
### Partial Mixin
558
559
Mixin for partial pipeline data storage.
560
561
```python { .api }
562
class PartialMixin:
563
"""
564
Mixin for partial pipeline data models.
565
566
Stores intermediate pipeline state when authentication
567
process is paused (e.g., for email validation).
568
"""
569
570
# Required fields
571
token: str # Partial pipeline token
572
data: dict # Pipeline data dictionary
573
next_step: int # Next pipeline step index
574
backend: str # Backend name
575
576
@property
577
def args(self):
578
"""Get pipeline args from data."""
579
580
@args.setter
581
def args(self, value):
582
"""Set pipeline args in data."""
583
584
@property
585
def kwargs(self):
586
"""Get pipeline kwargs from data."""
587
588
@kwargs.setter
589
def kwargs(self, value):
590
"""Set pipeline kwargs in data."""
591
592
def extend_kwargs(self, values):
593
"""
594
Extend kwargs with additional values.
595
596
Parameters:
597
- values: Dictionary of additional kwargs
598
"""
599
600
@classmethod
601
def generate_token(cls):
602
"""
603
Generate random partial token.
604
605
Returns:
606
Random token string
607
"""
608
609
@classmethod
610
def load(cls, token):
611
"""
612
Load partial pipeline data by token.
613
614
Parameters:
615
- token: Partial pipeline token
616
617
Returns:
618
Partial instance or None
619
620
Raises:
621
NotImplementedError: Must be implemented by subclasses
622
"""
623
624
@classmethod
625
def destroy(cls, token):
626
"""
627
Delete partial pipeline data.
628
629
Parameters:
630
- token: Partial pipeline token
631
632
Raises:
633
NotImplementedError: Must be implemented by subclasses
634
"""
635
636
@classmethod
637
def prepare(cls, backend, next_step, data):
638
"""
639
Prepare partial pipeline data.
640
641
Parameters:
642
- backend: Backend name
643
- next_step: Next pipeline step index
644
- data: Pipeline data dictionary
645
646
Returns:
647
Prepared partial instance
648
"""
649
650
@classmethod
651
def store(cls, partial):
652
"""
653
Store partial pipeline data.
654
655
Parameters:
656
- partial: Partial instance to store
657
658
Returns:
659
Stored partial instance
660
"""
661
```
662
663
### Base Storage Interface
664
665
Abstract base class defining the storage interface that must be implemented for each framework.
666
667
```python { .api }
668
class BaseStorage:
669
"""
670
Abstract base storage interface.
671
672
Defines the contract that storage implementations must fulfill
673
to provide data persistence for social authentication.
674
"""
675
676
user = None # User model class
677
nonce = None # Nonce model class
678
association = None # Association model class
679
680
def is_integrity_error(self, exception):
681
"""
682
Check if exception is an integrity error.
683
684
Parameters:
685
- exception: Exception instance to check
686
687
Returns:
688
Boolean indicating if this is a database integrity error
689
"""
690
691
def create_user(self, *args, **kwargs):
692
"""
693
Create new user account.
694
695
Returns:
696
New user instance
697
"""
698
699
def get_user(self, pk):
700
"""
701
Get user by primary key.
702
703
Parameters:
704
- pk: User primary key
705
706
Returns:
707
User instance or None if not found
708
"""
709
710
def create_social_auth(self, user, uid, provider):
711
"""
712
Create social authentication association.
713
714
Parameters:
715
- user: User instance
716
- uid: Provider user ID
717
- provider: Provider name
718
719
Returns:
720
New social auth instance
721
"""
722
723
def get_social_auth(self, provider, uid):
724
"""
725
Get social auth association.
726
727
Parameters:
728
- provider: Provider name
729
- uid: Provider user ID
730
731
Returns:
732
Social auth instance or None if not found
733
"""
734
735
def get_social_auth_for_user(self, user, provider=None):
736
"""
737
Get social auth associations for user.
738
739
Parameters:
740
- user: User instance
741
- provider: Provider name filter (optional)
742
743
Returns:
744
QuerySet or list of social auth instances
745
"""
746
747
def create_nonce(self, server_url, timestamp, salt):
748
"""
749
Create OpenID nonce.
750
751
Parameters:
752
- server_url: Provider server URL
753
- timestamp: Nonce timestamp
754
- salt: Nonce salt
755
756
Returns:
757
New nonce instance
758
"""
759
760
def get_nonce(self, server_url, timestamp, salt):
761
"""
762
Get OpenID nonce.
763
764
Parameters:
765
- server_url: Provider server URL
766
- timestamp: Nonce timestamp
767
- salt: Nonce salt
768
769
Returns:
770
Nonce instance or None if not found
771
"""
772
773
def create_association(self, server_url, handle, secret, issued, lifetime, assoc_type):
774
"""
775
Create OpenID association.
776
777
Parameters:
778
- server_url: Provider server URL
779
- handle: Association handle
780
- secret: Association secret
781
- issued: Issue timestamp
782
- lifetime: Lifetime in seconds
783
- assoc_type: Association type
784
785
Returns:
786
New association instance
787
"""
788
789
def get_association(self, server_url, handle=None):
790
"""
791
Get OpenID association.
792
793
Parameters:
794
- server_url: Provider server URL
795
- handle: Association handle (optional)
796
797
Returns:
798
Association instance or None if not found
799
"""
800
801
def remove_association(self, server_url, handle):
802
"""
803
Remove OpenID association.
804
805
Parameters:
806
- server_url: Provider server URL
807
- handle: Association handle
808
"""
809
```
810
811
### User Social Auth Manager
812
813
Base manager class for user social auth model management.
814
815
```python { .api }
816
class BaseUserSocialAuthManager:
817
"""
818
Base manager for user social auth models.
819
820
Provides common query methods for social authentication associations
821
that can be customized by framework-specific implementations.
822
"""
823
824
def get_social_auth(self, provider, uid):
825
"""
826
Get social auth by provider and UID.
827
828
Parameters:
829
- provider: Provider name
830
- uid: Provider user ID
831
832
Returns:
833
Social auth instance or raises DoesNotExist
834
"""
835
836
def get_social_auth_for_user(self, user, provider=None):
837
"""
838
Get social auths for user.
839
840
Parameters:
841
- user: User instance
842
- provider: Provider name filter (optional)
843
844
Returns:
845
QuerySet of social auth instances
846
"""
847
848
def create_social_auth(self, user, uid, provider):
849
"""
850
Create social auth association.
851
852
Parameters:
853
- user: User instance
854
- uid: Provider user ID
855
- provider: Provider name
856
857
Returns:
858
New social auth instance
859
"""
860
861
def username_max_length(self):
862
"""
863
Get maximum username length.
864
865
Returns:
866
Integer maximum length for username field
867
"""
868
869
def user_model(self):
870
"""
871
Get user model class.
872
873
Returns:
874
User model class for this storage implementation
875
"""
876
```
877
878
## Framework Integration Examples
879
880
### Django Implementation
881
882
```python
883
from django.db import models
884
from django.contrib.auth.models import AbstractUser
885
from social_core.storage import UserMixin, AssociationMixin
886
887
class User(AbstractUser):
888
"""Custom user model."""
889
pass
890
891
class UserSocialAuth(UserMixin, models.Model):
892
"""Social authentication association model."""
893
user = models.ForeignKey(User, related_name='social_auth', on_delete=models.CASCADE)
894
provider = models.CharField(max_length=32)
895
uid = models.CharField(max_length=255)
896
extra_data = models.JSONField(default=dict)
897
898
class Meta:
899
unique_together = ('provider', 'uid')
900
901
def save(self, *args, **kwargs):
902
super().save(*args, **kwargs)
903
904
class Association(AssociationMixin, models.Model):
905
"""OpenID association model."""
906
server_url = models.CharField(max_length=255)
907
handle = models.CharField(max_length=255)
908
secret = models.CharField(max_length=255)
909
issued = models.IntegerField()
910
lifetime = models.IntegerField()
911
assoc_type = models.CharField(max_length=64)
912
913
def save(self, *args, **kwargs):
914
super().save(*args, **kwargs)
915
```
916
917
### SQLAlchemy Implementation
918
919
```python
920
from sqlalchemy import Column, Integer, String, JSON, ForeignKey
921
from sqlalchemy.ext.declarative import declarative_base
922
from social_core.storage import UserMixin, AssociationMixin
923
924
Base = declarative_base()
925
926
class User(Base):
927
"""User model."""
928
__tablename__ = 'users'
929
id = Column(Integer, primary_key=True)
930
username = Column(String(150), unique=True)
931
email = Column(String(254))
932
933
class UserSocialAuth(UserMixin, Base):
934
"""Social auth model."""
935
__tablename__ = 'user_social_auth'
936
id = Column(Integer, primary_key=True)
937
user_id = Column(Integer, ForeignKey('users.id'))
938
provider = Column(String(32))
939
uid = Column(String(255))
940
extra_data = Column(JSON)
941
942
def save(self):
943
session.add(self)
944
session.commit()
945
```
946
947
The storage models provide the foundation for persisting social authentication data while maintaining flexibility to work with any database system or ORM through the abstract interface pattern.