pax_global_header00006660000000000000000000000064151744152330014517gustar00rootroot0000000000000052 comment=abbf3940e5cb8cd5c07c0c20717ed79f922a9a1d social-auth-app-django-5.9.0/000077500000000000000000000000001517441523300157615ustar00rootroot00000000000000social-auth-app-django-5.9.0/.coveragerc000066400000000000000000000001411517441523300200760ustar00rootroot00000000000000[run] branch = True omit = .venv/* .tox/* concurrency = multiprocessing [paths] source = . social-auth-app-django-5.9.0/.github/000077500000000000000000000000001517441523300173215ustar00rootroot00000000000000social-auth-app-django-5.9.0/.github/matchers/000077500000000000000000000000001517441523300211275ustar00rootroot00000000000000social-auth-app-django-5.9.0/.github/matchers/flake8.json000066400000000000000000000004441517441523300231760ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "flake8", "pattern": [ { "code": 4, "column": 3, "file": 1, "line": 2, "message": 5, "regexp": "^([^:]*):(\\d+):(\\d+): (\\w+\\d\\d\\d) (.*)$" } ] } ] } social-auth-app-django-5.9.0/.github/renovate.json000066400000000000000000000021071517441523300220370ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:best-practices", ":dependencyDashboard", "helpers:pinGitHubActionDigests" ], "automerge": true, "automergeType": "pr", "automergeStrategy": "rebase", "platformAutomerge": true, "pre-commit": { "enabled": true }, "poetry": { "enabled": false }, "packageRules": [ { "matchDatasources": [ "pypi" ], "rangeStrategy": "widen" } ], "customManagers": [ { "customType": "regex", "managerFilePatterns": [ "/\\.pre-commit-config\\.yaml/" ], "matchStrings": [ "(?[^'\" ]+)==(?[^'\" ,\\s]+)" ], "datasourceTemplate": "pypi", "versioningTemplate": "pep440" }, { "customType": "regex", "managerFilePatterns": [ "/\\.pre-commit-config\\.yaml/" ], "matchStrings": [ "(?[^'\" ]+)@(?[^'\" ,\\s]+)" ], "datasourceTemplate": "npm", "versioningTemplate": "npm" } ] } social-auth-app-django-5.9.0/.github/workflows/000077500000000000000000000000001517441523300213565ustar00rootroot00000000000000social-auth-app-django-5.9.0/.github/workflows/pre-commit.yml000066400000000000000000000014411517441523300241550ustar00rootroot00000000000000name: pre-commit check on: push: pull_request: permissions: contents: read jobs: pre-commit: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.cache/prek key: ${{ runner.os }}-prek-${{ hashFiles('.pre-commit-config.yaml', 'requirements*.txt') }} - name: Setup Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: 3.x - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - run: uvx prek run --all-files env: RUFF_OUTPUT_FORMAT: github social-auth-app-django-5.9.0/.github/workflows/release.yml000066400000000000000000000105021517441523300235170ustar00rootroot00000000000000name: Release on: push: pull_request: permissions: contents: read jobs: build-wheel: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Build wheel run: uv build --wheel - name: Upload wheel uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist-wheel path: dist/*.whl if-no-files-found: error build-sdist: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Build source distribution run: uv build --sdist - name: Upload source distribution uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist-sdist path: dist/*.tar.gz if-no-files-found: error lint-dist: runs-on: ubuntu-latest needs: - build-wheel - build-sdist steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Download distributions uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: dist-* path: dist merge-multiple: true - name: Verify wheel install run: | uv venv .venv-install-whl source .venv-install-whl/bin/activate uv pip install dist/*.whl - name: Verify source install run: | uv venv .venv-install-tar source .venv-install-tar/bin/activate uv pip install dist/*.tar.gz - name: Check package metadata run: uvx twine check dist/* - name: Check distribution contents run: uvx pydistcheck --inspect dist/* - name: Check source metadata run: uvx pyroma dist/*.tar.gz - name: Check wheel contents run: uvx check-wheel-contents dist/*.whl - name: Check source manifest run: uvx check-manifest -v publish-pypi: runs-on: ubuntu-latest needs: lint-dist if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') permissions: id-token: write steps: - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Download distributions uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: dist-* path: dist merge-multiple: true - name: Publish to PyPI run: uv publish --trusted-publishing always publish-github-release: runs-on: ubuntu-latest needs: lint-dist if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') permissions: contents: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Download distributions uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: dist-* path: dist merge-multiple: true - name: Extract release notes env: TAG_NAME: ${{ github.ref_name }} run: | version="${TAG_NAME#v}" awk -v version="$version" ' index($0, "## [" version "]") == 1 { in_section = 1; next } in_section && /^## / { exit } in_section { print } ' CHANGELOG.md > release-notes.md sed -i '1{/^$/d;}' release-notes.md if ! grep -q '[^[:space:]]' release-notes.md; then echo "No CHANGELOG.md section found for ${version}" >&2 exit 1 fi - name: Update GitHub release notes env: GH_TOKEN: ${{ github.token }} TAG_NAME: ${{ github.ref_name }} run: gh release edit "$TAG_NAME" --notes-file release-notes.md - name: Upload GitHub release assets env: GH_TOKEN: ${{ github.token }} TAG_NAME: ${{ github.ref_name }} run: gh release upload "$TAG_NAME" dist/*.whl dist/*.tar.gz --clobber social-auth-app-django-5.9.0/.github/workflows/scorecard.yml000066400000000000000000000065441517441523300240570ustar00rootroot00000000000000# This workflow uses actions that are not certified by GitHub. They are provided # by a third-party and are governed by separate terms of service, privacy # policy, and support documentation. name: Scorecard supply-chain security on: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - cron: 38 20 * * 6 push: branches: [master] # Declare default permissions as read only. permissions: read-all jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest # `publish_results: true` only works when run from the default branch. conditional can be removed if disabled. if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request' permissions: # Needed to upload the results to code-scanning dashboard. security-events: write # Needed to publish results and get a badge (see publish_results below). id-token: write # Uncomment the permissions below if installing in a private repository. # contents: read # actions: read steps: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Run analysis uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: # - you want to enable the Branch-Protection check on a *public* repository, or # - you are installing Scorecard on a *private* repository # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. # repo_token: ${{ secrets.SCORECARD_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers # - Allows the repository to include the Scorecard badge. # - See https://github.com/ossf/scorecard-action#publishing-results. # For private repositories: # - `publish_results` will always be set to `false`, regardless # of the value entered here. publish_results: true # (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore # file_mode: git # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: Upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: SARIF file path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: Upload to code-scanning uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: sarif_file: results.sarif social-auth-app-django-5.9.0/.github/workflows/test.yml000066400000000000000000000036401517441523300230630ustar00rootroot00000000000000name: Tests on: push: pull_request: schedule: - cron: 0 0 * * 0 permissions: contents: read jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: - '3.10' - '3.11' - '3.12' - '3.13' - '3.14' env: PYTHON_VERSION: ${{ matrix.python-version }} PYTHONUNBUFFERED: 1 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: cache-suffix: ${{ matrix.python-version }} - name: Django 5.2.x Test run: | uvx tox -e "py${PYTHON_VERSION/\./}-django52" if: ${{ env.PYTHON_VERSION == '3.10' || env.PYTHON_VERSION == '3.11' || env.PYTHON_VERSION == '3.12' || env.PYTHON_VERSION == '3.13' || env.PYTHON_VERSION == '3.14'}} - name: Django main Test run: | uvx tox -e "py${PYTHON_VERSION/\./}-djangomain" if: ${{ env.PYTHON_VERSION == '3.12' || env.PYTHON_VERSION == '3.13' || env.PYTHON_VERSION == '3.14' }} - name: social-core master Test run: | uvx tox -e "py${PYTHON_VERSION/\./}-socialmaster" if: ${{ env.PYTHON_VERSION == '3.10' || env.PYTHON_VERSION == '3.11' || env.PYTHON_VERSION == '3.13' || env.PYTHON_VERSION == '3.14' }} - name: Coverage run: | uvx coverage combine uvx coverage xml - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: flags: unittests name: Python ${{ matrix.python-version }} token: ${{secrets.CODECOV_TOKEN}} # zizmor: ignore[secrets-outside-env] social-auth-app-django-5.9.0/.gitignore000066400000000000000000000006731517441523300177570ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage .coverage.* .tox uv.lock # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # PyCharm .idea/ test.db local_settings.py sessions/ _build/ fabfile.py changelog.sh .DS_Store .\#* \#*\# .python-version .venv/ social-auth-app-django-5.9.0/.pre-commit-config.yaml000066400000000000000000000030551517441523300222450ustar00rootroot00000000000000# See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-merge-conflict - id: check-yaml - id: check-json - id: check-toml - id: check-merge-conflict - id: debug-statements - id: mixed-line-ending args: [--fix=lf] - id: pretty-format-json args: [--no-sort-keys, --autofix, --no-ensure-ascii] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.12 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - repo: meta hooks: - id: check-hooks-apply - id: check-useless-excludes - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks rev: v2.16.0 hooks: - id: pretty-format-yaml args: [--autofix, --indent, '2'] - repo: https://github.com/pappasam/toml-sort rev: v0.24.4 hooks: - id: toml-sort-fix - repo: https://github.com/abravalheri/validate-pyproject rev: v0.25 hooks: - id: validate-pyproject - repo: https://github.com/executablebooks/mdformat rev: 1.0.0 hooks: - id: mdformat additional_dependencies: - mdformat-gfm==1.0.0 - repo: https://github.com/codespell-project/codespell rev: v2.4.2 hooks: - id: codespell additional_dependencies: - tomli - repo: https://github.com/rhysd/actionlint rev: v1.7.12 hooks: - id: actionlint - repo: https://github.com/zizmorcore/zizmor-pre-commit rev: v1.24.1 hooks: - id: zizmor social-auth-app-django-5.9.0/.well-known/000077500000000000000000000000001517441523300201345ustar00rootroot00000000000000social-auth-app-django-5.9.0/.well-known/funding-manifest-urls000066400000000000000000000001051517441523300242740ustar00rootroot00000000000000https://github.com/python-social-auth/.github/blob/main/funding.json social-auth-app-django-5.9.0/CHANGELOG.md000066400000000000000000000226751517441523300176060ustar00rootroot00000000000000# Change Log All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [5.9.0](https://github.com/python-social-auth/social-app-django/releases/tag/5.9.0) - 2026-04-29 ### Changed - Added async support to `SocialAuthExceptionMiddleware` - Dropped support for Django 5.1, Django 5.2 is now the minimum supported version - Loosened the `social-auth-core` dependency to allow compatible 4.x releases - Improved release automation and GitHub release asset publishing ## [5.8.0](https://github.com/python-social-auth/social-app-django/releases/tag/5.8.0) - 2026-04-20 ### Changed - Added explicit Django 5.1, 5.2, and 6.0 package classifiers - `DjangoStrategy` now lazily creates a session when initialized without a request - Removed legacy `replaces` metadata from historical squashed migrations - Updated historical `unique_together` migration declarations for newer Django compatibility ## [5.7.0](https://github.com/python-social-auth/social-app-django/releases/tag/5.7.0) - 2025-12-18 ### Changed - Integrated with `social_core` using registry instead of monkey patching it ## [5.6.0](https://github.com/python-social-auth/social-app-django/releases/tag/5.6.0) - 2025-10-09 ### Changed - Fixed possibly unsafe account association ([CVE-2025-61783](https://github.com/python-social-auth/social-app-django/security/advisories/GHSA-wv4w-6qv2-qqfg)) - Storage now filters for active users, you might need to customize `SOCIAL_AUTH_ACTIVE_USERS_FILTER` if your custom model does not have the `is_active` field ### Added - Django 6.0 and Python 3.14 compatibility - Type annotations - LoginRequiredMiddleware compatibility - `RAISE_EXCEPTIONS` and `LOGIN_ERROR_URL` can be configured per backend ## [5.5.1](https://github.com/python-social-auth/social-app-django/releases/tag/5.5.1) - 2025-06-27 ### Changed - Fixed authentication with OpenID based services ## [5.5.0](https://github.com/python-social-auth/social-app-django/releases/tag/5.5.0) - 2025-06-27 ### Changed - Dropped support for older Django versions. - Added non-empty constraint on `uid`. - Added support for session restore with stricter SameSite cookie policy. ## [5.4.3](https://github.com/python-social-auth/social-app-django/releases/tag/5.4.3) - 2025-02-13 ### Changed - Tested with recent Django and Python - Modernized build system - Fixed rollback of migrations ## [5.4.2](https://github.com/python-social-auth/social-app-django/releases/tag/5.4.2) - 2024-07-12 ### Changed - Fixed `UserSocialAuth` creation by allowing `JSONField` to be blank - Fixed the assumption that UID can only be an integer (#571) - Fixed revert of migration `0013_migrate_extra_data.py` ## [5.4.1](https://github.com/python-social-auth/social-app-django/releases/tag/5.4.1) - 2024-04-24 ### Changed - Added reverse migration for JSON field - Fixed improper handling of case sensitivity with MySQL/MariaDB (CVE-2024-32879) ## [5.4.0](https://github.com/python-social-auth/social-app-django/releases/tag/5.4.0) - 2023-10-17 ### Changed - Improved JSON field migration performance - Introduce configuration to request POST only requests for social authentication - Updated list of supported Django and Python versions ## [5.3.0](https://github.com/python-social-auth/social-app-django/releases/tag/5.3.0) - 2023-09-01 ### Changed - Uses Django native JSON field ## [5.2.0](https://github.com/python-social-auth/social-app-django/releases/tag/5.2.0) - 2023-03-31 ### Changed - Removed support for Django\<3.2 - Fixed missing migration issue ## [5.1.0](https://github.com/python-social-auth/social-app-django/releases/tag/5.1.0) - 2023-03-15 ### Changed - Compatibility with recent Django and Python versions - Coding style improvements - Improved error handling in SocialAuthExceptionMiddleware ## [5.0.0](https://github.com/python-social-auth/social-app-django/releases/tag/5.0.0) - 2021-08-05 ### Changed - Removed compat shims for obsolete Django versions - Switch from deprecated `django.conf.urls.url` to `django.urls.path` - Use query `.exists()` instead of `.count() > 0` - Added testing for Django 3.0 - Drop support for Python 2 - Django generic `JSONField` support, details documented [here](https://python-social-auth.readthedocs.io/en/latest/configuration/django.html#json-field-support) - Django 3.2+ compatibility - Use `_default_manager` instead of `objects` ## [4.0.0](https://github.com/python-social-auth/social-app-django/releases/tag/4.0.0) - 2020-06-20 ### Changed - Dropped support for older Django versions (1.8, 1.9, 1.10, 2.0) - Fix `TypeError` when continuing a pipeline in Django 2.1 ## [3.4.0](https://github.com/python-social-auth/social-app-django/releases/tag/3.4.0) - 2020-05-30 ### Changed - Correct release mechanism ## [3.3.0](https://github.com/python-social-auth/social-app-django/releases/tag/3.3.0) - 2020-05-30 ### Changed - Updated release and tests mechanism ## [3.2.0](https://github.com/python-social-auth/social-app-django/releases/tag/3.2.0) - 2020-05-30 ### Changed - Increase social-core dependency version ### Added - Implement `get` and `delete` class methods for `DjangoNonceMixin` - Added `created` and `modified` fields to `UserSocialAuth` model ## [3.1.0](https://github.com/python-social-auth/social-app-django/releases/tag/3.1.0) - 2018-10-31 ### Changed - Updated `JSONField.from_db_value` signature to support multiple Django versions by accepting just the needed parameters. ## [3.0.0](https://github.com/python-social-auth/social-app-django/releases/tag/3.0.0) - 2018-10-28 ### Changed - Reduce log level of exceptions to `INFO` if messages app is installed - Encode association secret with `encodebytes` if available - Decode association secret for proper storage - Remove obsolete code from JSONField - Pass `user` as keyword argument to `do_complete` - Cleanup `username` when using email as username - Drop Python 3.3 support - Correct spelling errors - Correct version that renamed `field.rel` - Reduce error logs in `SocialAuthExceptionMiddleware` ## [2.1.0](https://github.com/python-social-auth/social-app-django/releases/tag/2.1.0) - 2017-12-22 ### Changed - Use Django `urlquote` since it handles unicode - Remove version check in favor of import error catch - Remove call to deprecated method `_get_val_from_obj()` - Drop Python 3.3 support ## [2.0.0](https://github.com/python-social-auth/social-app-django/releases/tag/2.0.0) - 2017-10-28 ### Changed - Better default when checking if the middleware should raise the exception - Update `JSONField` default value to `dict` callable - Updated `authenticate()` parameters cleanup to avoid double arguments errors - Fix imports to bring Django 2.0 support - Admin friendly label - Old Django versions (1.8 and below) compatibility dropped - Python 3.6 and Django 2.0 tests - Management command to clean stale data (partial sessions and codes) ### Added - Added `JSONField` support PostgreSQL builtin option if configured - Added strategy / models / views tests - Added timestamps to Partial and Code models ## [1.2.0](https://github.com/python-social-auth/social-app-django/releases/tag/1.2.0) - 2017-05-06 ### Added - Check for a `MAX_SESSION_LENGTH` setting when logging in and setting session expiry. ### Changed - Added `on_cascade` clauses to migrations. - Restrict association URL to just integer ids ## [1.1.0](https://github.com/python-social-auth/social-app-django/releases/tag/1.1.0) - 2017-02-10 ### Added - Authenticate cleanup method override to discard request parameter getting passed starting from Django 1.11 ## [1.0.1](https://github.com/python-social-auth/social-app-django/releases/tag/1.0.1) - 2017-01-29 ### Changed - Remove migration replacement to nonexistent reference - Ensure atomic transaction if active ## [1.0.0](https://github.com/python-social-auth/social-app-django/releases/tag/1.0.0) - 2017-01-22 ### Added - Partial pipeline DB storage implementation - Explicit app_label definition in model classes ### Changed - Monkey patch BaseAuth to load the current strategy to workaround django load_backend() call - Remove usage of set/get current strategy methods - Remove usage of `social_auth` related name since it should be consider a simple helper. ## [0.1.0](https://github.com/python-social-auth/social-app-django/releases/tag/0.1.0) - 2016-12-28 ### Added - Let Django resolve URL when getting from settings (port of [#905](https://github.com/omab/python-social-auth/pull/905) by webjunkie) - Add setting to fine-tune admin search fields (port of [#1035](https://github.com/omab/python-social-auth/pull/1035) by atugushev) ### Changed - Fixed `REDIRECT_URL_VALUE` value to be quoted by default. Refs [#875](https://github.com/omab/python-social-auth/issues/875) - Django strategy should respect X-Forwarded-Port (port of [#841](https://github.com/omab/python-social-auth/pull/841) by omarkhan) - Fixed use of old private API (port of [#822](https://github.com/omab/python-social-auth/pull/822) by eranmarom) - Add ON DELETE CASCADE for user fk (port of [#1015](https://github.com/omab/python-social-auth/pull/1015) by artofhuman) - Avoid usage of SubfieldBase on 1.8 and 1.9 versions (port of [#1008](https://github.com/omab/python-social-auth/pull/1008) by tom-dalton-fanduel) ## [0.0.1](https://github.com/python-social-auth/social-app-django/releases/tag/0.0.1) - 2016-11-27 ### Changed - Split from the monolitic [python-social-auth](https://github.com/omab/python-social-auth) codebase social-auth-app-django-5.9.0/LICENSE000066400000000000000000000027711517441523300167750ustar00rootroot00000000000000Copyright (c) 2012-2016, Matías Aguirre All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of this project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. social-auth-app-django-5.9.0/MANIFEST.in000066400000000000000000000003301517441523300175130ustar00rootroot00000000000000recursive-include social_django *.py recursive-include tests *.py include manage.py include *.txt CHANGELOG.md LICENSE README.md recursive-exclude social_django *.pyc recursive-include tests *.html exclude .tox .git social-auth-app-django-5.9.0/README.md000066400000000000000000000032651517441523300172460ustar00rootroot00000000000000# Python Social Auth - Django Python Social Auth is an easy to setup social authentication/registration mechanism with support for several frameworks and auth providers. ## Description This is the [Django](https://www.djangoproject.com/) component of the [python-social-auth ecosystem](https://github.com/python-social-auth/social-core), it implements the needed functionality to integrate [social-auth-core](https://github.com/python-social-auth/social-core) in a Django based project. ## Django version This project will focus on the currently supported Django releases as stated on the [Django Project Supported Versions table](https://www.djangoproject.com/download/#supported-versions). Backward compatibility with unsupported versions won't be enforced. ## Documentation Project documentation is available at https://python-social-auth.readthedocs.io/. ## Setup ```shell $ pip install social-auth-app-django ``` ## Contributing Contributions are welcome! Only the core and Django modules are currently in development. All others are in maintenance only mode, and maintainers are especially welcome there. See the [CONTRIBUTING.md](https://github.com/python-social-auth/.github/blob/main/CONTRIBUTING.md) document for details. ## Versioning This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html). ## License This project follows the BSD license. See the [LICENSE](LICENSE) for details. ## Donations This project welcomes donations to make the development sustainable, you can fund Python Social Auth on following platforms: - [GitHub Sponsors](https://github.com/sponsors/python-social-auth/) - [Open Collective](https://opencollective.com/python-social-auth) social-auth-app-django-5.9.0/manage.py000077500000000000000000000003701517441523300175660ustar00rootroot00000000000000#!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) social-auth-app-django-5.9.0/pyproject.toml000066400000000000000000000071701517441523300207020ustar00rootroot00000000000000[build-system] build-backend = "setuptools.build_meta" requires = ["setuptools>=78.0.2"] [dependency-groups] dev = [ "coverage>=3.6", "django-stubs-ext==6.0.3", "django-stubs==6.0.3", "mypy==1.20.2", "pre-commit", "pyright==1.1.409", "tox" ] [project] authors = [ {email = "matiasaguirre@gmail.com", name = "Matias Aguirre"}, {email = "michal@weblate.org", name = "Michal Čihař"} ] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Django :: 5.2", "Framework :: Django :: 6.0", "Framework :: Django", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python", "Topic :: Internet" ] dependencies = [ "asgiref>=3.8.1", "Django>=5.2", "social-auth-core>=4.8.3,<5.0.0" ] description = "Python Social Authentication, Django integration." keywords = ["django", "oauth", "openid", "saml", "social auth"] license = "BSD-3-Clause" license-files = ["LICENSE"] name = "social-auth-app-django" readme = "README.md" requires-python = ">=3.10" version = "5.9.0" [project.optional-dependencies] # This is present until pip implements supports for PEP 735 # see https://github.com/pypa/pip/issues/12963 dev = [ "coverage>=3.6", "django-stubs-ext==6.0.3", "django-stubs==6.0.3", "mypy==1.20.2", "pre-commit", "pyright==1.1.409", "tox" ] [project.urls] Changelog = "https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md" Funding = "https://opencollective.com/python-social-auth" Homepage = "https://github.com/python-social-auth/social-app-django" [tool.check-manifest] ignore = [ ".coveragerc", ".pre-commit-config.yaml", ".well-known/funding-manifest-urls", "tox.ini" ] [tool.check-wheel-contents] ignore = [ "W004" # Django migrations fail here ] [tool.django-stubs] django_settings_module = "tests.settings" strict_settings = false [tool.mypy] check_untyped_defs = true plugins = [ "mypy_django_plugin.main" ] [tool.pyright] pythonVersion = "3.10" reportAttributeAccessIssue = "none" reportOptionalMemberAccess = "none" reportPossiblyUnboundVariable = "none" [tool.ruff] # Ignore some well known paths exclude = [ "*.egg", ".tox", ".venv", "build", "db/env.py", "db/versions/*.py", "dist", "doc", "site" ] line-length = 120 [tool.ruff.lint] ignore = [ "ANN", # TODO: Missing type annotations "ARG001", # TODO: Unused function argument (mostly for API compatibility) "ARG002", # TODO: Unused method argument (mostly for API compatibility) "B026", # TODO: Star-arg unpacking after a keyword argument is strongly discouraged "COM812", # CONFIG: incompatible with formatter "D", # TODO: Missing documentation "D203", # CONFIG: incompatible with D211 "D212", # CONFIG: incompatible with D213 "DJ008", # TODO: Model does not define `__str__` method "FBT002", # TODO: Boolean default positional argument in function definition "FBT003", # TODO: Boolean positional value in function call "PT", # CONFIG: Not using pytest "RUF012" # TODO: ClassVar type annotations ] select = ["ALL"] [tool.ruff.lint.mccabe] max-complexity = 10 [tool.ruff.lint.per-file-ignores] "social_django/migrations/*.py" = ["RUF012"] "tests/settings.py" = ["PTH"] [tool.setuptools] packages = ["social_django"] [tool.tomlsort] ignore_case = true sort_inline_arrays = true sort_inline_tables = true sort_table_keys = true spaces_before_inline_comment = 2 social-auth-app-django-5.9.0/social_django/000077500000000000000000000000001517441523300205555ustar00rootroot00000000000000social-auth-app-django-5.9.0/social_django/__init__.py000066400000000000000000000001361517441523300226660ustar00rootroot00000000000000import importlib.metadata __version__ = importlib.metadata.version("social-auth-app-django") social-auth-app-django-5.9.0/social_django/admin.py000066400000000000000000000041641517441523300222240ustar00rootroot00000000000000"""Admin settings""" from itertools import chain from django.conf import settings from django.contrib import admin from social_core.utils import setting_name from .models import Association, Nonce, UserSocialAuth @admin.register(UserSocialAuth) class UserSocialAuthOption(admin.ModelAdmin): """Social Auth user options""" list_display = ("user", "id", "provider", "uid", "created", "modified") list_filter = ("provider",) raw_id_fields = ("user",) readonly_fields = ("created", "modified") list_select_related = True def get_search_fields(self, request=None): search_fields = getattr(settings, setting_name("ADMIN_USER_SEARCH_FIELDS"), None) if search_fields is None: _User = UserSocialAuth.user_model() # noqa: N806 username = getattr(_User, "USERNAME_FIELD", None) or (hasattr(_User, "username") and "username") or None fieldnames = ("first_name", "last_name", "email", username) all_names = self._get_all_field_names( _User._meta # noqa: SLF001 ) search_fields = [name for name in fieldnames if name and name in all_names] return ["user__" + name for name in search_fields] + getattr(settings, setting_name("ADMIN_SEARCH_FIELDS"), []) @staticmethod def _get_all_field_names(model): names = chain.from_iterable( (field.name, field.attname) if hasattr(field, "attname") else (field.name,) for field in model.get_fields() # For complete backwards compatibility, you may want to exclude # GenericForeignKey from the results. if not (field.many_to_one and field.related_model is None) ) return list(set(names)) @admin.register(Nonce) class NonceOption(admin.ModelAdmin): """Nonce options""" list_display = ("id", "server_url", "timestamp", "salt") search_fields = ("server_url",) @admin.register(Association) class AssociationOption(admin.ModelAdmin): """Association options""" list_display = ("id", "server_url", "assoc_type") list_filter = ("assoc_type",) search_fields = ("server_url",) social-auth-app-django-5.9.0/social_django/apps.py000066400000000000000000000016631517441523300221000ustar00rootroot00000000000000from django.apps import AppConfig from social_core.registry import REGISTRY class PythonSocialAuthConfig(AppConfig): # Explicitly set default auto field type to avoid migrations in Django 3.2+ default_auto_field = "django.db.models.BigAutoField" # Full Python path to the application eg. 'django.contrib.admin'. name = "social_django" # Last component of the Python path to the application eg. 'admin'. label = "social_django" # Human-readable name for the application eg. "Admin". verbose_name = "Python Social Auth" def ready(self) -> None: from .utils import load_strategy # noqa: PLC0415 super().ready() # django.contrib.auth.load_backend() will import and instantiate the # authentication backend ignoring the possibility that it might # require more arguments. Here we set a default strategy for that case. REGISTRY.default_strategy = load_strategy() social-auth-app-django-5.9.0/social_django/config.py000066400000000000000000000002071517441523300223730ustar00rootroot00000000000000# For backward compatibility. You should use the configuration from apps module from .apps import PythonSocialAuthConfig # noqa: F401 social-auth-app-django-5.9.0/social_django/context_processors.py000066400000000000000000000030701517441523300250750ustar00rootroot00000000000000from urllib.parse import quote from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME from django.http.multipartparser import MultiPartParserError from django.utils.functional import SimpleLazyObject, empty from social_core.backends.utils import user_backends_data from .utils import Storage class LazyDict(SimpleLazyObject): """Lazy dict initialization.""" def __getitem__(self, name): if self._wrapped is empty: self._setup() return self._wrapped[name] def __setitem__(self, name, value): if self._wrapped is empty: self._setup() self._wrapped[name] = value def backends(request): """ Load Social Auth current user data to context under the key 'backends'. Will return the output of social_core.backends.utils.user_backends_data. """ return {"backends": LazyDict(lambda: user_backends_data(request.user, settings.AUTHENTICATION_BACKENDS, Storage))} def login_redirect(request): """Load current redirect to context.""" try: value = (request.method == "POST" and request.POST.get(REDIRECT_FIELD_NAME)) or request.GET.get( REDIRECT_FIELD_NAME ) except MultiPartParserError: # request POST may be malformed value = None if value: value = quote(value) querystring = REDIRECT_FIELD_NAME + "=" + value else: querystring = "" return { "REDIRECT_FIELD_NAME": REDIRECT_FIELD_NAME, "REDIRECT_FIELD_VALUE": value, "REDIRECT_QUERYSTRING": querystring, } social-auth-app-django-5.9.0/social_django/fields.py000066400000000000000000000013211517441523300223720ustar00rootroot00000000000000""" This is legacy code used only in the database migrations. """ from django.conf import settings from django.db import models from social_core.utils import setting_name POSTGRES_JSONFIELD = getattr(settings, setting_name("POSTGRES_JSONFIELD"), False) JSONFIELD_ENABLED = True if POSTGRES_JSONFIELD else getattr(settings, setting_name("JSONFIELD_ENABLED"), False) if JSONFIELD_ENABLED: JSONFIELD_CUSTOM = getattr(settings, setting_name("JSONFIELD_CUSTOM"), None) if JSONFIELD_CUSTOM is not None: from django.utils.module_loading import import_string JSONField = import_string(JSONFIELD_CUSTOM) else: from django.db.models import JSONField else: JSONField = models.TextField social-auth-app-django-5.9.0/social_django/management/000077500000000000000000000000001517441523300226715ustar00rootroot00000000000000social-auth-app-django-5.9.0/social_django/management/__init__.py000066400000000000000000000000001517441523300247700ustar00rootroot00000000000000social-auth-app-django-5.9.0/social_django/management/commands/000077500000000000000000000000001517441523300244725ustar00rootroot00000000000000social-auth-app-django-5.9.0/social_django/management/commands/__init__.py000066400000000000000000000000001517441523300265710ustar00rootroot00000000000000social-auth-app-django-5.9.0/social_django/management/commands/clearsocial.py000066400000000000000000000015611517441523300273300ustar00rootroot00000000000000from datetime import timedelta from django.core.management.base import BaseCommand from django.utils import timezone from social_django.models import Code, Partial class Command(BaseCommand): help = "removes old not used verification codes and partials" def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( "--age", action="store", type=int, dest="age", default=14, help="how long to keep unused data (in days, defaults to 14)", ) def handle(self, *args, **options): age = timezone.now() - timedelta(days=options["age"]) # Delete old not verified codes Code.objects.filter(verified=False, timestamp__lt=age).delete() # Delete old partial data Partial.objects.filter(timestamp__lt=age).delete() social-auth-app-django-5.9.0/social_django/managers.py000066400000000000000000000006031517441523300227230ustar00rootroot00000000000000from django.db import models class UserSocialAuthManager(models.Manager): """Manager for the UserSocialAuth django model.""" class Meta: app_label = "social_django" def get_social_auth(self, provider, uid): try: return self.select_related("user").get(provider=provider, uid=uid) except self.model.DoesNotExist: return None social-auth-app-django-5.9.0/social_django/middleware.py000066400000000000000000000056121517441523300232500ustar00rootroot00000000000000from urllib.parse import quote from asgiref.sync import iscoroutinefunction, markcoroutinefunction from django.apps import apps from django.conf import settings from django.contrib import messages from django.contrib.messages.api import MessageFailure from django.shortcuts import redirect from django.utils.decorators import sync_and_async_middleware from social_core.exceptions import SocialAuthBaseException from social_core.utils import social_logger @sync_and_async_middleware class SocialAuthExceptionMiddleware: """ Middleware that handles Social Auth AuthExceptions by providing the user with a message, logging an error, and redirecting to some next location. By default, the exception message itself is sent to the user and they are redirected to the location specified in the SOCIAL_AUTH_LOGIN_ERROR_URL setting. This middleware can be extended by overriding the get_message or get_redirect_uri methods, which each accept request and exception. """ def __init__(self, get_response): self.get_response = get_response if iscoroutinefunction(get_response): markcoroutinefunction(self) def __call__(self, request): return self.get_response(request) def process_exception(self, request, exception): strategy = getattr(request, "social_strategy", None) if strategy is None or self.raise_exception(request, exception): return None if isinstance(exception, SocialAuthBaseException): backend = getattr(request, "backend", None) backend_name = getattr(backend, "name", "unknown-backend") message = self.get_message(request, exception) url = self.get_redirect_uri(request, exception) if apps.is_installed("django.contrib.messages"): social_logger.info(message) try: messages.error(request, message, extra_tags=f"social-auth {backend_name}") except MessageFailure: if url: url += (("?" in url and "&") or "?") + f"message={quote(message)}&backend={backend_name}" else: social_logger.error(message) if url: return redirect(url) return None return None def raise_exception(self, request, exception): strategy = getattr(request, "social_strategy", None) if strategy is not None: backend = getattr(request, "backend", None) return strategy.setting("RAISE_EXCEPTIONS", settings.DEBUG, backend=backend) return None def get_message(self, request, exception): return str(exception) def get_redirect_uri(self, request, exception): strategy = getattr(request, "social_strategy", None) backend = getattr(request, "backend", None) return strategy.setting("LOGIN_ERROR_URL", backend=backend) social-auth-app-django-5.9.0/social_django/migrations/000077500000000000000000000000001517441523300227315ustar00rootroot00000000000000social-auth-app-django-5.9.0/social_django/migrations/0001_initial.py000066400000000000000000000113041517441523300253730ustar00rootroot00000000000000from django.conf import settings from django.db import migrations, models from social_core.utils import setting_name from social_django.fields import JSONField from social_django.storage import DjangoAssociationMixin, DjangoCodeMixin, DjangoNonceMixin, DjangoUserMixin USER_MODEL = ( getattr(settings, setting_name("USER_MODEL"), None) or getattr(settings, "AUTH_USER_MODEL", None) or "auth.User" ) UID_LENGTH = getattr(settings, setting_name("UID_LENGTH"), 255) NONCE_SERVER_URL_LENGTH = getattr(settings, setting_name("NONCE_SERVER_URL_LENGTH"), 255) ASSOCIATION_SERVER_URL_LENGTH = getattr(settings, setting_name("ASSOCIATION_SERVER_URL_LENGTH"), 255) ASSOCIATION_HANDLE_LENGTH = getattr(settings, setting_name("ASSOCIATION_HANDLE_LENGTH"), 255) class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(USER_MODEL), ] operations = [ migrations.CreateModel( name="Association", fields=[ ( "id", models.AutoField( verbose_name="ID", serialize=False, auto_created=True, primary_key=True, ), ), ( "server_url", models.CharField(max_length=ASSOCIATION_SERVER_URL_LENGTH), ), ("handle", models.CharField(max_length=ASSOCIATION_HANDLE_LENGTH)), ("secret", models.CharField(max_length=255)), ("issued", models.IntegerField()), ("lifetime", models.IntegerField()), ("assoc_type", models.CharField(max_length=64)), ], options={ "db_table": "social_auth_association", }, bases=(models.Model, DjangoAssociationMixin), ), migrations.CreateModel( name="Code", fields=[ ( "id", models.AutoField( verbose_name="ID", serialize=False, auto_created=True, primary_key=True, ), ), ("email", models.EmailField(max_length=75)), ("code", models.CharField(max_length=32, db_index=True)), ("verified", models.BooleanField(default=False)), ], options={ "db_table": "social_auth_code", }, bases=(models.Model, DjangoCodeMixin), ), migrations.CreateModel( name="Nonce", fields=[ ( "id", models.AutoField( verbose_name="ID", serialize=False, auto_created=True, primary_key=True, ), ), ("server_url", models.CharField(max_length=NONCE_SERVER_URL_LENGTH)), ("timestamp", models.IntegerField()), ("salt", models.CharField(max_length=65)), ], options={ "db_table": "social_auth_nonce", }, bases=(models.Model, DjangoNonceMixin), ), migrations.CreateModel( name="UserSocialAuth", fields=[ ( "id", models.AutoField( verbose_name="ID", serialize=False, auto_created=True, primary_key=True, ), ), ("provider", models.CharField(max_length=32)), ("uid", models.CharField(max_length=UID_LENGTH)), ("extra_data", JSONField(default="{}")), ( "user", models.ForeignKey( related_name="social_auth", to=USER_MODEL, on_delete=models.CASCADE, ), ), ], options={ "db_table": "social_auth_usersocialauth", }, bases=(models.Model, DjangoUserMixin), ), migrations.AlterUniqueTogether( name="usersocialauth", unique_together={("provider", "uid")}, ), migrations.AlterUniqueTogether( name="code", unique_together={("email", "code")}, ), migrations.AlterUniqueTogether( name="nonce", unique_together={("server_url", "timestamp", "salt")}, ), ] social-auth-app-django-5.9.0/social_django/migrations/0002_add_related_name.py000066400000000000000000000012321517441523300271720ustar00rootroot00000000000000from django.conf import settings from django.db import migrations, models from social_core.utils import setting_name USER_MODEL = ( getattr(settings, setting_name("USER_MODEL"), None) or getattr(settings, "AUTH_USER_MODEL", None) or "auth.User" ) class Migration(migrations.Migration): dependencies = [ ("social_django", "0001_initial"), ] operations = [ migrations.AlterField( model_name="usersocialauth", name="user", field=models.ForeignKey( related_name="social_auth", to=USER_MODEL, on_delete=models.CASCADE, ), ), ] social-auth-app-django-5.9.0/social_django/migrations/0003_alter_email_max_length.py000066400000000000000000000007551517441523300304400ustar00rootroot00000000000000from django.conf import settings from django.db import migrations, models from social_core.utils import setting_name EMAIL_LENGTH = getattr(settings, setting_name("EMAIL_LENGTH"), 254) class Migration(migrations.Migration): dependencies = [ ("social_django", "0002_add_related_name"), ] operations = [ migrations.AlterField( model_name="code", name="email", field=models.EmailField(max_length=EMAIL_LENGTH), ), ] social-auth-app-django-5.9.0/social_django/migrations/0004_auto_20160423_0400.py000066400000000000000000000006021517441523300263400ustar00rootroot00000000000000from django.db import migrations from social_django.fields import JSONField class Migration(migrations.Migration): dependencies = [ ("social_django", "0003_alter_email_max_length"), ] operations = [ migrations.AlterField( model_name="usersocialauth", name="extra_data", field=JSONField(default=dict), ), ] social-auth-app-django-5.9.0/social_django/migrations/0005_auto_20160727_2333.py000066400000000000000000000005621517441523300263640ustar00rootroot00000000000000# Generated by Django 1.9.5 on 2016-07-28 02:33 from django.db import migrations class Migration(migrations.Migration): dependencies = [ ("social_django", "0004_auto_20160423_0400"), ] operations = [ migrations.AlterUniqueTogether( name="association", unique_together=(("server_url", "handle"),), ), ] social-auth-app-django-5.9.0/social_django/migrations/0006_partial.py000066400000000000000000000021721517441523300254060ustar00rootroot00000000000000# Generated by Django 1.10.4 on 2017-01-02 11:54 from django.db import migrations, models import social_django.fields import social_django.storage class Migration(migrations.Migration): dependencies = [ ("social_django", "0005_auto_20160727_2333"), ] operations = [ migrations.CreateModel( name="Partial", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("token", models.CharField(db_index=True, max_length=32)), ("next_step", models.PositiveSmallIntegerField(default=0)), ("backend", models.CharField(max_length=32)), ("data", social_django.fields.JSONField(default=dict)), ], options={ "db_table": "social_auth_partial", }, bases=(models.Model, social_django.storage.DjangoPartialMixin), ), ] social-auth-app-django-5.9.0/social_django/migrations/0007_code_timestamp.py000066400000000000000000000007571517441523300267570ustar00rootroot00000000000000# Generated by Django 1.10.7 on 2017-06-08 06:54 from django.db import migrations, models from django.utils import timezone class Migration(migrations.Migration): dependencies = [ ("social_django", "0006_partial"), ] operations = [ migrations.AddField( model_name="code", name="timestamp", field=models.DateTimeField(auto_now_add=True, db_index=True, default=timezone.now), preserve_default=False, ), ] social-auth-app-django-5.9.0/social_django/migrations/0008_partial_timestamp.py000066400000000000000000000007711517441523300274760ustar00rootroot00000000000000# Generated by Django 1.10.7 on 2017-06-08 06:57 from django.db import migrations, models from django.utils import timezone class Migration(migrations.Migration): dependencies = [ ("social_django", "0007_code_timestamp"), ] operations = [ migrations.AddField( model_name="partial", name="timestamp", field=models.DateTimeField(auto_now_add=True, db_index=True, default=timezone.now), preserve_default=False, ), ] social-auth-app-django-5.9.0/social_django/migrations/0009_auto_20191118_0520.py000066400000000000000000000013451517441523300263620ustar00rootroot00000000000000# Generated by Django 2.2.7 on 2019-11-18 05:20 import django.utils.timezone from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("social_django", "0008_partial_timestamp"), ] operations = [ migrations.AddField( model_name="usersocialauth", name="created", field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), preserve_default=False, ), migrations.AddField( model_name="usersocialauth", name="modified", field=models.DateTimeField(auto_now=True, default=django.utils.timezone.now), preserve_default=False, ), ] social-auth-app-django-5.9.0/social_django/migrations/0010_uid_db_index.py000066400000000000000000000007771517441523300263730ustar00rootroot00000000000000from django.conf import settings from django.db import migrations, models from social_core.utils import setting_name UID_LENGTH = getattr(settings, setting_name("UID_LENGTH"), 255) class Migration(migrations.Migration): dependencies = [ ("social_django", "0009_auto_20191118_0520"), ] operations = [ migrations.AlterField( model_name="usersocialauth", name="uid", field=models.CharField(max_length=UID_LENGTH, db_index=True), ), ] social-auth-app-django-5.9.0/social_django/migrations/0011_alter_id_fields.py000066400000000000000000000024131517441523300270550ustar00rootroot00000000000000# Generated by Django 3.2 on 2021-09-23 12:14 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("social_django", "0010_uid_db_index"), ] operations = [ migrations.AlterField( model_name="association", name="id", field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="code", name="id", field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="nonce", name="id", field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="partial", name="id", field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="usersocialauth", name="id", field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), ] social-auth-app-django-5.9.0/social_django/migrations/0012_usersocialauth_extra_data_new.py000066400000000000000000000010501517441523300320410ustar00rootroot00000000000000# Generated by Django 4.0 on 2023-06-10 07:10 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("social_django", "0011_alter_id_fields"), ] operations = [ migrations.AddField( model_name="usersocialauth", name="extra_data_new", field=models.JSONField(default=dict), ), migrations.AddField( model_name="partial", name="data_new", field=models.JSONField(default=dict), ), ] social-auth-app-django-5.9.0/social_django/migrations/0013_migrate_extra_data.py000066400000000000000000000055151517441523300276000ustar00rootroot00000000000000# Generated by Django 4.0 on 2023-06-10 07:10 import contextlib import json from django.db import migrations, models MAX_BATCH_SIZE = 1000 def migrate_json_field(apps, schema_editor): UserSocialAuth = apps.get_model("social_django", "UserSocialAuth") Partial = apps.get_model("social_django", "Partial") db_alias = schema_editor.connection.alias to_be_updated = [] for auth in UserSocialAuth.objects.using(db_alias).exclude(extra_data='""').iterator(): old_value = auth.extra_data if isinstance(old_value, str): with contextlib.suppress(json.JSONDecodeError): old_value = json.loads(old_value) auth.extra_data_new = old_value to_be_updated.append(auth) if len(to_be_updated) >= MAX_BATCH_SIZE: UserSocialAuth.objects.bulk_update(to_be_updated, ["extra_data_new"]) to_be_updated.clear() if to_be_updated: UserSocialAuth.objects.bulk_update(to_be_updated, ["extra_data_new"]) to_be_updated.clear() for auth in Partial.objects.using(db_alias).all(): old_value = auth.data if isinstance(old_value, str): with contextlib.suppress(json.JSONDecodeError): old_value = json.loads(old_value) auth.data_new = old_value auth.save(update_fields=["data_new"]) def migrate_json_field_backwards(apps, schema_editor): UserSocialAuth = apps.get_model("social_django", "UserSocialAuth") Partial = apps.get_model("social_django", "Partial") db_alias = schema_editor.connection.alias to_be_updated = [] is_text_field = isinstance( UserSocialAuth._meta.get_field("extra_data"), # noqa: SLF001 models.TextField, ) for auth in UserSocialAuth.objects.using(db_alias).iterator(): new_value = auth.extra_data_new if is_text_field: new_value = json.dumps(new_value) auth.extra_data = new_value to_be_updated.append(auth) if len(to_be_updated) >= MAX_BATCH_SIZE: UserSocialAuth.objects.bulk_update(to_be_updated, ["extra_data"]) to_be_updated.clear() if to_be_updated: UserSocialAuth.objects.bulk_update(to_be_updated, ["extra_data"]) to_be_updated.clear() is_text_field = issubclass( type(Partial._meta.get_field("data")), # noqa: SLF001 models.TextField, ) for auth in Partial.objects.using(db_alias).all(): new_value = auth.data_new if is_text_field: new_value = json.dumps(new_value) auth.data = new_value auth.save(update_fields=["data"]) class Migration(migrations.Migration): dependencies = [ ("social_django", "0012_usersocialauth_extra_data_new"), ] operations = [ migrations.RunPython(migrate_json_field, migrate_json_field_backwards, elidable=True), ] social-auth-app-django-5.9.0/social_django/migrations/0014_remove_usersocialauth_extra_data.py000066400000000000000000000006751517441523300325630ustar00rootroot00000000000000# Generated by Django 4.0 on 2023-06-10 07:18 from django.db import migrations class Migration(migrations.Migration): dependencies = [ ("social_django", "0013_migrate_extra_data"), ] operations = [ migrations.RemoveField( model_name="usersocialauth", name="extra_data", ), migrations.RemoveField( model_name="partial", name="data", ), ] 0015_rename_extra_data_new_usersocialauth_extra_data.py000066400000000000000000000010331517441523300355110ustar00rootroot00000000000000social-auth-app-django-5.9.0/social_django/migrations# Generated by Django 4.0 on 2023-06-10 07:18 from django.db import migrations class Migration(migrations.Migration): dependencies = [ ("social_django", "0014_remove_usersocialauth_extra_data"), ] operations = [ migrations.RenameField( model_name="usersocialauth", old_name="extra_data_new", new_name="extra_data", ), migrations.RenameField( model_name="partial", old_name="data_new", new_name="data", ), ] social-auth-app-django-5.9.0/social_django/migrations/0016_alter_usersocialauth_extra_data.py000066400000000000000000000006741517441523300323760ustar00rootroot00000000000000# Generated by Django 3.2.25 on 2024-05-28 19:28 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("social_django", "0015_rename_extra_data_new_usersocialauth_extra_data"), ] operations = [ migrations.AlterField( model_name="usersocialauth", name="extra_data", field=models.JSONField(blank=True, default=dict), ), ] 0017_usersocialauth_user_social_auth_uid_required.py000066400000000000000000000011511517441523300350760ustar00rootroot00000000000000social-auth-app-django-5.9.0/social_django/migrations# Generated by Django 5.1.7 on 2025-03-14 12:16 from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("social_django", "0016_alter_usersocialauth_extra_data"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AddConstraint( model_name="usersocialauth", constraint=models.CheckConstraint( condition=models.Q(("uid", ""), _negated=True), name="user_social_auth_uid_required", ), ), ] social-auth-app-django-5.9.0/social_django/migrations/__init__.py000066400000000000000000000000001517441523300250300ustar00rootroot00000000000000social-auth-app-django-5.9.0/social_django/models.py000066400000000000000000000113361517441523300224160ustar00rootroot00000000000000"""Django ORM models for Social Auth""" from django.conf import settings from django.db import models from django.db.utils import IntegrityError from social_core.utils import setting_name from .managers import UserSocialAuthManager from .storage import ( BaseDjangoStorage, DjangoAssociationMixin, DjangoCodeMixin, DjangoNonceMixin, DjangoPartialMixin, DjangoUserMixin, ) USER_MODEL = ( getattr(settings, setting_name("USER_MODEL"), None) or getattr(settings, "AUTH_USER_MODEL", None) or "auth.User" ) UID_LENGTH = getattr(settings, setting_name("UID_LENGTH"), 255) EMAIL_LENGTH = getattr(settings, setting_name("EMAIL_LENGTH"), 254) NONCE_SERVER_URL_LENGTH = getattr(settings, setting_name("NONCE_SERVER_URL_LENGTH"), 255) ASSOCIATION_SERVER_URL_LENGTH = getattr(settings, setting_name("ASSOCIATION_SERVER_URL_LENGTH"), 255) ASSOCIATION_HANDLE_LENGTH = getattr(settings, setting_name("ASSOCIATION_HANDLE_LENGTH"), 255) class AbstractUserSocialAuth(models.Model, DjangoUserMixin): """Abstract Social Auth association model""" user = models.ForeignKey(USER_MODEL, related_name="social_auth", on_delete=models.CASCADE) provider = models.CharField(max_length=32) uid = models.CharField(max_length=UID_LENGTH, db_index=True) extra_data = models.JSONField(default=dict, blank=True) created = models.DateTimeField(auto_now_add=True) modified = models.DateTimeField(auto_now=True) objects = UserSocialAuthManager() class Meta: constraints = [models.CheckConstraint(condition=~models.Q(uid=""), name="user_social_auth_uid_required")] abstract = True def __str__(self): return str(self.user) @classmethod def get_social_auth(cls, provider: str, uid: str | int): if not isinstance(uid, str): uid = str(uid) for social in cls.objects.select_related("user").filter(provider=provider, uid=uid): # We need to compare to filter out case-insensitive lookups in # some databases (MySQL/MariaDB) if social.uid == uid: return social return None @classmethod def username_max_length(cls): username_field = cls.username_field() field = cls.user_model()._meta.get_field(username_field) # noqa: SLF001 return field.max_length @classmethod def user_model(cls): return cls._meta.get_field("user").remote_field.model class UserSocialAuth(AbstractUserSocialAuth): """Social Auth association model""" class Meta(AbstractUserSocialAuth.Meta): """Meta data""" app_label = "social_django" unique_together = ("provider", "uid") db_table = "social_auth_usersocialauth" class Nonce(models.Model, DjangoNonceMixin): """One use numbers""" server_url = models.CharField(max_length=NONCE_SERVER_URL_LENGTH) timestamp = models.IntegerField() salt = models.CharField(max_length=65) class Meta: app_label = "social_django" unique_together = ("server_url", "timestamp", "salt") db_table = "social_auth_nonce" class Association(models.Model, DjangoAssociationMixin): """OpenId account association""" server_url = models.CharField(max_length=ASSOCIATION_SERVER_URL_LENGTH) handle = models.CharField(max_length=ASSOCIATION_HANDLE_LENGTH) secret = models.CharField(max_length=255) # Stored base64 encoded issued = models.IntegerField() lifetime = models.IntegerField() assoc_type = models.CharField(max_length=64) class Meta: app_label = "social_django" db_table = "social_auth_association" unique_together = ( "server_url", "handle", ) class Code(models.Model, DjangoCodeMixin): email = models.EmailField(max_length=EMAIL_LENGTH) code = models.CharField(max_length=32, db_index=True) verified = models.BooleanField(default=False) timestamp = models.DateTimeField(auto_now_add=True, db_index=True) class Meta: app_label = "social_django" db_table = "social_auth_code" unique_together = ("email", "code") class Partial(models.Model, DjangoPartialMixin): token = models.CharField(max_length=32, db_index=True) next_step = models.PositiveSmallIntegerField(default=0) backend = models.CharField(max_length=32) data = models.JSONField(default=dict) timestamp = models.DateTimeField(auto_now_add=True, db_index=True) class Meta: app_label = "social_django" db_table = "social_auth_partial" class DjangoStorage(BaseDjangoStorage): user = UserSocialAuth nonce = Nonce association = Association code = Code partial = Partial @classmethod def is_integrity_error(cls, exception): return exception.__class__ is IntegrityError social-auth-app-django-5.9.0/social_django/py.typed000066400000000000000000000000001517441523300222420ustar00rootroot00000000000000social-auth-app-django-5.9.0/social_django/storage.py000066400000000000000000000156671517441523300226120ustar00rootroot00000000000000"""Django ORM models for Social Auth""" from __future__ import annotations import base64 from typing import TYPE_CHECKING from django.conf import settings from django.core.exceptions import FieldDoesNotExist from django.db import router, transaction from django.db.utils import IntegrityError from social_core.exceptions import AuthAlreadyAssociated from social_core.storage import ( AssociationMixin, BaseStorage, CodeMixin, NonceMixin, PartialMixin, UserMixin, ) from social_core.utils import setting_name if TYPE_CHECKING: from django.db.models import QuerySet class DjangoUserMixin(UserMixin): """Social Auth association model""" @classmethod def changed(cls, user): user.save() def set_extra_data(self, extra_data=None): if super().set_extra_data(extra_data): self.save() @classmethod def allowed_to_disconnect(cls, user, backend_name, association_id=None): if association_id is not None: qs = cls.objects.exclude(id=association_id) else: qs = cls.objects.exclude(provider=backend_name) qs = qs.filter(user=user) valid_password = user.has_usable_password() if hasattr(user, "has_usable_password") else True return valid_password or qs.exists() @classmethod def disconnect(cls, entry): entry.delete() @classmethod def username_field(cls): return getattr(cls.user_model(), "USERNAME_FIELD", "username") @classmethod def user_exists(cls, *args, **kwargs): """ Return True/False if a User instance exists with the given arguments. Arguments are directly passed to filter() manager method. """ if "username" in kwargs: kwargs[cls.username_field()] = kwargs.pop("username") return cls.filter_users(*args, **kwargs).exists() @classmethod def get_username(cls, user): return getattr(user, cls.username_field(), None) @classmethod def create_user(cls, *args, **kwargs): username_field = cls.username_field() manager = cls.user_model()._default_manager # noqa: SLF001 if "username" in kwargs: if username_field not in kwargs: kwargs[username_field] = kwargs.pop("username") else: # If username_field is 'email' and there is no field named "username" # then latest should be removed from kwargs. try: cls.user_model()._meta.get_field("username") # noqa: SLF001 except FieldDoesNotExist: kwargs.pop("username") # If the create fails below due to an IntegrityError, ensure that the transaction # stays undamaged by wrapping the create in an atomic. using = router.db_for_write(cls.user_model()) try: with transaction.atomic(using=using): return manager.create_user(*args, **kwargs) except IntegrityError as exc: raise AuthAlreadyAssociated(None) from exc @classmethod def filter_users(cls, *args, **kwargs) -> QuerySet: model = cls.user_model() manager = model._default_manager # noqa: SLF001 return manager.filter(*args, **kwargs) @classmethod def filter_active_users(cls, *args, **kwargs) -> QuerySet: active_filter = getattr(settings, setting_name("ACTIVE_USERS_FILTER"), {"is_active": True}) kwargs.update(active_filter) return cls.filter_users(*args, **kwargs) @classmethod def get_user(cls, pk=None, **kwargs): if pk: kwargs = {"pk": pk} users = cls.filter_active_users(**kwargs) if len(users) != 1: return None return users[0] @classmethod def get_users_by_email(cls, email): user_model = cls.user_model() email_field = getattr(user_model, "EMAIL_FIELD", "email") return cls.filter_active_users(**{f"{email_field}__iexact": email}) @classmethod def get_social_auth(cls, provider, uid): if not isinstance(uid, str): uid = str(uid) try: return cls.objects.get(provider=provider, uid=uid) except cls.DoesNotExist: return None @classmethod def get_social_auth_for_user(cls, user, provider=None, id=None): # noqa: A002 qs = cls.objects.filter(user=user) if provider: qs = qs.filter(provider=provider) if id: qs = qs.filter(id=id) return qs @classmethod def create_social_auth(cls, user, uid, provider): if not isinstance(uid, str): uid = str(uid) # If the create fails below due to an IntegrityError, ensure that the transaction # stays undamaged by wrapping the create in an atomic. using = router.db_for_write(cls) with transaction.atomic(using=using): return cls.objects.create(user=user, uid=uid, provider=provider) class DjangoNonceMixin(NonceMixin): @classmethod def use(cls, server_url, timestamp, salt): return cls.objects.get_or_create(server_url=server_url, timestamp=timestamp, salt=salt)[1] @classmethod def get(cls, server_url, salt): return cls.objects.get( server_url=server_url, salt=salt, ) @classmethod def delete(cls, nonce): nonce.delete() class DjangoAssociationMixin(AssociationMixin): @classmethod def store(cls, server_url, association): # Don't use get_or_create because issued cannot be null try: assoc = cls.objects.get(server_url=server_url, handle=association.handle) except cls.DoesNotExist: assoc = cls(server_url=server_url, handle=association.handle) try: assoc.secret = base64.encodebytes(association.secret).decode() except AttributeError: assoc.secret = base64.encodestring(association.secret).decode() assoc.issued = association.issued assoc.lifetime = association.lifetime assoc.assoc_type = association.assoc_type assoc.save() @classmethod def get(cls, *args, **kwargs): return cls.objects.filter(*args, **kwargs) @classmethod def remove(cls, ids_to_delete): cls.objects.filter(pk__in=ids_to_delete).delete() class DjangoCodeMixin(CodeMixin): @classmethod def get_code(cls, code): try: return cls.objects.get(code=code) except cls.DoesNotExist: return None class DjangoPartialMixin(PartialMixin): @classmethod def load(cls, token): try: return cls.objects.get(token=token) except cls.DoesNotExist: return None @classmethod def destroy(cls, token): partial = cls.load(token) if partial: partial.delete() class BaseDjangoStorage(BaseStorage): user = DjangoUserMixin nonce = DjangoNonceMixin association = DjangoAssociationMixin code = DjangoCodeMixin social-auth-app-django-5.9.0/social_django/strategy.py000066400000000000000000000150241517441523300227730ustar00rootroot00000000000000from __future__ import annotations from importlib import import_module from typing import TYPE_CHECKING, Any from django.conf import settings from django.contrib.auth import authenticate, get_user from django.contrib.contenttypes.models import ContentType from django.db.models import Model from django.http import HttpRequest, HttpResponse from django.shortcuts import redirect, resolve_url from django.template import TemplateDoesNotExist, engines, loader from django.utils.crypto import get_random_string from django.utils.encoding import force_str from django.utils.functional import Promise from django.utils.translation import get_language from social_core.strategy import BaseStrategy, BaseTemplateStrategy if TYPE_CHECKING: from django.contrib.sessions.backends.base import SessionBase def render_template_string(request, html, context=None): """ Take a template in the form of a string and render it for the given context """ template = engines["django"].from_string(html) return template.render(context=context, request=request) class DjangoTemplateStrategy(BaseTemplateStrategy): def render_template(self, tpl, context): template = loader.get_template(tpl) return template.render(context=context, request=self.strategy.request) def render_string(self, html, context): return render_template_string(self.strategy.request, html, context) def create_session(session_key: str | None = None) -> SessionBase: engine = import_module(settings.SESSION_ENGINE) return engine.SessionStore(session_key) class DjangoStrategy(BaseStrategy): DEFAULT_TEMPLATE_STRATEGY = DjangoTemplateStrategy _session: SessionBase def __init__(self, storage, request: None | HttpRequest = None, tpl=None): self.request: HttpRequest = request if request: self.session = request.session super().__init__(storage, tpl) @property def session(self) -> SessionBase: try: return self._session except AttributeError: self._session = create_session() return self._session @session.setter def session(self, value: SessionBase) -> None: self._session = value def get_setting(self, name): value = getattr(settings, name) # Force text on URL named settings that are instance of Promise if name.endswith("_URL"): if isinstance(value, Promise): value = force_str(value) value = resolve_url(value) return value def request_data(self, merge=True): if not self.request: return {} if merge: data = self.request.GET.copy() data.update(self.request.POST) elif self.request.method == "POST": data = self.request.POST else: data = self.request.GET return data def request_host(self): if self.request: return self.request.get_host() return None def request_is_secure(self): """Is the request using HTTPS?""" return self.request.is_secure() def request_path(self): """Path of the current request""" return self.request.path def request_port(self): """Port in use for this request""" return self.request.get_port() def request_get(self): """Request GET data""" return self.request.GET.copy() def request_post(self): """Request POST data""" return self.request.POST.copy() def redirect(self, url): return redirect(url) def html(self, content): return HttpResponse(content, content_type="text/html;charset=UTF-8") def render_html(self, tpl=None, html=None, context=None): if not tpl and not html: msg = "Missing template or html parameters" raise ValueError(msg) context = context or {} try: template = loader.get_template(tpl) return template.render(context=context, request=self.request) except (TypeError, TemplateDoesNotExist): return render_template_string(self.request, html, context) def authenticate(self, backend, *args, **kwargs): kwargs["strategy"] = self kwargs["storage"] = self.storage kwargs["backend"] = backend return authenticate(*args, **kwargs) def clean_authenticate_args(self, request, *args, **kwargs): # pipelines don't want a positional request argument kwargs["request"] = request return args, kwargs def session_get(self, name, default=None): return self.session.get(name, default) def session_set(self, name, value): self.session[name] = value if hasattr(self.session, "modified"): self.session.modified = True def session_pop(self, name): return self.session.pop(name, None) def session_setdefault(self, name, value): return self.session.setdefault(name, value) def build_absolute_uri(self, path=None): if self.request: return self.request.build_absolute_uri(path) return path def random_string(self, length=12, chars=BaseStrategy.ALLOWED_CHARS): return get_random_string(length, chars) def to_session_value(self, val): """ Converts values that are instance of Model to a dictionary with enough information to retrieve the instance back later. """ if isinstance(val, Model): val = {"pk": val.pk, "ctype": ContentType.objects.get_for_model(val).pk} return val def from_session_value(self, val): """Converts back the instance saved by self._ctype function.""" if isinstance(val, dict) and "pk" in val and "ctype" in val: ctype = ContentType.objects.get_for_id(val["ctype"]) ModelClass = ctype.model_class() # noqa: N806 val = ModelClass._default_manager.get(pk=val["pk"]) # noqa: SLF001 return val def get_language(self): """Return current language""" return get_language() def get_session_id(self) -> str | None: return self.session.session_key def restore_session(self, session_id: str, kwargs: dict[str, Any]) -> None: # Load session self.request.session = self.session = create_session(session_id) # Update request user self.request.user = get_user(self.request) if "user" in kwargs and self.request.user.is_authenticated: kwargs["user"] = self.request.user # Rotate session key to avoid reuse self.session.cycle_key() social-auth-app-django-5.9.0/social_django/urls.py000066400000000000000000000012661517441523300221210ustar00rootroot00000000000000"""URLs module""" from django.conf import settings from django.urls import path from social_core.utils import setting_name from . import views extra = (getattr(settings, setting_name("TRAILING_SLASH"), True) and "/") or "" app_name = "social" urlpatterns = [ # authentication / association path(f"login/{extra}", views.auth, name="begin"), path(f"complete/{extra}", views.complete, name="complete"), # disconnection path(f"disconnect/{extra}", views.disconnect, name="disconnect"), path( f"disconnect//{extra}", views.disconnect, name="disconnect_individual", ), ] social-auth-app-django-5.9.0/social_django/utils.py000066400000000000000000000040201517441523300222630ustar00rootroot00000000000000from functools import wraps from django.conf import settings from django.http import Http404 from django.urls import reverse from django.views.decorators.http import require_POST from social_core.exceptions import MissingBackend from social_core.utils import get_strategy, module_member, setting_name STRATEGY = getattr(settings, setting_name("STRATEGY"), "social_django.strategy.DjangoStrategy") STORAGE = getattr(settings, setting_name("STORAGE"), "social_django.models.DjangoStorage") REQUIRE_POST = setting_name("REQUIRE_POST") Strategy = module_member(STRATEGY) Storage = module_member(STORAGE) def load_strategy(request=None): return get_strategy(STRATEGY, STORAGE, request) def load_backend(strategy, name, redirect_uri): return strategy.get_backend(name, redirect_uri=redirect_uri) def psa(redirect_uri=None, load_strategy=load_strategy): def decorator(func): @wraps(func) def wrapper(request, backend, *args, **kwargs): uri = redirect_uri if uri and not uri.startswith("/"): uri = reverse(redirect_uri, args=(backend,)) request.social_strategy = load_strategy(request) # backward compatibility in attribute name, only if not already # defined if not hasattr(request, "strategy"): request.strategy = request.social_strategy try: request.backend = load_backend(request.social_strategy, backend, redirect_uri=uri) except MissingBackend as error: msg = "Backend not found" raise Http404(msg) from error return func(request, backend, *args, **kwargs) return wrapper return decorator def maybe_require_post(func): @wraps(func) def wrapper(request, backend, *args, **kwargs): require_post = getattr(settings, REQUIRE_POST, False) if require_post: return require_POST(func)(request, backend, *args, **kwargs) return func(request, backend, *args, **kwargs) return wrapper social-auth-app-django-5.9.0/social_django/views.py000066400000000000000000000126621517441523300222730ustar00rootroot00000000000000from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME, login from django.contrib.auth.decorators import login_not_required, login_required from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt, csrf_protect from django.views.decorators.http import require_POST from social_core.actions import do_auth, do_complete, do_disconnect from social_core.utils import setting_name from .utils import maybe_require_post, psa NAMESPACE = getattr(settings, setting_name("URL_NAMESPACE"), None) or "social" # Calling `session.set_expiry(None)` results in a session lifetime equal to # platform default session lifetime. DEFAULT_SESSION_TIMEOUT = None @never_cache @login_not_required @maybe_require_post @psa(f"{NAMESPACE}:complete") def auth(request, backend): return do_auth(request.backend, redirect_name=REDIRECT_FIELD_NAME) @never_cache @login_not_required @csrf_exempt @psa(f"{NAMESPACE}:complete") def complete(request, backend, *args, **kwargs): """Authentication complete view""" return do_complete( request.backend, _do_login, user=request.user, redirect_name=REDIRECT_FIELD_NAME, request=request, *args, **kwargs, ) @never_cache @login_required @psa() @require_POST @csrf_protect def disconnect(request, backend, association_id=None): """Disconnects given backend from current logged in user.""" return do_disconnect(request.backend, request.user, association_id, redirect_name=REDIRECT_FIELD_NAME) def get_session_timeout(social_user, enable_session_expiration=False, max_session_length=None): if enable_session_expiration: # Retrieve an expiration date from the social user who just finished # logging in; this value was set by the social auth backend, and was # typically received from the server. expiration = social_user.expiration_datetime() # We've enabled session expiration. Check to see if we got # a specific expiration time from the provider for this user; # if not, use the platform default expiration. received_expiration_time = expiration.total_seconds() if expiration else DEFAULT_SESSION_TIMEOUT # Check to see if the backend set a value as a maximum length # that a session may be; if they did, then we should use the minimum # of that and the received session expiration time, if any, to # set the session length. if received_expiration_time is None and max_session_length is None: # We neither received an expiration length, nor have a maximum # session length. Use the platform default. session_expiry = DEFAULT_SESSION_TIMEOUT elif received_expiration_time is None and max_session_length is not None: # We only have a maximum session length; use that. session_expiry = max_session_length elif received_expiration_time is not None and max_session_length is None: # We only have an expiration time received by the backend # from the provider, with no set maximum. Use that. session_expiry = received_expiration_time else: # We received an expiration time from the backend, and we also # have a set maximum session length. Use the smaller of the two. session_expiry = min(received_expiration_time, max_session_length) # If there's an explicitly-set maximum session length, use that # even if we don't want to retrieve session expiry times from # the backend. If there isn't, then use the platform default. elif max_session_length is None: session_expiry = DEFAULT_SESSION_TIMEOUT else: session_expiry = max_session_length return session_expiry def _do_login(backend, user, social_user): user.backend = f"{backend.__module__}.{backend.__class__.__name__}" # Get these details early to avoid any issues involved in the # session switch that happens when we call login(). enable_session_expiration = backend.setting("SESSION_EXPIRATION", False) max_session_length_setting = backend.setting("MAX_SESSION_LENGTH", None) # Log the user in, creating a new session. login(backend.strategy.request, user) # Make sure that the max_session_length value is either an integer or # None. Because we get this as a setting from the backend, it can be set # to whatever the backend creator wants; we want to be resilient against # unexpected types being presented to us. try: max_session_length = int(max_session_length_setting) except (TypeError, ValueError): # We got a response that doesn't look like a number; use the default. max_session_length = None # Get the session expiration length based on the maximum session length # setting, combined with any session length received from the backend. session_expiry = get_session_timeout( social_user, enable_session_expiration=enable_session_expiration, max_session_length=max_session_length, ) try: # Set the session length to our previously determined expiry length. backend.strategy.request.session.set_expiry(session_expiry) except OverflowError: # The timestamp we used wasn't in the range of values supported by # Django for session length; use the platform default. We tried. backend.strategy.request.session.set_expiry(DEFAULT_SESSION_TIMEOUT) social-auth-app-django-5.9.0/tests/000077500000000000000000000000001517441523300171235ustar00rootroot00000000000000social-auth-app-django-5.9.0/tests/__init__.py000066400000000000000000000000001517441523300212220ustar00rootroot00000000000000social-auth-app-django-5.9.0/tests/settings.py000066400000000000000000000030341517441523300213350ustar00rootroot00000000000000import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DEBUG = True USE_TZ = True DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:", }, } ROOT_URLCONF = "tests.urls" INSTALLED_APPS = [ "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.sites", "django.contrib.messages", "django.contrib.admin", "social_django", ] SITE_ID = 1 MIDDLEWARE = ( "django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "social_django.middleware.SocialAuthExceptionMiddleware", ) AUTHENTICATION_BACKENDS = ( "social_core.backends.facebook.FacebookOAuth2", "django.contrib.auth.backends.ModelBackend", ) TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, "DIRS": [os.path.join(BASE_DIR, "templates")], "OPTIONS": { "context_processors": [ "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", "django.template.context_processors.request", "social_django.context_processors.backends", "social_django.context_processors.login_redirect", ], }, }, ] SECRET_KEY = "6p%gef2(6kvjsgl*7!51a7z8c3=u4uc&6ulpua0g1^&sthiifp" # noqa: S105 STATIC_URL = "/static/" social-auth-app-django-5.9.0/tests/templates/000077500000000000000000000000001517441523300211215ustar00rootroot00000000000000social-auth-app-django-5.9.0/tests/templates/test.html000066400000000000000000000000051517441523300227610ustar00rootroot00000000000000test social-auth-app-django-5.9.0/tests/test_admin.py000066400000000000000000000023141517441523300216240ustar00rootroot00000000000000from django.contrib.auth import get_user_model from django.test import TestCase from django.urls import reverse from social_django.models import UserSocialAuth class SocialAdminTest(TestCase): @classmethod def setUpTestData(cls): User = get_user_model() # noqa: N806 User._default_manager.create_superuser( # noqa: SLF001 username="admin", email="admin@test.com", first_name="Admin", password="super-duper-test", # noqa: S106 ) def test_admin_app_name(self): """The App name in the admin index page""" self.client.login( username="admin", password="super-duper-test", # noqa: S106 ) response = self.client.get(reverse("admin:index")) self.assertContains(response, "Python Social Auth") def test_social_auth_changelist(self): """The App name in the admin index page""" self.client.login( username="admin", password="super-duper-test", # noqa: S106 ) meta = UserSocialAuth._meta # noqa: SLF001 url_name = f"admin:{meta.app_label}_{meta.model_name}_changelist" self.client.get(reverse(url_name)) social-auth-app-django-5.9.0/tests/test_context_processors.py000066400000000000000000000021541517441523300245040ustar00rootroot00000000000000from django.test import RequestFactory, TestCase, override_settings from social_django.context_processors import login_redirect @override_settings(REDIRECT_FIELD_NAME="next") class TestContextProcessors(TestCase): def setUp(self): self.request_factory = RequestFactory() def test_login_redirect_unicode_quote(self): request = self.request_factory.get("/", data={"next": "profile/sjó"}) result = login_redirect(request) self.assertEqual( result, { "REDIRECT_FIELD_NAME": "next", "REDIRECT_FIELD_VALUE": "profile/sj%C3%B3", "REDIRECT_QUERYSTRING": "next=profile/sj%C3%B3", }, ) def test_login_redirect_malformed_post(self): request = self.request_factory.post("/", data="no boundary", content_type="multipart/form-data") result = login_redirect(request) self.assertEqual( result, { "REDIRECT_FIELD_NAME": "next", "REDIRECT_FIELD_VALUE": None, "REDIRECT_QUERYSTRING": "", }, ) social-auth-app-django-5.9.0/tests/test_middleware.py000066400000000000000000000073741517441523300226640ustar00rootroot00000000000000import logging from unittest import mock from asgiref.sync import iscoroutinefunction from django.contrib.messages import MessageFailure from django.http import HttpResponse, HttpResponseRedirect from django.test import AsyncRequestFactory, RequestFactory, TestCase, override_settings from django.urls import reverse from social_core.exceptions import AuthCanceled from social_django.middleware import SocialAuthExceptionMiddleware class MockAuthCanceled(AuthCanceled): def __init__(self, *args, **kwargs): if not args: kwargs.setdefault("backend", None) super().__init__(*args, **kwargs) @mock.patch("social_core.backends.base.BaseAuth.request", side_effect=MockAuthCanceled) class TestMiddleware(TestCase): def setUp(self): session = self.client.session session["facebook_state"] = "1" session.save() self.complete_url = reverse("social:complete", kwargs={"backend": "facebook"}) self.complete_url += "?code=2&state=1" def test_sync_middleware(self, mocked): expected = HttpResponse() get_response = mock.Mock(return_value=expected) rf = RequestFactory() request = rf.get("/") middleware = SocialAuthExceptionMiddleware(get_response) resp = middleware(request) self.assertFalse(iscoroutinefunction(middleware)) self.assertIs(resp, expected) get_response.assert_called_once_with(request) async def test_async_middleware(self, mocked): expected = HttpResponse() get_response = mock.AsyncMock(return_value=expected) async_rf = AsyncRequestFactory() request = async_rf.get("/") middleware = SocialAuthExceptionMiddleware(get_response) resp = await middleware(request) self.assertTrue(iscoroutinefunction(middleware)) self.assertIs(resp, expected) get_response.assert_awaited_once_with(request) def test_exception(self, mocked): with self.assertRaises(MockAuthCanceled): self.client.get(self.complete_url) @override_settings(DEBUG=True) def test_exception_debug(self, mocked): logging.disable(logging.CRITICAL) with self.assertRaises(MockAuthCanceled): self.client.get(self.complete_url) logging.disable(logging.NOTSET) @override_settings(SOCIAL_AUTH_LOGIN_ERROR_URL="/") def test_login_error_url(self, mocked): response = self.client.get(self.complete_url) self.assertTrue(isinstance(response, HttpResponseRedirect)) self.assertEqual(response.url, "/") @override_settings(SOCIAL_AUTH_LOGIN_ERROR_URL="/") @mock.patch("django.contrib.messages.error", side_effect=MessageFailure) def test_message_failure(self, mocked_request, mocked_error): response = self.client.get(self.complete_url) self.assertTrue(isinstance(response, HttpResponseRedirect)) self.assertEqual( response.url, "/?message=Authentication%20process%20canceled&backend=facebook", ) @override_settings( SOCIAL_AUTH_LOGIN_ERROR_URL="/default-error", SOCIAL_AUTH_FACEBOOK_LOGIN_ERROR_URL="/facebook-error", ) def test_backend_specific_login_error_url(self, mocked): response = self.client.get(self.complete_url) self.assertTrue(isinstance(response, HttpResponseRedirect)) self.assertEqual(response.url, "/facebook-error") @override_settings( DEBUG=False, SOCIAL_AUTH_RAISE_EXCEPTIONS=False, SOCIAL_AUTH_FACEBOOK_RAISE_EXCEPTIONS=True, ) def test_backend_specific_raise_exceptions(self, mocked): logging.disable(logging.CRITICAL) with self.assertRaises(MockAuthCanceled): self.client.get(self.complete_url) logging.disable(logging.NOTSET) social-auth-app-django-5.9.0/tests/test_migrations.py000066400000000000000000000010511517441523300227050ustar00rootroot00000000000000from io import StringIO from django.core.management import call_command from django.test import TestCase class PendingMigrationsTests(TestCase): def test_no_pending_migrations(self): out = StringIO() try: call_command( "makemigrations", "--dry-run", "--check", stdout=out, stderr=StringIO(), ) except SystemExit: # pragma: no cover raise AssertionError("Pending migrations:\n" + out.getvalue()) from None social-auth-app-django-5.9.0/tests/test_models.py000066400000000000000000000213711517441523300220230ustar00rootroot00000000000000from datetime import timedelta from unittest import mock from django.contrib.auth import get_user_model from django.core.management import call_command from django.db import IntegrityError from django.test import TestCase, override_settings from social_core.exceptions import AuthAlreadyAssociated from social_django.models import ( AbstractUserSocialAuth, Association, Code, DjangoStorage, Nonce, Partial, UserSocialAuth, ) class TestSocialAuthUser(TestCase): def test_user_relationship_none(self): """Accessing User.social_user outside of the pipeline doesn't work""" User = get_user_model() # noqa: N806 user = User._default_manager.create_user(username="randomtester") # noqa: SLF001 with self.assertRaises(AttributeError): user.social_user # noqa: B018 def test_user_existing_relationship(self): """Accessing User.social_user outside of the pipeline doesn't work""" User = get_user_model() # noqa: N806 user = User._default_manager.create_user(username="randomtester") # noqa: SLF001 UserSocialAuth.objects.create(user=user, provider="my-provider", uid="1234") with self.assertRaises(AttributeError): user.social_user # noqa: B018 def test_get_social_auth(self): User = get_user_model() # noqa: N806 user = User._default_manager.create_user(username="randomtester") # noqa: SLF001 user_social = UserSocialAuth.objects.create(user=user, provider="my-provider", uid="1234") other = UserSocialAuth.get_social_auth("my-provider", "1234") self.assertEqual(other, user_social) def test_get_social_auth_none(self): other = UserSocialAuth.get_social_auth("my-provider", "1234") self.assertIsNone(other) def test_cleanup(self): Code.objects.create(email="first@example.com") Code.objects.create(email="second@example.com") code = Code.objects.create(email="expire@example.com") code.timestamp -= timedelta(days=30) code.save() Partial.objects.create() partial = Partial.objects.create() partial.timestamp -= timedelta(days=30) partial.save() call_command("clearsocial") self.assertEqual(2, Code.objects.count()) self.assertEqual(1, Partial.objects.count()) class TestUserSocialAuth(TestCase): def setUp(self): self.user_model = get_user_model() self.user = self.user_model._default_manager.create_user(username="randomtester", email="user@example.com") # noqa: SLF001 self.usa = UserSocialAuth.objects.create(user=self.user, provider="my-provider", uid="1234") def test_changed(self): self.user.email = eml = "test@example.com" UserSocialAuth.changed(user=self.user) db_eml = self.user_model._default_manager.get(username=self.user.username).email # noqa: SLF001 self.assertEqual(db_eml, eml) def test_set_extra_data(self): self.usa.set_extra_data({"a": "b"}) self.usa.refresh_from_db() db_data = UserSocialAuth.objects.get(id=self.usa.id).extra_data self.assertEqual(db_data, {"a": "b"}) def test_disconnect(self): m = mock.Mock() UserSocialAuth.disconnect(m) self.assertListEqual(m.method_calls, [mock.call.delete()]) def test_username_field(self): self.assertEqual(UserSocialAuth.username_field(), "username") with mock.patch( "social_django.models.UserSocialAuth.user_model", return_value=mock.Mock(USERNAME_FIELD="test"), ): self.assertEqual(UserSocialAuth.username_field(), "test") def test_user_exists(self): self.assertTrue(UserSocialAuth.user_exists(username=self.user.username)) self.assertFalse(UserSocialAuth.user_exists(username="test")) def test_get_username(self): self.assertEqual(UserSocialAuth.get_username(self.user), self.user.username) def test_create_user(self): UserSocialAuth.create_user(username="testuser") def test_create_user_reraise(self): with self.assertRaises(AuthAlreadyAssociated): UserSocialAuth.create_user(username=self.user.username, email=None) @mock.patch("social_django.models.UserSocialAuth.username_field", return_value="email") @mock.patch("django.contrib.auth.models.UserManager.create_user", return_value="") def test_create_user_custom_username(self, *args): UserSocialAuth.create_user(username=self.user.email) @mock.patch("django.contrib.auth.models.UserManager.create_user", side_effect=IntegrityError) def test_create_user_existing(self, *args): with self.assertRaises(AuthAlreadyAssociated): UserSocialAuth.create_user(username=self.user.email) def test_get_user(self): self.assertEqual(UserSocialAuth.get_user(pk=self.user.pk), self.user) self.assertIsNone(UserSocialAuth.get_user(pk=123)) def test_get_users_by_email(self): qs = UserSocialAuth.get_users_by_email(email=self.user.email) self.assertEqual(qs.count(), 1) self.user.is_active = False self.user.save() qs = UserSocialAuth.get_users_by_email(email=self.user.email) self.assertEqual(qs.count(), 0) with override_settings(SOCIAL_AUTH_ACTIVE_USERS_FILTER={}): qs = UserSocialAuth.get_users_by_email(email=self.user.email) self.assertEqual(qs.count(), 1) def test_get_social_auth(self): usa = self.usa # Model self.assertEqual(UserSocialAuth.get_social_auth(provider=usa.provider, uid=usa.uid), usa) self.assertIsNone(UserSocialAuth.get_social_auth(provider="a", uid="1")) # Mixin self.assertEqual( super(AbstractUserSocialAuth, usa).get_social_auth(provider=usa.provider, uid=usa.uid), usa, ) self.assertIsNone(super(AbstractUserSocialAuth, usa).get_social_auth(provider="a", uid="1")) # Manager self.assertEqual( UserSocialAuth.objects.get_social_auth(provider=usa.provider, uid=usa.uid), usa, ) self.assertIsNone(UserSocialAuth.objects.get_social_auth(provider="a", uid="1")) def test_get_social_auth_int_uid(self): usa = self.usa int_uid = int(usa.uid) # Model self.assertEqual(UserSocialAuth.get_social_auth(provider=usa.provider, uid=int_uid), usa) # Mixin self.assertEqual( super(AbstractUserSocialAuth, usa).get_social_auth(provider=usa.provider, uid=usa.uid), usa, ) # Manager self.assertEqual( UserSocialAuth.get_social_auth(provider=usa.provider, uid=int_uid), usa, ) def test_get_social_auth_for_user(self): qs = UserSocialAuth.get_social_auth_for_user(user=self.user, provider=self.usa.provider, id=self.usa.id) self.assertEqual(qs.count(), 1) def test_create_social_auth(self): usa = UserSocialAuth.create_social_auth(user=self.user, provider="test", uid=1) self.assertEqual(usa.uid, "1") self.assertEqual(str(usa), str(self.user)) def test_username_max_length(self): self.assertEqual(UserSocialAuth.username_max_length(), 150) class TestNonce(TestCase): def test_use(self): self.assertEqual(Nonce.objects.count(), 0) self.assertTrue(Nonce.use(server_url="/", timestamp=1, salt="1")) self.assertFalse(Nonce.use(server_url="/", timestamp=1, salt="1")) self.assertEqual(Nonce.objects.count(), 1) class TestAssociation(TestCase): def test_store_get_remove(self): Association.store( server_url="/", association=mock.Mock(handle="a", secret=b"b", issued=1, lifetime=2, assoc_type="c"), ) qs = Association.get(handle="a") self.assertEqual(qs.count(), 1) self.assertEqual(qs[0].secret, "Yg==\n") Association.remove(ids_to_delete=[qs.first().id]) self.assertEqual(Association.objects.count(), 0) class TestCode(TestCase): def test_get_code(self): code1 = Code.objects.create(email="test@example.com", code="abc") code2 = Code.get_code(code="abc") self.assertEqual(code1, code2) self.assertIsNone(Code.get_code(code="xyz")) class TestPartial(TestCase): def test_load_destroy(self): token_value = "x" # noqa: S105 p = Partial.objects.create(token=token_value, backend="y", data={}) self.assertEqual(Partial.load(token=token_value), p) self.assertIsNone(Partial.load(token="y")) # noqa: S106 Partial.destroy(token=token_value) self.assertEqual(Partial.objects.count(), 0) class TestDjangoStorage(TestCase): def test_is_integrity_error(self): self.assertTrue(DjangoStorage.is_integrity_error(IntegrityError())) social-auth-app-django-5.9.0/tests/test_storage_integration.py000066400000000000000000000254721517441523300246150ustar00rootroot00000000000000"""Integration tests for storage layer to catch breaking changes in social-core API These tests are designed to catch breaking changes in the social-core storage API that could go unnoticed without integration testing. The issue that prompted these tests was: https://github.com/python-social-auth/social-core/pull/986 introduced a breaking change in the OpenID storage API (specifically in get_association method) that went unnoticed into release. The error manifested as: NotImplementedError: Implement in subclass File "social_core/storage.py", line 256, in get_association raise NotImplementedError("Implement in subclass") The breaking change happened when: 1. social-core's OpenIdStore.getAssociation() calls self.assoc.oids() 2. AssociationMixin.oids() calls cls.get(**kwargs) 3. If get() is not properly implemented in the Django storage layer, it raises NotImplementedError These integration tests ensure that: - DjangoAssociationMixin.get() works correctly and returns a QuerySet - DjangoAssociationMixin.oids() properly calls get() and converts results to OpenIdAssociation objects - OpenIdStore.getAssociation() can successfully retrieve associations through the full call stack - All OpenID association and nonce storage operations work end-to-end """ import time from unittest import mock from django.test import TestCase from social_core.store import OpenIdStore from social_core.strategy import BaseStrategy from social_django.models import Association, DjangoStorage, Nonce class TestStorageIntegration(TestCase): """Test integration between DjangoStorage and social-core's OpenIdStore""" def setUp(self): # Create a mock strategy with DjangoStorage self.strategy = mock.Mock(spec=BaseStrategy) self.strategy.storage = DjangoStorage self.store = OpenIdStore(self.strategy) def test_openid_store_association_workflow(self): """Test the full OpenID association workflow through OpenIdStore""" # Create a mock OpenID association (using string handle as in real openid library) mock_association = mock.Mock() mock_association.handle = "test_handle" mock_association.secret = b"test_secret" mock_association.issued = int(time.time()) mock_association.lifetime = 3600 mock_association.assoc_type = "HMAC-SHA1" server_url = "https://example.com/openid" # Test storeAssociation self.store.storeAssociation(server_url, mock_association) # Verify association was stored self.assertEqual(Association.objects.count(), 1) stored = Association.objects.first() self.assertEqual(stored.server_url, server_url) self.assertEqual(stored.handle, "test_handle") # Test getAssociation - this is the critical method that was breaking retrieved = self.store.getAssociation(server_url) self.assertIsNotNone(retrieved) self.assertEqual(retrieved.handle, "test_handle") # Test getAssociation with handle retrieved_with_handle = self.store.getAssociation(server_url, "test_handle") self.assertIsNotNone(retrieved_with_handle) self.assertEqual(retrieved_with_handle.handle, "test_handle") # Test removeAssociation self.store.removeAssociation(server_url, "test_handle") self.assertEqual(Association.objects.count(), 0) # Test getAssociation returns None after removal self.assertIsNone(self.store.getAssociation(server_url)) def test_openid_store_association_expiration(self): """Test that expired associations are handled correctly""" # Create an expired association mock_association = mock.Mock() mock_association.handle = "expired_handle" mock_association.secret = b"test_secret" mock_association.issued = int(time.time()) - 7200 # 2 hours ago mock_association.lifetime = 3600 # 1 hour lifetime, so expired mock_association.assoc_type = "HMAC-SHA1" server_url = "https://example.com/openid" self.store.storeAssociation(server_url, mock_association) self.assertEqual(Association.objects.count(), 1) # getAssociation should return None for expired associations and clean them up retrieved = self.store.getAssociation(server_url) self.assertIsNone(retrieved) self.assertEqual(Association.objects.count(), 0) def test_openid_store_multiple_associations(self): """Test handling multiple associations for the same server""" server_url = "https://example.com/openid" current_time = int(time.time()) # Store multiple associations with different handles for i in range(3): mock_association = mock.Mock() mock_association.handle = f"handle_{i}" mock_association.secret = b"test_secret" mock_association.issued = current_time + i mock_association.lifetime = 3600 mock_association.assoc_type = "HMAC-SHA1" self.store.storeAssociation(server_url, mock_association) self.assertEqual(Association.objects.count(), 3) # getAssociation should return the most recent one retrieved = self.store.getAssociation(server_url) self.assertIsNotNone(retrieved) self.assertEqual(retrieved.handle, "handle_2") # Get specific association by handle retrieved_specific = self.store.getAssociation(server_url, "handle_1") self.assertIsNotNone(retrieved_specific) self.assertEqual(retrieved_specific.handle, "handle_1") def test_openid_store_nonce_workflow(self): """Test the OpenID nonce workflow through OpenIdStore""" server_url = "https://example.com/openid" timestamp = int(time.time()) salt = "test_salt" # First use should succeed self.assertTrue(self.store.useNonce(server_url, timestamp, salt)) self.assertEqual(Nonce.objects.count(), 1) # Second use with same parameters should fail (nonce already used) self.assertFalse(self.store.useNonce(server_url, timestamp, salt)) self.assertEqual(Nonce.objects.count(), 1) # Different salt should succeed self.assertTrue(self.store.useNonce(server_url, timestamp, "different_salt")) self.assertEqual(Nonce.objects.count(), 2) def test_openid_store_nonce_timestamp_skew(self): """Test that nonces with excessive timestamp skew are rejected""" server_url = "https://example.com/openid" current_time = int(time.time()) old_timestamp = current_time - 7 * 60 * 60 # 7 hours ago (exceeds 6 hour skew) salt = "test_salt" # Old timestamp should be rejected self.assertFalse(self.store.useNonce(server_url, old_timestamp, salt)) self.assertEqual(Nonce.objects.count(), 0) class TestAssociationMixinIntegration(TestCase): """Test DjangoAssociationMixin methods used by social-core""" def test_oids_method(self): """Test the oids method that is called by OpenIdStore.getAssociation""" # Create test associations mock_assoc1 = mock.Mock(handle="handle1", secret=b"secret1", issued=1000, lifetime=3600, assoc_type="HMAC-SHA1") mock_assoc2 = mock.Mock(handle="handle2", secret=b"secret2", issued=2000, lifetime=3600, assoc_type="HMAC-SHA1") server_url = "https://example.com/openid" Association.store(server_url, mock_assoc1) Association.store(server_url, mock_assoc2) # Test oids() method - returns sorted list of (id, association) tuples oids_result = list(Association.oids(server_url)) self.assertEqual(len(oids_result), 2) # Should be sorted by issued timestamp (most recent first) self.assertEqual(oids_result[0][1].handle, "handle2") self.assertEqual(oids_result[1][1].handle, "handle1") def test_oids_method_with_handle(self): """Test oids method with specific handle filter""" mock_assoc1 = mock.Mock(handle="handle1", secret=b"secret1", issued=1000, lifetime=3600, assoc_type="HMAC-SHA1") mock_assoc2 = mock.Mock(handle="handle2", secret=b"secret2", issued=2000, lifetime=3600, assoc_type="HMAC-SHA1") server_url = "https://example.com/openid" Association.store(server_url, mock_assoc1) Association.store(server_url, mock_assoc2) # Test oids() with handle filter oids_result = list(Association.oids(server_url, "handle1")) self.assertEqual(len(oids_result), 1) self.assertEqual(oids_result[0][1].handle, "handle1") def test_get_method(self): """Test the get method that is called by oids""" mock_assoc = mock.Mock( handle="test_handle", secret=b"secret", issued=1000, lifetime=3600, assoc_type="HMAC-SHA1" ) server_url = "https://example.com/openid" Association.store(server_url, mock_assoc) # Test get() method returns QuerySet result = Association.get(server_url=server_url) self.assertEqual(result.count(), 1) self.assertEqual(result.first().handle, "test_handle") # Test get() with handle filter result_with_handle = Association.get(server_url=server_url, handle="test_handle") self.assertEqual(result_with_handle.count(), 1) # Test get() with non-existent handle result_none = Association.get(server_url=server_url, handle="nonexistent") self.assertEqual(result_none.count(), 0) class TestNonceMixinIntegration(TestCase): """Test DjangoNonceMixin methods used by social-core""" def test_use_method(self): """Test the use method that is called by OpenIdStore.useNonce""" server_url = "https://example.com/openid" timestamp = 1234567890 salt = "test_salt" # First use should return True (created) self.assertTrue(Nonce.use(server_url, timestamp, salt)) self.assertEqual(Nonce.objects.count(), 1) # Second use should return False (already exists) self.assertFalse(Nonce.use(server_url, timestamp, salt)) self.assertEqual(Nonce.objects.count(), 1) def test_get_method(self): """Test the get method for retrieving nonces""" server_url = "https://example.com/openid" timestamp = 1234567890 salt = "test_salt" Nonce.use(server_url, timestamp, salt) # Test get() method nonce = Nonce.get(server_url, salt) self.assertIsNotNone(nonce) self.assertEqual(nonce.server_url, server_url) self.assertEqual(nonce.salt, salt) def test_delete_method(self): """Test the delete method for nonces""" server_url = "https://example.com/openid" timestamp = 1234567890 salt = "test_salt" Nonce.use(server_url, timestamp, salt) nonce = Nonce.get(server_url, salt) # Test delete() method Nonce.delete(nonce) self.assertEqual(Nonce.objects.count(), 0) social-auth-app-django-5.9.0/tests/test_strategy.py000066400000000000000000000110711517441523300223760ustar00rootroot00000000000000from unittest import mock from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.contrib.sessions.middleware import SessionMiddleware from django.http import HttpResponse, QueryDict from django.test import RequestFactory, TestCase from django.utils.translation import gettext_lazy from social_django.utils import load_backend, load_strategy class TestStrategy(TestCase): def setUp(self): self.request_factory = RequestFactory() self.request = self.request_factory.get("/", data={"x": "1"}) SessionMiddleware(lambda: None).process_request(self.request) self.strategy = load_strategy(request=self.request) def test_request_methods(self): self.assertEqual(self.strategy.request_port(), "80") self.assertEqual(self.strategy.request_path(), "/") self.assertEqual(self.strategy.request_host(), "testserver") self.assertEqual(self.strategy.request_is_secure(), False) self.assertEqual(self.strategy.request_data(), QueryDict("x=1")) self.assertEqual(self.strategy.request_get(), QueryDict("x=1")) self.assertEqual(self.strategy.request_post(), {}) self.request.method = "POST" self.assertEqual(self.strategy.request_data(merge=False), {}) def test_build_absolute_uri(self): self.assertEqual(self.strategy.build_absolute_uri("/"), "http://testserver/") def test_settings(self): with self.settings(LOGIN_ERROR_URL="/"): self.assertEqual(self.strategy.get_setting("LOGIN_ERROR_URL"), "/") with self.settings(LOGIN_ERROR_URL=gettext_lazy("/")): self.assertEqual(self.strategy.get_setting("LOGIN_ERROR_URL"), "/") def test_session_methods(self): self.strategy.session_set("k", "v") self.assertEqual(self.strategy.session_get("k"), "v") self.assertEqual(self.strategy.session_setdefault("k", "x"), "v") self.assertEqual(self.strategy.session_pop("k"), "v") def test_random_string(self): rs1 = self.strategy.random_string() self.assertEqual(len(rs1), 12) self.assertNotEqual(rs1, self.strategy.random_string()) def test_session_value(self): user_model = get_user_model() user = user_model._default_manager.create_user(username="test") # noqa: SLF001 ctype = ContentType.objects.get_for_model(user_model) val = self.strategy.to_session_value(val=user) self.assertEqual(val, {"pk": user.pk, "ctype": ctype.pk}) instance = self.strategy.from_session_value(val=val) self.assertEqual(instance, user) def test_get_language(self): self.assertEqual(self.strategy.get_language(), "en-us") def test_html(self): result = self.strategy.render_html(tpl="test.html") self.assertEqual(result, "test\n") result = self.strategy.render_html(html="xoxo") self.assertEqual(result, "xoxo") with self.assertRaisesMessage(ValueError, "Missing template or html parameters"): self.strategy.render_html() result = self.strategy.html(content="xoxo") self.assertIsInstance(result, HttpResponse) self.assertEqual(result.content, b"xoxo") ctx = {"x": 1} result = self.strategy.tpl.render_template(tpl="test.html", context=ctx) self.assertEqual(result, "test\n") result = self.strategy.tpl.render_string(html="xoxo", context=ctx) self.assertEqual(result, "xoxo") def test_authenticate(self): backend = load_backend(strategy=self.strategy, name="facebook", redirect_uri="/") user = mock.Mock() with mock.patch("social_core.backends.base.BaseAuth.pipeline", return_value=user): result = self.strategy.authenticate(backend=backend, response=mock.Mock()) self.assertEqual(result, user) self.assertEqual(result.backend, "social_core.backends.facebook.FacebookOAuth2") def test_clean_authenticate_args(self): args, kwargs = self.strategy.clean_authenticate_args(self.request) self.assertEqual(args, ()) self.assertEqual(kwargs, {"request": self.request}) def test_clean_authenticate_args_none(self): # When called from continue_pipeline(), request is None. Issue #222 args, kwargs = self.strategy.clean_authenticate_args(None) self.assertEqual(args, ()) self.assertEqual(kwargs, {"request": None}) def test_session_creation_without_request(self): strategy = load_strategy() self.assertIsNone(strategy.request) self.assertIsNotNone(strategy.session) social-auth-app-django-5.9.0/tests/test_views.py000066400000000000000000000123431517441523300216740ustar00rootroot00000000000000from unittest import mock from django.contrib.auth import get_user_model from django.contrib.auth.models import AbstractBaseUser from django.test import TestCase, override_settings from django.urls import reverse from social_django.models import UserSocialAuth from social_django.views import get_session_timeout @override_settings(SOCIAL_AUTH_FACEBOOK_KEY="1", SOCIAL_AUTH_FACEBOOK_SECRET="2") # noqa: S106 class TestViews(TestCase): def setUp(self): session = self.client.session session["facebook_state"] = "1" session.save() def test_begin_view(self): response = self.client.get(reverse("social:begin", kwargs={"backend": "facebook"})) self.assertEqual(response.status_code, 302) url = reverse("social:begin", kwargs={"backend": "blabla"}) response = self.client.get(url) self.assertEqual(response.status_code, 404) def test_require_post_works(self): with override_settings(SOCIAL_AUTH_REQUIRE_POST=True): response = self.client.get(reverse("social:begin", kwargs={"backend": "facebook"})) self.assertEqual(response.status_code, 405) @mock.patch("social_core.backends.base.BaseAuth.request") def test_complete(self, mock_request): url = reverse("social:complete", kwargs={"backend": "facebook"}) url += "?code=2&state=1" mock_request.return_value.json.return_value = {"access_token": "123"} with mock.patch( "django.contrib.sessions.backends.base.SessionBase.set_expiry", side_effect=[OverflowError, None], ): response = self.client.get(url) self.assertEqual(response.status_code, 302) self.assertEqual(response.url, "/accounts/profile/") @mock.patch("social_core.backends.base.BaseAuth.request") def test_disconnect(self, _mock_request): user_model = get_user_model() user = user_model._default_manager.create_user( # noqa: SLF001 username="test", password="pwd", # noqa: S106 ) UserSocialAuth.objects.create(user=user, provider="facebook", uid="some-mock-facebook-uid") self.client.login(username="test", password="pwd") # noqa: S106 url = reverse("social:disconnect", kwargs={"backend": "facebook"}) response = self.client.post(url) self.assertEqual(response.status_code, 302) self.assertEqual(response.url, "http://testserver/accounts/profile/") url = reverse( "social:disconnect_individual", kwargs={"backend": "facebook", "association_id": "123"}, ) hup = AbstractBaseUser.has_usable_password del AbstractBaseUser.has_usable_password response = self.client.post(url) self.assertEqual(response.status_code, 302) self.assertEqual(response.url, "http://testserver/accounts/profile/") AbstractBaseUser.has_usable_password = hup class TestGetSessionTimeout(TestCase): """ Ensure that the branching logic of get_session_timeout behaves as expected. """ def setUp(self): self.social_user = mock.MagicMock() self.social_user.expiration_datetime.return_value = None super().setUp() def set_user_expiration(self, seconds): self.social_user.expiration_datetime.return_value = mock.MagicMock( total_seconds=mock.MagicMock(return_value=seconds), ) def test_expiration_disabled_no_max(self): self.set_user_expiration(60) expiration_length = get_session_timeout(self.social_user, enable_session_expiration=False) self.assertIsNone(expiration_length) def test_expiration_disabled_with_max(self): expiration_length = get_session_timeout( self.social_user, enable_session_expiration=False, max_session_length=60, ) self.assertEqual(expiration_length, 60) def test_expiration_disabled_with_zero_max(self): expiration_length = get_session_timeout(self.social_user, enable_session_expiration=False, max_session_length=0) self.assertEqual(expiration_length, 0) def test_user_has_session_length_no_max(self): self.set_user_expiration(60) expiration_length = get_session_timeout(self.social_user, enable_session_expiration=True) self.assertEqual(expiration_length, 60) def test_user_has_session_length_larger_max(self): self.set_user_expiration(60) expiration_length = get_session_timeout(self.social_user, enable_session_expiration=True, max_session_length=90) self.assertEqual(expiration_length, 60) def test_user_has_session_length_smaller_max(self): self.set_user_expiration(60) expiration_length = get_session_timeout(self.social_user, enable_session_expiration=True, max_session_length=30) self.assertEqual(expiration_length, 30) def test_user_has_no_session_length_with_max(self): expiration_length = get_session_timeout(self.social_user, enable_session_expiration=True, max_session_length=60) self.assertEqual(expiration_length, 60) def test_user_has_no_session_length_no_max(self): expiration_length = get_session_timeout(self.social_user, enable_session_expiration=True) self.assertIsNone(expiration_length) social-auth-app-django-5.9.0/tests/urls.py000066400000000000000000000003001517441523300204530ustar00rootroot00000000000000from django.contrib import admin from django.urls import include, path urlpatterns = [ path("admin/", admin.site.urls), path("", include("social_django.urls", namespace="social")), ] social-auth-app-django-5.9.0/tox.ini000066400000000000000000000010731517441523300172750ustar00rootroot00000000000000[tox] envlist = py{310,311,312,313,314}-django52 py{312,313,314}-django6 py{312,313.314}-djangomain py{310,311,312,313,314}-socialmaster [testenv] passenv = * allowlist_externals = uv install_command = uv pip install --no-binary lxml --no-binary xmlsec {opts} {packages} commands = coverage run manage.py test deps = django52: Django>=5.2,<5.3 django6: Django>=6.0,<6.1 djangomain: https://github.com/django/django/archive/main.tar.gz socialmaster: https://github.com/python-social-auth/social-core/archive/master.tar.gz .[dev]