or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

authentication-actions.mdauthentication-backends.mdexception-handling.mdindex.mdpipeline-system.mdstorage-models.mdstrategy-interface.mdutilities-helpers.md

storage-models.mddocs/

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.