pax_global_header00006660000000000000000000000064150036254720014516gustar00rootroot0000000000000052 comment=3c778c999d469e910eb0ba4cef581921334bcb4c social-auth-core-4.6.1/000077500000000000000000000000001500362547200146655ustar00rootroot00000000000000social-auth-core-4.6.1/.coveragerc000066400000000000000000000001411500362547200170020ustar00rootroot00000000000000[run] branch = True omit = .venv/* .tox/* concurrency = multiprocessing [paths] source = . social-auth-core-4.6.1/.github/000077500000000000000000000000001500362547200162255ustar00rootroot00000000000000social-auth-core-4.6.1/.github/matchers/000077500000000000000000000000001500362547200200335ustar00rootroot00000000000000social-auth-core-4.6.1/.github/matchers/flake8.json000066400000000000000000000004441500362547200221020ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "flake8", "pattern": [ { "code": 4, "column": 3, "file": 1, "line": 2, "message": 5, "regexp": "^([^:]*):(\\d+):(\\d+): (\\w+\\d\\d\\d) (.*)$" } ] } ] } social-auth-core-4.6.1/.github/renovate.json000066400000000000000000000016141500362547200207450ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended", ":dependencyDashboard", "helpers:pinGitHubActionDigests" ], "automerge": true, "automergeType": "pr", "automergeStrategy": "rebase", "platformAutomerge": true, "pre-commit": { "enabled": true }, "customManagers": [ { "customType": "regex", "fileMatch": [ "\\.pre-commit-config\\.yaml" ], "matchStrings": [ "(?[^'\" ]+)==(?[^'\" ,\\s]+)" ], "datasourceTemplate": "pypi", "versioningTemplate": "pep440" }, { "customType": "regex", "fileMatch": [ "\\.pre-commit-config\\.yaml" ], "matchStrings": [ "(?[^'\" ]+)@(?[^'\" ,\\s]+)" ], "datasourceTemplate": "npm", "versioningTemplate": "npm" } ] } social-auth-core-4.6.1/.github/workflows/000077500000000000000000000000001500362547200202625ustar00rootroot00000000000000social-auth-core-4.6.1/.github/workflows/pre-commit-shared.yml000066400000000000000000000014451500362547200243310ustar00rootroot00000000000000name: pre-commit check on: workflow_call: permissions: contents: read jobs: pre-commit: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: ~/.cache/pre-commit key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml', 'requirements*.txt') }} - name: Setup Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: 3.x - uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 - run: uvx pre-commit run --all env: RUFF_OUTPUT_FORMAT: github social-auth-core-4.6.1/.github/workflows/pre-commit.yml000066400000000000000000000002341500362547200230600ustar00rootroot00000000000000name: pre-commit check on: push: pull_request: permissions: contents: read jobs: pre-commit: uses: ./.github/workflows/pre-commit-shared.yml social-auth-core-4.6.1/.github/workflows/release.yml000066400000000000000000000024441500362547200224310ustar00rootroot00000000000000name: Release on: push: pull_request: permissions: contents: read jobs: release: runs-on: ubuntu-latest permissions: id-token: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 - run: uv build - 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 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/') with: name: dist path: | dist/*.tar.gz dist/*.whl - run: uvx twine check dist/* - run: uvx pydistcheck --inspect dist/* - run: uvx pyroma dist/*.tar.gz - run: uvx check-wheel-contents dist/*.whl - run: uvx check-manifest -v - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/') run: uv publish --trusted-publishing always social-auth-core-4.6.1/.github/workflows/test.yml000066400000000000000000000042541500362547200217710ustar00rootroot00000000000000name: Tests on: [push, pull_request] permissions: contents: read jobs: types: runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: python-version: - '3.9' - '3.13' env: PYTHON_VERSION: ${{ matrix.python-version }} PYTHONUNBUFFERED: 1 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.python-version }} - uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 with: cache-suffix: ${{ matrix.python-version }} - name: Install System dependencies run: | sudo apt-get update sudo apt-get install -qq -y --no-install-recommends libxmlsec1-dev swig - name: Type check with tox run: uvx tox -e "py${PYTHON_VERSION/\./}-pyright" test: runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: python-version: - '3.9' - '3.10' - '3.11' - '3.12' - '3.13' env: PYTHON_VERSION: ${{ matrix.python-version }} PYTHONUNBUFFERED: 1 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.python-version }} - uses: astral-sh/setup-uv@c7f87aa956e4c323abf06d5dec078e358f6b4d04 # v6.0.0 with: cache-suffix: ${{ matrix.python-version }} - name: Install System dependencies run: | sudo apt-get update sudo apt-get install -qq -y --no-install-recommends libxmlsec1-dev swig - name: Test with tox run: uvx tox -e "py${PYTHON_VERSION/\./}" - uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 with: flags: unittests name: Python ${{ matrix.python-version }} social-auth-core-4.6.1/.gitignore000066400000000000000000000007221500362547200166560ustar00rootroot00000000000000*.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 .tox nosetests.xml 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 .pytest_cache/ .env .venv* social-auth-core-4.6.1/.pre-commit-config.yaml000066400000000000000000000031721500362547200211510ustar00rootroot00000000000000# 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: v5.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.11.7 hooks: - id: ruff 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.14.0 hooks: - id: pretty-format-yaml args: [--autofix, --indent, '2'] - repo: https://github.com/pappasam/toml-sort rev: v0.24.2 hooks: - id: toml-sort-fix - repo: https://github.com/abravalheri/validate-pyproject rev: v0.24.1 hooks: - id: validate-pyproject - repo: https://github.com/executablebooks/mdformat rev: 0.7.22 hooks: - id: mdformat additional_dependencies: - mdformat-gfm==0.4.1 - mdformat-ruff==0.1.3 - mdformat-shfmt==0.2.0 - mdformat_tables==1.0.0 - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: - id: codespell additional_dependencies: - tomli - repo: https://github.com/rhysd/actionlint rev: v1.7.7 hooks: - id: actionlint - repo: https://github.com/woodruffw/zizmor-pre-commit rev: v1.6.0 hooks: - id: zizmor social-auth-core-4.6.1/.well-known/000077500000000000000000000000001500362547200170405ustar00rootroot00000000000000social-auth-core-4.6.1/.well-known/funding-manifest-urls000066400000000000000000000001051500362547200232000ustar00rootroot00000000000000https://github.com/python-social-auth/.github/blob/main/funding.json social-auth-core-4.6.1/CHANGELOG.md000066400000000000000000000542051500362547200165040ustar00rootroot00000000000000# 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/). ## [4.6.1](https://github.com/python-social-auth/social-core/releases/tag/4.6.1) - 2025-04-28 ### Changed - Fixed crash in partial pipelines for some backends ## [4.6.0](https://github.com/python-social-auth/social-core/releases/tag/4.6.0) - 2025-04-25 ### Changed - Added type annotations - Modernized build system - OAuth2 backends now default to POST method - Code cleanups - Tests use responses instead of HTTPretty - Improved error handling in case of missing parameters ### Added - Kick OAuth2 backend - OpenIdConnect-based backend for Fedora - Lifescience AAI backend - NFDI (OpenID Connect) backend ### Removed - Removed no longer available backends: khanacademy, professionali.ru, BitBucket OAuth 1.0 ## [4.5.6](https://github.com/python-social-auth/social-core/releases/tag/4.5.6) - 2025-02-13 ### Changed - Reverted unintended change in OpenId storage API. ## [4.5.5](https://github.com/python-social-auth/social-core/releases/tag/4.5.5) - 2025-02-13 ### Added - Allow per-backend user pipeline settings. - Add AzureADOauth2 backend using the v2.0 API. - Google One Tap implementation. ### Changed - Handle case where user has not registered a `family-name` with ORCID - Fix access token expiration and refresh token handling in GitHub backend - Allow overriding emails to always be fully lowercase with `SOCIAL_AUTH_FORCE_EMAIL_LOWERCASE`. - Dropped `tokens` alias for `access_token` on `UserMixin` which has been deprecated for 10 years now. - LinkedIn OAuth API updates. - Modernize release process. - Code cleanups. - Initial type annotations. ## [4.5.4](https://github.com/python-social-auth/social-core/releases/tag/4.5.4) - 2024-04-25 ### Added - LinkedIn supports refresh token ### Changed - SteamOpenId validation of identify URL - Box state redirestion - The `uid` is automatically converted to string in the pipeline - Mediawiki error handling ## [4.5.3](https://github.com/python-social-auth/social-core/releases/tag/4.5.3) - 2024-02-14 ### Added - OpenStreetMap OAuth2 ### Changed - Etsy backend fixes ## [4.5.2](https://github.com/python-social-auth/social-core/releases/tag/4.5.2) - 2024-01-26 ### Added - Etsy backend ### Changed - Updated Facebook API version to 18.0 - Make AppleID work with multiple identifiers ## [4.5.1](https://github.com/python-social-auth/social-core/releases/tag/4.5.1) - 2023-11-29 ### Changed - OpenID Connect skips `at_hash` validation when missing - `redirect_name` is now passed to backend on `do_complete` - `next` is preserved through SAML RelayState - Add Discogs backend - Add BitbucketDataCenterOAuth2 backend - Keycloak's `ID_KEY` is no longer configurable (it never worked) ## [4.5.0](https://github.com/python-social-auth/social-core/releases/tag/4.5.0) - 2023-10-31 ### Changed - Add backend for LinkedIn OpenID Connect - Add backend for EGI Check-in - Support Python 3.12 (and 3.11) - Add backend for the WLCG IAM testing site - Add ping identity OIDC backend - Add uffd oauth2 backend - Add Clever backend - Add Twitter OAuth2 backend - Add backend for e-infra.cz - Replace jose with pyjwt ## [4.4.2](https://github.com/python-social-auth/social-core/releases/tag/4.4.2) - 2023-04-22 ### Changed - Fixed Azure AD Tenant authentication with custom signing keys - Added CAS OIDC backend - Made Keycloak `ID_KEY` configurable ## [4.4.1](https://github.com/python-social-auth/social-core/releases/tag/4.4.1) - 2023-03-30 ### Changed - Moved Facebook Limited Login to a separate module to avoid extra dependency - Update Azure AD B2C base URL to match updated endpoints ## [4.4.0](https://github.com/python-social-auth/social-core/releases/tag/4.4.0) - 2023-03-15 ### Added - Backend for OpenInfra OpenID - Facebook Limited Login backend - Add support for Python 3.11 ### Changed - Removed OpenStackDevOpenId backend - Updated `user_data` method in `StripeOAuth2` to return `email` in `get_user_details` - Removes fixed version of `lxml` - Fixed OIDC crash on groups - Fixed Qiita users identification - Dropped support for TLSv1 - Coding style improvements ## [4.3.0](https://github.com/python-social-auth/social-core/releases/tag/4.3.0) - 2022-06-13 ### Added - Add backend for Hashicorp Vault OIDC backend - Add generic OpenID Connect backend - Add Grafana OAuth2 backend - Add MusicBrainz OAuth2 backend ### Changed - Fixed redirect state for Keycloak backend - Add fallback to RSA256 in OpenID Connect when alg is not set - Fixed Azure backend so it can be used with all Azure authority hosts ## [4.2.0](https://github.com/python-social-auth/social-core/releases/tag/4.2.0) - 2022-01-17 ### Added - Add fields that populate on create but not update `SOCIAL_AUTH_IMMUTABLE_USER_FIELDS` - Add Gitea oauth2 backend - Add Twitch OpenId backend - Add CI Logon backend - Add support for Python 3.10 ### Changed - Fixed Slack user identity API call with Bearer headers - Fixed microsoft-graph login error - Fixed Twitch OAuth2 backend - Fixed Facebook API version - Fixed Okta authentication URLs - Fixed Globus JWT signature algorithm - Fixed kid key rotation for OpenID Connect - Fixed e-mail fetching from Azure - Fixed vkontakte API version - Restricted lxml to 4.6.x to avoid problems in SAML ## [4.1.0](https://github.com/python-social-auth/social-core/releases/tag/4.1.0) - 2021-03-01 ### Added - Discourse backend - Osso backend - Add `get` and `delete` class methods for `NonceMixin` - Use strategies as interface to fetch backends ### Changed - Get Apple user first and last name from `self.data` - Instagram Legacy API has been replaced with Instagram Basic Display API since the first one was deprecated, [see](https://www.instagram.com/developer/). - Store `expires_in` for Zoom backend - Dropped support no longer working Dropbox v1 API - Several improvements to the ORCIDOAuth2 backend - Make WHITELIST\_\* settings properly case insensitive - Fixed token validation in the AzureADV2TenantOAuth2 backend ## [4.0.3](https://github.com/python-social-auth/social-core/releases/tag/4.0.3) - 2021-01-12 ### Changed - Updated PyJWT version to 2.0.0 - Remove six dependency ## [4.0.2](https://github.com/python-social-auth/social-core/releases/tag/4.0.2) - 2021-01-10 ### Changed - Fixes to Github-action release mechanism ## [4.0.1](https://github.com/python-social-auth/social-core/releases/tag/4.0.1) - 2021-01-10 ### Changed - Fixes to Github-action release mechanism ## [4.0.0](https://github.com/python-social-auth/social-core/releases/tag/4.0.0) - 2021-01-10 ### Added - PayPal backend - Fence OIDC-based backend ### Changed - Dropped Python 2 support from testing stack - Remove discontinued Google OpenId backend - Remove discontinued Yahoo OpenId backend - Fix `jwt.decode()` passed algorithm - Prevent `PyJWT` v2.0.0 being installed - Update Facebook Graph API to 8.0 - Update Amazon fetch-profile URL - Fix Azure AD Tenant, unable to load certificate - Fix Okta well-known URL - Updated Discord's API hostname from discordapp.com to discord.com - Pass `client_secret` in auth-complete on Kakao backend ## [3.4.0](https://github.com/python-social-auth/social-core/releases/tag/3.4.0) - 2020-06-21 ### Added - Zoom backend ### Changed - Directly use `access_token` in Azure Tenant backend - Support Apple JWT audience - Update partial session cleanup to remove old token from session too - Fetch user email in Okta integration - Improve Python 3.9 compatibility - Send proxies in request - Improve error handling in Apple backend - Properly support case insensitive matching of whitelist settings ## [3.3.3](https://github.com/python-social-auth/social-core/releases/tag/3.3.3) - 2020-04-16 ### Changed - Updated list of default user protected fields to include admin flags and password ## [3.3.2](https://github.com/python-social-auth/social-core/releases/tag/3.3.2) - 2020-03-25 ### Changed - Updated package upload method to use `twine` ## [3.3.1](https://github.com/python-social-auth/social-core/releases/tag/3.3.1) - 2020-03-25 ### Changed - Reverted [PR #388](https://github.com/python-social-auth/social-core/pull/388/) due to dependency license incompatibility ## [3.3.0](https://github.com/python-social-auth/social-core/releases/tag/3.3.0) - 2020-03-17 ### Added - Okta backend - Support for SAML Single Logout - SimpleLogin backend - SurveyMonkey backend - HubSpot backend - MRG backend - Sign in with Apple backend - Allow ignoring of default protected user fields with option `SOCIAL_AUTH_NO_DEFAULT_PROTECTED_USER_FIELDS` - Support for users field names mapping - Added GithubAppAuth backend ### Changed - Add refresh token to Strava backend, change username and remove email - Update test runner to PyTest - Add python 3.7 CI target - Send User-Agent header on Untappd backend - Updated Naver API support from XML to JSON format - Use `unidecode` to cleanup usernames from unicode characters - Update Twitch API support from v3 to v5 - Properly setup `pytest` version for Python2 and Python3 - Fix some spelling mistakes in docstrings - Fix old fields from FIELDS_STORED_IN_SESSION persisting in session - Github: pass access token in a header instead of in a query parameter. - Update Kakao API support from v1 to v2 - Update Twitch API support to v5 - Updated Patreon API support from v1 to v2 per issue #307 - Fix `user_details` in user pipeline to allow model attributes to be updated - Updated Atlassian API urls ## [3.2.0](https://github.com/python-social-auth/social-core/releases/tag/3.2.0) - 2019-05-30 ### Added - Cognito backend - OpenStack (openstackid and openstackid-dev) backends ### Changed - Updated Linkedin backend to v2 API - Facebook: Update to use the latest Graph API v3.2 - Send User-Agent header on GitHub backend - Remove profile scope and verification at hash on Elixir backend - Mark description as Markdown for PyPI - Use `hmac.compare_digest` for constant time comparison - Replace deprecated Google+ API usage in GoogleOpenIdConnect - Defined scope separator for Strava backend - Ensure `saml_config.json` is included by addint it to `MANIFEST.in` - Include `email_verified` as part of user details on Auth0 backend - Include Shopify `version` parameter on Shopify session setup - Define `SOCIAL_AUTH_SHOPIFY_API_VERSION` setting to override default API version - Check user `id` attribute existence before using it - Pull `last_name` from `family_name` in Cognito backend - Ignore key errors on Naver backend for missing attributes ## [3.1.0](https://github.com/python-social-auth/social-core/releases/tag/3.1.0) - 2019-02-20 ### Added - Universe Ticketing backend - Auth0.com authentication backend ### Changed - Update Bungie backend dropping any Django reference - Enable and fix JWT related tests - Remove PyPy support from Tox - Drop support for Python 3.4 in Tox - Allow to override JWT decode options in Open ID Connect base backend - Pass access token via Authorization header to Google user data url - Updated `user_data` method in `AzureADOAuth2` to return `access_token` if `id_token` is not present in response ## [3.0.0](https://github.com/python-social-auth/social-core/releases/tag/3.0.0) - 2019-01-14 ### Changed - Updated Azure B2C to extract first email from list if it's a list - Replace deprecated Google+ API usage with https://www.googleapis.com/oauth2/v3/userinfo - Updated Azure Tenant to fix Nonetype error - Updated comment denoting incorrect setting name - Yandex: do not fail when no email is present - Mediawiki: do not fail when no email is present - Mediawiki: enhance `get_user_details` to return more details ## [2.0.0](https://github.com/python-social-auth/social-core/releases/tag/2.0.0) - 2018-10-28 ### Added - Telegram authentication backend - Keycloak backend is added with preliminary OAuth2 support - Globus OpenId Connect backend - Discord OAuth2 backend - SciStarter OAuth2 backend - Flat OAuth2 backend - ELIXIR OpenId Connect backend - Atlassian OAuth2 backend ### Changed - GitHub backend now uses `state` parameter instead of `redirect_state` - Correct setting name on AzureAD Tenant backend - Introduce access token expired threshold of 5 seconds by default - Delete partial token from session if still present - Use `userPrincipalName` to set `username` and `email` accordingly - Send authorization headers to Kakao OAuth2, properly fill user details - LINE API update to v2.1 - Use `unitest2` with Python 3 - Update Slack backend to use computed usename on teams setups - Enforce `unicode_literals` on Slack backend - Update ORCID backend to support Member API - Updated Pixelpin backend to use the new OpenId Connect service - Update `sanitize_redirect` to invalidate redirects like `///evil.com` - Update Coinbase API endpoint - Dropped Python 3.3 support - Updated Weixin backend to use `urlencode` from `six` - Updated Google+ backend to properly process requests with `id_token` - Updated OpenId connect dependencies ## [1.7.0](https://github.com/python-social-auth/social-core/releases/tag/1.7.0) - 2018-02-20 ### Changed - Update EvenOnline token expiration key - Update OpenStreetMap URL to `https` - Fix LinkedIn backend to send the oauth_token as `Authorization` header - Fixed `extra_data` update to use the `alias` as key too - Make `signed_request` optional in Facebook App OAuth2 backend - Support string and lists on SAML permanent id value - Correct sending `params` sending on `GET` access-token retrieval case - Ensure b2c policy name check - Use `extras_requrie` to specify python specific version dependencies ### Added - Added support for AzureAD B2C OAuth2 - Added LinkedIn Mobile OAuth2 backend ## [1.6.0](https://github.com/python-social-auth/social-core/releases/tag/1.6.0) - 2017-12-22 ### Changed - Fix coinbase backend to use api v2 - Default `REDIRECT_STATE` to `False` in `FacebookOAuth2` backend. - Add revoke token url for Coinbase OAuth2 backend - Fix LinkedIn backend to send `oauth_token` as request header - Make partial step decorator handle arguments ### Added - Added support for ChatWork OAuth2 backend ## [1.5.0](https://github.com/python-social-auth/social-core/releases/tag/1.5.0) - 2017-10-28 ### Changed - Fix using the entire SAML2 nameid string - Prevent timing attacks against state token - Updated GitLab API version to v4 - Enforce UTC when calculating access token expiration time - Cleanup user attributes update from social details - Send authorization header on Reddit auth ### Added - Added support for tenant for Azure AD backend - Added JWT validation for Azure AD backend - Added support for Bungie.net OAuth2 backend - Added support for Eventbrite OAuth2 backend - Added support for OpenShift OAuth2 backend - Added support for Microsoft Graph OAuth2 backend ## [1.4.0](https://github.com/python-social-auth/social-core/releases/tag/1.4.0) - 2017-06-09 ### Changed - Fix path in import BaseOAuth2 for Monzo - Fix auth header formatting problem for Fitbit OAuth2 - Raise AuthForbidden when provider returns 401. - Update Facebook API to version 2.9 - Speed up authorization process for VKAppOAuth2 - Apply same sanitization as on connect to disconnect. - Disable `redirect_state` usage on Disqus backend ### Added - Added Udata OAuth2 backend - Added ORCID backend - Added feature to get all extra data from backend through `GET_ALL_EXTRA_DATA` boolean flag. - Added Patreon provider ## [1.3.0](https://github.com/python-social-auth/social-core/releases/tag/1.3.0) - 2017-05-06 ### Added - Use extra_data method when refreshing an `access_token`, ensure that auth-time is updated then - Added 500px OAuth1 backend - Added Monzo OAuth2 backend - Added `get_access_token` method that will refresh if expired ### Changed - Updated email validation to pass the partial pipeline token if given. - Prefer passed parameters in `authenticate` method - Properly discard already used verification codes - Save SAML attributes in `extra_data` - Note `id_token` in GooglePlusAuth's AuthMissingParameter ## [1.2.0](https://github.com/python-social-auth/social-core/releases/tag/1.2.0) - 2017-02-10 ### Added - Limit Slack by team through `SOCIAL_AUTH_SLACK_TEAM` setting ### Changed - Enable defining extra arguments for AzureAD backend. - Updated key `expires` to `expires_in` for Facebook OAuth2 backend - Updated Slack `id` fetch to default to user `id` if not present in response ## [1.1.0](https://github.com/python-social-auth/social-core/releases/tag/1.1.0) - 2017-01-31 ### Added - Mediawiki backend - Strategy method to let implementation cleanup arguments passed to the authenticate method ### Changed - Removed OneLogin SAML IDP dummy settings while generating metadata xml - Fixed Asana user details response handling - Enforce defusedxml version with support for Python 3.6 - Updated documentation URL in backends ## [1.0.1](https://github.com/python-social-auth/social-core/releases/tag/1.0.1) - 2017-01-23 ### Changed - Fixed broken dependencies while building the package ## [1.0.0](https://github.com/python-social-auth/social-core/releases/tag/1.0.0) - 2017-01-22 ### Added - Store partial pipeline data in an storage class - Store `auth_time` with the last time authentication took place, use `auth_time` to determine if access token expired - Ensure that `testkey.pem` is distributed - Added Asana OAuth2 backend ### Changed - Removed the old `save_status_to_session` to partialize a pipeline run ## [0.2.1](https://github.com/python-social-auth/social-core/releases/tag/0.2.1) - 2016-12-31 ### Added - Defined `extras` for SAML, and "all" that will install SAML and OpenIdConnect - Added `auth_time` in extra data by default to store the time that the authentication took place ### Changed - Remove set/get current strategy methods - Fixed the `extras` requirements defined in the setup.py script ## [0.2.0](https://github.com/python-social-auth/social-core/releases/tag/0.2.0) - 2016-12-31 ### Changed - Reorganize requirements, make OpenIdConnect optional - Split OpenIdConnect from OpenId module, install with `social-core[openidconnect]` ## [0.1.0](https://github.com/python-social-auth/social-core/releases/tag/0.1.0) - 2016-12-28 ### Added - Added support for GitLab OAuth2 backend. Refs [#2](https://github.com/python-social-auth/social-core/issues/2) - Added support for Facebook OAuth2 return_scopes parameter. Refs [#818](https://github.com/omab/python-social-auth/issues/818) - Added support for per-backend USER_FIELDS setting. Refs [#661](https://github.com/omab/python-social-auth/issues/661) - Added `expires_in` as `expires` for LinkedIn OAuth2. Refs [#666](https://github.com/omab/python-social-auth/issues/666) - Added `SOCIAL_AUTH_USER_AGENT` setting to override the default User-Agent header. Refs [#752](https://github.com/omab/python-social-auth/issues/752) - Enabled Python 3 SAML support through python3-saml package. Refs [#846](https://github.com/omab/python-social-auth/issues/846) - Added better username characters cleanup rules, support for a configurable cleanup function through SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION (import path) setting. - Added configurable option SOCIAL_AUTH_FACEBOOK\_\*\_API_VERSION to override the default Facebook API version used. - Add Lyft OAuth2 implementation to Python Social Auth (port from [#1036](https://github.com/omab/python-social-auth/pull/1036/files) by iampark) - Added the ability to specify a pipeline on a per backend basis (port from [#1019](https://github.com/omab/python-social-auth/pull/1019) by keattang) - Add support for MailChimp as an OAuth v2 backend (port from [#1037](https://github.com/omab/python-social-auth/pull/1037) by svvitale) - Added Shimmering backend (port from [#1054](https://github.com/omab/python-social-auth/pull/1054) by iamkhush) - Added Quizlet backend (port from [#1012](https://github.com/omab/python-social-auth/pull/1012) by s-alexey) - Added Dockerfile to simplify the running of tests (`make docker-tox`) ### Changed - Changed Facebook refresh token processing. Refs [#866](https://github.com/omab/python-social-auth/issues/866) - Update Google+ Auth tokeninfo API version, drop support for deprecated API scopes. Refs [#791](https://github.com/omab/python-social-auth/issues/791). - Fixed OAuth1/2 early state validation on error responses. - Disabled SAML test when running on Travis-ci on Python 3.5 since it [segfaults](https://travis-ci.org/python-social-auth/social-core/jobs/186790227) probably by a bad build in one of the dependencies - Fixed Xing backend testing broken by previous change - Fixed Xing backend dropping `callback_uri` and `oauth_verifier` parameters on authenticated API calls. Refs [#871](https://github.com/omab/python-social-auth/issues/871) - Updated slack backend implementation, update API endpoints used, add test case. - Changed Dailymotion user data API endpoint - Changed how "false" values are treated in the user attributes update pipeline - Fix google OpenID Connect (port from [#747](https://github.com/omab/python-social-auth/pull/747) by mvschaik) - Update Facebook api version to v2.8 (port from [#1047](https://github.com/omab/python-social-auth/pull/1047) by browniebroke) - Remove Facebook2OAuth2 and Facebook2AppOAuth2 backends (port from [#1046](https://github.com/omab/python-social-auth/pull/1046) by browniebroke) - change username, email and fullname keys (port from [#1028](https://github.com/omab/python-social-auth/pull/1028) by inlanger) - Moves fix convert username to string (port from [#1021](https://github.com/omab/python-social-auth/pull/1021) by WarmongeR1) - Fix auth_params for Stripe backend (port from [#1034](https://github.com/omab/python-social-auth/pull/1034) by dchanm) - Preserve order of backends in BACKENDSCACHE (port from [#1004](https://github.com/omab/python-social-auth/pull/1004) by tsouvarev) - Don't lose custom exception message on raising AuthCanceled (port from [#1062](https://github.com/omab/python-social-auth/pull/1062) by dotsbb) - Fixed VK backend (port from [#1007](https://github.com/omab/python-social-auth/pull/1007) by DeKaN) - Updated Dropbox backend (port from [#1018](https://github.com/omab/python-social-auth/pull/1018) by illing2005) ## [0.0.1](https://github.com/python-social-auth/social-core/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-core-4.6.1/LICENSE000066400000000000000000000027711500362547200157010ustar00rootroot00000000000000Copyright (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-core-4.6.1/MANIFEST.in000066400000000000000000000003361500362547200164250ustar00rootroot00000000000000recursive-include social_core *.py include *.txt CHANGELOG.md LICENSE README.md social_core/tests/testkey.pem recursive-include social_core/tests *.txt *.json recursive-exclude .tox * recursive-exclude social_core *.pyc social-auth-core-4.6.1/README.md000066400000000000000000000025211500362547200161440ustar00rootroot00000000000000# Python Social Auth - Core Python Social Auth is an easy to setup social authentication/registration mechanism with support for several frameworks and auth providers. ## Description This is the core component of the python-social-auth ecosystem, it implements the common interface to define new authentication backends to third parties services, implement integrations with web frameworks and storage solutions. ## Documentation Project documentation is available at https://python-social-auth.readthedocs.io/. ## Setup ```shell $ pip install social-auth-core ``` ## 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-core-4.6.1/pyproject.toml000066400000000000000000000143651500362547200176120ustar00rootroot00000000000000[build-system] build-backend = "setuptools.build_meta" requires = ["setuptools>=78.0.2"] [dependency-groups] dev = [ "coverage>=3.6", "flake8>=5.0.4", "google-auth-stubs==0.3.0", "mypy==1.15.0", "pyright==1.1.400", "pytest-cov>=2.7.1", "pytest-xdist>=3.6.1", "pytest>=4.5", "responses==0.25.7", "types-defusedxml==0.7.0.20240218", "types-oauthlib==3.2.0.20250408", "types-python-jose==3.4.0.20250224", "types-requests-oauthlib==2.0.0.20250306", "types-requests==2.32.0.20250328" ] [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", "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.9", "Programming Language :: Python", "Topic :: Internet" ] dependencies = [ "cryptography>=1.4", "defusedxml>=0.5.0", "oauthlib>=1.0.3", "PyJWT>=2.7.0", "python3-openid>=3.0.10", "requests-oauthlib>=0.6.1", "requests>=2.9.1" ] description = "Python social authentication made simple." keywords = ["oauth", "openid", "saml", "social auth"] license = "BSD-3-Clause" license-files = ["LICENSE"] name = "social-auth-core" readme = "README.md" requires-python = ">=3.9" version = "4.6.1" [project.optional-dependencies] all = [ "social-auth-core[azuread]", "social-auth-core[google-onetap]", "social-auth-core[saml]", "social-auth-core[shopify]" ] allpy3 = [ "social-auth-core[all]" ] azuread = [ "cryptography>=2.1.1" ] # This is present until pip implements supports for PEP 735 # see https://github.com/pypa/pip/issues/12963 dev = [ "coverage>=3.6", "flake8>=5.0.4", "google-auth-stubs==0.3.0", "mypy==1.15.0", "pyright==1.1.400", "pytest-cov>=2.7.1", "pytest-xdist>=3.6.1", "pytest>=4.5", "responses==0.25.7", "types-defusedxml==0.7.0.20240218", "types-oauthlib==3.2.0.20250408", "types-python-jose==3.4.0.20250224", "types-requests-oauthlib==2.0.0.20250306", "types-requests==2.32.0.20250328" ] google-onetap = [ "google-auth~=2.39.0" ] ping = [ "python-jose>=3.4.0,<4.0" ] saml = [ "python3-saml>=1.5.0" ] shopify = [ "ShopifyAPI" ] [project.urls] Homepage = "https://github.com/python-social-auth/social-core" [tool.check-manifest] ignore = [ ".coveragerc", ".pre-commit-config.yaml", ".well-known/funding-manifest-urls", "tox.ini" ] [tool.codespell] count = true ignore-words-list = "assertIn" [tool.mypy] check_untyped_defs = true [[tool.mypy.overrides]] disallow_untyped_defs = true ignore_missing_imports = true module = [ "google.appengine.*", "onelogin.*", "openid.*", "shopify.*" ] [tool.pyright] executionEnvironments = [ {reportMissingImports = "none", root = "social_core/backends/gae.py"} ] pythonVersion = "3.9" reportOptionalMemberAccess = "none" reportPossiblyUnboundVariable = "none" [tool.ruff.lint] extend-safe-fixes = [ "ANN", "D", "FA102", "FLY", "SIM", "TCH", "UP" ] ignore = [ "A001", # TODO: Variable is shadowing a Python builtin "A002", # TODO: Function argument is shadowing a Python builtin "ANN", # TODO: Missing type annotations "ARG001", # TODO: Unused function argument (mostly for API compatibility) "ARG002", # TODO: Unused method argument (mostly for API compatibility) "ARG003", # TODO: Unused class method argument "ARG005", # TODO: Unused lambda argument "B018", # TODO: Found useless expression. "B026", # TODO: Star-arg unpacking after a keyword argument is strongly discouraged "B904", # TODO: use raise from "BLE001", # TODO: Do not catch blind exception: `Exception` "COM", # CONFIG: No trailing commas "D", # TODO: Missing documentation "D203", # CONFIG: incompatible with D211 "D212", # CONFIG: incompatible with D213 "DTZ001", # TODO: `datetime.datetime()` called without a `tzinfo` argument "DTZ005", # TODO: `datetime.datetime.now()` called without a `tz` argument "E501", # WONTFIX: we accept long strings (rest is formatted by ruff) "EM101", # TODO: Exception must not use a string literal, assign to variable first "EM102", # TODO: Exception must not use an f-string literal, assign to variable first "ERA001", # TODO: Found commented-out code "FBT", # TODO: Boolean in function definition "FIX002", # CONFIG: we use TODO "ISC003", # TODO: Explicitly concatenated string should be implicitly concatenated "N", # TODO: Naming issues "PERF203", # WONTFIX: This rule is only enforced for Python versions prior to 3.11 "PLR2004", # TODO: Magic value used in comparison "PLW0603", # TODO: Using the global statement to update `BACKENDSCACHE` is discouraged "PLW2901", # TODO: loop variable overwritten by assignment target "PT", # CONFIG: Not using pytest "RUF012", # TODO: Type annotations "S101", # TODO: Use of `assert` detected "S105", # TODO: Possible hardcoded password assigned "S110", # TODO: `try`-`except`-`pass` detected, consider logging the exception "S301", # TODO: `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue "S318", # TODO: Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents "TD002", # CONFIG: no detailed TODO documentation is required "TD003", # CONFIG: no detailed TODO documentation is required "TID252", # TODO: Prefer absolute imports over relative imports from parent modules "TRY003", # TODO: Avoid specifying long messages outside the exception class "TRY301", # TODO: Abstract `raise` to an inner function 'ISC001' # CONFIG: formatter ] select = ["ALL"] [tool.ruff.lint.mccabe] max-complexity = 11 # TODO: should be lower [tool.ruff.lint.per-file-ignores] "social_core/pipeline/debug.py" = ["T201", "T203"] [tool.ruff.lint.pylint] # TODO: all these should be lower (or use defaults) max-args = 7 max-branches = 15 [tool.setuptools] packages = ["social_core"] [tool.tomlsort] ignore_case = true sort_inline_arrays = true sort_inline_tables = true sort_table_keys = true spaces_before_inline_comment = 2 social-auth-core-4.6.1/social_core/000077500000000000000000000000001500362547200171475ustar00rootroot00000000000000social-auth-core-4.6.1/social_core/__init__.py000066400000000000000000000001301500362547200212520ustar00rootroot00000000000000import importlib.metadata __version__ = importlib.metadata.version("social-auth-core") social-auth-core-4.6.1/social_core/actions.py000066400000000000000000000131031500362547200211570ustar00rootroot00000000000000from urllib.parse import quote from .utils import ( partial_pipeline_data, sanitize_redirect, setting_url, user_is_active, user_is_authenticated, ) def do_auth(backend, redirect_name="next"): # Save any defined next value into session data = backend.strategy.request_data(merge=False) # Save extra data into session. for field_name in backend.setting("FIELDS_STORED_IN_SESSION", []): if field_name in data: backend.strategy.session_set(field_name, data[field_name]) else: backend.strategy.session_set(field_name, None) if redirect_name in data: # Check and sanitize a user-defined GET/POST next field value redirect_uri = data[redirect_name] if backend.setting("SANITIZE_REDIRECTS", True): allowed_hosts = [ *backend.setting("ALLOWED_REDIRECT_HOSTS", []), backend.strategy.request_host(), ] redirect_uri = sanitize_redirect(allowed_hosts, redirect_uri) backend.strategy.session_set( redirect_name, redirect_uri or backend.setting("LOGIN_REDIRECT_URL") ) return backend.start() def do_complete(backend, login, user=None, redirect_name="next", *args, **kwargs): data = backend.strategy.request_data() is_authenticated = user_is_authenticated(user) user = user if is_authenticated else None partial = partial_pipeline_data(backend, user, *args, **kwargs) if partial: user = backend.continue_pipeline(partial) # clean partial data after usage backend.strategy.clean_partial_pipeline(partial.token) else: user = backend.complete(user=user, redirect_name=redirect_name, *args, **kwargs) # pop redirect value before the session is trashed on login(), but after # the pipeline so that the pipeline can change the redirect if needed redirect_value = backend.strategy.session_get(redirect_name, "") or data.get( redirect_name, "" ) # check if the output value is something else than a user and just # return it to the client user_model = backend.strategy.storage.user.user_model() if user and not isinstance(user, user_model): return user if is_authenticated: if not user: url = setting_url(backend, redirect_value, "LOGIN_REDIRECT_URL") else: url = setting_url( backend, redirect_value, "NEW_ASSOCIATION_REDIRECT_URL", "LOGIN_REDIRECT_URL", ) elif user: if user_is_active(user): # catch is_new/social_user in case login() resets the instance is_new = getattr(user, "is_new", False) social_user = user.social_user login(backend, user, social_user) # store last login backend name in session backend.strategy.session_set( "social_auth_last_login_backend", social_user.provider ) if is_new: url = setting_url( backend, "NEW_USER_REDIRECT_URL", redirect_value, "LOGIN_REDIRECT_URL", ) else: url = setting_url(backend, redirect_value, "LOGIN_REDIRECT_URL") else: if backend.setting("INACTIVE_USER_LOGIN", False): social_user = user.social_user login(backend, user, social_user) url = setting_url( backend, "INACTIVE_USER_URL", "LOGIN_ERROR_URL", "LOGIN_URL" ) else: url = setting_url(backend, "LOGIN_ERROR_URL", "LOGIN_URL") assert url, "By this point URL has to have been set" if redirect_value and redirect_value != url: redirect_value = quote(redirect_value) url += ("&" if "?" in url else "?") + f"{redirect_name}={redirect_value}" if backend.setting("SANITIZE_REDIRECTS", True): allowed_hosts = [ *backend.setting("ALLOWED_REDIRECT_HOSTS", []), backend.strategy.request_host(), ] url = sanitize_redirect(allowed_hosts, url) or backend.setting( "LOGIN_REDIRECT_URL" ) return backend.strategy.redirect(url) def do_disconnect( backend, user, association_id=None, redirect_name="next", *args, **kwargs ): partial = partial_pipeline_data(backend, user, *args, **kwargs) if partial: if association_id and not partial.kwargs.get("association_id"): partial.extend_kwargs({"association_id": association_id}) response = backend.disconnect(*partial.args, **partial.kwargs) # clean partial data after usage backend.strategy.clean_partial_pipeline(partial.token) else: response = backend.disconnect( user=user, association_id=association_id, *args, **kwargs ) if isinstance(response, dict): url = backend.strategy.absolute_uri( backend.strategy.request_data().get(redirect_name, "") or backend.setting("DISCONNECT_REDIRECT_URL") or backend.setting("LOGIN_REDIRECT_URL") ) if backend.setting("SANITIZE_REDIRECTS", True): allowed_hosts = [ *backend.setting("ALLOWED_REDIRECT_HOSTS", []), backend.strategy.request_host(), ] url = ( sanitize_redirect(allowed_hosts, url) or backend.setting("DISCONNECT_REDIRECT_URL") or backend.setting("LOGIN_REDIRECT_URL") ) response = backend.strategy.redirect(url) return response social-auth-core-4.6.1/social_core/backends/000077500000000000000000000000001500362547200207215ustar00rootroot00000000000000social-auth-core-4.6.1/social_core/backends/__init__.py000066400000000000000000000000001500362547200230200ustar00rootroot00000000000000social-auth-core-4.6.1/social_core/backends/amazon.py000066400000000000000000000027151500362547200225650ustar00rootroot00000000000000""" Amazon OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/amazon.html """ from .oauth import BaseOAuth2 class AmazonOAuth2(BaseOAuth2): name = "amazon" ID_KEY = "user_id" AUTHORIZATION_URL = "https://www.amazon.com/ap/oa" ACCESS_TOKEN_URL = "https://api.amazon.com/auth/o2/token" DEFAULT_SCOPE = ["profile"] REDIRECT_STATE = False EXTRA_DATA = [ ("refresh_token", "refresh_token", True), ("user_id", "user_id"), ("postal_code", "postal_code"), ] def get_user_details(self, response): """Return user details from amazon account""" name = response.get("name") or "" fullname, first_name, last_name = self.get_user_names(name) return { "username": name, "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Grab user profile information from amazon.""" response = self.get_json( "https://api.amazon.com/user/profile", params={"access_token": access_token} ) if "Profile" in response: response = { "user_id": response["Profile"]["CustomerId"], "name": response["Profile"]["Name"], "email": response["Profile"]["PrimaryEmail"], } return response social-auth-core-4.6.1/social_core/backends/angel.py000066400000000000000000000020231500362547200223560ustar00rootroot00000000000000""" Angel OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/angel.html """ from .oauth import BaseOAuth2 class AngelOAuth2(BaseOAuth2): name = "angel" AUTHORIZATION_URL = "https://angel.co/api/oauth/authorize/" ACCESS_TOKEN_URL = "https://angel.co/api/oauth/token/" REDIRECT_STATE = False def get_user_details(self, response): """Return user details from Angel account""" username = response["angellist_url"].split("/")[-1] email = response.get("email", "") fullname, first_name, last_name = self.get_user_names(response["name"]) return { "username": username, "fullname": fullname, "first_name": first_name, "last_name": last_name, "email": email, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://api.angel.co/1/me/", params={"access_token": access_token} ) social-auth-core-4.6.1/social_core/backends/aol.py000066400000000000000000000003421500362547200220450ustar00rootroot00000000000000""" AOL OpenId backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/aol.html """ from .open_id import OpenIdAuth class AOLOpenId(OpenIdAuth): name = "aol" URL = "http://openid.aol.com" social-auth-core-4.6.1/social_core/backends/apple.py000066400000000000000000000136031500362547200223770ustar00rootroot00000000000000""" Sign In With Apple authentication backend. Docs: * https://developer.apple.com/documentation/signinwithapplerestapi * https://developer.apple.com/documentation/signinwithapplerestapi/tokenresponse Settings: * `TEAM` - your team id; * `KEY` - your key id; * `CLIENT` - your client id; * `AUDIENCE` - a list of authorized client IDs, defaults to [CLIENT]. Use this if you need to accept both service and bundle id to be able to login both via iOS and ie a web form. * `SECRET` - your secret key; * `SCOPE` (optional) - e.g. `['name', 'email']`; * `EMAIL_AS_USERNAME` - use apple email is username is set, use apple id otherwise. * `AppleIdAuth.TOKEN_TTL_SEC` - time before JWT token expiration, seconds. * `SOCIAL_AUTH_APPLE_ID_INACTIVE_USER_LOGIN` - allow inactive users email to login """ from __future__ import annotations import json import time from typing import TYPE_CHECKING import jwt from jwt.algorithms import RSAAlgorithm from jwt.exceptions import PyJWTError from social_core.backends.oauth import BaseOAuth2 from social_core.exceptions import AuthFailed if TYPE_CHECKING: from jwt.types import JWKDict class AppleIdAuth(BaseOAuth2): name = "apple-id" JWK_URL = "https://appleid.apple.com/auth/keys" AUTHORIZATION_URL = "https://appleid.apple.com/auth/authorize" ACCESS_TOKEN_URL = "https://appleid.apple.com/auth/token" RESPONSE_MODE = None ID_KEY = "sub" TOKEN_KEY = "id_token" STATE_PARAMETER = True REDIRECT_STATE = False SCOPE_SEPARATOR = "%20" TOKEN_AUDIENCE = "https://appleid.apple.com" TOKEN_TTL_SEC = 6 * 30 * 24 * 60 * 60 def get_audience(self): client_id = self.setting("CLIENT") return self.setting("AUDIENCE", default=[client_id]) def auth_params(self, *args, **kwargs): """ Apple requires to set `response_mode` to `form_post` if `scope` parameter is passed. """ params = super().auth_params(*args, **kwargs) if self.RESPONSE_MODE: params["response_mode"] = self.RESPONSE_MODE elif self.get_scope(): params["response_mode"] = "form_post" return params def get_private_key(self): """ Return contents of the private key file. Override this method to provide secret key from another source if needed. """ return self.setting("SECRET") def generate_client_secret(self): now = int(time.time()) client_id = self.data.get("client_id", self.setting("CLIENT")) team_id = self.setting("TEAM") key_id = self.setting("KEY") private_key = self.get_private_key() headers = {"kid": key_id} payload = { "iss": team_id, "iat": now, "exp": now + self.TOKEN_TTL_SEC, "aud": self.TOKEN_AUDIENCE, "sub": client_id, } return jwt.encode(payload, key=private_key, algorithm="ES256", headers=headers) def get_key_and_secret(self): client_id = self.data.get("client_id", self.setting("CLIENT")) client_secret = self.generate_client_secret() return client_id, client_secret def get_apple_jwk(self, kid=None) -> str | JWKDict: """ Return requested Apple public key or all available. """ keys = self.get_json(url=self.JWK_URL).get("keys") if not isinstance(keys, list) or not keys: raise AuthFailed(self, "Invalid jwk response") if kid: return json.dumps(next(key for key in keys if key["kid"] == kid)) # TODO: this should actually return a JWKDict; the caller expects it. # I suspect this code path is never hit in practice return (json.dumps(key) for key in keys) # type:ignore[reportReturnType] def decode_id_token(self, id_token): """ Decode and validate JWT token from apple and return payload including user data. """ if not id_token: raise AuthFailed(self, "Missing id_token parameter") try: kid = jwt.get_unverified_header(id_token).get("kid") public_key = RSAAlgorithm.from_jwk(self.get_apple_jwk(kid)) decoded = jwt.decode( id_token, key=public_key, # type: ignore[reportArgumentType] audience=self.get_audience(), algorithms=["RS256"], ) except PyJWTError as error: raise AuthFailed(self, f"Token validation failed by {error}") return decoded def get_user_details(self, response): name = json.loads(self.data.get("user", "{}")).get("name", {}) fullname, first_name, last_name = self.get_user_names( fullname="", first_name=name.get("firstName", ""), last_name=name.get("lastName", ""), ) email = response.get("email", "") apple_id = response.get(self.ID_KEY, "") # prevent updating User with empty strings user_details = { "fullname": fullname or None, "first_name": first_name or None, "last_name": last_name or None, "email": email, } if email and self.setting("EMAIL_AS_USERNAME"): user_details["username"] = email if apple_id and not self.setting("EMAIL_AS_USERNAME"): user_details["username"] = apple_id return user_details def do_auth(self, access_token, *args, **kwargs): response = kwargs.pop("response", None) or {} jwt_string = response.get(self.TOKEN_KEY) or access_token if not jwt_string: raise AuthFailed(self, "Missing id_token parameter") decoded_data = self.decode_id_token(jwt_string) return super().do_auth(access_token, response=decoded_data, *args, **kwargs) social-auth-core-4.6.1/social_core/backends/appsfuel.py000066400000000000000000000027021500362547200231130ustar00rootroot00000000000000""" Appsfueld OAuth2 backend (with sandbox mode support), docs at: https://python-social-auth.readthedocs.io/en/latest/backends/appsfuel.html """ from .oauth import BaseOAuth2 class AppsfuelOAuth2(BaseOAuth2): name = "appsfuel" ID_KEY = "user_id" AUTHORIZATION_URL = "http://app.appsfuel.com/content/permission" ACCESS_TOKEN_URL = "https://api.appsfuel.com/v1/live/oauth/token" USER_DETAILS_URL = "https://api.appsfuel.com/v1/live/user" def get_user_details(self, response): """Return user details from Appsfuel account""" email = response.get("email", "") username = email.split("@")[0] if email else "" fullname, first_name, last_name = self.get_user_names( response.get("display_name", "") ) return { "username": username, "fullname": fullname, "first_name": first_name, "last_name": last_name, "email": email, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( self.USER_DETAILS_URL, params={"access_token": access_token} ) class AppsfuelOAuth2Sandbox(AppsfuelOAuth2): name = "appsfuel-sandbox" AUTHORIZATION_URL = "https://api.appsfuel.com/v1/sandbox/choose" ACCESS_TOKEN_URL = "https://api.appsfuel.com/v1/sandbox/oauth/token" USER_DETAILS_URL = "https://api.appsfuel.com/v1/sandbox/user" social-auth-core-4.6.1/social_core/backends/arcgis.py000066400000000000000000000017631500362547200225520ustar00rootroot00000000000000""" ArcGIS OAuth2 backend """ from .oauth import BaseOAuth2 class ArcGISOAuth2(BaseOAuth2): name = "arcgis" ID_KEY = "username" AUTHORIZATION_URL = "https://www.arcgis.com/sharing/rest/oauth2/authorize" ACCESS_TOKEN_URL = "https://www.arcgis.com/sharing/rest/oauth2/token" EXTRA_DATA = [("expires_in", "expires_in"), ("refresh_token", "refresh_token")] def get_user_details(self, response): """Return user details from ArcGIS account""" return { "username": response.get("username"), "email": response.get("email"), "fullname": response.get("fullName"), "first_name": response.get("firstName"), "last_name": response.get("lastName"), } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://www.arcgis.com/sharing/rest/community/self", params={"token": access_token, "f": "json"}, ) social-auth-core-4.6.1/social_core/backends/asana.py000066400000000000000000000026651500362547200223670ustar00rootroot00000000000000import datetime from .oauth import BaseOAuth2 class AsanaOAuth2(BaseOAuth2): name = "asana" AUTHORIZATION_URL = "https://app.asana.com/-/oauth_authorize" ACCESS_TOKEN_URL = "https://app.asana.com/-/oauth_token" REFRESH_TOKEN_URL = "https://app.asana.com/-/oauth_token" REDIRECT_STATE = False USER_DATA_URL = "https://app.asana.com/api/1.0/users/me" EXTRA_DATA = [ ("expires_in", "expires"), ("refresh_token", "refresh_token"), ("name", "name"), ] def get_user_details(self, response): data = response["data"] fullname, first_name, last_name = self.get_user_names(data["name"]) return { "email": data["email"], "username": data["email"], "fullname": fullname, "last_name": last_name, "first_name": first_name, } def user_data(self, access_token, *args, **kwargs): return self.get_json( self.USER_DATA_URL, headers={"Authorization": f"Bearer {access_token}"} ) def extra_data(self, user, uid, response, details=None, *args, **kwargs): data = super().extra_data(user, uid, response, details) if self.setting("ESTIMATE_EXPIRES_ON"): expires_on = datetime.datetime.now( datetime.timezone.utc ) + datetime.timedelta(seconds=data["expires"]) data["expires_on"] = expires_on.isoformat() return data social-auth-core-4.6.1/social_core/backends/atlassian.py000066400000000000000000000030171500362547200232530ustar00rootroot00000000000000from social_core.backends.oauth import BaseOAuth2 class AtlassianOAuth2(BaseOAuth2): name = "atlassian" AUTHORIZATION_URL = "https://auth.atlassian.com/authorize" ACCESS_TOKEN_URL = "https://auth.atlassian.com/oauth/token" DEFAULT_SCOPE = ["read:jira-user", "offline_access"] ID_KEY = "accountId" EXTRA_DATA = [ ("resources", "resources"), ("refresh_token", "refresh_token"), ("expires_in", "expires_in"), ] def auth_params(self, state=None): params = super().auth_params(state) params.update({"audience": "api.atlassian.com", "prompt": "consent"}) return params def get_user_details(self, response): fullname, first_name, last_name = self.get_user_names(response["displayName"]) return { "username": response["accountId"], "email": response["emailAddress"], "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): resources = self.get_json( "https://api.atlassian.com/oauth/token/accessible-resources", headers={"Authorization": f"Bearer {access_token}"}, ) user_info = self.get_json( "https://api.atlassian.com/ex/jira/{}/rest/api/2/myself".format( resources[0]["id"] ), headers={"Authorization": f"Bearer {access_token}"}, ) user_info["resources"] = resources return user_info social-auth-core-4.6.1/social_core/backends/auth0.py000066400000000000000000000045531500362547200223230ustar00rootroot00000000000000""" Auth0 implementation based on: https://auth0.com/docs/quickstart/webapp/django/01-login """ import jwt from .oauth import BaseOAuth2 class Auth0OAuth2(BaseOAuth2): """Auth0 OAuth authentication backend""" name = "auth0" SCOPE_SEPARATOR = " " EXTRA_DATA = [("picture", "picture")] def api_path(self, path=""): """Build API path for Auth0 domain""" return "https://{domain}/{path}".format( domain=self.setting("DOMAIN"), path=path ) def authorization_url(self): return self.api_path("authorize") def access_token_url(self): return self.api_path("oauth/token") def get_user_id(self, details, response): """Return current user id.""" return details["user_id"] def get_user_details(self, response): # Obtain JWT and the keys to validate the signature id_token = response.get("id_token") jwks = self.get_json(self.api_path(".well-known/jwks.json")) issuer = self.api_path() audience = self.setting("KEY") # CLIENT_ID try: # it could be a set of JWKs keys = jwt.PyJWKSet.from_dict(jwks).keys except jwt.PyJWKSetError: # let any error raise from here # try to get single JWK keys = [jwt.PyJWK.from_dict(jwks, "RS256")] signature_error = None for key in keys: try: payload = jwt.decode( id_token, key.key, algorithms=["RS256"], audience=audience, issuer=issuer, ) except (jwt.InvalidSignatureError, jwt.InvalidAlgorithmError) as ex: signature_error = ex else: break else: assert signature_error is not None # raise last esception found during iteration raise signature_error fullname, first_name, last_name = self.get_user_names(payload["name"]) return { "username": payload["nickname"], "email": payload["email"], "email_verified": payload.get("email_verified", False), "fullname": fullname, "first_name": first_name, "last_name": last_name, "picture": payload["picture"], "user_id": payload["sub"], } social-auth-core-4.6.1/social_core/backends/azuread.py000066400000000000000000000132371500362547200227340ustar00rootroot00000000000000""" Copyright (c) 2015 Microsoft Open Technologies, Inc. All rights reserved. MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Azure AD OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/azuread.html """ import time import jwt from ..exceptions import AuthMissingParameter, AuthTokenError from .oauth import BaseOAuth2 class AzureADOAuth2(BaseOAuth2): name = "azuread-oauth2" SCOPE_SEPARATOR = " " BASE_URL = "https://{authority_host}/{tenant_id}" AUTHORIZATION_URL = "{base_url}/oauth2/authorize" ACCESS_TOKEN_URL = "{base_url}/oauth2/token" REDIRECT_STATE = False DEFAULT_SCOPE = ["openid", "profile", "user_impersonation", "email"] EXTRA_DATA = [ ("access_token", "access_token"), ("id_token", "id_token"), ("refresh_token", "refresh_token"), ("expires_in", "expires"), ("expires_on", "expires_on"), ("not_before", "not_before"), ("given_name", "first_name"), ("family_name", "last_name"), ("token_type", "token_type"), ] @property def authority_host(self): return self.setting("AUTHORITY_HOST", "login.microsoftonline.com") @property def tenant_id(self): return "common" @property def base_url(self): return self.BASE_URL.format( authority_host=self.authority_host, tenant_id=self.tenant_id ) def authorization_url(self): return self.AUTHORIZATION_URL.format(base_url=self.base_url) def access_token_url(self): return self.ACCESS_TOKEN_URL.format(base_url=self.base_url) def get_user_id(self, details, response): """Use upn as unique id""" upn = response.get("upn") if upn is None: raise AuthMissingParameter(self, "upn") return upn def get_user_details(self, response): """Return user details from Azure AD account""" fullname, first_name, last_name = ( response.get("name", ""), response.get("given_name", ""), response.get("family_name", ""), ) return { "username": fullname, "email": response.get("email", response.get("upn")), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): response = kwargs.get("response") if response and response.get("id_token"): id_token = response.get("id_token") else: id_token = access_token try: decoded_id_token = jwt.decode(id_token, options={"verify_signature": False}) except (jwt.DecodeError, jwt.ExpiredSignatureError) as de: raise AuthTokenError(self, de) return decoded_id_token def auth_extra_arguments(self): """Return extra arguments needed on auth process. The defaults can be overridden by GET parameters.""" extra_arguments = super().auth_extra_arguments() resource = self.setting("RESOURCE") if resource: extra_arguments.update({"resource": resource}) return extra_arguments def extra_data(self, user, uid, response, details=None, *args, **kwargs): """Return access_token and extra defined names to store in extra_data field""" data = super().extra_data(user, uid, response, details, *args, **kwargs) data["resource"] = self.setting("RESOURCE") return data def refresh_token_params(self, token, *args, **kwargs): return { "client_id": self.setting("KEY"), "client_secret": self.setting("SECRET"), "refresh_token": token, "grant_type": "refresh_token", "resource": self.setting("RESOURCE"), } def get_auth_token(self, user_id): """Return the access token for the given user, after ensuring that it has not expired, or refreshing it if so.""" user = self.get_user(user_id=user_id) access_token = user.social_user.access_token expires_on = user.social_user.extra_data["expires_on"] if expires_on <= int(time.time()): new_token_response = self.refresh_token(token=access_token) access_token = new_token_response["access_token"] return access_token class AzureADOAuth2V2(AzureADOAuth2): """Version of the AzureADOAuth2 backend that uses the v2.0 API endpoints, supporting users with personal Microsoft accounts, if the app settings allow them.""" name = "azuread-oauth2-v2" AUTHORIZATION_URL = "{base_url}/oauth2/v2.0/authorize" ACCESS_TOKEN_URL = "{base_url}/oauth2/v2.0/token" DEFAULT_SCOPE = ["User.Read profile openid email"] social-auth-core-4.6.1/social_core/backends/azuread_b2c.py000066400000000000000000000150141500362547200234550ustar00rootroot00000000000000""" Copyright (c) 2018 Noderabbit Inc., d.b.a. Appsembler All rights reserved. MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. See https://nicksnettravels.builttoroam.com/post/2017/01/24/Verifying-Azure-Active-Directory-JWT-Tokens.aspx for verifying JWT tokens. """ import json from cryptography.hazmat.primitives import serialization from jwt import DecodeError, ExpiredSignatureError, get_unverified_header from jwt import decode as jwt_decode from jwt.algorithms import RSAAlgorithm from ..exceptions import AuthException, AuthTokenError from .azuread import AzureADOAuth2 class AzureADB2COAuth2(AzureADOAuth2): name = "azuread-b2c-oauth2" BASE_URL = "https://{authority_host}/{tenant_name}.onmicrosoft.com" AUTHORIZATION_URL = "{base_url}/oauth2/v2.0/authorize" OPENID_CONFIGURATION_URL = ( "{base_url}/v2.0/.well-known/openid-configuration?p={policy}" ) ACCESS_TOKEN_URL = "{base_url}/oauth2/v2.0/token?p={policy}" JWKS_URL = "{base_url}/discovery/v2.0/keys?p={policy}" DEFAULT_SCOPE = ["openid", "email"] EXTRA_DATA = [ ("access_token", "access_token"), ("id_token", "id_token"), ("refresh_token", "refresh_token"), ("id_token_expires_in", "expires"), ("exp", "expires_on"), ("not_before", "not_before"), ("given_name", "first_name"), ("family_name", "last_name"), ("tfp", "policy"), ("token_type", "token_type"), ] @property def authority_host(self): return self.setting("AUTHORITY_HOST", f"{self.tenant_name}.b2clogin.com") @property def tenant_name(self): return self.setting("TENANT_NAME") @property def policy(self): policy = self.setting("POLICY") if not policy or not policy.lower().startswith("b2c_"): raise AuthException( self, "SOCIAL_AUTH_AZUREAD_B2C_OAUTH2_POLICY is " "required and should start with `b2c_`", ) return policy @property def base_url(self): return self.BASE_URL.format( tenant_name=self.tenant_name, authority_host=self.authority_host ) def openid_configuration_url(self): return self.OPENID_CONFIGURATION_URL.format( base_url=self.base_url, policy=self.policy ) def authorization_url(self): # Policy is required, but added later by `auth_extra_arguments()` return self.AUTHORIZATION_URL.format(base_url=self.base_url) def access_token_url(self): return self.ACCESS_TOKEN_URL.format(base_url=self.base_url, policy=self.policy) def jwks_url(self): return self.JWKS_URL.format(base_url=self.base_url, policy=self.policy) def request_access_token(self, *args, **kwargs): """ This is probably a hack, but otherwise AzureADOAuth2 expects `access_token`. However, B2C backends provides `id_token`. """ response = super().request_access_token(*args, **kwargs) if "access_token" not in response: response["access_token"] = response["id_token"] return response def auth_extra_arguments(self): """ Return extra arguments needed on auth process. The defaults can be overridden by GET parameters. """ extra_arguments = super().auth_extra_arguments() extra_arguments["p"] = self.policy or self.data.get("p") return extra_arguments def jwt_key_to_pem(self, key_json_dict): """ Builds a PEM formatted key string from a JWT public key dict. """ pub_key = RSAAlgorithm.from_jwk(json.dumps(key_json_dict)) # TODO: clarify the types of this; JWKs can apparently include both public and private, # but this code assumes public. return pub_key.public_bytes( # type: ignore[reportAttributeAccessIssue] encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo, ) def get_user_id(self, details, response): """Use subject (sub) claim as unique id.""" return response.get("sub") def get_user_details(self, response): """ Email address is returned on a different attribute for AzureAD B2C backends. """ details = super().get_user_details(response) if not details["email"] and response.get("emails"): details["email"] = response["emails"] if isinstance(details.get("email"), (list, tuple)): details["email"] = details["email"][0] return details def get_public_key(self, kid): """ Retrieve JWT keys from the URL. """ resp = self.request(self.jwks_url(), method="GET") resp.raise_for_status() # find the proper key for the kid for key in resp.json()["keys"]: if key["kid"] == kid: return self.jwt_key_to_pem(key) raise DecodeError(f"Cannot find kid={kid}") def user_data(self, access_token, *args, **kwargs): response = kwargs.get("response") id_token = response.get("id_token") # `kid` is short for key id kid = get_unverified_header(id_token)["kid"] key = self.get_public_key(kid) try: return jwt_decode( id_token, key=key, algorithms=["RS256"], audience=self.setting("KEY"), leeway=self.setting("JWT_LEEWAY", default=0), ) except (DecodeError, ExpiredSignatureError) as error: raise AuthTokenError(self, error) social-auth-core-4.6.1/social_core/backends/azuread_tenant.py000066400000000000000000000114121500362547200242760ustar00rootroot00000000000000import base64 from cryptography.hazmat.backends import default_backend from cryptography.x509 import load_der_x509_certificate from jwt import DecodeError, ExpiredSignatureError, get_unverified_header from jwt import decode as jwt_decode from ..exceptions import AuthTokenError from .azuread import AzureADOAuth2 """ Copyright (c) 2015 Microsoft Open Technologies, Inc. All rights reserved. MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ """ Azure AD OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/azuread.html See https://nicksnettravels.builttoroam.com/post/2017/01/24/Verifying-Azure-Active-Directory-JWT-Tokens.aspx for verifying JWT tokens. """ class AzureADTenantOAuth2(AzureADOAuth2): name = "azuread-tenant-oauth2" OPENID_CONFIGURATION_URL = "{base_url}/.well-known/openid-configuration{appid}" JWKS_URL = "{base_url}/discovery/keys{appid}" @property def tenant_id(self): return self.setting("TENANT_ID", "common") def openid_configuration_url(self): return self.OPENID_CONFIGURATION_URL.format( base_url=self.base_url, appid=self._appid() ) def jwks_url(self): return self.JWKS_URL.format(base_url=self.base_url, appid=self._appid()) def _appid(self): return ( f"?appid={self.setting('KEY')}" if self.setting("KEY") is not None else "" ) def get_certificate(self, kid): # retrieve keys from jwks_url resp = self.request(self.jwks_url(), method="GET") resp.raise_for_status() # find the proper key for the kid for key in resp.json()["keys"]: if key["kid"] == kid: x5c = key["x5c"][0] break else: raise DecodeError(f"Cannot find kid={kid}") return load_der_x509_certificate(base64.b64decode(x5c), default_backend()) def get_user_id(self, details, response): """Use subject (sub) claim as unique id.""" return response.get("sub") def user_data(self, access_token, *args, **kwargs): response = kwargs.get("response") if response and response.get("id_token"): id_token = response.get("id_token") else: id_token = access_token # get key id and algorithm key_id = get_unverified_header(id_token)["kid"] try: # retrieve certificate for key_id certificate = self.get_certificate(key_id) return jwt_decode( id_token, key=certificate.public_key(), # type: ignore[reportArgumentType] algorithms=["RS256"], audience=self.setting("KEY"), ) except (DecodeError, ExpiredSignatureError) as error: raise AuthTokenError(self, error) class AzureADV2TenantOAuth2(AzureADTenantOAuth2): name = "azuread-v2-tenant-oauth2" OPENID_CONFIGURATION_URL = "{base_url}/v2.0/.well-known/openid-configuration{appid}" AUTHORIZATION_URL = "{base_url}/oauth2/v2.0/authorize" ACCESS_TOKEN_URL = "{base_url}/oauth2/v2.0/token" JWKS_URL = "{base_url}/discovery/v2.0/keys{appid}" DEFAULT_SCOPE = ["openid", "profile", "offline_access"] def get_user_id(self, details, response): """Use upn as unique id""" return response.get("preferred_username") def get_user_details(self, response): """Return user details from Azure AD account""" fullname, first_name, last_name = ( response.get("name", ""), response.get("given_name", ""), response.get("family_name", ""), ) return { "username": fullname, "email": response.get("preferred_username"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } social-auth-core-4.6.1/social_core/backends/base.py000066400000000000000000000257771500362547200222270ustar00rootroot00000000000000from __future__ import annotations import time from typing import TYPE_CHECKING, Any, Literal, cast import requests from requests import Response from ..exceptions import AuthConnectionError, AuthUnknownError from ..utils import module_member, parse_qs, user_agent if TYPE_CHECKING: from collections.abc import Mapping from requests.auth import AuthBase class BaseAuth: """A authentication backend that authenticates the user based on the provider response""" name = "" # provider name, it's stored in database supports_inactive_user = False # Django auth ID_KEY: str = "" EXTRA_DATA: list[str | tuple[str, str] | tuple[str, str, bool]] | None = None GET_ALL_EXTRA_DATA = False REQUIRES_EMAIL_VALIDATION = False SEND_USER_AGENT = False def __init__(self, strategy, redirect_uri=None): self.strategy = strategy self.redirect_uri = redirect_uri self.data = self.strategy.request_data() self.redirect_uri = self.strategy.absolute_uri(self.redirect_uri) def setting(self, name, default=None): """Return setting value from strategy""" return self.strategy.setting(name, default=default, backend=self) def start(self): if self.uses_redirect(): return self.strategy.redirect(self.auth_url()) return self.strategy.html(self.auth_html()) def complete(self, *args, **kwargs): return self.auth_complete(*args, **kwargs) def auth_url(self): """Must return redirect URL to auth provider""" raise NotImplementedError("Implement in subclass") def auth_html(self): """Must return login HTML content returned by provider""" raise NotImplementedError("Implement in subclass") def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" raise NotImplementedError("Implement in subclass") def process_error(self, data): """Process data for errors, raise exception if needed. Call this method on any override of auth_complete.""" def authenticate(self, *args, **kwargs): """Authenticate user using social credentials Authentication is made if this is the correct backend, backend verification is made by kwargs inspection for current backend name presence. """ # Validate backend and arguments. Require that the Social Auth # response be passed in as a keyword argument, to make sure we # don't match the username/password calling conventions of # authenticate. if ( "backend" not in kwargs or kwargs["backend"].name != self.name or "strategy" not in kwargs or "response" not in kwargs ): return None self.strategy = kwargs.get("strategy") or self.strategy self.redirect_uri = kwargs.get("redirect_uri") or self.redirect_uri self.data = self.strategy.request_data() kwargs.setdefault("is_new", False) pipeline = self.strategy.get_pipeline(self) args, kwargs = self.strategy.clean_authenticate_args(*args, **kwargs) return self.pipeline(pipeline, *args, **kwargs) def pipeline(self, pipeline, pipeline_index=0, *args, **kwargs): out = self.run_pipeline(pipeline, pipeline_index, *args, **kwargs) if not isinstance(out, dict): return out user = out.get("user") if user: user.social_user = out.get("social") user.is_new = out.get("is_new") return user def disconnect(self, *args, **kwargs): pipeline = self.strategy.get_disconnect_pipeline(self) kwargs["name"] = self.name kwargs["user_storage"] = self.strategy.storage.user return self.run_pipeline(pipeline, *args, **kwargs) def run_pipeline(self, pipeline, pipeline_index=0, *args, **kwargs): out = kwargs.copy() out.setdefault("strategy", self.strategy) out.setdefault("backend", out.pop(self.name, None) or self) out.setdefault("request", self.strategy.request_data()) out.setdefault("details", {}) if ( not isinstance(pipeline_index, int) or pipeline_index < 0 or pipeline_index >= len(pipeline) ): pipeline_index = 0 for idx, name in enumerate(pipeline[pipeline_index:]): out["pipeline_index"] = pipeline_index + idx func = module_member(name) result = func(*args, **out) or {} if not isinstance(result, dict): return result out.update(result) return out def extra_data( self, user, uid: str, response: dict[str, Any], details: dict[str, Any], *args, **kwargs, ) -> dict[str, Any]: """Return default extra data to store in extra_data field""" data: dict[str, Any] = { # store the last time authentication took place "auth_time": int(time.time()) } extra_data_entries: list[str | tuple[str, str] | tuple[str, str, bool]] = [] if self.GET_ALL_EXTRA_DATA or self.setting("GET_ALL_EXTRA_DATA", False): extra_data_entries = list(response.keys()) else: extra_data_entries = (self.EXTRA_DATA or []) + cast( "list[str | tuple[str, str] | tuple[str, str, bool]]", self.setting("EXTRA_DATA", []), ) for entry in extra_data_entries: if isinstance(entry, list): entry = tuple(cast("list[str]", entry)) discard = False if isinstance(entry, str): name = alias = entry elif len(entry) == 3: name, alias, discard = entry elif len(entry) == 2: name, alias = entry elif len(entry) == 1: name = alias = entry[0] else: raise AuthUnknownError(self, f"Invalid EXTRA_DATA item: {entry!r}") value = response.get(name, details.get(name, details.get(alias))) if discard and not value: continue data[alias] = value return data def auth_allowed(self, response, details): """Return True if the user should be allowed to authenticate, by default check if email is whitelisted (if there's a whitelist)""" emails = [email.lower() for email in self.setting("WHITELISTED_EMAILS", [])] domains = [domain.lower() for domain in self.setting("WHITELISTED_DOMAINS", [])] email = details.get("email") allowed = True if email and (emails or domains): email = email.lower() domain = email.split("@", 1)[1] allowed = email in emails or domain in domains return allowed def get_user_id(self, details, response): """Return a unique ID for the current user, by default from server response.""" return response.get(self.ID_KEY) def get_user_details(self, response): """Must return user details in a know internal struct: {'username': , 'email': , 'fullname': , 'first_name': , 'last_name': } """ raise NotImplementedError("Implement in subclass") def get_user_names(self, fullname="", first_name="", last_name=""): # Avoid None values fullname = fullname or "" first_name = first_name or "" last_name = last_name or "" if fullname and not (first_name or last_name): try: first_name, last_name = fullname.split(" ", 1) except ValueError: first_name = first_name or fullname or "" last_name = last_name or "" fullname = fullname or f"{first_name} {last_name}" return fullname.strip(), first_name.strip(), last_name.strip() def get_user(self, user_id): """ Return user with given ID from the User model used by this backend. This is called by django.contrib.auth.middleware. """ return self.strategy.get_user(user_id) def continue_pipeline(self, partial): """Continue previous halted pipeline""" return self.strategy.authenticate( self, pipeline_index=partial.next_step, *partial.args, **partial.kwargs ) def auth_extra_arguments(self): """Return extra arguments needed on auth process. The defaults can be overridden by GET parameters.""" extra_arguments = self.setting("AUTH_EXTRA_ARGUMENTS", {}).copy() extra_arguments.update( (key, self.data[key]) for key in extra_arguments if key in self.data ) return extra_arguments def uses_redirect(self) -> bool: """Return True if this provider uses redirect url method, otherwise return false.""" return True def request( self, url: str, *, method: Literal["GET", "POST", "DELETE"] = "GET", headers: Mapping[str, str | bytes] | None = None, data: dict | bytes | str | None = None, auth: tuple[str, str] | AuthBase | None = None, params: dict | None = None, ) -> Response: headers = {} if headers is None else dict(headers) proxies = self.setting("PROXIES") verify = self.setting("VERIFY_SSL", True) # if timeout is None: timeout = self.setting("REQUESTS_TIMEOUT") or self.setting("URLOPEN_TIMEOUT") if self.SEND_USER_AGENT and "User-Agent" not in headers: headers["User-Agent"] = self.setting("USER_AGENT") or user_agent() try: response = requests.request( method, url, headers=headers, data=data, auth=auth, params=params, timeout=timeout, proxies=proxies, verify=verify, ) except requests.ConnectionError as err: raise AuthConnectionError(self, str(err)) from err response.raise_for_status() return response def get_json( self, url: str, method: Literal["GET", "POST", "DELETE"] = "GET", headers: Mapping[str, str | bytes] | None = None, data: dict | bytes | str | None = None, auth: tuple[str, str] | AuthBase | None = None, params: dict | None = None, ) -> dict[Any, Any]: return self.request( url, method=method, headers=headers, data=data, auth=auth, params=params ).json() def get_querystring(self, url, *args, **kwargs) -> dict[str, str]: return parse_qs(self.request(url, *args, **kwargs).text) def get_key_and_secret(self) -> tuple[str, str]: """Return tuple with Consumer Key and Consumer Secret for current service provider. Must return (key, secret), order *must* be respected. """ return self.setting("KEY"), self.setting("SECRET") social-auth-core-4.6.1/social_core/backends/battlenet.py000066400000000000000000000033331500362547200232570ustar00rootroot00000000000000from .oauth import BaseOAuth2 # This provides a backend for python-social-auth. This should not be confused # with officially battle.net offerings. This piece of code is not officially # affiliated with Blizzard Entertainment, copyrights to their respective # owners. See http://us.battle.net/en/forum/topic/13979588015 for more details. class BattleNetOAuth2(BaseOAuth2): """battle.net Oauth2 backend""" name = "battlenet-oauth2" ID_KEY = "accountId" REDIRECT_STATE = False AUTHORIZATION_URL = "https://eu.battle.net/oauth/authorize" ACCESS_TOKEN_URL = "https://eu.battle.net/oauth/token" REVOKE_TOKEN_METHOD = "GET" DEFAULT_SCOPE = ["wow.profile"] EXTRA_DATA = [ ("refresh_token", "refresh_token", True), ("expires_in", "expires"), ("token_type", "token_type", True), ] def get_characters(self, access_token): """ Fetches the character list from the battle.net API. Returns list of characters or empty list if the request fails. """ params = {"access_token": access_token} if self.setting("API_LOCALE"): params["locale"] = self.setting("API_LOCALE") response = self.get_json( "https://eu.api.battle.net/wow/user/characters", params=params ) return response.get("characters") or [] def get_user_details(self, response): """Return user details from Battle.net account""" return {"battletag": response.get("battletag")} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://eu.api.battle.net/account/user", params={"access_token": access_token}, ) social-auth-core-4.6.1/social_core/backends/beats.py000066400000000000000000000045331500362547200223760ustar00rootroot00000000000000""" Beats backend, docs at: https://developer.beatsmusic.com/docs """ import base64 from social_core.exceptions import AuthUnknownError from ..utils import handle_http_errors from .oauth import BaseOAuth2 class BeatsOAuth2(BaseOAuth2): name = "beats" SCOPE_SEPARATOR = " " ID_KEY = "user_context" AUTHORIZATION_URL = "https://partner.api.beatsmusic.com/v1/oauth2/authorize" ACCESS_TOKEN_URL = "https://partner.api.beatsmusic.com/oauth2/token" REDIRECT_STATE = False def get_user_id(self, details, response): return response["result"][BeatsOAuth2.ID_KEY] def auth_headers(self): return { "Authorization": "Basic {}".format( base64.urlsafe_b64encode( "{}:{}".format(*self.get_key_and_secret()).encode() ) ) } @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" self.process_error(self.data) response = self.request_access_token( self.ACCESS_TOKEN_URL, data=self.auth_complete_params(self.validate_state()), headers=self.auth_headers(), method=self.ACCESS_TOKEN_METHOD, ) self.process_error(response) # mashery wraps in jsonrpc if response.get("jsonrpc", None): response = response.get("result", None) if response is None: raise AuthUnknownError(self, "Invalid authentication response") return self.do_auth( response["access_token"], response=response, *args, **kwargs ) def get_user_details(self, response): """Return user details from Beats account""" response = response["result"] fullname, first_name, last_name = self.get_user_names( response.get("display_name") ) return { "username": response.get("id"), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://partner.api.beatsmusic.com/v1/api/me", headers={"Authorization": f"Bearer {access_token}"}, ) social-auth-core-4.6.1/social_core/backends/behance.py000066400000000000000000000027201500362547200226610ustar00rootroot00000000000000""" Behance OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/behance.html """ from .oauth import BaseOAuth2 class BehanceOAuth2(BaseOAuth2): """Behance OAuth authentication backend""" name = "behance" AUTHORIZATION_URL = "https://www.behance.net/v2/oauth/authenticate" ACCESS_TOKEN_URL = "https://www.behance.net/v2/oauth/token" SCOPE_SEPARATOR = "|" EXTRA_DATA = [("username", "username")] REDIRECT_STATE = False def get_user_id(self, details, response): return response["user"]["id"] def get_user_details(self, response): """Return user details from Behance account""" user = response["user"] fullname, first_name, last_name = self.get_user_names( user["display_name"], user["first_name"], user["last_name"] ) return { "username": user["username"], "fullname": fullname, "first_name": first_name, "last_name": last_name, "email": "", } def extra_data(self, user, uid, response, details=None, *args, **kwargs): # Pull up the embedded user attributes so they can be found as extra # data. See the example token response for possible attributes: # http://www.behance.net/dev/authentication#step-by-step data = response.copy() data.update(response["user"]) return super().extra_data(user, uid, data, details, *args, **kwargs) social-auth-core-4.6.1/social_core/backends/belgiumeid.py000066400000000000000000000005301500362547200233770ustar00rootroot00000000000000""" Belgium EID OpenId backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/belgium_eid.html """ from .open_id import OpenIdAuth class BelgiumEIDOpenId(OpenIdAuth): """Belgium e-ID OpenID authentication backend""" name = "belgiumeid" URL = "https://www.e-contract.be/eid-idp/endpoints/openid/auth" social-auth-core-4.6.1/social_core/backends/bitbucket.py000066400000000000000000000045001500362547200232460ustar00rootroot00000000000000""" Bitbucket OAuth2 and OAuth1 backends, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/bitbucket.html """ from __future__ import annotations from ..exceptions import AuthForbidden from .oauth import BaseOAuth2 class BitbucketOAuth2(BaseOAuth2): name = "bitbucket-oauth2" SCOPE_SEPARATOR = " " AUTHORIZATION_URL = "https://bitbucket.org/site/oauth2/authorize" ACCESS_TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token" REDIRECT_STATE = False EXTRA_DATA = [ ("scopes", "scopes"), ("expires_in", "expires"), ("token_type", "token_type"), ("refresh_token", "refresh_token"), ] ID_KEY = "uuid" def get_user_id(self, details, response): id_key = self.ID_KEY if self.setting("USERNAME_AS_ID", False): id_key = "username" return response.get(id_key) def get_user_details(self, response): """Return user details from Bitbucket account""" fullname, first_name, last_name = self.get_user_names(response["display_name"]) return { "username": response.get("username", ""), "email": response.get("email", ""), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Return user data provided""" emails = self._get_emails(access_token) email = None for address in reversed(emails["values"]): email = address["email"] if address["is_primary"]: break if self.setting("VERIFIED_EMAILS_ONLY", False) and not address["is_confirmed"]: raise AuthForbidden(self, "Bitbucket account has no verified email") user = self._get_user(access_token) if email: user["email"] = email return user def auth_complete_credentials(self): return self.get_key_and_secret() def _get_user(self, access_token=None): return self.get_json( "https://api.bitbucket.org/2.0/user", params={"access_token": access_token} ) def _get_emails(self, access_token=None): return self.get_json( "https://api.bitbucket.org/2.0/user/emails", params={"access_token": access_token}, ) social-auth-core-4.6.1/social_core/backends/bitbucket_datacenter.py000066400000000000000000000102701500362547200254410ustar00rootroot00000000000000""" Bitbucket Data Center OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/bitbucket_datacenter_oauth2.html https://confluence.atlassian.com/bitbucketserver/bitbucket-oauth-2-0-provider-api-1108483661.html """ from .oauth import BaseOAuth2PKCE class BitbucketDataCenterOAuth2(BaseOAuth2PKCE): """ Implements client for Bitbucket Data Center OAuth 2.0 provider API. ref: https://confluence.atlassian.com/bitbucketserver/bitbucket-oauth-2-0-provider-api-1108483661.html """ name = "bitbucket-datacenter-oauth2" ID_KEY = "id" SCOPE_SEPARATOR = " " REFRESH_TOKEN_METHOD = "POST" REDIRECT_STATE = False STATE_PARAMETER = True # ref: https://confluence.atlassian.com/bitbucketserver/bitbucket-oauth-2-0-provider-api-1108483661.html#BitbucketOAuth2.0providerAPI-scopes DEFAULT_SCOPE = ["PUBLIC_REPOS"] USE_BASIC_AUTH = False EXTRA_DATA = [ ("token_type", "token_type"), ("access_token", "access_token"), ("refresh_token", "refresh_token"), ("expires_in", "expires"), ("scope", "scope"), # extra user profile fields ("first_name", "first_name"), ("last_name", "last_name"), ("email", "email"), ("name", "name"), ("username", "username"), ("display_name", "display_name"), ("type", "type"), ("active", "active"), ("url", "url"), ("avatar_url", "avatar_url"), ] PKCE_DEFAULT_CODE_CHALLENGE_METHOD = "s256" # can be "plain" or "s256" PKCE_DEFAULT_CODE_VERIFIER_LENGTH = 48 # must be b/w 43-127 chars DEFAULT_USE_PKCE = True DEFAULT_USER_AVATAR_SIZE = 48 @property def server_base_oauth2_api_url(self) -> str: base_url = self.setting("URL") return f"{base_url}/rest/oauth2/latest" @property def server_base_rest_api_url(self) -> str: base_url = self.setting("URL") return f"{base_url}/rest/api/latest" def authorization_url(self) -> str: return f"{self.server_base_oauth2_api_url}/authorize" def access_token_url(self) -> str: return f"{self.server_base_oauth2_api_url}/token" def get_user_details(self, response) -> dict: """Return user details for the Bitbucket Data Center account""" # `response` here is the return value of `user_data` method user_data = response _, first_name, last_name = self.get_user_names(user_data["displayName"]) uid = self.get_user_id(details=None, response=response) return { "uid": uid, "first_name": first_name, "last_name": last_name, "email": user_data["emailAddress"], "name": user_data["name"], "username": user_data["slug"], "display_name": user_data["displayName"], "type": user_data["type"], "active": user_data["active"], "url": user_data["links"]["self"][0]["href"], "avatar_url": user_data["avatarUrl"], } def user_data(self, access_token, *args, **kwargs) -> dict: """Fetch user data from Bitbucket Data Center REST API""" # At this point, we don't know the current user's username # and Bitbucket doesn't provide any API to do so. # However, the current user's username is sent in every response header. # ref: https://community.developer.atlassian.com/t/obtain-authorised-users-username-from-api/24422/2 headers = {"Authorization": f"Bearer {access_token}"} response = self.request( url=f"{self.server_base_rest_api_url}/application-properties", method="GET", headers=headers, ) # ref: https://developer.atlassian.com/server/bitbucket/rest/v815/api-group-system-maintenance/#api-api-latest-users-userslug-get username = response.headers["x-ausername"] return self.get_json( url=f"{self.server_base_rest_api_url}/users/{username}", headers=headers, params={ "avatarSize": self.setting( "USER_AVATAR_SIZE", default=self.DEFAULT_USER_AVATAR_SIZE ) # to force `avatarUrl` in response }, ) social-auth-core-4.6.1/social_core/backends/box.py000066400000000000000000000041411500362547200220630ustar00rootroot00000000000000""" Box.net OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/box.html """ from .oauth import BaseOAuth2 class BoxOAuth2(BaseOAuth2): """Box.net OAuth authentication backend""" name = "box" AUTHORIZATION_URL = "https://www.box.com/api/oauth2/authorize" ACCESS_TOKEN_URL = "https://www.box.com/api/oauth2/token" REVOKE_TOKEN_URL = "https://www.box.com/api/oauth2/revoke" SCOPE_SEPARATOR = "," REDIRECT_STATE = False EXTRA_DATA = [ ("refresh_token", "refresh_token", True), ("id", "id"), ("expires", "expires"), ] def do_auth(self, access_token, response=None, *args, **kwargs): response = response or {} data = self.user_data(access_token) data["access_token"] = response.get("access_token") data["refresh_token"] = response.get("refresh_token") data["expires"] = response.get("expires_in") kwargs.update({"backend": self, "response": data}) return self.strategy.authenticate(*args, **kwargs) def get_user_details(self, response): """Return user details Box.net account""" fullname, first_name, last_name = self.get_user_names(response.get("name")) return { "username": response.get("login"), "email": response.get("login") or "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" params = self.setting("PROFILE_EXTRA_PARAMS", {}) params["access_token"] = access_token return self.get_json("https://api.box.com/2.0/users/me", params=params) def refresh_token(self, token, *args, **kwargs): params = self.refresh_token_params(token, *args, **kwargs) request = self.request( self.REFRESH_TOKEN_URL or self.ACCESS_TOKEN_URL, data=params, headers=self.auth_headers(), method="POST", ) return self.process_refresh_token_response(request, *args, **kwargs) social-auth-core-4.6.1/social_core/backends/bungie.py000066400000000000000000000061421500362547200225470ustar00rootroot00000000000000""" Bungie OAuth2 backend """ from social_core.backends.oauth import BaseOAuth2 class BungieOAuth2(BaseOAuth2): name = "bungie" ID_KEY = "membership_id" AUTHORIZATION_URL = "https://www.bungie.net/en/oauth/authorize/" ACCESS_TOKEN_URL = "https://www.bungie.net/platform/app/oauth/token/" REFRESH_TOKEN_URL = "https://www.bungie.net/platform/app/oauth/token/" REDIRECT_STATE = False EXTRA_DATA = [ ("refresh_token", "refresh_token", True), ("access_token", "access_token", True), ("expires_in", "expires"), ("membership_id", "membership_id"), ("refresh_expires_in", "refresh_expires_in"), ] def auth_html(self): """Abstract Method Inclusion""" def auth_headers(self): """Adds X-API-KEY and Origin""" return { "X-API-KEY": self.setting("API_KEY"), "Content-Type": "application/x-www-form-urlencoded", "Origin": self.setting("ORIGIN"), "Accept": "application/json", } def make_bungie_request(self, url, access_token, kwargs): """Helper function to get username data keyed off displayName""" headers = self.auth_headers() headers["Authorization"] = "Bearer " + access_token return self.get_json(url, headers=headers) def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" self.process_error(self.data) state = self.validate_state() response = self.request_access_token( self.access_token_url(), data=self.auth_complete_params(state), headers=self.auth_headers(), auth=self.auth_complete_credentials(), method=self.ACCESS_TOKEN_METHOD, ) self.process_error(response) return self.do_auth( response["access_token"], response=response, *args, **kwargs ) def do_auth(self, access_token, *args, **kwargs): """Finish the auth process once the access_token was retrieved""" data = self.user_data(access_token, *args, **kwargs) response = kwargs.get("response") or {} response.update(data or {}) if "access_token" not in response: response["Response"]["access_token"]["value"] = access_token kwargs.update({"response": response, "backend": self}) return self.strategy.authenticate(*args, **kwargs) def user_data(self, access_token, *args, **kwargs): """Grab user profile information from Bunige""" membership_id = kwargs["response"]["membership_id"] url = "https://www.bungie.net/Platform/User/GetBungieNetUser/" response = self.make_bungie_request(url, access_token, kwargs) username = response["Response"]["user"]["displayName"] return {"username": username, "uid": membership_id} def get_user_details(self, response, *args, **kwargs): """Return user details from Bungie account""" username = response["username"] return { "first_name": username, "username": username, "uid": response["uid"], } social-auth-core-4.6.1/social_core/backends/cas.py000066400000000000000000000041601500362547200220420ustar00rootroot00000000000000""" CAS OIDC backend https://apereo.github.io/cas/6.6.x/authentication/OIDC-Authentication.html Backend for authenticating with Apereo CAS using OIDC. This backend handles the minor implementation differences between the Apereo CAS OIDC server implementation and the standard OIDC implementation in Python Social Auth. """ import logging from .open_id_connect import OpenIdConnectAuth logger = logging.getLogger("social") class CASOpenIdConnectAuth(OpenIdConnectAuth): """ Open ID Connect backends for use with Apereo CAS. Currently only the code response type is supported. It can also be directly instantiated as a generic OIDC backend. To use it you will need to set at minimum: SOCIAL_AUTH_CAS_OIDC_ENDPOINT = 'https://.....' # endpoint without /.well-known/openid-configuration SOCIAL_AUTH_CAS_KEY = '' SOCIAL_AUTH_CAS_SECRET = '' """ name = "cas" STATE_PARAMETER = True def oidc_endpoint(self): endpoint = self.setting("OIDC_ENDPOINT", self.OIDC_ENDPOINT) logger.debug("backend: CAS, endpoint: %s", endpoint) return endpoint def get_user_id(self, details, response): logger.debug( "backend: CAS, method: get_user_id, details: %s, %s", details, response ) return details.get("username") def user_data(self, access_token, *args, **kwargs): data = self.get_json( self.userinfo_url(), headers={"Authorization": f"Bearer {access_token}"} ) logger.debug("backend: CAS, user_data: %s", data) return data.get("attributes", {}) def get_user_details(self, response): username_key = self.setting("USERNAME_KEY", self.USERNAME_KEY) logger.debug("backend: CAS, username_key: %s", username_key) attributes = self.user_data(response.get("access_token")) return { "username": attributes.get(username_key), "email": attributes.get("email"), "fullname": attributes.get("name"), "first_name": attributes.get("given_name"), "last_name": attributes.get("family_name"), } social-auth-core-4.6.1/social_core/backends/changetip.py000066400000000000000000000015041500362547200232350ustar00rootroot00000000000000from .oauth import BaseOAuth2 class ChangeTipOAuth2(BaseOAuth2): """ChangeTip OAuth authentication backend https://www.changetip.com/api """ name = "changetip" AUTHORIZATION_URL = "https://www.changetip.com/o/authorize/" ACCESS_TOKEN_URL = "https://www.changetip.com/o/token/" SCOPE_SEPARATOR = " " def get_user_details(self, response): """Return user details from ChangeTip account""" return { "username": response["username"], "email": response.get("email", ""), "first_name": "", "last_name": "", } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://api.changetip.com/v2/me/", params={"access_token": access_token} ) social-auth-core-4.6.1/social_core/backends/chatwork.py000066400000000000000000000040201500362547200231110ustar00rootroot00000000000000""" Chatwork OAuth2 backend """ import base64 from .oauth import BaseOAuth2 class ChatworkOAuth2(BaseOAuth2): """Chatwork OAuth authentication backend""" name = "chatwork" API_URL = "https://api.chatwork.com/v2" AUTHORIZATION_URL = "https://www.chatwork.com/packages/oauth2/login.php" ACCESS_TOKEN_URL = "https://oauth.chatwork.com/token" REDIRECT_STATE = True DEFAULT_SCOPE = ["users.profile.me:read"] ID_KEY = "account_id" EXTRA_DATA = [("expires_in", "expires"), ("refresh_token", "refresh_token")] def api_url(self, path): api_url = self.setting("API_URL") or self.API_URL return "{}{}".format(api_url.rstrip("/"), path) def auth_headers(self): return { "Authorization": b"Basic " + base64.b64encode("{}:{}".format(*self.get_key_and_secret()).encode()) } def auth_complete_params(self, state=None): return { "grant_type": "authorization_code", "code": self.data.get("code", ""), "redirect_uri": self.get_redirect_uri(state), } def get_user_details(self, response): """Return user details from Chatwork account""" fullname, first_name, last_name = self.get_user_names(response.get("name")) username = ( response.get("chatwork_id") or response.get("login_mail") or response.get("account_id") ) email = response.get("mail") or response.get("login_mail") or "" return { "username": username, "email": email, "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" headers = {"Authorization": "Bearer " + access_token} return self.get_json(self.api_url("/me"), headers=headers) def refresh_token_params(self, token, *args, **kwargs): return {"refresh_token": token, "grant_type": "refresh_token"} social-auth-core-4.6.1/social_core/backends/cilogon.py000066400000000000000000000027251500362547200227330ustar00rootroot00000000000000from .oauth import BaseOAuth2 class CILogonOAuth2(BaseOAuth2): """ CI Logon Authentication Backend Docs: https://www.cilogon.org/oidc """ name = "cilogon-oauth2" AUTHORIZATION_URL = "https://cilogon.org/authorize" ACCESS_TOKEN_URL = "https://cilogon.org/oauth2/token" DEFAULT_SCOPE = ["openid", "email", "profile", "org.cilogon.userinfo"] REDIRECT_STATE = False SCOPE_SEPARATOR = "+" def user_data(self, token, *args, **kwargs): """Loads user data from endpoint""" url = "https://cilogon.org/oauth2/userinfo" data = {"access_token": token} try: return self.get_json(url, method="POST", data=data) except ValueError: return None def get_user_id(self, details, response): """Return user unique id provided by service In this case it is a combination of the `sub` and `iss` respective values.""" return response.get("sub", "") + " " + response.get("iss", "") def get_user_details(self, response): """Return user details from CI Logon service""" fullname, first_name, last_name = self.get_user_names( first_name=response.get("given_name"), last_name=response.get("family_name") ) return { "username": response.get("email"), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } social-auth-core-4.6.1/social_core/backends/classlink.py000066400000000000000000000026221500362547200232600ustar00rootroot00000000000000from .oauth import BaseOAuth2 class ClasslinkOAuth(BaseOAuth2): """ Classlink OAuth authentication backend. Docs: https://developer.classlink.com/docs/oauth2-workflow """ name = "classlink" AUTHORIZATION_URL = "https://launchpad.classlink.com/oauth2/v2/auth" ACCESS_TOKEN_URL = "https://launchpad.classlink.com/oauth2/v2/token" DEFAULT_SCOPE = ["profile"] REDIRECT_STATE = False SCOPE_SEPARATOR = " " def get_user_id(self, details, response): """Return user unique id provided by service""" return response["UserId"] def get_user_details(self, response): """Return user details from Classlink account""" fullname, first_name, last_name = self.get_user_names( first_name=response.get("FirstName"), last_name=response.get("LastName") ) return { "username": response.get("Email") or response.get("LoginId"), "email": response.get("Email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, token, *args, **kwargs): """Loads user data from service""" url = "https://nodeapi.classlink.com/v2/my/info" auth_header = {"Authorization": f"Bearer {token}"} try: return self.get_json(url, headers=auth_header) except ValueError: return None social-auth-core-4.6.1/social_core/backends/clef.py000066400000000000000000000032341500362547200222060ustar00rootroot00000000000000""" Clef OAuth support. This contribution adds support for Clef OAuth service. The settings SOCIAL_AUTH_CLEF_KEY and SOCIAL_AUTH_CLEF_SECRET must be defined with the values given by Clef application registration process. """ from .oauth import BaseOAuth2 class ClefOAuth2(BaseOAuth2): """Clef OAuth authentication backend""" name = "clef" AUTHORIZATION_URL = "https://clef.io/iframes/qr" ACCESS_TOKEN_URL = "https://clef.io/api/v1/authorize" SCOPE_SEPARATOR = "," def auth_params(self, *args, **kwargs): params = super().auth_params(*args, **kwargs) params["app_id"] = params.pop("client_id") params["redirect_url"] = params.pop("redirect_uri") return params def get_user_id(self, response, details): # type: ignore[reportIncompatibleMethodOverride] return details.get("info").get("id") def get_user_details(self, response): """Return user details from Github account""" info = response.get("info") fullname, first_name, last_name = self.get_user_names( first_name=info.get("first_name"), last_name=info.get("last_name") ) email = info.get("email", "") username = email.split("@", 1)[0] if email else info.get("id") return { "username": username, "email": email, "fullname": fullname, "first_name": first_name, "last_name": last_name, "phone_number": info.get("phone_number", ""), } def user_data(self, access_token, *args, **kwargs): return self.get_json( "https://clef.io/api/v1/info", params={"access_token": access_token} ) social-auth-core-4.6.1/social_core/backends/clever.py000066400000000000000000000041701500362547200225550ustar00rootroot00000000000000from .oauth import BaseOAuth2 class CleverOAuth2(BaseOAuth2): """ Clever OAuth authentication backend. Docs: https://dev.clever.com/docs/classroom-with-oauth """ name = "clever" AUTHORIZATION_URL = "https://clever.com/oauth/authorize" ACCESS_TOKEN_URL = "https://clever.com/oauth/tokens" REDIRECT_STATE = False STATE_PARAMETER = False SCOPE_SEPARATOR = " " def get_user_id(self, details, response): """Return user unique id provided by service""" return response.get("data", {}).get("id") def get_user_type(self, data): return next(iter(data.get("data", {}).get("roles", {}).keys())) def get_user_details(self, response): """Return user details from Classlink account""" fullname, first_name, last_name = self.get_user_names( first_name=response.get("data", {}).get("name", {}).get("first", None), last_name=response.get("data", {}).get("name", {}).get("last", None), ) email = response.get("data", {}).get("email") username = ( response.get("data", {}) .get("roles", {}) .get(self.get_user_type(response), {}) .get("credentials", {}) .get("district_username", email.split("@", 1)[0]) ) return { "username": username, "email": email, "fullname": fullname, "first_name": first_name, "last_name": last_name, "user_type": self.get_user_type(response), } def user_data(self, token, *args, **kwargs): """Loads user data from service""" identity_url = "https://api.clever.com/v3.0/me" user_details_url = "https://api.clever.com/v3.0/users" auth_header = {"Authorization": f"Bearer {token}"} try: response = self.get_json(identity_url, headers=auth_header) user_id = response.get("data", {}).get("id") user_details_url = f"https://api.clever.com/v3.0/users/{user_id}" return self.get_json(user_details_url, headers=auth_header) except ValueError: return None social-auth-core-4.6.1/social_core/backends/coding.py000066400000000000000000000025541500362547200225440ustar00rootroot00000000000000""" Coding OAuth2 backend, docs at: """ from urllib.parse import urljoin from .oauth import BaseOAuth2 class CodingOAuth2(BaseOAuth2): """Coding OAuth authentication backend""" name = "coding" API_URL = "https://coding.net/api/" AUTHORIZATION_URL = "https://coding.net/oauth_authorize.html" ACCESS_TOKEN_URL = "https://coding.net/api/oauth/access_token" SCOPE_SEPARATOR = "," DEFAULT_SCOPE = ["user"] REDIRECT_STATE = False def api_url(self): return self.API_URL def get_user_details(self, response): """Return user details from Github account""" fullname, first_name, last_name = self.get_user_names(response.get("name")) return { "username": response.get("name"), "email": response.get("email") or "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" data = self._user_data(access_token) if data.get("code") != 0: # 获取失败 pass return data.get("data") def _user_data(self, access_token, path=None): url = urljoin(self.api_url(), "account/current_user{}".format(path or "")) return self.get_json(url, params={"access_token": access_token}) social-auth-core-4.6.1/social_core/backends/cognito.py000066400000000000000000000031661500362547200227430ustar00rootroot00000000000000from social_core.backends.oauth import BaseOAuth2 class CognitoOAuth2(BaseOAuth2): name = "cognito" ID_KEY = "username" DEFAULT_SCOPE = ["openid", "profile", "email"] REDIRECT_STATE = False def user_pool_domain(self): return self.setting("POOL_DOMAIN") def authorization_url(self): return f"{self.user_pool_domain()}/login" def access_token_url(self): return f"{self.user_pool_domain()}/oauth2/token" def user_data_url(self): return f"{self.user_pool_domain()}/oauth2/userInfo" def get_user_details(self, response): """Return user details from their cognito pool account""" first_name = response.get("given_name") or "" last_name = response.get("family_name") or "" fullname, first_name, last_name = self.get_user_names( first_name=first_name, last_name=last_name, ) return { "username": response.get("username") or response.get("email"), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Grab user profile information from cognito.""" response = self.get_json( url=self.user_data_url(), headers={"Authorization": f"Bearer {access_token}"}, ) return { "given_name": response.get("given_name"), "family_name": response.get("family_name"), "username": response.get("username"), "email": response.get("email"), } social-auth-core-4.6.1/social_core/backends/coinbase.py000066400000000000000000000025131500362547200230570ustar00rootroot00000000000000""" Coinbase OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/coinbase.html """ from .oauth import BaseOAuth2 class CoinbaseOAuth2(BaseOAuth2): name = "coinbase" SCOPE_SEPARATOR = "+" DEFAULT_SCOPE = ["user", "balance"] AUTHORIZATION_URL = "https://www.coinbase.com/oauth/authorize" ACCESS_TOKEN_URL = "https://api.coinbase.com/oauth/token" REVOKE_TOKEN_URL = "https://api.coinbase.com/oauth/revoke" REDIRECT_STATE = False def get_user_id(self, details, response): return response["data"]["id"] def get_user_details(self, response): """Return user details from Coinbase account""" user_data = response["data"] email = user_data.get("email", "") name = user_data["name"] username = user_data.get("username") fullname, first_name, last_name = self.get_user_names(name) return { "username": username, "fullname": fullname, "first_name": first_name, "last_name": last_name, "email": email, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://api.coinbase.com/v2/user", headers={"Authorization": "Bearer " + access_token}, ) social-auth-core-4.6.1/social_core/backends/coursera.py000066400000000000000000000026151500362547200231220ustar00rootroot00000000000000""" Coursera OAuth2 backend, docs at: https://tech.coursera.org/app-platform/oauth2/ """ from .oauth import BaseOAuth2 class CourseraOAuth2(BaseOAuth2): """Coursera OAuth2 authentication backend""" name = "coursera" ID_KEY = "username" AUTHORIZATION_URL = "https://accounts.coursera.org/oauth2/v1/auth" ACCESS_TOKEN_URL = "https://accounts.coursera.org/oauth2/v1/token" REDIRECT_STATE = False SCOPE_SEPARATOR = "," DEFAULT_SCOPE = ["view_profile"] def _get_username_from_response(self, response): elements = response.get("elements", []) for element in elements: if "id" in element: return element.get("id") return None def get_user_details(self, response): """Return user details from Coursera account""" return {"username": self._get_username_from_response(response)} def get_user_id(self, details, response): """Return a username prepared in get_user_details as uid""" return details.get(self.ID_KEY) def user_data(self, access_token, *args, **kwargs): """Load user data from the service""" return self.get_json( "https://api.coursera.org/api/externalBasicProfiles.v1?q=me", headers=self.get_auth_header(access_token), ) def get_auth_header(self, access_token): return {"Authorization": f"Bearer {access_token}"} social-auth-core-4.6.1/social_core/backends/dailymotion.py000066400000000000000000000015261500362547200236270ustar00rootroot00000000000000""" DailyMotion OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/dailymotion.html """ from .oauth import BaseOAuth2 class DailymotionOAuth2(BaseOAuth2): """Dailymotion OAuth authentication backend""" name = "dailymotion" EXTRA_DATA = [("id", "id")] ID_KEY = "username" AUTHORIZATION_URL = "https://api.dailymotion.com/oauth/authorize" REQUEST_TOKEN_URL = "https://api.dailymotion.com/oauth/token" ACCESS_TOKEN_URL = "https://api.dailymotion.com/oauth/token" def get_user_details(self, response): return {"username": response.get("screenname")} def user_data(self, access_token, *args, **kwargs): """Return user data provided""" return self.get_json( "https://api.dailymotion.com/auth/", params={"access_token": access_token} ) social-auth-core-4.6.1/social_core/backends/deezer.py000066400000000000000000000031161500362547200225520ustar00rootroot00000000000000""" Deezer backend, docs at: https://developers.deezer.com/api/oauth https://developers.deezer.com/api/permissions """ from urllib.parse import parse_qsl from .oauth import BaseOAuth2 class DeezerOAuth2(BaseOAuth2): """Deezer OAuth2 authentication backend""" name = "deezer" ID_KEY = "name" AUTHORIZATION_URL = "https://connect.deezer.com/oauth/auth.php" ACCESS_TOKEN_URL = "https://connect.deezer.com/oauth/access_token.php" SCOPE_SEPARATOR = "," REDIRECT_STATE = False def auth_complete_params(self, state=None): client_id, client_secret = self.get_key_and_secret() return { "app_id": client_id, "secret": client_secret, "code": self.data.get("code"), } def request_access_token(self, *args, **kwargs): response = self.request(*args, **kwargs) return dict(parse_qsl(response.text)) def get_user_details(self, response): """Return user details from Deezer account""" fullname, first_name, last_name = self.get_user_names( first_name=response.get("firstname"), last_name=response.get("lastname") ) return { "username": response.get("name"), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "http://api.deezer.com/user/me", params={"access_token": access_token} ) social-auth-core-4.6.1/social_core/backends/digitalocean.py000066400000000000000000000025611500362547200237220ustar00rootroot00000000000000from .oauth import BaseOAuth2 class DigitalOceanOAuth(BaseOAuth2): """ DigitalOcean OAuth authentication backend. Docs: https://developers.digitalocean.com/documentation/oauth/ """ name = "digitalocean" AUTHORIZATION_URL = "https://cloud.digitalocean.com/v1/oauth/authorize" ACCESS_TOKEN_URL = "https://cloud.digitalocean.com/v1/oauth/token" SCOPE_SEPARATOR = " " EXTRA_DATA = [("expires_in", "expires_in")] def get_user_id(self, details, response): """Return user unique id provided by service""" return response["account"].get("uuid") def get_user_details(self, response): """Return user details from DigitalOcean account""" fullname, first_name, last_name = self.get_user_names( response.get("name") or "" ) return { "username": response["account"].get("email"), "email": response["account"].get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, token, *args, **kwargs): """Loads user data from service""" url = "https://api.digitalocean.com/v2/account" auth_header = {"Authorization": f"Bearer {token}"} try: return self.get_json(url, headers=auth_header) except ValueError: return None social-auth-core-4.6.1/social_core/backends/discogs.py000066400000000000000000000021501500362547200227240ustar00rootroot00000000000000""" Discogs OAuth1 backend, docs at: https://www.discogs.com/developers/ """ from social_core.backends.oauth import BaseOAuth1 class DiscogsOAuth1(BaseOAuth1): """ Implements the OAuth1 authentication mechanism for https://www.discogs.com """ name = "discogs" OAUTH_TOKEN_PARAMETER_NAME = "oauth_token" AUTHORIZATION_URL = "https://www.discogs.com/oauth/authorize" REQUEST_TOKEN_URL = "https://api.discogs.com/oauth/request_token" ACCESS_TOKEN_URL = "https://api.discogs.com/oauth/access_token" def get_user_details(self, user_data): # type: ignore[reportIncompatibleMethodOverride] return { "username": user_data["username"], "id": user_data["id"], "profile": user_data["profile"], "name": user_data["name"], } def user_data(self, access_token, *args, **kwargs): identity = self.get_json( "https://api.discogs.com/oauth/identity", auth=self.oauth_auth(access_token) ) return self.get_json( identity["resource_url"], auth=self.oauth_auth(access_token) ) social-auth-core-4.6.1/social_core/backends/discord.py000066400000000000000000000017701500362547200227270ustar00rootroot00000000000000""" Discord Auth OAuth2 backend, docs at: https://discord.com/developers/docs/topics/oauth2 """ from .oauth import BaseOAuth2 class DiscordOAuth2(BaseOAuth2): name = "discord" HOSTNAME = "discord.com" AUTHORIZATION_URL = f"https://{HOSTNAME}/api/oauth2/authorize" ACCESS_TOKEN_URL = f"https://{HOSTNAME}/api/oauth2/token" REVOKE_TOKEN_URL = f"https://{HOSTNAME}/api/oauth2/token/revoke" REVOKE_TOKEN_METHOD = "GET" DEFAULT_SCOPE = ["identify"] SCOPE_SEPARATOR = "+" REDIRECT_STATE = False EXTRA_DATA = [("expires_in", "expires"), ("refresh_token", "refresh_token")] def get_user_details(self, response): return { "username": response.get("username"), "email": response.get("email") or "", } def user_data(self, access_token, *args, **kwargs): url = f"https://{self.HOSTNAME}/api/users/@me" auth_header = {"Authorization": f"Bearer {access_token}"} return self.get_json(url, headers=auth_header) social-auth-core-4.6.1/social_core/backends/discourse.py000066400000000000000000000062041500362547200232750ustar00rootroot00000000000000import hmac import time from base64 import urlsafe_b64decode, urlsafe_b64encode from hashlib import sha256 from urllib.parse import urlencode from ..exceptions import AuthException, AuthTokenError from ..utils import parse_qs from .base import BaseAuth class DiscourseAuth(BaseAuth): name = "discourse" EXTRA_DATA = ["username", "name", "avatar_url"] def auth_url(self): """ Get the URL to which we must redirect in order to authenticate the user """ return_url = self.redirect_uri nonce = self.strategy.random_string(64) self.add_nonce(nonce) payload = urlencode({"nonce": nonce, "return_sso_url": return_url}) base_64_payload = urlsafe_b64encode(payload.encode("utf8")).decode("ascii") payload_signature = hmac.new( self.setting("SECRET").encode("utf8"), base_64_payload.encode("utf8"), sha256, ).hexdigest() encoded_params = urlencode({"sso": base_64_payload, "sig": payload_signature}) return f"{self.get_idp_url()}?{encoded_params}" def get_idp_url(self): return self.setting("SERVER_URL") + "/session/sso_provider" def get_user_id(self, details, response): return response["email"] def get_user_details(self, response): return { "username": response.get("username"), "email": response.get("email"), "name": response.get("name"), "groups": response.get("groups", "").split(","), "is_staff": response.get("admin") == "true" or response.get("moderator") == "true", "is_superuser": response.get("admin") == "true", } def add_nonce(self, nonce): self.strategy.storage.nonce.use(self.setting("SERVER_URL"), time.time(), nonce) def get_nonce(self, nonce): return self.strategy.storage.nonce.get(self.setting("SERVER_URL"), nonce) def delete_nonce(self, nonce): self.strategy.storage.nonce.delete(nonce) def auth_complete(self, *args, **kwargs): """ The user has been redirected back from the IdP and we should now log them in, if everything checks out. """ request_data = self.strategy.request_data() sso_params = request_data.get("sso") sso_signature = request_data.get("sig") param_signature = hmac.new( self.setting("SECRET").encode("utf8"), sso_params.encode("utf8"), sha256 ).hexdigest() if not hmac.compare_digest(str(sso_signature), str(param_signature)): raise AuthException(self, "Could not verify discourse login") decoded_params = urlsafe_b64decode(sso_params.encode("utf8")).decode("ascii") # Validate the nonce to ensure the request was not modified response = parse_qs(decoded_params) nonce_obj = self.get_nonce(response.get("nonce")) if nonce_obj: self.delete_nonce(nonce_obj) else: raise AuthTokenError(self, "Incorrect id_token: nonce") kwargs.update({"sso": "", "sig": "", "backend": self, "response": response}) return self.strategy.authenticate(*args, **kwargs) social-auth-core-4.6.1/social_core/backends/disqus.py000066400000000000000000000033141500362547200226040ustar00rootroot00000000000000""" Disqus OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/disqus.html """ from .oauth import BaseOAuth2 class DisqusOAuth2(BaseOAuth2): name = "disqus" AUTHORIZATION_URL = "https://disqus.com/api/oauth/2.0/authorize/" ACCESS_TOKEN_URL = "https://disqus.com/api/oauth/2.0/access_token/" REDIRECT_STATE = False SCOPE_SEPARATOR = "," EXTRA_DATA = [ ("avatar", "avatar"), ("connections", "connections"), ("user_id", "user_id"), ("email", "email"), ("email_hash", "emailHash"), ("expires", "expires"), ("location", "location"), ("meta", "response"), ("name", "name"), ("username", "username"), ] def get_user_id(self, details, response): return response["response"]["id"] def get_user_details(self, response): """Return user details from Disqus account""" rr = response.get("response", {}) return { "username": rr.get("username", ""), "user_id": response.get("user_id", ""), "email": rr.get("email", ""), "name": rr.get("name", ""), } def extra_data(self, user, uid, response, details=None, *args, **kwargs): meta_response = dict(response, **response.get("response", {})) return super().extra_data(user, uid, meta_response, details, *args, **kwargs) def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" key, secret = self.get_key_and_secret() return self.get_json( "https://disqus.com/api/3.0/users/details.json", params={"access_token": access_token, "api_secret": secret}, ) social-auth-core-4.6.1/social_core/backends/docker.py000066400000000000000000000030601500362547200225410ustar00rootroot00000000000000""" Docker Hub OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/docker.html """ from .oauth import BaseOAuth2 class DockerOAuth2(BaseOAuth2): name = "docker" ID_KEY = "user_id" AUTHORIZATION_URL = "https://hub.docker.com/api/v1.1/o/authorize/" ACCESS_TOKEN_URL = "https://hub.docker.com/api/v1.1/o/token/" REFRESH_TOKEN_URL = "https://hub.docker.com/api/v1.1/o/token/" REDIRECT_STATE = False EXTRA_DATA = [ ("refresh_token", "refresh_token", True), ("user_id", "user_id"), ("email", "email"), ("full_name", "fullname"), ("location", "location"), ("url", "url"), ("company", "company"), ("gravatar_email", "gravatar_email"), ] def get_user_details(self, response): """Return user details from Docker Hub account""" fullname, first_name, last_name = self.get_user_names( response.get("full_name") or response.get("username") or "" ) return { "username": response.get("username"), "fullname": fullname, "first_name": first_name, "last_name": last_name, "email": response.get("email", ""), } def user_data(self, access_token, *args, **kwargs): """Grab user profile information from Docker Hub.""" username = kwargs["response"]["username"] return self.get_json( f"https://hub.docker.com/api/v1.1/users/{username}/", headers={"Authorization": f"Bearer {access_token}"}, ) social-auth-core-4.6.1/social_core/backends/douban.py000066400000000000000000000037701500362547200225520ustar00rootroot00000000000000""" Douban OAuth1 and OAuth2 backends, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/douban.html """ from .oauth import BaseOAuth1, BaseOAuth2 class DoubanOAuth(BaseOAuth1): """Douban OAuth authentication backend""" name = "douban" EXTRA_DATA = [("id", "id")] AUTHORIZATION_URL = "http://www.douban.com/service/auth/authorize" REQUEST_TOKEN_URL = "http://www.douban.com/service/auth/request_token" ACCESS_TOKEN_URL = "http://www.douban.com/service/auth/access_token" def get_user_id(self, details, response): return response["db:uid"]["$t"] def get_user_details(self, response): """Return user details from Douban""" return {"username": response["db:uid"]["$t"], "email": ""} def user_data(self, access_token, *args, **kwargs): """Return user data provided""" return self.get_json( "http://api.douban.com/people/%40me?&alt=json", auth=self.oauth_auth(access_token), ) class DoubanOAuth2(BaseOAuth2): """Douban OAuth authentication backend""" name = "douban-oauth2" AUTHORIZATION_URL = "https://www.douban.com/service/auth2/auth" ACCESS_TOKEN_URL = "https://www.douban.com/service/auth2/token" REDIRECT_STATE = False EXTRA_DATA = [ ("id", "id"), ("uid", "username"), ("refresh_token", "refresh_token"), ] def get_user_details(self, response): """Return user details from Douban""" fullname, first_name, last_name = self.get_user_names(response.get("name", "")) return { "username": response.get("uid", ""), "fullname": fullname, "first_name": first_name, "last_name": last_name, "email": "", } def user_data(self, access_token, *args, **kwargs): """Return user data provided""" return self.get_json( "https://api.douban.com/v2/user/~me", headers={"Authorization": f"Bearer {access_token}"}, ) social-auth-core-4.6.1/social_core/backends/dribbble.py000066400000000000000000000041401500362547200230370ustar00rootroot00000000000000""" Dribbble OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/dribbble.html http://developer.dribbble.com/v1/oauth/ """ from .oauth import BaseOAuth2 class DribbbleOAuth2(BaseOAuth2): """Dribbble OAuth authentication backend""" name = "dribbble" AUTHORIZATION_URL = "https://dribbble.com/oauth/authorize" ACCESS_TOKEN_URL = "https://dribbble.com/oauth/token" SCOPE_SEPARATOR = "," EXTRA_DATA = [ ("id", "id"), ("name", "name"), ("html_url", "html_url"), ("avatar_url", "avatar_url"), ("bio", "bio"), ("location", "location"), ("links", "links"), ("buckets_count", "buckets_count"), ("comments_received_count", "comments_received_count"), ("followers_count", "followers_count"), ("followings_count", "followings_count"), ("likes_count", "likes_count"), ("likes_received_count", "likes_received_count"), ("projects_count", "projects_count"), ("rebounds_received_count", "rebounds_received_count"), ("shots_count", "shots_count"), ("teams_count", "teams_count"), ("pro", "pro"), ("buckets_url", "buckets_url"), ("followers_url", "followers_url"), ("following_url", "following_url"), ("likes_url", "shots_url"), ("teams_url", "teams_url"), ("created_at", "created_at"), ("updated_at", "updated_at"), ] def get_user_details(self, response): """Return user details from Dribbble account""" fullname, first_name, last_name = self.get_user_names(response.get("name")) return { "username": response.get("username"), "email": response.get("email", ""), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://api.dribbble.com/v1/user", headers={"Authorization": f"Bearer {access_token}"}, ) social-auth-core-4.6.1/social_core/backends/drip.py000066400000000000000000000015071500362547200222340ustar00rootroot00000000000000""" Drip OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/drip.html """ from .oauth import BaseOAuth2 class DripOAuth(BaseOAuth2): name = "drip" AUTHORIZATION_URL = "https://www.getdrip.com/oauth/authorize" ACCESS_TOKEN_URL = "https://www.getdrip.com/oauth/token" def get_user_id(self, details, response): return details["email"] def get_user_details(self, response): return { "email": response["users"][0]["email"], "fullname": response["users"][0]["name"], "username": response["users"][0]["email"], } def user_data(self, access_token, *args, **kwargs): return self.get_json( "https://api.getdrip.com/v2/user", headers={"Authorization": f"Bearer {access_token}"}, ) social-auth-core-4.6.1/social_core/backends/dropbox.py000066400000000000000000000021171500362547200227510ustar00rootroot00000000000000""" Dropbox OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/dropbox.html """ from .oauth import BaseOAuth2 class DropboxOAuth2V2(BaseOAuth2): name = "dropbox-oauth2" ID_KEY = "uid" AUTHORIZATION_URL = "https://www.dropbox.com/oauth2/authorize" ACCESS_TOKEN_URL = "https://api.dropboxapi.com/oauth2/token" REDIRECT_STATE = False def get_user_details(self, response): """Return user details from Dropbox account""" name = response.get("name") return { "username": str(response.get("account_id")), "email": response.get("email"), "fullname": name.get("display_name"), "first_name": name.get("given_name"), "last_name": name.get("surname"), } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://api.dropboxapi.com/2/users/get_current_account", headers={"Authorization": f"Bearer {access_token}"}, method="POST", ) social-auth-core-4.6.1/social_core/backends/echosign.py000066400000000000000000000014651500362547200231000ustar00rootroot00000000000000from .oauth import BaseOAuth2 class EchosignOAuth2(BaseOAuth2): name = "echosign" REDIRECT_STATE = False REFRESH_TOKEN_METHOD = "POST" REVOKE_TOKEN_METHOD = "POST" AUTHORIZATION_URL = "https://secure.echosign.com/public/oauth" ACCESS_TOKEN_URL = "https://secure.echosign.com/oauth/token" REFRESH_TOKEN_URL = "https://secure.echosign.com/oauth/refresh" REVOKE_TOKEN_URL = "https://secure.echosign.com/oauth/revoke" def get_user_details(self, response): return response def get_user_id(self, details, response): return details["userInfoList"][0]["userId"] def user_data(self, access_token, *args, **kwargs): return self.get_json( "https://api.echosign.com/api/rest/v3/users", headers={"Access-Token": access_token}, ) social-auth-core-4.6.1/social_core/backends/edmodo.py000066400000000000000000000020561500362547200225450ustar00rootroot00000000000000""" Edmodo OAuth2 Sign-in backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/edmodo.html """ from .oauth import BaseOAuth2 class EdmodoOAuth2(BaseOAuth2): """Edmodo OAuth2""" name = "edmodo" AUTHORIZATION_URL = "https://api.edmodo.com/oauth/authorize" ACCESS_TOKEN_URL = "https://api.edmodo.com/oauth/token" def get_user_details(self, response): """Return user details from Edmodo account""" fullname, first_name, last_name = self.get_user_names( first_name=response.get("first_name"), last_name=response.get("last_name") ) return { "username": response.get("username"), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from Edmodo""" return self.get_json( "https://api.edmodo.com/users/me", params={"access_token": access_token} ) social-auth-core-4.6.1/social_core/backends/egi_checkin.py000066400000000000000000000060421500362547200235250ustar00rootroot00000000000000""" Backend for OpenID Connect EGI Check-in https://www.egi.eu/service/check-in/ """ from __future__ import annotations from typing import Literal from social_core.backends.open_id_connect import OpenIdConnectAuth CHECKIN_ENV_ENDPOINTS = { "prod": "https://aai.egi.eu/auth/realms/egi", "demo": "https://aai-demo.egi.eu/auth/realms/egi", "dev": "https://aai-dev.egi.eu/auth/realms/egi", } class EGICheckinOpenIdConnect(OpenIdConnectAuth): name = "egi-checkin" # Check-in provides 3 environments: production, demo and development # Set the one to use as "prod", "demo" or "dev" CHECKIN_ENV: Literal["prod", "demo", "dev"] = "prod" # This is a opaque and unique id for every user that looks like an email # see https://docs.egi.eu/providers/check-in/sp/#1-community-user-identifier USERNAME_KEY = "voperson_id" EXTRA_DATA = [ ("expires_in", "expires_in", True), ("refresh_token", "refresh_token", True), ("id_token", "id_token", True), ] # In order to get any scopes, you have to register your service with # Check-in, see documentation at https://docs.egi.eu/providers/check-in/sp/ DEFAULT_SCOPE = [ "openid", "profile", "email", "voperson_id", "eduperson_entitlement", "offline_access", ] # This is the list of entitlements that are allowed to login into the # service. A user with any of these will be allowed. If empty, all # users will be allowed ALLOWED_ENTITLEMENTS: list[str] = [] def oidc_endpoint(self): endpoint = self.setting("OIDC_ENDPOINT", self.OIDC_ENDPOINT) if endpoint: return endpoint checkin_env = self.setting("CHECKIN_ENV", self.CHECKIN_ENV) return CHECKIN_ENV_ENDPOINTS.get(checkin_env, "") def get_user_details(self, response): username_key = self.setting("USERNAME_KEY", default=self.USERNAME_KEY) fullname, first_name, last_name = self.get_user_names( response.get("name") or "", response.get("given_name") or "", response.get("family_name") or "", ) return { "username": response.get(username_key), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def entitlement_allowed(self, user_entitlements): allowed = True allowed_ent = self.setting("ALLOWED_ENTITLEMENTS", self.ALLOWED_ENTITLEMENTS) if allowed_ent: allowed = any(e in user_entitlements for e in allowed_ent) return allowed def auth_allowed(self, response, details): """Check-in promotes the use of eduperson_entitlements for AuthZ, if ALLOWED_ENTITLEMENTS is defined then use them to allow or not users""" allowed = super().auth_allowed(response, details) if allowed: user_entitlements = response.get("eduperson_entitlement") or [] allowed = self.entitlement_allowed(user_entitlements) return allowed social-auth-core-4.6.1/social_core/backends/einfracz.py000066400000000000000000000022011500362547200230670ustar00rootroot00000000000000""" Backend for OpenID Connect e-INFRA CZ AAI https://www.e-infra.cz """ from social_core.backends.open_id_connect import OpenIdConnectAuth class EInfraCZOpenIdConnect(OpenIdConnectAuth): name = "e-infra_cz" OIDC_ENDPOINT = "https://login.e-infra.cz/oidc" EXTRA_DATA = [ ("expires_in", "expires_in", True), ("refresh_token", "refresh_token", True), ("id_token", "id_token", True), ("other_tokens", "other_tokens", True), ] # In order to get any scopes, you have to register your service with # e-INFRA CZ AAI at https://spadmin.e-infra.cz/ DEFAULT_SCOPE = ["openid", "email"] JWT_DECODE_OPTIONS = {"verify_at_hash": False} def get_user_details(self, response): username_key = self.setting("USERNAME_KEY", default=self.USERNAME_KEY) name = response.get("name") or "" fullname, first_name, last_name = self.get_user_names(name) return { "username": response.get(username_key), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } social-auth-core-4.6.1/social_core/backends/elixir.py000066400000000000000000000023011500362547200225630ustar00rootroot00000000000000""" Backend for OpenID Connect ELIXIR AAI https://www.elixir-europe.org/services/compute/aai """ from social_core.backends.open_id_connect import OpenIdConnectAuth class ElixirOpenIdConnect(OpenIdConnectAuth): name = "elixir" OIDC_ENDPOINT = "https://login.elixir-czech.org/oidc" EXTRA_DATA = [ ("expires_in", "expires_in", True), ("refresh_token", "refresh_token", True), ("id_token", "id_token", True), ("other_tokens", "other_tokens", True), ] # In order to get any scopes, you have to register your service with # ELIXIR, see documentation at # https://www.elixir-europe.org/services/compute/aai DEFAULT_SCOPE = ["openid", "email"] JWT_DECODE_OPTIONS = {"verify_at_hash": False} def get_user_details(self, response): username_key = self.setting("USERNAME_KEY", default=self.USERNAME_KEY) name = response.get("name") or "" fullname, first_name, last_name = self.get_user_names(name) return { "username": response.get(username_key), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } social-auth-core-4.6.1/social_core/backends/email.py000066400000000000000000000004321500362547200223610ustar00rootroot00000000000000""" Legacy Email backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/email.html """ from .legacy import LegacyAuth class EmailAuth(LegacyAuth): name = "email" ID_KEY = "email" REQUIRES_EMAIL_VALIDATION = True EXTRA_DATA = ["email"] social-auth-core-4.6.1/social_core/backends/etsy.py000066400000000000000000000030601500362547200222560ustar00rootroot00000000000000from .oauth import BaseOAuth2PKCE class EtsyOAuth2(BaseOAuth2PKCE): name = "etsy" ID_KEY = "user_id" AUTHORIZATION_URL = "https://www.etsy.com/oauth/connect" ACCESS_TOKEN_URL = "https://api.etsy.com/v3/public/oauth/token" REFRESH_TOKEN_URL = "https://api.etsy.com/v3/public/oauth/token" PKCE_DEFAULT_CODE_CHALLENGE_METHOD = "S256" REQUEST_TOKEN_METHOD = "POST" SCOPE_SEPARATOR = " " REDIRECT_STATE = False EXTRA_DATA = [ ("refresh_token", "refresh_token"), ("expires_in", "expires_in"), ("token_type", "token_type"), ("access_token", "access_token"), # User Data Fields ("primary_email", "primary_email"), ("first_name", "first_name"), ("last_name", "last_name"), ("image_url_75x75", "image_url_75x75"), ] def user_data(self, access_token, *args, **kwargs) -> dict: client_id, _ = self.get_key_and_secret() user_id = access_token.split(".")[0] headers = {"Authorization": f"Bearer {access_token}", "x-api-key": client_id} return self.get_json( url=f"https://openapi.etsy.com/v3/application/users/{user_id}", headers=headers, ) def get_user_details(self, response): return { "user_id": response["user_id"], "first_name": response["first_name"], "last_name": response["last_name"], "email": response["primary_email"], "image_url_75x75": response["image_url_75x75"], "username": str(response["user_id"]), } social-auth-core-4.6.1/social_core/backends/eventbrite.py000066400000000000000000000020101500362547200234330ustar00rootroot00000000000000from .oauth import BaseOAuth2 class EventbriteOAuth2(BaseOAuth2): """Eventbrite OAuth2 authentication backend""" name = "eventbrite" AUTHORIZATION_URL = "https://www.eventbrite.com/oauth/authorize" ACCESS_TOKEN_URL = "https://www.eventbrite.com/oauth/token" METADATA_URL = "https://www.eventbriteapi.com/v3/users/me" STATE_PARAMETER = False REDIRECT_STATE = False def get_user_details(self, response): """Return user details from an Eventbrite metadata response""" email = next(iter(filter(lambda x: x["primary"], response["emails"])))["email"] return { "username": email, "email": email, "first_name": response["first_name"], "last_name": response["last_name"], } def user_data(self, access_token, *args, **kwargs): """Loads user data and datacenter information from service""" return self.get_json( self.METADATA_URL, headers={"Authorization": "Bearer " + access_token} ) social-auth-core-4.6.1/social_core/backends/eveonline.py000066400000000000000000000025631500362547200232650ustar00rootroot00000000000000""" EVE Online Single Sign-On (SSO) OAuth2 backend Documentation at https://eveonline-third-party-documentation.readthedocs.io/en/latest/sso/index.html """ from .oauth import BaseOAuth2 class EVEOnlineOAuth2(BaseOAuth2): """EVE Online OAuth authentication backend""" name = "eveonline" BASE_URL = "https://login.eveonline.com/oauth" AUTHORIZATION_URL = BASE_URL + "/authorize" ACCESS_TOKEN_URL = BASE_URL + "/token" ID_KEY = "CharacterID" EXTRA_DATA = [ ("CharacterID", "id"), ("expires_in", "expires"), ("CharacterOwnerHash", "owner_hash", True), ("refresh_token", "refresh_token", True), ] def get_user_details(self, response): """Return user details from EVE Online account""" user_data = self.user_data(response["access_token"]) fullname, first_name, last_name = self.get_user_names( user_data["CharacterName"] ) return { "email": "", "username": fullname, "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Get Character data from EVE server""" return self.get_json( "https://login.eveonline.com/oauth/verify", headers={"Authorization": f"Bearer {access_token}"}, ) social-auth-core-4.6.1/social_core/backends/evernote.py000066400000000000000000000051141500362547200231230ustar00rootroot00000000000000""" Evernote OAuth1 backend (with sandbox mode support), docs at: https://python-social-auth.readthedocs.io/en/latest/backends/evernote.html """ from requests import HTTPError from ..exceptions import AuthCanceled from .oauth import BaseOAuth1 class EvernoteOAuth(BaseOAuth1): """ Evernote OAuth authentication backend. Possible Values: {'edam_expires': ['1367525289541'], 'edam_noteStoreUrl': [ 'https://sandbox.evernote.com/shard/s1/notestore' ], 'edam_shard': ['s1'], 'edam_userId': ['123841'], 'edam_webApiUrlPrefix': ['https://sandbox.evernote.com/shard/s1/'], 'oauth_token': [ 'S=s1:U=1e3c1:E=13e66dbee45:C=1370f2ac245:P=185:A=my_user:' \ 'H=411443c5e8b20f8718ed382a19d4ae38' ]} """ name = "evernote" ID_KEY = "edam_userId" AUTHORIZATION_URL = "https://www.evernote.com/OAuth.action" REQUEST_TOKEN_URL = "https://www.evernote.com/oauth" ACCESS_TOKEN_URL = "https://www.evernote.com/oauth" ACCESS_TOKEN_METHOD = "GET" EXTRA_DATA = [ ("access_token", "access_token"), ("oauth_token", "oauth_token"), ("edam_noteStoreUrl", "store_url"), ("edam_expires", "expires"), ] def get_user_details(self, response): """Return user details from Evernote account""" return {"username": response["edam_userId"], "email": ""} def access_token(self, token): """Return request for access token value""" try: return self.get_querystring( self.ACCESS_TOKEN_URL, auth=self.oauth_auth(token) ) except HTTPError as err: # Evernote returns a 401 error when AuthCanceled if err.response.status_code == 401: raise AuthCanceled(self, response=err.response) raise def extra_data(self, user, uid, response, details, *args, **kwargs): data = super().extra_data(user, uid, response, details, *args, **kwargs) # Evernote returns expiration timestamp in milliseconds, so it needs to # be normalized. if "expires" in data: data["expires"] = int(data["expires"]) / 1000 return data def user_data(self, access_token, *args, **kwargs): """Return user data provided""" return access_token.copy() class EvernoteSandboxOAuth(EvernoteOAuth): name = "evernote-sandbox" AUTHORIZATION_URL = "https://sandbox.evernote.com/OAuth.action" REQUEST_TOKEN_URL = "https://sandbox.evernote.com/oauth" ACCESS_TOKEN_URL = "https://sandbox.evernote.com/oauth" social-auth-core-4.6.1/social_core/backends/exacttarget.py000066400000000000000000000076121500362547200236140ustar00rootroot00000000000000""" ExactTarget OAuth support. Support Authentication from IMH using JWT token and pre-shared key. Requires package pyjwt """ from datetime import datetime, timedelta, timezone import jwt from ..exceptions import AuthCanceled, AuthFailed from .oauth import BaseOAuth2 class ExactTargetOAuth2(BaseOAuth2): name = "exacttarget" def get_user_details(self, response): """Use the email address of the user, suffixed by _et""" user = response.get("token", {}).get("request", {}).get("user", {}) if "email" in user: user["username"] = user["email"] return user def get_user_id(self, details, response): """ Create a user ID from the ET user ID. Uses details rather than the default response, as only the token is available in response. details is much richer: { 'expiresIn': 1200, 'username': 'example@example.com', 'refreshToken': '1234567890abcdef', 'internalOauthToken': 'jwttoken.......', 'oauthToken': 'yetanothertoken', 'id': 123456, 'culture': 'en-US', 'timezone': { 'shortName': 'CST', 'offset': -6.0, 'dst': False, 'longName': '(GMT-06:00) Central Time (No Daylight Saving)' }, 'email': 'example@example.com' } """ return "{}".format(details.get("id")) def uses_redirect(self): return False def auth_url(self): return None def process_error(self, data): if data.get("error"): error = self.data.get("error_description") or self.data["error"] raise AuthFailed(self, error) def do_auth(self, token, *args, **kwargs): dummy, secret = self.get_key_and_secret() try: # Decode the token, using the Application Signature from settings decoded = jwt.decode(token, secret, algorithms=["HS256"]) except jwt.DecodeError: # Wrong signature, fail authentication raise AuthCanceled(self) kwargs.update({"response": {"token": decoded}, "backend": self}) return self.strategy.authenticate(*args, **kwargs) def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" token = self.data.get("jwt", {}) if not token: raise AuthFailed(self, "Authentication Failed") return self.do_auth(token, *args, **kwargs) def extra_data(self, user, uid, response, details=None, *args, **kwargs): """Load extra details from the JWT token""" data = { "id": details.get("id"), "email": details.get("email"), # OAuth token, for use with legacy SOAP API calls: # http://bit.ly/13pRHfo "internalOauthToken": details.get("internalOauthToken"), # Token for use with the Application ClientID for the FUEL API "oauthToken": details.get("oauthToken"), # If the token has expired, use the FUEL API to get a new token see # http://bit.ly/10v1K5l and http://bit.ly/11IbI6F - set legacy=1 "refreshToken": details.get("refreshToken"), } # The expiresIn value determines how long the tokens are valid for. # Take a bit off, then convert to an int timestamp expiresSeconds = details.get("expiresIn", 0) - 30 expires = datetime.now(timezone.utc) + timedelta(seconds=expiresSeconds) data["expires"] = (expires - datetime(1970, 1, 1)).total_seconds() if response.get("token"): token = response["token"] org = token.get("request", {}).get("organization") if org: data["stack"] = org.get("stackKey") data["enterpriseId"] = org.get("enterpriseId") return data social-auth-core-4.6.1/social_core/backends/facebook.py000066400000000000000000000216021500362547200230450ustar00rootroot00000000000000""" Facebook OAuth2, and Canvas Application backends, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/facebook.html """ import base64 import hashlib import hmac import json import time from ..exceptions import ( AuthCanceled, AuthException, AuthMissingParameter, AuthUnknownError, ) from ..utils import constant_time_compare, handle_http_errors, parse_qs from .oauth import BaseOAuth2 API_VERSION = 18.0 class FacebookOAuth2(BaseOAuth2): """Facebook OAuth2 authentication backend""" name = "facebook" REDIRECT_STATE = False RESPONSE_TYPE = None SCOPE_SEPARATOR = "," AUTHORIZATION_URL = "https://www.facebook.com/v{version}/dialog/oauth" ACCESS_TOKEN_URL = "https://graph.facebook.com/v{version}/oauth/access_token" ACCESS_TOKEN_METHOD = "GET" REVOKE_TOKEN_URL = "https://graph.facebook.com/v{version}/{uid}/permissions" REVOKE_TOKEN_METHOD = "DELETE" USER_DATA_URL = "https://graph.facebook.com/v{version}/me" EXTRA_DATA = [ ("id", "id"), ("expires", "expires"), ("granted_scopes", "granted_scopes"), ("denied_scopes", "denied_scopes"), ] def auth_params(self, state=None): params = super().auth_params(state) params["return_scopes"] = "true" return params def authorization_url(self): version = self.setting("API_VERSION", API_VERSION) return self.AUTHORIZATION_URL.format(version=version) def access_token_url(self): version = self.setting("API_VERSION", API_VERSION) return self.ACCESS_TOKEN_URL.format(version=version) def get_user_details(self, response): """Return user details from Facebook account""" fullname, first_name, last_name = self.get_user_names( response.get("name", ""), response.get("first_name", ""), response.get("last_name", ""), ) return { "username": response.get("username", response.get("name")), "email": response.get("email", ""), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" params = self.setting("PROFILE_EXTRA_PARAMS", {}).copy() params["access_token"] = access_token if self.setting("APPSECRET_PROOF", True): _, secret = self.get_key_and_secret() params["appsecret_proof"] = hmac.new( secret.encode("utf8"), msg=access_token.encode("utf8"), digestmod=hashlib.sha256, ).hexdigest() version = self.setting("API_VERSION", API_VERSION) return self.get_json(self.USER_DATA_URL.format(version=version), params=params) def process_error(self, data): super().process_error(data) if data.get("error_code"): raise AuthCanceled( self, data.get("error_message") or data.get("error_code") ) @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" self.process_error(self.data) if not self.data.get("code"): raise AuthMissingParameter(self, "code") state = self.validate_state() key, secret = self.get_key_and_secret() response = self.request( self.access_token_url(), params={ "client_id": key, "redirect_uri": self.get_redirect_uri(state), "client_secret": secret, "code": self.data["code"], }, method=self.ACCESS_TOKEN_METHOD, ) # API v2.3 returns a JSON, according to the documents linked at issue # #592, but it seems that this needs to be enabled(?), otherwise the # usual querystring type response is returned. try: response = response.json() except ValueError: response = parse_qs(response.text) access_token = response["access_token"] return self.do_auth(access_token, response, *args, **kwargs) def process_refresh_token_response(self, response, *args, **kwargs): try: return response.json() except ValueError: return parse_qs(response.content) def refresh_token_params(self, token, *args, **kwargs): client_id, client_secret = self.get_key_and_secret() return { "fb_exchange_token": token, "grant_type": "fb_exchange_token", "client_id": client_id, "client_secret": client_secret, } @handle_http_errors def do_auth(self, access_token, response=None, *args, **kwargs): response = response or {} data = self.user_data(access_token) if not isinstance(data, dict): # From time to time Facebook responds back a JSON with just # False as value, the reason is still unknown, but since the # data is needed (it contains the user ID used to identify the # account on further logins), this app cannot allow it to # continue with the auth process. raise AuthUnknownError( self, "An error occurred while retrieving users Facebook data" ) data["access_token"] = access_token if "expires_in" in response: data["expires"] = response["expires_in"] if self.data.get("granted_scopes"): data["granted_scopes"] = self.data["granted_scopes"].split(",") if self.data.get("denied_scopes"): data["denied_scopes"] = self.data["denied_scopes"].split(",") kwargs.update({"backend": self, "response": data}) return self.strategy.authenticate(*args, **kwargs) def revoke_token_url(self, token, uid): version = self.setting("API_VERSION", API_VERSION) return self.REVOKE_TOKEN_URL.format(version=version, uid=uid) def revoke_token_params(self, token, uid): return {"access_token": token} def process_revoke_token_response(self, response): return ( super().process_revoke_token_response(response) and response.content == "true" ) class FacebookAppOAuth2(FacebookOAuth2): """Facebook Application Authentication support""" name = "facebook-app" def uses_redirect(self): return False def auth_complete(self, *args, **kwargs): access_token = self.data.get("access_token") response = {} if "signed_request" in self.data: key, secret = self.get_key_and_secret() response = self.load_signed_request(self.data["signed_request"]) assert response, "Missing signed_request response" if "user_id" not in response and "oauth_token" not in response: raise AuthException(self) if response is not None: access_token = ( response.get("access_token") or response.get("oauth_token") or self.data.get("access_token") ) if access_token is None: if self.data.get("error") == "access_denied": raise AuthCanceled(self) raise AuthException(self) return self.do_auth(access_token, response, *args, **kwargs) def auth_html(self): key, secret = self.get_key_and_secret() namespace = self.setting("NAMESPACE", None) scope = self.setting("SCOPE", "") if scope: scope = self.SCOPE_SEPARATOR.join(scope) ctx = { "FACEBOOK_APP_NAMESPACE": namespace or key, "FACEBOOK_KEY": key, "FACEBOOK_EXTENDED_PERMISSIONS": scope, "FACEBOOK_COMPLETE_URI": self.redirect_uri, } tpl = self.setting("LOCAL_HTML", "facebook.html") return self.strategy.render_html(tpl=tpl, context=ctx) def load_signed_request(self, signed_request): def base64_url_decode(data): data = data.encode("ascii") data += b"=" * (4 - (len(data) % 4)) return base64.urlsafe_b64decode(data) key, secret = self.get_key_and_secret() try: sig, payload = signed_request.split(".", 1) except ValueError: pass # ignore if can't split on dot else: sig = base64_url_decode(sig) payload_json_bytes = base64_url_decode(payload) data = json.loads(payload_json_bytes.decode("utf-8", "replace")) expected_sig = hmac.new( secret.encode("ascii"), msg=payload.encode("ascii"), digestmod=hashlib.sha256, ).digest() # allow the signed_request to function for upto 1 day if constant_time_compare(sig, expected_sig) and data["issued_at"] > ( time.time() - 86400 ): return data social-auth-core-4.6.1/social_core/backends/facebook_limited.py000066400000000000000000000032261500362547200245560ustar00rootroot00000000000000""" Facebook Limited Login backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/facebook.html """ from ..exceptions import AuthTokenError from .open_id_connect import OpenIdConnectAuth class FacebookLimitedLogin(OpenIdConnectAuth): """Facebook Limited Login (OIDC) backend""" name = "facebook-limited-login" OIDC_ENDPOINT = "https://www.facebook.com" ACCESS_TOKEN_URL = "https://facebook.com/dialog/oauth/" ID_TOKEN_MAX_AGE = 3600 def authenticate(self, *args, **kwargs): if ( "backend" not in kwargs or kwargs["backend"].name != self.name or "strategy" not in kwargs or "response" not in kwargs ): return None # Replace response with the decoded JWT raw_jwt = kwargs.get("response", {}).get("access_token") kwargs["response"] = self.validate_and_return_id_token(raw_jwt, "") return super().authenticate(*args, **kwargs) def get_user_details(self, response): return { "fullname": response.get("name"), "email": response.get("email"), "picture": response.get("picture"), } def user_data(self, access_token, *args, **kwargs): # We don't have an access token to call any API for the user details. return {} def validate_claims(self, id_token): try: super().validate_claims(id_token) except AuthTokenError as e: if "Incorrect id_token: nonce" in e.args: # Ignore errors about nonce. We can't validate it since it's not generated server-side. return raise social-auth-core-4.6.1/social_core/backends/fedora.py000066400000000000000000000020051500362547200225300ustar00rootroot00000000000000""" Fedora OpenId backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/fedora.html """ from .open_id import OpenIdAuth from .open_id_connect import OpenIdConnectAuth class FedoraOpenIdConnect(OpenIdConnectAuth): """ Fedora OpenID Connect backend. To use it, you need to set the SOCIAL_AUTH_FEDORA_OIDC_KEY and SOCIAL_AUTH_FEDORA_OIDC_SECRET configuration variables to the client_id and client_secret values that the Fedora Infrastructure gave you. """ name = "fedora-oidc" USERNAME_KEY = "nickname" OIDC_ENDPOINT = "https://id.fedoraproject.org" DEFAULT_SCOPE = [ "openid", "profile", "email", "https://id.fedoraproject.org/scope/agreements", "https://id.fedoraproject.org/scope/groups", ] class FedoraOpenId(OpenIdAuth): """ Fedora OpenID backend. DEPRECATED, please use the OpenID Connect backend. """ name = "fedora" URL = "https://id.fedoraproject.org" USERNAME_KEY = "nickname" social-auth-core-4.6.1/social_core/backends/fence.py000066400000000000000000000022031500362547200223500ustar00rootroot00000000000000from __future__ import annotations from typing import cast from urllib.parse import urljoin from social_core.utils import cache from ..utils import append_slash from .open_id_connect import OpenIdConnectAuth class Fence(OpenIdConnectAuth): name = "fence" OIDC_ENDPOINT = "https://nci-crdc.datacommons.io" ID_KEY = "username" DEFAULT_SCOPE = ["openid", "user"] JWT_DECODE_OPTIONS = {"verify_at_hash": False} def _url(self, path): return urljoin(append_slash(cast("str", self.OIDC_ENDPOINT)), path) def authorization_url(self): return self._url("user/oauth2/authorize") def access_token_url(self): return self._url("user/oauth2/token") @cache(ttl=86400) def oidc_config(self): return self.get_json(self._url(".well-known/openid-configuration")) def get_user_details(self, response): return { "username": response.get("preferred_username"), "email": response.get("username"), "fullname": response.get("name"), "first_name": response.get("given_name"), "last_name": response.get("family_name"), } social-auth-core-4.6.1/social_core/backends/fitbit.py000066400000000000000000000042761500362547200225650ustar00rootroot00000000000000""" Fitbit OAuth backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/fitbit.html """ import base64 from .oauth import BaseOAuth1, BaseOAuth2 class FitbitOAuth1(BaseOAuth1): """Fitbit OAuth1 authentication backend""" name = "fitbit" AUTHORIZATION_URL = "https://www.fitbit.com/oauth/authorize" REQUEST_TOKEN_URL = "https://api.fitbit.com/oauth/request_token" ACCESS_TOKEN_URL = "https://api.fitbit.com/oauth/access_token" ID_KEY = "encodedId" EXTRA_DATA = [("encodedId", "id"), ("displayName", "username")] def get_user_details(self, response): """Return user details from Fitbit account""" return {"username": response.get("displayName"), "email": ""} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://api.fitbit.com/1/user/-/profile.json", auth=self.oauth_auth(access_token), )["user"] class FitbitOAuth2(BaseOAuth2): """Fitbit OAuth2 authentication backend""" name = "fitbit" AUTHORIZATION_URL = "https://www.fitbit.com/oauth2/authorize" ACCESS_TOKEN_URL = "https://api.fitbit.com/oauth2/token" REFRESH_TOKEN_URL = "https://api.fitbit.com/oauth2/token" DEFAULT_SCOPE = ["profile"] ID_KEY = "encodedId" REDIRECT_STATE = False EXTRA_DATA = [ ("expires_in", "expires"), ("refresh_token", "refresh_token", True), ("encodedId", "id"), ("displayName", "username"), ] def get_user_details(self, response): """Return user details from Fitbit account""" return {"username": response.get("displayName"), "email": ""} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" auth_header = {"Authorization": f"Bearer {access_token}"} return self.get_json( "https://api.fitbit.com/1/user/-/profile.json", headers=auth_header )["user"] def auth_headers(self): tokens = "{}:{}".format(*self.get_key_and_secret()) tokens = base64.urlsafe_b64encode(tokens.encode()) tokens = tokens.decode() return {"Authorization": f"Basic {tokens}"} social-auth-core-4.6.1/social_core/backends/five_hundred_px.py000066400000000000000000000022471500362547200244510ustar00rootroot00000000000000""" 500px OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/five_hundred_px.html """ from .oauth import BaseOAuth1 class FiveHundredPxOAuth(BaseOAuth1): """500px OAuth authentication backend""" name = "500px" AUTHORIZATION_URL = "https://api.500px.com/v1/oauth/authorize" REQUEST_TOKEN_URL = "https://api.500px.com/v1/oauth/request_token" ACCESS_TOKEN_URL = "https://api.500px.com/v1/oauth/access_token" def get_user_details(self, user): # type: ignore[reportIncompatibleMethodOverride] """Return user details from 500px account""" fullname, first_name, last_name = self.get_user_names(user.get("fullname")) return { "username": user.get("username") or user.get("id"), "email": user.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Return user data provided""" response = self.get_json( "https://api.500px.com/v1/users", auth=self.oauth_auth(access_token) ) return response.get("user") social-auth-core-4.6.1/social_core/backends/flat.py000066400000000000000000000017231500362547200222240ustar00rootroot00000000000000""" Flat OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/flat.html """ from .oauth import BaseOAuth2 class FlatOAuth2(BaseOAuth2): """Flat OAuth2""" name = "flat" DEFAULT_SCOPE = ["account.public_profile"] AUTHORIZATION_URL = "https://flat.io/auth/oauth" ACCESS_TOKEN_URL = "https://api.flat.io/oauth/access_token" def get_user_id(self, details, response): return response.get("id") def get_user_details(self, response): """Return user details from Flat account""" return { "email": response.get("email"), "username": response.get("username"), "fullname": response.get("printableName"), } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://api.flat.io/v2/me", headers={"Authorization": "Bearer " + access_token}, ) social-auth-core-4.6.1/social_core/backends/flickr.py000066400000000000000000000026371500362547200225550ustar00rootroot00000000000000""" Flickr OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/flickr.html """ from .oauth import BaseOAuth1 class FlickrOAuth(BaseOAuth1): """Flickr OAuth authentication backend""" name = "flickr" AUTHORIZATION_URL = "https://www.flickr.com/services/oauth/authorize" REQUEST_TOKEN_URL = "https://www.flickr.com/services/oauth/request_token" ACCESS_TOKEN_URL = "https://www.flickr.com/services/oauth/access_token" EXTRA_DATA = [("id", "id"), ("username", "username"), ("expires", "expires")] def get_user_details(self, response): """Return user details from Flickr account""" fullname, first_name, last_name = self.get_user_names(response.get("fullname")) return { "username": response.get("username") or response.get("id"), "email": "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return { "id": access_token["user_nsid"], "username": access_token["username"], "fullname": access_token.get("fullname", ""), } def auth_extra_arguments(self): params = super().auth_extra_arguments() or {} if "perms" not in params: params["perms"] = "read" return params social-auth-core-4.6.1/social_core/backends/foursquare.py000066400000000000000000000024371500362547200234750ustar00rootroot00000000000000""" Foursquare OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/foursquare.html """ from .oauth import BaseOAuth2 class FoursquareOAuth2(BaseOAuth2): name = "foursquare" AUTHORIZATION_URL = "https://foursquare.com/oauth2/authenticate" ACCESS_TOKEN_URL = "https://foursquare.com/oauth2/access_token" API_VERSION = "20140128" def get_user_id(self, details, response): return response["response"]["user"]["id"] def get_user_details(self, response): """Return user details from Foursquare account""" info = response["response"]["user"] email = info["contact"]["email"] fullname, first_name, last_name = self.get_user_names( first_name=info.get("firstName", ""), last_name=info.get("lastName", "") ) return { "username": first_name + " " + last_name, "fullname": fullname, "first_name": first_name, "last_name": last_name, "email": email, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://api.foursquare.com/v2/users/self", params={"oauth_token": access_token, "v": self.API_VERSION}, ) social-auth-core-4.6.1/social_core/backends/gae.py000066400000000000000000000023631500362547200220330ustar00rootroot00000000000000""" Google App Engine support using User API """ from google.appengine.api import users from ..exceptions import AuthException from .base import BaseAuth class GoogleAppEngineAuth(BaseAuth): """GoogleAppengine authentication backend""" name = "google-appengine" def get_user_id(self, details, response): """Return current user id.""" user = users.get_current_user() if user: return user.user_id() return None def get_user_details(self, response): """Return user basic information (id and email only).""" user = users.get_current_user() return { "username": user.user_id(), "email": user.email(), "fullname": "", "first_name": "", "last_name": "", } def auth_url(self): """Build and return complete URL.""" return users.create_login_url(self.redirect_uri) def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance.""" if not users.get_current_user(): raise AuthException(self, "Authentication error") kwargs.update({"response": "", "backend": self}) return self.strategy.authenticate(*args, **kwargs) social-auth-core-4.6.1/social_core/backends/gitea.py000066400000000000000000000030501500362547200223620ustar00rootroot00000000000000""" Gitea OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/gitea.html """ from .oauth import BaseOAuth2 class GiteaOAuth2(BaseOAuth2): """Gitea OAuth authentication backend""" name = "gitea" API_URL = "https://gitea.com" AUTHORIZATION_URL = "https://gitea.com/login/oauth/authorize" ACCESS_TOKEN_URL = "https://gitea.com/login/oauth/access_token" SCOPE_SEPARATOR = "," REDIRECT_STATE = False STATE_PARAMETER = True EXTRA_DATA = [ ("id", "id"), ("expires_in", "expires"), ("refresh_token", "refresh_token"), ] def api_url(self, path): api_url = self.setting("API_URL") or self.API_URL return "{}{}".format(api_url.rstrip("/"), path) def authorization_url(self): return self.api_url("/login/oauth/authorize") def access_token_url(self): return self.api_url("/login/oauth/access_token") def get_user_details(self, response): """Return user details from Gitea account""" fullname, first_name, last_name = self.get_user_names(response.get("fullname")) return { "username": response.get("login"), "email": response.get("email") or "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( self.api_url("/api/v1/user"), params={"access_token": access_token} ) social-auth-core-4.6.1/social_core/backends/github.py000066400000000000000000000120561500362547200225610ustar00rootroot00000000000000""" Github OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/github.html """ from urllib.parse import urljoin from requests import HTTPError from ..exceptions import AuthFailed from .oauth import BaseOAuth2 class GithubOAuth2(BaseOAuth2): """Github OAuth authentication backend""" name = "github" API_URL = "https://api.github.com/" AUTHORIZATION_URL = "https://github.com/login/oauth/authorize" ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token" SCOPE_SEPARATOR = "," REDIRECT_STATE = False STATE_PARAMETER = True SEND_USER_AGENT = True EXTRA_DATA = [ ("id", "id"), ("expires_in", "expires"), ("login", "login"), ("refresh_token", "refresh_token"), ] def api_url(self) -> str: return self.API_URL def get_user_details(self, response): """Return user details from Github account""" fullname, first_name, last_name = self.get_user_names(response.get("name")) return { "username": response.get("login"), "email": response.get("email") or "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" data = self._user_data(access_token) if not data.get("email"): try: emails = self._user_data(access_token, "/emails") except (HTTPError, ValueError, TypeError): emails = [] if emails: email = emails[0] primary_emails = [ e for e in emails if not isinstance(e, dict) or e.get("primary") ] if primary_emails: email = primary_emails[0] if isinstance(email, dict): email = email.get("email", "") data["email"] = email return data def _user_data(self, access_token, path=None): url = urljoin(self.api_url(), "user{}".format(path or "")) return self.get_json(url, headers={"Authorization": f"token {access_token}"}) class GithubMemberOAuth2(GithubOAuth2): no_member_string = "" def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" user_data = super().user_data(access_token, *args, **kwargs) headers = {"Authorization": f"token {access_token}"} try: self.request(self.member_url(user_data), headers=headers) except HTTPError as err: # if the user is a member of the organization, response code # will be 204, see http://bit.ly/ZS6vFl if err.response.status_code != 204: raise AuthFailed(self, "User doesn't belong to the organization") return user_data def member_url(self, user_data): raise NotImplementedError("Implement in subclass") class GithubOrganizationOAuth2(GithubMemberOAuth2): """Github OAuth2 authentication backend for organizations""" name = "github-org" no_member_string = "User doesn't belong to the organization" def member_url(self, user_data): return urljoin( self.api_url(), "orgs/{org}/members/{username}".format( org=self.setting("NAME"), username=user_data.get("login") ), ) class GithubTeamOAuth2(GithubMemberOAuth2): """Github OAuth2 authentication backend for teams""" name = "github-team" no_member_string = "User doesn't belong to the team" def member_url(self, user_data): return urljoin( self.api_url(), "teams/{team_id}/members/{username}".format( team_id=self.setting("ID"), username=user_data.get("login") ), ) class GithubAppAuth(GithubOAuth2): """GitHub App OAuth authentication backend""" name = "github-app" def validate_state(self): """ Scenario 1: user clicks an icon/button on your website and initiates social login. This works exacltly like standard OAuth and we have `state` and `redirect_uri`. Scenario 2: user starts from http://github.com/apps/your-app and clicks 'Install & Authorize' button! They still get a temporary `code` (used to fetch `access_token`) but there's no `state` or `redirect_uri` here. Note: Scenario 2 only happens when your GitHub App is configured with `Request user authorization (OAuth) during installation` turned on! This causes GitHub to redirect the person back to `/complete/github/`. If the above setting is turned off then GitHub will redirect to another URL called Setup URL and the person may need to login first before they can continue! """ if self.data.get("installation_id") and self.data.get("setup_action"): return None return super().validate_state() social-auth-core-4.6.1/social_core/backends/github_enterprise.py000066400000000000000000000025211500362547200250150ustar00rootroot00000000000000""" Github Enterprise OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/github_enterprise.html """ from urllib.parse import urljoin from ..utils import append_slash from .github import GithubOAuth2, GithubOrganizationOAuth2, GithubTeamOAuth2 class GithubEnterpriseMixin: def api_url(self): return append_slash(self.setting("API_URL")) # type: ignore[reportAttributeAccessIssue] def authorization_url(self): return self._url("login/oauth/authorize") def access_token_url(self): return self._url("login/oauth/access_token") def _url(self, path): return urljoin(append_slash(self.setting("URL")), path) # type: ignore[reportAttributeAccessIssue] class GithubEnterpriseOAuth2(GithubEnterpriseMixin, GithubOAuth2): """Github Enterprise OAuth authentication backend""" name = "github-enterprise" class GithubEnterpriseOrganizationOAuth2( GithubEnterpriseMixin, GithubOrganizationOAuth2 ): """Github Enterprise OAuth2 authentication backend for organizations""" name = "github-enterprise-org" DEFAULT_SCOPE = ["read:org"] class GithubEnterpriseTeamOAuth2(GithubEnterpriseMixin, GithubTeamOAuth2): """Github Enterprise OAuth2 authentication backend for teams""" name = "github-enterprise-team" DEFAULT_SCOPE = ["read:org"] social-auth-core-4.6.1/social_core/backends/gitlab.py000066400000000000000000000034131500362547200225360ustar00rootroot00000000000000""" GitLab OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/gitlab.html Thanks to [@saily](https://github.com/saily) who published an implementation for GitLab support on his blog post [Weblate with GitLab as OAuth provider](http://widerin.net/blog/weblate-gitlab-oauth-login/). His code was a great reference when working on this implementation. """ from .oauth import BaseOAuth2 class GitLabOAuth2(BaseOAuth2): """GitLab OAuth authentication backend""" name = "gitlab" API_URL = "https://gitlab.com" AUTHORIZATION_URL = "https://gitlab.com/oauth/authorize" ACCESS_TOKEN_URL = "https://gitlab.com/oauth/token" REDIRECT_STATE = False DEFAULT_SCOPE = ["read_user"] EXTRA_DATA = [ ("id", "id"), ("expires_in", "expires"), ("refresh_token", "refresh_token"), ] def api_url(self, path): api_url = self.setting("API_URL") or self.API_URL return "{}{}".format(api_url.rstrip("/"), path) def authorization_url(self): return self.api_url("/oauth/authorize") def access_token_url(self): return self.api_url("/oauth/token") def get_user_details(self, response): """Return user details from GitLab account""" fullname, first_name, last_name = self.get_user_names(response.get("name")) return { "username": response.get("username"), "email": response.get("email") or "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( self.api_url("/api/v4/user"), params={"access_token": access_token} ) social-auth-core-4.6.1/social_core/backends/globus.py000066400000000000000000000020141500362547200225630ustar00rootroot00000000000000""" Globus Auth OpenID Connect backend, docs at: https://docs.globus.org/api/auth http://globus-integration-examples.readthedocs.io """ from social_core.backends.open_id_connect import OpenIdConnectAuth class GlobusOpenIdConnect(OpenIdConnectAuth): name = "globus" OIDC_ENDPOINT = "https://auth.globus.org" JWT_ALGORITHMS = ["RS256", "RS512"] EXTRA_DATA = [ ("expires_in", "expires_in", True), ("refresh_token", "refresh_token", True), ("id_token", "id_token", True), ("other_tokens", "other_tokens", True), ] def get_user_details(self, response): username_key = self.setting("USERNAME_KEY", default=self.USERNAME_KEY) name = response.get("name") or "" fullname, first_name, last_name = self.get_user_names(name) return { "username": response.get(username_key), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } social-auth-core-4.6.1/social_core/backends/goclio.py000066400000000000000000000022631500362547200225520ustar00rootroot00000000000000from .oauth import BaseOAuth2 class GoClioOAuth2(BaseOAuth2): name = "goclio" AUTHORIZATION_URL = "https://app.goclio.com/oauth/authorize/" ACCESS_TOKEN_URL = "https://app.goclio.com/oauth/token/" REDIRECT_STATE = False STATE_PARAMETER = False def get_user_details(self, response): """Return user details from GoClio account""" user = response.get("user", {}) username = user.get("id", None) email = user.get("email", None) first_name, last_name = ( user.get("first_name", None), user.get("last_name", None), ) fullname = f"{first_name} {last_name}" return { "username": username, "fullname": fullname, "first_name": first_name, "last_name": last_name, "email": email, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://app.goclio.com/api/v2/users/who_am_i", params={"access_token": access_token}, ) def get_user_id(self, details, response): return response.get("user", {}).get("id") social-auth-core-4.6.1/social_core/backends/goclioeu.py000066400000000000000000000007241500362547200231040ustar00rootroot00000000000000from .goclio import GoClioOAuth2 class GoClioEuOAuth2(GoClioOAuth2): name = "goclioeu" AUTHORIZATION_URL = "https://app.goclio.eu/oauth/authorize/" ACCESS_TOKEN_URL = "https://app.goclio.eu/oauth/token/" def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://app.goclio.eu/api/v2/users/who_am_i", params={"access_token": access_token}, ) social-auth-core-4.6.1/social_core/backends/google.py000066400000000000000000000141271500362547200225540ustar00rootroot00000000000000""" Google OpenId, OAuth2, OAuth1, Google+ Sign-in backends, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/google.html """ from social_core.backends.base import BaseAuth from ..exceptions import AuthMissingParameter from ..utils import handle_http_errors from .oauth import BaseOAuth1, BaseOAuth2 class BaseGoogleAuth(BaseAuth): def get_user_id(self, details, response): """Use google email as unique id""" if self.setting("USE_UNIQUE_USER_ID", False): if "sub" in response: return response["sub"] return response["id"] return details["email"] def get_user_details(self, response): """Return user details from Google API account""" email = response.get("email", "") name, given_name, family_name = ( response.get("name", ""), response.get("given_name", ""), response.get("family_name", ""), ) fullname, first_name, last_name = self.get_user_names( name, given_name, family_name ) return { "username": email.split("@", 1)[0], "email": email, "fullname": fullname, "first_name": first_name, "last_name": last_name, } class BaseGoogleOAuth2API(BaseGoogleAuth): def user_data(self, access_token, *args, **kwargs): """Return user data from Google API""" return self.get_json( "https://www.googleapis.com/oauth2/v3/userinfo", headers={ "Authorization": f"Bearer {access_token}", }, ) def revoke_token_params(self, token, uid): return {"token": token} def revoke_token_headers(self, token, uid): return {"Content-type": "application/json"} class GoogleOAuth2(BaseGoogleOAuth2API, BaseOAuth2): """Google OAuth2 authentication backend""" name = "google-oauth2" REDIRECT_STATE = False AUTHORIZATION_URL = "https://accounts.google.com/o/oauth2/auth" ACCESS_TOKEN_URL = "https://accounts.google.com/o/oauth2/token" REVOKE_TOKEN_URL = "https://accounts.google.com/o/oauth2/revoke" REVOKE_TOKEN_METHOD = "GET" # The order of the default scope is important DEFAULT_SCOPE = ["openid", "email", "profile"] EXTRA_DATA = [ ("refresh_token", "refresh_token", True), ("expires_in", "expires"), ("token_type", "token_type", True), ] class GooglePlusAuth(BaseGoogleOAuth2API, BaseOAuth2): name = "google-plus" REDIRECT_STATE = False STATE_PARAMETER = False AUTHORIZATION_URL = "https://accounts.google.com/o/oauth2/auth" ACCESS_TOKEN_URL = "https://accounts.google.com/o/oauth2/token" REVOKE_TOKEN_URL = "https://accounts.google.com/o/oauth2/revoke" REVOKE_TOKEN_METHOD = "GET" DEFAULT_SCOPE = [ "https://www.googleapis.com/auth/plus.login", "https://www.googleapis.com/auth/plus.me", ] EXTRA_DATA = [ ("id", "user_id"), ("refresh_token", "refresh_token", True), ("expires_in", "expires"), ("access_type", "access_type", True), ("code", "code"), ] def auth_complete_params(self, state=None): params = super().auth_complete_params(state) if self.data.get("access_token"): # Don't add postmessage if this is plain server-side workflow params["redirect_uri"] = "postmessage" return params @handle_http_errors def auth_complete(self, *args, **kwargs): if "access_token" in self.data: # Client-side workflow token = self.data.get("access_token") response = self.get_json( "https://www.googleapis.com/oauth2/v3/tokeninfo", params={"access_token": token}, ) self.process_error(response) return self.do_auth(token, response=response, *args, **kwargs) if "code" in self.data: # Server-side workflow response = self.request_access_token( self.ACCESS_TOKEN_URL, data=self.auth_complete_params(), headers=self.auth_headers(), method=self.ACCESS_TOKEN_METHOD, ) self.process_error(response) return self.do_auth( response["access_token"], response=response, *args, **kwargs ) if "id_token" in self.data: # Client-side workflow token = self.data.get("id_token") return self.do_auth(token, *args, **kwargs) raise AuthMissingParameter(self, "access_token, id_token, or code") def user_data(self, access_token, *args, **kwargs): if "id_token" not in self.data: return super().user_data(access_token, *args, **kwargs) response = self.get_json( "https://www.googleapis.com/oauth2/v3/tokeninfo", params={"id_token": access_token}, ) self.process_error(response) return response class GoogleOAuth(BaseGoogleAuth, BaseOAuth1): """Google OAuth authorization mechanism""" name = "google-oauth" AUTHORIZATION_URL = "https://www.google.com/accounts/OAuthAuthorizeToken" REQUEST_TOKEN_URL = "https://www.google.com/accounts/OAuthGetRequestToken" ACCESS_TOKEN_URL = "https://www.google.com/accounts/OAuthGetAccessToken" DEFAULT_SCOPE = ["https://www.googleapis.com/auth/userinfo#email"] def user_data(self, access_token, *args, **kwargs): """Return user data from Google API""" return self.get_querystring( "https://www.googleapis.com/userinfo/email", auth=self.oauth_auth(access_token), ) def get_key_and_secret(self): """Return Google OAuth Consumer Key and Consumer Secret pair, uses anonymous by default, beware that this marks the application as not registered and a security badge is displayed on authorization page. http://code.google.com/apis/accounts/docs/OAuth_ref.html#SigningOAuth """ key_secret = super().get_key_and_secret() if key_secret == (None, None): key_secret = ("anonymous", "anonymous") return key_secret social-auth-core-4.6.1/social_core/backends/google_onetap.py000066400000000000000000000033631500362547200241220ustar00rootroot00000000000000from google.auth.transport import requests as transport_requests from google.oauth2 import id_token from social_core.backends.base import BaseAuth from social_core.backends.google import BaseGoogleAuth from social_core.exceptions import AuthException, AuthTokenError class GoogleOneTap(BaseGoogleAuth, BaseAuth): name = "google-onetap" CSRF_KEY = "g_csrf_token" CREDENTIAL_KEY = "credential" def auth_url(self): raise AuthException(self, "Cannot start login flow for Google One Tap") def verify_csrf(self, request): csrf_token_body = self.data.get(self.CSRF_KEY) csrf_token_cookie = request.COOKIES.get(self.CSRF_KEY) if not csrf_token_body: raise AuthTokenError(self, "Missing csrf token from response") # csrf_token_cookie can be missing due to https://issuetracker.google.com/issues/226157137 if not csrf_token_cookie and self.setting("IGNORE_MISSING_CSRF_COOKIE", False): return if csrf_token_body != csrf_token_cookie: raise AuthTokenError( self, "csrf token from cookie and response does not match" ) def get_decoded_info(self): try: idinfo = id_token.verify_oauth2_token( self.data.get(self.CREDENTIAL_KEY), transport_requests.Request(), self.setting("KEY"), ) except ValueError: raise AuthException(self, "Invalid response from Google") return idinfo def auth_complete(self, *args, **kwargs): self.verify_csrf(kwargs["request"]) response = self.get_decoded_info() kwargs.update({"response": response, "backend": self}) return self.strategy.authenticate(*args, **kwargs) social-auth-core-4.6.1/social_core/backends/google_openidconnect.py000066400000000000000000000014111500362547200254540ustar00rootroot00000000000000""" Google OpenIdConnect: https://python-social-auth.readthedocs.io/en/latest/backends/google.html """ from .google import GoogleOAuth2 from .open_id_connect import OpenIdConnectAuth class GoogleOpenIdConnect(GoogleOAuth2, OpenIdConnectAuth): name = "google-openidconnect" OIDC_ENDPOINT = "https://accounts.google.com" # differs from value in discovery document # http://openid.net/specs/openid-connect-core-1_0.html#rfc.section.15.6.2 ID_TOKEN_ISSUER = "accounts.google.com" def user_data(self, access_token, *args, **kwargs): """Return user data from Google API""" return self.get_json( "https://openidconnect.googleapis.com/v1/userinfo", params={"access_token": access_token, "alt": "json"}, ) social-auth-core-4.6.1/social_core/backends/grafana.py000066400000000000000000000016631500362547200227000ustar00rootroot00000000000000from social_core.backends.oauth import BaseOAuth2 class GrafanaOAuth2(BaseOAuth2): """Grafana OAuth authentication backend""" name = "grafana" AUTHORIZATION_URL = "https://grafana.com/oauth2/authorize" ACCESS_TOKEN_URL = "https://grafana.com/api/oauth2/token" DEFAULT_SCOPE = ["profile", "email"] SCOPE_SEPARATOR = "," USER_DETAILS_URL = "https://grafana.com/api/oauth2/user" def get_user_details(self, response): """Return user details from Grafana account""" return { "username": response.get("login"), "email": response.get("email") or "", "first_name": response.get("name"), "last_name": "-", } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( self.USER_DETAILS_URL, headers={"Authorization": f"Bearer {access_token}"}, ) social-auth-core-4.6.1/social_core/backends/hubspot.py000066400000000000000000000021401500362547200227540ustar00rootroot00000000000000""" HubSpot OAuth2 backend, docs at: https://developers.hubspot.com/docs/methods/oauth2/oauth2-overview """ from .oauth import BaseOAuth2 class HubSpotOAuth2(BaseOAuth2): """HubSpot OAuth2 authentication backend""" name = "hubspot" AUTHORIZATION_URL = "https://app.hubspot.com/oauth/authorize" ACCESS_TOKEN_URL = "https://api.hubapi.com/oauth/v1/token" USER_DATA_URL = "https://api.hubapi.com/oauth/v1/access-tokens/" DEFAULT_SCOPE = ["oauth"] EXTRA_DATA = [ ("hub_domain", "hub_domain"), ("hub_id", "hub_id"), ("app_id", "app_id"), ("user_id", "user_id"), ("refresh_token", "refresh_token"), ("expires_in", "expires"), ] def get_user_details(self, response): """Return user details""" response["email"] = response["user"] return response def user_data(self, access_token, *args, **kwargs): """Loads user data information from service""" return self.get_json( self.USER_DATA_URL + access_token, headers={"Authorization": "Bearer " + access_token}, ) social-auth-core-4.6.1/social_core/backends/instagram.py000066400000000000000000000027401500362547200232630ustar00rootroot00000000000000""" Instagram OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/instagram.html """ from .oauth import BaseOAuth2 class InstagramOAuth2(BaseOAuth2): name = "instagram" AUTHORIZATION_URL = "https://api.instagram.com/oauth/authorize" ACCESS_TOKEN_URL = "https://api.instagram.com/oauth/access_token" def get_user_id(self, details, response): user = response.get("user") or {} return user.get("id") def get_user_details(self, response): """Return user details from Instagram account""" user = response.get("user") or {} username = user["username"] email = user.get("email", "") fullname, first_name, last_name = self.get_user_names(user.get("full_name", "")) return { "username": username, "fullname": fullname, "first_name": first_name, "last_name": last_name, "email": email, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" # For more fields see: # https://developers.facebook.com/docs/instagram-basic-display-api/reference/user#fields # In fact there are not very many of them. fields = "id,username" params = {"access_token": access_token, "fields": fields} response = self.get_json("https://graph.instagram.com/me", params=params) return {"user": response} def auth_html(self): pass social-auth-core-4.6.1/social_core/backends/itembase.py000066400000000000000000000065001500362547200230650ustar00rootroot00000000000000import time from ..utils import handle_http_errors from .oauth import BaseOAuth2 class ItembaseOAuth2(BaseOAuth2): name = "itembase" ID_KEY = "uuid" AUTHORIZATION_URL = "https://accounts.itembase.com/oauth/v2/auth" ACCESS_TOKEN_URL = "https://accounts.itembase.com/oauth/v2/token" USER_DETAILS_URL = "https://users.itembase.com/v1/me" ACTIVATION_ENDPOINT = "https://solutionservice.itembase.com/activate" DEFAULT_SCOPE = ["user.minimal"] EXTRA_DATA = [ ("access_token", "access_token"), ("token_type", "token_type"), ("refresh_token", "refresh_token"), ("expires_in", "expires_in"), # seconds to expiration ("expires", "expires"), # expiration timestamp in UTC ("uuid", "uuid"), ("username", "username"), ("email", "email"), ("first_name", "first_name"), ("middle_name", "middle_name"), ("last_name", "last_name"), ("name_format", "name_format"), ("locale", "locale"), ("preferred_currency", "preferred_currency"), ] def add_expires(self, data): data["expires"] = int(time.time()) + data.get("expires_in", 0) return data def extra_data(self, user, uid, response, details=None, *args, **kwargs): data = BaseOAuth2.extra_data( self, user, uid, response, details=details, *args, **kwargs ) return self.add_expires(data) def process_refresh_token_response(self, response, *args, **kwargs): data = BaseOAuth2.process_refresh_token_response( self, response, *args, **kwargs ) return self.add_expires(data) def get_user_details(self, response): """Return user details from Itembase account""" return response def user_data(self, access_token, *args, **kwargs): return self.get_json( self.USER_DETAILS_URL, headers={"Authorization": f"Bearer {access_token}"} ) def activation_data(self, response): # returns activation_data dict with activation_url inside # see http://developers.itembase.com/authentication/activation return self.get_json( self.ACTIVATION_ENDPOINT, headers={"Authorization": "Bearer {}".format(response["access_token"])}, ) @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" state = self.validate_state() self.process_error(self.data) # itembase needs GET request with params instead of just data response = self.request_access_token( self.access_token_url(), params=self.auth_complete_params(state), headers=self.auth_headers(), auth=self.auth_complete_credentials(), method=self.ACCESS_TOKEN_METHOD, ) self.process_error(response) return self.do_auth( response["access_token"], response=response, *args, **kwargs ) class ItembaseOAuth2Sandbox(ItembaseOAuth2): name = "itembase-sandbox" AUTHORIZATION_URL = "http://sandbox.accounts.itembase.io/oauth/v2/auth" ACCESS_TOKEN_URL = "http://sandbox.accounts.itembase.io/oauth/v2/token" USER_DETAILS_URL = "http://sandbox.users.itembase.io/v1/me" ACTIVATION_ENDPOINT = "http://sandbox.solutionservice.itembase.io/activate" social-auth-core-4.6.1/social_core/backends/jawbone.py000066400000000000000000000051731500362547200227260ustar00rootroot00000000000000""" Jawbone OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/jawbone.html """ from ..exceptions import AuthCanceled, AuthUnknownError from ..utils import handle_http_errors from .oauth import BaseOAuth2 class JawboneOAuth2(BaseOAuth2): name = "jawbone" AUTHORIZATION_URL = "https://jawbone.com/auth/oauth2/auth" ACCESS_TOKEN_URL = "https://jawbone.com/auth/oauth2/token" SCOPE_SEPARATOR = " " REDIRECT_STATE = False def get_user_id(self, details, response): return response["data"]["xid"] def get_user_details(self, response): """Return user details from Jawbone account""" data = response["data"] fullname, first_name, last_name = self.get_user_names( first_name=data.get("first", ""), last_name=data.get("last", "") ) return { "username": first_name + " " + last_name, "fullname": fullname, "first_name": first_name, "last_name": last_name, "dob": data.get("dob", ""), "gender": data.get("gender", ""), "height": data.get("height", ""), "weight": data.get("weight", ""), } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://jawbone.com/nudge/api/users/@me", headers={"Authorization": "Bearer " + access_token}, ) def process_error(self, data): error = data.get("error") if error: if error == "access_denied": raise AuthCanceled(self) raise AuthUnknownError(self, f"Jawbone error was {error}") return super().process_error(data) def auth_complete_params(self, state=None): client_id, client_secret = self.get_key_and_secret() return { "grant_type": "authorization_code", # request auth code "code": self.data.get("code", ""), # server response code "client_id": client_id, "client_secret": client_secret, } @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" self.process_error(self.data) response = self.request_access_token( self.ACCESS_TOKEN_URL, params=self.auth_complete_params(self.validate_state()), headers=self.auth_headers(), method=self.ACCESS_TOKEN_METHOD, ) self.process_error(response) return self.do_auth( response["access_token"], response=response, *args, **kwargs ) social-auth-core-4.6.1/social_core/backends/justgiving.py000066400000000000000000000041211500362547200234620ustar00rootroot00000000000000from requests.auth import HTTPBasicAuth from ..utils import handle_http_errors from .oauth import BaseOAuth2 class JustGivingOAuth2(BaseOAuth2): """Just Giving OAuth authentication backend""" name = "justgiving" ID_KEY = "userId" AUTHORIZATION_URL = "https://identity.justgiving.com/connect/authorize" ACCESS_TOKEN_URL = "https://identity.justgiving.com/connect/token" USER_DATA_URL = "https://api.justgiving.com/v1/account" DEFAULT_SCOPE = ["openid", "account", "profile", "email", "fundraise"] def get_user_details(self, response): """Return user details from Just Giving account""" fullname, first_name, last_name = self.get_user_names( "", response.get("firstName"), response.get("lastName") ) return { "username": response.get("email"), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" key, secret = self.get_key_and_secret() return self.get_json( self.USER_DATA_URL, headers={ "Authorization": f"Bearer {access_token}", "Content-Type": "application/json", "x-application-key": secret, "x-api-key": key, }, ) @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" state = self.validate_state() self.process_error(self.data) key, secret = self.get_key_and_secret() response = self.request_access_token( self.access_token_url(), data=self.auth_complete_params(state), headers=self.auth_headers(), auth=HTTPBasicAuth(key, secret), method=self.ACCESS_TOKEN_METHOD, ) self.process_error(response) return self.do_auth( response["access_token"], response=response, *args, **kwargs ) social-auth-core-4.6.1/social_core/backends/kakao.py000066400000000000000000000035641500362547200223710ustar00rootroot00000000000000""" Kakao OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/kakao.html """ from .oauth import BaseOAuth2 class KakaoOAuth2(BaseOAuth2): """Kakao OAuth authentication backend""" name = "kakao" AUTHORIZATION_URL = "https://kauth.kakao.com/oauth/authorize" ACCESS_TOKEN_URL = "https://kauth.kakao.com/oauth/token" REDIRECT_STATE = False EXTRA_DATA = [ ("properties", "properties"), ] def get_user_id(self, details, response): return response["id"] def get_user_details(self, response): """Return user details from Kakao account""" kaccount_email = "" kakao_account = response.get("kakao_account", "") if kakao_account: kaccount_email = kakao_account.get("email", "") properties = response.get("properties", "") nickname = properties.get("nickname") if properties else "" return { "username": nickname, "email": kaccount_email, "fullname": nickname, "first_name": nickname[1:] if nickname else "", "last_name": nickname[0] if nickname else "", } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://kapi.kakao.com/v2/user/me", headers={ "Authorization": f"Bearer {access_token}", "Content_Type": "application/x-www-form-urlencoded;charset=utf-8", }, params={"access_token": access_token}, ) def auth_complete_params(self, state=None): client_id, client_secret = self.get_key_and_secret() return { "grant_type": "authorization_code", "code": self.data.get("code", ""), "client_id": client_id, "client_secret": client_secret, } social-auth-core-4.6.1/social_core/backends/keycloak.py000066400000000000000000000122731500362547200231020ustar00rootroot00000000000000import jwt from social_core.backends.oauth import BaseOAuth2 class KeycloakOAuth2(BaseOAuth2): # pylint: disable=abstract-method """Keycloak OAuth2 authentication backend This backend has been tested working with a standard Keycloak installation, but you might have to specialize it and tune the parameters per your configuration. This setup specializes the OAuth2 backend which, strictly speaking, offers authorization without authentication capabilities. Keycloak does offer a full OpenID Connect implementation, but the implementation is rather labor intensive to implement. This backend is configured to get an access token instead, and assume that the access token contains the necessary user details for authentication. The integrity of the authentication process is followed by public key verification for the `access_token` along with OpenID Connect specification `aud` field checking. To set up, please take the following steps: 1. Create a new Keycloak client in the Clients section: a. Choose the `Client ID` in the `General Settings` pane. b. Select `Client authentication` and `Authorization` in the `Capability config` pane. 2. Configure the following parameters in the Client setup: Settings > Client ID (copy to settings as `KEY` value) Credentials > Client Authenticator > Use `Client Id and Secret` and copy the `Client secret` value to settings as `SECRET` value 3. For the tokens to work with the JWT setup the following configuration has to be made in Keycloak: Advanced > Fine grain OpenID Connect configuration > User Info Signed Response Algorithm > RS256 Advanced > Fine grain OpenID Connect configuration > Request Object Signature Algorithm > RS256 4. Re-enable the audience (see https://issues.redhat.com/browse/KEYCLOAK-6638 for context): Go to Client scopes > YOUR-CLIENT-ID-dedicated > Add mapper > Audience, pick a name for the mapper and select the Client ID corresponding to your client in `Included Client Audience`. 5. Get the public key (copy to settings as `PUBLIC_KEY` value) to be used with the backend: Realm Settings > Keys > Public key 6. Configure access token fields are configured via the Keycloak Client mappers: Clients > Client ID > Mappers They have to include at least the `ID_KEY` value and the dictionary keys defined in the `get_user_details` method. 7. Configure your web backend. Example setting values for Django settings could be: SOCIAL_AUTH_KEYCLOAK_KEY = 'example' SOCIAL_AUTH_KEYCLOAK_SECRET = '1234abcd-1234-abcd-1234-abcd1234adcd' SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY = \ 'pempublickeythatis2048bitsinbase64andhaseg392characters' SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL = \ 'https://sso.com/auth/realms/example/protocol/openid-connect/auth' SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL = \ 'https://sso.com/auth/realms/example/protocol/openid-connect/token' 8. The default behaviour is to associate users via username field, but you can change the key with e.g. SOCIAL_AUTH_KEYCLOAK_ID_KEY = 'email' Please make sure your Keycloak user database and Django user database do not conflict and that there is no risk of user account hijacking by false account association. """ name = "keycloak" ID_KEY = "username" REDIRECT_STATE = False def authorization_url(self): return self.setting("AUTHORIZATION_URL") def access_token_url(self): return self.setting("ACCESS_TOKEN_URL") def audience(self): return self.setting("KEY") def algorithm(self): return self.setting("ALGORITHM", default="RS256") def public_key(self): return "\n".join( [ "-----BEGIN PUBLIC KEY-----", self.setting("PUBLIC_KEY"), "-----END PUBLIC KEY-----", ] ) def user_data(self, access_token, *args, **kwargs): # pylint: disable=unused-argument """Decode user data from the access_token You can specialize this method to e.g. get information from the Keycloak backend if you do not want to include the user information in the access_token. """ return jwt.decode( access_token, key=self.public_key(), algorithms=self.algorithm(), audience=self.audience(), ) def get_user_details(self, response): """Map fields in user_data into Django User fields""" return { "username": response.get("preferred_username"), "email": response.get("email"), "fullname": response.get("name"), "first_name": response.get("given_name"), "last_name": response.get("family_name"), } def get_user_id(self, details, response): """Get and associate Django User by the field indicated by ID_KEY""" return details.get(self.ID_KEY) social-auth-core-4.6.1/social_core/backends/kick.py000066400000000000000000000041251500362547200222160ustar00rootroot00000000000000""" Kick OAuth2 backend, docs at: https://docs.kick.com/getting-started/generating-tokens-oauth2-flow """ from .oauth import BaseOAuth2PKCE class KickOAuth2(BaseOAuth2PKCE): """Kick OAuth2 authentication backend""" name = "kick" HOSTNAME = "id.kick.com" API_HOSTNAME = "api.kick.com" AUTHORIZATION_URL = f"https://{HOSTNAME}/oauth/authorize" ACCESS_TOKEN_URL = f"https://{HOSTNAME}/oauth/token" REFRESH_TOKEN_URL = f"https://{HOSTNAME}/oauth/token" REVOKE_TOKEN_URL = f"https://{HOSTNAME}/oauth/revoke" DEFAULT_SCOPE = ["user:read"] SCOPE_SEPARATOR = " " PKCE_DEFAULT_CODE_CHALLENGE_METHOD = "S256" EXTRA_DATA = [ ("access_token", "access_token"), ("refresh_token", "refresh_token"), ("expires_in", "expires"), ("token_type", "token_type"), ("scope", "scope"), ] def get_user_id(self, details, response): """ Use Kick user id as unique id """ return response.get("user_id") def get_user_details(self, response): """Return user details from Kick account""" return { "username": response.get( "name" ), # API returns 'name' instead of 'username' "email": response.get("email") or "", "fullname": response.get("name") or "", # Using 'name' as fullname "first_name": "", "last_name": "", "profile_picture": response.get("profile_picture") or "", "user_id": response.get("user_id"), } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" url = f"https://{self.API_HOSTNAME}/public/v1/users" auth_header = {"Authorization": f"Bearer {access_token}"} response = self.get_json(url, headers=auth_header) # The API returns data in a 'data' field with an array of users # For the authenticated user (when no user IDs are specified), we get the first item if response and "data" in response and response["data"]: return response["data"][0] return {} social-auth-core-4.6.1/social_core/backends/lastfm.py000066400000000000000000000037231500362547200225660ustar00rootroot00000000000000import hashlib from ..utils import handle_http_errors from .base import BaseAuth class LastFmAuth(BaseAuth): """ Last.Fm authentication backend. Requires two settings: SOCIAL_AUTH_LASTFM_KEY SOCIAL_AUTH_LASTFM_SECRET Don't forget to set the Last.fm callback to something sensible like http://your.site/lastfm/complete """ name = "lastfm" AUTH_URL = "http://www.last.fm/api/auth/?api_key={api_key}" EXTRA_DATA = [("key", "session_key")] def auth_url(self): return self.AUTH_URL.format(api_key=self.setting("KEY")) @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" key, secret = self.get_key_and_secret() token = self.data["token"] # Usage of md5 is mandated by the API: https://www.last.fm/api/webauth signature = hashlib.md5( # noqa: S324 f"api_key{key}methodauth.getSessiontoken{token}{secret}".encode() ).hexdigest() response = self.get_json( "http://ws.audioscrobbler.com/2.0/", data={ "method": "auth.getSession", "api_key": key, "token": token, "api_sig": signature, "format": "json", }, method="POST", ) kwargs.update({"response": response["session"], "backend": self}) return self.strategy.authenticate(*args, **kwargs) def get_user_id(self, details, response): """Return a unique ID for the current user, by default from server response.""" return response.get("name") def get_user_details(self, response): fullname, first_name, last_name = self.get_user_names(response["name"]) return { "username": response["name"], "email": "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } social-auth-core-4.6.1/social_core/backends/launchpad.py000066400000000000000000000003041500362547200232270ustar00rootroot00000000000000""" Launchpad OpenId backend """ from .open_id import OpenIdAuth class LaunchpadOpenId(OpenIdAuth): name = "launchpad" URL = "https://login.launchpad.net" USERNAME_KEY = "nickname" social-auth-core-4.6.1/social_core/backends/legacy.py000066400000000000000000000026721500362547200225460ustar00rootroot00000000000000from ..exceptions import AuthMissingParameter from .base import BaseAuth class LegacyAuth(BaseAuth): def get_user_id(self, details, response): return details.get(self.ID_KEY) or response.get(self.ID_KEY) def auth_url(self): return self.setting("FORM_URL") def auth_html(self): return self.strategy.render_html(tpl=self.setting("FORM_HTML")) def uses_redirect(self): return self.setting("FORM_URL") and not self.setting("FORM_HTML") def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" if self.ID_KEY not in self.data: raise AuthMissingParameter(self, self.ID_KEY) kwargs.update({"response": self.data, "backend": self}) return self.strategy.authenticate(*args, **kwargs) def get_user_details(self, response): """Return user details""" email = response.get("email", "") username = response.get("username", "") fullname, first_name, last_name = self.get_user_names( response.get("fullname", ""), response.get("first_name", ""), response.get("last_name", ""), ) if email and not username: username = email.split("@", 1)[0] return { "username": username, "email": email, "fullname": fullname, "first_name": first_name, "last_name": last_name, } social-auth-core-4.6.1/social_core/backends/lifescience.py000066400000000000000000000023071500362547200235460ustar00rootroot00000000000000""" Backend for OpenID Connect Life Science AAI https://lifescience-ri.eu/ls-login.html """ from social_core.backends.open_id_connect import OpenIdConnectAuth class LifeScienceOpenIdConnect(OpenIdConnectAuth): name = "life_science" OIDC_ENDPOINT = "https://login.aai.lifescience-ri.eu/oidc" EXTRA_DATA = [ ("expires_in", "expires_in", True), ("refresh_token", "refresh_token", True), ("id_token", "id_token", True), ("other_tokens", "other_tokens", True), ] # In order to get any scopes, you have to register your service with # Life science, see documentation at # https://lifescience-ri.eu/ls-login.html DEFAULT_SCOPE = ["openid", "email"] JWT_DECODE_OPTIONS = {"verify_at_hash": False} def get_user_details(self, response): username_key = self.setting("USERNAME_KEY", default=self.USERNAME_KEY) name = response.get("name") or "" fullname, first_name, last_name = self.get_user_names(name) return { "username": response.get(username_key), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } social-auth-core-4.6.1/social_core/backends/line.py000066400000000000000000000065661500362547200222370ustar00rootroot00000000000000""" LINE Login OAuth2 backend, docs at: https://developers.line.me/en/docs/line-login/ """ import json import requests from ..exceptions import AuthFailed from ..utils import handle_http_errors from .oauth import BaseOAuth2 class LineOAuth2(BaseOAuth2): name = "line" AUTHORIZATION_URL = "https://access.line.me/oauth2/v2.1/authorize" ACCESS_TOKEN_URL = "https://api.line.me/oauth2/v2.1/token" BASE_API_URL = "https://api.line.me" USER_INFO_URL = BASE_API_URL + "/v2/profile" STATE_PARAMETER = True DEFAULT_SCOPE = ["profile"] REDIRECT_STATE = True ID_KEY = "userId" EXTRA_DATA = [ ("userId", "id"), ("picture_url", "picture_url"), ("status_message", "status_message"), ("expires_in", "expire"), ("refresh_token", "refresh_token"), ] def auth_params(self, state=None): client_id, client_secret = self.get_key_and_secret() return { "response_type": self.RESPONSE_TYPE, "client_id": client_id, "redirect_uri": self.get_redirect_uri(), "state": self.get_or_create_state(), "scope": self.get_scope(), } def process_error(self, data): error_code = ( data.get("errorCode") or data.get("statusCode") or data.get("error") ) error_message = data.get("errorMessage") or data.get("error_description") if error_code is not None or error_message is not None: raise AuthFailed(self, error_message or error_code) @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" self.process_error(self.data) try: response = self.request_access_token( self.access_token_url(), method=self.ACCESS_TOKEN_METHOD, headers=self.auth_headers(), data=self.auth_complete_params(), ) self.process_error(response) return self.do_auth( response["access_token"], response=response, *args, **kwargs ) except requests.HTTPError as err: self.process_error(json.loads(err.response.content)) def get_user_details(self, response): fullname, first_name, last_name = self.get_user_names( response.get("displayName") ) username = response.get("userId") picture_url = response.get("pictureUrl") status_message = response.get("statusMessage") return { "username": username, "fullname": fullname, "first_name": first_name, "last_name": last_name, "picture_url": picture_url, "status_message": status_message, } def get_user_id(self, details, response): """ Return a unique ID for the current user, by default from server response. """ return response.get(self.ID_KEY) def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" try: response = self.get_json( self.USER_INFO_URL, headers={"Authorization": f"Bearer {access_token}"} ) self.process_error(response) except requests.HTTPError as err: self.process_error(err.response.json()) return None return response social-auth-core-4.6.1/social_core/backends/linkedin.py000066400000000000000000000123771500362547200231020ustar00rootroot00000000000000""" LinkedIn OAuth1 and OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/linkedin.html """ import datetime from calendar import timegm from social_core.backends.open_id_connect import OpenIdConnectAuth from social_core.exceptions import AuthCanceled, AuthTokenError from .oauth import BaseOAuth2 class LinkedinOpenIdConnect(OpenIdConnectAuth): """ Linkedin OpenID Connect backend. Oauth2 has been deprecated as of August 1, 2023. https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2?context=linkedin/consumer/context """ name = "linkedin-openidconnect" # Settings from https://www.linkedin.com/oauth/.well-known/openid-configuration OIDC_ENDPOINT = "https://www.linkedin.com/oauth" # https://developer.okta.com/docs/reference/api/oidc/#response-example-success-9 # Override this value as it is not provided by Linkedin. # else our request falls back to basic auth which is not supported. TOKEN_ENDPOINT_AUTH_METHOD = "client_secret_post" def validate_claims(self, id_token): """Copy of the regular validate_claims method without the nonce validation.""" utc_timestamp = timegm(datetime.datetime.now(datetime.timezone.utc).timetuple()) if "nbf" in id_token and utc_timestamp < id_token["nbf"]: raise AuthTokenError(self, "Incorrect id_token: nbf") # Verify the token was issued in the last 10 minutes iat_leeway = self.setting("ID_TOKEN_MAX_AGE", self.ID_TOKEN_MAX_AGE) if utc_timestamp > id_token["iat"] + iat_leeway: raise AuthTokenError(self, "Incorrect id_token: iat") # Skip the nonce validation for linkedin as it does not provide any nonce. # https://stackoverflow.com/questions/76889585/issues-with-sign-in-with-linkedin-using-openid-connect class LinkedinOAuth2(BaseOAuth2): name = "linkedin-oauth2" AUTHORIZATION_URL = "https://www.linkedin.com/oauth/v2/authorization" ACCESS_TOKEN_URL = "https://www.linkedin.com/oauth/v2/accessToken" USER_DETAILS_URL = "https://api.linkedin.com/v2/userinfo?projection=({projection})" USER_EMAILS_URL = ( "https://api.linkedin.com/v2/emailAddress" "?q=members&projection=(elements*(handle~))" ) REDIRECT_STATE = False DEFAULT_SCOPE = ["email", "profile", "openid"] EXTRA_DATA = [ ("id", "id"), ("expires_in", "expires"), ("firstName", "first_name"), ("lastName", "last_name"), ("refresh_token", "refresh_token"), ("refresh_token_expires_in", "refresh_expires_in"), ] def user_details_url(self): return self.USER_DETAILS_URL def user_emails_url(self): return self.USER_EMAILS_URL def user_data(self, access_token, *args, **kwargs): response = self.get_json( self.user_details_url(), headers=self.user_data_headers(access_token) ) if "email" in set(self.setting("FIELD_SELECTORS", [])): emails = self.email_data(access_token, *args, **kwargs) if emails: response["email"] = emails[0] return response def email_data(self, access_token, *args, **kwargs): response = self.get_json( self.user_emails_url(), headers=self.user_data_headers(access_token) ) email_addresses = [] for element in response.get("elements", []): email_address = element.get("handle~", {}).get("email") email_addresses.append(email_address) return list(filter(None, email_addresses)) def get_user_details(self, response): """Return user details from Linkedin account""" response = self.user_data(access_token=response["access_token"]) fullname, first_name, last_name = self.get_user_names( first_name=response["given_name"], last_name=response["family_name"], ) email = response.get("email", "") return { "id": response.get("sub", ""), "username": first_name + last_name, "fullname": fullname, "first_name": first_name, "last_name": last_name, "email": email, } def user_data_headers(self, access_token): headers = {} lang = self.setting("FORCE_PROFILE_LANGUAGE") if lang: headers["Accept-Language"] = ( lang if lang is not True else self.strategy.get_language() ) headers["Authorization"] = f"Bearer {access_token}" return headers def request_access_token(self, *args, **kwargs): # LinkedIn expects a POST request with querystring parameters, despite # the spec http://tools.ietf.org/html/rfc6749#section-4.1.3 kwargs["params"] = kwargs.pop("data") return super().request_access_token(*args, **kwargs) def process_error(self, data): super().process_error(data) if data.get("serviceErrorCode"): raise AuthCanceled(self, data.get("message") or data.get("status")) class LinkedinMobileOAuth2(LinkedinOAuth2): name = "linkedin-mobile-oauth2" def user_data_headers(self, access_token): headers = super().user_data_headers(access_token) headers["x-li-src"] = "msdk" return headers social-auth-core-4.6.1/social_core/backends/live.py000066400000000000000000000027731500362547200222430ustar00rootroot00000000000000""" Live OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/live.html """ from .oauth import BaseOAuth2 class LiveOAuth2(BaseOAuth2): name = "live" AUTHORIZATION_URL = "https://login.live.com/oauth20_authorize.srf" ACCESS_TOKEN_URL = "https://login.live.com/oauth20_token.srf" SCOPE_SEPARATOR = "," DEFAULT_SCOPE = ["wl.basic", "wl.emails"] EXTRA_DATA = [ ("id", "id"), ("access_token", "access_token"), ("authentication_token", "authentication_token"), ("refresh_token", "refresh_token"), ("expires_in", "expires"), ("email", "email"), ("first_name", "first_name"), ("last_name", "last_name"), ("token_type", "token_type"), ] REDIRECT_STATE = False def get_user_details(self, response): """Return user details from Live Connect account""" fullname, first_name, last_name = self.get_user_names( first_name=response.get("first_name"), last_name=response.get("last_name") ) return { "username": response.get("name"), "email": response.get("emails", {}).get("account", ""), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://apis.live.net/v5.0/me", params={"access_token": access_token} ) social-auth-core-4.6.1/social_core/backends/livejournal.py000066400000000000000000000016561500362547200236350ustar00rootroot00000000000000""" LiveJournal OpenId backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/livejournal.html """ from urllib.parse import urlsplit from ..exceptions import AuthMissingParameter from .open_id import OpenIdAuth class LiveJournalOpenId(OpenIdAuth): """LiveJournal OpenID authentication backend""" name = "livejournal" def get_user_details(self, response): """Generate username from identity url""" values = super().get_user_details(response) values["username"] = ( values.get("username") or urlsplit(response.identity_url).netloc.split(".", 1)[0] ) return values def openid_url(self): """Returns LiveJournal authentication URL""" if not self.data.get("openid_lj_user"): raise AuthMissingParameter(self, "openid_lj_user") return "http://{}.livejournal.com".format(self.data["openid_lj_user"]) social-auth-core-4.6.1/social_core/backends/loginradius.py000066400000000000000000000047761500362547200236310ustar00rootroot00000000000000""" LoginRadius BaseOAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/loginradius.html """ from .oauth import BaseOAuth2 class LoginRadiusAuth(BaseOAuth2): """LoginRadius BaseOAuth2 authentication backend.""" name = "loginradius" ID_KEY = "ID" ACCESS_TOKEN_URL = "https://api.loginradius.com/api/v2/access_token" PROFILE_URL = "https://api.loginradius.com/api/v2/userprofile" REDIRECT_STATE = False STATE_PARAMETER = False def uses_redirect(self): """Return False because we return HTML instead.""" return False def auth_html(self): key, secret = self.get_key_and_secret() tpl = self.setting("TEMPLATE", "loginradius.html") return self.strategy.render_html( tpl=tpl, context={ "backend": self, "LOGINRADIUS_KEY": key, "LOGINRADIUS_REDIRECT_URL": self.get_redirect_uri(), }, ) def request_access_token(self, *args, **kwargs): return self.get_json( params={"token": self.data.get("token"), "secret": self.setting("SECRET")}, *args, **kwargs, ) def user_data(self, access_token, *args, **kwargs): """Loads user data from service. Implement in subclass.""" return self.get_json( self.PROFILE_URL, params={"access_token": access_token}, data=self.auth_complete_params(self.validate_state()), headers=self.auth_headers(), method="GET", ) def get_user_details(self, response): """Must return user details in a know internal struct: {'username': , 'email': , 'fullname': , 'first_name': , 'last_name': } """ return { "username": response["NickName"] or "", "email": response["Email"][0]["Value"] or "", "fullname": response["FullName"] or "", "first_name": response["FirstName"] or "", "last_name": response["LastName"] or "", } def get_user_id(self, details, response): """Return a unique ID for the current user, by default from server response. Since LoginRadius handles multiple providers, we need to distinguish them to prevent conflicts.""" return "{}-{}".format(response.get("Provider"), response.get(self.ID_KEY)) social-auth-core-4.6.1/social_core/backends/lyft.py000066400000000000000000000031661500362547200222570ustar00rootroot00000000000000""" Lyft OAuth2 backend. Read more about the API at https://developer.lyft.com/docs """ from .oauth import BaseOAuth2 class LyftOAuth2(BaseOAuth2): name = "lyft" ID_KEY = "id" SCOPE_SEPARATOR = " " AUTHORIZATION_URL = "https://api.lyft.com/oauth/authorize" ACCESS_TOKEN_URL = "https://api.lyft.com/oauth/token" REFRESH_TOKEN_URL = "https://api.lyft.com/oauth/token" USER_DATA_URL = "https://api.lyft.com/v1/profile" DEFAULT_SCOPE = ["public", "profile", "rides.read", "rides.request"] RESPONSE_TYPE = "code" STATE_PARAMETER = True EXTRA_DATA = [ ("id", "id"), ("username", "username"), ("access_token", "access_token"), ("refresh_token", "refresh_token"), ("token_type", "token_type"), ("expires_in", "expires_in"), ("scope", "scope"), ] def get_user_details(self, response): """Return user details from Lyft account""" return {"id": response["id"], "username": response["id"]} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( self.USER_DATA_URL, headers={"Authorization": f"Bearer {access_token}"} ) def auth_complete_params(self, state=None): client_id, client_secret = self.get_key_and_secret() return {"grant_type": "authorization_code", "code": self.data["code"]} def auth_complete_credentials(self): return self.get_key_and_secret() def refresh_token_params(self, refresh_token, *args, **kwargs): return {"refresh_token": refresh_token, "grant_type": "refresh_token"} social-auth-core-4.6.1/social_core/backends/mailchimp.py000066400000000000000000000020641500362547200232400ustar00rootroot00000000000000from .oauth import BaseOAuth2 class MailChimpOAuth2(BaseOAuth2): """MailChimp OAuth2 authentication backend""" name = "mailchimp" AUTHORIZATION_URL = "https://login.mailchimp.com/oauth2/authorize" ACCESS_TOKEN_URL = "https://login.mailchimp.com/oauth2/token" METADATA_URL = "https://login.mailchimp.com/oauth2/metadata" STATE_PARAMETER = False REDIRECT_STATE = False ID_KEY = "user_id" EXTRA_DATA = [ ("accountname", "accountname"), ("api_endpoint", "api_endpoint"), ("role", "role"), ("login", "login"), ] def get_user_details(self, response): """Return user details from a Mailchimp metadata response""" return { "username": response["login"]["login_name"], "email": response["login"]["email"], } def user_data(self, access_token, *args, **kwargs): """Loads user data and datacenter information from service""" return self.get_json( self.METADATA_URL, headers={"Authorization": "OAuth " + access_token} ) social-auth-core-4.6.1/social_core/backends/mailru.py000066400000000000000000000052711500362547200225710ustar00rootroot00000000000000""" Mail.ru OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/mailru.html """ from hashlib import md5 from urllib.parse import unquote from .oauth import BaseOAuth2 class MailruOAuth2(BaseOAuth2): """Mail.ru authentication backend""" name = "mailru-oauth2" ID_KEY = "uid" AUTHORIZATION_URL = "https://connect.mail.ru/oauth/authorize" ACCESS_TOKEN_URL = "https://connect.mail.ru/oauth/token" EXTRA_DATA = [("refresh_token", "refresh_token"), ("expires_in", "expires")] def get_user_details(self, response): """Return user details from Mail.ru request""" fullname, first_name, last_name = self.get_user_names( first_name=unquote(response["first_name"]), last_name=unquote(response["last_name"]), ) return { "username": unquote(response["nick"]), "email": unquote(response["email"]), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Return user data from Mail.ru REST API""" key, secret = self.get_key_and_secret() data = { "method": "users.getInfo", "session_key": access_token, "app_id": key, "secure": "1", } param_list = sorted(f"{item}={value}" for item, value in data.values()) # Usage of md5 is mandated by the API: https://api.mail.ru/docs/guides/restapi/#client data["sig"] = md5(("".join(param_list) + secret).encode("utf-8")).hexdigest() # noqa: S324 return self.get_json("http://www.appsmail.ru/platform/api", params=data)[0] class MRGOAuth2(BaseOAuth2): name = "mailru" ID_KEY = "email" AUTHORIZATION_URL = "https://oauth.mail.ru/login" ACCESS_TOKEN_URL = "https://oauth.mail.ru/token" EXTRA_DATA = [("refresh_token", "refresh_token"), ("expires_in", "expires")] REDIRECT_STATE = False def get_user_details(self, response): return { "gender": response.get("gender"), "fullname": response.get("name"), "username": response.get("name"), "first_name": response.get("first_name"), "last_name": response.get("last_name"), "locale": response.get("locale"), "email": response.get("email"), "address": response.get("address"), "birthday": response.get("birthday"), "image": response.get("image"), } def user_data(self, access_token, *args, **kwargs): return self.get_json( "https://oauth.mail.ru/userinfo", params={"access_token": access_token} ) social-auth-core-4.6.1/social_core/backends/mapmyfitness.py000066400000000000000000000026161500362547200240170ustar00rootroot00000000000000""" MapMyFitness OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/mapmyfitness.html """ from .oauth import BaseOAuth2 class MapMyFitnessOAuth2(BaseOAuth2): """MapMyFitness OAuth authentication backend""" name = "mapmyfitness" AUTHORIZATION_URL = "https://www.mapmyfitness.com/v7.0/oauth2/authorize" ACCESS_TOKEN_URL = "https://oauth2-api.mapmyapi.com/v7.0/oauth2/access_token" REQUEST_TOKEN_METHOD = "POST" REDIRECT_STATE = False EXTRA_DATA = [ ("refresh_token", "refresh_token"), ] def auth_headers(self): key = self.get_key_and_secret()[0] return {"Api-Key": key} def get_user_id(self, details, response): return response["id"] def get_user_details(self, response): first = response.get("first_name", "") last = response.get("last_name", "") full = (first + last).strip() return { "username": response["username"], "email": response["email"], "fullname": full, "first_name": first, "last_name": last, } def user_data(self, access_token, *args, **kwargs): key = self.get_key_and_secret()[0] url = "https://oauth2-api.mapmyapi.com/v7.0/user/self/" headers = {"Authorization": f"Bearer {access_token}", "Api-Key": key} return self.get_json(url, headers=headers) social-auth-core-4.6.1/social_core/backends/mediawiki.py000066400000000000000000000135551500362547200232470ustar00rootroot00000000000000""" MediaWiki OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/mediawiki.html """ import re import time from urllib.parse import parse_qs, urlencode, urlparse import jwt from requests_oauthlib import OAuth1 from ..exceptions import AuthException from .oauth import BaseOAuth1 def force_unicode(value): """ Return string in unicode. """ if isinstance(value, str): return value return str(value, "unicode-escape") class MediaWiki(BaseOAuth1): """ Handles the handshake with Mediawiki and fetching of user data. """ name = "mediawiki" MEDIAWIKI_URL = "https://meta.wikimedia.org/w/index.php" SOCIAL_AUTH_MEDIAWIKI_CALLBACK = "oob" LEEWAY = 10.0 def unauthorized_token(self): """ Return request for unauthorized token (first stage) Mediawiki request token is requested from e.g.: * https://en.wikipedia.org/w/index.php?title=Special:OAuth/initiate """ params = self.request_token_extra_arguments() params.update(self.get_scope_argument()) params["title"] = "Special:OAuth/initiate" key, secret = self.get_key_and_secret() response = self.request( self.setting("MEDIAWIKI_URL"), params=params, auth=OAuth1(key, secret, callback_uri=self.setting("CALLBACK")), method=self.REQUEST_TOKEN_METHOD, ) if response.content.decode().startswith("Error"): raise AuthException(self, response.content.decode()) return response.content.decode() def oauth_authorization_request(self, token): """ Generates the URL for the authorization link """ if not isinstance(token, dict): token = parse_qs(token) oauth_token = token.get(self.OAUTH_TOKEN_PARAMETER_NAME)[0] # type: ignore[reportOptionalSubscript] state = self.get_or_create_state() base_url = self.setting("MEDIAWIKI_URL") return "{}?{}".format( base_url, urlencode( { "title": "Special:Oauth/authenticate", self.OAUTH_TOKEN_PARAMETER_NAME: oauth_token, self.REDIRECT_URI_PARAMETER_NAME: self.get_redirect_uri(state), } ), ) def access_token(self, token): """ Fetches the Mediawiki access token. """ auth_token = self.oauth_auth(token) response = self.request( self.setting("MEDIAWIKI_URL"), method="POST", params={"title": "Special:Oauth/token"}, auth=auth_token, ) if response.content.decode().startswith("Error"): raise AuthException(self, response.content.decode()) credentials = parse_qs(response.content) oauth_token_key = credentials.get(b"oauth_token")[0] # type: ignore[reportOptionalSubscript] oauth_token_secret = credentials.get(b"oauth_token_secret")[0] # type: ignore[reportOptionalSubscript] oauth_token_key = oauth_token_key.decode() oauth_token_secret = oauth_token_secret.decode() return { "oauth_token": oauth_token_key, "oauth_token_secret": oauth_token_secret, } def get_user_details(self, response): """ Gets the user details from Special:OAuth/identify """ key, secret = self.get_key_and_secret() access_token = response["access_token"] auth = OAuth1( key, client_secret=secret, resource_owner_key=access_token["oauth_token"], resource_owner_secret=access_token["oauth_token_secret"], ) req_resp = self.request( self.setting("MEDIAWIKI_URL"), method="POST", params={"title": "Special:OAuth/identify"}, auth=auth, ) try: identity = jwt.decode( req_resp.content, secret, audience=key, algorithms=["HS256"], leeway=self.LEEWAY, ) except jwt.InvalidTokenError as exception: raise AuthException( self, "An error occurred while trying to read json " + f"content: {exception}", ) issuer = urlparse(identity["iss"]).netloc expected_domain = urlparse(self.setting("MEDIAWIKI_URL")).netloc if not issuer == expected_domain: raise AuthException( self, f"Unexpected issuer {issuer}, expected {expected_domain}", ) now = time.time() issued_at = float(identity["iat"]) if not now >= (issued_at - self.LEEWAY): raise AuthException( self, f"Identity issued {issued_at - now} seconds in the future" ) authorization_header = force_unicode(req_resp.request.headers["Authorization"]) request_nonce = re.search(r'oauth_nonce="(.*?)"', authorization_header).group(1) if identity["nonce"] != request_nonce: raise AuthException( self, "Replay attack detected: {} != {}".format( identity["nonce"], request_nonce ), ) return { "username": identity["username"], "userID": identity["sub"], "email": identity.get("email"), "confirmed_email": identity.get("confirmed_email"), "editcount": identity.get("editcount"), "rights": identity.get("rights"), "groups": identity.get("groups"), "registered": identity.get("registered"), "blocked": identity.get("blocked"), } def get_user_id(self, details, response): """ Get the unique Mediawiki user ID. """ return details["userID"] social-auth-core-4.6.1/social_core/backends/meetup.py000066400000000000000000000022021500362547200225660ustar00rootroot00000000000000""" Meetup OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/meetup.html """ from .oauth import BaseOAuth2 class MeetupOAuth2(BaseOAuth2): """Meetup OAuth2 authentication backend""" name = "meetup" AUTHORIZATION_URL = "https://secure.meetup.com/oauth2/authorize" ACCESS_TOKEN_URL = "https://secure.meetup.com/oauth2/access" DEFAULT_SCOPE = ["basic"] SCOPE_SEPARATOR = "," REDIRECT_STATE = False STATE_PARAMETER = True def get_user_details(self, response): """Return user details from Meetup account""" fullname, first_name, last_name = self.get_user_names(response.get("name")) return { "username": response.get("username"), "email": response.get("email") or "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://api.meetup.com/2/member/self", params={"access_token": access_token}, ) social-auth-core-4.6.1/social_core/backends/mendeley.py000066400000000000000000000041721500362547200231010ustar00rootroot00000000000000""" Mendeley OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/mendeley.html """ from .oauth import BaseOAuth1, BaseOAuth2 BASE_EXTRA_DATA = [("profile_id", "profile_id"), ("name", "name"), ("bio", "bio")] class MendeleyMixin: SCOPE_SEPARATOR = "+" def get_user_id(self, details, response): return response["id"] def get_user_details(self, response): """Return user details from Mendeley account""" profile_id = response["id"] name = response["display_name"] bio = response["link"] return {"profile_id": profile_id, "name": name, "bio": bio} def user_data(self, access_token, *args, **kwargs): """Return user data provided""" values = self.get_user_data(access_token) values.update(values) return values def get_user_data(self, access_token): raise NotImplementedError("Implement in subclass") class MendeleyOAuth(MendeleyMixin, BaseOAuth1): name = "mendeley" AUTHORIZATION_URL = "http://api.mendeley.com/oauth/authorize/" REQUEST_TOKEN_URL = "http://api.mendeley.com/oauth/request_token/" ACCESS_TOKEN_URL = "http://api.mendeley.com/oauth/access_token/" EXTRA_DATA = [*BASE_EXTRA_DATA] def get_user_data(self, access_token): return self.get_json( "http://api.mendeley.com/oapi/profiles/info/me/", auth=self.oauth_auth(access_token), ) class MendeleyOAuth2(MendeleyMixin, BaseOAuth2): name = "mendeley-oauth2" AUTHORIZATION_URL = "https://api-oauth2.mendeley.com/oauth/authorize" ACCESS_TOKEN_URL = "https://api-oauth2.mendeley.com/oauth/token" DEFAULT_SCOPE = ["all"] REDIRECT_STATE = False EXTRA_DATA = [ *BASE_EXTRA_DATA, ("refresh_token", "refresh_token"), ("expires_in", "expires_in"), ("token_type", "token_type"), ] def get_user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://api.mendeley.com/profiles/me/", headers={"Authorization": f"Bearer {access_token}"}, ) social-auth-core-4.6.1/social_core/backends/microsoft.py000066400000000000000000000061171500362547200233050ustar00rootroot00000000000000""" OAuth2 Backend to work with microsoft graph. """ import time from ..exceptions import AuthMissingParameter from .oauth import BaseOAuth2 class MicrosoftOAuth2(BaseOAuth2): name = "microsoft-graph" SCOPE_SEPARATOR = " " AUTHORIZATION_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" ACCESS_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token" REDIRECT_STATE = False DEFAULT_SCOPE = ["User.Read"] def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" self.process_error(self.data) state = self.validate_state() response = self.request_access_token( self.access_token_url(), data=self.auth_complete_params(state), headers=self.auth_headers(), auth=self.auth_complete_credentials(), method=self.ACCESS_TOKEN_METHOD, ) self.process_error(response) return self.do_auth( response["access_token"], response=response, *args, **kwargs ) def get_user_id(self, details, response): """Use user account id as unique id""" return response.get("id") def get_user_details(self, response): """Return user details from Microsoft online account""" email = response.get("mail") username = response.get("userPrincipalName") if not username: raise AuthMissingParameter(self, "userPrincipalName") if "@" in username: if not email: email = username username = username.split("@", 1)[0] return { "username": username, "email": email, "fullname": response.get("displayName", ""), "first_name": response.get("givenName", ""), "last_name": response.get("surname", ""), } def user_data(self, access_token, *args, **kwargs): """Return user data by querying Microsoft service""" return self.get_json( "https://graph.microsoft.com/v1.0/me", headers={ "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", "Authorization": "Bearer " + access_token, }, method="GET", ) def refresh_token_params(self, token, *args, **kwargs): return { "client_id": self.setting("KEY"), "client_secret": self.setting("SECRET"), "refresh_token": token, "grant_type": "refresh_token", } def get_auth_token(self, user_id): """Return the access token for the given user, after ensuring that it has not expired, or refreshing it if so.""" user = self.get_user(user_id=user_id) access_token = user.social_user.access_token expires_on = user.social_user.extra_data["expires_on"] if expires_on <= int(time.time()): new_token_response = self.refresh_token(token=access_token) access_token = new_token_response["access_token"] return access_token social-auth-core-4.6.1/social_core/backends/mineid.py000066400000000000000000000022511500362547200225400ustar00rootroot00000000000000from .oauth import BaseOAuth2 class MineIDOAuth2(BaseOAuth2): """MineID OAuth2 authentication backend""" name = "mineid" _AUTHORIZATION_URL = "%(scheme)s://%(host)s/oauth/authorize" _ACCESS_TOKEN_URL = "%(scheme)s://%(host)s/oauth/access_token" SCOPE_SEPARATOR = "," EXTRA_DATA = [] def get_user_details(self, response): """Return user details""" return {"email": response.get("email"), "username": response.get("email")} def user_data(self, access_token, *args, **kwargs): return self._user_data(access_token) def _user_data(self, access_token, path=None): url = "{scheme}://{host}/api/user".format(**self.get_mineid_url_params()) return self.get_json(url, params={"access_token": access_token}) @property def AUTHORIZATION_URL(self): return self._AUTHORIZATION_URL % self.get_mineid_url_params() @property def ACCESS_TOKEN_URL(self): return self._ACCESS_TOKEN_URL % self.get_mineid_url_params() def get_mineid_url_params(self): return { "host": self.setting("HOST", "www.mineid.org"), "scheme": self.setting("SCHEME", "https"), } social-auth-core-4.6.1/social_core/backends/mixcloud.py000066400000000000000000000016141500362547200231210ustar00rootroot00000000000000""" Mixcloud OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/mixcloud.html """ from .oauth import BaseOAuth2 class MixcloudOAuth2(BaseOAuth2): name = "mixcloud" ID_KEY = "username" AUTHORIZATION_URL = "https://www.mixcloud.com/oauth/authorize" ACCESS_TOKEN_URL = "https://www.mixcloud.com/oauth/access_token" def get_user_details(self, response): fullname, first_name, last_name = self.get_user_names(response["name"]) return { "username": response["username"], "email": None, "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): return self.get_json( "https://api.mixcloud.com/me/", params={"access_token": access_token, "alt": "json"}, ) social-auth-core-4.6.1/social_core/backends/monzo.py000066400000000000000000000015211500362547200224340ustar00rootroot00000000000000from .oauth import BaseOAuth2 class MonzoOAuth2(BaseOAuth2): """ Monzo OAuth2 authentication backend. """ name = "monzo" AUTHORIZATION_URL = "https://auth.getmondo.co.uk/" ACCESS_TOKEN_URL = "https://api.monzo.com/oauth2/token" REDIRECT_STATE = False def get_user_details(self, response): fullname, first_name, last_name = self.get_user_names( response["accounts"][0]["description"], ) return { "username": str(response.get("user_id")), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): return self.get_json( "https://api.monzo.com/accounts", headers={"Authorization": f"Bearer {access_token}"}, ) social-auth-core-4.6.1/social_core/backends/moves.py000066400000000000000000000017201500362547200224240ustar00rootroot00000000000000""" Moves OAuth2 backend, docs at: https://dev.moves-app.com/docs/authentication Written by Avi Alkalay Certified to work with Django 1.6 """ from .oauth import BaseOAuth2 class MovesOAuth2(BaseOAuth2): """Moves OAuth authentication backend""" name = "moves" ID_KEY = "user_id" AUTHORIZATION_URL = "https://api.moves-app.com/oauth/v1/authorize" ACCESS_TOKEN_URL = "https://api.moves-app.com/oauth/v1/access_token" EXTRA_DATA = [ ("refresh_token", "refresh_token", True), ("expires_in", "expires"), ] def get_user_details(self, response): """Return user details Moves account""" return {"username": str(response.get("user_id"))} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://api.moves-app.com/api/1.1/user/profile", params={"access_token": access_token}, ) social-auth-core-4.6.1/social_core/backends/musicbrainz.py000066400000000000000000000020161500362547200236200ustar00rootroot00000000000000from social_core.backends.oauth import BaseOAuth2 class MusicBrainzOAuth2(BaseOAuth2): """MusicBrainz OAuth authentication backend""" name = "musicbrainz" AUTHORIZATION_URL = "https://musicbrainz.org/oauth2/authorize" ACCESS_TOKEN_URL = "https://musicbrainz.org/oauth2/token" ID_KEY = "metabrainz_user_id" DEFAULT_SCOPE = ["profile", "email"] SCOPE_SEPARATOR = " " REDIRECT_STATE = False EXTRA_DATA = [ ("metabrainz_user_id", "id"), ("expires_in", "expires"), ] def get_user_details(self, response): """Return user details from MusicBrainz account""" return { "username": response.get("sub"), "email": response.get("email") or "", "first_name": response.get("sub"), } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://musicbrainz.org/oauth2/userinfo", params={"access_token": access_token}, ) social-auth-core-4.6.1/social_core/backends/nationbuilder.py000066400000000000000000000027521500362547200241400ustar00rootroot00000000000000""" NationBuilder OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/nationbuilder.html """ from .oauth import BaseOAuth2 class NationBuilderOAuth2(BaseOAuth2): """NationBuilder OAuth2 authentication backend""" name = "nationbuilder" AUTHORIZATION_URL = "https://{slug}.nationbuilder.com/oauth/authorize" ACCESS_TOKEN_URL = "https://{slug}.nationbuilder.com/oauth/token" REDIRECT_STATE = False SCOPE_SEPARATOR = "," EXTRA_DATA = [("id", "id"), ("expires", "expires")] def authorization_url(self): return self.AUTHORIZATION_URL.format(slug=self.slug) def access_token_url(self): return self.ACCESS_TOKEN_URL.format(slug=self.slug) @property def slug(self): return self.setting("SLUG") def get_user_details(self, response): """Return user details from Github account""" email = response.get("email") or "" username = email.split("@")[0] if email else "" return { "username": username, "email": email, "fullname": response.get("full_name") or "", "first_name": response.get("first_name") or "", "last_name": response.get("last_name") or "", } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" url = f"https://{self.slug}.nationbuilder.com/api/v1/people/me" return self.get_json(url, params={"access_token": access_token})["person"] social-auth-core-4.6.1/social_core/backends/naver.py000066400000000000000000000035511500362547200224120ustar00rootroot00000000000000from .oauth import BaseOAuth2 class NaverOAuth2(BaseOAuth2): """Naver OAuth authentication backend""" name = "naver" AUTHORIZATION_URL = "https://nid.naver.com/oauth2.0/authorize" ACCESS_TOKEN_URL = "https://nid.naver.com/oauth2.0/token" EXTRA_DATA = [ ("id", "id"), ] def get_user_id(self, details, response): return response.get("id") def get_user_details(self, response): """Return user details from Naver account""" return { "username": response.get("username"), "email": response.get("email"), "fullname": response.get("username"), } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" response = self.request( "https://openapi.naver.com/v1/nid/me", headers={ "Authorization": f"Bearer {access_token}", "Content_Type": "text/json", }, ) data = response.json() return { "id": self._fetch(data, "id"), "email": self._fetch(data, "email"), "username": self._fetch(data, "name"), "nickname": self._fetch(data, "nickname"), "gender": self._fetch(data, "gender"), "age": self._fetch(data, "age"), "birthday": self._fetch(data, "birthday"), "profile_image": self._fetch(data, "profile_image"), } def auth_headers(self): client_id, client_secret = self.get_key_and_secret() return { "grant_type": "authorization_code", "code": self.data.get("code"), "client_id": client_id, "client_secret": client_secret, } def _fetch(self, data, key): try: return data["response"][key] except (KeyError, TypeError): return "" social-auth-core-4.6.1/social_core/backends/nfdi.py000066400000000000000000000140501500362547200222130ustar00rootroot00000000000000""" Backend for OpenID Connect OPs of the German NFDI initiative https://doc.nfdi-aai.de/ This is conceptually based on the egi_checkin backend """ from social_core.backends.open_id_connect import OpenIdConnectAuth NFDI_ENDPOINTS = { # AcademicID "xcs": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud", "textplus": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud", # didmos "mardi": "https://auth.didmos.nfdi-aai.de", "objects": "https://auth.didmos.nfdi-aai.de", "culture": "https://auth.aai.nfdi4culture.de", # regapp "cat": "https://regapp.nfdi-aai.de/oidc/realms/nfdi", "chem": "https://regapp.nfdi-aai.de/oidc/realms/nfdi", "datascience": "https://regapp.nfdi-aai.de/oidc/realms/nfdi", "energy": "https://regapp.nfdi-aai.de/oidc/realms/nfdi", "ing": "https://regapp.nfdi-aai.de/oidc/realms/nfdi", "matWerk": "https://regapp.nfdi-aai.de/oidc/realms/nfdi", # unity "daphne": "https://login.helmholtz.de/oauth2", "fairmat": "https://login.helmholtz.de/oauth2", "immuno": "https://login.helmholtz.de/oauth2", "punch": "https://login.helmholtz.de/punch-oauth2", "helmholtz": "https://login.helmholtz.de/oauth2", # infraproxy "infraproxy-staging": "https://infraproxy-staging.nfdi-aai.dfn.de", "infraproxy": "https://infraproxy.nfdi-aai.dfn.de", # eduid "eduid": "https://proxy.edu-id.dfn.de", "eduid-staging": "", # other # DataPLANT # GHGA # NFDI4Biodiversity # unassigned # BERD@NFDI # FAIRagro # KonsortSWD # NFDI4BIOIMAGE # NFDI4Earth # NFDI4Health # NFDI4Memory # NFDI4Microbiota } class NFDIOpenIdConnect(OpenIdConnectAuth): name = "helmholtz" OIDC_ENDPOINT = "https://login.helmholtz.de/oauth2" # In order to get any scopes, you have to register your service with # the OP USERNAME_KEY = "preferred_username" EXTRA_DATA = [ ("expires_in", "expires_in", True), ("refresh_token", "refresh_token", True), ("id_token", "id_token", True), ] DEFAULT_SCOPE = [ "openid", "profile", "email", "voperson_id", "eduperson_entitlement", # "entitlement", "eduperson_scoped_affiliation", "voperson_external_affiliation", "eduperson_assurance", "orcid", # "offline_access", ] # This is the list of entitlements that are allowed to login into the # service. A user with any of these will be allowed. If empty, all # users will be allowed ALLOWED_ENTITLEMENTS: list[str] = [] def get_user_details(self, response): username_key = self.setting("USERNAME_KEY", default=self.USERNAME_KEY) fullname, first_name, last_name = self.get_user_names( response.get("name") or "", response.get("given_name") or "", response.get("family_name") or "", ) return { "username": response.get(username_key), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def entitlement_allowed(self, user_entitlements): allowed = True allowed_ent = self.setting("ALLOWED_ENTITLEMENTS", self.ALLOWED_ENTITLEMENTS) if allowed_ent: allowed = any(e in user_entitlements for e in allowed_ent) return allowed def auth_allowed(self, response, details): """Check-in promotes the use of eduperson_entitlements for AuthZ, if ALLOWED_ENTITLEMENTS is defined then use them to allow or not users""" allowed = super().auth_allowed(response, details) if allowed: user_entitlements = response.get("eduperson_entitlement") or [] allowed = self.entitlement_allowed(user_entitlements) return allowed # AcademicID class XcsOpenIdConnect(NFDIOpenIdConnect): name = "xcs" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] class TextplusOpenIdConnect(NFDIOpenIdConnect): name = "textplus" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] # didmos class MardiOpenIdConnect(NFDIOpenIdConnect): name = "mardi" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] class ObjectsOpenIdConnect(NFDIOpenIdConnect): name = "objects" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] class CultureOpenIdConnect(NFDIOpenIdConnect): name = "culture" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] # regapp class CatOpenIdConnect(NFDIOpenIdConnect): name = "cat" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] class ChemOpenIdConnect(NFDIOpenIdConnect): name = "chem" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] class DatascienceOpenIdConnect(NFDIOpenIdConnect): name = "datascience" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] class EnergyOpenIdConnect(NFDIOpenIdConnect): name = "energy" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] class IngOpenIdConnect(NFDIOpenIdConnect): name = "ing" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] class MatWerkOpenIdConnect(NFDIOpenIdConnect): name = "matWerk" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] # unity class DaphneOpenIdConnect(NFDIOpenIdConnect): name = "daphne" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] class FairmatOpenIdConnect(NFDIOpenIdConnect): name = "fairmat" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] class ImmunoOpenIdConnect(NFDIOpenIdConnect): name = "immuno" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] class PunchOpenIdConnect(NFDIOpenIdConnect): name = "punch" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] class HelmholtzOpenIdConnect(NFDIOpenIdConnect): name = "helmholtz" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] # infraproxy class InfraproxyStagingOpenIdConnect(NFDIOpenIdConnect): name = "infraproxy-staging" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] class InfraproxyOpenIdConnect(NFDIOpenIdConnect): name = "infraproxy" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] # eduid class EduidOpenIdConnect(NFDIOpenIdConnect): name = "eduid" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] class EduidStagingOpenIdConnect(NFDIOpenIdConnect): name = "eduid-staging" OIDC_ENDPOINT = NFDI_ENDPOINTS[name] social-auth-core-4.6.1/social_core/backends/ngpvan.py000066400000000000000000000043161500362547200225700ustar00rootroot00000000000000""" NGP VAN's `ActionID` Provider http://developers.ngpvan.com/action-id """ from openid.extensions import ax from .open_id import OpenIdAuth class ActionIDOpenID(OpenIdAuth): """ NGP VAN's ActionID OpenID 1.1 authentication backend """ name = "actionid-openid" URL = "https://accounts.ngpvan.com/Home/Xrds" USERNAME_KEY = "email" def get_ax_attributes(self): """ Return the AX attributes that ActionID responds with, as well as the user data result that it must map to. """ return [ ("http://openid.net/schema/contact/internet/email", "email"), ("http://openid.net/schema/contact/phone/business", "phone"), ("http://openid.net/schema/namePerson/first", "first_name"), ("http://openid.net/schema/namePerson/last", "last_name"), ("http://openid.net/schema/namePerson", "fullname"), ] def setup_request(self, params=None): """ Setup the OpenID request Because ActionID does not advertise the availability of AX attributes nor use standard attribute aliases, we need to setup the attributes manually instead of rely on the parent OpenIdAuth.setup_request() """ request = self.openid_request(params) fetch_request = ax.FetchRequest() fetch_request.add( ax.AttrInfo( "http://openid.net/schema/contact/internet/email", alias="ngpvanemail", required=True, ) ) fetch_request.add( ax.AttrInfo( "http://openid.net/schema/contact/phone/business", alias="ngpvanphone", required=False, ) ) fetch_request.add( ax.AttrInfo( "http://openid.net/schema/namePerson/first", alias="ngpvanfirstname", required=False, ) ) fetch_request.add( ax.AttrInfo( "http://openid.net/schema/namePerson/last", alias="ngpvanlastname", required=False, ) ) request.addExtension(fetch_request) return request social-auth-core-4.6.1/social_core/backends/nk.py000066400000000000000000000046321500362547200217100ustar00rootroot00000000000000from urllib.parse import urlencode from requests_oauthlib import OAuth1 from .oauth import BaseOAuth2 class NKOAuth2(BaseOAuth2): """NK OAuth authentication backend""" name = "nk" AUTHORIZATION_URL = "https://nk.pl/oauth2/login" ACCESS_TOKEN_URL = "https://nk.pl/oauth2/token" SCOPE_SEPARATOR = "," SIGNATURE_TYPE_AUTH_HEADER = "AUTH_HEADER" EXTRA_DATA = [ ("id", "id"), ] def get_user_details(self, response): """Return user details from NK account""" entry = response["entry"] return { "username": entry.get("displayName"), "email": entry["emails"][0]["value"], "first_name": entry.get("displayName").split(" ")[0], "id": entry.get("id"), } def auth_complete_params(self, state=None): client_id, client_secret = self.get_key_and_secret() return { "grant_type": "authorization_code", # request auth code "code": self.data.get("code", ""), # server response code "client_id": client_id, "client_secret": client_secret, "redirect_uri": self.get_redirect_uri(state), "scope": self.get_scope_argument(), } def get_user_id(self, details, response): """Return a unique ID for the current user, by default from server response.""" return details.get(self.ID_KEY) def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" url = "http://opensocial.nk-net.pl/v09/social/rest/people/@me?" + urlencode( { "nk_token": access_token, "fields": "name,surname,avatar,localization,age," + "gender,emails,birthdate", } ) return self.get_json(url, auth=self.oauth_auth(access_token)) def oauth_auth( self, token=None, oauth_verifier=None, signature_type=SIGNATURE_TYPE_AUTH_HEADER ): key, secret = self.get_key_and_secret() oauth_verifier = oauth_verifier or self.data.get("oauth_verifier") token = token or {} state = self.get_or_create_state() return OAuth1( key, secret, resource_owner_key=None, resource_owner_secret=None, callback_uri=self.get_redirect_uri(state), verifier=oauth_verifier, signature_type=signature_type, ) social-auth-core-4.6.1/social_core/backends/oauth.py000066400000000000000000000475621500362547200224310ustar00rootroot00000000000000from __future__ import annotations import base64 import hashlib from typing import TYPE_CHECKING, Any, Literal, cast from urllib.parse import urlencode from oauthlib.oauth1 import SIGNATURE_TYPE_AUTH_HEADER from requests_oauthlib import OAuth1 from ..exceptions import ( AuthCanceled, AuthException, AuthFailed, AuthMissingParameter, AuthStateForbidden, AuthStateMissing, AuthTokenError, AuthUnknownError, ) from ..utils import ( constant_time_compare, handle_http_errors, parse_qs, url_add_parameters, ) from .base import BaseAuth if TYPE_CHECKING: from collections.abc import Mapping from requests import Response from requests.auth import AuthBase class OAuthAuth(BaseAuth): """OAuth authentication backend base class. Settings will be inspected to get more values names that should be stored on extra_data field. The setting name is created following the pattern SOCIAL_AUTH__EXTRA_DATA. access_token is always stored. URLs settings: AUTHORIZATION_URL Authorization service url ACCESS_TOKEN_URL Access token URL """ AUTHORIZATION_URL = "" ACCESS_TOKEN_URL = "" ACCESS_TOKEN_METHOD: Literal["GET", "POST"] = "POST" REVOKE_TOKEN_URL: str = "" REVOKE_TOKEN_METHOD: Literal["GET", "POST", "DELETE"] = "POST" ID_KEY = "id" SCOPE_PARAMETER_NAME = "scope" DEFAULT_SCOPE: list[str] | None = None SCOPE_SEPARATOR = " " REDIRECT_STATE = False STATE_PARAMETER = False def extra_data(self, user, uid, response, details=None, *args, **kwargs): """Return access_token and extra defined names to store in extra_data field""" data = super().extra_data(user, uid, response, details, *args, **kwargs) data["access_token"] = response.get("access_token", "") or kwargs.get( "access_token" ) return data def state_token(self): """Generate csrf token to include as state parameter.""" return self.strategy.random_string(32) def get_or_create_state(self) -> str | None: if self.STATE_PARAMETER or self.REDIRECT_STATE: # Store state in session for further request validation. The state # value is passed as state parameter (as specified in OAuth2 spec), # but also added to redirect, that way we can still verify the # request if the provider doesn't implement the state parameter. # Reuse token if any. name = self.name + "_state" state = self.strategy.session_get(name) if state is None: state = self.state_token() self.strategy.session_set(name, state) else: state = None return state def get_session_state(self): return self.strategy.session_get(self.name + "_state") def get_request_state(self): request_state = self.data.get("state") or self.data.get("redirect_state") if request_state and isinstance(request_state, list): request_state = request_state[0] return request_state def validate_state(self): """Validate state value. Raises exception on error, returns state value if valid.""" if not self.STATE_PARAMETER and not self.REDIRECT_STATE: return None state = self.get_session_state() request_state = self.get_request_state() if not request_state: raise AuthMissingParameter(self, "state") if not state: raise AuthStateMissing(self, "state") if not constant_time_compare(request_state, state): raise AuthStateForbidden(self) return state def get_redirect_uri(self, state: str | None = None) -> str: """Build redirect with redirect_state parameter.""" uri = cast("str", self.redirect_uri) if self.REDIRECT_STATE and state: uri = url_add_parameters(uri, {"redirect_state": state}) return uri def get_scope(self): """Return list with needed access scope""" scope = self.setting("SCOPE", []) if not self.setting("IGNORE_DEFAULT_SCOPE", False): scope = scope + (self.DEFAULT_SCOPE or []) return scope def get_scope_argument(self): param = {} scope = self.get_scope() if scope: param[self.SCOPE_PARAMETER_NAME] = self.SCOPE_SEPARATOR.join(scope) return param def user_data(self, access_token, *args, **kwargs) -> dict[str, Any] | None: """Loads user data from service. Implement in subclass""" return {} def authorization_url(self) -> str: return self.AUTHORIZATION_URL def access_token_url(self) -> str: return self.ACCESS_TOKEN_URL def revoke_token_url(self, token, uid) -> str: return self.REVOKE_TOKEN_URL def revoke_token_params(self, token, uid) -> dict[str, Any]: return {} def revoke_token_headers(self, token, uid) -> dict[str, Any]: return {} def process_revoke_token_response(self, response): return response.status_code == 200 def revoke_token(self, token, uid): if revoke_token_url := self.revoke_token_url(token, uid): params = self.revoke_token_params(token, uid) headers = self.revoke_token_headers(token, uid) data = urlencode(params) if self.REVOKE_TOKEN_METHOD != "GET" else None response = self.request( revoke_token_url, params=params, headers=headers, data=data, method=self.REVOKE_TOKEN_METHOD, ) return self.process_revoke_token_response(response) return None class BaseOAuth1(OAuthAuth): """Consumer based mechanism OAuth authentication, fill the needed parameters to communicate properly with authentication service. URLs settings: REQUEST_TOKEN_URL Request token URL """ REQUEST_TOKEN_URL = "" REQUEST_TOKEN_METHOD: Literal["GET", "POST"] = "GET" OAUTH_TOKEN_PARAMETER_NAME = "oauth_token" REDIRECT_URI_PARAMETER_NAME = "redirect_uri" UNATHORIZED_TOKEN_SUFIX = "unauthorized_token_name" def auth_url(self) -> str | bytes | None: """Return redirect url""" token = self.set_unauthorized_token() return self.oauth_authorization_request(token) def process_error(self, data): if "oauth_problem" in data: if data["oauth_problem"] == "user_refused": raise AuthCanceled(self, "User refused the access") raise AuthUnknownError(self, "Error was " + data["oauth_problem"]) @handle_http_errors def auth_complete(self, *args, **kwargs): """Return user, might be logged in""" # Multiple unauthorized tokens are supported (see #521) self.process_error(self.data) self.validate_state() token = self.get_unauthorized_token() access_token = self.access_token(token) return self.do_auth(access_token, *args, **kwargs) @handle_http_errors def do_auth(self, access_token, *args, **kwargs): """Finish the auth process once the access_token was retrieved""" if not isinstance(access_token, dict): access_token = parse_qs(access_token) data = self.user_data(access_token) if data is not None and "access_token" not in data: data["access_token"] = access_token kwargs.update({"response": data, "backend": self}) return self.strategy.authenticate(*args, **kwargs) def get_unauthorized_token(self): name = self.name + self.UNATHORIZED_TOKEN_SUFIX unauthed_tokens = self.strategy.session_get(name, []) if not unauthed_tokens: raise AuthTokenError(self, "Missing unauthorized token") data_token = self.data.get(self.OAUTH_TOKEN_PARAMETER_NAME) if data_token is None: raise AuthTokenError(self, "Missing unauthorized token") token = None for utoken in unauthed_tokens: orig_utoken = utoken if not isinstance(utoken, dict): utoken = parse_qs(utoken) if utoken.get(self.OAUTH_TOKEN_PARAMETER_NAME) == data_token: self.strategy.session_set( name, list(set(unauthed_tokens) - {orig_utoken}) ) token = utoken break else: raise AuthTokenError(self, "Incorrect tokens") return token def set_unauthorized_token(self): token = self.unauthorized_token() name = self.name + self.UNATHORIZED_TOKEN_SUFIX tokens = [*self.strategy.session_get(name, []), token] self.strategy.session_set(name, tokens) return token def request_token_extra_arguments(self): """Return extra arguments needed on request-token process""" return self.setting("REQUEST_TOKEN_EXTRA_ARGUMENTS", {}) def unauthorized_token(self): """Return request for unauthorized token (first stage)""" params = self.request_token_extra_arguments() params.update(self.get_scope_argument()) key, secret = self.get_key_and_secret() state = self.get_or_create_state() response = self.request( self.REQUEST_TOKEN_URL, params=params, auth=OAuth1(key, secret, callback_uri=self.get_redirect_uri(state)), method=self.REQUEST_TOKEN_METHOD, ) content = response.content if response.encoding or response.apparent_encoding: content = content.decode(response.encoding or response.apparent_encoding) else: content = response.content.decode() return content def oauth_authorization_request(self, token): """Generate OAuth request to authorize token.""" if not isinstance(token, dict): token = parse_qs(token) params = self.auth_extra_arguments() or {} params.update(self.get_scope_argument()) params[self.OAUTH_TOKEN_PARAMETER_NAME] = token.get( self.OAUTH_TOKEN_PARAMETER_NAME ) state = self.get_or_create_state() params[self.REDIRECT_URI_PARAMETER_NAME] = self.get_redirect_uri(state) return url_add_parameters(self.authorization_url(), params) def oauth_auth( self, token: dict | None = None, oauth_verifier=None, signature_type=SIGNATURE_TYPE_AUTH_HEADER, ): key, secret = self.get_key_and_secret() oauth_verifier = oauth_verifier or self.data.get("oauth_verifier") if token: resource_owner_key = token.get("oauth_token") resource_owner_secret = token.get("oauth_token_secret") if not resource_owner_key: raise AuthTokenError(self, "Missing oauth_token") if not resource_owner_secret: raise AuthTokenError(self, "Missing oauth_token_secret") else: resource_owner_key = None resource_owner_secret = None state = self.get_or_create_state() return OAuth1( key, secret, resource_owner_key=resource_owner_key, resource_owner_secret=resource_owner_secret, callback_uri=self.get_redirect_uri(state), verifier=oauth_verifier, signature_type=signature_type, ) def oauth_request( self, token: dict, url: str, params=None, method: Literal["GET", "POST"] = "GET" ) -> Response: """Generate OAuth request, setups callback url""" return self.request( url, method=method, params=params, auth=self.oauth_auth(token) ) def access_token(self, token: dict) -> dict[str, str]: """Return request for access token value""" return self.get_querystring( self.access_token_url(), auth=self.oauth_auth(token), method=self.ACCESS_TOKEN_METHOD, ) class BaseOAuth2(OAuthAuth): """Base class for OAuth2 providers. OAuth2 details at: https://datatracker.ietf.org/doc/html/rfc6749 """ REFRESH_TOKEN_URL: str | None = None REFRESH_TOKEN_METHOD = "POST" RESPONSE_TYPE: str | None = "code" REDIRECT_STATE = True STATE_PARAMETER = True USE_BASIC_AUTH = False def use_basic_auth(self) -> bool: return self.USE_BASIC_AUTH def auth_params(self, state: str | None = None) -> dict[str, str]: client_id, client_secret = self.get_key_and_secret() params = {"client_id": client_id, "redirect_uri": self.get_redirect_uri(state)} if self.STATE_PARAMETER and state: params["state"] = state if self.RESPONSE_TYPE: params["response_type"] = self.RESPONSE_TYPE return params def auth_url(self) -> str | bytes | None: """Return redirect url""" state = self.get_or_create_state() params = self.auth_params(state) params.update(self.get_scope_argument()) params.update(self.auth_extra_arguments()) # when self.REDIRECT_STATE is False, redirect_uri matching is strictly enforced, # so match the providers value exactly. return url_add_parameters( self.authorization_url(), params, not self.REDIRECT_STATE ) def auth_complete_params(self, state=None): params = { "grant_type": "authorization_code", # request auth code "code": self.data.get("code", ""), # server response code "redirect_uri": self.get_redirect_uri(state), } if not self.use_basic_auth(): client_id, client_secret = self.get_key_and_secret() params.update( { "client_id": client_id, "client_secret": client_secret, } ) return params def auth_complete_credentials(self): if self.use_basic_auth(): return self.get_key_and_secret() return None def auth_headers(self) -> Mapping[str, str | bytes]: return { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", } def extra_data(self, user, uid, response, details, *args, **kwargs): """Return access_token, token_type, and extra defined names to store in extra_data field""" data = super().extra_data(user, uid, response, details=details, *args, **kwargs) data["token_type"] = response.get("token_type") or kwargs.get("token_type") return data def request_access_token( self, url: str, method: Literal["GET", "POST", "DELETE"] = "GET", headers: Mapping[str, str | bytes] | None = None, data: dict | bytes | str | None = None, auth: tuple[str, str] | AuthBase | None = None, params: dict | None = None, ) -> dict[Any, Any]: return self.get_json( url, method=method, headers=headers, data=data, auth=auth, params=params ) def process_error(self, data): if data.get("error"): if "denied" in data["error"] or "cancelled" in data["error"]: raise AuthCanceled(self, data.get("error_description", "")) raise AuthFailed(self, data.get("error_description") or data["error"]) if "denied" in data: raise AuthCanceled(self, data["denied"]) @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" self.process_error(self.data) state = self.validate_state() data, params = None, None if self.ACCESS_TOKEN_METHOD == "GET": params = self.auth_complete_params(state) else: data = self.auth_complete_params(state) response = self.request_access_token( self.access_token_url(), data=data, params=params, headers=self.auth_headers(), auth=self.auth_complete_credentials(), method=self.ACCESS_TOKEN_METHOD, ) self.process_error(response) return self.do_auth( response["access_token"], response=response, *args, **kwargs ) @handle_http_errors def do_auth(self, access_token, *args, **kwargs): """Finish the auth process once the access_token was retrieved""" data = self.user_data(access_token, *args, **kwargs) response = kwargs.get("response") or {} response.update(data or {}) if "access_token" not in response: response["access_token"] = access_token kwargs.update({"response": response, "backend": self}) return self.strategy.authenticate(*args, **kwargs) def refresh_token_params(self, token, *args, **kwargs): client_id, client_secret = self.get_key_and_secret() return { "refresh_token": token, "grant_type": "refresh_token", "client_id": client_id, "client_secret": client_secret, } def process_refresh_token_response(self, response, *args, **kwargs): return response.json() def refresh_token(self, token, *args, **kwargs): params = self.refresh_token_params(token, *args, **kwargs) url = self.refresh_token_url() method = self.REFRESH_TOKEN_METHOD key = "params" if method == "GET" else "data" request_args = {"headers": self.auth_headers(), "method": method, key: params} request = self.request(url, **request_args) return self.process_refresh_token_response(request, *args, **kwargs) def refresh_token_url(self): return self.REFRESH_TOKEN_URL or self.access_token_url() class BaseOAuth2PKCE(BaseOAuth2): """ Base class for providers using OAuth2 with Proof Key for Code Exchange (PKCE). OAuth2 details at: https://datatracker.ietf.org/doc/html/rfc6749 PKCE details at: https://datatracker.ietf.org/doc/html/rfc7636 """ PKCE_DEFAULT_CODE_CHALLENGE_METHOD = "s256" PKCE_DEFAULT_CODE_VERIFIER_LENGTH = 32 DEFAULT_USE_PKCE = True def create_code_verifier(self): name = f"{self.name}_code_verifier" code_verifier_len = self.setting( "PKCE_CODE_VERIFIER_LENGTH", default=self.PKCE_DEFAULT_CODE_VERIFIER_LENGTH ) code_verifier = self.strategy.random_string(code_verifier_len) self.strategy.session_set(name, code_verifier) return code_verifier def get_code_verifier(self): name = f"{self.name}_code_verifier" return self.strategy.session_get(name) def generate_code_challenge(self, code_verifier, challenge_method): method = challenge_method.lower() if method == "s256": hashed = hashlib.sha256(code_verifier.encode()).digest() encoded = base64.urlsafe_b64encode(hashed) return encoded.decode().replace("=", "") # remove padding if method == "plain": return code_verifier raise AuthException(self, "Unsupported code challenge method.") def auth_params(self, state=None): params = super().auth_params(state=state) if self.setting("USE_PKCE", default=self.DEFAULT_USE_PKCE): code_challenge_method = self.setting( "PKCE_CODE_CHALLENGE_METHOD", default=self.PKCE_DEFAULT_CODE_CHALLENGE_METHOD, ) code_verifier = self.create_code_verifier() code_challenge = self.generate_code_challenge( code_verifier, code_challenge_method ) params["code_challenge_method"] = code_challenge_method params["code_challenge"] = code_challenge return params def auth_complete_params(self, state=None): params = super().auth_complete_params(state=state) if self.setting("USE_PKCE", default=self.DEFAULT_USE_PKCE): code_verifier = self.get_code_verifier() params["code_verifier"] = code_verifier return params social-auth-core-4.6.1/social_core/backends/odnoklassniki.py000066400000000000000000000155001500362547200241440ustar00rootroot00000000000000""" Odnoklassniki OAuth2 and Iframe Application backends, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/odnoklassnikiru.html """ from hashlib import md5 from urllib.parse import unquote from ..exceptions import AuthFailed from .base import BaseAuth from .oauth import BaseOAuth2 def odnoklassniki_sig(payload: str) -> str: """ Calculates a payload signature using md5. See https://apiok.ru/en/ext/invite_suggest#calculating-request-signature-stsignature """ return md5(payload.encode("utf-8")).hexdigest() # noqa: S324 class OdnoklassnikiOAuth2(BaseOAuth2): """Odnoklassniki authentication backend""" name = "odnoklassniki-oauth2" ID_KEY = "uid" SCOPE_SEPARATOR = ";" AUTHORIZATION_URL = "https://connect.ok.ru/oauth/authorize" ACCESS_TOKEN_URL = "https://api.ok.ru/oauth/token.do" EXTRA_DATA = [("refresh_token", "refresh_token"), ("expires_in", "expires")] def get_user_details(self, response): """Return user details from Odnoklassniki request""" fullname, first_name, last_name = self.get_user_names( fullname=unquote(response["name"]), first_name=unquote(response["first_name"]), last_name=unquote(response["last_name"]), ) return { "username": response["uid"], "email": response.get("email", ""), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Return user data from Odnoklassniki REST API""" data = {"access_token": access_token, "method": "users.getCurrentUser"} key, secret = self.get_key_and_secret() public_key = self.setting("PUBLIC_NAME") return odnoklassniki_api( self, data, "https://api.ok.ru/", public_key, secret, "oauth" ) class OdnoklassnikiApp(BaseAuth): """Odnoklassniki iframe app authentication backend""" name = "odnoklassniki-app" ID_KEY = "uid" def extra_data(self, user, uid, response, details=None, *args, **kwargs): return { key: value for key, value in response.items() if key in response["extra_data_list"] } def get_user_details(self, response): fullname, first_name, last_name = self.get_user_names( fullname=unquote(response["name"]), first_name=unquote(response["first_name"]), last_name=unquote(response["last_name"]), ) return { "username": response["uid"], "email": "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } def auth_complete(self, *args, **kwargs): self.verify_auth_sig() response = self.get_response() fields = ( "uid", "first_name", "last_name", "name", *self.setting("EXTRA_USER_DATA_LIST", ()), ) data = { "method": "users.getInfo", "uids": "{}".format(response["logged_user_id"]), "fields": ",".join(fields), } client_key, client_secret = self.get_key_and_secret() public_key = self.setting("PUBLIC_NAME") details = odnoklassniki_api( self, data, response["api_server"], public_key, client_secret, "iframe_nosession", ) if len(details) == 1 and "uid" in details[0]: details = details[0] auth_data_fields = self.setting( "EXTRA_AUTH_DATA_LIST", ( "api_server", "apiconnection", "session_key", "authorized", "session_secret_key", ), ) for field in auth_data_fields: details[field] = response[field] details["extra_data_list"] = fields + auth_data_fields kwargs.update({"backend": self, "response": details}) else: raise AuthFailed(self, "Cannot get user details: API error") return self.strategy.authenticate(*args, **kwargs) def get_auth_sig(self): return odnoklassniki_sig( "{:s}{:s}{:s}".format( self.data["logged_user_id"], self.data["session_key"], self.setting("SECRET"), ) ) def get_response(self): fields = ( "logged_user_id", "api_server", "application_key", "session_key", "session_secret_key", "authorized", "apiconnection", ) return {name: self.data[name] for name in fields if name in self.data} def verify_auth_sig(self): correct_key = self.get_auth_sig() key = self.data["auth_sig"].lower() if correct_key != key: raise AuthFailed(self, "Wrong authorization key") def odnoklassniki_oauth_sig(data, client_secret): """ Calculates signature of request data access_token value must be included Algorithm is described at https://apiok.ru/wiki/pages/viewpage.action?pageId=12878032, search for "little bit different way" """ suffix = odnoklassniki_sig("{:s}{:s}".format(data["access_token"], client_secret)) check_list = sorted( f"{key:s}={value:s}" for key, value in data.items() if key != "access_token" ) return odnoklassniki_sig("".join(check_list) + suffix) def odnoklassniki_iframe_sig(data, client_secret_or_session_secret): """ Calculates signature as described at: https://apiok.ru/wiki/display/ok/Authentication+and+Authorization If API method requires session context, request is signed with session secret key. Otherwise it is signed with application secret key """ param_list = sorted(f"{key:s}={value:s}" for key, value in data.items()) return odnoklassniki_sig("".join(param_list) + client_secret_or_session_secret) def odnoklassniki_api( backend, data, api_url, public_key, client_secret, request_type="oauth" ): """Calls Odnoklassniki REST API method https://apiok.ru/wiki/display/ok/Odnoklassniki+Rest+API""" data.update({"application_key": public_key, "format": "JSON"}) if request_type == "oauth": data["sig"] = odnoklassniki_oauth_sig(data, client_secret) elif request_type == "iframe_session": data["sig"] = odnoklassniki_iframe_sig(data, data["session_secret_key"]) elif request_type == "iframe_nosession": data["sig"] = odnoklassniki_iframe_sig(data, client_secret) else: msg = "Unknown request type {0}. How should it be signed?" raise AuthFailed(backend, msg.format(request_type)) return backend.get_json(api_url + "fb.do", params=data) social-auth-core-4.6.1/social_core/backends/okta.py000066400000000000000000000034241500362547200222340ustar00rootroot00000000000000""" Okta OAuth2 and OpenIdConnect: https://python-social-auth.readthedocs.io/en/latest/backends/okta.html """ from urllib.parse import urljoin from ..utils import append_slash from .oauth import BaseOAuth2 class OktaMixin(BaseOAuth2): def api_url(self): return append_slash(self.setting("API_URL")) def authorization_url(self): return self._url("v1/authorize") def access_token_url(self): return self._url("v1/token") def _url(self, path): return urljoin(append_slash(self.setting("API_URL")), path) def oidc_config(self): return self.get_json( self._url( "/.well-known/openid-configuration?client_id={}".format( self.setting("KEY") ) ) ) class OktaOAuth2(OktaMixin, BaseOAuth2): """Okta OAuth authentication backend""" name = "okta-oauth2" REDIRECT_STATE = False SCOPE_SEPARATOR = " " ID_KEY = "preferred_username" DEFAULT_SCOPE = ["openid", "profile", "email"] EXTRA_DATA = [ ("refresh_token", "refresh_token", True), ("expires_in", "expires"), ("token_type", "token_type", True), ] def get_user_details(self, response): """Return user details from Okta account""" return { "username": response.get("preferred_username"), "email": response.get("email") or "", "first_name": response.get("given_name"), "last_name": response.get("family_name"), } def user_data(self, access_token, *args, **kwargs): """Loads user data from Okta""" return self.get_json( self._url("v1/userinfo"), headers={ "Authorization": f"Bearer {access_token}", }, ) social-auth-core-4.6.1/social_core/backends/okta_openidconnect.py000066400000000000000000000006051500362547200251420ustar00rootroot00000000000000""" Okta OAuth2 and OpenIdConnect: https://python-social-auth.readthedocs.io/en/latest/backends/okta.html """ from .okta import OktaOAuth2 from .open_id_connect import OpenIdConnectAuth class OktaOpenIdConnect(OktaOAuth2, OpenIdConnectAuth): """Okta OpenID-Connect authentication backend""" name = "okta-openidconnect" REDIRECT_STATE = False RESPONSE_TYPE = "code" social-auth-core-4.6.1/social_core/backends/open_id.py000066400000000000000000000237601500362547200227200ustar00rootroot00000000000000from __future__ import annotations from openid.consumer.consumer import CANCEL, FAILURE, SUCCESS, Consumer from openid.consumer.discover import DiscoveryFailure from openid.extensions import ax, pape, sreg from ..exceptions import ( AuthCanceled, AuthException, AuthFailed, AuthMissingParameter, AuthUnknownError, ) from ..utils import url_add_parameters from .base import BaseAuth # OpenID configuration OLD_AX_ATTRS = [ ("http://schema.openid.net/contact/email", "old_email"), ("http://schema.openid.net/namePerson", "old_fullname"), ("http://schema.openid.net/namePerson/friendly", "old_nickname"), ] AX_SCHEMA_ATTRS = [ # Request both the full name and first/last components since some # providers offer one but not the other. ("http://axschema.org/contact/email", "email"), ("http://axschema.org/namePerson", "fullname"), ("http://axschema.org/namePerson/first", "first_name"), ("http://axschema.org/namePerson/last", "last_name"), ("http://axschema.org/namePerson/friendly", "nickname"), ] SREG_ATTR = [("email", "email"), ("fullname", "fullname"), ("nickname", "nickname")] OPENID_ID_FIELD = "openid_identifier" SESSION_NAME = "openid" class OpenIdAuth(BaseAuth): """Generic OpenID authentication backend""" name = "openid" URL: str | None = None USERNAME_KEY = "username" def get_user_id(self, details, response): """Return user unique id provided by service""" return response.identity_url def get_ax_attributes(self): attrs = self.setting("AX_SCHEMA_ATTRS", []) if attrs and self.setting("IGNORE_DEFAULT_AX_ATTRS", True): return attrs return attrs + AX_SCHEMA_ATTRS + OLD_AX_ATTRS def get_sreg_attributes(self): return self.setting("SREG_ATTR") or SREG_ATTR def values_from_response(self, response, sreg_names=None, ax_names=None): """Return values from SimpleRegistration response or AttributeExchange response if present. @sreg_names and @ax_names must be a list of name and aliases for such name. The alias will be used as mapping key. """ values = {} # Use Simple Registration attributes if provided if sreg_names: # pyright does not detect the classmethod correctly resp = sreg.SRegResponse.fromSuccessResponse(response) # type: ignore[reportCallIssue] if resp: values.update( (alias, resp.get(name) or "") for name, alias in sreg_names ) # Use Attribute Exchange attributes if provided if ax_names: # pyright does not detect the classmethod correctly resp = ax.FetchResponse.fromSuccessResponse(response) # type: ignore[reportCallIssue] if resp: for src, alias in ax_names: name = alias.replace("old_", "") values[name] = resp.getSingle(src, "") or values.get(name) return values def get_user_details(self, response): """Return user details from an OpenID request""" values = { "username": "", "email": "", "fullname": "", "first_name": "", "last_name": "", } # update values using SimpleRegistration or AttributeExchange # values values.update( self.values_from_response( response, self.get_sreg_attributes(), self.get_ax_attributes() ) ) fullname = values.get("fullname") or "" first_name = values.get("first_name") or "" last_name = values.get("last_name") or "" email = values.get("email") or "" if not fullname and first_name and last_name: fullname = first_name + " " + last_name elif fullname: try: first_name, last_name = fullname.rsplit(" ", 1) except ValueError: last_name = fullname username_key = self.setting("USERNAME_KEY") or self.USERNAME_KEY values.update( { "fullname": fullname, "first_name": first_name, "last_name": last_name, "username": values.get(username_key) or (first_name.title() + last_name.title()), "email": email, } ) return values def extra_data(self, user, uid, response, details=None, *args, **kwargs): """Return defined extra data names to store in extra_data field. Settings will be inspected to get more values names that should be stored on extra_data field. Setting name is created from current backend name (all uppercase) plus _SREG_EXTRA_DATA and _AX_EXTRA_DATA because values can be returned by SimpleRegistration or AttributeExchange schemas. Both list must be a value name and an alias mapping similar to SREG_ATTR, OLD_AX_ATTRS or AX_SCHEMA_ATTRS """ sreg_names = self.setting("SREG_EXTRA_DATA") ax_names = self.setting("AX_EXTRA_DATA") values = self.values_from_response(response, sreg_names, ax_names) from_details = super().extra_data(user, uid, {}, details, *args, **kwargs) values.update(from_details) return values def auth_url(self): """Return auth URL returned by service""" openid_request = self.setup_request(self.auth_extra_arguments()) # Construct completion URL, including page we should redirect to return_to = self.strategy.absolute_uri(self.redirect_uri) return openid_request.redirectURL(self.trust_root(), return_to) def auth_html(self): """Return auth HTML returned by service""" openid_request = self.setup_request(self.auth_extra_arguments()) return_to = self.strategy.absolute_uri(self.redirect_uri) form_tag = {"id": "openid_message"} return openid_request.htmlMarkup( self.trust_root(), return_to, form_tag_attrs=form_tag ) def trust_root(self): """Return trust-root option""" return self.setting("OPENID_TRUST_ROOT") or self.strategy.absolute_uri("/") def continue_pipeline(self, partial): """Continue previous halted pipeline""" response = self.consumer().complete( dict(self.data.items()), self.strategy.absolute_uri(self.redirect_uri) ) return self.strategy.authenticate( self, response=response, pipeline_index=partial.next_step, *partial.args, **partial.kwargs, ) def auth_complete(self, *args, **kwargs): """Complete auth process""" response = self.consumer().complete( dict(self.data.items()), self.strategy.absolute_uri(self.redirect_uri) ) self.process_error(response) return self.strategy.authenticate(self, response=response, *args, **kwargs) def process_error(self, data): if not data: raise AuthException(self, "OpenID relying party endpoint") if data.status == FAILURE: raise AuthFailed(self, data.message) if data.status == CANCEL: raise AuthCanceled(self) if data.status != SUCCESS: raise AuthUnknownError(self, data.status) def setup_request(self, params=None): """Setup request""" request = self.openid_request(params) # Request some user details. Use attribute exchange if provider # advertises support. if request.endpoint.supportsType(ax.AXMessage.ns_uri): fetch_request = ax.FetchRequest() # Mark all attributes as required, Google ignores optional ones for attr, alias in self.get_ax_attributes(): fetch_request.add(ax.AttrInfo(attr, alias=alias, required=True)) else: fetch_request = sreg.SRegRequest( optional=list(dict(self.get_sreg_attributes()).keys()) ) request.addExtension(fetch_request) # Add PAPE Extension for if configured preferred_policies = self.setting("OPENID_PAPE_PREFERRED_AUTH_POLICIES") preferred_level_types = self.setting("OPENID_PAPE_PREFERRED_AUTH_LEVEL_TYPES") max_age = self.setting("OPENID_PAPE_MAX_AUTH_AGE") if max_age is not None: try: max_age = int(max_age) except (ValueError, TypeError): max_age = None if max_age is not None or preferred_policies or preferred_level_types: pape_request = pape.Request( max_auth_age=max_age, preferred_auth_policies=preferred_policies, preferred_auth_level_types=preferred_level_types, ) request.addExtension(pape_request) return request def consumer(self): """Create an OpenID Consumer object for the given Django request.""" if not hasattr(self, "_consumer"): self._consumer = self.create_consumer(self.strategy.openid_store()) return self._consumer def create_consumer(self, store=None): return Consumer(self.strategy.openid_session_dict(SESSION_NAME), store) def uses_redirect(self): """Return true if openid request will be handled with redirect or HTML content will be returned. """ return self.openid_request().shouldSendRedirect() def openid_request(self, params: dict[str, str] | None = None): """Return openid request""" try: return self.consumer().begin(url_add_parameters(self.openid_url(), params)) except DiscoveryFailure as err: raise AuthException(self, f"OpenID discovery error: {err}") def openid_url(self): """Return service provider URL. This base class is generic accepting a POST parameter that specifies provider URL.""" if self.URL: return self.URL if OPENID_ID_FIELD in self.data: return self.data[OPENID_ID_FIELD] raise AuthMissingParameter(self, OPENID_ID_FIELD) social-auth-core-4.6.1/social_core/backends/open_id_connect.py000066400000000000000000000245051500362547200244270ustar00rootroot00000000000000from __future__ import annotations import base64 import datetime import json from calendar import timegm from typing import Any import jwt from jwt import ( ExpiredSignatureError, InvalidAudienceError, InvalidTokenError, PyJWTError, ) from jwt.utils import base64url_decode from social_core.backends.oauth import BaseOAuth2 from social_core.exceptions import AuthMissingParameter, AuthTokenError from social_core.utils import cache class OpenIdConnectAssociation: """Use Association model to save the nonce by force.""" def __init__(self, handle, secret="", issued=0, lifetime=0, assoc_type=""): self.handle = handle # as nonce self.secret = secret.encode() # not use self.issued = issued # not use self.lifetime = lifetime # not use self.assoc_type = assoc_type # as state class OpenIdConnectAuth(BaseOAuth2): """ Base class for Open ID Connect backends. Currently only the code response type is supported. It can also be directly instantiated as a generic OIDC backend. To use it you will need to set at minimum: SOCIAL_AUTH_OIDC_OIDC_ENDPOINT = 'https://.....' # endpoint without /.well-known/openid-configuration SOCIAL_AUTH_OIDC_KEY = '' SOCIAL_AUTH_OIDC_SECRET = '' """ name = "oidc" # Override OIDC_ENDPOINT in your subclass to enable autoconfig of OIDC OIDC_ENDPOINT: str | None = None ID_TOKEN_MAX_AGE = 600 DEFAULT_SCOPE = ["openid", "profile", "email"] EXTRA_DATA = ["id_token", "refresh_token", ("sub", "id")] REDIRECT_STATE = False REVOKE_TOKEN_METHOD = "GET" ID_KEY = "sub" USERNAME_KEY = "preferred_username" JWT_ALGORITHMS = ["RS256"] JWT_DECODE_OPTIONS: dict[str, Any] = {} # When these options are unspecified, server will choose via openid autoconfiguration ID_TOKEN_ISSUER = "" ACCESS_TOKEN_URL = "" AUTHORIZATION_URL = "" REVOKE_TOKEN_URL = "" USERINFO_URL = "" JWKS_URI = "" TOKEN_ENDPOINT_AUTH_METHOD = "" def __init__(self, *args, **kwargs): self.id_token = None super().__init__(*args, **kwargs) def get_setting_config( self, setting_name: str, oidc_name: str, default: str ) -> str: value = self.setting(setting_name, default) if not value: value = self.oidc_config().get(oidc_name) if not isinstance(value, str): raise AuthMissingParameter(self, setting_name) return value def authorization_url(self) -> str: return self.get_setting_config( "AUTHORIZATION_URL", "authorization_endpoint", self.AUTHORIZATION_URL ) def access_token_url(self) -> str: return self.get_setting_config( "ACCESS_TOKEN_URL", "token_endpoint", self.ACCESS_TOKEN_URL ) def revoke_token_url(self, token, uid) -> str: return self.get_setting_config( "REVOKE_TOKEN_URL", "revocation_endpoint", self.REVOKE_TOKEN_URL ) def id_token_issuer(self) -> str: return self.get_setting_config( "ID_TOKEN_ISSUER", "issuer", self.ID_TOKEN_ISSUER ) def userinfo_url(self) -> str: return self.get_setting_config( "USERINFO_URL", "userinfo_endpoint", self.USERINFO_URL ) def jwks_uri(self) -> str: return self.get_setting_config("JWKS_URI", "jwks_uri", self.JWKS_URI) def use_basic_auth(self) -> bool: method = self.setting( "TOKEN_ENDPOINT_AUTH_METHOD", self.TOKEN_ENDPOINT_AUTH_METHOD ) if method: return method == "client_secret_basic" methods = self.oidc_config().get("token_endpoint_auth_methods_supported", []) return not methods or "client_secret_basic" in methods def oidc_endpoint(self) -> str: return self.setting("OIDC_ENDPOINT", self.OIDC_ENDPOINT) @cache(ttl=86400) def oidc_config(self) -> dict[Any, Any]: return self.get_json(self.oidc_endpoint() + "/.well-known/openid-configuration") @cache(ttl=86400) def get_jwks_keys(self): return self.get_remote_jwks_keys() # Add client secret as oct key so it can be used for HMAC signatures # client_id, client_secret = self.get_key_and_secret() # keys.append({'key': client_secret, 'kty': 'oct'}) def get_remote_jwks_keys(self): response = self.request(self.jwks_uri()) return json.loads(response.text)["keys"] def auth_params(self, state=None): """Return extra arguments needed on auth process.""" params = super().auth_params(state) params["nonce"] = self.get_and_store_nonce(self.authorization_url(), state) return params def get_and_store_nonce(self, url, state): # Create a nonce nonce = self.strategy.random_string(64) # Store the nonce association = OpenIdConnectAssociation(nonce, assoc_type=state) self.strategy.storage.association.store(url, association) return nonce def get_nonce(self, nonce): try: return self.strategy.storage.association.get( server_url=self.authorization_url(), handle=nonce )[0] except IndexError: pass def remove_nonce(self, nonce_id): self.strategy.storage.association.remove([nonce_id]) def validate_claims(self, id_token): utc_timestamp = timegm(datetime.datetime.now(datetime.timezone.utc).timetuple()) if "nbf" in id_token and utc_timestamp < id_token["nbf"]: raise AuthTokenError(self, "Incorrect id_token: nbf") # Verify the token was issued in the last 10 minutes iat_leeway = self.setting("ID_TOKEN_MAX_AGE", self.ID_TOKEN_MAX_AGE) if utc_timestamp > id_token["iat"] + iat_leeway: raise AuthTokenError(self, "Incorrect id_token: iat") # Validate the nonce to ensure the request was not modified nonce = id_token.get("nonce") if not nonce: raise AuthTokenError(self, "Incorrect id_token: nonce") nonce_obj = self.get_nonce(nonce) if nonce_obj: self.remove_nonce(nonce_obj.id) else: raise AuthTokenError(self, "Incorrect id_token: nonce") def find_valid_key(self, id_token): kid = jwt.get_unverified_header(id_token).get("kid") keys = self.get_jwks_keys() if kid is not None: for key in keys: if kid == key.get("kid"): break else: # In case the key id is not found in the cached keys, just # reload the JWKS keys. Ideally this should be done by # invalidating the cache. self.get_jwks_keys.invalidate() # type: ignore[reportFunctionMemberAccess] keys = self.get_jwks_keys() for key in keys: if kid is None or kid == key.get("kid"): if "alg" not in key: key["alg"] = self.setting("JWT_ALGORITHMS", self.JWT_ALGORITHMS)[0] rsakey = jwt.PyJWK(key) message, encoded_sig = id_token.rsplit(".", 1) decoded_sig = base64url_decode(encoded_sig.encode("utf-8")) if rsakey.Algorithm.verify( message.encode("utf-8"), rsakey.key, decoded_sig ): return key return None def validate_and_return_id_token(self, id_token, access_token): """ Validates the id_token according to the steps at http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation. """ client_id, client_secret = self.get_key_and_secret() key = self.find_valid_key(id_token) if not key: raise AuthTokenError(self, "Signature verification failed") rsakey = jwt.PyJWK(key) try: claims = jwt.decode( id_token, rsakey.key, algorithms=self.setting("JWT_ALGORITHMS", self.JWT_ALGORITHMS), audience=client_id, issuer=self.id_token_issuer(), options=self.setting("JWT_DECODE_OPTIONS", self.JWT_DECODE_OPTIONS), ) except ExpiredSignatureError: raise AuthTokenError(self, "Signature has expired") except InvalidAudienceError: # compatibility with jose error message raise AuthTokenError(self, "Token error: Invalid audience") except InvalidTokenError as error: raise AuthTokenError(self, str(error)) except PyJWTError: raise AuthTokenError(self, "Invalid signature") # pyjwt does not validate OIDC claims # see https://github.com/jpadilla/pyjwt/pull/296 if "at_hash" in claims and claims["at_hash"] != self.calc_at_hash( access_token, key["alg"] ): raise AuthTokenError(self, "Invalid access token") self.validate_claims(claims) return claims def request_access_token(self, *args, **kwargs): """ Retrieve the access token. Also, validate the id_token and store it (temporarily). """ response = self.get_json(*args, **kwargs) self.id_token = self.validate_and_return_id_token( response["id_token"], response["access_token"] ) return response def user_data(self, access_token, *args, **kwargs): return self.get_json( self.userinfo_url(), headers={"Authorization": f"Bearer {access_token}"} ) def get_user_details(self, response): username_key = self.setting("USERNAME_KEY", self.USERNAME_KEY) return { "username": response.get(username_key), "email": response.get("email"), "fullname": response.get("name"), "first_name": response.get("given_name"), "last_name": response.get("family_name"), } @staticmethod def calc_at_hash(access_token, algorithm): """ Calculates "at_hash" claim which is not done by pyjwt. See https://pyjwt.readthedocs.io/en/stable/usage.html#oidc-login-flow """ alg_obj = jwt.get_algorithm_by_name(algorithm) digest = alg_obj.compute_hash_digest(access_token.encode("utf-8")) return ( base64.urlsafe_b64encode(digest[: (len(digest) // 2)]) .decode("utf-8") .rstrip("=") ) social-auth-core-4.6.1/social_core/backends/openinfra.py000066400000000000000000000025611500362547200232600ustar00rootroot00000000000000""" OpenInfra OpenId backend """ from urllib.parse import urlsplit from openid.extensions import ax from .open_id import OpenIdAuth class OpenInfraOpenId(OpenIdAuth): name = "openinfra" URL = "id.openinfra.dev" def get_user_details(self, response): """Generate username from identity url""" values = super().get_user_details(response) values["username"] = values.get("username") or urlsplit( response.identity_url ).path.strip("/") values["nickname"] = values.get("nickname", "") return values def setup_request(self, params=None): """Fetch email, firstname, lastname from openid""" request = self.openid_request(params) # TODO: use sreg instead ax request to fetch nickname as username fetch_request = ax.FetchRequest() fetch_request.add( ax.AttrInfo( "http://axschema.org/contact/email", alias="email", required=True ) ) fetch_request.add( ax.AttrInfo( "http://axschema.org/namePerson/first", alias="firstname", required=True ) ) fetch_request.add( ax.AttrInfo( "http://axschema.org/namePerson/last", alias="lastname", required=True ) ) request.addExtension(fetch_request) return request social-auth-core-4.6.1/social_core/backends/openshift.py000066400000000000000000000020421500362547200232700ustar00rootroot00000000000000""" Openshift OAuth2 backend """ from urllib.parse import urljoin from ..utils import append_slash from .oauth import BaseOAuth2 class OpenshiftOAuth2(BaseOAuth2): name = "openshift" def access_token_url(self): return urljoin(append_slash(self.setting("URL")), "oauth/token") def authorization_url(self): return urljoin(append_slash(self.setting("URL")), "oauth/authorize") def get_user_id(self, details, response): return response["metadata"]["uid"] def get_user_details(self, response): """Return user details from openshift account""" username = response["metadata"]["name"] email = response["metadata"]["name"] return {"username": username, "email": email} def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" headers = {"Authorization": "Bearer " + access_token} return self.request( urljoin(append_slash(self.setting("URL")), "oapi/v1/users/~"), headers=headers, ).json() social-auth-core-4.6.1/social_core/backends/openstack.py000066400000000000000000000025601500362547200232650ustar00rootroot00000000000000""" OpenStack OpenId backend """ from urllib.parse import urlsplit from openid.extensions import ax from .open_id import OpenIdAuth class OpenStackOpenId(OpenIdAuth): name = "openstack" URL = "openstackid.org" def get_user_details(self, response): """Generate username from identity url""" values = super().get_user_details(response) values["username"] = values.get("username") or urlsplit( response.identity_url ).path.strip("/") values["nickname"] = values.get("nickname", "") return values def setup_request(self, params=None): """Fetch email, firstname, lastname from openid""" request = self.openid_request(params) # TODO: use sreg instead ax request to fetch nickname as username fetch_request = ax.FetchRequest() fetch_request.add( ax.AttrInfo( "http://axschema.org/contact/email", alias="email", required=True ) ) fetch_request.add( ax.AttrInfo( "http://axschema.org/namePerson/first", alias="firstname", required=True ) ) fetch_request.add( ax.AttrInfo( "http://axschema.org/namePerson/last", alias="lastname", required=True ) ) request.addExtension(fetch_request) return request social-auth-core-4.6.1/social_core/backends/openstreetmap.py000066400000000000000000000035731500362547200241710ustar00rootroot00000000000000""" OpenStreetMap OAuth support. This adds support for OpenStreetMap OAuth service. An application must be registered first on OpenStreetMap and the settings SOCIAL_AUTH_OPENSTREETMAP_KEY and SOCIAL_AUTH_OPENSTREETMAP_SECRET must be defined with the corresponding values. More info: https://wiki.openstreetmap.org/wiki/OAuth """ from xml.dom import minidom from .oauth import BaseOAuth1 class OpenStreetMapOAuth(BaseOAuth1): """OpenStreetMap OAuth authentication backend""" name = "openstreetmap" AUTHORIZATION_URL = "https://www.openstreetmap.org/oauth/authorize" REQUEST_TOKEN_URL = "https://www.openstreetmap.org/oauth/request_token" ACCESS_TOKEN_URL = "https://www.openstreetmap.org/oauth/access_token" EXTRA_DATA = [ ("id", "id"), ("avatar", "avatar"), ("account_created", "account_created"), ] def get_user_details(self, response): """Return user details from OpenStreetMap account""" return { "username": response["username"], "email": "", "fullname": "", "first_name": "", "last_name": "", } def user_data(self, access_token, *args, **kwargs): """Return user data provided""" response = self.oauth_request( access_token, "https://api.openstreetmap.org/api/0.6/user/details" ) try: dom = minidom.parseString(response.content) except ValueError: return None user = dom.getElementsByTagName("user")[0] try: avatar = dom.getElementsByTagName("img")[0].getAttribute("href") except IndexError: avatar = None return { "id": user.getAttribute("id"), "username": user.getAttribute("display_name"), "account_created": user.getAttribute("account_created"), "avatar": avatar, } social-auth-core-4.6.1/social_core/backends/openstreetmap_oauth2.py000066400000000000000000000034201500362547200254420ustar00rootroot00000000000000""" OpenStreetMap OAuth 2.0 support. This adds support for OpenStreetMap OAuth service. An application must be registered first on OpenStreetMap and the settings SOCIAL_AUTH_OPENSTREETMAP_OAUTH2_KEY and SOCIAL_AUTH_OPENSTREETMAP_OAUTH2_SECRET must be defined with the corresponding values. More info: https://wiki.openstreetmap.org/wiki/OAuth """ from .oauth import BaseOAuth2PKCE class OpenStreetMapOAuth2(BaseOAuth2PKCE): """OpenStreetMap OAuth2 authentication backend""" name = "openstreetmap-oauth2" AUTHORIZATION_URL = "https://www.openstreetmap.org/oauth2/authorize" ACCESS_TOKEN_URL = "https://www.openstreetmap.org/oauth2/token" SCOPE_SEPARATOR = " " STATE_PARAMETER = True DEFAULT_SCOPE = ["read_prefs"] EXTRA_DATA = [ ("id", "id"), ("avatar", "avatar"), ("account_created", "account_created"), ] PKCE_DEFAULT_CODE_CHALLENGE_METHOD = "S256" DEFAULT_USE_PKCE = True def get_user_details(self, response): """Return user details from OpenStreetMap account""" return { "username": response["username"], "email": "", "fullname": "", "first_name": "", "last_name": "", } def user_data(self, access_token, *args, **kwargs): """Return user data provided""" headers = {"Authorization": f"Bearer {access_token}"} response = self.get_json( url="https://api.openstreetmap.org/api/0.6/user/details.json", headers=headers, ) return { "id": response["user"]["id"], "username": response["user"]["display_name"], "account_created": response["user"]["account_created"], "avatar": response["user"].get("img", {}).get("href"), } social-auth-core-4.6.1/social_core/backends/orbi.py000066400000000000000000000023431500362547200222300ustar00rootroot00000000000000""" Orbi OAuth2 backend """ from .oauth import BaseOAuth2 class OrbiOAuth2(BaseOAuth2): """Orbi OAuth2 authentication backend""" name = "orbi" AUTHORIZATION_URL = "https://login.orbi.kr/oauth/authorize" ACCESS_TOKEN_URL = "https://login.orbi.kr/oauth/token" EXTRA_DATA = [ ("imin", "imin"), ("nick", "nick"), ("photo", "photo"), ("sex", "sex"), ("birth", "birth"), ] def get_user_id(self, details, response): return response.get("id") def get_user_details(self, response): fullname, first_name, last_name = self.get_user_names( response.get("name", ""), response.get("first_name", ""), response.get("last_name", ""), ) return { "username": response.get("username", response.get("name")), "email": response.get("email", ""), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Load user data from orbi""" return self.get_json( "https://login.orbi.kr/oauth/user/get", params={"access_token": access_token}, ) social-auth-core-4.6.1/social_core/backends/orcid.py000066400000000000000000000130651500362547200224000ustar00rootroot00000000000000""" ORCID OAuth2 Application backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/orcid.html """ from .oauth import BaseOAuth2 class ORCIDOAuth2(BaseOAuth2): """ORCID OAuth2 authentication backend""" name = "orcid" ID_KEY = "orcid" AUTHORIZATION_URL = "https://orcid.org/oauth/authorize" ACCESS_TOKEN_URL = "https://orcid.org/oauth/token" USER_ID_URL = "https://orcid.org/oauth/userinfo" USER_DATA_URL = "https://pub.orcid.org/v2.0/{}" DEFAULT_SCOPE = ["/authenticate"] EXTRA_DATA = [ ("orcid", "id"), ("expires_in", "expires"), ("refresh_token", "refresh_token"), ] def auth_params(self, state=None): return super().auth_params(state) def get_user_details(self, response): """Return user details from ORCID account""" # response data will be of the following format: # { # 'orcid-identifier': { # 'uri': 'http://orcid.org/0000-0002-2601-8132', # 'path': '0000-0002-2601-8132', # 'host': 'orcid.org' # }, # 'person': { # 'last-modified-date': None, # 'name': { # 'created-date': { # 'value': 1578249746904 # }, # 'last-modified-date': { # 'value': 1578249746904 # }, # 'given-names': { # 'value': 'Janani Kantharooban' # }, # 'family-name': { # 'value': 'Umachanger' # }, # 'credit-name': None, # 'source': None, # 'visibility': 'PUBLIC', # 'path': '0000-0002-2601-8132' # }, # } # } orcid_identifier = response.get("orcid-identifier") fullname = first_name = last_name = email = username = "" person = response.get("person") # Although we're checking here, the response will always have the orcid-identifier key: if orcid_identifier: username = orcid_identifier["path"] if person: name = person.get("name") fullname = name if name: first_name = name.get("given-names", {}).get("value", "") if (family_name := name.get("family-name", None)) is not None: last_name = family_name.get("value", "") emails = person.get("emails") if emails: emails_list = emails.get("email") if emails_list and len(emails_list) > 0: email = emails_list[0].get("email", "") if len(emails_list) > 1: for email_dict in emails_list: if email_dict.get("primary"): email = email_dict["email"] break else: email = emails_list[0].get("email", "") return { "username": username, "email": email, "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" params = self.setting("PROFILE_EXTRA_PARAMS", {}) params["access_token"] = access_token # Reference Docs: ORCID Auth Flow: # https://github.com/ORCID/ORCID-Source/blob/master/orcid-web/ORCID_AUTH_WITH_OPENID_CONNECT.md#other-endpoints # Sample headers: -H "Accept: application/json" -H "Authorization: Bearer " # This will respond with a json document like this: # { # "sub":"0000-0002-2601-8132", # "name":"Credit Name", # "family_name":"Jones", # "given_name":"Tom" # } try: response = self.get_json( self.USER_ID_URL, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {access_token!s}", }, ) # Update Jan 28 2021: Now we definitely have an ORCID id of format "0000-0000-0000-0000" orcid = response["sub"] except Exception: pass # We can now attempt to access the ORCID public API with the Orcid: try: return self.get_json( self.USER_DATA_URL.format(orcid), headers={"Content-Type": "application/json"}, params=params, ) except Exception: return None class ORCIDOAuth2Sandbox(ORCIDOAuth2): """ORCID OAuth2 Sandbox authentication backend""" name = "orcid-sandbox" AUTHORIZATION_URL = "https://sandbox.orcid.org/oauth/authorize" ACCESS_TOKEN_URL = "https://sandbox.orcid.org/oauth/token" USER_ID_URL = "https://sandbox.orcid.org/oauth/userinfo" USER_DATA_URL = "https://pub.sandbox.orcid.org/v2.0/{}" class ORCIDMemberOAuth2(ORCIDOAuth2): """ORCID OAuth2 authentication backend that uses ORCID Member API""" USER_DATA_URL = "https://api.orcid.org/v2.0/{}" DEFAULT_SCOPE = ["/authenticate", "/read-limited"] class ORCIDMemberOAuth2Sandbox(ORCIDOAuth2Sandbox): """ORCID OAuth2 Sandbox authentication backend that uses ORCID Member Sandbox API""" USER_DATA_URL = "https://api.sandbox.orcid.org/v2.0/{}" DEFAULT_SCOPE = ["/authenticate", "/read-limited"] social-auth-core-4.6.1/social_core/backends/osso.py000066400000000000000000000032251500362547200222600ustar00rootroot00000000000000from urllib.parse import urlencode from .oauth import BaseOAuth2 class OssoOAuth2(BaseOAuth2): """Osso OAuth authentication backend""" name = "osso" REDIRECT_STATE = False STATE_PARAMETER = True AUTHORIZATION_URL = "{osso_base_url}/oauth/authorize" ACCESS_TOKEN_URL = "{osso_base_url}/oauth/token" @property def osso_base_url(self): return self.setting("OSSO_BASE_URL", "https://demo.ossoapp.com") def authorization_url(self): return self.AUTHORIZATION_URL.format(osso_base_url=self.osso_base_url) def access_token_url(self): return self.ACCESS_TOKEN_URL.format(osso_base_url=self.osso_base_url) def auth_params(self, state=None): client_id, _client_secret = self.get_key_and_secret() params = {"client_id": client_id, "redirect_uri": self.get_redirect_uri(state)} if self.data.get("email"): params["email"] = self.data.get("email") if self.data.get("domain") and not self.data.get("email"): params["domain"] = self.data.get("domain") if self.STATE_PARAMETER and state: params["state"] = state if self.RESPONSE_TYPE: params["response_type"] = self.RESPONSE_TYPE return params def get_user_details(self, response): """Return user details from Osso""" return {"username": response.get("email"), "email": response.get("email")} def user_data(self, access_token, *args, **kwargs): """Loads normalized user profile from Osso""" url = f"{self.osso_base_url}/oauth/me?" + urlencode( {"access_token": access_token} ) return self.get_json(url) social-auth-core-4.6.1/social_core/backends/patreon.py000066400000000000000000000024171500362547200227470ustar00rootroot00000000000000""" Patreon OAuth2 backend https://www.patreon.com/platform/documentation/oauth """ from .oauth import BaseOAuth2 class PatreonOAuth2(BaseOAuth2): """Patreon OAuth2 authentication backend""" name = "patreon" AUTHORIZATION_URL = "https://www.patreon.com/oauth2/authorize" ACCESS_TOKEN_URL = "https://www.patreon.com/api/oauth2/token" REVOKE_TOKEN_URL = "https://www.patreon.com/oauth2/revoke" REDIRECT_STATE = False ID_KEY = "id" EXTRA_DATA = [ ("id", "id"), ] def get_user_details(self, response): details = response["attributes"] return { "username": details.get("full_name"), "email": details.get("email"), "fullname": details.get("full_name"), "first_name": details.get("first_name"), "last_name": details.get("last_name"), } def user_data(self, access_token, *args, **kwargs): return self.get_api(access_token, "identity")["data"] def get_api(self, access_token, suffix): return self.get_json( f"https://www.patreon.com/api/oauth2/v2/{suffix}", headers=self.get_auth_header(access_token), ) def get_auth_header(self, access_token): return {"Authorization": f"Bearer {access_token}"} social-auth-core-4.6.1/social_core/backends/paypal.py000066400000000000000000000046211500362547200225640ustar00rootroot00000000000000import base64 from .oauth import BaseOAuth2 class PayPalOAuth2(BaseOAuth2): """ PayPal OAuth2 backend, docs at: https://developer.paypal.com/docs/connect-with-paypal/integrate/ """ name = "paypal-oauth2" ID_KEY = "user_id" AUTHORIZATION_URL = "https://www.paypal.com/connect" ACCESS_TOKEN_URL = "https://api.paypal.com/v1/oauth2/token" USER_DATA_URL = ( "https://api.paypal.com/v1/identity/oauth2/userinfo?schema=paypalv1.1" ) DEFAULT_SCOPE = ["openid", "profile"] REFRESH_TOKEN_METHOD = "POST" REDIRECT_STATE = False def user_data(self, access_token, *args, **kwargs): auth_header = {"Authorization": f"Bearer {access_token}"} return self.get_json(self.USER_DATA_URL, headers=auth_header) def get_user_details(self, response): username = response.get(self.ID_KEY).split("/")[-1] fullname, first_name, last_name = self.get_user_names( response.get("name", ""), response.get("given_name", ""), response.get("family_name", ""), ) emails = response.get("emails", []) email = self.get_email(emails) return { "username": username, "email": email, "fullname": fullname, "first_name": first_name, "last_name": last_name, } def auth_complete_params(self, state=None): return { "grant_type": "authorization_code", "code": self.data.get("code", ""), } def auth_headers(self): auth = ("{}:{}".format(*self.get_key_and_secret())).encode() return {"Authorization": b"Basic " + base64.urlsafe_b64encode(auth)} def refresh_token_params(self, token, *args, **kwargs): return {"refresh_token": token, "grant_type": "refresh_token"} @staticmethod def get_email(emails): if not emails: return "" primary_emails = (email for email in emails if email.get("primary", False)) primary_or_first = next(primary_emails, emails[0]) return primary_or_first.get("value") class PayPalOAuth2Sandbox(PayPalOAuth2): name = "paypal-oauth2-sandbox" AUTHORIZATION_URL = "https://www.sandbox.paypal.com/connect" ACCESS_TOKEN_URL = "https://api.sandbox.paypal.com/v1/oauth2/token" USER_DATA_URL = ( "https://api.sandbox.paypal.com/v1/identity/oauth2/userinfo?schema=paypalv1.1" ) social-auth-core-4.6.1/social_core/backends/persona.py000066400000000000000000000035421500362547200227460ustar00rootroot00000000000000""" Mozilla Persona authentication backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/persona.html """ from ..exceptions import AuthFailed, AuthMissingParameter from ..utils import handle_http_errors from .base import BaseAuth class PersonaAuth(BaseAuth): """BrowserID authentication backend""" name = "persona" def get_user_id(self, details, response): """Use BrowserID email as ID""" return details["email"] def get_user_details(self, response): """Return user details, BrowserID only provides Email.""" # {'status': 'okay', # 'audience': 'localhost:8000', # 'expires': 1328983575529, # 'email': 'name@server.com', # 'issuer': 'browserid.org'} email = response["email"] return { "username": email.split("@", 1)[0], "email": email, "fullname": "", "first_name": "", "last_name": "", } def extra_data(self, user, uid, response, details=None, *args, **kwargs): """Return users extra data""" return {"audience": response["audience"], "issuer": response["issuer"]} @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" if "assertion" not in self.data: raise AuthMissingParameter(self, "assertion") response = self.get_json( "https://browserid.org/verify", data={ "assertion": self.data["assertion"], "audience": self.strategy.request_host(), }, method="POST", ) if response.get("status") == "failure": raise AuthFailed(self) kwargs.update({"response": response, "backend": self}) return self.strategy.authenticate(*args, **kwargs) social-auth-core-4.6.1/social_core/backends/phabricator.py000066400000000000000000000027761500362547200236050ustar00rootroot00000000000000""" Phabricator OAuth2 backend, docs at: https://secure.phabricator.com/book/phabcontrib/article/using_oauthserver/ """ from .oauth import BaseOAuth2 class PhabricatorOAuth2(BaseOAuth2): """Phabricator OAuth authentication backend""" name = "phabricator" API_URL = "https://secure.phabricator.com" AUTHORIZATION_URL = "https://secure.phabricator.com/oauthserver/auth/" ACCESS_TOKEN_URL = "https://secure.phabricator.com/oauthserver/token/" REDIRECT_STATE = False def api_url(self, path): api_url = self.setting("API_URL") or self.API_URL return "{}{}".format(api_url.rstrip("/"), path) def authorization_url(self): return self.api_url("/oauthserver/auth/") def access_token_url(self): return self.api_url("/oauthserver/token/") def get_user_details(self, response): """Return user details from Phabricator""" fullname, first_name, last_name = self.get_user_names(response.get("realName")) return { "id": response.get("phid"), "username": response.get("userName"), "email": response.get("primaryEmail", ""), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from API""" return self.get_json( self.api_url("/api/user.whoami"), params={ "access_token": access_token, }, ) social-auth-core-4.6.1/social_core/backends/ping.py000066400000000000000000000051201500362547200222260ustar00rootroot00000000000000""" Ping Auth OpenID Connect backend """ from jose import jwk, jwt from jose.exceptions import ExpiredSignatureError, JWTClaimsError, JWTError from jose.utils import base64url_decode from social_core.backends.open_id_connect import OpenIdConnectAuth from social_core.exceptions import AuthTokenError class PingOpenIdConnect(OpenIdConnectAuth): name = "ping" # OIDC_ENDPOINT has the form 'https://auth.pingone.com//as' OIDC_ENDPOINT = "" REDIRECT_STATE = False RESPONSE_TYPE = "code" USERNAME_KEY = "preferred_username" def find_valid_key(self, id_token): for key in self.get_jwks_keys(): rsakey = jwk.construct(key, algorithm="RS256") message, encoded_sig = id_token.rsplit(".", 1) decoded_sig = base64url_decode(encoded_sig.encode("utf-8")) if rsakey.verify(message.encode("utf-8"), decoded_sig): return key return None def validate_and_return_id_token(self, id_token, access_token): """ Validates the id_token according to the steps at http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation. """ client_id, client_secret = self.get_key_and_secret() key = self.find_valid_key(id_token) if not key: raise AuthTokenError(self, "Signature verification failed") rsakey = jwk.construct(key, algorithm="RS256") try: claims = jwt.decode( id_token, rsakey.to_pem().decode("utf-8"), algorithms=self.JWT_ALGORITHMS, audience=client_id, issuer=self.id_token_issuer(), access_token=access_token, options=self.JWT_DECODE_OPTIONS, ) except ExpiredSignatureError: raise AuthTokenError(self, "Signature has expired") except JWTClaimsError as error: raise AuthTokenError(self, str(error)) except JWTError: raise AuthTokenError(self, "Invalid signature") self.validate_claims(claims) return claims def get_user_details(self, response): username_key = self.setting("USERNAME_KEY", default=self.USERNAME_KEY) fullname, first_name, last_name = self.get_user_names( first_name=response.get("given_name"), last_name=response.get("family_name") ) return { "username": response.get(username_key), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } social-auth-core-4.6.1/social_core/backends/pinterest.py000066400000000000000000000025011500362547200233060ustar00rootroot00000000000000""" Pinterest OAuth2 backend, docs at: https://developers.pinterest.com/docs/api/authentication/ """ from .oauth import BaseOAuth2 class PinterestOAuth2(BaseOAuth2): name = "pinterest" ID_KEY = "user_id" AUTHORIZATION_URL = "https://api.pinterest.com/oauth/" ACCESS_TOKEN_URL = "https://api.pinterest.com/v1/oauth/token" REDIRECT_STATE = False def user_data(self, access_token, *args, **kwargs): response = self.get_json( "https://api.pinterest.com/v1/me/", params={"access_token": access_token} ) if "data" in response: username = response["data"]["url"].strip("/").split("/")[-1] response = { "user_id": response["data"]["id"], "first_name": response["data"]["first_name"], "last_name": response["data"]["last_name"], "username": username, } return response def get_user_details(self, response): fullname, first_name, last_name = self.get_user_names( first_name=response["first_name"], last_name=response["last_name"] ) return { "username": response.get("username"), "email": None, "fullname": fullname, "first_name": first_name, "last_name": last_name, } social-auth-core-4.6.1/social_core/backends/pixelpin.py000066400000000000000000000023071500362547200231250ustar00rootroot00000000000000from .open_id_connect import OpenIdConnectAuth class PixelPinOpenIDConnect(OpenIdConnectAuth): """PixelPin OpenID Connect authentication backend""" name = "pixelpin-openidconnect" ID_KEY = "sub" AUTHORIZATION_URL = "https://login.pixelpin.io/connect/authorize" ACCESS_TOKEN_URL = "https://login.pixelpin.io/connect/token" OIDC_ENDPOINT = "https://login.pixelpin.io" JWKS_URI = "https://login.pixelpin.io/.well-known/jwks" REQUIRES_EMAIL_VALIDATION = False def get_user_details(self, response): """Return user details from PixelPin account""" first_name = response.get("given_name") last_name = response.get("family_name") sub = response.get("sub") username = first_name + last_name + sub return { "username": username, "email": response.get("email"), "fullname": first_name + " " + last_name, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): return self.get_json( "https://login.pixelpin.io/connect/userinfo", headers={"Authorization": f"Bearer {access_token}"}, ) social-auth-core-4.6.1/social_core/backends/pocket.py000066400000000000000000000031651500362547200225650ustar00rootroot00000000000000""" Pocket OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/pocket.html """ from ..utils import handle_http_errors from .base import BaseAuth class PocketAuth(BaseAuth): name = "pocket" AUTHORIZATION_URL = "https://getpocket.com/auth/authorize" ACCESS_TOKEN_URL = "https://getpocket.com/v3/oauth/authorize" REQUEST_TOKEN_URL = "https://getpocket.com/v3/oauth/request" ID_KEY = "username" def get_json(self, url, *args, **kwargs): headers = {"X-Accept": "application/json"} kwargs.update({"method": "POST", "headers": headers}) return super().get_json(url, *args, **kwargs) def get_user_details(self, response): return {"username": response["username"]} def extra_data(self, user, uid, response, details=None, *args, **kwargs): return response def auth_url(self): data = { "consumer_key": self.setting("KEY"), "redirect_uri": self.redirect_uri, } token = self.get_json(self.REQUEST_TOKEN_URL, data=data)["code"] self.strategy.session_set("pocket_request_token", token) return f"{self.AUTHORIZATION_URL}?request_token={token}&redirect_uri={self.redirect_uri}" @handle_http_errors def auth_complete(self, *args, **kwargs): data = { "consumer_key": self.setting("KEY"), "code": self.strategy.session_get("pocket_request_token"), } response = self.get_json(self.ACCESS_TOKEN_URL, data=data) kwargs.update({"response": response, "backend": self}) return self.strategy.authenticate(*args, **kwargs) social-auth-core-4.6.1/social_core/backends/podio.py000066400000000000000000000023271500362547200224110ustar00rootroot00000000000000""" Podio OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/podio.html """ from .oauth import BaseOAuth2 class PodioOAuth2(BaseOAuth2): """Podio OAuth authentication backend""" name = "podio" AUTHORIZATION_URL = "https://podio.com/oauth/authorize" ACCESS_TOKEN_URL = "https://podio.com/oauth/token" EXTRA_DATA = [ ("access_token", "access_token"), ("token_type", "token_type"), ("expires_in", "expires"), ("refresh_token", "refresh_token"), ] def get_user_id(self, details, response): return response["ref"]["id"] def get_user_details(self, response): fullname, first_name, last_name = self.get_user_names( response["profile"]["name"] ) return { "username": f"user_{response['user']['user_id']}", "email": response["user"]["mail"], "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): return self.get_json( "https://api.podio.com/user/status", headers={"Authorization": "OAuth2 " + access_token}, ) social-auth-core-4.6.1/social_core/backends/pushbullet.py000066400000000000000000000014261500362547200234650ustar00rootroot00000000000000import base64 from .oauth import BaseOAuth2 class PushbulletOAuth2(BaseOAuth2): """pushbullet OAuth authentication backend""" name = "pushbullet" EXTRA_DATA = [("id", "id")] ID_KEY = "username" AUTHORIZATION_URL = "https://www.pushbullet.com/authorize" REQUEST_TOKEN_URL = "https://api.pushbullet.com/oauth2/token" ACCESS_TOKEN_URL = "https://api.pushbullet.com/oauth2/token" STATE_PARAMETER = False def get_user_details(self, response): return {"username": response.get("access_token")} def get_user_id(self, details, response): auth = "Basic {}".format(base64.b64encode(details["username"])) return self.get_json( "https://api.pushbullet.com/v2/users/me", headers={"Authorization": auth} )["iden"] social-auth-core-4.6.1/social_core/backends/qiita.py000066400000000000000000000056441500362547200224130ustar00rootroot00000000000000""" Qiita OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/qiita.html http://qiita.com/api/v2/docs#get-apiv2oauthauthorize https://qiita.com/api/v2/docs#get-apiv2authenticated_user """ import json from social_core.exceptions import AuthException from .oauth import BaseOAuth2 class QiitaOAuth2(BaseOAuth2): """Qiita OAuth authentication backend""" name = "qiita" AUTHORIZATION_URL = "https://qiita.com/api/v2/oauth/authorize" ACCESS_TOKEN_URL = "https://qiita.com/api/v2/access_tokens" SCOPE_SEPARATOR = " " REDIRECT_STATE = True EXTRA_DATA = [ ("description", "description"), ("facebook_id", "facebook_id"), ("followees_count", "followees_count"), ("followers_count", "followers_count"), ("github_login_name", "github_login_name"), ("id", "id"), ("items_count", "items_count"), ("linkedin_id", "linkedin_id"), ("location", "location"), ("name", "name"), ("organization", "organization"), ("permanent_id", "permanent_id"), ("profile_image_url", "profile_image_url"), ("team_only", "team_only"), ("twitter_screen_name", "twitter_screen_name"), ("website_url", "website_url"), ("image_monthly_upload_limit", "image_monthly_upload_limit"), ("image_monthly_upload_remaining", "image_monthly_upload_remaining"), ] # TODO: I am pretty sure this method returns the wrong type; it should # return a dict def auth_complete_params(self, state=None): # type: ignore[reportIncompatibleMethodOverride] data = super().auth_complete_params(state) if "grant_type" in data: del data["grant_type"] if "redirect_uri" in data: del data["redirect_uri"] return json.dumps(data) def auth_headers(self): return {"Content-Type": "application/json"} def request_access_token(self, *args, **kwargs): data = super().request_access_token(*args, **kwargs) data.update({"access_token": data["token"]}) return data def get_user_details(self, response): """Return user details from Qiita account""" return { "username": response["id"], "fullname": response["name"], } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://qiita.com/api/v2/authenticated_user", headers={"Authorization": f"Bearer {access_token}"}, ) def get_user_id(self, details, response): """Return user id""" user_id = None if self.setting("IDENTIFIED_BY_PERMANENT_ID"): user_id = response.get("permanent_id") else: user_id = response.get("id") if user_id is not None: return str(user_id) raise AuthException(self, "failed to get user id") social-auth-core-4.6.1/social_core/backends/qq.py000066400000000000000000000042031500362547200217130ustar00rootroot00000000000000""" Created on May 13, 2014 @author: Yong Zhang (zyfyfe@gmail.com) """ import json from ..utils import parse_qs from .oauth import BaseOAuth2 class QQOAuth2(BaseOAuth2): name = "qq" ID_KEY = "openid" AUTHORIZE_URL = "https://graph.qq.com/oauth2.0/authorize" ACCESS_TOKEN_URL = "https://graph.qq.com/oauth2.0/token" AUTHORIZATION_URL = "https://graph.qq.com/oauth2.0/authorize" OPENID_URL = "https://graph.qq.com/oauth2.0/me" REDIRECT_STATE = False EXTRA_DATA = [ ("nickname", "username"), ("figureurl_qq_1", "profile_image_url"), ("gender", "gender"), ] def get_user_details(self, response): """ Return user detail from QQ account sometimes nickname will duplicate with another qq account, to avoid this issue it's possible to use openid as username. """ if self.setting("USE_OPENID_AS_USERNAME", False): username = response.get("openid", "") else: username = response.get("nickname", "") fullname, first_name, last_name = self.get_user_names( first_name=response.get("nickname", "") ) return { "username": username, "fullname": fullname, "first_name": first_name, "last_name": last_name, } def get_openid(self, access_token): response = self.request(self.OPENID_URL, params={"access_token": access_token}) content = response.content.decode() data = json.loads(content[10:-3]) return data["openid"] def user_data(self, access_token, *args, **kwargs): openid = self.get_openid(access_token) response = self.get_json( "https://graph.qq.com/user/get_user_info", params={ "access_token": access_token, "oauth_consumer_key": self.setting("KEY"), "openid": openid, }, ) response["openid"] = openid return response def request_access_token(self, url, data, *args, **kwargs): response = self.request(url, *args, **kwargs) return parse_qs(response.content) social-auth-core-4.6.1/social_core/backends/quizlet.py000066400000000000000000000011671500362547200227750ustar00rootroot00000000000000""" Quizlet OAuth2 Sign-in backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/quizlet.html """ from .oauth import BaseOAuth2 class QuizletOAuth2(BaseOAuth2): """Quizlet OAuth2""" name = "quizlet" ID_KEY = "user_id" API_URL = "https://api.quizlet.com/2.0/" AUTHORIZATION_URL = "https://quizlet.com/authorize" ACCESS_TOKEN_URL = "https://api.quizlet.com/oauth/token" SCOPE_SEPARATOR = " " DEFAULT_SCOPE = ["read"] def get_user_details(self, response): """Return user details from Quizlet account""" return {"username": response.get("user_id")} social-auth-core-4.6.1/social_core/backends/rdio.py000066400000000000000000000050141500362547200222300ustar00rootroot00000000000000""" Rdio OAuth1 and OAuth2 backends, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/rdio.html """ from .oauth import BaseOAuth1, BaseOAuth2, OAuthAuth RDIO_API = "https://www.rdio.com/api/1/" class BaseRdio(OAuthAuth): ID_KEY = "key" def get_user_details(self, response): fullname, first_name, last_name = self.get_user_names( fullname=response["displayName"], first_name=response["firstName"], last_name=response["lastName"], ) return { "username": response["username"], "fullname": fullname, "first_name": first_name, "last_name": last_name, } class RdioOAuth1(BaseRdio, BaseOAuth1): """Rdio OAuth authentication backend""" name = "rdio-oauth1" REQUEST_TOKEN_URL = "http://api.rdio.com/oauth/request_token" AUTHORIZATION_URL = "https://www.rdio.com/oauth/authorize" ACCESS_TOKEN_URL = "http://api.rdio.com/oauth/access_token" EXTRA_DATA = [ ("key", "rdio_id"), ("icon", "rdio_icon_url"), ("url", "rdio_profile_url"), ("username", "rdio_username"), ("streamRegion", "rdio_stream_region"), ] def user_data(self, access_token, *args, **kwargs): """Return user data provided""" params = { "method": "currentUser", "extras": "username,displayName,streamRegion", } request = self.oauth_request(access_token, RDIO_API, params, method="POST") # TODO: I don't think to_postdata exists. return self.get_json(request.url, method="POST", data=request.to_postdata())[ # type: ignore[reportAttributeAccessIssue] "result" ] class RdioOAuth2(BaseRdio, BaseOAuth2): name = "rdio-oauth2" AUTHORIZATION_URL = "https://www.rdio.com/oauth2/authorize" ACCESS_TOKEN_URL = "https://www.rdio.com/oauth2/token" EXTRA_DATA = [ ("key", "rdio_id"), ("icon", "rdio_icon_url"), ("url", "rdio_profile_url"), ("username", "rdio_username"), ("streamRegion", "rdio_stream_region"), ("refresh_token", "refresh_token", True), ("token_type", "token_type", True), ] def user_data(self, access_token, *args, **kwargs): return self.get_json( RDIO_API, method="POST", data={ "method": "currentUser", "extras": "username,displayName,streamRegion", "access_token": access_token, }, )["result"] social-auth-core-4.6.1/social_core/backends/readability.py000066400000000000000000000024311500362547200235640ustar00rootroot00000000000000""" Readability OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/readability.html """ from .oauth import BaseOAuth1 READABILITY_API = "https://www.readability.com/api/rest/v1" class ReadabilityOAuth(BaseOAuth1): """Readability OAuth authentication backend""" name = "readability" ID_KEY = "username" AUTHORIZATION_URL = f"{READABILITY_API}/oauth/authorize/" REQUEST_TOKEN_URL = f"{READABILITY_API}/oauth/request_token/" ACCESS_TOKEN_URL = f"{READABILITY_API}/oauth/access_token/" EXTRA_DATA = [ ("date_joined", "date_joined"), ("kindle_email_address", "kindle_email_address"), ("avatar_url", "avatar_url"), ("email_into_address", "email_into_address"), ] def get_user_details(self, response): fullname, first_name, last_name = self.get_user_names( first_name=response["first_name"], last_name=response["last_name"] ) return { "username": response["username"], "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token): return self.get_json( READABILITY_API + "/users/_current", auth=self.oauth_auth(access_token) ) social-auth-core-4.6.1/social_core/backends/reddit.py000066400000000000000000000034701500362547200225520ustar00rootroot00000000000000""" Reddit OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/reddit.html """ import base64 from .oauth import BaseOAuth2 class RedditOAuth2(BaseOAuth2): """Reddit OAuth2 authentication backend""" name = "reddit" AUTHORIZATION_URL = "https://ssl.reddit.com/api/v1/authorize" ACCESS_TOKEN_URL = "https://ssl.reddit.com/api/v1/access_token" REFRESH_TOKEN_METHOD = "POST" REDIRECT_STATE = False SCOPE_SEPARATOR = "," DEFAULT_SCOPE = ["identity"] SEND_USER_AGENT = True EXTRA_DATA = [ ("id", "id"), ("name", "username"), ("link_karma", "link_karma"), ("comment_karma", "comment_karma"), ("refresh_token", "refresh_token"), ("expires_in", "expires"), ] def get_user_details(self, response): """Return user details from Reddit account""" return { "username": response.get("name"), "email": "", "fullname": "", "first_name": "", "last_name": "", } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://oauth.reddit.com/api/v1/me.json", headers={"Authorization": "bearer " + access_token}, ) def auth_headers(self): return { "Authorization": b"Basic " + base64.urlsafe_b64encode( "{}:{}".format(*self.get_key_and_secret()).encode() ) } def refresh_token_params(self, token, redirect_uri=None, *args, **kwargs): params = super().refresh_token_params(token) params["redirect_uri"] = self.redirect_uri or redirect_uri return params def auth_complete_credentials(self): return self.get_key_and_secret() social-auth-core-4.6.1/social_core/backends/runkeeper.py000066400000000000000000000033611500362547200232760ustar00rootroot00000000000000""" RunKeeper OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/runkeeper.html """ from .oauth import BaseOAuth2 class RunKeeperOAuth2(BaseOAuth2): """RunKeeper OAuth authentication backend""" name = "runkeeper" AUTHORIZATION_URL = "https://runkeeper.com/apps/authorize" ACCESS_TOKEN_URL = "https://runkeeper.com/apps/token" EXTRA_DATA = [ ("userID", "id"), ] def get_user_id(self, details, response): return response["userID"] def get_user_details(self, response): """Parse username from profile link""" username = None profile_url = response.get("profile") if len(profile_url): profile_url_parts = profile_url.split("http://runkeeper.com/user/") if len(profile_url_parts) > 1 and len(profile_url_parts[1]): username = profile_url_parts[1] fullname, first_name, last_name = self.get_user_names( fullname=response.get("name") ) return { "username": username, "email": response.get("email") or "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): # We need to use the /user endpoint to get the user id, the /profile # endpoint contains name, user name, location, gender user_data = self._user_data(access_token, "/user") profile_data = self._user_data(access_token, "/profile") return dict(user_data, **profile_data) def _user_data(self, access_token, path): url = f"https://api.runkeeper.com{path}" return self.get_json(url, params={"access_token": access_token}) social-auth-core-4.6.1/social_core/backends/salesforce.py000066400000000000000000000033761500362547200234320ustar00rootroot00000000000000from urllib.parse import urlencode from .oauth import BaseOAuth2 class SalesforceOAuth2(BaseOAuth2): """Salesforce OAuth2 authentication backend""" name = "salesforce-oauth2" AUTHORIZATION_URL = "https://login.salesforce.com/services/oauth2/authorize" ACCESS_TOKEN_URL = "https://login.salesforce.com/services/oauth2/token" REVOKE_TOKEN_URL = "https://login.salesforce.com/services/oauth2/revoke" REFRESH_TOKEN_METHOD = "POST" SCOPE_SEPARATOR = " " EXTRA_DATA = [ ("id", "id"), ("instance_url", "instance_url"), ("issued_at", "issued_at"), ("signature", "signature"), ("refresh_token", "refresh_token"), ] def get_user_details(self, response): """Return user details from a Salesforce account""" return { "username": response.get("username"), "email": response.get("email") or "", "first_name": response.get("first_name"), "last_name": response.get("last_name"), "fullname": response.get("display_name"), } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" user_id_url = kwargs.get("response").get("id") url = user_id_url + "?" + urlencode({"access_token": access_token}) try: return self.get_json(url) except ValueError: return None class SalesforceOAuth2Sandbox(SalesforceOAuth2): """Salesforce OAuth2 authentication testing backend""" name = "salesforce-oauth2-sandbox" AUTHORIZATION_URL = "https://test.salesforce.com/services/oauth2/authorize" ACCESS_TOKEN_URL = "https://test.salesforce.com/services/oauth2/token" REVOKE_TOKEN_URL = "https://test.salesforce.com/services/oauth2/revoke" social-auth-core-4.6.1/social_core/backends/saml.py000066400000000000000000000350011500362547200222260ustar00rootroot00000000000000""" Backend for SAML 2.0 support Terminology: "Service Provider" (SP): Your web app "Identity Provider" (IdP): The third-party site that is authenticating users via SAML """ import json from onelogin.saml2.auth import OneLogin_Saml2_Auth from onelogin.saml2.settings import OneLogin_Saml2_Settings from ..exceptions import AuthFailed, AuthMissingParameter from .base import BaseAuth # Helpful constants: OID_COMMON_NAME = "urn:oid:2.5.4.3" OID_EDU_PERSON_PRINCIPAL_NAME = "urn:oid:1.3.6.1.4.1.5923.1.1.1.6" OID_EDU_PERSON_ENTITLEMENT = "urn:oid:1.3.6.1.4.1.5923.1.1.1.7" OID_GIVEN_NAME = "urn:oid:2.5.4.42" OID_MAIL = "urn:oid:0.9.2342.19200300.100.1.3" OID_SURNAME = "urn:oid:2.5.4.4" OID_USERID = "urn:oid:0.9.2342.19200300.100.1.1" class SAMLIdentityProvider: """Wrapper around configuration for a SAML Identity provider""" def __init__(self, name, **kwargs): """Load and parse configuration""" self.name = name # name should be a slug and must not contain a colon, which # could conflict with uid prefixing: assert ":" not in self.name and " " not in self.name, ( 'IdP "name" should be a slug (short, no spaces)' ) self.conf = kwargs def get_user_permanent_id(self, attributes): """ The most important method: Get a permanent, unique identifier for this user from the attributes supplied by the IdP. If you want to use the NameID, it's available via attributes['name_id'] """ uid = attributes[self.conf.get("attr_user_permanent_id", OID_USERID)] if isinstance(uid, list): uid = uid[0] return uid # Attributes processing: def get_user_details(self, attributes): """ Given the SAML attributes extracted from the SSO response, get the user data like name. """ return { "fullname": self.get_attr(attributes, "attr_full_name", OID_COMMON_NAME), "first_name": self.get_attr(attributes, "attr_first_name", OID_GIVEN_NAME), "last_name": self.get_attr(attributes, "attr_last_name", OID_SURNAME), "username": self.get_attr(attributes, "attr_username", OID_USERID), "email": self.get_attr(attributes, "attr_email", OID_MAIL), } def get_attr(self, attributes, conf_key, default_attribute): """ Internal helper method. Get the attribute 'default_attribute' out of the attributes, unless self.conf[conf_key] overrides the default by specifying another attribute to use. """ key = self.conf.get(conf_key, default_attribute) value = attributes.get(key, None) if isinstance(value, list): value = value[0] if value else None return value @property def entity_id(self): """Get the entity ID for this IdP""" # Required. e.g. "https://idp.testshib.org/idp/shibboleth" return self.conf["entity_id"] @property def sso_url(self): """Get the SSO URL for this IdP""" # Required. e.g. # "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO" return self.conf["url"] @property def slo_url(self): """Get the SLO URL for this IdP""" return self.conf.get("slo_url") @property def saml_config_dict(self): """Get the IdP configuration dict in the format required by python-saml""" result = { "entityId": self.entity_id, "singleSignOnService": { "url": self.sso_url, # python-saml only supports Redirect "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", }, } if self.slo_url: result["singleLogoutService"] = { "url": self.slo_url, "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", } cert = self.conf.get("x509cert", None) if cert: result["x509cert"] = cert return result cert = self.conf.get("x509certMulti", None) if cert: result["x509certMulti"] = cert return result raise KeyError("IDP must contain x509cert or x509certMulti") class DummySAMLIdentityProvider(SAMLIdentityProvider): """ A placeholder IdP used when we must specify something, e.g. when generating SP metadata. If OneLogin_Saml2_Auth is modified to not always require IdP config, this can be removed. """ def __init__(self): super().__init__( "dummy", entity_id="https://dummy.none/saml2", url="https://dummy.none/SSO", x509cert="", ) class SAMLAuth(BaseAuth): """ PSA Backend that implements SAML 2.0 Service Provider (SP) functionality. Unlike all of the other backends, this one can be configured to work with many identity providers (IdPs). For example, a University that belongs to a Shibboleth federation may support authentication via ~100 partner universities. Also, the IdP configuration can be changed at runtime if you require that functionality - just subclass this and override `get_idp()`. Several settings are required. Here's an example: SOCIAL_AUTH_SAML_SP_ENTITY_ID = "https://saml.example.com/" SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = "... X.509 certificate string ..." SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = "... private key ..." SOCIAL_AUTH_SAML_ORG_INFO = { "en-US": { "name": "example", "displayname": "Example Inc.", "url": "http://example.com" } } SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = { "givenName": "Tech Gal", "emailAddress": "technical@example.com" } SOCIAL_AUTH_SAML_SUPPORT_CONTACT = { "givenName": "Support Guy", "emailAddress": "support@example.com" } SOCIAL_AUTH_SAML_ENABLED_IDPS = { "testshib": { "entity_id": "https://idp.testshib.org/idp/shibboleth", "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO", "x509cert": "MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0B... ...8Bbnl+ev0peYzxFyF5sQA==", } } Optional settings: SOCIAL_AUTH_SAML_SP_EXTRA = {} SOCIAL_AUTH_SAML_SECURITY_CONFIG = {} """ name = "saml" EXTRA_DATA = [] def get_idp(self, idp_name): """Given the name of an IdP, get a SAMLIdentityProvider instance""" idp_config = self.setting("ENABLED_IDPS")[idp_name] return SAMLIdentityProvider(idp_name, **idp_config) def generate_saml_config(self, idp=None): """ Generate the configuration required to instantiate OneLogin_Saml2_Auth """ # The shared absolute URL that all IdPs redirect back to - # this is specified in our metadata.xml: abs_completion_url = self.redirect_uri config = { "contactPerson": { "technical": self.setting("TECHNICAL_CONTACT"), "support": self.setting("SUPPORT_CONTACT"), }, "debug": True, "idp": idp.saml_config_dict if idp else {}, "organization": self.setting("ORG_INFO"), "security": { "metadataValidUntil": "", "metadataCacheDuration": "P10D", # metadata valid for ten days }, "sp": { "assertionConsumerService": { "url": abs_completion_url, # python-saml only supports HTTP-POST "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", }, "entityId": self.setting("SP_ENTITY_ID"), "x509cert": self.setting("SP_PUBLIC_CERT"), "privateKey": self.setting("SP_PRIVATE_KEY"), }, "strict": True, # We must force strict mode - for security } config["security"].update(self.setting("SECURITY_CONFIG", {})) config["sp"].update(self.setting("SP_EXTRA", {})) return config def generate_metadata_xml(self): """ Helper method that can be used from your web app to generate the XML metadata required to link your web app as a Service Provider. Returns (metadata XML string, list of errors) Example usage (Django): from ..apps.django_app.utils import load_strategy, \ load_backend def saml_metadata_view(request): complete_url = reverse('social:complete', args=("saml", )) saml_backend = load_backend(load_strategy(request), "saml", complete_url) metadata, errors = saml_backend.generate_metadata_xml() if not errors: return HttpResponse(content=metadata, content_type='text/xml') return HttpResponseServerError(content=', '.join(errors)) """ config = self.generate_saml_config() saml_settings = OneLogin_Saml2_Settings(config, sp_validation_only=True) metadata = saml_settings.get_sp_metadata() errors = saml_settings.validate_metadata(metadata) return metadata, errors def _create_saml_auth(self, idp): """Get an instance of OneLogin_Saml2_Auth""" config = self.generate_saml_config(idp) request_info = { "https": "on" if self.strategy.request_is_secure() else "off", "http_host": self.strategy.request_host(), "script_name": self.strategy.request_path(), "get_data": self.strategy.request_get(), "post_data": self.strategy.request_post(), } return OneLogin_Saml2_Auth(request_info, config) def auth_url(self): """Get the URL to which we must redirect in order to authenticate the user""" try: idp_name = self.strategy.request_data()["idp"] except KeyError: raise AuthMissingParameter(self, "idp") auth = self._create_saml_auth(idp=self.get_idp(idp_name)) # Below, return_to sets the RelayState, which can contain # arbitrary data. We use it to store the specific SAML IdP # name, since we multiple IdPs share the same auth_complete # URL, and the URL to redirect to after auth completes. relay_state = { "idp": idp_name, "next": self.data.get("next"), } return auth.login(return_to=json.dumps(relay_state)) def get_user_details(self, response): """Get user details like full name, email, etc. from the response - see auth_complete""" idp = self.get_idp(response["idp_name"]) return idp.get_user_details(response["attributes"]) def get_user_id(self, details, response): """ Get the permanent ID for this user from the response. We prefix each ID with the name of the IdP so that we can connect multiple IdPs to this user. """ idp = self.get_idp(response["idp_name"]) uid = idp.get_user_permanent_id(response["attributes"]) return f"{idp.name}:{uid}" def auth_complete(self, *args, **kwargs): """ The user has been redirected back from the IdP and we should now log them in, if everything checks out. """ try: relay_state_str = self.strategy.request_data()["RelayState"] except KeyError: raise AuthMissingParameter(self, "RelayState") try: relay_state = json.loads(relay_state_str) if not isinstance(relay_state, dict) or "idp" not in relay_state: raise ValueError( "RelayState is expected to contain a JSON object with an 'idp' key" ) except ValueError: # Assume RelayState is just the idp_name, as it used to be in previous versions of this code. # This ensures compatibility with previous versions. idp_name = relay_state_str else: idp_name = relay_state["idp"] if next_url := relay_state.get("next"): # The do_complete action expects the "next" URL to be in session state or the request params. self.strategy.session_set(kwargs.get("redirect_name", "next"), next_url) idp = self.get_idp(idp_name) auth = self._create_saml_auth(idp) auth.process_response() errors = auth.get_errors() if errors or not auth.is_authenticated(): reason = auth.get_last_error_reason() raise AuthFailed(self, f"SAML login failed: {errors} ({reason})") attributes = auth.get_attributes() attributes["name_id"] = auth.get_nameid() self._check_entitlements(idp, attributes) response = { "idp_name": idp_name, "attributes": attributes, "session_index": auth.get_session_index(), } kwargs.update({"response": response, "backend": self}) return self.strategy.authenticate(*args, **kwargs) def extra_data(self, user, uid, response, details=None, *args, **kwargs): extra_data = super().extra_data( user, uid, response["attributes"], details=details, *args, **kwargs ) extra_data["session_index"] = response["session_index"] extra_data["name_id"] = response["attributes"]["name_id"] return extra_data def request_logout(self, idp_name, social_auth, return_to=None): idp = self.get_idp(idp_name) auth = self._create_saml_auth(idp) name_id = social_auth.extra_data["name_id"] session_index = social_auth.extra_data["session_index"] return auth.logout( name_id=name_id, session_index=session_index, return_to=return_to ) def process_logout(self, idp_name, delete_session_cb): idp = self.get_idp(idp_name) auth = self._create_saml_auth(idp) url = auth.process_slo(delete_session_cb=delete_session_cb) errors = auth.get_errors() return url, errors def _check_entitlements(self, idp, attributes): """ Additional verification of a SAML response before authenticating the user. Subclasses can override this method if they need custom validation code, such as requiring the presence of an eduPersonEntitlement. raise social_core.exceptions.AuthForbidden if the user should not be authenticated, or do nothing to allow the login pipeline to continue. """ social-auth-core-4.6.1/social_core/backends/scistarter.py000066400000000000000000000040161500362547200234570ustar00rootroot00000000000000"""SciStarter OAuth2 Auth""" from .oauth import BaseOAuth2 class SciStarterOAuth2(BaseOAuth2): name = "scistarter" ID_KEY = "email" SCOPE_PARAMETER_NAME = "scope" DEFAULT_SCOPE = ["login", "extensive"] SCOPE_SEPARATOR = " " AUTHORIZATION_URL = "https://scistarter.com/authorize" ACCESS_TOKEN_URL = "https://scistarter.com/token?key={key}" USER_ACCESS_URL = ( "https://scistarter.com/api/user_info?client_id={clientid}&key={key}" ) REFRESH_TOKEN_URL = None RESPONSE_TYPE = "code" STATE_PARAMETER = True REDIRECT_STATE = True EXTRA_DATA = [("profile_id", "profile_id"), ("expires", "expires")] def get_redirect_uri(self, state=None): """Build redirect with redirect_state parameter.""" return self.redirect_uri.rstrip("/") def authorization_url(self): return self.AUTHORIZATION_URL def get_user_details(self, response): return { "username": response.get("handle"), "email": response.get("email") or "", "first_name": response.get("first_name"), "last_name": response.get("last_name"), } def user_data(self, access_token, *args, **kwards): client_id, client_secret = self.get_key_and_secret() return self.get_json( self.USER_ACCESS_URL.format(clientid=client_id, key=client_secret), headers={"Authorization": "Bearer " + access_token}, ) def access_token(self, token): """Return request for access token value""" # TODO: confirm if this should be OAuth2 or OAuth1; the `oauth_auth` method # is for OAuth1, but this class inherits from OAuth2 return self.get_querystring( self.access_token_url(), auth=self.oauth_auth(token), # type: ignore[reportAttributeAccessIssue] method=self.ACCESS_TOKEN_METHOD, ) def access_token_url(self): client_id, client_secret = self.get_key_and_secret() return self.ACCESS_TOKEN_URL.format(key=client_secret) social-auth-core-4.6.1/social_core/backends/seznam.py000066400000000000000000000032311500362547200225670ustar00rootroot00000000000000""" Seznam OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/seznam.html """ from .oauth import BaseOAuth2 class SeznamOAuth2(BaseOAuth2): """Seznam OAuth authentication backend""" name = "seznam-oauth2" API_URL = "https://login.szn.cz/api/v1/user" AUTHORIZATION_URL = "https://login.szn.cz/api/v1/oauth/auth" ACCESS_TOKEN_URL = "https://login.szn.cz/api/v1/oauth/token" ID_KEY = "oauth_user_id" STATE_PARAMETER = True DEFAULT_SCOPE = ["identity"] def api_url(self): return self.setting("API_URL") or self.API_URL def authorization_url(self): return self.setting("AUTHORIZATION_URL") or self.AUTHORIZATION_URL def access_token_url(self): return self.setting("ACCESS_TOKEN_URL") or self.ACCESS_TOKEN_URL def get_user_id(self, details, response): return response.get(self.setting("ID_KEY") or self.ID_KEY) def get_user_details(self, response): """Return user details from Seznam account""" fullname, first_name, last_name = self.get_user_names( response.get("name"), first_name=response.get("firstname"), last_name=response.get("lastname"), ) return { "username": response.get("username"), "email": response.get("email") or "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( self.api_url(), headers={"Authorization": f"bearer {access_token}"} ) social-auth-core-4.6.1/social_core/backends/shimmering.py000066400000000000000000000021661500362547200234420ustar00rootroot00000000000000""" Shimmering Oauth """ from .oauth import BaseOAuth2 class ShimmeringOAuth2(BaseOAuth2): """Shimmering Verify OAuth2 authentication backend""" name = "shimmering" ID_KEY = "id" AUTHORIZATION_URL = "http://developers.shimmeringverify.com/o/authorize/" ACCESS_TOKEN_URL = "http://developers.shimmeringverify.com/o/token/" def get_user_details(self, response): """Return user details from Shimmering""" first_name = response.get("first_name") last_name = response.get("last_name") email = response.get("email") username = response.get("username") fullname = f"{first_name} {last_name}" return { "username": username, "fullname": fullname, "first_name": first_name, "last_name": last_name, "email": email, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" headers = {"Authorization": f"Bearer {access_token}"} return self.get_json( "http://developers.shimmeringverify.com/user_info/", headers=headers ) social-auth-core-4.6.1/social_core/backends/shopify.py000066400000000000000000000060641500362547200227620ustar00rootroot00000000000000""" Shopify OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/shopify.html """ import shopify from ..exceptions import AuthCanceled, AuthFailed from ..utils import handle_http_errors from .oauth import BaseOAuth2 class ShopifyOAuth2(BaseOAuth2): """Shopify OAuth2 authentication backend""" name = "shopify" ID_KEY = "shop" EXTRA_DATA = [("shop", "shop"), ("website", "website"), ("expires", "expires")] REDIRECT_STATE = False @property def shopify_api_version(self): return self.setting("API_VERSION", "2020-10") def get_user_details(self, response): """Use the shopify store name as the username""" return {"username": str(response.get("shop", "")).replace(".myshopify.com", "")} def extra_data(self, user, uid, response, details=None, *args, **kwargs): """Return access_token and extra defined names to store in extra_data field""" data = super().extra_data(user, uid, response, details, *args, **kwargs) session = shopify.Session( self.data.get("shop").strip(), version=self.shopify_api_version ) # Get, and store the permanent token token = session.request_token(data["access_token"]) data["access_token"] = token return dict(data) def auth_url(self): key, secret = self.get_key_and_secret() shopify.Session.setup(api_key=key, secret=secret) scope = self.get_scope() state = self.state_token() self.strategy.session_set(self.name + "_state", state) redirect_uri = self.get_redirect_uri(state) session = shopify.Session( self.data.get("shop").strip(), version=self.shopify_api_version ) return session.create_permission_url(scope=scope, redirect_uri=redirect_uri) @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" self.process_error(self.data) access_token = None key, secret = self.get_key_and_secret() try: shop_url = self.data.get("shop") shopify.Session.setup(api_key=key, secret=secret) shopify_session = shopify.Session( shop_url, version=self.shopify_api_version, token=self.data ) access_token = shopify_session.token except shopify.ValidationException: raise AuthCanceled(self) else: if not access_token: raise AuthFailed(self, "Authentication Failed") return self.do_auth( access_token, shop_url, shopify_session.url, *args, **kwargs ) def do_auth(self, access_token, shop_url, website, *args, **kwargs): kwargs.update( { "backend": self, "response": { "shop": shop_url, "website": f"http://{website}", "access_token": access_token, }, } ) return self.strategy.authenticate(*args, **kwargs) social-auth-core-4.6.1/social_core/backends/simplelogin.py000066400000000000000000000024271500362547200236220ustar00rootroot00000000000000""" SimpleLogin OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/simplelogin.html """ from .oauth import BaseOAuth2 class SimpleLoginOAuth2(BaseOAuth2): """SimpleLogin OAuth authentication backend""" name = "simplelogin" AUTHORIZATION_URL = "https://app.simplelogin.io/oauth2/authorize" ACCESS_TOKEN_URL = "https://app.simplelogin.io/oauth2/token" REDIRECT_STATE = False STATE_PARAMETER = True SEND_USER_AGENT = True EXTRA_DATA = [ ("name", "name"), ("email", "email"), ("avatar_url", "avatar_url"), ] # endpoint to get user info USERINFO_URL = "https://app.simplelogin.io/oauth2/userinfo" def get_user_details(self, response): """Return user details from SimpleLogin account""" fullname, first_name, last_name = self.get_user_names(response.get("name")) return { "username": response.get("email"), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json(self.USERINFO_URL, params={"access_token": access_token}) social-auth-core-4.6.1/social_core/backends/sketchfab.py000066400000000000000000000024461500362547200232330ustar00rootroot00000000000000""" Sketchfab OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/sketchfab.html https://sketchfab.com/developers/oauth """ from .oauth import BaseOAuth2 class SketchfabOAuth2(BaseOAuth2): name = "sketchfab" ID_KEY = "uid" AUTHORIZATION_URL = "https://sketchfab.com/oauth2/authorize/" ACCESS_TOKEN_URL = "https://sketchfab.com/oauth2/token/" REDIRECT_STATE = False REQUIRES_EMAIL_VALIDATION = False EXTRA_DATA = [("username", "username"), ("apiToken", "apiToken")] def get_user_details(self, response): """Return user details from Sketchfab account""" user_data = response email = user_data.get("email", "") username = user_data["username"] name = user_data.get("displayName", "") fullname, first_name, last_name = self.get_user_names(name) return { "username": username, "fullname": fullname, "first_name": first_name, "last_name": last_name, "email": email, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://sketchfab.com/v2/users/me", headers={"Authorization": f"Bearer {access_token}"}, ) social-auth-core-4.6.1/social_core/backends/skyrock.py000066400000000000000000000022611500362547200227610ustar00rootroot00000000000000""" Skyrock OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/skyrock.html """ from .oauth import BaseOAuth1 class SkyrockOAuth(BaseOAuth1): """Skyrock OAuth authentication backend""" name = "skyrock" ID_KEY = "id_user" AUTHORIZATION_URL = "https://api.skyrock.com/v2/oauth/authenticate" REQUEST_TOKEN_URL = "https://api.skyrock.com/v2/oauth/initiate" ACCESS_TOKEN_URL = "https://api.skyrock.com/v2/oauth/token" EXTRA_DATA = [("id", "id")] def get_user_details(self, response): """Return user details from Skyrock account""" fullname, first_name, last_name = self.get_user_names( first_name=response["firstname"], last_name=response["name"] ) return { "username": response["username"], "email": response["email"], "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token): """Return user data provided""" return self.get_json( "https://api.skyrock.com/v2/user/get.json", auth=self.oauth_auth(access_token), ) social-auth-core-4.6.1/social_core/backends/slack.py000066400000000000000000000037111500362547200223720ustar00rootroot00000000000000""" Slack OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/slack.html https://api.slack.com/docs/oauth """ from .oauth import BaseOAuth2 class SlackOAuth2(BaseOAuth2): """Slack OAuth authentication backend""" name = "slack" AUTHORIZATION_URL = "https://slack.com/oauth/authorize" ACCESS_TOKEN_URL = "https://slack.com/api/oauth.access" DEFAULT_SCOPE = ["identity.basic", "identity.email"] SCOPE_SEPARATOR = "," REDIRECT_STATE = False EXTRA_DATA = [("id", "id"), ("name", "name"), ("real_name", "real_name")] def auth_extra_arguments(self): params = super().auth_extra_arguments() or {} if self.setting("TEAM"): params["team"] = self.setting("TEAM") return params def get_user_details(self, response): """Return user details from Slack account""" # Build the username with the team $username@$team_url # Necessary to get unique names for all of slack user = response["user"] team = response.get("team") name = user["name"] email = user.get("email") username = (email and email.split("@", 1)[0]) or name fullname, first_name, last_name = self.get_user_names(name) if self.setting("USERNAME_WITH_TEAM", True) and team and "name" in team: username = "{}@{}".format(username, response["team"]["name"]) return { "username": username, "email": email, "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" response = self.get_json( "https://slack.com/api/users.identity", headers={"Authorization": f"Bearer {access_token}"}, ) if not response.get("id", None): response["id"] = response["user"]["id"] return response social-auth-core-4.6.1/social_core/backends/soundcloud.py000066400000000000000000000041121500362547200234500ustar00rootroot00000000000000""" Soundcloud OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/soundcloud.html """ from ..utils import url_add_parameters from .oauth import BaseOAuth2 class SoundcloudOAuth2(BaseOAuth2): """Soundcloud OAuth authentication backend""" name = "soundcloud" AUTHORIZATION_URL = "https://soundcloud.com/connect" ACCESS_TOKEN_URL = "https://api.soundcloud.com/oauth2/token" SCOPE_SEPARATOR = "," REDIRECT_STATE = False EXTRA_DATA = [ ("id", "id"), ("refresh_token", "refresh_token"), ("expires", "expires"), ] def get_user_details(self, response): """Return user details from Soundcloud account""" fullname, first_name, last_name = self.get_user_names(response.get("full_name")) return { "username": response.get("username"), "email": response.get("email") or "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://api.soundcloud.com/me.json", params={"oauth_token": access_token} ) def auth_url(self): """Return redirect url""" state = None if self.STATE_PARAMETER or self.REDIRECT_STATE: # Store state in session for further request validation. The state # value is passed as state parameter (as specified in OAuth2 spec), # but also added to redirect_uri, that way we can still verify the # request if the provider doesn't implement the state parameter. # Reuse token if any. name = self.name + "_state" state = self.strategy.session_get(name) or self.state_token() self.strategy.session_set(name, state) params = self.auth_params(state) params.update(self.get_scope_argument()) params.update(self.auth_extra_arguments()) return url_add_parameters(self.AUTHORIZATION_URL, params, True) social-auth-core-4.6.1/social_core/backends/spotify.py000066400000000000000000000027161500362547200227760ustar00rootroot00000000000000""" Spotify backend, docs at: https://developer.spotify.com/spotify-web-api/ https://developer.spotify.com/spotify-web-api/authorization-guide/ """ import base64 from .oauth import BaseOAuth2 class SpotifyOAuth2(BaseOAuth2): """Spotify OAuth2 authentication backend""" name = "spotify" ID_KEY = "id" AUTHORIZATION_URL = "https://accounts.spotify.com/authorize" ACCESS_TOKEN_URL = "https://accounts.spotify.com/api/token" SCOPE_SEPARATOR = " " REDIRECT_STATE = False EXTRA_DATA = [ ("refresh_token", "refresh_token"), ] def auth_headers(self): auth_str = "{}:{}".format(*self.get_key_and_secret()) b64_auth_str = base64.urlsafe_b64encode(auth_str.encode()).decode() return {"Authorization": f"Basic {b64_auth_str}"} def get_user_details(self, response): """Return user details from Spotify account""" fullname, first_name, last_name = self.get_user_names( response.get("display_name") ) return { "username": response.get("id"), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://api.spotify.com/v1/me", headers={"Authorization": f"Bearer {access_token}"}, ) social-auth-core-4.6.1/social_core/backends/stackoverflow.py000066400000000000000000000026131500362547200241660ustar00rootroot00000000000000""" Stackoverflow OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/stackoverflow.html """ from .oauth import BaseOAuth2 class StackoverflowOAuth2(BaseOAuth2): """Stackoverflow OAuth2 authentication backend""" name = "stackoverflow" ID_KEY = "user_id" AUTHORIZATION_URL = "https://stackexchange.com/oauth" ACCESS_TOKEN_URL = "https://stackexchange.com/oauth/access_token" SCOPE_SEPARATOR = "," EXTRA_DATA = [("id", "id"), ("expires", "expires")] def get_user_details(self, response): """Return user details from Stackoverflow account""" fullname, first_name, last_name = self.get_user_names( response.get("display_name") ) return { "username": response.get("link").rsplit("/", 1)[-1], "full_name": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://api.stackexchange.com/2.1/me", params={ "site": "stackoverflow", "access_token": access_token, "key": self.setting("API_KEY"), }, )["items"][0] def request_access_token(self, *args, **kwargs): return self.get_querystring(*args, **kwargs) social-auth-core-4.6.1/social_core/backends/steam.py000066400000000000000000000032671500362547200224140ustar00rootroot00000000000000""" Steam OpenId backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/steam.html """ from ..exceptions import AuthFailed from .open_id import OpenIdAuth USER_INFO = "http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?" class SteamOpenId(OpenIdAuth): name = "steam" URL = "https://steamcommunity.com/openid" def get_user_id(self, details, response): """Return user unique id provided by service""" return self._user_id(response) def get_user_details(self, response): player = self.get_json( USER_INFO, params={ "key": self.setting("API_KEY"), "steamids": self._user_id(response), }, ) if len(player["response"]["players"]) > 0: player = player["response"]["players"][0] details = { "username": player.get("personaname"), "email": "", "fullname": "", "first_name": "", "last_name": "", "player": player, } else: details = {} return details def consumer(self): # Steam seems to support stateless mode only, ignore store if not hasattr(self, "_consumer"): self._consumer = self.create_consumer() return self._consumer def _user_id(self, response): if not response.identity_url.startswith(self.URL): raise AuthFailed(self, "Openid identifier mismatch") user_id = response.identity_url.rsplit("/", 1)[-1] if not user_id.isdigit(): raise AuthFailed(self, "Missing Steam Id") return user_id social-auth-core-4.6.1/social_core/backends/stocktwits.py000066400000000000000000000024751500362547200235210ustar00rootroot00000000000000""" Stocktwits OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/stocktwits.html """ from .oauth import BaseOAuth2 class StocktwitsOAuth2(BaseOAuth2): """Stockwiths OAuth2 backend""" name = "stocktwits" AUTHORIZATION_URL = "https://api.stocktwits.com/api/2/oauth/authorize" ACCESS_TOKEN_URL = "https://api.stocktwits.com/api/2/oauth/token" SCOPE_SEPARATOR = "," DEFAULT_SCOPE = [ "read", "publish_messages", "publish_watch_lists", "follow_users", "follow_stocks", ] def get_user_id(self, details, response): return response["user"]["id"] def get_user_details(self, response): """Return user details from Stocktwits account""" fullname, first_name, last_name = self.get_user_names(response["user"]["name"]) return { "username": response["user"]["username"], "email": "", # not supplied "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://api.stocktwits.com/api/2/account/verify.json", params={"access_token": access_token}, ) social-auth-core-4.6.1/social_core/backends/strava.py000066400000000000000000000034411500362547200225750ustar00rootroot00000000000000""" Strava OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/strava.html """ from .oauth import BaseOAuth2 class StravaOAuth(BaseOAuth2): name = "strava" AUTHORIZATION_URL = "https://www.strava.com/oauth/authorize" ACCESS_TOKEN_URL = "https://www.strava.com/oauth/token" # Strava doesn't check for parameters in redirect_uri and directly appends # the auth parameters to it, ending with an URL like: # http://example.com/complete/strava?redirect_state=xxx?code=xxx&state=xxx # Check issue #259 for details. REDIRECT_STATE = False REVOKE_TOKEN_URL = "https://www.strava.com/oauth/deauthorize" SCOPE_SEPARATOR = "," EXTRA_DATA = [ ("refresh_token", "refresh_token"), ("expires_in", "expires"), ] def get_user_id(self, details, response): return response["athlete"]["id"] def get_user_details(self, response): """Return user details from Strava account""" username = response["athlete"].get("username", "") fullname, first_name, last_name = self.get_user_names( first_name=response["athlete"].get("firstname", ""), last_name=response["athlete"].get("lastname", ""), ) return { "username": username, "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://www.strava.com/api/v3/athlete", params={"access_token": access_token}, ) def revoke_token_params(self, token, uid): params = super().revoke_token_params(token, uid) params["access_token"] = token return params social-auth-core-4.6.1/social_core/backends/stripe.py000066400000000000000000000040101500362547200225740ustar00rootroot00000000000000""" Stripe OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/stripe.html """ from .oauth import BaseOAuth2 class StripeOAuth2(BaseOAuth2): """Stripe OAuth2 authentication backend""" name = "stripe" ID_KEY = "stripe_user_id" AUTHORIZATION_URL = "https://connect.stripe.com/oauth/authorize" ACCESS_TOKEN_URL = "https://connect.stripe.com/oauth/token" REDIRECT_STATE = False EXTRA_DATA = [ ("stripe_publishable_key", "stripe_publishable_key"), ("access_token", "access_token"), ("livemode", "livemode"), ("token_type", "token_type"), ("refresh_token", "refresh_token"), ("stripe_user_id", "stripe_user_id"), ] def user_data(self, access_token, *args, **kwargs): """Grab user profile information from Stripe""" return self.get_json( "https://api.stripe.com/v1/account", headers={ "Authorization": f"Bearer {access_token}", }, ) def get_user_details(self, response): """Return user details from Stripe account""" return { "email": response.get("email"), "username": response.get("stripe_user_id"), "first_name": response.get("first_name", ""), "last_name": response.get("last_name", ""), } def auth_complete_params(self, state=None): client_id, client_secret = self.get_key_and_secret() return { "grant_type": "authorization_code", "client_id": client_id, "scope": self.SCOPE_SEPARATOR.join(self.get_scope()), "code": self.data["code"], } def auth_headers(self): client_id, client_secret = self.get_key_and_secret() return { "Accept": "application/json", "Authorization": f"Bearer {client_secret}", } def refresh_token_params(self, refresh_token, *args, **kwargs): return {"refresh_token": refresh_token, "grant_type": "refresh_token"} social-auth-core-4.6.1/social_core/backends/surveymonkey.py000066400000000000000000000020711500362547200240530ustar00rootroot00000000000000""" SurveyMonkey OAuth2 backend, docs at: https://developer.surveymonkey.com/api/v3/#authentication """ from .oauth import BaseOAuth2 class SurveyMonkeyOAuth2(BaseOAuth2): """SurveyMonkey OAuth2 authentication backend""" name = "surveymonkey" AUTHORIZATION_URL = "https://api.surveymonkey.com/oauth/authorize" ACCESS_TOKEN_URL = "https://api.surveymonkey.com/oauth/token" USER_DATA_URL = "/v3/users/me" STATE_PARAMETER = False REDIRECT_STATE = False EXTRA_DATA = [ ("access_url", "access_url"), ] def get_user_details(self, response): """Return user details from a SurveyMonkey /users/me response""" response["name"] = response["first_name"] + " " + response["last_name"] return response def user_data(self, access_token, *args, **kwargs): """Loads user data information from service""" base_url = kwargs["response"]["access_url"] return self.get_json( base_url + self.USER_DATA_URL, headers={"Authorization": "bearer " + access_token}, ) social-auth-core-4.6.1/social_core/backends/suse.py000066400000000000000000000007171500362547200222570ustar00rootroot00000000000000""" Open Suse OpenId backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/suse.html """ from .open_id import OpenIdAuth class OpenSUSEOpenId(OpenIdAuth): name = "opensuse" URL = "https://www.opensuse.org/openid/user/" def get_user_id(self, details, response): """ Return user unique id provided by service. For openSUSE the nickname is original. """ return details["nickname"] social-auth-core-4.6.1/social_core/backends/taobao.py000066400000000000000000000016211500362547200225400ustar00rootroot00000000000000from .oauth import BaseOAuth2 class TAOBAOAuth(BaseOAuth2): """Taobao OAuth authentication mechanism""" name = "taobao" ID_KEY = "taobao_user_id" AUTHORIZATION_URL = "https://oauth.taobao.com/authorize" ACCESS_TOKEN_URL = "https://oauth.taobao.com/token" def user_data(self, access_token, *args, **kwargs): """Return user data provided""" try: return self.get_json( "https://eco.taobao.com/router/rest", params={ "method": "taobao.user.get", "fomate": "json", "v": "2.0", "access_token": access_token, }, ) except ValueError: return None def get_user_details(self, response): """Return user details from Taobao account""" return {"username": response.get("taobao_user_nick")} social-auth-core-4.6.1/social_core/backends/telegram.py000066400000000000000000000040521500362547200230740ustar00rootroot00000000000000import hashlib import hmac import time from ..exceptions import AuthFailed, AuthMissingParameter from ..utils import handle_http_errors from .base import BaseAuth class TelegramAuth(BaseAuth): name = "telegram" ID_KEY = "id" def verify_data(self, response): bot_token = self.setting("BOT_TOKEN") if bot_token is None: raise AuthMissingParameter(self, "SOCIAL_AUTH_TELEGRAM_BOT_TOKEN") received_hash_string = response.get("hash") auth_date = response.get("auth_date") if received_hash_string is None or auth_date is None: raise AuthMissingParameter(self, "hash or auth_date") data_check_string = [f"{k}={v}" for k, v in response.items() if k != "hash"] data_check_string = "\n".join(sorted(data_check_string)) secret_key = hashlib.sha256(bot_token.encode()).digest() built_hash = hmac.new( secret_key, msg=data_check_string.encode(), digestmod=hashlib.sha256 ).hexdigest() current_timestamp = int(time.time()) auth_timestamp = int(auth_date) if current_timestamp - auth_timestamp > 86400: raise AuthFailed(self, "Auth date is outdated") if built_hash != received_hash_string: raise AuthFailed(self, "Invalid hash supplied") def extra_data(self, user, uid, response, details=None, *args, **kwargs): return response def get_user_details(self, response): first_name = response.get("first_name", "") last_name = response.get("last_name", "") fullname = f"{first_name} {last_name}".strip() return { "username": response.get("username") or str(response[self.ID_KEY]), "first_name": first_name, "last_name": last_name, "fullname": fullname, } @handle_http_errors def auth_complete(self, *args, **kwargs): response = self.data self.verify_data(response) kwargs.update({"response": self.data, "backend": self}) return self.strategy.authenticate(*args, **kwargs) social-auth-core-4.6.1/social_core/backends/thisismyjam.py000066400000000000000000000022711500362547200236360ustar00rootroot00000000000000""" ThisIsMyJam OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/thisismyjam.html """ from .oauth import BaseOAuth1 class ThisIsMyJamOAuth1(BaseOAuth1): """ThisIsMyJam OAuth1 authentication backend""" name = "thisismyjam" REQUEST_TOKEN_URL = "http://www.thisismyjam.com/oauth/request_token" AUTHORIZATION_URL = "http://www.thisismyjam.com/oauth/authorize" ACCESS_TOKEN_URL = "http://www.thisismyjam.com/oauth/access_token" REDIRECT_URI_PARAMETER_NAME = "oauth_callback" def get_user_details(self, response): """Return user details from ThisIsMyJam account""" info = response.get("person") fullname, first_name, last_name = self.get_user_names(info.get("fullname")) return { "username": info.get("name"), "email": "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "http://api.thisismyjam.com/1/verify.json", auth=self.oauth_auth(access_token), ) social-auth-core-4.6.1/social_core/backends/trello.py000066400000000000000000000027401500362547200225770ustar00rootroot00000000000000""" Trello OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/trello.html """ from .oauth import BaseOAuth1 class TrelloOAuth(BaseOAuth1): """Trello OAuth authentication backend""" name = "trello" ID_KEY = "username" AUTHORIZATION_URL = "https://trello.com/1/OAuthAuthorizeToken" REQUEST_TOKEN_URL = "https://trello.com/1/OAuthGetRequestToken" ACCESS_TOKEN_URL = "https://trello.com/1/OAuthGetAccessToken" EXTRA_DATA = [ ("username", "username"), ("email", "email"), ("fullName", "fullName"), ] def get_user_details(self, response): """Return user details from Trello account""" fullname, first_name, last_name = self.get_user_names(response.get("fullName")) return { "username": response.get("username"), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token): """Return user data provided""" url = "https://trello.com/1/members/me" try: return self.get_json(url, auth=self.oauth_auth(access_token)) except ValueError: return None def auth_extra_arguments(self): return { "name": self.setting("APP_NAME", ""), # trello default expiration is '30days' "expiration": self.setting("EXPIRATION", "never"), } social-auth-core-4.6.1/social_core/backends/tripit.py000066400000000000000000000036421500362547200226130ustar00rootroot00000000000000""" Tripit OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/tripit.html """ from __future__ import annotations from typing import TYPE_CHECKING, cast from defusedxml import minidom from .oauth import BaseOAuth1 if TYPE_CHECKING: from xml.dom.minidom import Element class TripItOAuth(BaseOAuth1): """TripIt OAuth authentication backend""" name = "tripit" AUTHORIZATION_URL = "https://www.tripit.com/oauth/authorize" REQUEST_TOKEN_URL = "https://api.tripit.com/oauth/request_token" ACCESS_TOKEN_URL = "https://api.tripit.com/oauth/access_token" EXTRA_DATA = [("screen_name", "screen_name")] def get_user_details(self, response): """Return user details from TripIt account""" fullname, first_name, last_name = self.get_user_names(response["name"]) return { "username": response["screen_name"], "email": response["email"], "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Return user data provided""" content: str = self.oauth_request( access_token, "https://api.tripit.com/v1/get/profile" ).text dom = minidom.parseString(content) profiles = dom.getElementsByTagName("Profile") public_display_names = dom.getElementsByTagName("public_display_name") screen_names = dom.getElementsByTagName("screen_name") primary_email = cast( "Element", dom.getElementsByTagName("is_primary")[0].parentNode ).getElementsByTagName("address") return { "id": profiles[0].getAttribute("ref"), "name": public_display_names[0].childNodes[0].nodeValue, "screen_name": screen_names[0].childNodes[0].nodeValue, "email": primary_email[0].childNodes[0].nodeValue, } social-auth-core-4.6.1/social_core/backends/tumblr.py000066400000000000000000000021141500362547200225760ustar00rootroot00000000000000""" Tumblr OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/tumblr.html """ from ..utils import first from .oauth import BaseOAuth1 class TumblrOAuth(BaseOAuth1): name = "tumblr" ID_KEY = "name" AUTHORIZATION_URL = "http://www.tumblr.com/oauth/authorize" REQUEST_TOKEN_URL = "http://www.tumblr.com/oauth/request_token" REQUEST_TOKEN_METHOD = "POST" ACCESS_TOKEN_URL = "http://www.tumblr.com/oauth/access_token" def get_user_id(self, details, response): return response["response"]["user"][self.ID_KEY] def get_user_details(self, response): # http://www.tumblr.com/docs/en/api/v2#user-methods user_info = response["response"]["user"] data = {"username": user_info["name"]} blog = first(lambda blog: blog["primary"], user_info["blogs"]) if blog: data["fullname"] = blog["title"] return data def user_data(self, access_token): return self.get_json( "http://api.tumblr.com/v2/user/info", auth=self.oauth_auth(access_token) ) social-auth-core-4.6.1/social_core/backends/twilio.py000066400000000000000000000025511500362547200226050ustar00rootroot00000000000000""" Twilio auth backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/twilio.html """ from re import sub from urllib.parse import urlencode from .base import BaseAuth class TwilioAuth(BaseAuth): name = "twilio" ID_KEY = "AccountSid" def get_user_details(self, response): """Return twilio details, Twilio only provides AccountSID as parameters.""" # /complete/twilio/?AccountSid=ACc65ea16c9ebd4d4684edf814995b27e return { "username": response["AccountSid"], "email": "", "fullname": "", "first_name": "", "last_name": "", } def auth_url(self): """Return authorization redirect url.""" key, secret = self.get_key_and_secret() callback = self.strategy.absolute_uri(self.redirect_uri) callback = sub(r"^https", "http", callback) query = urlencode({"cb": callback}) return f"https://www.twilio.com/authorize/{key}?{query}" def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" account_sid = self.data.get("AccountSid") if not account_sid: raise ValueError("No AccountSid returned") kwargs.update({"response": self.data, "backend": self}) return self.strategy.authenticate(*args, **kwargs) social-auth-core-4.6.1/social_core/backends/twitch.py000066400000000000000000000043431500362547200226010ustar00rootroot00000000000000""" Twitch OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/twitch.html """ from .oauth import BaseOAuth2 from .open_id_connect import OpenIdConnectAuth class TwitchOpenIdConnect(OpenIdConnectAuth): """Twitch OpenID Connect authentication backend""" name = "twitch" USERNAME_KEY = "preferred_username" OIDC_ENDPOINT = "https://id.twitch.tv/oauth2" DEFAULT_SCOPE = ["openid", "user:read:email"] TWITCH_CLAIMS = ( '{"id_token":{"email": null,"email_verified":null,"preferred_username":null}}' ) def auth_params(self, state=None): params = super().auth_params(state) # Twitch uses a non-compliant OpenID implementation where the claims must be passed as a param params["claims"] = self.TWITCH_CLAIMS return params def get_user_details(self, response): assert self.id_token, "No id_token to parse" return { "username": self.id_token["preferred_username"], "email": self.id_token["email"], # Twitch does not provide this information "fullname": "", "first_name": "", "last_name": "", } class TwitchOAuth2(BaseOAuth2): """Twitch OAuth authentication backend""" name = "twitch" ID_KEY = "_id" AUTHORIZATION_URL = "https://id.twitch.tv/oauth2/authorize" ACCESS_TOKEN_URL = "https://id.twitch.tv/oauth2/token" DEFAULT_SCOPE = ["user:read:email"] REDIRECT_STATE = False def get_user_id(self, details, response): """ Use twitch user id as unique id """ return response.get("id") def get_user_details(self, response): return { "username": response.get("login"), "email": response.get("email"), "first_name": "", "last_name": "", } def user_data(self, access_token, *args, **kwargs): client_id, _ = self.get_key_and_secret() auth_headers = { "Authorization": f"Bearer {access_token}", "Client-Id": client_id, } url = "https://api.twitch.tv/helix/users" data = self.get_json(url, headers=auth_headers) return data["data"][0] if data.get("data") else {} social-auth-core-4.6.1/social_core/backends/twitter.py000066400000000000000000000026441500362547200230030ustar00rootroot00000000000000""" Twitter OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/twitter.html """ from ..exceptions import AuthCanceled from .oauth import BaseOAuth1 class TwitterOAuth(BaseOAuth1): """Twitter OAuth authentication backend""" name = "twitter" EXTRA_DATA = [("id", "id")] REQUEST_TOKEN_METHOD = "POST" AUTHORIZATION_URL = "https://api.twitter.com/oauth/authenticate" REQUEST_TOKEN_URL = "https://api.twitter.com/oauth/request_token" ACCESS_TOKEN_URL = "https://api.twitter.com/oauth/access_token" REDIRECT_STATE = True def process_error(self, data): if "denied" in data: raise AuthCanceled(self) super().process_error(data) def get_user_details(self, response): """Return user details from Twitter account""" fullname, first_name, last_name = self.get_user_names(response["name"]) return { "username": response["screen_name"], "email": response.get("email", ""), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Return user data provided""" return self.get_json( "https://api.twitter.com/1.1/account/verify_credentials.json", params={"include_email": "true"}, auth=self.oauth_auth(access_token), ) social-auth-core-4.6.1/social_core/backends/twitter_oauth2.py000066400000000000000000000065661500362547200242740ustar00rootroot00000000000000""" Twitter OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/twitter-oauth2.html https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code """ from .oauth import BaseOAuth2PKCE class TwitterOAuth2(BaseOAuth2PKCE): """Twitter OAuth2 authentication backend""" name = "twitter-oauth2" AUTHORIZATION_URL = "https://twitter.com/i/oauth2/authorize" ACCESS_TOKEN_URL = "https://api.twitter.com/2/oauth2/token" DEFAULT_SCOPE = ["users.read", "tweet.read"] SCOPE_SEPARATOR = " " REDIRECT_STATE = False STATE_PARAMETER = True USE_BASIC_AUTH = True ID_KEY = "id" EXTRA_DATA = [ ("id", "id"), ("username", "username"), ("fullname", "fullname"), ("first_name", "first_name"), ("last_name", "last_name"), ("created_at", "created_at"), ("verified", "verified"), ("verified_type", "verified_type"), ("proteted", "protected"), ("description", "description"), ("url", "url"), ("profile_image_url", "profile_image_url"), ("pinned_tweet_id", "pinned_tweet_id"), ("public_metrics", "public_metrics"), ] PKCE_DEFAULT_CODE_CHALLENGE_METHOD = "s256" PKCE_DEFAULT_CODE_VERIFIER_LENGTH = 32 DEFAULT_USE_PKCE = True def get_user_details(self, response): """Return user details from Twitter account""" user = response user_id = user["id"] name = user["name"] username = user["username"] created_at = user.get("created_at") verified = user.get("verified") verified_type = user.get("verified_type") protected = user.get("protected") description = user.get("description") url = user.get("url") profile_image_url = user.get("profile_image_url") pinned_tweet_id = user.get("pinned_tweet_id") public_metrics = user.get("public_metrics") fullname, first_name, last_name = self.get_user_names(name) return { "id": user_id, "username": username, "fullname": fullname, "first_name": first_name, "last_name": last_name, "created_at": created_at, "verified": verified, "verified_type": verified_type, "protected": protected, "description": description, "url": url, "pinned_tweet_id": pinned_tweet_id, "profile_image_url": profile_image_url, "public_metrics": public_metrics, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" # https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me fields = [ "created_at", "description", "entities", "id", "location", "name", "pinned_tweet_id", "profile_image_url", "protected", "public_metrics", "url", "username", "verified", "verified_type", "withheld", ] response = self.get_json( "https://api.twitter.com/2/users/me", params={"user.fields": ",".join(fields)}, headers={"Authorization": f"Bearer {access_token}"}, ) return response["data"] social-auth-core-4.6.1/social_core/backends/uber.py000066400000000000000000000024471500362547200222370ustar00rootroot00000000000000""" Uber OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/uber.html """ from .oauth import BaseOAuth2 class UberOAuth2(BaseOAuth2): name = "uber" ID_KEY = "uuid" SCOPE_SEPARATOR = " " AUTHORIZATION_URL = "https://login.uber.com/oauth/authorize" ACCESS_TOKEN_URL = "https://login.uber.com/oauth/token" def auth_complete_credentials(self): return self.get_key_and_secret() def get_user_details(self, response): """Return user details from Uber account""" email = response.get("email", "") fullname, first_name, last_name = self.get_user_names( "", response.get("first_name", ""), response.get("last_name", "") ) return { "username": email, "email": email, "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" response = kwargs.pop("response") return self.get_json( "https://api.uber.com/v1/me", headers={ "Authorization": "{} {}".format( response.get("token_type"), access_token ) }, ) social-auth-core-4.6.1/social_core/backends/ubuntu.py000066400000000000000000000005631500362547200226210ustar00rootroot00000000000000""" Ubuntu One OpenId backend """ from .open_id import OpenIdAuth class UbuntuOpenId(OpenIdAuth): name = "ubuntu" URL = "https://login.ubuntu.com" def get_user_id(self, details, response): """ Return user unique id provided by service. For Ubuntu One the nickname should be original. """ return details["nickname"] social-auth-core-4.6.1/social_core/backends/udata.py000066400000000000000000000024551500362547200223770ustar00rootroot00000000000000""" Udata related backends. Docs at: https://python-social-auth.readthedocs.io/en/latest/backends/udata.html """ from __future__ import annotations from social_core.exceptions import AuthMissingParameter from .oauth import BaseOAuth2 class UdataBaseOAuth2(BaseOAuth2): """Udata base OAuth authentication backend.""" SCOPE_SEPARATOR = "," REDIRECT_STATE = False DEFAULT_SCOPE = ["default"] USER_DATA_URL: str | None = None def get_user_details(self, response): """Return user details from Udata account.""" return { "username": response.get("first_name"), "email": response.get("email") or "", "first_name": response.get("first_name"), } def user_data(self, access_token, *args, **kwargs): """Load user data from service.""" if self.USER_DATA_URL is None: raise AuthMissingParameter(self, "USER_DATA_URL") return self.get_json(self.USER_DATA_URL, params={"access_token": access_token}) class DatagouvfrOAuth2(UdataBaseOAuth2): """Datagouvfr OAuth authentication backend.""" name = "datagouv" ACCESS_TOKEN_URL = "https://www.data.gouv.fr/oauth/token" AUTHORIZATION_URL = "https://www.data.gouv.fr/oauth/authorize" USER_DATA_URL = "https://www.data.gouv.fr/api/1/me/" social-auth-core-4.6.1/social_core/backends/uffd.py000066400000000000000000000030711500362547200222200ustar00rootroot00000000000000from typing import Any from urllib.parse import urlencode from .oauth import BaseOAuth2 class UffdOAuth2(BaseOAuth2): """Uffd OAuth2 authentication backend You need to set the following config: SOCIAL_AUTH_UFFD_KEY - client id SOCIAL_AUTH_UFFD_SECRET - client secret SOCIAL_AUTH_UFFD_BASE_URL - base url to uffd installation """ name = "uffd" REFRESH_TOKEN_METHOD = "POST" SCOPE_SEPARATOR = " " STATE_PARAMETER = True REDIRECT_STATE = False EXTRA_DATA = [ ("id", "id"), ] def get_user_details(self, response): """Return user details from a Uffd account""" fullname, first_name, last_name = self.get_user_names( fullname=response.get("name") ) return { "username": response.get("nickname"), "email": response.get("email") or "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" url = self.userinfo_url() + "?" + urlencode({"access_token": access_token}) try: user_data: dict[str, Any] = self.get_json(url) except ValueError: return None return user_data def authorization_url(self): return self.setting("BASE_URL") + "/oauth2/authorize" def access_token_url(self): return self.setting("BASE_URL") + "/oauth2/token" def userinfo_url(self): return self.setting("BASE_URL") + "/oauth2/userinfo" social-auth-core-4.6.1/social_core/backends/universe.py000066400000000000000000000022531500362547200231350ustar00rootroot00000000000000from .oauth import BaseOAuth2 class UniverseOAuth2(BaseOAuth2): """Universe Ticketing OAuth2 authentication backend""" name = "universe" AUTHORIZATION_URL = "https://www.universe.com/oauth/authorize" ACCESS_TOKEN_URL = "https://www.universe.com/oauth/token" BASE_API_URL = "https://www.universe.com/api" USER_INFO_URL = BASE_API_URL + "/v2/current_user" STATE_PARAMETER = True REDIRECT_STATE = True EXTRA_DATA = [ ("id", "id"), ("slug", "slug"), ("created_at", "created_at"), ("updated_at", "updated_at"), ] def get_user_id(self, details, response): return response["current_user"][self.ID_KEY] def get_user_details(self, response): """Return user details from a Universe account""" # Start with the user data as it was returned user_details = response["current_user"] user_details["username"] = user_details["email"] return user_details def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( self.USER_INFO_URL, headers={"Authorization": f"Bearer {access_token}"} ) social-auth-core-4.6.1/social_core/backends/untappd.py000066400000000000000000000074321500362547200227540ustar00rootroot00000000000000import requests from ..exceptions import AuthFailed from ..utils import handle_http_errors from .oauth import BaseOAuth2 class UntappdOAuth2(BaseOAuth2): """Untappd OAuth2 authentication backend""" name = "untappd" AUTHORIZATION_URL = "https://untappd.com/oauth/authenticate/" ACCESS_TOKEN_URL = "https://untappd.com/oauth/authorize/" BASE_API_URL = "https://api.untappd.com" USER_INFO_URL = BASE_API_URL + "/v4/user/info/" ACCESS_TOKEN_METHOD = "GET" STATE_PARAMETER = False REDIRECT_STATE = False SEND_USER_AGENT = True EXTRA_DATA = [ ("id", "id"), ("bio", "bio"), ("date_joined", "date_joined"), ("location", "location"), ("url", "url"), ("user_avatar", "user_avatar"), ("user_avatar_hd", "user_avatar_hd"), ("user_cover_photo", "user_cover_photo"), ] def auth_params(self, state=None): client_id, client_secret = self.get_key_and_secret() return { "client_id": client_id, "redirect_url": self.get_redirect_uri(), "response_type": self.RESPONSE_TYPE, } def process_error(self, data): """ All errors from Untappd are contained in the 'meta' key of the response. """ response_code = data.get("meta", {}).get("http_code") if response_code is not None and response_code != requests.codes.ok: raise AuthFailed(self, data["meta"]["error_detail"]) @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" client_id, client_secret = self.get_key_and_secret() code = self.data.get("code") self.process_error(self.data) # Untapped sends the access token request with URL parameters, # not a body response = self.request_access_token( self.access_token_url(), method=self.ACCESS_TOKEN_METHOD, params={ "response_type": "code", "code": code, "client_id": client_id, "client_secret": client_secret, "redirect_url": self.get_redirect_uri(), }, ) self.process_error(response) # Both the access_token and the rest of the response are # buried in the 'response' key return self.do_auth( response["response"]["access_token"], response=response["response"], *args, **kwargs, ) def get_user_details(self, response): """Return user details from an Untappd account""" # Start with the user data as it was returned user_data = response["user"] # Make a few updates to match expected key names user_data.update( { "username": user_data.get("user_name"), "email": user_data.get("settings", {}).get("email_address", ""), "first_name": user_data.get("first_name"), "last_name": user_data.get("last_name"), "fullname": " ".join( [user_data.get("first_name"), user_data.get("last_name")] ), } ) return user_data def get_user_id(self, details, response): """ Return a unique ID for the current user, by default from server response. """ return response["user"].get(self.ID_KEY) def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" response = self.get_json( self.USER_INFO_URL, params={"access_token": access_token, "compact": "true"} ) self.process_error(response) # The response data is buried in the 'response' key return response["response"] social-auth-core-4.6.1/social_core/backends/upwork.py000066400000000000000000000025101500362547200226200ustar00rootroot00000000000000""" Upwork OAuth1 backend """ from .oauth import BaseOAuth1 class UpworkOAuth(BaseOAuth1): """Upwork OAuth authentication backend""" name = "upwork" ID_KEY = "id" AUTHORIZATION_URL = "https://www.upwork.com/services/api/auth" REQUEST_TOKEN_URL = "https://www.upwork.com/api/auth/v1/oauth/token/request" REQUEST_TOKEN_METHOD = "POST" ACCESS_TOKEN_URL = "https://www.upwork.com/api/auth/v1/oauth/token/access" REDIRECT_URI_PARAMETER_NAME = "oauth_callback" def get_user_details(self, response): """Return user details from Upwork account""" info = response.get("info", {}) auth_user = response.get("auth_user", {}) first_name = auth_user.get("first_name") last_name = auth_user.get("last_name") fullname = f"{first_name} {last_name}" profile_url = info.get("profile_url", "") username = profile_url.rsplit("/")[-1].replace("~", "") return { "username": username, "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "https://www.upwork.com/api/auth/v1/info.json", auth=self.oauth_auth(access_token), ) social-auth-core-4.6.1/social_core/backends/username.py000066400000000000000000000004071500362547200231130ustar00rootroot00000000000000""" Legacy Username backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/username.html """ from .legacy import LegacyAuth class UsernameAuth(LegacyAuth): name = "username" ID_KEY = "username" EXTRA_DATA = ["username"] social-auth-core-4.6.1/social_core/backends/utils.py000066400000000000000000000060521500362547200224360ustar00rootroot00000000000000from collections import OrderedDict from ..exceptions import MissingBackend from ..utils import module_member, user_is_authenticated from .base import BaseAuth # Cache for discovered backends. BACKENDSCACHE = OrderedDict() def load_backends(backends, force_load=False): """ Load backends defined on SOCIAL_AUTH_AUTHENTICATION_BACKENDS, backends will be imported and cached on BACKENDSCACHE. The key in that dict will be the backend name, and the value is the backend class. Only subclasses of BaseAuth (and sub-classes) are considered backends. Previously there was a BACKENDS attribute expected on backends modules, this is not needed anymore since it's enough with the AUTHENTICATION_BACKENDS setting. BACKENDS was used because backends used to be split on two classes the authentication backend and another class that dealt with the auth mechanism with the provider, those classes are joined now. A force_load boolean argument is also provided so that get_backend below can retry a requested backend that may not yet be discovered. """ global BACKENDSCACHE if force_load: BACKENDSCACHE = OrderedDict() if not BACKENDSCACHE: for auth_backend in backends: backend = module_member(auth_backend) if issubclass(backend, BaseAuth): BACKENDSCACHE[backend.name] = backend return BACKENDSCACHE def get_backend(backends, name): """Returns a backend by name. Backends are stored in the BACKENDSCACHE cache dict. If not found, each of the modules referenced in AUTHENTICATION_BACKENDS is imported and checked for a BACKENDS definition. If the named backend is found in the module's BACKENDS definition, it's then stored in the cache for future access. """ try: # Cached backend which has previously been discovered return BACKENDSCACHE[name] except KeyError: # Reload BACKENDS to ensure a missing backend hasn't been missed load_backends(backends, force_load=True) try: return BACKENDSCACHE[name] except KeyError: raise MissingBackend(name) def user_backends_data(user, backends, storage): """ Will return backends data for given user, the return value will have the following keys: associated: UserSocialAuth model instances for currently associated accounts not_associated: Not associated (yet) backend names backends: All backend names. If user is not authenticated, then 'associated' list is empty, and there's no difference between 'not_associated' and 'backends'. """ available = list(load_backends(backends).keys()) values = {"associated": [], "not_associated": available, "backends": available} if user_is_authenticated(user): associated = storage.user.get_social_auth_for_user(user) not_associated = list(set(available) - {assoc.provider for assoc in associated}) values["associated"] = associated values["not_associated"] = not_associated return values social-auth-core-4.6.1/social_core/backends/vault.py000066400000000000000000000005711500362547200224310ustar00rootroot00000000000000""" Backend for Hashicorp Vault OIDC Identity Provider in Vault 1.9+ https://www.vaultproject.io/docs/secrets/identity/oidc-provider """ from social_core.backends.open_id_connect import OpenIdConnectAuth class VaultOpenIdConnect(OpenIdConnectAuth): """ Vault OIDC authentication backend This is an alias for the generic OIDC backend """ name = "vault" social-auth-core-4.6.1/social_core/backends/vend.py000066400000000000000000000022051500362547200222260ustar00rootroot00000000000000""" Vend OAuth2 backend: """ from .oauth import BaseOAuth2 class VendOAuth2(BaseOAuth2): name = "vend" AUTHORIZATION_URL = "https://secure.vendhq.com/connect" ACCESS_TOKEN_URL = "https://{0}.vendhq.com/api/1.0/token" REDIRECT_STATE = False EXTRA_DATA = [ ("refresh_token", "refresh_token"), ("domain_prefix", "domain_prefix"), ] def access_token_url(self): return self.ACCESS_TOKEN_URL.format(self.data["domain_prefix"]) def get_user_details(self, response): email = response["email"] username = response.get("username") or email.split("@", 1)[0] return { "username": username, "email": email, "fullname": "", "first_name": "", "last_name": "", } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" prefix = kwargs["response"]["domain_prefix"] url = f"https://{prefix}.vendhq.com/api/users" data = self.get_json(url, headers={"Authorization": f"Bearer {access_token}"}) return data["users"][0] if data.get("users") else {} social-auth-core-4.6.1/social_core/backends/vimeo.py000066400000000000000000000054621500362547200224210ustar00rootroot00000000000000from __future__ import annotations from typing import cast from .oauth import BaseOAuth1, BaseOAuth2 class VimeoOAuth1(BaseOAuth1): """Vimeo OAuth authentication backend""" name = "vimeo" AUTHORIZATION_URL = "https://vimeo.com/oauth/authorize" REQUEST_TOKEN_URL = "https://vimeo.com/oauth/request_token" ACCESS_TOKEN_URL = "https://vimeo.com/oauth/access_token" def get_user_id(self, details, response): return response.get("person", {}).get("id") def get_user_details(self, response): """Return user details from Twitter account""" person = response.get("person", {}) fullname, first_name, last_name = self.get_user_names( person.get("display_name", "") ) return { "username": person.get("username", ""), "email": "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Return user data provided""" return self.get_json( "https://vimeo.com/api/rest/v2", params={"format": "json", "method": "vimeo.people.getInfo"}, auth=self.oauth_auth(access_token), ) class VimeoOAuth2(BaseOAuth2): """Vimeo OAuth2 authentication backend""" name = "vimeo-oauth2" AUTHORIZATION_URL = "https://api.vimeo.com/oauth/authorize" ACCESS_TOKEN_URL = "https://api.vimeo.com/oauth/access_token" REFRESH_TOKEN_URL = "https://api.vimeo.com/oauth/request_token" SCOPE_SEPARATOR = "," API_ACCEPT_HEADER = {"Accept": "application/vnd.vimeo.*+json;version=3.0"} def get_redirect_uri(self, state: str | None = None) -> str: """ Build redirect with redirect_state parameter. @Vimeo API 3 requires exact redirect uri without additional additional state parameter included """ return cast("str", self.redirect_uri) def get_user_id(self, details, response): """Return user id""" try: user_id = response.get("user", {})["uri"].split("/")[-1] except KeyError: user_id = None return user_id def get_user_details(self, response): """Return user details from account""" user = response.get("user", {}) fullname, first_name, last_name = self.get_user_names(user.get("name", "")) return { "username": fullname, "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Return user data provided""" return self.get_json( "https://api.vimeo.com/me", params={"access_token": access_token}, headers=VimeoOAuth2.API_ACCEPT_HEADER, ) social-auth-core-4.6.1/social_core/backends/vk.py000066400000000000000000000163231500362547200217200ustar00rootroot00000000000000""" VK.com OpenAPI, OAuth2 and Iframe application OAuth2 backends, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/vk.html """ from __future__ import annotations import json from hashlib import md5 from time import time from typing import Any, cast from ..exceptions import AuthException, AuthTokenRevoked from ..utils import parse_qs from .base import BaseAuth from .oauth import BaseOAuth2 def vk_sig(payload: str) -> str: """ Calculates signature using md5. https://dev.vk.com/en/api/open-api/getting-started#Authorization%20on%20the%20Remote%20Side """ return md5(payload.encode("utf-8")).hexdigest() # noqa: S324 class VKontakteOpenAPI(BaseAuth): """VK.COM OpenAPI authentication backend""" name = "vk-openapi" ID_KEY = "id" def get_user_details(self, response): """Return user details from VK.com request""" nickname = response.get("nickname") or "" fullname, first_name, last_name = self.get_user_names( first_name=response.get("first_name", [""])[0], last_name=response.get("last_name", [""])[0], ) return { "username": response["id"] if len(nickname) == 0 else nickname, "email": "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): return self.data def auth_html(self): """Returns local VK authentication page, not necessary for VK to authenticate. """ ctx = { "VK_APP_ID": self.setting("APP_ID"), "VK_COMPLETE_URL": self.redirect_uri, } local_html = self.setting("LOCAL_HTML", "vkontakte.html") return self.strategy.render_html(tpl=local_html, context=ctx) def auth_complete(self, *args, **kwargs): """Performs check of authentication in VKontakte, returns User if succeeded""" session_value = self.strategy.session_get("vk_app_" + self.setting("APP_ID")) if "id" not in self.data or not session_value: raise ValueError("VK.com authentication is not completed") mapping = parse_qs(session_value) check_str = "".join( item + "=" + mapping[item] for item in ["expire", "mid", "secret", "sid"] ) key, secret = self.get_key_and_secret() hash = vk_sig(check_str + secret) if hash != mapping["sig"] or int(mapping["expire"]) < time(): raise ValueError("VK.com authentication failed: Invalid Hash") kwargs.update({"backend": self, "response": self.user_data(mapping["mid"])}) return self.strategy.authenticate(*args, **kwargs) def uses_redirect(self): """VK.com does not require visiting server url in order to do authentication, so auth_xxx methods are not needed to be called. Their current implementation is just an example""" return False class VKOAuth2(BaseOAuth2): """VKOAuth2 authentication backend""" name = "vk-oauth2" ID_KEY = "id" AUTHORIZATION_URL = "https://oauth.vk.com/authorize" ACCESS_TOKEN_URL = "https://oauth.vk.com/access_token" EXTRA_DATA = [("id", "id"), ("expires_in", "expires")] def get_user_details(self, response): """Return user details from VK.com account""" fullname, first_name, last_name = self.get_user_names( first_name=response.get("first_name"), last_name=response.get("last_name") ) return { "username": response.get("screen_name"), "email": response.get("email", ""), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" request_data = [ "first_name", "last_name", "screen_name", "nickname", "photo", *self.setting("EXTRA_DATA", []), ] fields = ",".join(set(request_data)) response = self.vk_api( "users.get", { "access_token": access_token, "fields": fields, }, ) if response and response.get("error"): error = response["error"] msg = error.get("error_msg", "Unknown error") if error.get("error_code") == 5: raise AuthTokenRevoked(self, msg) raise AuthException(self, msg) if response: data = cast("list[dict[str, str | None]]", response.get("response"))[0] data["user_photo"] = data.get("photo") # Backward compatibility return data return {} def vk_api(self, method: str, data: dict[str, str]) -> dict[Any, Any] | None: """ Calls VK.com OpenAPI method, check: https://vk.com/apiclub http://goo.gl/yLcaa """ # We need to perform server-side call if no access_token data["v"] = self.setting("API_VERSION", "5.131") if "access_token" not in data: key, secret = self.get_key_and_secret() if "api_id" not in data: data["api_id"] = key data["method"] = method data["format"] = "json" url = "https://api.vk.com/api.php" param_list = sorted(item + "=" + data[item] for item in data) data["sig"] = vk_sig("".join(param_list) + secret) else: url = "https://api.vk.com/method/" + method try: return self.get_json(url, params=data) except (TypeError, KeyError, OSError, ValueError, IndexError): return None class VKAppOAuth2(VKOAuth2): """VK.com Application Authentication support""" name = "vk-app" def auth_complete(self, *args, **kwargs): required_params = ("is_app_user", "viewer_id", "access_token", "api_id") if not all(param in self.data for param in required_params): return None auth_key = self.data.get("auth_key") # Verify signature, if present key, secret = self.get_key_and_secret() if auth_key: check_key = vk_sig("_".join([key, self.data.get("viewer_id"), secret])) if check_key != auth_key: raise ValueError("VK.com authentication failed: invalid auth key") user_check = self.setting("USERMODE") user_id = self.data.get("viewer_id") if user_check is not None: user_check = int(user_check) if user_check == 1: is_user = self.data.get("is_app_user") elif user_check == 2: is_user = self.vk_api("isAppUser", {"user_id": user_id}).get( "response", 0 ) if not int(is_user): return None auth_data = { "auth": self, "backend": self, "request": self.strategy.request_data(), "response": { self.ID_KEY: user_id, }, } auth_data["response"].update( json.loads(auth_data["request"]["api_result"])["response"][0] ) return self.strategy.authenticate(*args, **auth_data) social-auth-core-4.6.1/social_core/backends/weibo.py000066400000000000000000000041711500362547200224030ustar00rootroot00000000000000# author:hepochen@gmail.com https://github.com/hepochen """ Weibo OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/weibo.html """ from .oauth import BaseOAuth2 class WeiboOAuth2(BaseOAuth2): """Weibo (of sina) OAuth authentication backend""" name = "weibo" ID_KEY = "uid" AUTHORIZATION_URL = "https://api.weibo.com/oauth2/authorize" REQUEST_TOKEN_URL = "https://api.weibo.com/oauth2/request_token" ACCESS_TOKEN_URL = "https://api.weibo.com/oauth2/access_token" REDIRECT_STATE = False EXTRA_DATA = [ ("id", "id"), ("name", "username"), ("profile_image_url", "profile_image_url"), ("gender", "gender"), ] def get_user_details(self, response): """Return user details from Weibo. API URL is: https://api.weibo.com/2/users/show.json/?uid=&access_token= """ if self.setting("DOMAIN_AS_USERNAME"): username = response.get("domain", "") else: username = response.get("name", "") fullname, first_name, last_name = self.get_user_names( first_name=response.get("screen_name", "") ) return { "username": username, "fullname": fullname, "first_name": first_name, "last_name": last_name, } def get_uid(self, access_token): """Return uid by access_token""" data = self.get_json( "https://api.weibo.com/oauth2/get_token_info", method="POST", params={"access_token": access_token}, ) return data["uid"] def user_data(self, access_token, response=None, *args, **kwargs): """Return user data""" # If user id was not retrieved in the response, then get it directly # from weibo get_token_info endpoint uid = (response and response.get("uid")) or self.get_uid(access_token) user_data = self.get_json( "https://api.weibo.com/2/users/show.json", params={"access_token": access_token, "uid": uid}, ) user_data["uid"] = uid return user_data social-auth-core-4.6.1/social_core/backends/weixin.py000066400000000000000000000137231500362547200226040ustar00rootroot00000000000000# author:duoduo3369@gmail.com https://github.com/duoduo369 """ Weixin OAuth2 backend """ from urllib.parse import urlencode from requests import HTTPError from ..exceptions import AuthCanceled, AuthUnknownError from .oauth import BaseOAuth2 class WeixinOAuth2(BaseOAuth2): """Weixin OAuth authentication backend""" name = "weixin" ID_KEY = "openid" AUTHORIZATION_URL = "https://open.weixin.qq.com/connect/qrconnect" ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token" DEFAULT_SCOPE = ["snsapi_login"] REDIRECT_STATE = False EXTRA_DATA = [ ("nickname", "username"), ("headimgurl", "profile_image_url"), ] def get_user_details(self, response): """Return user details from Weixin. API URL is: https://api.weixin.qq.com/sns/userinfo """ if self.setting("DOMAIN_AS_USERNAME"): username = response.get("domain", "") else: username = response.get("nickname", "") return { "username": username, "profile_image_url": response.get("headimgurl", ""), } def user_data(self, access_token, *args, **kwargs): data = self.get_json( "https://api.weixin.qq.com/sns/userinfo", params={ "access_token": access_token, "openid": kwargs["response"]["openid"], }, ) nickname = data.get("nickname") if nickname: # weixin api has some encode bug, here need handle data["nickname"] = nickname.encode("raw_unicode_escape").decode("utf-8") return data def auth_params(self, state=None): appid, secret = self.get_key_and_secret() params = {"appid": appid, "redirect_uri": self.get_redirect_uri(state)} if self.STATE_PARAMETER and state: params["state"] = state if self.RESPONSE_TYPE: params["response_type"] = self.RESPONSE_TYPE return params def auth_complete_params(self, state=None): appid, secret = self.get_key_and_secret() return { "grant_type": "authorization_code", # request auth code "code": self.data.get("code", ""), # server response code "appid": appid, "secret": secret, "redirect_uri": self.get_redirect_uri(state), } def refresh_token_params(self, token, *args, **kwargs): appid, secret = self.get_key_and_secret() return { "refresh_token": token, "grant_type": "refresh_token", "appid": appid, "secret": secret, } def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" self.process_error(self.data) try: response = self.request_access_token( self.ACCESS_TOKEN_URL, data=self.auth_complete_params(self.validate_state()), headers=self.auth_headers(), method=self.ACCESS_TOKEN_METHOD, ) except HTTPError as err: if err.response.status_code == 400: raise AuthCanceled(self, response=err.response) raise except KeyError: raise AuthUnknownError(self) if "errcode" in response: raise AuthCanceled(self) self.process_error(response) return self.do_auth( response["access_token"], response=response, *args, **kwargs ) class WeixinOAuth2APP(WeixinOAuth2): """ Weixin OAuth authentication backend Can't use in web, only in weixin app """ name = "weixinapp" ID_KEY = "openid" AUTHORIZATION_URL = "https://open.weixin.qq.com/connect/oauth2/authorize" ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token" REDIRECT_STATE = False def auth_url(self): if self.STATE_PARAMETER or self.REDIRECT_STATE: # Store state in session for further request validation. The state # value is passed as state parameter (as specified in OAuth2 spec), # but also added to redirect, that way we can still verify the # request if the provider doesn't implement the state parameter. # Reuse token if any. name = self.name + "_state" state = self.strategy.session_get(name) if state is None: state = self.state_token() self.strategy.session_set(name, state) else: state = None params = self.auth_params(state) params.update(self.get_scope_argument()) params.update(self.auth_extra_arguments()) params = urlencode(sorted(params.items())) return "{}#wechat_redirect".format(self.AUTHORIZATION_URL + "?" + params) def auth_complete_params(self, state=None): appid, secret = self.get_key_and_secret() return { "grant_type": "authorization_code", # request auth code "code": self.data.get("code", ""), # server response code "appid": appid, "secret": secret, } def validate_state(self): return None def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" self.process_error(self.data) try: response = self.request_access_token( self.ACCESS_TOKEN_URL, data=self.auth_complete_params(self.validate_state()), headers=self.auth_headers(), method=self.ACCESS_TOKEN_METHOD, ) except HTTPError as err: if err.response.status_code == 400: raise AuthCanceled(self) raise except KeyError: raise AuthUnknownError(self) if "errcode" in response: raise AuthCanceled(self) self.process_error(response) return self.do_auth( response["access_token"], response=response, *args, **kwargs ) social-auth-core-4.6.1/social_core/backends/withings.py000066400000000000000000000010161500362547200231250ustar00rootroot00000000000000from .oauth import BaseOAuth1 class WithingsOAuth(BaseOAuth1): name = "withings" AUTHORIZATION_URL = "https://developer.health.nokia.com/account/authorize" REQUEST_TOKEN_URL = "https://developer.health.nokia.com/account/request_token" ACCESS_TOKEN_URL = "https://developer.health.nokia.com/account/access_token" ID_KEY = "userid" def get_user_details(self, response): """Return user details from Withings account""" return {"userid": response["access_token"]["userid"], "email": ""} social-auth-core-4.6.1/social_core/backends/wlcg.py000066400000000000000000000023551500362547200222340ustar00rootroot00000000000000from urllib.parse import urlencode from .oauth import BaseOAuth2 class WLCGOAuth2(BaseOAuth2): """ WLCG IAM Authentication Backend """ name = "wlcg" API_URL = "https://wlcg.cloud.cnaf.infn.it" AUTHORIZATION_URL = "https://wlcg.cloud.cnaf.infn.it/authorize" ACCESS_TOKEN_URL = "https://wlcg.cloud.cnaf.infn.it/token" REFRESH_TOKEN_URL = "https://wlcg.cloud.cnaf.infn.it/token" DEFAULT_SCOPE = ["openid", "email", "profile", "wlcg", "offline_access"] REDIRECT_STATE = False def get_user_details(self, response): """Return user details from WLCG IAM service""" fullname, first_name, last_name = self.get_user_names( first_name=response.get("given_name"), last_name=response.get("family_name") ) return { "username": response.get("email"), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" url = "https://wlcg.cloud.cnaf.infn.it/userinfo?" + urlencode( {"access_token": access_token} ) return self.get_json(url) social-auth-core-4.6.1/social_core/backends/wunderlist.py000066400000000000000000000017451500362547200235020ustar00rootroot00000000000000from .oauth import BaseOAuth2 class WunderlistOAuth2(BaseOAuth2): """Wunderlist OAuth2 authentication backend""" name = "wunderlist" AUTHORIZATION_URL = "https://www.wunderlist.com/oauth/authorize" ACCESS_TOKEN_URL = "https://www.wunderlist.com/oauth/access_token" REDIRECT_STATE = False def get_user_details(self, response): """Return user details from Wunderlist account""" fullname, first_name, last_name = self.get_user_names(response.get("name")) return { "username": str(response.get("id")), "email": response.get("email"), "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" headers = {"X-Access-Token": access_token, "X-Client-ID": self.setting("KEY")} return self.get_json("https://a.wunderlist.com/api/v1/user", headers=headers) social-auth-core-4.6.1/social_core/backends/xing.py000066400000000000000000000045761500362547200222540ustar00rootroot00000000000000""" XING OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/xing.html """ from oauthlib.oauth1 import SIGNATURE_TYPE_AUTH_HEADER from requests_oauthlib import OAuth1 from ..exceptions import AuthTokenError from .oauth import BaseOAuth1 class XingOAuth(BaseOAuth1): """Xing OAuth authentication backend""" name = "xing" AUTHORIZATION_URL = "https://api.xing.com/v1/authorize" REQUEST_TOKEN_URL = "https://api.xing.com/v1/request_token" ACCESS_TOKEN_URL = "https://api.xing.com/v1/access_token" SCOPE_SEPARATOR = "+" EXTRA_DATA = [("id", "id"), ("user_id", "user_id")] def get_user_details(self, response): """Return user details from Xing account""" email = response.get("email", "") fullname, first_name, last_name = self.get_user_names( first_name=response["first_name"], last_name=response["last_name"] ) return { "username": first_name + last_name, "fullname": fullname, "first_name": first_name, "last_name": last_name, "email": email, } def clean_oauth_auth(self, access_token): """Override of oauth_auth since Xing doesn't like callback_uri and oauth_verifier on authenticated API calls""" key, secret = self.get_key_and_secret() resource_owner_key = access_token.get("oauth_token") resource_owner_secret = access_token.get("oauth_token_secret") if not resource_owner_key: raise AuthTokenError(self, "Missing oauth_token") if not resource_owner_secret: raise AuthTokenError(self, "Missing oauth_token_secret") return OAuth1( key, secret, resource_owner_key=resource_owner_key, resource_owner_secret=resource_owner_secret, signature_type=SIGNATURE_TYPE_AUTH_HEADER, ) def user_data(self, access_token, *args, **kwargs): """Return user data provided""" profile = self.get_json( "https://api.xing.com/v1/users/me.json", auth=self.clean_oauth_auth(access_token), )["users"][0] return { "user_id": profile["id"], "id": profile["id"], "first_name": profile["first_name"], "last_name": profile["last_name"], "email": profile["active_email"], } social-auth-core-4.6.1/social_core/backends/yahoo.py000066400000000000000000000123721500362547200224170ustar00rootroot00000000000000""" Yahoo OpenId, OAuth1 and OAuth2 backends, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/yahoo.html """ from requests.auth import HTTPBasicAuth from ..utils import handle_http_errors from .oauth import BaseOAuth1, BaseOAuth2 class YahooOAuth(BaseOAuth1): """Yahoo OAuth authentication backend. DEPRECATED""" name = "yahoo-oauth" ID_KEY = "guid" AUTHORIZATION_URL = "https://api.login.yahoo.com/oauth/v2/request_auth" REQUEST_TOKEN_URL = "https://api.login.yahoo.com/oauth/v2/get_request_token" ACCESS_TOKEN_URL = "https://api.login.yahoo.com/oauth/v2/get_token" EXTRA_DATA = [ ("guid", "id"), ("access_token", "access_token"), ("expires", "expires"), ] def get_user_details(self, response): """Return user details from Yahoo Profile""" fullname, first_name, last_name = self.get_user_names( first_name=response.get("givenName"), last_name=response.get("familyName") ) emails = [email for email in response.get("emails", []) if email.get("handle")] emails.sort(key=lambda e: e.get("primary", False), reverse=True) return { "username": response.get("nickname"), "email": emails[0]["handle"] if emails else "", "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" url = "https://social.yahooapis.com/v1/user/{0}/profile?format=json" return self.get_json( url.format(self._get_guid(access_token)), auth=self.oauth_auth(access_token) )["profile"] def _get_guid(self, access_token): """ Because you have to provide GUID for every API request it's also returned during one of OAuth calls """ return self.get_json( "https://social.yahooapis.com/v1/me/guid?format=json", auth=self.oauth_auth(access_token), )["guid"]["value"] class YahooOAuth2(BaseOAuth2): """Yahoo OAuth2 authentication backend""" name = "yahoo-oauth2" ID_KEY = "sub" AUTHORIZATION_URL = "https://api.login.yahoo.com/oauth2/request_auth" ACCESS_TOKEN_URL = "https://api.login.yahoo.com/oauth2/get_token" EXTRA_DATA = [ ("sub", "id"), ("access_token", "access_token"), ("expires_in", "expires"), ("refresh_token", "refresh_token"), ("token_type", "token_type"), ] def get_user_names(self, first_name, last_name): # type: ignore[reportIncompatibleMethodOverride] if first_name or last_name: return f"{first_name} {last_name}", first_name, last_name return None, None, None def get_user_details(self, response): """ Return user details from Yahoo Profile. To Get user email you need the profile private read permission. """ fullname, first_name, last_name = self.get_user_names( first_name=response.get("given_name"), last_name=response.get("family_name") ) email = response.get("email") return { "username": response.get("nickname"), "email": email, "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" url = "https://api.login.yahoo.com/openid/v1/userinfo" return self.get_json( url, headers={"Authorization": f"Bearer {access_token}"}, method="GET" ) @handle_http_errors def auth_complete(self, *args, **kwargs): """Completes login process, must return user instance""" self.process_error(self.data) response = self.request_access_token( self.ACCESS_TOKEN_URL, auth=HTTPBasicAuth(*self.get_key_and_secret()), data=self.auth_complete_params(self.validate_state()), headers=self.auth_headers(), method=self.ACCESS_TOKEN_METHOD, ) self.process_error(response) return self.do_auth( response["access_token"], response=response, *args, **kwargs ) def refresh_token_params(self, token, *args, **kwargs): return { "refresh_token": token, "grant_type": "refresh_token", "redirect_uri": "oob", # out of bounds } def refresh_token(self, token, *args, **kwargs): params = self.refresh_token_params(token, *args, **kwargs) url = self.REFRESH_TOKEN_URL or self.ACCESS_TOKEN_URL method = self.REFRESH_TOKEN_METHOD key = "params" if method == "GET" else "data" request_args = {"headers": self.auth_headers(), "method": method, key: params} request = self.request( url, auth=HTTPBasicAuth(*self.get_key_and_secret()), **request_args ) return self.process_refresh_token_response(request, *args, **kwargs) def auth_complete_params(self, state=None): return { "grant_type": "authorization_code", # request auth code "code": self.data.get("code", ""), # server response code "redirect_uri": self.get_redirect_uri(state), } social-auth-core-4.6.1/social_core/backends/yammer.py000066400000000000000000000027501500362547200225710ustar00rootroot00000000000000""" Yammer OAuth2 production and staging backends, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/yammer.html """ from .oauth import BaseOAuth2 class YammerOAuth2(BaseOAuth2): name = "yammer" AUTHORIZATION_URL = "https://www.yammer.com/dialog/oauth" ACCESS_TOKEN_URL = "https://www.yammer.com/oauth2/access_token" EXTRA_DATA = [("id", "id"), ("expires", "expires"), ("mugshot_url", "mugshot_url")] def get_user_id(self, details, response): return response["user"]["id"] def get_user_details(self, response): username = response["user"]["name"] fullname, first_name, last_name = self.get_user_names( fullname=response["user"]["full_name"], first_name=response["user"]["first_name"], last_name=response["user"]["last_name"], ) email = response["user"]["contact"]["email_addresses"][0]["address"] mugshot_url = response["user"]["mugshot_url"] return { "username": username, "email": email, "fullname": fullname, "first_name": first_name, "last_name": last_name, "picture_url": mugshot_url, } class YammerStagingOAuth2(YammerOAuth2): name = "yammer-staging" AUTHORIZATION_URL = "https://www.staging.yammer.com/dialog/oauth" ACCESS_TOKEN_URL = "https://www.staging.yammer.com/oauth2/access_token" REQUEST_TOKEN_URL = "https://www.staging.yammer.com/oauth2/request_token" social-auth-core-4.6.1/social_core/backends/yandex.py000066400000000000000000000056111500362547200225660ustar00rootroot00000000000000""" Yandex OpenID and OAuth2 support. This contribution adds support for Yandex.ru OpenID service in the form openid.yandex.ru/user. Username is retrieved from the identity url. If username is not specified, OpenID 2.0 url used for authentication. """ from urllib.parse import urlsplit from .oauth import BaseOAuth2 from .open_id import OpenIdAuth class YandexOpenId(OpenIdAuth): """Yandex OpenID authentication backend""" name = "yandex-openid" URL = "http://openid.yandex.ru" def get_user_id(self, details, response): return details["email"] or response.identity_url def get_user_details(self, response): """Generate username from identity url""" values = super().get_user_details(response) values["username"] = values.get("username") or urlsplit( response.identity_url ).path.strip("/") values["email"] = values.get("email", "") return values class YandexOAuth2(BaseOAuth2): """Legacy Yandex OAuth2 authentication backend""" name = "yandex-oauth2" AUTHORIZATION_URL = "https://oauth.yandex.com/authorize" ACCESS_TOKEN_URL = "https://oauth.yandex.com/token" REDIRECT_STATE = False def get_user_details(self, response): fullname, first_name, last_name = self.get_user_names( response.get("real_name") or response.get("display_name") or "" ) email = response.get("default_email") if not email: emails = response.get("emails") email = emails[0] if emails else "" return { "username": response.get("display_name"), "email": email, "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): return self.get_json( "https://login.yandex.ru/info", params={"oauth_token": access_token, "format": "json"}, ) class YaruOAuth2(BaseOAuth2): name = "yaru" AUTHORIZATION_URL = "https://oauth.yandex.com/authorize" ACCESS_TOKEN_URL = "https://oauth.yandex.com/token" REDIRECT_STATE = False def get_user_details(self, response): fullname, first_name, last_name = self.get_user_names( response.get("real_name") or response.get("display_name") or "" ) email = response.get("default_email") if not email: emails = response.get("emails") email = emails[0] if emails else "" return { "username": response.get("display_name"), "email": email, "fullname": fullname, "first_name": first_name, "last_name": last_name, } def user_data(self, access_token, *args, **kwargs): return self.get_json( "https://login.yandex.ru/info", params={"oauth_token": access_token, "format": "json"}, ) social-auth-core-4.6.1/social_core/backends/zoom.py000066400000000000000000000034141500362547200222610ustar00rootroot00000000000000import base64 from .oauth import BaseOAuth2 class ZoomOAuth2(BaseOAuth2): """ Zoom OAuth2 authentication backend Doc Reference: https://marketplace.zoom.us/docs/guides/auth/oauth """ name = "zoom-oauth2" AUTHORIZATION_URL = "https://zoom.us/oauth/authorize" ACCESS_TOKEN_URL = "https://zoom.us/oauth/token" USER_DETAILS_URL = "https://api.zoom.us/v2/users/me" DEFAULT_SCOPE = ["user:read"] REFRESH_TOKEN_METHOD = "POST" REDIRECT_STATE = False EXTRA_DATA = [("expires_in", "expires")] def user_data(self, access_token, *args, **kwargs): return self.get_json( self.USER_DETAILS_URL, headers={"Authorization": f"Bearer {access_token}"}, ) def get_user_details(self, response): username = response.get("id", "") first_name = response.get("first_name", "") last_name = response.get("last_name", "") email = response.get("email", "") fullname = "" return { "username": username, "email": email, "fullname": fullname, "first_name": first_name, "last_name": last_name, } def auth_complete_params(self, state=None): return { "grant_type": "authorization_code", # request auth code "code": self.data.get("code", ""), # server response code "redirect_uri": self.get_redirect_uri(state), } def auth_headers(self): return { "Authorization": b"Basic " + base64.urlsafe_b64encode( "{}:{}".format(*self.get_key_and_secret()).encode() ) } def refresh_token_params(self, token, *args, **kwargs): return {"refresh_token": token, "grant_type": "refresh_token"} social-auth-core-4.6.1/social_core/backends/zotero.py000066400000000000000000000016621500362547200226220ustar00rootroot00000000000000""" Zotero OAuth1 backends, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/zotero.html """ from .oauth import BaseOAuth1 class ZoteroOAuth(BaseOAuth1): """Zotero OAuth authorization mechanism""" name = "zotero" AUTHORIZATION_URL = "https://www.zotero.org/oauth/authorize" REQUEST_TOKEN_URL = "https://www.zotero.org/oauth/request" ACCESS_TOKEN_URL = "https://www.zotero.org/oauth/access" def get_user_id(self, details, response): """ Return user unique id provided by service. For Ubuntu One the nickname should be original. """ return details["userID"] def get_user_details(self, response): """Return user details from Zotero API account""" access_token = response.get("access_token", {}) return { "username": access_token.get("username", ""), "userID": access_token.get("userID", ""), } social-auth-core-4.6.1/social_core/exceptions.py000066400000000000000000000071711500362547200217100ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from .backends.base import BaseAuth class SocialAuthBaseException(ValueError): """Base class for pipeline exceptions.""" class WrongBackend(SocialAuthBaseException): def __init__(self, backend_name: str): self.backend_name = backend_name def __str__(self): return f'Incorrect authentication service "{self.backend_name}"' class MissingBackend(WrongBackend): def __str__(self): return f'Missing backend "{self.backend_name}" entry' class NotAllowedToDisconnect(SocialAuthBaseException): """User is not allowed to disconnect it's social account.""" def __str__(self): return "This account is not allowed to be disconnected." class AuthException(SocialAuthBaseException): """Auth process exception.""" def __init__(self, backend: BaseAuth, *args, **kwargs): self.backend = backend super().__init__(*args, **kwargs) class AuthFailed(AuthException): """Auth process failed for some reason.""" def __str__(self): msg = super().__str__() if msg == "access_denied": return "Authentication process was canceled" return f"Authentication failed: {msg}" class AuthCanceled(AuthException): """Auth process was canceled by user.""" def __init__(self, *args, **kwargs): self.response = kwargs.pop("response", None) super().__init__(*args, **kwargs) def __str__(self): msg = super().__str__() if msg: return f"Authentication process canceled: {msg}" return "Authentication process canceled" class AuthUnknownError(AuthException): """Unknown auth process error.""" def __str__(self): msg = super().__str__() return f"An unknown error happened while authenticating {msg}" class AuthTokenError(AuthException): """Auth token error.""" def __str__(self): msg = super().__str__() return f"Token error: {msg}" class AuthMissingParameter(AuthException): """Missing parameter needed to start or complete the process.""" def __init__(self, backend: BaseAuth, parameter: str, *args, **kwargs): self.parameter = parameter super().__init__(backend, *args, **kwargs) def __str__(self): return f"Missing needed parameter {self.parameter}" class AuthStateMissing(AuthException): """State parameter is incorrect.""" def __str__(self): return "Session value state missing." class AuthStateForbidden(AuthException): """State parameter is incorrect.""" def __str__(self): return "Wrong state parameter given." class AuthAlreadyAssociated(AuthException): """A different user has already associated the target social account""" def __str__(self): return "This account is already in use." class AuthTokenRevoked(AuthException): """User revoked the access_token in the provider.""" def __str__(self): return "User revoke access to the token" class AuthForbidden(AuthException): """Authentication for this user is forbidden""" def __str__(self): return "Your credentials aren't allowed" class AuthUnreachableProvider(AuthException): """Cannot reach the provider""" def __str__(self): return "The authentication provider could not be reached" class InvalidEmail(AuthException): def __str__(self): return "Email couldn't be validated" class AuthConnectionError(AuthException): """Connection error duing authentication.""" def __str__(self): msg = super().__str__() return f"Connection error: {msg}" social-auth-core-4.6.1/social_core/pipeline/000077500000000000000000000000001500362547200207545ustar00rootroot00000000000000social-auth-core-4.6.1/social_core/pipeline/__init__.py000066400000000000000000000046231500362547200230720ustar00rootroot00000000000000DEFAULT_AUTH_PIPELINE = ( # Get the information we can about the user and return it in a simple # format to create the user instance later. On some cases the details are # already part of the auth response from the provider, but sometimes this # could hit a provider API. "social_core.pipeline.social_auth.social_details", # Get the social uid from whichever service we're authing thru. The uid is # the unique identifier of the given user in the provider. "social_core.pipeline.social_auth.social_uid", # Verifies that the current auth process is valid within the current # project, this is where emails and domains whitelists are applied (if # defined). "social_core.pipeline.social_auth.auth_allowed", # Checks if the current social-account is already associated in the site. "social_core.pipeline.social_auth.social_user", # Make up a username for this person, appends a random string at the end if # there's any collision. "social_core.pipeline.user.get_username", # Send a validation email to the user to verify its email address. # 'social_core.pipeline.mail.mail_validation', # Associates the current social details with another user account with # a similar email address. # 'social_core.pipeline.social_auth.associate_by_email', # Create a user account if we haven't found one yet. "social_core.pipeline.user.create_user", # Create the record that associated the social account with this user. "social_core.pipeline.social_auth.associate_user", # Populate the extra_data field in the social record with the values # specified by settings (and the default ones like access_token, etc). "social_core.pipeline.social_auth.load_extra_data", # Update the user record with any changed info from the auth service. "social_core.pipeline.user.user_details", ) DEFAULT_DISCONNECT_PIPELINE = ( # Verifies that the social association can be disconnected from the current # user (ensure that the user login mechanism is not compromised by this # disconnection). "social_core.pipeline.disconnect.allowed_to_disconnect", # Collects the social associations to disconnect. "social_core.pipeline.disconnect.get_entries", # Revoke any access_token when possible. "social_core.pipeline.disconnect.revoke_tokens", # Removes the social associations. "social_core.pipeline.disconnect.disconnect", ) social-auth-core-4.6.1/social_core/pipeline/debug.py000066400000000000000000000003741500362547200224200ustar00rootroot00000000000000from pprint import pprint def debug(response, details, *args, **kwargs): print("=" * 80) pprint(response) print("=" * 80) pprint(details) print("=" * 80) pprint(args) print("=" * 80) pprint(kwargs) print("=" * 80) social-auth-core-4.6.1/social_core/pipeline/disconnect.py000066400000000000000000000017311500362547200234610ustar00rootroot00000000000000from ..exceptions import NotAllowedToDisconnect def allowed_to_disconnect( strategy, user, name, user_storage, association_id=None, *args, **kwargs ): if not user_storage.allowed_to_disconnect(user, name, association_id): raise NotAllowedToDisconnect def get_entries( strategy, user, name, user_storage, association_id=None, *args, **kwargs ): return { "entries": user_storage.get_social_auth_for_user(user, name, association_id) } def revoke_tokens(strategy, entries, *args, **kwargs): revoke_tokens = strategy.setting("REVOKE_TOKENS_ON_DISCONNECT", False) if revoke_tokens: for entry in entries: if "access_token" in entry.extra_data: backend = entry.get_backend_instance(strategy) backend.revoke_token(entry.extra_data["access_token"], entry.uid) def disconnect(strategy, entries, user_storage, *args, **kwargs): for entry in entries: user_storage.disconnect(entry) social-auth-core-4.6.1/social_core/pipeline/mail.py000066400000000000000000000022241500362547200222500ustar00rootroot00000000000000from ..exceptions import InvalidEmail from .partial import partial @partial def mail_validation(backend, details, is_new=False, *args, **kwargs): requires_validation = backend.REQUIRES_EMAIL_VALIDATION or backend.setting( "FORCE_EMAIL_VALIDATION", False ) send_validation = details.get("email") and ( is_new or backend.setting("PASSWORDLESS", False) ) if requires_validation and send_validation: data = backend.strategy.request_data() if "verification_code" in data: backend.strategy.session_pop("email_validation_address") if not backend.strategy.validate_email( details["email"], data["verification_code"] ): raise InvalidEmail(backend) return None current_partial = kwargs.get("current_partial") backend.strategy.send_email_validation( backend, details["email"], current_partial.token ) backend.strategy.session_set("email_validation_address", details["email"]) return backend.strategy.redirect( backend.strategy.setting("EMAIL_VALIDATION_URL") ) return None social-auth-core-4.6.1/social_core/pipeline/partial.py000066400000000000000000000035571500362547200227740ustar00rootroot00000000000000from functools import wraps from ..utils import PARTIAL_TOKEN_SESSION_NAME from .utils import partial_prepare def partial_step(save_to_session): """Wraps func to behave like a partial pipeline step, any output that's not None or {} will be considered a response object and will be returned to user. The pipeline function will receive a current_partial object, it contains the partial pipeline data and a token that is used to identify it when it's continued, this is useful to build links with the token. The default value for this parameter is partial_token, but can be overridden by SOCIAL_AUTH_PARTIAL_PIPELINE_TOKEN_NAME setting. The token is also stored in the session under the PARTIAL_TOKEN_SESSION_NAME (partial_pipeline_token) key when the save_to_session parameter is True. """ def decorator(func): @wraps(func) def wrapper(strategy, backend, pipeline_index, *args, **kwargs): current_partial = partial_prepare( strategy, backend, pipeline_index, *args, **kwargs ) out = ( func( strategy=strategy, backend=backend, pipeline_index=pipeline_index, current_partial=current_partial, *args, **kwargs, ) or {} ) if not isinstance(out, dict): strategy.storage.partial.store(current_partial) if save_to_session: strategy.session_set( PARTIAL_TOKEN_SESSION_NAME, current_partial.token ) return out return wrapper return decorator # Backward compatible partial decorator, that stores the token in the session partial = partial_step(save_to_session=True) social-auth-core-4.6.1/social_core/pipeline/social_auth.py000066400000000000000000000065671500362547200236370ustar00rootroot00000000000000from ..exceptions import AuthAlreadyAssociated, AuthException, AuthForbidden def social_details(backend, details, response, *args, **kwargs): return {"details": dict(backend.get_user_details(response), **details)} def social_uid(backend, details, response, *args, **kwargs): return {"uid": str(backend.get_user_id(details, response))} def auth_allowed(backend, details, response, *args, **kwargs): if not backend.auth_allowed(response, details): raise AuthForbidden(backend) def social_user(backend, uid, user=None, *args, **kwargs): provider = backend.name social = backend.strategy.storage.user.get_social_auth(provider, uid) if social: if user and social.user != user: raise AuthAlreadyAssociated(backend) if not user: user = social.user return { "social": social, "user": user, "is_new": user is None, "new_association": social is None, } def associate_user(backend, uid, user=None, social=None, *args, **kwargs): if user and not social: try: social = backend.strategy.storage.user.create_social_auth( user, uid, backend.name ) except Exception as err: if not backend.strategy.storage.is_integrity_error(err): raise # Protect for possible race condition, those bastard with FTL # clicking capabilities, check issue #131: # https://github.com/omab/django-social-auth/issues/131 result = social_user(backend, uid, user, *args, **kwargs) # Check if matching social auth really exists. In case it does # not, the integrity error probably had different cause than # existing entry and should not be hidden. if not result["social"]: raise return result else: return {"social": social, "user": social.user, "new_association": True} return None def associate_by_email(backend, details, user=None, *args, **kwargs): """ Associate current auth with a user with the same email address in the DB. This pipeline entry is not 100% secure unless you know that the providers enabled enforce email verification on their side, otherwise a user can attempt to take over another user account by using the same (not validated) email address on some provider. This pipeline entry is disabled by default. """ if user: return None email = details.get("email") if email: # Try to associate accounts registered with the same email address, # only if it's a single object. AuthException is raised if multiple # objects are returned. users = list(backend.strategy.storage.user.get_users_by_email(email)) if len(users) == 0: return None if len(users) > 1: raise AuthException( backend, "The given email address is associated with another account" ) return {"user": users[0], "is_new": False} return None def load_extra_data(backend, details, response, uid, user, *args, **kwargs): social = kwargs.get("social") or backend.strategy.storage.user.get_social_auth( backend.name, uid ) if social: extra_data = backend.extra_data(user, uid, response, details, *args, **kwargs) social.set_extra_data(extra_data) social-auth-core-4.6.1/social_core/pipeline/user.py000066400000000000000000000111171500362547200223050ustar00rootroot00000000000000from uuid import uuid4 from ..utils import module_member, slugify USER_FIELDS = ["username", "email"] def get_username(strategy, details, backend, user=None, *args, **kwargs): if "username" not in backend.setting("USER_FIELDS", USER_FIELDS): return None storage = strategy.storage if not user: email_as_username = backend.setting("USERNAME_IS_FULL_EMAIL", False) uuid_length = backend.setting("UUID_LENGTH", 16) max_length = storage.user.username_max_length() do_slugify = backend.setting("SLUGIFY_USERNAMES", False) do_clean = backend.setting("CLEAN_USERNAMES", True) def identity_func(val): return val if do_clean: override_clean = backend.setting("CLEAN_USERNAME_FUNCTION") if override_clean: clean_func = module_member(override_clean) else: clean_func = storage.user.clean_username else: clean_func = identity_func if do_slugify: override_slug = backend.setting("SLUGIFY_FUNCTION") slug_func = module_member(override_slug) if override_slug else slugify else: slug_func = identity_func if email_as_username and details.get("email"): username = details["email"] elif details.get("username"): username = details["username"] else: username = uuid4().hex short_username = ( username[: max_length - uuid_length] if max_length is not None else username ) final_username = slug_func(clean_func(username[:max_length])) # Generate a unique username for current user using username # as base but adding a unique hash at the end. Original # username is cut to avoid any field max_length. # The final_username may be empty and will skip the loop. while not final_username or storage.user.user_exists(username=final_username): username = short_username + uuid4().hex[:uuid_length] final_username = slug_func(clean_func(username[:max_length])) else: final_username = storage.user.get_username(user) return {"username": final_username} def create_user(strategy, details, backend, user=None, *args, **kwargs): if user: return {"is_new": False} fields = { name: kwargs.get(name, details.get(name)) for name in backend.setting("USER_FIELDS", USER_FIELDS) } if not fields: return None # Allow overriding the email field if desired by application specification if backend.setting("FORCE_EMAIL_LOWERCASE", False): emailfield = fields.get("email") if emailfield: fields["email"] = emailfield.lower() return {"is_new": True, "user": strategy.create_user(**fields)} def user_details(strategy, details, backend, user=None, *args, **kwargs): """Update user details using data from provider.""" if not user: return changed = False # flag to track changes # Default protected user fields (username, id, pk and email) can be ignored # by setting the SOCIAL_AUTH_NO_DEFAULT_PROTECTED_USER_FIELDS to True if strategy.setting("NO_DEFAULT_PROTECTED_USER_FIELDS", backend=backend) is True: protected = () else: protected = ( "username", "id", "pk", "email", "password", "is_active", "is_staff", "is_superuser", ) protected = protected + tuple( strategy.setting("PROTECTED_USER_FIELDS", [], backend=backend) ) # Update user model attributes with the new data sent by the current # provider. Update on some attributes is disabled by default, for # example username and id fields. It's also possible to disable update # on fields defined in SOCIAL_AUTH_PROTECTED_USER_FIELDS. field_mapping = strategy.setting("USER_FIELD_MAPPING", {}, backend=backend) for name, value in details.items(): # Convert to existing user field if mapping exists name = field_mapping.get(name, name) if value is None or not hasattr(user, name) or name in protected: continue current_value = getattr(user, name, None) if current_value == value: continue immutable_fields = tuple( strategy.setting("IMMUTABLE_USER_FIELDS", [], backend=backend) ) if name in immutable_fields and current_value: continue changed = True setattr(user, name, value) if changed: strategy.storage.user.changed(user) social-auth-core-4.6.1/social_core/pipeline/utils.py000066400000000000000000000045701500362547200224740ustar00rootroot00000000000000SERIALIZABLE_TYPES = (dict, list, tuple, set, bool, type(None), int, str, bytes) def is_dict_type(value): """Treat any dict, MergeDict, MultiDict instance as dict type""" # Check by class name to avoid importing Django MergeDict or # Werkzeug MultiDict return isinstance(value, dict) or value.__class__.__name__ in ( "MergeDict", "MultiDict", ) def partial_prepare( strategy, backend, next_step, user=None, social=None, *args, **kwargs ): kwargs.update( { "response": kwargs.get("response") or {}, "details": kwargs.get("details") or {}, "username": kwargs.get("username"), "uid": kwargs.get("uid"), "is_new": kwargs.get("is_new") or False, "new_association": kwargs.get("new_association") or False, "user": (hasattr(user, "id") and user.id) or None, "social": (social and {"provider": social.provider, "uid": social.uid}) or None, } ) clean_args = [strategy.to_session_value(val) for val in args] # Clean any MergeDict data type from the values clean_kwargs = {} for name, value in kwargs.items(): value = dict(value) if is_dict_type(value) else value if isinstance(value, SERIALIZABLE_TYPES): clean_kwargs[name] = strategy.to_session_value(value) return strategy.storage.partial.prepare( backend.name, next_step, {"args": clean_args, "kwargs": clean_kwargs} ) def partial_store( strategy, backend, next_step, user=None, social=None, *args, **kwargs ): partial = partial_prepare( strategy, backend, next_step, user=user, social=social, *args, **kwargs ) return strategy.storage.partial.store(partial) def partial_load(strategy, token): partial = strategy.storage.partial.load(token) if partial: args = partial.args kwargs = partial.kwargs.copy() user = kwargs.get("user") social = kwargs.get("social") if isinstance(social, dict): kwargs["social"] = strategy.storage.user.get_social_auth(**social) if user: kwargs["user"] = strategy.storage.user.get_user(user) partial.args = [strategy.from_session_value(val) for val in args] partial.kwargs = { key: strategy.from_session_value(val) for key, val in kwargs.items() } return partial social-auth-core-4.6.1/social_core/py.typed000066400000000000000000000000001500362547200206340ustar00rootroot00000000000000social-auth-core-4.6.1/social_core/storage.py000066400000000000000000000245341500362547200211750ustar00rootroot00000000000000"""Models mixins for Social Auth""" from __future__ import annotations import base64 import re import uuid from abc import abstractmethod from datetime import datetime, timedelta, timezone from typing import Any from openid.association import Association as OpenIdAssociation from .exceptions import MissingBackend NO_ASCII_REGEX = re.compile(r"[^\x00-\x7F]+") NO_SPECIAL_REGEX = re.compile(r"[^\w.@+_-]+", re.UNICODE) class UserMixin: # Consider tokens that expire in 5 seconds as already expired ACCESS_TOKEN_EXPIRED_THRESHOLD = 5 provider = "" uid = None extra_data = None @abstractmethod def save(self): ... def get_backend(self, strategy): return strategy.get_backend_class(self.provider) def get_backend_instance(self, strategy): try: return strategy.get_backend(self.provider) except MissingBackend: return None @property def access_token(self): """Return access_token stored in extra_data or None""" return self.extra_data.get("access_token") def refresh_token(self, strategy, *args, **kwargs): token = self.extra_data.get("refresh_token") or self.extra_data.get( "access_token" ) backend = self.get_backend_instance(strategy) if token and backend and hasattr(backend, "refresh_token"): response = backend.refresh_token(token, *args, **kwargs) extra_data = backend.extra_data(self, self.uid, response, self.extra_data) if self.set_extra_data(extra_data): self.save() def expiration_timedelta(self): """Return provider session live seconds. Returns a timedelta ready to use with session.set_expiry(). If provider returns a timestamp instead of session seconds to live, the timedelta is inferred from current time (using UTC timezone). None is returned if there's no value stored or it's invalid. """ if self.extra_data and "expires" in self.extra_data: try: expires = int(self.extra_data.get("expires")) except (ValueError, TypeError): return None now = datetime.now(timezone.utc) # Detect if expires is a timestamp if expires > now.timestamp(): # expires is a datetime, return the remaining difference expiry_time = datetime.fromtimestamp(expires, tz=timezone.utc) return expiry_time - now # expires is the time to live seconds since creation, # check against auth_time if present, otherwise return # the value auth_time = self.extra_data.get("auth_time") if auth_time: reference = datetime.fromtimestamp(auth_time, tz=timezone.utc) return (reference + timedelta(seconds=expires)) - now return timedelta(seconds=expires) return None def expiration_datetime(self): # backward compatible alias return self.expiration_timedelta() def access_token_expired(self): """Return true / false if access token is already expired""" expiration = self.expiration_timedelta() return ( expiration and expiration.total_seconds() <= self.ACCESS_TOKEN_EXPIRED_THRESHOLD ) def get_access_token(self, strategy): """Returns a valid access token.""" if self.access_token_expired(): self.refresh_token(strategy) return self.access_token def set_extra_data(self, extra_data=None): if extra_data and self.extra_data != extra_data: if self.extra_data and not isinstance(self.extra_data, str): self.extra_data.update(extra_data) else: self.extra_data = extra_data return True return None @classmethod def clean_username(cls, value): """Clean username removing any unsupported character""" value = NO_ASCII_REGEX.sub("", value) return NO_SPECIAL_REGEX.sub("", value) @classmethod def changed(cls, user): """The given user instance is ready to be saved""" raise NotImplementedError("Implement in subclass") @classmethod def get_username(cls, user): """Return the username for given user""" raise NotImplementedError("Implement in subclass") @classmethod def user_model(cls): """Return the user model""" raise NotImplementedError("Implement in subclass") @classmethod def username_max_length(cls): """Return the max length for username""" raise NotImplementedError("Implement in subclass") @classmethod def allowed_to_disconnect(cls, user, backend_name, association_id=None): """Return if it's safe to disconnect the social account for the given user""" raise NotImplementedError("Implement in subclass") @classmethod def disconnect(cls, entry): """Disconnect the social account for the given user""" raise NotImplementedError("Implement in subclass") @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. """ raise NotImplementedError("Implement in subclass") @classmethod def create_user(cls, *args, **kwargs): """Create a user instance""" raise NotImplementedError("Implement in subclass") @classmethod def get_user(cls, pk): """Return user instance for given id""" raise NotImplementedError("Implement in subclass") @classmethod def get_users_by_email(cls, email): """Return users instances for given email address""" raise NotImplementedError("Implement in subclass") @classmethod def get_social_auth(cls, provider, uid): """Return UserSocialAuth for given provider and uid""" raise NotImplementedError("Implement in subclass") @classmethod def get_social_auth_for_user(cls, user, provider=None, id=None): """Return all the UserSocialAuth instances for given user""" raise NotImplementedError("Implement in subclass") @classmethod def create_social_auth(cls, user, uid, provider): """Create a UserSocialAuth instance for given user""" raise NotImplementedError("Implement in subclass") class NonceMixin: """One use numbers""" server_url = "" timestamp = 0 salt = "" @classmethod def use(cls, server_url, timestamp, salt): """Create a Nonce instance""" raise NotImplementedError("Implement in subclass") @classmethod def get(cls, server_url, salt): """Retrieve a Nonce instance""" raise NotImplementedError("Implement in subclass") @classmethod def delete(cls, nonce): """Delete a Nonce instance""" raise NotImplementedError("Implement in subclass") class AssociationMixin: """OpenId account association""" server_url = "" handle = "" secret = "" issued = 0 lifetime = 0 assoc_type = "" @classmethod def oids(cls, server_url, handle=None): kwargs = {"server_url": server_url} if handle is not None: kwargs["handle"] = handle return sorted( ((assoc.id, cls.openid_association(assoc)) for assoc in cls.get(**kwargs)), key=lambda x: x[1].issued, reverse=True, ) @classmethod def openid_association(cls, assoc): secret = assoc.secret if not isinstance(secret, bytes): secret = secret.encode() return OpenIdAssociation( assoc.handle, base64.decodebytes(secret), assoc.issued, assoc.lifetime, assoc.assoc_type, ) @classmethod def store(cls, server_url, association): """Create an Association instance""" raise NotImplementedError("Implement in subclass") @classmethod def get(cls, server_url: str | None = None, handle: str | None = None): """Get an Association instance""" raise NotImplementedError("Implement in subclass") @classmethod def remove(cls, ids_to_delete): """Remove an Association instance""" raise NotImplementedError("Implement in subclass") class CodeMixin: email = "" code = "" verified = False @abstractmethod def save(self): ... def verify(self): self.verified = True self.save() @classmethod def generate_code(cls): return uuid.uuid4().hex @classmethod def make_code(cls, email): code = cls() code.email = email code.code = cls.generate_code() code.verified = False code.save() return code @classmethod def get_code(cls, code): raise NotImplementedError("Implement in subclass") class PartialMixin: token = "" data: dict[str, Any] = {} next_step: int backend = "" @property def args(self): return self.data.get("args", []) @args.setter def args(self, value): self.data["args"] = value @property def kwargs(self): return self.data.get("kwargs", {}) @kwargs.setter def kwargs(self, value): self.data["kwargs"] = value def extend_kwargs(self, values): self.data["kwargs"].update(values) @classmethod def generate_token(cls): return uuid.uuid4().hex @classmethod def load(cls, token): raise NotImplementedError("Implement in subclass") @classmethod def destroy(cls, token): raise NotImplementedError("Implement in subclass") @classmethod def prepare(cls, backend, next_step: int, data: dict[str, Any]): partial = cls() partial.backend = backend partial.next_step = next_step partial.data = data partial.token = cls.generate_token() return partial @classmethod def store(cls, partial): partial.save() return partial class BaseStorage: user = UserMixin nonce = NonceMixin association = AssociationMixin code = CodeMixin partial = PartialMixin @classmethod def is_integrity_error(cls, exception): """Check if given exception flags an integrity error in the DB""" raise NotImplementedError("Implement in subclass") social-auth-core-4.6.1/social_core/store.py000066400000000000000000000050241500362547200206560ustar00rootroot00000000000000import pickle import time from openid.store.interface import OpenIDStore as BaseOpenIDStore from openid.store.nonce import SKEW class OpenIdStore(BaseOpenIDStore): """Storage class""" def __init__(self, strategy): """Init method""" super().__init__() self.strategy = strategy self.storage = strategy.storage self.assoc = self.storage.association self.nonce = self.storage.nonce self.max_nonce_age = 6 * 60 * 60 # Six hours def storeAssociation(self, server_url, association): """Store new association if it does not exist""" self.assoc.store(server_url, association) def removeAssociation(self, server_url, handle): """Remove association""" associations_ids = list(dict(self.assoc.oids(server_url, handle)).keys()) if associations_ids: self.assoc.remove(associations_ids) def expiresIn(self, assoc): if hasattr(assoc, "getExpiresIn"): return assoc.getExpiresIn() # python3-openid 3.0.2 return assoc.expiresIn def getAssociation(self, server_url, handle=None): """Return stored association""" associations, expired = [], [] for assoc_id, association in self.assoc.oids(server_url, handle): expires = self.expiresIn(association) if expires > 0: associations.append(association) elif expires == 0: expired.append(assoc_id) if expired: # clear expired associations self.assoc.remove(expired) if associations: # return most recet association return associations[0] return None def useNonce(self, server_url, timestamp, salt): """Generate one use number and return *if* it was created""" if abs(timestamp - time.time()) > SKEW: return False return self.nonce.use(server_url, timestamp, salt) class OpenIdSessionWrapper(dict): pickle_instances = ( "_yadis_services__openid_consumer_", "_openid_consumer_last_token", ) def __getitem__(self, name): value = super().__getitem__(name) if name in self.pickle_instances: value = pickle.loads(value) return value def __setitem__(self, name, value): if name in self.pickle_instances: value = pickle.dumps(value, 0) super().__setitem__(name, value) def get(self, name, default=None): try: return self[name] except KeyError: return default social-auth-core-4.6.1/social_core/strategy.py000066400000000000000000000205171500362547200213700ustar00rootroot00000000000000from __future__ import annotations import secrets from typing import TYPE_CHECKING from .backends.utils import get_backend from .pipeline import DEFAULT_AUTH_PIPELINE, DEFAULT_DISCONNECT_PIPELINE from .pipeline.utils import partial_load, partial_prepare, partial_store from .store import OpenIdSessionWrapper, OpenIdStore from .utils import PARTIAL_TOKEN_SESSION_NAME, module_member, setting_name if TYPE_CHECKING: from .backends.base import BaseAuth class BaseTemplateStrategy: def __init__(self, strategy): self.strategy = strategy def render(self, tpl=None, html=None, context=None): if not tpl and not html: raise ValueError("Missing template or html parameters") context = context or {} if tpl: return self.render_template(tpl, context) return self.render_string(html, context) def render_template(self, tpl, context): raise NotImplementedError("Implement in subclass") def render_string(self, html, context): raise NotImplementedError("Implement in subclass") class BaseStrategy: ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" DEFAULT_TEMPLATE_STRATEGY = BaseTemplateStrategy def __init__(self, storage=None, tpl=None): self.storage = storage self.tpl = (tpl or self.DEFAULT_TEMPLATE_STRATEGY)(self) def setting(self, name: str, default=None, backend: BaseAuth | None = None): names = [setting_name(name), name] if backend: names.insert(0, setting_name(backend.name, name)) for value in names: try: return self.get_setting(value) except (AttributeError, KeyError): pass return default def create_user(self, *args, **kwargs): return self.storage.user.create_user(*args, **kwargs) def get_user(self, *args, **kwargs): return self.storage.user.get_user(*args, **kwargs) def session_setdefault(self, name, value): self.session_set(name, value) return self.session_get(name) def openid_session_dict(self, name): # Many frameworks are switching the session serialization from Pickle # to JSON to avoid code execution risks. Flask did this from Flask # 0.10, Django is switching to JSON by default from version 1.6. # # Sadly python-openid stores classes instances in the session which # fails the JSON serialization, the classes are: # # openid.yadis.manager.YadisServiceManager # openid.consumer.discover.OpenIDServiceEndpoint # # This method will return a wrapper over the session value used with # openid (a dict) which will automatically keep a pickled value for the # mentioned classes. return OpenIdSessionWrapper(self.session_setdefault(name, {})) def to_session_value(self, val): return val def from_session_value(self, val): return val def partial_save(self, next_step, backend: BaseAuth, *args, **kwargs): return partial_store(self, backend, next_step, *args, **kwargs) def partial_prepare(self, next_step, backend: BaseAuth, *args, **kwargs): return partial_prepare(self, backend, next_step, *args, **kwargs) def partial_load(self, token): return partial_load(self, token) def clean_partial_pipeline(self, token): self.storage.partial.destroy(token) current_token_in_session = self.session_get(PARTIAL_TOKEN_SESSION_NAME) if current_token_in_session == token: self.session_pop(PARTIAL_TOKEN_SESSION_NAME) def openid_store(self): return OpenIdStore(self) def get_pipeline(self, backend: BaseAuth | None = None): return self.setting("PIPELINE", DEFAULT_AUTH_PIPELINE, backend) def get_disconnect_pipeline(self, backend: BaseAuth | None = None): return self.setting("DISCONNECT_PIPELINE", DEFAULT_DISCONNECT_PIPELINE, backend) def random_string(self, length: int = 12, chars: str = ALLOWED_CHARS) -> str: return "".join([secrets.choice(chars) for i in range(length)]) def absolute_uri(self, path=None): uri = self.build_absolute_uri(path) if uri and self.setting("REDIRECT_IS_HTTPS"): uri = uri.replace("http://", "https://") return uri def get_language(self): """Return current language""" return "" def send_email_validation( self, backend: BaseAuth, email: str, partial_token: str | None = None ) -> str: email_validation = self.setting("EMAIL_VALIDATION_FUNCTION") send_email = module_member(email_validation) code = self.storage.code.make_code(email) send_email(self, backend, code, partial_token) return code def validate_email(self, email: str, code: str) -> bool: verification_code = self.storage.code.get_code(code) if not verification_code or verification_code.code != code: return False if verification_code.email != email: return False if verification_code.verified: return False verification_code.verify() return True def render_html(self, tpl=None, html=None, context=None): """Render given template or raw html with given context""" return self.tpl.render(tpl, html, context) def authenticate(self, backend: BaseAuth, *args, **kwargs): """Trigger the authentication mechanism tied to the current framework""" kwargs["strategy"] = self kwargs["storage"] = self.storage kwargs["backend"] = backend args, kwargs = self.clean_authenticate_args(*args, **kwargs) return backend.authenticate(*args, **kwargs) def clean_authenticate_args(self, *args, **kwargs): """Take authenticate arguments and return a "cleaned" version of them""" return args, kwargs def get_backends(self): """Return configured backends""" return self.setting("AUTHENTICATION_BACKENDS", []) def get_backend_class(self, name): """Return a configured backend class""" return get_backend(self.get_backends(), name) def get_backend(self, name, redirect_uri=None, *args, **kwargs): """Return a configured backend instance""" Backend = self.get_backend_class(name) return Backend(self, redirect_uri=redirect_uri, *args, **kwargs) # Implement the following methods on strategies sub-classes def redirect(self, url): """Return a response redirect to the given URL""" raise NotImplementedError("Implement in subclass") def get_setting(self, name): """Return value for given setting name""" raise NotImplementedError("Implement in subclass") def html(self, content): """Return HTTP response with given content""" raise NotImplementedError("Implement in subclass") def request_data(self, merge=True): """Return current request data (POST or GET)""" raise NotImplementedError("Implement in subclass") def request_host(self): """Return current host value""" raise NotImplementedError("Implement in subclass") def session_get(self, name, default=None): """Return session value for given key""" raise NotImplementedError("Implement in subclass") def session_set(self, name, value): """Set session value for given key""" raise NotImplementedError("Implement in subclass") def session_pop(self, name): """Pop session value for given key""" raise NotImplementedError("Implement in subclass") def build_absolute_uri(self, path=None): """Build absolute URI with given (optional) path""" raise NotImplementedError("Implement in subclass") def request_is_secure(self): """Is the request using HTTPS?""" raise NotImplementedError("Implement in subclass") def request_path(self): """path of the current request""" raise NotImplementedError("Implement in subclass") def request_port(self): """Port in use for this request""" raise NotImplementedError("Implement in subclass") def request_get(self): """Request GET data""" raise NotImplementedError("Implement in subclass") def request_post(self): """Request POST data""" raise NotImplementedError("Implement in subclass") social-auth-core-4.6.1/social_core/tests/000077500000000000000000000000001500362547200203115ustar00rootroot00000000000000social-auth-core-4.6.1/social_core/tests/__init__.py000066400000000000000000000000001500362547200224100ustar00rootroot00000000000000social-auth-core-4.6.1/social_core/tests/actions/000077500000000000000000000000001500362547200217515ustar00rootroot00000000000000social-auth-core-4.6.1/social_core/tests/actions/__init__.py000066400000000000000000000000001500362547200240500ustar00rootroot00000000000000social-auth-core-4.6.1/social_core/tests/actions/actions.py000066400000000000000000000215111500362547200237630ustar00rootroot00000000000000import json import unittest from urllib.parse import urlparse import requests import responses from ...actions import do_auth, do_complete from ...backends.oauth import BaseOAuth2 from ...utils import module_member, parse_qs from ..models import TestAssociation, TestNonce, TestStorage, TestUserSocialAuth, User from ..strategy import TestStrategy class BaseActionTest(unittest.TestCase): user_data_url = "https://api.github.com/user" login_redirect_url = "/success" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "login": "foobar", "id": 1, "avatar_url": "https://github.com/images/error/foobar_happy.gif", "gravatar_id": "somehexcode", "url": "https://api.github.com/users/foobar", "name": "monalisa foobar", "company": "GitHub", "blog": "https://github.com/blog", "location": "San Francisco", "email": "foo@bar.com", "hireable": False, "bio": "There once was...", "public_repos": 2, "public_gists": 1, "followers": 20, "following": 0, "html_url": "https://github.com/foobar", "created_at": "2008-01-14T04:33:35Z", "type": "User", "total_private_repos": 100, "owned_private_repos": 100, "private_gists": 81, "disk_usage": 10000, "collaborators": 8, "plan": { "name": "Medium", "space": 400, "collaborators": 10, "private_repos": 20, }, } ) strategy: TestStrategy backend: BaseOAuth2 def setUp(self): responses.start() User.reset_cache() TestUserSocialAuth.reset_cache() TestNonce.reset_cache() TestAssociation.reset_cache() Backend = module_member("social_core.backends.github.GithubOAuth2") if not hasattr(self, "strategy"): self.strategy = TestStrategy(TestStorage) if not hasattr(self, "backend"): self.backend = Backend(self.strategy, redirect_uri="/complete/github") self.user = None def tearDown(self): del self.backend del self.strategy self.user = None User.reset_cache() User.set_active(True) TestUserSocialAuth.reset_cache() TestNonce.reset_cache() TestAssociation.reset_cache() responses.stop() responses.reset() def do_login( self, after_complete_checks=True, user_data_body=None, expected_username=None ): self.strategy.set_settings( { "SOCIAL_AUTH_GITHUB_KEY": "a-key", "SOCIAL_AUTH_GITHUB_SECRET": "a-secret-key", "SOCIAL_AUTH_LOGIN_REDIRECT_URL": self.login_redirect_url, "SOCIAL_AUTH_AUTHENTICATION_BACKENDS": ( "social_core.backends.github.GithubOAuth2", ), } ) start_url = do_auth(self.backend).url target_url = self.strategy.build_absolute_uri("/complete/github/?code=foobar") start_query = parse_qs(urlparse(start_url).query) location_url = ( target_url + ("&" if "?" in target_url else "?") + "state=" + start_query["state"] ) location_query = parse_qs(urlparse(location_url).query) responses.add( responses.GET, start_url, status=301, headers={"Location": location_url}, ) responses.add(responses.GET, location_url, status=200, body="foobar") response = requests.get(start_url, timeout=1) self.assertEqual(response.url, location_url) self.assertEqual(response.text, "foobar") responses.add( responses.POST, url=self.backend.ACCESS_TOKEN_URL, status=200, body=self.access_token_body or "", content_type="text/json", ) if self.user_data_url: user_data_body = user_data_body or self.user_data_body or "" responses.add( responses.GET, self.user_data_url, body=user_data_body, content_type="text/json", ) self.strategy.set_request_data(location_query, self.backend) def _login(backend, user, social_user): backend.strategy.session_set("username", user.username) user_email = getattr(user, "email", None) if user_email: backend.strategy.session_set("email", user_email) redirect = do_complete(self.backend, user=self.user, login=_login) if after_complete_checks: self.assertEqual( self.strategy.session_get("username"), expected_username or self.expected_username, ) self.assertEqual(redirect.url, self.login_redirect_url) return redirect def do_login_with_partial_pipeline(self, before_complete=None): self.strategy.set_settings( { "SOCIAL_AUTH_GITHUB_KEY": "a-key", "SOCIAL_AUTH_GITHUB_SECRET": "a-secret-key", "SOCIAL_AUTH_LOGIN_REDIRECT_URL": self.login_redirect_url, "SOCIAL_AUTH_AUTHENTICATION_BACKENDS": ( "social_core.backends.github.GithubOAuth2", ), "SOCIAL_AUTH_PIPELINE": ( "social_core.pipeline.social_auth.social_details", "social_core.pipeline.social_auth.social_uid", "social_core.pipeline.social_auth.auth_allowed", "social_core.tests.pipeline.ask_for_password", "social_core.pipeline.social_auth.social_user", "social_core.pipeline.user.get_username", "social_core.pipeline.user.create_user", "social_core.pipeline.social_auth.associate_user", "social_core.pipeline.social_auth.load_extra_data", "social_core.tests.pipeline.set_password", "social_core.pipeline.user.user_details", ), } ) start_url = do_auth(self.backend).url target_url = self.strategy.build_absolute_uri("/complete/github/?code=foobar") start_query = parse_qs(urlparse(start_url).query) location_url = ( target_url + ("&" if "?" in target_url else "?") + "state=" + start_query["state"] ) location_query = parse_qs(urlparse(location_url).query) responses.add( responses.GET, start_url, status=301, headers={"Location": location_url}, ) responses.add(responses.GET, location_url, status=200, body="foobar") response = requests.get(start_url, timeout=1) self.assertEqual(response.url, location_url) self.assertEqual(response.text, "foobar") responses.add( responses.POST, url=self.backend.ACCESS_TOKEN_URL, status=200, body=self.access_token_body or "", content_type="text/json", ) if self.user_data_url: responses.add( responses.GET, self.user_data_url, body=self.user_data_body or "", content_type="text/json", ) self.strategy.set_request_data(location_query, self.backend) def _login(backend, user, social_user): backend.strategy.session_set("username", user.username) user_email = getattr(user, "email", None) if user_email: backend.strategy.session_set("email", user_email) redirect = do_complete(self.backend, user=self.user, login=_login) url = self.strategy.build_absolute_uri("/password") self.assertEqual(redirect.url, url) responses.add(responses.GET, redirect.url, status=200, body="foobar") responses.add(responses.POST, redirect.url, status=200) password = "foobar" requests.get(url, timeout=1) requests.post(url, data={"password": password}, timeout=1) data = parse_qs(responses.calls[-1].request.body) self.assertEqual(data["password"], password) self.strategy.session_set("password", data["password"]) if before_complete: before_complete() redirect = do_complete(self.backend, user=self.user, login=_login) self.assertEqual(self.strategy.session_get("username"), self.expected_username) self.assertEqual(redirect.url, self.login_redirect_url) def _logout(self, backend): backend.strategy.session_set("username", None) social-auth-core-4.6.1/social_core/tests/actions/test_associate.py000066400000000000000000000055541500362547200253460ustar00rootroot00000000000000import json from ...exceptions import AuthAlreadyAssociated from ..models import User from .actions import BaseActionTest class AssociateActionTest(BaseActionTest): expected_username = "foobar" def setUp(self): super().setUp() self.user = User(username="foobar", email="foo@bar.com") self.backend.strategy.session_set("username", self.user.username) def test_associate(self): self.do_login() self.assertTrue(len(self.user.social), 1) self.assertEqual(self.user.social[0].provider, "github") def test_associate_with_partial_pipeline(self): self.do_login_with_partial_pipeline() self.assertEqual(len(self.user.social), 1) self.assertEqual(self.user.social[0].provider, "github") class MultipleAccountsTest(AssociateActionTest): alternative_user_data_body = json.dumps( { "login": "foobar2", "id": 2, "avatar_url": "https://github.com/images/error/foobar2_happy.gif", "gravatar_id": "somehexcode", "url": "https://api.github.com/users/foobar2", "name": "monalisa foobar2", "company": "GitHub", "blog": "https://github.com/blog", "location": "San Francisco", "email": "foo@bar.com", "hireable": False, "bio": "There once was...", "public_repos": 2, "public_gists": 1, "followers": 20, "following": 0, "html_url": "https://github.com/foobar2", "created_at": "2008-01-14T04:33:35Z", "type": "User", "total_private_repos": 100, "owned_private_repos": 100, "private_gists": 81, "disk_usage": 10000, "collaborators": 8, "plan": { "name": "Medium", "space": 400, "collaborators": 10, "private_repos": 20, }, } ) def test_multiple_social_accounts(self): self.do_login() self.do_login(user_data_body=self.alternative_user_data_body) self.assertEqual(len(self.user.social), 2) self.assertEqual(self.user.social[0].provider, "github") self.assertEqual(self.user.social[1].provider, "github") class AlreadyAssociatedErrorTest(BaseActionTest): def setUp(self): super().setUp() self.user1 = User(username="foobar", email="foo@bar.com") self.user = None def tearDown(self): super().tearDown() self.user1 = None self.user = None def test_already_associated_error(self): self.user = self.user1 self.do_login() self.user = User(username="foobar2", email="foo2@bar2.com") with self.assertRaisesRegex( AuthAlreadyAssociated, "This account is already in use." ): self.do_login() social-auth-core-4.6.1/social_core/tests/actions/test_disconnect.py000066400000000000000000000051631500362547200255200ustar00rootroot00000000000000import requests import responses from ...actions import do_disconnect from ...exceptions import NotAllowedToDisconnect from ...utils import parse_qs from ..models import TestUserSocialAuth, User from .actions import BaseActionTest class DisconnectActionTest(BaseActionTest): def test_not_allowed_to_disconnect(self): self.do_login() user = User.get(self.expected_username) with self.assertRaisesRegex( NotAllowedToDisconnect, "This account is not allowed to be disconnected." ): do_disconnect(self.backend, user) def test_disconnect(self): self.do_login() user = User.get(self.expected_username) user.password = "password" do_disconnect(self.backend, user) self.assertEqual(len(user.social), 0) def test_disconnect_with_association_id(self): self.do_login() user = User.get(self.expected_username) user.password = "password" association_id = user.social[0].id second_usa = TestUserSocialAuth(user, user.social[0].provider, "uid2") self.assertEqual(len(user.social), 2) do_disconnect(self.backend, user, association_id) self.assertEqual(len(user.social), 1) self.assertEqual(user.social[0], second_usa) def test_disconnect_with_partial_pipeline(self): self.strategy.set_settings( { "SOCIAL_AUTH_DISCONNECT_PIPELINE": ( "social_core.tests.pipeline.ask_for_password", "social_core.tests.pipeline.set_password", "social_core.pipeline.disconnect.allowed_to_disconnect", "social_core.pipeline.disconnect.get_entries", "social_core.pipeline.disconnect.revoke_tokens", "social_core.pipeline.disconnect.disconnect", ) } ) self.do_login() user = User.get(self.expected_username) redirect = do_disconnect(self.backend, user) url = self.strategy.build_absolute_uri("/password") self.assertEqual(redirect.url, url) responses.add(responses.GET, redirect.url, status=200, body="foobar") responses.add(responses.POST, redirect.url, status=200) password = "foobar" requests.get(url, timeout=1) requests.post(url, data={"password": password}, timeout=1) data = parse_qs(responses.calls[-1].request.body) self.assertEqual(data["password"], password) self.strategy.session_set("password", data["password"]) redirect = do_disconnect(self.backend, user) self.assertEqual(len(user.social), 0) social-auth-core-4.6.1/social_core/tests/actions/test_login.py000066400000000000000000000117371500362547200245030ustar00rootroot00000000000000from ...backends.oauth import BaseOAuth2 from ...utils import PARTIAL_TOKEN_SESSION_NAME from ..models import TestUserSocialAuth, User from .actions import BaseActionTest class BackendThatControlsRedirect(BaseOAuth2): """ A fake backend that sets the URL to redirect to after login. It is not always possible to set the redirect URL in the session state prior to auth and then retrieve it when auth is complete, because the session cookie might not be available post-auth. For example, for SAML, a POST request redirects the user from the IdP (Identity Provider) back to the SP (Service Provider) to complete the auth process, but the session cookie will not be present if the session cookie's `SameSite` attribute is not set to "None". To mitigate this, SAML provides a `RelayState` parameter to pass data like a redirect URL from the SP to the IdP and back again. In that case, the redirect URL is only known in `auth_complete`, and must be communicated back to the `do_complete` action via session state so that it can issue the intended redirect. """ ACCESS_TOKEN_URL = "https://example.com/oauth/access_token" def auth_url(self): return "https://example.com/oauth/auth?state=foo" def auth_complete(self, *args, **kwargs): # Put the redirect URL in the session state, as this is where the `do_complete` action looks for it. self.strategy.session_set(kwargs["redirect_name"], "/after-login") return kwargs["user"] class LoginActionTest(BaseActionTest): def test_login(self): self.do_login() def test_login_with_partial_pipeline(self): self.do_login_with_partial_pipeline() def test_fields_stored_in_session(self): self.strategy.set_settings( {"SOCIAL_AUTH_FIELDS_STORED_IN_SESSION": ["foo", "bar"]} ) self.strategy.set_request_data({"foo": "1", "bar": "2"}, self.backend) self.do_login() self.assertEqual(self.strategy.session_get("foo"), "1") self.assertEqual(self.strategy.session_get("bar"), "2") self._logout(self.backend) # The _logout helper function doesn't clear the session. self.assertEqual(self.strategy.session_get("foo"), "1") self.assertEqual(self.strategy.session_get("bar"), "2") # Login again - without the 'bar' request param and make # sure its value didn't persist in the session. self.strategy.remove_from_request_data("bar") self.strategy.set_request_data({"foo": "3"}, self.backend) self.do_login() self.assertEqual(self.strategy.session_get("foo"), "3") self.assertEqual(self.strategy.session_get("bar"), None) def test_redirect_value(self): self.strategy.set_request_data({"next": "/after-login"}, self.backend) redirect = self.do_login(after_complete_checks=False) self.assertEqual(redirect.url, "/after-login") def test_redirect_value_set_by_backend(self): self.backend = BackendThatControlsRedirect(self.strategy) self.user = TestUserSocialAuth.create_user("test-user") redirect = self.do_login(after_complete_checks=False) self.assertEqual(redirect.url, "/after-login") def test_login_with_invalid_partial_pipeline(self): def before_complete(): partial_token = self.strategy.session_get(PARTIAL_TOKEN_SESSION_NAME) partial = self.strategy.storage.partial.load(partial_token) partial.data["backend"] = "foobar" self.do_login_with_partial_pipeline(before_complete) def test_new_user(self): self.strategy.set_settings({"SOCIAL_AUTH_NEW_USER_REDIRECT_URL": "/new-user"}) redirect = self.do_login(after_complete_checks=False) self.assertEqual(redirect.url, "/new-user") def test_inactive_user(self): self.strategy.set_settings({"SOCIAL_AUTH_INACTIVE_USER_URL": "/inactive"}) User.set_active(False) redirect = self.do_login(after_complete_checks=False) self.assertEqual(redirect.url, "/inactive") def test_invalid_user(self): self.strategy.set_settings( { "SOCIAL_AUTH_LOGIN_ERROR_URL": "/error", "SOCIAL_AUTH_PIPELINE": ( "social_core.pipeline.social_auth.social_details", "social_core.pipeline.social_auth.social_uid", "social_core.pipeline.social_auth.auth_allowed", "social_core.pipeline.social_auth.social_user", "social_core.pipeline.user.get_username", "social_core.pipeline.user.create_user", "social_core.pipeline.social_auth.associate_user", "social_core.pipeline.social_auth.load_extra_data", "social_core.pipeline.user.user_details", "social_core.tests.pipeline.remove_user", ), } ) redirect = self.do_login(after_complete_checks=False) self.assertEqual(redirect.url, "/error") social-auth-core-4.6.1/social_core/tests/backends/000077500000000000000000000000001500362547200220635ustar00rootroot00000000000000social-auth-core-4.6.1/social_core/tests/backends/__init__.py000066400000000000000000000000001500362547200241620ustar00rootroot00000000000000social-auth-core-4.6.1/social_core/tests/backends/base.py000066400000000000000000000142721500362547200233550ustar00rootroot00000000000000from __future__ import annotations import unittest from typing import Generic, TypeVar import requests import responses from ...backends.base import BaseAuth from ...backends.utils import load_backends, user_backends_data from ...utils import PARTIAL_TOKEN_SESSION_NAME, module_member, parse_qs from ..models import ( TestAssociation, TestCode, TestNonce, TestStorage, TestUserSocialAuth, User, ) from ..strategy import TestStrategy BackendT = TypeVar("BackendT", bound=BaseAuth) class BaseBackendTest(unittest.TestCase, Generic[BackendT]): backend: BackendT backend_path: str = "" name: str = "" complete_url = "" raw_complete_url = "/complete/{0}" expected_username: str = "" def setUp(self): responses.start() Backend = module_member(self.backend_path) self.strategy = TestStrategy(TestStorage) self.backend = Backend(self.strategy, redirect_uri=self.complete_url) self.name = self.backend.name.upper().replace("-", "_") self.complete_url = self.strategy.build_absolute_uri( self.raw_complete_url.format(self.backend.name) ) backends = ( self.backend_path, "social_core.tests.backends.test_broken.BrokenBackendAuth", ) self.strategy.set_settings({"SOCIAL_AUTH_AUTHENTICATION_BACKENDS": backends}) self.strategy.set_settings(self.extra_settings()) # Force backends loading to trash PSA cache load_backends(backends, force_load=True) User.reset_cache() TestUserSocialAuth.reset_cache() TestNonce.reset_cache() TestAssociation.reset_cache() TestCode.reset_cache() def tearDown(self): del self.backend self.strategy = None self.name = "" self.complete_url = None User.reset_cache() TestUserSocialAuth.reset_cache() TestNonce.reset_cache() TestAssociation.reset_cache() TestCode.reset_cache() responses.stop() responses.reset() def extra_settings(self) -> dict[str, str]: return {} def do_start(self): raise NotImplementedError("Implement in subclass") def do_login(self): user = self.do_start() username = self.expected_username self.assertEqual(user.username, username) self.assertEqual(self.strategy.session_get("username"), username) self.assertEqual(self.strategy.get_user(user.id), user) self.assertEqual(self.backend.get_user(user.id), user) user_backends = user_backends_data( user, self.strategy.get_setting("SOCIAL_AUTH_AUTHENTICATION_BACKENDS"), self.strategy.storage, ) self.assertEqual(len(list(user_backends.keys())), 3) self.assertEqual("associated" in user_backends, True) self.assertEqual("not_associated" in user_backends, True) self.assertEqual("backends" in user_backends, True) self.assertEqual(len(user_backends["associated"]), 1) self.assertEqual(len(user_backends["not_associated"]), 1) self.assertEqual(len(user_backends["backends"]), 2) return user def pipeline_settings(self): self.strategy.set_settings( { "SOCIAL_AUTH_PIPELINE": ( "social_core.pipeline.social_auth.social_details", "social_core.pipeline.social_auth.social_uid", "social_core.pipeline.social_auth.auth_allowed", "social_core.tests.pipeline.ask_for_password", "social_core.tests.pipeline.ask_for_slug", "social_core.pipeline.social_auth.social_user", "social_core.pipeline.user.get_username", "social_core.pipeline.social_auth.associate_by_email", "social_core.pipeline.user.create_user", "social_core.pipeline.social_auth.associate_user", "social_core.pipeline.social_auth.load_extra_data", "social_core.tests.pipeline.set_password", "social_core.tests.pipeline.set_slug", "social_core.pipeline.user.user_details", ) } ) def pipeline_handlers(self, url): responses.add(responses.GET, url, status=200, body="foobar") responses.add(responses.POST, url, status=200) def pipeline_password_handling(self, url): password = "foobar" requests.get(url, timeout=1) requests.post(url, data={"password": password}, timeout=1) data = parse_qs(responses.calls[-1].request.body) self.assertEqual(data["password"], password) self.strategy.session_set("password", data["password"]) return password def pipeline_slug_handling(self, url): slug = "foo-bar" requests.get(url, timeout=1) requests.post(url, data={"slug": slug}, timeout=1) data = parse_qs(responses.calls[-1].request.body) self.assertEqual(data["slug"], slug) self.strategy.session_set("slug", data["slug"]) return slug def do_partial_pipeline(self): url = self.strategy.build_absolute_uri("/password") self.pipeline_settings() redirect = self.do_start() self.assertEqual(redirect.url, url) self.pipeline_handlers(url) password = self.pipeline_password_handling(url) token = self.strategy.session_pop(PARTIAL_TOKEN_SESSION_NAME) partial = self.strategy.partial_load(token) self.assertEqual(partial.backend, self.backend.name) redirect = self.backend.continue_pipeline(partial) url = self.strategy.build_absolute_uri("/slug") self.assertEqual(redirect.url, url) self.pipeline_handlers(url) slug = self.pipeline_slug_handling(url) token = self.strategy.session_pop(PARTIAL_TOKEN_SESSION_NAME) partial = self.strategy.partial_load(token) self.assertEqual(partial.backend, self.backend.name) user = self.backend.continue_pipeline(partial) self.assertEqual(user.username, self.expected_username) self.assertEqual(user.slug, slug) self.assertEqual(user.password, password) return user social-auth-core-4.6.1/social_core/tests/backends/data/000077500000000000000000000000001500362547200227745ustar00rootroot00000000000000social-auth-core-4.6.1/social_core/tests/backends/data/saml_config.json000066400000000000000000000077551500362547200261660ustar00rootroot00000000000000{ "SOCIAL_AUTH_SAML_SP_ENTITY_ID": "https://github.com/omab/python-social-auth/saml-test", "SOCIAL_AUTH_SAML_SP_PUBLIC_CERT": "MIICsDCCAhmgAwIBAgIJAO7BwdjDZcUWMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNBMRkwFwYDVQQIExBCcml0aXNoIENvbHVtYmlhMRswGQYDVQQKExJweXRob24tc29jaWFsLWF1dGgwHhcNMTUwNTA4MDc1ODQ2WhcNMjUwNTA3MDc1ODQ2WjBFMQswCQYDVQQGEwJDQTEZMBcGA1UECBMQQnJpdGlzaCBDb2x1bWJpYTEbMBkGA1UEChMScHl0aG9uLXNvY2lhbC1hdXRoMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCq3g1Cl+3uR5vCnN4HbgjTg+m3nHhteEMyb++ycZYre2bxUfsshER6x33l23tHckRYwm7MdBbrp3LrVoiOCdPblTml1IhEPTCwKMhBKvvWqTvgfcSSnRzAWkLlQYSusayyZK4n9qcYkV5MFni1rbjx+Mr5aOEmb5u33amMKLwSTwIDAQABo4GnMIGkMB0GA1UdDgQWBBRRiBR6zS66fKVokp0yJHbgv3RYmjB1BgNVHSMEbjBsgBRRiBR6zS66fKVokp0yJHbgv3RYmqFJpEcwRTELMAkGA1UEBhMCQ0ExGTAXBgNVBAgTEEJyaXRpc2ggQ29sdW1iaWExGzAZBgNVBAoTEnB5dGhvbi1zb2NpYWwtYXV0aIIJAO7BwdjDZcUWMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJwsMU3YSaybVjuJ8US0fUhlPOlM40QFCGL4vB3TEbb24Mq8HrjUwrU0JFPGls9a2OYzN2B3e35NorMuxs+grGtr2yP6LvuX+nV6A93wb4ooGHoGfC7VLlyxSSns937SS5R1pzQ4gWzZma2KGWKICWph5zQ0ARVhL63967mGLmoI=", "SOCIAL_AUTH_SAML_SP_PRIVATE_KEY": "MIICXgIBAAKBgQCq3g1Cl+3uR5vCnN4HbgjTg+m3nHhteEMyb++ycZYre2bxUfsshER6x33l23tHckRYwm7MdBbrp3LrVoiOCdPblTml1IhEPTCwKMhBKvvWqTvgfcSSnRzAWkLlQYSusayyZK4n9qcYkV5MFni1rbjx+Mr5aOEmb5u33amMKLwSTwIDAQABAoGBAIHAg6NJSiYC/NYpVzWfKlasuoNy78R5adXYSNZiCR5V5FNm5OzmODZgXUt6g0A7FomshIT/txQWoV7y5FmwPs8n13JY3Hdt4tJ6MHw2feLo710+OEp9VBQus3JsB2F8ONYrGvs00hPPL7h5av/rzTdE8F67YM1mSgeg7xEF6BghAkEA12OOqSzp2MLTNY7PqOaLDzy4aAMVNN3Ntv2jBN0jq7s1b5ilQ2PGkLwdtkicq/VZcRyUqVbZbMwz05II3nqx3wJBAMsVhRQ5sdFCRBzEbSAm2YEJaFh5u6QT3+zWHMFpPJRnaBAWz3RXKEnleJ+DS2Xz1Jm6ZrmLdZiwMx/8dK5rDZECQQC7GTdWi7ZC3dIcpwaKIGHRhZxmda8ZMkc9Wwwd8H7I8aFUZFPCu0xEc7SXoHHACit8zyfwBYpvMN8gPK3JnOkfAkEAsUSpk0wBMT38one7IZOHzCDgGkq4RbKrhdon45Pus0PIDDM9BrqFimtpbSN4DxhVfZK91DwtfAhhuAvv9cewYQJAPMhpAqv3PBGYmtRDUlWXJQv2JRJJkrvbbqgBed2OX5RRgj5V3SR6PBhLbcTZ+q+1tdPkMFzZo5U6MN5m/6oXvQ==", "SOCIAL_AUTH_SAML_ORG_INFO": { "en-US": { "name": "psa", "displayname": "PSA", "url": "https://github.com/omab/python-social-auth/" } }, "SOCIAL_AUTH_SAML_TECHNICAL_CONTACT": { "givenName": "Tech Gal", "emailAddress": "technical@example.com" }, "SOCIAL_AUTH_SAML_SUPPORT_CONTACT": { "givenName": "Support Guy", "emailAddress": "support@example.com" }, "SOCIAL_AUTH_SAML_ENABLED_IDPS": { "testshib": { "entity_id": "https://idp.testshib.org/idp/shibboleth", "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO", "x509cert": "MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYDVQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQIEwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRlc3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7CyVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aTNPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWHgWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0GA1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ869nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBlbm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNoaWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRLI4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4/SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAjGeka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA==" }, "other": { "entity_id": "https://unused.saml.example.com", "url": "https://unused.saml.example.com/SAML2/Redirect/SSO" } } } social-auth-core-4.6.1/social_core/tests/backends/data/saml_response.txt000066400000000000000000000422251500362547200264140ustar00rootroot00000000000000http://myapp.com/?RelayState=%7b%22idp%22%3a+%22testshib%22%2c+%22next%22%3a+%22%2ffoo%2fbar%22%7d&SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL215YXBwLmNvbSIgSUQ9Il8yNTk2NTFlOTY3ZGIwOGZjYTQ4MjdkODI3YWY1M2RkMCIgSW5SZXNwb25zZVRvPSJURVNUX0lEIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDUtMDlUMDM6NTc6NDMuNzkyWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI%2BaHR0cHM6Ly9pZHAudGVzdHNoaWIub3JnL2lkcC9zaGliYm9sZXRoPC9zYW1sMjpJc3N1ZXI%2BPHNhbWwycDpTdGF0dXM%2BPHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM%2BPHNhbWwyOkVuY3J5cHRlZEFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIElkPSJfMGM0NzYzNzIyOWFkNmEzMTY1OGU0MDc2ZDNlYzBmNmQiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiLz48ZHM6S2V5SW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI%2BPHhlbmM6RW5jcnlwdGVkS2V5IElkPSJfYjZmNmU2YWZjMzYyNGI3NmM1N2JmOWZhODA5YzAzNmMiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDc0RDQ0FobWdBd0lCQWdJSkFPN0J3ZGpEWmNVV01BMEdDU3FHU0liM0RRRUJCUVVBTUVVeEN6QUpCZ05WQkFZVEFrTkJNUmt3CkZ3WURWUVFJRXhCQ2NtbDBhWE5vSUVOdmJIVnRZbWxoTVJzd0dRWURWUVFLRXhKd2VYUm9iMjR0YzI5amFXRnNMV0YxZEdnd0hoY04KTVRVd05UQTRNRGMxT0RRMldoY05NalV3TlRBM01EYzFPRFEyV2pCRk1Rc3dDUVlEVlFRR0V3SkRRVEVaTUJjR0ExVUVDQk1RUW5KcApkR2x6YUNCRGIyeDFiV0pwWVRFYk1Ca0dBMVVFQ2hNU2NIbDBhRzl1TFhOdlkybGhiQzFoZFhSb01JR2ZNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNEdOQURDQmlRS0JnUUNxM2cxQ2wrM3VSNXZDbk40SGJnalRnK20zbkhodGVFTXliKyt5Y1pZcmUyYnhVZnNzaEVSNngzM2wKMjN0SGNrUll3bTdNZEJicnAzTHJWb2lPQ2RQYmxUbWwxSWhFUFRDd0tNaEJLdnZXcVR2Z2ZjU1NuUnpBV2tMbFFZU3VzYXl5Wks0bgo5cWNZa1Y1TUZuaTFyYmp4K01yNWFPRW1iNXUzM2FtTUtMd1NUd0lEQVFBQm80R25NSUdrTUIwR0ExVWREZ1FXQkJSUmlCUjZ6UzY2CmZLVm9rcDB5SkhiZ3YzUlltakIxQmdOVkhTTUViakJzZ0JSUmlCUjZ6UzY2ZktWb2twMHlKSGJndjNSWW1xRkpwRWN3UlRFTE1Ba0cKQTFVRUJoTUNRMEV4R1RBWEJnTlZCQWdURUVKeWFYUnBjMmdnUTI5c2RXMWlhV0V4R3pBWkJnTlZCQW9URW5CNWRHaHZiaTF6YjJOcApZV3d0WVhWMGFJSUpBTzdCd2RqRFpjVVdNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ1lFQUp3c01VM1lTCmF5YlZqdUo4VVMwZlVobFBPbE00MFFGQ0dMNHZCM1RFYmIyNE1xOEhyalV3clUwSkZQR2xzOWEyT1l6TjJCM2UzNU5vck11eHMrZ3IKR3RyMnlQNkx2dVgrblY2QTkzd2I0b29HSG9HZkM3VkxseXhTU25zOTM3U1M1UjFwelE0Z1d6Wm1hMktHV0tJQ1dwaDV6UTBBUlZoTAo2Mzk2N21HTG1vST08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BTElQdkVNVUVGeXhrVHowQ2N4QVA5TjV4Y3NYT2V4aVV4cXBvR2VIeVFMV0R5RVBBUDVnZ1daL3NLZ1ViL2xWSk92bCtuQXhSdVhXUlc5dGxSWWx3R2orRVhIOWhIbmdEY1BWMDNqSUJMQnFJbElBL1RmMGw4cVliOHFKRy9ZM0RTS2RQNkwvUURtYXBtTXpFM29YOEJxMW5Ea3YrUWh4cmQwMGVGK2ZMYVQ0PTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BRVpUWDhHTkM0My9yWStTUVlBMXRudHlUTTVVNkN2dUNCaktsVEVlekZPRjBZZHhCWUdFQVVjYU8xNVNKOXBMemJ1L1h0WGxzTkVMZTdKdEx4RUpwYUxubWFENnIranNWczdLaTBLNHRTMGNBUERDWHV2R1FoMmFOVjVQOGJ3N1JWUGhLOGQwYlJ1RklGR09FOHMwTTZYOUpxWDN4S0MvL1lSbVVoeDlybnU3ZWlwMGh5ZitPaUZiVGR2SDY2NTB2LzQ3aVdKcDNZeFlUV0QyMHBNbVRJMUpwWUEwYjByWVFQRkR0RU93d0JxYktxanRJc3ZYVFJzeXJhQkxvbnFOeHN5dHpEWHEra0JsMXp3WGUvSE5QcUVQblczdnNxaFhZcDVGM3dkWThkKzNCOTRMZlpOdUd4a0p3VDNzdVR0OGY5VHRBSlI4VytBUmtzT2M4eDBVaVNsVG5BNHFHOTBLMTR5dkVoVHcvd2drZjFXV01RT3dpZDNpakFYbUV4MU5MbVZvYUxYb3p4VExkTjN6YnJ6VEJIRXc3R2J3ZEdrdU5pMlhZOW16YUgwaWtGRm51VUxjMHUwc0pycEdGdzlaK0VlUk44RzNVUVZ5MjhtS2g3ZFBwWU5KbzhyajIxZFFaK2JaeUtTUHZablU3REkyakdJRE5US1g2ZkVyVWFINGlOTzN4cUU2Vk90L2d4T3BMNE5VNUhLV0Q0bG93VzcwdUJjVEVQRmhwaThpYUovdTB6YzUvTEhvdVBjMzByc1RLZFc5cmJLL2NWaHNQUHErZzA5WHZpZ0QweTJvN2tOc1pVL25tRXFiSzBKOTBrazhCR3I5cXRSczY4bUJnSURtUHVwUkhwWjM4eXNnU2VZN3V0VlVaSG5tQ0dzTzZ2NDJ6OTVOK05Pb3RCTEVZbFd1ZEdzYnowQWc4VkRDSlY5ak95QW95MDZyL1AyUHBsOFhjdmJza2d2T1BMMWdDNnVYbVJJS1lmOEw4UDJCNXVjN0haK0dtUHNOWXRLS2VKRDFFUHovdCt2NlBIbXNVb3dsSDhSd3FMRHdtMUF4dlNLQTR3UXBlQ0dQd3A5YXRYS0lWMS84NUZzRWMzajVzNjd6VlRybThrVEpydXV2MDZEdFVRZDNMOFdwTkV4cWhQait6RUp6U3RxSG04ckhNMVhNQUVxdVozc0xycTVqLzFSNlpqS0dOdFJCbjhwOE5ERGtrWm0vWTV5TXlJNXJJS3U5bnA3bXdaaEVpeWVHeHdxblV3VVMvUzVDRjNnMHVidnd4eVVnalVvd1ZvTkNqYktBbkdtT2VCSW5abkh0eGdIVUhVOUVlTFdyd2pRc3JtUmpJV0R2RkZQa3l6SzJDL20yaitubmNxc2E1OGRLVXZxcGR1VTRJYnNPQng3UGpXdXRBNmY5bXd6YWxyRU1NK0lGR3VPdk9HMC93eUdzQjZLREV6bldjUC83NkQ4angzaHZFSlAzN3REbFgreGM4Qno5TXdKdkd6VG4xbTdCb2xoR0lzSXlCTys1ZXpXa3RDWVVIUURGVE9wbXA0MDlOWHp6ZUNTUGY1U2NDWG5YYjRPd01ULy9VM1JFUnRRbGMrNmU2WG1JRjhoRkJVc0taUUJsS2ppSDkwZHlzYWlsNmN2V3UyQW55Q3QxbWxXcHFLc0MzU2RTRVZDTG1qRjlUQUFUMEtFSGdZQjg3RjZtZUpTTysvOXkyZkRuYVVvUUlUVzdubnVuSCtkT3dWSGZMU0wyL2N5YTltNlQzR29TSVNMbGJPMVRzalhKclVkZW55OTcvM2tkNmhFQlphdGY1U3NETFQ3SjNsQUVJNDROeXJ0NkIxQWdod2JNdkpqd1JNTXRNdUJLc3ltUytKVzc4UFNEWXQ4MG9waDJQTTc1N0tBNCtUMTAvYnZaQkE5Vk1OdVpqNVV3NXRWMnFIS3dwS0t6ZVVETUFiQlBRaGpYcXlQZzFKa09rd2RQMUpnOHRITjJTelBZQTlmT1htV0pBZGJDS2tMb0F4ZTV6cDZBUzYzS3FXMmFmSUt6SHJ3RTJmS1VtamppeURvMnNuMkJHbWtBaTRzbnpiVzc2SUQvSVgwd044aDBaQ2VRc29vKzdtb1RCMEJxSnBkS1MycXlsUktoc3BSTC9henVQdmxaK1pwckJxdXpJdEZkNFVLMkpzQkp6VXcwZkpxcTV1bk9PZENzVWM3SUU3QTNmZ1NmZ3NBd1R3WFZJMEVoME5ySWZpMkFKV1Z2VFpEMys2eFZ3dS96WWhuVjc0VXkvMFE4Mi8yQWtpSGpFRjNJVGNLWHdTNTB6bWtLakxjZDJqa2h5TUFYMWRoQ0wwZElFMUJoN0RNamVvNC9YbjBqSlpPL3Rrbi9xZmYzc3RNb1BYVG9KTnBIU1RjR2ZheGtaMzJYNCt3Q0xPc0VBRWxlMVZSY0kwUkZyOFhHTSsxWU9BTjBodFdGcFMxaG9kSi9OczJqL1FnUVNEemNpQ1FZeUFDd3lFRWZDZjZybnR0VmJyTlJQZWlmSHhBM3B2UnZ5ZGRhNDE5cXl0ZXI0akJ3cmw3ZUpuVnJ2VEprR2VhU2FRbDdXWk5SQXBscXRnNnZPYmpiMHZDRWlFaFhKbmNzQUhxcXp5QTRGeWFUVGQ2R0FySU9adUNxRWVoWk51T01lOVlrMVpya0VkR3pIalJESWk3Q1BKQk12NEZ4ZHI3bnJvN0I1WEhKb0ZMNE1DSUtOWWU2aWZiTUtYOU5uN1FWdnphUmY2UXlaSW1BWENQZndvU1BkN2x6NXl3UDJLSUIyaGhFMWt5eVZ5YVc5T0praWpUY3dvUnZrSXhIU0RqMXFqeGxueXh0QzhVZ1pNWmlwcGgzQXJpcjRiekIzUDhIbGIzejZ0OW51KzZMemNiN2ZObVo0UHluaU50Vk9OQ0lHbEh4dTBSY3hQK3cwUXNsM1BtTzJLaHBpc2RIanhvSUJ1YVY1NXdoTlFFNmdNNFBrT0xINDc4Rzg4bUxkd2s2RFpkWVl4L2d6RWE3b3ZIL0pReFp2TzRLdFVTNmZjZHJxV2thTFg1cEhkNkdneFBGZ2NFc2Nad1ZqM2hCS0xFQmE5L0dodERINEhzRnNRbmpPZnNDQkNzN0tjRitmTi9oSUdUeHFqTVlKVHJRYmNtdWF5dk9xR3RQMDFPcXltR24rVm5FSVkzKytQcm95SFN3K0Q0b0JIVG1maFNXRmJLZCtuTlVFS3BhRVIxNkdCU256WktQRVRVSmdRWEw5QWJRQ3RXVjFHb0UzRWNnMDZYaVd2aHFHakpGNldtdEU4dHY4Q25rZmxMNm91TDRvNldpbmx2WnNEdkZrS0R6TDkwUTNsWC9NanBtRTFpWU9uYzdISXdEVGwraFRRcHdsYXJiTDVUNGNkZTg1akNwYU0xU3p1TStiQU5zMHlXVDA0ZXJUVFc2cnhlbXFDTHAra202TVVMTlZOcE1CazBiQjJpRU82UlRtc3VpRlhDUU1xdU5xZjdkWXUwTFFCZzQ0MkJzU1pBV1ZrWEVZblduOURLdTRSby8veEFsb2h5VHozWlZmSkhuWVBSdDloSUErRHVUL3c4T2ZzTURIWnlCelUvL0JEa1NiNkxjMHdraVA3QlhIdjBoNVdud2dNWUxlZDBPalR5UWI2aGxpVnQ5b0FjaDRFVy9EZUlBdkpaQ1BYVm1pUFFYTGVsOVJIRko2bXFiYVo0TCtaZG1ONmQwcFZNZ1FveXhmQTR3dEwwYVpiNnFZYkhibjJMd2VBQVZwL3M2TzVlMVExdnZpZDRTWHo0a2l3RW1LSStIeXZEQ1pnekpQQVN5Z1gvWDJFWEZ0NGV3SjVmUFQyVXZmWnhQWlpqMFZGSFpyUFQwWVd2VE16bjUva3hoT09oM2drVGdDSmNwNWVsZnp4cEFPNFl1a0NoNHJXdVNndDRqVUJyaWNYbFdWdWo5U3JSZVhUalNHTktLK202NWovUDllNHRHT0RkMk9BbjNKTVQ3Q3FuaDhreTZpZjVjbmpVMmU3UDhTZnBONGwxWEFiZEZEcGk5bVJYamEyTzR1RWFHNGNvNW4xcWNDT3ZNMWYyblFBY1ZGNUFoSXhueS96TWhmU2l2RXdOQ0Zyd2tBWDRyQVE0WldUNldFakFyUG5jb1Y4Z1VRclhxQVA4NDJmK1lNWWI5RHFncmFicEg1a3ZuMnQzcWRldGJHODJ0QWlTamhPcUxNYW9iU2F4cXdWa1lUOHRTMW9rUUt2MWZoZ2t6elpEOE5IQnVQQzdNVHdXS0VCS2tDRUUzRWRFMXhNQURLd1B1M3NSaGpSaExXZyszZ2srejJtdlU4cTBhTlc0Y3hObUdoekx4eEY0Q3NFNStMQ1cwOWFpUVJOM1VvWmg1aktBZzBiMlh3WHBLS3pycUVTY1BYdnI0L1dWUTMyMm5qRWRvQVdXR0t2WnBKMlRlREo0eDdiT21LVElFc2RHWU1UZzFVaEU2eFFQcnhqS3dWeGFJNVJyaVE4a0xpaGgwa0t0WHQvYTVsSDhzUjVwR0ZISGZ3dlNVb3liQTB1eUVDNnNRVitPbTVReUZmRmpqZHFCOGNpOGxQS1hLTHFCTHJ6bjNmUkh3TmQwbzFiRTg0aGllTkx5UlhZVmhrRCtFNEpGaVd3ZWt3U3VWM3BjQk9ybnRVU3RoWmx6M3hIUURUVGNJNWliOFJyQ2swZEZ6YTgvQmw3VUdtWlUwSXZ2UmdvVXF2TXNHT2dMY3pGWmRpZnJ5aGNiUTY4a2ZzZ3lCMHppdC9MN1BSV3V4RkdYdDFoTVZSVUZ3WXBJS04zVkI3cXVKZlgwamZsU1JaRndMaXdlK3VhYndmTVZ6c2doajUvOXZNNzcwK0JaMGtJcE45NzBTMG5BbHl6R0h0aW1nTUl1RXFhbUt5QTNTQlI1aHZIYmRyNENnTHFUbXIzbFFnWmpnSkNvN1FXYUJWTXdCR0RpdzVOVVhUUnBycWc4U3h2eDlnNWZwbXMrL0o2QjFEelNTM3ZRZzgxdHFRU1ZDWVJpc0Y3M2VqZlFuZk4zcUszd3RJRDkxQnRISmFvMEFaUUdKVFpKOXVsZ0kzV3hzdWR4ejB0VHVpNlJlSWpmSWsxekZRdFpwRExGMnB3NGpTQVdQTlJqNDBYdVIrRzFUVlI3OVFiME9FYkw4RDFoTU5zWmo3MTZNbUhSOTlKaUxNdm1FWHV5a1V4VGhGYjRMTzZVbW1kU3UwTlBpMXQ2NmNkYURpQWhMaVBFTGdUNkZsenA2T2FGSGNSNjRncEtyemtTNDJONEhJeFpNa2R6M0FsYkRhK2pOWHZPR1l3UWl5K0xNNENZWGtrTWtHR3ZTWis5R2xWQ0l5RXBJaXIzbEQ3bmdzZGk4emxGWDYvekNaczlQSUtwZFZlSGJGZi9GS20wV3AreHI0Ykd0R0RrVHR2Nk1Manh2YU8zanFHaUFWeERKVWFkTVBlS2VHSm5uempTdnpKbGdOVHV3c3grRnF5L2dPMkwxMGowWmhDWi92dE9NelVjNjl3cGhKZm9FNzU3V3lOeFJOcThJc0Y1Tkg5Y0x0b3UvbUNxOTc3YnZPSkRrSURCN3lKWEJ6YUhVQkJuSXJra1Qyemg3bGJmUm5SREJUSFZraVZMazVESUxqeC9XL1BSZEZpUUM2SzRmZGx4Y29JbzlMcnM4ZFVWZkt2TTNNYnJ6c1hGT3ZtVVh0K3NsZldvd3UyTC9ndG9mRFhvTUJZZnlEcWIvWlRaRWZ0MC83blliRm1relBEUlZacU5SR0F3YWZVNTU1UjB2SWtNbGR2VjdKUzhNT1BNYWlXQVBpelNLRG4yRzNvcys1MzRFQytaOGZnWmFPVWpZL0xLME9vME9RMmhvNUV6MGNMYWpwUjFINk9FNEhvUm1ydjQzZkFjdGpYc0hYdi81RXg3emdrWk1NZXZhTFNEdjZtcjFGcDk4QXR4L296VTFGVDBoMDUxcVcwR0g2VWpRRXk5aExSZDBBMnFkUTRMZXpReDNvbDFTblhsamt2MG4zTXFlaFozOC94bzZhdHFDdkJtQkc3amlUdXd6YnlVUngzRm1TM0NCNllOYnFON3hPYVRZRnlkOEZDL01nY0xGQmMwS3F4MXllQ2VUd1hucldQb0dvdllVQlYxYjA1cWtIa1d5V0RUaCsveXJFNzF0RjNxbUQvd3F6cUJyNE04NERtWWVuQkdFOWxtb3FIZEMyWnRpK09KVFZKcmlHZWxQQ3RjZnZRaUlQcHdDZ3BFNmg1ekZhRndLajRuZGtBUkRpTC95L1EwWTZxNU5rM1g5RURlTmdjY1pIcFdmOUpKQ3M2a29wdXRtYjdDczIrbVJYdER1S09DaGY5UVUyN3Bmb1NJaklYK3NGdHY1c0hhSms2aHBZMlpzUUhzaTBYbFowc3FMTnQ5ayszdTVnYnBSU1JCczlHaC9BaVY0dkNyYTRkOTh5U0dCdzRSR1FhSStpQ29RaG9YK3lxc3VrYkx6bXJUU3FXMVRXaXJReUlHZ1Q5VnFERE1mUzAxeGdQSlNFSTlIWlp6TGlFVXVGMm1CMi81Y2dqaEFUaWQrdGV1UVB4aldhN2NSc2t5YUhuTENjQURVUU9ESUFPVjJDWXROcnAwY29ZL091S3ZzaXlJT0lacVJ5dE1PMGVNZ1ZJWTBzWmdxeVEycXlubUx0NDBmWmd3SFVyV245Zm9TYTNtMkVRTy9uOS8yU2NuelJWdVZpVnNjM0tCSElQL3AzNlJlSWowTGlNcCtPQ0p3SHlLVW1UeDRBU1V0dXVhWktlRHl1QjlxcXJuUEFNWUVCeElsTGFvdXMzV1pHakIrcW9ub3QvNmk1UE40bUZjbHFDcUxhMGJHbks4ZnJxYy9yd2tuVGV0YUE0c2tXTEw1L21qNEd5MitFQkh3a0x3UXd2K0FKdmZTOXYvNDl1LzY0N1ZFYW15UzdZQ2ZEUHNBQUREQ1FFcWJNQ1h2Ui8xVmEwWi9YUWhoNlkrZUt0MEVpRDdpNmRZODJtQkFoNEJMRmRVV3VGZHVrdUVwaGZ2WXB3N2loVjNxTjB1NFM1NTRXU0dUa0ZsdlpYNG1hbkF4a1g2ekQxS0NWaEFMdEJnSDgzdkhxam9uc0lwOFMydHgwZ0tiYzEreHVaRVppVWlNVVlVdTByQVFsRFcrZHJoN3lVRHZqekFHSnBmTk01eThaMW45em93VzZ5YW5VZWFBNjhSZDd5TUxobFd0NVh6bGhBTVZDZmZYZ0pFelR1YzJEbENVOXNMLzVTVkRaV2N4R1E5aFM1cnJtK2VyQ1Jxd2FJQk1DNUtza0RCZHdOWmh2Q0FCdEpqS2Vla1FUSjd5MFp4SGNhbGVCaU1rbkYwZVRDZzFvUEhPUVZLQ3V3NE94cHRZUS9xS1V0TEFIWFZ2OTlLMGRWcWZDMmpVQWlHQmVYa0t3aGRYTGtJYlZxU0EyZmxraXBBeEhYNnByUEExQjF3eTVab3hPUFg4RVExOW92eXpBbFg1dHU0OXEwWC9PSExFN1o5T1cxenltRXR6ZFpyNXJZbWtFcVdtcHVSNU5jeHFwTWlZam93dUNXZWhubzIyeG5JM09IQ0xDZkFKaHRrcklhL1hPc0tZRFpCRzFJMGJsN2taR2R5cEtUQlhYdXl6WE5WUlU5L005ejhaVytwdG1oZ2NOUzBJS2VaaSs5bFl4cWRlS3lnbldTTTV3czdSYUpmNlRRZTNSaWJZUjFvNkhwRzB2VHpiTEtQZTZnRjJGODdiWlBJei9mcTNLWnZiM3UrSnhZcCtJVjBtQi9VN29YelhRRk1RK3VmWllpNzUxbkx6WlVxRE1ybU53TFJPVUFNUk8rVnJtblkwSVB1cFBVMXc0b0hBb1dnVGRnTk5pNk1uTFQ4V0pmUlhjT0pKMk1lbUc2K2ZNeHNZUU52UVJwa1RGY05vaFV6Y3ZjcHJ3NUV3WEVZQTJzbzczL2MvY3RIRGcreU05YlF4REppUlltRnFydkhYb29hS1JyekxnUjZLVWdoM3ltaWxaQ0lSSm9KbTE3aEtHM1pxTTE0Lzl5OUc5OE9BZjNkVTlqMDk3aUNlaEc3a2VxYXRJQ2hFWmJqbmQ4Y00rS3djN2FtVWp2ekQzQmNvMHl3MDJxT054OWF3OGhSblZiWDZhdkRJbGhySHZ6SU44MzFvUjljRHBwMG1DUEJXZFVDQlNqVGJ1RkZqRC90WElSbGxlT2JraFFKSUdSNlE2U1MxcXkzT29WT1VheFl6THY0U2s3dndrQUMwUitGREVIeVFZbFVhbVVkTWcyUmdwRUdhSVd1V3IxaGNnRm10QmREV2g3ZFBuWTF0U3VKOC95MXp4NkRvN2ZJYmNFenBBK2E0ODNtRG5vemdld3VmaFdqVCsvUS85WlEreFQ5UWJBT1pQSXhHV3VhSXVrVk8zSWxvZDhJM1NGZFJCTHY5ZXBDNzFLeXpSdVlpMktkOHJ5NVNINit1WnMxUHlZUlpRakdDK3Q4VzRtSE82Z1lFRWVXSkJ1UWhnSHdmV2xhZXlWb3hac0NBQVZKRUllT3hPZDZtNW45OHRCUDdHTmgxT1M0eDRCS2FVN1A0UVQzNVVIZW5meE84WWFQUThmbXlobUJhSVJVZklBTVN2ZTJZRFp5SWNNTTkrN0tNSVVabzJ0eXRvYzdCOGVvZzBNaUkrVkpFdFg0c29FRjFSWkhQZVV3NWlCTjI4OTh2MmVTcGNnVUJhWHFzOUN5VlZtTVJQMEtLUDJ1REt4MUdJcUhjS0ZCOXVQVWRkQS9vT3dNa0tVUWsraFZVVDVPbEVMdjd1a0FBUEE0eE4rZkczVmYxeUVKV0FiVGx5dWtGcThjNXBTRkY1cXVHbUgwVmVpQzVvVEFka1VES3Z6WGhWWUs5c3BRYjNVZ1Z0Qld6N1ZScnlOUVVST3BIZU5xeDlhZHA4YWREWCtRSHJUKytYblN4VVI3SVdGanlNTkZJRWlMWmkxdks1UVVrZlRDUU9qdjh2SHdiUi9MRHF3Z3M5bXdsT3pPY0RLdVBVK0dTb2lnVFdRejRWN0N2SHRaVDI3WUdKVG44RFFFM3IzdjB4aWxvODJ2U3VXSDg0WEU3VEJsTUpFb2R5eDNDRngwVUVkc3VhRHBPSEV3UjZYNlUyU0xseERYSXVZeEhlNXh2NjI4bXU0bDRMSnBYUjhkYmljTEZKQW55Q0FVeDJLb2dDamt1cmU4bXNUZktDbG8wamFlN1hNR05PSk15b0ZYbVlHZUh2eGhNUGMzTEtYLy9VY1p0c3p3dFJrQmNFdURXQysvQWNWZVBOSHVOWWI5MEpIcnRucGg1ZDlhL1lpTkpzY1N3QTFwUVZrdW1TQWtPQWdLdWRzcnl3c0N3Zkg1anNydVpHUTJDd1hKRXQzUU4wU2NLUlVnT1NCQ3FYa1BqZDVSVzJuOFZpamt4anovbWptakhCNmk0eHM5NEU2Nzk5STAyaldYNVd3UDZhTFRaTGt5TjhxNDUxT0RmeUZVZEY5WWsyZXQ5VUpsV1NzRFJMSWVCd0ZyQkEyZTdyRWsybWFLVUNCRW5PUWM2bUhVMXQvZ3gzK1VXVVFXbkpMZVUxbWUvbkFEdy96UGUwd3d0Vm9BaERZdDBoR1hQblJydjFoUHRGS01CeWtqckg3a0J5U0R3WDlQMi9XZkNkQlE5K1J4cHRsR2hvRmdpMUs0NVlOeEpEd05wTmd5MDV2WXUzVUtrMkpRYVNGUzcwK0Y1NzluRE5RenZpK0pPRlRsdDFmWDJGNXk5NEV2NHZobWRQSmRVOFVVRjU2Ymx0emxKREVFdmsySlFrOTM0aHpwTXJGZ1d3ZHUxUkxxSEhCN2h2T2hnaHNqV0ZGY01zNjZaRUtWcVhKUytxWWNVMHk0akwySVQrNlF2N2pvQ3BWbUdzUWtGY1FyblhxOUJiOTdaUS96UCtwaldmWTU0UmNRVlMydUU1YURObVVyVkdLK3E0d0xRcUhuRVViT2puSHFFeGlacUtxOVdRaUtUK2c3QS96bVlIQ2k0YzFTejRNVWhHb0t6U2l4aXoxYUNJUEJXdy9vczR2cUVqbXgzOGx6YnV0OWNWbElzeGNkTUpUTERRK3ZOZ0YyY1ZRaVcxRTQ0d3lWcnI3TUFaOE9KRVpFSzlEZWt5MzJQUkFuSkRUVXVqdGFscmJ0T2VOczhyS09uTjcvNFRqUEwvZmRlbEI4bjA4WXdSNXdmbU42VGpGWUhRSDFjbUZmK1AvNUxVMTI4Q1pEYjNQUStxMlFJazV3aE40eGwvcy9lb29pallmeWtDcm5aSEhHWkluTGhoU2pWbk5ISWdTL203VWV0NlhBTDdvZUl5UFRLeHVnbDJzRWtUQzNnZ0tjTnFZR0E5U3ZlYVlaQ00vWHNQRUtQbWs3QmlRNmprWFBKaE1yREd4Vkc0SW9aSDgrYjBrUWJYR2l0Mkw0L3hZdHh1bTVzcFNPSjdsTDltVFpRNnBxM2JOaTEwZU1mZ0ZWaDc3NU5JRlc0SEp3U1FtaTU0bk11blZTQjhxdjZKc0w3SGlsZ2N0ZHFSNThTTjVad1lCa2dOR1hzYjA1QXJWemVXbHh1Y21BSHNPT3dyczFnMzh6bTRZN2ZPZmducmFhV1kxanZZOFlEODZQZThkZzR4cE5paTg3UnNDZk5WK2NKVmMraktFdnpuZVY1Zzd0RmlxZCtsZHp4STlKemdSS2t0WUV6RUpRSVU5M2UvclJaN1lrVkZtNVV1cjVhMWYzcG83T0VtYkJUc2MrQ1FaOGNnYmIvbUphRXJoa3NyL3JURjBNcjNxeDl5SlJWSEJ6YWNWd0dScEFRaURPdnJkWU4xQXBVOTRyR1lrVFVzdWs1YjE1Wll2QVZxRlRzVlVMaS9HY29mbEljMm01Z2RFTFZOblRmdXY1Zlk5S1NlWHFoUU80S0pOYVZmbHAwQ0VKYWFFZFNLUXJJNXRaT2w1RkE4VXZlNmxTWVd5TVk0REl4a1RiT1JoWHVBdzR6b1RTMjgrN3d2TXhydVBkZnlKbUJCTkhQdCtEYmdKNHovcHJZWUhpTmFMTXNZamtQZE44ajNKZDczQXJFZk92Um52MzYxSVVVMFg1RDc1dlRSdlpkbzMzWERzanRlOU4weUo3K2lIQnF1a1FJY2pIVW9ic2RQN0hOajBVYWNSMHIvTmRlVTlGNFBNc1VLY2t6Tk4rZGhyMVI2d1J2R1VZb1pDRWJaWlJMWEt4QnA3SElUNEVQUktHakIvdW1xTFhhMXl6RWx2QW1WQUJhMDFZN3dGdk4wM2Ywb25FbUhTM2w1d1paRmV6cjVibnN5T01XVGxhMU5kaW1ZNXNVeE15VFliZmc4dzB2cXNEc28zWFAxYndLdzZ3M3VIRGQ1UHBSWnVDSnR0eWk0ZzJGeWI0Ymg1UU42ZkdORTI2ekRGN1Y4QmJwZXJLNkFKQ0xTWm5kaDZMMTlPUTBram4xUGpEMGk4c1BZcGFXOWxVeVJkZElPKzRWQS9LemxPUzJ4M2s5VUtUdElsTTBUSVdtZXFIS0dYUVpocGpvVGI2VlNKN203cjZaaVlQMnVsQVVvZmVWL0o2eCtzckxEQXkyQ2ZFNnFrREZ1OU9NWDBBSXVnN3loQUtOMDRyT3hVNk5tcGtjOUZ4bXUvVS9vR3hHdmIzeFVFTDYwdE1sSE9EaWtqY1I5RDJrKzRwbEc1WnV0d0FIY2kwRU02WHRrVEhQOU5QMlRTR1VFN1E5SGYvU0VEc2V0a25hZXhvWmhDczJLWDFMeU5JS0U0N2pkMkR3MTUreDRRVXV0VUFTbzU5Q1lHMVFBeW9BVVhrV3dtbXkzTGdTUWp5T3ZLV25qaE8veWpPd0FyWGd0NFBrSVVnZDQ1N05ReFpMbU41K0J4NVJoQ0FHdkUxYmxOZjlMek9keGJiaG5VZ2Z1RDM5MXVSRkhjS2RYREY3ZmVqb3gveThtaWZJcTRWVzQyajBHQnFOQUtkK0prMnJCMW9hOTRiT2hxcVVzanhqWnlRaGRXTzhNblR6T2tOaGVpZXU2blYxcW5yZ3JHU2huWTNJMlczb29GNFNnczRjZ3drZ2h2dHpFa0xUbU5OUm83RTdudVRuMkxJcmlGSnlvTmZQdUp0aWN0S0JtNzRGZytkWVBTMlIzTzNmOWxBZWxiVWZjbzZGNU9EL3hkS1VuRTh0V3FOMExVcDlWQUptWVZYZFVDaGJ4MjM4MWtDaStLNDJoRzUydFNQYU1hb1dTb0xQY2Zrb24rc1pYdjdEdEtwZi9HTzdhcUMza1pzRGpva29haHJGZGJWSlNTZWhrNGp5K3RzRHplQnJKSjBrMVZrUnJHN1NoVHZjTmd1cjVucVRUTEE5dlJMQmJNTTlhNlI1NEZ0Z1pQOWFKMU1aMEdCcUVpMnF6Ui8yd2tYQlhwcFhZdi9TcU1RV1dhbTVsSHBMVktxaDN4ZHRjNFdmck9mYldsbU1PNXA5Z0JUSFp1YUcxVGFkZXFRVVpKQmZBS01ENFdSR0NsMDFaeDRTVzE0YzZrdnFKdXExL080N215L3RsVHlLWndpYlBkQTNRMVVGd0I3R2Z4anEwaDN2ckxFbUNrS3Vsc0VBUkN6UnZNVjJSVnBVbFpUV240Y1Boc0hjcTNROElHSUYyKy9nOENFSU4vMU8xcVMvMkpXcXlDNmtIb0w4Y2R2R0VHbmkxSTNDTk1JcXhxaHhJL1V0R3REc2VwYmwrSHI0elh4MzZna3BCbXBoT2xkTFVYTHAzVEtibVVZRWJSWHcvZmRmeFQ3WDdZUFhHQ0hHVG1uTzk4WkxDOTA2Zmkvekd2b04rNlpzbCs3MkpWMGxJWEo0V3dZdWxFUmZHbkFDWGNoa0Yzei9ITWR3elcwTUFFaXptQmwvREo2ZUoyU01PSG1Uc25YbElGRDRlcFRrYnFBQ0dpZ2I1UExFdHdQRVRjYkNRckM5YUtTU1FnSTdEZXd1aWlxM2J0Y0RUWkIzeEI5WWxlbmhpU0FXNjIwcmwzc2ZjY3d3eGFSOHBDV2Rzd0x3dmFxcDhjM01PV3RCc2xPcmVTSkNEcWgvdzBYbm1WMFJVWFpNM2JvUmkwVXhsaHVUeDFlM1NTd09pbTlOczNYV3NoTmI4Lzc3VkhnUWhRVFlSUU1NRllYaWRmMElCKzBtSUpocWNoQTlUeUY3dGRjSDhrUUJUSHNEWS96bFpqK3EwNlFMd0JkbTkxc3IyK3VzZmxlaXB3WUMrcmdiNHROVnA3VU5rYkVqTnR6ZWZsTi9VRTlkbHZtT2x6V1dtZkh2NGVkUGkzMmJmeUNRS1d6SGJVVEV3NU0yVFpsZnpNaTFWUjVsaDBxQ1lqaDNITUlmL2MwcHBKd2I1b1lFTnBBenlxbnlmdmlTV3lBYzc2L1l1VWwvb2FVaysrYzBZc2d1TGo5ZGFQdVVvemhoZ3VjSytQRGlNckI0ODU1Mk83VWg0aHRwNmZ3S2dJa1JCTVFIUTd6MmV5WXovV1AwQm9ZZVhjOGc3aUprclhFNzA1bFo1bXhGU0poT3E1WlNleVJSb21pUm41K3VRemM5ZFdWQjBYb2JURXdOc0VRM2FIZ25JY29BczY2UGplUT09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDI6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg== social-auth-core-4.6.1/social_core/tests/backends/data/saml_response_legacy.txt000066400000000000000000000421301500362547200277330ustar00rootroot00000000000000http://myapp.com/?RelayState=testshib&SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL215YXBwLmNvbSIgSUQ9Il8yNTk2NTFlOTY3ZGIwOGZjYTQ4MjdkODI3YWY1M2RkMCIgSW5SZXNwb25zZVRvPSJURVNUX0lEIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDUtMDlUMDM6NTc6NDMuNzkyWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI%2BaHR0cHM6Ly9pZHAudGVzdHNoaWIub3JnL2lkcC9zaGliYm9sZXRoPC9zYW1sMjpJc3N1ZXI%2BPHNhbWwycDpTdGF0dXM%2BPHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM%2BPHNhbWwyOkVuY3J5cHRlZEFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIElkPSJfMGM0NzYzNzIyOWFkNmEzMTY1OGU0MDc2ZDNlYzBmNmQiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiLz48ZHM6S2V5SW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI%2BPHhlbmM6RW5jcnlwdGVkS2V5IElkPSJfYjZmNmU2YWZjMzYyNGI3NmM1N2JmOWZhODA5YzAzNmMiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDc0RDQ0FobWdBd0lCQWdJSkFPN0J3ZGpEWmNVV01BMEdDU3FHU0liM0RRRUJCUVVBTUVVeEN6QUpCZ05WQkFZVEFrTkJNUmt3CkZ3WURWUVFJRXhCQ2NtbDBhWE5vSUVOdmJIVnRZbWxoTVJzd0dRWURWUVFLRXhKd2VYUm9iMjR0YzI5amFXRnNMV0YxZEdnd0hoY04KTVRVd05UQTRNRGMxT0RRMldoY05NalV3TlRBM01EYzFPRFEyV2pCRk1Rc3dDUVlEVlFRR0V3SkRRVEVaTUJjR0ExVUVDQk1RUW5KcApkR2x6YUNCRGIyeDFiV0pwWVRFYk1Ca0dBMVVFQ2hNU2NIbDBhRzl1TFhOdlkybGhiQzFoZFhSb01JR2ZNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNEdOQURDQmlRS0JnUUNxM2cxQ2wrM3VSNXZDbk40SGJnalRnK20zbkhodGVFTXliKyt5Y1pZcmUyYnhVZnNzaEVSNngzM2wKMjN0SGNrUll3bTdNZEJicnAzTHJWb2lPQ2RQYmxUbWwxSWhFUFRDd0tNaEJLdnZXcVR2Z2ZjU1NuUnpBV2tMbFFZU3VzYXl5Wks0bgo5cWNZa1Y1TUZuaTFyYmp4K01yNWFPRW1iNXUzM2FtTUtMd1NUd0lEQVFBQm80R25NSUdrTUIwR0ExVWREZ1FXQkJSUmlCUjZ6UzY2CmZLVm9rcDB5SkhiZ3YzUlltakIxQmdOVkhTTUViakJzZ0JSUmlCUjZ6UzY2ZktWb2twMHlKSGJndjNSWW1xRkpwRWN3UlRFTE1Ba0cKQTFVRUJoTUNRMEV4R1RBWEJnTlZCQWdURUVKeWFYUnBjMmdnUTI5c2RXMWlhV0V4R3pBWkJnTlZCQW9URW5CNWRHaHZiaTF6YjJOcApZV3d0WVhWMGFJSUpBTzdCd2RqRFpjVVdNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ1lFQUp3c01VM1lTCmF5YlZqdUo4VVMwZlVobFBPbE00MFFGQ0dMNHZCM1RFYmIyNE1xOEhyalV3clUwSkZQR2xzOWEyT1l6TjJCM2UzNU5vck11eHMrZ3IKR3RyMnlQNkx2dVgrblY2QTkzd2I0b29HSG9HZkM3VkxseXhTU25zOTM3U1M1UjFwelE0Z1d6Wm1hMktHV0tJQ1dwaDV6UTBBUlZoTAo2Mzk2N21HTG1vST08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BTElQdkVNVUVGeXhrVHowQ2N4QVA5TjV4Y3NYT2V4aVV4cXBvR2VIeVFMV0R5RVBBUDVnZ1daL3NLZ1ViL2xWSk92bCtuQXhSdVhXUlc5dGxSWWx3R2orRVhIOWhIbmdEY1BWMDNqSUJMQnFJbElBL1RmMGw4cVliOHFKRy9ZM0RTS2RQNkwvUURtYXBtTXpFM29YOEJxMW5Ea3YrUWh4cmQwMGVGK2ZMYVQ0PTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BRVpUWDhHTkM0My9yWStTUVlBMXRudHlUTTVVNkN2dUNCaktsVEVlekZPRjBZZHhCWUdFQVVjYU8xNVNKOXBMemJ1L1h0WGxzTkVMZTdKdEx4RUpwYUxubWFENnIranNWczdLaTBLNHRTMGNBUERDWHV2R1FoMmFOVjVQOGJ3N1JWUGhLOGQwYlJ1RklGR09FOHMwTTZYOUpxWDN4S0MvL1lSbVVoeDlybnU3ZWlwMGh5ZitPaUZiVGR2SDY2NTB2LzQ3aVdKcDNZeFlUV0QyMHBNbVRJMUpwWUEwYjByWVFQRkR0RU93d0JxYktxanRJc3ZYVFJzeXJhQkxvbnFOeHN5dHpEWHEra0JsMXp3WGUvSE5QcUVQblczdnNxaFhZcDVGM3dkWThkKzNCOTRMZlpOdUd4a0p3VDNzdVR0OGY5VHRBSlI4VytBUmtzT2M4eDBVaVNsVG5BNHFHOTBLMTR5dkVoVHcvd2drZjFXV01RT3dpZDNpakFYbUV4MU5MbVZvYUxYb3p4VExkTjN6YnJ6VEJIRXc3R2J3ZEdrdU5pMlhZOW16YUgwaWtGRm51VUxjMHUwc0pycEdGdzlaK0VlUk44RzNVUVZ5MjhtS2g3ZFBwWU5KbzhyajIxZFFaK2JaeUtTUHZablU3REkyakdJRE5US1g2ZkVyVWFINGlOTzN4cUU2Vk90L2d4T3BMNE5VNUhLV0Q0bG93VzcwdUJjVEVQRmhwaThpYUovdTB6YzUvTEhvdVBjMzByc1RLZFc5cmJLL2NWaHNQUHErZzA5WHZpZ0QweTJvN2tOc1pVL25tRXFiSzBKOTBrazhCR3I5cXRSczY4bUJnSURtUHVwUkhwWjM4eXNnU2VZN3V0VlVaSG5tQ0dzTzZ2NDJ6OTVOK05Pb3RCTEVZbFd1ZEdzYnowQWc4VkRDSlY5ak95QW95MDZyL1AyUHBsOFhjdmJza2d2T1BMMWdDNnVYbVJJS1lmOEw4UDJCNXVjN0haK0dtUHNOWXRLS2VKRDFFUHovdCt2NlBIbXNVb3dsSDhSd3FMRHdtMUF4dlNLQTR3UXBlQ0dQd3A5YXRYS0lWMS84NUZzRWMzajVzNjd6VlRybThrVEpydXV2MDZEdFVRZDNMOFdwTkV4cWhQait6RUp6U3RxSG04ckhNMVhNQUVxdVozc0xycTVqLzFSNlpqS0dOdFJCbjhwOE5ERGtrWm0vWTV5TXlJNXJJS3U5bnA3bXdaaEVpeWVHeHdxblV3VVMvUzVDRjNnMHVidnd4eVVnalVvd1ZvTkNqYktBbkdtT2VCSW5abkh0eGdIVUhVOUVlTFdyd2pRc3JtUmpJV0R2RkZQa3l6SzJDL20yaitubmNxc2E1OGRLVXZxcGR1VTRJYnNPQng3UGpXdXRBNmY5bXd6YWxyRU1NK0lGR3VPdk9HMC93eUdzQjZLREV6bldjUC83NkQ4angzaHZFSlAzN3REbFgreGM4Qno5TXdKdkd6VG4xbTdCb2xoR0lzSXlCTys1ZXpXa3RDWVVIUURGVE9wbXA0MDlOWHp6ZUNTUGY1U2NDWG5YYjRPd01ULy9VM1JFUnRRbGMrNmU2WG1JRjhoRkJVc0taUUJsS2ppSDkwZHlzYWlsNmN2V3UyQW55Q3QxbWxXcHFLc0MzU2RTRVZDTG1qRjlUQUFUMEtFSGdZQjg3RjZtZUpTTysvOXkyZkRuYVVvUUlUVzdubnVuSCtkT3dWSGZMU0wyL2N5YTltNlQzR29TSVNMbGJPMVRzalhKclVkZW55OTcvM2tkNmhFQlphdGY1U3NETFQ3SjNsQUVJNDROeXJ0NkIxQWdod2JNdkpqd1JNTXRNdUJLc3ltUytKVzc4UFNEWXQ4MG9waDJQTTc1N0tBNCtUMTAvYnZaQkE5Vk1OdVpqNVV3NXRWMnFIS3dwS0t6ZVVETUFiQlBRaGpYcXlQZzFKa09rd2RQMUpnOHRITjJTelBZQTlmT1htV0pBZGJDS2tMb0F4ZTV6cDZBUzYzS3FXMmFmSUt6SHJ3RTJmS1VtamppeURvMnNuMkJHbWtBaTRzbnpiVzc2SUQvSVgwd044aDBaQ2VRc29vKzdtb1RCMEJxSnBkS1MycXlsUktoc3BSTC9henVQdmxaK1pwckJxdXpJdEZkNFVLMkpzQkp6VXcwZkpxcTV1bk9PZENzVWM3SUU3QTNmZ1NmZ3NBd1R3WFZJMEVoME5ySWZpMkFKV1Z2VFpEMys2eFZ3dS96WWhuVjc0VXkvMFE4Mi8yQWtpSGpFRjNJVGNLWHdTNTB6bWtLakxjZDJqa2h5TUFYMWRoQ0wwZElFMUJoN0RNamVvNC9YbjBqSlpPL3Rrbi9xZmYzc3RNb1BYVG9KTnBIU1RjR2ZheGtaMzJYNCt3Q0xPc0VBRWxlMVZSY0kwUkZyOFhHTSsxWU9BTjBodFdGcFMxaG9kSi9OczJqL1FnUVNEemNpQ1FZeUFDd3lFRWZDZjZybnR0VmJyTlJQZWlmSHhBM3B2UnZ5ZGRhNDE5cXl0ZXI0akJ3cmw3ZUpuVnJ2VEprR2VhU2FRbDdXWk5SQXBscXRnNnZPYmpiMHZDRWlFaFhKbmNzQUhxcXp5QTRGeWFUVGQ2R0FySU9adUNxRWVoWk51T01lOVlrMVpya0VkR3pIalJESWk3Q1BKQk12NEZ4ZHI3bnJvN0I1WEhKb0ZMNE1DSUtOWWU2aWZiTUtYOU5uN1FWdnphUmY2UXlaSW1BWENQZndvU1BkN2x6NXl3UDJLSUIyaGhFMWt5eVZ5YVc5T0praWpUY3dvUnZrSXhIU0RqMXFqeGxueXh0QzhVZ1pNWmlwcGgzQXJpcjRiekIzUDhIbGIzejZ0OW51KzZMemNiN2ZObVo0UHluaU50Vk9OQ0lHbEh4dTBSY3hQK3cwUXNsM1BtTzJLaHBpc2RIanhvSUJ1YVY1NXdoTlFFNmdNNFBrT0xINDc4Rzg4bUxkd2s2RFpkWVl4L2d6RWE3b3ZIL0pReFp2TzRLdFVTNmZjZHJxV2thTFg1cEhkNkdneFBGZ2NFc2Nad1ZqM2hCS0xFQmE5L0dodERINEhzRnNRbmpPZnNDQkNzN0tjRitmTi9oSUdUeHFqTVlKVHJRYmNtdWF5dk9xR3RQMDFPcXltR24rVm5FSVkzKytQcm95SFN3K0Q0b0JIVG1maFNXRmJLZCtuTlVFS3BhRVIxNkdCU256WktQRVRVSmdRWEw5QWJRQ3RXVjFHb0UzRWNnMDZYaVd2aHFHakpGNldtdEU4dHY4Q25rZmxMNm91TDRvNldpbmx2WnNEdkZrS0R6TDkwUTNsWC9NanBtRTFpWU9uYzdISXdEVGwraFRRcHdsYXJiTDVUNGNkZTg1akNwYU0xU3p1TStiQU5zMHlXVDA0ZXJUVFc2cnhlbXFDTHAra202TVVMTlZOcE1CazBiQjJpRU82UlRtc3VpRlhDUU1xdU5xZjdkWXUwTFFCZzQ0MkJzU1pBV1ZrWEVZblduOURLdTRSby8veEFsb2h5VHozWlZmSkhuWVBSdDloSUErRHVUL3c4T2ZzTURIWnlCelUvL0JEa1NiNkxjMHdraVA3QlhIdjBoNVdud2dNWUxlZDBPalR5UWI2aGxpVnQ5b0FjaDRFVy9EZUlBdkpaQ1BYVm1pUFFYTGVsOVJIRko2bXFiYVo0TCtaZG1ONmQwcFZNZ1FveXhmQTR3dEwwYVpiNnFZYkhibjJMd2VBQVZwL3M2TzVlMVExdnZpZDRTWHo0a2l3RW1LSStIeXZEQ1pnekpQQVN5Z1gvWDJFWEZ0NGV3SjVmUFQyVXZmWnhQWlpqMFZGSFpyUFQwWVd2VE16bjUva3hoT09oM2drVGdDSmNwNWVsZnp4cEFPNFl1a0NoNHJXdVNndDRqVUJyaWNYbFdWdWo5U3JSZVhUalNHTktLK202NWovUDllNHRHT0RkMk9BbjNKTVQ3Q3FuaDhreTZpZjVjbmpVMmU3UDhTZnBONGwxWEFiZEZEcGk5bVJYamEyTzR1RWFHNGNvNW4xcWNDT3ZNMWYyblFBY1ZGNUFoSXhueS96TWhmU2l2RXdOQ0Zyd2tBWDRyQVE0WldUNldFakFyUG5jb1Y4Z1VRclhxQVA4NDJmK1lNWWI5RHFncmFicEg1a3ZuMnQzcWRldGJHODJ0QWlTamhPcUxNYW9iU2F4cXdWa1lUOHRTMW9rUUt2MWZoZ2t6elpEOE5IQnVQQzdNVHdXS0VCS2tDRUUzRWRFMXhNQURLd1B1M3NSaGpSaExXZyszZ2srejJtdlU4cTBhTlc0Y3hObUdoekx4eEY0Q3NFNStMQ1cwOWFpUVJOM1VvWmg1aktBZzBiMlh3WHBLS3pycUVTY1BYdnI0L1dWUTMyMm5qRWRvQVdXR0t2WnBKMlRlREo0eDdiT21LVElFc2RHWU1UZzFVaEU2eFFQcnhqS3dWeGFJNVJyaVE4a0xpaGgwa0t0WHQvYTVsSDhzUjVwR0ZISGZ3dlNVb3liQTB1eUVDNnNRVitPbTVReUZmRmpqZHFCOGNpOGxQS1hLTHFCTHJ6bjNmUkh3TmQwbzFiRTg0aGllTkx5UlhZVmhrRCtFNEpGaVd3ZWt3U3VWM3BjQk9ybnRVU3RoWmx6M3hIUURUVGNJNWliOFJyQ2swZEZ6YTgvQmw3VUdtWlUwSXZ2UmdvVXF2TXNHT2dMY3pGWmRpZnJ5aGNiUTY4a2ZzZ3lCMHppdC9MN1BSV3V4RkdYdDFoTVZSVUZ3WXBJS04zVkI3cXVKZlgwamZsU1JaRndMaXdlK3VhYndmTVZ6c2doajUvOXZNNzcwK0JaMGtJcE45NzBTMG5BbHl6R0h0aW1nTUl1RXFhbUt5QTNTQlI1aHZIYmRyNENnTHFUbXIzbFFnWmpnSkNvN1FXYUJWTXdCR0RpdzVOVVhUUnBycWc4U3h2eDlnNWZwbXMrL0o2QjFEelNTM3ZRZzgxdHFRU1ZDWVJpc0Y3M2VqZlFuZk4zcUszd3RJRDkxQnRISmFvMEFaUUdKVFpKOXVsZ0kzV3hzdWR4ejB0VHVpNlJlSWpmSWsxekZRdFpwRExGMnB3NGpTQVdQTlJqNDBYdVIrRzFUVlI3OVFiME9FYkw4RDFoTU5zWmo3MTZNbUhSOTlKaUxNdm1FWHV5a1V4VGhGYjRMTzZVbW1kU3UwTlBpMXQ2NmNkYURpQWhMaVBFTGdUNkZsenA2T2FGSGNSNjRncEtyemtTNDJONEhJeFpNa2R6M0FsYkRhK2pOWHZPR1l3UWl5K0xNNENZWGtrTWtHR3ZTWis5R2xWQ0l5RXBJaXIzbEQ3bmdzZGk4emxGWDYvekNaczlQSUtwZFZlSGJGZi9GS20wV3AreHI0Ykd0R0RrVHR2Nk1Manh2YU8zanFHaUFWeERKVWFkTVBlS2VHSm5uempTdnpKbGdOVHV3c3grRnF5L2dPMkwxMGowWmhDWi92dE9NelVjNjl3cGhKZm9FNzU3V3lOeFJOcThJc0Y1Tkg5Y0x0b3UvbUNxOTc3YnZPSkRrSURCN3lKWEJ6YUhVQkJuSXJra1Qyemg3bGJmUm5SREJUSFZraVZMazVESUxqeC9XL1BSZEZpUUM2SzRmZGx4Y29JbzlMcnM4ZFVWZkt2TTNNYnJ6c1hGT3ZtVVh0K3NsZldvd3UyTC9ndG9mRFhvTUJZZnlEcWIvWlRaRWZ0MC83blliRm1relBEUlZacU5SR0F3YWZVNTU1UjB2SWtNbGR2VjdKUzhNT1BNYWlXQVBpelNLRG4yRzNvcys1MzRFQytaOGZnWmFPVWpZL0xLME9vME9RMmhvNUV6MGNMYWpwUjFINk9FNEhvUm1ydjQzZkFjdGpYc0hYdi81RXg3emdrWk1NZXZhTFNEdjZtcjFGcDk4QXR4L296VTFGVDBoMDUxcVcwR0g2VWpRRXk5aExSZDBBMnFkUTRMZXpReDNvbDFTblhsamt2MG4zTXFlaFozOC94bzZhdHFDdkJtQkc3amlUdXd6YnlVUngzRm1TM0NCNllOYnFON3hPYVRZRnlkOEZDL01nY0xGQmMwS3F4MXllQ2VUd1hucldQb0dvdllVQlYxYjA1cWtIa1d5V0RUaCsveXJFNzF0RjNxbUQvd3F6cUJyNE04NERtWWVuQkdFOWxtb3FIZEMyWnRpK09KVFZKcmlHZWxQQ3RjZnZRaUlQcHdDZ3BFNmg1ekZhRndLajRuZGtBUkRpTC95L1EwWTZxNU5rM1g5RURlTmdjY1pIcFdmOUpKQ3M2a29wdXRtYjdDczIrbVJYdER1S09DaGY5UVUyN3Bmb1NJaklYK3NGdHY1c0hhSms2aHBZMlpzUUhzaTBYbFowc3FMTnQ5ayszdTVnYnBSU1JCczlHaC9BaVY0dkNyYTRkOTh5U0dCdzRSR1FhSStpQ29RaG9YK3lxc3VrYkx6bXJUU3FXMVRXaXJReUlHZ1Q5VnFERE1mUzAxeGdQSlNFSTlIWlp6TGlFVXVGMm1CMi81Y2dqaEFUaWQrdGV1UVB4aldhN2NSc2t5YUhuTENjQURVUU9ESUFPVjJDWXROcnAwY29ZL091S3ZzaXlJT0lacVJ5dE1PMGVNZ1ZJWTBzWmdxeVEycXlubUx0NDBmWmd3SFVyV245Zm9TYTNtMkVRTy9uOS8yU2NuelJWdVZpVnNjM0tCSElQL3AzNlJlSWowTGlNcCtPQ0p3SHlLVW1UeDRBU1V0dXVhWktlRHl1QjlxcXJuUEFNWUVCeElsTGFvdXMzV1pHakIrcW9ub3QvNmk1UE40bUZjbHFDcUxhMGJHbks4ZnJxYy9yd2tuVGV0YUE0c2tXTEw1L21qNEd5MitFQkh3a0x3UXd2K0FKdmZTOXYvNDl1LzY0N1ZFYW15UzdZQ2ZEUHNBQUREQ1FFcWJNQ1h2Ui8xVmEwWi9YUWhoNlkrZUt0MEVpRDdpNmRZODJtQkFoNEJMRmRVV3VGZHVrdUVwaGZ2WXB3N2loVjNxTjB1NFM1NTRXU0dUa0ZsdlpYNG1hbkF4a1g2ekQxS0NWaEFMdEJnSDgzdkhxam9uc0lwOFMydHgwZ0tiYzEreHVaRVppVWlNVVlVdTByQVFsRFcrZHJoN3lVRHZqekFHSnBmTk01eThaMW45em93VzZ5YW5VZWFBNjhSZDd5TUxobFd0NVh6bGhBTVZDZmZYZ0pFelR1YzJEbENVOXNMLzVTVkRaV2N4R1E5aFM1cnJtK2VyQ1Jxd2FJQk1DNUtza0RCZHdOWmh2Q0FCdEpqS2Vla1FUSjd5MFp4SGNhbGVCaU1rbkYwZVRDZzFvUEhPUVZLQ3V3NE94cHRZUS9xS1V0TEFIWFZ2OTlLMGRWcWZDMmpVQWlHQmVYa0t3aGRYTGtJYlZxU0EyZmxraXBBeEhYNnByUEExQjF3eTVab3hPUFg4RVExOW92eXpBbFg1dHU0OXEwWC9PSExFN1o5T1cxenltRXR6ZFpyNXJZbWtFcVdtcHVSNU5jeHFwTWlZam93dUNXZWhubzIyeG5JM09IQ0xDZkFKaHRrcklhL1hPc0tZRFpCRzFJMGJsN2taR2R5cEtUQlhYdXl6WE5WUlU5L005ejhaVytwdG1oZ2NOUzBJS2VaaSs5bFl4cWRlS3lnbldTTTV3czdSYUpmNlRRZTNSaWJZUjFvNkhwRzB2VHpiTEtQZTZnRjJGODdiWlBJei9mcTNLWnZiM3UrSnhZcCtJVjBtQi9VN29YelhRRk1RK3VmWllpNzUxbkx6WlVxRE1ybU53TFJPVUFNUk8rVnJtblkwSVB1cFBVMXc0b0hBb1dnVGRnTk5pNk1uTFQ4V0pmUlhjT0pKMk1lbUc2K2ZNeHNZUU52UVJwa1RGY05vaFV6Y3ZjcHJ3NUV3WEVZQTJzbzczL2MvY3RIRGcreU05YlF4REppUlltRnFydkhYb29hS1JyekxnUjZLVWdoM3ltaWxaQ0lSSm9KbTE3aEtHM1pxTTE0Lzl5OUc5OE9BZjNkVTlqMDk3aUNlaEc3a2VxYXRJQ2hFWmJqbmQ4Y00rS3djN2FtVWp2ekQzQmNvMHl3MDJxT054OWF3OGhSblZiWDZhdkRJbGhySHZ6SU44MzFvUjljRHBwMG1DUEJXZFVDQlNqVGJ1RkZqRC90WElSbGxlT2JraFFKSUdSNlE2U1MxcXkzT29WT1VheFl6THY0U2s3dndrQUMwUitGREVIeVFZbFVhbVVkTWcyUmdwRUdhSVd1V3IxaGNnRm10QmREV2g3ZFBuWTF0U3VKOC95MXp4NkRvN2ZJYmNFenBBK2E0ODNtRG5vemdld3VmaFdqVCsvUS85WlEreFQ5UWJBT1pQSXhHV3VhSXVrVk8zSWxvZDhJM1NGZFJCTHY5ZXBDNzFLeXpSdVlpMktkOHJ5NVNINit1WnMxUHlZUlpRakdDK3Q4VzRtSE82Z1lFRWVXSkJ1UWhnSHdmV2xhZXlWb3hac0NBQVZKRUllT3hPZDZtNW45OHRCUDdHTmgxT1M0eDRCS2FVN1A0UVQzNVVIZW5meE84WWFQUThmbXlobUJhSVJVZklBTVN2ZTJZRFp5SWNNTTkrN0tNSVVabzJ0eXRvYzdCOGVvZzBNaUkrVkpFdFg0c29FRjFSWkhQZVV3NWlCTjI4OTh2MmVTcGNnVUJhWHFzOUN5VlZtTVJQMEtLUDJ1REt4MUdJcUhjS0ZCOXVQVWRkQS9vT3dNa0tVUWsraFZVVDVPbEVMdjd1a0FBUEE0eE4rZkczVmYxeUVKV0FiVGx5dWtGcThjNXBTRkY1cXVHbUgwVmVpQzVvVEFka1VES3Z6WGhWWUs5c3BRYjNVZ1Z0Qld6N1ZScnlOUVVST3BIZU5xeDlhZHA4YWREWCtRSHJUKytYblN4VVI3SVdGanlNTkZJRWlMWmkxdks1UVVrZlRDUU9qdjh2SHdiUi9MRHF3Z3M5bXdsT3pPY0RLdVBVK0dTb2lnVFdRejRWN0N2SHRaVDI3WUdKVG44RFFFM3IzdjB4aWxvODJ2U3VXSDg0WEU3VEJsTUpFb2R5eDNDRngwVUVkc3VhRHBPSEV3UjZYNlUyU0xseERYSXVZeEhlNXh2NjI4bXU0bDRMSnBYUjhkYmljTEZKQW55Q0FVeDJLb2dDamt1cmU4bXNUZktDbG8wamFlN1hNR05PSk15b0ZYbVlHZUh2eGhNUGMzTEtYLy9VY1p0c3p3dFJrQmNFdURXQysvQWNWZVBOSHVOWWI5MEpIcnRucGg1ZDlhL1lpTkpzY1N3QTFwUVZrdW1TQWtPQWdLdWRzcnl3c0N3Zkg1anNydVpHUTJDd1hKRXQzUU4wU2NLUlVnT1NCQ3FYa1BqZDVSVzJuOFZpamt4anovbWptakhCNmk0eHM5NEU2Nzk5STAyaldYNVd3UDZhTFRaTGt5TjhxNDUxT0RmeUZVZEY5WWsyZXQ5VUpsV1NzRFJMSWVCd0ZyQkEyZTdyRWsybWFLVUNCRW5PUWM2bUhVMXQvZ3gzK1VXVVFXbkpMZVUxbWUvbkFEdy96UGUwd3d0Vm9BaERZdDBoR1hQblJydjFoUHRGS01CeWtqckg3a0J5U0R3WDlQMi9XZkNkQlE5K1J4cHRsR2hvRmdpMUs0NVlOeEpEd05wTmd5MDV2WXUzVUtrMkpRYVNGUzcwK0Y1NzluRE5RenZpK0pPRlRsdDFmWDJGNXk5NEV2NHZobWRQSmRVOFVVRjU2Ymx0emxKREVFdmsySlFrOTM0aHpwTXJGZ1d3ZHUxUkxxSEhCN2h2T2hnaHNqV0ZGY01zNjZaRUtWcVhKUytxWWNVMHk0akwySVQrNlF2N2pvQ3BWbUdzUWtGY1FyblhxOUJiOTdaUS96UCtwaldmWTU0UmNRVlMydUU1YURObVVyVkdLK3E0d0xRcUhuRVViT2puSHFFeGlacUtxOVdRaUtUK2c3QS96bVlIQ2k0YzFTejRNVWhHb0t6U2l4aXoxYUNJUEJXdy9vczR2cUVqbXgzOGx6YnV0OWNWbElzeGNkTUpUTERRK3ZOZ0YyY1ZRaVcxRTQ0d3lWcnI3TUFaOE9KRVpFSzlEZWt5MzJQUkFuSkRUVXVqdGFscmJ0T2VOczhyS09uTjcvNFRqUEwvZmRlbEI4bjA4WXdSNXdmbU42VGpGWUhRSDFjbUZmK1AvNUxVMTI4Q1pEYjNQUStxMlFJazV3aE40eGwvcy9lb29pallmeWtDcm5aSEhHWkluTGhoU2pWbk5ISWdTL203VWV0NlhBTDdvZUl5UFRLeHVnbDJzRWtUQzNnZ0tjTnFZR0E5U3ZlYVlaQ00vWHNQRUtQbWs3QmlRNmprWFBKaE1yREd4Vkc0SW9aSDgrYjBrUWJYR2l0Mkw0L3hZdHh1bTVzcFNPSjdsTDltVFpRNnBxM2JOaTEwZU1mZ0ZWaDc3NU5JRlc0SEp3U1FtaTU0bk11blZTQjhxdjZKc0w3SGlsZ2N0ZHFSNThTTjVad1lCa2dOR1hzYjA1QXJWemVXbHh1Y21BSHNPT3dyczFnMzh6bTRZN2ZPZmducmFhV1kxanZZOFlEODZQZThkZzR4cE5paTg3UnNDZk5WK2NKVmMraktFdnpuZVY1Zzd0RmlxZCtsZHp4STlKemdSS2t0WUV6RUpRSVU5M2UvclJaN1lrVkZtNVV1cjVhMWYzcG83T0VtYkJUc2MrQ1FaOGNnYmIvbUphRXJoa3NyL3JURjBNcjNxeDl5SlJWSEJ6YWNWd0dScEFRaURPdnJkWU4xQXBVOTRyR1lrVFVzdWs1YjE1Wll2QVZxRlRzVlVMaS9HY29mbEljMm01Z2RFTFZOblRmdXY1Zlk5S1NlWHFoUU80S0pOYVZmbHAwQ0VKYWFFZFNLUXJJNXRaT2w1RkE4VXZlNmxTWVd5TVk0REl4a1RiT1JoWHVBdzR6b1RTMjgrN3d2TXhydVBkZnlKbUJCTkhQdCtEYmdKNHovcHJZWUhpTmFMTXNZamtQZE44ajNKZDczQXJFZk92Um52MzYxSVVVMFg1RDc1dlRSdlpkbzMzWERzanRlOU4weUo3K2lIQnF1a1FJY2pIVW9ic2RQN0hOajBVYWNSMHIvTmRlVTlGNFBNc1VLY2t6Tk4rZGhyMVI2d1J2R1VZb1pDRWJaWlJMWEt4QnA3SElUNEVQUktHakIvdW1xTFhhMXl6RWx2QW1WQUJhMDFZN3dGdk4wM2Ywb25FbUhTM2w1d1paRmV6cjVibnN5T01XVGxhMU5kaW1ZNXNVeE15VFliZmc4dzB2cXNEc28zWFAxYndLdzZ3M3VIRGQ1UHBSWnVDSnR0eWk0ZzJGeWI0Ymg1UU42ZkdORTI2ekRGN1Y4QmJwZXJLNkFKQ0xTWm5kaDZMMTlPUTBram4xUGpEMGk4c1BZcGFXOWxVeVJkZElPKzRWQS9LemxPUzJ4M2s5VUtUdElsTTBUSVdtZXFIS0dYUVpocGpvVGI2VlNKN203cjZaaVlQMnVsQVVvZmVWL0o2eCtzckxEQXkyQ2ZFNnFrREZ1OU9NWDBBSXVnN3loQUtOMDRyT3hVNk5tcGtjOUZ4bXUvVS9vR3hHdmIzeFVFTDYwdE1sSE9EaWtqY1I5RDJrKzRwbEc1WnV0d0FIY2kwRU02WHRrVEhQOU5QMlRTR1VFN1E5SGYvU0VEc2V0a25hZXhvWmhDczJLWDFMeU5JS0U0N2pkMkR3MTUreDRRVXV0VUFTbzU5Q1lHMVFBeW9BVVhrV3dtbXkzTGdTUWp5T3ZLV25qaE8veWpPd0FyWGd0NFBrSVVnZDQ1N05ReFpMbU41K0J4NVJoQ0FHdkUxYmxOZjlMek9keGJiaG5VZ2Z1RDM5MXVSRkhjS2RYREY3ZmVqb3gveThtaWZJcTRWVzQyajBHQnFOQUtkK0prMnJCMW9hOTRiT2hxcVVzanhqWnlRaGRXTzhNblR6T2tOaGVpZXU2blYxcW5yZ3JHU2huWTNJMlczb29GNFNnczRjZ3drZ2h2dHpFa0xUbU5OUm83RTdudVRuMkxJcmlGSnlvTmZQdUp0aWN0S0JtNzRGZytkWVBTMlIzTzNmOWxBZWxiVWZjbzZGNU9EL3hkS1VuRTh0V3FOMExVcDlWQUptWVZYZFVDaGJ4MjM4MWtDaStLNDJoRzUydFNQYU1hb1dTb0xQY2Zrb24rc1pYdjdEdEtwZi9HTzdhcUMza1pzRGpva29haHJGZGJWSlNTZWhrNGp5K3RzRHplQnJKSjBrMVZrUnJHN1NoVHZjTmd1cjVucVRUTEE5dlJMQmJNTTlhNlI1NEZ0Z1pQOWFKMU1aMEdCcUVpMnF6Ui8yd2tYQlhwcFhZdi9TcU1RV1dhbTVsSHBMVktxaDN4ZHRjNFdmck9mYldsbU1PNXA5Z0JUSFp1YUcxVGFkZXFRVVpKQmZBS01ENFdSR0NsMDFaeDRTVzE0YzZrdnFKdXExL080N215L3RsVHlLWndpYlBkQTNRMVVGd0I3R2Z4anEwaDN2ckxFbUNrS3Vsc0VBUkN6UnZNVjJSVnBVbFpUV240Y1Boc0hjcTNROElHSUYyKy9nOENFSU4vMU8xcVMvMkpXcXlDNmtIb0w4Y2R2R0VHbmkxSTNDTk1JcXhxaHhJL1V0R3REc2VwYmwrSHI0elh4MzZna3BCbXBoT2xkTFVYTHAzVEtibVVZRWJSWHcvZmRmeFQ3WDdZUFhHQ0hHVG1uTzk4WkxDOTA2Zmkvekd2b04rNlpzbCs3MkpWMGxJWEo0V3dZdWxFUmZHbkFDWGNoa0Yzei9ITWR3elcwTUFFaXptQmwvREo2ZUoyU01PSG1Uc25YbElGRDRlcFRrYnFBQ0dpZ2I1UExFdHdQRVRjYkNRckM5YUtTU1FnSTdEZXd1aWlxM2J0Y0RUWkIzeEI5WWxlbmhpU0FXNjIwcmwzc2ZjY3d3eGFSOHBDV2Rzd0x3dmFxcDhjM01PV3RCc2xPcmVTSkNEcWgvdzBYbm1WMFJVWFpNM2JvUmkwVXhsaHVUeDFlM1NTd09pbTlOczNYV3NoTmI4Lzc3VkhnUWhRVFlSUU1NRllYaWRmMElCKzBtSUpocWNoQTlUeUY3dGRjSDhrUUJUSHNEWS96bFpqK3EwNlFMd0JkbTkxc3IyK3VzZmxlaXB3WUMrcmdiNHROVnA3VU5rYkVqTnR6ZWZsTi9VRTlkbHZtT2x6V1dtZkh2NGVkUGkzMmJmeUNRS1d6SGJVVEV3NU0yVFpsZnpNaTFWUjVsaDBxQ1lqaDNITUlmL2MwcHBKd2I1b1lFTnBBenlxbnlmdmlTV3lBYzc2L1l1VWwvb2FVaysrYzBZc2d1TGo5ZGFQdVVvemhoZ3VjSytQRGlNckI0ODU1Mk83VWg0aHRwNmZ3S2dJa1JCTVFIUTd6MmV5WXovV1AwQm9ZZVhjOGc3aUprclhFNzA1bFo1bXhGU0poT3E1WlNleVJSb21pUm41K3VRemM5ZFdWQjBYb2JURXdOc0VRM2FIZ25JY29BczY2UGplUT09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDI6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg== social-auth-core-4.6.1/social_core/tests/backends/data/saml_response_no_idp_name.txt000066400000000000000000000421261500362547200307440ustar00rootroot00000000000000http://myapp.com/?RelayState=%7b%7d&SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL215YXBwLmNvbSIgSUQ9Il8yNTk2NTFlOTY3ZGIwOGZjYTQ4MjdkODI3YWY1M2RkMCIgSW5SZXNwb25zZVRvPSJURVNUX0lEIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDUtMDlUMDM6NTc6NDMuNzkyWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI%2BaHR0cHM6Ly9pZHAudGVzdHNoaWIub3JnL2lkcC9zaGliYm9sZXRoPC9zYW1sMjpJc3N1ZXI%2BPHNhbWwycDpTdGF0dXM%2BPHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM%2BPHNhbWwyOkVuY3J5cHRlZEFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIElkPSJfMGM0NzYzNzIyOWFkNmEzMTY1OGU0MDc2ZDNlYzBmNmQiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiLz48ZHM6S2V5SW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI%2BPHhlbmM6RW5jcnlwdGVkS2V5IElkPSJfYjZmNmU2YWZjMzYyNGI3NmM1N2JmOWZhODA5YzAzNmMiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDc0RDQ0FobWdBd0lCQWdJSkFPN0J3ZGpEWmNVV01BMEdDU3FHU0liM0RRRUJCUVVBTUVVeEN6QUpCZ05WQkFZVEFrTkJNUmt3CkZ3WURWUVFJRXhCQ2NtbDBhWE5vSUVOdmJIVnRZbWxoTVJzd0dRWURWUVFLRXhKd2VYUm9iMjR0YzI5amFXRnNMV0YxZEdnd0hoY04KTVRVd05UQTRNRGMxT0RRMldoY05NalV3TlRBM01EYzFPRFEyV2pCRk1Rc3dDUVlEVlFRR0V3SkRRVEVaTUJjR0ExVUVDQk1RUW5KcApkR2x6YUNCRGIyeDFiV0pwWVRFYk1Ca0dBMVVFQ2hNU2NIbDBhRzl1TFhOdlkybGhiQzFoZFhSb01JR2ZNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNEdOQURDQmlRS0JnUUNxM2cxQ2wrM3VSNXZDbk40SGJnalRnK20zbkhodGVFTXliKyt5Y1pZcmUyYnhVZnNzaEVSNngzM2wKMjN0SGNrUll3bTdNZEJicnAzTHJWb2lPQ2RQYmxUbWwxSWhFUFRDd0tNaEJLdnZXcVR2Z2ZjU1NuUnpBV2tMbFFZU3VzYXl5Wks0bgo5cWNZa1Y1TUZuaTFyYmp4K01yNWFPRW1iNXUzM2FtTUtMd1NUd0lEQVFBQm80R25NSUdrTUIwR0ExVWREZ1FXQkJSUmlCUjZ6UzY2CmZLVm9rcDB5SkhiZ3YzUlltakIxQmdOVkhTTUViakJzZ0JSUmlCUjZ6UzY2ZktWb2twMHlKSGJndjNSWW1xRkpwRWN3UlRFTE1Ba0cKQTFVRUJoTUNRMEV4R1RBWEJnTlZCQWdURUVKeWFYUnBjMmdnUTI5c2RXMWlhV0V4R3pBWkJnTlZCQW9URW5CNWRHaHZiaTF6YjJOcApZV3d0WVhWMGFJSUpBTzdCd2RqRFpjVVdNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ1lFQUp3c01VM1lTCmF5YlZqdUo4VVMwZlVobFBPbE00MFFGQ0dMNHZCM1RFYmIyNE1xOEhyalV3clUwSkZQR2xzOWEyT1l6TjJCM2UzNU5vck11eHMrZ3IKR3RyMnlQNkx2dVgrblY2QTkzd2I0b29HSG9HZkM3VkxseXhTU25zOTM3U1M1UjFwelE0Z1d6Wm1hMktHV0tJQ1dwaDV6UTBBUlZoTAo2Mzk2N21HTG1vST08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BTElQdkVNVUVGeXhrVHowQ2N4QVA5TjV4Y3NYT2V4aVV4cXBvR2VIeVFMV0R5RVBBUDVnZ1daL3NLZ1ViL2xWSk92bCtuQXhSdVhXUlc5dGxSWWx3R2orRVhIOWhIbmdEY1BWMDNqSUJMQnFJbElBL1RmMGw4cVliOHFKRy9ZM0RTS2RQNkwvUURtYXBtTXpFM29YOEJxMW5Ea3YrUWh4cmQwMGVGK2ZMYVQ0PTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BRVpUWDhHTkM0My9yWStTUVlBMXRudHlUTTVVNkN2dUNCaktsVEVlekZPRjBZZHhCWUdFQVVjYU8xNVNKOXBMemJ1L1h0WGxzTkVMZTdKdEx4RUpwYUxubWFENnIranNWczdLaTBLNHRTMGNBUERDWHV2R1FoMmFOVjVQOGJ3N1JWUGhLOGQwYlJ1RklGR09FOHMwTTZYOUpxWDN4S0MvL1lSbVVoeDlybnU3ZWlwMGh5ZitPaUZiVGR2SDY2NTB2LzQ3aVdKcDNZeFlUV0QyMHBNbVRJMUpwWUEwYjByWVFQRkR0RU93d0JxYktxanRJc3ZYVFJzeXJhQkxvbnFOeHN5dHpEWHEra0JsMXp3WGUvSE5QcUVQblczdnNxaFhZcDVGM3dkWThkKzNCOTRMZlpOdUd4a0p3VDNzdVR0OGY5VHRBSlI4VytBUmtzT2M4eDBVaVNsVG5BNHFHOTBLMTR5dkVoVHcvd2drZjFXV01RT3dpZDNpakFYbUV4MU5MbVZvYUxYb3p4VExkTjN6YnJ6VEJIRXc3R2J3ZEdrdU5pMlhZOW16YUgwaWtGRm51VUxjMHUwc0pycEdGdzlaK0VlUk44RzNVUVZ5MjhtS2g3ZFBwWU5KbzhyajIxZFFaK2JaeUtTUHZablU3REkyakdJRE5US1g2ZkVyVWFINGlOTzN4cUU2Vk90L2d4T3BMNE5VNUhLV0Q0bG93VzcwdUJjVEVQRmhwaThpYUovdTB6YzUvTEhvdVBjMzByc1RLZFc5cmJLL2NWaHNQUHErZzA5WHZpZ0QweTJvN2tOc1pVL25tRXFiSzBKOTBrazhCR3I5cXRSczY4bUJnSURtUHVwUkhwWjM4eXNnU2VZN3V0VlVaSG5tQ0dzTzZ2NDJ6OTVOK05Pb3RCTEVZbFd1ZEdzYnowQWc4VkRDSlY5ak95QW95MDZyL1AyUHBsOFhjdmJza2d2T1BMMWdDNnVYbVJJS1lmOEw4UDJCNXVjN0haK0dtUHNOWXRLS2VKRDFFUHovdCt2NlBIbXNVb3dsSDhSd3FMRHdtMUF4dlNLQTR3UXBlQ0dQd3A5YXRYS0lWMS84NUZzRWMzajVzNjd6VlRybThrVEpydXV2MDZEdFVRZDNMOFdwTkV4cWhQait6RUp6U3RxSG04ckhNMVhNQUVxdVozc0xycTVqLzFSNlpqS0dOdFJCbjhwOE5ERGtrWm0vWTV5TXlJNXJJS3U5bnA3bXdaaEVpeWVHeHdxblV3VVMvUzVDRjNnMHVidnd4eVVnalVvd1ZvTkNqYktBbkdtT2VCSW5abkh0eGdIVUhVOUVlTFdyd2pRc3JtUmpJV0R2RkZQa3l6SzJDL20yaitubmNxc2E1OGRLVXZxcGR1VTRJYnNPQng3UGpXdXRBNmY5bXd6YWxyRU1NK0lGR3VPdk9HMC93eUdzQjZLREV6bldjUC83NkQ4angzaHZFSlAzN3REbFgreGM4Qno5TXdKdkd6VG4xbTdCb2xoR0lzSXlCTys1ZXpXa3RDWVVIUURGVE9wbXA0MDlOWHp6ZUNTUGY1U2NDWG5YYjRPd01ULy9VM1JFUnRRbGMrNmU2WG1JRjhoRkJVc0taUUJsS2ppSDkwZHlzYWlsNmN2V3UyQW55Q3QxbWxXcHFLc0MzU2RTRVZDTG1qRjlUQUFUMEtFSGdZQjg3RjZtZUpTTysvOXkyZkRuYVVvUUlUVzdubnVuSCtkT3dWSGZMU0wyL2N5YTltNlQzR29TSVNMbGJPMVRzalhKclVkZW55OTcvM2tkNmhFQlphdGY1U3NETFQ3SjNsQUVJNDROeXJ0NkIxQWdod2JNdkpqd1JNTXRNdUJLc3ltUytKVzc4UFNEWXQ4MG9waDJQTTc1N0tBNCtUMTAvYnZaQkE5Vk1OdVpqNVV3NXRWMnFIS3dwS0t6ZVVETUFiQlBRaGpYcXlQZzFKa09rd2RQMUpnOHRITjJTelBZQTlmT1htV0pBZGJDS2tMb0F4ZTV6cDZBUzYzS3FXMmFmSUt6SHJ3RTJmS1VtamppeURvMnNuMkJHbWtBaTRzbnpiVzc2SUQvSVgwd044aDBaQ2VRc29vKzdtb1RCMEJxSnBkS1MycXlsUktoc3BSTC9henVQdmxaK1pwckJxdXpJdEZkNFVLMkpzQkp6VXcwZkpxcTV1bk9PZENzVWM3SUU3QTNmZ1NmZ3NBd1R3WFZJMEVoME5ySWZpMkFKV1Z2VFpEMys2eFZ3dS96WWhuVjc0VXkvMFE4Mi8yQWtpSGpFRjNJVGNLWHdTNTB6bWtLakxjZDJqa2h5TUFYMWRoQ0wwZElFMUJoN0RNamVvNC9YbjBqSlpPL3Rrbi9xZmYzc3RNb1BYVG9KTnBIU1RjR2ZheGtaMzJYNCt3Q0xPc0VBRWxlMVZSY0kwUkZyOFhHTSsxWU9BTjBodFdGcFMxaG9kSi9OczJqL1FnUVNEemNpQ1FZeUFDd3lFRWZDZjZybnR0VmJyTlJQZWlmSHhBM3B2UnZ5ZGRhNDE5cXl0ZXI0akJ3cmw3ZUpuVnJ2VEprR2VhU2FRbDdXWk5SQXBscXRnNnZPYmpiMHZDRWlFaFhKbmNzQUhxcXp5QTRGeWFUVGQ2R0FySU9adUNxRWVoWk51T01lOVlrMVpya0VkR3pIalJESWk3Q1BKQk12NEZ4ZHI3bnJvN0I1WEhKb0ZMNE1DSUtOWWU2aWZiTUtYOU5uN1FWdnphUmY2UXlaSW1BWENQZndvU1BkN2x6NXl3UDJLSUIyaGhFMWt5eVZ5YVc5T0praWpUY3dvUnZrSXhIU0RqMXFqeGxueXh0QzhVZ1pNWmlwcGgzQXJpcjRiekIzUDhIbGIzejZ0OW51KzZMemNiN2ZObVo0UHluaU50Vk9OQ0lHbEh4dTBSY3hQK3cwUXNsM1BtTzJLaHBpc2RIanhvSUJ1YVY1NXdoTlFFNmdNNFBrT0xINDc4Rzg4bUxkd2s2RFpkWVl4L2d6RWE3b3ZIL0pReFp2TzRLdFVTNmZjZHJxV2thTFg1cEhkNkdneFBGZ2NFc2Nad1ZqM2hCS0xFQmE5L0dodERINEhzRnNRbmpPZnNDQkNzN0tjRitmTi9oSUdUeHFqTVlKVHJRYmNtdWF5dk9xR3RQMDFPcXltR24rVm5FSVkzKytQcm95SFN3K0Q0b0JIVG1maFNXRmJLZCtuTlVFS3BhRVIxNkdCU256WktQRVRVSmdRWEw5QWJRQ3RXVjFHb0UzRWNnMDZYaVd2aHFHakpGNldtdEU4dHY4Q25rZmxMNm91TDRvNldpbmx2WnNEdkZrS0R6TDkwUTNsWC9NanBtRTFpWU9uYzdISXdEVGwraFRRcHdsYXJiTDVUNGNkZTg1akNwYU0xU3p1TStiQU5zMHlXVDA0ZXJUVFc2cnhlbXFDTHAra202TVVMTlZOcE1CazBiQjJpRU82UlRtc3VpRlhDUU1xdU5xZjdkWXUwTFFCZzQ0MkJzU1pBV1ZrWEVZblduOURLdTRSby8veEFsb2h5VHozWlZmSkhuWVBSdDloSUErRHVUL3c4T2ZzTURIWnlCelUvL0JEa1NiNkxjMHdraVA3QlhIdjBoNVdud2dNWUxlZDBPalR5UWI2aGxpVnQ5b0FjaDRFVy9EZUlBdkpaQ1BYVm1pUFFYTGVsOVJIRko2bXFiYVo0TCtaZG1ONmQwcFZNZ1FveXhmQTR3dEwwYVpiNnFZYkhibjJMd2VBQVZwL3M2TzVlMVExdnZpZDRTWHo0a2l3RW1LSStIeXZEQ1pnekpQQVN5Z1gvWDJFWEZ0NGV3SjVmUFQyVXZmWnhQWlpqMFZGSFpyUFQwWVd2VE16bjUva3hoT09oM2drVGdDSmNwNWVsZnp4cEFPNFl1a0NoNHJXdVNndDRqVUJyaWNYbFdWdWo5U3JSZVhUalNHTktLK202NWovUDllNHRHT0RkMk9BbjNKTVQ3Q3FuaDhreTZpZjVjbmpVMmU3UDhTZnBONGwxWEFiZEZEcGk5bVJYamEyTzR1RWFHNGNvNW4xcWNDT3ZNMWYyblFBY1ZGNUFoSXhueS96TWhmU2l2RXdOQ0Zyd2tBWDRyQVE0WldUNldFakFyUG5jb1Y4Z1VRclhxQVA4NDJmK1lNWWI5RHFncmFicEg1a3ZuMnQzcWRldGJHODJ0QWlTamhPcUxNYW9iU2F4cXdWa1lUOHRTMW9rUUt2MWZoZ2t6elpEOE5IQnVQQzdNVHdXS0VCS2tDRUUzRWRFMXhNQURLd1B1M3NSaGpSaExXZyszZ2srejJtdlU4cTBhTlc0Y3hObUdoekx4eEY0Q3NFNStMQ1cwOWFpUVJOM1VvWmg1aktBZzBiMlh3WHBLS3pycUVTY1BYdnI0L1dWUTMyMm5qRWRvQVdXR0t2WnBKMlRlREo0eDdiT21LVElFc2RHWU1UZzFVaEU2eFFQcnhqS3dWeGFJNVJyaVE4a0xpaGgwa0t0WHQvYTVsSDhzUjVwR0ZISGZ3dlNVb3liQTB1eUVDNnNRVitPbTVReUZmRmpqZHFCOGNpOGxQS1hLTHFCTHJ6bjNmUkh3TmQwbzFiRTg0aGllTkx5UlhZVmhrRCtFNEpGaVd3ZWt3U3VWM3BjQk9ybnRVU3RoWmx6M3hIUURUVGNJNWliOFJyQ2swZEZ6YTgvQmw3VUdtWlUwSXZ2UmdvVXF2TXNHT2dMY3pGWmRpZnJ5aGNiUTY4a2ZzZ3lCMHppdC9MN1BSV3V4RkdYdDFoTVZSVUZ3WXBJS04zVkI3cXVKZlgwamZsU1JaRndMaXdlK3VhYndmTVZ6c2doajUvOXZNNzcwK0JaMGtJcE45NzBTMG5BbHl6R0h0aW1nTUl1RXFhbUt5QTNTQlI1aHZIYmRyNENnTHFUbXIzbFFnWmpnSkNvN1FXYUJWTXdCR0RpdzVOVVhUUnBycWc4U3h2eDlnNWZwbXMrL0o2QjFEelNTM3ZRZzgxdHFRU1ZDWVJpc0Y3M2VqZlFuZk4zcUszd3RJRDkxQnRISmFvMEFaUUdKVFpKOXVsZ0kzV3hzdWR4ejB0VHVpNlJlSWpmSWsxekZRdFpwRExGMnB3NGpTQVdQTlJqNDBYdVIrRzFUVlI3OVFiME9FYkw4RDFoTU5zWmo3MTZNbUhSOTlKaUxNdm1FWHV5a1V4VGhGYjRMTzZVbW1kU3UwTlBpMXQ2NmNkYURpQWhMaVBFTGdUNkZsenA2T2FGSGNSNjRncEtyemtTNDJONEhJeFpNa2R6M0FsYkRhK2pOWHZPR1l3UWl5K0xNNENZWGtrTWtHR3ZTWis5R2xWQ0l5RXBJaXIzbEQ3bmdzZGk4emxGWDYvekNaczlQSUtwZFZlSGJGZi9GS20wV3AreHI0Ykd0R0RrVHR2Nk1Manh2YU8zanFHaUFWeERKVWFkTVBlS2VHSm5uempTdnpKbGdOVHV3c3grRnF5L2dPMkwxMGowWmhDWi92dE9NelVjNjl3cGhKZm9FNzU3V3lOeFJOcThJc0Y1Tkg5Y0x0b3UvbUNxOTc3YnZPSkRrSURCN3lKWEJ6YUhVQkJuSXJra1Qyemg3bGJmUm5SREJUSFZraVZMazVESUxqeC9XL1BSZEZpUUM2SzRmZGx4Y29JbzlMcnM4ZFVWZkt2TTNNYnJ6c1hGT3ZtVVh0K3NsZldvd3UyTC9ndG9mRFhvTUJZZnlEcWIvWlRaRWZ0MC83blliRm1relBEUlZacU5SR0F3YWZVNTU1UjB2SWtNbGR2VjdKUzhNT1BNYWlXQVBpelNLRG4yRzNvcys1MzRFQytaOGZnWmFPVWpZL0xLME9vME9RMmhvNUV6MGNMYWpwUjFINk9FNEhvUm1ydjQzZkFjdGpYc0hYdi81RXg3emdrWk1NZXZhTFNEdjZtcjFGcDk4QXR4L296VTFGVDBoMDUxcVcwR0g2VWpRRXk5aExSZDBBMnFkUTRMZXpReDNvbDFTblhsamt2MG4zTXFlaFozOC94bzZhdHFDdkJtQkc3amlUdXd6YnlVUngzRm1TM0NCNllOYnFON3hPYVRZRnlkOEZDL01nY0xGQmMwS3F4MXllQ2VUd1hucldQb0dvdllVQlYxYjA1cWtIa1d5V0RUaCsveXJFNzF0RjNxbUQvd3F6cUJyNE04NERtWWVuQkdFOWxtb3FIZEMyWnRpK09KVFZKcmlHZWxQQ3RjZnZRaUlQcHdDZ3BFNmg1ekZhRndLajRuZGtBUkRpTC95L1EwWTZxNU5rM1g5RURlTmdjY1pIcFdmOUpKQ3M2a29wdXRtYjdDczIrbVJYdER1S09DaGY5UVUyN3Bmb1NJaklYK3NGdHY1c0hhSms2aHBZMlpzUUhzaTBYbFowc3FMTnQ5ayszdTVnYnBSU1JCczlHaC9BaVY0dkNyYTRkOTh5U0dCdzRSR1FhSStpQ29RaG9YK3lxc3VrYkx6bXJUU3FXMVRXaXJReUlHZ1Q5VnFERE1mUzAxeGdQSlNFSTlIWlp6TGlFVXVGMm1CMi81Y2dqaEFUaWQrdGV1UVB4aldhN2NSc2t5YUhuTENjQURVUU9ESUFPVjJDWXROcnAwY29ZL091S3ZzaXlJT0lacVJ5dE1PMGVNZ1ZJWTBzWmdxeVEycXlubUx0NDBmWmd3SFVyV245Zm9TYTNtMkVRTy9uOS8yU2NuelJWdVZpVnNjM0tCSElQL3AzNlJlSWowTGlNcCtPQ0p3SHlLVW1UeDRBU1V0dXVhWktlRHl1QjlxcXJuUEFNWUVCeElsTGFvdXMzV1pHakIrcW9ub3QvNmk1UE40bUZjbHFDcUxhMGJHbks4ZnJxYy9yd2tuVGV0YUE0c2tXTEw1L21qNEd5MitFQkh3a0x3UXd2K0FKdmZTOXYvNDl1LzY0N1ZFYW15UzdZQ2ZEUHNBQUREQ1FFcWJNQ1h2Ui8xVmEwWi9YUWhoNlkrZUt0MEVpRDdpNmRZODJtQkFoNEJMRmRVV3VGZHVrdUVwaGZ2WXB3N2loVjNxTjB1NFM1NTRXU0dUa0ZsdlpYNG1hbkF4a1g2ekQxS0NWaEFMdEJnSDgzdkhxam9uc0lwOFMydHgwZ0tiYzEreHVaRVppVWlNVVlVdTByQVFsRFcrZHJoN3lVRHZqekFHSnBmTk01eThaMW45em93VzZ5YW5VZWFBNjhSZDd5TUxobFd0NVh6bGhBTVZDZmZYZ0pFelR1YzJEbENVOXNMLzVTVkRaV2N4R1E5aFM1cnJtK2VyQ1Jxd2FJQk1DNUtza0RCZHdOWmh2Q0FCdEpqS2Vla1FUSjd5MFp4SGNhbGVCaU1rbkYwZVRDZzFvUEhPUVZLQ3V3NE94cHRZUS9xS1V0TEFIWFZ2OTlLMGRWcWZDMmpVQWlHQmVYa0t3aGRYTGtJYlZxU0EyZmxraXBBeEhYNnByUEExQjF3eTVab3hPUFg4RVExOW92eXpBbFg1dHU0OXEwWC9PSExFN1o5T1cxenltRXR6ZFpyNXJZbWtFcVdtcHVSNU5jeHFwTWlZam93dUNXZWhubzIyeG5JM09IQ0xDZkFKaHRrcklhL1hPc0tZRFpCRzFJMGJsN2taR2R5cEtUQlhYdXl6WE5WUlU5L005ejhaVytwdG1oZ2NOUzBJS2VaaSs5bFl4cWRlS3lnbldTTTV3czdSYUpmNlRRZTNSaWJZUjFvNkhwRzB2VHpiTEtQZTZnRjJGODdiWlBJei9mcTNLWnZiM3UrSnhZcCtJVjBtQi9VN29YelhRRk1RK3VmWllpNzUxbkx6WlVxRE1ybU53TFJPVUFNUk8rVnJtblkwSVB1cFBVMXc0b0hBb1dnVGRnTk5pNk1uTFQ4V0pmUlhjT0pKMk1lbUc2K2ZNeHNZUU52UVJwa1RGY05vaFV6Y3ZjcHJ3NUV3WEVZQTJzbzczL2MvY3RIRGcreU05YlF4REppUlltRnFydkhYb29hS1JyekxnUjZLVWdoM3ltaWxaQ0lSSm9KbTE3aEtHM1pxTTE0Lzl5OUc5OE9BZjNkVTlqMDk3aUNlaEc3a2VxYXRJQ2hFWmJqbmQ4Y00rS3djN2FtVWp2ekQzQmNvMHl3MDJxT054OWF3OGhSblZiWDZhdkRJbGhySHZ6SU44MzFvUjljRHBwMG1DUEJXZFVDQlNqVGJ1RkZqRC90WElSbGxlT2JraFFKSUdSNlE2U1MxcXkzT29WT1VheFl6THY0U2s3dndrQUMwUitGREVIeVFZbFVhbVVkTWcyUmdwRUdhSVd1V3IxaGNnRm10QmREV2g3ZFBuWTF0U3VKOC95MXp4NkRvN2ZJYmNFenBBK2E0ODNtRG5vemdld3VmaFdqVCsvUS85WlEreFQ5UWJBT1pQSXhHV3VhSXVrVk8zSWxvZDhJM1NGZFJCTHY5ZXBDNzFLeXpSdVlpMktkOHJ5NVNINit1WnMxUHlZUlpRakdDK3Q4VzRtSE82Z1lFRWVXSkJ1UWhnSHdmV2xhZXlWb3hac0NBQVZKRUllT3hPZDZtNW45OHRCUDdHTmgxT1M0eDRCS2FVN1A0UVQzNVVIZW5meE84WWFQUThmbXlobUJhSVJVZklBTVN2ZTJZRFp5SWNNTTkrN0tNSVVabzJ0eXRvYzdCOGVvZzBNaUkrVkpFdFg0c29FRjFSWkhQZVV3NWlCTjI4OTh2MmVTcGNnVUJhWHFzOUN5VlZtTVJQMEtLUDJ1REt4MUdJcUhjS0ZCOXVQVWRkQS9vT3dNa0tVUWsraFZVVDVPbEVMdjd1a0FBUEE0eE4rZkczVmYxeUVKV0FiVGx5dWtGcThjNXBTRkY1cXVHbUgwVmVpQzVvVEFka1VES3Z6WGhWWUs5c3BRYjNVZ1Z0Qld6N1ZScnlOUVVST3BIZU5xeDlhZHA4YWREWCtRSHJUKytYblN4VVI3SVdGanlNTkZJRWlMWmkxdks1UVVrZlRDUU9qdjh2SHdiUi9MRHF3Z3M5bXdsT3pPY0RLdVBVK0dTb2lnVFdRejRWN0N2SHRaVDI3WUdKVG44RFFFM3IzdjB4aWxvODJ2U3VXSDg0WEU3VEJsTUpFb2R5eDNDRngwVUVkc3VhRHBPSEV3UjZYNlUyU0xseERYSXVZeEhlNXh2NjI4bXU0bDRMSnBYUjhkYmljTEZKQW55Q0FVeDJLb2dDamt1cmU4bXNUZktDbG8wamFlN1hNR05PSk15b0ZYbVlHZUh2eGhNUGMzTEtYLy9VY1p0c3p3dFJrQmNFdURXQysvQWNWZVBOSHVOWWI5MEpIcnRucGg1ZDlhL1lpTkpzY1N3QTFwUVZrdW1TQWtPQWdLdWRzcnl3c0N3Zkg1anNydVpHUTJDd1hKRXQzUU4wU2NLUlVnT1NCQ3FYa1BqZDVSVzJuOFZpamt4anovbWptakhCNmk0eHM5NEU2Nzk5STAyaldYNVd3UDZhTFRaTGt5TjhxNDUxT0RmeUZVZEY5WWsyZXQ5VUpsV1NzRFJMSWVCd0ZyQkEyZTdyRWsybWFLVUNCRW5PUWM2bUhVMXQvZ3gzK1VXVVFXbkpMZVUxbWUvbkFEdy96UGUwd3d0Vm9BaERZdDBoR1hQblJydjFoUHRGS01CeWtqckg3a0J5U0R3WDlQMi9XZkNkQlE5K1J4cHRsR2hvRmdpMUs0NVlOeEpEd05wTmd5MDV2WXUzVUtrMkpRYVNGUzcwK0Y1NzluRE5RenZpK0pPRlRsdDFmWDJGNXk5NEV2NHZobWRQSmRVOFVVRjU2Ymx0emxKREVFdmsySlFrOTM0aHpwTXJGZ1d3ZHUxUkxxSEhCN2h2T2hnaHNqV0ZGY01zNjZaRUtWcVhKUytxWWNVMHk0akwySVQrNlF2N2pvQ3BWbUdzUWtGY1FyblhxOUJiOTdaUS96UCtwaldmWTU0UmNRVlMydUU1YURObVVyVkdLK3E0d0xRcUhuRVViT2puSHFFeGlacUtxOVdRaUtUK2c3QS96bVlIQ2k0YzFTejRNVWhHb0t6U2l4aXoxYUNJUEJXdy9vczR2cUVqbXgzOGx6YnV0OWNWbElzeGNkTUpUTERRK3ZOZ0YyY1ZRaVcxRTQ0d3lWcnI3TUFaOE9KRVpFSzlEZWt5MzJQUkFuSkRUVXVqdGFscmJ0T2VOczhyS09uTjcvNFRqUEwvZmRlbEI4bjA4WXdSNXdmbU42VGpGWUhRSDFjbUZmK1AvNUxVMTI4Q1pEYjNQUStxMlFJazV3aE40eGwvcy9lb29pallmeWtDcm5aSEhHWkluTGhoU2pWbk5ISWdTL203VWV0NlhBTDdvZUl5UFRLeHVnbDJzRWtUQzNnZ0tjTnFZR0E5U3ZlYVlaQ00vWHNQRUtQbWs3QmlRNmprWFBKaE1yREd4Vkc0SW9aSDgrYjBrUWJYR2l0Mkw0L3hZdHh1bTVzcFNPSjdsTDltVFpRNnBxM2JOaTEwZU1mZ0ZWaDc3NU5JRlc0SEp3U1FtaTU0bk11blZTQjhxdjZKc0w3SGlsZ2N0ZHFSNThTTjVad1lCa2dOR1hzYjA1QXJWemVXbHh1Y21BSHNPT3dyczFnMzh6bTRZN2ZPZmducmFhV1kxanZZOFlEODZQZThkZzR4cE5paTg3UnNDZk5WK2NKVmMraktFdnpuZVY1Zzd0RmlxZCtsZHp4STlKemdSS2t0WUV6RUpRSVU5M2UvclJaN1lrVkZtNVV1cjVhMWYzcG83T0VtYkJUc2MrQ1FaOGNnYmIvbUphRXJoa3NyL3JURjBNcjNxeDl5SlJWSEJ6YWNWd0dScEFRaURPdnJkWU4xQXBVOTRyR1lrVFVzdWs1YjE1Wll2QVZxRlRzVlVMaS9HY29mbEljMm01Z2RFTFZOblRmdXY1Zlk5S1NlWHFoUU80S0pOYVZmbHAwQ0VKYWFFZFNLUXJJNXRaT2w1RkE4VXZlNmxTWVd5TVk0REl4a1RiT1JoWHVBdzR6b1RTMjgrN3d2TXhydVBkZnlKbUJCTkhQdCtEYmdKNHovcHJZWUhpTmFMTXNZamtQZE44ajNKZDczQXJFZk92Um52MzYxSVVVMFg1RDc1dlRSdlpkbzMzWERzanRlOU4weUo3K2lIQnF1a1FJY2pIVW9ic2RQN0hOajBVYWNSMHIvTmRlVTlGNFBNc1VLY2t6Tk4rZGhyMVI2d1J2R1VZb1pDRWJaWlJMWEt4QnA3SElUNEVQUktHakIvdW1xTFhhMXl6RWx2QW1WQUJhMDFZN3dGdk4wM2Ywb25FbUhTM2w1d1paRmV6cjVibnN5T01XVGxhMU5kaW1ZNXNVeE15VFliZmc4dzB2cXNEc28zWFAxYndLdzZ3M3VIRGQ1UHBSWnVDSnR0eWk0ZzJGeWI0Ymg1UU42ZkdORTI2ekRGN1Y4QmJwZXJLNkFKQ0xTWm5kaDZMMTlPUTBram4xUGpEMGk4c1BZcGFXOWxVeVJkZElPKzRWQS9LemxPUzJ4M2s5VUtUdElsTTBUSVdtZXFIS0dYUVpocGpvVGI2VlNKN203cjZaaVlQMnVsQVVvZmVWL0o2eCtzckxEQXkyQ2ZFNnFrREZ1OU9NWDBBSXVnN3loQUtOMDRyT3hVNk5tcGtjOUZ4bXUvVS9vR3hHdmIzeFVFTDYwdE1sSE9EaWtqY1I5RDJrKzRwbEc1WnV0d0FIY2kwRU02WHRrVEhQOU5QMlRTR1VFN1E5SGYvU0VEc2V0a25hZXhvWmhDczJLWDFMeU5JS0U0N2pkMkR3MTUreDRRVXV0VUFTbzU5Q1lHMVFBeW9BVVhrV3dtbXkzTGdTUWp5T3ZLV25qaE8veWpPd0FyWGd0NFBrSVVnZDQ1N05ReFpMbU41K0J4NVJoQ0FHdkUxYmxOZjlMek9keGJiaG5VZ2Z1RDM5MXVSRkhjS2RYREY3ZmVqb3gveThtaWZJcTRWVzQyajBHQnFOQUtkK0prMnJCMW9hOTRiT2hxcVVzanhqWnlRaGRXTzhNblR6T2tOaGVpZXU2blYxcW5yZ3JHU2huWTNJMlczb29GNFNnczRjZ3drZ2h2dHpFa0xUbU5OUm83RTdudVRuMkxJcmlGSnlvTmZQdUp0aWN0S0JtNzRGZytkWVBTMlIzTzNmOWxBZWxiVWZjbzZGNU9EL3hkS1VuRTh0V3FOMExVcDlWQUptWVZYZFVDaGJ4MjM4MWtDaStLNDJoRzUydFNQYU1hb1dTb0xQY2Zrb24rc1pYdjdEdEtwZi9HTzdhcUMza1pzRGpva29haHJGZGJWSlNTZWhrNGp5K3RzRHplQnJKSjBrMVZrUnJHN1NoVHZjTmd1cjVucVRUTEE5dlJMQmJNTTlhNlI1NEZ0Z1pQOWFKMU1aMEdCcUVpMnF6Ui8yd2tYQlhwcFhZdi9TcU1RV1dhbTVsSHBMVktxaDN4ZHRjNFdmck9mYldsbU1PNXA5Z0JUSFp1YUcxVGFkZXFRVVpKQmZBS01ENFdSR0NsMDFaeDRTVzE0YzZrdnFKdXExL080N215L3RsVHlLWndpYlBkQTNRMVVGd0I3R2Z4anEwaDN2ckxFbUNrS3Vsc0VBUkN6UnZNVjJSVnBVbFpUV240Y1Boc0hjcTNROElHSUYyKy9nOENFSU4vMU8xcVMvMkpXcXlDNmtIb0w4Y2R2R0VHbmkxSTNDTk1JcXhxaHhJL1V0R3REc2VwYmwrSHI0elh4MzZna3BCbXBoT2xkTFVYTHAzVEtibVVZRWJSWHcvZmRmeFQ3WDdZUFhHQ0hHVG1uTzk4WkxDOTA2Zmkvekd2b04rNlpzbCs3MkpWMGxJWEo0V3dZdWxFUmZHbkFDWGNoa0Yzei9ITWR3elcwTUFFaXptQmwvREo2ZUoyU01PSG1Uc25YbElGRDRlcFRrYnFBQ0dpZ2I1UExFdHdQRVRjYkNRckM5YUtTU1FnSTdEZXd1aWlxM2J0Y0RUWkIzeEI5WWxlbmhpU0FXNjIwcmwzc2ZjY3d3eGFSOHBDV2Rzd0x3dmFxcDhjM01PV3RCc2xPcmVTSkNEcWgvdzBYbm1WMFJVWFpNM2JvUmkwVXhsaHVUeDFlM1NTd09pbTlOczNYV3NoTmI4Lzc3VkhnUWhRVFlSUU1NRllYaWRmMElCKzBtSUpocWNoQTlUeUY3dGRjSDhrUUJUSHNEWS96bFpqK3EwNlFMd0JkbTkxc3IyK3VzZmxlaXB3WUMrcmdiNHROVnA3VU5rYkVqTnR6ZWZsTi9VRTlkbHZtT2x6V1dtZkh2NGVkUGkzMmJmeUNRS1d6SGJVVEV3NU0yVFpsZnpNaTFWUjVsaDBxQ1lqaDNITUlmL2MwcHBKd2I1b1lFTnBBenlxbnlmdmlTV3lBYzc2L1l1VWwvb2FVaysrYzBZc2d1TGo5ZGFQdVVvemhoZ3VjSytQRGlNckI0ODU1Mk83VWg0aHRwNmZ3S2dJa1JCTVFIUTd6MmV5WXovV1AwQm9ZZVhjOGc3aUprclhFNzA1bFo1bXhGU0poT3E1WlNleVJSb21pUm41K3VRemM5ZFdWQjBYb2JURXdOc0VRM2FIZ25JY29BczY2UGplUT09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDI6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg== social-auth-core-4.6.1/social_core/tests/backends/data/saml_response_no_next_url.txt000066400000000000000000000421611500362547200310270ustar00rootroot00000000000000http://myapp.com/?RelayState=%7b%22idp%22%3a+%22testshib%22%7d&SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL215YXBwLmNvbSIgSUQ9Il8yNTk2NTFlOTY3ZGIwOGZjYTQ4MjdkODI3YWY1M2RkMCIgSW5SZXNwb25zZVRvPSJURVNUX0lEIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDUtMDlUMDM6NTc6NDMuNzkyWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI%2BaHR0cHM6Ly9pZHAudGVzdHNoaWIub3JnL2lkcC9zaGliYm9sZXRoPC9zYW1sMjpJc3N1ZXI%2BPHNhbWwycDpTdGF0dXM%2BPHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM%2BPHNhbWwyOkVuY3J5cHRlZEFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIElkPSJfMGM0NzYzNzIyOWFkNmEzMTY1OGU0MDc2ZDNlYzBmNmQiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiLz48ZHM6S2V5SW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI%2BPHhlbmM6RW5jcnlwdGVkS2V5IElkPSJfYjZmNmU2YWZjMzYyNGI3NmM1N2JmOWZhODA5YzAzNmMiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDc0RDQ0FobWdBd0lCQWdJSkFPN0J3ZGpEWmNVV01BMEdDU3FHU0liM0RRRUJCUVVBTUVVeEN6QUpCZ05WQkFZVEFrTkJNUmt3CkZ3WURWUVFJRXhCQ2NtbDBhWE5vSUVOdmJIVnRZbWxoTVJzd0dRWURWUVFLRXhKd2VYUm9iMjR0YzI5amFXRnNMV0YxZEdnd0hoY04KTVRVd05UQTRNRGMxT0RRMldoY05NalV3TlRBM01EYzFPRFEyV2pCRk1Rc3dDUVlEVlFRR0V3SkRRVEVaTUJjR0ExVUVDQk1RUW5KcApkR2x6YUNCRGIyeDFiV0pwWVRFYk1Ca0dBMVVFQ2hNU2NIbDBhRzl1TFhOdlkybGhiQzFoZFhSb01JR2ZNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNEdOQURDQmlRS0JnUUNxM2cxQ2wrM3VSNXZDbk40SGJnalRnK20zbkhodGVFTXliKyt5Y1pZcmUyYnhVZnNzaEVSNngzM2wKMjN0SGNrUll3bTdNZEJicnAzTHJWb2lPQ2RQYmxUbWwxSWhFUFRDd0tNaEJLdnZXcVR2Z2ZjU1NuUnpBV2tMbFFZU3VzYXl5Wks0bgo5cWNZa1Y1TUZuaTFyYmp4K01yNWFPRW1iNXUzM2FtTUtMd1NUd0lEQVFBQm80R25NSUdrTUIwR0ExVWREZ1FXQkJSUmlCUjZ6UzY2CmZLVm9rcDB5SkhiZ3YzUlltakIxQmdOVkhTTUViakJzZ0JSUmlCUjZ6UzY2ZktWb2twMHlKSGJndjNSWW1xRkpwRWN3UlRFTE1Ba0cKQTFVRUJoTUNRMEV4R1RBWEJnTlZCQWdURUVKeWFYUnBjMmdnUTI5c2RXMWlhV0V4R3pBWkJnTlZCQW9URW5CNWRHaHZiaTF6YjJOcApZV3d0WVhWMGFJSUpBTzdCd2RqRFpjVVdNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ1lFQUp3c01VM1lTCmF5YlZqdUo4VVMwZlVobFBPbE00MFFGQ0dMNHZCM1RFYmIyNE1xOEhyalV3clUwSkZQR2xzOWEyT1l6TjJCM2UzNU5vck11eHMrZ3IKR3RyMnlQNkx2dVgrblY2QTkzd2I0b29HSG9HZkM3VkxseXhTU25zOTM3U1M1UjFwelE0Z1d6Wm1hMktHV0tJQ1dwaDV6UTBBUlZoTAo2Mzk2N21HTG1vST08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BTElQdkVNVUVGeXhrVHowQ2N4QVA5TjV4Y3NYT2V4aVV4cXBvR2VIeVFMV0R5RVBBUDVnZ1daL3NLZ1ViL2xWSk92bCtuQXhSdVhXUlc5dGxSWWx3R2orRVhIOWhIbmdEY1BWMDNqSUJMQnFJbElBL1RmMGw4cVliOHFKRy9ZM0RTS2RQNkwvUURtYXBtTXpFM29YOEJxMW5Ea3YrUWh4cmQwMGVGK2ZMYVQ0PTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BRVpUWDhHTkM0My9yWStTUVlBMXRudHlUTTVVNkN2dUNCaktsVEVlekZPRjBZZHhCWUdFQVVjYU8xNVNKOXBMemJ1L1h0WGxzTkVMZTdKdEx4RUpwYUxubWFENnIranNWczdLaTBLNHRTMGNBUERDWHV2R1FoMmFOVjVQOGJ3N1JWUGhLOGQwYlJ1RklGR09FOHMwTTZYOUpxWDN4S0MvL1lSbVVoeDlybnU3ZWlwMGh5ZitPaUZiVGR2SDY2NTB2LzQ3aVdKcDNZeFlUV0QyMHBNbVRJMUpwWUEwYjByWVFQRkR0RU93d0JxYktxanRJc3ZYVFJzeXJhQkxvbnFOeHN5dHpEWHEra0JsMXp3WGUvSE5QcUVQblczdnNxaFhZcDVGM3dkWThkKzNCOTRMZlpOdUd4a0p3VDNzdVR0OGY5VHRBSlI4VytBUmtzT2M4eDBVaVNsVG5BNHFHOTBLMTR5dkVoVHcvd2drZjFXV01RT3dpZDNpakFYbUV4MU5MbVZvYUxYb3p4VExkTjN6YnJ6VEJIRXc3R2J3ZEdrdU5pMlhZOW16YUgwaWtGRm51VUxjMHUwc0pycEdGdzlaK0VlUk44RzNVUVZ5MjhtS2g3ZFBwWU5KbzhyajIxZFFaK2JaeUtTUHZablU3REkyakdJRE5US1g2ZkVyVWFINGlOTzN4cUU2Vk90L2d4T3BMNE5VNUhLV0Q0bG93VzcwdUJjVEVQRmhwaThpYUovdTB6YzUvTEhvdVBjMzByc1RLZFc5cmJLL2NWaHNQUHErZzA5WHZpZ0QweTJvN2tOc1pVL25tRXFiSzBKOTBrazhCR3I5cXRSczY4bUJnSURtUHVwUkhwWjM4eXNnU2VZN3V0VlVaSG5tQ0dzTzZ2NDJ6OTVOK05Pb3RCTEVZbFd1ZEdzYnowQWc4VkRDSlY5ak95QW95MDZyL1AyUHBsOFhjdmJza2d2T1BMMWdDNnVYbVJJS1lmOEw4UDJCNXVjN0haK0dtUHNOWXRLS2VKRDFFUHovdCt2NlBIbXNVb3dsSDhSd3FMRHdtMUF4dlNLQTR3UXBlQ0dQd3A5YXRYS0lWMS84NUZzRWMzajVzNjd6VlRybThrVEpydXV2MDZEdFVRZDNMOFdwTkV4cWhQait6RUp6U3RxSG04ckhNMVhNQUVxdVozc0xycTVqLzFSNlpqS0dOdFJCbjhwOE5ERGtrWm0vWTV5TXlJNXJJS3U5bnA3bXdaaEVpeWVHeHdxblV3VVMvUzVDRjNnMHVidnd4eVVnalVvd1ZvTkNqYktBbkdtT2VCSW5abkh0eGdIVUhVOUVlTFdyd2pRc3JtUmpJV0R2RkZQa3l6SzJDL20yaitubmNxc2E1OGRLVXZxcGR1VTRJYnNPQng3UGpXdXRBNmY5bXd6YWxyRU1NK0lGR3VPdk9HMC93eUdzQjZLREV6bldjUC83NkQ4angzaHZFSlAzN3REbFgreGM4Qno5TXdKdkd6VG4xbTdCb2xoR0lzSXlCTys1ZXpXa3RDWVVIUURGVE9wbXA0MDlOWHp6ZUNTUGY1U2NDWG5YYjRPd01ULy9VM1JFUnRRbGMrNmU2WG1JRjhoRkJVc0taUUJsS2ppSDkwZHlzYWlsNmN2V3UyQW55Q3QxbWxXcHFLc0MzU2RTRVZDTG1qRjlUQUFUMEtFSGdZQjg3RjZtZUpTTysvOXkyZkRuYVVvUUlUVzdubnVuSCtkT3dWSGZMU0wyL2N5YTltNlQzR29TSVNMbGJPMVRzalhKclVkZW55OTcvM2tkNmhFQlphdGY1U3NETFQ3SjNsQUVJNDROeXJ0NkIxQWdod2JNdkpqd1JNTXRNdUJLc3ltUytKVzc4UFNEWXQ4MG9waDJQTTc1N0tBNCtUMTAvYnZaQkE5Vk1OdVpqNVV3NXRWMnFIS3dwS0t6ZVVETUFiQlBRaGpYcXlQZzFKa09rd2RQMUpnOHRITjJTelBZQTlmT1htV0pBZGJDS2tMb0F4ZTV6cDZBUzYzS3FXMmFmSUt6SHJ3RTJmS1VtamppeURvMnNuMkJHbWtBaTRzbnpiVzc2SUQvSVgwd044aDBaQ2VRc29vKzdtb1RCMEJxSnBkS1MycXlsUktoc3BSTC9henVQdmxaK1pwckJxdXpJdEZkNFVLMkpzQkp6VXcwZkpxcTV1bk9PZENzVWM3SUU3QTNmZ1NmZ3NBd1R3WFZJMEVoME5ySWZpMkFKV1Z2VFpEMys2eFZ3dS96WWhuVjc0VXkvMFE4Mi8yQWtpSGpFRjNJVGNLWHdTNTB6bWtLakxjZDJqa2h5TUFYMWRoQ0wwZElFMUJoN0RNamVvNC9YbjBqSlpPL3Rrbi9xZmYzc3RNb1BYVG9KTnBIU1RjR2ZheGtaMzJYNCt3Q0xPc0VBRWxlMVZSY0kwUkZyOFhHTSsxWU9BTjBodFdGcFMxaG9kSi9OczJqL1FnUVNEemNpQ1FZeUFDd3lFRWZDZjZybnR0VmJyTlJQZWlmSHhBM3B2UnZ5ZGRhNDE5cXl0ZXI0akJ3cmw3ZUpuVnJ2VEprR2VhU2FRbDdXWk5SQXBscXRnNnZPYmpiMHZDRWlFaFhKbmNzQUhxcXp5QTRGeWFUVGQ2R0FySU9adUNxRWVoWk51T01lOVlrMVpya0VkR3pIalJESWk3Q1BKQk12NEZ4ZHI3bnJvN0I1WEhKb0ZMNE1DSUtOWWU2aWZiTUtYOU5uN1FWdnphUmY2UXlaSW1BWENQZndvU1BkN2x6NXl3UDJLSUIyaGhFMWt5eVZ5YVc5T0praWpUY3dvUnZrSXhIU0RqMXFqeGxueXh0QzhVZ1pNWmlwcGgzQXJpcjRiekIzUDhIbGIzejZ0OW51KzZMemNiN2ZObVo0UHluaU50Vk9OQ0lHbEh4dTBSY3hQK3cwUXNsM1BtTzJLaHBpc2RIanhvSUJ1YVY1NXdoTlFFNmdNNFBrT0xINDc4Rzg4bUxkd2s2RFpkWVl4L2d6RWE3b3ZIL0pReFp2TzRLdFVTNmZjZHJxV2thTFg1cEhkNkdneFBGZ2NFc2Nad1ZqM2hCS0xFQmE5L0dodERINEhzRnNRbmpPZnNDQkNzN0tjRitmTi9oSUdUeHFqTVlKVHJRYmNtdWF5dk9xR3RQMDFPcXltR24rVm5FSVkzKytQcm95SFN3K0Q0b0JIVG1maFNXRmJLZCtuTlVFS3BhRVIxNkdCU256WktQRVRVSmdRWEw5QWJRQ3RXVjFHb0UzRWNnMDZYaVd2aHFHakpGNldtdEU4dHY4Q25rZmxMNm91TDRvNldpbmx2WnNEdkZrS0R6TDkwUTNsWC9NanBtRTFpWU9uYzdISXdEVGwraFRRcHdsYXJiTDVUNGNkZTg1akNwYU0xU3p1TStiQU5zMHlXVDA0ZXJUVFc2cnhlbXFDTHAra202TVVMTlZOcE1CazBiQjJpRU82UlRtc3VpRlhDUU1xdU5xZjdkWXUwTFFCZzQ0MkJzU1pBV1ZrWEVZblduOURLdTRSby8veEFsb2h5VHozWlZmSkhuWVBSdDloSUErRHVUL3c4T2ZzTURIWnlCelUvL0JEa1NiNkxjMHdraVA3QlhIdjBoNVdud2dNWUxlZDBPalR5UWI2aGxpVnQ5b0FjaDRFVy9EZUlBdkpaQ1BYVm1pUFFYTGVsOVJIRko2bXFiYVo0TCtaZG1ONmQwcFZNZ1FveXhmQTR3dEwwYVpiNnFZYkhibjJMd2VBQVZwL3M2TzVlMVExdnZpZDRTWHo0a2l3RW1LSStIeXZEQ1pnekpQQVN5Z1gvWDJFWEZ0NGV3SjVmUFQyVXZmWnhQWlpqMFZGSFpyUFQwWVd2VE16bjUva3hoT09oM2drVGdDSmNwNWVsZnp4cEFPNFl1a0NoNHJXdVNndDRqVUJyaWNYbFdWdWo5U3JSZVhUalNHTktLK202NWovUDllNHRHT0RkMk9BbjNKTVQ3Q3FuaDhreTZpZjVjbmpVMmU3UDhTZnBONGwxWEFiZEZEcGk5bVJYamEyTzR1RWFHNGNvNW4xcWNDT3ZNMWYyblFBY1ZGNUFoSXhueS96TWhmU2l2RXdOQ0Zyd2tBWDRyQVE0WldUNldFakFyUG5jb1Y4Z1VRclhxQVA4NDJmK1lNWWI5RHFncmFicEg1a3ZuMnQzcWRldGJHODJ0QWlTamhPcUxNYW9iU2F4cXdWa1lUOHRTMW9rUUt2MWZoZ2t6elpEOE5IQnVQQzdNVHdXS0VCS2tDRUUzRWRFMXhNQURLd1B1M3NSaGpSaExXZyszZ2srejJtdlU4cTBhTlc0Y3hObUdoekx4eEY0Q3NFNStMQ1cwOWFpUVJOM1VvWmg1aktBZzBiMlh3WHBLS3pycUVTY1BYdnI0L1dWUTMyMm5qRWRvQVdXR0t2WnBKMlRlREo0eDdiT21LVElFc2RHWU1UZzFVaEU2eFFQcnhqS3dWeGFJNVJyaVE4a0xpaGgwa0t0WHQvYTVsSDhzUjVwR0ZISGZ3dlNVb3liQTB1eUVDNnNRVitPbTVReUZmRmpqZHFCOGNpOGxQS1hLTHFCTHJ6bjNmUkh3TmQwbzFiRTg0aGllTkx5UlhZVmhrRCtFNEpGaVd3ZWt3U3VWM3BjQk9ybnRVU3RoWmx6M3hIUURUVGNJNWliOFJyQ2swZEZ6YTgvQmw3VUdtWlUwSXZ2UmdvVXF2TXNHT2dMY3pGWmRpZnJ5aGNiUTY4a2ZzZ3lCMHppdC9MN1BSV3V4RkdYdDFoTVZSVUZ3WXBJS04zVkI3cXVKZlgwamZsU1JaRndMaXdlK3VhYndmTVZ6c2doajUvOXZNNzcwK0JaMGtJcE45NzBTMG5BbHl6R0h0aW1nTUl1RXFhbUt5QTNTQlI1aHZIYmRyNENnTHFUbXIzbFFnWmpnSkNvN1FXYUJWTXdCR0RpdzVOVVhUUnBycWc4U3h2eDlnNWZwbXMrL0o2QjFEelNTM3ZRZzgxdHFRU1ZDWVJpc0Y3M2VqZlFuZk4zcUszd3RJRDkxQnRISmFvMEFaUUdKVFpKOXVsZ0kzV3hzdWR4ejB0VHVpNlJlSWpmSWsxekZRdFpwRExGMnB3NGpTQVdQTlJqNDBYdVIrRzFUVlI3OVFiME9FYkw4RDFoTU5zWmo3MTZNbUhSOTlKaUxNdm1FWHV5a1V4VGhGYjRMTzZVbW1kU3UwTlBpMXQ2NmNkYURpQWhMaVBFTGdUNkZsenA2T2FGSGNSNjRncEtyemtTNDJONEhJeFpNa2R6M0FsYkRhK2pOWHZPR1l3UWl5K0xNNENZWGtrTWtHR3ZTWis5R2xWQ0l5RXBJaXIzbEQ3bmdzZGk4emxGWDYvekNaczlQSUtwZFZlSGJGZi9GS20wV3AreHI0Ykd0R0RrVHR2Nk1Manh2YU8zanFHaUFWeERKVWFkTVBlS2VHSm5uempTdnpKbGdOVHV3c3grRnF5L2dPMkwxMGowWmhDWi92dE9NelVjNjl3cGhKZm9FNzU3V3lOeFJOcThJc0Y1Tkg5Y0x0b3UvbUNxOTc3YnZPSkRrSURCN3lKWEJ6YUhVQkJuSXJra1Qyemg3bGJmUm5SREJUSFZraVZMazVESUxqeC9XL1BSZEZpUUM2SzRmZGx4Y29JbzlMcnM4ZFVWZkt2TTNNYnJ6c1hGT3ZtVVh0K3NsZldvd3UyTC9ndG9mRFhvTUJZZnlEcWIvWlRaRWZ0MC83blliRm1relBEUlZacU5SR0F3YWZVNTU1UjB2SWtNbGR2VjdKUzhNT1BNYWlXQVBpelNLRG4yRzNvcys1MzRFQytaOGZnWmFPVWpZL0xLME9vME9RMmhvNUV6MGNMYWpwUjFINk9FNEhvUm1ydjQzZkFjdGpYc0hYdi81RXg3emdrWk1NZXZhTFNEdjZtcjFGcDk4QXR4L296VTFGVDBoMDUxcVcwR0g2VWpRRXk5aExSZDBBMnFkUTRMZXpReDNvbDFTblhsamt2MG4zTXFlaFozOC94bzZhdHFDdkJtQkc3amlUdXd6YnlVUngzRm1TM0NCNllOYnFON3hPYVRZRnlkOEZDL01nY0xGQmMwS3F4MXllQ2VUd1hucldQb0dvdllVQlYxYjA1cWtIa1d5V0RUaCsveXJFNzF0RjNxbUQvd3F6cUJyNE04NERtWWVuQkdFOWxtb3FIZEMyWnRpK09KVFZKcmlHZWxQQ3RjZnZRaUlQcHdDZ3BFNmg1ekZhRndLajRuZGtBUkRpTC95L1EwWTZxNU5rM1g5RURlTmdjY1pIcFdmOUpKQ3M2a29wdXRtYjdDczIrbVJYdER1S09DaGY5UVUyN3Bmb1NJaklYK3NGdHY1c0hhSms2aHBZMlpzUUhzaTBYbFowc3FMTnQ5ayszdTVnYnBSU1JCczlHaC9BaVY0dkNyYTRkOTh5U0dCdzRSR1FhSStpQ29RaG9YK3lxc3VrYkx6bXJUU3FXMVRXaXJReUlHZ1Q5VnFERE1mUzAxeGdQSlNFSTlIWlp6TGlFVXVGMm1CMi81Y2dqaEFUaWQrdGV1UVB4aldhN2NSc2t5YUhuTENjQURVUU9ESUFPVjJDWXROcnAwY29ZL091S3ZzaXlJT0lacVJ5dE1PMGVNZ1ZJWTBzWmdxeVEycXlubUx0NDBmWmd3SFVyV245Zm9TYTNtMkVRTy9uOS8yU2NuelJWdVZpVnNjM0tCSElQL3AzNlJlSWowTGlNcCtPQ0p3SHlLVW1UeDRBU1V0dXVhWktlRHl1QjlxcXJuUEFNWUVCeElsTGFvdXMzV1pHakIrcW9ub3QvNmk1UE40bUZjbHFDcUxhMGJHbks4ZnJxYy9yd2tuVGV0YUE0c2tXTEw1L21qNEd5MitFQkh3a0x3UXd2K0FKdmZTOXYvNDl1LzY0N1ZFYW15UzdZQ2ZEUHNBQUREQ1FFcWJNQ1h2Ui8xVmEwWi9YUWhoNlkrZUt0MEVpRDdpNmRZODJtQkFoNEJMRmRVV3VGZHVrdUVwaGZ2WXB3N2loVjNxTjB1NFM1NTRXU0dUa0ZsdlpYNG1hbkF4a1g2ekQxS0NWaEFMdEJnSDgzdkhxam9uc0lwOFMydHgwZ0tiYzEreHVaRVppVWlNVVlVdTByQVFsRFcrZHJoN3lVRHZqekFHSnBmTk01eThaMW45em93VzZ5YW5VZWFBNjhSZDd5TUxobFd0NVh6bGhBTVZDZmZYZ0pFelR1YzJEbENVOXNMLzVTVkRaV2N4R1E5aFM1cnJtK2VyQ1Jxd2FJQk1DNUtza0RCZHdOWmh2Q0FCdEpqS2Vla1FUSjd5MFp4SGNhbGVCaU1rbkYwZVRDZzFvUEhPUVZLQ3V3NE94cHRZUS9xS1V0TEFIWFZ2OTlLMGRWcWZDMmpVQWlHQmVYa0t3aGRYTGtJYlZxU0EyZmxraXBBeEhYNnByUEExQjF3eTVab3hPUFg4RVExOW92eXpBbFg1dHU0OXEwWC9PSExFN1o5T1cxenltRXR6ZFpyNXJZbWtFcVdtcHVSNU5jeHFwTWlZam93dUNXZWhubzIyeG5JM09IQ0xDZkFKaHRrcklhL1hPc0tZRFpCRzFJMGJsN2taR2R5cEtUQlhYdXl6WE5WUlU5L005ejhaVytwdG1oZ2NOUzBJS2VaaSs5bFl4cWRlS3lnbldTTTV3czdSYUpmNlRRZTNSaWJZUjFvNkhwRzB2VHpiTEtQZTZnRjJGODdiWlBJei9mcTNLWnZiM3UrSnhZcCtJVjBtQi9VN29YelhRRk1RK3VmWllpNzUxbkx6WlVxRE1ybU53TFJPVUFNUk8rVnJtblkwSVB1cFBVMXc0b0hBb1dnVGRnTk5pNk1uTFQ4V0pmUlhjT0pKMk1lbUc2K2ZNeHNZUU52UVJwa1RGY05vaFV6Y3ZjcHJ3NUV3WEVZQTJzbzczL2MvY3RIRGcreU05YlF4REppUlltRnFydkhYb29hS1JyekxnUjZLVWdoM3ltaWxaQ0lSSm9KbTE3aEtHM1pxTTE0Lzl5OUc5OE9BZjNkVTlqMDk3aUNlaEc3a2VxYXRJQ2hFWmJqbmQ4Y00rS3djN2FtVWp2ekQzQmNvMHl3MDJxT054OWF3OGhSblZiWDZhdkRJbGhySHZ6SU44MzFvUjljRHBwMG1DUEJXZFVDQlNqVGJ1RkZqRC90WElSbGxlT2JraFFKSUdSNlE2U1MxcXkzT29WT1VheFl6THY0U2s3dndrQUMwUitGREVIeVFZbFVhbVVkTWcyUmdwRUdhSVd1V3IxaGNnRm10QmREV2g3ZFBuWTF0U3VKOC95MXp4NkRvN2ZJYmNFenBBK2E0ODNtRG5vemdld3VmaFdqVCsvUS85WlEreFQ5UWJBT1pQSXhHV3VhSXVrVk8zSWxvZDhJM1NGZFJCTHY5ZXBDNzFLeXpSdVlpMktkOHJ5NVNINit1WnMxUHlZUlpRakdDK3Q4VzRtSE82Z1lFRWVXSkJ1UWhnSHdmV2xhZXlWb3hac0NBQVZKRUllT3hPZDZtNW45OHRCUDdHTmgxT1M0eDRCS2FVN1A0UVQzNVVIZW5meE84WWFQUThmbXlobUJhSVJVZklBTVN2ZTJZRFp5SWNNTTkrN0tNSVVabzJ0eXRvYzdCOGVvZzBNaUkrVkpFdFg0c29FRjFSWkhQZVV3NWlCTjI4OTh2MmVTcGNnVUJhWHFzOUN5VlZtTVJQMEtLUDJ1REt4MUdJcUhjS0ZCOXVQVWRkQS9vT3dNa0tVUWsraFZVVDVPbEVMdjd1a0FBUEE0eE4rZkczVmYxeUVKV0FiVGx5dWtGcThjNXBTRkY1cXVHbUgwVmVpQzVvVEFka1VES3Z6WGhWWUs5c3BRYjNVZ1Z0Qld6N1ZScnlOUVVST3BIZU5xeDlhZHA4YWREWCtRSHJUKytYblN4VVI3SVdGanlNTkZJRWlMWmkxdks1UVVrZlRDUU9qdjh2SHdiUi9MRHF3Z3M5bXdsT3pPY0RLdVBVK0dTb2lnVFdRejRWN0N2SHRaVDI3WUdKVG44RFFFM3IzdjB4aWxvODJ2U3VXSDg0WEU3VEJsTUpFb2R5eDNDRngwVUVkc3VhRHBPSEV3UjZYNlUyU0xseERYSXVZeEhlNXh2NjI4bXU0bDRMSnBYUjhkYmljTEZKQW55Q0FVeDJLb2dDamt1cmU4bXNUZktDbG8wamFlN1hNR05PSk15b0ZYbVlHZUh2eGhNUGMzTEtYLy9VY1p0c3p3dFJrQmNFdURXQysvQWNWZVBOSHVOWWI5MEpIcnRucGg1ZDlhL1lpTkpzY1N3QTFwUVZrdW1TQWtPQWdLdWRzcnl3c0N3Zkg1anNydVpHUTJDd1hKRXQzUU4wU2NLUlVnT1NCQ3FYa1BqZDVSVzJuOFZpamt4anovbWptakhCNmk0eHM5NEU2Nzk5STAyaldYNVd3UDZhTFRaTGt5TjhxNDUxT0RmeUZVZEY5WWsyZXQ5VUpsV1NzRFJMSWVCd0ZyQkEyZTdyRWsybWFLVUNCRW5PUWM2bUhVMXQvZ3gzK1VXVVFXbkpMZVUxbWUvbkFEdy96UGUwd3d0Vm9BaERZdDBoR1hQblJydjFoUHRGS01CeWtqckg3a0J5U0R3WDlQMi9XZkNkQlE5K1J4cHRsR2hvRmdpMUs0NVlOeEpEd05wTmd5MDV2WXUzVUtrMkpRYVNGUzcwK0Y1NzluRE5RenZpK0pPRlRsdDFmWDJGNXk5NEV2NHZobWRQSmRVOFVVRjU2Ymx0emxKREVFdmsySlFrOTM0aHpwTXJGZ1d3ZHUxUkxxSEhCN2h2T2hnaHNqV0ZGY01zNjZaRUtWcVhKUytxWWNVMHk0akwySVQrNlF2N2pvQ3BWbUdzUWtGY1FyblhxOUJiOTdaUS96UCtwaldmWTU0UmNRVlMydUU1YURObVVyVkdLK3E0d0xRcUhuRVViT2puSHFFeGlacUtxOVdRaUtUK2c3QS96bVlIQ2k0YzFTejRNVWhHb0t6U2l4aXoxYUNJUEJXdy9vczR2cUVqbXgzOGx6YnV0OWNWbElzeGNkTUpUTERRK3ZOZ0YyY1ZRaVcxRTQ0d3lWcnI3TUFaOE9KRVpFSzlEZWt5MzJQUkFuSkRUVXVqdGFscmJ0T2VOczhyS09uTjcvNFRqUEwvZmRlbEI4bjA4WXdSNXdmbU42VGpGWUhRSDFjbUZmK1AvNUxVMTI4Q1pEYjNQUStxMlFJazV3aE40eGwvcy9lb29pallmeWtDcm5aSEhHWkluTGhoU2pWbk5ISWdTL203VWV0NlhBTDdvZUl5UFRLeHVnbDJzRWtUQzNnZ0tjTnFZR0E5U3ZlYVlaQ00vWHNQRUtQbWs3QmlRNmprWFBKaE1yREd4Vkc0SW9aSDgrYjBrUWJYR2l0Mkw0L3hZdHh1bTVzcFNPSjdsTDltVFpRNnBxM2JOaTEwZU1mZ0ZWaDc3NU5JRlc0SEp3U1FtaTU0bk11blZTQjhxdjZKc0w3SGlsZ2N0ZHFSNThTTjVad1lCa2dOR1hzYjA1QXJWemVXbHh1Y21BSHNPT3dyczFnMzh6bTRZN2ZPZmducmFhV1kxanZZOFlEODZQZThkZzR4cE5paTg3UnNDZk5WK2NKVmMraktFdnpuZVY1Zzd0RmlxZCtsZHp4STlKemdSS2t0WUV6RUpRSVU5M2UvclJaN1lrVkZtNVV1cjVhMWYzcG83T0VtYkJUc2MrQ1FaOGNnYmIvbUphRXJoa3NyL3JURjBNcjNxeDl5SlJWSEJ6YWNWd0dScEFRaURPdnJkWU4xQXBVOTRyR1lrVFVzdWs1YjE1Wll2QVZxRlRzVlVMaS9HY29mbEljMm01Z2RFTFZOblRmdXY1Zlk5S1NlWHFoUU80S0pOYVZmbHAwQ0VKYWFFZFNLUXJJNXRaT2w1RkE4VXZlNmxTWVd5TVk0REl4a1RiT1JoWHVBdzR6b1RTMjgrN3d2TXhydVBkZnlKbUJCTkhQdCtEYmdKNHovcHJZWUhpTmFMTXNZamtQZE44ajNKZDczQXJFZk92Um52MzYxSVVVMFg1RDc1dlRSdlpkbzMzWERzanRlOU4weUo3K2lIQnF1a1FJY2pIVW9ic2RQN0hOajBVYWNSMHIvTmRlVTlGNFBNc1VLY2t6Tk4rZGhyMVI2d1J2R1VZb1pDRWJaWlJMWEt4QnA3SElUNEVQUktHakIvdW1xTFhhMXl6RWx2QW1WQUJhMDFZN3dGdk4wM2Ywb25FbUhTM2w1d1paRmV6cjVibnN5T01XVGxhMU5kaW1ZNXNVeE15VFliZmc4dzB2cXNEc28zWFAxYndLdzZ3M3VIRGQ1UHBSWnVDSnR0eWk0ZzJGeWI0Ymg1UU42ZkdORTI2ekRGN1Y4QmJwZXJLNkFKQ0xTWm5kaDZMMTlPUTBram4xUGpEMGk4c1BZcGFXOWxVeVJkZElPKzRWQS9LemxPUzJ4M2s5VUtUdElsTTBUSVdtZXFIS0dYUVpocGpvVGI2VlNKN203cjZaaVlQMnVsQVVvZmVWL0o2eCtzckxEQXkyQ2ZFNnFrREZ1OU9NWDBBSXVnN3loQUtOMDRyT3hVNk5tcGtjOUZ4bXUvVS9vR3hHdmIzeFVFTDYwdE1sSE9EaWtqY1I5RDJrKzRwbEc1WnV0d0FIY2kwRU02WHRrVEhQOU5QMlRTR1VFN1E5SGYvU0VEc2V0a25hZXhvWmhDczJLWDFMeU5JS0U0N2pkMkR3MTUreDRRVXV0VUFTbzU5Q1lHMVFBeW9BVVhrV3dtbXkzTGdTUWp5T3ZLV25qaE8veWpPd0FyWGd0NFBrSVVnZDQ1N05ReFpMbU41K0J4NVJoQ0FHdkUxYmxOZjlMek9keGJiaG5VZ2Z1RDM5MXVSRkhjS2RYREY3ZmVqb3gveThtaWZJcTRWVzQyajBHQnFOQUtkK0prMnJCMW9hOTRiT2hxcVVzanhqWnlRaGRXTzhNblR6T2tOaGVpZXU2blYxcW5yZ3JHU2huWTNJMlczb29GNFNnczRjZ3drZ2h2dHpFa0xUbU5OUm83RTdudVRuMkxJcmlGSnlvTmZQdUp0aWN0S0JtNzRGZytkWVBTMlIzTzNmOWxBZWxiVWZjbzZGNU9EL3hkS1VuRTh0V3FOMExVcDlWQUptWVZYZFVDaGJ4MjM4MWtDaStLNDJoRzUydFNQYU1hb1dTb0xQY2Zrb24rc1pYdjdEdEtwZi9HTzdhcUMza1pzRGpva29haHJGZGJWSlNTZWhrNGp5K3RzRHplQnJKSjBrMVZrUnJHN1NoVHZjTmd1cjVucVRUTEE5dlJMQmJNTTlhNlI1NEZ0Z1pQOWFKMU1aMEdCcUVpMnF6Ui8yd2tYQlhwcFhZdi9TcU1RV1dhbTVsSHBMVktxaDN4ZHRjNFdmck9mYldsbU1PNXA5Z0JUSFp1YUcxVGFkZXFRVVpKQmZBS01ENFdSR0NsMDFaeDRTVzE0YzZrdnFKdXExL080N215L3RsVHlLWndpYlBkQTNRMVVGd0I3R2Z4anEwaDN2ckxFbUNrS3Vsc0VBUkN6UnZNVjJSVnBVbFpUV240Y1Boc0hjcTNROElHSUYyKy9nOENFSU4vMU8xcVMvMkpXcXlDNmtIb0w4Y2R2R0VHbmkxSTNDTk1JcXhxaHhJL1V0R3REc2VwYmwrSHI0elh4MzZna3BCbXBoT2xkTFVYTHAzVEtibVVZRWJSWHcvZmRmeFQ3WDdZUFhHQ0hHVG1uTzk4WkxDOTA2Zmkvekd2b04rNlpzbCs3MkpWMGxJWEo0V3dZdWxFUmZHbkFDWGNoa0Yzei9ITWR3elcwTUFFaXptQmwvREo2ZUoyU01PSG1Uc25YbElGRDRlcFRrYnFBQ0dpZ2I1UExFdHdQRVRjYkNRckM5YUtTU1FnSTdEZXd1aWlxM2J0Y0RUWkIzeEI5WWxlbmhpU0FXNjIwcmwzc2ZjY3d3eGFSOHBDV2Rzd0x3dmFxcDhjM01PV3RCc2xPcmVTSkNEcWgvdzBYbm1WMFJVWFpNM2JvUmkwVXhsaHVUeDFlM1NTd09pbTlOczNYV3NoTmI4Lzc3VkhnUWhRVFlSUU1NRllYaWRmMElCKzBtSUpocWNoQTlUeUY3dGRjSDhrUUJUSHNEWS96bFpqK3EwNlFMd0JkbTkxc3IyK3VzZmxlaXB3WUMrcmdiNHROVnA3VU5rYkVqTnR6ZWZsTi9VRTlkbHZtT2x6V1dtZkh2NGVkUGkzMmJmeUNRS1d6SGJVVEV3NU0yVFpsZnpNaTFWUjVsaDBxQ1lqaDNITUlmL2MwcHBKd2I1b1lFTnBBenlxbnlmdmlTV3lBYzc2L1l1VWwvb2FVaysrYzBZc2d1TGo5ZGFQdVVvemhoZ3VjSytQRGlNckI0ODU1Mk83VWg0aHRwNmZ3S2dJa1JCTVFIUTd6MmV5WXovV1AwQm9ZZVhjOGc3aUprclhFNzA1bFo1bXhGU0poT3E1WlNleVJSb21pUm41K3VRemM5ZFdWQjBYb2JURXdOc0VRM2FIZ25JY29BczY2UGplUT09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDI6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg== social-auth-core-4.6.1/social_core/tests/backends/legacy.py000066400000000000000000000027521500362547200237070ustar00rootroot00000000000000import requests import responses from ...utils import parse_qs from .base import BaseBackendTest class BaseLegacyTest(BaseBackendTest): form = "" response_body = "" def setUp(self): super().setUp() self.strategy.set_settings( { f"SOCIAL_AUTH_{self.name}_FORM_URL": self.strategy.build_absolute_uri( f"/login/{self.backend.name}" ) } ) def extra_settings(self): return {f"SOCIAL_AUTH_{self.name}_FORM_URL": f"/login/{self.backend.name}"} def do_start(self): start_url = self.strategy.build_absolute_uri(self.backend.start().url) complete_url = self.complete_url assert complete_url, "Subclasses must set the complete_url attribute" responses.add( responses.GET, start_url, status=200, body=self.form.format(complete_url), ) responses.add( responses.POST, complete_url, status=200, body=self.response_body, content_type="application/x-www-form-urlencoded", ) response = requests.get(start_url, timeout=1) self.assertEqual(response.text, self.form.format(complete_url)) response = requests.post( complete_url, data=parse_qs(self.response_body), timeout=1 ) self.strategy.set_request_data(parse_qs(response.text), self.backend) return self.backend.complete() social-auth-core-4.6.1/social_core/tests/backends/oauth.py000066400000000000000000000204621500362547200235610ustar00rootroot00000000000000from __future__ import annotations from typing import Generic, TypeVar, cast from unittest.mock import patch from urllib.parse import urlparse import requests import responses from ...backends.oauth import BaseOAuth1, BaseOAuth2, OAuthAuth from ...utils import get_querystring, parse_qs, url_add_parameters from ..models import User from .base import BaseBackendTest OAuthBackendT = TypeVar("OAuthBackendT", bound=OAuthAuth) class BaseOAuthTest(BaseBackendTest[OAuthBackendT], Generic[OAuthBackendT]): user_data_body: str | None = None user_data_url: str = "" user_data_url_post: bool = False user_data_content_type: str = "application/json" access_token_body: str | None = None access_token_status: int = 200 def extra_settings(self): assert self.name, "Subclasses must set the name attribute" return { "SOCIAL_AUTH_" + self.name + "_KEY": "a-key", "SOCIAL_AUTH_" + self.name + "_SECRET": "a-secret-key", } def _method(self, method): return {"GET": responses.GET, "POST": responses.POST}[method] def handle_state(self, start_url: str, target_url: str) -> str: start_query = parse_qs(urlparse(start_url).query) redirect_uri = start_query.get("redirect_uri") if getattr(self.backend, "STATE_PARAMETER", False) and start_query.get("state"): target_url = url_add_parameters(target_url, {"state": start_query["state"]}) if redirect_uri and getattr(self.backend, "REDIRECT_STATE", False): redirect_query = parse_qs(urlparse(redirect_uri).query) if redirect_query.get("redirect_state"): target_url = url_add_parameters( target_url, {"redirect_state": redirect_query["redirect_state"]} ) return target_url def auth_handlers(self, start_url: str) -> str: target_url = self.handle_state( start_url, self.strategy.build_absolute_uri(self.complete_url) ) responses.add( responses.GET, start_url, status=301, headers={"Location": target_url}, ) responses.add(responses.GET, target_url, status=200, body="foobar") if self.user_data_url: responses.add( responses.POST if self.user_data_url_post else responses.GET, self.user_data_url, body=self.user_data_body or "", content_type=self.user_data_content_type, ) return target_url def pre_complete_callback(self, start_url): responses.add( self._method(self.backend.ACCESS_TOKEN_METHOD), url=self.backend.access_token_url(), status=self.access_token_status, body=self.access_token_body or "", content_type="application/json", ) def do_start(self): start_url = self.backend.start().url target_url = self.auth_handlers(start_url) response = requests.get(start_url, timeout=1) self.assertEqual(response.url, target_url) self.assertEqual(response.text, "foobar") self.strategy.set_request_data( parse_qs(urlparse(start_url).query), self.backend ) self.strategy.set_request_data( parse_qs(urlparse(target_url).query), self.backend ) self.pre_complete_callback(start_url) return self.backend.complete() BaseOAuth1BackendT = TypeVar("BaseOAuth1BackendT", bound=BaseOAuth1) class OAuth1Test(BaseOAuthTest[BaseOAuth1BackendT], Generic[BaseOAuth1BackendT]): request_token_body: str raw_complete_url = "/complete/{0}/?oauth_verifier=bazqux&oauth_token=foobar" def request_token_handler(self): assert self.request_token_body, "Subclasses must set request_token_body" responses.add( self._method(self.backend.REQUEST_TOKEN_METHOD), self.backend.REQUEST_TOKEN_URL, body=self.request_token_body, status=200, ) def do_start(self): self.request_token_handler() return super().do_start() BaseOAuth2BackendT = TypeVar("BaseOAuth2BackendT", bound=BaseOAuth2) class OAuth2Test(BaseOAuthTest[BaseOAuth2BackendT], Generic[BaseOAuth2BackendT]): raw_complete_url = "/complete/{0}/?code=foobar" refresh_token_body = "" def refresh_token_arguments(self): return {} def do_refresh_token(self): self.do_login() responses.add( self._method(self.backend.REFRESH_TOKEN_METHOD), self.backend.refresh_token_url(), status=200, body=self.refresh_token_body, ) user = next(iter(User.cache.values())) social = user.social[0] social.refresh_token(strategy=self.strategy, **self.refresh_token_arguments()) return user, social class OAuth2PkcePlainTest(OAuth2Test): def extra_settings(self): settings = super().extra_settings() settings.update( {f"SOCIAL_AUTH_{self.name}_PKCE_CODE_CHALLENGE_METHOD": "plain"} ) return settings def do_login(self): user = super().do_login() auth_request = next( r.request for r in responses.calls if r.request.url.startswith(self.backend.authorization_url()) ) code_challenge = get_querystring(cast("str", auth_request.url)).get( "code_challenge" ) code_challenge_method = get_querystring(cast("str", auth_request.url)).get( "code_challenge_method" ) self.assertIsNotNone(code_challenge) self.assertEqual(code_challenge_method, "plain") auth_complete = next( r.request for r in responses.calls if r.request.url.startswith(self.backend.access_token_url()) ) code_verifier = parse_qs(auth_complete.body).get("code_verifier") self.assertEqual(code_challenge, code_verifier) return user class OAuth2PkceS256Test(OAuth2Test): def do_login(self): # use default value of PKCE_CODE_CHALLENGE_METHOD (s256) user = super().do_login() auth_request = next( r.request for r in responses.calls if r.request.url.startswith(self.backend.authorization_url()) ) code_challenge = get_querystring(cast("str", auth_request.url)).get( "code_challenge" ) code_challenge_method = get_querystring(cast("str", auth_request.url)).get( "code_challenge_method" ) self.assertIsNotNone(code_challenge) self.assertTrue(code_challenge_method in ["s256", "S256"]) auth_complete = next( r.request for r in responses.calls if r.request.url.startswith(self.backend.access_token_url()) ) code_verifier = parse_qs(auth_complete.body).get("code_verifier") self.assertEqual( self.backend.generate_code_challenge(code_verifier, code_challenge_method), code_challenge, ) return user class BaseAuthUrlTestMixin(Generic[OAuthBackendT]): backend: OAuthBackendT def check_parameters_in_authorization_url(self, auth_url_key="AUTHORIZATION_URL"): """ Check the parameters in authorization url When inserting parameters directly into AUTHORIZATION_URL, we expect the other parameters to be added to the end of the url """ original_url = ( self.backend.AUTHORIZATION_URL or self.backend.authorization_url() ) with ( patch.object( self.backend, "authorization_url", return_value=original_url + "?param1=value1¶m2=value2", ), patch.object( self.backend, auth_url_key, original_url + "?param1=value1¶m2=value2", ), ): # we expect an & symbol to join the different parameters assert "?param1=value1¶m2=value2&" in self.backend.auth_url() def test_auth_url_parameters(self): self.check_parameters_in_authorization_url() class OAuth1AuthUrlTestMixin(BaseAuthUrlTestMixin): def test_auth_url_parameters(self): self.request_token_handler() # type: ignore[attr-defined] self.check_parameters_in_authorization_url() social-auth-core-4.6.1/social_core/tests/backends/open_id.py000066400000000000000000000056151500362547200240610ustar00rootroot00000000000000# pyright: reportAttributeAccessIssue=false import sys from html.parser import HTMLParser import requests import responses from openid import oidutil from ...backends.utils import load_backends from ...utils import module_member, parse_qs from ..models import TestAssociation, TestNonce, TestStorage, TestUserSocialAuth, User from ..strategy import TestStrategy from .base import BaseBackendTest sys.path.insert(0, "..") # Patch to remove the too-verbose output until a new version is released oidutil.log = lambda *args, **kwargs: None class FormHTMLParser(HTMLParser): form = {} inputs = {} def handle_starttag(self, tag, attrs): attrs = dict(attrs) if tag == "form": self.form.update(attrs) elif tag == "input" and "name" in attrs: self.inputs[attrs["name"]] = attrs["value"] class OpenIdTest(BaseBackendTest): def setUp(self): responses.start() Backend = module_member(self.backend_path) self.strategy = TestStrategy(TestStorage) self.complete_url = self.raw_complete_url.format(Backend.name) self.backend = Backend(self.strategy, redirect_uri=self.complete_url) self.strategy.set_settings( { "SOCIAL_AUTH_AUTHENTICATION_BACKENDS": ( self.backend_path, "social_core.tests.backends.test_broken.BrokenBackendAuth", ) } ) # Force backends loading to trash PSA cache load_backends( self.strategy.get_setting("SOCIAL_AUTH_AUTHENTICATION_BACKENDS"), force_load=True, ) def tearDown(self): self.strategy = None User.reset_cache() TestUserSocialAuth.reset_cache() TestNonce.reset_cache() TestAssociation.reset_cache() responses.stop() responses.reset() def get_form_data(self, html): parser = FormHTMLParser() parser.feed(html) return parser.form, parser.inputs def openid_url(self): return self.backend.openid_url() def post_start(self): pass def do_start(self): responses.add( responses.GET, self.openid_url(), status=200, body=self.discovery_body, content_type="application/xrds+xml", ) start = self.backend.start() self.post_start() form, inputs = self.get_form_data(start) action = form.get("action") assert action, "The form action must be set in the test" responses.add(responses.POST, action, status=200, body=self.server_response) response = requests.post(action, data=inputs, timeout=1) self.strategy.set_request_data(parse_qs(response.content), self.backend) responses.add( responses.POST, form.get("action"), status=200, body="is_valid:true\n" ) return self.backend.complete() social-auth-core-4.6.1/social_core/tests/backends/open_id_connect.py000066400000000000000000000167661500362547200256030ustar00rootroot00000000000000from __future__ import annotations import base64 import datetime import json from calendar import timegm from typing import Generic, TypeVar from urllib.parse import urlparse import jwt import responses from social_core.backends.open_id_connect import OpenIdConnectAuth from ...exceptions import AuthTokenError from ...utils import parse_qs from .oauth import BaseAuthUrlTestMixin, OAuth2Test JWK_KEY = { "kty": "RSA", "d": "ZmswNokEvBcxW_Kvcy8mWUQOQCBdGbnM0xR7nhvGHC-Q24z3XAQWlMWbsmGc_R1o" "_F3zK7DBlc3BokdRaO1KJirNmnHCw5TlnBlJrXiWpFBtVglUg98-4sRRO0VWnGXK" "JPOkBQ6b_DYRO3b0o8CSpWowpiV6HB71cjXTqKPZf-aXU9WjCCAtxVjfIxgQFu5I" "-G1Qah8mZeY8HK_y99L4f0siZcbUoaIcfeWBhxi14ODyuSAHt0sNEkhiIVBZE7QZ" "m-SEP1ryT9VAaljbwHHPmg7NC26vtLZhvaBGbTTJnEH0ZubbN2PMzsfeNyoCIHy4" "4QDSpQDCHfgcGOlHY_t5gQ", "e": "AQAB", "use": "sig", "kid": "testkey", "alg": "RS256", "n": "pUfcJ8WFrVue98Ygzb6KEQXHBzi8HavCu8VENB2As943--bHPcQ-nScXnrRFAUg8" "H5ZltuOcHWvsGw_AQifSLmOCSWJAPkdNb0w0QzY7Re8NrPjCsP58Tytp5LicF0Ao" "Ag28UK3JioY9hXHGvdZsWR1Rp3I-Z3nRBP6HyO18pEgcZ91c9aAzsqu80An9X4DA" "b1lExtZorvcd5yTBzZgr-MUeytVRni2lDNEpa6OFuopHXmg27Hn3oWAaQlbymd4g" "ifc01oahcwl3ze2tMK6gJxa_TdCf1y99Yq6oilmVvZJ8kwWWnbPE-oDmOVPVnEyT" "vYVCvN4rBT1DQ-x0F1mo2Q", } JWK_PUBLIC_KEY = {key: value for key, value in JWK_KEY.items() if key != "d"} OpenIdConnectAuthT = TypeVar("OpenIdConnectAuthT", bound=OpenIdConnectAuth) class OpenIdConnectTest( OAuth2Test[OpenIdConnectAuthT], BaseAuthUrlTestMixin, Generic[OpenIdConnectAuthT] ): """ Mixin to test OpenID Connect consumers. Inheriting classes should also inherit OAuth2Test. """ client_key = "a-key" client_secret = "a-secret-key" issuer: str # id_token issuer openid_config_body: str key: dict[str, str] # Avoid sharing access_token_kwargs between different subclasses def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) cls.access_token_kwargs = getattr(cls, "access_token_kwargs", {}) def setUp(self): if self.__class__.__name__ == "OpenIdConnectTest": self.skipTest("base class") super().setUp() self.key = JWK_KEY.copy() self.public_key = JWK_PUBLIC_KEY.copy() assert self.openid_config_body, "openid_config_body must be set" responses.add( responses.GET, self.backend.oidc_endpoint() + "/.well-known/openid-configuration", status=200, body=self.openid_config_body, ) oidc_config = json.loads(self.openid_config_body) def jwks(_request, _uri, headers): return 200, headers, json.dumps({"keys": [self.key]}) responses.add( responses.GET, oidc_config.get("jwks_uri"), status=200, body=json.dumps({"keys": [self.public_key]}), ) def extra_settings(self): settings = super().extra_settings() settings.update( { f"SOCIAL_AUTH_{self.name}_KEY": self.client_key, f"SOCIAL_AUTH_{self.name}_SECRET": self.client_secret, f"SOCIAL_AUTH_{self.name}_ID_TOKEN_DECRYPTION_KEY": self.client_secret, } ) return settings def get_id_token( self, client_key=None, expiration_datetime=None, issue_datetime=None, nonce=None, issuer=None, ): """ Return the id_token to be added to the access token body. """ return { "iss": issuer, "nonce": nonce, "aud": client_key, "azp": client_key, "exp": expiration_datetime, "iat": issue_datetime, "sub": "1234", } def prepare_access_token_body( self, client_key=None, tamper_message=False, expiration_datetime=None, kid=None, issue_datetime=None, nonce=None, issuer=None, ): """ Prepares a provider access token response. Arguments: client_id -- (str) OAuth ID for the client that requested authentication. expiration_time -- (datetime) Date and time after which the response should be considered invalid. """ body = {"access_token": "foobar", "token_type": "bearer"} client_key = client_key or self.client_key now = datetime.datetime.now(datetime.timezone.utc) expiration_datetime = expiration_datetime or ( now + datetime.timedelta(seconds=30) ) issue_datetime = issue_datetime or now nonce = nonce or "a-nonce" issuer = issuer or self.issuer id_token = self.get_id_token( client_key, timegm(expiration_datetime.timetuple()), timegm(issue_datetime.timetuple()), nonce, issuer, ) # calc at_hash id_token["at_hash"] = OpenIdConnectAuth.calc_at_hash("foobar", "RS256") body["id_token"] = jwt.encode( id_token, key=jwt.PyJWK( dict(self.key, iat=timegm(issue_datetime.timetuple()), nonce=nonce) ).key, algorithm="RS256", headers={"kid": kid} if kid else None, ) if tamper_message: header, msg, sig = body["id_token"].split(".") id_token["sub"] = "1235" msg = base64.encodebytes(json.dumps(id_token).encode()).decode() body["id_token"] = f"{header}.{msg}.{sig}" return json.dumps(body) def authtoken_raised(self, expected_message, **access_token_kwargs): self.access_token_kwargs = access_token_kwargs with self.assertRaisesRegex(AuthTokenError, expected_message): self.do_login() def pre_complete_callback(self, start_url): nonce = parse_qs(urlparse(start_url).query)["nonce"] self.access_token_kwargs.setdefault("nonce", nonce) self.access_token_body = self.prepare_access_token_body( **self.access_token_kwargs ) super().pre_complete_callback(start_url) def test_invalid_signature(self): self.authtoken_raised( "Token error: Signature verification failed", tamper_message=True ) def test_expired_signature(self): expiration_datetime = datetime.datetime.now( datetime.timezone.utc ) - datetime.timedelta(seconds=30) self.authtoken_raised( "Token error: Signature has expired", expiration_datetime=expiration_datetime, ) def test_invalid_issuer(self): self.authtoken_raised("Token error: Invalid issuer", issuer="someone-else") def test_invalid_audience(self): self.authtoken_raised( "Token error: Invalid audience", client_key="someone-else" ) def test_invalid_issue_time(self): expiration_datetime = datetime.datetime.now( datetime.timezone.utc ) - datetime.timedelta(seconds=self.backend.ID_TOKEN_MAX_AGE * 2) self.authtoken_raised( "Token error: Incorrect id_token: iat", issue_datetime=expiration_datetime ) def test_invalid_nonce(self): self.authtoken_raised( "Token error: Incorrect id_token: nonce", nonce="something-wrong", kid="testkey", ) def test_invalid_kid(self): self.authtoken_raised( "Token error: Signature verification failed", kid="doesnotexist" ) social-auth-core-4.6.1/social_core/tests/backends/test_amazon.py000066400000000000000000000032221500362547200247600ustar00rootroot00000000000000import json import responses from .oauth import BaseAuthUrlTestMixin, OAuth2Test class AmazonOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.amazon.AmazonOAuth2" user_data_url = "https://api.amazon.com/user/profile" expected_username = "FooBar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "user_id": "amzn1.account.ABCDE1234", "email": "foo@bar.com", "name": "Foo Bar", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() class AmazonOAuth2BrokenServerResponseTest(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.amazon.AmazonOAuth2" user_data_url = "https://www.amazon.com/ap/user/profile" expected_username = "FooBar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "Request-Id": "02GGTU7CWMNFTV3KH3J6", "Profile": { "Name": "Foo Bar", "CustomerId": "amzn1.account.ABCDE1234", "PrimaryEmail": "foo@bar.com", }, } ) def setUp(self): super().setUp() responses.add( responses.GET, "https://api.amazon.com/user/profile", status=200, body=self.user_data_body, content_type="application/json", ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_angel.py000066400000000000000000000022621500362547200245640ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class AngelOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.angel.AngelOAuth2" user_data_url = "https://api.angel.co/1/me/" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "facebook_url": "http://www.facebook.com/foobar", "bio": None, "name": "Foo Bar", "roles": [], "github_url": None, "angellist_url": "https://angel.co/foobar", "image": "https://graph.facebook.com/foobar/picture?type=square", "linkedin_url": None, "locations": [], "twitter_url": None, "what_ive_built": None, "dribbble_url": None, "behance_url": None, "blog_url": None, "aboutme_url": None, "follower_count": 0, "online_bio_url": None, "id": 101010, } ) expected_username = "foobar" def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_apple.py000066400000000000000000000035601500362547200246010ustar00rootroot00000000000000import json from unittest.mock import patch from .oauth import BaseAuthUrlTestMixin, OAuth2Test TEST_KEY = """ -----BEGIN EC PRIVATE KEY----- MHcCAQEEIKQya8aIoeoOLeThk7Ad/lLyAo2fTp9IuhIpy2CivH/qoAoGCCqGSM49 AwEHoUQDQgAEyEY7IMlNJtyaF/pdcM/PpQ8OCe19Sf1Yxq4HQsrB2b7QogB95Vjt 6mTZDAhlXIBtuM/JLrdkMfPmwjVKLgxHAQ== -----END EC PRIVATE KEY----- """ token_data = { "sub": "11011110101011011011111011101111", "first_name": "Foo", "last_name": "Bar", "email": "foobar@apple.com", } class AppleIdTest(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.apple.AppleIdAuth" user_data_url = "https://appleid.apple.com/auth/authorize/" id_token = "a-id-token" access_token_body = json.dumps( {"id_token": id_token, "access_token": "a-test-token"} ) expected_username = token_data["sub"] def extra_settings(self): assert self.name, "Name must be set in subclasses" return { "SOCIAL_AUTH_" + self.name + "_TEAM": "a-team-id", "SOCIAL_AUTH_" + self.name + "_KEY": "a-key-id", "SOCIAL_AUTH_" + self.name + "_CLIENT": "a-client-id", "SOCIAL_AUTH_" + self.name + "_SECRET": TEST_KEY, "SOCIAL_AUTH_" + self.name + "_SCOPE": ["name", "email"], } def test_login(self): with patch( "{}.{}".format(self.backend_path, "decode_id_token"), return_value=token_data, ) as decode_mock: self.do_login() assert decode_mock.called assert decode_mock.call_args[0] == (self.id_token,) def test_partial_pipeline(self): with patch( "{}.{}".format(self.backend_path, "decode_id_token"), return_value=token_data, ) as decode_mock: self.do_partial_pipeline() assert decode_mock.called assert decode_mock.call_args[0] == (self.id_token,) social-auth-core-4.6.1/social_core/tests/backends/test_arcgis.py000066400000000000000000000016711500362547200247510ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class ArcGISOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): user_data_url = "https://www.arcgis.com/sharing/rest/community/self" backend_path = "social_core.backends.arcgis.ArcGISOAuth2" expected_username = "gis@rocks.com" user_data_body = json.dumps( { "first_name": "Gis", "last_name": "Rocks", "email": "gis@rocks.com", "fullName": "Gis Rocks", "username": "gis@rocks.com", } ) access_token_body = json.dumps( { "access_token": "CM-gcB85taGhRmoI7l3PSGaXUNsaLkTg-dHH7XtA9Dnlin" "PYKBBrIvFzhd1JtDhh7hEwSv_6eLLcLtUqe3gD6i1yaYYF" "pUQJwy8KEujke5AE87tP9XIoMtp4_l320pUL", "expires_in": 86400, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_asana.py000066400000000000000000000015541500362547200245640ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class AsanaOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.asana.AsanaOAuth2" user_data_url = "https://app.asana.com/api/1.0/users/me" expected_username = "erlich@bachmanity.com" access_token_body = json.dumps({"access_token": "aviato", "token_type": "bearer"}) # https://asana.com/developers/api-reference/users user_data_body = json.dumps( { "data": { "id": 12345, "name": "Erlich Bachman", "email": "erlich@bachmanity.com", "photo": None, "workspaces": [{"id": 123456, "name": "Pied Piper"}], } } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_atlassian.py000066400000000000000000000042731500362547200254610ustar00rootroot00000000000000import json import responses from .oauth import BaseAuthUrlTestMixin, OAuth2Test class AtlassianOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.atlassian.AtlassianOAuth2" tenant_url = "https://api.atlassian.com/oauth/token/accessible-resources" user_data_url = "https://api.atlassian.com/ex/jira/FAKED_CLOUD_ID/rest/api/2/myself" expected_username = "9927935d01-92a7-4687-8272-a9b8d3b2ae2e" access_token_body = json.dumps({"access_token": "aviato", "token_type": "bearer"}) tenant_data_body = json.dumps( [ { "id": "FAKED_CLOUD_ID", "name": "bachmanity.com", "avatarUrl": "https://bachmanity.atlassian.net/avatars/240/site.png", "scopes": ["read:jira-user"], } ] ) user_data_body = json.dumps( { "self": "http://bachmanity.atlassian.net/rest/api/3/user?username=erlich", "key": "erlich", "accountId": "99:27935d01-92a7-4687-8272-a9b8d3b2ae2e", "emailAddress": "erlich@bachmanity.com", "avatarUrls": { "48x48": "http://bachmanity.atlassian.net/secure/useravatar?size=large&ownerId=erlich", "24x24": "http://bachmanity.atlassian.net/secure/useravatar?size=small&ownerId=erlich", "16x16": "http://bachmanity.atlassian.net/secure/useravatar?size=xsmall&ownerId=erlich", "32x32": "http://bachmanity.atlassian.net/secure/useravatar?size=medium&ownerId=erlich", }, "displayName": "Erlich Bachman", "active": True, "timeZone": "Australia/Sydney", "groups": {"size": 3, "items": []}, "applicationRoles": {"size": 1, "items": []}, } ) def auth_handlers(self, start_url): target_url = super().auth_handlers(start_url) responses.add( responses.GET, self.tenant_url, body=self.tenant_data_body, content_type="application/json", ) return target_url def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_auth0.py000066400000000000000000000050351500362547200245200ustar00rootroot00000000000000import json import jwt import responses from .oauth import BaseAuthUrlTestMixin, OAuth2Test JWK_KEY = { "kty": "RSA", "d": "ZmswNokEvBcxW_Kvcy8mWUQOQCBdGbnM0xR7nhvGHC-Q24z3XAQWlMWbsmGc_R1o" "_F3zK7DBlc3BokdRaO1KJirNmnHCw5TlnBlJrXiWpFBtVglUg98-4sRRO0VWnGXK" "JPOkBQ6b_DYRO3b0o8CSpWowpiV6HB71cjXTqKPZf-aXU9WjCCAtxVjfIxgQFu5I" "-G1Qah8mZeY8HK_y99L4f0siZcbUoaIcfeWBhxi14ODyuSAHt0sNEkhiIVBZE7QZ" "m-SEP1ryT9VAaljbwHHPmg7NC26vtLZhvaBGbTTJnEH0ZubbN2PMzsfeNyoCIHy4" "4QDSpQDCHfgcGOlHY_t5gQ", "e": "AQAB", "use": "sig", "kid": "foobar", "alg": "RS256", "n": "pUfcJ8WFrVue98Ygzb6KEQXHBzi8HavCu8VENB2As943--bHPcQ-nScXnrRFAUg8" "H5ZltuOcHWvsGw_AQifSLmOCSWJAPkdNb0w0QzY7Re8NrPjCsP58Tytp5LicF0Ao" "Ag28UK3JioY9hXHGvdZsWR1Rp3I-Z3nRBP6HyO18pEgcZ91c9aAzsqu80An9X4DA" "b1lExtZorvcd5yTBzZgr-MUeytVRni2lDNEpa6OFuopHXmg27Hn3oWAaQlbymd4g" "ifc01oahcwl3ze2tMK6gJxa_TdCf1y99Yq6oilmVvZJ8kwWWnbPE-oDmOVPVnEyT" "vYVCvN4rBT1DQ-x0F1mo2Q", } JWK_PUBLIC_KEY = {key: value for key, value in JWK_KEY.items() if key != "d"} DOMAIN = "foobar.auth0.com" class Auth0OAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.auth0.Auth0OAuth2" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", "expires_in": 86400, "id_token": jwt.encode( { "nickname": "foobar", "email": "foobar@auth0.com", "name": "John Doe", "picture": "http://example.com/image.png", "sub": "123456", "iss": f"https://{DOMAIN}/", "aud": "a-key", }, jwt.PyJWK(JWK_KEY).key, algorithm="RS256", ), } ) expected_username = "foobar" jwks_url = "https://foobar.auth0.com/.well-known/jwks.json" def extra_settings(self): assert self.name, "Subclasses must set the name attribute" settings = super().extra_settings() settings["SOCIAL_AUTH_" + self.name + "_DOMAIN"] = DOMAIN return settings def auth_handlers(self, start_url): responses.add( responses.GET, self.jwks_url, body=json.dumps({"keys": [JWK_PUBLIC_KEY]}), content_type="application/json", ) return super().auth_handlers(start_url) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_azuread.py000066400000000000000000000061621500362547200251340ustar00rootroot00000000000000""" Copyright (c) 2015 Microsoft Open Technologies, Inc. All rights reserved. MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class AzureADOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.azuread.AzureADOAuth2" user_data_url = "https://graph.windows.net/me" expected_username = "foobar" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL" "3N0cy53aW5kb3dzLm5ldC83Mjc0MDZhYy03MDY4LTQ4ZmEtOTJiOS1jMmQ" "2NzIxMWJjNTAvIiwiaWF0IjpudWxsLCJleHAiOm51bGwsImF1ZCI6IjAyO" "WNjMDEwLWJiNzQtNGQyYi1hMDQwLWY5Y2VkM2ZkMmM3NiIsInN1YiI6In" "FVOHhrczltSHFuVjZRMzR6aDdTQVpvY2loOUV6cnJJOW1wVlhPSWJWQTg" "iLCJ2ZXIiOiIxLjAiLCJ0aWQiOiI3Mjc0MDZhYy03MDY4LTQ4ZmEtOTJi" "OS1jMmQ2NzIxMWJjNTAiLCJvaWQiOiI3ZjhlMTk2OS04YjgxLTQzOGMtO" "GQ0ZS1hZDZmNTYyYjI4YmIiLCJ1cG4iOiJmb29iYXJAdGVzdC5vbm1pY3" "Jvc29mdC5jb20iLCJnaXZlbl9uYW1lIjoiZm9vIiwiZmFtaWx5X25hbWU" "iOiJiYXIiLCJuYW1lIjoiZm9vIGJhciIsInVuaXF1ZV9uYW1lIjoiZm9v" "YmFyQHRlc3Qub25taWNyb3NvZnQuY29tIiwicHdkX2V4cCI6IjQ3MzMwO" "TY4IiwicHdkX3VybCI6Imh0dHBzOi8vcG9ydGFsLm1pY3Jvc29mdG9ubG" "luZS5jb20vQ2hhbmdlUGFzc3dvcmQuYXNweCJ9.3V50dHXTZOHj9UWtkn" "2g7BjX5JxNe8skYlK4PdhiLz4", "expires_in": 3600, "expires_on": 1423650396, "not_before": 1423646496, } ) refresh_token_body = json.dumps( { "access_token": "foobar-new-token", "token_type": "bearer", "expires_in": 3600, "refresh_token": "foobar-new-refresh-token", "scope": "identity", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() def test_refresh_token(self): user, social = self.do_refresh_token() self.assertEqual(social.extra_data["access_token"], "foobar-new-token") social-auth-core-4.6.1/social_core/tests/backends/test_azuread_b2c.py000066400000000000000000000176311500362547200256650ustar00rootroot00000000000000""" Copyright (c) 2017 Noderabbit Inc., d.b.a. Appsembler All rights reserved. MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ from __future__ import annotations import json from time import time from typing import TYPE_CHECKING, cast import jwt import responses from jwt.algorithms import RSAAlgorithm from .oauth import BaseAuthUrlTestMixin, OAuth2Test if TYPE_CHECKING: from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey # Dummy private and private keys: RSA_PUBLIC_JWT_KEY = { # https://github.com/jpadilla/pyjwt/blob/06f461a/tests/keys/jwk_rsa_pub.json "kty": "RSA", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-" + "XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-" + "QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-" + "OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrt" + "IQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7" + "jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", "e": "AQAB", } RSA_PRIVATE_JWT_KEY = { # https://github.com/jpadilla/pyjwt/blob/06f461a/tests/keys/jwk_rsa_key.json "kty": "RSA", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-" + "XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-" + "QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-" + "OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQ" + "bS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3" + "__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", "e": "AQAB", "d": "bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78eiZkpXxXQ0" + "UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRldY7sNA_AKZGh-Q661_" + "42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-bMwWWSDFu2MUBYwkHTMEzLYGqO" + "e04noqeq1hExBTHBOBdkMXiuFhUq1BU6l-DqEiWxqg82sXt2h-LMnT3046AOYJoR" + "ioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFa" + "u7gdsPfHPxxjVOcOpBrQzwQ", "p": "3Slxg_DwTXJcb6095RoXygQCAZ5RnAvZlno1yhHtnUex_fp7AZ_9nRaO7HX_-" + "SFfGQeutao2TDjDAWU4Vupk8rw9JR0AzZ0N2fvuIAmr_WCsmGpeNqQnev1T7I" + "yEsnh8UMt-n5CafhkikzhEsrmndH6LxOrvRJlsPp6Zv8bUq0k", "q": "uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_" + "cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7" + "z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc", "dp": "B8PVvXkvJrj2L-GYQ7v3y9r6Kw5g9SahXBwsWUzp19TVlgI-YV85q1NIb1rxQtD" + "-IsXXR3-TanevuRPRt5OBOdiMGQp8pbt26gljYfKU_E9xn-RULHz0-ed9E9gXLKD" + "4VGngpz-PfQ_q29pk5xWHoJp009Qf1HvChixRX59ehik", "dq": "CLDmDGduhylc9o7r84rEUVn7pzQ6PF83Y-iBZx5NT-TpnOZKF1pErAMVeKzFEl4" + "1DlHHqqBLSM0W1sOFbwTxYWZDm6sI6og5iTbwQGIC3gnJKbi_7k_vJgGHwHxgPaX" + "2PnvP-zyEkDERuf-ry4c_Z11Cq9AqC2yeL6kdKT1cYF8", "qi": "3PiqvXQN0zwMeE-sBvZgi289XP9XCQF3VWqPzMKnIgQp7_Tugo6-NZBKCQsMf3H" + "aEGBjTVJs_jcK8-TRXvaKe-7ZMaQj8VfBdYkssbu0NKDDhjJ-GtiseaDVWt7dcH" + "0cfwxgFUHpQh7FoCrjFJ6h6ZEpMF6xmujs4qMpPz8aaI4", } class AzureADB2COAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): AUTH_KEY = "abcdef12-1234-9876-0000-abcdef098765" EXPIRES_IN = 3600 AUTH_TIME = int(time()) EXPIRES_ON = AUTH_TIME + EXPIRES_IN backend_path = "social_core.backends.azuread_b2c.AzureADB2COAuth2" expected_username = "FooBar" refresh_token_body = json.dumps( { "access_token": "foobar-new-token", "token_type": "bearer", "expires_in": EXPIRES_IN, "refresh_token": "foobar-new-refresh-token", "scope": "identity", } ) access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", "id_token": jwt.encode( key=cast( "RSAPrivateKey", RSAAlgorithm.from_jwk(json.dumps(RSA_PRIVATE_JWT_KEY)), ), headers={ "kid": RSA_PRIVATE_JWT_KEY["kid"], }, algorithm="RS256", payload={ "aud": AUTH_KEY, "auth_time": AUTH_TIME, "country": "Axphain", "emails": ["foobar@example.com"], "exp": EXPIRES_ON, "family_name": "Bar", "given_name": "Foo", "iat": AUTH_TIME, "iss": "https://foobar.b2clogin.com/9a9a9a9a-1111-5555-0000-bc24adfdae00/v2.0/", "name": "FooBar", "nbf": AUTH_TIME, "oid": "11223344-5566-7788-9999-aabbccddeeff", "postalCode": "00000", "sub": "11223344-5566-7788-9999-aabbccddeeff", "tfp": "B2C_1_SignIn", "ver": "1.0", }, ), "expires_in": EXPIRES_IN, "expires_on": EXPIRES_ON, "not_before": AUTH_TIME, } ) def extra_settings(self): settings = super().extra_settings() assert self.name, "Name must be set in subclasses" settings.update( { "SOCIAL_AUTH_" + self.name + "_POLICY": "b2c_1_signin", "SOCIAL_AUTH_" + self.name + "_KEY": self.AUTH_KEY, "SOCIAL_AUTH_" + self.name + "_TENANT_NAME": "footenant", } ) return settings def setUp(self): super().setUp() keys_url = "https://footenant.b2clogin.com/footenant.onmicrosoft.com/discovery/v2.0/keys?p=b2c_1_signin" keys_body = json.dumps( { "keys": [ { # Dummy public key that pairs with `access_token_body` key: # https://github.com/jpadilla/pyjwt/blob/06f461a/tests/keys/jwk_rsa_pub.json "kty": "RSA", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-X" "V2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_Ns" "YOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHY" "pPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCu" "EHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_" "mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw", "e": "AQAB", } ], } ) responses.add(responses.GET, keys_url, status=200, body=keys_body) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() def test_refresh_token(self): user, social = self.do_refresh_token() self.assertEqual(social.extra_data["access_token"], "foobar-new-token") social-auth-core-4.6.1/social_core/tests/backends/test_behance.py000066400000000000000000000032471500362547200250670ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class BehanceOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.behance.BehanceOAuth2" access_token_body = json.dumps( { "access_token": "foobar", "valid": 1, "user": { "username": "foobar", "city": "Foo City", "first_name": "Foo", "last_name": "Bar", "display_name": "Foo Bar", "url": "http://www.behance.net/foobar", "country": "Fooland", "company": "", "created_on": 1355152329, "state": "", "fields": ["Programming", "Web Design", "Web Development"], "images": { "32": "https://www.behance.net/assets/img/profile/no-image-32.jpg", "50": "https://www.behance.net/assets/img/profile/no-image-50.jpg", "115": "https://www.behance.net/assets/img/profile/" "no-image-138.jpg", "129": "https://www.behance.net/assets/img/profile/" "no-image-138.jpg", "138": "https://www.behance.net/assets/img/profile/" "no-image-138.jpg", "78": "https://www.behance.net/assets/img/profile/no-image-78.jpg", }, "id": 1010101, "occupation": "Software Developer", }, } ) expected_username = "foobar" def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_bitbucket.py000066400000000000000000000102271500362547200254520ustar00rootroot00000000000000import json import responses from ...exceptions import AuthForbidden from .oauth import BaseAuthUrlTestMixin, OAuth2Test class BitbucketOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.bitbucket.BitbucketOAuth2" access_token_body = json.dumps( { "access_token": "foobar_access", "scopes": "foo_scope", "expires_in": 3600, "refresh_token": "foobar_refresh", "token_type": "bearer", } ) user_data_url = "https://api.bitbucket.org/2.0/user" expected_username = "foobar" bb_api_user_emails = "https://api.bitbucket.org/2.0/user/emails" user_data_body = json.dumps( { "created_on": "2012-03-29T18:07:38+00:00", "display_name": "Foo Bar", "links": { "avatar": {"href": "https://bitbucket.org/account/foobar/avatar/32/"}, "followers": { "href": "https://api.bitbucket.org/2.0/users/foobar/followers" }, "following": { "href": "https://api.bitbucket.org/2.0/users/foobar/following" }, "hooks": {"href": "https://api.bitbucket.org/2.0/users/foobar/hooks"}, "html": {"href": "https://bitbucket.org/foobar"}, "repositories": { "href": "https://api.bitbucket.org/2.0/repositories/foobar" }, "self": {"href": "https://api.bitbucket.org/2.0/users/foobar"}, }, "location": "Fooville, Bar", "type": "user", "username": "foobar", "uuid": "{397621dc-0f78-329f-8d6d-727396248e3f}", "website": "http://foobar.com", } ) emails_body = json.dumps( { "page": 1, "pagelen": 10, "size": 2, "values": [ { "email": "foo@bar.com", "is_confirmed": True, "is_primary": True, "links": { "self": { "href": "https://api.bitbucket.org/2.0/user/emails/foo@bar.com" } }, "type": "email", }, { "email": "not@confirme.com", "is_confirmed": False, "is_primary": False, "links": { "self": { "href": "https://api.bitbucket.org/2.0/user/emails/not@confirmed.com" } }, "type": "email", }, ], } ) def test_login(self): responses.add( responses.GET, self.bb_api_user_emails, status=200, body=self.emails_body ) self.do_login() def test_partial_pipeline(self): responses.add( responses.GET, self.bb_api_user_emails, status=200, body=self.emails_body ) self.do_partial_pipeline() class BitbucketOAuth2FailTest(BitbucketOAuth2Test): emails_body = json.dumps( { "page": 1, "pagelen": 10, "size": 1, "values": [ { "email": "foo@bar.com", "is_confirmed": False, "is_primary": True, "links": { "self": { "href": "https://api.bitbucket.org/2.0/user/emails/foo@bar.com" } }, "type": "email", } ], } ) def test_login(self): self.strategy.set_settings( {"SOCIAL_AUTH_BITBUCKET_OAUTH2_VERIFIED_EMAILS_ONLY": True} ) with self.assertRaises(AuthForbidden): super().test_login() def test_partial_pipeline(self): self.strategy.set_settings( {"SOCIAL_AUTH_BITBUCKET_OAUTH2_VERIFIED_EMAILS_ONLY": True} ) with self.assertRaises(AuthForbidden): super().test_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_bitbucket_datacenter.py000066400000000000000000000130271500362547200276450ustar00rootroot00000000000000# pyright: reportAttributeAccessIssue=false import json import responses from .oauth import OAuth2PkcePlainTest, OAuth2PkceS256Test class BitbucketDataCenterOAuth2Mixin: backend_path = "social_core.backends.bitbucket_datacenter.BitbucketDataCenterOAuth2" application_properties_url = ( "https://bachmanity.atlassian.net/rest/api/latest/application-properties" ) application_properties_headers = {"x-ausername": "erlich-bachman"} application_properties_body = json.dumps( { "version": "8.15.0", "buildNumber": "8015000", "buildDate": "1697764661289", "displayName": "Bitbucket", } ) user_data_url = ( "https://bachmanity.atlassian.net/rest/api/latest/users/erlich-bachman" ) user_data_body = json.dumps( { "name": "erlich-bachman", "emailAddress": "erlich@bachmanity.com", "active": True, "displayName": "Erlich Bachman", "id": 1, "slug": "erlich-bachman", "type": "NORMAL", "links": { "self": [ {"href": "https://bachmanity.atlassian.net/users/erlich-bachman"} ] }, "avatarUrl": "http://www.gravatar.com/avatar/af7d968fe79ea45271e3100391824b79.jpg?s=48&d=mm", } ) access_token_body = json.dumps( { "scope": "PUBLIC_REPOS", "access_token": "dummy_access_token", "token_type": "bearer", "expires_in": 3600, "refresh_token": "dummy_refresh_token", } ) refresh_token_body = json.dumps( { "scope": "PUBLIC_REPOS", "access_token": "dummy_access_token_refreshed", "token_type": "bearer", "expires_in": 3600, "refresh_token": "dummy_refresh_token_refreshed", } ) expected_username = "erlich-bachman" def extra_settings(self): settings = super().extra_settings() settings.update( {f"SOCIAL_AUTH_{self.name}_URL": "https://bachmanity.atlassian.net"} ) return settings def auth_handlers(self, start_url): target_url = super().auth_handlers(start_url) responses.add( responses.GET, self.application_properties_url, body=self.application_properties_body, adding_headers=self.application_properties_headers, content_type="text/json", ) return target_url def test_login(self): user = self.do_login() self.assertEqual(len(user.social), 1) social = user.social[0] self.assertEqual(social.uid, "1") self.assertEqual(social.extra_data["first_name"], "Erlich") self.assertEqual(social.extra_data["last_name"], "Bachman") self.assertEqual(social.extra_data["email"], "erlich@bachmanity.com") self.assertEqual(social.extra_data["name"], "erlich-bachman") self.assertEqual(social.extra_data["username"], "erlich-bachman") self.assertEqual(social.extra_data["display_name"], "Erlich Bachman") self.assertEqual(social.extra_data["type"], "NORMAL") self.assertEqual(social.extra_data["active"], True) self.assertEqual( social.extra_data["url"], "https://bachmanity.atlassian.net/users/erlich-bachman", ) self.assertEqual( social.extra_data["avatar_url"], "http://www.gravatar.com/avatar/af7d968fe79ea45271e3100391824b79.jpg?s=48&d=mm", ) self.assertEqual(social.extra_data["scope"], "PUBLIC_REPOS") self.assertEqual(social.extra_data["access_token"], "dummy_access_token") self.assertEqual(social.extra_data["token_type"], "bearer") self.assertEqual(social.extra_data["expires"], 3600) self.assertEqual(social.extra_data["refresh_token"], "dummy_refresh_token") def test_refresh_token(self): _, social = self.do_refresh_token() self.assertEqual(social.uid, "1") self.assertEqual(social.extra_data["first_name"], "Erlich") self.assertEqual(social.extra_data["last_name"], "Bachman") self.assertEqual(social.extra_data["email"], "erlich@bachmanity.com") self.assertEqual(social.extra_data["name"], "erlich-bachman") self.assertEqual(social.extra_data["username"], "erlich-bachman") self.assertEqual(social.extra_data["display_name"], "Erlich Bachman") self.assertEqual(social.extra_data["type"], "NORMAL") self.assertEqual(social.extra_data["active"], True) self.assertEqual( social.extra_data["url"], "https://bachmanity.atlassian.net/users/erlich-bachman", ) self.assertEqual( social.extra_data["avatar_url"], "http://www.gravatar.com/avatar/af7d968fe79ea45271e3100391824b79.jpg?s=48&d=mm", ) self.assertEqual(social.extra_data["scope"], "PUBLIC_REPOS") self.assertEqual( social.extra_data["access_token"], "dummy_access_token_refreshed" ) self.assertEqual(social.extra_data["token_type"], "bearer") self.assertEqual(social.extra_data["expires"], 3600) self.assertEqual( social.extra_data["refresh_token"], "dummy_refresh_token_refreshed" ) class BitbucketDataCenterOAuth2TestPkcePlain( BitbucketDataCenterOAuth2Mixin, OAuth2PkcePlainTest, ): pass class BitbucketDataCenterOAuth2TestPkceS256( BitbucketDataCenterOAuth2Mixin, OAuth2PkceS256Test, ): pass social-auth-core-4.6.1/social_core/tests/backends/test_box.py000066400000000000000000000046771500362547200243020ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class BoxOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.box.BoxOAuth2" user_data_url = "https://api.box.com/2.0/users/me" expected_username = "sean+awesome@box.com" access_token_body = json.dumps( { "access_token": "T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl", "expires_in": 3600, "restricted_to": [], "token_type": "bearer", "refresh_token": "J7rxTiWOHMoSC1isKZKBZWizoRXjkQzig5C6jFgCVJ9bU" "nsUfGMinKBDLZWP9BgR", } ) user_data_body = json.dumps( { "type": "user", "id": "181216415", "name": "sean rose", "login": "sean+awesome@box.com", "created_at": "2012-05-03T21:39:11-07:00", "modified_at": "2012-11-14T11:21:32-08:00", "role": "admin", "language": "en", "space_amount": 11345156112, "space_used": 1237009912, "max_upload_size": 2147483648, "tracking_codes": [], "can_see_managed_users": True, "is_sync_enabled": True, "status": "active", "job_title": "", "phone": "6509241374", "address": "", "avatar_url": "https://www.box.com/api/avatar/large/181216415", "is_exempt_from_device_limits": False, "is_exempt_from_login_verification": False, "enterprise": { "type": "enterprise", "id": "17077211", "name": "seanrose enterprise", }, } ) refresh_token_body = json.dumps( { "access_token": "T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl", "expires_in": 3600, "restricted_to": [], "token_type": "bearer", "refresh_token": "J7rxTiWOHMoSC1isKZKBZWizoRXjkQzig5C6jFgCVJ9b" "UnsUfGMinKBDLZWP9BgR", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() def refresh_token_arguments(self): uri = self.strategy.build_absolute_uri("/complete/box/") return {"redirect_uri": uri} def test_refresh_token(self): user, social = self.do_refresh_token() self.assertEqual( social.extra_data["access_token"], "T9cE5asGnuyYCCqIZFoWjFHvNbvVqHjl" ) social-auth-core-4.6.1/social_core/tests/backends/test_broken.py000066400000000000000000000017451500362547200247630ustar00rootroot00000000000000import unittest from ...backends.base import BaseAuth from ..models import TestStorage from ..strategy import TestStrategy class BrokenBackendAuth(BaseAuth): name = "broken" class BrokenBackendTest(unittest.TestCase): def setUp(self): self.backend = BrokenBackendAuth(TestStrategy(TestStorage)) def tearDown(self): self.backend = None def test_auth_url(self): with self.assertRaisesRegex(NotImplementedError, "Implement in subclass"): self.backend.auth_url() def test_auth_html(self): with self.assertRaisesRegex(NotImplementedError, "Implement in subclass"): self.backend.auth_html() def test_auth_complete(self): with self.assertRaisesRegex(NotImplementedError, "Implement in subclass"): self.backend.auth_complete() def test_get_user_details(self): with self.assertRaisesRegex(NotImplementedError, "Implement in subclass"): self.backend.get_user_details(None) social-auth-core-4.6.1/social_core/tests/backends/test_cas.py000066400000000000000000000036751500362547200242550ustar00rootroot00000000000000import json import responses from .oauth import BaseAuthUrlTestMixin from .open_id_connect import OpenIdConnectTest ROOT_URL = "https://cas.example.net/" class CASOpenIdConnectTest(OpenIdConnectTest, BaseAuthUrlTestMixin): backend_path = "social_core.backends.cas.CASOpenIdConnectAuth" issuer = f"{ROOT_URL}oidc" openid_config_body = json.dumps( { "issuer": f"{ROOT_URL}oidc", "jwks_uri": f"{ROOT_URL}oidc/jwks", "authorization_endpoint": f"{ROOT_URL}oidc/oidcAuthorize", "token_endpoint": f"{ROOT_URL}oidc/oidcAccessToken", "userinfo_endpoint": f"{ROOT_URL}oidc/oidcProfile", "request_uri_parameter_supported": False, "grant_types_supported": ["authorization_code"], "token_endpoint_auth_methods_supported": ["client_secret_basic"], } ) expected_username = "cartman" user_data_body = json.dumps( { "sub": "Cartman", "service": "https://cas.example.net/complete/cas/", "auth_time": 1677057708, "attributes": { "name": "Eric", "groups": ["users", "admins"], "preferred_username": "cartman", "email": "cartman@example.net", }, "id": "Cartman", "client_id": "dev", } ) def extra_settings(self): settings = super().extra_settings() settings.update( { f"SOCIAL_AUTH_{self.name}_OIDC_ENDPOINT": f"{ROOT_URL}oidc", } ) return settings def pre_complete_callback(self, start_url): super().pre_complete_callback(start_url) responses.add( responses.GET, url=self.backend.userinfo_url(), status=200, body=self.user_data_body, content_type="text/json", ) def test_everything_works(self): self.do_login() social-auth-core-4.6.1/social_core/tests/backends/test_chatwork.py000066400000000000000000000026671500362547200253310ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class ChatworkOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.chatwork.ChatworkOAuth2" user_data_url = "https://api.chatwork.com/v2/me" expected_username = "hogehoge" access_token_body = json.dumps( { "access_token": "pyopyopyopyopyopyopyopyopyopyo", "token_type": "Bearer", "expires_in": "1501138041000", "refresh_token": "pyopyopyopyopyopyo", "scope": "rooms.all:read_write", } ) user_data_body = json.dumps( { "account_id": 123, "room_id": 322, "name": "Foo Bar", "chatwork_id": "hogehoge", "organization_id": 101, "organization_name": "Foo foobar", "department": "Support", "title": "CMO", "url": "http://www.example.com", "introduction": "", "mail": "hogehoge@example.com", "tel_organization": "", "tel_extension": "", "tel_mobile": "", "skype": "", "facebook": "", "twitter": "", "avatar_image_url": "https://www.example.com/hogehoge.jpg", "login_mail": "hogehoge@example.com", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_cilogon.py000066400000000000000000000026661500362547200251400ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class CILogonOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.cilogon.CILogonOAuth2" user_data_url = "https://cilogon.org/oauth2/userinfo" user_data_url_post = True expected_username = "jbasney@illinois.edu" access_token_body = json.dumps( { "access_token": "https://cilogon.org/oauth2/accessToken/sample-token123", "refresh_token": "https://cilogon.org/oauth2/refreshToken/sample123/refresh-token123", "id_token": "aBigStringOfRandomChars.123abc", "token_type": "Bearer", "expires_in": 900, } ) user_data_body = json.dumps( { "sub": "http://cilogon.org/serverA/users/534", "idp_name": "University of Illinois at Urbana-Champaign", "idp": "urn:mace:incommon:uiuc.edu", "affiliation": "employee@illinois.edu;member@illinois.edu;staff@illinois.edu", "eppn": "jbasney@illinois.edu", "eptid": "urn:mace:incommon:uiuc.edu!https://cilogon.org/shibboleth!cyXC3O5fi0t1NBsW1NsOxZDyDd4=", "name": "James Alan Basney", "given_name": "James", "family_name": "Basney", "email": "jbasney@illinois.edu", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_clef.py000066400000000000000000000012731500362547200244100ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class ClefOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.clef.ClefOAuth2" user_data_url = "https://clef.io/api/v1/info" expected_username = "test" access_token_body = json.dumps({"access_token": "foobar"}) user_data_body = json.dumps( { "info": { "id": "123456789", "first_name": "Test", "last_name": "User", "email": "test@example.com", } } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_cognito.py000066400000000000000000000022631500362547200251410ustar00rootroot00000000000000# pyright: reportAttributeAccessIssue=false import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class CognitoAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.cognito.CognitoOAuth2" pool_domain = "https://social_core.auth.eu-west-1.amazoncognito.com" user_data_url = ( "https://social_core.auth.eu-west-1.amazoncognito.com/oauth2/userInfo" ) expected_username = "cognito.account.ABCDE1234" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "given_name": "John", "family_name": "Doe", "username": "cognito.account.ABCDE1234", "email": "john@doe.test", } ) def extra_settings(self): settings = super().extra_settings() assert self.name, "subclasses must set the name attribute" settings.update( { "SOCIAL_AUTH_" + self.name + "_POOL_DOMAIN": self.pool_domain, } ) return settings def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_coinbase.py000066400000000000000000000017711500362547200252650ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class CoinbaseOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.coinbase.CoinbaseOAuth2" user_data_url = "https://api.coinbase.com/v2/user" expected_username = "satoshi_nakomoto" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "data": { "id": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "name": "Satoshi Nakamoto", "username": "satoshi_nakomoto", "profile_location": None, "profile_bio": None, "profile_url": "https://coinbase.com/satoshi_nakomoto", "avatar_url": None, "resource": "user", "resource_path": "/v2/user", } } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_coursera.py000066400000000000000000000022411500362547200253160ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class CourseraOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.coursera.CourseraOAuth2" user_data_url = "https://api.coursera.org/api/externalBasicProfiles.v1?q=me" expected_username = "560e7ed2076e0d589e88bd74b6aad4b7" access_token_body = json.dumps( {"access_token": "foobar", "token_type": "Bearer", "expires_in": 1795} ) request_token_body = json.dumps( { "code": "foobar-code", "client_id": "foobar-client-id", "client_secret": "foobar-client-secret", "redirect_uri": "http://localhost:8000/accounts/coursera/", "grant_type": "authorization_code", } ) user_data_body = json.dumps( { "token_type": "Bearer", "paging": None, "elements": [{"id": "560e7ed2076e0d589e88bd74b6aad4b7"}], "access_token": "foobar", "expires_in": 1800, "linked": None, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_dailymotion.py000066400000000000000000000011001500362547200260140ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class DailymotionOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.dailymotion.DailymotionOAuth2" user_data_url = "https://api.dailymotion.com/auth/" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps({"id": "foobar", "screenname": "foobar"}) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_deezer.py000066400000000000000000000025061500362547200247550ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class DeezerOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.deezer.DeezerOAuth2" user_data_url = "http://api.deezer.com/user/me" expected_username = "foobar" access_token_body = "access_token=foobar&expires=0" user_data_body = json.dumps( { "id": "1", "name": "foobar", "lastname": "", "firstname": "", "status": 0, "birthday": "1970-01-01", "inscription_date": "2015-01-01", "gender": "M", "link": "https://www.deezer.com/profile/1", "picture": "https://api.deezer.com/user/1/image", "picture_small": "https://cdns-images.dzcdn.net/images/user//56x56-000000-80-0-0.jpg", "picture_medium": "https://cdns-images.dzcdn.net/images/user//250x250-000000-80-0-0.jpg", "picture_big": "https://cdns-images.dzcdn.net/images/user//500x500-000000-80-0-0.jpg", "country": "FR", "lang": "FR", "is_kid": False, "tracklist": "https://api.deezer.com/user/1/flow", "type": "user", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_digitalocean.py000066400000000000000000000020611500362547200261160ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class DigitalOceanOAuthTest(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.digitalocean.DigitalOceanOAuth" user_data_url = "https://api.digitalocean.com/v2/account" expected_username = "sammy@digitalocean.com" access_token_body = json.dumps( { "access_token": "547cac21118ae7", "token_type": "bearer", "expires_in": 2592000, "refresh_token": "00a3aae641658d", "scope": "read write", "info": {"name": "Sammy Shark", "email": "sammy@digitalocean.com"}, } ) user_data_body = json.dumps( { "account": { "droplet_limit": 25, "email": "sammy@digitalocean.com", "uuid": "b6fr89dbf6d9156cace5f3c78dc9851d957381ef", "email_verified": True, } } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_discogs.py000066400000000000000000000064601500362547200251350ustar00rootroot00000000000000import json from urllib.parse import urlencode import responses from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test class DiscsogsOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): _test_token = "lalala123boink" backend_path = "social_core.backends.discogs.DiscogsOAuth1" expected_username = "rodneyfool" raw_complete_url = ( f"/complete/{0}/?oauth_verifier=wimblewomblefartfart&oauth_token={_test_token}" ) access_token_body = json.dumps( {"access_token": _test_token, "token_type": "bearer"} ) request_token_body = urlencode( { "oauth_token": _test_token, "oauth_token_secret": "xyz789", "oauth_callback_confirmed": "true", } ) user_data_body = json.dumps( { "profile": "I am a software developer for Discogs.\r\n\r\n[img=http://i.imgur.com/IAk3Ukk.gif]", "wantlist_url": "https://api.discogs.com/users/rodneyfool/wants", "rank": 149, "num_pending": 61, "id": 1578108, "num_for_sale": 0, "home_page": "", "location": "I live in the good ol' Pacific NW", "collection_folders_url": "https://api.discogs.com/users/rodneyfool/collection/folders", "username": expected_username, "collection_fields_url": "https://api.discogs.com/users/rodneyfool/collection/fields", "releases_contributed": 5, "registered": "2012-08-15T21:13:36-07:00", "rating_avg": 3.47, "num_collection": 78, "releases_rated": 116, "num_lists": 0, "name": "Rodney", "num_wantlist": 160, "inventory_url": "https://api.discogs.com/users/rodneyfool/inventory", "avatar_url": "http://www.gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=52&r=pg&d=mm", "banner_url": ( "https://img.discogs.com/dhuJe-pRJmod7hN3cdVi2PugEh4=/1600x400/" "filters:strip_icc():format(jpeg)/discogs-banners/B-1578108-user-1436314164-9231.jpg.jpg" ), "uri": "https://www.discogs.com/user/rodneyfool", "resource_url": "https://api.discogs.com/users/rodneyfool", "buyer_rating": 100.00, "buyer_rating_stars": 5, "buyer_num_ratings": 144, "seller_rating": 100.00, "seller_rating_stars": 5, "seller_num_ratings": 21, "curr_abbr": "USD", } ) def _mock(self): responses.add( responses.GET, url="https://api.discogs.com/oauth/identity", status=200, body=json.dumps( { "id": 1, "username": self.expected_username, "resource_url": f"https://api.discogs.com/users/{self.expected_username}", "consumer_name": "SocialCore Discogs Test", } ), ) responses.add( responses.GET, f"https://api.discogs.com/users/{self.expected_username}", status=200, body=self.user_data_body, ) def test_login(self): self._mock() self.do_login() def test_partial_pipeline(self): self._mock() self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_discourse.py000066400000000000000000000046311500362547200255000ustar00rootroot00000000000000import hashlib import hmac from base64 import b64encode from urllib.parse import parse_qs, urlencode, urlparse import requests import responses from ...exceptions import AuthException from .base import BaseBackendTest TEST_KEY = "foo" class DiscourseTest(BaseBackendTest): backend_path = "social_core.backends.discourse.DiscourseAuth" expected_username = "beepboop" raw_complete_url = "/complete/{0}/" def post_start(self): pass def do_start(self): self.post_start() start = self.backend.start() start_url = start.url return_url = self.backend.redirect_uri sso = b64encode( urlencode( { "email": "user@example.com", "username": "beepboop", "nonce": "6YRje7xlXhpyeJ6qtvBeTUjHkXo1UCTQmCrzN8GXfja3AoAFk2CieDRYgSqMYi4W", "return_sso_url": "http://myapp.com/", } ).encode() ).decode() sig = hmac.new(TEST_KEY.encode(), sso.encode(), hashlib.sha256).hexdigest() response_query_params = f"sso={sso}&sig={sig}" response_url = f"{return_url}/?{response_query_params}" responses.add( responses.GET, start_url, status=301, headers={"Location": response_url}, ) responses.add( responses.GET, return_url, status=200, content_type="text/html", ) response = requests.get(start_url, timeout=1) query_values = { k: v[0] for k, v in parse_qs(urlparse(response.url).query).items() } self.strategy.set_request_data(query_values, self.backend) return self.backend.complete() def test_login(self): """ Test that we can authenticate with the Discourse IdP """ # pretend we've started with a URL like /login/discourse: self.strategy.set_settings( {"SERVER_URL": "http://example.com", "SECRET": TEST_KEY} ) self.do_login() def test_failed_login(self): """ Test that authentication fails when our request is signed with a different secret than our payload """ self.strategy.set_settings( {"SERVER_URL": "http://example.com", "SECRET": "bar"} ) with self.assertRaises(AuthException): self.do_login() social-auth-core-4.6.1/social_core/tests/backends/test_disqus.py000066400000000000000000000043141500362547200250060ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class DisqusOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.disqus.DisqusOAuth2" user_data_url = "https://disqus.com/api/3.0/users/details.json" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "code": 0, "response": { "username": "foobar", "numFollowers": 0, "isFollowing": False, "numFollowing": 0, "name": "Foo Bar", "numPosts": 0, "url": "", "isAnonymous": False, "rep": 1.231755, "about": "", "isFollowedBy": False, "connections": {}, "emailHash": "5280f14cedf530b544aecc31fcfe0240", "reputation": 1.231755, "avatar": { "small": { "permalink": "https://disqus.com/api/users/avatars/foobar.jpg", "cache": "https://securecdn.disqus.com/uploads/" "users/453/4556/avatar32.jpg?1285535379", }, "isCustom": False, "permalink": "https://disqus.com/api/users/avatars/foobar.jpg", "cache": "https://securecdn.disqus.com/uploads/users/453/" "4556/avatar92.jpg?1285535379", "large": { "permalink": "https://disqus.com/api/users/avatars/foobar.jpg", "cache": "https://securecdn.disqus.com/uploads/users/" "453/4556/avatar92.jpg?1285535379", }, }, "profileUrl": "http://disqus.com/foobar/", "numLikesReceived": 0, "isPrimary": True, "joinedAt": "2010-09-26T21:09:39", "id": "1010101", "location": "", }, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_dribbble.py000066400000000000000000000011271500362547200252420ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class DribbbleOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.dribbble.DribbbleOAuth2" user_data_url = "https://api.dribbble.com/v1/user" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( {"id": "foobar", "username": "foobar", "name": "Foo Bar"} ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_drip.py000066400000000000000000000011751500362547200244360ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class DripOAuthTest(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.drip.DripOAuth" user_data_url = "https://api.getdrip.com/v2/user" expected_username = "other@example.com" access_token_body = json.dumps( {"access_token": "822bbf7cd12243df", "token_type": "bearer", "scope": "public"} ) user_data_body = json.dumps( {"users": [{"email": "other@example.com", "name": None}]} ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_dropbox.py000066400000000000000000000017071500362547200251560ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class DropboxOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.dropbox.DropboxOAuth2V2" user_data_url = "https://api.dropboxapi.com/2/users/get_current_account" user_data_url_post = True expected_username = "dbidAAH4f99T0taONIb-OurWxbNQ6ywGRopQngc" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "account_id": "dbid:AAH4f99T0taONIb-OurWxbNQ6ywGRopQngc", "name": { "given_name": "Franz", "surname": "Ferdinand", "familiar_name": "Franz", "display_name": "Franz Ferdinand (Personal)", "abbreviated_name": "FF", }, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_dummy.py000066400000000000000000000116761500362547200246420ustar00rootroot00000000000000# pyright: reportAttributeAccessIssue=false import datetime import json import responses from ...actions import do_disconnect from ...backends.oauth import BaseOAuth2 from ...exceptions import AuthForbidden from ..models import User from .oauth import BaseAuthUrlTestMixin, OAuth2Test class DummyOAuth2(BaseOAuth2): name = "dummy" AUTHORIZATION_URL = "http://dummy.com/oauth/authorize" ACCESS_TOKEN_URL = "http://dummy.com/oauth/access_token" REVOKE_TOKEN_URL = "https://dummy.com/oauth/revoke" REVOKE_TOKEN_METHOD = "GET" GET_ALL_EXTRA_DATA = False EXTRA_DATA = [("id", "id"), ("expires", "expires"), ("empty", "empty", True), "url"] def get_user_details(self, response): """Return user details from Github account""" return { "username": response.get("username"), "email": response.get("email", ""), "first_name": response.get("first_name", ""), "last_name": response.get("last_name", ""), } def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" return self.get_json( "http://dummy.com/user", params={"access_token": access_token} ) class Dummy2OAuth2(DummyOAuth2): GET_ALL_EXTRA_DATA = True class DummyOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.tests.backends.test_dummy.DummyOAuth2" user_data_url = "http://dummy.com/user" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "id": 1, "username": "foobar", "url": "http://dummy.com/user/foobar", "first_name": "Foo", "last_name": "Bar", "email": "foo@bAr.coM", # mixed case domain for testing case sensitivity } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() def test_tokens(self): user = self.do_login() self.assertEqual(user.social[0].access_token, "foobar") def test_revoke_token(self): self.strategy.set_settings({"SOCIAL_AUTH_REVOKE_TOKENS_ON_DISCONNECT": True}) self.do_login() user = User.get(self.expected_username) user.password = "password" responses.add( self._method(self.backend.REVOKE_TOKEN_METHOD), self.backend.REVOKE_TOKEN_URL, status=200, ) do_disconnect(self.backend, user) class WhitelistEmailsTest(DummyOAuth2Test): def test_valid_login(self): self.strategy.set_settings({"SOCIAL_AUTH_WHITELISTED_EMAILS": ["foo@bar.com"]}) self.do_login() def test_invalid_login(self): self.strategy.set_settings({"SOCIAL_AUTH_WHITELISTED_EMAILS": ["foo2@bar.com"]}) with self.assertRaises(AuthForbidden): self.do_login() def test_login_case_sensitive_local_part(self): self.strategy.set_settings({"SOCIAL_AUTH_WHITELISTED_EMAILS": ["fOo@bar.com"]}) self.do_login() def test_login_case_sensitive_domain(self): self.strategy.set_settings({"SOCIAL_AUTH_WHITELISTED_EMAILS": ["foo@bAR.com"]}) self.do_login() class WhitelistDomainsTest(DummyOAuth2Test): def test_valid_login(self): self.strategy.set_settings({"SOCIAL_AUTH_WHITELISTED_DOMAINS": ["bar.com"]}) self.do_login() def test_invalid_login(self): self.strategy.set_settings({"SOCIAL_AUTH_WHITELISTED_EMAILS": ["bar2.com"]}) with self.assertRaises(AuthForbidden): self.do_login() DELTA = datetime.timedelta(days=1) class ExpirationTimeTest(DummyOAuth2Test): user_data_body = json.dumps( { "id": 1, "username": "foobar", "url": "http://dummy.com/user/foobar", "first_name": "Foo", "last_name": "Bar", "email": "foo@bar.com", "expires": (datetime.datetime.now() + DELTA).timestamp(), } ) def test_expires_time(self): user = self.do_login() social = user.social[0] expiration = social.expiration_timedelta() self.assertEqual(expiration <= DELTA, True) class AllExtraDataTest(DummyOAuth2Test): backend_path = "social_core.tests.backends.test_dummy.Dummy2OAuth2" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "id": 1, "username": "foobar", "url": "http://dummy.com/user/foobar", "first_name": "Foo", "last_name": "Bar", "email": "foo@bar.com", "not_normally_in_extra_data": "value", } ) def test_get_all_extra_data(self): user = self.do_login() social = user.social[0] self.assertIn("not_normally_in_extra_data", social.extra_data) self.assertEqual(len(social.extra_data), 10) # Includes auth_time. social-auth-core-4.6.1/social_core/tests/backends/test_edmodo.py000066400000000000000000000026611500362547200247500ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class EdmodoOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.edmodo.EdmodoOAuth2" user_data_url = "https://api.edmodo.com/users/me" expected_username = "foobar12345" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "username": "foobar12345", "coppa_verified": False, "first_name": "Foo", "last_name": "Bar", "premium": False, "verified_institution_member": False, "url": "https://api.edmodo.com/users/12345", "type": "teacher", "time_zone": None, "end_level": None, "start_level": None, "locale": "en", "subjects": None, "utc_offset": None, "email": "foo.bar@example.com", "gender": None, "about": None, "user_title": None, "id": 12345, "avatars": { "small": "https://api.edmodo.com/users/12345/avatar?type=small&u=5a15xug93m53mi4ey3ck4fvkq", "large": "https://api.edmodo.com/users/12345/avatar?type=large&u=5a15xug93m53mi4ey3ck4fvkq", }, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_egi_checkin.py000066400000000000000000000243441500362547200257330ustar00rootroot00000000000000from social_core.backends.egi_checkin import EGICheckinOpenIdConnect from .oauth import BaseAuthUrlTestMixin from .open_id_connect import OpenIdConnectTest class EGICheckinOpenIdConnectTest( OpenIdConnectTest[EGICheckinOpenIdConnect], BaseAuthUrlTestMixin ): backend_path = "social_core.backends.egi_checkin.EGICheckinOpenIdConnect" issuer = "https://aai.egi.eu/auth/realms/egi" openid_config_body = """ { "issuer": "https://aai.egi.eu/auth/realms/egi", "authorization_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/auth", "token_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/token", "introspection_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/token/introspect", "userinfo_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/userinfo", "end_session_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/logout", "frontchannel_logout_session_supported": true, "frontchannel_logout_supported": true, "jwks_uri": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/certs", "check_session_iframe": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/login-status-iframe.html", "grant_types_supported": [ "authorization_code", "implicit", "refresh_token", "password", "client_credentials", "urn:ietf:params:oauth:grant-type:device_code", "urn:openid:params:grant-type:ciba", "urn:ietf:params:oauth:grant-type:token-exchange" ], "acr_values_supported": ["0", "1"], "response_types_supported": [ "code", "none", "id_token", "token", "id_token token", "code id_token", "code token", "code id_token token" ], "subject_types_supported": ["public", "pairwise"], "id_token_signing_alg_values_supported": [ "PS384", "ES384", "RS384", "HS256", "HS512", "ES256", "RS256", "HS384", "ES512", "PS256", "PS512", "RS512" ], "id_token_encryption_alg_values_supported": [ "RSA-OAEP", "RSA-OAEP-256", "RSA1_5" ], "id_token_encryption_enc_values_supported": [ "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512" ], "userinfo_signing_alg_values_supported": [ "PS384", "ES384", "RS384", "HS256", "HS512", "ES256", "RS256", "HS384", "ES512", "PS256", "PS512", "RS512", "none" ], "userinfo_encryption_alg_values_supported": [ "RSA-OAEP", "RSA-OAEP-256", "RSA1_5" ], "userinfo_encryption_enc_values_supported": [ "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512" ], "request_object_signing_alg_values_supported": [ "PS384", "ES384", "RS384", "HS256", "HS512", "ES256", "RS256", "HS384", "ES512", "PS256", "PS512", "RS512", "none" ], "request_object_encryption_alg_values_supported": [ "RSA-OAEP", "RSA-OAEP-256", "RSA1_5" ], "request_object_encryption_enc_values_supported": [ "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512" ], "response_modes_supported": [ "query", "fragment", "form_post", "query.jwt", "fragment.jwt", "form_post.jwt", "jwt" ], "registration_endpoint": "https://aai.egi.eu/auth/realms/egi/clients-registrations/openid-connect", "token_endpoint_auth_methods_supported": [ "private_key_jwt", "client_secret_basic", "client_secret_post", "tls_client_auth", "client_secret_jwt" ], "token_endpoint_auth_signing_alg_values_supported": [ "PS384", "ES384", "RS384", "HS256", "HS512", "ES256", "RS256", "HS384", "ES512", "PS256", "PS512", "RS512" ], "introspection_endpoint_auth_methods_supported": [ "private_key_jwt", "client_secret_basic", "client_secret_post", "tls_client_auth", "client_secret_jwt" ], "introspection_endpoint_auth_signing_alg_values_supported": [ "PS384", "ES384", "RS384", "HS256", "HS512", "ES256", "RS256", "HS384", "ES512", "PS256", "PS512", "RS512" ], "authorization_signing_alg_values_supported": [ "PS384", "ES384", "RS384", "HS256", "HS512", "ES256", "RS256", "HS384", "ES512", "PS256", "PS512", "RS512" ], "authorization_encryption_alg_values_supported": [ "RSA-OAEP", "RSA-OAEP-256", "RSA1_5" ], "authorization_encryption_enc_values_supported": [ "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512" ], "claims_supported": [ "acr", "cert_entitlement", "eduperson_assurance", "eduperson_entitlement", "eduperson_scoped_affiliation", "eduperson_unique_id", "email", "email_verified", "family_name", "given_name", "name", "orcid", "preferred_username", "ssh_public_key", "sub", "voperson_external_affiliation", "voperson_id", "voperson_verified_email" ], "claim_types_supported": ["normal"], "claims_parameter_supported": true, "scopes_supported": [ "openid", "voperson_external_affiliation", "email", "orcid", "aarc", "cert_entitlement", "eduperson_scoped_affiliation", "voperson_id", "ssh_public_key", "profile", "offline_access", "eduperson_unique_id", "eduperson_entitlement" ], "request_parameter_supported": true, "request_uri_parameter_supported": true, "require_request_uri_registration": true, "code_challenge_methods_supported": ["plain", "S256"], "tls_client_certificate_bound_access_tokens": true, "revocation_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/revoke", "revocation_endpoint_auth_methods_supported": [ "private_key_jwt", "client_secret_basic", "client_secret_post", "tls_client_auth", "client_secret_jwt" ], "revocation_endpoint_auth_signing_alg_values_supported": [ "PS384", "ES384", "RS384", "HS256", "HS512", "ES256", "RS256", "HS384", "ES512", "PS256", "PS512", "RS512" ], "backchannel_logout_supported": true, "backchannel_logout_session_supported": true, "device_authorization_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/auth/device", "backchannel_token_delivery_modes_supported": ["poll", "ping"], "backchannel_authentication_request_signing_alg_values_supported": [ "PS384", "ES384", "RS384", "ES256", "RS256", "ES512", "PS256", "PS512", "RS512" ], "require_pushed_authorization_requests": false, "mtls_endpoint_aliases": { "token_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/token", "revocation_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/revoke", "introspection_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/token/introspect", "device_authorization_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/auth/device", "registration_endpoint": "https://aai.egi.eu/auth/realms/egi/clients-registrations/openid-connect", "userinfo_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/userinfo" } } """ def test_do_not_override_endpoint(self): self.backend.OIDC_ENDPOINT = self.issuer self.assertEqual(self.backend.oidc_endpoint(), self.issuer) def test_checkin_env_prod(self): self.assertEqual( self.backend.oidc_endpoint(), "https://aai.egi.eu/auth/realms/egi" ) def test_checkin_env_demo(self): self.backend.CHECKIN_ENV = "demo" self.assertEqual( self.backend.oidc_endpoint(), "https://aai-demo.egi.eu/auth/realms/egi" ) def test_checkin_env_dev(self): self.backend.CHECKIN_ENV = "dev" self.assertEqual( self.backend.oidc_endpoint(), "https://aai-dev.egi.eu/auth/realms/egi" ) def test_entitlements_empty(self): self.assertEqual(self.backend.entitlement_allowed([]), True) def test_entitlements_allowed(self): self.backend.ALLOWED_ENTITLEMENTS = ["foo", "baz"] self.assertEqual(self.backend.entitlement_allowed(["foo", "bar"]), True) def test_entitlements_not_allowed(self): self.backend.ALLOWED_ENTITLEMENTS = ["baz"] self.assertEqual(self.backend.entitlement_allowed(["foo"]), False) social-auth-core-4.6.1/social_core/tests/backends/test_einfracz.py000066400000000000000000000116561500362547200253060ustar00rootroot00000000000000from .oauth import BaseAuthUrlTestMixin from .open_id_connect import OpenIdConnectTest class EInfraCZOpenIdConnectTest(OpenIdConnectTest, BaseAuthUrlTestMixin): backend_path = "social_core.backends.einfracz.EInfraCZOpenIdConnect" issuer = "https://login.e-infra.cz/oidc/" openid_config_body = """ { "request_parameter_supported":true, "claims_parameter_supported":false, "introspection_endpoint":"https://login.e-infra.cz/oidc/introspect", "scopes_supported":[ "openid", "profile", "email", "address", "phone", "offline_access" ], "issuer":"https://login.e-infra.cz/oidc/", "userinfo_encryption_enc_values_supported":[ "XC20P", "A256CBC+HS512", "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512", "A128CBC+HS256" ], "id_token_encryption_enc_values_supported":[ "XC20P", "A256CBC+HS512", "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512", "A128CBC+HS256" ], "authorization_endpoint":"https://login.e-infra.cz/oidc/authorize", "service_documentation":"https://login.e-infra.cz/oidc/about", "request_object_encryption_enc_values_supported":[ "XC20P", "A256CBC+HS512", "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512", "A128CBC+HS256" ], "device_authorization_endpoint":"https://login.e-infra.cz/oidc/devicecode", "userinfo_signing_alg_values_supported":[ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512" ], "claims_supported":[ "sub", "name", "preferred_username", "given_name", "family_name", "middle_name", "nickname", "profile", "picture", "website", "gender", "zoneinfo", "locale", "updated_at", "birthdate", "email", "email_verified", "phone_number", "phone_number_verified", "address" ], "claim_types_supported":[ "normal" ], "op_policy_uri":"https://login.e-infra.cz/oidc/about", "token_endpoint_auth_methods_supported":[ "client_secret_post", "client_secret_basic", "client_secret_jwt", "private_key_jwt", "none" ], "token_endpoint":"https://login.e-infra.cz/oidc/token", "response_types_supported":[ "code", "token" ], "request_uri_parameter_supported":false, "userinfo_encryption_alg_values_supported":[ "RSA-OAEP-512", "RSA-OAEP", "RSA-OAEP-256", "RSA1_5", "RSA-OAEP-384" ], "grant_types_supported":[ "authorization_code", "implicit", "urn:ietf:params:oauth:grant-type:jwt-bearer", "client_credentials", "urn:ietf:params:oauth:grant_type:redelegate", "urn:ietf:params:oauth:grant-type:device_code", "refresh_token" ], "end_session_endpoint":"https://login.e-infra.cz/oidc/endsession", "revocation_endpoint":"https://login.e-infra.cz/oidc/revoke", "userinfo_endpoint":"https://login.e-infra.cz/oidc/userinfo", "token_endpoint_auth_signing_alg_values_supported":[ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512" ], "op_tos_uri":"https://login.e-infra.cz/oidc/about", "require_request_uri_registration":false, "code_challenge_methods_supported":[ "plain", "S256" ], "id_token_encryption_alg_values_supported":[ "RSA-OAEP-512", "RSA-OAEP", "RSA-OAEP-256", "RSA1_5", "RSA-OAEP-384" ], "jwks_uri":"https://login.e-infra.cz/oidc/jwk", "subject_types_supported":[ "public", "pairwise" ], "id_token_signing_alg_values_supported":[ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "none" ], "registration_endpoint":"https://login.e-infra.cz/oidc/register", "request_object_signing_alg_values_supported":[ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512" ], "request_object_encryption_alg_values_supported":[ "RSA-OAEP-512", "RSA-OAEP", "RSA-OAEP-256", "RSA1_5", "RSA-OAEP-384" ] } """ social-auth-core-4.6.1/social_core/tests/backends/test_elixir.py000066400000000000000000000125771500362547200250040ustar00rootroot00000000000000from .oauth import BaseAuthUrlTestMixin from .open_id_connect import OpenIdConnectTest class ElixirOpenIdConnectTest(OpenIdConnectTest, BaseAuthUrlTestMixin): backend_path = "social_core.backends.elixir.ElixirOpenIdConnect" issuer = "https://login.elixir-czech.org/oidc/" openid_config_body = """ { "claims_supported": [ "sub", "name", "preferred_username", "given_name", "family_name", "middle_name", "nickname", "profile", "picture", "website", "gender", "zoneinfo", "locale", "updated_at", "birthdate", "email", "email_verified", "phone_number", "phone_number_verified", "address" ], "op_policy_uri": "https://login.elixir-czech.org/oidc/about", "subject_types_supported": [ "public", "pairwise" ], "request_parameter_supported": true, "userinfo_signing_alg_values_supported": [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512" ], "revocation_endpoint": "https://login.elixir-czech.org/oidc/revoke", "issuer": "https://login.elixir-czech.org/oidc/", "id_token_encryption_enc_values_supported": [ "A256CBC+HS512", "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512", "A128CBC+HS256" ], "require_request_uri_registration": false, "grant_types_supported": [ "authorization_code", "implicit", "urn:ietf:params:oauth:grant-type:jwt-bearer", "client_credentials", "urn:ietf:params:oauth:grant_type:redelegate", "urn:ietf:params:oauth:grant-type:device_code" ], "token_endpoint": "https://login.elixir-czech.org/oidc/token", "request_uri_parameter_supported": false, "service_documentation": "https://login.elixir-czech.org/oidc/about", "registration_endpoint": "https://login.elixir-czech.org/oidc/register", "jwks_uri": "https://login.elixir-czech.org/oidc/jwk", "userinfo_encryption_alg_values_supported": [ "RSA-OAEP", "RSA-OAEP-256", "RSA1_5" ], "scopes_supported": [], "token_endpoint_auth_methods_supported": [ "client_secret_post", "client_secret_basic", "client_secret_jwt", "private_key_jwt", "none" ], "userinfo_encryption_enc_values_supported": [ "A256CBC+HS512", "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512", "A128CBC+HS256" ], "claim_types_supported": [ "normal" ], "request_object_encryption_enc_values_supported": [ "A256CBC+HS512", "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512", "A128CBC+HS256" ], "claims_parameter_supported": false, "id_token_encryption_alg_values_supported": [ "RSA-OAEP", "RSA-OAEP-256", "RSA1_5" ], "code_challenge_methods_supported": [ "plain", "S256" ], "token_endpoint_auth_signing_alg_values_supported": [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512" ], "userinfo_endpoint": "https://login.elixir-czech.org/oidc/userinfo", "introspection_endpoint": "https://login.elixir-czech.org/oidc/introspect", "id_token_signing_alg_values_supported": [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "none" ], "device_authorization_endpoint": "https://login.elixir-czech.org/oidc/devicecode", "op_tos_uri": "https://login.elixir-czech.org/oidc/about", "request_object_encryption_alg_values_supported": [ "RSA-OAEP", "RSA-OAEP-256", "RSA1_5" ], "request_object_signing_alg_values_supported": [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512" ], "response_types_supported": [ "code", "token" ], "end_session_endpoint": "https://login.elixir-czech.org/oidc/endsession", "authorization_endpoint": "https://login.elixir-czech.org/oidc/authorize" } """ social-auth-core-4.6.1/social_core/tests/backends/test_email.py000066400000000000000000000007241500362547200245660ustar00rootroot00000000000000from .legacy import BaseLegacyTest class EmailTest(BaseLegacyTest): backend_path = "social_core.backends.email.EmailAuth" expected_username = "foo" response_body = "email=foo@bar.com" form = """
""" def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_etsy.py000066400000000000000000000061341500362547200244640ustar00rootroot00000000000000# pyright: reportAttributeAccessIssue=false import json from .oauth import OAuth2PkceS256Test class EtsyOAuth2Mixin: backend_path = "social_core.backends.etsy.EtsyOAuth2" access_token_body = json.dumps( { "access_token": "dummy_user_id.dummy_access_token", "token_type": "bearer", "expires_in": 3600, "refresh_token": "dummy_user_id.dummy_refresh_token", } ) refresh_token_body = json.dumps( { "access_token": "dummy_user_id.dummy_access_token_refreshed", "token_type": "bearer", "expires_in": 3600, "refresh_token": "dummy_user_id.dummy_refresh_token_refreshed", } ) user_data_url = "https://openapi.etsy.com/v3/application/users/dummy_user_id" user_data_body = json.dumps( { "user_id": "dummy_user_id", "primary_email": "amitray@developer.com", "first_name": "Amit", "last_name": "Ray", "image_url_75x75": "http://www.gravatar.com/avatar/af7d968fe79ea45271e3100391824b79.jpg?s=48&d=mm", } ) expected_username = "dummy_user_id" def test_login(self): user = self.do_login() self.assertEqual(len(user.social), 1) social = user.social[0] self.assertEqual(social.uid, "dummy_user_id") self.assertEqual(social.extra_data["first_name"], "Amit") self.assertEqual(social.extra_data["last_name"], "Ray") self.assertEqual(social.extra_data["primary_email"], "amitray@developer.com") self.assertEqual( social.extra_data["image_url_75x75"], "http://www.gravatar.com/avatar/af7d968fe79ea45271e3100391824b79.jpg?s=48&d=mm", ) self.assertEqual( social.extra_data["access_token"], "dummy_user_id.dummy_access_token" ) self.assertEqual(social.extra_data["token_type"], "bearer") self.assertEqual(social.extra_data["expires_in"], 3600) self.assertEqual( social.extra_data["refresh_token"], "dummy_user_id.dummy_refresh_token" ) def test_refresh_token(self): _, social = self.do_refresh_token() self.assertEqual(social.uid, "dummy_user_id") self.assertEqual(social.extra_data["first_name"], "Amit") self.assertEqual(social.extra_data["last_name"], "Ray") self.assertEqual(social.extra_data["primary_email"], "amitray@developer.com") self.assertEqual( social.extra_data["image_url_75x75"], "http://www.gravatar.com/avatar/af7d968fe79ea45271e3100391824b79.jpg?s=48&d=mm", ) self.assertEqual( social.extra_data["access_token"], "dummy_user_id.dummy_access_token_refreshed", ) self.assertEqual(social.extra_data["token_type"], "bearer") self.assertEqual(social.extra_data["expires_in"], 3600) self.assertEqual( social.extra_data["refresh_token"], "dummy_user_id.dummy_refresh_token_refreshed", ) class EtsyOAuth2TestPkceS256( EtsyOAuth2Mixin, OAuth2PkceS256Test, ): pass social-auth-core-4.6.1/social_core/tests/backends/test_eventbrite.py000066400000000000000000000022361500362547200256460ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class EventbriteAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.eventbrite.EventbriteOAuth2" user_data_url = "https://www.eventbriteapi.com/v3/users/me" expected_username = "sean+awesome@eventbrite.com" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "first_name": "sean", "last_name": "rose", "name": "sean rose", "access_token": "YQEN5H2W2OTKQZWZMYER", "emails": [ { "verified": True, "email": "sean+awesome2@eventbrite.com", "primary": False, }, { "verified": True, "email": "sean+awesome@eventbrite.com", "primary": True, }, ], "token_type": "bearer", "image_id": None, "is_public": False, "id": "128559602587", } ) def test_login(self): self.do_login() social-auth-core-4.6.1/social_core/tests/backends/test_evernote.py000066400000000000000000000033401500362547200253230ustar00rootroot00000000000000from urllib.parse import urlencode from requests import HTTPError from ...exceptions import AuthCanceled from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test class EvernoteOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.evernote.EvernoteOAuth" expected_username = "101010" access_token_body = urlencode( { "edam_webApiUrlPrefix": "https://sandbox.evernote.com/shard/s1/", "edam_shard": "s1", "oauth_token": "foobar", "edam_expires": "1395118279645", "edam_userId": "101010", "edam_noteStoreUrl": "https://sandbox.evernote.com/shard/s1/notestore", } ) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", "oauth_callback_confirmed": "true", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() class EvernoteOAuth1CanceledTest(EvernoteOAuth1Test): access_token_status = 401 def test_login(self): with self.assertRaises(AuthCanceled) as cm: self.do_login() self.assertTrue(cm.exception.response is not None) def test_partial_pipeline(self): with self.assertRaises(AuthCanceled) as cm: self.do_partial_pipeline() self.assertTrue(cm.exception.response is not None) class EvernoteOAuth1ErrorTest(EvernoteOAuth1Test): access_token_status = 500 def test_login(self): with self.assertRaises(HTTPError): self.do_login() def test_partial_pipeline(self): with self.assertRaises(HTTPError): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_facebook.py000066400000000000000000000061001500362547200252420ustar00rootroot00000000000000import json from ...backends.facebook import API_VERSION from ...exceptions import AuthCanceled, AuthUnknownError from .oauth import BaseAuthUrlTestMixin, OAuth2Test from .open_id_connect import OpenIdConnectTest class FacebookOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.facebook.FacebookOAuth2" user_data_url = f"https://graph.facebook.com/v{API_VERSION}/me" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "username": "foobar", "first_name": "Foo", "last_name": "Bar", "verified": True, "name": "Foo Bar", "gender": "male", "updated_time": "2013-02-13T14:59:42+0000", "link": "http://www.facebook.com/foobar", "id": "110011001100010", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() class FacebookOAuth2WrongUserDataTest(FacebookOAuth2Test): user_data_body = "null" def test_login(self): with self.assertRaises(AuthUnknownError): self.do_login() def test_partial_pipeline(self): with self.assertRaises(AuthUnknownError): self.do_partial_pipeline() class FacebookOAuth2AuthCancelTest(FacebookOAuth2Test): access_token_status = 400 access_token_body = json.dumps( { "error": { "message": "redirect_uri isn't an absolute URI. Check RFC 3986.", "code": 191, "type": "OAuthException", "fbtrace_id": "123Abc", } } ) def test_login(self): with self.assertRaises(AuthCanceled) as cm: self.do_login() self.assertIn("error", cm.exception.response.json()) def test_partial_pipeline(self): with self.assertRaises(AuthCanceled) as cm: self.do_partial_pipeline() self.assertIn("error", cm.exception.response.json()) class FacebookLimitedLoginTest(OpenIdConnectTest): backend_path = "social_core.backends.facebook_limited.FacebookLimitedLogin" issuer = "https://facebook.com" openid_config_body = """ { "issuer": "https://facebook.com", "authorization_endpoint": "https://facebook.com/dialog/oauth/", "jwks_uri": "https://facebook.com/.well-known/oauth/openid/jwks/", "response_types_supported": [ "id_token", "token id_token" ], "subject_types_supported": "pairwise", "id_token_signing_alg_values_supported": [ "RS256" ], "claims_supported": [ "iss", "aud", "sub", "iat", "exp", "jti", "nonce", "at_hash", "name", "email", "picture", "user_friends", "user_birthday", "user_age_range" ] } """ def test_invalid_nonce(self): # The nonce isn't generated server-side so the test isn't relevant here. pass social-auth-core-4.6.1/social_core/tests/backends/test_fedora.py000066400000000000000000000107671500362547200247470ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin from .open_id_connect import OpenIdConnectTest class FedoraOpenIdConnectTest(OpenIdConnectTest, BaseAuthUrlTestMixin): backend_path = "social_core.backends.fedora.FedoraOpenIdConnect" user_data_url = "https://id.fedoraproject.org/openidc/UserInfo" issuer = "https://id.fedoraproject.org/openidc/" openid_config_body = json.dumps( { "issuer": "https://id.fedoraproject.org/openidc/", "authorization_endpoint": "https://id.fedoraproject.org/openidc/Authorization", "token_endpoint": "https://id.fedoraproject.org/openidc/Token", "token_introspection_endpoint": "https://id.fedoraproject.org/openidc/TokenInfo", "introspection_endpoint": "https://id.fedoraproject.org/openidc/TokenInfo", "userinfo_endpoint": "https://id.fedoraproject.org/openidc/UserInfo", "jwks_uri": "https://id.fedoraproject.org/openidc/Jwks", "scopes_supported": [ "openid", "profile", "email", "address", "phone", "https://id.fedoraproject.org/scope/groups", "https://id.fedoraproject.org/scope/agreements", "https://id.fedoraproject.org/scope/fas-attributes", "https://github.com/jmflinuxtx/kerneltest-harness/oidc/upload_test_run", "https://src.fedoraproject.org/push", "https://waiverdb.fedoraproject.org/oidc/create-waiver", "https://fedoraproject.org/wiki/api", ], "response_types_supported": ["code", "id_token", "token", "token id_token"], "response_modes_supported": [ "query", "fragment", "form_post", "oob", "none", ], "grant_types_supported": [ "authorization_code", "implicit", "refresh_token", ], "acr_values_supported": ["0"], "subject_types_supported": ["pairwise", "public"], "id_token_signing_alg_values_supported": ["RS256"], "id_token_encryption_alg_values_supported": [], "id_token_encryption_enc_values_supported": [], "userinfo_signing_alg_values_supported": ["RS256"], "userinfo_encryption_alg_values_supported": [], "userinfo_encryption_enc_values_supported": [], "request_object_signing_alg_values_supported": [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "EdDSA", "ES256K", "none", ], "request_object_encryption_alg_values_supported": [], "request_object_encryption_enc_values_supported": [], "token_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post", ], "token_endpoint_auth_signing_alg_values_supported": ["RS256"], "display_values_supported": ["page", "popup"], "claim_types_supported": ["normal"], "claims_supported": [ "sub", "name", "given_name", "family_name", "middle_name", "nickname", "preferred_username", "profile", "picture", "website", "email", "email_verified", "gender", "birthdate", "zoneinfo", "locale", "phone_number", "phone_number_verified", "address", "updated_at", ], "service_documentation": "https://fedoraproject.org/wiki/Infrastructure/Authentication/", "ui_locales_supported": ["en"], "claims_parameter_supported": True, "request_parameter_supported": True, "request_uri_parameter_supported": True, "require_request_uri_registration": False, "op_policy_uri": "https://fedoraproject.org/wiki/Legal:PrivacyPolicy/", "op_tos_uri": "https://fedoraproject.org/wiki/Legal:PrivacyPolicy/", } ) social-auth-core-4.6.1/social_core/tests/backends/test_fence.py000066400000000000000000000032241500362547200245550ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin from .open_id_connect import OpenIdConnectTest class FenceOpenIdConnectTest(OpenIdConnectTest, BaseAuthUrlTestMixin): backend_path = "social_core.backends.fence.Fence" issuer = "https://nci-crdc.datacommons.io/" openid_config_body = json.dumps( { "issuer": "https://nci-crdc.datacommons.io/", "authorization_endpoint": "https://nci-crdc.datacommons.io/user/oauth2/authorize", "userinfo_endpoint": "https://nci-crdc.datacommons.io/user/user/", "token_endpoint": "https://nci-crdc.datacommons.io/user/oauth2/token", "revocation_endpoint": "https://nci-crdc.datacommons.io/user/oauth2/revoke", "jwks_uri": "https://auth.globus.org/jwk.json", "response_types_supported": ["code", "token", "token id_token", "id_token"], "id_token_signing_alg_values_supported": ["RS512"], "scopes_supported": [ "ga4gh_passport_v1", "openid", "google_credentials", "google_service_account", "data", "user", "google_link", "admin", "fence", ], "token_endpoint_auth_methods_supported": ["authorization_code", "implicit"], "claims_supported": [ "aud", "sub", "iss", "exp", "jti", "auth_time", "azp", "nonce", "context", ], "subject_types_supported": ["public"], } ) social-auth-core-4.6.1/social_core/tests/backends/test_fitbit.py000066400000000000000000000033271500362547200247620ustar00rootroot00000000000000import json from urllib.parse import urlencode from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test class FitbitOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.fitbit.FitbitOAuth1" expected_username = "foobar" access_token_body = urlencode( { "oauth_token_secret": "a-secret", "encoded_user_id": "101010", "oauth_token": "foobar", } ) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", "oauth_callback_confirmed": "true", } ) user_data_url = "https://api.fitbit.com/1/user/-/profile.json" user_data_body = json.dumps( { "user": { "weightUnit": "en_US", "strideLengthWalking": 0, "displayName": "foobar", "weight": 62.6, "foodsLocale": "en_US", "heightUnit": "en_US", "locale": "en_US", "gender": "NA", "memberSince": "2011-12-26", "offsetFromUTCMillis": -25200000, "height": 0, "timezone": "America/Los_Angeles", "dateOfBirth": "", "encodedId": "101010", "avatar": "http://www.fitbit.com/images/profile/" "defaultProfile_100_male.gif", "waterUnit": "en_US", "distanceUnit": "en_US", "glucoseUnit": "en_US", "strideLengthRunning": 0, } } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_five_hundred_px.py000066400000000000000000000077741500362547200266640ustar00rootroot00000000000000import json from urllib.parse import urlencode from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test class FiveHundredPxOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.five_hundred_px.FiveHundredPxOAuth" user_data_url = "https://api.500px.com/v1/users" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", "oauth_callback_confirmed": "true", } ) user_data_body = json.dumps( { "user": { "id": 10101010, "username": "foobar", "firstname": "", "lastname": "", "birthday": None, "sex": 1, "city": "", "state": "", "country": "", "registration_date": "2011-11-11T00:00:00-00:00", "about": "", "usertype": 0, "domain": "UserName.500px.com", "fotomoto_on": False, "locale": "en", "show_nude": False, "allow_sale_requests": 1, "fullname": "UserName", "userpic_url": "https://graph.facebook.com/v2.7/" "1000000000/picture?height=100&width=100", "userpic_https_url": "https://graph.facebook.com/v2.7/" "1000000000/picture?" "height=100&width=100", "cover_url": None, "upgrade_status": 0, "store_on": False, "photos_count": 0, "galleries_count": 0, "affection": 51, "in_favorites_count": 0, "friends_count": 2, "followers_count": 3, "analytics_code": None, "invite_pending": False, "invite_accepted": False, "email": "user@user.com", "shadow_email": "user@user.com", "upload_limit": 20, "upload_limit_expiry": "2021-11-11T00:00:00-00:00", "upgrade_type": 0, "upgrade_status_expiry": "2011-11-21", "portfolio_enabled": False, "auth": {"facebook": 1, "twitter": 0, "google_oauth2": 1}, "presubmit_for_licensing": None, "contacts": {"facebook": "1000000000"}, "equipment": {}, "avatars": { "default": { "http": "https://graph.facebook.com/v2.7/" "1000000000/picture?height=100&width=100", "https": "https://graph.facebook.com/v2.7/" "1000000000/picture?height=100&width=100", }, "large": { "http": "https://graph.facebook.com/v2.7/" "1000000000/picture?height=100&width=100", "https": "https://graph.facebook.com/v2.7/" "1000000000/picture?height=100&width=100", }, "small": { "http": "https://graph.facebook.com/v2.7/" "1000000000/picture?height=100&width=100", "https": "https://graph.facebook.com/v2.7/" "1000000000/picture?height=100&width=100", }, "tiny": { "http": "https://graph.facebook.com/v2.7/" "1000000000/picture?height=100&width=100", "https": "https://graph.facebook.com/v2.7/" "1000000000/picture?height=100&width=100", }, }, } } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_flat.py000066400000000000000000000022611500362547200244230ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class FlatOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.flat.FlatOAuth2" user_data_url = "https://api.flat.io/v2/me" expected_username = "vincent" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "id": "541a137946fd04d57cb2e3c0", "username": "vincent", "name": "Vincent Foo", "printableName": "Vincent Foo", "bio": "Foo bio", "instruments": [], "picture": "https://flat-prod-public.s3.amazonaws.com/00000000/a0d2cb86-ab1e-4fdb-9286-dd94aa6d386c.jpeg", "registrationDate": "2014-09-17T23:04:25.042Z", "htmlUrl": "https://flat.io/vincent", "starredScoresCount": 85, "likedScoresCount": 85, "followersCount": 183, "followingCount": 52, "ownedPublicScoresCount": 15, "isPowerUser": True, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_flickr.py000066400000000000000000000014231500362547200247460ustar00rootroot00000000000000from urllib.parse import urlencode from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test class FlickrOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.flickr.FlickrOAuth" expected_username = "foobar" access_token_body = urlencode( { "oauth_token_secret": "a-secret", "username": "foobar", "oauth_token": "foobar", "user_nsid": "10101010@N01", } ) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", "oauth_callback_confirmed": "true", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_foursquare.py000066400000000000000000000077111500362547200256760ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class FoursquareOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.foursquare.FoursquareOAuth2" user_data_url = "https://api.foursquare.com/v2/users/self" expected_username = "FooBar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "notifications": [{"item": {"unreadCount": 0}, "type": "notificationTray"}], "meta": { "errorType": "deprecated", "code": 200, "errorDetail": "Please provide an API version to avoid future " "errors.See http://bit.ly/vywCav", }, "response": { "user": { "photo": "https://is0.4sqi.net/userpix_thumbs/BYKIT01VN4T4BISN.jpg", "pings": False, "homeCity": "Foo, Bar", "id": "1010101", "badges": {"count": 0, "items": []}, "friends": { "count": 1, "groups": [ { "count": 0, "items": [], "type": "friends", "name": "Mutual friends", }, { "count": 1, "items": [ { "bio": "", "gender": "male", "firstName": "Baz", "relationship": "friend", "photo": "https://is0.4sqi.net/userpix_thumbs/" "BYKIT01VN4T4BISN.jpg", "lists": { "groups": [ { "count": 1, "items": [], "type": "created", } ] }, "homeCity": "Baz, Qux", "lastName": "Qux", "tips": {"count": 0}, "id": "10101010", } ], "type": "others", "name": "Other friends", }, ], }, "referralId": "u-1010101", "tips": {"count": 0}, "type": "user", "todos": {"count": 0}, "bio": "", "relationship": "self", "lists": {"groups": [{"count": 1, "items": [], "type": "created"}]}, "photos": {"count": 0, "items": []}, "checkinPings": "off", "scores": {"max": 0, "checkinsCount": 0, "goal": 50, "recent": 0}, "checkins": {"count": 0}, "firstName": "Foo", "gender": "male", "contact": {"email": "foo@bar.com"}, "lastName": "Bar", "following": {"count": 0}, "requests": {"count": 0}, "mayorships": {"count": 0, "items": []}, } }, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_gitea.py000066400000000000000000000044351500362547200245730ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class GiteaOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.gitea.GiteaOAuth2" user_data_url = "https://gitea.com/api/v1/user" expected_username = "foobar" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", "expires_in": 7200, "refresh_token": "barfoo", } ) user_data_body = json.dumps( { "id": 123456, "login": "foobar", "full_name": "Foo Bar", "email": "foobar@example.com", "avatar_url": "https://gitea.com/user/avatar/foobar/-1", "language": "en-US", "is_admin": False, "last_login": "2016-12-28T12:26:19+01:00", "created": "2016-12-28T12:26:19+01:00", "restricted": False, "username": "foobar", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() class GiteaCustomDomainOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.gitea.GiteaOAuth2" user_data_url = "https://example.com/api/v1/user" expected_username = "foobar" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", "expires_in": 7200, "refresh_token": "barfoo", } ) user_data_body = json.dumps( { "id": 123456, "login": "foobar", "full_name": "Foo Bar", "email": "foobar@example.com", "avatar_url": "https://example.com/user/avatar/foobar/-1", "language": "en-US", "is_admin": False, "last_login": "2016-12-28T12:26:19+01:00", "created": "2016-12-28T12:26:19+01:00", "restricted": False, "username": "foobar", } ) def test_login(self): self.strategy.set_settings({"SOCIAL_AUTH_GITEA_API_URL": "https://example.com"}) self.do_login() def test_partial_pipeline(self): self.strategy.set_settings({"SOCIAL_AUTH_GITEA_API_URL": "https://example.com"}) self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_github.py000066400000000000000000000206071500362547200247630ustar00rootroot00000000000000import json import responses from ...exceptions import AuthFailed from .oauth import BaseAuthUrlTestMixin, OAuth2Test class GithubOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.github.GithubOAuth2" user_data_url = "https://api.github.com/user" expected_username = "foobar" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", "expires_in": 28800, "refresh_token": "foobar-refresh-token", } ) refresh_token_body = json.dumps( { "access_token": "foobar-new-token", "token_type": "bearer", "expires_in": 28800, "refresh_token": "foobar-new-refresh-token", "refresh_token_expires_in": 15897600, "scope": "", } ) user_data_body = json.dumps( { "login": "foobar", "id": 1, "avatar_url": "https://github.com/images/error/foobar_happy.gif", "gravatar_id": "somehexcode", "url": "https://api.github.com/users/foobar", "name": "monalisa foobar", "company": "GitHub", "blog": "https://github.com/blog", "location": "San Francisco", "email": "foo@bar.com", "hireable": False, "bio": "There once was...", "public_repos": 2, "public_gists": 1, "followers": 20, "following": 0, "html_url": "https://github.com/foobar", "created_at": "2008-01-14T04:33:35Z", "type": "User", "total_private_repos": 100, "owned_private_repos": 100, "private_gists": 81, "disk_usage": 10000, "collaborators": 8, "plan": { "name": "Medium", "space": 400, "collaborators": 10, "private_repos": 20, }, } ) def do_login(self): user = super().do_login() social = user.social[0] self.assertIsNotNone(social.extra_data["expires"]) self.assertIsNotNone(social.extra_data["refresh_token"]) return user def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() def test_refresh_token(self): user, social = self.do_refresh_token() self.assertEqual(social.extra_data["access_token"], "foobar-new-token") class GithubOAuth2NoEmailTest(GithubOAuth2Test): user_data_body = json.dumps( { "login": "foobar", "id": 1, "avatar_url": "https://github.com/images/error/foobar_happy.gif", "gravatar_id": "somehexcode", "url": "https://api.github.com/users/foobar", "name": "monalisa foobar", "company": "GitHub", "blog": "https://github.com/blog", "location": "San Francisco", "email": "", "hireable": False, "bio": "There once was...", "public_repos": 2, "public_gists": 1, "followers": 20, "following": 0, "html_url": "https://github.com/foobar", "created_at": "2008-01-14T04:33:35Z", "type": "User", "total_private_repos": 100, "owned_private_repos": 100, "private_gists": 81, "disk_usage": 10000, "collaborators": 8, "plan": { "name": "Medium", "space": 400, "collaborators": 10, "private_repos": 20, }, } ) def test_login(self): url = "https://api.github.com/user/emails" responses.add( responses.GET, url, status=200, body=json.dumps(["foo@bar.com"]), content_type="application/json", ) self.do_login() def test_login_next_format(self): url = "https://api.github.com/user/emails" responses.add( responses.GET, url, status=200, body=json.dumps([{"email": "foo@bar.com"}]), content_type="application/json", ) self.do_login() def test_partial_pipeline(self): url = "https://api.github.com/user/emails" responses.add( responses.GET, url, status=200, body=json.dumps([{"email": "foo@bar.com"}]), content_type="application/json", ) self.do_partial_pipeline() def test_refresh_token(self): url = "https://api.github.com/user/emails" responses.add( responses.GET, url, status=200, body=json.dumps([{"email": "foo@bar.com"}]), content_type="application/json", ) self.do_refresh_token() class GithubOrganizationOAuth2Test(GithubOAuth2Test): backend_path = "social_core.backends.github.GithubOrganizationOAuth2" def auth_handlers(self, start_url): url = "https://api.github.com/orgs/foobar/members/foobar" responses.add(responses.GET, url, status=204, body="") return super().auth_handlers(start_url) def test_login(self): self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_ORG_NAME": "foobar"}) self.do_login() def test_partial_pipeline(self): self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_ORG_NAME": "foobar"}) self.do_partial_pipeline() def test_refresh_token(self): self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_ORG_NAME": "foobar"}) self.do_refresh_token() class GithubOrganizationOAuth2FailTest(GithubOAuth2Test): backend_path = "social_core.backends.github.GithubOrganizationOAuth2" def auth_handlers(self, start_url): url = "https://api.github.com/orgs/foobar/members/foobar" responses.add( responses.GET, url, status=404, body='{"message": "Not Found"}', content_type="application/json", ) return super().auth_handlers(start_url) def test_login(self): self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_ORG_NAME": "foobar"}) with self.assertRaises(AuthFailed): self.do_login() def test_partial_pipeline(self): self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_ORG_NAME": "foobar"}) with self.assertRaises(AuthFailed): self.do_partial_pipeline() def test_refresh_token(self): self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_ORG_NAME": "foobar"}) with self.assertRaises(AuthFailed): self.do_refresh_token() class GithubTeamOAuth2Test(GithubOAuth2Test): backend_path = "social_core.backends.github.GithubTeamOAuth2" def auth_handlers(self, start_url): url = "https://api.github.com/teams/123/members/foobar" responses.add(responses.GET, url, status=204, body="") return super().auth_handlers(start_url) def test_login(self): self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_TEAM_ID": "123"}) self.do_login() def test_partial_pipeline(self): self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_TEAM_ID": "123"}) self.do_partial_pipeline() def test_refresh_token(self): self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_TEAM_ID": "123"}) self.do_refresh_token() class GithubTeamOAuth2FailTest(GithubOAuth2Test): backend_path = "social_core.backends.github.GithubTeamOAuth2" def auth_handlers(self, start_url): url = "https://api.github.com/teams/123/members/foobar" responses.add( responses.GET, url, status=404, body='{"message": "Not Found"}', content_type="application/json", ) return super().auth_handlers(start_url) def test_login(self): self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_TEAM_ID": "123"}) with self.assertRaises(AuthFailed): self.do_login() def test_partial_pipeline(self): self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_TEAM_ID": "123"}) with self.assertRaises(AuthFailed): self.do_partial_pipeline() def test_refresh_token(self): self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_TEAM_ID": "123"}) with self.assertRaises(AuthFailed): self.do_refresh_token() social-auth-core-4.6.1/social_core/tests/backends/test_github_enterprise.py000066400000000000000000000251141500362547200272210ustar00rootroot00000000000000import json import responses from ...exceptions import AuthFailed from .oauth import BaseAuthUrlTestMixin, OAuth2Test class GithubEnterpriseOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.github_enterprise.GithubEnterpriseOAuth2" user_data_url = "https://www.example.com/api/v3/user" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "login": "foobar", "id": 1, "avatar_url": "https://www.example.com/images/error/foobar_happy.gif", "gravatar_id": "somehexcode", "url": "https://www.example.com/api/v3/users/foobar", "name": "monalisa foobar", "company": "GitHub", "blog": "https://www.example.com/blog", "location": "San Francisco", "email": "foo@bar.com", "hireable": False, "bio": "There once was...", "public_repos": 2, "public_gists": 1, "followers": 20, "following": 0, "html_url": "https://www.example.com/foobar", "created_at": "2008-01-14T04:33:35Z", "type": "User", "total_private_repos": 100, "owned_private_repos": 100, "private_gists": 81, "disk_usage": 10000, "collaborators": 8, "plan": { "name": "Medium", "space": 400, "collaborators": 10, "private_repos": 20, }, } ) def test_login(self): self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_URL": "https://www.example.com"} ) self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL": "https://www.example.com/api/v3"} ) self.do_login() def test_partial_pipeline(self): self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_URL": "https://www.example.com"} ) self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL": "https://www.example.com/api/v3"} ) self.do_partial_pipeline() class GithubEnterpriseOAuth2NoEmailTest(GithubEnterpriseOAuth2Test): user_data_body = json.dumps( { "login": "foobar", "id": 1, "avatar_url": "https://www.example.com/images/error/foobar_happy.gif", "gravatar_id": "somehexcode", "url": "https://www.example.com/api/v3/users/foobar", "name": "monalisa foobar", "company": "GitHub", "blog": "https://www.example.com/blog", "location": "San Francisco", "email": "", "hireable": False, "bio": "There once was...", "public_repos": 2, "public_gists": 1, "followers": 20, "following": 0, "html_url": "https://www.example.com/foobar", "created_at": "2008-01-14T04:33:35Z", "type": "User", "total_private_repos": 100, "owned_private_repos": 100, "private_gists": 81, "disk_usage": 10000, "collaborators": 8, "plan": { "name": "Medium", "space": 400, "collaborators": 10, "private_repos": 20, }, } ) def test_login(self): self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_URL": "https://www.example.com"} ) self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL": "https://www.example.com/api/v3"} ) url = "https://www.example.com/api/v3/user/emails" responses.add( responses.GET, url, status=200, body=json.dumps(["foo@bar.com"]), content_type="application/json", ) self.do_login() def test_login_next_format(self): self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_URL": "https://www.example.com"} ) self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL": "https://www.example.com/api/v3"} ) url = "https://www.example.com/api/v3/user/emails" responses.add( responses.GET, url, status=200, body=json.dumps([{"email": "foo@bar.com"}]), content_type="application/json", ) self.do_login() def test_partial_pipeline(self): self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_URL": "https://www.example.com"} ) self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL": "https://www.example.com/api/v3"} ) responses.add( responses.GET, "https://www.example.com/api/v3/user/emails", status=200, body=json.dumps([{"email": "foo@bar.com"}]), content_type="application/json", ) self.do_partial_pipeline() class GithubEnterpriseOrganizationOAuth2Test(GithubEnterpriseOAuth2Test): backend_path = ( "social_core.backends.github_enterprise.GithubEnterpriseOrganizationOAuth2" ) def auth_handlers(self, start_url): url = "https://www.example.com/api/v3/orgs/foobar/members/foobar" responses.add(responses.GET, url, status=204, body="") return super().auth_handlers(start_url) def test_login(self): self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL": "https://www.example.com"} ) self.strategy.set_settings( { "SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL": "https://www.example.com/api/v3" } ) self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME": "foobar"}) self.do_login() def test_partial_pipeline(self): self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL": "https://www.example.com"} ) self.strategy.set_settings( { "SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL": "https://www.example.com/api/v3" } ) self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME": "foobar"}) self.do_partial_pipeline() class GithubEnterpriseOrganizationOAuth2FailTest(GithubEnterpriseOAuth2Test): backend_path = ( "social_core.backends.github_enterprise.GithubEnterpriseOrganizationOAuth2" ) def auth_handlers(self, start_url): url = "https://www.example.com/api/v3/orgs/foobar/members/foobar" responses.add( responses.GET, url, status=404, body='{"message": "Not Found"}', content_type="application/json", ) return super().auth_handlers(start_url) def test_login(self): self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL": "https://www.example.com"} ) self.strategy.set_settings( { "SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL": "https://www.example.com/api/v3" } ) self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME": "foobar"}) with self.assertRaises(AuthFailed): self.do_login() def test_partial_pipeline(self): self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL": "https://www.example.com"} ) self.strategy.set_settings( { "SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL": "https://www.example.com/api/v3" } ) self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME": "foobar"}) with self.assertRaises(AuthFailed): self.do_partial_pipeline() class GithubEnterpriseTeamOAuth2Test(GithubEnterpriseOAuth2Test): backend_path = "social_core.backends.github_enterprise.GithubEnterpriseTeamOAuth2" def auth_handlers(self, start_url): url = "https://www.example.com/api/v3/teams/123/members/foobar" responses.add(responses.GET, url, status=204, body="") return super().auth_handlers(start_url) def test_login(self): self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL": "https://www.example.com"} ) self.strategy.set_settings( { "SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL": "https://www.example.com/api/v3" } ) self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID": "123"}) self.do_login() def test_partial_pipeline(self): self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL": "https://www.example.com"} ) self.strategy.set_settings( { "SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL": "https://www.example.com/api/v3" } ) self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID": "123"}) self.do_partial_pipeline() class GithubEnterpriseTeamOAuth2FailTest(GithubEnterpriseOAuth2Test): backend_path = "social_core.backends.github_enterprise.GithubEnterpriseTeamOAuth2" def auth_handlers(self, start_url): url = "https://www.example.com/api/v3/teams/123/members/foobar" responses.add( responses.GET, url, status=404, body='{"message": "Not Found"}', content_type="application/json", ) return super().auth_handlers(start_url) def test_login(self): self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL": "https://www.example.com"} ) self.strategy.set_settings( { "SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL": "https://www.example.com/api/v3" } ) self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID": "123"}) with self.assertRaises(AuthFailed): self.do_login() def test_partial_pipeline(self): self.strategy.set_settings( {"SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL": "https://www.example.com"} ) self.strategy.set_settings( { "SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL": "https://www.example.com/api/v3" } ) self.strategy.set_settings({"SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID": "123"}) with self.assertRaises(AuthFailed): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_gitlab.py000066400000000000000000000070731500362547200247450ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class GitLabOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.gitlab.GitLabOAuth2" user_data_url = "https://gitlab.com/api/v4/user" expected_username = "foobar" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", "expires_in": 7200, "refresh_token": "barfoo", } ) user_data_body = json.dumps( { "two_factor_enabled": False, "can_create_project": True, "confirmed_at": "2016-12-28T12:26:19.256Z", "twitter": "", "linkedin": "", "color_scheme_id": 1, "web_url": "https://gitlab.com/foobar", "skype": "", "identities": [], "id": 123456, "projects_limit": 100000, "current_sign_in_at": "2016-12-28T12:26:19.795Z", "state": "active", "location": None, "email": "foobar@example.com", "website_url": "", "username": "foobar", "bio": None, "last_sign_in_at": "2016-12-28T12:26:19.795Z", "is_admin": False, "external": False, "organization": None, "name": "Foo Bar", "can_create_group": True, "created_at": "2016-12-28T12:26:19.638Z", "avatar_url": "https://secure.gravatar.com/avatar/94d093eda664addd6e450d7e9881bcae?s=32&d=identicon", "theme_id": 2, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() class GitLabCustomDomainOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.gitlab.GitLabOAuth2" user_data_url = "https://example.com/api/v4/user" expected_username = "foobar" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", "expires_in": 7200, "refresh_token": "barfoo", } ) user_data_body = json.dumps( { "two_factor_enabled": False, "can_create_project": True, "confirmed_at": "2016-12-28T12:26:19.256Z", "twitter": "", "linkedin": "", "color_scheme_id": 1, "web_url": "https://example.com/foobar", "skype": "", "identities": [], "id": 123456, "projects_limit": 100000, "current_sign_in_at": "2016-12-28T12:26:19.795Z", "state": "active", "location": None, "email": "foobar@example.com", "website_url": "", "username": "foobar", "bio": None, "last_sign_in_at": "2016-12-28T12:26:19.795Z", "is_admin": False, "external": False, "organization": None, "name": "Foo Bar", "can_create_group": True, "created_at": "2016-12-28T12:26:19.638Z", "avatar_url": "https://secure.gravatar.com/avatar/94d093eda664addd6e450d7e9881bcae?s=32&d=identicon", "theme_id": 2, } ) def test_login(self): self.strategy.set_settings( {"SOCIAL_AUTH_GITLAB_API_URL": "https://example.com"} ) self.do_login() def test_partial_pipeline(self): self.strategy.set_settings( {"SOCIAL_AUTH_GITLAB_API_URL": "https://example.com"} ) self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_globus.py000066400000000000000000000026211500362547200247700ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin from .open_id_connect import OpenIdConnectTest class GlobusOpenIdConnectTest(OpenIdConnectTest, BaseAuthUrlTestMixin): backend_path = "social_core.backends.globus.GlobusOpenIdConnect" issuer = "https://auth.globus.org" openid_config_body = json.dumps( { "issuer": "https://auth.globus.org", "authorization_endpoint": "https://auth.globus.org/v2/oauth2/authorize", "userinfo_endpoint": "https://auth.globus.org/v2/oauth2/userinfo", "token_endpoint": "https://auth.globus.org/v2/oauth2/token", "revocation_endpoint": "https://auth.globus.org/v2/oauth2/token/revoke", "jwks_uri": "https://auth.globus.org/jwk.json", "response_types_supported": ["code", "token", "token id_token", "id_token"], "id_token_signing_alg_values_supported": ["RS512"], "scopes_supported": ["openid", "email", "profile"], "token_endpoint_auth_methods_supported": ["client_secret_basic"], "claims_supported": [ "at_hash", "aud", "email", "exp", "name", "nonce", "preferred_username", "iat", "iss", "sub", ], "subject_types_supported": ["public"], } ) social-auth-core-4.6.1/social_core/tests/backends/test_google.py000066400000000000000000000223621500362547200247550ustar00rootroot00000000000000import json import time from unittest import mock from urllib.parse import urlencode import jwt import responses from ...actions import do_disconnect from ...exceptions import AuthException, AuthTokenError from ..models import User from .base import BaseBackendTest from .oauth import BaseAuthUrlTestMixin, OAuth1AuthUrlTestMixin, OAuth1Test, OAuth2Test from .open_id_connect import OpenIdConnectTest class GoogleOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.google.GoogleOAuth2" user_data_url = "https://www.googleapis.com/oauth2/v3/userinfo" expected_username = "foo" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "profile": "https://plus.google.com/101010101010101010101", "family_name": "Bar", "sub": "101010101010101010101", "picture": "https://lh5.googleusercontent.com/-ui-GqpNh5Ms/" "AAAAAAAAAAI/AAAAAAAAAZw/a7puhHMO_fg/photo.jpg", "locale": "en", "email_verified": True, "given_name": "Foo", "email": "foo@bar.com", "name": "Foo Bar", } ) def test_login(self): self.do_login() last_request = responses.calls[-1].request self.assertEqual(last_request.method, "GET") self.assertEqual(self.user_data_url, last_request.url) self.assertEqual( last_request.headers["Authorization"], "Bearer foobar", ) def test_partial_pipeline(self): self.do_partial_pipeline() def test_with_unique_user_id(self): self.strategy.set_settings( { "SOCIAL_AUTH_GOOGLE_OAUTH2_USE_UNIQUE_USER_ID": True, } ) self.do_login() class GoogleOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.google.GoogleOAuth" user_data_url = "https://www.googleapis.com/userinfo/email" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", "oauth_callback_confirmed": "true", } ) user_data_body = urlencode( { "email": "foobar@gmail.com", "isVerified": "true", "id": "101010101010101010101", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() def test_with_unique_user_id(self): self.strategy.set_settings( {"SOCIAL_AUTH_GOOGLE_OAUTH_USE_UNIQUE_USER_ID": True} ) self.do_login() def test_with_anonymous_key_and_secret(self): self.strategy.set_settings( { "SOCIAL_AUTH_GOOGLE_OAUTH_KEY": None, "SOCIAL_AUTH_GOOGLE_OAUTH_SECRET": None, } ) self.do_login() class GoogleRevokeTokenTest(GoogleOAuth2Test): def test_revoke_token(self): self.strategy.set_settings( {"SOCIAL_AUTH_GOOGLE_OAUTH2_REVOKE_TOKENS_ON_DISCONNECT": True} ) self.do_login() user = User.get(self.expected_username) user.password = "password" responses.add( self._method(self.backend.REVOKE_TOKEN_METHOD), self.backend.REVOKE_TOKEN_URL, status=200, ) do_disconnect(self.backend, user) class GoogleOpenIdConnectTest(OpenIdConnectTest): backend_path = "social_core.backends.google_openidconnect.GoogleOpenIdConnect" user_data_url = "https://www.googleapis.com/plus/v1/people/me/openIdConnect" issuer = "accounts.google.com" openid_config_body = json.dumps( { "issuer": "https://accounts.google.com", "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth", "token_endpoint": "https://www.googleapis.com/oauth2/v4/token", "userinfo_endpoint": "https://www.googleapis.com/oauth2/v3/userinfo", "revocation_endpoint": "https://accounts.google.com/o/oauth2/revoke", "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs", "response_types_supported": [ "code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token", "none", ], "subject_types_supported": [ "public", ], "id_token_signing_alg_values_supported": [ "RS256", ], "scopes_supported": [ "openid", "email", "profile", ], "token_endpoint_auth_methods_supported": [ "client_secret_post", "client_secret_basic", ], "claims_supported": [ "aud", "email", "email_verified", "exp", "family_name", "given_name", "iat", "iss", "locale", "name", "picture", "sub", ], } ) class GoogleOneTapTest(BaseBackendTest): backend_path = "social_core.backends.google_onetap.GoogleOneTap" private_key = """-----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQC6lDOJ0zKiNKJM3p0nTOJgaaHhxPoIJARcRzNSkzG0vC4QnXbB I3E42elTL3Nujyt80frzIji12KIJqAZIFsG6SSzKcYNeXf+dPwehIrWa7z/N5HD4 x9Fufj5GDT7SyHCAHi3BDHkA599fpOw8odBambK2cshMNkjyNDP+MvGORQIDAQAB AoGBAJUZYaY+VDQzg4+SRlvloPIS9/6HfpeK0ME9VNIkNpCL4PP+IaxuOkiIO2Dy hnhPiR0SYExziIYpPDQjRgHNzblGXa2jq+jy/SWj+t0+E0vPhg0kBSA4cYclH+c/ xwp0iW0ocVjod4RxFu6qSMU1TY83NNc4khBON3/GZiU99ImBAkEA5+17SuWvbUA/ jQhuAmzMND1QK4cteYFUnkpqpMQsitq/SC/NgcqUKdqteghPPoJOlGH8UfaM+0EK uf0Dd327XQJBAM3xw6M8TyiPOY6Qvfadk+IqBdsUb9T7Q9xIB0kMEcfwRdKJ3KgR CvUjxBdXNf7ZuhykIDle80we41yzZK+2WAkCQCM2PQfMA2xU2tEwvHMFzaMIxAk3 xsGxzwURS0uktRaHy47MIylXdlM8biYe6NkWs5N3pPVUt2bWIyjFrycPIckCQQCe cmGol1/3vqnzy9y7fuUmXlp/AaxA2siNFEW2p7iOcYfmwfaov+QEUu4tXwXF+9G6 83NvcGQTrrgSvFq87bexAkB2f5dFbl2tYWRQ7wGmmX++JDuHwHDI99rnsUxVhSFk 1LiRk3XACJa5y1peU9rkTfWeu5aoFb5WyheQacNQcu80 -----END RSA PRIVATE KEY-----""" public_key = """-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6lDOJ0zKiNKJM3p0nTOJgaaHh xPoIJARcRzNSkzG0vC4QnXbBI3E42elTL3Nujyt80frzIji12KIJqAZIFsG6SSzK cYNeXf+dPwehIrWa7z/N5HD4x9Fufj5GDT7SyHCAHi3BDHkA599fpOw8odBambK2 cshMNkjyNDP+MvGORQIDAQAB -----END PUBLIC KEY-----""" client_id = "a-key" def setUp(self): super().setUp() responses.add( responses.GET, "https://www.googleapis.com/oauth2/v1/certs", status=200, body=json.dumps({"test_key": self.public_key}), ) def _get_jwt_payload(self): claimed_at = int(time.time()) return { "given_name": "test name", "email": "test@test.com", "aud": self.client_id, "iat": claimed_at, "exp": claimed_at + 30, "iss": "accounts.google.com", } def test_auth_url(self): with self.assertRaises(AuthException): self.backend.start() def test_verify_csrf_no_csrf_token_body(self): with self.assertRaises(AuthTokenError): self.backend.verify_csrf(request=mock.Mock()) def test_verify_csrf_no_csrf_token_cookie_not_ignored(self): self.backend.data = {"g_csrf_token": "csrf"} with self.assertRaises(AuthTokenError): self.backend.verify_csrf(request=mock.Mock(COOKIES={})) def test_verify_csrf_no_csrf_token_cookie_ignored(self): self.strategy.set_settings( {"SOCIAL_AUTH_GOOGLE_ONETAP_IGNORE_MISSING_CSRF_COOKIE": True} ) self.backend.data = {"g_csrf_token": "csrf"} self.backend.verify_csrf(request=mock.Mock(COOKIES={})) def test_verify_csrf_valid(self): self.backend.data = {"g_csrf_token": "csrf"} self.backend.verify_csrf(request=mock.Mock(COOKIES={"g_csrf_token": "csrf"})) def test_get_decoded_info_error(self): payload = self._get_jwt_payload() payload["exp"] -= 31 self.backend.data = { "credential": jwt.encode( payload, self.private_key, algorithm="RS256", headers={"kid": "test_key"}, ), "g_csrf_token": "csrf", } request = mock.Mock(COOKIES={"g_csrf_token": "csrf"}) with self.assertRaises(AuthException): self.backend.auth_complete(request=request) def test_get_decoded_info_success(self): self.backend.data = { "credential": jwt.encode( self._get_jwt_payload(), self.private_key, algorithm="RS256", headers={"kid": "test_key"}, ), "g_csrf_token": "csrf", } request = mock.Mock(COOKIES={"g_csrf_token": "csrf"}) user = self.backend.auth_complete(request=request) self.assertEqual(user.email, "test@test.com") self.assertEqual(user.first_name, "test name") social-auth-core-4.6.1/social_core/tests/backends/test_grafana.py000066400000000000000000000012131500362547200250700ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class GrafanaOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.grafana.GrafanaOAuth2" user_data_url = "https://grafana.com/api/oauth2/user" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", } ) user_data_body = json.dumps( {"login": "fooboy", "email": "foo@bar.com", "name": "Foo Bar"} ) expected_username = "fooboy" def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_instagram.py000066400000000000000000000013601500362547200254610ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class InstagramOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.instagram.InstagramOAuth2" user_data_url = "https://graph.instagram.com/me" expected_username = "foobar" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", "meta": {"code": 200}, "user": {"username": "foobar", "id": "101010101"}, } ) user_data_body = json.dumps( {"meta": {"code": 200}, "username": "foobar", "id": "101010101"} ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_itembase.py000066400000000000000000000030331500362547200252640ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class ItembaseOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.itembase.ItembaseOAuth2" user_data_url = "https://users.itembase.com/v1/me" expected_username = "foobar" access_token_body = json.dumps( { "access_token": "foobar-token", "expires_in": 2592000, "token_type": "bearer", "scope": "user.minimal", "refresh_token": "foobar-refresh-token", } ) user_data_body = json.dumps( { "uuid": "a4b91ee7-ec1a-49b9-afce-371dc8797749", "username": "foobar", "email": "foobar@itembase.biz", "first_name": "Foo", "middle_name": None, "last_name": "Bar", "name_format": "first middle last", "locale": "en", "preferred_currency": "EUR", } ) refresh_token_body = json.dumps( { "access_token": "foobar-new-token", "expires_in": 2592000, "token_type": "bearer", "scope": "user.minimal", "refresh_token": "foobar-new-refresh-token", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() class ItembaseOAuth2SandboxTest(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.itembase.ItembaseOAuth2Sandbox" user_data_url = "http://sandbox.users.itembase.io/v1/me" social-auth-core-4.6.1/social_core/tests/backends/test_kakao.py000066400000000000000000000016521500362547200245660ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class KakaoOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.kakao.KakaoOAuth2" user_data_url = "https://kapi.kakao.com/v2/user/me" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar"}) user_data_body = json.dumps( { "id": "101010101", "properties": { "nickname": "foobar", "thumbnail_image": "http://mud-kage.kakao.co.kr/14/dn/btqbh1AKmRf/" "ujlHpQhxtMSbhKrBisrxe1/o.jpg", "profile_image": "http://mud-kage.kakao.co.kr/14/dn/btqbjCnl06Q/" "wbMJSVAUZB7lzSImgGdsoK/o.jpg", }, "kakao_account": {"email": "a"}, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_keycloak.py000066400000000000000000000107131500362547200253000ustar00rootroot00000000000000from __future__ import annotations import json import time import jwt from .oauth import BaseAuthUrlTestMixin, OAuth2Test _PRIVATE_KEY_HEADERLESS = """ MIIEowIBAAKCAQEAvyo2hx1L3ALHeUd/6xk/lIhTyZ/HJZ+Sss/ge6T6gPdES4Dw BvwGlAp21iEbmjmizsv6+ZsyuKZUiC1J4A90lmIA57aYXHHoh9GBWQZzXeCNgghP JpGYYSCN+1qeD4nbwD9cQBtPrGBpPpPtv2a/xdPqDm5ko6adMhmbm8e4Me/ppWPi 0U+skWQJepBhjEt3x+AOMKDv2TUBWOc3mYFNkr9qNOPe7FxnqUk6ZtkI3QNjZTky AU7cat87u1vT5thAxVY18i1GfSZwtQbU3Ba6hXI5SIHB1lS88SJ9/+E/flJJPD2N Nzv2z3HAVuTUOYi48fnXFHpJLGv+mGLNtE77hwIDAQABAoIBAQCUyQYno2Wnl4Ip orys/rm9oV2VUAZwAgLrqV/O3Fkch1dgbLpktUNpdbuIbbxODQ3qZliwbVrM3Khu VNFq0pyrbxvFPRjY2s9g5m8GGz8vkdaRnmX8XtV6wxu+xoi/D006FBZ4zsj0IRXI 3tnsXsxj7Mv+72zk8ojmtYend4qlUfzBVTpMRDc4XDC4Ya91fgFgfibtUE1qc8Ap ctCzk9wZgN9SOKXHcKANhqC8BmQv7NspI0RT0Oq0n/U921P/+y1M++Z0Z2vQvjiR GvDpSlnmlLB3S3E7zHbmksyUyK4Ab7xYi51yFKgrYjtaM3QLCTlSmTQBM8EhjZNG VJvgEI4BAoGBAOQzd4PdB3jW6OnUXDrzWqfCAgpMxWtAmP5h+LrmsrV+upXggRnv iNzTqiq0QpdrgYrikFGDkGEsqfrnd7IkskDMT8PMuncmDkuF67kg6G4/vzyz5QyA jolf6qmHQPMCfxtCPUZZOUcuRKnj5KVIGfJu2gu5Z2lEysuV5ZQE0OiNAoGBANZz wqLhqN376MT3YEbsOcWXYBDXX+FzWAYHsxf4APHJrNr6pkM4dTsvXU2tQIUV4N3c SJvMVSI91VL8mdgQxHKaECUizye19brJ1BysbeFjBCK+Y6bYMd8n0Hdxxi/ZyeF9 AzfIPQN2uZSzU/I+Nt0tlz/SCoL4Qi3FQpgtLoFjAoGAb8XVsDy+wC1jf8SIOEei C7E3FpxrxhCp309VaRY+Si98bJS+J1nwC1mRa8FHLKt3k/NNBOAQA8jAqShetF7N AHgSSbEpU9rL/anmv5Kixf1rSexDMFB3gEn+wnKBGYYLg+p54M8rAvZio2QARgR+ 0QQCwONbB3Cuc/FDtbB2MrECgYAhSqtGmf2bKIZEPZsGp5l4YT2an7TUzRE3Lm7R I8ERyBs7i3nQKa2ZWIsFigXgIztbdd0Xwqrcu/in/2rqrf+xQtWKzlKWeZsCOl7h bKtKOBLmSeQyfJGRcR7dzB3WQ9shVETxnfZK2V2KBiTcEGh4AaHfWH4lQuETNfJW qXz0vQKBgDVz+ZvULA/OZWXrOI1il7KoahWdb9vr8VhWgHKnDW7hInDFh6SEQHn6 mSNns0AssDwr4TheET7klb7AvbBKrNSP/Tz9AzkwMz148T2ffkPFMZRuvRT+eQ5Z ey4gIBKESJF6X9fefiawCrI3+PC7x9x0ngP9R4t/OzDWVAYn9gmd """.strip() _PRIVATE_KEY = f""" -----BEGIN RSA PRIVATE KEY----- {_PRIVATE_KEY_HEADERLESS} -----END RSA PRIVATE KEY----- """ _PUBLIC_KEY_HEADERLESS = """ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvyo2hx1L3ALHeUd/6xk/ lIhTyZ/HJZ+Sss/ge6T6gPdES4DwBvwGlAp21iEbmjmizsv6+ZsyuKZUiC1J4A90 lmIA57aYXHHoh9GBWQZzXeCNgghPJpGYYSCN+1qeD4nbwD9cQBtPrGBpPpPtv2a/ xdPqDm5ko6adMhmbm8e4Me/ppWPi0U+skWQJepBhjEt3x+AOMKDv2TUBWOc3mYFN kr9qNOPe7FxnqUk6ZtkI3QNjZTkyAU7cat87u1vT5thAxVY18i1GfSZwtQbU3Ba6 hXI5SIHB1lS88SJ9/+E/flJJPD2NNzv2z3HAVuTUOYi48fnXFHpJLGv+mGLNtE77 hwIDAQAB """.strip() _PUBLIC_KEY = f""" -----BEGIN PUBLIC KEY----- {_PUBLIC_KEY_HEADERLESS} -----END PUBLIC KEY----- """ _KEY = "example" _SECRET = "1234abcd-1234-abcd-1234-abcd1234adcd" _AUTHORIZATION_URL = ( "https://sso.example.com/auth/realms/example/protocol/openid-connect/auth" ) _ACCESS_TOKEN_URL = ( "https://sso.example.com/auth/realms/example/protocol/openid-connect/token" ) _ALGORITHM = "RS256" _AUTH_TIME = int(time.time()) _PAYLOAD = { "preferred_username": "john.doe", "email": "john.doe@example.com", "name": "John Doe", "given_name": "John", "family_name": "Doe", "iss": "https://sso.example.com", "sub": "john.doe", "aud": _KEY, "exp": _AUTH_TIME + 3600, "iat": _AUTH_TIME, } def _encode( payload: dict[str, str | int], key: str = _PRIVATE_KEY, algorithm: str = _ALGORITHM ) -> str: return jwt.encode(payload, key=key, algorithm=algorithm) def _decode( token: str, key: str = _PUBLIC_KEY, algorithms: list[str] | None = None, audience: str = _KEY, ) -> dict[str, str | int]: if algorithms is None: algorithms = [_ALGORITHM] return jwt.decode(token, key=key, algorithms=algorithms, audience=audience) class KeycloakOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.keycloak.KeycloakOAuth2" expected_username = "john.doe" access_token_body = json.dumps( { "token_type": "Bearer", "id_token": _encode(_PAYLOAD), "access_token": _encode(_PAYLOAD), } ) def extra_settings(self): return { "SOCIAL_AUTH_KEYCLOAK_KEY": _KEY, "SOCIAL_AUTH_KEYCLOAK_SECRET": _SECRET, "SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY": _PUBLIC_KEY_HEADERLESS, "SOCIAL_AUTH_KEYCLOAK_ALGORITHM": _ALGORITHM, "SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL": _AUTHORIZATION_URL, "SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL": _ACCESS_TOKEN_URL, } def test_encode_decode(self): token = _encode(_PAYLOAD) self.assertEqual(_PAYLOAD, _decode(token)) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_kick.py000066400000000000000000000020151500362547200244130ustar00rootroot00000000000000import json from .oauth import OAuth2Test class KickOAuth2Test(OAuth2Test): backend_path = "social_core.backends.kick.KickOAuth2" user_data_url = "https://api.kick.com/public/v1/users" expected_username = "foobar" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", "refresh_token": "refresh_foobar", "expires_in": 3600, "scope": "user:read", } ) # The API returns data in a 'data' field with an array of users user_data_body = json.dumps( { "data": [ { "user_id": 123456, "name": "foobar", "email": "foobar@example.com", "profile_picture": "https://example.com/avatar.jpg", } ], "message": "Success", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_lifescience.py000066400000000000000000000201011500362547200257370ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin from .open_id_connect import OpenIdConnectTest class LifeScienceOpenIdConnectTest(OpenIdConnectTest, BaseAuthUrlTestMixin): backend_path = "social_core.backends.lifescience.LifeScienceOpenIdConnect" issuer = "https://login.aai.lifescience-ri.eu/oidc/" user_data_url = "https://login.aai.lifescience-ri.eu/oidc/userinfo" openid_config_body = """ { "request_parameter_supported": true, "claims_parameter_supported": false, "introspection_endpoint": "https://login.aai.lifescience-ri.eu/oidc/introspect", "scopes_supported": [ "openid", "profile", "email", "address", "phone", "offline_access", "perun_api", "eduperson_principal_name", "country", "eduperson_assurance", "negotiator_api", "beacon_network_api", "beacon_api", "ssh_public_key", "crypt4ghPublicKeys", "gpgPublicKeys", "negotiator_monitoring", "eduperson_entitlement", "voperson_external_affiliation", "ga4gh_passport_v1", "perun_admin", "eduperson_orcid", "elixir_eduperson_unique_id", "elixir_eduperson_principal_name", "schac_home_organization", "eduperson_scoped_affiliation", "voperson_current_external_affiliation", "authenticating_entity", "minio_policies", "max_user_authentication_capability", "voperson_external_id", "eduperson_unique_id" ], "issuer": "https://login.aai.lifescience-ri.eu/oidc/", "acr_values_supported": [ "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified", "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport", "https://refeds.org/profile/sfa", "https://refeds.org/profile/mfa" ], "userinfo_encryption_enc_values_supported": [ "XC20P", "A256CBC+HS512", "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512", "A128CBC+HS256" ], "id_token_encryption_enc_values_supported": [ "XC20P", "A256CBC+HS512", "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512", "A128CBC+HS256" ], "authorization_endpoint": "https://login.aai.lifescience-ri.eu/oidc/authorize", "request_object_encryption_enc_values_supported": [ "XC20P", "A256CBC+HS512", "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512", "A128CBC+HS256" ], "device_authorization_endpoint": "https://login.aai.lifescience-ri.eu/oidc/devicecode", "userinfo_signing_alg_values_supported": [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512" ], "claims_supported": [ "schac_home_organization", "sub", "country", "zoneinfo", "negotiator_monitoring", "voperson_external_affiliation", "birthdate", "beacon_api", "elixir_eduperson_unique_id", "gender", "preferred_username", "locale", "eduperson_principal_name", "eduperson_entitlement", "elixir_eduperson_principal_name", "minio_policies", "updated_at", "crypt4ghPublicKeys", "nickname", "eduperson_scoped_affiliation", "eduperson_unique_id", "voperson_current_external_affiliation", "email", "voperson_external_id", "website", "email_verified", "address", "profile", "phone_number_verified", "max_user_authentication_capability", "given_name", "middle_name", "picture", "ssh_public_key", "authenticating_entity", "beacon_network_api", "name", "phone_number", "eduperson_assurance", "perun_admin", "family_name", "ga4gh_passport_v1", "perun_api", "negotiator_api", "gpgPublicKeys", "eduperson_orcid" ], "claim_types_supported": [ "normal" ], "token_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post", "none" ], "token_endpoint": "https://login.aai.lifescience-ri.eu/oidc/token", "response_types_supported": [ "code", "token id_token" ], "request_uri_parameter_supported": false, "userinfo_encryption_alg_values_supported": [ "RSA-OAEP-512", "RSA-OAEP", "RSA-OAEP-256", "RSA1_5", "RSA-OAEP-384" ], "grant_types_supported": [ "authorization_code", "implicit", "client_credentials", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange", "urn:ietf:params:oauth:grant-type:device_code" ], "end_session_endpoint": "https://login.aai.lifescience-ri.eu/oidc/endsession", "revocation_endpoint": "https://login.aai.lifescience-ri.eu/oidc/revoke", "userinfo_endpoint": "https://login.aai.lifescience-ri.eu/oidc/userinfo", "token_endpoint_auth_signing_alg_values_supported": [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512" ], "require_request_uri_registration": false, "code_challenge_methods_supported": [ "plain", "S256", "none" ], "id_token_encryption_alg_values_supported": [ "RSA-OAEP-512", "RSA-OAEP", "RSA-OAEP-256", "RSA1_5", "RSA-OAEP-384" ], "jwks_uri": "https://login.aai.lifescience-ri.eu/oidc/jwk", "subject_types_supported": [ "public" ], "id_token_signing_alg_values_supported": [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "none" ], "registration_endpoint": "https://login.aai.lifescience-ri.eu/oidc/register", "request_object_signing_alg_values_supported": [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512" ], "request_object_encryption_alg_values_supported": [ "RSA-OAEP-512", "RSA-OAEP", "RSA-OAEP-256", "RSA1_5", "RSA-OAEP-384" ] } """ expected_username = "foo@lifescience-ri.eu" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "preferred_username": "foo@lifescience-ri.eu", "email": "foo@bar.com", "name": "Foo Bar", } ) def test_login(self): self.do_login() social-auth-core-4.6.1/social_core/tests/backends/test_linkedin.py000066400000000000000000000052251500362547200252750ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test from .open_id_connect import OpenIdConnectTest class LinkedinOpenIdConnectTest(OpenIdConnectTest, BaseAuthUrlTestMixin): backend_path = "social_core.backends.linkedin.LinkedinOpenIdConnect" user_data_url = "https://api.linkedin.com/v2/userinfo" issuer = "https://www.linkedin.com" openid_config_body = json.dumps( { "issuer": "https://www.linkedin.com", "authorization_endpoint": "https://www.linkedin.com/oauth/v2/authorization", "token_endpoint": "https://www.linkedin.com/oauth/v2/accessToken", "userinfo_endpoint": "https://api.linkedin.com/v2/userinfo", "jwks_uri": "https://www.linkedin.com/oauth/openid/jwks", "response_types_supported": ["code"], "subject_types_supported": ["pairwise"], "id_token_signing_alg_values_supported": ["RS256"], "scopes_supported": ["openid", "profile", "email"], "claims_supported": [ "iss", "aud", "iat", "exp", "sub", "name", "given_name", "family_name", "picture", "email", "email_verified", "locale", ], } ) def test_invalid_nonce(self): """Skip the invalid nonce test as LinkedIn does not provide any nonce.""" class BaseLinkedinTest: user_data_url = "https://api.linkedin.com/v2/userinfo" expected_username = "FooBar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) # Reference: # https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self # -serve/sign-in-with-linkedin-v2#response-body-schema user_data_body = json.dumps( { "sub": "782bbtaQ", "name": "FooBar", "given_name": "Foo", "family_name": "Bar", "picture": "https://media.licdn-ei.com/dms/image/C5F03AQHqK8v7tB1HCQ/profile-displayphoto-shrink_100_100/0/", "locale": "en-US", "email": "doe@email.com", "email_verified": True, } ) def test_login(self): self.do_login() # type: ignore[attr-defined] def test_partial_pipeline(self): self.do_partial_pipeline() # type: ignore[attr-defined] class LinkedinOAuth2Test(BaseLinkedinTest, OAuth2Test): backend_path = "social_core.backends.linkedin.LinkedinOAuth2" class LinkedinMobileOAuth2Test(BaseLinkedinTest, OAuth2Test): backend_path = "social_core.backends.linkedin.LinkedinMobileOAuth2" social-auth-core-4.6.1/social_core/tests/backends/test_live.py000066400000000000000000000020131500362547200244270ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class LiveOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.live.LiveOAuth2" user_data_url = "https://apis.live.net/v5.0/me" expected_username = "FooBar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "first_name": "Foo", "last_name": "Bar", "name": "Foo Bar", "locale": "en_US", "gender": None, "emails": { "personal": None, "account": "foobar@live.com", "business": None, "preferred": "foobar@live.com", }, "link": "https://profile.live.com/", "updated_time": "2013-03-17T05:51:30+0000", "id": "1010101010101010", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_livejournal.py000066400000000000000000000070561500362547200260360ustar00rootroot00000000000000import datetime from urllib.parse import urlencode import pytest import responses from ...exceptions import AuthMissingParameter from .open_id import OpenIdTest JANRAIN_NONCE = datetime.datetime.now(datetime.timezone.utc).strftime( "%Y-%m-%dT%H:%M:%SZ" ) class LiveJournalOpenIdTest(OpenIdTest): backend_path = "social_core.backends.livejournal.LiveJournalOpenId" expected_username = "foobar" discovery_body = """ http://specs.openid.net/auth/2.0/signon http://www.livejournal.com/openid/server.bml http://foobar.livejournal.com/ """ server_response = urlencode( { "janrain_nonce": JANRAIN_NONCE, "openid.mode": "id_res", "openid.claimed_id": "http://foobar.livejournal.com/", "openid.identity": "http://foobar.livejournal.com/", "openid.op_endpoint": "http://www.livejournal.com/openid/server.bml", "openid.return_to": "http://myapp.com/complete/livejournal/?" "janrain_nonce=" + JANRAIN_NONCE, "openid.response_nonce": JANRAIN_NONCE + "wGp2rj", "openid.assoc_handle": "1364932966:ZTiur8sem3r2jzZougMZ:4d1cc3b44e", "openid.ns": "http://specs.openid.net/auth/2.0", "openid.signed": "mode,claimed_id,identity,op_endpoint,return_to," "response_nonce,assoc_handle", "openid.sig": "Z8MOozVPTOBhHG5ZS1NeGofxs1Q=", } ) server_bml_body = """assoc_handle:1364935340:ZhruPQ7DJ9eGgUkeUA9A:27f8c32464 assoc_type:HMAC-SHA1 dh_server_public:WzsRyLomvAV3vwvGUrfzXDgfqnTF+m1l3JWb55fyHO7visPT4tmQiTjqFFnSVAtAOvQzoViMiZQisxNwnqSK4lYexoez1z6pP5ry3pqxJAEYj60vFGvRztictEo0brjhmO1SNfjK1ppjOymdykqLpZeaL5fsuLtMCwTnR/JQZVA= enc_mac_key:LiOEVlLJSVUqfNvb5zPd76nEfvc= expires_in:1207060 ns:http://specs.openid.net/auth/2.0 session_type:DH-SHA1 """ def openid_url(self): return super().openid_url() + "/data/yadis" def post_start(self): self.strategy.remove_from_request_data("openid_lj_user") def _setup_handlers(self): responses.add( responses.POST, "http://www.livejournal.com/openid/server.bml", headers={ "Accept-Encoding": "identity", "Content-Type": "application/x-www-form-urlencoded", }, status=200, body=self.server_bml_body, ) responses.add( responses.GET, "http://foobar.livejournal.com/", headers={ "Accept-Encoding": "identity", "Accept": "text/html; q=0.3," "application/xhtml+xml; q=0.5," "application/xrds+xml", }, status=200, body=self.discovery_body, ) @pytest.mark.xfail(reason="responses mocking does not work for openid") def test_login(self): self.strategy.set_request_data({"openid_lj_user": "foobar"}, self.backend) self._setup_handlers() self.do_login() @pytest.mark.xfail(reason="responses mocking does not work for openid") def test_partial_pipeline(self): self.strategy.set_request_data({"openid_lj_user": "foobar"}, self.backend) self._setup_handlers() self.do_partial_pipeline() def test_failed_login(self): self._setup_handlers() with self.assertRaises(AuthMissingParameter): self.do_login() social-auth-core-4.6.1/social_core/tests/backends/test_lyft.py000066400000000000000000000013741500362547200244570ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class LyftOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.lyft.LyftOAuth2" user_data_url = "https://api.lyft.com/v1/profile" access_token_body = json.dumps( { "access_token": "atoken_foo", "refresh_token": "rtoken_bar", "token_type": "bearer", "expires_in": 3600, "scope": "public profile rides.read rides.request", "id": "user_foobar", } ) user_data_body = json.dumps({"id": "user_foobar"}) expected_username = "user_foobar" def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_mailru.py000066400000000000000000000015161500362547200247700ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class MRGOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.mailru.MRGOAuth2" user_data_url = "https://oauth.mail.ru/userinfo" expected_username = "FooBar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "first_name": "Foo", "last_name": "Bar", "name": "Foo Bar", "locale": "ru_RU", "email": "foobar@example.com", "birthday": "11.07.1970", "gender": "m", "image": "http://cs7003.vk.me/v7003815/22a1/xgG9fb-IJ3Y.jpg", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_mapmyfitness.py000066400000000000000000000106731500362547200262220ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class MapMyFitnessOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.mapmyfitness.MapMyFitnessOAuth2" user_data_url = "https://oauth2-api.mapmyapi.com/v7.0/user/self/" expected_username = "FredFlinstone" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "Bearer", "expires_in": 4000000, "refresh_token": "bambaz", "scope": "read", } ) user_data_body = json.dumps( { "last_name": "Flinstone", "weight": 91.17206637, "communication": { "promotions": True, "newsletter": True, "system_messages": True, }, "height": 1.778, "token_type": "Bearer", "id": 112233, "date_joined": "2011-08-26T06:06:19+00:00", "first_name": "Fred", "display_name": "Fred Flinstone", "display_measurement_system": "imperial", "expires_in": 4000000, "_links": { "stats": [ { "href": "/v7.0/user_stats/112233/?aggregate_by_period=month", "id": "112233", "name": "month", }, { "href": "/v7.0/user_stats/112233/?aggregate_by_period=year", "id": "112233", "name": "year", }, { "href": "/v7.0/user_stats/112233/?aggregate_by_period=day", "id": "112233", "name": "day", }, { "href": "/v7.0/user_stats/112233/?aggregate_by_period=week", "id": "112233", "name": "week", }, { "href": "/v7.0/user_stats/112233/?aggregate_by_period=lifetime", "id": "112233", "name": "lifetime", }, ], "friendships": [{"href": "/v7.0/friendship/?from_user=112233"}], "privacy": [ {"href": "/v7.0/privacy_option/3/", "id": "3", "name": "profile"}, {"href": "/v7.0/privacy_option/3/", "id": "3", "name": "workout"}, { "href": "/v7.0/privacy_option/3/", "id": "3", "name": "activity_feed", }, {"href": "/v7.0/privacy_option/1/", "id": "1", "name": "food_log"}, { "href": "/v7.0/privacy_option/3/", "id": "3", "name": "email_search", }, {"href": "/v7.0/privacy_option/3/", "id": "3", "name": "route"}, ], "image": [ { "href": "/v7.0/user_profile_photo/112233/", "id": "112233", "name": "user_profile_photo", } ], "documentation": [{"href": "https://www.mapmyapi.com/docs/User"}], "workouts": [ {"href": "/v7.0/workout/?user=112233&order_by=-start_datetime"} ], "deactivation": [{"href": "/v7.0/user_deactivation/"}], "self": [{"href": "/v7.0/user/112233/", "id": "112233"}], }, "location": { "country": "US", "region": "NC", "locality": "Bedrock", "address": "150 Dinosaur Ln", }, "last_login": "2014-02-23T22:36:52+00:00", "email": "fredflinstone@gmail.com", "username": "FredFlinstone", "sharing": {"twitter": False, "facebook": False}, "scope": "read", "refresh_token": "bambaz", "last_initial": "S.", "access_token": "foobar", "gender": "M", "time_zone": "America/Denver", "birthdate": "1983-04-15", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_microsoft.py000066400000000000000000000030431500362547200255010ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class MicrosoftOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.microsoft.MicrosoftOAuth2" user_data_url = "https://graph.microsoft.com/v1.0/me" expected_username = "foobar" user_data_body = json.dumps( { "displayName": "foo bar", "givenName": "foobar", "jobTitle": "Auditor", "mail": "foobar@foobar.com", "mobilePhone": None, "officeLocation": "12/1110", "preferredLanguage": "en-US", "surname": "Bowen", "userPrincipalName": "foobar", "id": "48d31887-5fad-4d73-a9f5-3c356e68a038", } ) access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", "id_token": "", "expires_in": 3600, "expires_on": 1423650396, "not_before": 1423646496, } ) refresh_token_body = json.dumps( { "access_token": "foobar-new-token", "token_type": "bearer", "expires_in": 3600, "refresh_token": "foobar-new-refresh-token", "scope": "identity", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() def test_refresh_token(self): user, social = self.do_refresh_token() self.assertEqual(social.extra_data["access_token"], "foobar-new-token") social-auth-core-4.6.1/social_core/tests/backends/test_mineid.py000066400000000000000000000013401500362547200247370ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class MineIDOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.mineid.MineIDOAuth2" user_data_url = "https://www.mineid.org/api/user" expected_username = "foo@bar.com" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "email": "foo@bar.com", "primary_profile": None, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() def test_auth_url_parameters(self): self.check_parameters_in_authorization_url("_AUTHORIZATION_URL") social-auth-core-4.6.1/social_core/tests/backends/test_mixcloud.py000066400000000000000000000046061500362547200253260ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class MixcloudOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.mixcloud.MixcloudOAuth2" user_data_url = "https://api.mixcloud.com/me/" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "username": "foobar", "cloudcast_count": 0, "following_count": 0, "url": "http://www.mixcloud.com/foobar/", "pictures": { "medium": "http://images-mix.netdna-ssl.com/w/100/h/100/q/85/" "images/graphics/33_Profile/default_user_600x600-v4.png", "320wx320h": "http://images-mix.netdna-ssl.com/w/320/h/320/q/85/" "images/graphics/33_Profile/" "default_user_600x600-v4.png", "extra_large": "http://images-mix.netdna-ssl.com/w/600/h/600/q/85/" "images/graphics/33_Profile/" "default_user_600x600-v4.png", "large": "http://images-mix.netdna-ssl.com/w/300/h/300/q/85/" "images/graphics/33_Profile/default_user_600x600-v4.png", "640wx640h": "http://images-mix.netdna-ssl.com/w/640/h/640/q/85/" "images/graphics/33_Profile/" "default_user_600x600-v4.png", "medium_mobile": "http://images-mix.netdna-ssl.com/w/80/h/80/q/75/" "images/graphics/33_Profile/" "default_user_600x600-v4.png", "small": "http://images-mix.netdna-ssl.com/w/25/h/25/q/85/images/" "graphics/33_Profile/default_user_600x600-v4.png", "thumbnail": "http://images-mix.netdna-ssl.com/w/50/h/50/q/85/" "images/graphics/33_Profile/" "default_user_600x600-v4.png", }, "is_current_user": True, "listen_count": 0, "updated_time": "2013-03-17T06:26:31Z", "following": False, "follower": False, "key": "/foobar/", "created_time": "2013-03-17T06:26:31Z", "follower_count": 0, "favorite_count": 0, "name": "foobar", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_musicbrainz.py000066400000000000000000000014431500362547200260240ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class MusicBrainzAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.musicbrainz.MusicBrainzOAuth2" user_data_url = "https://musicbrainz.org/oauth2/userinfo" expected_username = "foobar" access_token_body = json.dumps( { "access_token": "GjtKfJS6G4lupbQcCOiTKo4HcLXUgI1p", "expires_in": 3600, "token_type": "Bearer", "refresh_token": "GjSCBBjp4fnbE0AKo3uFu9qq9K2fFm4u", } ) user_data_body = json.dumps( { "sub": "foobar", "email": "foo@bar.com", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_nationbuilder.py000066400000000000000000000221331500362547200263340ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class NationBuilderOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.nationbuilder.NationBuilderOAuth2" user_data_url = "https://foobar.nationbuilder.com/api/v1/people/me" expected_username = "foobar" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", "created_at": 1422937981, "expires_in": 2592000, } ) user_data_body = json.dumps( { "person": { "twitter_followers_count": None, "last_name": "Bar", "rule_violations_count": 0, "linkedin_id": None, "recruiter_id": None, "membership_expires_at": None, "donations_raised_count": 0, "last_contacted_at": None, "prefix": None, "profile_content_html": None, "email4": None, "email2": None, "availability": None, "occupation": None, "user_submitted_address": None, "could_vote_status": None, "state_upper_district": None, "salesforce_id": None, "van_id": None, "phone_time": None, "profile_content": None, "auto_import_id": None, "parent_id": None, "email4_is_bad": False, "twitter_updated_at": None, "email3_is_bad": False, "bio": None, "party_member": None, "unsubscribed_at": None, "fax_number": None, "last_contacted_by": None, "active_customer_expires_at": None, "federal_donotcall": False, "warnings_count": 0, "first_supporter_at": "2015-02-02T19:30:28-08:00", "previous_party": None, "donations_raised_amount_this_cycle_in_cents": 0, "call_status_name": None, "marital_status": None, "facebook_updated_at": None, "donations_count": 0, "note_updated_at": None, "closed_invoices_count": None, "profile_headline": None, "fire_district": None, "mobile_normalized": None, "import_id": None, "last_call_id": None, "donations_raised_amount_in_cents": 0, "facebook_address": None, "is_profile_private": False, "last_rule_violation_at": None, "sex": None, "full_name": "Foo Bar", "last_donated_at": None, "donations_pledged_amount_in_cents": 0, "primary_email_id": 1, "media_market_name": None, "capital_amount_in_cents": 500, "datatrust_id": None, "precinct_code": None, "email3": None, "religion": None, "first_prospect_at": None, "judicial_district": None, "donations_count_this_cycle": 0, "work_address": None, "is_twitter_follower": False, "email1": "foobar@gmail.com", "email": "foobar@gmail.com", "contact_status_name": None, "mobile_opt_in": True, "twitter_description": None, "parent": None, "tags": [], "first_volunteer_at": None, "inferred_support_level": None, "banned_at": None, "first_invoice_at": None, "donations_raised_count_this_cycle": 0, "is_donor": False, "twitter_location": None, "email1_is_bad": False, "legal_name": None, "language": None, "registered_at": None, "call_status_id": None, "last_invoice_at": None, "school_sub_district": None, "village_district": None, "twitter_name": None, "membership_started_at": None, "subnations": [], "meetup_address": None, "author_id": None, "registered_address": None, "external_id": None, "twitter_login": None, "inferred_party": None, "spent_capital_amount_in_cents": 0, "suffix": None, "mailing_address": None, "is_leaderboardable": True, "twitter_website": None, "nbec_guid": None, "city_district": None, "church": None, "is_profile_searchable": True, "employer": None, "is_fundraiser": False, "email_opt_in": True, "recruits_count": 0, "email2_is_bad": False, "county_district": None, "recruiter": None, "twitter_friends_count": None, "facebook_username": None, "active_customer_started_at": None, "pf_strat_id": None, "locale": None, "twitter_address": None, "is_supporter": True, "do_not_call": False, "profile_image_url_ssl": "https://d3n8a8pro7vhmx.cloudfront.net" "/assets/icons/buddy.png", "invoices_amount_in_cents": None, "username": None, "donations_amount_in_cents": 0, "is_volunteer": False, "civicrm_id": None, "supranational_district": None, "precinct_name": None, "invoice_payments_amount_in_cents": None, "work_phone_number": None, "phone": "213.394.4623", "received_capital_amount_in_cents": 500, "primary_address": None, "is_possible_duplicate": False, "invoice_payments_referred_amount_in_cents": None, "donations_amount_this_cycle_in_cents": 0, "priority_level": None, "first_fundraised_at": None, "phone_normalized": "2133944623", "rnc_regid": None, "twitter_id": None, "birthdate": None, "mobile": None, "federal_district": None, "donations_to_raise_amount_in_cents": 0, "support_probability_score": None, "invoices_count": None, "nbec_precinct_code": None, "website": None, "closed_invoices_amount_in_cents": None, "home_address": None, "school_district": None, "support_level": None, "demo": None, "children_count": 0, "updated_at": "2015-02-02T19:30:28-08:00", "membership_level_name": None, "billing_address": None, "is_ignore_donation_limits": False, "signup_type": 0, "precinct_id": None, "rnc_id": None, "id": 2, "ethnicity": None, "is_survey_question_private": False, "middle_name": None, "author": None, "last_fundraised_at": None, "state_file_id": None, "note": None, "submitted_address": None, "support_level_changed_at": None, "party": None, "contact_status_id": None, "outstanding_invoices_amount_in_cents": None, "page_slug": None, "outstanding_invoices_count": None, "first_recruited_at": None, "county_file_id": None, "first_name": "Foo", "facebook_profile_url": None, "city_sub_district": None, "has_facebook": False, "is_deceased": False, "labour_region": None, "state_lower_district": None, "dw_id": None, "created_at": "2015-02-02T19:30:28-08:00", "is_prospect": False, "priority_level_changed_at": None, "is_mobile_bad": False, "overdue_invoices_count": None, "ngp_id": None, "do_not_contact": False, "first_donated_at": None, "turnout_probability_score": None, }, "precinct": None, } ) def test_login(self): self.strategy.set_settings({"SOCIAL_AUTH_NATIONBUILDER_SLUG": "foobar"}) self.do_login() def test_partial_pipeline(self): self.strategy.set_settings({"SOCIAL_AUTH_NATIONBUILDER_SLUG": "foobar"}) self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_naver.py000066400000000000000000000021031500362547200246030ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class NaverOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.naver.NaverOAuth2" user_data_url = "https://openapi.naver.com/v1/nid/me" expected_username = "foobar" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", } ) user_data_content_type = "text/json" user_data_body = json.dumps( { "resultcode": "00", "message": "success", "response": { "email": "openapi@naver.com", "nickname": "foobar", "profile_image": "https://ssl.pstatic.net/static/pwe/address/nodata_33x33.gif", "age": "40-49", "gender": "F", "id": "32742776", "name": "foobar", "birthday": "10-01", }, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_nfdi.py000066400000000000000000000537511500362547200244270ustar00rootroot00000000000000import json from typing import cast from .oauth import BaseAuthUrlTestMixin from .open_id_connect import OpenIdConnectTest OIDC_CONFIG_ACADEMIC_ID = """ { "issuer": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud", "authorization_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/auth", "token_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/token", "introspection_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/token/introspect", "userinfo_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/userinfo", "end_session_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/logout", "frontchannel_logout_session_supported": true, "frontchannel_logout_supported": true, "jwks_uri": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/certs", "check_session_iframe": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/login-status-iframe.html", "grant_types_supported": [ "authorization_code", "implicit", "refresh_token", "password", "client_credentials", "urn:openid:params:grant-type:ciba", "urn:ietf:params:oauth:grant-type:device_code" ], "acr_values_supported": [ "0", "1" ], "response_types_supported": [ "code", "none", "id_token", "token", "id_token token", "code id_token", "code token", "code id_token token" ], "subject_types_supported": [ "public", "pairwise" ], "id_token_signing_alg_values_supported": [ "PS384", "RS384", "EdDSA", "ES384", "HS256", "HS512", "ES256", "RS256", "HS384", "ES512", "PS256", "PS512", "RS512" ], "id_token_encryption_alg_values_supported": [ "RSA-OAEP", "RSA-OAEP-256", "RSA1_5" ], "id_token_encryption_enc_values_supported": [ "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512" ], "userinfo_signing_alg_values_supported": [ "PS384", "RS384", "EdDSA", "ES384", "HS256", "HS512", "ES256", "RS256", "HS384", "ES512", "PS256", "PS512", "RS512", "none" ], "userinfo_encryption_alg_values_supported": [ "RSA-OAEP", "RSA-OAEP-256", "RSA1_5" ], "userinfo_encryption_enc_values_supported": [ "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512" ], "request_object_signing_alg_values_supported": [ "PS384", "RS384", "EdDSA", "ES384", "HS256", "HS512", "ES256", "RS256", "HS384", "ES512", "PS256", "PS512", "RS512", "none" ], "request_object_encryption_alg_values_supported": [ "RSA-OAEP", "RSA-OAEP-256", "RSA1_5" ], "request_object_encryption_enc_values_supported": [ "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512" ], "response_modes_supported": [ "query", "fragment", "form_post", "query.jwt", "fragment.jwt", "form_post.jwt", "jwt" ], "registration_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/clients-registrations/openid-connect", "token_endpoint_auth_methods_supported": [ "private_key_jwt", "client_secret_basic", "client_secret_post", "tls_client_auth", "client_secret_jwt" ], "token_endpoint_auth_signing_alg_values_supported": [ "PS384", "RS384", "EdDSA", "ES384", "HS256", "HS512", "ES256", "RS256", "HS384", "ES512", "PS256", "PS512", "RS512" ], "introspection_endpoint_auth_methods_supported": [ "private_key_jwt", "client_secret_basic", "client_secret_post", "tls_client_auth", "client_secret_jwt" ], "introspection_endpoint_auth_signing_alg_values_supported": [ "PS384", "RS384", "EdDSA", "ES384", "HS256", "HS512", "ES256", "RS256", "HS384", "ES512", "PS256", "PS512", "RS512" ], "authorization_signing_alg_values_supported": [ "PS384", "RS384", "EdDSA", "ES384", "HS256", "HS512", "ES256", "RS256", "HS384", "ES512", "PS256", "PS512", "RS512" ], "authorization_encryption_alg_values_supported": [ "RSA-OAEP", "RSA-OAEP-256", "RSA1_5" ], "authorization_encryption_enc_values_supported": [ "A256GCM", "A192GCM", "A128GCM", "A128CBC-HS256", "A192CBC-HS384", "A256CBC-HS512" ], "claims_supported": [ "aud", "sub", "iss", "auth_time", "name", "given_name", "family_name", "preferred_username", "email", "acr" ], "claim_types_supported": [ "normal" ], "claims_parameter_supported": true, "scopes_supported": [ "initials", "address", "goeidToSub", "MPGR_entryUUID_as_preferred_username", "org", "userservices", "accounttype", "oxUserId", "basic", "employeenumber", "syncAndShareInstance", "phone", "roles", "gwdg-internal-role", "microprofile-jwt", "oxContextId", "emailCS", "goesternMatrikelnummer", "vpngroupsNetworkroles", "subVPNUserType", "acr", "owncloud", "displayName_to_preferred_username", "usRoles", "profile", "goeId", "offline_access", "userContextId", "goesternSAMAccountName", "goesternQuellSystem", "userServices_to_authorities", "memberOfGoeId", "openid", "email", "nfdi_attributes", "memberofdisplayname", "syncAttribute1", "prefuid", "AuthnContextClassRef", "web-origins", "mailAddresses", "uniElectionHash" ], "request_parameter_supported": true, "request_uri_parameter_supported": true, "require_request_uri_registration": true, "code_challenge_methods_supported": [ "plain", "S256" ], "tls_client_certificate_bound_access_tokens": true, "revocation_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/revoke", "revocation_endpoint_auth_methods_supported": [ "private_key_jwt", "client_secret_basic", "client_secret_post", "tls_client_auth", "client_secret_jwt" ], "revocation_endpoint_auth_signing_alg_values_supported": [ "PS384", "RS384", "EdDSA", "ES384", "HS256", "HS512", "ES256", "RS256", "HS384", "ES512", "PS256", "PS512", "RS512" ], "backchannel_logout_supported": true, "backchannel_logout_session_supported": true, "device_authorization_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/auth/device", "backchannel_token_delivery_modes_supported": [ "poll", "ping" ], "backchannel_authentication_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/ext/ciba/auth", "backchannel_authentication_request_signing_alg_values_supported": [ "PS384", "RS384", "EdDSA", "ES384", "ES256", "RS256", "ES512", "PS256", "PS512", "RS512" ], "require_pushed_authorization_requests": false, "pushed_authorization_request_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/ext/par/request", "mtls_endpoint_aliases": { "token_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/token", "revocation_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/revoke", "introspection_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/token/introspect", "device_authorization_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/auth/device", "registration_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/clients-registrations/openid-connect", "userinfo_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/userinfo", "pushed_authorization_request_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/ext/par/request", "backchannel_authentication_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/ext/ciba/auth" }, "authorization_response_iss_parameter_supported": true } """ OIDC_CONFIG_DIDMOS = """ { "version": "3.0", "token_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post" ], "claims_parameter_supported": true, "request_parameter_supported": false, "request_uri_parameter_supported": false, "require_request_uri_registration": false, "grant_types_supported": [ "authorization_code", "implicit" ], "frontchannel_logout_supported": false, "frontchannel_logout_session_supported": false, "backchannel_logout_supported": false, "backchannel_logout_session_supported": false, "issuer": "https://auth.didmos.nfdi-aai.de", "authorization_endpoint": "https://auth.didmos.nfdi-aai.de/OIDC/authorization", "jwks_uri": "https://auth.didmos.nfdi-aai.de/OIDC/jwks", "response_types_supported": [ "code", "id_token token" ], "id_token_signing_alg_values_supported": [ "RS256" ], "response_modes_supported": [ "fragment", "query" ], "subject_types_supported": [ "public" ], "claim_types_supported": [ "normal" ], "claims_supported": [ "email", "sub", "preferred_username", "name", "given_name", "family_name", "schac_home_organization", "eduperson_scoped_affiliation", "voperson_external_affiliation", "eduperson_assurance", "voperson_id", "eduperson_unique_id", "voperson_verified_email", "entitlements", "voperson_policy_agreement", "orcid", "voperson_external_id", "eduperson_principal_name", "eduperson_target_id", "session_id" ], "scopes_supported": [ "eduperson_assurance", "schac_home_organization", "profile", "voperson_policy_agreement", "voperson_external_affiliation", "eduperson_target_id", "eduperson_unique_id", "voperson_id", "email", "voperson_verified_email", "entitlements", "openid", "voperson_external_id", "eduperson_scoped_affiliation", "orcid" ], "token_endpoint": "https://auth.didmos.nfdi-aai.de/OIDC/token", "registration_endpoint": "https://auth.didmos.nfdi-aai.de/OIDC/registration", "end_session_endpoint": "https://auth.didmos.nfdi-aai.de/didmos/logout", "code_challenge_methods_supported": [ "S256", "S384", "S512" ], "userinfo_endpoint": "https://auth.didmos.nfdi-aai.de/OIDC/userinfo" } """ OIDC_CONFIG_REGAPP = """ { "response_types_supported": [ "code", "id_token", "code id_token" ], "request_uri_parameter_supported": true, "introspection_endpoint": "https://regapp.nfdi-aai.de/oidc/realms/nfdi/protocol/openid-connect/tokeninfo", "grant_types_supported": [ "authorization_code", "refresh_token" ], "scopes_supported": [ "openid", "profile", "email" ], "issuer": "https://regapp.nfdi-aai.de/oidc/realms/nfdi", "authorization_endpoint": "https://regapp.nfdi-aai.de/oidc/realms/nfdi/protocol/openid-connect/auth", "userinfo_endpoint": "https://regapp.nfdi-aai.de/oidc/realms/nfdi/protocol/openid-connect/userinfo", "claims_supported": [ "sub", "iss", "aud", "mail", "name" ], "jwks_uri": "https://regapp.nfdi-aai.de/oidc/realms/nfdi/protocol/openid-connect/certs", "subject_types_supported": [ "pairwise", "public" ], "id_token_signing_alg_values_supported": [ "RS256" ], "token_endpoint_auth_methods_supported": [ "client_secret_post", "private_key_jwt", "client_secret_basic" ], "response_modes_supported": [ "query", "fragment" ], "token_endpoint": "https://regapp.nfdi-aai.de/oidc/realms/nfdi/protocol/openid-connect/token" } """ OIDC_CONFIG_UNITY = """ { "authorization_endpoint": "https://login.helmholtz.de/oauth2-as/oauth2-authz", "token_endpoint": "https://login.helmholtz.de/oauth2/token", "introspection_endpoint": "https://login.helmholtz.de/oauth2/introspect", "revocation_endpoint": "https://login.helmholtz.de/oauth2/revoke", "issuer": "https://login.helmholtz.de/oauth2", "jwks_uri": "https://login.helmholtz.de/oauth2/jwk", "scopes_supported": [ "openid", "display_name", "sn", "single-logout", "offline_access", "voperson_id", "voperson_external_affiliation", "entitlements", "org_domain", "email", "profile", "credentials", "eduperson_scoped_affiliation", "eduperson_entitlement", "eduperson_principal_name", "eduperson_unique_id", "eduperson_assurance", "sys:scim:read_profile", "sys:scim:read_memberships", "sys:scim:read_self_group" ], "response_types_supported": [ "code", "token", "id_token", "code id_token", "id_token token", "code token", "code id_token token" ], "response_modes_supported": [ "query", "fragment" ], "grant_types_supported": [ "authorization_code", "implicit" ], "code_challenge_methods_supported": [ "plain", "S256" ], "request_uri_parameter_supported": true, "subject_types_supported": [ "public" ], "userinfo_endpoint": "https://login.helmholtz.de/oauth2/userinfo", "id_token_signing_alg_values_supported": [ "RS256", "ES256" ] } """ OIDC_CONFIG_UNITY_PUNCH = """ { "authorization_endpoint": "https://login.helmholtz.de/punch-oauth2-as/oauth2-authz", "token_endpoint": "https://login.helmholtz.de/punch-oauth2/token", "introspection_endpoint": "https://login.helmholtz.de/punch-oauth2/introspect", "revocation_endpoint": "https://login.helmholtz.de/punch-oauth2/revoke", "issuer": "https://login.helmholtz.de/punch-oauth2", "jwks_uri": "https://login.helmholtz.de/punch-oauth2/jwk", "scopes_supported": [ "openid", "display_name", "sn", "single-logout", "offline_access", "email", "profile", "credentials", "eduperson_scoped_affiliation", "eduperson_entitlement", "eduperson_principal_name", "eduperson_unique_id", "eduperson_assurance", "sys:scim:read_profile", "sys:scim:read_memberships", "sys:scim:read_self_group" ], "response_types_supported": [ "code", "token", "id_token", "code id_token", "id_token token", "code token", "code id_token token" ], "response_modes_supported": [ "query", "fragment" ], "grant_types_supported": [ "authorization_code", "implicit" ], "code_challenge_methods_supported": [ "plain", "S256" ], "request_uri_parameter_supported": true, "subject_types_supported": [ "public" ], "userinfo_endpoint": "https://login.helmholtz.de/punch-oauth2/userinfo", "id_token_signing_alg_values_supported": [ "RS256", "ES256" ] } """ class NFDIOpenIdConnectTest(OpenIdConnectTest, BaseAuthUrlTestMixin): backend_path = "social_core.backends.nfdi.HelmholtzOpenIdConnect" issuer = "https://login.helmholtz.de/oauth2" openid_config_body = OIDC_CONFIG_UNITY expected_username = "donald" user_data_url = "https://login.helmholtz.de/oauth2/userinfo" user_data_body = json.dumps( { "display_name": "Donald Duck", "eduperson_assurance": [ "https://refeds.org/assurance", "https://refeds.org/assurance/ID/unique", "https://refeds.org/assurance/ID/eppn-unique-no-reassign", "https://refeds.org/assurance/ATP/ePA-1d", "https://refeds.org/assurance/ATP/ePA-1m", "https://refeds.org/assurance/IAP/local-enterprise", "https://refeds.org/assurance/IAP/low", "https://refeds.org/assurance/IAP/medium", "https://refeds.org/assurance/profile/cappuccino", "https://aarc-project.eu/policy/authn-assurance/assam", ], "eduperson_entitlement": [ "urn:mace:dir:entitlement:common-lib-terms", "http://bwidm.de/entitlement/bwLSDF-SyncShare", "urn:canard:mouseton.edu:group:KIT#login.mouseton.edu", "urn:canard:h-df.de:group:m-team:feudal-developers#login.mouseton.edu", "urn:canard:mouseton.edu:group:Arbeitskreise#login.mouseton.edu", "urn:canard:mouseton.edu:group:HIFIS:Associates#login.mouseton.edu", "urn:canard:mouseton.edu:group:Arbeitskreise:AG IT Services#login.mouseton.edu", "urn:canard:mouseton.edu:group:Helmholtz-member#login.mouseton.edu", "urn:canard:h-df.de:group:m-team#login.mouseton.edu", "urn:canard:mouseton.edu:group:Helmholtz-all#login.mouseton.edu", "urn:canard:mouseton.edu:group:HIFIS#login.mouseton.edu", ], "eduperson_principal_name": "lo0018@duckburg.edu", "eduperson_scoped_affiliation": [ "employee@login.mouseton.edu", "member@login.mouseton.edu", ], "eduperson_unique_id": "42234223422342234223422342234223@login.mouseton.edu", "email": "donald.hardt@duckburg.edu", "email_verified": True, "entitlements": [ "urn:mace:dir:entitlement:common-lib-terms", "http://duckburg.edu/entitlement/sync-and-share", "urn:canard:mouseton.edu:group:KIT#login.mouseton.edu", "urn:canard:duckburg.edu:group:m-team:developers#login.mouseton.edu", "urn:canard:mouseton.edu:group:Workgroup:IT Services#login.mouseton.edu", "urn:canard:mouseton.edu:group:mouseton-member#login.mouseton.edu", "urn:canard:mouseton.edu:group:m-team#login.mouseton.edu", "urn:canard:mouseton.edu:group:mouseton-all#login.mouseton.edu", ], "family_name": "Duck", "given_name": "Donald", "iss": "https://login.mouseton.edu/oauth2", "name": "Donald Duck", "org_domain": "duckburg.edu", "preferred_username": "donald", "sn": "Duck", "ssh_public_key": "ssh-ed25519 AAAAC3N4224224224224224224224224224224224224224224224224224224223ym/ donald@home\n", "sub": "42234223-4223-4223-4223-422342234223", "voperson_external_affiliation": [ "employee@duckburg.edu", "faculty@duckburg.edu", "member@duckburg.edu", ], "voperson_id": "42234223422342234223422342234223@login.mouseton.edu", } ) def test_do_not_override_endpoint(self): self.backend.OIDC_ENDPOINT = self.issuer self.assertEqual(self.backend.oidc_endpoint(), self.issuer) def test_entitlements_empty(self): self.assertEqual(self.backend.entitlement_allowed([]), True) def test_entitlements_allowed(self): self.backend.ALLOWED_ENTITLEMENTS = ["foo", "baz"] self.assertEqual(self.backend.entitlement_allowed(["foo", "bar"]), True) def test_entitlements_not_allowed(self): self.backend.ALLOWED_ENTITLEMENTS = ["baz"] self.assertEqual(self.backend.entitlement_allowed(["foo"]), False) def test_get_user_details(self): testdata = self.backend.get_user_details( json.loads(cast("str", self.user_data_body)) ) self.assertEqual(testdata["username"], "donald") self.assertEqual(testdata["email"], "donald.hardt@duckburg.edu") self.assertEqual(testdata["fullname"], "Donald Duck") self.assertEqual(testdata["first_name"], "Donald") self.assertEqual(testdata["last_name"], "Duck") def test_login(self): self.do_login() social-auth-core-4.6.1/social_core/tests/backends/test_ngpvan.py000066400000000000000000000212661500362547200247740ustar00rootroot00000000000000"""Tests for NGP VAN ActionID Backend""" import datetime from urllib.parse import urlencode import pytest import responses from .open_id import OpenIdTest JANRAIN_NONCE = datetime.datetime.now(datetime.timezone.utc).strftime( "%Y-%m-%dT%H:%M:%SZ" ) class NGPVANActionIDOpenIDTest(OpenIdTest): """Test the NGP VAN ActionID OpenID 1.1 Backend""" backend_path = "social_core.backends.ngpvan.ActionIDOpenID" expected_username = "testuser@user.local" discovery_body = """ http://specs.openid.net/auth/2.0/signon http://openid.net/extensions/sreg/1.1 http://axschema.org/contact/email https://accounts.ngpvan.com/OpenId/Provider http://openid.net/signon/1.0 http://openid.net/extensions/sreg/1.1 http://axschema.org/contact/email https://accounts.ngpvan.com/OpenId/Provider """ server_response = urlencode( { "openid.claimed_id": "https://accounts.ngpvan.com/user/abcd123", "openid.identity": "https://accounts.ngpvan.com/user/abcd123", "openid.sig": "Midw8F/rCDwW7vMz3y+vK6rjz6s=", "openid.signed": "claimed_id,identity,assoc_handle,op_endpoint,return_" "to,response_nonce,ns.alias3,alias3.mode,alias3.type." "alias1,alias3.value.alias1,alias3.type.alias2,alias3" ".value.alias2,alias3.type.alias3,alias3.value.alias3" ",alias3.type.alias4,alias3.value.alias4,alias3.type." "alias5,alias3.value.alias5,alias3.type.alias6,alias3" ".value.alias6,alias3.type.alias7,alias3.value.alias7" ",alias3.type.alias8,alias3.value.alias8,ns.sreg,sreg" ".fullname", "openid.assoc_handle": "{635790678917902781}{GdSyFA==}{20}", "openid.op_endpoint": "https://accounts.ngpvan.com/OpenId/Provider", "openid.return_to": "http://myapp.com/complete/actionid-openid/", "openid.response_nonce": JANRAIN_NONCE + "MMgBGEre", "openid.mode": "id_res", "openid.ns": "http://specs.openid.net/auth/2.0", "openid.ns.alias3": "http://openid.net/srv/ax/1.0", "openid.alias3.mode": "fetch_response", "openid.alias3.type.alias1": "http://openid.net/schema/contact/phone/b" "usiness", "openid.alias3.value.alias1": "+12015555555", "openid.alias3.type.alias2": "http://openid.net/schema/contact/interne" "t/email", "openid.alias3.value.alias2": "testuser@user.local", "openid.alias3.type.alias3": "http://openid.net/schema/namePerson/first", "openid.alias3.value.alias3": "John", "openid.alias3.type.alias4": "http://openid.net/schema/namePerson/last", "openid.alias3.value.alias4": "Smith", "openid.alias3.type.alias5": "http://axschema.org/namePerson/first", "openid.alias3.value.alias5": "John", "openid.alias3.type.alias6": "http://axschema.org/namePerson/last", "openid.alias3.value.alias6": "Smith", "openid.alias3.type.alias7": "http://axschema.org/namePerson", "openid.alias3.value.alias7": "John Smith", "openid.alias3.type.alias8": "http://openid.net/schema/namePerson", "openid.alias3.value.alias8": "John Smith", "openid.ns.sreg": "http://openid.net/extensions/sreg/1.1", "openid.sreg.fullname": "John Smith", } ) def setUp(self): """Setup the test""" super().setUp() # Mock out the NGP VAN endpoints responses.add( responses.GET, "https://accounts.ngpvan.com/Home/Xrds", status=200, body=self.discovery_body, ) responses.add( responses.GET, "https://accounts.ngpvan.com/user/abcd123", status=200, body=self.discovery_body, ) responses.add( responses.GET, "https://accounts.ngpvan.com/OpenId/Provider", status=200, body=self.discovery_body, ) @pytest.mark.xfail(reason="responses mocking does not work for openid") def test_login(self): """Test the login flow using python-social-auth's built in test""" self.do_login() @pytest.mark.xfail(reason="responses mocking does not work for openid") def test_partial_pipeline(self): """Test the partial flow using python-social-auth's built in test""" self.do_partial_pipeline() def test_get_ax_attributes(self): """Test that the AX attributes that NGP VAN responds with are present""" records = self.backend.get_ax_attributes() self.assertEqual( records, [ ("http://openid.net/schema/contact/internet/email", "email"), ("http://openid.net/schema/contact/phone/business", "phone"), ("http://openid.net/schema/namePerson/first", "first_name"), ("http://openid.net/schema/namePerson/last", "last_name"), ("http://openid.net/schema/namePerson", "fullname"), ], ) @pytest.mark.xfail(reason="responses mocking does not work for openid") def test_setup_request(self): """Test the setup_request functionality in the NGP VAN backend""" # We can grab the requested attributes by grabbing the HTML of the # OpenID auth form and pulling out the hidden fields _, inputs = self.get_form_data(self.backend.auth_html()) # Confirm that the only required attribute is email self.assertEqual(inputs["openid.ax.required"], "ngpvanemail") # Confirm that the 3 optional attributes are requested "if available" self.assertIn("ngpvanphone", inputs["openid.ax.if_available"]) self.assertIn("ngpvanfirstname", inputs["openid.ax.if_available"]) self.assertIn("ngpvanlastname", inputs["openid.ax.if_available"]) # Verify the individual attribute properties self.assertEqual( inputs["openid.ax.type.ngpvanemail"], "http://openid.net/schema/contact/internet/email", ) self.assertEqual( inputs["openid.ax.type.ngpvanfirstname"], "http://openid.net/schema/namePerson/first", ) self.assertEqual( inputs["openid.ax.type.ngpvanlastname"], "http://openid.net/schema/namePerson/last", ) self.assertEqual( inputs["openid.ax.type.ngpvanphone"], "http://openid.net/schema/contact/phone/business", ) @pytest.mark.xfail(reason="responses mocking does not work for openid") def test_user_data(self): """Ensure that the correct user data is being passed to create_user""" self.strategy.set_settings( { "USER_FIELDS": [ "email", "first_name", "last_name", "username", "phone", "fullname", ] } ) user = self.do_start() self.assertEqual(user.username, "testuser@user.local") self.assertEqual(user.email, "testuser@user.local") self.assertEqual(user.extra_user_fields["phone"], "+12015555555") self.assertEqual(user.extra_user_fields["first_name"], "John") self.assertEqual(user.extra_user_fields["last_name"], "Smith") self.assertEqual(user.extra_user_fields["fullname"], "John Smith") @pytest.mark.xfail(reason="responses mocking does not work for openid") def test_extra_data_phone(self): """Confirm that you can get a phone number via the relevant setting""" self.strategy.set_settings( { "SOCIAL_AUTH_ACTIONID_OPENID_AX_EXTRA_DATA": [ ("http://openid.net/schema/contact/phone/business", "phone") ] } ) user = self.do_start() self.assertEqual(user.social_user.extra_data["phone"], "+12015555555") @pytest.mark.xfail(reason="responses mocking does not work for openid") def test_association_uid(self): """Test that the correct association uid is stored in the database""" user = self.do_start() self.assertEqual( user.social_user.uid, "https://accounts.ngpvan.com/user/abcd123" ) social-auth-core-4.6.1/social_core/tests/backends/test_okta.py000066400000000000000000000146021500362547200244350ustar00rootroot00000000000000import json import responses from social_core.tests.backends.oauth import BaseAuthUrlTestMixin, OAuth2Test from .open_id_connect import OpenIdConnectTest JWK_KEY = { "kty": "RSA", "d": "ZmswNokEvBcxW_Kvcy8mWUQOQCBdGbnM0xR7nhvGHC-Q24z3XAQWlMWbsmGc_R1o" "_F3zK7DBlc3BokdRaO1KJirNmnHCw5TlnBlJrXiWpFBtVglUg98-4sRRO0VWnGXK" "JPOkBQ6b_DYRO3b0o8CSpWowpiV6HB71cjXTqKPZf-aXU9WjCCAtxVjfIxgQFu5I" "-G1Qah8mZeY8HK_y99L4f0siZcbUoaIcfeWBhxi14ODyuSAHt0sNEkhiIVBZE7QZ" "m-SEP1ryT9VAaljbwHHPmg7NC26vtLZhvaBGbTTJnEH0ZubbN2PMzsfeNyoCIHy4" "4QDSpQDCHfgcGOlHY_t5gQ", "e": "AQAB", "use": "sig", "kid": "testkey", "alg": "RS256", "n": "pUfcJ8WFrVue98Ygzb6KEQXHBzi8HavCu8VENB2As943--bHPcQ-nScXnrRFAUg8" "H5ZltuOcHWvsGw_AQifSLmOCSWJAPkdNb0w0QzY7Re8NrPjCsP58Tytp5LicF0Ao" "Ag28UK3JioY9hXHGvdZsWR1Rp3I-Z3nRBP6HyO18pEgcZ91c9aAzsqu80An9X4DA" "b1lExtZorvcd5yTBzZgr-MUeytVRni2lDNEpa6OFuopHXmg27Hn3oWAaQlbymd4g" "ifc01oahcwl3ze2tMK6gJxa_TdCf1y99Yq6oilmVvZJ8kwWWnbPE-oDmOVPVnEyT" "vYVCvN4rBT1DQ-x0F1mo2Q", } JWK_PUBLIC_KEY = {key: value for key, value in JWK_KEY.items() if key != "d"} class OktaOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.okta.OktaOAuth2" user_data_url = "https://dev-000000.oktapreview.com/oauth2/v1/userinfo" expected_username = "foo" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "family_name": "Bar", "sub": "101010101010101010101", "locale": "en", "email_verified": True, "given_name": "Foo", "email": "foo@bar.com", "name": "Foo Bar", "nickname": "foobar", "middle_name": "", "profile": "https://example.com/foo.bar", "zoneinfo": "America/Los_Angeles", "updated_at": 1311280970, "preferred_username": "foo", } ) def test_login(self): self.strategy.set_settings( { "SOCIAL_AUTH_OKTA_OAUTH2_API_URL": "https://dev-000000.oktapreview.com/oauth2" } ) self.do_login() def test_partial_pipeline(self): self.strategy.set_settings( { "SOCIAL_AUTH_OKTA_OAUTH2_API_URL": "https://dev-000000.oktapreview.com/oauth2" } ) self.do_partial_pipeline() class OktaOpenIdConnectTest(OpenIdConnectTest): backend_path = "social_core.backends.okta_openidconnect.OktaOpenIdConnect" user_data_url = "https://dev-000000.oktapreview.com/oauth2/v1/userinfo" issuer = "https://dev-000000.oktapreview.com/oauth2" openid_config_body = json.dumps( { "issuer": "https://dev-000000.oktapreview.com/oauth2", "authorization_endpoint": "https://dev-000000.oktapreview.com/oauth2/v1/authorize", "token_endpoint": "https://dev-000000.oktapreview.com/oauth2/v1/token", "userinfo_endpoint": "https://dev-000000.oktapreview.com/oauth2/v1/userinfo", "jwks_uri": "https://dev-000000.oktapreview.com/oauth2/v1/keys", "response_types_supported": [ "code", "token", "id_token", "code token", "code id_token", "token id_token", "code token id_token", "none", ], "subject_types_supported": [ "public", ], "id_token_signing_alg_values_supported": [ "RS256", ], "scopes_supported": [ "openid", "email", "profile", ], "token_endpoint_auth_methods_supported": [ "client_secret_post", "client_secret_basic", ], "claims_supported": [ "aud", "email", "email_verified", "exp", "family_name", "given_name", "iat", "iss", "locale", "name", "picture", "sub", ], } ) user_data_body = json.dumps( { "family_name": "Bar", "sub": "101010101010101010101", "locale": "en", "email_verified": True, "given_name": "Foo", "email": "foo@bar.com", "name": "Foo Bar", "nickname": "foobar", "middle_name": "", "profile": "https://example.com/foo.bar", "zoneinfo": "America/Los_Angeles", "updated_at": 1311280970, "preferred_username": "foo", } ) expected_username = "foo" def extra_settings(self): settings = super().extra_settings() # Settings for Okta oidc_config = json.loads(self.openid_config_body) settings.update( { "SOCIAL_AUTH_OKTA_OPENIDCONNECT_API_URL": "https://dev-000000.oktapreview.com/oauth2", "SOCIAL_AUTH_OKTA_OPENIDCONNECT_OIDC_ENDPOINT": "https://dev-000000.oktapreview.com/oauth2", "SOCIAL_AUTH_OKTA_OPENIDCONNECT_JWKS_URI": oidc_config.get("jwks_uri"), "SOCIAL_AUTH_OKTA_OPENIDCONNECT_ID_TOKEN_ISSUER": oidc_config.get( "issuer" ), } ) return settings def setUp(self): responses.add( responses.GET, # Note: okta.py strips the /oauth2 prefix using urljoin with absolute path "https://dev-000000.oktapreview.com/.well-known/openid-configuration?client_id=a-key", status=200, body=self.openid_config_body, content_type="application/json", ) oidc_config = json.loads(self.openid_config_body) responses.add( responses.GET, oidc_config.get("jwks_uri"), status=200, json={"keys": [JWK_PUBLIC_KEY]}, ) super().setUp() def pre_complete_callback(self, start_url): super().pre_complete_callback(start_url) responses.add( responses.GET, url=self.backend.userinfo_url(), status=200, json={"preferred_username": self.expected_username}, ) def test_everything_works(self): self.do_login() social-auth-core-4.6.1/social_core/tests/backends/test_open_id_connect.py000066400000000000000000000052371500362547200266310ustar00rootroot00000000000000from __future__ import annotations import json import responses from social_core.backends.open_id_connect import OpenIdConnectAuth from .oauth import BaseAuthUrlTestMixin from .open_id_connect import OpenIdConnectTest class BaseOpenIdConnectTest(OpenIdConnectTest, BaseAuthUrlTestMixin): backend_path = "social_core.backends.open_id_connect.OpenIdConnectAuth" issuer = "https://example.com" openid_config_body = json.dumps( { "issuer": "https://example.com", "authorization_endpoint": "https://example.com/oidc/auth", "token_endpoint": "https://example.com/oidc/token", "userinfo_endpoint": "https://example.com/oidc/userinfo", "revocation_endpoint": "https://example.com/oidc/revoke", "jwks_uri": "https://example.com/oidc/certs", } ) expected_username = "cartman" def extra_settings(self): settings = super().extra_settings() settings.update( { "SOCIAL_AUTH_OIDC_OIDC_ENDPOINT": "https://example.com/oidc", } ) return settings def pre_complete_callback(self, start_url): super().pre_complete_callback(start_url) responses.add( "GET", url=self.backend.userinfo_url(), status=200, body=json.dumps({"preferred_username": self.expected_username}), content_type="text/json", ) def test_everything_works(self): self.do_login() class ExampleOpenIdConnectAuth(OpenIdConnectAuth): name = "example123" OIDC_ENDPOINT = "https://example.com/oidc" class ExampleOpenIdConnectTest(OpenIdConnectTest): backend_path = ( "social_core.tests.backends.test_open_id_connect.ExampleOpenIdConnectAuth" ) issuer = "https://example.com" openid_config_body = json.dumps( { "issuer": "https://example.com", "authorization_endpoint": "https://example.com/oidc/auth", "token_endpoint": "https://example.com/oidc/token", "userinfo_endpoint": "https://example.com/oidc/userinfo", "revocation_endpoint": "https://example.com/oidc/revoke", "jwks_uri": "https://example.com/oidc/certs", } ) expected_username = "cartman" def pre_complete_callback(self, start_url): super().pre_complete_callback(start_url) responses.add( responses.GET, url=self.backend.userinfo_url(), status=200, body=json.dumps({"preferred_username": self.expected_username}), content_type="text/json", ) def test_everything_works(self): self.do_login() social-auth-core-4.6.1/social_core/tests/backends/test_openstreetmap_oauth2.py000066400000000000000000000020141500362547200276410ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class OpenStreetMapOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.openstreetmap_oauth2.OpenStreetMapOAuth2" user_data_url = "https://api.openstreetmap.org/api/0.6/user/details.json" expected_username = "Steve" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "version": "0.6", "generator": "OpenStreetMap server", "copyright": "OpenStreetMap and contributors", "attribution": "http://www.openstreetmap.org/copyright", "license": "http://opendatacommons.org/licenses/odbl/1-0/", "user": { "id": 1, "display_name": "Steve", "account_created": "2005-09-13T15:32:57Z", }, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_orbi.py000066400000000000000000000015711500362547200244330ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class OrbiOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.orbi.OrbiOAuth2" user_data_url = "https://login.orbi.kr/oauth/user/get" expected_username = "foobar" access_token_body = json.dumps( { "access_token": "foobar", } ) user_data_body = json.dumps( { "username": "foobar", "first_name": "Foo", "last_name": "Bar", "name": "Foo Bar", "imin": "100000", "nick": "foobar", "photo": "http://s3.orbi.kr/data/member/wi/wizetdev_132894975780.jpeg", "sex": "M", "birth": "1973-08-03", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_orcid.py000066400000000000000000000053241500362547200246000ustar00rootroot00000000000000import json from social_core.backends.orcid import ORCIDOAuth2 from .oauth import BaseAuthUrlTestMixin, OAuth2Test class ORCIDOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.orcid.ORCIDOAuth2" user_data_url = ORCIDOAuth2.USER_ID_URL expected_username = "0000-0002-2601-8132" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", "orcid-identifier": {"path": "0000-0002-2601-8132"}, } ) user_data_body = json.dumps( { "orcid-identifier": { "uri": "http://orcid.org/0000-0002-2601-8132", "path": "0000-0002-2601-8132", "host": "orcid.org", }, "person": { "last-modified-date": None, "name": { "created-date": {"value": 1578249746904}, "last-modified-date": {"value": 1578249746904}, "given-names": {"value": "Janani Kantharooban"}, "family-name": {"value": "Umachanger"}, "credit-name": None, "source": None, "visibility": "PUBLIC", "path": "0000-0002-2601-8132", }, "other-names": { "last-modified-date": None, "other-name": [], "path": "/0000-0002-2601-8132/other-names", }, "biography": None, "researcher-urls": { "last-modified-date": None, "researcher-url": [], "path": "/0000-0002-2601-8132/researcher-urls", }, "emails": { "last-modified-date": None, "email": [], "path": "/0000-0002-2601-8132/email", }, "addresses": { "last-modified-date": None, "address": [], "path": "/0000-0002-2601-8132/address", }, "keywords": { "last-modified-date": None, "keyword": [], "path": "/0000-0002-2601-8132/keywords", }, "external-identifiers": { "last-modified-date": None, "external-identifier": [], "path": "/0000-0002-2601-8132/external-identifiers", }, "path": "/0000-0002-2601-8132/person", }, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_osso.py000066400000000000000000000015611500362547200244620ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class OssoOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.osso.OssoOAuth2" user_data_url = "https://demo.ossoapp.com/oauth/me" expected_username = "user@example.com" access_token_body = json.dumps( { "access_token": "3633395cffe739bb87089235c152155ae73b6794f7af353b2aa189aeeacee1ec", "token_type": "bearer", "expires_in": 600, } ) user_data_body = json.dumps( { "email": "user@example.com", "id": "f23611a5-2817-43e2-94b7-99b25235ad2d", "idp": "Okta", "requested": {"email": None, "domain": "example.com"}, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_patreon.py000066400000000000000000000043231500362547200251460ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class PatreonOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.patreon.PatreonOAuth2" user_data_url = "https://www.patreon.com/api/oauth2/v2/identity" expected_username = "JohnInterwebs" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", } ) user_data_body = json.dumps( { "data": { "relationships": { "pledges": {"data": [{"type": "pledge", "id": "123456"}]} }, "attributes": { "last_name": "Interwebs", "is_suspended": False, "has_password": True, "full_name": "John Interwebs", "is_nuked": False, "first_name": "John", "social_connections": { "spotify": None, "discord": None, "twitter": None, "youtube": None, "facebook": None, "deviantart": None, "twitch": None, }, "twitter": None, "is_email_verified": True, "facebook_id": None, "email": "john@example.com", "facebook": None, "thumb_url": "https://c8.patreon.com/100/123456", "vanity": None, "about": None, "is_deleted": False, "created": "2017-05-05T05:16:34+00:00", "url": "https://www.patreon.com/user?u=123456", "gender": 0, "youtube": None, "discord_id": None, "image_url": "https://c8.patreon.com/400/123456", "twitch": None, }, "type": "user", "id": "123456", } } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_paypal.py000066400000000000000000000052521500362547200247660ustar00rootroot00000000000000import json from social_core.backends.paypal import PayPalOAuth2 from .oauth import BaseAuthUrlTestMixin, OAuth2Test class PayPalOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.paypal.PayPalOAuth2" user_data_url = ( "https://api.paypal.com/v1/identity/oauth2/userinfo?schema=paypalv1.1" ) expected_username = "mWq6_1sU85v5EG9yHdPxJRrhGHrnMJ-1PQKtX6pcsmA" access_token_body = json.dumps( { "token_type": "Bearer", "expires_in": 28800, "refresh_token": "foobar-refresh-token", "access_token": "foobar-token", } ) user_data_body = json.dumps( { "user_id": "https://www.paypal.com/webapps/auth/identity/user/mWq6_1sU85v5EG9yHdPxJRrhGHrnMJ-1PQKtX6pcsmA", "name": "identity test", "given_name": "identity", "family_name": "test", "payer_id": "WDJJHEBZ4X2LY", "address": { "street_address": "1 Main St", "locality": "San Jose", "region": "CA", "postal_code": "95131", "country": "US", }, "verified_account": True, "emails": [{"value": "user1@example.com", "primary": True}], } ) refresh_token_body = json.dumps( { "access_token": "foobar-new-token", "token_type": "Bearer", "refresh_token": "foobar-new-refresh-token", "expires_in": 28800, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() def test_refresh_token(self): user, social = self.do_refresh_token() self.assertEqual(user.username, self.expected_username) self.assertEqual(social.extra_data["access_token"], "foobar-new-token") def test_get_email_no_emails(self): emails = [] email = PayPalOAuth2.get_email(emails) self.assertEqual(email, "") def test_get_email_multiple_emails(self): expected_email = "mail2@example.com" emails = [ {"value": "mail1@example.com", "primary": False}, {"value": expected_email, "primary": True}, ] email = PayPalOAuth2.get_email(emails) self.assertEqual(email, expected_email) def test_get_email_multiple_emails_no_primary(self): expected_email = "mail1@example.com" emails = [ {"value": expected_email, "primary": False}, {"value": "mail2@example.com", "primary": False}, ] email = PayPalOAuth2.get_email(emails) self.assertEqual(email, expected_email) social-auth-core-4.6.1/social_core/tests/backends/test_phabricator.py000066400000000000000000000051661500362547200260020ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class PhabricatorOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.phabricator.PhabricatorOAuth2" user_data_url = "https://secure.phabricator.com/api/user.whoami" expected_username = "user" access_token_body = json.dumps( { "access_token": "loremipsumdolorsitametenim", "token_type": "Bearer", "expires_in": 7200, "refresh_token": "foobar", } ) user_data_body = json.dumps( { "phid": "PHID-USER-jbfcsj7c6nkt2tv3trb6", "userName": "user", "realName": "FirstName LastName", "image": "https://secure.phabricator.com/file/data/4qjdpmvca4wwkfw2wevc" "/PHID-FILE-t37vxezr54fjuvbrblkp/alphanumeric_lato-white_U.png" "-_3f674d-255%2C255%2C255%2C0.7.png", "uri": "https://secure.phabricator.com/p/user/", "roles": ["admin", "verified", "approved", "activated"], "primaryEmail": "user@example.com", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() class PhabricatorCustomDomainOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.phabricator.PhabricatorOAuth2" user_data_url = "https://example.com/api/user.whoami" expected_username = "user" access_token_body = json.dumps( { "access_token": "loremipsumdolorsitametenim", "token_type": "Bearer", "expires_in": 7200, "refresh_token": "foobar", } ) user_data_body = json.dumps( { "phid": "PHID-USER-jbfcsj7c6nkt2tv3trb6", "userName": "user", "realName": "FirstName LastName", "image": "https://example.com/file/data/4qjdpmvca4wwkfw2wevc/PHID-FILE-" "t37vxezr54fjuvbrblkp/alphanumeric_lato-white_U.png-_3f674d-25" "5%2C255%2C255%2C0.7.png", "uri": "https://example.com/p/user/", "roles": ["admin", "verified", "approved", "activated"], "primaryEmail": "user@example.com", } ) def test_login(self): self.strategy.set_settings( { "SOCIAL_AUTH_PHABRICATOR_API_URL": "https://example.com", } ) self.do_login() def test_partial_pipeline(self): self.strategy.set_settings( { "SOCIAL_AUTH_PHABRICATOR_API_URL": "https://example.com", } ) self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_pinterest.py000066400000000000000000000026031500362547200255120ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class PinterestOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.pinterest.PinterestOAuth2" user_data_url = "https://api.pinterest.com/v1/me/" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "id": "4788400174839062", "first_name": "Foo", "last_name": "Bar", "username": "foobar", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() class PinterestOAuth2BrokenServerResponseTest(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.pinterest.PinterestOAuth2" user_data_url = "https://api.pinterest.com/v1/me/" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "data": { "id": "4788400174839062", "first_name": "Foo", "last_name": "Bar", "url": "https://www.pinterest.com/foobar/", } } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_podio.py000066400000000000000000000036061500362547200246130ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class PodioOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.podio.PodioOAuth2" user_data_url = "https://api.podio.com/user/status" expected_username = "user_1010101010" access_token_body = json.dumps( { "token_type": "bearer", "access_token": "11309ea9016a4ad99f1a3bcb9bc7a9d1", "refresh_token": "52d01df8b9ac46a4a6be1333d9f81ef2", "expires_in": 28800, "ref": { "type": "user", "id": 1010101010, }, } ) user_data_body = json.dumps( { "user": { "user_id": 1010101010, "activated_on": "2012-11-22 09:37:21", "created_on": "2012-11-21 12:23:47", "locale": "en_GB", "timezone": "Europe/Copenhagen", "mail": "foo@bar.com", "mails": [ { "disabled": False, "mail": "foobar@example.com", "primary": False, "verified": True, }, { "disabled": False, "mail": "foo@bar.com", "primary": True, "verified": True, }, ], # more properties ... }, "profile": { "last_seen_on": "2013-05-16 12:21:13", "link": "https://podio.com/users/1010101010", "name": "Foo Bar", # more properties ... }, # more properties ... } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_qiita.py000066400000000000000000000063521500362547200246110ustar00rootroot00000000000000import json from social_core.exceptions import AuthException from .oauth import BaseAuthUrlTestMixin, OAuth2Test class QiitaOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.qiita.QiitaOAuth2" user_data_url = "https://qiita.com/api/v2/authenticated_user" expected_username = "foobar" access_token_body = json.dumps({"token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "id": "foobar", "name": "Foo Bar", "description": "Hello, world.", "facebook_id": "qiita", "followees_count": 100, "followers_count": 200, "github_login_name": "qiitan", "items_count": 300, "linkedin_id": "qiita", "location": "Tokyo, Japan", "organization": "Qiita Inc.", "permanent_id": 12345, "profile_image_url": "https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/88" "/ccf90b557a406157dbb9d2d7e543dae384dbb561/large.png?1575443439", "team_only": False, "twitter_screen_name": "qiita", "website_url": "https://qiita.com", "image_monthly_upload_limit": 1048576, "image_monthly_upload_remaining": 524288, } ) def test_login(self): user = self.do_login() self.assertEqual(len(user.social), 1) social = user.social[0] self.assertEqual(social.uid, "foobar") self.assertEqual(social.extra_data["permanent_id"], 12345) def test_partial_pipeline(self): user = self.do_partial_pipeline() self.assertEqual(len(user.social), 1) social = user.social[0] self.assertEqual(social.uid, "foobar") self.assertEqual(social.extra_data["permanent_id"], 12345) class QiitaOAuth2TestIdentifiedByPermanentId(QiitaOAuth2Test): def test_login(self): self.strategy.set_settings( {"SOCIAL_AUTH_QIITA_IDENTIFIED_BY_PERMANENT_ID": True} ) user = self.do_login() self.assertEqual(len(user.social), 1) social = user.social[0] self.assertEqual(social.uid, "12345") self.assertEqual(social.extra_data["permanent_id"], 12345) def test_partial_pipeline(self): self.strategy.set_settings( {"SOCIAL_AUTH_QIITA_IDENTIFIED_BY_PERMANENT_ID": True} ) user = self.do_partial_pipeline() self.assertEqual(len(user.social), 1) social = user.social[0] self.assertEqual(social.uid, "12345") self.assertEqual(social.extra_data["permanent_id"], 12345) class QiitaOAuth2TestIdentifiedByPermanentIdAuthException(QiitaOAuth2Test): user_data_body = json.dumps( { "id": "foobar", "name": "Foo Bar", } ) def test_login(self): self.strategy.set_settings( {"SOCIAL_AUTH_QIITA_IDENTIFIED_BY_PERMANENT_ID": True} ) with self.assertRaises(AuthException): self.do_login() def test_partial_pipeline(self): self.strategy.set_settings( {"SOCIAL_AUTH_QIITA_IDENTIFIED_BY_PERMANENT_ID": True} ) with self.assertRaises(AuthException): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_quizlet.py000066400000000000000000000011351500362547200251710ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class QuizletOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.quizlet.QuizletOAuth2" expected_username = "foo_bar" access_token_body = json.dumps( { "access_token": "EE1IDxytP04tJ767GbjH7ED9PpGmYvL", "token_type": "Bearer", "expires_in": 3600, "scope": "read", "user_id": "foo_bar", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_readability.py000066400000000000000000000025531500362547200257720ustar00rootroot00000000000000import json from urllib.parse import urlencode from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test class ReadabilityOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.readability.ReadabilityOAuth" user_data_url = "https://www.readability.com/api/rest/v1/users/_current" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", "oauth_callback_confirmed": "true", } ) user_data_body = json.dumps( { "username": "foobar", "first_name": "Foo", "last_name": "Bar", "has_active_subscription": False, "tags": [], "is_publisher": False, "email_into_address": "foobar+sharp@inbox.readability.com", "kindle_email_address": None, "avatar_url": "https://secure.gravatar.com/avatar/" "5280f15cedf540b544eecc30fcf3027c?d=" "https://www.readability.com/media/images/" "avatar.png&s=36", "date_joined": "2013-03-18 02:51:02", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_reddit.py000066400000000000000000000036471500362547200247610ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class RedditOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.reddit.RedditOAuth2" user_data_url = "https://oauth.reddit.com/api/v1/me.json" expected_username = "foobar" access_token_body = json.dumps( { "name": "foobar", "created": 1203420772.0, "access_token": "foobar-token", "created_utc": 1203420772.0, "expires_in": 3600.0, "link_karma": 34, "token_type": "bearer", "comment_karma": 167, "over_18": True, "is_gold": False, "is_mod": True, "scope": "identity", "has_verified_email": False, "id": "33bma", "refresh_token": "foobar-refresh-token", } ) user_data_body = json.dumps( { "name": "foobar", "created": 1203420772.0, "created_utc": 1203420772.0, "link_karma": 34, "comment_karma": 167, "over_18": True, "is_gold": False, "is_mod": True, "has_verified_email": False, "id": "33bma", } ) refresh_token_body = json.dumps( { "access_token": "foobar-new-token", "token_type": "bearer", "expires_in": 3600.0, "refresh_token": "foobar-new-refresh-token", "scope": "identity", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() def refresh_token_arguments(self): uri = self.strategy.build_absolute_uri("/complete/reddit/") return {"redirect_uri": uri} def test_refresh_token(self): user, social = self.do_refresh_token() self.assertEqual(social.extra_data["access_token"], "foobar-new-token") social-auth-core-4.6.1/social_core/tests/backends/test_saml.py000066400000000000000000000140641500362547200244350ustar00rootroot00000000000000import json import re import sys import unittest from pathlib import Path from unittest.mock import patch from urllib.parse import parse_qs, urlencode, urlparse, urlunparse import requests import responses try: from onelogin.saml2.utils import OneLogin_Saml2_Utils SAML_MODULE_ENABLED = True except ImportError: SAML_MODULE_ENABLED = False from ...exceptions import AuthMissingParameter from .base import BaseBackendTest DATA_DIR = Path(__file__).parent / "data" @unittest.skipIf( "__pypy__" in sys.builtin_module_names, "dm.xmlsec not compatible with pypy" ) @unittest.skipUnless(SAML_MODULE_ENABLED, "Only run if onelogin.saml2 is installed") class SAMLTest(BaseBackendTest): backend_path = "social_core.backends.saml.SAMLAuth" expected_username = "myself" response_fixture = "saml_response.txt" def extra_settings(self): file = DATA_DIR / "saml_config.json" config_str = file.read_text() return json.loads(config_str) def setUp(self): """Patch the time so that we can replay canned request/response pairs""" super().setUp() @staticmethod def fixed_time(): return OneLogin_Saml2_Utils.parse_SAML_to_time("2015-05-09T03:57:22Z") now_patch = patch.object(OneLogin_Saml2_Utils, "now", fixed_time) now_patch.start() self.addCleanup(now_patch.stop) def install_http_intercepts(self, start_url, return_url): # When we request start_url # (https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO...) # we will eventually get a redirect back, with SAML assertion # data in the query string. A pre-recorded correct response # is kept in this .txt file: file = DATA_DIR / self.response_fixture response_url = file.read_text() responses.add( responses.GET, start_url, status=301, headers={"Location": response_url}, ) responses.add(responses.GET, return_url, status=200, body="foobar") def do_start(self): start_url = self.backend.start().url # Modify the start URL to make the SAML request consistent # from test to test: start_url = self.modify_start_url(start_url) # If the SAML Identity Provider recognizes the user, we will # be redirected back to: return_url = self.backend.redirect_uri self.install_http_intercepts(start_url, return_url) response = requests.get(start_url, timeout=1) self.assertTrue(response.url.startswith(return_url)) self.assertEqual(response.text, "foobar") query_values = { k: v[0] for k, v in parse_qs(urlparse(response.url).query).items() } self.assertNotIn(" ", query_values["SAMLResponse"]) self.strategy.set_request_data(query_values, self.backend) return self.backend.complete() def test_metadata_generation(self): """Test that we can generate the metadata without error""" xml, errors = self.backend.generate_metadata_xml() self.assertEqual(len(errors), 0) self.assertEqual(xml.decode()[0], "<") def test_login_with_next_url(self): """ Test that we login and then redirect to the "next" URL. """ # pretend we've started with a URL like /login/saml/?idp=testshib&next=/foo/bar self.strategy.set_request_data( {"idp": "testshib", "next": "/foo/bar"}, self.backend ) self.do_login() # The core `do_complete` action assumes the "next" URL is stored in session state or the request data. self.assertEqual(self.strategy.session_get("next"), "/foo/bar") def test_login_no_next_url(self): """ Test that we handle "next" being omitted from the request data and RelayState. """ self.response_fixture = "saml_response_no_next_url.txt" # pretend we've started with a URL like /login/saml/?idp=testshib self.strategy.set_request_data({"idp": "testshib"}, self.backend) self.do_login() self.assertEqual(self.strategy.session_get("next"), None) def test_login_with_legacy_relay_state(self): """ Test that we handle legacy RelayState (i.e. just the IDP name, not a JSON object). This is the form that RelayState had in prior versions of this library. It should be supported for backwards compatibility. """ self.response_fixture = "saml_response_legacy.txt" self.strategy.set_request_data({"idp": "testshib"}, self.backend) self.do_login() def test_login_no_idp_in_initial_request(self): """ Logging in without an idp param should raise AuthMissingParameter """ with self.assertRaises(AuthMissingParameter): self.do_start() def test_login_no_idp_in_saml_response(self): """ The RelayState should always contain a JSON object with an "idp" key, or be just the IDP name as a string. This tests that an exception is raised if it is a JSON object, but is missing the "idp" key. """ self.response_fixture = "saml_response_no_idp_name.txt" with self.assertRaises(AuthMissingParameter): self.do_start() def modify_start_url(self, start_url): """ Given a SAML redirect URL, parse it and change the ID to a consistent value, so the request is always identical. """ # Parse the SAML Request URL to get the XML being sent to TestShib url_parts = urlparse(start_url) query = {k: v[0] for (k, v) in parse_qs(url_parts.query).items()} xml = OneLogin_Saml2_Utils.decode_base64_and_inflate(query["SAMLRequest"]) # Modify the XML: xml = xml.decode() xml, changed = re.subn(r'ID="[^"]+"', 'ID="TEST_ID"', xml) self.assertEqual(changed, 1) # Update the URL to use the modified query string: query["SAMLRequest"] = OneLogin_Saml2_Utils.deflate_and_base64_encode(xml) url_parts = list(url_parts) url_parts[4] = urlencode(query) return urlunparse(url_parts) social-auth-core-4.6.1/social_core/tests/backends/test_scistarter.py000066400000000000000000000015131500362547200256570ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class ScistarterOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.scistarter.SciStarterOAuth2" user_data_url = "https://scistarter.com/api/user_info" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "profile_id": 42006, "user_id": 5, "url": "https://scistarter.com/user/foobar", "result": "success", "handle": "foobar", "email": "foo@bar.com", "first_name": "foo", "last_name": "bar", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_seznam.py000066400000000000000000000017121500362547200247720ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class SeznamOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.seznam.SeznamOAuth2" user_data_url = "https://login.szn.cz/api/v1/user" expected_username = "krasty" access_token_body = json.dumps( { "access_token": "foo", "account_name": "krasty@seznam.cz", "expires_in": 31536000, "oauth_user_id": "0123abcd", "refresh_token": "bar", "scopes": ["identity"], "token_type": "bearer", } ) user_data_body = json.dumps( { "email": "krasty@seznam.cz", "firstname": "Krasty", "lastname": "Dog", "oauth_user_id": "0123abcd", "username": "krasty", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_simplelogin.py000066400000000000000000000014341500362547200260200ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class SimpleLoginOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.simplelogin.SimpleLoginOAuth2" user_data_url = "https://app.simplelogin.io/oauth2/userinfo" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "client": "Continental", "email": "john@wick.com", "email_verified": True, "id": 1, "name": "John Wick", "avatar_url": "http://wick.com/john.png", } ) expected_username = "john@wick.com" def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_sketchfab.py000066400000000000000000000013171500362547200254300ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class SketchfabOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.sketchfab.SketchfabOAuth2" user_data_url = "https://sketchfab.com/v2/users/me" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "uid": "42", "email": "foo@bar.com", "displayName": "foo bar", "username": "foobar", "apiToken": "XXX", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_skyrock.py000066400000000000000000000027211500362547200251630ustar00rootroot00000000000000import json from urllib.parse import urlencode from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test class SkyrockOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.skyrock.SkyrockOAuth" user_data_url = "https://api.skyrock.com/v2/user/get.json" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", } ) user_data_body = json.dumps( { "locale": "en_US", "city": "", "has_blog": False, "web_messager_enabled": True, "email": "foo@bar.com", "username": "foobar", "firstname": "Foo", "user_url": "", "address1": "", "address2": "", "has_profile": False, "allow_messages_from": "everybody", "is_online": False, "postalcode": "", "lang": "en", "id_user": 10101010, "name": "Bar", "gender": 0, "avatar_url": "http://www.skyrock.com/img/avatars/default-0.jpg", "nb_friends": 0, "country": "US", "birth_date": "1980-06-10", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_slack.py000066400000000000000000000035351500362547200245770ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class SlackOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.slack.SlackOAuth2" user_data_url = "https://slack.com/api/users.identity" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "ok": True, "user": {"email": "foobar@example.com", "name": "Foo Bar", "id": "123456"}, "team": {"id": "456789"}, "scope": "identity.basic,identity.email", } ) expected_username = "foobar" def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() class SlackOAuth2TestTeamName(SlackOAuth2Test): expected_username = "foobar@Square" user_data_body = json.dumps( { "ok": True, "user": {"email": "foobar@example.com", "name": "Foo Bar", "id": "123456"}, "team": { "id": "456789", "name": "Square", }, "scope": "identity.basic,identity.email,identity.team", } ) class SlackOAuth2TestUnicodeTeamName(SlackOAuth2Test): user_data_body = json.dumps( { "ok": True, "user": {"email": "foobar@example.com", "name": "Foo Bar", "id": "123456"}, "team": { "id": "456789", "name": "Square \u221a team", }, "scope": "identity.basic,identity.email,identity.team", } ) def test_login(self): self.strategy.set_settings({"SOCIAL_AUTH_SLACK_USERNAME_WITH_TEAM": False}) self.do_login() def test_partial_pipeline(self): self.strategy.set_settings({"SOCIAL_AUTH_SLACK_USERNAME_WITH_TEAM": False}) self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_soundcloud.py000066400000000000000000000033571500362547200256630ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class SoundcloudOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.soundcloud.SoundcloudOAuth2" user_data_url = "https://api.soundcloud.com/me.json" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "website": None, "myspace_name": None, "public_favorites_count": 0, "followings_count": 0, "full_name": "Foo Bar", "id": 10101010, "city": None, "track_count": 0, "playlist_count": 0, "discogs_name": None, "private_tracks_count": 0, "followers_count": 0, "online": True, "username": "foobar", "description": None, "subscriptions": [], "kind": "user", "quota": { "unlimited_upload_quota": False, "upload_seconds_left": 7200, "upload_seconds_used": 0, }, "website_title": None, "primary_email_confirmed": False, "permalink_url": "http://soundcloud.com/foobar", "private_playlists_count": 0, "permalink": "foobar", "upload_seconds_left": 7200, "country": None, "uri": "https://api.soundcloud.com/users/10101010", "avatar_url": "https://a1.sndcdn.com/images/" "default_avatar_large.png?ca77017", "plan": "Free", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_spotify.py000066400000000000000000000016051500362547200251730ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class SpotifyOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.spotify.SpotifyOAuth2" user_data_url = "https://api.spotify.com/v1/me" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "display_name": None, "external_urls": {"spotify": "https://open.spotify.com/user/foobar"}, "followers": {"href": None, "total": 0}, "href": "https://api.spotify.com/v1/users/foobar", "id": "foobar", "images": [], "type": "user", "uri": "spotify:user:foobar", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_stackoverflow.py000066400000000000000000000034501500362547200263670ustar00rootroot00000000000000import json from urllib.parse import urlencode from .oauth import BaseAuthUrlTestMixin, OAuth2Test class StackoverflowOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.stackoverflow.StackoverflowOAuth2" user_data_url = "https://api.stackexchange.com/2.1/me" expected_username = "foobar" access_token_body = urlencode({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "items": [ { "user_id": 101010, "user_type": "registered", "creation_date": 1278525551, "display_name": "foobar", "profile_image": "http: //www.gravatar.com/avatar/" "5280f15cedf540b544eecc30fcf3027c?" "d=identicon&r=PG", "reputation": 547, "reputation_change_day": 0, "reputation_change_week": 0, "reputation_change_month": 0, "reputation_change_quarter": 65, "reputation_change_year": 65, "age": 22, "last_access_date": 1363544705, "last_modified_date": 1354035327, "is_employee": False, "link": "http: //stackoverflow.com/users/101010/foobar", "location": "Fooland", "account_id": 101010, "badge_counts": {"gold": 0, "silver": 3, "bronze": 6}, } ], "quota_remaining": 9997, "quota_max": 10000, "has_more": False, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_steam.py000066400000000000000000000153711500362547200246140ustar00rootroot00000000000000import datetime import json from urllib.parse import urlencode import pytest import responses from ...exceptions import AuthFailed from .open_id import OpenIdTest INFO_URL = "http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?" JANRAIN_NONCE = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") class SteamOpenIdTest(OpenIdTest): backend_path = "social_core.backends.steam.SteamOpenId" expected_username = "foobar" discovery_body = """ http://specs.openid.net/auth/2.0/server https://steamcommunity.com/openid/login """ user_discovery_body = """ http://specs.openid.net/auth/2.0/signon https://steamcommunity.com/openid/login """ server_response = urlencode( { "janrain_nonce": JANRAIN_NONCE, "openid.ns": "http://specs.openid.net/auth/2.0", "openid.mode": "id_res", "openid.op_endpoint": "https://steamcommunity.com/openid/login", "openid.claimed_id": "https://steamcommunity.com/openid/id/123", "openid.identity": "https://steamcommunity.com/openid/id/123", "openid.return_to": "http://myapp.com/complete/steam/?" "janrain_nonce=" + JANRAIN_NONCE, "openid.response_nonce": JANRAIN_NONCE + "oD4UZ3w9chOAiQXk0AqDipqFYRA=", "openid.assoc_handle": "1234567890", "openid.signed": "signed,op_endpoint,claimed_id,identity,return_to," "response_nonce,assoc_handle", "openid.sig": "1az53vj9SVdiBwhk8%2BFQ68R2plo=", } ) player_details = json.dumps( { "response": { "players": [ { "steamid": "123", "primaryclanid": "1234", "timecreated": 1360768416, "personaname": "foobar", "personastate": 0, "communityvisibilitystate": 3, "profileurl": "http://steamcommunity.com/profiles/123/", "avatar": "http://media.steampowered.com/steamcommunity/" "public/images/avatars/fe/fef49e7fa7e1997310d7" "05b2a6158ff8dc1cdfeb.jpg", "avatarfull": "http://media.steampowered.com/steamcommunity/" "public/images/avatars/fe/fef49e7fa7e1997310d7" "05b2a6158ff8dc1cdfeb_full.jpg", "avatarmedium": "http://media.steampowered.com/steamcommunity/" "public/images/avatars/fe/fef49e7fa7e1997310d7" "05b2a6158ff8dc1cdfeb_medium.jpg", "lastlogoff": 1360790014, } ] } } ) def _login_setup(self, user_url=None): self.strategy.set_settings({"SOCIAL_AUTH_STEAM_API_KEY": "123abc"}) responses.add( responses.POST, "https://steamcommunity.com/openid/login", status=200, body=self.server_response, ) responses.add( responses.GET, user_url or "https://steamcommunity.com/openid/id/123", status=200, body=self.user_discovery_body, ) responses.add(responses.GET, INFO_URL, status=200, body=self.player_details) @pytest.mark.xfail(reason="responses mocking does not work for openid") def test_login(self): self._login_setup() self.do_login() @pytest.mark.xfail(reason="responses mocking does not work for openid") def test_partial_pipeline(self): self._login_setup() self.do_partial_pipeline() class SteamOpenIdMissingSteamIdTest(SteamOpenIdTest): server_response = urlencode( { "janrain_nonce": JANRAIN_NONCE, "openid.ns": "http://specs.openid.net/auth/2.0", "openid.mode": "id_res", "openid.op_endpoint": "https://steamcommunity.com/openid/login", "openid.claimed_id": "https://steamcommunity.com/openid/BROKEN", "openid.identity": "https://steamcommunity.com/openid/BROKEN", "openid.return_to": "http://myapp.com/complete/steam/?" "janrain_nonce=" + JANRAIN_NONCE, "openid.response_nonce": JANRAIN_NONCE + "oD4UZ3w9chOAiQXk0AqDipqFYRA=", "openid.assoc_handle": "1234567890", "openid.signed": "signed,op_endpoint,claimed_id,identity,return_to," "response_nonce,assoc_handle", "openid.sig": "1az53vj9SVdiBwhk8%2BFQ68R2plo=", } ) def test_login(self): self._login_setup(user_url="https://steamcommunity.com/openid/BROKEN") with self.assertRaises(AuthFailed): self.do_login() def test_partial_pipeline(self): self._login_setup(user_url="https://steamcommunity.com/openid/BROKEN") with self.assertRaises(AuthFailed): self.do_partial_pipeline() class SteamOpenIdFakeSteamIdTest(SteamOpenIdTest): server_response = urlencode( { "janrain_nonce": JANRAIN_NONCE, "openid.ns": "http://specs.openid.net/auth/2.0", "openid.mode": "id_res", "openid.op_endpoint": "https://steamcommunity.com/openid/login", "openid.claimed_id": "https://fakesteamcommunity.com/openid/123", "openid.identity": "https://fakesteamcommunity.com/openid/123", "openid.return_to": "http://myapp.com/complete/steam/?" "janrain_nonce=" + JANRAIN_NONCE, "openid.response_nonce": JANRAIN_NONCE + "oD4UZ3w9chOAiQXk0AqDipqFYRA=", "openid.assoc_handle": "1234567890", "openid.signed": "signed,op_endpoint,claimed_id,identity,return_to," "response_nonce,assoc_handle", "openid.sig": "1az53vj9SVdiBwhk8%2BFQ68R2plo=", } ) @pytest.mark.xfail(reason="responses mocking does not work for openid") def test_login(self): self._login_setup(user_url="https://fakesteamcommunity.com/openid/123") with self.assertRaises(AuthFailed): self.do_login() @pytest.mark.xfail(reason="responses mocking does not work for openid") def test_partial_pipeline(self): self._login_setup(user_url="https://fakesteamcommunity.com/openid/123") with self.assertRaises(AuthFailed): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_stocktwits.py000066400000000000000000000032341500362547200257140ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class StocktwitsOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.stocktwits.StocktwitsOAuth2" user_data_url = "https://api.stocktwits.com/api/2/account/verify.json" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "response": {"status": 200}, "user": { "username": "foobar", "name": "Foo Bar", "classification": [], "avatar_url": "http://avatars.stocktwits.net/images/" "default_avatar_thumb.jpg", "avatar_url_ssl": "https://s3.amazonaws.com/st-avatars/images/" "default_avatar_thumb.jpg", "id": 101010, "identity": "User", }, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() class StocktwitsOAuth2UsernameAlternativeTest(StocktwitsOAuth2Test): user_data_body = json.dumps( { "response": {"status": 200}, "user": { "username": "foobar", "name": "Foobar", "classification": [], "avatar_url": "http://avatars.stocktwits.net/images/" "default_avatar_thumb.jpg", "avatar_url_ssl": "https://s3.amazonaws.com/st-avatars/images/" "default_avatar_thumb.jpg", "id": 101010, "identity": "User", }, } ) social-auth-core-4.6.1/social_core/tests/backends/test_strava.py000066400000000000000000000053131500362547200247760ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class StravaOAuthTest(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.strava.StravaOAuth" user_data_url = "https://www.strava.com/api/v3/athlete" expected_username = "marianne_v" access_token_body = json.dumps( { "token_type": "Bearer", "expires_at": 1572805000, "expires_in": 227615, "refresh_token": "f51defab4632d27255dd0d106504dfd7568fd1df6", "access_token": "83ebeabdec09f6670863766f792ead24d61fe3f9", "athlete": { "id": 1234567890987654321, "username": "marianne_v", "resource_state": 2, "firstname": "Marianne", "lastname": "V.", "city": "Francisco", "state": "California", "country": "United States", "sex": "F", "premium": "true", "summit": "true", "created_at": "2017-11-14T02:30:05Z", "updated_at": "2018-02-06T19:32:20Z", "badge_type_id": 4, "profile_medium": "https://xxxxxx.cloudfront.net/pictures/athletes/123456789/123456789/2/medium.jpg", "profile": "https://xxxxx.cloudfront.net/pictures/athletes/123456789/123456789/2/large.jpg", "friend": "null", "follower": "null", }, } ) user_data_body = json.dumps( { "id": 1234567890987654321, "username": "marianne_v", "resource_state": 3, "firstname": "Marianne", "lastname": "V.", "city": "San Francisco", "state": "CA", "country": "US", "sex": "F", "premium": "true", "created_at": "2017-11-14T02:30:05Z", "updated_at": "2018-02-06T19:32:20Z", "badge_type_id": 4, "profile_medium": "https://xxxxxx.cloudfront.net/pictures/athletes/123456789/123456789/2/medium.jpg", "profile": "https://xxxxx.cloudfront.net/pictures/athletes/123456789/123456789/2/large.jpg", "friend": "null", "follower": "null", "follower_count": 5, "friend_count": 5, "mutual_friend_count": 0, "athlete_type": 1, "date_preference": "%m/%d/%Y", "measurement_preference": "feet", "clubs": [], "ftp": "null", "weight": 0, "bikes": [], "shoes": [], } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_stripe.py000066400000000000000000000030631500362547200250040ustar00rootroot00000000000000# pyright: reportAttributeAccessIssue=false import json import requests import responses from .oauth import BaseAuthUrlTestMixin, OAuth2Test class StripeOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.stripe.StripeOAuth2" account_data_url = "https://api.stripe.com/v1/account" access_token_body = json.dumps( { "stripe_publishable_key": "pk_test_foobar", "access_token": "foobar", "livemode": False, "token_type": "bearer", "scope": "read_only", "refresh_token": "rt_foobar", "stripe_user_id": "acct_foobar", } ) expected_username = "acct_foobar" user_data_body = json.dumps( { "id": "acct_1LUYJiECsxMRIeT8", "object": "account", "country": "FR", "created": 1659974194, "default_currency": "eur", "details_submitted": True, "email": "foobar@yahoo.com", "type": "express", } ) def setUp(self): super().setUp() responses.add( responses.GET, self.account_data_url, status=200, body=self.user_data_body ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() def test_get_user_details(self): response = requests.get(self.account_data_url, timeout=1) user_details = self.backend.get_user_details(response.json()) self.assertEqual(user_details["email"], "foobar@yahoo.com") social-auth-core-4.6.1/social_core/tests/backends/test_taobao.py000066400000000000000000000014651500362547200247470ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class TaobaoOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.taobao.TAOBAOAuth" user_data_url = "https://eco.taobao.com/router/rest" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "w2_expires_in": 0, "taobao_user_id": "1", "taobao_user_nick": "foobar", "w1_expires_in": 1800, "re_expires_in": 0, "r2_expires_in": 0, "expires_in": 86400, "r1_expires_in": 1800, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_thisismyjam.py000066400000000000000000000015401500362547200260350ustar00rootroot00000000000000import json from urllib.parse import urlencode from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test class ThisIsMyJameOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.thisismyjam.ThisIsMyJamOAuth1" user_data_url = "http://api.thisismyjam.com/1/verify.json" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", "oauth_callback_confirmed": "true", } ) user_data_body = json.dumps( {"id": 10101010, "person": {"name": "foobar", "fullname": "Foo Bar"}} ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_tripit.py000066400000000000000000000066021500362547200250130ustar00rootroot00000000000000import json from urllib.parse import urlencode from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test class TripitOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.tripit.TripItOAuth" user_data_url = "https://api.tripit.com/v1/get/profile" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", "oauth_callback_confirmed": "true", } ) user_data_content_type = "text/xml" user_data_body = ( "" "1363590451" "1040" '' "" "" "
foobar@gmail.com
" "false" "true" "true" "" "true" "" "
" "
" "true" "false" "foobar" "Foo Bar" "people/foobar" "Foo, Barland" "" "https://www.tripit.com/feed/activities/private/" "ignore-this/activities.atom" "" "" "https://www.tripit.com/feed/alerts/private/" "ignore-this/alerts.atom" "" "" "webcal://www.tripit.com/feed/ical/private/" "ignore-this/tripit.ics" "" "
" "
" ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() class TripitOAuth1UsernameAlternativesTest(TripitOAuth1Test): user_data_body = ( "" "1363590451" "1040" '' "" "" "
foobar@gmail.com
" "false" "true" "true" "" "true" "" "
" "
" "true" "false" "foobar" "Foobar" "people/foobar" "Foo, Barland" "" "https://www.tripit.com/feed/activities/private/" "ignore-this/activities.atom" "" "" "https://www.tripit.com/feed/alerts/private/" "ignore-this/alerts.atom" "" "" "webcal://www.tripit.com/feed/ical/private/" "ignore-this/tripit.ics" "" "
" "
" ) social-auth-core-4.6.1/social_core/tests/backends/test_tumblr.py000066400000000000000000000037341500362547200250100ustar00rootroot00000000000000import json from urllib.parse import urlencode from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test class TumblrOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.tumblr.TumblrOAuth" user_data_url = "http://api.tumblr.com/v2/user/info" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", "oauth_callback_confirmed": "true", } ) user_data_body = json.dumps( { "meta": {"status": 200, "msg": "OK"}, "response": { "user": { "following": 1, "blogs": [ { "updated": 0, "description": "", "drafts": 0, "title": "Untitled", "url": "http://foobar.tumblr.com/", "messages": 0, "tweet": "N", "share_likes": True, "posts": 0, "primary": True, "queue": 0, "admin": True, "followers": 0, "ask": False, "facebook": "N", "type": "public", "facebook_opengraph_enabled": "N", "name": "foobar", } ], "default_post_format": "html", "name": "foobar", "likes": 0, } }, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_twitch.py000066400000000000000000000055641500362547200250100ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test from .open_id_connect import OpenIdConnectTest class TwitchOpenIdConnectTest(OpenIdConnectTest): backend_path = "social_core.backends.twitch.TwitchOpenIdConnect" user_data_url = "https://id.twitch.tv/oauth2/userinfo" issuer = "https://id.twitch.tv/oauth2" expected_username = "test_user1" openid_config_body = json.dumps( { "authorization_endpoint": "https://id.twitch.tv/oauth2/authorize", "claims_parameter_supported": True, "claims_supported": [ "iss", "azp", "preferred_username", "updated_at", "aud", "exp", "iat", "picture", "sub", "email", "email_verified", ], "id_token_signing_alg_values_supported": [ "RS256", ], "issuer": "https://id.twitch.tv/oauth2", "jwks_uri": "https://id.twitch.tv/oauth2/keys", "response_types_supported": [ "id_token", "code", "token", "code id_token", "token id_token", ], "scopes_supported": [ "openid", ], "subject_types_supported": [ "public", ], "token_endpoint": "https://id.twitch.tv/oauth2/token", "token_endpoint_auth_methods_supported": [ "client_secret_post", ], "userinfo_endpoint": "https://id.twitch.tv/oauth2/userinfo", } ) class TwitchOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.twitch.TwitchOAuth2" user_data_url = "https://api.twitch.tv/helix/users" expected_username = "test_user1" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", } ) user_data_body = json.dumps( { "data": [ { "id": "689563726", "login": "test_user1", "display_name": "test_user1", "type": "", "broadcaster_type": "", "description": "", "profile_image_url": "https://static-cdn.jtvnw.net/jtv_user_pictures/foo.png", "offline_image_url": "", "view_count": 0, "email": "example@reply.com", "created_at": "2021-05-21T18:59:25Z", "access_token": "hmkgz15x7j54jm63rpwfwhcnue6t4fxwv", } ] } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_twitter.py000066400000000000000000000231401500362547200251760ustar00rootroot00000000000000import json from urllib.parse import urlencode from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test class TwitterOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.twitter.TwitterOAuth" user_data_url = "https://api.twitter.com/1.1/account/verify_credentials.json" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", "oauth_callback_confirmed": "true", } ) user_data_body = json.dumps( { "follow_request_sent": False, "profile_use_background_image": True, "id": 10101010, "description": "Foo bar baz qux", "verified": False, "entities": {"description": {"urls": []}}, "profile_image_url_https": "https://twimg0-a.akamaihd.net/" "profile_images/532018826/" "n587119531_1939735_9305_normal.jpg", "profile_sidebar_fill_color": "252429", "profile_text_color": "666666", "followers_count": 77, "profile_sidebar_border_color": "181A1E", "location": "Fooland", "default_profile_image": False, "listed_count": 4, "status": { "favorited": False, "contributors": None, "retweeted_status": { "favorited": False, "contributors": None, "truncated": False, "source": "web", "text": '"Foo foo foo foo', "created_at": "Fri Dec 21 18:12:00 +0000 2012", "retweeted": True, "in_reply_to_status_id": None, "coordinates": None, "id": 101010101010101010, "entities": {"user_mentions": [], "hashtags": [], "urls": []}, "in_reply_to_status_id_str": None, "place": None, "id_str": "101010101010101010", "in_reply_to_screen_name": None, "retweet_count": 8, "geo": None, "in_reply_to_user_id_str": None, "in_reply_to_user_id": None, }, "truncated": False, "source": "web", "text": 'RT @foo: "Foo foo foo foo', "created_at": "Fri Dec 21 18:24:10 +0000 2012", "retweeted": True, "in_reply_to_status_id": None, "coordinates": None, "id": 101010101010101010, "entities": { "user_mentions": [ { "indices": [3, 10], "id": 10101010, "screen_name": "foo", "id_str": "10101010", "name": "Foo", } ], "hashtags": [], "urls": [], }, "in_reply_to_status_id_str": None, "place": None, "id_str": "101010101010101010", "in_reply_to_screen_name": None, "retweet_count": 8, "geo": None, "in_reply_to_user_id_str": None, "in_reply_to_user_id": None, }, "utc_offset": -10800, "statuses_count": 191, "profile_background_color": "1A1B1F", "friends_count": 151, "profile_background_image_url_https": "https://twimg0-a.akamaihd.net/" "images/themes/theme9/bg.gif", "profile_link_color": "2FC2EF", "profile_image_url": "http://a0.twimg.com/profile_images/532018826/" "n587119531_1939735_9305_normal.jpg", "is_translator": False, "geo_enabled": False, "id_str": "74313638", "profile_background_image_url": "http://a0.twimg.com/images/themes/" "theme9/bg.gif", "screen_name": "foobar", "lang": "en", "profile_background_tile": False, "favourites_count": 2, "name": "Foo", "notifications": False, "url": None, "created_at": "Tue Sep 15 00:26:17 +0000 2009", "contributors_enabled": False, "time_zone": "Buenos Aires", "protected": False, "default_profile": False, "following": False, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() class TwitterOAuth1IncludeEmailTest(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.twitter.TwitterOAuth" user_data_url = ( "https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true" ) expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", "oauth_callback_confirmed": "true", } ) user_data_body = json.dumps( { "follow_request_sent": False, "profile_use_background_image": True, "id": 10101010, "description": "Foo bar baz qux", "verified": False, "entities": {"description": {"urls": []}}, "profile_image_url_https": "https://twimg0-a.akamaihd.net/" "profile_images/532018826/" "n587119531_1939735_9305_normal.jpg", "profile_sidebar_fill_color": "252429", "profile_text_color": "666666", "followers_count": 77, "profile_sidebar_border_color": "181A1E", "location": "Fooland", "default_profile_image": False, "listed_count": 4, "status": { "favorited": False, "contributors": None, "retweeted_status": { "favorited": False, "contributors": None, "truncated": False, "source": "web", "text": '"Foo foo foo foo', "created_at": "Fri Dec 21 18:12:00 +0000 2012", "retweeted": True, "in_reply_to_status_id": None, "coordinates": None, "id": 101010101010101010, "entities": {"user_mentions": [], "hashtags": [], "urls": []}, "in_reply_to_status_id_str": None, "place": None, "id_str": "101010101010101010", "in_reply_to_screen_name": None, "retweet_count": 8, "geo": None, "in_reply_to_user_id_str": None, "in_reply_to_user_id": None, }, "truncated": False, "source": "web", "text": 'RT @foo: "Foo foo foo foo', "created_at": "Fri Dec 21 18:24:10 +0000 2012", "retweeted": True, "in_reply_to_status_id": None, "coordinates": None, "id": 101010101010101010, "entities": { "user_mentions": [ { "indices": [3, 10], "id": 10101010, "screen_name": "foo", "id_str": "10101010", "name": "Foo", } ], "hashtags": [], "urls": [], }, "in_reply_to_status_id_str": None, "place": None, "id_str": "101010101010101010", "in_reply_to_screen_name": None, "retweet_count": 8, "geo": None, "in_reply_to_user_id_str": None, "in_reply_to_user_id": None, }, "utc_offset": -10800, "statuses_count": 191, "profile_background_color": "1A1B1F", "friends_count": 151, "profile_background_image_url_https": "https://twimg0-a.akamaihd.net/" "images/themes/theme9/bg.gif", "profile_link_color": "2FC2EF", "profile_image_url": "http://a0.twimg.com/profile_images/532018826/" "n587119531_1939735_9305_normal.jpg", "is_translator": False, "geo_enabled": False, "id_str": "74313638", "profile_background_image_url": "http://a0.twimg.com/images/themes/" "theme9/bg.gif", "screen_name": "foobar", "lang": "en", "profile_background_tile": False, "favourites_count": 2, "name": "Foo", "notifications": False, "url": None, "created_at": "Tue Sep 15 00:26:17 +0000 2009", "contributors_enabled": False, "time_zone": "Buenos Aires", "protected": False, "default_profile": False, "following": False, "email": "foo@bar.bas", } ) def test_login(self): user = self.do_login() self.assertEqual(user.email, "foo@bar.bas") def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_twitter_oauth2.py000066400000000000000000000173161500362547200264700ustar00rootroot00000000000000# pyright: reportAttributeAccessIssue=false import json from social_core.exceptions import AuthException from .oauth import ( BaseAuthUrlTestMixin, OAuth2PkcePlainTest, OAuth2PkceS256Test, OAuth2Test, ) class TwitterOAuth2Mixin: backend_path = "social_core.backends.twitter_oauth2.TwitterOAuth2" user_data_url = "https://api.twitter.com/2/users/me" access_token_body = json.dumps( { "token_type": "bearer", "expires_in": 7200, "access_token": "foobar", "scope": "users.read", } ) user_data_body = json.dumps( { "data": { "id": "1234567890123456789", "username": "twitter_username", "name": "first last", "created_at": "2023-03-06T06:18:59.000Z", "public_metrics": { "followers_count": 69, "following_count": 129, "tweet_count": 40, "listed_count": 7, }, "profile_image_url": "https://social-core-test-url.com/image.png", "verified_type": "none", "pinned_tweet_id": "9876543210987654321", "url": "https://social-core-test-url.com", "verified": False, "protected": True, "description": "description str", "entities": { "url": { "urls": [ { "start": 0, "end": 23, "url": "entities-url-urls-url", "expanded_url": "entities-url-urls-expanded_url", "display_url": "entities-url-urls-display_url", } ] }, "description": { "urls": [ { "start": 133, "end": 156, "url": "entities-description-urls-url", "expanded_url": "entities-description-urls-expanded_url", "display_url": "entities-description-urls-display_url", } ], "hashtags": [ { "start": 36, "end": 44, "tag": "entities-description-hashtags-tag", } ], }, }, }, }, ) expected_username = "twitter_username" def test_login(self): user = self.do_login() self.assertEqual(len(user.social), 1) social = user.social[0] self.assertEqual(social.uid, "1234567890123456789") self.assertEqual(social.extra_data["first_name"], "first") self.assertEqual(social.extra_data["last_name"], "last") self.assertEqual(social.extra_data["fullname"], "first last") self.assertEqual(social.extra_data["created_at"], "2023-03-06T06:18:59.000Z") self.assertEqual(social.extra_data["verified"], False) self.assertEqual(social.extra_data["verified_type"], "none") self.assertEqual(social.extra_data["protected"], True) self.assertEqual(social.extra_data["description"], "description str") self.assertEqual(social.extra_data["url"], "https://social-core-test-url.com") self.assertEqual(social.extra_data["pinned_tweet_id"], "9876543210987654321") self.assertEqual( social.extra_data["profile_image_url"], "https://social-core-test-url.com/image.png", ) self.assertEqual(social.extra_data["public_metrics"]["followers_count"], 69) self.assertEqual(social.extra_data["public_metrics"]["following_count"], 129) self.assertEqual(social.extra_data["public_metrics"]["tweet_count"], 40) self.assertEqual(social.extra_data["public_metrics"]["listed_count"], 7) def test_partial_pipeline(self): user = self.do_partial_pipeline() self.assertEqual(len(user.social), 1) social = user.social[0] self.assertEqual(social.uid, "1234567890123456789") self.assertEqual(social.extra_data["first_name"], "first") self.assertEqual(social.extra_data["last_name"], "last") self.assertEqual(social.extra_data["fullname"], "first last") self.assertEqual(social.extra_data["created_at"], "2023-03-06T06:18:59.000Z") self.assertEqual(social.extra_data["verified"], False) self.assertEqual(social.extra_data["verified_type"], "none") self.assertEqual(social.extra_data["protected"], True) self.assertEqual(social.extra_data["description"], "description str") self.assertEqual(social.extra_data["url"], "https://social-core-test-url.com") self.assertEqual(social.extra_data["pinned_tweet_id"], "9876543210987654321") self.assertEqual( social.extra_data["profile_image_url"], "https://social-core-test-url.com/image.png", ) self.assertEqual(social.extra_data["public_metrics"]["followers_count"], 69) self.assertEqual(social.extra_data["public_metrics"]["following_count"], 129) self.assertEqual(social.extra_data["public_metrics"]["tweet_count"], 40) self.assertEqual(social.extra_data["public_metrics"]["listed_count"], 7) class TwitterOAuth2TestMissingOptionalValue(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.twitter_oauth2.TwitterOAuth2" user_data_url = "https://api.twitter.com/2/users/me" access_token_body = json.dumps( { "token_type": "bearer", "expires_in": 7200, "access_token": "foobar", "scope": "users.read", } ) user_data_body = json.dumps( { "data": { "id": "1234567890123456789", "username": "twitter_username", "name": "first last", }, }, ) expected_username = "twitter_username" def test_login(self): user = self.do_login() self.assertEqual(len(user.social), 1) social = user.social[0] self.assertEqual(social.uid, "1234567890123456789") self.assertEqual(social.extra_data["first_name"], "first") self.assertEqual(social.extra_data["last_name"], "last") self.assertEqual(social.extra_data["fullname"], "first last") self.assertIsNone(social.extra_data.get("created_at")) self.assertIsNone(social.extra_data.get("verified")) self.assertIsNone(social.extra_data.get("verified_type")) self.assertIsNone(social.extra_data.get("protected")) self.assertIsNone(social.extra_data.get("description")) self.assertIsNone(social.extra_data.get("url")) self.assertIsNone(social.extra_data.get("pinned_tweet_id")) self.assertIsNone(social.extra_data.get("profile_image_url")) self.assertIsNone(social.extra_data.get("public_metrics")) class TwitterOAuth2TestPkcePlain(TwitterOAuth2Mixin, OAuth2PkcePlainTest): pass class TwitterOAuth2TestPkceS256(TwitterOAuth2Mixin, OAuth2PkceS256Test): pass class TwitterOAuth2TestInvalidCodeChallengeMethod( TwitterOAuth2Mixin, OAuth2PkcePlainTest ): def test_login__error(self): self.strategy.set_settings( { f"SOCIAL_AUTH_{self.name}_PKCE_CODE_CHALLENGE_METHOD": "invalidmethodname", } ) with self.assertRaises(AuthException): self.do_login() social-auth-core-4.6.1/social_core/tests/backends/test_uber.py000066400000000000000000000017401500362547200244330ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class UberOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): user_data_url = "https://api.uber.com/v1/me" backend_path = "social_core.backends.uber.UberOAuth2" expected_username = "foo@bar.com" user_data_body = json.dumps( { "first_name": "Foo", "last_name": "Bar", "email": "foo@bar.com", "picture": "https://", "promo_code": "barfoo", "uuid": "91d81273-45c2-4b57-8124-d0165f8240c0", } ) access_token_body = json.dumps( { "access_token": "EE1IDxytP04tJ767GbjH7ED9PpGmYvL", "token_type": "Bearer", "expires_in": 2592000, "refresh_token": "Zx8fJ8qdSRRseIVlsGgtgQ4wnZBehr", "scope": "profile history request", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_udata.py000066400000000000000000000015741500362547200246010ustar00rootroot00000000000000import json from urllib.parse import urlencode from .oauth import BaseAuthUrlTestMixin, OAuth2Test class DatagouvfrOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.udata.DatagouvfrOAuth2" user_data_url = "https://www.data.gouv.fr/api/1/me/" expected_username = "foobar" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", "first_name": "foobar", "email": "foobar@example.com", } ) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", "oauth_callback_confirmed": "true", } ) user_data_body = json.dumps({}) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_universe.py000066400000000000000000000016561500362547200253440ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class UniverseAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.universe.UniverseOAuth2" user_data_url = "https://www.universe.com/api/v2/current_user" expected_username = "scott+awesome@universe.com" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "current_user": { "id": "123456", "slug": "foo-bar", "first_name": "Scott", "last_name": "Vitale", "created_at": "2019-01-08T15:49:42.514Z", "updated_at": "2019-01-17T19:41:39.711Z", "email": "scott+awesome@universe.com", } } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_upwork.py000066400000000000000000000031261500362547200250250ustar00rootroot00000000000000import json from urllib.parse import urlencode from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test class UpworkOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.upwork.UpworkOAuth" user_data_url = "https://www.upwork.com/api/auth/v1/info.json" expected_username = "10101010" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", "oauth_callback_confirmed": "true", } ) user_data_body = json.dumps( { "info": { "portrait_32_img": "", "capacity": { "buyer": "no", "affiliate_manager": "no", "provider": "yes", }, "company_url": "", "has_agency": "1", "portrait_50_img": "", "portrait_100_img": "", "location": {"city": "New York", "state": "", "country": "USA"}, "ref": "9755314", "profile_url": "https://www.upwork.com/users/~10101010", }, "auth_user": { "timezone": "USA/New York", "first_name": "Foo", "last_name": "Bar", "timezone_offset": "10000", }, "server_time": "1111111111", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_username.py000066400000000000000000000007411500362547200253150ustar00rootroot00000000000000from .legacy import BaseLegacyTest class UsernameTest(BaseLegacyTest): backend_path = "social_core.backends.username.UsernameAuth" expected_username = "foobar" response_body = "username=foobar" form = """
""" def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_utils.py000066400000000000000000000035201500362547200246340ustar00rootroot00000000000000import unittest from ...backends.github import GithubOAuth2 from ...backends.utils import get_backend, load_backends from ...exceptions import MissingBackend from ..models import TestStorage from ..strategy import TestStrategy class BaseBackendUtilsTest(unittest.TestCase): def setUp(self): self.strategy = TestStrategy(storage=TestStorage) def tearDown(self): self.strategy = None class LoadBackendsTest(BaseBackendUtilsTest): def test_load_backends(self): loaded_backends = load_backends( ( "social_core.backends.github.GithubOAuth2", "social_core.backends.facebook.FacebookOAuth2", "social_core.backends.flickr.FlickrOAuth", ), force_load=True, ) keys = list(loaded_backends.keys()) self.assertEqual(keys, ["github", "facebook", "flickr"]) backends = () loaded_backends = load_backends(backends, force_load=True) self.assertEqual(len(list(loaded_backends.keys())), 0) class GetBackendTest(BaseBackendUtilsTest): def test_get_backend(self): backend = get_backend( ( "social_core.backends.github.GithubOAuth2", "social_core.backends.facebook.FacebookOAuth2", "social_core.backends.flickr.FlickrOAuth", ), "github", ) self.assertEqual(backend, GithubOAuth2) def test_get_missing_backend(self): with self.assertRaisesRegex(MissingBackend, 'Missing backend "foobar" entry'): get_backend( ( "social_core.backends.github.GithubOAuth2", "social_core.backends.facebook.FacebookOAuth2", "social_core.backends.flickr.FlickrOAuth", ), "foobar", ) social-auth-core-4.6.1/social_core/tests/backends/test_vault.py000066400000000000000000000033421500362547200246310ustar00rootroot00000000000000import json import responses from .oauth import BaseAuthUrlTestMixin from .open_id_connect import OpenIdConnectTest ROOT_URL = "https://vault.example.net:8200/" class VaultOpenIdConnectTest(OpenIdConnectTest, BaseAuthUrlTestMixin): backend_path = "social_core.backends.vault.VaultOpenIdConnect" issuer = f"{ROOT_URL}v1/identity/oidc/provider/default" openid_config_body = json.dumps( { "issuer": f"{ROOT_URL}v1/identity/oidc/provider/default", "jwks_uri": f"{ROOT_URL}v1/identity/oidc/provider/default/.well-known/keys", "authorization_endpoint": f"{ROOT_URL}ui/vault/identity/oidc/provider/default/authorize", "token_endpoint": f"{ROOT_URL}v1/identity/oidc/provider/default/token", "userinfo_endpoint": f"{ROOT_URL}v1/identity/oidc/provider/default/userinfo", "request_uri_parameter_supported": False, "grant_types_supported": ["authorization_code"], "token_endpoint_auth_methods_supported": ["client_secret_basic"], } ) expected_username = "cartman" def extra_settings(self): settings = super().extra_settings() settings.update( { f"SOCIAL_AUTH_{self.name}_OIDC_ENDPOINT": f"{ROOT_URL}v1/identity/oidc/provider/default", } ) return settings def pre_complete_callback(self, start_url): super().pre_complete_callback(start_url) responses.add( responses.GET, url=self.backend.userinfo_url(), status=200, body=json.dumps({"preferred_username": self.expected_username}), content_type="text/json", ) def test_everything_works(self): self.do_login() social-auth-core-4.6.1/social_core/tests/backends/test_vk.py000066400000000000000000000016121500362547200241140ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class VKOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.vk.VKOAuth2" user_data_url = "https://api.vk.com/method/users.get" expected_username = "durov" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "response": [ { "uid": "1", "first_name": "Павел", "last_name": "Дуров", "screen_name": "durov", "nickname": "", "photo": r"http:\/\/cs7003.vk.me\/v7003815\/22a1\/xgG9fb-IJ3Y.jpg", } ] } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_wlcg.py000066400000000000000000000014031500362547200244260ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class WLCGOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.wlcg.WLCGOAuth2" user_data_url = "https://wlcg.cloud.cnaf.infn.it/userinfo" expected_username = "foo@bar.com" access_token_body = json.dumps( { "access_token": "foobar", "token_type": "bearer", } ) user_data_body = json.dumps( { "email": "foo@bar.com", "family_name": "Bar", "given_name": "Foo", "name": "Foo Bar", "email_verified": True, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_wunderlist.py000066400000000000000000000015021500362547200256720ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class WunderlistOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.wunderlist.WunderlistOAuth2" user_data_url = "https://a.wunderlist.com/api/v1/user" expected_username = "12345" access_token_body = json.dumps( {"access_token": "foobar-token", "token_type": "foobar"} ) user_data_body = json.dumps( { "created_at": "2015-01-21T00:56:51.442Z", "email": "foo@bar.com", "id": 12345, "name": "foobar", "revision": 1, "type": "user", "updated_at": "2015-01-21T00:56:51.442Z", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_xing.py000066400000000000000000000170531500362547200244470ustar00rootroot00000000000000import json from urllib.parse import urlencode from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test class XingOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.xing.XingOAuth" user_data_url = "https://api.xing.com/v1/users/me.json" expected_username = "FooBar" access_token_body = urlencode( { "access_token": "foobar", "token_type": "bearer", "user_id": "123456_abcdef", "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", } ) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", "oauth_callback_confirmed": "true", } ) user_data_body = json.dumps( { "users": [ { "id": "123456_abcdef", "first_name": "Foo", "last_name": "Bar", "display_name": "Foo Bar", "page_name": "Foo_Bar", "permalink": "https://www.xing.com/profile/Foo_Bar", "gender": "m", "birth_date": {"day": 12, "month": 8, "year": 1963}, "active_email": "foo@bar.com", "time_zone": {"name": "Europe/Copenhagen", "utc_offset": 2.0}, "premium_services": ["SEARCH", "PRIVATEMESSAGES"], "badges": ["PREMIUM", "MODERATOR"], "wants": "Nothing", "haves": "Skills", "interests": "Foo Foo", "organisation_member": "ACM, GI", "languages": { "de": "NATIVE", "en": "FLUENT", "fr": None, "zh": "BASIC", }, "private_address": { "city": "Foo", "country": "DE", "zip_code": "20357", "street": "Bar", "phone": "12|34|1234560", "fax": "||", "province": "Foo", "email": "foo@bar.com", "mobile_phone": "12|3456|1234567", }, "business_address": { "city": "Foo", "country": "DE", "zip_code": "20357", "street": "Bar", "phone": "12|34|1234569", "fax": "12|34|1234561", "province": "Foo", "email": "foo@bar.com", "mobile_phone": "12|345|12345678", }, "web_profiles": { "qype": ["http://qype.de/users/foo"], "google_plus": ["http://plus.google.com/foo"], "blog": ["http://blog.example.org"], "homepage": ["http://example.org", "http://other-example.org"], }, "instant_messaging_accounts": { "skype": "foobar", "googletalk": "foobar", }, "professional_experience": { "primary_company": { "name": "XING AG", "title": "Softwareentwickler", "company_size": "201-500", "tag": None, "url": "http://www.xing.com", "career_level": "PROFESSIONAL_EXPERIENCED", "begin_date": "2010-01", "description": None, "end_date": None, "industry": "AEROSPACE", }, "non_primary_companies": [ { "name": "Ninja Ltd.", "title": "DevOps", "company_size": None, "tag": "NINJA", "url": "http://www.ninja-ltd.co.uk", "career_level": None, "begin_date": "2009-04", "description": None, "end_date": "2010-07", "industry": "ALTERNATIVE_MEDICINE", }, { "name": None, "title": "Wiss. Mitarbeiter", "company_size": None, "tag": "OFFIS", "url": "http://www.uni.de", "career_level": None, "begin_date": "2007", "description": None, "end_date": "2008", "industry": "APPAREL_AND_FASHION", }, { "name": None, "title": "TEST NINJA", "company_size": "201-500", "tag": "TESTCOMPANY", "url": None, "career_level": "ENTRY_LEVEL", "begin_date": "1998-12", "description": None, "end_date": "1999-05", "industry": "ARTS_AND_CRAFTS", }, ], "awards": [ { "name": "Awesome Dude Of The Year", "date_awarded": 2007, "url": None, } ], }, "educational_background": { "schools": [ { "name": "Foo University", "degree": "MSc CE/CS", "notes": None, "subject": None, "begin_date": "1998-08", "end_date": "2005-02", } ], "qualifications": ["TOEFLS", "PADI AOWD"], }, "photo_urls": { "large": "http://www.xing.com/img/users/e/3/d/" "f94ef165a.123456,1.140x185.jpg", "mini_thumb": "http://www.xing.com/img/users/e/3/d/" "f94ef165a.123456,1.18x24.jpg", "thumb": "http://www.xing.com/img/users/e/3/d/" "f94ef165a.123456,1.30x40.jpg", "medium_thumb": "http://www.xing.com/img/users/e/3/d/" "f94ef165a.123456,1.57x75.jpg", "maxi_thumb": "http://www.xing.com/img/users/e/3/d/" "f94ef165a.123456,1.70x93.jpg", }, } ] } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_yahoo.py000066400000000000000000000055001500362547200246130ustar00rootroot00000000000000# pyright: reportAttributeAccessIssue=false import json from urllib.parse import urlencode import requests import responses from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test class YahooOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.yahoo.YahooOAuth" user_data_url = "https://social.yahooapis.com/v1/user/a-guid/profile?format=json" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", "oauth_callback_confirmed": "true", } ) guid_body = json.dumps( {"guid": {"uri": "https://social.yahooapis.com/v1/me/guid", "value": "a-guid"}} ) user_data_body = json.dumps( { "profile": { "bdRestricted": True, "memberSince": "2007-12-11T14:40:30Z", "image": { "width": 192, "imageUrl": "http://l.yimg.com/dh/ap/social/profile/" "profile_b192.png", "size": "192x192", "height": 192, }, "created": "2013-03-18T04:15:08Z", "uri": "https://social.yahooapis.com/v1/user/a-guid/profile", "isConnected": False, "profileUrl": "http://profile.yahoo.com/a-guid", "guid": "a-guid", "nickname": "foobar", "emails": [ { "handle": "foobar@yahoo.com", "id": 1, "primary": True, "type": "HOME", }, { "handle": "foobar@email.com", "id": 2, "type": "HOME", }, ], } } ) def test_login(self): responses.add( responses.GET, "https://social.yahooapis.com/v1/me/guid?format=json", status=200, body=self.guid_body, ) self.do_login() def test_partial_pipeline(self): responses.add( responses.GET, "https://social.yahooapis.com/v1/me/guid?format=json", status=200, body=self.guid_body, ) self.do_partial_pipeline() def test_get_user_details(self): responses.add( responses.GET, self.user_data_url, status=200, body=self.user_data_body ) response = requests.get(self.user_data_url, timeout=1) user_details = self.backend.get_user_details(response.json()["profile"]) self.assertEqual(user_details["email"], "foobar@yahoo.com") social-auth-core-4.6.1/social_core/tests/backends/test_yammer.py000066400000000000000000000100771500362547200247730ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class YammerOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.yammer.YammerOAuth2" expected_username = "foobar" access_token_body = json.dumps( { "access_token": { "user_id": 1010101010, "view_groups": True, "modify_messages": True, "network_id": 101010, "created_at": "2013/03/17 16:39:56 +0000", "view_members": True, "authorized_at": "2013/03/17 16:39:56 +0000", "view_subscriptions": True, "view_messages": True, "modify_subscriptions": True, "token": "foobar", "expires_at": None, "network_permalink": "foobar.com", "view_tags": True, "network_name": "foobar.com", }, "user": { "last_name": "Bar", "web_url": "https://www.yammer.com/foobar/users/foobar", "expertise": None, "full_name": "Foo Bar", "timezone": "Pacific Time (US & Canada)", "mugshot_url": "https://mug0.assets-yammer.com/mugshot/images/" "48x48/no_photo.png", "guid": None, "network_name": "foobar", "id": 1010101010, "previous_companies": [], "first_name": "Foo", "stats": {"following": 0, "followers": 0, "updates": 1}, "hire_date": None, "state": "active", "location": None, "department": "Software Development", "type": "user", "show_ask_for_photo": True, "job_title": "Software Developer", "interests": None, "kids_names": None, "activated_at": "2013/03/17 16:27:50 +0000", "verified_admin": "false", "can_broadcast": "false", "schools": [], "admin": "false", "network_domains": ["foobar.com"], "name": "foobar", "external_urls": [], "url": "https://www.yammer.com/api/v1/users/1010101010", "settings": {"xdr_proxy": "https://xdrproxy.yammer.com"}, "summary": None, "network_id": 101010, "contact": { "phone_numbers": [], "im": {"username": "", "provider": ""}, "email_addresses": [{"type": "primary", "address": "foo@bar.com"}], "has_fake_email": False, }, "birth_date": "", "mugshot_url_template": "https://mug0.assets-yammer.com/mugshot/" "images/{width}x{height}/no_photo.png", "significant_other": None, }, "network": { "show_upgrade_banner": False, "header_text_color": "#FFFFFF", "is_org_chart_enabled": True, "name": "foobar.com", "is_group_enabled": True, "header_background_color": "#396B9A", "created_at": "2012/12/26 16:52:35 +0000", "profile_fields_config": { "enable_work_phone": True, "enable_mobile_phone": True, "enable_job_title": True, }, "permalink": "foobar.com", "paid": False, "id": 101010, "is_chat_enabled": True, "web_url": "https://www.yammer.com/foobar.com", "moderated": False, "community": False, "type": "network", "navigation_background_color": "#38699F", "navigation_text_color": "#FFFFFF", }, } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_yandex.py000066400000000000000000000030551500362547200247670ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class YandexOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.yandex.YandexOAuth2" user_data_url = "https://login.yandex.ru/info" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "display_name": "foobar", "real_name": "Foo Bar", "sex": None, "id": "101010101", "default_email": "foobar@yandex.com", "emails": ["foobar@yandex.com"], } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() class YandexOAuth2TestEmptyEmail(OAuth2Test, BaseAuthUrlTestMixin): """ When user log in to yandex service with social network account (e.g. vk.com), they `default_email` could be empty. """ backend_path = "social_core.backends.yandex.YandexOAuth2" user_data_url = "https://login.yandex.ru/info" expected_username = "foobar" access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"}) user_data_body = json.dumps( { "display_name": "foobar", "real_name": "Foo Bar", "sex": None, "id": "101010101", "default_email": "", "emails": [], } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/backends/test_zoom.py000066400000000000000000000043071500362547200244640ustar00rootroot00000000000000import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test class ZoomOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.zoom.ZoomOAuth2" user_data_url = "https://api.zoom.us/v2/users/me" expected_username = "foobar" access_token_body = json.dumps( { "access_token": "foobar-token", "token_type": "bearer", "refresh_token": "foobar-refresh-token", "expires_in": 3599, "scope": "identity", } ) user_data_body = json.dumps( { "id": "foobar", "first_name": "Foo", "last_name": "Bar", "email": "foobar@email.com", "type": 2, "role_name": "Foobar", "pmi": 1234567890, "use_pmi": False, "vanity_url": "https://foobar.zoom.us/my/foobar", "personal_meeting_url": "https://foobar.zoom.us/j/1234567890", "timezone": "America/Denver", "verified": 1, "dept": "", "created_at": "2019-04-05T15:24:32Z", "last_login_time": "2019-12-16T18:02:48Z", "last_client_version": "version", "pic_url": "https://foobar.zoom.us/p/123456789", "host_key": "123456", "jid": "foobar@xmpp.zoom.us", "group_ids": [], "im_group_ids": ["foobar-group-id"], "account_id": "foobar-account-id", "language": "en-US", "phone_country": "US", "phone_number": "+1 1234567891", "status": "active", } ) refresh_token_body = json.dumps( { "access_token": "foobar-new-token", "token_type": "bearer", "refresh_token": "foobar-new-refresh-token", "expires_in": 3599, "scope": "identity", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() def test_refresh_token(self): user, social = self.do_refresh_token() self.assertEqual(user.username, self.expected_username) self.assertEqual(social.extra_data["access_token"], "foobar-new-token") social-auth-core-4.6.1/social_core/tests/backends/test_zotero.py000066400000000000000000000014411500362547200250160ustar00rootroot00000000000000from urllib.parse import urlencode from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test class ZoteroOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.zotero.ZoteroOAuth" expected_username = "FooBar" access_token_body = urlencode( { "oauth_token": "foobar", "oauth_token_secret": "rodgsNDK4hLJU1504Atk131G", "userID": "123456_abcdef", "username": "FooBar", } ) request_token_body = urlencode( { "oauth_token_secret": "foobar-secret", "oauth_token": "foobar", "oauth_callback_confirmed": "true", } ) def test_login(self): self.do_login() def test_partial_pipeline(self): self.do_partial_pipeline() social-auth-core-4.6.1/social_core/tests/models.py000066400000000000000000000152141500362547200221510ustar00rootroot00000000000000# pyright: reportAttributeAccessIssue=false from __future__ import annotations import base64 from typing import TypeVar from typing_extensions import Self from ..storage import ( AssociationMixin, BaseStorage, CodeMixin, NonceMixin, PartialMixin, UserMixin, ) ModelT = TypeVar("ModelT", bound="BaseModel") class BaseModel: @classmethod def next_id(cls): cls.NEXT_ID += 1 return cls.NEXT_ID - 1 @classmethod def get(cls, key) -> Self | None: return cls.cache.get(key) @classmethod def reset_cache(cls): cls.cache = {} class User(BaseModel): NEXT_ID = 1 cache = {} _is_active = True def __init__(self, username, email=None, **extra_user_fields): self.id = User.next_id() self.username = username self.email = email self.first_name = None self.password = None self.slug = None self.social = [] self.extra_data = {} self.extra_user_fields = extra_user_fields self.save() def is_active(self): return self._is_active @classmethod def set_active(cls, is_active=True): cls._is_active = is_active def set_password(self, password): self.password = password def save(self): User.cache[self.username] = self class TestUserSocialAuth(UserMixin, BaseModel): __test__ = False NEXT_ID = 1 cache = {} cache_by_uid = {} def __init__(self, user, provider, uid, extra_data=None): self.id = TestUserSocialAuth.next_id() self.user = user self.provider = provider self.uid = uid self.extra_data = extra_data or {} self.user.social.append(self) TestUserSocialAuth.cache_by_uid[uid] = self def save(self): pass @classmethod def reset_cache(cls): cls.cache = {} cls.cache_by_uid = {} @classmethod def changed(cls, user): pass @classmethod def get_username(cls, user): return user.username @classmethod def user_model(cls): return User @classmethod def username_max_length(cls): return 1024 @classmethod def allowed_to_disconnect(cls, user, backend_name, association_id=None): return user.password or len(user.social) > 1 @classmethod def disconnect(cls, entry): cls.cache.pop(entry.id, None) entry.user.social = [s for s in entry.user.social if entry != s] @classmethod def user_exists(cls, username): return User.cache.get(username) is not None @classmethod def create_user(cls, username, email=None, **extra_user_fields): return User(username=username, email=email, **extra_user_fields) @classmethod def get_user(cls, pk): for user in User.cache.values(): if user.id == pk: return user return None @classmethod def get_social_auth(cls, provider, uid): social_user = cls.cache_by_uid.get(uid) if social_user and social_user.provider == provider: return social_user return None @classmethod def get_social_auth_for_user(cls, user, provider=None, id=None): return [ usa for usa in user.social if provider in (None, usa.provider) and id in (None, usa.id) ] @classmethod def create_social_auth(cls, user, uid, provider): return cls(user=user, provider=provider, uid=uid) @classmethod def get_users_by_email(cls, email): return [user for user in User.cache.values() if user.email == email] class TestNonce(NonceMixin, BaseModel): __test__ = False NEXT_ID = 1 cache = {} def __init__(self, server_url, timestamp, salt): self.id = TestNonce.next_id() self.server_url = server_url self.timestamp = timestamp self.salt = salt @classmethod def use(cls, server_url, timestamp, salt): nonce = TestNonce(server_url, timestamp, salt) TestNonce.cache[server_url] = nonce return nonce @classmethod def get( # type: ignore[override] cls, server_url, salt ): return TestNonce.cache[server_url] @classmethod def delete(cls, nonce): server_url = nonce.server_url del TestNonce.cache[server_url] class TestAssociation(AssociationMixin, BaseModel): __test__ = False NEXT_ID = 1 cache = {} def __init__(self, server_url, handle): self.id = TestAssociation.next_id() self.server_url = server_url self.handle = handle def save(self): TestAssociation.cache[(self.server_url, self.handle)] = self @classmethod def store(cls, server_url, association): assoc = TestAssociation.cache.get((server_url, association.handle)) if assoc is None: assoc = TestAssociation(server_url=server_url, handle=association.handle) assoc.secret = base64.encodebytes(association.secret) assoc.issued = association.issued assoc.lifetime = association.lifetime assoc.assoc_type = association.assoc_type assoc.save() @classmethod def get( # type: ignore[override] cls: type[TestAssociation], server_url: str | None = None, handle: str | None = None, ) -> list[AssociationMixin]: result = [] for assoc in TestAssociation.cache.values(): if server_url and assoc.server_url != server_url: continue if handle and assoc.handle != handle: continue result.append(assoc) return result @classmethod def remove(cls, ids_to_delete): assoc = filter(lambda a: a.id in ids_to_delete, TestAssociation.cache.values()) for a in list(assoc): TestAssociation.cache.pop((a.server_url, a.handle), None) class TestCode(CodeMixin, BaseModel): __test__ = False NEXT_ID = 1 cache = {} @classmethod def get_code(cls, code): for c in cls.cache.values(): if c.code == code: return c return None class TestPartial(PartialMixin, BaseModel): __test__ = False NEXT_ID = 1 cache = {} def save(self): TestPartial.cache[self.token] = self @classmethod def load(cls, token): return cls.cache.get(token) @classmethod def destroy(cls, token): cls.cache.pop(token) class TestStorage(BaseStorage): __test__ = False user = TestUserSocialAuth nonce = TestNonce association = TestAssociation code = TestCode partial = TestPartial @classmethod def is_integrity_error(cls, exception) -> bool | None: pass social-auth-core-4.6.1/social_core/tests/pipeline.py000066400000000000000000000022361500362547200224730ustar00rootroot00000000000000from ..pipeline.partial import partial @partial def ask_for_password(strategy, *args, **kwargs): if strategy.session_get("password"): return {"password": strategy.session_get("password")} return strategy.redirect(strategy.build_absolute_uri("/password")) @partial def ask_for_slug(strategy, *args, **kwargs): if strategy.session_get("slug"): return {"slug": strategy.session_get("slug")} return strategy.redirect(strategy.build_absolute_uri("/slug")) def set_password(strategy, user, *args, **kwargs): user.set_password(kwargs["password"]) def set_slug(strategy, user, *args, **kwargs): user.slug = kwargs["slug"] def remove_user(strategy, user, *args, **kwargs): return {"user": None} @partial def set_user_from_kwargs(strategy, *args, **kwargs): if strategy.session_get("attribute"): kwargs["user"].id return None return strategy.redirect(strategy.build_absolute_uri("/attribute")) @partial def set_user_from_args(strategy, user, *args, **kwargs): if strategy.session_get("attribute"): user.id return None return strategy.redirect(strategy.build_absolute_uri("/attribute")) social-auth-core-4.6.1/social_core/tests/strategy.py000066400000000000000000000072771500362547200225420ustar00rootroot00000000000000from ..strategy import BaseStrategy, BaseTemplateStrategy TEST_URI = "http://myapp.com" TEST_HOST = "myapp.com" class Redirect: def __init__(self, url): self.url = url class TestTemplateStrategy(BaseTemplateStrategy): __test__ = False def render_template(self, tpl, context): return tpl def render_string(self, html, context): return html class TestStrategy(BaseStrategy): __test__ = False DEFAULT_TEMPLATE_STRATEGY = TestTemplateStrategy def __init__(self, storage, tpl=None): self._request_data = {} self._settings = {} self._session = {} super().__init__(storage, tpl) def redirect(self, url): return Redirect(url) def get_setting(self, name): """Return value for given setting name""" return self._settings[name] def html(self, content): """Return HTTP response with given content""" return content def render_html(self, tpl=None, html=None, context=None): """Render given template or raw html with given context""" return tpl or html def request_data(self, merge=True): """Return current request data (POST or GET)""" return self._request_data def request_host(self): """Return current host value""" return TEST_HOST def request_is_secure(self): """Is the request using HTTPS?""" return False def request_path(self): """path of the current request""" return "" def request_port(self): """Port in use for this request""" return 80 def request_get(self): """Request GET data""" return self._request_data.copy() def request_post(self): """Request POST data""" return self._request_data.copy() def session_get(self, name, default=None): """Return session value for given key""" return self._session.get(name, default) def session_set(self, name, value): """Set session value for given key""" self._session[name] = value def session_pop(self, name): """Pop session value for given key""" return self._session.pop(name, None) def build_absolute_uri(self, path=None): """Build absolute URI with given (optional) path""" path = path or "" if path.startswith(("http://", "https://")): return path return TEST_URI + path def set_settings(self, values): self._settings.update(values) def set_request_data(self, values, backend): self._request_data.update(values) backend.data = self._request_data def remove_from_request_data(self, name): self._request_data.pop(name, None) def authenticate(self, *args, **kwargs): user = super().authenticate(*args, **kwargs) if isinstance(user, self.storage.user.user_model()): self.session_set("username", user.username) return user def get_pipeline(self, backend=None): return self.setting( "PIPELINE", ( "social_core.pipeline.social_auth.social_details", "social_core.pipeline.social_auth.social_uid", "social_core.pipeline.social_auth.auth_allowed", "social_core.pipeline.social_auth.social_user", "social_core.pipeline.user.get_username", "social_core.pipeline.social_auth.associate_by_email", "social_core.pipeline.user.create_user", "social_core.pipeline.social_auth.associate_user", "social_core.pipeline.social_auth.load_extra_data", "social_core.pipeline.user.user_details", ), backend, ) social-auth-core-4.6.1/social_core/tests/test_exceptions.py000066400000000000000000000101441500362547200241030ustar00rootroot00000000000000import unittest from ..backends.base import BaseAuth from ..exceptions import ( AuthAlreadyAssociated, AuthCanceled, AuthException, AuthFailed, AuthForbidden, AuthMissingParameter, AuthStateForbidden, AuthStateMissing, AuthTokenError, AuthTokenRevoked, AuthUnknownError, AuthUnreachableProvider, InvalidEmail, MissingBackend, NotAllowedToDisconnect, SocialAuthBaseException, WrongBackend, ) from .models import TestStorage from .strategy import TestStrategy class BaseExceptionTestCase(unittest.TestCase): exception: SocialAuthBaseException = SocialAuthBaseException("base test") expected_message: str = "base test" def test_exception_message(self): try: raise self.exception except SocialAuthBaseException as err: self.assertEqual(str(err), self.expected_message) class WrongBackendTest(BaseExceptionTestCase): exception = WrongBackend("foobar") expected_message = 'Incorrect authentication service "foobar"' class AuthFailedTest(BaseExceptionTestCase): exception = AuthFailed(BaseAuth(TestStrategy(TestStorage)), "wrong_user") expected_message = "Authentication failed: wrong_user" class AuthFailedDeniedTest(BaseExceptionTestCase): exception = AuthFailed(BaseAuth(TestStrategy(TestStorage)), "access_denied") expected_message = "Authentication process was canceled" class AuthTokenErrorTest(BaseExceptionTestCase): exception = AuthTokenError(BaseAuth(TestStrategy(TestStorage)), "Incorrect tokens") expected_message = "Token error: Incorrect tokens" class AuthMissingParameterTest(BaseExceptionTestCase): exception = AuthMissingParameter(BaseAuth(TestStrategy(TestStorage)), "username") expected_message = "Missing needed parameter username" class AuthStateMissingTest(BaseExceptionTestCase): exception = AuthStateMissing(BaseAuth(TestStrategy(TestStorage))) expected_message = "Session value state missing." class NotAllowedToDisconnectTest(BaseExceptionTestCase): exception = NotAllowedToDisconnect() expected_message = "This account is not allowed to be disconnected." class AuthExceptionTest(BaseExceptionTestCase): exception = AuthException(BaseAuth(TestStrategy(TestStorage)), "message") expected_message = "message" class AuthCanceledTest(BaseExceptionTestCase): exception = AuthCanceled(BaseAuth(TestStrategy(TestStorage))) expected_message = "Authentication process canceled" class AuthCanceledWithExtraMessageTest(BaseExceptionTestCase): exception = AuthCanceled(BaseAuth(TestStrategy(TestStorage)), "error_message") expected_message = "Authentication process canceled: error_message" class AuthUnknownErrorTest(BaseExceptionTestCase): exception = AuthUnknownError(BaseAuth(TestStrategy(TestStorage)), "some error") expected_message = "An unknown error happened while authenticating some error" class AuthStateForbiddenTest(BaseExceptionTestCase): exception = AuthStateForbidden(BaseAuth(TestStrategy(TestStorage))) expected_message = "Wrong state parameter given." class AuthAlreadyAssociatedTest(BaseExceptionTestCase): exception = AuthAlreadyAssociated(BaseAuth(TestStrategy(TestStorage))) expected_message = "This account is already in use." class AuthTokenRevokedTest(BaseExceptionTestCase): exception = AuthTokenRevoked(BaseAuth(TestStrategy(TestStorage))) expected_message = "User revoke access to the token" class AuthForbiddenTest(BaseExceptionTestCase): exception = AuthForbidden(BaseAuth(TestStrategy(TestStorage))) expected_message = "Your credentials aren't allowed" class AuthUnreachableProviderTest(BaseExceptionTestCase): exception = AuthUnreachableProvider(BaseAuth(TestStrategy(TestStorage))) expected_message = "The authentication provider could not be reached" class InvalidEmailTest(BaseExceptionTestCase): exception = InvalidEmail(BaseAuth(TestStrategy(TestStorage))) expected_message = "Email couldn't be validated" class MissingBackendTest(BaseExceptionTestCase): exception = MissingBackend("backend") expected_message = 'Missing backend "backend" entry' social-auth-core-4.6.1/social_core/tests/test_partial.py000066400000000000000000000102021500362547200233510ustar00rootroot00000000000000import unittest from unittest.mock import Mock, patch from ..pipeline.partial import partial, partial_step from ..utils import PARTIAL_TOKEN_SESSION_NAME class PartialDecoratorTestCase(unittest.TestCase): def setUp(self): super().setUp() self.mock_current_partial_token = Mock() self.mock_current_partial = Mock(token=self.mock_current_partial_token) self.mock_strategy = Mock() self.mock_backend = Mock() self.mock_pipeline_index = Mock() self.mock_partial_store = Mock() self.mock_strategy.storage.partial.store = self.mock_partial_store self.mock_session_set = Mock() self.mock_strategy.session_set = self.mock_session_set def test_save_to_session(self): # GIVEN expected_response = Mock() @partial_step(save_to_session=True) def decorated_func(*args, **kwargs): return expected_response # WHEN with patch( "social_core.pipeline.partial.partial_prepare", return_value=self.mock_current_partial, ): response = decorated_func( self.mock_strategy, self.mock_backend, self.mock_pipeline_index ) # THEN self.assertEqual(expected_response, response) self.assertEqual(1, self.mock_partial_store.call_count) self.assertEqual( (self.mock_current_partial,), self.mock_partial_store.call_args[0] ) self.assertEqual(1, self.mock_session_set.call_count) self.assertEqual( (PARTIAL_TOKEN_SESSION_NAME, self.mock_current_partial_token), self.mock_session_set.call_args[0], ) def test_not_to_save_to_session(self): # GIVEN expected_response = Mock() @partial_step(save_to_session=False) def decorated_func(*args, **kwargs): return expected_response # WHEN with patch( "social_core.pipeline.partial.partial_prepare", return_value=self.mock_current_partial, ): response = decorated_func( self.mock_strategy, self.mock_backend, self.mock_pipeline_index ) # THEN self.assertEqual(expected_response, response) self.assertEqual(1, self.mock_partial_store.call_count) self.assertEqual( (self.mock_current_partial,), self.mock_partial_store.call_args[0] ) self.assertEqual(0, self.mock_session_set.call_count) def test_save_to_session_by_backward_compatible_decorator(self): # GIVEN expected_response = Mock() @partial def decorated_func(*args, **kwargs): return expected_response # WHEN with patch( "social_core.pipeline.partial.partial_prepare", return_value=self.mock_current_partial, ): response = decorated_func( self.mock_strategy, self.mock_backend, self.mock_pipeline_index ) # THEN self.assertEqual(expected_response, response) self.assertEqual(1, self.mock_partial_store.call_count) self.assertEqual( (self.mock_current_partial,), self.mock_partial_store.call_args[0] ) self.assertEqual(1, self.mock_session_set.call_count) self.assertEqual( (PARTIAL_TOKEN_SESSION_NAME, self.mock_current_partial_token), self.mock_session_set.call_args[0], ) def test_not_to_save_to_session_when_the_response_is_a_dict(self): # GIVEN expected_response = {"test_key": "test_value"} @partial_step(save_to_session=True) def decorated_func(*args, **kwargs): return expected_response # WHEN response = decorated_func( self.mock_strategy, self.mock_backend, self.mock_pipeline_index ) # THEN self.assertEqual(expected_response, response) self.assertEqual(0, self.mock_partial_store.call_count) self.assertEqual(0, self.mock_session_set.call_count) social-auth-core-4.6.1/social_core/tests/test_pipeline.py000066400000000000000000000236761500362547200235450ustar00rootroot00000000000000import json from ..exceptions import AuthException from ..pipeline.user import user_details from ..utils import PARTIAL_TOKEN_SESSION_NAME from .actions.actions import BaseActionTest from .models import TestStorage, TestUserSocialAuth, User from .strategy import TestStrategy class IntegrityError(Exception): pass class UnknownError(Exception): pass class IntegrityErrorUserSocialAuth(TestUserSocialAuth): @classmethod def create_social_auth(cls, user, uid, provider): raise IntegrityError @classmethod def get_social_auth(cls, provider, uid): if not hasattr(cls, "_called_times"): cls._called_times = 0 cls._called_times += 1 if cls._called_times == 2: user = next(iter(User.cache.values())) return IntegrityErrorUserSocialAuth(user, provider, uid) return super().get_social_auth(provider, uid) class IntegrityErrorStorage(TestStorage): user = IntegrityErrorUserSocialAuth @classmethod def is_integrity_error(cls, exception): """Check if given exception flags an integrity error in the DB""" return isinstance(exception, IntegrityError) class UnknownErrorUserSocialAuth(TestUserSocialAuth): @classmethod def create_social_auth(cls, user, uid, provider): raise UnknownError class UnknownErrorStorage(IntegrityErrorStorage): user = UnknownErrorUserSocialAuth class IntegrityErrorOnLoginTest(BaseActionTest): def setUp(self): self.strategy = TestStrategy(IntegrityErrorStorage) super().setUp() def test_integrity_error(self): self.do_login() class UnknownErrorOnLoginTest(BaseActionTest): def setUp(self): self.strategy = TestStrategy(UnknownErrorStorage) super().setUp() def test_unknown_error(self): with self.assertRaises(UnknownError): self.do_login() class EmailAsUsernameTest(BaseActionTest): expected_username = "foo@bar.com" def test_email_as_username(self): self.strategy.set_settings({"SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL": True}) self.do_login() class RandomUsernameTest(BaseActionTest): user_data_body = json.dumps( { "id": 1, "avatar_url": "https://github.com/images/error/foobar_happy.gif", "gravatar_id": "somehexcode", "url": "https://api.github.com/users/foobar", "name": "monalisa foobar", "company": "GitHub", "blog": "https://github.com/blog", "location": "San Francisco", "email": "foo@bar.com", "hireable": False, "bio": "There once was...", "public_repos": 2, "public_gists": 1, "followers": 20, "following": 0, "html_url": "https://github.com/foobar", "created_at": "2008-01-14T04:33:35Z", "type": "User", "total_private_repos": 100, "owned_private_repos": 100, "private_gists": 81, "disk_usage": 10000, "collaborators": 8, "plan": { "name": "Medium", "space": 400, "collaborators": 10, "private_repos": 20, }, } ) def test_random_username(self): self.do_login(after_complete_checks=False) class SluggedUsernameTest(BaseActionTest): expected_username = "foo-bar" user_data_body = json.dumps( { "login": "Foo Bar", "id": 1, "avatar_url": "https://github.com/images/error/foobar_happy.gif", "gravatar_id": "somehexcode", "url": "https://api.github.com/users/foobar", "name": "monalisa foobar", "company": "GitHub", "blog": "https://github.com/blog", "location": "San Francisco", "email": "foo@bar.com", "hireable": False, "bio": "There once was...", "public_repos": 2, "public_gists": 1, "followers": 20, "following": 0, "html_url": "https://github.com/foobar", "created_at": "2008-01-14T04:33:35Z", "type": "User", "total_private_repos": 100, "owned_private_repos": 100, "private_gists": 81, "disk_usage": 10000, "collaborators": 8, "plan": { "name": "Medium", "space": 400, "collaborators": 10, "private_repos": 20, }, } ) def test_random_username(self): self.strategy.set_settings( { "SOCIAL_AUTH_CLEAN_USERNAMES": False, "SOCIAL_AUTH_SLUGIFY_USERNAMES": True, } ) self.do_login() class RepeatedUsernameTest(BaseActionTest): def test_random_username(self): User(username="foobar") self.do_login(after_complete_checks=False) self.assertTrue(self.strategy.session_get("username").startswith("foobar")) class AssociateByEmailTest(BaseActionTest): def test_multiple_accounts_with_same_email(self): user = User(username="foobar1") user.email = "foo@bar.com" self.do_login(after_complete_checks=False) self.assertTrue(self.strategy.session_get("username").startswith("foobar")) class MultipleAccountsWithSameEmailTest(BaseActionTest): def test_multiple_accounts_with_same_email(self): user1 = User(username="foobar1") user2 = User(username="foobar2") user1.email = "foo@bar.com" user2.email = "foo@bar.com" with self.assertRaises(AuthException): self.do_login(after_complete_checks=False) class UserPersistsInPartialPipeline(BaseActionTest): def test_user_persists_in_partial_pipeline_kwargs(self): user = User(username="foobar1") user.email = "foo@bar.com" self.strategy.set_settings( { "SOCIAL_AUTH_PIPELINE": ( "social_core.pipeline.social_auth.social_details", "social_core.pipeline.social_auth.social_uid", "social_core.pipeline.social_auth.associate_by_email", "social_core.tests.pipeline.set_user_from_kwargs", ) } ) self.do_login(after_complete_checks=False) # Handle the partial pipeline self.strategy.session_set("attribute", "testing") token = self.strategy.session_pop(PARTIAL_TOKEN_SESSION_NAME) partial = self.strategy.partial_load(token) self.backend.continue_pipeline(partial) def test_user_persists_in_partial_pipeline(self): user = User(username="foobar1") user.email = "foo@bar.com" self.strategy.set_settings( { "SOCIAL_AUTH_PIPELINE": ( "social_core.pipeline.social_auth.social_details", "social_core.pipeline.social_auth.social_uid", "social_core.pipeline.social_auth.associate_by_email", "social_core.tests.pipeline.set_user_from_args", ) } ) self.do_login(after_complete_checks=False) # Handle the partial pipeline self.strategy.session_set("attribute", "testing") token = self.strategy.session_pop(PARTIAL_TOKEN_SESSION_NAME) partial = self.strategy.partial_load(token) self.backend.continue_pipeline(partial) class TestUserDetails(BaseActionTest): def test_user_details(self): self.strategy.set_settings({}) details = {"first_name": "Test"} user = User(username="foobar") backend = None user_details(self.strategy, details, backend, user) self.assertEqual(user.first_name, "Test") # Also test mutation details = {"first_name": "Test2"} user_details(self.strategy, details, backend, user) self.assertEqual(user.first_name, "Test2") def test_user_details_(self): self.strategy.set_settings( {"SOCIAL_AUTH_IMMUTABLE_USER_FIELDS": ("first_name",)} ) details = {"first_name": "Test"} user = User(username="foobar") backend = None user_details(self.strategy, details, backend, user) self.assertEqual(user.first_name, "Test") # Also test mutation does not change field details = {"first_name": "Test2"} user_details(self.strategy, details, backend, user) self.assertEqual(user.first_name, "Test") class TestLowerCaseEmailOverride(BaseActionTest): user_data_body = json.dumps( { "login": "foobar", "id": 1, "avatar_url": "https://github.com/images/error/foobar_happy.gif", "gravatar_id": "somehexcode", "url": "https://api.github.com/users/foobar", "name": "monalisa foobar", "company": "GitHub", "blog": "https://github.com/blog", "location": "San Francisco", "email": "Foo@bar.com", "hireable": False, "bio": "There once was...", "public_repos": 2, "public_gists": 1, "followers": 20, "following": 0, "html_url": "https://github.com/foobar", "created_at": "2008-01-14T04:33:35Z", "type": "User", "total_private_repos": 100, "owned_private_repos": 100, "private_gists": 81, "disk_usage": 10000, "collaborators": 8, "plan": { "name": "Medium", "space": 400, "collaborators": 10, "private_repos": 20, }, } ) def test_lowercase_email(self): self.strategy.set_settings( { "SOCIAL_AUTH_FORCE_EMAIL_LOWERCASE": True, } ) self.do_login(after_complete_checks=False) self.assertEqual(self.strategy.session_get("email"), "foo@bar.com") social-auth-core-4.6.1/social_core/tests/test_storage.py000066400000000000000000000141021500362547200233640ustar00rootroot00000000000000import random import unittest from ..storage import AssociationMixin, BaseStorage, CodeMixin, NonceMixin, UserMixin from ..strategy import BaseStrategy from .models import User NOT_IMPLEMENTED_MSG = "Implement in subclass" class BrokenUser(UserMixin): pass class BrokenAssociation(AssociationMixin): pass class BrokenNonce(NonceMixin): pass class BrokenCode(CodeMixin): pass class BrokenStrategy(BaseStrategy): pass class BrokenStrategyWithSettings(BrokenStrategy): def get_setting(self, name): raise AttributeError class BrokenStorage(BaseStorage): pass class BrokenUserTests(unittest.TestCase): def setUp(self): self.user = BrokenUser def tearDown(self): self.user = None def test_get_username(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.user.get_username(User("foobar")) def test_user_model(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.user.user_model() def test_username_max_length(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.user.username_max_length() def test_get_user(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.user.get_user(1) def test_get_social_auth(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.user.get_social_auth("foo", 1) def test_get_social_auth_for_user(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.user.get_social_auth_for_user(User("foobar")) def test_create_social_auth(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.user.create_social_auth(User("foobar"), 1, "foo") def test_disconnect(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.user.disconnect(BrokenUser()) class BrokenAssociationTests(unittest.TestCase): def setUp(self): self.association = BrokenAssociation def tearDown(self): self.association = None def test_store(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.association.store("http://foobar.com", BrokenAssociation()) def test_get(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.association.get() def test_remove(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.association.remove([1, 2, 3]) class BrokenNonceTests(unittest.TestCase): def setUp(self): self.nonce = BrokenNonce def tearDown(self): self.nonce = None def test_use(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.nonce.use("http://foobar.com", 1364951922, "foobar123") class BrokenCodeTest(unittest.TestCase): def setUp(self): self.code = BrokenCode def tearDown(self): self.code = None def test_get_code(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.code.get_code("foobar") class BrokenStrategyTests(unittest.TestCase): def setUp(self): self.strategy = BrokenStrategy(storage=BrokenStorage) def tearDown(self): self.strategy = None def test_redirect(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.strategy.redirect("http://foobar.com") def test_get_setting(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.strategy.get_setting("foobar") def test_html(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.strategy.html("

foobar

") def test_request_data(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.strategy.request_data() def test_request_host(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.strategy.request_host() def test_session_get(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.strategy.session_get("foobar") def test_session_set(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.strategy.session_set("foobar", 123) def test_session_pop(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.strategy.session_pop("foobar") def test_build_absolute_uri(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.strategy.build_absolute_uri("/foobar") def test_render_html_with_tpl(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.strategy.render_html("foobar.html", context={}) def test_render_html_with_html(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.strategy.render_html(html="

foobar

", context={}) def test_render_html_with_none(self): with self.assertRaisesRegex(ValueError, "Missing template or html parameters"): self.strategy.render_html() def test_is_integrity_error(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): self.strategy.storage.is_integrity_error(None) def test_random_string(self): self.assertTrue(isinstance(self.strategy.random_string(), str)) def test_random_string_without_systemrandom(self): def SystemRandom(): raise NotImplementedError orig_random = getattr(random, "SystemRandom", None) random.SystemRandom = SystemRandom strategy = BrokenStrategyWithSettings(storage=BrokenStorage) self.assertTrue(isinstance(strategy.random_string(), str)) random.SystemRandom = orig_random social-auth-core-4.6.1/social_core/tests/test_utils.py000066400000000000000000000145511500362547200230700ustar00rootroot00000000000000import unittest from unittest.mock import Mock from ..utils import ( build_absolute_uri, partial_pipeline_data, sanitize_redirect, slugify, user_is_active, user_is_authenticated, ) from .models import TestPartial class SanitizeRedirectTest(unittest.TestCase): def test_none_redirect(self): self.assertEqual(sanitize_redirect(["myapp.com"], None), None) def test_empty_redirect(self): self.assertEqual(sanitize_redirect(["myapp.com"], ""), None) def test_dict_redirect(self): self.assertEqual(sanitize_redirect(["myapp.com"], {}), None) def test_invalid_redirect(self): self.assertEqual(sanitize_redirect(["myapp.com"], {"foo": "bar"}), None) def test_wrong_path_redirect(self): self.assertEqual( sanitize_redirect(["myapp.com"], "http://notmyapp.com/path/"), None ) def test_invalid_evil_redirect(self): self.assertEqual(sanitize_redirect(["myapp.com"], "///evil.com"), None) def test_valid_absolute_redirect(self): self.assertEqual( sanitize_redirect(["myapp.com"], "http://myapp.com/path/"), "http://myapp.com/path/", ) def test_valid_relative_redirect(self): self.assertEqual(sanitize_redirect(["myapp.com"], "/path/"), "/path/") def test_multiple_hosts(self): allowed_hosts = ["myapp1.com", "myapp2.com"] for host in allowed_hosts: url = f"http://{host}/path/" self.assertEqual(sanitize_redirect(allowed_hosts, url), url) def test_multiple_hosts_wrong_host(self): self.assertEqual( sanitize_redirect( ["myapp1.com", "myapp2.com"], "http://notmyapp.com/path/" ), None, ) class UserIsAuthenticatedTest(unittest.TestCase): def test_user_is_none(self): self.assertEqual(user_is_authenticated(None), False) def test_user_is_not_none(self): self.assertEqual(user_is_authenticated(object()), True) def test_user_has_is_authenticated(self): class User: is_authenticated = True self.assertEqual(user_is_authenticated(User()), True) def test_user_has_is_authenticated_callable(self): class User: def is_authenticated(self): return True self.assertEqual(user_is_authenticated(User()), True) class UserIsActiveTest(unittest.TestCase): def test_user_is_none(self): self.assertEqual(user_is_active(None), False) def test_user_is_not_none(self): self.assertEqual(user_is_active(object()), True) def test_user_has_is_active(self): class User: is_active = True self.assertEqual(user_is_active(User()), True) def test_user_has_is_active_callable(self): class User: def is_active(self): return True self.assertEqual(user_is_active(User()), True) class SlugifyTest(unittest.TestCase): def test_slugify_formats(self): self.assertEqual(slugify("FooBar"), "foobar") self.assertEqual(slugify("Foo Bar"), "foo-bar") self.assertEqual(slugify("Foo (Bar)"), "foo-bar") class BuildAbsoluteURITest(unittest.TestCase): def setUp(self): self.host = "http://foobar.com" def tearDown(self): self.host = None def test_path_none(self): self.assertEqual(build_absolute_uri(self.host), self.host) def test_path_empty(self): self.assertEqual(build_absolute_uri(self.host, ""), self.host) def test_path_http(self): self.assertEqual( build_absolute_uri(self.host, "http://barfoo.com"), "http://barfoo.com" ) def test_path_https(self): self.assertEqual( build_absolute_uri(self.host, "https://barfoo.com"), "https://barfoo.com" ) def test_host_ends_with_slash_and_path_starts_with_slash(self): assert self.host, "Subclasses must set the host attribute" self.assertEqual( build_absolute_uri(self.host + "/", "/foo/bar"), "http://foobar.com/foo/bar" ) def test_absolute_uri(self): self.assertEqual( build_absolute_uri(self.host, "/foo/bar"), "http://foobar.com/foo/bar" ) class PartialPipelineData(unittest.TestCase): def test_returns_partial_when_uid_and_email_do_match(self): email = "foo@example.com" backend = self._backend({"uid": email}) backend.strategy.request_data.return_value = {backend.ID_KEY: email} key, val = ("foo", "bar") partial = partial_pipeline_data(backend, None, *(), **{key: val}) self.assertTrue(key in partial.kwargs) self.assertEqual(partial.kwargs[key], val) self.assertEqual(backend.strategy.clean_partial_pipeline.call_count, 0) def test_clean_pipeline_when_uid_does_not_match(self): backend = self._backend({"uid": "foo@example.com"}) backend.strategy.request_data.return_value = {backend.ID_KEY: "bar@example.com"} key, val = ("foo", "bar") partial = partial_pipeline_data(backend, None, *(), **{key: val}) self.assertIsNone(partial) self.assertEqual(backend.strategy.clean_partial_pipeline.call_count, 1) def test_kwargs_included_in_result(self): backend = self._backend() key, val = ("foo", "bar") partial = partial_pipeline_data(backend, None, *(), **{key: val}) self.assertTrue(key in partial.kwargs) self.assertEqual(partial.kwargs[key], val) self.assertEqual(backend.strategy.clean_partial_pipeline.call_count, 0) def test_update_user(self): user = object() backend = self._backend(session_kwargs={"user": None}) partial = partial_pipeline_data(backend, user) self.assertTrue("user" in partial.kwargs) self.assertEqual(partial.kwargs["user"], user) self.assertEqual(backend.strategy.clean_partial_pipeline.call_count, 0) def _backend(self, session_kwargs=None): backend = Mock() backend.ID_KEY = "email" backend.name = "mock-backend" strategy = Mock() strategy.request = None strategy.request_data.return_value = {} strategy.session_get.return_value = object() strategy.partial_load.return_value = TestPartial.prepare( backend.name, 0, {"args": [], "kwargs": session_kwargs or {}} ) backend.strategy = strategy return backend social-auth-core-4.6.1/social_core/tests/testkey.pem000066400000000000000000000032171500362547200225070ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEAtavb41y83n7xY/sIS23Rv+TEwI85xmML57BT9dyb9JeAFPYG RIaN/kWYW8pR0CZDu2nLwU2lUA0d0wKTOMaHQFs6L/rPN1zOx13HYO+TKybgU+e8 TaOncKmJeyp3MP/OM6KBYp7Cx6eLlyvw3HJh6nT+Y5vj3rwS4PYjX1hUcr51IOcq Ls5Sx/UQ4OUVHJLlcUxP6k2n44nGoDYK8zRc5dEBxbTL4yKzCR9unWAlDHVQYXOx vzxxFtUID7dy38e0MZ+3RnmIY2wl7/PIRCsbIKrsAiONnLkJQitMVkU8jTEPrXNC Lj5Hzgf0kDD+4ukQdUPCXlv9y668anaxabXFIwIDAQABAoIBAQCUAfT+UiKpnoBp dS53zXvSO740Jzvpre4YD2cH6njAl+FIsnu+vmTWoLqVJcrrqxEJkhLnZKvnARio PfPVkKDGRMyuJEdIEnuAn519ExIyWNTwZt7Z3hffSLByQNGkgPPsy1tgDnogERc/ zRPJdgLh8fkDDBFk+JLk5oNA/YSnibV5MtriNPoDONxaHsguJ7JvikgLdQs5nyuQ loMx4JhqP+fXsGIU0DFwaLPvEEntKodaRZXvAMLOuGi3O83OGkhevVvEVFSzCeS1 fYnKwySQgNctztoBpf09zATeL9xt8tipVnyBKOIBJyf/BQV7fESXp0R8d+rW1cs0 21qakP0pAoGBAOUFPT/pLP3w676dGvggK0kDy8gjgDHl2/x3fhbeua/6yfetVpZE CuFez/9E24kMS4/GCi3WgPA6DHDYBH5t1S4e0/cgGJ8sjuX8ELLgy4q+WmvgTwDS FbXKqY929Hlu65hUvl2xVP2MYUGLHiUE8yKk4RKdZO+3GkWXHhq9A+mvAoGBAMsS rBYyAt9XeI8W8w2dWgZnBp+7WQeMUAZfCpq65urycD9on9R3gJ5zbOfq6CG2FXML kslY1R4CVYge+HoWITFECvwPdbDYx/QIVvuhRH8omrG5iYY+ljGuF6wlzETgW117 vYTSf5xXJ4LiuFD6TY4rxZ0UdjKba9bJfsQb45zNAoGBAMm/zM4xVn46LyLS+YAd rqP6oRklFPhf+mQ0y7HP8Royvprea0milUcmI+tHOHJJj4MPPcZVkW6OZIk/8u2B SewAIlAwSSBnu1akr/00hHor6DHh/xbE+3UTHD4S15jl+stN1Alrf2iAsuhvalXI P3uEbfXdC58U2SL21gFOA1VnAoGBAIv93EoUwewrhb3GyVlaXyRIw8U7xv0wGj66 KDpQnD3dUco0mvJCS5Vv5uTeCJasyo6brN5G8ewVGdeT2iF3vfwTdysakTyPxRAS 67veCbVLTZE1YXv9C1dGN6WCDRZyQCnq0tSMtFIXtvJAz2VrmClpPXqzD2SFxkq0 b0JMI5YFAoGAL+1E+JUfPHf69K8XDXY+zrZL/KRPuXHcGk4qm7JXCDxr90zMVJYj RWTQZHgGCEZGGlChJD8JwGiWEtsn0zlhn4H4dPWzeS7D1fsJK30lhYpDp3TuOY0x lQAatQXzunglAxXwlOOazX4uoVtiFLjY3U4G6gCwKYEtjP1r0GULlk0= -----END RSA PRIVATE KEY----- social-auth-core-4.6.1/social_core/utils.py000066400000000000000000000216361500362547200206710ustar00rootroot00000000000000from __future__ import annotations import functools import hmac import logging import re import sys import time import unicodedata from typing import Any from urllib.parse import parse_qs as battery_parse_qs from urllib.parse import unquote, urlencode, urlparse, urlunparse import requests import social_core from .exceptions import AuthCanceled, AuthForbidden, AuthUnreachableProvider SETTING_PREFIX = "SOCIAL_AUTH" PARTIAL_TOKEN_SESSION_NAME = "partial_pipeline_token" social_logger = logging.getLogger("social") def import_module(name): __import__(name) return sys.modules[name] def module_member(name): mod, member = name.rsplit(".", 1) module = import_module(mod) return getattr(module, member) def user_agent(): """Builds a simple User-Agent string to send in requests""" return "social-auth-" + social_core.__version__ def url_add_parameters( url: str, params: dict[str, str] | None, _unquote_query: bool = False ) -> str: """Adds parameters to URL, parameter will be repeated if already present""" if params: fragments = list(urlparse(url)) value = parse_qs(fragments[4]) value.update(params) fragments[4] = urlencode(value) if _unquote_query: fragments[4] = unquote(fragments[4]) url = urlunparse(fragments) return url def to_setting_name(*names: str) -> str: return "_".join([name.upper().replace("-", "_") for name in names if name]) def setting_name(*names: str) -> str: return to_setting_name(*((SETTING_PREFIX, *names))) def sanitize_redirect(hosts, redirect_to): """ Given a list of hostnames and an untrusted URL to redirect to, this method tests it to make sure it isn't garbage/harmful and returns it, else returns None, similar as how's it done on django.contrib.auth.views. """ # Avoid redirect on evil URLs like ///evil.com if ( not redirect_to or not hasattr(redirect_to, "startswith") or redirect_to.startswith("///") ): return None try: # Don't redirect to a host that's not in the list netloc = urlparse(redirect_to)[1] or hosts[0] except (TypeError, AttributeError): pass else: if netloc in hosts: return redirect_to def user_is_authenticated(user): if user and hasattr(user, "is_authenticated"): if callable(user.is_authenticated): authenticated = user.is_authenticated() else: authenticated = user.is_authenticated elif user: authenticated = True else: authenticated = False return authenticated def user_is_active(user): if user and hasattr(user, "is_active"): is_active = user.is_active() if callable(user.is_active) else user.is_active elif user: is_active = True else: is_active = False return is_active # This slugify version was borrowed from django revision a61dbd6 def slugify(value): """Converts to lowercase, removes non-word characters (alphanumerics and underscores) and converts spaces to hyphens. Also strips leading and trailing whitespace.""" value = ( unicodedata.normalize("NFKD", str(value)) .encode("ascii", "ignore") .decode("ascii") ) value = re.sub(r"[^\w\s-]", "", value).strip().lower() return re.sub(r"[-\s]+", "-", value) def first(func, items): """Return the first item in the list for what func returns True""" for item in items: if func(item): return item return None def parse_qs(value): """Like urlparse.parse_qs but transform list values to single items""" return drop_lists(battery_parse_qs(value)) def get_querystring(url: str): return parse_qs(urlparse(url).query) def drop_lists(value): out = {} for key, val in value.items(): val = val[0] if isinstance(key, bytes): key = str(key, "utf-8") if isinstance(val, bytes): val = str(val, "utf-8") out[key] = val return out def partial_pipeline_data(backend, user=None, partial_token=None, *args, **kwargs): request_data = backend.strategy.request_data() partial_argument_name = backend.setting( "PARTIAL_PIPELINE_TOKEN_NAME", "partial_token" ) partial_token = ( partial_token or request_data.get(partial_argument_name) or backend.strategy.session_get(PARTIAL_TOKEN_SESSION_NAME, None) ) if partial_token: partial = backend.strategy.partial_load(partial_token) partial_matches_request = False if partial and partial.backend == backend.name: partial_matches_request = True # Normally when resuming a pipeline, request_data will be empty. We # only need to check for a uid match if new data was provided (i.e. # if current request specifies the ID_KEY). if backend.ID_KEY and backend.ID_KEY in request_data: id_from_partial = partial.kwargs.get("uid") id_from_request = request_data.get(backend.ID_KEY) if id_from_partial != id_from_request: partial_matches_request = False if partial_matches_request: if user: # don't update user if it's None kwargs.setdefault("user", user) kwargs.setdefault("request", request_data) partial.extend_kwargs(kwargs) return partial backend.strategy.clean_partial_pipeline(partial_token) return None return None def build_absolute_uri(host_url, path=None): """Build absolute URI with given (optional) path""" path = path or "" if path.startswith(("http://", "https://")): return path if host_url.endswith("/") and path.startswith("/"): path = path[1:] return host_url + path def constant_time_compare(val1, val2): """Compare two values and prevent timing attacks for cryptographic use.""" if isinstance(val1, str): val1 = val1.encode("utf-8") if isinstance(val2, str): val2 = val2.encode("utf-8") return hmac.compare_digest(val1, val2) def is_url(value): return value and (value.startswith(("http://", "https://", "/"))) def setting_url(backend, *names): for name in names: if is_url(name): return name value = backend.setting(name) if is_url(value): return value return None def handle_http_errors(func): @functools.wraps(func) def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except requests.HTTPError as err: social_logger.exception( "Request failed with %d: %s", err.response.status_code, err.response.text, ) if err.response.status_code == 400: raise AuthCanceled(args[0], response=err.response) if err.response.status_code == 401: raise AuthForbidden(args[0]) if err.response.status_code == 503: raise AuthUnreachableProvider(args[0]) raise return wrapper def append_slash(url): """Make sure we append a slash at the end of the URL otherwise we have issues with urljoin Example: >>> urlparse.urljoin('http://www.example.com/api/v3', 'user/1/') 'http://www.example.com/api/user/1/' """ if url and not url.endswith("/"): url = f"{url}/" return url def get_strategy(strategy, storage, *args, **kwargs): Strategy = module_member(strategy) Storage = module_member(storage) return Strategy(Storage, *args, **kwargs) class cache: """ Cache decorator that caches the return value of a method for a specified time. It maintains a cache per class, so subclasses have a different cache entry for the same cached method. Does not work for methods with arguments. """ def __init__(self, ttl: int): self.ttl = ttl self.cache: dict[type, Any] = {} def __call__(self, fn): def wrapped(this): now = time.time() last_updated = None cached_value = None if this.__class__ in self.cache: last_updated, cached_value = self.cache[this.__class__] # ignoring this type issue is safe; if cached_value is returned, last_updated # is also set, but the type checker doesn't know it. if not cached_value or not last_updated or now - last_updated > self.ttl: try: cached_value = fn(this) self.cache[this.__class__] = (now, cached_value) except Exception: # Use previously cached value when call fails, if available if not cached_value: raise return cached_value wrapped.invalidate = self._invalidate # type: ignore[attr-defined] return wrapped def _invalidate(self): self.cache.clear() social-auth-core-4.6.1/tox.ini000066400000000000000000000012751500362547200162050ustar00rootroot00000000000000# Tox (http://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = py39,py310,py311,py312,py313,py39-pyright,p313-pyright [testenv] passenv = * allowlist_externals = uv install_command = uv pip install --no-binary lxml --no-binary xmlsec {opts} {packages} deps = py{39,310,311,312,313}: -e .[dev,all] commands = pytest {posargs:-v --cov=social_core --cov-report=xml} [testenv:py39-pyright] deps = -e .[dev,all] commands = pyright [testenv:py313-pyright] deps = -e .[dev,all] commands = pyright