From 26d89dbb8165e28aba4f10611d3fb3ffd2c91bd4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 01:04:55 +0000 Subject: [PATCH 001/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.10.1 → v3.11.0](https://github.com/asottile/pyupgrade/compare/v3.10.1...v3.11.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9762e6b20..34563fe7f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: isort args: [--profile=black] - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 + rev: v3.11.0 hooks: - id: pyupgrade args: [--py38-plus] From ca841a0d1c63eff2d5a67b2ef5fdadbb035c73c0 Mon Sep 17 00:00:00 2001 From: Maiken Pedersen Date: Tue, 19 Sep 2023 15:46:08 +0200 Subject: [PATCH 002/152] New backend for the WLCG IAM testing site (#820) * New backend for the WLCG IAM testing site * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update wlcg.py Adding email scope in default scope * Adding test for wlcg backend Co-authored-by: Maiken Pedersen --- social_core/backends/wlcg.py | 38 +++++++++++++++++++++++++ social_core/tests/backends/test_wlcg.py | 30 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 social_core/backends/wlcg.py create mode 100644 social_core/tests/backends/test_wlcg.py diff --git a/social_core/backends/wlcg.py b/social_core/backends/wlcg.py new file mode 100644 index 000000000..cc4861a2c --- /dev/null +++ b/social_core/backends/wlcg.py @@ -0,0 +1,38 @@ +from 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" + ACCESS_TOKEN_METHOD = "POST" + 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) diff --git a/social_core/tests/backends/test_wlcg.py b/social_core/tests/backends/test_wlcg.py new file mode 100644 index 000000000..08fcfa8dc --- /dev/null +++ b/social_core/tests/backends/test_wlcg.py @@ -0,0 +1,30 @@ +import json + +from .oauth import OAuth2Test + + +class WLCGOAuth2Test(OAuth2Test): + 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() From 251887e116eac0f8cf6355d35682a8cefdfec425 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 26 Sep 2023 01:19:57 +0000 Subject: [PATCH 003/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.11.0 → v3.13.0](https://github.com/asottile/pyupgrade/compare/v3.11.0...v3.13.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 34563fe7f..ffac02e6b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: isort args: [--profile=black] - repo: https://github.com/asottile/pyupgrade - rev: v3.11.0 + rev: v3.13.0 hooks: - id: pyupgrade args: [--py38-plus] From ffc0ac4c4decf58311ae043fc0cfcaa31717be77 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 01:53:23 +0000 Subject: [PATCH 004/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.13.0 → v3.14.0](https://github.com/asottile/pyupgrade/compare/v3.13.0...v3.14.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ffac02e6b..4deca099d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: isort args: [--profile=black] - repo: https://github.com/asottile/pyupgrade - rev: v3.13.0 + rev: v3.14.0 hooks: - id: pyupgrade args: [--py38-plus] From 7f314b56ad6f59e6ccfe9fd1906327bc01ccb91b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 18:55:43 +0000 Subject: [PATCH 005/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0) - [github.com/asottile/pyupgrade: v3.14.0 → v3.15.0](https://github.com/asottile/pyupgrade/compare/v3.14.0...v3.15.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4deca099d..c39f4c8d4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-merge-conflict - id: check-json @@ -15,7 +15,7 @@ repos: - id: isort args: [--profile=black] - repo: https://github.com/asottile/pyupgrade - rev: v3.14.0 + rev: v3.15.0 hooks: - id: pyupgrade args: [--py38-plus] From b7a36c77d7c548d81a1bd0bef164bcedd73b245f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Oct 2023 17:42:12 +0000 Subject: [PATCH 006/152] build(deps-dev): bump pre-commit from 3.4.0 to 3.5.0 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.4.0 to 3.5.0. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.4.0...v3.5.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 42517ef39..959c06088 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1 @@ -pre-commit==3.4.0 +pre-commit==3.5.0 From a8cf324f675324d939b06c0a250a3b350c484ee1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 18:37:18 +0000 Subject: [PATCH 007/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.10.0 → v2.11.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.10.0...v2.11.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c39f4c8d4..527d6949f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: - id: check-hooks-apply - id: check-useless-excludes - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks - rev: v2.10.0 + rev: v2.11.0 hooks: - id: pretty-format-yaml args: [--autofix, --indent, '2'] From 8e21c610a190c410b1dfc97ee89f38c6fad3da0d Mon Sep 17 00:00:00 2001 From: Pascal F Date: Tue, 17 Oct 2023 08:40:00 +0200 Subject: [PATCH 008/152] Support Python 3.12 (and 3.11) (#839) --- .github/workflows/test.yml | 1 + docker-compose.yml | 4 ++-- setup.py | 1 + tox.ini | 8 ++++---- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 05131423b..3d598095b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,7 @@ jobs: - '3.9' - '3.10' - '3.11' + - '3.12' env: PYTHON_VERSION: ${{ matrix.python-version }} PYTHONUNBUFFERED: 1 diff --git a/docker-compose.yml b/docker-compose.yml index 2d4acc679..452793e81 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,8 +19,8 @@ services: context: . dockerfile: ./files/tests/Dockerfile args: - - PYTHON_VERSIONS=3.8.17 3.9.17 3.10.12 3.11.4 + - PYTHON_VERSIONS=3.8.17 3.9.17 3.10.12 3.11.4 3.12.0 environment: - - PYTHON_VERSIONS=3.8.17 3.9.17 3.10.12 3.11.4 + - PYTHON_VERSIONS=3.8.17 3.9.17 3.10.12 3.11.4 3.12.0 volumes: - .:/code diff --git a/setup.py b/setup.py index e448b94af..df062bc1c 100644 --- a/setup.py +++ b/setup.py @@ -90,6 +90,7 @@ def read_tests_requirements(filename): "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ], package_data={ "social_core/tests": [ diff --git a/tox.ini b/tox.ini index bd541273a..5a9b747ba 100644 --- a/tox.ini +++ b/tox.ini @@ -4,13 +4,13 @@ # and then run "tox" from this directory. [tox] -envlist = py38,py39,py310,py311 +envlist = py38,py39,py310,py311,py312 [testenv] passenv = * deps = - py{38,39,310,311}: -rsocial_core/tests/requirements.txt + py{38,39,310,311,312}: -rsocial_core/tests/requirements.txt commands = - py{38,39,310,311}: pip install -e .[all] - py{38,39,310,311}: pip install --force-reinstall --no-binary lxml lxml + py{38,39,310,311,312}: pip install -e .[all] + py{38,39,310,311,312}: pip install --force-reinstall --no-binary lxml lxml pytest {posargs:-v --cov=social_core} From 4cdf89070cfb9c91cb7ea95fbc80a84477044964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enol=20Fern=C3=A1ndez?= Date: Tue, 17 Oct 2023 07:44:34 +0100 Subject: [PATCH 009/152] Add new backend for EGI Check-in (#836) * Add new backend for EGI Check-in Learn more at https://www.egi.eu/service/check-in/ * Fix json string in test * Rename to make EGI more prominent * Fix module name --- social_core/backends/egi_checkin.py | 79 +++++ .../tests/backends/test_egi_checkin.py | 325 ++++++++++++++++++ 2 files changed, 404 insertions(+) create mode 100644 social_core/backends/egi_checkin.py create mode 100644 social_core/tests/backends/test_egi_checkin.py diff --git a/social_core/backends/egi_checkin.py b/social_core/backends/egi_checkin.py new file mode 100644 index 000000000..b2a6b5cb7 --- /dev/null +++ b/social_core/backends/egi_checkin.py @@ -0,0 +1,79 @@ +""" +Backend for OpenID Connect EGI Check-in +https://www.egi.eu/service/check-in/ +""" + +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 = "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 = [] + + 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 diff --git a/social_core/tests/backends/test_egi_checkin.py b/social_core/tests/backends/test_egi_checkin.py new file mode 100644 index 000000000..bb5142083 --- /dev/null +++ b/social_core/tests/backends/test_egi_checkin.py @@ -0,0 +1,325 @@ +from .oauth import OAuth2Test +from .test_open_id_connect import OpenIdConnectTestMixin + + +class EGICheckinOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): + 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) From 9995648160c003f804adccd542e3d773829435ab Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:53:53 +0000 Subject: [PATCH 010/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.9.1 → 23.10.0](https://github.com/psf/black/compare/23.9.1...23.10.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 527d6949f..26c02508c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 From 9c630a6c1bb4b265d5f26d46cefa59885ea1b79f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:45:12 +0000 Subject: [PATCH 011/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.10.0 → 23.10.1](https://github.com/psf/black/compare/23.10.0...23.10.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 26c02508c..ed8e39b15 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.10.0 + rev: 23.10.1 hooks: - id: black - repo: https://github.com/PyCQA/flake8 From e0cb88838ab3a0460dc92560c93b32a4824daa62 Mon Sep 17 00:00:00 2001 From: Richard Stromer Date: Tue, 31 Oct 2023 03:05:58 -0700 Subject: [PATCH 012/152] Add new backend for LinkedIn OpenID Connect (#833) * create linkedin openid connect backend * add docstring with link to official docs and deprecation notice for oauth2 * define oidc endpoint for linkedin * override token auth method to provide client id and secret in payload (not basic auth) * copy and override validate_claims to remove not support nonce validation * remove already disabled code section for nonce check * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add test case for linkedin openid (still breaking, inspired from google) * skip invalid nonce test as linkedin does not provide any nonce --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- social_core/backends/linkedin.py | 38 ++++++++++++++++++++- social_core/tests/backends/test_linkedin.py | 38 +++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/social_core/backends/linkedin.py b/social_core/backends/linkedin.py index 624dc21bc..07fbf0be5 100644 --- a/social_core/backends/linkedin.py +++ b/social_core/backends/linkedin.py @@ -2,11 +2,47 @@ LinkedIn OAuth1 and OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/linkedin.html """ -from social_core.exceptions import AuthCanceled +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.utcnow().utctimetuple()) + + 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" diff --git a/social_core/tests/backends/test_linkedin.py b/social_core/tests/backends/test_linkedin.py index ff9f7d27c..b768d3172 100644 --- a/social_core/tests/backends/test_linkedin.py +++ b/social_core/tests/backends/test_linkedin.py @@ -1,6 +1,44 @@ import json from .oauth import OAuth2Test +from .test_open_id_connect import OpenIdConnectTestMixin + + +class LinkedinOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): + 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.""" + pass class BaseLinkedinTest: From 6c69c603a6d2e614754ad3cb3e8de55ab46d159d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Tue, 31 Oct 2023 12:12:19 +0100 Subject: [PATCH 013/152] Version bump 4.5.0 --- CHANGELOG.md | 16 +++++++++++++++- social_core/__init__.py | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b8cc9541..6330d9c99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,21 @@ 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.4.2](https://github.com/python-social-auth/social-core/releases/tag/4.4.2) - 2023-43-22 +## [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 diff --git a/social_core/__init__.py b/social_core/__init__.py index 30357c8bf..9faa2c2dd 100644 --- a/social_core/__init__.py +++ b/social_core/__init__.py @@ -1 +1 @@ -__version__ = "4.4.2" +__version__ = "4.5.0" From 8fed29b83f2394dbef05f0156fc5b2d24b3cf957 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Tue, 7 Nov 2023 17:11:27 +0000 Subject: [PATCH 014/152] Skip 'at_hash' claim validation when missing --- social_core/backends/open_id_connect.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/social_core/backends/open_id_connect.py b/social_core/backends/open_id_connect.py index edba10fed..db7f84d23 100644 --- a/social_core/backends/open_id_connect.py +++ b/social_core/backends/open_id_connect.py @@ -236,7 +236,9 @@ def validate_and_return_id_token(self, id_token, access_token): # pyjwt does not validate OIDC claims # see https://github.com/jpadilla/pyjwt/pull/296 - if claims.get("at_hash") != self.calc_at_hash(access_token, key["alg"]): + 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) From 2f08a38de4bbd1a1f13caeb034f1c3ee53d3c1fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 9 Nov 2023 09:03:48 +0100 Subject: [PATCH 015/152] Add documentation to PR checklist --- .github/PULL_REQUEST_TEMPLATE.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2d5a9ef64..97e68690b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -26,14 +26,9 @@ your code._ - [ ] Lint and unit tests pass locally with my changes - [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] I have added documentation to https://github.com/python-social-auth/social-docs ## Other information Any other information that is important to this PR such as screenshots of how the component looks before and after the change. - - From b9d9937251b3f61435db8fca6203b516acbeea2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 9 Nov 2023 09:41:41 +0100 Subject: [PATCH 016/152] Use shared templates from the .github repo --- .github/ISSUE_TEMPLATE.md | 30 ---------------------------- .github/PULL_REQUEST_TEMPLATE.md | 34 -------------------------------- 2 files changed, 64 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 086847640..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,30 +0,0 @@ -### Expected behaviour - -Describe what should happen. - -### Actual behaviour - -Describe what happens instead and why is it an issue. - -### What are the steps to reproduce this issue? - -Input clear steps to reproduce the issue for a maintainer. - -1. ... -2. ... -3. ... - -### Any logs, error output, etc? - -Add any code, log or error output that you see fit for this issue, wrap any code -and / or console output with the proper code blocks. - -### Any other comments? - -Expand the issue with any details you find appropriate to solve or reproduce it. - - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 97e68690b..000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,34 +0,0 @@ -## Proposed changes - -Describe the big picture of your changes here to communicate to the maintainers -why we should accept this pull request. If it fixes a bug or resolves a feature -request, be sure to link to that issue. - -## Types of changes - -Please check the type of change your PR introduces: - -- [ ] Release (new release request) -- [ ] Bugfix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Code style update (PEP8, lint, formatting, renaming, etc) -- [ ] Refactoring (no functional changes, no api changes) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] Build related changes (build process, tests runner, etc) -- [ ] Other (please describe): - -## Checklist - -_Put an `x` in the boxes that apply. You can also fill these out after creating -the PR. If you're unsure about any of them, don't hesitate to ask. We're here to -help! This is simply a reminder of what we are going to look for before merging -your code._ - -- [ ] Lint and unit tests pass locally with my changes -- [ ] I have added tests that prove my fix is effective or that my feature works -- [ ] I have added documentation to https://github.com/python-social-auth/social-docs - -## Other information - -Any other information that is important to this PR such as screenshots of how -the component looks before and after the change. From 0d629f4bcb4e930da7ae8b7c8c7065e8910e9f7c Mon Sep 17 00:00:00 2001 From: Rob Percival Date: Mon, 6 Nov 2023 12:05:13 +0000 Subject: [PATCH 017/152] Pass `redirect_name` from `do_complete` action to Backend Almost all other arguments to `do_complete` were being passed to the backend, except for this. Without it, the backend cannot reliably access the redirect URL in the session state (which is useful if the backend wants to set the redirect URL, e.g. based on SAML RelayState). --- social_core/actions.py | 2 +- social_core/tests/actions/actions.py | 5 +++- social_core/tests/actions/test_login.py | 33 ++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/social_core/actions.py b/social_core/actions.py index 2d74149a7..4ac2b43ab 100644 --- a/social_core/actions.py +++ b/social_core/actions.py @@ -46,7 +46,7 @@ def do_complete(backend, login, user=None, redirect_name="next", *args, **kwargs # clean partial data after usage backend.strategy.clean_partial_pipeline(partial.token) else: - user = backend.complete(user=user, *args, **kwargs) + 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 diff --git a/social_core/tests/actions/actions.py b/social_core/tests/actions/actions.py index 017b4adc3..9db7a2fff 100644 --- a/social_core/tests/actions/actions.py +++ b/social_core/tests/actions/actions.py @@ -53,6 +53,7 @@ class BaseActionTest(unittest.TestCase): def __init__(self, *args, **kwargs): self.strategy = None + self.backend = None super().__init__(*args, **kwargs) def setUp(self): @@ -63,7 +64,9 @@ def setUp(self): TestAssociation.reset_cache() Backend = module_member("social_core.backends.github.GithubOAuth2") self.strategy = self.strategy or TestStrategy(TestStorage) - self.backend = Backend(self.strategy, redirect_uri="/complete/github") + self.backend = self.backend or Backend( + self.strategy, redirect_uri="/complete/github" + ) self.user = None def tearDown(self): diff --git a/social_core/tests/actions/test_login.py b/social_core/tests/actions/test_login.py index 7a5654907..09819fdfe 100644 --- a/social_core/tests/actions/test_login.py +++ b/social_core/tests/actions/test_login.py @@ -1,8 +1,33 @@ +from ...backends.base import BaseAuth from ...utils import PARTIAL_TOKEN_SESSION_NAME -from ..models import User +from ..models import TestUserSocialAuth, User from .actions import BaseActionTest +class BackendThatControlsRedirect(BaseAuth): + """ + 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() @@ -37,6 +62,12 @@ def test_redirect_value(self): 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) From b6317968afe5b1701cdec8fd1d00d03f1a16acbc Mon Sep 17 00:00:00 2001 From: Rob Percival Date: Mon, 6 Nov 2023 12:02:46 +0000 Subject: [PATCH 018/152] Pass "next" URL through SAML RelayState This is the primary use case for RelayState, but the SAML backend did not previously support this use. Instead, it relied on the Strategy to store the "next" URL in session state and restore it from there after login. Unfortunately, in the case of SAML, this does not work if the server has set the session cookie's `SameSite` attribute to "Lax" or "Strict", as the SAML POST request from the IdP will not contain the session cookie. The new `RelayState` format (a JSON object) allows for further extensibility, as arbitrary additional fields can be added in the future. --- social_core/backends/saml.py | 28 ++++++++-- .../tests/backends/data/saml_response.txt | 2 +- .../backends/data/saml_response_legacy.txt | 1 + .../data/saml_response_no_idp_name.txt | 1 + .../data/saml_response_no_next_url.txt | 1 + social_core/tests/backends/test_saml.py | 54 ++++++++++++++++--- 6 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 social_core/tests/backends/data/saml_response_legacy.txt create mode 100644 social_core/tests/backends/data/saml_response_no_idp_name.txt create mode 100644 social_core/tests/backends/data/saml_response_no_next_url.txt diff --git a/social_core/backends/saml.py b/social_core/backends/saml.py index 4e39dd25f..32cb76b98 100644 --- a/social_core/backends/saml.py +++ b/social_core/backends/saml.py @@ -7,6 +7,8 @@ "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 @@ -278,8 +280,12 @@ def auth_url(self): # 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. - return auth.login(return_to=idp_name) + # 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 @@ -303,10 +309,26 @@ def auth_complete(self, *args, **kwargs): now log them in, if everything checks out. """ try: - idp_name = self.strategy.request_data()["RelayState"] + 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() diff --git a/social_core/tests/backends/data/saml_response.txt b/social_core/tests/backends/data/saml_response.txt index 557bb59e8..7f617611b 100644 --- a/social_core/tests/backends/data/saml_response.txt +++ b/social_core/tests/backends/data/saml_response.txt @@ -1 +1 @@ -http://myapp.com/?RelayState=testshib&SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL215YXBwLmNvbSIgSUQ9Il8yNTk2NTFlOTY3ZGIwOGZjYTQ4MjdkODI3YWY1M2RkMCIgSW5SZXNwb25zZVRvPSJURVNUX0lEIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDUtMDlUMDM6NTc6NDMuNzkyWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI%2BaHR0cHM6Ly9pZHAudGVzdHNoaWIub3JnL2lkcC9zaGliYm9sZXRoPC9zYW1sMjpJc3N1ZXI%2BPHNhbWwycDpTdGF0dXM%2BPHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM%2BPHNhbWwyOkVuY3J5cHRlZEFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIElkPSJfMGM0NzYzNzIyOWFkNmEzMTY1OGU0MDc2ZDNlYzBmNmQiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiLz48ZHM6S2V5SW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI%2BPHhlbmM6RW5jcnlwdGVkS2V5IElkPSJfYjZmNmU2YWZjMzYyNGI3NmM1N2JmOWZhODA5YzAzNmMiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDc0RDQ0FobWdBd0lCQWdJSkFPN0J3ZGpEWmNVV01BMEdDU3FHU0liM0RRRUJCUVVBTUVVeEN6QUpCZ05WQkFZVEFrTkJNUmt3CkZ3WURWUVFJRXhCQ2NtbDBhWE5vSUVOdmJIVnRZbWxoTVJzd0dRWURWUVFLRXhKd2VYUm9iMjR0YzI5amFXRnNMV0YxZEdnd0hoY04KTVRVd05UQTRNRGMxT0RRMldoY05NalV3TlRBM01EYzFPRFEyV2pCRk1Rc3dDUVlEVlFRR0V3SkRRVEVaTUJjR0ExVUVDQk1RUW5KcApkR2x6YUNCRGIyeDFiV0pwWVRFYk1Ca0dBMVVFQ2hNU2NIbDBhRzl1TFhOdlkybGhiQzFoZFhSb01JR2ZNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNEdOQURDQmlRS0JnUUNxM2cxQ2wrM3VSNXZDbk40SGJnalRnK20zbkhodGVFTXliKyt5Y1pZcmUyYnhVZnNzaEVSNngzM2wKMjN0SGNrUll3bTdNZEJicnAzTHJWb2lPQ2RQYmxUbWwxSWhFUFRDd0tNaEJLdnZXcVR2Z2ZjU1NuUnpBV2tMbFFZU3VzYXl5Wks0bgo5cWNZa1Y1TUZuaTFyYmp4K01yNWFPRW1iNXUzM2FtTUtMd1NUd0lEQVFBQm80R25NSUdrTUIwR0ExVWREZ1FXQkJSUmlCUjZ6UzY2CmZLVm9rcDB5SkhiZ3YzUlltakIxQmdOVkhTTUViakJzZ0JSUmlCUjZ6UzY2ZktWb2twMHlKSGJndjNSWW1xRkpwRWN3UlRFTE1Ba0cKQTFVRUJoTUNRMEV4R1RBWEJnTlZCQWdURUVKeWFYUnBjMmdnUTI5c2RXMWlhV0V4R3pBWkJnTlZCQW9URW5CNWRHaHZiaTF6YjJOcApZV3d0WVhWMGFJSUpBTzdCd2RqRFpjVVdNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ1lFQUp3c01VM1lTCmF5YlZqdUo4VVMwZlVobFBPbE00MFFGQ0dMNHZCM1RFYmIyNE1xOEhyalV3clUwSkZQR2xzOWEyT1l6TjJCM2UzNU5vck11eHMrZ3IKR3RyMnlQNkx2dVgrblY2QTkzd2I0b29HSG9HZkM3VkxseXhTU25zOTM3U1M1UjFwelE0Z1d6Wm1hMktHV0tJQ1dwaDV6UTBBUlZoTAo2Mzk2N21HTG1vST08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BTElQdkVNVUVGeXhrVHowQ2N4QVA5TjV4Y3NYT2V4aVV4cXBvR2VIeVFMV0R5RVBBUDVnZ1daL3NLZ1ViL2xWSk92bCtuQXhSdVhXUlc5dGxSWWx3R2orRVhIOWhIbmdEY1BWMDNqSUJMQnFJbElBL1RmMGw4cVliOHFKRy9ZM0RTS2RQNkwvUURtYXBtTXpFM29YOEJxMW5Ea3YrUWh4cmQwMGVGK2ZMYVQ0PTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BRVpUWDhHTkM0My9yWStTUVlBMXRudHlUTTVVNkN2dUNCaktsVEVlekZPRjBZZHhCWUdFQVVjYU8xNVNKOXBMemJ1L1h0WGxzTkVMZTdKdEx4RUpwYUxubWFENnIranNWczdLaTBLNHRTMGNBUERDWHV2R1FoMmFOVjVQOGJ3N1JWUGhLOGQwYlJ1RklGR09FOHMwTTZYOUpxWDN4S0MvL1lSbVVoeDlybnU3ZWlwMGh5ZitPaUZiVGR2SDY2NTB2LzQ3aVdKcDNZeFlUV0QyMHBNbVRJMUpwWUEwYjByWVFQRkR0RU93d0JxYktxanRJc3ZYVFJzeXJhQkxvbnFOeHN5dHpEWHEra0JsMXp3WGUvSE5QcUVQblczdnNxaFhZcDVGM3dkWThkKzNCOTRMZlpOdUd4a0p3VDNzdVR0OGY5VHRBSlI4VytBUmtzT2M4eDBVaVNsVG5BNHFHOTBLMTR5dkVoVHcvd2drZjFXV01RT3dpZDNpakFYbUV4MU5MbVZvYUxYb3p4VExkTjN6YnJ6VEJIRXc3R2J3ZEdrdU5pMlhZOW16YUgwaWtGRm51VUxjMHUwc0pycEdGdzlaK0VlUk44RzNVUVZ5MjhtS2g3ZFBwWU5KbzhyajIxZFFaK2JaeUtTUHZablU3REkyakdJRE5US1g2ZkVyVWFINGlOTzN4cUU2Vk90L2d4T3BMNE5VNUhLV0Q0bG93VzcwdUJjVEVQRmhwaThpYUovdTB6YzUvTEhvdVBjMzByc1RLZFc5cmJLL2NWaHNQUHErZzA5WHZpZ0QweTJvN2tOc1pVL25tRXFiSzBKOTBrazhCR3I5cXRSczY4bUJnSURtUHVwUkhwWjM4eXNnU2VZN3V0VlVaSG5tQ0dzTzZ2NDJ6OTVOK05Pb3RCTEVZbFd1ZEdzYnowQWc4VkRDSlY5ak95QW95MDZyL1AyUHBsOFhjdmJza2d2T1BMMWdDNnVYbVJJS1lmOEw4UDJCNXVjN0haK0dtUHNOWXRLS2VKRDFFUHovdCt2NlBIbXNVb3dsSDhSd3FMRHdtMUF4dlNLQTR3UXBlQ0dQd3A5YXRYS0lWMS84NUZzRWMzajVzNjd6VlRybThrVEpydXV2MDZEdFVRZDNMOFdwTkV4cWhQait6RUp6U3RxSG04ckhNMVhNQUVxdVozc0xycTVqLzFSNlpqS0dOdFJCbjhwOE5ERGtrWm0vWTV5TXlJNXJJS3U5bnA3bXdaaEVpeWVHeHdxblV3VVMvUzVDRjNnMHVidnd4eVVnalVvd1ZvTkNqYktBbkdtT2VCSW5abkh0eGdIVUhVOUVlTFdyd2pRc3JtUmpJV0R2RkZQa3l6SzJDL20yaitubmNxc2E1OGRLVXZxcGR1VTRJYnNPQng3UGpXdXRBNmY5bXd6YWxyRU1NK0lGR3VPdk9HMC93eUdzQjZLREV6bldjUC83NkQ4angzaHZFSlAzN3REbFgreGM4Qno5TXdKdkd6VG4xbTdCb2xoR0lzSXlCTys1ZXpXa3RDWVVIUURGVE9wbXA0MDlOWHp6ZUNTUGY1U2NDWG5YYjRPd01ULy9VM1JFUnRRbGMrNmU2WG1JRjhoRkJVc0taUUJsS2ppSDkwZHlzYWlsNmN2V3UyQW55Q3QxbWxXcHFLc0MzU2RTRVZDTG1qRjlUQUFUMEtFSGdZQjg3RjZtZUpTTysvOXkyZkRuYVVvUUlUVzdubnVuSCtkT3dWSGZMU0wyL2N5YTltNlQzR29TSVNMbGJPMVRzalhKclVkZW55OTcvM2tkNmhFQlphdGY1U3NETFQ3SjNsQUVJNDROeXJ0NkIxQWdod2JNdkpqd1JNTXRNdUJLc3ltUytKVzc4UFNEWXQ4MG9waDJQTTc1N0tBNCtUMTAvYnZaQkE5Vk1OdVpqNVV3NXRWMnFIS3dwS0t6ZVVETUFiQlBRaGpYcXlQZzFKa09rd2RQMUpnOHRITjJTelBZQTlmT1htV0pBZGJDS2tMb0F4ZTV6cDZBUzYzS3FXMmFmSUt6SHJ3RTJmS1VtamppeURvMnNuMkJHbWtBaTRzbnpiVzc2SUQvSVgwd044aDBaQ2VRc29vKzdtb1RCMEJxSnBkS1MycXlsUktoc3BSTC9henVQdmxaK1pwckJxdXpJdEZkNFVLMkpzQkp6VXcwZkpxcTV1bk9PZENzVWM3SUU3QTNmZ1NmZ3NBd1R3WFZJMEVoME5ySWZpMkFKV1Z2VFpEMys2eFZ3dS96WWhuVjc0VXkvMFE4Mi8yQWtpSGpFRjNJVGNLWHdTNTB6bWtLakxjZDJqa2h5TUFYMWRoQ0wwZElFMUJoN0RNamVvNC9YbjBqSlpPL3Rrbi9xZmYzc3RNb1BYVG9KTnBIU1RjR2ZheGtaMzJYNCt3Q0xPc0VBRWxlMVZSY0kwUkZyOFhHTSsxWU9BTjBodFdGcFMxaG9kSi9OczJqL1FnUVNEemNpQ1FZeUFDd3lFRWZDZjZybnR0VmJyTlJQZWlmSHhBM3B2UnZ5ZGRhNDE5cXl0ZXI0akJ3cmw3ZUpuVnJ2VEprR2VhU2FRbDdXWk5SQXBscXRnNnZPYmpiMHZDRWlFaFhKbmNzQUhxcXp5QTRGeWFUVGQ2R0FySU9adUNxRWVoWk51T01lOVlrMVpya0VkR3pIalJESWk3Q1BKQk12NEZ4ZHI3bnJvN0I1WEhKb0ZMNE1DSUtOWWU2aWZiTUtYOU5uN1FWdnphUmY2UXlaSW1BWENQZndvU1BkN2x6NXl3UDJLSUIyaGhFMWt5eVZ5YVc5T0praWpUY3dvUnZrSXhIU0RqMXFqeGxueXh0QzhVZ1pNWmlwcGgzQXJpcjRiekIzUDhIbGIzejZ0OW51KzZMemNiN2ZObVo0UHluaU50Vk9OQ0lHbEh4dTBSY3hQK3cwUXNsM1BtTzJLaHBpc2RIanhvSUJ1YVY1NXdoTlFFNmdNNFBrT0xINDc4Rzg4bUxkd2s2RFpkWVl4L2d6RWE3b3ZIL0pReFp2TzRLdFVTNmZjZHJxV2thTFg1cEhkNkdneFBGZ2NFc2Nad1ZqM2hCS0xFQmE5L0dodERINEhzRnNRbmpPZnNDQkNzN0tjRitmTi9oSUdUeHFqTVlKVHJRYmNtdWF5dk9xR3RQMDFPcXltR24rVm5FSVkzKytQcm95SFN3K0Q0b0JIVG1maFNXRmJLZCtuTlVFS3BhRVIxNkdCU256WktQRVRVSmdRWEw5QWJRQ3RXVjFHb0UzRWNnMDZYaVd2aHFHakpGNldtdEU4dHY4Q25rZmxMNm91TDRvNldpbmx2WnNEdkZrS0R6TDkwUTNsWC9NanBtRTFpWU9uYzdISXdEVGwraFRRcHdsYXJiTDVUNGNkZTg1akNwYU0xU3p1TStiQU5zMHlXVDA0ZXJUVFc2cnhlbXFDTHAra202TVVMTlZOcE1CazBiQjJpRU82UlRtc3VpRlhDUU1xdU5xZjdkWXUwTFFCZzQ0MkJzU1pBV1ZrWEVZblduOURLdTRSby8veEFsb2h5VHozWlZmSkhuWVBSdDloSUErRHVUL3c4T2ZzTURIWnlCelUvL0JEa1NiNkxjMHdraVA3QlhIdjBoNVdud2dNWUxlZDBPalR5UWI2aGxpVnQ5b0FjaDRFVy9EZUlBdkpaQ1BYVm1pUFFYTGVsOVJIRko2bXFiYVo0TCtaZG1ONmQwcFZNZ1FveXhmQTR3dEwwYVpiNnFZYkhibjJMd2VBQVZwL3M2TzVlMVExdnZpZDRTWHo0a2l3RW1LSStIeXZEQ1pnekpQQVN5Z1gvWDJFWEZ0NGV3SjVmUFQyVXZmWnhQWlpqMFZGSFpyUFQwWVd2VE16bjUva3hoT09oM2drVGdDSmNwNWVsZnp4cEFPNFl1a0NoNHJXdVNndDRqVUJyaWNYbFdWdWo5U3JSZVhUalNHTktLK202NWovUDllNHRHT0RkMk9BbjNKTVQ3Q3FuaDhreTZpZjVjbmpVMmU3UDhTZnBONGwxWEFiZEZEcGk5bVJYamEyTzR1RWFHNGNvNW4xcWNDT3ZNMWYyblFBY1ZGNUFoSXhueS96TWhmU2l2RXdOQ0Zyd2tBWDRyQVE0WldUNldFakFyUG5jb1Y4Z1VRclhxQVA4NDJmK1lNWWI5RHFncmFicEg1a3ZuMnQzcWRldGJHODJ0QWlTamhPcUxNYW9iU2F4cXdWa1lUOHRTMW9rUUt2MWZoZ2t6elpEOE5IQnVQQzdNVHdXS0VCS2tDRUUzRWRFMXhNQURLd1B1M3NSaGpSaExXZyszZ2srejJtdlU4cTBhTlc0Y3hObUdoekx4eEY0Q3NFNStMQ1cwOWFpUVJOM1VvWmg1aktBZzBiMlh3WHBLS3pycUVTY1BYdnI0L1dWUTMyMm5qRWRvQVdXR0t2WnBKMlRlREo0eDdiT21LVElFc2RHWU1UZzFVaEU2eFFQcnhqS3dWeGFJNVJyaVE4a0xpaGgwa0t0WHQvYTVsSDhzUjVwR0ZISGZ3dlNVb3liQTB1eUVDNnNRVitPbTVReUZmRmpqZHFCOGNpOGxQS1hLTHFCTHJ6bjNmUkh3TmQwbzFiRTg0aGllTkx5UlhZVmhrRCtFNEpGaVd3ZWt3U3VWM3BjQk9ybnRVU3RoWmx6M3hIUURUVGNJNWliOFJyQ2swZEZ6YTgvQmw3VUdtWlUwSXZ2UmdvVXF2TXNHT2dMY3pGWmRpZnJ5aGNiUTY4a2ZzZ3lCMHppdC9MN1BSV3V4RkdYdDFoTVZSVUZ3WXBJS04zVkI3cXVKZlgwamZsU1JaRndMaXdlK3VhYndmTVZ6c2doajUvOXZNNzcwK0JaMGtJcE45NzBTMG5BbHl6R0h0aW1nTUl1RXFhbUt5QTNTQlI1aHZIYmRyNENnTHFUbXIzbFFnWmpnSkNvN1FXYUJWTXdCR0RpdzVOVVhUUnBycWc4U3h2eDlnNWZwbXMrL0o2QjFEelNTM3ZRZzgxdHFRU1ZDWVJpc0Y3M2VqZlFuZk4zcUszd3RJRDkxQnRISmFvMEFaUUdKVFpKOXVsZ0kzV3hzdWR4ejB0VHVpNlJlSWpmSWsxekZRdFpwRExGMnB3NGpTQVdQTlJqNDBYdVIrRzFUVlI3OVFiME9FYkw4RDFoTU5zWmo3MTZNbUhSOTlKaUxNdm1FWHV5a1V4VGhGYjRMTzZVbW1kU3UwTlBpMXQ2NmNkYURpQWhMaVBFTGdUNkZsenA2T2FGSGNSNjRncEtyemtTNDJONEhJeFpNa2R6M0FsYkRhK2pOWHZPR1l3UWl5K0xNNENZWGtrTWtHR3ZTWis5R2xWQ0l5RXBJaXIzbEQ3bmdzZGk4emxGWDYvekNaczlQSUtwZFZlSGJGZi9GS20wV3AreHI0Ykd0R0RrVHR2Nk1Manh2YU8zanFHaUFWeERKVWFkTVBlS2VHSm5uempTdnpKbGdOVHV3c3grRnF5L2dPMkwxMGowWmhDWi92dE9NelVjNjl3cGhKZm9FNzU3V3lOeFJOcThJc0Y1Tkg5Y0x0b3UvbUNxOTc3YnZPSkRrSURCN3lKWEJ6YUhVQkJuSXJra1Qyemg3bGJmUm5SREJUSFZraVZMazVESUxqeC9XL1BSZEZpUUM2SzRmZGx4Y29JbzlMcnM4ZFVWZkt2TTNNYnJ6c1hGT3ZtVVh0K3NsZldvd3UyTC9ndG9mRFhvTUJZZnlEcWIvWlRaRWZ0MC83blliRm1relBEUlZacU5SR0F3YWZVNTU1UjB2SWtNbGR2VjdKUzhNT1BNYWlXQVBpelNLRG4yRzNvcys1MzRFQytaOGZnWmFPVWpZL0xLME9vME9RMmhvNUV6MGNMYWpwUjFINk9FNEhvUm1ydjQzZkFjdGpYc0hYdi81RXg3emdrWk1NZXZhTFNEdjZtcjFGcDk4QXR4L296VTFGVDBoMDUxcVcwR0g2VWpRRXk5aExSZDBBMnFkUTRMZXpReDNvbDFTblhsamt2MG4zTXFlaFozOC94bzZhdHFDdkJtQkc3amlUdXd6YnlVUngzRm1TM0NCNllOYnFON3hPYVRZRnlkOEZDL01nY0xGQmMwS3F4MXllQ2VUd1hucldQb0dvdllVQlYxYjA1cWtIa1d5V0RUaCsveXJFNzF0RjNxbUQvd3F6cUJyNE04NERtWWVuQkdFOWxtb3FIZEMyWnRpK09KVFZKcmlHZWxQQ3RjZnZRaUlQcHdDZ3BFNmg1ekZhRndLajRuZGtBUkRpTC95L1EwWTZxNU5rM1g5RURlTmdjY1pIcFdmOUpKQ3M2a29wdXRtYjdDczIrbVJYdER1S09DaGY5UVUyN3Bmb1NJaklYK3NGdHY1c0hhSms2aHBZMlpzUUhzaTBYbFowc3FMTnQ5ayszdTVnYnBSU1JCczlHaC9BaVY0dkNyYTRkOTh5U0dCdzRSR1FhSStpQ29RaG9YK3lxc3VrYkx6bXJUU3FXMVRXaXJReUlHZ1Q5VnFERE1mUzAxeGdQSlNFSTlIWlp6TGlFVXVGMm1CMi81Y2dqaEFUaWQrdGV1UVB4aldhN2NSc2t5YUhuTENjQURVUU9ESUFPVjJDWXROcnAwY29ZL091S3ZzaXlJT0lacVJ5dE1PMGVNZ1ZJWTBzWmdxeVEycXlubUx0NDBmWmd3SFVyV245Zm9TYTNtMkVRTy9uOS8yU2NuelJWdVZpVnNjM0tCSElQL3AzNlJlSWowTGlNcCtPQ0p3SHlLVW1UeDRBU1V0dXVhWktlRHl1QjlxcXJuUEFNWUVCeElsTGFvdXMzV1pHakIrcW9ub3QvNmk1UE40bUZjbHFDcUxhMGJHbks4ZnJxYy9yd2tuVGV0YUE0c2tXTEw1L21qNEd5MitFQkh3a0x3UXd2K0FKdmZTOXYvNDl1LzY0N1ZFYW15UzdZQ2ZEUHNBQUREQ1FFcWJNQ1h2Ui8xVmEwWi9YUWhoNlkrZUt0MEVpRDdpNmRZODJtQkFoNEJMRmRVV3VGZHVrdUVwaGZ2WXB3N2loVjNxTjB1NFM1NTRXU0dUa0ZsdlpYNG1hbkF4a1g2ekQxS0NWaEFMdEJnSDgzdkhxam9uc0lwOFMydHgwZ0tiYzEreHVaRVppVWlNVVlVdTByQVFsRFcrZHJoN3lVRHZqekFHSnBmTk01eThaMW45em93VzZ5YW5VZWFBNjhSZDd5TUxobFd0NVh6bGhBTVZDZmZYZ0pFelR1YzJEbENVOXNMLzVTVkRaV2N4R1E5aFM1cnJtK2VyQ1Jxd2FJQk1DNUtza0RCZHdOWmh2Q0FCdEpqS2Vla1FUSjd5MFp4SGNhbGVCaU1rbkYwZVRDZzFvUEhPUVZLQ3V3NE94cHRZUS9xS1V0TEFIWFZ2OTlLMGRWcWZDMmpVQWlHQmVYa0t3aGRYTGtJYlZxU0EyZmxraXBBeEhYNnByUEExQjF3eTVab3hPUFg4RVExOW92eXpBbFg1dHU0OXEwWC9PSExFN1o5T1cxenltRXR6ZFpyNXJZbWtFcVdtcHVSNU5jeHFwTWlZam93dUNXZWhubzIyeG5JM09IQ0xDZkFKaHRrcklhL1hPc0tZRFpCRzFJMGJsN2taR2R5cEtUQlhYdXl6WE5WUlU5L005ejhaVytwdG1oZ2NOUzBJS2VaaSs5bFl4cWRlS3lnbldTTTV3czdSYUpmNlRRZTNSaWJZUjFvNkhwRzB2VHpiTEtQZTZnRjJGODdiWlBJei9mcTNLWnZiM3UrSnhZcCtJVjBtQi9VN29YelhRRk1RK3VmWllpNzUxbkx6WlVxRE1ybU53TFJPVUFNUk8rVnJtblkwSVB1cFBVMXc0b0hBb1dnVGRnTk5pNk1uTFQ4V0pmUlhjT0pKMk1lbUc2K2ZNeHNZUU52UVJwa1RGY05vaFV6Y3ZjcHJ3NUV3WEVZQTJzbzczL2MvY3RIRGcreU05YlF4REppUlltRnFydkhYb29hS1JyekxnUjZLVWdoM3ltaWxaQ0lSSm9KbTE3aEtHM1pxTTE0Lzl5OUc5OE9BZjNkVTlqMDk3aUNlaEc3a2VxYXRJQ2hFWmJqbmQ4Y00rS3djN2FtVWp2ekQzQmNvMHl3MDJxT054OWF3OGhSblZiWDZhdkRJbGhySHZ6SU44MzFvUjljRHBwMG1DUEJXZFVDQlNqVGJ1RkZqRC90WElSbGxlT2JraFFKSUdSNlE2U1MxcXkzT29WT1VheFl6THY0U2s3dndrQUMwUitGREVIeVFZbFVhbVVkTWcyUmdwRUdhSVd1V3IxaGNnRm10QmREV2g3ZFBuWTF0U3VKOC95MXp4NkRvN2ZJYmNFenBBK2E0ODNtRG5vemdld3VmaFdqVCsvUS85WlEreFQ5UWJBT1pQSXhHV3VhSXVrVk8zSWxvZDhJM1NGZFJCTHY5ZXBDNzFLeXpSdVlpMktkOHJ5NVNINit1WnMxUHlZUlpRakdDK3Q4VzRtSE82Z1lFRWVXSkJ1UWhnSHdmV2xhZXlWb3hac0NBQVZKRUllT3hPZDZtNW45OHRCUDdHTmgxT1M0eDRCS2FVN1A0UVQzNVVIZW5meE84WWFQUThmbXlobUJhSVJVZklBTVN2ZTJZRFp5SWNNTTkrN0tNSVVabzJ0eXRvYzdCOGVvZzBNaUkrVkpFdFg0c29FRjFSWkhQZVV3NWlCTjI4OTh2MmVTcGNnVUJhWHFzOUN5VlZtTVJQMEtLUDJ1REt4MUdJcUhjS0ZCOXVQVWRkQS9vT3dNa0tVUWsraFZVVDVPbEVMdjd1a0FBUEE0eE4rZkczVmYxeUVKV0FiVGx5dWtGcThjNXBTRkY1cXVHbUgwVmVpQzVvVEFka1VES3Z6WGhWWUs5c3BRYjNVZ1Z0Qld6N1ZScnlOUVVST3BIZU5xeDlhZHA4YWREWCtRSHJUKytYblN4VVI3SVdGanlNTkZJRWlMWmkxdks1UVVrZlRDUU9qdjh2SHdiUi9MRHF3Z3M5bXdsT3pPY0RLdVBVK0dTb2lnVFdRejRWN0N2SHRaVDI3WUdKVG44RFFFM3IzdjB4aWxvODJ2U3VXSDg0WEU3VEJsTUpFb2R5eDNDRngwVUVkc3VhRHBPSEV3UjZYNlUyU0xseERYSXVZeEhlNXh2NjI4bXU0bDRMSnBYUjhkYmljTEZKQW55Q0FVeDJLb2dDamt1cmU4bXNUZktDbG8wamFlN1hNR05PSk15b0ZYbVlHZUh2eGhNUGMzTEtYLy9VY1p0c3p3dFJrQmNFdURXQysvQWNWZVBOSHVOWWI5MEpIcnRucGg1ZDlhL1lpTkpzY1N3QTFwUVZrdW1TQWtPQWdLdWRzcnl3c0N3Zkg1anNydVpHUTJDd1hKRXQzUU4wU2NLUlVnT1NCQ3FYa1BqZDVSVzJuOFZpamt4anovbWptakhCNmk0eHM5NEU2Nzk5STAyaldYNVd3UDZhTFRaTGt5TjhxNDUxT0RmeUZVZEY5WWsyZXQ5VUpsV1NzRFJMSWVCd0ZyQkEyZTdyRWsybWFLVUNCRW5PUWM2bUhVMXQvZ3gzK1VXVVFXbkpMZVUxbWUvbkFEdy96UGUwd3d0Vm9BaERZdDBoR1hQblJydjFoUHRGS01CeWtqckg3a0J5U0R3WDlQMi9XZkNkQlE5K1J4cHRsR2hvRmdpMUs0NVlOeEpEd05wTmd5MDV2WXUzVUtrMkpRYVNGUzcwK0Y1NzluRE5RenZpK0pPRlRsdDFmWDJGNXk5NEV2NHZobWRQSmRVOFVVRjU2Ymx0emxKREVFdmsySlFrOTM0aHpwTXJGZ1d3ZHUxUkxxSEhCN2h2T2hnaHNqV0ZGY01zNjZaRUtWcVhKUytxWWNVMHk0akwySVQrNlF2N2pvQ3BWbUdzUWtGY1FyblhxOUJiOTdaUS96UCtwaldmWTU0UmNRVlMydUU1YURObVVyVkdLK3E0d0xRcUhuRVViT2puSHFFeGlacUtxOVdRaUtUK2c3QS96bVlIQ2k0YzFTejRNVWhHb0t6U2l4aXoxYUNJUEJXdy9vczR2cUVqbXgzOGx6YnV0OWNWbElzeGNkTUpUTERRK3ZOZ0YyY1ZRaVcxRTQ0d3lWcnI3TUFaOE9KRVpFSzlEZWt5MzJQUkFuSkRUVXVqdGFscmJ0T2VOczhyS09uTjcvNFRqUEwvZmRlbEI4bjA4WXdSNXdmbU42VGpGWUhRSDFjbUZmK1AvNUxVMTI4Q1pEYjNQUStxMlFJazV3aE40eGwvcy9lb29pallmeWtDcm5aSEhHWkluTGhoU2pWbk5ISWdTL203VWV0NlhBTDdvZUl5UFRLeHVnbDJzRWtUQzNnZ0tjTnFZR0E5U3ZlYVlaQ00vWHNQRUtQbWs3QmlRNmprWFBKaE1yREd4Vkc0SW9aSDgrYjBrUWJYR2l0Mkw0L3hZdHh1bTVzcFNPSjdsTDltVFpRNnBxM2JOaTEwZU1mZ0ZWaDc3NU5JRlc0SEp3U1FtaTU0bk11blZTQjhxdjZKc0w3SGlsZ2N0ZHFSNThTTjVad1lCa2dOR1hzYjA1QXJWemVXbHh1Y21BSHNPT3dyczFnMzh6bTRZN2ZPZmducmFhV1kxanZZOFlEODZQZThkZzR4cE5paTg3UnNDZk5WK2NKVmMraktFdnpuZVY1Zzd0RmlxZCtsZHp4STlKemdSS2t0WUV6RUpRSVU5M2UvclJaN1lrVkZtNVV1cjVhMWYzcG83T0VtYkJUc2MrQ1FaOGNnYmIvbUphRXJoa3NyL3JURjBNcjNxeDl5SlJWSEJ6YWNWd0dScEFRaURPdnJkWU4xQXBVOTRyR1lrVFVzdWs1YjE1Wll2QVZxRlRzVlVMaS9HY29mbEljMm01Z2RFTFZOblRmdXY1Zlk5S1NlWHFoUU80S0pOYVZmbHAwQ0VKYWFFZFNLUXJJNXRaT2w1RkE4VXZlNmxTWVd5TVk0REl4a1RiT1JoWHVBdzR6b1RTMjgrN3d2TXhydVBkZnlKbUJCTkhQdCtEYmdKNHovcHJZWUhpTmFMTXNZamtQZE44ajNKZDczQXJFZk92Um52MzYxSVVVMFg1RDc1dlRSdlpkbzMzWERzanRlOU4weUo3K2lIQnF1a1FJY2pIVW9ic2RQN0hOajBVYWNSMHIvTmRlVTlGNFBNc1VLY2t6Tk4rZGhyMVI2d1J2R1VZb1pDRWJaWlJMWEt4QnA3SElUNEVQUktHakIvdW1xTFhhMXl6RWx2QW1WQUJhMDFZN3dGdk4wM2Ywb25FbUhTM2w1d1paRmV6cjVibnN5T01XVGxhMU5kaW1ZNXNVeE15VFliZmc4dzB2cXNEc28zWFAxYndLdzZ3M3VIRGQ1UHBSWnVDSnR0eWk0ZzJGeWI0Ymg1UU42ZkdORTI2ekRGN1Y4QmJwZXJLNkFKQ0xTWm5kaDZMMTlPUTBram4xUGpEMGk4c1BZcGFXOWxVeVJkZElPKzRWQS9LemxPUzJ4M2s5VUtUdElsTTBUSVdtZXFIS0dYUVpocGpvVGI2VlNKN203cjZaaVlQMnVsQVVvZmVWL0o2eCtzckxEQXkyQ2ZFNnFrREZ1OU9NWDBBSXVnN3loQUtOMDRyT3hVNk5tcGtjOUZ4bXUvVS9vR3hHdmIzeFVFTDYwdE1sSE9EaWtqY1I5RDJrKzRwbEc1WnV0d0FIY2kwRU02WHRrVEhQOU5QMlRTR1VFN1E5SGYvU0VEc2V0a25hZXhvWmhDczJLWDFMeU5JS0U0N2pkMkR3MTUreDRRVXV0VUFTbzU5Q1lHMVFBeW9BVVhrV3dtbXkzTGdTUWp5T3ZLV25qaE8veWpPd0FyWGd0NFBrSVVnZDQ1N05ReFpMbU41K0J4NVJoQ0FHdkUxYmxOZjlMek9keGJiaG5VZ2Z1RDM5MXVSRkhjS2RYREY3ZmVqb3gveThtaWZJcTRWVzQyajBHQnFOQUtkK0prMnJCMW9hOTRiT2hxcVVzanhqWnlRaGRXTzhNblR6T2tOaGVpZXU2blYxcW5yZ3JHU2huWTNJMlczb29GNFNnczRjZ3drZ2h2dHpFa0xUbU5OUm83RTdudVRuMkxJcmlGSnlvTmZQdUp0aWN0S0JtNzRGZytkWVBTMlIzTzNmOWxBZWxiVWZjbzZGNU9EL3hkS1VuRTh0V3FOMExVcDlWQUptWVZYZFVDaGJ4MjM4MWtDaStLNDJoRzUydFNQYU1hb1dTb0xQY2Zrb24rc1pYdjdEdEtwZi9HTzdhcUMza1pzRGpva29haHJGZGJWSlNTZWhrNGp5K3RzRHplQnJKSjBrMVZrUnJHN1NoVHZjTmd1cjVucVRUTEE5dlJMQmJNTTlhNlI1NEZ0Z1pQOWFKMU1aMEdCcUVpMnF6Ui8yd2tYQlhwcFhZdi9TcU1RV1dhbTVsSHBMVktxaDN4ZHRjNFdmck9mYldsbU1PNXA5Z0JUSFp1YUcxVGFkZXFRVVpKQmZBS01ENFdSR0NsMDFaeDRTVzE0YzZrdnFKdXExL080N215L3RsVHlLWndpYlBkQTNRMVVGd0I3R2Z4anEwaDN2ckxFbUNrS3Vsc0VBUkN6UnZNVjJSVnBVbFpUV240Y1Boc0hjcTNROElHSUYyKy9nOENFSU4vMU8xcVMvMkpXcXlDNmtIb0w4Y2R2R0VHbmkxSTNDTk1JcXhxaHhJL1V0R3REc2VwYmwrSHI0elh4MzZna3BCbXBoT2xkTFVYTHAzVEtibVVZRWJSWHcvZmRmeFQ3WDdZUFhHQ0hHVG1uTzk4WkxDOTA2Zmkvekd2b04rNlpzbCs3MkpWMGxJWEo0V3dZdWxFUmZHbkFDWGNoa0Yzei9ITWR3elcwTUFFaXptQmwvREo2ZUoyU01PSG1Uc25YbElGRDRlcFRrYnFBQ0dpZ2I1UExFdHdQRVRjYkNRckM5YUtTU1FnSTdEZXd1aWlxM2J0Y0RUWkIzeEI5WWxlbmhpU0FXNjIwcmwzc2ZjY3d3eGFSOHBDV2Rzd0x3dmFxcDhjM01PV3RCc2xPcmVTSkNEcWgvdzBYbm1WMFJVWFpNM2JvUmkwVXhsaHVUeDFlM1NTd09pbTlOczNYV3NoTmI4Lzc3VkhnUWhRVFlSUU1NRllYaWRmMElCKzBtSUpocWNoQTlUeUY3dGRjSDhrUUJUSHNEWS96bFpqK3EwNlFMd0JkbTkxc3IyK3VzZmxlaXB3WUMrcmdiNHROVnA3VU5rYkVqTnR6ZWZsTi9VRTlkbHZtT2x6V1dtZkh2NGVkUGkzMmJmeUNRS1d6SGJVVEV3NU0yVFpsZnpNaTFWUjVsaDBxQ1lqaDNITUlmL2MwcHBKd2I1b1lFTnBBenlxbnlmdmlTV3lBYzc2L1l1VWwvb2FVaysrYzBZc2d1TGo5ZGFQdVVvemhoZ3VjSytQRGlNckI0ODU1Mk83VWg0aHRwNmZ3S2dJa1JCTVFIUTd6MmV5WXovV1AwQm9ZZVhjOGc3aUprclhFNzA1bFo1bXhGU0poT3E1WlNleVJSb21pUm41K3VRemM5ZFdWQjBYb2JURXdOc0VRM2FIZ25JY29BczY2UGplUT09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDI6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg== \ No newline at end of file +http://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== \ No newline at end of file diff --git a/social_core/tests/backends/data/saml_response_legacy.txt b/social_core/tests/backends/data/saml_response_legacy.txt new file mode 100644 index 000000000..557bb59e8 --- /dev/null +++ b/social_core/tests/backends/data/saml_response_legacy.txt @@ -0,0 +1 @@ +http://myapp.com/?RelayState=testshib&SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL215YXBwLmNvbSIgSUQ9Il8yNTk2NTFlOTY3ZGIwOGZjYTQ4MjdkODI3YWY1M2RkMCIgSW5SZXNwb25zZVRvPSJURVNUX0lEIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDUtMDlUMDM6NTc6NDMuNzkyWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI%2BaHR0cHM6Ly9pZHAudGVzdHNoaWIub3JnL2lkcC9zaGliYm9sZXRoPC9zYW1sMjpJc3N1ZXI%2BPHNhbWwycDpTdGF0dXM%2BPHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM%2BPHNhbWwyOkVuY3J5cHRlZEFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIElkPSJfMGM0NzYzNzIyOWFkNmEzMTY1OGU0MDc2ZDNlYzBmNmQiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiLz48ZHM6S2V5SW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI%2BPHhlbmM6RW5jcnlwdGVkS2V5IElkPSJfYjZmNmU2YWZjMzYyNGI3NmM1N2JmOWZhODA5YzAzNmMiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDc0RDQ0FobWdBd0lCQWdJSkFPN0J3ZGpEWmNVV01BMEdDU3FHU0liM0RRRUJCUVVBTUVVeEN6QUpCZ05WQkFZVEFrTkJNUmt3CkZ3WURWUVFJRXhCQ2NtbDBhWE5vSUVOdmJIVnRZbWxoTVJzd0dRWURWUVFLRXhKd2VYUm9iMjR0YzI5amFXRnNMV0YxZEdnd0hoY04KTVRVd05UQTRNRGMxT0RRMldoY05NalV3TlRBM01EYzFPRFEyV2pCRk1Rc3dDUVlEVlFRR0V3SkRRVEVaTUJjR0ExVUVDQk1RUW5KcApkR2x6YUNCRGIyeDFiV0pwWVRFYk1Ca0dBMVVFQ2hNU2NIbDBhRzl1TFhOdlkybGhiQzFoZFhSb01JR2ZNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNEdOQURDQmlRS0JnUUNxM2cxQ2wrM3VSNXZDbk40SGJnalRnK20zbkhodGVFTXliKyt5Y1pZcmUyYnhVZnNzaEVSNngzM2wKMjN0SGNrUll3bTdNZEJicnAzTHJWb2lPQ2RQYmxUbWwxSWhFUFRDd0tNaEJLdnZXcVR2Z2ZjU1NuUnpBV2tMbFFZU3VzYXl5Wks0bgo5cWNZa1Y1TUZuaTFyYmp4K01yNWFPRW1iNXUzM2FtTUtMd1NUd0lEQVFBQm80R25NSUdrTUIwR0ExVWREZ1FXQkJSUmlCUjZ6UzY2CmZLVm9rcDB5SkhiZ3YzUlltakIxQmdOVkhTTUViakJzZ0JSUmlCUjZ6UzY2ZktWb2twMHlKSGJndjNSWW1xRkpwRWN3UlRFTE1Ba0cKQTFVRUJoTUNRMEV4R1RBWEJnTlZCQWdURUVKeWFYUnBjMmdnUTI5c2RXMWlhV0V4R3pBWkJnTlZCQW9URW5CNWRHaHZiaTF6YjJOcApZV3d0WVhWMGFJSUpBTzdCd2RqRFpjVVdNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ1lFQUp3c01VM1lTCmF5YlZqdUo4VVMwZlVobFBPbE00MFFGQ0dMNHZCM1RFYmIyNE1xOEhyalV3clUwSkZQR2xzOWEyT1l6TjJCM2UzNU5vck11eHMrZ3IKR3RyMnlQNkx2dVgrblY2QTkzd2I0b29HSG9HZkM3VkxseXhTU25zOTM3U1M1UjFwelE0Z1d6Wm1hMktHV0tJQ1dwaDV6UTBBUlZoTAo2Mzk2N21HTG1vST08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BTElQdkVNVUVGeXhrVHowQ2N4QVA5TjV4Y3NYT2V4aVV4cXBvR2VIeVFMV0R5RVBBUDVnZ1daL3NLZ1ViL2xWSk92bCtuQXhSdVhXUlc5dGxSWWx3R2orRVhIOWhIbmdEY1BWMDNqSUJMQnFJbElBL1RmMGw4cVliOHFKRy9ZM0RTS2RQNkwvUURtYXBtTXpFM29YOEJxMW5Ea3YrUWh4cmQwMGVGK2ZMYVQ0PTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BRVpUWDhHTkM0My9yWStTUVlBMXRudHlUTTVVNkN2dUNCaktsVEVlekZPRjBZZHhCWUdFQVVjYU8xNVNKOXBMemJ1L1h0WGxzTkVMZTdKdEx4RUpwYUxubWFENnIranNWczdLaTBLNHRTMGNBUERDWHV2R1FoMmFOVjVQOGJ3N1JWUGhLOGQwYlJ1RklGR09FOHMwTTZYOUpxWDN4S0MvL1lSbVVoeDlybnU3ZWlwMGh5ZitPaUZiVGR2SDY2NTB2LzQ3aVdKcDNZeFlUV0QyMHBNbVRJMUpwWUEwYjByWVFQRkR0RU93d0JxYktxanRJc3ZYVFJzeXJhQkxvbnFOeHN5dHpEWHEra0JsMXp3WGUvSE5QcUVQblczdnNxaFhZcDVGM3dkWThkKzNCOTRMZlpOdUd4a0p3VDNzdVR0OGY5VHRBSlI4VytBUmtzT2M4eDBVaVNsVG5BNHFHOTBLMTR5dkVoVHcvd2drZjFXV01RT3dpZDNpakFYbUV4MU5MbVZvYUxYb3p4VExkTjN6YnJ6VEJIRXc3R2J3ZEdrdU5pMlhZOW16YUgwaWtGRm51VUxjMHUwc0pycEdGdzlaK0VlUk44RzNVUVZ5MjhtS2g3ZFBwWU5KbzhyajIxZFFaK2JaeUtTUHZablU3REkyakdJRE5US1g2ZkVyVWFINGlOTzN4cUU2Vk90L2d4T3BMNE5VNUhLV0Q0bG93VzcwdUJjVEVQRmhwaThpYUovdTB6YzUvTEhvdVBjMzByc1RLZFc5cmJLL2NWaHNQUHErZzA5WHZpZ0QweTJvN2tOc1pVL25tRXFiSzBKOTBrazhCR3I5cXRSczY4bUJnSURtUHVwUkhwWjM4eXNnU2VZN3V0VlVaSG5tQ0dzTzZ2NDJ6OTVOK05Pb3RCTEVZbFd1ZEdzYnowQWc4VkRDSlY5ak95QW95MDZyL1AyUHBsOFhjdmJza2d2T1BMMWdDNnVYbVJJS1lmOEw4UDJCNXVjN0haK0dtUHNOWXRLS2VKRDFFUHovdCt2NlBIbXNVb3dsSDhSd3FMRHdtMUF4dlNLQTR3UXBlQ0dQd3A5YXRYS0lWMS84NUZzRWMzajVzNjd6VlRybThrVEpydXV2MDZEdFVRZDNMOFdwTkV4cWhQait6RUp6U3RxSG04ckhNMVhNQUVxdVozc0xycTVqLzFSNlpqS0dOdFJCbjhwOE5ERGtrWm0vWTV5TXlJNXJJS3U5bnA3bXdaaEVpeWVHeHdxblV3VVMvUzVDRjNnMHVidnd4eVVnalVvd1ZvTkNqYktBbkdtT2VCSW5abkh0eGdIVUhVOUVlTFdyd2pRc3JtUmpJV0R2RkZQa3l6SzJDL20yaitubmNxc2E1OGRLVXZxcGR1VTRJYnNPQng3UGpXdXRBNmY5bXd6YWxyRU1NK0lGR3VPdk9HMC93eUdzQjZLREV6bldjUC83NkQ4angzaHZFSlAzN3REbFgreGM4Qno5TXdKdkd6VG4xbTdCb2xoR0lzSXlCTys1ZXpXa3RDWVVIUURGVE9wbXA0MDlOWHp6ZUNTUGY1U2NDWG5YYjRPd01ULy9VM1JFUnRRbGMrNmU2WG1JRjhoRkJVc0taUUJsS2ppSDkwZHlzYWlsNmN2V3UyQW55Q3QxbWxXcHFLc0MzU2RTRVZDTG1qRjlUQUFUMEtFSGdZQjg3RjZtZUpTTysvOXkyZkRuYVVvUUlUVzdubnVuSCtkT3dWSGZMU0wyL2N5YTltNlQzR29TSVNMbGJPMVRzalhKclVkZW55OTcvM2tkNmhFQlphdGY1U3NETFQ3SjNsQUVJNDROeXJ0NkIxQWdod2JNdkpqd1JNTXRNdUJLc3ltUytKVzc4UFNEWXQ4MG9waDJQTTc1N0tBNCtUMTAvYnZaQkE5Vk1OdVpqNVV3NXRWMnFIS3dwS0t6ZVVETUFiQlBRaGpYcXlQZzFKa09rd2RQMUpnOHRITjJTelBZQTlmT1htV0pBZGJDS2tMb0F4ZTV6cDZBUzYzS3FXMmFmSUt6SHJ3RTJmS1VtamppeURvMnNuMkJHbWtBaTRzbnpiVzc2SUQvSVgwd044aDBaQ2VRc29vKzdtb1RCMEJxSnBkS1MycXlsUktoc3BSTC9henVQdmxaK1pwckJxdXpJdEZkNFVLMkpzQkp6VXcwZkpxcTV1bk9PZENzVWM3SUU3QTNmZ1NmZ3NBd1R3WFZJMEVoME5ySWZpMkFKV1Z2VFpEMys2eFZ3dS96WWhuVjc0VXkvMFE4Mi8yQWtpSGpFRjNJVGNLWHdTNTB6bWtLakxjZDJqa2h5TUFYMWRoQ0wwZElFMUJoN0RNamVvNC9YbjBqSlpPL3Rrbi9xZmYzc3RNb1BYVG9KTnBIU1RjR2ZheGtaMzJYNCt3Q0xPc0VBRWxlMVZSY0kwUkZyOFhHTSsxWU9BTjBodFdGcFMxaG9kSi9OczJqL1FnUVNEemNpQ1FZeUFDd3lFRWZDZjZybnR0VmJyTlJQZWlmSHhBM3B2UnZ5ZGRhNDE5cXl0ZXI0akJ3cmw3ZUpuVnJ2VEprR2VhU2FRbDdXWk5SQXBscXRnNnZPYmpiMHZDRWlFaFhKbmNzQUhxcXp5QTRGeWFUVGQ2R0FySU9adUNxRWVoWk51T01lOVlrMVpya0VkR3pIalJESWk3Q1BKQk12NEZ4ZHI3bnJvN0I1WEhKb0ZMNE1DSUtOWWU2aWZiTUtYOU5uN1FWdnphUmY2UXlaSW1BWENQZndvU1BkN2x6NXl3UDJLSUIyaGhFMWt5eVZ5YVc5T0praWpUY3dvUnZrSXhIU0RqMXFqeGxueXh0QzhVZ1pNWmlwcGgzQXJpcjRiekIzUDhIbGIzejZ0OW51KzZMemNiN2ZObVo0UHluaU50Vk9OQ0lHbEh4dTBSY3hQK3cwUXNsM1BtTzJLaHBpc2RIanhvSUJ1YVY1NXdoTlFFNmdNNFBrT0xINDc4Rzg4bUxkd2s2RFpkWVl4L2d6RWE3b3ZIL0pReFp2TzRLdFVTNmZjZHJxV2thTFg1cEhkNkdneFBGZ2NFc2Nad1ZqM2hCS0xFQmE5L0dodERINEhzRnNRbmpPZnNDQkNzN0tjRitmTi9oSUdUeHFqTVlKVHJRYmNtdWF5dk9xR3RQMDFPcXltR24rVm5FSVkzKytQcm95SFN3K0Q0b0JIVG1maFNXRmJLZCtuTlVFS3BhRVIxNkdCU256WktQRVRVSmdRWEw5QWJRQ3RXVjFHb0UzRWNnMDZYaVd2aHFHakpGNldtdEU4dHY4Q25rZmxMNm91TDRvNldpbmx2WnNEdkZrS0R6TDkwUTNsWC9NanBtRTFpWU9uYzdISXdEVGwraFRRcHdsYXJiTDVUNGNkZTg1akNwYU0xU3p1TStiQU5zMHlXVDA0ZXJUVFc2cnhlbXFDTHAra202TVVMTlZOcE1CazBiQjJpRU82UlRtc3VpRlhDUU1xdU5xZjdkWXUwTFFCZzQ0MkJzU1pBV1ZrWEVZblduOURLdTRSby8veEFsb2h5VHozWlZmSkhuWVBSdDloSUErRHVUL3c4T2ZzTURIWnlCelUvL0JEa1NiNkxjMHdraVA3QlhIdjBoNVdud2dNWUxlZDBPalR5UWI2aGxpVnQ5b0FjaDRFVy9EZUlBdkpaQ1BYVm1pUFFYTGVsOVJIRko2bXFiYVo0TCtaZG1ONmQwcFZNZ1FveXhmQTR3dEwwYVpiNnFZYkhibjJMd2VBQVZwL3M2TzVlMVExdnZpZDRTWHo0a2l3RW1LSStIeXZEQ1pnekpQQVN5Z1gvWDJFWEZ0NGV3SjVmUFQyVXZmWnhQWlpqMFZGSFpyUFQwWVd2VE16bjUva3hoT09oM2drVGdDSmNwNWVsZnp4cEFPNFl1a0NoNHJXdVNndDRqVUJyaWNYbFdWdWo5U3JSZVhUalNHTktLK202NWovUDllNHRHT0RkMk9BbjNKTVQ3Q3FuaDhreTZpZjVjbmpVMmU3UDhTZnBONGwxWEFiZEZEcGk5bVJYamEyTzR1RWFHNGNvNW4xcWNDT3ZNMWYyblFBY1ZGNUFoSXhueS96TWhmU2l2RXdOQ0Zyd2tBWDRyQVE0WldUNldFakFyUG5jb1Y4Z1VRclhxQVA4NDJmK1lNWWI5RHFncmFicEg1a3ZuMnQzcWRldGJHODJ0QWlTamhPcUxNYW9iU2F4cXdWa1lUOHRTMW9rUUt2MWZoZ2t6elpEOE5IQnVQQzdNVHdXS0VCS2tDRUUzRWRFMXhNQURLd1B1M3NSaGpSaExXZyszZ2srejJtdlU4cTBhTlc0Y3hObUdoekx4eEY0Q3NFNStMQ1cwOWFpUVJOM1VvWmg1aktBZzBiMlh3WHBLS3pycUVTY1BYdnI0L1dWUTMyMm5qRWRvQVdXR0t2WnBKMlRlREo0eDdiT21LVElFc2RHWU1UZzFVaEU2eFFQcnhqS3dWeGFJNVJyaVE4a0xpaGgwa0t0WHQvYTVsSDhzUjVwR0ZISGZ3dlNVb3liQTB1eUVDNnNRVitPbTVReUZmRmpqZHFCOGNpOGxQS1hLTHFCTHJ6bjNmUkh3TmQwbzFiRTg0aGllTkx5UlhZVmhrRCtFNEpGaVd3ZWt3U3VWM3BjQk9ybnRVU3RoWmx6M3hIUURUVGNJNWliOFJyQ2swZEZ6YTgvQmw3VUdtWlUwSXZ2UmdvVXF2TXNHT2dMY3pGWmRpZnJ5aGNiUTY4a2ZzZ3lCMHppdC9MN1BSV3V4RkdYdDFoTVZSVUZ3WXBJS04zVkI3cXVKZlgwamZsU1JaRndMaXdlK3VhYndmTVZ6c2doajUvOXZNNzcwK0JaMGtJcE45NzBTMG5BbHl6R0h0aW1nTUl1RXFhbUt5QTNTQlI1aHZIYmRyNENnTHFUbXIzbFFnWmpnSkNvN1FXYUJWTXdCR0RpdzVOVVhUUnBycWc4U3h2eDlnNWZwbXMrL0o2QjFEelNTM3ZRZzgxdHFRU1ZDWVJpc0Y3M2VqZlFuZk4zcUszd3RJRDkxQnRISmFvMEFaUUdKVFpKOXVsZ0kzV3hzdWR4ejB0VHVpNlJlSWpmSWsxekZRdFpwRExGMnB3NGpTQVdQTlJqNDBYdVIrRzFUVlI3OVFiME9FYkw4RDFoTU5zWmo3MTZNbUhSOTlKaUxNdm1FWHV5a1V4VGhGYjRMTzZVbW1kU3UwTlBpMXQ2NmNkYURpQWhMaVBFTGdUNkZsenA2T2FGSGNSNjRncEtyemtTNDJONEhJeFpNa2R6M0FsYkRhK2pOWHZPR1l3UWl5K0xNNENZWGtrTWtHR3ZTWis5R2xWQ0l5RXBJaXIzbEQ3bmdzZGk4emxGWDYvekNaczlQSUtwZFZlSGJGZi9GS20wV3AreHI0Ykd0R0RrVHR2Nk1Manh2YU8zanFHaUFWeERKVWFkTVBlS2VHSm5uempTdnpKbGdOVHV3c3grRnF5L2dPMkwxMGowWmhDWi92dE9NelVjNjl3cGhKZm9FNzU3V3lOeFJOcThJc0Y1Tkg5Y0x0b3UvbUNxOTc3YnZPSkRrSURCN3lKWEJ6YUhVQkJuSXJra1Qyemg3bGJmUm5SREJUSFZraVZMazVESUxqeC9XL1BSZEZpUUM2SzRmZGx4Y29JbzlMcnM4ZFVWZkt2TTNNYnJ6c1hGT3ZtVVh0K3NsZldvd3UyTC9ndG9mRFhvTUJZZnlEcWIvWlRaRWZ0MC83blliRm1relBEUlZacU5SR0F3YWZVNTU1UjB2SWtNbGR2VjdKUzhNT1BNYWlXQVBpelNLRG4yRzNvcys1MzRFQytaOGZnWmFPVWpZL0xLME9vME9RMmhvNUV6MGNMYWpwUjFINk9FNEhvUm1ydjQzZkFjdGpYc0hYdi81RXg3emdrWk1NZXZhTFNEdjZtcjFGcDk4QXR4L296VTFGVDBoMDUxcVcwR0g2VWpRRXk5aExSZDBBMnFkUTRMZXpReDNvbDFTblhsamt2MG4zTXFlaFozOC94bzZhdHFDdkJtQkc3amlUdXd6YnlVUngzRm1TM0NCNllOYnFON3hPYVRZRnlkOEZDL01nY0xGQmMwS3F4MXllQ2VUd1hucldQb0dvdllVQlYxYjA1cWtIa1d5V0RUaCsveXJFNzF0RjNxbUQvd3F6cUJyNE04NERtWWVuQkdFOWxtb3FIZEMyWnRpK09KVFZKcmlHZWxQQ3RjZnZRaUlQcHdDZ3BFNmg1ekZhRndLajRuZGtBUkRpTC95L1EwWTZxNU5rM1g5RURlTmdjY1pIcFdmOUpKQ3M2a29wdXRtYjdDczIrbVJYdER1S09DaGY5UVUyN3Bmb1NJaklYK3NGdHY1c0hhSms2aHBZMlpzUUhzaTBYbFowc3FMTnQ5ayszdTVnYnBSU1JCczlHaC9BaVY0dkNyYTRkOTh5U0dCdzRSR1FhSStpQ29RaG9YK3lxc3VrYkx6bXJUU3FXMVRXaXJReUlHZ1Q5VnFERE1mUzAxeGdQSlNFSTlIWlp6TGlFVXVGMm1CMi81Y2dqaEFUaWQrdGV1UVB4aldhN2NSc2t5YUhuTENjQURVUU9ESUFPVjJDWXROcnAwY29ZL091S3ZzaXlJT0lacVJ5dE1PMGVNZ1ZJWTBzWmdxeVEycXlubUx0NDBmWmd3SFVyV245Zm9TYTNtMkVRTy9uOS8yU2NuelJWdVZpVnNjM0tCSElQL3AzNlJlSWowTGlNcCtPQ0p3SHlLVW1UeDRBU1V0dXVhWktlRHl1QjlxcXJuUEFNWUVCeElsTGFvdXMzV1pHakIrcW9ub3QvNmk1UE40bUZjbHFDcUxhMGJHbks4ZnJxYy9yd2tuVGV0YUE0c2tXTEw1L21qNEd5MitFQkh3a0x3UXd2K0FKdmZTOXYvNDl1LzY0N1ZFYW15UzdZQ2ZEUHNBQUREQ1FFcWJNQ1h2Ui8xVmEwWi9YUWhoNlkrZUt0MEVpRDdpNmRZODJtQkFoNEJMRmRVV3VGZHVrdUVwaGZ2WXB3N2loVjNxTjB1NFM1NTRXU0dUa0ZsdlpYNG1hbkF4a1g2ekQxS0NWaEFMdEJnSDgzdkhxam9uc0lwOFMydHgwZ0tiYzEreHVaRVppVWlNVVlVdTByQVFsRFcrZHJoN3lVRHZqekFHSnBmTk01eThaMW45em93VzZ5YW5VZWFBNjhSZDd5TUxobFd0NVh6bGhBTVZDZmZYZ0pFelR1YzJEbENVOXNMLzVTVkRaV2N4R1E5aFM1cnJtK2VyQ1Jxd2FJQk1DNUtza0RCZHdOWmh2Q0FCdEpqS2Vla1FUSjd5MFp4SGNhbGVCaU1rbkYwZVRDZzFvUEhPUVZLQ3V3NE94cHRZUS9xS1V0TEFIWFZ2OTlLMGRWcWZDMmpVQWlHQmVYa0t3aGRYTGtJYlZxU0EyZmxraXBBeEhYNnByUEExQjF3eTVab3hPUFg4RVExOW92eXpBbFg1dHU0OXEwWC9PSExFN1o5T1cxenltRXR6ZFpyNXJZbWtFcVdtcHVSNU5jeHFwTWlZam93dUNXZWhubzIyeG5JM09IQ0xDZkFKaHRrcklhL1hPc0tZRFpCRzFJMGJsN2taR2R5cEtUQlhYdXl6WE5WUlU5L005ejhaVytwdG1oZ2NOUzBJS2VaaSs5bFl4cWRlS3lnbldTTTV3czdSYUpmNlRRZTNSaWJZUjFvNkhwRzB2VHpiTEtQZTZnRjJGODdiWlBJei9mcTNLWnZiM3UrSnhZcCtJVjBtQi9VN29YelhRRk1RK3VmWllpNzUxbkx6WlVxRE1ybU53TFJPVUFNUk8rVnJtblkwSVB1cFBVMXc0b0hBb1dnVGRnTk5pNk1uTFQ4V0pmUlhjT0pKMk1lbUc2K2ZNeHNZUU52UVJwa1RGY05vaFV6Y3ZjcHJ3NUV3WEVZQTJzbzczL2MvY3RIRGcreU05YlF4REppUlltRnFydkhYb29hS1JyekxnUjZLVWdoM3ltaWxaQ0lSSm9KbTE3aEtHM1pxTTE0Lzl5OUc5OE9BZjNkVTlqMDk3aUNlaEc3a2VxYXRJQ2hFWmJqbmQ4Y00rS3djN2FtVWp2ekQzQmNvMHl3MDJxT054OWF3OGhSblZiWDZhdkRJbGhySHZ6SU44MzFvUjljRHBwMG1DUEJXZFVDQlNqVGJ1RkZqRC90WElSbGxlT2JraFFKSUdSNlE2U1MxcXkzT29WT1VheFl6THY0U2s3dndrQUMwUitGREVIeVFZbFVhbVVkTWcyUmdwRUdhSVd1V3IxaGNnRm10QmREV2g3ZFBuWTF0U3VKOC95MXp4NkRvN2ZJYmNFenBBK2E0ODNtRG5vemdld3VmaFdqVCsvUS85WlEreFQ5UWJBT1pQSXhHV3VhSXVrVk8zSWxvZDhJM1NGZFJCTHY5ZXBDNzFLeXpSdVlpMktkOHJ5NVNINit1WnMxUHlZUlpRakdDK3Q4VzRtSE82Z1lFRWVXSkJ1UWhnSHdmV2xhZXlWb3hac0NBQVZKRUllT3hPZDZtNW45OHRCUDdHTmgxT1M0eDRCS2FVN1A0UVQzNVVIZW5meE84WWFQUThmbXlobUJhSVJVZklBTVN2ZTJZRFp5SWNNTTkrN0tNSVVabzJ0eXRvYzdCOGVvZzBNaUkrVkpFdFg0c29FRjFSWkhQZVV3NWlCTjI4OTh2MmVTcGNnVUJhWHFzOUN5VlZtTVJQMEtLUDJ1REt4MUdJcUhjS0ZCOXVQVWRkQS9vT3dNa0tVUWsraFZVVDVPbEVMdjd1a0FBUEE0eE4rZkczVmYxeUVKV0FiVGx5dWtGcThjNXBTRkY1cXVHbUgwVmVpQzVvVEFka1VES3Z6WGhWWUs5c3BRYjNVZ1Z0Qld6N1ZScnlOUVVST3BIZU5xeDlhZHA4YWREWCtRSHJUKytYblN4VVI3SVdGanlNTkZJRWlMWmkxdks1UVVrZlRDUU9qdjh2SHdiUi9MRHF3Z3M5bXdsT3pPY0RLdVBVK0dTb2lnVFdRejRWN0N2SHRaVDI3WUdKVG44RFFFM3IzdjB4aWxvODJ2U3VXSDg0WEU3VEJsTUpFb2R5eDNDRngwVUVkc3VhRHBPSEV3UjZYNlUyU0xseERYSXVZeEhlNXh2NjI4bXU0bDRMSnBYUjhkYmljTEZKQW55Q0FVeDJLb2dDamt1cmU4bXNUZktDbG8wamFlN1hNR05PSk15b0ZYbVlHZUh2eGhNUGMzTEtYLy9VY1p0c3p3dFJrQmNFdURXQysvQWNWZVBOSHVOWWI5MEpIcnRucGg1ZDlhL1lpTkpzY1N3QTFwUVZrdW1TQWtPQWdLdWRzcnl3c0N3Zkg1anNydVpHUTJDd1hKRXQzUU4wU2NLUlVnT1NCQ3FYa1BqZDVSVzJuOFZpamt4anovbWptakhCNmk0eHM5NEU2Nzk5STAyaldYNVd3UDZhTFRaTGt5TjhxNDUxT0RmeUZVZEY5WWsyZXQ5VUpsV1NzRFJMSWVCd0ZyQkEyZTdyRWsybWFLVUNCRW5PUWM2bUhVMXQvZ3gzK1VXVVFXbkpMZVUxbWUvbkFEdy96UGUwd3d0Vm9BaERZdDBoR1hQblJydjFoUHRGS01CeWtqckg3a0J5U0R3WDlQMi9XZkNkQlE5K1J4cHRsR2hvRmdpMUs0NVlOeEpEd05wTmd5MDV2WXUzVUtrMkpRYVNGUzcwK0Y1NzluRE5RenZpK0pPRlRsdDFmWDJGNXk5NEV2NHZobWRQSmRVOFVVRjU2Ymx0emxKREVFdmsySlFrOTM0aHpwTXJGZ1d3ZHUxUkxxSEhCN2h2T2hnaHNqV0ZGY01zNjZaRUtWcVhKUytxWWNVMHk0akwySVQrNlF2N2pvQ3BWbUdzUWtGY1FyblhxOUJiOTdaUS96UCtwaldmWTU0UmNRVlMydUU1YURObVVyVkdLK3E0d0xRcUhuRVViT2puSHFFeGlacUtxOVdRaUtUK2c3QS96bVlIQ2k0YzFTejRNVWhHb0t6U2l4aXoxYUNJUEJXdy9vczR2cUVqbXgzOGx6YnV0OWNWbElzeGNkTUpUTERRK3ZOZ0YyY1ZRaVcxRTQ0d3lWcnI3TUFaOE9KRVpFSzlEZWt5MzJQUkFuSkRUVXVqdGFscmJ0T2VOczhyS09uTjcvNFRqUEwvZmRlbEI4bjA4WXdSNXdmbU42VGpGWUhRSDFjbUZmK1AvNUxVMTI4Q1pEYjNQUStxMlFJazV3aE40eGwvcy9lb29pallmeWtDcm5aSEhHWkluTGhoU2pWbk5ISWdTL203VWV0NlhBTDdvZUl5UFRLeHVnbDJzRWtUQzNnZ0tjTnFZR0E5U3ZlYVlaQ00vWHNQRUtQbWs3QmlRNmprWFBKaE1yREd4Vkc0SW9aSDgrYjBrUWJYR2l0Mkw0L3hZdHh1bTVzcFNPSjdsTDltVFpRNnBxM2JOaTEwZU1mZ0ZWaDc3NU5JRlc0SEp3U1FtaTU0bk11blZTQjhxdjZKc0w3SGlsZ2N0ZHFSNThTTjVad1lCa2dOR1hzYjA1QXJWemVXbHh1Y21BSHNPT3dyczFnMzh6bTRZN2ZPZmducmFhV1kxanZZOFlEODZQZThkZzR4cE5paTg3UnNDZk5WK2NKVmMraktFdnpuZVY1Zzd0RmlxZCtsZHp4STlKemdSS2t0WUV6RUpRSVU5M2UvclJaN1lrVkZtNVV1cjVhMWYzcG83T0VtYkJUc2MrQ1FaOGNnYmIvbUphRXJoa3NyL3JURjBNcjNxeDl5SlJWSEJ6YWNWd0dScEFRaURPdnJkWU4xQXBVOTRyR1lrVFVzdWs1YjE1Wll2QVZxRlRzVlVMaS9HY29mbEljMm01Z2RFTFZOblRmdXY1Zlk5S1NlWHFoUU80S0pOYVZmbHAwQ0VKYWFFZFNLUXJJNXRaT2w1RkE4VXZlNmxTWVd5TVk0REl4a1RiT1JoWHVBdzR6b1RTMjgrN3d2TXhydVBkZnlKbUJCTkhQdCtEYmdKNHovcHJZWUhpTmFMTXNZamtQZE44ajNKZDczQXJFZk92Um52MzYxSVVVMFg1RDc1dlRSdlpkbzMzWERzanRlOU4weUo3K2lIQnF1a1FJY2pIVW9ic2RQN0hOajBVYWNSMHIvTmRlVTlGNFBNc1VLY2t6Tk4rZGhyMVI2d1J2R1VZb1pDRWJaWlJMWEt4QnA3SElUNEVQUktHakIvdW1xTFhhMXl6RWx2QW1WQUJhMDFZN3dGdk4wM2Ywb25FbUhTM2w1d1paRmV6cjVibnN5T01XVGxhMU5kaW1ZNXNVeE15VFliZmc4dzB2cXNEc28zWFAxYndLdzZ3M3VIRGQ1UHBSWnVDSnR0eWk0ZzJGeWI0Ymg1UU42ZkdORTI2ekRGN1Y4QmJwZXJLNkFKQ0xTWm5kaDZMMTlPUTBram4xUGpEMGk4c1BZcGFXOWxVeVJkZElPKzRWQS9LemxPUzJ4M2s5VUtUdElsTTBUSVdtZXFIS0dYUVpocGpvVGI2VlNKN203cjZaaVlQMnVsQVVvZmVWL0o2eCtzckxEQXkyQ2ZFNnFrREZ1OU9NWDBBSXVnN3loQUtOMDRyT3hVNk5tcGtjOUZ4bXUvVS9vR3hHdmIzeFVFTDYwdE1sSE9EaWtqY1I5RDJrKzRwbEc1WnV0d0FIY2kwRU02WHRrVEhQOU5QMlRTR1VFN1E5SGYvU0VEc2V0a25hZXhvWmhDczJLWDFMeU5JS0U0N2pkMkR3MTUreDRRVXV0VUFTbzU5Q1lHMVFBeW9BVVhrV3dtbXkzTGdTUWp5T3ZLV25qaE8veWpPd0FyWGd0NFBrSVVnZDQ1N05ReFpMbU41K0J4NVJoQ0FHdkUxYmxOZjlMek9keGJiaG5VZ2Z1RDM5MXVSRkhjS2RYREY3ZmVqb3gveThtaWZJcTRWVzQyajBHQnFOQUtkK0prMnJCMW9hOTRiT2hxcVVzanhqWnlRaGRXTzhNblR6T2tOaGVpZXU2blYxcW5yZ3JHU2huWTNJMlczb29GNFNnczRjZ3drZ2h2dHpFa0xUbU5OUm83RTdudVRuMkxJcmlGSnlvTmZQdUp0aWN0S0JtNzRGZytkWVBTMlIzTzNmOWxBZWxiVWZjbzZGNU9EL3hkS1VuRTh0V3FOMExVcDlWQUptWVZYZFVDaGJ4MjM4MWtDaStLNDJoRzUydFNQYU1hb1dTb0xQY2Zrb24rc1pYdjdEdEtwZi9HTzdhcUMza1pzRGpva29haHJGZGJWSlNTZWhrNGp5K3RzRHplQnJKSjBrMVZrUnJHN1NoVHZjTmd1cjVucVRUTEE5dlJMQmJNTTlhNlI1NEZ0Z1pQOWFKMU1aMEdCcUVpMnF6Ui8yd2tYQlhwcFhZdi9TcU1RV1dhbTVsSHBMVktxaDN4ZHRjNFdmck9mYldsbU1PNXA5Z0JUSFp1YUcxVGFkZXFRVVpKQmZBS01ENFdSR0NsMDFaeDRTVzE0YzZrdnFKdXExL080N215L3RsVHlLWndpYlBkQTNRMVVGd0I3R2Z4anEwaDN2ckxFbUNrS3Vsc0VBUkN6UnZNVjJSVnBVbFpUV240Y1Boc0hjcTNROElHSUYyKy9nOENFSU4vMU8xcVMvMkpXcXlDNmtIb0w4Y2R2R0VHbmkxSTNDTk1JcXhxaHhJL1V0R3REc2VwYmwrSHI0elh4MzZna3BCbXBoT2xkTFVYTHAzVEtibVVZRWJSWHcvZmRmeFQ3WDdZUFhHQ0hHVG1uTzk4WkxDOTA2Zmkvekd2b04rNlpzbCs3MkpWMGxJWEo0V3dZdWxFUmZHbkFDWGNoa0Yzei9ITWR3elcwTUFFaXptQmwvREo2ZUoyU01PSG1Uc25YbElGRDRlcFRrYnFBQ0dpZ2I1UExFdHdQRVRjYkNRckM5YUtTU1FnSTdEZXd1aWlxM2J0Y0RUWkIzeEI5WWxlbmhpU0FXNjIwcmwzc2ZjY3d3eGFSOHBDV2Rzd0x3dmFxcDhjM01PV3RCc2xPcmVTSkNEcWgvdzBYbm1WMFJVWFpNM2JvUmkwVXhsaHVUeDFlM1NTd09pbTlOczNYV3NoTmI4Lzc3VkhnUWhRVFlSUU1NRllYaWRmMElCKzBtSUpocWNoQTlUeUY3dGRjSDhrUUJUSHNEWS96bFpqK3EwNlFMd0JkbTkxc3IyK3VzZmxlaXB3WUMrcmdiNHROVnA3VU5rYkVqTnR6ZWZsTi9VRTlkbHZtT2x6V1dtZkh2NGVkUGkzMmJmeUNRS1d6SGJVVEV3NU0yVFpsZnpNaTFWUjVsaDBxQ1lqaDNITUlmL2MwcHBKd2I1b1lFTnBBenlxbnlmdmlTV3lBYzc2L1l1VWwvb2FVaysrYzBZc2d1TGo5ZGFQdVVvemhoZ3VjSytQRGlNckI0ODU1Mk83VWg0aHRwNmZ3S2dJa1JCTVFIUTd6MmV5WXovV1AwQm9ZZVhjOGc3aUprclhFNzA1bFo1bXhGU0poT3E1WlNleVJSb21pUm41K3VRemM5ZFdWQjBYb2JURXdOc0VRM2FIZ25JY29BczY2UGplUT09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDI6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/social_core/tests/backends/data/saml_response_no_idp_name.txt b/social_core/tests/backends/data/saml_response_no_idp_name.txt new file mode 100644 index 000000000..2f57b07ae --- /dev/null +++ b/social_core/tests/backends/data/saml_response_no_idp_name.txt @@ -0,0 +1 @@ +http://myapp.com/?RelayState=%7b%7d&SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL215YXBwLmNvbSIgSUQ9Il8yNTk2NTFlOTY3ZGIwOGZjYTQ4MjdkODI3YWY1M2RkMCIgSW5SZXNwb25zZVRvPSJURVNUX0lEIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDUtMDlUMDM6NTc6NDMuNzkyWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI%2BaHR0cHM6Ly9pZHAudGVzdHNoaWIub3JnL2lkcC9zaGliYm9sZXRoPC9zYW1sMjpJc3N1ZXI%2BPHNhbWwycDpTdGF0dXM%2BPHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM%2BPHNhbWwyOkVuY3J5cHRlZEFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIElkPSJfMGM0NzYzNzIyOWFkNmEzMTY1OGU0MDc2ZDNlYzBmNmQiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiLz48ZHM6S2V5SW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI%2BPHhlbmM6RW5jcnlwdGVkS2V5IElkPSJfYjZmNmU2YWZjMzYyNGI3NmM1N2JmOWZhODA5YzAzNmMiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDc0RDQ0FobWdBd0lCQWdJSkFPN0J3ZGpEWmNVV01BMEdDU3FHU0liM0RRRUJCUVVBTUVVeEN6QUpCZ05WQkFZVEFrTkJNUmt3CkZ3WURWUVFJRXhCQ2NtbDBhWE5vSUVOdmJIVnRZbWxoTVJzd0dRWURWUVFLRXhKd2VYUm9iMjR0YzI5amFXRnNMV0YxZEdnd0hoY04KTVRVd05UQTRNRGMxT0RRMldoY05NalV3TlRBM01EYzFPRFEyV2pCRk1Rc3dDUVlEVlFRR0V3SkRRVEVaTUJjR0ExVUVDQk1RUW5KcApkR2x6YUNCRGIyeDFiV0pwWVRFYk1Ca0dBMVVFQ2hNU2NIbDBhRzl1TFhOdlkybGhiQzFoZFhSb01JR2ZNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNEdOQURDQmlRS0JnUUNxM2cxQ2wrM3VSNXZDbk40SGJnalRnK20zbkhodGVFTXliKyt5Y1pZcmUyYnhVZnNzaEVSNngzM2wKMjN0SGNrUll3bTdNZEJicnAzTHJWb2lPQ2RQYmxUbWwxSWhFUFRDd0tNaEJLdnZXcVR2Z2ZjU1NuUnpBV2tMbFFZU3VzYXl5Wks0bgo5cWNZa1Y1TUZuaTFyYmp4K01yNWFPRW1iNXUzM2FtTUtMd1NUd0lEQVFBQm80R25NSUdrTUIwR0ExVWREZ1FXQkJSUmlCUjZ6UzY2CmZLVm9rcDB5SkhiZ3YzUlltakIxQmdOVkhTTUViakJzZ0JSUmlCUjZ6UzY2ZktWb2twMHlKSGJndjNSWW1xRkpwRWN3UlRFTE1Ba0cKQTFVRUJoTUNRMEV4R1RBWEJnTlZCQWdURUVKeWFYUnBjMmdnUTI5c2RXMWlhV0V4R3pBWkJnTlZCQW9URW5CNWRHaHZiaTF6YjJOcApZV3d0WVhWMGFJSUpBTzdCd2RqRFpjVVdNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ1lFQUp3c01VM1lTCmF5YlZqdUo4VVMwZlVobFBPbE00MFFGQ0dMNHZCM1RFYmIyNE1xOEhyalV3clUwSkZQR2xzOWEyT1l6TjJCM2UzNU5vck11eHMrZ3IKR3RyMnlQNkx2dVgrblY2QTkzd2I0b29HSG9HZkM3VkxseXhTU25zOTM3U1M1UjFwelE0Z1d6Wm1hMktHV0tJQ1dwaDV6UTBBUlZoTAo2Mzk2N21HTG1vST08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BTElQdkVNVUVGeXhrVHowQ2N4QVA5TjV4Y3NYT2V4aVV4cXBvR2VIeVFMV0R5RVBBUDVnZ1daL3NLZ1ViL2xWSk92bCtuQXhSdVhXUlc5dGxSWWx3R2orRVhIOWhIbmdEY1BWMDNqSUJMQnFJbElBL1RmMGw4cVliOHFKRy9ZM0RTS2RQNkwvUURtYXBtTXpFM29YOEJxMW5Ea3YrUWh4cmQwMGVGK2ZMYVQ0PTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BRVpUWDhHTkM0My9yWStTUVlBMXRudHlUTTVVNkN2dUNCaktsVEVlekZPRjBZZHhCWUdFQVVjYU8xNVNKOXBMemJ1L1h0WGxzTkVMZTdKdEx4RUpwYUxubWFENnIranNWczdLaTBLNHRTMGNBUERDWHV2R1FoMmFOVjVQOGJ3N1JWUGhLOGQwYlJ1RklGR09FOHMwTTZYOUpxWDN4S0MvL1lSbVVoeDlybnU3ZWlwMGh5ZitPaUZiVGR2SDY2NTB2LzQ3aVdKcDNZeFlUV0QyMHBNbVRJMUpwWUEwYjByWVFQRkR0RU93d0JxYktxanRJc3ZYVFJzeXJhQkxvbnFOeHN5dHpEWHEra0JsMXp3WGUvSE5QcUVQblczdnNxaFhZcDVGM3dkWThkKzNCOTRMZlpOdUd4a0p3VDNzdVR0OGY5VHRBSlI4VytBUmtzT2M4eDBVaVNsVG5BNHFHOTBLMTR5dkVoVHcvd2drZjFXV01RT3dpZDNpakFYbUV4MU5MbVZvYUxYb3p4VExkTjN6YnJ6VEJIRXc3R2J3ZEdrdU5pMlhZOW16YUgwaWtGRm51VUxjMHUwc0pycEdGdzlaK0VlUk44RzNVUVZ5MjhtS2g3ZFBwWU5KbzhyajIxZFFaK2JaeUtTUHZablU3REkyakdJRE5US1g2ZkVyVWFINGlOTzN4cUU2Vk90L2d4T3BMNE5VNUhLV0Q0bG93VzcwdUJjVEVQRmhwaThpYUovdTB6YzUvTEhvdVBjMzByc1RLZFc5cmJLL2NWaHNQUHErZzA5WHZpZ0QweTJvN2tOc1pVL25tRXFiSzBKOTBrazhCR3I5cXRSczY4bUJnSURtUHVwUkhwWjM4eXNnU2VZN3V0VlVaSG5tQ0dzTzZ2NDJ6OTVOK05Pb3RCTEVZbFd1ZEdzYnowQWc4VkRDSlY5ak95QW95MDZyL1AyUHBsOFhjdmJza2d2T1BMMWdDNnVYbVJJS1lmOEw4UDJCNXVjN0haK0dtUHNOWXRLS2VKRDFFUHovdCt2NlBIbXNVb3dsSDhSd3FMRHdtMUF4dlNLQTR3UXBlQ0dQd3A5YXRYS0lWMS84NUZzRWMzajVzNjd6VlRybThrVEpydXV2MDZEdFVRZDNMOFdwTkV4cWhQait6RUp6U3RxSG04ckhNMVhNQUVxdVozc0xycTVqLzFSNlpqS0dOdFJCbjhwOE5ERGtrWm0vWTV5TXlJNXJJS3U5bnA3bXdaaEVpeWVHeHdxblV3VVMvUzVDRjNnMHVidnd4eVVnalVvd1ZvTkNqYktBbkdtT2VCSW5abkh0eGdIVUhVOUVlTFdyd2pRc3JtUmpJV0R2RkZQa3l6SzJDL20yaitubmNxc2E1OGRLVXZxcGR1VTRJYnNPQng3UGpXdXRBNmY5bXd6YWxyRU1NK0lGR3VPdk9HMC93eUdzQjZLREV6bldjUC83NkQ4angzaHZFSlAzN3REbFgreGM4Qno5TXdKdkd6VG4xbTdCb2xoR0lzSXlCTys1ZXpXa3RDWVVIUURGVE9wbXA0MDlOWHp6ZUNTUGY1U2NDWG5YYjRPd01ULy9VM1JFUnRRbGMrNmU2WG1JRjhoRkJVc0taUUJsS2ppSDkwZHlzYWlsNmN2V3UyQW55Q3QxbWxXcHFLc0MzU2RTRVZDTG1qRjlUQUFUMEtFSGdZQjg3RjZtZUpTTysvOXkyZkRuYVVvUUlUVzdubnVuSCtkT3dWSGZMU0wyL2N5YTltNlQzR29TSVNMbGJPMVRzalhKclVkZW55OTcvM2tkNmhFQlphdGY1U3NETFQ3SjNsQUVJNDROeXJ0NkIxQWdod2JNdkpqd1JNTXRNdUJLc3ltUytKVzc4UFNEWXQ4MG9waDJQTTc1N0tBNCtUMTAvYnZaQkE5Vk1OdVpqNVV3NXRWMnFIS3dwS0t6ZVVETUFiQlBRaGpYcXlQZzFKa09rd2RQMUpnOHRITjJTelBZQTlmT1htV0pBZGJDS2tMb0F4ZTV6cDZBUzYzS3FXMmFmSUt6SHJ3RTJmS1VtamppeURvMnNuMkJHbWtBaTRzbnpiVzc2SUQvSVgwd044aDBaQ2VRc29vKzdtb1RCMEJxSnBkS1MycXlsUktoc3BSTC9henVQdmxaK1pwckJxdXpJdEZkNFVLMkpzQkp6VXcwZkpxcTV1bk9PZENzVWM3SUU3QTNmZ1NmZ3NBd1R3WFZJMEVoME5ySWZpMkFKV1Z2VFpEMys2eFZ3dS96WWhuVjc0VXkvMFE4Mi8yQWtpSGpFRjNJVGNLWHdTNTB6bWtLakxjZDJqa2h5TUFYMWRoQ0wwZElFMUJoN0RNamVvNC9YbjBqSlpPL3Rrbi9xZmYzc3RNb1BYVG9KTnBIU1RjR2ZheGtaMzJYNCt3Q0xPc0VBRWxlMVZSY0kwUkZyOFhHTSsxWU9BTjBodFdGcFMxaG9kSi9OczJqL1FnUVNEemNpQ1FZeUFDd3lFRWZDZjZybnR0VmJyTlJQZWlmSHhBM3B2UnZ5ZGRhNDE5cXl0ZXI0akJ3cmw3ZUpuVnJ2VEprR2VhU2FRbDdXWk5SQXBscXRnNnZPYmpiMHZDRWlFaFhKbmNzQUhxcXp5QTRGeWFUVGQ2R0FySU9adUNxRWVoWk51T01lOVlrMVpya0VkR3pIalJESWk3Q1BKQk12NEZ4ZHI3bnJvN0I1WEhKb0ZMNE1DSUtOWWU2aWZiTUtYOU5uN1FWdnphUmY2UXlaSW1BWENQZndvU1BkN2x6NXl3UDJLSUIyaGhFMWt5eVZ5YVc5T0praWpUY3dvUnZrSXhIU0RqMXFqeGxueXh0QzhVZ1pNWmlwcGgzQXJpcjRiekIzUDhIbGIzejZ0OW51KzZMemNiN2ZObVo0UHluaU50Vk9OQ0lHbEh4dTBSY3hQK3cwUXNsM1BtTzJLaHBpc2RIanhvSUJ1YVY1NXdoTlFFNmdNNFBrT0xINDc4Rzg4bUxkd2s2RFpkWVl4L2d6RWE3b3ZIL0pReFp2TzRLdFVTNmZjZHJxV2thTFg1cEhkNkdneFBGZ2NFc2Nad1ZqM2hCS0xFQmE5L0dodERINEhzRnNRbmpPZnNDQkNzN0tjRitmTi9oSUdUeHFqTVlKVHJRYmNtdWF5dk9xR3RQMDFPcXltR24rVm5FSVkzKytQcm95SFN3K0Q0b0JIVG1maFNXRmJLZCtuTlVFS3BhRVIxNkdCU256WktQRVRVSmdRWEw5QWJRQ3RXVjFHb0UzRWNnMDZYaVd2aHFHakpGNldtdEU4dHY4Q25rZmxMNm91TDRvNldpbmx2WnNEdkZrS0R6TDkwUTNsWC9NanBtRTFpWU9uYzdISXdEVGwraFRRcHdsYXJiTDVUNGNkZTg1akNwYU0xU3p1TStiQU5zMHlXVDA0ZXJUVFc2cnhlbXFDTHAra202TVVMTlZOcE1CazBiQjJpRU82UlRtc3VpRlhDUU1xdU5xZjdkWXUwTFFCZzQ0MkJzU1pBV1ZrWEVZblduOURLdTRSby8veEFsb2h5VHozWlZmSkhuWVBSdDloSUErRHVUL3c4T2ZzTURIWnlCelUvL0JEa1NiNkxjMHdraVA3QlhIdjBoNVdud2dNWUxlZDBPalR5UWI2aGxpVnQ5b0FjaDRFVy9EZUlBdkpaQ1BYVm1pUFFYTGVsOVJIRko2bXFiYVo0TCtaZG1ONmQwcFZNZ1FveXhmQTR3dEwwYVpiNnFZYkhibjJMd2VBQVZwL3M2TzVlMVExdnZpZDRTWHo0a2l3RW1LSStIeXZEQ1pnekpQQVN5Z1gvWDJFWEZ0NGV3SjVmUFQyVXZmWnhQWlpqMFZGSFpyUFQwWVd2VE16bjUva3hoT09oM2drVGdDSmNwNWVsZnp4cEFPNFl1a0NoNHJXdVNndDRqVUJyaWNYbFdWdWo5U3JSZVhUalNHTktLK202NWovUDllNHRHT0RkMk9BbjNKTVQ3Q3FuaDhreTZpZjVjbmpVMmU3UDhTZnBONGwxWEFiZEZEcGk5bVJYamEyTzR1RWFHNGNvNW4xcWNDT3ZNMWYyblFBY1ZGNUFoSXhueS96TWhmU2l2RXdOQ0Zyd2tBWDRyQVE0WldUNldFakFyUG5jb1Y4Z1VRclhxQVA4NDJmK1lNWWI5RHFncmFicEg1a3ZuMnQzcWRldGJHODJ0QWlTamhPcUxNYW9iU2F4cXdWa1lUOHRTMW9rUUt2MWZoZ2t6elpEOE5IQnVQQzdNVHdXS0VCS2tDRUUzRWRFMXhNQURLd1B1M3NSaGpSaExXZyszZ2srejJtdlU4cTBhTlc0Y3hObUdoekx4eEY0Q3NFNStMQ1cwOWFpUVJOM1VvWmg1aktBZzBiMlh3WHBLS3pycUVTY1BYdnI0L1dWUTMyMm5qRWRvQVdXR0t2WnBKMlRlREo0eDdiT21LVElFc2RHWU1UZzFVaEU2eFFQcnhqS3dWeGFJNVJyaVE4a0xpaGgwa0t0WHQvYTVsSDhzUjVwR0ZISGZ3dlNVb3liQTB1eUVDNnNRVitPbTVReUZmRmpqZHFCOGNpOGxQS1hLTHFCTHJ6bjNmUkh3TmQwbzFiRTg0aGllTkx5UlhZVmhrRCtFNEpGaVd3ZWt3U3VWM3BjQk9ybnRVU3RoWmx6M3hIUURUVGNJNWliOFJyQ2swZEZ6YTgvQmw3VUdtWlUwSXZ2UmdvVXF2TXNHT2dMY3pGWmRpZnJ5aGNiUTY4a2ZzZ3lCMHppdC9MN1BSV3V4RkdYdDFoTVZSVUZ3WXBJS04zVkI3cXVKZlgwamZsU1JaRndMaXdlK3VhYndmTVZ6c2doajUvOXZNNzcwK0JaMGtJcE45NzBTMG5BbHl6R0h0aW1nTUl1RXFhbUt5QTNTQlI1aHZIYmRyNENnTHFUbXIzbFFnWmpnSkNvN1FXYUJWTXdCR0RpdzVOVVhUUnBycWc4U3h2eDlnNWZwbXMrL0o2QjFEelNTM3ZRZzgxdHFRU1ZDWVJpc0Y3M2VqZlFuZk4zcUszd3RJRDkxQnRISmFvMEFaUUdKVFpKOXVsZ0kzV3hzdWR4ejB0VHVpNlJlSWpmSWsxekZRdFpwRExGMnB3NGpTQVdQTlJqNDBYdVIrRzFUVlI3OVFiME9FYkw4RDFoTU5zWmo3MTZNbUhSOTlKaUxNdm1FWHV5a1V4VGhGYjRMTzZVbW1kU3UwTlBpMXQ2NmNkYURpQWhMaVBFTGdUNkZsenA2T2FGSGNSNjRncEtyemtTNDJONEhJeFpNa2R6M0FsYkRhK2pOWHZPR1l3UWl5K0xNNENZWGtrTWtHR3ZTWis5R2xWQ0l5RXBJaXIzbEQ3bmdzZGk4emxGWDYvekNaczlQSUtwZFZlSGJGZi9GS20wV3AreHI0Ykd0R0RrVHR2Nk1Manh2YU8zanFHaUFWeERKVWFkTVBlS2VHSm5uempTdnpKbGdOVHV3c3grRnF5L2dPMkwxMGowWmhDWi92dE9NelVjNjl3cGhKZm9FNzU3V3lOeFJOcThJc0Y1Tkg5Y0x0b3UvbUNxOTc3YnZPSkRrSURCN3lKWEJ6YUhVQkJuSXJra1Qyemg3bGJmUm5SREJUSFZraVZMazVESUxqeC9XL1BSZEZpUUM2SzRmZGx4Y29JbzlMcnM4ZFVWZkt2TTNNYnJ6c1hGT3ZtVVh0K3NsZldvd3UyTC9ndG9mRFhvTUJZZnlEcWIvWlRaRWZ0MC83blliRm1relBEUlZacU5SR0F3YWZVNTU1UjB2SWtNbGR2VjdKUzhNT1BNYWlXQVBpelNLRG4yRzNvcys1MzRFQytaOGZnWmFPVWpZL0xLME9vME9RMmhvNUV6MGNMYWpwUjFINk9FNEhvUm1ydjQzZkFjdGpYc0hYdi81RXg3emdrWk1NZXZhTFNEdjZtcjFGcDk4QXR4L296VTFGVDBoMDUxcVcwR0g2VWpRRXk5aExSZDBBMnFkUTRMZXpReDNvbDFTblhsamt2MG4zTXFlaFozOC94bzZhdHFDdkJtQkc3amlUdXd6YnlVUngzRm1TM0NCNllOYnFON3hPYVRZRnlkOEZDL01nY0xGQmMwS3F4MXllQ2VUd1hucldQb0dvdllVQlYxYjA1cWtIa1d5V0RUaCsveXJFNzF0RjNxbUQvd3F6cUJyNE04NERtWWVuQkdFOWxtb3FIZEMyWnRpK09KVFZKcmlHZWxQQ3RjZnZRaUlQcHdDZ3BFNmg1ekZhRndLajRuZGtBUkRpTC95L1EwWTZxNU5rM1g5RURlTmdjY1pIcFdmOUpKQ3M2a29wdXRtYjdDczIrbVJYdER1S09DaGY5UVUyN3Bmb1NJaklYK3NGdHY1c0hhSms2aHBZMlpzUUhzaTBYbFowc3FMTnQ5ayszdTVnYnBSU1JCczlHaC9BaVY0dkNyYTRkOTh5U0dCdzRSR1FhSStpQ29RaG9YK3lxc3VrYkx6bXJUU3FXMVRXaXJReUlHZ1Q5VnFERE1mUzAxeGdQSlNFSTlIWlp6TGlFVXVGMm1CMi81Y2dqaEFUaWQrdGV1UVB4aldhN2NSc2t5YUhuTENjQURVUU9ESUFPVjJDWXROcnAwY29ZL091S3ZzaXlJT0lacVJ5dE1PMGVNZ1ZJWTBzWmdxeVEycXlubUx0NDBmWmd3SFVyV245Zm9TYTNtMkVRTy9uOS8yU2NuelJWdVZpVnNjM0tCSElQL3AzNlJlSWowTGlNcCtPQ0p3SHlLVW1UeDRBU1V0dXVhWktlRHl1QjlxcXJuUEFNWUVCeElsTGFvdXMzV1pHakIrcW9ub3QvNmk1UE40bUZjbHFDcUxhMGJHbks4ZnJxYy9yd2tuVGV0YUE0c2tXTEw1L21qNEd5MitFQkh3a0x3UXd2K0FKdmZTOXYvNDl1LzY0N1ZFYW15UzdZQ2ZEUHNBQUREQ1FFcWJNQ1h2Ui8xVmEwWi9YUWhoNlkrZUt0MEVpRDdpNmRZODJtQkFoNEJMRmRVV3VGZHVrdUVwaGZ2WXB3N2loVjNxTjB1NFM1NTRXU0dUa0ZsdlpYNG1hbkF4a1g2ekQxS0NWaEFMdEJnSDgzdkhxam9uc0lwOFMydHgwZ0tiYzEreHVaRVppVWlNVVlVdTByQVFsRFcrZHJoN3lVRHZqekFHSnBmTk01eThaMW45em93VzZ5YW5VZWFBNjhSZDd5TUxobFd0NVh6bGhBTVZDZmZYZ0pFelR1YzJEbENVOXNMLzVTVkRaV2N4R1E5aFM1cnJtK2VyQ1Jxd2FJQk1DNUtza0RCZHdOWmh2Q0FCdEpqS2Vla1FUSjd5MFp4SGNhbGVCaU1rbkYwZVRDZzFvUEhPUVZLQ3V3NE94cHRZUS9xS1V0TEFIWFZ2OTlLMGRWcWZDMmpVQWlHQmVYa0t3aGRYTGtJYlZxU0EyZmxraXBBeEhYNnByUEExQjF3eTVab3hPUFg4RVExOW92eXpBbFg1dHU0OXEwWC9PSExFN1o5T1cxenltRXR6ZFpyNXJZbWtFcVdtcHVSNU5jeHFwTWlZam93dUNXZWhubzIyeG5JM09IQ0xDZkFKaHRrcklhL1hPc0tZRFpCRzFJMGJsN2taR2R5cEtUQlhYdXl6WE5WUlU5L005ejhaVytwdG1oZ2NOUzBJS2VaaSs5bFl4cWRlS3lnbldTTTV3czdSYUpmNlRRZTNSaWJZUjFvNkhwRzB2VHpiTEtQZTZnRjJGODdiWlBJei9mcTNLWnZiM3UrSnhZcCtJVjBtQi9VN29YelhRRk1RK3VmWllpNzUxbkx6WlVxRE1ybU53TFJPVUFNUk8rVnJtblkwSVB1cFBVMXc0b0hBb1dnVGRnTk5pNk1uTFQ4V0pmUlhjT0pKMk1lbUc2K2ZNeHNZUU52UVJwa1RGY05vaFV6Y3ZjcHJ3NUV3WEVZQTJzbzczL2MvY3RIRGcreU05YlF4REppUlltRnFydkhYb29hS1JyekxnUjZLVWdoM3ltaWxaQ0lSSm9KbTE3aEtHM1pxTTE0Lzl5OUc5OE9BZjNkVTlqMDk3aUNlaEc3a2VxYXRJQ2hFWmJqbmQ4Y00rS3djN2FtVWp2ekQzQmNvMHl3MDJxT054OWF3OGhSblZiWDZhdkRJbGhySHZ6SU44MzFvUjljRHBwMG1DUEJXZFVDQlNqVGJ1RkZqRC90WElSbGxlT2JraFFKSUdSNlE2U1MxcXkzT29WT1VheFl6THY0U2s3dndrQUMwUitGREVIeVFZbFVhbVVkTWcyUmdwRUdhSVd1V3IxaGNnRm10QmREV2g3ZFBuWTF0U3VKOC95MXp4NkRvN2ZJYmNFenBBK2E0ODNtRG5vemdld3VmaFdqVCsvUS85WlEreFQ5UWJBT1pQSXhHV3VhSXVrVk8zSWxvZDhJM1NGZFJCTHY5ZXBDNzFLeXpSdVlpMktkOHJ5NVNINit1WnMxUHlZUlpRakdDK3Q4VzRtSE82Z1lFRWVXSkJ1UWhnSHdmV2xhZXlWb3hac0NBQVZKRUllT3hPZDZtNW45OHRCUDdHTmgxT1M0eDRCS2FVN1A0UVQzNVVIZW5meE84WWFQUThmbXlobUJhSVJVZklBTVN2ZTJZRFp5SWNNTTkrN0tNSVVabzJ0eXRvYzdCOGVvZzBNaUkrVkpFdFg0c29FRjFSWkhQZVV3NWlCTjI4OTh2MmVTcGNnVUJhWHFzOUN5VlZtTVJQMEtLUDJ1REt4MUdJcUhjS0ZCOXVQVWRkQS9vT3dNa0tVUWsraFZVVDVPbEVMdjd1a0FBUEE0eE4rZkczVmYxeUVKV0FiVGx5dWtGcThjNXBTRkY1cXVHbUgwVmVpQzVvVEFka1VES3Z6WGhWWUs5c3BRYjNVZ1Z0Qld6N1ZScnlOUVVST3BIZU5xeDlhZHA4YWREWCtRSHJUKytYblN4VVI3SVdGanlNTkZJRWlMWmkxdks1UVVrZlRDUU9qdjh2SHdiUi9MRHF3Z3M5bXdsT3pPY0RLdVBVK0dTb2lnVFdRejRWN0N2SHRaVDI3WUdKVG44RFFFM3IzdjB4aWxvODJ2U3VXSDg0WEU3VEJsTUpFb2R5eDNDRngwVUVkc3VhRHBPSEV3UjZYNlUyU0xseERYSXVZeEhlNXh2NjI4bXU0bDRMSnBYUjhkYmljTEZKQW55Q0FVeDJLb2dDamt1cmU4bXNUZktDbG8wamFlN1hNR05PSk15b0ZYbVlHZUh2eGhNUGMzTEtYLy9VY1p0c3p3dFJrQmNFdURXQysvQWNWZVBOSHVOWWI5MEpIcnRucGg1ZDlhL1lpTkpzY1N3QTFwUVZrdW1TQWtPQWdLdWRzcnl3c0N3Zkg1anNydVpHUTJDd1hKRXQzUU4wU2NLUlVnT1NCQ3FYa1BqZDVSVzJuOFZpamt4anovbWptakhCNmk0eHM5NEU2Nzk5STAyaldYNVd3UDZhTFRaTGt5TjhxNDUxT0RmeUZVZEY5WWsyZXQ5VUpsV1NzRFJMSWVCd0ZyQkEyZTdyRWsybWFLVUNCRW5PUWM2bUhVMXQvZ3gzK1VXVVFXbkpMZVUxbWUvbkFEdy96UGUwd3d0Vm9BaERZdDBoR1hQblJydjFoUHRGS01CeWtqckg3a0J5U0R3WDlQMi9XZkNkQlE5K1J4cHRsR2hvRmdpMUs0NVlOeEpEd05wTmd5MDV2WXUzVUtrMkpRYVNGUzcwK0Y1NzluRE5RenZpK0pPRlRsdDFmWDJGNXk5NEV2NHZobWRQSmRVOFVVRjU2Ymx0emxKREVFdmsySlFrOTM0aHpwTXJGZ1d3ZHUxUkxxSEhCN2h2T2hnaHNqV0ZGY01zNjZaRUtWcVhKUytxWWNVMHk0akwySVQrNlF2N2pvQ3BWbUdzUWtGY1FyblhxOUJiOTdaUS96UCtwaldmWTU0UmNRVlMydUU1YURObVVyVkdLK3E0d0xRcUhuRVViT2puSHFFeGlacUtxOVdRaUtUK2c3QS96bVlIQ2k0YzFTejRNVWhHb0t6U2l4aXoxYUNJUEJXdy9vczR2cUVqbXgzOGx6YnV0OWNWbElzeGNkTUpUTERRK3ZOZ0YyY1ZRaVcxRTQ0d3lWcnI3TUFaOE9KRVpFSzlEZWt5MzJQUkFuSkRUVXVqdGFscmJ0T2VOczhyS09uTjcvNFRqUEwvZmRlbEI4bjA4WXdSNXdmbU42VGpGWUhRSDFjbUZmK1AvNUxVMTI4Q1pEYjNQUStxMlFJazV3aE40eGwvcy9lb29pallmeWtDcm5aSEhHWkluTGhoU2pWbk5ISWdTL203VWV0NlhBTDdvZUl5UFRLeHVnbDJzRWtUQzNnZ0tjTnFZR0E5U3ZlYVlaQ00vWHNQRUtQbWs3QmlRNmprWFBKaE1yREd4Vkc0SW9aSDgrYjBrUWJYR2l0Mkw0L3hZdHh1bTVzcFNPSjdsTDltVFpRNnBxM2JOaTEwZU1mZ0ZWaDc3NU5JRlc0SEp3U1FtaTU0bk11blZTQjhxdjZKc0w3SGlsZ2N0ZHFSNThTTjVad1lCa2dOR1hzYjA1QXJWemVXbHh1Y21BSHNPT3dyczFnMzh6bTRZN2ZPZmducmFhV1kxanZZOFlEODZQZThkZzR4cE5paTg3UnNDZk5WK2NKVmMraktFdnpuZVY1Zzd0RmlxZCtsZHp4STlKemdSS2t0WUV6RUpRSVU5M2UvclJaN1lrVkZtNVV1cjVhMWYzcG83T0VtYkJUc2MrQ1FaOGNnYmIvbUphRXJoa3NyL3JURjBNcjNxeDl5SlJWSEJ6YWNWd0dScEFRaURPdnJkWU4xQXBVOTRyR1lrVFVzdWs1YjE1Wll2QVZxRlRzVlVMaS9HY29mbEljMm01Z2RFTFZOblRmdXY1Zlk5S1NlWHFoUU80S0pOYVZmbHAwQ0VKYWFFZFNLUXJJNXRaT2w1RkE4VXZlNmxTWVd5TVk0REl4a1RiT1JoWHVBdzR6b1RTMjgrN3d2TXhydVBkZnlKbUJCTkhQdCtEYmdKNHovcHJZWUhpTmFMTXNZamtQZE44ajNKZDczQXJFZk92Um52MzYxSVVVMFg1RDc1dlRSdlpkbzMzWERzanRlOU4weUo3K2lIQnF1a1FJY2pIVW9ic2RQN0hOajBVYWNSMHIvTmRlVTlGNFBNc1VLY2t6Tk4rZGhyMVI2d1J2R1VZb1pDRWJaWlJMWEt4QnA3SElUNEVQUktHakIvdW1xTFhhMXl6RWx2QW1WQUJhMDFZN3dGdk4wM2Ywb25FbUhTM2w1d1paRmV6cjVibnN5T01XVGxhMU5kaW1ZNXNVeE15VFliZmc4dzB2cXNEc28zWFAxYndLdzZ3M3VIRGQ1UHBSWnVDSnR0eWk0ZzJGeWI0Ymg1UU42ZkdORTI2ekRGN1Y4QmJwZXJLNkFKQ0xTWm5kaDZMMTlPUTBram4xUGpEMGk4c1BZcGFXOWxVeVJkZElPKzRWQS9LemxPUzJ4M2s5VUtUdElsTTBUSVdtZXFIS0dYUVpocGpvVGI2VlNKN203cjZaaVlQMnVsQVVvZmVWL0o2eCtzckxEQXkyQ2ZFNnFrREZ1OU9NWDBBSXVnN3loQUtOMDRyT3hVNk5tcGtjOUZ4bXUvVS9vR3hHdmIzeFVFTDYwdE1sSE9EaWtqY1I5RDJrKzRwbEc1WnV0d0FIY2kwRU02WHRrVEhQOU5QMlRTR1VFN1E5SGYvU0VEc2V0a25hZXhvWmhDczJLWDFMeU5JS0U0N2pkMkR3MTUreDRRVXV0VUFTbzU5Q1lHMVFBeW9BVVhrV3dtbXkzTGdTUWp5T3ZLV25qaE8veWpPd0FyWGd0NFBrSVVnZDQ1N05ReFpMbU41K0J4NVJoQ0FHdkUxYmxOZjlMek9keGJiaG5VZ2Z1RDM5MXVSRkhjS2RYREY3ZmVqb3gveThtaWZJcTRWVzQyajBHQnFOQUtkK0prMnJCMW9hOTRiT2hxcVVzanhqWnlRaGRXTzhNblR6T2tOaGVpZXU2blYxcW5yZ3JHU2huWTNJMlczb29GNFNnczRjZ3drZ2h2dHpFa0xUbU5OUm83RTdudVRuMkxJcmlGSnlvTmZQdUp0aWN0S0JtNzRGZytkWVBTMlIzTzNmOWxBZWxiVWZjbzZGNU9EL3hkS1VuRTh0V3FOMExVcDlWQUptWVZYZFVDaGJ4MjM4MWtDaStLNDJoRzUydFNQYU1hb1dTb0xQY2Zrb24rc1pYdjdEdEtwZi9HTzdhcUMza1pzRGpva29haHJGZGJWSlNTZWhrNGp5K3RzRHplQnJKSjBrMVZrUnJHN1NoVHZjTmd1cjVucVRUTEE5dlJMQmJNTTlhNlI1NEZ0Z1pQOWFKMU1aMEdCcUVpMnF6Ui8yd2tYQlhwcFhZdi9TcU1RV1dhbTVsSHBMVktxaDN4ZHRjNFdmck9mYldsbU1PNXA5Z0JUSFp1YUcxVGFkZXFRVVpKQmZBS01ENFdSR0NsMDFaeDRTVzE0YzZrdnFKdXExL080N215L3RsVHlLWndpYlBkQTNRMVVGd0I3R2Z4anEwaDN2ckxFbUNrS3Vsc0VBUkN6UnZNVjJSVnBVbFpUV240Y1Boc0hjcTNROElHSUYyKy9nOENFSU4vMU8xcVMvMkpXcXlDNmtIb0w4Y2R2R0VHbmkxSTNDTk1JcXhxaHhJL1V0R3REc2VwYmwrSHI0elh4MzZna3BCbXBoT2xkTFVYTHAzVEtibVVZRWJSWHcvZmRmeFQ3WDdZUFhHQ0hHVG1uTzk4WkxDOTA2Zmkvekd2b04rNlpzbCs3MkpWMGxJWEo0V3dZdWxFUmZHbkFDWGNoa0Yzei9ITWR3elcwTUFFaXptQmwvREo2ZUoyU01PSG1Uc25YbElGRDRlcFRrYnFBQ0dpZ2I1UExFdHdQRVRjYkNRckM5YUtTU1FnSTdEZXd1aWlxM2J0Y0RUWkIzeEI5WWxlbmhpU0FXNjIwcmwzc2ZjY3d3eGFSOHBDV2Rzd0x3dmFxcDhjM01PV3RCc2xPcmVTSkNEcWgvdzBYbm1WMFJVWFpNM2JvUmkwVXhsaHVUeDFlM1NTd09pbTlOczNYV3NoTmI4Lzc3VkhnUWhRVFlSUU1NRllYaWRmMElCKzBtSUpocWNoQTlUeUY3dGRjSDhrUUJUSHNEWS96bFpqK3EwNlFMd0JkbTkxc3IyK3VzZmxlaXB3WUMrcmdiNHROVnA3VU5rYkVqTnR6ZWZsTi9VRTlkbHZtT2x6V1dtZkh2NGVkUGkzMmJmeUNRS1d6SGJVVEV3NU0yVFpsZnpNaTFWUjVsaDBxQ1lqaDNITUlmL2MwcHBKd2I1b1lFTnBBenlxbnlmdmlTV3lBYzc2L1l1VWwvb2FVaysrYzBZc2d1TGo5ZGFQdVVvemhoZ3VjSytQRGlNckI0ODU1Mk83VWg0aHRwNmZ3S2dJa1JCTVFIUTd6MmV5WXovV1AwQm9ZZVhjOGc3aUprclhFNzA1bFo1bXhGU0poT3E1WlNleVJSb21pUm41K3VRemM5ZFdWQjBYb2JURXdOc0VRM2FIZ25JY29BczY2UGplUT09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDI6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/social_core/tests/backends/data/saml_response_no_next_url.txt b/social_core/tests/backends/data/saml_response_no_next_url.txt new file mode 100644 index 000000000..37b6e1c19 --- /dev/null +++ b/social_core/tests/backends/data/saml_response_no_next_url.txt @@ -0,0 +1 @@ +http://myapp.com/?RelayState=%7b%22idp%22%3a+%22testshib%22%7d&SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL215YXBwLmNvbSIgSUQ9Il8yNTk2NTFlOTY3ZGIwOGZjYTQ4MjdkODI3YWY1M2RkMCIgSW5SZXNwb25zZVRvPSJURVNUX0lEIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDUtMDlUMDM6NTc6NDMuNzkyWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI%2BaHR0cHM6Ly9pZHAudGVzdHNoaWIub3JnL2lkcC9zaGliYm9sZXRoPC9zYW1sMjpJc3N1ZXI%2BPHNhbWwycDpTdGF0dXM%2BPHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM%2BPHNhbWwyOkVuY3J5cHRlZEFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIElkPSJfMGM0NzYzNzIyOWFkNmEzMTY1OGU0MDc2ZDNlYzBmNmQiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiLz48ZHM6S2V5SW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI%2BPHhlbmM6RW5jcnlwdGVkS2V5IElkPSJfYjZmNmU2YWZjMzYyNGI3NmM1N2JmOWZhODA5YzAzNmMiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDc0RDQ0FobWdBd0lCQWdJSkFPN0J3ZGpEWmNVV01BMEdDU3FHU0liM0RRRUJCUVVBTUVVeEN6QUpCZ05WQkFZVEFrTkJNUmt3CkZ3WURWUVFJRXhCQ2NtbDBhWE5vSUVOdmJIVnRZbWxoTVJzd0dRWURWUVFLRXhKd2VYUm9iMjR0YzI5amFXRnNMV0YxZEdnd0hoY04KTVRVd05UQTRNRGMxT0RRMldoY05NalV3TlRBM01EYzFPRFEyV2pCRk1Rc3dDUVlEVlFRR0V3SkRRVEVaTUJjR0ExVUVDQk1RUW5KcApkR2x6YUNCRGIyeDFiV0pwWVRFYk1Ca0dBMVVFQ2hNU2NIbDBhRzl1TFhOdlkybGhiQzFoZFhSb01JR2ZNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNEdOQURDQmlRS0JnUUNxM2cxQ2wrM3VSNXZDbk40SGJnalRnK20zbkhodGVFTXliKyt5Y1pZcmUyYnhVZnNzaEVSNngzM2wKMjN0SGNrUll3bTdNZEJicnAzTHJWb2lPQ2RQYmxUbWwxSWhFUFRDd0tNaEJLdnZXcVR2Z2ZjU1NuUnpBV2tMbFFZU3VzYXl5Wks0bgo5cWNZa1Y1TUZuaTFyYmp4K01yNWFPRW1iNXUzM2FtTUtMd1NUd0lEQVFBQm80R25NSUdrTUIwR0ExVWREZ1FXQkJSUmlCUjZ6UzY2CmZLVm9rcDB5SkhiZ3YzUlltakIxQmdOVkhTTUViakJzZ0JSUmlCUjZ6UzY2ZktWb2twMHlKSGJndjNSWW1xRkpwRWN3UlRFTE1Ba0cKQTFVRUJoTUNRMEV4R1RBWEJnTlZCQWdURUVKeWFYUnBjMmdnUTI5c2RXMWlhV0V4R3pBWkJnTlZCQW9URW5CNWRHaHZiaTF6YjJOcApZV3d0WVhWMGFJSUpBTzdCd2RqRFpjVVdNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ1lFQUp3c01VM1lTCmF5YlZqdUo4VVMwZlVobFBPbE00MFFGQ0dMNHZCM1RFYmIyNE1xOEhyalV3clUwSkZQR2xzOWEyT1l6TjJCM2UzNU5vck11eHMrZ3IKR3RyMnlQNkx2dVgrblY2QTkzd2I0b29HSG9HZkM3VkxseXhTU25zOTM3U1M1UjFwelE0Z1d6Wm1hMktHV0tJQ1dwaDV6UTBBUlZoTAo2Mzk2N21HTG1vST08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BTElQdkVNVUVGeXhrVHowQ2N4QVA5TjV4Y3NYT2V4aVV4cXBvR2VIeVFMV0R5RVBBUDVnZ1daL3NLZ1ViL2xWSk92bCtuQXhSdVhXUlc5dGxSWWx3R2orRVhIOWhIbmdEY1BWMDNqSUJMQnFJbElBL1RmMGw4cVliOHFKRy9ZM0RTS2RQNkwvUURtYXBtTXpFM29YOEJxMW5Ea3YrUWh4cmQwMGVGK2ZMYVQ0PTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BRVpUWDhHTkM0My9yWStTUVlBMXRudHlUTTVVNkN2dUNCaktsVEVlekZPRjBZZHhCWUdFQVVjYU8xNVNKOXBMemJ1L1h0WGxzTkVMZTdKdEx4RUpwYUxubWFENnIranNWczdLaTBLNHRTMGNBUERDWHV2R1FoMmFOVjVQOGJ3N1JWUGhLOGQwYlJ1RklGR09FOHMwTTZYOUpxWDN4S0MvL1lSbVVoeDlybnU3ZWlwMGh5ZitPaUZiVGR2SDY2NTB2LzQ3aVdKcDNZeFlUV0QyMHBNbVRJMUpwWUEwYjByWVFQRkR0RU93d0JxYktxanRJc3ZYVFJzeXJhQkxvbnFOeHN5dHpEWHEra0JsMXp3WGUvSE5QcUVQblczdnNxaFhZcDVGM3dkWThkKzNCOTRMZlpOdUd4a0p3VDNzdVR0OGY5VHRBSlI4VytBUmtzT2M4eDBVaVNsVG5BNHFHOTBLMTR5dkVoVHcvd2drZjFXV01RT3dpZDNpakFYbUV4MU5MbVZvYUxYb3p4VExkTjN6YnJ6VEJIRXc3R2J3ZEdrdU5pMlhZOW16YUgwaWtGRm51VUxjMHUwc0pycEdGdzlaK0VlUk44RzNVUVZ5MjhtS2g3ZFBwWU5KbzhyajIxZFFaK2JaeUtTUHZablU3REkyakdJRE5US1g2ZkVyVWFINGlOTzN4cUU2Vk90L2d4T3BMNE5VNUhLV0Q0bG93VzcwdUJjVEVQRmhwaThpYUovdTB6YzUvTEhvdVBjMzByc1RLZFc5cmJLL2NWaHNQUHErZzA5WHZpZ0QweTJvN2tOc1pVL25tRXFiSzBKOTBrazhCR3I5cXRSczY4bUJnSURtUHVwUkhwWjM4eXNnU2VZN3V0VlVaSG5tQ0dzTzZ2NDJ6OTVOK05Pb3RCTEVZbFd1ZEdzYnowQWc4VkRDSlY5ak95QW95MDZyL1AyUHBsOFhjdmJza2d2T1BMMWdDNnVYbVJJS1lmOEw4UDJCNXVjN0haK0dtUHNOWXRLS2VKRDFFUHovdCt2NlBIbXNVb3dsSDhSd3FMRHdtMUF4dlNLQTR3UXBlQ0dQd3A5YXRYS0lWMS84NUZzRWMzajVzNjd6VlRybThrVEpydXV2MDZEdFVRZDNMOFdwTkV4cWhQait6RUp6U3RxSG04ckhNMVhNQUVxdVozc0xycTVqLzFSNlpqS0dOdFJCbjhwOE5ERGtrWm0vWTV5TXlJNXJJS3U5bnA3bXdaaEVpeWVHeHdxblV3VVMvUzVDRjNnMHVidnd4eVVnalVvd1ZvTkNqYktBbkdtT2VCSW5abkh0eGdIVUhVOUVlTFdyd2pRc3JtUmpJV0R2RkZQa3l6SzJDL20yaitubmNxc2E1OGRLVXZxcGR1VTRJYnNPQng3UGpXdXRBNmY5bXd6YWxyRU1NK0lGR3VPdk9HMC93eUdzQjZLREV6bldjUC83NkQ4angzaHZFSlAzN3REbFgreGM4Qno5TXdKdkd6VG4xbTdCb2xoR0lzSXlCTys1ZXpXa3RDWVVIUURGVE9wbXA0MDlOWHp6ZUNTUGY1U2NDWG5YYjRPd01ULy9VM1JFUnRRbGMrNmU2WG1JRjhoRkJVc0taUUJsS2ppSDkwZHlzYWlsNmN2V3UyQW55Q3QxbWxXcHFLc0MzU2RTRVZDTG1qRjlUQUFUMEtFSGdZQjg3RjZtZUpTTysvOXkyZkRuYVVvUUlUVzdubnVuSCtkT3dWSGZMU0wyL2N5YTltNlQzR29TSVNMbGJPMVRzalhKclVkZW55OTcvM2tkNmhFQlphdGY1U3NETFQ3SjNsQUVJNDROeXJ0NkIxQWdod2JNdkpqd1JNTXRNdUJLc3ltUytKVzc4UFNEWXQ4MG9waDJQTTc1N0tBNCtUMTAvYnZaQkE5Vk1OdVpqNVV3NXRWMnFIS3dwS0t6ZVVETUFiQlBRaGpYcXlQZzFKa09rd2RQMUpnOHRITjJTelBZQTlmT1htV0pBZGJDS2tMb0F4ZTV6cDZBUzYzS3FXMmFmSUt6SHJ3RTJmS1VtamppeURvMnNuMkJHbWtBaTRzbnpiVzc2SUQvSVgwd044aDBaQ2VRc29vKzdtb1RCMEJxSnBkS1MycXlsUktoc3BSTC9henVQdmxaK1pwckJxdXpJdEZkNFVLMkpzQkp6VXcwZkpxcTV1bk9PZENzVWM3SUU3QTNmZ1NmZ3NBd1R3WFZJMEVoME5ySWZpMkFKV1Z2VFpEMys2eFZ3dS96WWhuVjc0VXkvMFE4Mi8yQWtpSGpFRjNJVGNLWHdTNTB6bWtLakxjZDJqa2h5TUFYMWRoQ0wwZElFMUJoN0RNamVvNC9YbjBqSlpPL3Rrbi9xZmYzc3RNb1BYVG9KTnBIU1RjR2ZheGtaMzJYNCt3Q0xPc0VBRWxlMVZSY0kwUkZyOFhHTSsxWU9BTjBodFdGcFMxaG9kSi9OczJqL1FnUVNEemNpQ1FZeUFDd3lFRWZDZjZybnR0VmJyTlJQZWlmSHhBM3B2UnZ5ZGRhNDE5cXl0ZXI0akJ3cmw3ZUpuVnJ2VEprR2VhU2FRbDdXWk5SQXBscXRnNnZPYmpiMHZDRWlFaFhKbmNzQUhxcXp5QTRGeWFUVGQ2R0FySU9adUNxRWVoWk51T01lOVlrMVpya0VkR3pIalJESWk3Q1BKQk12NEZ4ZHI3bnJvN0I1WEhKb0ZMNE1DSUtOWWU2aWZiTUtYOU5uN1FWdnphUmY2UXlaSW1BWENQZndvU1BkN2x6NXl3UDJLSUIyaGhFMWt5eVZ5YVc5T0praWpUY3dvUnZrSXhIU0RqMXFqeGxueXh0QzhVZ1pNWmlwcGgzQXJpcjRiekIzUDhIbGIzejZ0OW51KzZMemNiN2ZObVo0UHluaU50Vk9OQ0lHbEh4dTBSY3hQK3cwUXNsM1BtTzJLaHBpc2RIanhvSUJ1YVY1NXdoTlFFNmdNNFBrT0xINDc4Rzg4bUxkd2s2RFpkWVl4L2d6RWE3b3ZIL0pReFp2TzRLdFVTNmZjZHJxV2thTFg1cEhkNkdneFBGZ2NFc2Nad1ZqM2hCS0xFQmE5L0dodERINEhzRnNRbmpPZnNDQkNzN0tjRitmTi9oSUdUeHFqTVlKVHJRYmNtdWF5dk9xR3RQMDFPcXltR24rVm5FSVkzKytQcm95SFN3K0Q0b0JIVG1maFNXRmJLZCtuTlVFS3BhRVIxNkdCU256WktQRVRVSmdRWEw5QWJRQ3RXVjFHb0UzRWNnMDZYaVd2aHFHakpGNldtdEU4dHY4Q25rZmxMNm91TDRvNldpbmx2WnNEdkZrS0R6TDkwUTNsWC9NanBtRTFpWU9uYzdISXdEVGwraFRRcHdsYXJiTDVUNGNkZTg1akNwYU0xU3p1TStiQU5zMHlXVDA0ZXJUVFc2cnhlbXFDTHAra202TVVMTlZOcE1CazBiQjJpRU82UlRtc3VpRlhDUU1xdU5xZjdkWXUwTFFCZzQ0MkJzU1pBV1ZrWEVZblduOURLdTRSby8veEFsb2h5VHozWlZmSkhuWVBSdDloSUErRHVUL3c4T2ZzTURIWnlCelUvL0JEa1NiNkxjMHdraVA3QlhIdjBoNVdud2dNWUxlZDBPalR5UWI2aGxpVnQ5b0FjaDRFVy9EZUlBdkpaQ1BYVm1pUFFYTGVsOVJIRko2bXFiYVo0TCtaZG1ONmQwcFZNZ1FveXhmQTR3dEwwYVpiNnFZYkhibjJMd2VBQVZwL3M2TzVlMVExdnZpZDRTWHo0a2l3RW1LSStIeXZEQ1pnekpQQVN5Z1gvWDJFWEZ0NGV3SjVmUFQyVXZmWnhQWlpqMFZGSFpyUFQwWVd2VE16bjUva3hoT09oM2drVGdDSmNwNWVsZnp4cEFPNFl1a0NoNHJXdVNndDRqVUJyaWNYbFdWdWo5U3JSZVhUalNHTktLK202NWovUDllNHRHT0RkMk9BbjNKTVQ3Q3FuaDhreTZpZjVjbmpVMmU3UDhTZnBONGwxWEFiZEZEcGk5bVJYamEyTzR1RWFHNGNvNW4xcWNDT3ZNMWYyblFBY1ZGNUFoSXhueS96TWhmU2l2RXdOQ0Zyd2tBWDRyQVE0WldUNldFakFyUG5jb1Y4Z1VRclhxQVA4NDJmK1lNWWI5RHFncmFicEg1a3ZuMnQzcWRldGJHODJ0QWlTamhPcUxNYW9iU2F4cXdWa1lUOHRTMW9rUUt2MWZoZ2t6elpEOE5IQnVQQzdNVHdXS0VCS2tDRUUzRWRFMXhNQURLd1B1M3NSaGpSaExXZyszZ2srejJtdlU4cTBhTlc0Y3hObUdoekx4eEY0Q3NFNStMQ1cwOWFpUVJOM1VvWmg1aktBZzBiMlh3WHBLS3pycUVTY1BYdnI0L1dWUTMyMm5qRWRvQVdXR0t2WnBKMlRlREo0eDdiT21LVElFc2RHWU1UZzFVaEU2eFFQcnhqS3dWeGFJNVJyaVE4a0xpaGgwa0t0WHQvYTVsSDhzUjVwR0ZISGZ3dlNVb3liQTB1eUVDNnNRVitPbTVReUZmRmpqZHFCOGNpOGxQS1hLTHFCTHJ6bjNmUkh3TmQwbzFiRTg0aGllTkx5UlhZVmhrRCtFNEpGaVd3ZWt3U3VWM3BjQk9ybnRVU3RoWmx6M3hIUURUVGNJNWliOFJyQ2swZEZ6YTgvQmw3VUdtWlUwSXZ2UmdvVXF2TXNHT2dMY3pGWmRpZnJ5aGNiUTY4a2ZzZ3lCMHppdC9MN1BSV3V4RkdYdDFoTVZSVUZ3WXBJS04zVkI3cXVKZlgwamZsU1JaRndMaXdlK3VhYndmTVZ6c2doajUvOXZNNzcwK0JaMGtJcE45NzBTMG5BbHl6R0h0aW1nTUl1RXFhbUt5QTNTQlI1aHZIYmRyNENnTHFUbXIzbFFnWmpnSkNvN1FXYUJWTXdCR0RpdzVOVVhUUnBycWc4U3h2eDlnNWZwbXMrL0o2QjFEelNTM3ZRZzgxdHFRU1ZDWVJpc0Y3M2VqZlFuZk4zcUszd3RJRDkxQnRISmFvMEFaUUdKVFpKOXVsZ0kzV3hzdWR4ejB0VHVpNlJlSWpmSWsxekZRdFpwRExGMnB3NGpTQVdQTlJqNDBYdVIrRzFUVlI3OVFiME9FYkw4RDFoTU5zWmo3MTZNbUhSOTlKaUxNdm1FWHV5a1V4VGhGYjRMTzZVbW1kU3UwTlBpMXQ2NmNkYURpQWhMaVBFTGdUNkZsenA2T2FGSGNSNjRncEtyemtTNDJONEhJeFpNa2R6M0FsYkRhK2pOWHZPR1l3UWl5K0xNNENZWGtrTWtHR3ZTWis5R2xWQ0l5RXBJaXIzbEQ3bmdzZGk4emxGWDYvekNaczlQSUtwZFZlSGJGZi9GS20wV3AreHI0Ykd0R0RrVHR2Nk1Manh2YU8zanFHaUFWeERKVWFkTVBlS2VHSm5uempTdnpKbGdOVHV3c3grRnF5L2dPMkwxMGowWmhDWi92dE9NelVjNjl3cGhKZm9FNzU3V3lOeFJOcThJc0Y1Tkg5Y0x0b3UvbUNxOTc3YnZPSkRrSURCN3lKWEJ6YUhVQkJuSXJra1Qyemg3bGJmUm5SREJUSFZraVZMazVESUxqeC9XL1BSZEZpUUM2SzRmZGx4Y29JbzlMcnM4ZFVWZkt2TTNNYnJ6c1hGT3ZtVVh0K3NsZldvd3UyTC9ndG9mRFhvTUJZZnlEcWIvWlRaRWZ0MC83blliRm1relBEUlZacU5SR0F3YWZVNTU1UjB2SWtNbGR2VjdKUzhNT1BNYWlXQVBpelNLRG4yRzNvcys1MzRFQytaOGZnWmFPVWpZL0xLME9vME9RMmhvNUV6MGNMYWpwUjFINk9FNEhvUm1ydjQzZkFjdGpYc0hYdi81RXg3emdrWk1NZXZhTFNEdjZtcjFGcDk4QXR4L296VTFGVDBoMDUxcVcwR0g2VWpRRXk5aExSZDBBMnFkUTRMZXpReDNvbDFTblhsamt2MG4zTXFlaFozOC94bzZhdHFDdkJtQkc3amlUdXd6YnlVUngzRm1TM0NCNllOYnFON3hPYVRZRnlkOEZDL01nY0xGQmMwS3F4MXllQ2VUd1hucldQb0dvdllVQlYxYjA1cWtIa1d5V0RUaCsveXJFNzF0RjNxbUQvd3F6cUJyNE04NERtWWVuQkdFOWxtb3FIZEMyWnRpK09KVFZKcmlHZWxQQ3RjZnZRaUlQcHdDZ3BFNmg1ekZhRndLajRuZGtBUkRpTC95L1EwWTZxNU5rM1g5RURlTmdjY1pIcFdmOUpKQ3M2a29wdXRtYjdDczIrbVJYdER1S09DaGY5UVUyN3Bmb1NJaklYK3NGdHY1c0hhSms2aHBZMlpzUUhzaTBYbFowc3FMTnQ5ayszdTVnYnBSU1JCczlHaC9BaVY0dkNyYTRkOTh5U0dCdzRSR1FhSStpQ29RaG9YK3lxc3VrYkx6bXJUU3FXMVRXaXJReUlHZ1Q5VnFERE1mUzAxeGdQSlNFSTlIWlp6TGlFVXVGMm1CMi81Y2dqaEFUaWQrdGV1UVB4aldhN2NSc2t5YUhuTENjQURVUU9ESUFPVjJDWXROcnAwY29ZL091S3ZzaXlJT0lacVJ5dE1PMGVNZ1ZJWTBzWmdxeVEycXlubUx0NDBmWmd3SFVyV245Zm9TYTNtMkVRTy9uOS8yU2NuelJWdVZpVnNjM0tCSElQL3AzNlJlSWowTGlNcCtPQ0p3SHlLVW1UeDRBU1V0dXVhWktlRHl1QjlxcXJuUEFNWUVCeElsTGFvdXMzV1pHakIrcW9ub3QvNmk1UE40bUZjbHFDcUxhMGJHbks4ZnJxYy9yd2tuVGV0YUE0c2tXTEw1L21qNEd5MitFQkh3a0x3UXd2K0FKdmZTOXYvNDl1LzY0N1ZFYW15UzdZQ2ZEUHNBQUREQ1FFcWJNQ1h2Ui8xVmEwWi9YUWhoNlkrZUt0MEVpRDdpNmRZODJtQkFoNEJMRmRVV3VGZHVrdUVwaGZ2WXB3N2loVjNxTjB1NFM1NTRXU0dUa0ZsdlpYNG1hbkF4a1g2ekQxS0NWaEFMdEJnSDgzdkhxam9uc0lwOFMydHgwZ0tiYzEreHVaRVppVWlNVVlVdTByQVFsRFcrZHJoN3lVRHZqekFHSnBmTk01eThaMW45em93VzZ5YW5VZWFBNjhSZDd5TUxobFd0NVh6bGhBTVZDZmZYZ0pFelR1YzJEbENVOXNMLzVTVkRaV2N4R1E5aFM1cnJtK2VyQ1Jxd2FJQk1DNUtza0RCZHdOWmh2Q0FCdEpqS2Vla1FUSjd5MFp4SGNhbGVCaU1rbkYwZVRDZzFvUEhPUVZLQ3V3NE94cHRZUS9xS1V0TEFIWFZ2OTlLMGRWcWZDMmpVQWlHQmVYa0t3aGRYTGtJYlZxU0EyZmxraXBBeEhYNnByUEExQjF3eTVab3hPUFg4RVExOW92eXpBbFg1dHU0OXEwWC9PSExFN1o5T1cxenltRXR6ZFpyNXJZbWtFcVdtcHVSNU5jeHFwTWlZam93dUNXZWhubzIyeG5JM09IQ0xDZkFKaHRrcklhL1hPc0tZRFpCRzFJMGJsN2taR2R5cEtUQlhYdXl6WE5WUlU5L005ejhaVytwdG1oZ2NOUzBJS2VaaSs5bFl4cWRlS3lnbldTTTV3czdSYUpmNlRRZTNSaWJZUjFvNkhwRzB2VHpiTEtQZTZnRjJGODdiWlBJei9mcTNLWnZiM3UrSnhZcCtJVjBtQi9VN29YelhRRk1RK3VmWllpNzUxbkx6WlVxRE1ybU53TFJPVUFNUk8rVnJtblkwSVB1cFBVMXc0b0hBb1dnVGRnTk5pNk1uTFQ4V0pmUlhjT0pKMk1lbUc2K2ZNeHNZUU52UVJwa1RGY05vaFV6Y3ZjcHJ3NUV3WEVZQTJzbzczL2MvY3RIRGcreU05YlF4REppUlltRnFydkhYb29hS1JyekxnUjZLVWdoM3ltaWxaQ0lSSm9KbTE3aEtHM1pxTTE0Lzl5OUc5OE9BZjNkVTlqMDk3aUNlaEc3a2VxYXRJQ2hFWmJqbmQ4Y00rS3djN2FtVWp2ekQzQmNvMHl3MDJxT054OWF3OGhSblZiWDZhdkRJbGhySHZ6SU44MzFvUjljRHBwMG1DUEJXZFVDQlNqVGJ1RkZqRC90WElSbGxlT2JraFFKSUdSNlE2U1MxcXkzT29WT1VheFl6THY0U2s3dndrQUMwUitGREVIeVFZbFVhbVVkTWcyUmdwRUdhSVd1V3IxaGNnRm10QmREV2g3ZFBuWTF0U3VKOC95MXp4NkRvN2ZJYmNFenBBK2E0ODNtRG5vemdld3VmaFdqVCsvUS85WlEreFQ5UWJBT1pQSXhHV3VhSXVrVk8zSWxvZDhJM1NGZFJCTHY5ZXBDNzFLeXpSdVlpMktkOHJ5NVNINit1WnMxUHlZUlpRakdDK3Q4VzRtSE82Z1lFRWVXSkJ1UWhnSHdmV2xhZXlWb3hac0NBQVZKRUllT3hPZDZtNW45OHRCUDdHTmgxT1M0eDRCS2FVN1A0UVQzNVVIZW5meE84WWFQUThmbXlobUJhSVJVZklBTVN2ZTJZRFp5SWNNTTkrN0tNSVVabzJ0eXRvYzdCOGVvZzBNaUkrVkpFdFg0c29FRjFSWkhQZVV3NWlCTjI4OTh2MmVTcGNnVUJhWHFzOUN5VlZtTVJQMEtLUDJ1REt4MUdJcUhjS0ZCOXVQVWRkQS9vT3dNa0tVUWsraFZVVDVPbEVMdjd1a0FBUEE0eE4rZkczVmYxeUVKV0FiVGx5dWtGcThjNXBTRkY1cXVHbUgwVmVpQzVvVEFka1VES3Z6WGhWWUs5c3BRYjNVZ1Z0Qld6N1ZScnlOUVVST3BIZU5xeDlhZHA4YWREWCtRSHJUKytYblN4VVI3SVdGanlNTkZJRWlMWmkxdks1UVVrZlRDUU9qdjh2SHdiUi9MRHF3Z3M5bXdsT3pPY0RLdVBVK0dTb2lnVFdRejRWN0N2SHRaVDI3WUdKVG44RFFFM3IzdjB4aWxvODJ2U3VXSDg0WEU3VEJsTUpFb2R5eDNDRngwVUVkc3VhRHBPSEV3UjZYNlUyU0xseERYSXVZeEhlNXh2NjI4bXU0bDRMSnBYUjhkYmljTEZKQW55Q0FVeDJLb2dDamt1cmU4bXNUZktDbG8wamFlN1hNR05PSk15b0ZYbVlHZUh2eGhNUGMzTEtYLy9VY1p0c3p3dFJrQmNFdURXQysvQWNWZVBOSHVOWWI5MEpIcnRucGg1ZDlhL1lpTkpzY1N3QTFwUVZrdW1TQWtPQWdLdWRzcnl3c0N3Zkg1anNydVpHUTJDd1hKRXQzUU4wU2NLUlVnT1NCQ3FYa1BqZDVSVzJuOFZpamt4anovbWptakhCNmk0eHM5NEU2Nzk5STAyaldYNVd3UDZhTFRaTGt5TjhxNDUxT0RmeUZVZEY5WWsyZXQ5VUpsV1NzRFJMSWVCd0ZyQkEyZTdyRWsybWFLVUNCRW5PUWM2bUhVMXQvZ3gzK1VXVVFXbkpMZVUxbWUvbkFEdy96UGUwd3d0Vm9BaERZdDBoR1hQblJydjFoUHRGS01CeWtqckg3a0J5U0R3WDlQMi9XZkNkQlE5K1J4cHRsR2hvRmdpMUs0NVlOeEpEd05wTmd5MDV2WXUzVUtrMkpRYVNGUzcwK0Y1NzluRE5RenZpK0pPRlRsdDFmWDJGNXk5NEV2NHZobWRQSmRVOFVVRjU2Ymx0emxKREVFdmsySlFrOTM0aHpwTXJGZ1d3ZHUxUkxxSEhCN2h2T2hnaHNqV0ZGY01zNjZaRUtWcVhKUytxWWNVMHk0akwySVQrNlF2N2pvQ3BWbUdzUWtGY1FyblhxOUJiOTdaUS96UCtwaldmWTU0UmNRVlMydUU1YURObVVyVkdLK3E0d0xRcUhuRVViT2puSHFFeGlacUtxOVdRaUtUK2c3QS96bVlIQ2k0YzFTejRNVWhHb0t6U2l4aXoxYUNJUEJXdy9vczR2cUVqbXgzOGx6YnV0OWNWbElzeGNkTUpUTERRK3ZOZ0YyY1ZRaVcxRTQ0d3lWcnI3TUFaOE9KRVpFSzlEZWt5MzJQUkFuSkRUVXVqdGFscmJ0T2VOczhyS09uTjcvNFRqUEwvZmRlbEI4bjA4WXdSNXdmbU42VGpGWUhRSDFjbUZmK1AvNUxVMTI4Q1pEYjNQUStxMlFJazV3aE40eGwvcy9lb29pallmeWtDcm5aSEhHWkluTGhoU2pWbk5ISWdTL203VWV0NlhBTDdvZUl5UFRLeHVnbDJzRWtUQzNnZ0tjTnFZR0E5U3ZlYVlaQ00vWHNQRUtQbWs3QmlRNmprWFBKaE1yREd4Vkc0SW9aSDgrYjBrUWJYR2l0Mkw0L3hZdHh1bTVzcFNPSjdsTDltVFpRNnBxM2JOaTEwZU1mZ0ZWaDc3NU5JRlc0SEp3U1FtaTU0bk11blZTQjhxdjZKc0w3SGlsZ2N0ZHFSNThTTjVad1lCa2dOR1hzYjA1QXJWemVXbHh1Y21BSHNPT3dyczFnMzh6bTRZN2ZPZmducmFhV1kxanZZOFlEODZQZThkZzR4cE5paTg3UnNDZk5WK2NKVmMraktFdnpuZVY1Zzd0RmlxZCtsZHp4STlKemdSS2t0WUV6RUpRSVU5M2UvclJaN1lrVkZtNVV1cjVhMWYzcG83T0VtYkJUc2MrQ1FaOGNnYmIvbUphRXJoa3NyL3JURjBNcjNxeDl5SlJWSEJ6YWNWd0dScEFRaURPdnJkWU4xQXBVOTRyR1lrVFVzdWs1YjE1Wll2QVZxRlRzVlVMaS9HY29mbEljMm01Z2RFTFZOblRmdXY1Zlk5S1NlWHFoUU80S0pOYVZmbHAwQ0VKYWFFZFNLUXJJNXRaT2w1RkE4VXZlNmxTWVd5TVk0REl4a1RiT1JoWHVBdzR6b1RTMjgrN3d2TXhydVBkZnlKbUJCTkhQdCtEYmdKNHovcHJZWUhpTmFMTXNZamtQZE44ajNKZDczQXJFZk92Um52MzYxSVVVMFg1RDc1dlRSdlpkbzMzWERzanRlOU4weUo3K2lIQnF1a1FJY2pIVW9ic2RQN0hOajBVYWNSMHIvTmRlVTlGNFBNc1VLY2t6Tk4rZGhyMVI2d1J2R1VZb1pDRWJaWlJMWEt4QnA3SElUNEVQUktHakIvdW1xTFhhMXl6RWx2QW1WQUJhMDFZN3dGdk4wM2Ywb25FbUhTM2w1d1paRmV6cjVibnN5T01XVGxhMU5kaW1ZNXNVeE15VFliZmc4dzB2cXNEc28zWFAxYndLdzZ3M3VIRGQ1UHBSWnVDSnR0eWk0ZzJGeWI0Ymg1UU42ZkdORTI2ekRGN1Y4QmJwZXJLNkFKQ0xTWm5kaDZMMTlPUTBram4xUGpEMGk4c1BZcGFXOWxVeVJkZElPKzRWQS9LemxPUzJ4M2s5VUtUdElsTTBUSVdtZXFIS0dYUVpocGpvVGI2VlNKN203cjZaaVlQMnVsQVVvZmVWL0o2eCtzckxEQXkyQ2ZFNnFrREZ1OU9NWDBBSXVnN3loQUtOMDRyT3hVNk5tcGtjOUZ4bXUvVS9vR3hHdmIzeFVFTDYwdE1sSE9EaWtqY1I5RDJrKzRwbEc1WnV0d0FIY2kwRU02WHRrVEhQOU5QMlRTR1VFN1E5SGYvU0VEc2V0a25hZXhvWmhDczJLWDFMeU5JS0U0N2pkMkR3MTUreDRRVXV0VUFTbzU5Q1lHMVFBeW9BVVhrV3dtbXkzTGdTUWp5T3ZLV25qaE8veWpPd0FyWGd0NFBrSVVnZDQ1N05ReFpMbU41K0J4NVJoQ0FHdkUxYmxOZjlMek9keGJiaG5VZ2Z1RDM5MXVSRkhjS2RYREY3ZmVqb3gveThtaWZJcTRWVzQyajBHQnFOQUtkK0prMnJCMW9hOTRiT2hxcVVzanhqWnlRaGRXTzhNblR6T2tOaGVpZXU2blYxcW5yZ3JHU2huWTNJMlczb29GNFNnczRjZ3drZ2h2dHpFa0xUbU5OUm83RTdudVRuMkxJcmlGSnlvTmZQdUp0aWN0S0JtNzRGZytkWVBTMlIzTzNmOWxBZWxiVWZjbzZGNU9EL3hkS1VuRTh0V3FOMExVcDlWQUptWVZYZFVDaGJ4MjM4MWtDaStLNDJoRzUydFNQYU1hb1dTb0xQY2Zrb24rc1pYdjdEdEtwZi9HTzdhcUMza1pzRGpva29haHJGZGJWSlNTZWhrNGp5K3RzRHplQnJKSjBrMVZrUnJHN1NoVHZjTmd1cjVucVRUTEE5dlJMQmJNTTlhNlI1NEZ0Z1pQOWFKMU1aMEdCcUVpMnF6Ui8yd2tYQlhwcFhZdi9TcU1RV1dhbTVsSHBMVktxaDN4ZHRjNFdmck9mYldsbU1PNXA5Z0JUSFp1YUcxVGFkZXFRVVpKQmZBS01ENFdSR0NsMDFaeDRTVzE0YzZrdnFKdXExL080N215L3RsVHlLWndpYlBkQTNRMVVGd0I3R2Z4anEwaDN2ckxFbUNrS3Vsc0VBUkN6UnZNVjJSVnBVbFpUV240Y1Boc0hjcTNROElHSUYyKy9nOENFSU4vMU8xcVMvMkpXcXlDNmtIb0w4Y2R2R0VHbmkxSTNDTk1JcXhxaHhJL1V0R3REc2VwYmwrSHI0elh4MzZna3BCbXBoT2xkTFVYTHAzVEtibVVZRWJSWHcvZmRmeFQ3WDdZUFhHQ0hHVG1uTzk4WkxDOTA2Zmkvekd2b04rNlpzbCs3MkpWMGxJWEo0V3dZdWxFUmZHbkFDWGNoa0Yzei9ITWR3elcwTUFFaXptQmwvREo2ZUoyU01PSG1Uc25YbElGRDRlcFRrYnFBQ0dpZ2I1UExFdHdQRVRjYkNRckM5YUtTU1FnSTdEZXd1aWlxM2J0Y0RUWkIzeEI5WWxlbmhpU0FXNjIwcmwzc2ZjY3d3eGFSOHBDV2Rzd0x3dmFxcDhjM01PV3RCc2xPcmVTSkNEcWgvdzBYbm1WMFJVWFpNM2JvUmkwVXhsaHVUeDFlM1NTd09pbTlOczNYV3NoTmI4Lzc3VkhnUWhRVFlSUU1NRllYaWRmMElCKzBtSUpocWNoQTlUeUY3dGRjSDhrUUJUSHNEWS96bFpqK3EwNlFMd0JkbTkxc3IyK3VzZmxlaXB3WUMrcmdiNHROVnA3VU5rYkVqTnR6ZWZsTi9VRTlkbHZtT2x6V1dtZkh2NGVkUGkzMmJmeUNRS1d6SGJVVEV3NU0yVFpsZnpNaTFWUjVsaDBxQ1lqaDNITUlmL2MwcHBKd2I1b1lFTnBBenlxbnlmdmlTV3lBYzc2L1l1VWwvb2FVaysrYzBZc2d1TGo5ZGFQdVVvemhoZ3VjSytQRGlNckI0ODU1Mk83VWg0aHRwNmZ3S2dJa1JCTVFIUTd6MmV5WXovV1AwQm9ZZVhjOGc3aUprclhFNzA1bFo1bXhGU0poT3E1WlNleVJSb21pUm41K3VRemM5ZFdWQjBYb2JURXdOc0VRM2FIZ25JY29BczY2UGplUT09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDI6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg== \ No newline at end of file diff --git a/social_core/tests/backends/test_saml.py b/social_core/tests/backends/test_saml.py index a59fbebaa..02d6af00c 100644 --- a/social_core/tests/backends/test_saml.py +++ b/social_core/tests/backends/test_saml.py @@ -32,6 +32,7 @@ class SAMLTest(BaseBackendTest): backend_path = "social_core.backends.saml.SAMLAuth" expected_username = "myself" + response_fixture = "saml_response.txt" def extra_settings(self): name = path.join(DATA_DIR, "saml_config.json") @@ -58,7 +59,7 @@ def install_http_intercepts(self, start_url, return_url): # 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: - name = path.join(DATA_DIR, "saml_response.txt") + name = path.join(DATA_DIR, self.response_fixture) with open(name) as response_file: response_url = response_file.read() HTTPretty.register_uri( @@ -91,14 +92,55 @@ def test_metadata_generation(self): self.assertEqual(len(errors), 0) self.assertEqual(xml.decode()[0], "<") - def test_login(self): - """Test that we can authenticate with a SAML IdP (TestShib)""" - # pretend we've started with a URL like /login/saml/?idp=testshib: + 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" - def test_login_no_idp(self): - """Logging in without an idp param should raise AuthMissingParameter""" with self.assertRaises(AuthMissingParameter): self.do_start() From 1025578471b02a494ef4253d3cea7f3bb861182c Mon Sep 17 00:00:00 2001 From: Carl Crowder Date: Thu, 9 Nov 2023 10:22:05 +0700 Subject: [PATCH 019/152] Create a backend for OAuth1 connection to Discogs --- social_core/backends/discogs.py | 37 +++++++++ social_core/tests/backends/test_discogs.py | 94 ++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 social_core/backends/discogs.py create mode 100644 social_core/tests/backends/test_discogs.py diff --git a/social_core/backends/discogs.py b/social_core/backends/discogs.py new file mode 100644 index 000000000..dad46eefc --- /dev/null +++ b/social_core/backends/discogs.py @@ -0,0 +1,37 @@ +""" +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): + 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) + ) diff --git a/social_core/tests/backends/test_discogs.py b/social_core/tests/backends/test_discogs.py new file mode 100644 index 000000000..394215d66 --- /dev/null +++ b/social_core/tests/backends/test_discogs.py @@ -0,0 +1,94 @@ +import json +from urllib.parse import urlencode + +from httpretty import HTTPretty + +from .oauth import OAuth1Test + + +class DiscsogsOAuth1Test(OAuth1Test): + _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): + HTTPretty.register_uri( + HTTPretty.GET, + uri="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", + } + ), + ) + HTTPretty.register_uri( + HTTPretty.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() From a670659949c46be89b3634acad861080d431e686 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 18:56:45 +0000 Subject: [PATCH 020/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.10.1 → 23.11.0](https://github.com/psf/black/compare/23.10.1...23.11.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ed8e39b15..a838e12b9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 23.11.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 From 51ff887052317b923c5e7d818fb7714b9b96991b Mon Sep 17 00:00:00 2001 From: Eshaan Bansal Date: Wed, 22 Nov 2023 21:04:39 +0530 Subject: [PATCH 021/152] feat: add new backend `BitbucketDataCenterOAuth2` (#856) * feat: add new backend BitbucketDataCenterOAuth2 * abstract away PKCE logic in BaseOAuth2PKCE for reuse * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * abstract away PKCE logic in BaseOAuth2PKCE for reuse * noqa flake8 line length rule for URLs in comments * chore: add BitbucketDataCenterOAuth2Test * chore: abstract PKCE tests in OAuth2PkcePlainTest, OAuth2PkceS256Test * noqa flake8 line length rule for URLs in comments * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * chore: fix isort errors * chore: improvements, address review suggestions * fix: docs URL * Apply suggestions from code review Co-authored-by: Johan Castiblanco <51926076+johanv26@users.noreply.github.com> --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Johan Castiblanco <51926076+johanv26@users.noreply.github.com> --- social_core/backends/bitbucket_datacenter.py | 107 +++++++++++++ social_core/backends/oauth.py | 68 ++++++++ social_core/backends/twitter_oauth2.py | 58 +------ social_core/tests/backends/oauth.py | 56 ++++++- .../backends/test_bitbucket_datacenter.py | 151 ++++++++++++++++++ .../tests/backends/test_twitter_oauth2.py | 59 ++----- 6 files changed, 395 insertions(+), 104 deletions(-) create mode 100644 social_core/backends/bitbucket_datacenter.py create mode 100644 social_core/tests/backends/test_bitbucket_datacenter.py diff --git a/social_core/backends/bitbucket_datacenter.py b/social_core/backends/bitbucket_datacenter.py new file mode 100644 index 000000000..419761486 --- /dev/null +++ b/social_core/backends/bitbucket_datacenter.py @@ -0,0 +1,107 @@ +""" +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 = " " + ACCESS_TOKEN_METHOD = "POST" + 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 # noqa + 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 # noqa + 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 # noqa + 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 + }, + ) diff --git a/social_core/backends/oauth.py b/social_core/backends/oauth.py index ae00aa5b2..57e146a59 100644 --- a/social_core/backends/oauth.py +++ b/social_core/backends/oauth.py @@ -1,3 +1,5 @@ +import base64 +import hashlib from urllib.parse import unquote, urlencode from oauthlib.oauth1 import SIGNATURE_TYPE_AUTH_HEADER @@ -5,6 +7,7 @@ from ..exceptions import ( AuthCanceled, + AuthException, AuthFailed, AuthMissingParameter, AuthStateForbidden, @@ -459,3 +462,68 @@ def refresh_token(self, token, *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" + code_verifier = self.strategy.session_get(name) + return code_verifier + + 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) + code_challenge = encoded.decode().replace("=", "") # remove padding + return code_challenge + if method == "plain": + return code_verifier + raise AuthException("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 diff --git a/social_core/backends/twitter_oauth2.py b/social_core/backends/twitter_oauth2.py index 4172fc36f..ead85b473 100644 --- a/social_core/backends/twitter_oauth2.py +++ b/social_core/backends/twitter_oauth2.py @@ -3,14 +3,10 @@ https://python-social-auth.readthedocs.io/en/latest/backends/twitter-oauth2.html https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code """ -import base64 -import hashlib +from .oauth import BaseOAuth2PKCE -from ..exceptions import AuthException -from .oauth import BaseOAuth2 - -class TwitterOAuth2(BaseOAuth2): +class TwitterOAuth2(BaseOAuth2PKCE): """Twitter OAuth2 authentication backend""" name = "twitter-oauth2" @@ -40,7 +36,8 @@ class TwitterOAuth2(BaseOAuth2): ("public_metrics", "public_metrics"), ] PKCE_DEFAULT_CODE_CHALLENGE_METHOD = "s256" - USE_PKCE = True + PKCE_DEFAULT_CODE_VERIFIER_LENGTH = 32 + DEFAULT_USE_PKCE = True def get_user_details(self, response): """Return user details from Twitter account""" @@ -104,50 +101,3 @@ def user_data(self, access_token, *args, **kwargs): headers={"Authorization": "Bearer %s" % access_token}, ) return response["data"] - - def create_code_verifier(self): - name = self.name + "_code_verifier" - code_verifier = self.strategy.random_string(32) - self.strategy.session_set(name, code_verifier) - return code_verifier - - def get_code_verifier(self): - name = self.name + "_code_verifier" - code_verifier = self.strategy.session_get(name) - return code_verifier - - 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) - code_challenge = encoded.decode().replace("=", "") # remove padding - return code_challenge - elif method == "plain": - return code_verifier - else: - raise AuthException("Unsupported code challenge method.") - - def auth_params(self, state=None): - params = super().auth_params(state=state) - - if self.USE_PKCE: - code_challenge_method = self.setting("PKCE_CODE_CHALLENGE_METHOD") - if not code_challenge_method: - code_challenge_method = 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.USE_PKCE: - code_verifier = self.get_code_verifier() - params["code_verifier"] = code_verifier - - return params diff --git a/social_core/tests/backends/oauth.py b/social_core/tests/backends/oauth.py index 44946d419..c00e014f7 100644 --- a/social_core/tests/backends/oauth.py +++ b/social_core/tests/backends/oauth.py @@ -1,7 +1,7 @@ from urllib.parse import urlparse import requests -from httpretty import HTTPretty +from httpretty import HTTPretty, latest_requests from ...utils import parse_qs, url_add_parameters from ..models import User @@ -121,3 +121,57 @@ def do_refresh_token(self): 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() + + requests = latest_requests() + auth_request = [ + r for r in requests if self.backend.authorization_url() in r.url + ][0] + code_challenge = auth_request.querystring.get("code_challenge")[0] + code_challenge_method = auth_request.querystring.get("code_challenge_method")[0] + self.assertIsNotNone(code_challenge) + self.assertEqual(code_challenge_method, "plain") + + auth_complete = [ + r for r in requests if self.backend.access_token_url() in r.url + ][0] + code_verifier = auth_complete.parsed_body.get("code_verifier")[0] + 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() + + requests = latest_requests() + auth_request = [ + r for r in requests if self.backend.authorization_url() in r.url + ][0] + code_challenge = auth_request.querystring.get("code_challenge")[0] + code_challenge_method = auth_request.querystring.get("code_challenge_method")[0] + self.assertIsNotNone(code_challenge) + self.assertEqual(code_challenge_method, "s256") + + auth_complete = [ + r for r in requests if self.backend.access_token_url() in r.url + ][0] + code_verifier = auth_complete.parsed_body.get("code_verifier")[0] + self.assertEqual( + self.backend.generate_code_challenge(code_verifier, "s256"), code_challenge + ) + + return user diff --git a/social_core/tests/backends/test_bitbucket_datacenter.py b/social_core/tests/backends/test_bitbucket_datacenter.py new file mode 100644 index 000000000..8a3d3f9c5 --- /dev/null +++ b/social_core/tests/backends/test_bitbucket_datacenter.py @@ -0,0 +1,151 @@ +import json + +from httpretty import HTTPretty + +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) + HTTPretty.register_uri( + HTTPretty.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 diff --git a/social_core/tests/backends/test_twitter_oauth2.py b/social_core/tests/backends/test_twitter_oauth2.py index d003fb77f..5ed76abfd 100644 --- a/social_core/tests/backends/test_twitter_oauth2.py +++ b/social_core/tests/backends/test_twitter_oauth2.py @@ -1,13 +1,11 @@ import json -import httpretty - from social_core.exceptions import AuthException -from .oauth import OAuth2Test +from .oauth import OAuth2PkcePlainTest, OAuth2PkceS256Test, OAuth2Test -class TwitterOAuth2Test(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( @@ -172,58 +170,21 @@ def test_login(self): self.assertIsNone(social.extra_data.get("public_metrics")) -class TwitterOAuth2TestPkcePlain(TwitterOAuth2Test): - def test_login(self): - self.strategy.set_settings( - {"SOCIAL_AUTH_TWITTER_OAUTH2_PKCE_CODE_CHALLENGE_METHOD": "plain"} - ) - - self.do_login() - - requests = httpretty.latest_requests() - auth_request = [ - r for r in requests if "https://twitter.com/i/oauth2/authorize" in r.url - ][0] - code_challenge = auth_request.querystring.get("code_challenge")[0] - code_challenge_method = auth_request.querystring.get("code_challenge_method")[0] - self.assertIsNotNone(code_challenge) - self.assertEqual(code_challenge_method, "plain") - - auth_complete = [ - r for r in requests if "https://api.twitter.com/2/oauth2/token" in r.url - ][0] - code_verifier = auth_complete.parsed_body.get("code_verifier")[0] - self.assertEqual(code_challenge, code_verifier) +class TwitterOAuth2TestPkcePlain(TwitterOAuth2Mixin, OAuth2PkcePlainTest): + pass -class TwitterOAuth2TestPkceS256(TwitterOAuth2Test): - def test_login(self): - # use default value of PKCE_CODE_CHALLENGE_METHOD (s256) - self.do_login() - - requests = httpretty.latest_requests() - auth_request = [ - r for r in requests if "https://twitter.com/i/oauth2/authorize" in r.url - ][0] - code_challenge = auth_request.querystring.get("code_challenge")[0] - code_challenge_method = auth_request.querystring.get("code_challenge_method")[0] - self.assertIsNotNone(code_challenge) - self.assertEqual(code_challenge_method, "s256") - - auth_complete = [ - r for r in requests if "https://api.twitter.com/2/oauth2/toke" in r.url - ][0] - code_verifier = auth_complete.parsed_body.get("code_verifier")[0] - self.assertEqual( - self.backend.generate_code_challenge(code_verifier, "s256"), code_challenge - ) +class TwitterOAuth2TestPkceS256(TwitterOAuth2Mixin, OAuth2PkceS256Test): + pass -class TwitterOAuth2TestInvalidCodeChallengeMethod(TwitterOAuth2Test): +class TwitterOAuth2TestInvalidCodeChallengeMethod( + TwitterOAuth2Mixin, OAuth2PkcePlainTest +): def test_login__error(self): self.strategy.set_settings( { - "SOCIAL_AUTH_TWITTER_OAUTH2_PKCE_CODE_CHALLENGE_METHOD": "invalidmethodname" + f"SOCIAL_AUTH_{self.name}_PKCE_CODE_CHALLENGE_METHOD": "invalidmethodname", } ) From 0925304a9e437f8b729862687d3a808c7fb88a95 Mon Sep 17 00:00:00 2001 From: Rob Percival Date: Wed, 15 Nov 2023 16:26:14 +0000 Subject: [PATCH 022/152] Revert "Make Keycloak's ID_KEY configurable" This reverts commit 70d7713ba8bd61a0fa711f062b73da707efb8ef0. --- CHANGELOG.md | 1 - social_core/backends/keycloak.py | 14 +++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6330d9c99..c438d9ef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### 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 diff --git a/social_core/backends/keycloak.py b/social_core/backends/keycloak.py index d5d2751f8..494ddabdb 100644 --- a/social_core/backends/keycloak.py +++ b/social_core/backends/keycloak.py @@ -96,6 +96,7 @@ class KeycloakOAuth2(BaseOAuth2): # pylint: disable=abstract-method """ name = "keycloak" + ID_KEY = "username" ACCESS_TOKEN_METHOD = "POST" REDIRECT_STATE = False @@ -120,9 +121,6 @@ def public_key(self): ] ) - def id_key(self): - return self.setting("ID_KEY", default="username") - def user_data( self, access_token, *args, **kwargs ): # pylint: disable=unused-argument @@ -151,11 +149,5 @@ def get_user_details(self, response): } def get_user_id(self, details, response): - """Get and associate Django User by the field indicated by ID_KEY - - The ID_KEY can be any field in the user details or the access token. - """ - id_key = self.id_key() - if id_key in details: - return details[id_key] - return response.get(id_key) + """Get and associate Django User by the field indicated by ID_KEY""" + return details.get(self.ID_KEY) From f1b9fa5e9084a592af112f50ef2c90e2fa71190b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Wed, 29 Nov 2023 09:49:17 +0100 Subject: [PATCH 023/152] Version bump 4.5.1 --- CHANGELOG.md | 11 +++++++++++ social_core/__init__.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c438d9ef3..7892b7fa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ 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.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 @@ -24,6 +34,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### 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 diff --git a/social_core/__init__.py b/social_core/__init__.py index 9faa2c2dd..f9f7166e6 100644 --- a/social_core/__init__.py +++ b/social_core/__init__.py @@ -1 +1 @@ -__version__ = "4.5.0" +__version__ = "4.5.1" From a01ba25646b4e9dd0a26acc8a670ed44ddb22546 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 17:23:47 +0000 Subject: [PATCH 024/152] build(deps): bump actions/setup-python from 4 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/flake8.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml index f8a63ced1..b70c68ba1 100644 --- a/.github/workflows/flake8.yml +++ b/.github/workflows/flake8.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.x cache: pip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3f839b7d7..5e440fabe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.9' cache: pip diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3d598095b..8bdd020a4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip From d5847011cca8a6cdc3bd26488df7fd84cff12903 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 18:53:57 +0000 Subject: [PATCH 025/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pycqa/isort: 5.12.0 → 5.13.0](https://github.com/pycqa/isort/compare/5.12.0...5.13.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a838e12b9..929bf4f92 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: mixed-line-ending args: [--fix=lf] - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.13.0 hooks: - id: isort args: [--profile=black] From 26bdff5edc07827f1ac2d14053f24568e3beb0ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 17:14:58 +0000 Subject: [PATCH 026/152] build(deps-dev): bump pre-commit from 3.5.0 to 3.6.0 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.5.0 to 3.6.0. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.5.0...v3.6.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 959c06088..5cef2202d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1 @@ -pre-commit==3.5.0 +pre-commit==3.6.0 From 9498b4e7bf899ebd822670b844ae26a5827f953b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:37:59 +0000 Subject: [PATCH 027/152] build(deps): bump actions/upload-artifact from 3 to 4 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e440fabe..982bedbd7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: run: python setup.py sdist bdist_wheel --python-tag py3 - name: Archive dist - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: dist path: | From 044f3c1f68d153d60129ec8346102753d9c5d969 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 18:58:30 +0000 Subject: [PATCH 028/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pycqa/isort: 5.13.0 → 5.13.2](https://github.com/pycqa/isort/compare/5.13.0...5.13.2) - [github.com/psf/black: 23.11.0 → 23.12.0](https://github.com/psf/black/compare/23.11.0...23.12.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 929bf4f92..1796246c2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: mixed-line-ending args: [--fix=lf] - repo: https://github.com/pycqa/isort - rev: 5.13.0 + rev: 5.13.2 hooks: - id: isort args: [--profile=black] @@ -20,7 +20,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.11.0 + rev: 23.12.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 From 61a5ec1196731d69ed3540a06a6d5ad555f6ebac Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 18:48:01 +0000 Subject: [PATCH 029/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.12.0 → 23.12.1](https://github.com/psf/black/compare/23.12.0...23.12.1) - [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.11.0 → v2.12.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.11.0...v2.12.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1796246c2..09238ccdb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.12.0 + rev: 23.12.1 hooks: - id: black - repo: https://github.com/PyCQA/flake8 @@ -32,7 +32,7 @@ repos: - id: check-hooks-apply - id: check-useless-excludes - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks - rev: v2.11.0 + rev: v2.12.0 hooks: - id: pretty-format-yaml args: [--autofix, --indent, '2'] From 03f67ae837f7ab04be4db2f82cb3adf5584d1be7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 18:46:17 +0000 Subject: [PATCH 030/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 6.1.0 → 7.0.0](https://github.com/PyCQA/flake8/compare/6.1.0...7.0.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 09238ccdb..c577a94c2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 + rev: 7.0.0 hooks: - id: flake8 - repo: meta From 34e474673ce06059e144ec2acebf689a2dde76f2 Mon Sep 17 00:00:00 2001 From: Jeppe Fihl-Pearson Date: Fri, 12 Jan 2024 09:28:24 +0000 Subject: [PATCH 031/152] Update the Facebook API version from 12.0 to 18.0 Version 12.0 of the API is only available until February 8th, 2024 after which requests will be force upgraded to version 13.0. Version 18.0 is the latest available version and there are no changes to the endpoints requested by social-core so we can jump straight to the latest available version. Changelog: https://developers.facebook.com/docs/graph-api/changelog. --- CHANGELOG.md | 6 ++++++ social_core/backends/facebook.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7892b7fa7..1c8d3b5f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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/). +## [Unreleased] + +### Changed +- Updated Facebook API version to 18.0 + + ## [4.5.1](https://github.com/python-social-auth/social-core/releases/tag/4.5.1) - 2023-11-29 ### Changed diff --git a/social_core/backends/facebook.py b/social_core/backends/facebook.py index c54dadf58..9e7d9ba73 100644 --- a/social_core/backends/facebook.py +++ b/social_core/backends/facebook.py @@ -17,7 +17,7 @@ from ..utils import constant_time_compare, handle_http_errors, parse_qs from .oauth import BaseOAuth2 -API_VERSION = 12.0 +API_VERSION = 18.0 class FacebookOAuth2(BaseOAuth2): From 9d3ba91a632e4bf031842558aedaa5a7cf64ae7d Mon Sep 17 00:00:00 2001 From: Amit Ray <51674969+amitray007@users.noreply.github.com> Date: Fri, 12 Jan 2024 22:46:29 +0530 Subject: [PATCH 032/152] feat: Added Backend 'EtsyOAuth2' (#874) * feat: Added Backend 'EtsyOAuth2' * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- social_core/backends/etsy.py | 42 ++++++++++++ social_core/tests/backends/test_etsy.py | 86 +++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 social_core/backends/etsy.py create mode 100644 social_core/tests/backends/test_etsy.py diff --git a/social_core/backends/etsy.py b/social_core/backends/etsy.py new file mode 100644 index 000000000..980d7059b --- /dev/null +++ b/social_core/backends/etsy.py @@ -0,0 +1,42 @@ +from .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" + ACCESS_TOKEN_METHOD = "POST" + REQUEST_TOKEN_METHOD = "POST" + SCOPE_SEPARATOR = " " + 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": response["user_id"], + } diff --git a/social_core/tests/backends/test_etsy.py b/social_core/tests/backends/test_etsy.py new file mode 100644 index 000000000..f7d1e9cce --- /dev/null +++ b/social_core/tests/backends/test_etsy.py @@ -0,0 +1,86 @@ +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 From a5a34d50e9a095461abda7546e2ac94ca101cb4b Mon Sep 17 00:00:00 2001 From: Alisson Patricio Date: Thu, 21 Dec 2023 13:17:45 -0300 Subject: [PATCH 033/152] Make AppleID work with multiple identifiers When creating an application that uses 1 backend and multiple frontends (webapp and iOS), the backend needs to know to what App ID or Service ID you want the access token from Apple. --- social_core/backends/apple.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social_core/backends/apple.py b/social_core/backends/apple.py index 75b834947..2325a257f 100644 --- a/social_core/backends/apple.py +++ b/social_core/backends/apple.py @@ -75,7 +75,7 @@ def get_private_key(self): def generate_client_secret(self): now = int(time.time()) - client_id = self.setting("CLIENT") + 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() @@ -92,7 +92,7 @@ def generate_client_secret(self): return jwt.encode(payload, key=private_key, algorithm="ES256", headers=headers) def get_key_and_secret(self): - client_id = self.setting("CLIENT") + client_id = self.data.get("client_id", self.setting("CLIENT")) client_secret = self.generate_client_secret() return client_id, client_secret From ca41f2a7ba354a2b57708d6a25017f3f2f1ad28c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 26 Jan 2024 11:16:26 +0100 Subject: [PATCH 034/152] Version bump 4.5.2 --- CHANGELOG.md | 7 +++++-- social_core/__init__.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c8d3b5f6..f8fd5c971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,14 @@ 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/). -## [Unreleased] +## [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 diff --git a/social_core/__init__.py b/social_core/__init__.py index f9f7166e6..f490a9647 100644 --- a/social_core/__init__.py +++ b/social_core/__init__.py @@ -1 +1 @@ -__version__ = "4.5.1" +__version__ = "4.5.2" From c19ae66620117aa3fa18be20a119929b88ff8fda Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:44:51 +0000 Subject: [PATCH 035/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.12.1 → 24.1.1](https://github.com/psf/black/compare/23.12.1...24.1.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c577a94c2..06fab77ef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.12.1 + rev: 24.1.1 hooks: - id: black - repo: https://github.com/PyCQA/flake8 From 22dcdaabeb1a7f83340a9fdd95ac7d536bac63a0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:45:10 +0000 Subject: [PATCH 036/152] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- social_core/backends/angel.py | 1 + social_core/backends/aol.py | 1 + social_core/backends/appsfuel.py | 1 + social_core/backends/arcgis.py | 1 + social_core/backends/auth0.py | 1 + social_core/backends/beats.py | 1 + social_core/backends/behance.py | 1 + social_core/backends/belgiumeid.py | 1 + social_core/backends/bitbucket.py | 1 + social_core/backends/box.py | 1 + social_core/backends/bungie.py | 1 + social_core/backends/chatwork.py | 1 + social_core/backends/coding.py | 1 + social_core/backends/coinbase.py | 1 + social_core/backends/coursera.py | 1 + social_core/backends/dailymotion.py | 1 + social_core/backends/deezer.py | 1 + social_core/backends/discord.py | 1 + social_core/backends/disqus.py | 1 + social_core/backends/docker.py | 1 + social_core/backends/douban.py | 1 + social_core/backends/drip.py | 1 + social_core/backends/edmodo.py | 1 + social_core/backends/email.py | 1 + social_core/backends/eveonline.py | 1 + social_core/backends/evernote.py | 1 + social_core/backends/exacttarget.py | 1 + social_core/backends/facebook.py | 1 + social_core/backends/facebook_limited.py | 1 + social_core/backends/fedora.py | 1 + social_core/backends/fitbit.py | 1 + social_core/backends/five_hundred_px.py | 1 + social_core/backends/flat.py | 1 + social_core/backends/flickr.py | 1 + social_core/backends/foursquare.py | 1 + social_core/backends/gitea.py | 1 + social_core/backends/github.py | 1 + social_core/backends/github_enterprise.py | 1 + social_core/backends/gitlab.py | 1 + social_core/backends/google.py | 1 + social_core/backends/google_openidconnect.py | 1 + social_core/backends/hubspot.py | 1 + social_core/backends/instagram.py | 1 + social_core/backends/jawbone.py | 1 + social_core/backends/kakao.py | 1 + social_core/backends/khanacademy.py | 1 + social_core/backends/line.py | 1 + social_core/backends/linkedin.py | 1 + social_core/backends/live.py | 1 + social_core/backends/livejournal.py | 1 + social_core/backends/loginradius.py | 1 + social_core/backends/lyft.py | 1 + social_core/backends/mailru.py | 1 + social_core/backends/mapmyfitness.py | 1 + social_core/backends/meetup.py | 1 + social_core/backends/mendeley.py | 1 + social_core/backends/microsoft.py | 1 + social_core/backends/mixcloud.py | 1 + social_core/backends/moves.py | 1 + social_core/backends/nationbuilder.py | 1 + social_core/backends/ngpvan.py | 1 + social_core/backends/odnoklassniki.py | 1 + social_core/backends/okta.py | 1 + social_core/backends/okta_openidconnect.py | 1 + social_core/backends/openinfra.py | 1 + social_core/backends/openshift.py | 1 + social_core/backends/openstack.py | 1 + social_core/backends/openstreetmap.py | 1 + social_core/backends/orbi.py | 1 + social_core/backends/orcid.py | 1 + social_core/backends/patreon.py | 1 + social_core/backends/persona.py | 1 + social_core/backends/phabricator.py | 1 + social_core/backends/pocket.py | 1 + social_core/backends/podio.py | 1 + social_core/backends/professionali.py | 1 + social_core/backends/qiita.py | 1 + social_core/backends/quizlet.py | 1 + social_core/backends/rdio.py | 1 + social_core/backends/readability.py | 1 + social_core/backends/reddit.py | 1 + social_core/backends/runkeeper.py | 1 + social_core/backends/saml.py | 1 + social_core/backends/seznam.py | 1 + social_core/backends/shimmering.py | 1 + social_core/backends/shopify.py | 1 + social_core/backends/sketchfab.py | 1 + social_core/backends/skyrock.py | 1 + social_core/backends/soundcloud.py | 1 + social_core/backends/spotify.py | 1 + social_core/backends/stackoverflow.py | 1 + social_core/backends/steam.py | 1 + social_core/backends/stocktwits.py | 1 + social_core/backends/strava.py | 1 + social_core/backends/stripe.py | 1 + social_core/backends/surveymonkey.py | 1 + social_core/backends/suse.py | 1 + social_core/backends/thisismyjam.py | 1 + social_core/backends/trello.py | 2 +- social_core/backends/tripit.py | 1 + social_core/backends/tumblr.py | 1 + social_core/backends/twilio.py | 1 + social_core/backends/twitch.py | 1 + social_core/backends/twitter.py | 1 + social_core/backends/twitter_oauth2.py | 1 + social_core/backends/uber.py | 1 + social_core/backends/ubuntu.py | 1 + social_core/backends/udata.py | 1 + social_core/backends/upwork.py | 1 + social_core/backends/username.py | 1 + social_core/backends/vault.py | 1 + social_core/backends/vend.py | 1 + social_core/backends/vk.py | 1 + social_core/backends/xing.py | 1 + social_core/backends/yahoo.py | 1 + social_core/backends/yammer.py | 1 + social_core/backends/yandex.py | 1 + social_core/backends/zotero.py | 2 +- social_core/storage.py | 1 + social_core/tests/backends/test_azuread_b2c.py | 1 + social_core/tests/backends/test_ngpvan.py | 1 + social_core/tests/backends/test_podio.py | 2 +- 122 files changed, 122 insertions(+), 3 deletions(-) diff --git a/social_core/backends/angel.py b/social_core/backends/angel.py index 4793c4bdf..abeff0c3c 100644 --- a/social_core/backends/angel.py +++ b/social_core/backends/angel.py @@ -2,6 +2,7 @@ Angel OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/angel.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/aol.py b/social_core/backends/aol.py index 511c42cff..ab9aac93e 100644 --- a/social_core/backends/aol.py +++ b/social_core/backends/aol.py @@ -2,6 +2,7 @@ AOL OpenId backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/aol.html """ + from .open_id import OpenIdAuth diff --git a/social_core/backends/appsfuel.py b/social_core/backends/appsfuel.py index 29f1e400b..e9c1ab050 100644 --- a/social_core/backends/appsfuel.py +++ b/social_core/backends/appsfuel.py @@ -2,6 +2,7 @@ Appsfueld OAuth2 backend (with sandbox mode support), docs at: https://python-social-auth.readthedocs.io/en/latest/backends/appsfuel.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/arcgis.py b/social_core/backends/arcgis.py index 03e4e691a..6db1cda54 100644 --- a/social_core/backends/arcgis.py +++ b/social_core/backends/arcgis.py @@ -1,6 +1,7 @@ """ ArcGIS OAuth2 backend """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/auth0.py b/social_core/backends/auth0.py index 23ef453ba..c2d188138 100644 --- a/social_core/backends/auth0.py +++ b/social_core/backends/auth0.py @@ -2,6 +2,7 @@ Auth0 implementation based on: https://auth0.com/docs/quickstart/webapp/django/01-login """ + import jwt from .oauth import BaseOAuth2 diff --git a/social_core/backends/beats.py b/social_core/backends/beats.py index 7e70c7618..531e8e328 100644 --- a/social_core/backends/beats.py +++ b/social_core/backends/beats.py @@ -2,6 +2,7 @@ Beats backend, docs at: https://developer.beatsmusic.com/docs """ + import base64 from ..utils import handle_http_errors diff --git a/social_core/backends/behance.py b/social_core/backends/behance.py index b04552dc7..299c6b684 100644 --- a/social_core/backends/behance.py +++ b/social_core/backends/behance.py @@ -2,6 +2,7 @@ Behance OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/behance.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/belgiumeid.py b/social_core/backends/belgiumeid.py index 1cfc29722..760234b0f 100644 --- a/social_core/backends/belgiumeid.py +++ b/social_core/backends/belgiumeid.py @@ -2,6 +2,7 @@ Belgium EID OpenId backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/belgium_eid.html """ + from .open_id import OpenIdAuth diff --git a/social_core/backends/bitbucket.py b/social_core/backends/bitbucket.py index 728913140..bc65759ae 100644 --- a/social_core/backends/bitbucket.py +++ b/social_core/backends/bitbucket.py @@ -2,6 +2,7 @@ Bitbucket OAuth2 and OAuth1 backends, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/bitbucket.html """ + from ..exceptions import AuthForbidden from .oauth import BaseOAuth1, BaseOAuth2 diff --git a/social_core/backends/box.py b/social_core/backends/box.py index 2fc86dd28..02721e88a 100644 --- a/social_core/backends/box.py +++ b/social_core/backends/box.py @@ -2,6 +2,7 @@ Box.net OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/box.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/bungie.py b/social_core/backends/bungie.py index cba20e402..e062dc280 100644 --- a/social_core/backends/bungie.py +++ b/social_core/backends/bungie.py @@ -1,6 +1,7 @@ """ Bungie OAuth2 backend """ + from social_core.backends.oauth import BaseOAuth2 diff --git a/social_core/backends/chatwork.py b/social_core/backends/chatwork.py index 43efd0367..382685a1a 100644 --- a/social_core/backends/chatwork.py +++ b/social_core/backends/chatwork.py @@ -1,6 +1,7 @@ """ Chatwork OAuth2 backend """ + import base64 from .oauth import BaseOAuth2 diff --git a/social_core/backends/coding.py b/social_core/backends/coding.py index 10dd8f0ea..51255d69c 100644 --- a/social_core/backends/coding.py +++ b/social_core/backends/coding.py @@ -1,6 +1,7 @@ """ Coding OAuth2 backend, docs at: """ + from urllib.parse import urljoin from .oauth import BaseOAuth2 diff --git a/social_core/backends/coinbase.py b/social_core/backends/coinbase.py index 2f5cf1527..550a1ae8c 100644 --- a/social_core/backends/coinbase.py +++ b/social_core/backends/coinbase.py @@ -2,6 +2,7 @@ Coinbase OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/coinbase.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/coursera.py b/social_core/backends/coursera.py index ede3220b2..f5b783dc1 100644 --- a/social_core/backends/coursera.py +++ b/social_core/backends/coursera.py @@ -2,6 +2,7 @@ Coursera OAuth2 backend, docs at: https://tech.coursera.org/app-platform/oauth2/ """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/dailymotion.py b/social_core/backends/dailymotion.py index 90c77b2fb..6b2f34cc4 100644 --- a/social_core/backends/dailymotion.py +++ b/social_core/backends/dailymotion.py @@ -2,6 +2,7 @@ DailyMotion OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/dailymotion.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/deezer.py b/social_core/backends/deezer.py index 334de4b5d..270896f48 100644 --- a/social_core/backends/deezer.py +++ b/social_core/backends/deezer.py @@ -3,6 +3,7 @@ https://developers.deezer.com/api/oauth https://developers.deezer.com/api/permissions """ + from urllib.parse import parse_qsl from .oauth import BaseOAuth2 diff --git a/social_core/backends/discord.py b/social_core/backends/discord.py index a8cb7dc74..023eb6318 100644 --- a/social_core/backends/discord.py +++ b/social_core/backends/discord.py @@ -2,6 +2,7 @@ Discord Auth OAuth2 backend, docs at: https://discord.com/developers/docs/topics/oauth2 """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/disqus.py b/social_core/backends/disqus.py index a6da5947a..f204b6c27 100644 --- a/social_core/backends/disqus.py +++ b/social_core/backends/disqus.py @@ -2,6 +2,7 @@ Disqus OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/disqus.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/docker.py b/social_core/backends/docker.py index df3642e5f..ffb5e394c 100644 --- a/social_core/backends/docker.py +++ b/social_core/backends/docker.py @@ -2,6 +2,7 @@ Docker Hub OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/docker.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/douban.py b/social_core/backends/douban.py index d021f0e40..362e64c08 100644 --- a/social_core/backends/douban.py +++ b/social_core/backends/douban.py @@ -2,6 +2,7 @@ Douban OAuth1 and OAuth2 backends, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/douban.html """ + from .oauth import BaseOAuth1, BaseOAuth2 diff --git a/social_core/backends/drip.py b/social_core/backends/drip.py index 3d5741894..7c163dbb1 100644 --- a/social_core/backends/drip.py +++ b/social_core/backends/drip.py @@ -2,6 +2,7 @@ Drip OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/drip.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/edmodo.py b/social_core/backends/edmodo.py index 3ec29e71e..d954dbe4d 100644 --- a/social_core/backends/edmodo.py +++ b/social_core/backends/edmodo.py @@ -2,6 +2,7 @@ Edmodo OAuth2 Sign-in backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/edmodo.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/email.py b/social_core/backends/email.py index c41f0c2e4..bd34c24cb 100644 --- a/social_core/backends/email.py +++ b/social_core/backends/email.py @@ -2,6 +2,7 @@ Legacy Email backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/email.html """ + from .legacy import LegacyAuth diff --git a/social_core/backends/eveonline.py b/social_core/backends/eveonline.py index 2487355b4..ae215bce3 100644 --- a/social_core/backends/eveonline.py +++ b/social_core/backends/eveonline.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/evernote.py b/social_core/backends/evernote.py index 768478743..7147e2dcc 100644 --- a/social_core/backends/evernote.py +++ b/social_core/backends/evernote.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/exacttarget.py b/social_core/backends/exacttarget.py index cdaf7ca87..b8679a7ee 100644 --- a/social_core/backends/exacttarget.py +++ b/social_core/backends/exacttarget.py @@ -3,6 +3,7 @@ Support Authentication from IMH using JWT token and pre-shared key. Requires package pyjwt """ + from datetime import datetime, timedelta import jwt diff --git a/social_core/backends/facebook.py b/social_core/backends/facebook.py index 9e7d9ba73..c495b1d18 100644 --- a/social_core/backends/facebook.py +++ b/social_core/backends/facebook.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/facebook_limited.py b/social_core/backends/facebook_limited.py index 9e09d9bca..11df8a07e 100644 --- a/social_core/backends/facebook_limited.py +++ b/social_core/backends/facebook_limited.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/fedora.py b/social_core/backends/fedora.py index b212581cd..429177373 100644 --- a/social_core/backends/fedora.py +++ b/social_core/backends/fedora.py @@ -2,6 +2,7 @@ Fedora OpenId backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/fedora.html """ + from .open_id import OpenIdAuth diff --git a/social_core/backends/fitbit.py b/social_core/backends/fitbit.py index 024d3c219..40b77e789 100644 --- a/social_core/backends/fitbit.py +++ b/social_core/backends/fitbit.py @@ -2,6 +2,7 @@ Fitbit OAuth backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/fitbit.html """ + import base64 from .oauth import BaseOAuth1, BaseOAuth2 diff --git a/social_core/backends/five_hundred_px.py b/social_core/backends/five_hundred_px.py index 30fb87fbe..317701bce 100644 --- a/social_core/backends/five_hundred_px.py +++ b/social_core/backends/five_hundred_px.py @@ -2,6 +2,7 @@ 500px OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/five_hundred_px.html """ + from .oauth import BaseOAuth1 diff --git a/social_core/backends/flat.py b/social_core/backends/flat.py index f14755bda..773beb886 100644 --- a/social_core/backends/flat.py +++ b/social_core/backends/flat.py @@ -2,6 +2,7 @@ Flat OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/flat.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/flickr.py b/social_core/backends/flickr.py index a2e5b4117..21983f879 100644 --- a/social_core/backends/flickr.py +++ b/social_core/backends/flickr.py @@ -2,6 +2,7 @@ Flickr OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/flickr.html """ + from .oauth import BaseOAuth1 diff --git a/social_core/backends/foursquare.py b/social_core/backends/foursquare.py index cf87a2c74..096dd9c66 100644 --- a/social_core/backends/foursquare.py +++ b/social_core/backends/foursquare.py @@ -2,6 +2,7 @@ Foursquare OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/foursquare.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/gitea.py b/social_core/backends/gitea.py index 3c67d8e26..4fe3fe01c 100644 --- a/social_core/backends/gitea.py +++ b/social_core/backends/gitea.py @@ -2,6 +2,7 @@ Gitea OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/gitea.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/github.py b/social_core/backends/github.py index 40b039692..e97322d22 100644 --- a/social_core/backends/github.py +++ b/social_core/backends/github.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/github_enterprise.py b/social_core/backends/github_enterprise.py index 7c1d0daaf..907177a71 100644 --- a/social_core/backends/github_enterprise.py +++ b/social_core/backends/github_enterprise.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/gitlab.py b/social_core/backends/gitlab.py index fbb8797d0..38851bcfb 100644 --- a/social_core/backends/gitlab.py +++ b/social_core/backends/gitlab.py @@ -7,6 +7,7 @@ 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 diff --git a/social_core/backends/google.py b/social_core/backends/google.py index 211313cfa..aafc2c761 100644 --- a/social_core/backends/google.py +++ b/social_core/backends/google.py @@ -2,6 +2,7 @@ Google OpenId, OAuth2, OAuth1, Google+ Sign-in backends, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/google.html """ + from ..exceptions import AuthMissingParameter from ..utils import handle_http_errors from .oauth import BaseOAuth1, BaseOAuth2 diff --git a/social_core/backends/google_openidconnect.py b/social_core/backends/google_openidconnect.py index 6d1fbf638..e76da94cb 100644 --- a/social_core/backends/google_openidconnect.py +++ b/social_core/backends/google_openidconnect.py @@ -2,6 +2,7 @@ Google OpenIdConnect: https://python-social-auth.readthedocs.io/en/latest/backends/google.html """ + from .google import GoogleOAuth2 from .open_id_connect import OpenIdConnectAuth diff --git a/social_core/backends/hubspot.py b/social_core/backends/hubspot.py index 8c47a6fd4..134b53f0f 100644 --- a/social_core/backends/hubspot.py +++ b/social_core/backends/hubspot.py @@ -2,6 +2,7 @@ HubSpot OAuth2 backend, docs at: https://developers.hubspot.com/docs/methods/oauth2/oauth2-overview """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/instagram.py b/social_core/backends/instagram.py index 1dbb777c5..f7aac0d25 100644 --- a/social_core/backends/instagram.py +++ b/social_core/backends/instagram.py @@ -2,6 +2,7 @@ Instagram OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/instagram.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/jawbone.py b/social_core/backends/jawbone.py index 4fe2440c1..ca77f42dd 100644 --- a/social_core/backends/jawbone.py +++ b/social_core/backends/jawbone.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/kakao.py b/social_core/backends/kakao.py index 717719c2f..0441e593e 100644 --- a/social_core/backends/kakao.py +++ b/social_core/backends/kakao.py @@ -2,6 +2,7 @@ Kakao OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/kakao.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/khanacademy.py b/social_core/backends/khanacademy.py index 00015bb5c..16c65cf9d 100644 --- a/social_core/backends/khanacademy.py +++ b/social_core/backends/khanacademy.py @@ -2,6 +2,7 @@ Khan Academy OAuth backend, docs at: https://github.com/Khan/khan-api/wiki/Khan-Academy-API-Authentication """ + from urllib.parse import urlencode from oauthlib.oauth1 import SIGNATURE_HMAC, SIGNATURE_TYPE_QUERY diff --git a/social_core/backends/line.py b/social_core/backends/line.py index d9ff989c5..757bc05cb 100644 --- a/social_core/backends/line.py +++ b/social_core/backends/line.py @@ -2,6 +2,7 @@ LINE Login OAuth2 backend, docs at: https://developers.line.me/en/docs/line-login/ """ + import json import requests diff --git a/social_core/backends/linkedin.py b/social_core/backends/linkedin.py index 07fbf0be5..2edb72f48 100644 --- a/social_core/backends/linkedin.py +++ b/social_core/backends/linkedin.py @@ -2,6 +2,7 @@ LinkedIn OAuth1 and OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/linkedin.html """ + import datetime from calendar import timegm diff --git a/social_core/backends/live.py b/social_core/backends/live.py index d075ccc36..d09f63ffa 100644 --- a/social_core/backends/live.py +++ b/social_core/backends/live.py @@ -2,6 +2,7 @@ Live OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/live.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/livejournal.py b/social_core/backends/livejournal.py index 6f367bb50..636a25f2b 100644 --- a/social_core/backends/livejournal.py +++ b/social_core/backends/livejournal.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/loginradius.py b/social_core/backends/loginradius.py index 42bd574cf..b7e5b3b12 100644 --- a/social_core/backends/loginradius.py +++ b/social_core/backends/loginradius.py @@ -2,6 +2,7 @@ LoginRadius BaseOAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/loginradius.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/lyft.py b/social_core/backends/lyft.py index f48170315..2bb041e6e 100644 --- a/social_core/backends/lyft.py +++ b/social_core/backends/lyft.py @@ -2,6 +2,7 @@ Lyft OAuth2 backend. Read more about the API at https://developer.lyft.com/docs """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/mailru.py b/social_core/backends/mailru.py index cabf9a550..5968a4a99 100644 --- a/social_core/backends/mailru.py +++ b/social_core/backends/mailru.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/mapmyfitness.py b/social_core/backends/mapmyfitness.py index 7ea8cb4ca..038ca340f 100644 --- a/social_core/backends/mapmyfitness.py +++ b/social_core/backends/mapmyfitness.py @@ -2,6 +2,7 @@ MapMyFitness OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/mapmyfitness.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/meetup.py b/social_core/backends/meetup.py index a064c270f..00de0d287 100644 --- a/social_core/backends/meetup.py +++ b/social_core/backends/meetup.py @@ -2,6 +2,7 @@ Meetup OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/meetup.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/mendeley.py b/social_core/backends/mendeley.py index 9ed5d5779..677351fba 100644 --- a/social_core/backends/mendeley.py +++ b/social_core/backends/mendeley.py @@ -2,6 +2,7 @@ Mendeley OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/mendeley.html """ + from .oauth import BaseOAuth1, BaseOAuth2 diff --git a/social_core/backends/microsoft.py b/social_core/backends/microsoft.py index 1114822f5..7817230b1 100644 --- a/social_core/backends/microsoft.py +++ b/social_core/backends/microsoft.py @@ -1,6 +1,7 @@ """ OAuth2 Backend to work with microsoft graph. """ + import time from .oauth import BaseOAuth2 diff --git a/social_core/backends/mixcloud.py b/social_core/backends/mixcloud.py index 04c00ec58..508e88ee5 100644 --- a/social_core/backends/mixcloud.py +++ b/social_core/backends/mixcloud.py @@ -2,6 +2,7 @@ Mixcloud OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/mixcloud.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/moves.py b/social_core/backends/moves.py index 5e6f51f76..a282b49ff 100644 --- a/social_core/backends/moves.py +++ b/social_core/backends/moves.py @@ -5,6 +5,7 @@ Written by Avi Alkalay Certified to work with Django 1.6 """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/nationbuilder.py b/social_core/backends/nationbuilder.py index c71fa96b3..a9362a95c 100644 --- a/social_core/backends/nationbuilder.py +++ b/social_core/backends/nationbuilder.py @@ -2,6 +2,7 @@ NationBuilder OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/nationbuilder.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/ngpvan.py b/social_core/backends/ngpvan.py index 8db9823d9..51c4a3c3c 100644 --- a/social_core/backends/ngpvan.py +++ b/social_core/backends/ngpvan.py @@ -3,6 +3,7 @@ http://developers.ngpvan.com/action-id """ + from openid.extensions import ax from .open_id import OpenIdAuth diff --git a/social_core/backends/odnoklassniki.py b/social_core/backends/odnoklassniki.py index cb573f978..e744689fb 100644 --- a/social_core/backends/odnoklassniki.py +++ b/social_core/backends/odnoklassniki.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/okta.py b/social_core/backends/okta.py index ecdc2135d..0aee79b63 100644 --- a/social_core/backends/okta.py +++ b/social_core/backends/okta.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/okta_openidconnect.py b/social_core/backends/okta_openidconnect.py index 8b74db299..a82f60f7b 100644 --- a/social_core/backends/okta_openidconnect.py +++ b/social_core/backends/okta_openidconnect.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/openinfra.py b/social_core/backends/openinfra.py index 57c77918e..1fd2ef8af 100644 --- a/social_core/backends/openinfra.py +++ b/social_core/backends/openinfra.py @@ -1,6 +1,7 @@ """ OpenInfra OpenId backend """ + from urllib.parse import urlsplit from openid.extensions import ax diff --git a/social_core/backends/openshift.py b/social_core/backends/openshift.py index efc5d0fdf..0e958d8ca 100644 --- a/social_core/backends/openshift.py +++ b/social_core/backends/openshift.py @@ -1,6 +1,7 @@ """ Openshift OAuth2 backend """ + from urllib.parse import urljoin import requests diff --git a/social_core/backends/openstack.py b/social_core/backends/openstack.py index de1df0d4f..ef2d1d01a 100644 --- a/social_core/backends/openstack.py +++ b/social_core/backends/openstack.py @@ -1,6 +1,7 @@ """ OpenStack OpenId backend """ + from urllib.parse import urlsplit from openid.extensions import ax diff --git a/social_core/backends/openstreetmap.py b/social_core/backends/openstreetmap.py index 4cf40bc48..5b67d3c25 100644 --- a/social_core/backends/openstreetmap.py +++ b/social_core/backends/openstreetmap.py @@ -8,6 +8,7 @@ More info: https://wiki.openstreetmap.org/wiki/OAuth """ + from xml.dom import minidom from .oauth import BaseOAuth1 diff --git a/social_core/backends/orbi.py b/social_core/backends/orbi.py index 9106d5011..7e599f208 100644 --- a/social_core/backends/orbi.py +++ b/social_core/backends/orbi.py @@ -1,6 +1,7 @@ """ Orbi OAuth2 backend """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/orcid.py b/social_core/backends/orcid.py index 459ccd3f3..a2be9eaac 100644 --- a/social_core/backends/orcid.py +++ b/social_core/backends/orcid.py @@ -2,6 +2,7 @@ ORCID OAuth2 Application backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/orcid.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/patreon.py b/social_core/backends/patreon.py index 3532a4731..ef92648e7 100644 --- a/social_core/backends/patreon.py +++ b/social_core/backends/patreon.py @@ -2,6 +2,7 @@ Patreon OAuth2 backend https://www.patreon.com/platform/documentation/oauth """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/persona.py b/social_core/backends/persona.py index f4b2ddedb..4f56d82ca 100644 --- a/social_core/backends/persona.py +++ b/social_core/backends/persona.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/phabricator.py b/social_core/backends/phabricator.py index a650a64dd..520d3e8ec 100644 --- a/social_core/backends/phabricator.py +++ b/social_core/backends/phabricator.py @@ -2,6 +2,7 @@ Phabricator OAuth2 backend, docs at: https://secure.phabricator.com/book/phabcontrib/article/using_oauthserver/ """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/pocket.py b/social_core/backends/pocket.py index 93d12c819..05b4e98f2 100644 --- a/social_core/backends/pocket.py +++ b/social_core/backends/pocket.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/podio.py b/social_core/backends/podio.py index 11a064a9b..55f19de6d 100644 --- a/social_core/backends/podio.py +++ b/social_core/backends/podio.py @@ -2,6 +2,7 @@ Podio OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/podio.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/professionali.py b/social_core/backends/professionali.py index a7090ea1a..1ee10f35a 100644 --- a/social_core/backends/professionali.py +++ b/social_core/backends/professionali.py @@ -4,6 +4,7 @@ This contribution adds support for professionaly.ru OAuth 2.0. Username is retrieved from the identity returned by server. """ + from time import time from ..utils import parse_qs diff --git a/social_core/backends/qiita.py b/social_core/backends/qiita.py index 6e3c2b3c9..dc8d5aa9b 100644 --- a/social_core/backends/qiita.py +++ b/social_core/backends/qiita.py @@ -4,6 +4,7 @@ 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 diff --git a/social_core/backends/quizlet.py b/social_core/backends/quizlet.py index bb44e3895..7cdc79f66 100644 --- a/social_core/backends/quizlet.py +++ b/social_core/backends/quizlet.py @@ -2,6 +2,7 @@ Quizlet OAuth2 Sign-in backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/quizlet.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/rdio.py b/social_core/backends/rdio.py index 9711014ae..8acf810c8 100644 --- a/social_core/backends/rdio.py +++ b/social_core/backends/rdio.py @@ -2,6 +2,7 @@ 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/" diff --git a/social_core/backends/readability.py b/social_core/backends/readability.py index e037ef03a..81d7cba14 100644 --- a/social_core/backends/readability.py +++ b/social_core/backends/readability.py @@ -2,6 +2,7 @@ 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" diff --git a/social_core/backends/reddit.py b/social_core/backends/reddit.py index 8d35682d4..acab0229c 100644 --- a/social_core/backends/reddit.py +++ b/social_core/backends/reddit.py @@ -2,6 +2,7 @@ Reddit OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/reddit.html """ + import base64 from .oauth import BaseOAuth2 diff --git a/social_core/backends/runkeeper.py b/social_core/backends/runkeeper.py index fb9eae5aa..af22197b1 100644 --- a/social_core/backends/runkeeper.py +++ b/social_core/backends/runkeeper.py @@ -2,6 +2,7 @@ RunKeeper OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/runkeeper.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/saml.py b/social_core/backends/saml.py index 32cb76b98..cb00da79a 100644 --- a/social_core/backends/saml.py +++ b/social_core/backends/saml.py @@ -7,6 +7,7 @@ "Identity Provider" (IdP): The third-party site that is authenticating users via SAML """ + import json from onelogin.saml2.auth import OneLogin_Saml2_Auth diff --git a/social_core/backends/seznam.py b/social_core/backends/seznam.py index a7b4cb001..6cc1534f3 100644 --- a/social_core/backends/seznam.py +++ b/social_core/backends/seznam.py @@ -2,6 +2,7 @@ Seznam OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/seznam.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/shimmering.py b/social_core/backends/shimmering.py index d626093f7..1e9c2626d 100644 --- a/social_core/backends/shimmering.py +++ b/social_core/backends/shimmering.py @@ -1,6 +1,7 @@ """ Shimmering Oauth """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/shopify.py b/social_core/backends/shopify.py index 60602c900..f59333066 100644 --- a/social_core/backends/shopify.py +++ b/social_core/backends/shopify.py @@ -2,6 +2,7 @@ Shopify OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/shopify.html """ + import imp from ..exceptions import AuthCanceled, AuthFailed diff --git a/social_core/backends/sketchfab.py b/social_core/backends/sketchfab.py index 6fb293e77..9e34b868c 100644 --- a/social_core/backends/sketchfab.py +++ b/social_core/backends/sketchfab.py @@ -3,6 +3,7 @@ https://python-social-auth.readthedocs.io/en/latest/backends/sketchfab.html https://sketchfab.com/developers/oauth """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/skyrock.py b/social_core/backends/skyrock.py index 428907b5b..7de822a8e 100644 --- a/social_core/backends/skyrock.py +++ b/social_core/backends/skyrock.py @@ -2,6 +2,7 @@ Skyrock OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/skyrock.html """ + from .oauth import BaseOAuth1 diff --git a/social_core/backends/soundcloud.py b/social_core/backends/soundcloud.py index a4861dbfc..a8297433c 100644 --- a/social_core/backends/soundcloud.py +++ b/social_core/backends/soundcloud.py @@ -2,6 +2,7 @@ Soundcloud OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/soundcloud.html """ + from urllib.parse import urlencode from .oauth import BaseOAuth2 diff --git a/social_core/backends/spotify.py b/social_core/backends/spotify.py index a47b5deda..baa341385 100644 --- a/social_core/backends/spotify.py +++ b/social_core/backends/spotify.py @@ -3,6 +3,7 @@ https://developer.spotify.com/spotify-web-api/ https://developer.spotify.com/spotify-web-api/authorization-guide/ """ + import base64 from .oauth import BaseOAuth2 diff --git a/social_core/backends/stackoverflow.py b/social_core/backends/stackoverflow.py index 85fa3fcd7..22ac3c7eb 100644 --- a/social_core/backends/stackoverflow.py +++ b/social_core/backends/stackoverflow.py @@ -2,6 +2,7 @@ Stackoverflow OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/stackoverflow.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/steam.py b/social_core/backends/steam.py index 160b06a5b..0a4177335 100644 --- a/social_core/backends/steam.py +++ b/social_core/backends/steam.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/stocktwits.py b/social_core/backends/stocktwits.py index 65af8477c..4b53f5eab 100644 --- a/social_core/backends/stocktwits.py +++ b/social_core/backends/stocktwits.py @@ -2,6 +2,7 @@ Stocktwits OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/stocktwits.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/strava.py b/social_core/backends/strava.py index 2b52847fe..561602171 100644 --- a/social_core/backends/strava.py +++ b/social_core/backends/strava.py @@ -2,6 +2,7 @@ Strava OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/strava.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/stripe.py b/social_core/backends/stripe.py index 3d28cbe5f..aa2c747cf 100644 --- a/social_core/backends/stripe.py +++ b/social_core/backends/stripe.py @@ -2,6 +2,7 @@ Stripe OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/stripe.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/surveymonkey.py b/social_core/backends/surveymonkey.py index 155c53e35..da467354e 100644 --- a/social_core/backends/surveymonkey.py +++ b/social_core/backends/surveymonkey.py @@ -2,6 +2,7 @@ SurveyMonkey OAuth2 backend, docs at: https://developer.surveymonkey.com/api/v3/#authentication """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/suse.py b/social_core/backends/suse.py index ca1121e8e..1dd5eafe7 100644 --- a/social_core/backends/suse.py +++ b/social_core/backends/suse.py @@ -2,6 +2,7 @@ Open Suse OpenId backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/suse.html """ + from .open_id import OpenIdAuth diff --git a/social_core/backends/thisismyjam.py b/social_core/backends/thisismyjam.py index fac610215..7d9241671 100644 --- a/social_core/backends/thisismyjam.py +++ b/social_core/backends/thisismyjam.py @@ -2,6 +2,7 @@ ThisIsMyJam OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/thisismyjam.html """ + from .oauth import BaseOAuth1 diff --git a/social_core/backends/trello.py b/social_core/backends/trello.py index 70a1d7934..ee4a569c1 100644 --- a/social_core/backends/trello.py +++ b/social_core/backends/trello.py @@ -2,11 +2,11 @@ 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" diff --git a/social_core/backends/tripit.py b/social_core/backends/tripit.py index f6f1ea786..95b6a5c2f 100644 --- a/social_core/backends/tripit.py +++ b/social_core/backends/tripit.py @@ -2,6 +2,7 @@ Tripit OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/tripit.html """ + from xml.dom import minidom from .oauth import BaseOAuth1 diff --git a/social_core/backends/tumblr.py b/social_core/backends/tumblr.py index 516ae8c5e..9e8d38c53 100644 --- a/social_core/backends/tumblr.py +++ b/social_core/backends/tumblr.py @@ -2,6 +2,7 @@ Tumblr OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/tumblr.html """ + from ..utils import first from .oauth import BaseOAuth1 diff --git a/social_core/backends/twilio.py b/social_core/backends/twilio.py index 90cb1e960..9ed658c50 100644 --- a/social_core/backends/twilio.py +++ b/social_core/backends/twilio.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/twitch.py b/social_core/backends/twitch.py index 38dd29b66..1d9b3ef1d 100644 --- a/social_core/backends/twitch.py +++ b/social_core/backends/twitch.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/twitter.py b/social_core/backends/twitter.py index 0bb12f5a6..669c89f74 100644 --- a/social_core/backends/twitter.py +++ b/social_core/backends/twitter.py @@ -2,6 +2,7 @@ Twitter OAuth1 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/twitter.html """ + from ..exceptions import AuthCanceled from .oauth import BaseOAuth1 diff --git a/social_core/backends/twitter_oauth2.py b/social_core/backends/twitter_oauth2.py index ead85b473..c3695ea2c 100644 --- a/social_core/backends/twitter_oauth2.py +++ b/social_core/backends/twitter_oauth2.py @@ -3,6 +3,7 @@ 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 diff --git a/social_core/backends/uber.py b/social_core/backends/uber.py index e7ca8bb41..bb4ce1388 100644 --- a/social_core/backends/uber.py +++ b/social_core/backends/uber.py @@ -2,6 +2,7 @@ Uber OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/uber.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/ubuntu.py b/social_core/backends/ubuntu.py index 8d4c2ed83..c272a9a77 100644 --- a/social_core/backends/ubuntu.py +++ b/social_core/backends/ubuntu.py @@ -1,6 +1,7 @@ """ Ubuntu One OpenId backend """ + from .open_id import OpenIdAuth diff --git a/social_core/backends/udata.py b/social_core/backends/udata.py index c2b043206..2c56a7587 100644 --- a/social_core/backends/udata.py +++ b/social_core/backends/udata.py @@ -4,6 +4,7 @@ Docs at: https://python-social-auth.readthedocs.io/en/latest/backends/udata.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/upwork.py b/social_core/backends/upwork.py index eb9815ac4..341a13f45 100644 --- a/social_core/backends/upwork.py +++ b/social_core/backends/upwork.py @@ -1,6 +1,7 @@ """ Upwork OAuth1 backend """ + from .oauth import BaseOAuth1 diff --git a/social_core/backends/username.py b/social_core/backends/username.py index be0be109c..dde9aa6cb 100644 --- a/social_core/backends/username.py +++ b/social_core/backends/username.py @@ -2,6 +2,7 @@ Legacy Username backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/username.html """ + from .legacy import LegacyAuth diff --git a/social_core/backends/vault.py b/social_core/backends/vault.py index 4064072de..58f8d0dfd 100644 --- a/social_core/backends/vault.py +++ b/social_core/backends/vault.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/vend.py b/social_core/backends/vend.py index 01646475e..9f0fd5ac1 100644 --- a/social_core/backends/vend.py +++ b/social_core/backends/vend.py @@ -1,6 +1,7 @@ """ Vend OAuth2 backend: """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/vk.py b/social_core/backends/vk.py index 88fe53ad7..ff6ca4240 100644 --- a/social_core/backends/vk.py +++ b/social_core/backends/vk.py @@ -2,6 +2,7 @@ VK.com OpenAPI, OAuth2 and Iframe application OAuth2 backends, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/vk.html """ + import json from hashlib import md5 from time import time diff --git a/social_core/backends/xing.py b/social_core/backends/xing.py index 7ea4e4151..4b6d2a8e0 100644 --- a/social_core/backends/xing.py +++ b/social_core/backends/xing.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/yahoo.py b/social_core/backends/yahoo.py index b42ba0bbd..a5ef64b7c 100644 --- a/social_core/backends/yahoo.py +++ b/social_core/backends/yahoo.py @@ -2,6 +2,7 @@ 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 diff --git a/social_core/backends/yammer.py b/social_core/backends/yammer.py index e4fab68e6..24c0ab960 100644 --- a/social_core/backends/yammer.py +++ b/social_core/backends/yammer.py @@ -2,6 +2,7 @@ Yammer OAuth2 production and staging backends, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/yammer.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/yandex.py b/social_core/backends/yandex.py index d8498a1df..c437d9020 100644 --- a/social_core/backends/yandex.py +++ b/social_core/backends/yandex.py @@ -6,6 +6,7 @@ If username is not specified, OpenID 2.0 url used for authentication. """ + from urllib.parse import urlsplit from .oauth import BaseOAuth2 diff --git a/social_core/backends/zotero.py b/social_core/backends/zotero.py index 8684d34f9..d6cc2f54f 100644 --- a/social_core/backends/zotero.py +++ b/social_core/backends/zotero.py @@ -2,11 +2,11 @@ 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" diff --git a/social_core/storage.py b/social_core/storage.py index 7c0208279..eb591f220 100644 --- a/social_core/storage.py +++ b/social_core/storage.py @@ -1,4 +1,5 @@ """Models mixins for Social Auth""" + import base64 import re import time diff --git a/social_core/tests/backends/test_azuread_b2c.py b/social_core/tests/backends/test_azuread_b2c.py index 4a2682e46..96244fc5c 100644 --- a/social_core/tests/backends/test_azuread_b2c.py +++ b/social_core/tests/backends/test_azuread_b2c.py @@ -23,6 +23,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + import json from time import time diff --git a/social_core/tests/backends/test_ngpvan.py b/social_core/tests/backends/test_ngpvan.py index f80cc9963..53e6738da 100644 --- a/social_core/tests/backends/test_ngpvan.py +++ b/social_core/tests/backends/test_ngpvan.py @@ -1,4 +1,5 @@ """Tests for NGP VAN ActionID Backend""" + import datetime from urllib.parse import urlencode diff --git a/social_core/tests/backends/test_podio.py b/social_core/tests/backends/test_podio.py index 3615bac4b..a81a05d24 100644 --- a/social_core/tests/backends/test_podio.py +++ b/social_core/tests/backends/test_podio.py @@ -49,7 +49,7 @@ class PodioOAuth2Test(OAuth2Test): "link": "https://podio.com/users/1010101010", "name": "Foo Bar", # more properties ... - } + }, # more properties ... } ) From f6d81fd4c83f93545ca541f8a43233dae841f359 Mon Sep 17 00:00:00 2001 From: mmd Date: Tue, 30 Jan 2024 15:19:02 +0100 Subject: [PATCH 037/152] [feat] Add OAuth2 support for OpenStreetMap (#877) * [feat] Add OAuth2 support for OpenStreetMap Fixes #758 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- social_core/backends/openstreetmap_oauth2.py | 57 +++++++++++++++++++ .../backends/test_openstreetmap_oauth2.py | 30 ++++++++++ 2 files changed, 87 insertions(+) create mode 100644 social_core/backends/openstreetmap_oauth2.py create mode 100644 social_core/tests/backends/test_openstreetmap_oauth2.py diff --git a/social_core/backends/openstreetmap_oauth2.py b/social_core/backends/openstreetmap_oauth2.py new file mode 100644 index 000000000..6df409273 --- /dev/null +++ b/social_core/backends/openstreetmap_oauth2.py @@ -0,0 +1,57 @@ +""" +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" + ACCESS_TOKEN_METHOD = "POST" + 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"), + } diff --git a/social_core/tests/backends/test_openstreetmap_oauth2.py b/social_core/tests/backends/test_openstreetmap_oauth2.py new file mode 100644 index 000000000..7ff69ad6c --- /dev/null +++ b/social_core/tests/backends/test_openstreetmap_oauth2.py @@ -0,0 +1,30 @@ +import json + +from .oauth import OAuth2Test + + +class OpenStreetMapOAuth2Test(OAuth2Test): + 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() From 4bb29b1eaa60cb0288606c703e7e9aeea2a8184d Mon Sep 17 00:00:00 2001 From: Amit Ray <51674969+amitray007@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:37:40 +0530 Subject: [PATCH 038/152] fix: Etsy OAuth2 Authentication bugs (#880) * fix: Etsy OAuth2 Authentication bugs * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * refactor: removed unnecessary change * fix equality --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- social_core/backends/etsy.py | 4 +++- social_core/tests/backends/oauth.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/social_core/backends/etsy.py b/social_core/backends/etsy.py index 980d7059b..281399c4f 100644 --- a/social_core/backends/etsy.py +++ b/social_core/backends/etsy.py @@ -7,9 +7,11 @@ class EtsyOAuth2(BaseOAuth2PKCE): 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" ACCESS_TOKEN_METHOD = "POST" REQUEST_TOKEN_METHOD = "POST" SCOPE_SEPARATOR = " " + REDIRECT_STATE = False EXTRA_DATA = [ ("refresh_token", "refresh_token"), ("expires_in", "expires_in"), @@ -38,5 +40,5 @@ def get_user_details(self, response): "last_name": response["last_name"], "email": response["primary_email"], "image_url_75x75": response["image_url_75x75"], - "username": response["user_id"], + "username": str(response["user_id"]), } diff --git a/social_core/tests/backends/oauth.py b/social_core/tests/backends/oauth.py index c00e014f7..4129ee714 100644 --- a/social_core/tests/backends/oauth.py +++ b/social_core/tests/backends/oauth.py @@ -164,14 +164,15 @@ def do_login(self): code_challenge = auth_request.querystring.get("code_challenge")[0] code_challenge_method = auth_request.querystring.get("code_challenge_method")[0] self.assertIsNotNone(code_challenge) - self.assertEqual(code_challenge_method, "s256") + self.assertTrue(code_challenge_method in ["s256", "S256"]) auth_complete = [ r for r in requests if self.backend.access_token_url() in r.url ][0] code_verifier = auth_complete.parsed_body.get("code_verifier")[0] self.assertEqual( - self.backend.generate_code_challenge(code_verifier, "s256"), code_challenge + self.backend.generate_code_challenge(code_verifier, code_challenge_method), + code_challenge, ) return user From 4d0dae7efd01f588c93347fb0c8cd4898d7ead86 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 17:50:04 +0000 Subject: [PATCH 039/152] build(deps-dev): bump pre-commit from 3.6.0 to 3.6.1 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.6.0 to 3.6.1. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.6.0...v3.6.1) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5cef2202d..2e6e37675 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1 @@ -pre-commit==3.6.0 +pre-commit==3.6.1 From 5c4f403f9e1f37ec886090517bd90697ed7c7290 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 22:03:11 +0000 Subject: [PATCH 040/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 24.1.1 → 24.2.0](https://github.com/psf/black/compare/24.1.1...24.2.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 06fab77ef..e6fb18bc4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 24.1.1 + rev: 24.2.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 From 27bbf67ce0abc00cdedeeb3c1ec472d9c039fd50 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Wed, 14 Feb 2024 11:20:07 +0100 Subject: [PATCH 041/152] Release 4.5.3 (#885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Version bump 4.5.3 * Update CHANGELOG.md --------- Co-authored-by: Michal Čihař --- CHANGELOG.md | 8 ++++++++ social_core/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8fd5c971..f7e3329c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ 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.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 diff --git a/social_core/__init__.py b/social_core/__init__.py index f490a9647..5b43808f6 100644 --- a/social_core/__init__.py +++ b/social_core/__init__.py @@ -1 +1 @@ -__version__ = "4.5.2" +__version__ = "4.5.3" From 213b391e4e89ba0d0fe8b414c0540f4ec2f5f2ac Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 22:23:48 +0000 Subject: [PATCH 042/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.15.0 → v3.15.1](https://github.com/asottile/pyupgrade/compare/v3.15.0...v3.15.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e6fb18bc4..ba38494f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: isort args: [--profile=black] - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.15.1 hooks: - id: pyupgrade args: [--py38-plus] From 2dfad718d553c18ff5217a0c0c6f6809c85893f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 17:55:55 +0000 Subject: [PATCH 043/152] build(deps-dev): bump pre-commit from 3.6.1 to 3.6.2 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.6.1 to 3.6.2. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.6.1...v3.6.2) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 2e6e37675..c74ef551d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1 @@ -pre-commit==3.6.1 +pre-commit==3.6.2 From dbc880c0d7a7a560de6c09e53ed7a2cf4bc0df01 Mon Sep 17 00:00:00 2001 From: Adrian Pascu Date: Tue, 20 Feb 2024 11:49:42 +0100 Subject: [PATCH 044/152] Add `refresh_token` into Linkedin `EXTRA_DATA` (#890) * Add `refresh_token` into Linkedin `EXTRA_DATA` * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- social_core/backends/linkedin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/social_core/backends/linkedin.py b/social_core/backends/linkedin.py index 2edb72f48..31748eb03 100644 --- a/social_core/backends/linkedin.py +++ b/social_core/backends/linkedin.py @@ -61,6 +61,8 @@ class LinkedinOAuth2(BaseOAuth2): ("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): From 51359baed9ca1a37a48a41fcec109a3115ea0c14 Mon Sep 17 00:00:00 2001 From: igorgaming <69463610+igorgaming@users.noreply.github.com> Date: Thu, 29 Feb 2024 18:44:47 +0300 Subject: [PATCH 045/152] Fix values check in extra_data --- social_core/backends/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social_core/backends/base.py b/social_core/backends/base.py index ba924e42e..8d3bd2b96 100644 --- a/social_core/backends/base.py +++ b/social_core/backends/base.py @@ -146,7 +146,7 @@ def extra_data(self, user, uid, response, details=None, *args, **kwargs): elif size == 1: name = alias = entry[0] discard = False - value = response.get(name) or details.get(name) or details.get(alias) + value = response.get(name, details.get(name, details.get(alias))) if discard and not value: continue data[alias] = value From 93aca9e12e61734c9410deb79e310e44242a83fd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 20:00:46 +0000 Subject: [PATCH 046/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 24.2.0 → 24.3.0](https://github.com/psf/black/compare/24.2.0...24.3.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ba38494f4..38436f267 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 24.2.0 + rev: 24.3.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 From 830d9566b6e1db73ee002c1a2269109612f48c80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 17:19:07 +0000 Subject: [PATCH 047/152] build(deps-dev): bump pre-commit from 3.6.2 to 3.7.0 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.6.2 to 3.7.0. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.6.2...v3.7.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c74ef551d..68372830e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1 @@ -pre-commit==3.6.2 +pre-commit==3.7.0 From d03a57298254042fdb08aca5dc0bd7a8e78605fa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 19:50:39 +0000 Subject: [PATCH 048/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.15.1 → v3.15.2](https://github.com/asottile/pyupgrade/compare/v3.15.1...v3.15.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 38436f267..e9ba6e1b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: isort args: [--profile=black] - repo: https://github.com/asottile/pyupgrade - rev: v3.15.1 + rev: v3.15.2 hooks: - id: pyupgrade args: [--py38-plus] From 61d58d605472a60cd778c62a7a4ac5abc8218cb3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 19:57:52 +0000 Subject: [PATCH 049/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.12.0 → v2.13.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.12.0...v2.13.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9ba6e1b1..454252604 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: - id: check-hooks-apply - id: check-useless-excludes - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks - rev: v2.12.0 + rev: v2.13.0 hooks: - id: pretty-format-yaml args: [--autofix, --indent, '2'] From d216c96d1f601ad3bd145e02b0138adaa567f199 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:14:05 +0000 Subject: [PATCH 050/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 454252604..cd37097d6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-merge-conflict - id: check-json From 019cb70ce09687398b682f816e11dc7d19fc3cfa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 20:07:11 +0000 Subject: [PATCH 051/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 24.3.0 → 24.4.0](https://github.com/psf/black/compare/24.3.0...24.4.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd37097d6..029fd0864 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 24.3.0 + rev: 24.4.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 From 948235ee57464636197ec1755004160ab85abb12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 25 Apr 2024 08:27:44 +0200 Subject: [PATCH 052/152] tox: remove lxml/xmlsec workaround It now breaks things instead of fixing them - both modules now ship wheels which are compatible. --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5a9b747ba..58a4cc800 100644 --- a/tox.ini +++ b/tox.ini @@ -12,5 +12,4 @@ deps = py{38,39,310,311,312}: -rsocial_core/tests/requirements.txt commands = py{38,39,310,311,312}: pip install -e .[all] - py{38,39,310,311,312}: pip install --force-reinstall --no-binary lxml lxml pytest {posargs:-v --cov=social_core} From 146a634521b76c4273c78dd357fb081592c05d05 Mon Sep 17 00:00:00 2001 From: Alberto Leoncio <40328018+albertoleoncio@users.noreply.github.com> Date: Thu, 4 Apr 2024 13:21:42 -0300 Subject: [PATCH 053/152] Update mediawiki.py +AuthException at access_token --- social_core/backends/mediawiki.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/social_core/backends/mediawiki.py b/social_core/backends/mediawiki.py index b4b1d9d60..5ceb220b7 100644 --- a/social_core/backends/mediawiki.py +++ b/social_core/backends/mediawiki.py @@ -91,6 +91,8 @@ def access_token(self, token): 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] oauth_token_secret = credentials.get(b"oauth_token_secret")[0] From 70aad432c81766c9bdae9932f80c5745527a0f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 25 Apr 2024 08:19:37 +0200 Subject: [PATCH 054/152] pipeline: make sure uid is string It is stored as string in the database, so make conversion early. Fixes https://github.com/python-social-auth/social-app-django/issues/568 --- social_core/pipeline/social_auth.py | 2 +- social_core/tests/backends/test_bitbucket_datacenter.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/social_core/pipeline/social_auth.py b/social_core/pipeline/social_auth.py index 1fd124aff..1c5940e75 100644 --- a/social_core/pipeline/social_auth.py +++ b/social_core/pipeline/social_auth.py @@ -6,7 +6,7 @@ def social_details(backend, details, response, *args, **kwargs): def social_uid(backend, details, response, *args, **kwargs): - return {"uid": backend.get_user_id(details, response)} + return {"uid": str(backend.get_user_id(details, response))} def auth_allowed(backend, details, response, *args, **kwargs): diff --git a/social_core/tests/backends/test_bitbucket_datacenter.py b/social_core/tests/backends/test_bitbucket_datacenter.py index 8a3d3f9c5..dda5fbb6f 100644 --- a/social_core/tests/backends/test_bitbucket_datacenter.py +++ b/social_core/tests/backends/test_bitbucket_datacenter.py @@ -83,7 +83,7 @@ def test_login(self): self.assertEqual(len(user.social), 1) social = user.social[0] - self.assertEqual(social.uid, 1) + 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") @@ -109,7 +109,7 @@ def test_login(self): def test_refresh_token(self): _, social = self.do_refresh_token() - self.assertEqual(social.uid, 1) + 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") From 7eb6c063bd4393355ba419759a82c1de6aa5760a Mon Sep 17 00:00:00 2001 From: Arnautt <40998274+Arnautt@users.noreply.github.com> Date: Mon, 19 Feb 2024 18:25:06 +0100 Subject: [PATCH 055/152] Set redirect_state to False for box backend --- social_core/backends/box.py | 1 + 1 file changed, 1 insertion(+) diff --git a/social_core/backends/box.py b/social_core/backends/box.py index 02721e88a..f534a1899 100644 --- a/social_core/backends/box.py +++ b/social_core/backends/box.py @@ -15,6 +15,7 @@ class BoxOAuth2(BaseOAuth2): 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"), From 1f706e1df3465af27443e8e6969ca04e59c6060a Mon Sep 17 00:00:00 2001 From: zzhuang Date: Thu, 25 Apr 2024 02:48:23 -0400 Subject: [PATCH 056/152] fix: SteamOpenId does not validate identity url (#807) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: SteamOpenId does not validate identity url * Fix failing test * Fix referencing self --------- Co-authored-by: async42 Co-authored-by: Michal Čihař --- social_core/backends/steam.py | 2 ++ social_core/tests/backends/test_steam.py | 30 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/social_core/backends/steam.py b/social_core/backends/steam.py index 0a4177335..bfd37d5e7 100644 --- a/social_core/backends/steam.py +++ b/social_core/backends/steam.py @@ -46,6 +46,8 @@ def consumer(self): 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") diff --git a/social_core/tests/backends/test_steam.py b/social_core/tests/backends/test_steam.py index d0e31cbd7..6798e453c 100644 --- a/social_core/tests/backends/test_steam.py +++ b/social_core/tests/backends/test_steam.py @@ -140,3 +140,33 @@ 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=", + } + ) + + def test_login(self): + self._login_setup(user_url="https://fakesteamcommunity.com/openid/123") + with self.assertRaises(AuthFailed): + self.do_login() + + def test_partial_pipeline(self): + self._login_setup(user_url="https://fakesteamcommunity.com/openid/123") + with self.assertRaises(AuthFailed): + self.do_partial_pipeline() From 4a7d470dee9cda3ffaf2d32f6f275d466b6f12a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 25 Apr 2024 08:56:36 +0200 Subject: [PATCH 057/152] Release 4.5.4 --- CHANGELOG.md | 11 +++++++++++ social_core/__init__.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7e3329c0..1783c8652 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ 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.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 diff --git a/social_core/__init__.py b/social_core/__init__.py index 5b43808f6..bb160d035 100644 --- a/social_core/__init__.py +++ b/social_core/__init__.py @@ -1 +1 @@ -__version__ = "4.5.3" +__version__ = "4.5.4" From 4384f382e7d60b07dc26868045cbdf19866248ec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 20:12:41 +0000 Subject: [PATCH 058/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 24.4.0 → 24.4.2](https://github.com/psf/black/compare/24.4.0...24.4.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 029fd0864..2ffb6e46e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 24.4.0 + rev: 24.4.2 hooks: - id: black - repo: https://github.com/PyCQA/flake8 From 654eda0040f1466b769f3b5400aa19583f64fdc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Dalfors?= Date: Mon, 6 May 2024 13:50:17 +0200 Subject: [PATCH 059/152] deps: Fixes defusedxml requirement (#912) Remove release candidate version --- requirements-base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-base.txt b/requirements-base.txt index 9c0e31bfb..46afbb6c4 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -3,5 +3,5 @@ oauthlib>=1.0.3 requests-oauthlib>=0.6.1 PyJWT>=2.7.0 cryptography>=1.4 -defusedxml>=0.5.0rc1 +defusedxml>=0.5.0 python3-openid>=3.0.10 From 2c53cb13c892a0a813c32c790aeb798fddbe9208 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 May 2024 17:29:29 +0000 Subject: [PATCH 060/152] build(deps-dev): bump pre-commit from 3.7.0 to 3.7.1 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.7.0 to 3.7.1. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.7.0...v3.7.1) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 68372830e..a93b8c1e4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1 @@ -pre-commit==3.7.0 +pre-commit==3.7.1 From 4328bc69c82a06e4e0aa7e8c48ea1f1ea468436d Mon Sep 17 00:00:00 2001 From: Nikolaus Lieb <4014270+nikoder@users.noreply.github.com> Date: Wed, 15 May 2024 18:01:35 +0800 Subject: [PATCH 061/152] Fix social-app-django/issues/355 Handle the case where the user has not registered a `family-name` and ORCID returns `None`. --- CHANGELOG.md | 5 +++++ social_core/backends/orcid.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1783c8652..6b570d970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ 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/). +## [Unreleased] + +### Changed +- Handle case where user has not registered a `family-name` with ORCID + ## [4.5.4](https://github.com/python-social-auth/social-core/releases/tag/4.5.4) - 2024-04-25 ### Added diff --git a/social_core/backends/orcid.py b/social_core/backends/orcid.py index a2be9eaac..1f815f3bf 100644 --- a/social_core/backends/orcid.py +++ b/social_core/backends/orcid.py @@ -76,7 +76,8 @@ def get_user_details(self, response): if name: first_name = name.get("given-names", {}).get("value", "") - last_name = name.get("family-name", {}).get("value", "") + if name.get("family-name", None) is not None: + last_name = name.get("family-name").get("value", "") emails = person.get("emails") if emails: From 95316b0914386af59d8649917c373a594f1766de Mon Sep 17 00:00:00 2001 From: Nikolaus Lieb <4014270+nikoder@users.noreply.github.com> Date: Thu, 16 May 2024 14:23:56 +0800 Subject: [PATCH 062/152] Nicer implementation of ORCID login fix for missing family name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michal Čihař --- social_core/backends/orcid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social_core/backends/orcid.py b/social_core/backends/orcid.py index 1f815f3bf..8717fc003 100644 --- a/social_core/backends/orcid.py +++ b/social_core/backends/orcid.py @@ -76,8 +76,8 @@ def get_user_details(self, response): if name: first_name = name.get("given-names", {}).get("value", "") - if name.get("family-name", None) is not None: - last_name = name.get("family-name").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: From d7bba223c0036581b63b01d05e53b115c606dbec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 20:07:16 +0000 Subject: [PATCH 063/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.15.2 → v3.16.0](https://github.com/asottile/pyupgrade/compare/v3.15.2...v3.16.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2ffb6e46e..9f9043acf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: isort args: [--profile=black] - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 + rev: v3.16.0 hooks: - id: pyupgrade args: [--py38-plus] From 49d589e6ee3a46e93c9929db040ba0f24286d9c8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 20:03:17 +0000 Subject: [PATCH 064/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/flake8: 7.0.0 → 7.1.0](https://github.com/PyCQA/flake8/compare/7.0.0...7.1.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9f9043acf..5361a42a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 + rev: 7.1.0 hooks: - id: flake8 - repo: meta From 3449faf7504471d7d857f627b77cc8cf81d3e026 Mon Sep 17 00:00:00 2001 From: Fleapse <51268198+Fleapse@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:37:45 +0300 Subject: [PATCH 065/152] fix(telegram): ensure username is string even if based on id Make username always a string to avoid errors in the user pipeline. Fix https://github.com/python-social-auth/social-core/issues/918 (#921) Co-authored-by: Nikita Pozdnyakov --- social_core/backends/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social_core/backends/telegram.py b/social_core/backends/telegram.py index b82aa2d19..9e9e14947 100644 --- a/social_core/backends/telegram.py +++ b/social_core/backends/telegram.py @@ -43,7 +43,7 @@ def get_user_details(self, response): last_name = response.get("last_name", "") fullname = f"{first_name} {last_name}".strip() return { - "username": response.get("username") or response[self.ID_KEY], + "username": response.get("username") or str(response[self.ID_KEY]), "first_name": first_name, "last_name": last_name, "fullname": fullname, From ec8d84fc2cd0e93de17ce46e1fac88f0f73f6cff Mon Sep 17 00:00:00 2001 From: Alex Dehnert Date: Mon, 15 Jul 2024 02:59:55 -0400 Subject: [PATCH 066/152] fix: Allow per-backend user pipeline settings (#677) According to https://python-social-auth.readthedocs.io/en/latest/configuration/settings.html, "All settings can be defined per-backend by adding the backend name to the setting name, like SOCIAL_AUTH_TWITTER_LOGIN_URL". This changes user.py to actually use per-backend settings for options like USERNAME_IS_FULL_EMAIL. Note that get_username is always called with a backend, but user_details isn't, so a different version of `setting' is needed for the two. --- social_core/pipeline/user.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/social_core/pipeline/user.py b/social_core/pipeline/user.py index 9e4d17ac5..45dd339c8 100644 --- a/social_core/pipeline/user.py +++ b/social_core/pipeline/user.py @@ -11,17 +11,17 @@ def get_username(strategy, details, backend, user=None, *args, **kwargs): storage = strategy.storage if not user: - email_as_username = strategy.setting("USERNAME_IS_FULL_EMAIL", False) - uuid_length = strategy.setting("UUID_LENGTH", 16) + 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 = strategy.setting("SLUGIFY_USERNAMES", False) - do_clean = strategy.setting("CLEAN_USERNAMES", True) + 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 = strategy.setting("CLEAN_USERNAME_FUNCTION") + override_clean = backend.setting("CLEAN_USERNAME_FUNCTION") if override_clean: clean_func = module_member(override_clean) else: @@ -30,7 +30,7 @@ def identity_func(val): clean_func = identity_func if do_slugify: - override_slug = strategy.setting("SLUGIFY_FUNCTION") + override_slug = backend.setting("SLUGIFY_FUNCTION") slug_func = module_member(override_slug) if override_slug else slugify else: slug_func = identity_func @@ -82,7 +82,7 @@ def user_details(strategy, details, backend, user=None, *args, **kwargs): # 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") is True: + if strategy.setting("NO_DEFAULT_PROTECTED_USER_FIELDS", backend=backend) is True: protected = () else: protected = ( @@ -96,13 +96,15 @@ def user_details(strategy, details, backend, user=None, *args, **kwargs): "is_superuser", ) - protected = protected + tuple(strategy.setting("PROTECTED_USER_FIELDS", [])) + 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) + 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) @@ -113,7 +115,9 @@ def user_details(strategy, details, backend, user=None, *args, **kwargs): if current_value == value: continue - immutable_fields = tuple(strategy.setting("IMMUTABLE_USER_FIELDS", [])) + immutable_fields = tuple( + strategy.setting("IMMUTABLE_USER_FIELDS", [], backend=backend) + ) if name in immutable_fields and current_value: continue From e9f996a85a5c52a78f53e2e25d9fb94111f57f9c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 20:12:56 +0000 Subject: [PATCH 067/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.13.0 → v2.14.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.13.0...v2.14.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5361a42a2..659f77105 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: - id: check-hooks-apply - id: check-useless-excludes - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks - rev: v2.13.0 + rev: v2.14.0 hooks: - id: pretty-format-yaml args: [--autofix, --indent, '2'] From fd7dd903eedd666027ffee65bf8f5f39647e9fbe Mon Sep 17 00:00:00 2001 From: Jeppe Fihl-Pearson Date: Wed, 17 Jul 2024 10:54:45 +0100 Subject: [PATCH 068/152] Use timezone-aware datetime objects Rather than using the `datetime.utcnow()` and `datetime.utcfromtimestamp()` methods which produce timezone-naive datetime objects, we should instead use `datetime.now()` and `datetime.fromtimestamp()` with UTC provided as the timezone, in order to create timezone-aware datetime objects. This removes the DeprecationWarning that otherwise is raised since Python 3.12 for these methods. --- social_core/backends/asana.py | 6 +++--- social_core/backends/exacttarget.py | 4 ++-- social_core/backends/linkedin.py | 2 +- social_core/backends/open_id_connect.py | 2 +- social_core/storage.py | 8 ++++---- social_core/tests/backends/test_dummy.py | 4 +++- .../tests/backends/test_livejournal.py | 4 +++- social_core/tests/backends/test_ngpvan.py | 4 +++- .../tests/backends/test_open_id_connect.py | 20 +++++++++---------- 9 files changed, 30 insertions(+), 24 deletions(-) diff --git a/social_core/backends/asana.py b/social_core/backends/asana.py index 0fb413e8f..38be84227 100644 --- a/social_core/backends/asana.py +++ b/social_core/backends/asana.py @@ -36,8 +36,8 @@ def user_data(self, access_token, *args, **kwargs): 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.utcnow() + datetime.timedelta( - seconds=data["expires"] - ) + expires_on = datetime.datetime.now( + datetime.timezone.utc + ) + datetime.timedelta(seconds=data["expires"]) data["expires_on"] = expires_on.isoformat() return data diff --git a/social_core/backends/exacttarget.py b/social_core/backends/exacttarget.py index b8679a7ee..816555551 100644 --- a/social_core/backends/exacttarget.py +++ b/social_core/backends/exacttarget.py @@ -4,7 +4,7 @@ Requires package pyjwt """ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import jwt @@ -91,7 +91,7 @@ def extra_data(self, user, uid, response, details=None, *args, **kwargs): # 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.utcnow() + timedelta(seconds=expiresSeconds) + expires = datetime.now(timezone.utc) + timedelta(seconds=expiresSeconds) data["expires"] = (expires - datetime(1970, 1, 1)).total_seconds() if response.get("token"): diff --git a/social_core/backends/linkedin.py b/social_core/backends/linkedin.py index 31748eb03..f402a191a 100644 --- a/social_core/backends/linkedin.py +++ b/social_core/backends/linkedin.py @@ -30,7 +30,7 @@ class LinkedinOpenIdConnect(OpenIdConnectAuth): def validate_claims(self, id_token): """Copy of the regular validate_claims method without the nonce validation.""" - utc_timestamp = timegm(datetime.datetime.utcnow().utctimetuple()) + 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") diff --git a/social_core/backends/open_id_connect.py b/social_core/backends/open_id_connect.py index db7f84d23..e17b66440 100644 --- a/social_core/backends/open_id_connect.py +++ b/social_core/backends/open_id_connect.py @@ -152,7 +152,7 @@ def remove_nonce(self, nonce_id): self.strategy.storage.association.remove([nonce_id]) def validate_claims(self, id_token): - utc_timestamp = timegm(datetime.datetime.utcnow().utctimetuple()) + 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") diff --git a/social_core/storage.py b/social_core/storage.py index eb591f220..ee3019ff8 100644 --- a/social_core/storage.py +++ b/social_core/storage.py @@ -5,7 +5,7 @@ import time import uuid import warnings -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from openid.association import Association as OpenIdAssociation @@ -68,19 +68,19 @@ def expiration_timedelta(self): except (ValueError, TypeError): return None - now = datetime.utcnow() + now = datetime.now(timezone.utc) # Detect if expires is a timestamp if expires > time.mktime(now.timetuple()): # expires is a datetime, return the remaining difference - return datetime.utcfromtimestamp(expires) - now + return datetime.fromtimestamp(expires, tz=timezone.utc) - now else: # 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.utcfromtimestamp(auth_time) + reference = datetime.fromtimestamp(auth_time, tz=timezone.utc) return (reference + timedelta(seconds=expires)) - now else: return timedelta(seconds=expires) diff --git a/social_core/tests/backends/test_dummy.py b/social_core/tests/backends/test_dummy.py index 71531a37f..29bbdd673 100644 --- a/social_core/tests/backends/test_dummy.py +++ b/social_core/tests/backends/test_dummy.py @@ -121,7 +121,9 @@ class ExpirationTimeTest(DummyOAuth2Test): "first_name": "Foo", "last_name": "Bar", "email": "foo@bar.com", - "expires": time.mktime((datetime.datetime.utcnow() + DELTA).timetuple()), + "expires": time.mktime( + (datetime.datetime.now(datetime.timezone.utc) + DELTA).timetuple() + ), } ) diff --git a/social_core/tests/backends/test_livejournal.py b/social_core/tests/backends/test_livejournal.py index 6c4f30421..b25836613 100644 --- a/social_core/tests/backends/test_livejournal.py +++ b/social_core/tests/backends/test_livejournal.py @@ -6,7 +6,9 @@ from ...exceptions import AuthMissingParameter from .open_id import OpenIdTest -JANRAIN_NONCE = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") +JANRAIN_NONCE = datetime.datetime.now(datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%SZ" +) class LiveJournalOpenIdTest(OpenIdTest): diff --git a/social_core/tests/backends/test_ngpvan.py b/social_core/tests/backends/test_ngpvan.py index 53e6738da..9173ff6e6 100644 --- a/social_core/tests/backends/test_ngpvan.py +++ b/social_core/tests/backends/test_ngpvan.py @@ -7,7 +7,9 @@ from .open_id import OpenIdTest -JANRAIN_NONCE = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") +JANRAIN_NONCE = datetime.datetime.now(datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%SZ" +) class NGPVANActionIDOpenIDTest(OpenIdTest): diff --git a/social_core/tests/backends/test_open_id_connect.py b/social_core/tests/backends/test_open_id_connect.py index 739c5efed..b9e998923 100644 --- a/social_core/tests/backends/test_open_id_connect.py +++ b/social_core/tests/backends/test_open_id_connect.py @@ -136,7 +136,7 @@ def prepare_access_token_body( body = {"access_token": "foobar", "token_type": "bearer"} client_key = client_key or self.client_key - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) expiration_datetime = expiration_datetime or ( now + datetime.timedelta(seconds=30) ) @@ -145,8 +145,8 @@ def prepare_access_token_body( issuer = issuer or self.issuer id_token = self.get_id_token( client_key, - timegm(expiration_datetime.utctimetuple()), - timegm(issue_datetime.utctimetuple()), + timegm(expiration_datetime.timetuple()), + timegm(issue_datetime.timetuple()), nonce, issuer, ) @@ -156,7 +156,7 @@ def prepare_access_token_body( body["id_token"] = jwt.encode( id_token, key=jwt.PyJWK( - dict(self.key, iat=timegm(issue_datetime.utctimetuple()), nonce=nonce) + dict(self.key, iat=timegm(issue_datetime.timetuple()), nonce=nonce) ).key, algorithm="RS256", headers=dict(kid=kid) if kid else None, @@ -190,9 +190,9 @@ def test_invalid_signature(self): ) def test_expired_signature(self): - expiration_datetime = datetime.datetime.utcnow() - datetime.timedelta( - seconds=30 - ) + expiration_datetime = datetime.datetime.now( + datetime.timezone.utc + ) - datetime.timedelta(seconds=30) self.authtoken_raised( "Token error: Signature has expired", expiration_datetime=expiration_datetime, @@ -207,9 +207,9 @@ def test_invalid_audience(self): ) def test_invalid_issue_time(self): - expiration_datetime = datetime.datetime.utcnow() - datetime.timedelta( - seconds=self.backend.ID_TOKEN_MAX_AGE * 2 - ) + 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 ) From f803525bae46f4ab1814634747a1eb3400b0cbca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:46:06 +0000 Subject: [PATCH 069/152] build(deps-dev): bump pre-commit from 3.7.1 to 3.8.0 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.7.1 to 3.8.0. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.7.1...v3.8.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index a93b8c1e4..1509fe7c3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1 @@ -pre-commit==3.7.1 +pre-commit==3.8.0 From b051334a0b0b1e44b8380ce5fb6fcc0da1677164 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 20:13:18 +0000 Subject: [PATCH 070/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.16.0 → v3.17.0](https://github.com/asottile/pyupgrade/compare/v3.16.0...v3.17.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 659f77105..56db22173 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: isort args: [--profile=black] - repo: https://github.com/asottile/pyupgrade - rev: v3.16.0 + rev: v3.17.0 hooks: - id: pyupgrade args: [--py38-plus] From 5899cb92ba1c3799a1c614f9b444c490bee2e25c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 20:41:03 +0000 Subject: [PATCH 071/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 24.4.2 → 24.8.0](https://github.com/psf/black/compare/24.4.2...24.8.0) - [github.com/PyCQA/flake8: 7.1.0 → 7.1.1](https://github.com/PyCQA/flake8/compare/7.1.0...7.1.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 56db22173..01ddd80a7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,11 +20,11 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 - rev: 7.1.0 + rev: 7.1.1 hooks: - id: flake8 - repo: meta From 19ae97dcfe65c0a294d7be566d6a996fcf965769 Mon Sep 17 00:00:00 2001 From: George Margaritis Date: Fri, 30 Aug 2024 18:43:04 +0300 Subject: [PATCH 072/152] Fix access_token expiration and refresh handling in GitHub backend Ensure the correct key is used for access_token expiration in the GitHub backend's extra_data, and save the refresh_token. Previously, the expiration of the access_token was not stored, causing the refresh_token functionality to be skipped. Signed-off-by: George Margaritis --- CHANGELOG.md | 1 + social_core/backends/github.py | 7 ++- social_core/tests/backends/test_github.py | 61 ++++++++++++++++++++++- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b570d970..c4b573162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed - Handle case where user has not registered a `family-name` with ORCID +- Fix access token expiration and refresh token handling in GitHub backend ## [4.5.4](https://github.com/python-social-auth/social-core/releases/tag/4.5.4) - 2024-04-25 diff --git a/social_core/backends/github.py b/social_core/backends/github.py index e97322d22..14bb7931b 100644 --- a/social_core/backends/github.py +++ b/social_core/backends/github.py @@ -23,7 +23,12 @@ class GithubOAuth2(BaseOAuth2): REDIRECT_STATE = False STATE_PARAMETER = True SEND_USER_AGENT = True - EXTRA_DATA = [("id", "id"), ("expires", "expires"), ("login", "login")] + EXTRA_DATA = [ + ("id", "id"), + ("expires_in", "expires"), + ("login", "login"), + ("refresh_token", "refresh_token"), + ] def api_url(self): return self.API_URL diff --git a/social_core/tests/backends/test_github.py b/social_core/tests/backends/test_github.py index a4edc8d67..eac2d9cbc 100644 --- a/social_core/tests/backends/test_github.py +++ b/social_core/tests/backends/test_github.py @@ -10,7 +10,24 @@ class GithubOAuth2Test(OAuth2Test): 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"}) + 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", @@ -46,12 +63,25 @@ class GithubOAuth2Test(OAuth2Test): } ) + 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( @@ -122,6 +152,17 @@ def test_partial_pipeline(self): ) self.do_partial_pipeline() + def test_refresh_token(self): + url = "https://api.github.com/user/emails" + HTTPretty.register_uri( + HTTPretty.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" @@ -139,6 +180,10 @@ 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" @@ -164,6 +209,11 @@ def test_partial_pipeline(self): 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" @@ -181,6 +231,10 @@ 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" @@ -205,3 +259,8 @@ 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() From d9554fa40e751c85ae60231fe2f5bd5a528c4452 Mon Sep 17 00:00:00 2001 From: Mateusz Mandera Date: Thu, 22 Aug 2024 20:27:01 +0200 Subject: [PATCH 073/152] Add AzureADOauth2 backend using the v2.0 API. AzureADOAuth2 uses the v1.0 API which doesn't support personal accounts. Updating the endpoints used by the original class may break backward compatibility, so add this as just an additional subclass. --- social_core/backends/azuread.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/social_core/backends/azuread.py b/social_core/backends/azuread.py index 837d2c546..29db25b57 100644 --- a/social_core/backends/azuread.py +++ b/social_core/backends/azuread.py @@ -145,3 +145,14 @@ def get_auth_token(self, user_id): 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"] From e294f034ae407d4df16b847684138682eb353062 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:31:37 +0000 Subject: [PATCH 074/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0) - [github.com/psf/black: 24.8.0 → 24.10.0](https://github.com/psf/black/compare/24.8.0...24.10.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01ddd80a7..027772182 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-merge-conflict - id: check-json @@ -20,7 +20,7 @@ repos: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 24.8.0 + rev: 24.10.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 From 7e36f5b50ece63ba22b6141e124c42569a444553 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:21:39 +0000 Subject: [PATCH 075/152] build(deps-dev): bump pre-commit from 3.8.0 to 4.0.0 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.8.0 to 4.0.0. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.8.0...v4.0.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1509fe7c3..7ffa20c41 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1 @@ -pre-commit==3.8.0 +pre-commit==4.0.0 From 1d6b17344f60397b1168546b76d1cb0be667af9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:16:58 +0000 Subject: [PATCH 076/152] build(deps-dev): bump pre-commit from 4.0.0 to 4.0.1 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.0.0 to 4.0.1. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v4.0.0...v4.0.1) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7ffa20c41..e88d27155 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1 @@ -pre-commit==4.0.0 +pre-commit==4.0.1 From 5b7ec012e28a835c6f5769e0f7a06f63f43d6e97 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 20:42:11 +0000 Subject: [PATCH 077/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.17.0 → v3.18.0](https://github.com/asottile/pyupgrade/compare/v3.17.0...v3.18.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 027772182..a41015914 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: isort args: [--profile=black] - repo: https://github.com/asottile/pyupgrade - rev: v3.17.0 + rev: v3.18.0 hooks: - id: pyupgrade args: [--py38-plus] From c2b71d7ae104cf5ff3067fd998c45410611704f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 17 Oct 2024 08:06:58 +0200 Subject: [PATCH 078/152] chore: add funding.json url --- .well-known/funding-manifest-urls | 1 + 1 file changed, 1 insertion(+) create mode 100644 .well-known/funding-manifest-urls diff --git a/.well-known/funding-manifest-urls b/.well-known/funding-manifest-urls new file mode 100644 index 000000000..81328121f --- /dev/null +++ b/.well-known/funding-manifest-urls @@ -0,0 +1 @@ +https://github.com/python-social-auth/.github/blob/main/funding.json From f6d0cddf0273ef49fd0d31ab4780d84b8721c3ca Mon Sep 17 00:00:00 2001 From: Fredrik Stockman Date: Thu, 24 Oct 2024 23:26:43 +0200 Subject: [PATCH 079/152] Fix deprecated Atlassian field `name` --- social_core/backends/atlassian.py | 2 +- social_core/tests/backends/test_atlassian.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/social_core/backends/atlassian.py b/social_core/backends/atlassian.py index 2c3e1a4c1..d82cbaefc 100644 --- a/social_core/backends/atlassian.py +++ b/social_core/backends/atlassian.py @@ -22,7 +22,7 @@ def auth_params(self, state=None): def get_user_details(self, response): fullname, first_name, last_name = self.get_user_names(response["displayName"]) return { - "username": response["name"], + "username": response["accountId"], "email": response["emailAddress"], "fullname": fullname, "first_name": first_name, diff --git a/social_core/tests/backends/test_atlassian.py b/social_core/tests/backends/test_atlassian.py index caa26e193..cc74e6f3c 100644 --- a/social_core/tests/backends/test_atlassian.py +++ b/social_core/tests/backends/test_atlassian.py @@ -9,7 +9,7 @@ class AtlassianOAuth2Test(OAuth2Test): 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 = "erlich" + expected_username = "9927935d01-92a7-4687-8272-a9b8d3b2ae2e" access_token_body = json.dumps({"access_token": "aviato", "token_type": "bearer"}) tenant_data_body = json.dumps( [ @@ -26,7 +26,6 @@ class AtlassianOAuth2Test(OAuth2Test): "self": "http://bachmanity.atlassian.net/rest/api/3/user?username=erlich", "key": "erlich", "accountId": "99:27935d01-92a7-4687-8272-a9b8d3b2ae2e", - "name": "erlich", "emailAddress": "erlich@bachmanity.com", "avatarUrls": { "48x48": "http://bachmanity.atlassian.net/secure/useravatar?size=large&ownerId=erlich", From a7f94789f0e2b45fa04d24d4c808fdc9e381d9f7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 20:48:55 +0000 Subject: [PATCH 080/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.18.0 → v3.19.0](https://github.com/asottile/pyupgrade/compare/v3.18.0...v3.19.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a41015914..32317b3e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: isort args: [--profile=black] - repo: https://github.com/asottile/pyupgrade - rev: v3.18.0 + rev: v3.19.0 hooks: - id: pyupgrade args: [--py38-plus] From 02900f52a72263b5701d988841d3050b41536f1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Nov 2024 17:27:50 +0000 Subject: [PATCH 081/152] build(deps): bump codecov/codecov-action from 3 to 5 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8bdd020a4..8bb7fb9aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: - name: Test with tox run: tox -e "py${PYTHON_VERSION/\./}" - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v5 with: flags: unittests name: Python ${{ matrix.python-version }} From 7ad0dc4525cd38548fb03c0ea2459569bbcd7b0d Mon Sep 17 00:00:00 2001 From: selten Date: Mon, 25 Nov 2024 19:12:27 +0100 Subject: [PATCH 082/152] Allow overriding emails to be fully lowercase --- CHANGELOG.md | 1 + social_core/pipeline/user.py | 7 ++++ social_core/tests/test_pipeline.py | 51 ++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b573162..58e8b3164 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### 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`. ## [4.5.4](https://github.com/python-social-auth/social-core/releases/tag/4.5.4) - 2024-04-25 diff --git a/social_core/pipeline/user.py b/social_core/pipeline/user.py index 45dd339c8..a7ff5ee91 100644 --- a/social_core/pipeline/user.py +++ b/social_core/pipeline/user.py @@ -70,6 +70,13 @@ def create_user(strategy, details, backend, user=None, *args, **kwargs): if not fields: return + # Allow overriding the email field if desired by application specification + if backend.setting('FORCE_EMAIL_LOWERCASE', False): + emailfield = fields.get('email', None) + if emailfield: + fields['email'] = emailfield.lower() + + return {"is_new": True, "user": strategy.create_user(**fields)} diff --git a/social_core/tests/test_pipeline.py b/social_core/tests/test_pipeline.py index f36b2d362..8d0804f43 100644 --- a/social_core/tests/test_pipeline.py +++ b/social_core/tests/test_pipeline.py @@ -266,3 +266,54 @@ def test_user_details_(self): 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(user.first_name, "Test2") + + self.assertEqual( + self.strategy.session_get("email"), + 'foo@bar.com' + ) From 8f288e42cc85d545396a1b99a9433629df3a06ca Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 18:13:43 +0000 Subject: [PATCH 083/152] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- social_core/pipeline/user.py | 7 +++---- social_core/tests/test_pipeline.py | 5 +---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/social_core/pipeline/user.py b/social_core/pipeline/user.py index a7ff5ee91..bb88d3c49 100644 --- a/social_core/pipeline/user.py +++ b/social_core/pipeline/user.py @@ -71,11 +71,10 @@ def create_user(strategy, details, backend, user=None, *args, **kwargs): return # Allow overriding the email field if desired by application specification - if backend.setting('FORCE_EMAIL_LOWERCASE', False): - emailfield = fields.get('email', None) + if backend.setting("FORCE_EMAIL_LOWERCASE", False): + emailfield = fields.get("email", None) if emailfield: - fields['email'] = emailfield.lower() - + fields["email"] = emailfield.lower() return {"is_new": True, "user": strategy.create_user(**fields)} diff --git a/social_core/tests/test_pipeline.py b/social_core/tests/test_pipeline.py index 8d0804f43..2ced5b7bb 100644 --- a/social_core/tests/test_pipeline.py +++ b/social_core/tests/test_pipeline.py @@ -313,7 +313,4 @@ def test_lowercase_email(self): self.do_login(after_complete_checks=False) self.assertEqual(user.first_name, "Test2") - self.assertEqual( - self.strategy.session_get("email"), - 'foo@bar.com' - ) + self.assertEqual(self.strategy.session_get("email"), "foo@bar.com") From f71316a457c5352a4c30c26bd2f071ed72cfe994 Mon Sep 17 00:00:00 2001 From: selten Date: Mon, 25 Nov 2024 19:16:43 +0100 Subject: [PATCH 084/152] Cleanup erroneous copy-paste for testing of lowercase emails --- social_core/tests/test_pipeline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/social_core/tests/test_pipeline.py b/social_core/tests/test_pipeline.py index 2ced5b7bb..0acd4f0fa 100644 --- a/social_core/tests/test_pipeline.py +++ b/social_core/tests/test_pipeline.py @@ -311,6 +311,4 @@ def test_lowercase_email(self): } ) self.do_login(after_complete_checks=False) - self.assertEqual(user.first_name, "Test2") - self.assertEqual(self.strategy.session_get("email"), "foo@bar.com") From 073f6a1e651f03f32a2c272df2cc2efcd5d0919a Mon Sep 17 00:00:00 2001 From: selten Date: Mon, 25 Nov 2024 19:22:27 +0100 Subject: [PATCH 085/152] Ensure returning the email is supported --- social_core/tests/actions/actions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/social_core/tests/actions/actions.py b/social_core/tests/actions/actions.py index 9db7a2fff..726990923 100644 --- a/social_core/tests/actions/actions.py +++ b/social_core/tests/actions/actions.py @@ -134,6 +134,9 @@ def do_login( 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) @@ -209,6 +212,9 @@ def do_login_with_partial_pipeline(self, before_complete=None): 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") From 326d4b3c4aa69e92c19b472b437dd718c5ee5bd8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 18:22:42 +0000 Subject: [PATCH 086/152] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- social_core/tests/actions/actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/social_core/tests/actions/actions.py b/social_core/tests/actions/actions.py index 726990923..376a3cf36 100644 --- a/social_core/tests/actions/actions.py +++ b/social_core/tests/actions/actions.py @@ -134,7 +134,7 @@ def do_login( def _login(backend, user, social_user): backend.strategy.session_set("username", user.username) - user_email = getattr(user, 'email', None) + user_email = getattr(user, "email", None) if user_email: backend.strategy.session_set("email", user_email) @@ -212,7 +212,7 @@ def do_login_with_partial_pipeline(self, before_complete=None): def _login(backend, user, social_user): backend.strategy.session_set("username", user.username) - user_email = getattr(user, 'email', None) + user_email = getattr(user, "email", None) if user_email: backend.strategy.session_set("email", user_email) From 9e08c3d24b743af80c9bf4cc863b484502e86be9 Mon Sep 17 00:00:00 2001 From: Gersona Date: Mon, 9 Dec 2024 22:00:31 +0300 Subject: [PATCH 087/152] 944 fix params in auth url (#945) * handle extra parameters in auth url * updading tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix import errors * fix unsupported feature in 3.8 Fixes #944 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- social_core/backends/oauth.py | 16 ++++----- social_core/backends/soundcloud.py | 5 ++- social_core/tests/backends/oauth.py | 36 +++++++++++++++++++ social_core/tests/backends/test_amazon.py | 6 ++-- social_core/tests/backends/test_angel.py | 4 +-- social_core/tests/backends/test_apple.py | 4 +-- social_core/tests/backends/test_arcgis.py | 4 +-- social_core/tests/backends/test_asana.py | 4 +-- social_core/tests/backends/test_atlassian.py | 4 +-- social_core/tests/backends/test_auth0.py | 4 +-- social_core/tests/backends/test_azuread.py | 4 +-- .../tests/backends/test_azuread_b2c.py | 4 +-- social_core/tests/backends/test_behance.py | 4 +-- social_core/tests/backends/test_bitbucket.py | 6 ++-- social_core/tests/backends/test_box.py | 4 +-- social_core/tests/backends/test_cas.py | 4 +-- social_core/tests/backends/test_chatwork.py | 4 +-- social_core/tests/backends/test_cilogon.py | 4 +-- social_core/tests/backends/test_clef.py | 4 +-- social_core/tests/backends/test_cognito.py | 4 +-- social_core/tests/backends/test_coinbase.py | 4 +-- social_core/tests/backends/test_coursera.py | 4 +-- .../tests/backends/test_dailymotion.py | 4 +-- social_core/tests/backends/test_deezer.py | 4 +-- .../tests/backends/test_digitalocean.py | 4 +-- social_core/tests/backends/test_discogs.py | 4 +-- social_core/tests/backends/test_disqus.py | 4 +-- social_core/tests/backends/test_dribbble.py | 4 +-- social_core/tests/backends/test_drip.py | 4 +-- social_core/tests/backends/test_dropbox.py | 4 +-- social_core/tests/backends/test_dummy.py | 4 +-- social_core/tests/backends/test_edmodo.py | 4 +-- .../tests/backends/test_egi_checkin.py | 6 ++-- social_core/tests/backends/test_einfracz.py | 6 ++-- social_core/tests/backends/test_elixir.py | 4 +-- social_core/tests/backends/test_eventbrite.py | 4 +-- social_core/tests/backends/test_evernote.py | 4 +-- social_core/tests/backends/test_facebook.py | 4 +-- social_core/tests/backends/test_fence.py | 4 +-- social_core/tests/backends/test_fitbit.py | 4 +-- .../tests/backends/test_five_hundred_px.py | 4 +-- social_core/tests/backends/test_flat.py | 4 +-- social_core/tests/backends/test_flickr.py | 4 +-- social_core/tests/backends/test_foursquare.py | 4 +-- social_core/tests/backends/test_gitea.py | 6 ++-- social_core/tests/backends/test_github.py | 4 +-- .../tests/backends/test_github_enterprise.py | 4 +-- social_core/tests/backends/test_gitlab.py | 6 ++-- social_core/tests/backends/test_globus.py | 4 +-- social_core/tests/backends/test_google.py | 6 ++-- social_core/tests/backends/test_grafana.py | 4 +-- social_core/tests/backends/test_instagram.py | 4 +-- social_core/tests/backends/test_itembase.py | 6 ++-- social_core/tests/backends/test_kakao.py | 4 +-- social_core/tests/backends/test_keycloak.py | 4 +-- social_core/tests/backends/test_linkedin.py | 6 ++-- social_core/tests/backends/test_live.py | 4 +-- social_core/tests/backends/test_lyft.py | 4 +-- social_core/tests/backends/test_mailru.py | 4 +-- .../tests/backends/test_mapmyfitness.py | 4 +-- social_core/tests/backends/test_microsoft.py | 4 +-- social_core/tests/backends/test_mineid.py | 7 ++-- social_core/tests/backends/test_mixcloud.py | 4 +-- .../tests/backends/test_musicbrainz.py | 4 +-- .../tests/backends/test_nationbuilder.py | 4 +-- social_core/tests/backends/test_naver.py | 4 +-- social_core/tests/backends/test_okta.py | 4 +-- .../tests/backends/test_open_id_connect.py | 4 +-- .../backends/test_openstreetmap_oauth2.py | 4 +-- social_core/tests/backends/test_orbi.py | 4 +-- social_core/tests/backends/test_orcid.py | 4 +-- social_core/tests/backends/test_osso.py | 4 +-- social_core/tests/backends/test_patreon.py | 4 +-- social_core/tests/backends/test_paypal.py | 4 +-- .../tests/backends/test_phabricator.py | 6 ++-- social_core/tests/backends/test_pinterest.py | 6 ++-- social_core/tests/backends/test_podio.py | 4 +-- social_core/tests/backends/test_qiita.py | 4 +-- social_core/tests/backends/test_quizlet.py | 4 +-- .../tests/backends/test_readability.py | 4 +-- social_core/tests/backends/test_reddit.py | 4 +-- social_core/tests/backends/test_scistarter.py | 4 +-- social_core/tests/backends/test_seznam.py | 4 +-- .../tests/backends/test_simplelogin.py | 4 +-- social_core/tests/backends/test_sketchfab.py | 4 +-- social_core/tests/backends/test_skyrock.py | 4 +-- social_core/tests/backends/test_slack.py | 4 +-- social_core/tests/backends/test_soundcloud.py | 4 +-- social_core/tests/backends/test_spotify.py | 4 +-- .../tests/backends/test_stackoverflow.py | 4 +-- social_core/tests/backends/test_stocktwits.py | 4 +-- social_core/tests/backends/test_strava.py | 4 +-- social_core/tests/backends/test_stripe.py | 4 +-- social_core/tests/backends/test_taobao.py | 4 +-- .../tests/backends/test_thisismyjam.py | 4 +-- social_core/tests/backends/test_tripit.py | 4 +-- social_core/tests/backends/test_tumblr.py | 4 +-- social_core/tests/backends/test_twitch.py | 4 +-- social_core/tests/backends/test_twitter.py | 6 ++-- .../tests/backends/test_twitter_oauth2.py | 9 +++-- social_core/tests/backends/test_uber.py | 4 +-- social_core/tests/backends/test_udata.py | 4 +-- social_core/tests/backends/test_universe.py | 4 +-- social_core/tests/backends/test_upwork.py | 4 +-- social_core/tests/backends/test_vault.py | 4 +-- social_core/tests/backends/test_vk.py | 4 +-- social_core/tests/backends/test_wlcg.py | 4 +-- social_core/tests/backends/test_wunderlist.py | 4 +-- social_core/tests/backends/test_xing.py | 4 +-- social_core/tests/backends/test_yahoo.py | 4 +-- social_core/tests/backends/test_yammer.py | 4 +-- social_core/tests/backends/test_yandex.py | 6 ++-- social_core/tests/backends/test_zoom.py | 4 +-- social_core/tests/backends/test_zotero.py | 4 +-- social_core/utils.py | 6 ++-- 115 files changed, 296 insertions(+), 245 deletions(-) diff --git a/social_core/backends/oauth.py b/social_core/backends/oauth.py index 57e146a59..4d724ef9c 100644 --- a/social_core/backends/oauth.py +++ b/social_core/backends/oauth.py @@ -1,6 +1,6 @@ import base64 import hashlib -from urllib.parse import unquote, urlencode +from urllib.parse import urlencode from oauthlib.oauth1 import SIGNATURE_TYPE_AUTH_HEADER from requests_oauthlib import OAuth1 @@ -277,7 +277,7 @@ def oauth_authorization_request(self, token): ) state = self.get_or_create_state() params[self.REDIRECT_URI_PARAMETER_NAME] = self.get_redirect_uri(state) - return f"{self.authorization_url()}?{urlencode(params)}" + return url_add_parameters(self.authorization_url(), params) def oauth_auth( self, token=None, oauth_verifier=None, signature_type=SIGNATURE_TYPE_AUTH_HEADER @@ -352,12 +352,12 @@ def auth_url(self): params = self.auth_params(state) params.update(self.get_scope_argument()) params.update(self.auth_extra_arguments()) - params = urlencode(params) - if not self.REDIRECT_STATE: - # redirect_uri matching is strictly enforced, so match the - # providers value exactly. - params = unquote(params) - return f"{self.authorization_url()}?{params}" + + # 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 = { diff --git a/social_core/backends/soundcloud.py b/social_core/backends/soundcloud.py index a8297433c..5a7ca0fbc 100644 --- a/social_core/backends/soundcloud.py +++ b/social_core/backends/soundcloud.py @@ -3,8 +3,7 @@ https://python-social-auth.readthedocs.io/en/latest/backends/soundcloud.html """ -from urllib.parse import urlencode - +from ..utils import url_add_parameters from .oauth import BaseOAuth2 @@ -56,4 +55,4 @@ def auth_url(self): params = self.auth_params(state) params.update(self.get_scope_argument()) params.update(self.auth_extra_arguments()) - return self.AUTHORIZATION_URL + "?" + urlencode(params) + return url_add_parameters(self.AUTHORIZATION_URL, params, True) diff --git a/social_core/tests/backends/oauth.py b/social_core/tests/backends/oauth.py index 4129ee714..f59a6f6ef 100644 --- a/social_core/tests/backends/oauth.py +++ b/social_core/tests/backends/oauth.py @@ -1,3 +1,4 @@ +from unittest.mock import patch from urllib.parse import urlparse import requests @@ -176,3 +177,38 @@ def do_login(self): ) return user + + +class BaseAuthUrlTestMixin: + + 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", + ): + with 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() + self.check_parameters_in_authorization_url() diff --git a/social_core/tests/backends/test_amazon.py b/social_core/tests/backends/test_amazon.py index 24cebae10..58298b5e4 100644 --- a/social_core/tests/backends/test_amazon.py +++ b/social_core/tests/backends/test_amazon.py @@ -2,10 +2,10 @@ from httpretty import HTTPretty -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class AmazonOAuth2Test(OAuth2Test): +class AmazonOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.amazon.AmazonOAuth2" user_data_url = "https://api.amazon.com/user/profile" expected_username = "FooBar" @@ -25,7 +25,7 @@ def test_partial_pipeline(self): self.do_partial_pipeline() -class AmazonOAuth2BrokenServerResponseTest(OAuth2Test): +class AmazonOAuth2BrokenServerResponseTest(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.amazon.AmazonOAuth2" user_data_url = "https://www.amazon.com/ap/user/profile" expected_username = "FooBar" diff --git a/social_core/tests/backends/test_angel.py b/social_core/tests/backends/test_angel.py index cfeb85e6a..8987e57c2 100644 --- a/social_core/tests/backends/test_angel.py +++ b/social_core/tests/backends/test_angel.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class AngelOAuth2Test(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"}) diff --git a/social_core/tests/backends/test_apple.py b/social_core/tests/backends/test_apple.py index 7d58ee0e6..c6471230a 100644 --- a/social_core/tests/backends/test_apple.py +++ b/social_core/tests/backends/test_apple.py @@ -1,7 +1,7 @@ import json from unittest.mock import patch -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test TEST_KEY = """ -----BEGIN EC PRIVATE KEY----- @@ -20,7 +20,7 @@ } -class AppleIdTest(OAuth2Test): +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" diff --git a/social_core/tests/backends/test_arcgis.py b/social_core/tests/backends/test_arcgis.py index e8164c368..3a33ec2bf 100644 --- a/social_core/tests/backends/test_arcgis.py +++ b/social_core/tests/backends/test_arcgis.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class ArcGISOAuth2Test(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" diff --git a/social_core/tests/backends/test_asana.py b/social_core/tests/backends/test_asana.py index efc394a0b..221ccb5a7 100644 --- a/social_core/tests/backends/test_asana.py +++ b/social_core/tests/backends/test_asana.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class AsanaOAuth2Test(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" diff --git a/social_core/tests/backends/test_atlassian.py b/social_core/tests/backends/test_atlassian.py index cc74e6f3c..077a0fc3d 100644 --- a/social_core/tests/backends/test_atlassian.py +++ b/social_core/tests/backends/test_atlassian.py @@ -2,10 +2,10 @@ from httpretty import HTTPretty -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class AtlassianOAuth2Test(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" diff --git a/social_core/tests/backends/test_auth0.py b/social_core/tests/backends/test_auth0.py index 659a9ef1d..e77723b0f 100644 --- a/social_core/tests/backends/test_auth0.py +++ b/social_core/tests/backends/test_auth0.py @@ -3,7 +3,7 @@ import jwt from httpretty import HTTPretty -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test JWK_KEY = { "kty": "RSA", @@ -30,7 +30,7 @@ DOMAIN = "foobar.auth0.com" -class Auth0OAuth2Test(OAuth2Test): +class Auth0OAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.auth0.Auth0OAuth2" access_token_body = json.dumps( { diff --git a/social_core/tests/backends/test_azuread.py b/social_core/tests/backends/test_azuread.py index a24b87dfa..83e7c3ccc 100644 --- a/social_core/tests/backends/test_azuread.py +++ b/social_core/tests/backends/test_azuread.py @@ -26,10 +26,10 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class AzureADOAuth2Test(OAuth2Test): +class AzureADOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.azuread.AzureADOAuth2" user_data_url = "https://graph.windows.net/me" expected_username = "foobar" diff --git a/social_core/tests/backends/test_azuread_b2c.py b/social_core/tests/backends/test_azuread_b2c.py index 96244fc5c..709b7da0c 100644 --- a/social_core/tests/backends/test_azuread_b2c.py +++ b/social_core/tests/backends/test_azuread_b2c.py @@ -31,7 +31,7 @@ from httpretty import HTTPretty from jwt.algorithms import RSAAlgorithm -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test # Dummy private and private keys: RSA_PUBLIC_JWT_KEY = { @@ -84,7 +84,7 @@ } -class AzureADB2COAuth2Test(OAuth2Test): +class AzureADB2COAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): AUTH_KEY = "abcdef12-1234-9876-0000-abcdef098765" EXPIRES_IN = 3600 AUTH_TIME = int(time()) diff --git a/social_core/tests/backends/test_behance.py b/social_core/tests/backends/test_behance.py index d94caf920..357f9ce12 100644 --- a/social_core/tests/backends/test_behance.py +++ b/social_core/tests/backends/test_behance.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class BehanceOAuth2Test(OAuth2Test): +class BehanceOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.behance.BehanceOAuth2" access_token_body = json.dumps( { diff --git a/social_core/tests/backends/test_bitbucket.py b/social_core/tests/backends/test_bitbucket.py index e8259d099..69d2a07d5 100644 --- a/social_core/tests/backends/test_bitbucket.py +++ b/social_core/tests/backends/test_bitbucket.py @@ -4,7 +4,7 @@ from httpretty import HTTPretty from ...exceptions import AuthForbidden -from .oauth import OAuth1Test, OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth1AuthUrlTestMixin, OAuth1Test, OAuth2Test class BitbucketOAuthMixin: @@ -72,7 +72,7 @@ class BitbucketOAuthMixin: ) -class BitbucketOAuth1Test(BitbucketOAuthMixin, OAuth1Test): +class BitbucketOAuth1Test(BitbucketOAuthMixin, OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.bitbucket.BitbucketOAuth" request_token_body = urlencode( @@ -131,7 +131,7 @@ def test_partial_pipeline(self): super().test_partial_pipeline() -class BitbucketOAuth2Test(BitbucketOAuthMixin, OAuth2Test): +class BitbucketOAuth2Test(BitbucketOAuthMixin, OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.bitbucket.BitbucketOAuth2" access_token_body = json.dumps( diff --git a/social_core/tests/backends/test_box.py b/social_core/tests/backends/test_box.py index 5f70394ce..45a212ccf 100644 --- a/social_core/tests/backends/test_box.py +++ b/social_core/tests/backends/test_box.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class BoxOAuth2Test(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" diff --git a/social_core/tests/backends/test_cas.py b/social_core/tests/backends/test_cas.py index 82d0dbd44..1d182140d 100644 --- a/social_core/tests/backends/test_cas.py +++ b/social_core/tests/backends/test_cas.py @@ -2,13 +2,13 @@ from httpretty import HTTPretty -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test from .test_open_id_connect import OpenIdConnectTestMixin ROOT_URL = "https://cas.example.net/" -class CASOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): +class CASOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.cas.CASOpenIdConnectAuth" issuer = f"{ROOT_URL}oidc" openid_config_body = json.dumps( diff --git a/social_core/tests/backends/test_chatwork.py b/social_core/tests/backends/test_chatwork.py index ce4ae92b4..95e1f55ee 100644 --- a/social_core/tests/backends/test_chatwork.py +++ b/social_core/tests/backends/test_chatwork.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class ChatworkOAuth2Test(OAuth2Test): +class ChatworkOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.chatwork.ChatworkOAuth2" user_data_url = "https://api.chatwork.com/v2/me" expected_username = "hogehoge" diff --git a/social_core/tests/backends/test_cilogon.py b/social_core/tests/backends/test_cilogon.py index b8b9feb1c..f03b9c3d7 100644 --- a/social_core/tests/backends/test_cilogon.py +++ b/social_core/tests/backends/test_cilogon.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class CILogonOAuth2Test(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 diff --git a/social_core/tests/backends/test_clef.py b/social_core/tests/backends/test_clef.py index d50e05f59..ade7224ce 100644 --- a/social_core/tests/backends/test_clef.py +++ b/social_core/tests/backends/test_clef.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class ClefOAuth2Test(OAuth2Test): +class ClefOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.clef.ClefOAuth2" user_data_url = "https://clef.io/api/v1/info" expected_username = "test" diff --git a/social_core/tests/backends/test_cognito.py b/social_core/tests/backends/test_cognito.py index aa4f8228b..b09563453 100644 --- a/social_core/tests/backends/test_cognito.py +++ b/social_core/tests/backends/test_cognito.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class CognitoAuth2Test(OAuth2Test): +class CognitoAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.cognito.CognitoOAuth2" pool_domain = "https://social_core.auth.eu-west-1.amazoncognito.com" expected_username = "cognito.account.ABCDE1234" diff --git a/social_core/tests/backends/test_coinbase.py b/social_core/tests/backends/test_coinbase.py index 5d3c0d2d5..8b4248074 100644 --- a/social_core/tests/backends/test_coinbase.py +++ b/social_core/tests/backends/test_coinbase.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class CoinbaseOAuth2Test(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" diff --git a/social_core/tests/backends/test_coursera.py b/social_core/tests/backends/test_coursera.py index b85ca702a..cc943b54d 100644 --- a/social_core/tests/backends/test_coursera.py +++ b/social_core/tests/backends/test_coursera.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class CourseraOAuth2Test(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" diff --git a/social_core/tests/backends/test_dailymotion.py b/social_core/tests/backends/test_dailymotion.py index 2204f11a2..4e3a4892c 100644 --- a/social_core/tests/backends/test_dailymotion.py +++ b/social_core/tests/backends/test_dailymotion.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class DailymotionOAuth2Test(OAuth2Test): +class DailymotionOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.dailymotion.DailymotionOAuth2" user_data_url = "https://api.dailymotion.com/auth/" expected_username = "foobar" diff --git a/social_core/tests/backends/test_deezer.py b/social_core/tests/backends/test_deezer.py index 3fe682280..05bd333bb 100644 --- a/social_core/tests/backends/test_deezer.py +++ b/social_core/tests/backends/test_deezer.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class DeezerOAuth2Test(OAuth2Test): +class DeezerOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.deezer.DeezerOAuth2" user_data_url = "http://api.deezer.com/user/me" expected_username = "foobar" diff --git a/social_core/tests/backends/test_digitalocean.py b/social_core/tests/backends/test_digitalocean.py index 67f69271b..c726bc13a 100644 --- a/social_core/tests/backends/test_digitalocean.py +++ b/social_core/tests/backends/test_digitalocean.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class DigitalOceanOAuthTest(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" diff --git a/social_core/tests/backends/test_discogs.py b/social_core/tests/backends/test_discogs.py index 394215d66..166a1b7d8 100644 --- a/social_core/tests/backends/test_discogs.py +++ b/social_core/tests/backends/test_discogs.py @@ -3,10 +3,10 @@ from httpretty import HTTPretty -from .oauth import OAuth1Test +from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test -class DiscsogsOAuth1Test(OAuth1Test): +class DiscsogsOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): _test_token = "lalala123boink" backend_path = "social_core.backends.discogs.DiscogsOAuth1" expected_username = "rodneyfool" diff --git a/social_core/tests/backends/test_disqus.py b/social_core/tests/backends/test_disqus.py index 5b6dfef29..ffcba5e66 100644 --- a/social_core/tests/backends/test_disqus.py +++ b/social_core/tests/backends/test_disqus.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class DisqusOAuth2Test(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" diff --git a/social_core/tests/backends/test_dribbble.py b/social_core/tests/backends/test_dribbble.py index e6f5be499..448ad258e 100644 --- a/social_core/tests/backends/test_dribbble.py +++ b/social_core/tests/backends/test_dribbble.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class DribbbleOAuth2Test(OAuth2Test): +class DribbbleOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.dribbble.DribbbleOAuth2" user_data_url = "https://api.dribbble.com/v1/user" expected_username = "foobar" diff --git a/social_core/tests/backends/test_drip.py b/social_core/tests/backends/test_drip.py index 117707d51..b49f485e1 100644 --- a/social_core/tests/backends/test_drip.py +++ b/social_core/tests/backends/test_drip.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class DripOAuthTest(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" diff --git a/social_core/tests/backends/test_dropbox.py b/social_core/tests/backends/test_dropbox.py index 65154aef1..82ba1ca20 100644 --- a/social_core/tests/backends/test_dropbox.py +++ b/social_core/tests/backends/test_dropbox.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class DropboxOAuth2Test(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 diff --git a/social_core/tests/backends/test_dummy.py b/social_core/tests/backends/test_dummy.py index 29bbdd673..cb16f5094 100644 --- a/social_core/tests/backends/test_dummy.py +++ b/social_core/tests/backends/test_dummy.py @@ -8,7 +8,7 @@ from ...backends.oauth import BaseOAuth2 from ...exceptions import AuthForbidden from ..models import User -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test class DummyOAuth2(BaseOAuth2): @@ -40,7 +40,7 @@ class Dummy2OAuth2(DummyOAuth2): GET_ALL_EXTRA_DATA = True -class DummyOAuth2Test(OAuth2Test): +class DummyOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.tests.backends.test_dummy.DummyOAuth2" user_data_url = "http://dummy.com/user" expected_username = "foobar" diff --git a/social_core/tests/backends/test_edmodo.py b/social_core/tests/backends/test_edmodo.py index b2ea1bd6d..6a6bef302 100644 --- a/social_core/tests/backends/test_edmodo.py +++ b/social_core/tests/backends/test_edmodo.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class EdmodoOAuth2Test(OAuth2Test): +class EdmodoOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.edmodo.EdmodoOAuth2" user_data_url = "https://api.edmodo.com/users/me" expected_username = "foobar12345" diff --git a/social_core/tests/backends/test_egi_checkin.py b/social_core/tests/backends/test_egi_checkin.py index bb5142083..3df87dca8 100644 --- a/social_core/tests/backends/test_egi_checkin.py +++ b/social_core/tests/backends/test_egi_checkin.py @@ -1,8 +1,10 @@ -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test from .test_open_id_connect import OpenIdConnectTestMixin -class EGICheckinOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): +class EGICheckinOpenIdConnectTest( + OpenIdConnectTestMixin, OAuth2Test, BaseAuthUrlTestMixin +): backend_path = "social_core.backends.egi_checkin.EGICheckinOpenIdConnect" issuer = "https://aai.egi.eu/auth/realms/egi" openid_config_body = """ diff --git a/social_core/tests/backends/test_einfracz.py b/social_core/tests/backends/test_einfracz.py index bd65a167a..1e68e42dc 100644 --- a/social_core/tests/backends/test_einfracz.py +++ b/social_core/tests/backends/test_einfracz.py @@ -1,8 +1,10 @@ -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test from .test_open_id_connect import OpenIdConnectTestMixin -class EInfraCZOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): +class EInfraCZOpenIdConnectTest( + OpenIdConnectTestMixin, OAuth2Test, BaseAuthUrlTestMixin +): backend_path = "social_core.backends.einfracz.EInfraCZOpenIdConnect" issuer = "https://login.e-infra.cz/oidc/" openid_config_body = """ diff --git a/social_core/tests/backends/test_elixir.py b/social_core/tests/backends/test_elixir.py index 2792eead7..09b875342 100644 --- a/social_core/tests/backends/test_elixir.py +++ b/social_core/tests/backends/test_elixir.py @@ -1,8 +1,8 @@ -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test from .test_open_id_connect import OpenIdConnectTestMixin -class ElixirOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): +class ElixirOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.elixir.ElixirOpenIdConnect" issuer = "https://login.elixir-czech.org/oidc/" openid_config_body = """ diff --git a/social_core/tests/backends/test_eventbrite.py b/social_core/tests/backends/test_eventbrite.py index 76ef5aa0d..2f9ef11e0 100644 --- a/social_core/tests/backends/test_eventbrite.py +++ b/social_core/tests/backends/test_eventbrite.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class EventbriteAuth2Test(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" diff --git a/social_core/tests/backends/test_evernote.py b/social_core/tests/backends/test_evernote.py index d61c82afa..65e048f02 100644 --- a/social_core/tests/backends/test_evernote.py +++ b/social_core/tests/backends/test_evernote.py @@ -3,10 +3,10 @@ from requests import HTTPError from ...exceptions import AuthCanceled -from .oauth import OAuth1Test +from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test -class EvernoteOAuth1Test(OAuth1Test): +class EvernoteOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.evernote.EvernoteOAuth" expected_username = "101010" access_token_body = urlencode( diff --git a/social_core/tests/backends/test_facebook.py b/social_core/tests/backends/test_facebook.py index 983166a2f..abdcabcd6 100644 --- a/social_core/tests/backends/test_facebook.py +++ b/social_core/tests/backends/test_facebook.py @@ -2,11 +2,11 @@ from ...backends.facebook import API_VERSION from ...exceptions import AuthCanceled, AuthUnknownError -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test from .test_open_id_connect import OpenIdConnectTestMixin -class FacebookOAuth2Test(OAuth2Test): +class FacebookOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.facebook.FacebookOAuth2" user_data_url = "https://graph.facebook.com/v{version}/me".format( version=API_VERSION diff --git a/social_core/tests/backends/test_fence.py b/social_core/tests/backends/test_fence.py index 83f136848..0d1610158 100644 --- a/social_core/tests/backends/test_fence.py +++ b/social_core/tests/backends/test_fence.py @@ -1,10 +1,10 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test from .test_open_id_connect import OpenIdConnectTestMixin -class FenceOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): +class FenceOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.fence.Fence" issuer = "https://nci-crdc.datacommons.io/" openid_config_body = json.dumps( diff --git a/social_core/tests/backends/test_fitbit.py b/social_core/tests/backends/test_fitbit.py index 3734d3f37..b76bd1c6a 100644 --- a/social_core/tests/backends/test_fitbit.py +++ b/social_core/tests/backends/test_fitbit.py @@ -1,10 +1,10 @@ import json from urllib.parse import urlencode -from .oauth import OAuth1Test +from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test -class FitbitOAuth1Test(OAuth1Test): +class FitbitOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.fitbit.FitbitOAuth1" expected_username = "foobar" access_token_body = urlencode( diff --git a/social_core/tests/backends/test_five_hundred_px.py b/social_core/tests/backends/test_five_hundred_px.py index 3d2100135..4cfdc9abf 100644 --- a/social_core/tests/backends/test_five_hundred_px.py +++ b/social_core/tests/backends/test_five_hundred_px.py @@ -1,10 +1,10 @@ import json from urllib.parse import urlencode -from .oauth import OAuth1Test +from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test -class FiveHundredPxOAuth1Test(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" diff --git a/social_core/tests/backends/test_flat.py b/social_core/tests/backends/test_flat.py index 5a3005683..8b3031cfc 100644 --- a/social_core/tests/backends/test_flat.py +++ b/social_core/tests/backends/test_flat.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class FlatOAuth2Test(OAuth2Test): +class FlatOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.flat.FlatOAuth2" user_data_url = "https://api.flat.io/v2/me" expected_username = "vincent" diff --git a/social_core/tests/backends/test_flickr.py b/social_core/tests/backends/test_flickr.py index 2c4409727..aef50fe1f 100644 --- a/social_core/tests/backends/test_flickr.py +++ b/social_core/tests/backends/test_flickr.py @@ -1,9 +1,9 @@ from urllib.parse import urlencode -from .oauth import OAuth1Test +from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test -class FlickrOAuth1Test(OAuth1Test): +class FlickrOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.flickr.FlickrOAuth" expected_username = "foobar" access_token_body = urlencode( diff --git a/social_core/tests/backends/test_foursquare.py b/social_core/tests/backends/test_foursquare.py index 64c2878fb..9773ed6db 100644 --- a/social_core/tests/backends/test_foursquare.py +++ b/social_core/tests/backends/test_foursquare.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class FoursquareOAuth2Test(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" diff --git a/social_core/tests/backends/test_gitea.py b/social_core/tests/backends/test_gitea.py index fb0c46cb0..0d2ebea15 100644 --- a/social_core/tests/backends/test_gitea.py +++ b/social_core/tests/backends/test_gitea.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class GiteaOAuth2Test(OAuth2Test): +class GiteaOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.gitea.GiteaOAuth2" user_data_url = "https://gitea.com/api/v1/user" expected_username = "foobar" @@ -38,7 +38,7 @@ def test_partial_pipeline(self): self.do_partial_pipeline() -class GiteaCustomDomainOAuth2Test(OAuth2Test): +class GiteaCustomDomainOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.gitea.GiteaOAuth2" user_data_url = "https://example.com/api/v1/user" expected_username = "foobar" diff --git a/social_core/tests/backends/test_github.py b/social_core/tests/backends/test_github.py index eac2d9cbc..0f7db643a 100644 --- a/social_core/tests/backends/test_github.py +++ b/social_core/tests/backends/test_github.py @@ -3,10 +3,10 @@ from httpretty import HTTPretty from ...exceptions import AuthFailed -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class GithubOAuth2Test(OAuth2Test): +class GithubOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.github.GithubOAuth2" user_data_url = "https://api.github.com/user" expected_username = "foobar" diff --git a/social_core/tests/backends/test_github_enterprise.py b/social_core/tests/backends/test_github_enterprise.py index 115be7873..727892bf3 100644 --- a/social_core/tests/backends/test_github_enterprise.py +++ b/social_core/tests/backends/test_github_enterprise.py @@ -3,10 +3,10 @@ from httpretty import HTTPretty from ...exceptions import AuthFailed -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class GithubEnterpriseOAuth2Test(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" diff --git a/social_core/tests/backends/test_gitlab.py b/social_core/tests/backends/test_gitlab.py index 97048191d..7fa6e819b 100644 --- a/social_core/tests/backends/test_gitlab.py +++ b/social_core/tests/backends/test_gitlab.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class GitLabOAuth2Test(OAuth2Test): +class GitLabOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.gitlab.GitLabOAuth2" user_data_url = "https://gitlab.com/api/v4/user" expected_username = "foobar" @@ -54,7 +54,7 @@ def test_partial_pipeline(self): self.do_partial_pipeline() -class GitLabCustomDomainOAuth2Test(OAuth2Test): +class GitLabCustomDomainOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.gitlab.GitLabOAuth2" user_data_url = "https://example.com/api/v4/user" expected_username = "foobar" diff --git a/social_core/tests/backends/test_globus.py b/social_core/tests/backends/test_globus.py index b5eecf221..65cd72d0f 100644 --- a/social_core/tests/backends/test_globus.py +++ b/social_core/tests/backends/test_globus.py @@ -1,10 +1,10 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test from .test_open_id_connect import OpenIdConnectTestMixin -class GlobusOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): +class GlobusOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.globus.GlobusOpenIdConnect" issuer = "https://auth.globus.org" openid_config_body = json.dumps( diff --git a/social_core/tests/backends/test_google.py b/social_core/tests/backends/test_google.py index c20021cbb..d9c3ff5be 100644 --- a/social_core/tests/backends/test_google.py +++ b/social_core/tests/backends/test_google.py @@ -5,11 +5,11 @@ from ...actions import do_disconnect from ..models import User -from .oauth import OAuth1Test, OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth1AuthUrlTestMixin, OAuth1Test, OAuth2Test from .test_open_id_connect import OpenIdConnectTestMixin -class GoogleOAuth2Test(OAuth2Test): +class GoogleOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.google.GoogleOAuth2" user_data_url = "https://www.googleapis.com/oauth2/v3/userinfo" expected_username = "foo" @@ -52,7 +52,7 @@ def test_with_unique_user_id(self): self.do_login() -class GoogleOAuth1Test(OAuth1Test): +class GoogleOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.google.GoogleOAuth" user_data_url = "https://www.googleapis.com/userinfo/email" expected_username = "foobar" diff --git a/social_core/tests/backends/test_grafana.py b/social_core/tests/backends/test_grafana.py index 36df09c4c..d26c24838 100644 --- a/social_core/tests/backends/test_grafana.py +++ b/social_core/tests/backends/test_grafana.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class GrafanaOAuth2Test(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( diff --git a/social_core/tests/backends/test_instagram.py b/social_core/tests/backends/test_instagram.py index eb4fd3b42..114ec56bb 100644 --- a/social_core/tests/backends/test_instagram.py +++ b/social_core/tests/backends/test_instagram.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class InstagramOAuth2Test(OAuth2Test): +class InstagramOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.instagram.InstagramOAuth2" user_data_url = "https://graph.instagram.com/me" expected_username = "foobar" diff --git a/social_core/tests/backends/test_itembase.py b/social_core/tests/backends/test_itembase.py index 1e4a7baf7..ccea591a0 100644 --- a/social_core/tests/backends/test_itembase.py +++ b/social_core/tests/backends/test_itembase.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class ItembaseOAuth2Test(OAuth2Test): +class ItembaseOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.itembase.ItembaseOAuth2" user_data_url = "https://users.itembase.com/v1/me" expected_username = "foobar" @@ -46,6 +46,6 @@ def test_partial_pipeline(self): self.do_partial_pipeline() -class ItembaseOAuth2SandboxTest(OAuth2Test): +class ItembaseOAuth2SandboxTest(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.itembase.ItembaseOAuth2Sandbox" user_data_url = "http://sandbox.users.itembase.io/v1/me" diff --git a/social_core/tests/backends/test_kakao.py b/social_core/tests/backends/test_kakao.py index ebb48f3ba..11b439df3 100644 --- a/social_core/tests/backends/test_kakao.py +++ b/social_core/tests/backends/test_kakao.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class KakaoOAuth2Test(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" diff --git a/social_core/tests/backends/test_keycloak.py b/social_core/tests/backends/test_keycloak.py index 2c88b37bb..9ef764f53 100644 --- a/social_core/tests/backends/test_keycloak.py +++ b/social_core/tests/backends/test_keycloak.py @@ -3,7 +3,7 @@ import jwt -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test _PRIVATE_KEY_HEADERLESS = """ MIIEowIBAAKCAQEAvyo2hx1L3ALHeUd/6xk/lIhTyZ/HJZ+Sss/ge6T6gPdES4Dw @@ -93,7 +93,7 @@ def _decode(token, key=_PUBLIC_KEY, algorithms=[_ALGORITHM], audience=_KEY): return jwt.decode(token, key=key, algorithms=algorithms, audience=audience) -class KeycloakOAuth2Test(OAuth2Test): +class KeycloakOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.keycloak.KeycloakOAuth2" expected_username = "john.doe" access_token_body = json.dumps( diff --git a/social_core/tests/backends/test_linkedin.py b/social_core/tests/backends/test_linkedin.py index b768d3172..c2d2b49ce 100644 --- a/social_core/tests/backends/test_linkedin.py +++ b/social_core/tests/backends/test_linkedin.py @@ -1,10 +1,12 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test from .test_open_id_connect import OpenIdConnectTestMixin -class LinkedinOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): +class LinkedinOpenIdConnectTest( + OpenIdConnectTestMixin, OAuth2Test, BaseAuthUrlTestMixin +): backend_path = "social_core.backends.linkedin.LinkedinOpenIdConnect" user_data_url = "https://api.linkedin.com/v2/userinfo" issuer = "https://www.linkedin.com" diff --git a/social_core/tests/backends/test_live.py b/social_core/tests/backends/test_live.py index 0edebee93..499ad3af9 100644 --- a/social_core/tests/backends/test_live.py +++ b/social_core/tests/backends/test_live.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class LiveOAuth2Test(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" diff --git a/social_core/tests/backends/test_lyft.py b/social_core/tests/backends/test_lyft.py index b71cf1cb8..d340fd2cc 100644 --- a/social_core/tests/backends/test_lyft.py +++ b/social_core/tests/backends/test_lyft.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class LyftOAuth2Test(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( diff --git a/social_core/tests/backends/test_mailru.py b/social_core/tests/backends/test_mailru.py index 5a946da6c..72f17ceb9 100644 --- a/social_core/tests/backends/test_mailru.py +++ b/social_core/tests/backends/test_mailru.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class MRGOAuth2Test(OAuth2Test): +class MRGOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.mailru.MRGOAuth2" user_data_url = "https://oauth.mail.ru/userinfo" expected_username = "FooBar" diff --git a/social_core/tests/backends/test_mapmyfitness.py b/social_core/tests/backends/test_mapmyfitness.py index 9c8aac794..e4e605b95 100644 --- a/social_core/tests/backends/test_mapmyfitness.py +++ b/social_core/tests/backends/test_mapmyfitness.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class MapMyFitnessOAuth2Test(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" diff --git a/social_core/tests/backends/test_microsoft.py b/social_core/tests/backends/test_microsoft.py index 66c988d69..03d68ca8b 100644 --- a/social_core/tests/backends/test_microsoft.py +++ b/social_core/tests/backends/test_microsoft.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class MicrosoftOAuth2Test(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" diff --git a/social_core/tests/backends/test_mineid.py b/social_core/tests/backends/test_mineid.py index c5dd68113..0a9c212b8 100644 --- a/social_core/tests/backends/test_mineid.py +++ b/social_core/tests/backends/test_mineid.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class MineIDOAuth2Test(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" @@ -20,3 +20,6 @@ def test_login(self): def test_partial_pipeline(self): self.do_partial_pipeline() + + def test_auth_url_parameters(self): + self.check_parameters_in_authorization_url("_AUTHORIZATION_URL") diff --git a/social_core/tests/backends/test_mixcloud.py b/social_core/tests/backends/test_mixcloud.py index 4eb0d65b6..c35859726 100644 --- a/social_core/tests/backends/test_mixcloud.py +++ b/social_core/tests/backends/test_mixcloud.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class MixcloudOAuth2Test(OAuth2Test): +class MixcloudOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.mixcloud.MixcloudOAuth2" user_data_url = "https://api.mixcloud.com/me/" expected_username = "foobar" diff --git a/social_core/tests/backends/test_musicbrainz.py b/social_core/tests/backends/test_musicbrainz.py index 4c3005df0..9804c6848 100644 --- a/social_core/tests/backends/test_musicbrainz.py +++ b/social_core/tests/backends/test_musicbrainz.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class MusicBrainzAuth2Test(OAuth2Test): +class MusicBrainzAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.musicbrainz.MusicBrainzOAuth2" user_data_url = "https://musicbrainz.org/oauth2/userinfo" expected_username = "foobar" diff --git a/social_core/tests/backends/test_nationbuilder.py b/social_core/tests/backends/test_nationbuilder.py index 39e6ad453..84fee342d 100644 --- a/social_core/tests/backends/test_nationbuilder.py +++ b/social_core/tests/backends/test_nationbuilder.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class NationBuilderOAuth2Test(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" diff --git a/social_core/tests/backends/test_naver.py b/social_core/tests/backends/test_naver.py index b69d8cea5..d3d420090 100644 --- a/social_core/tests/backends/test_naver.py +++ b/social_core/tests/backends/test_naver.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class NaverOAuth2Test(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" diff --git a/social_core/tests/backends/test_okta.py b/social_core/tests/backends/test_okta.py index 5885ecc7a..a78e99827 100644 --- a/social_core/tests/backends/test_okta.py +++ b/social_core/tests/backends/test_okta.py @@ -2,7 +2,7 @@ from httpretty import HTTPretty -from social_core.tests.backends.oauth import OAuth2Test +from social_core.tests.backends.oauth import BaseAuthUrlTestMixin, OAuth2Test from social_core.tests.backends.test_open_id_connect import OpenIdConnectTestMixin JWK_KEY = { @@ -28,7 +28,7 @@ JWK_PUBLIC_KEY = {key: value for key, value in JWK_KEY.items() if key != "d"} -class OktaOAuth2Test(OAuth2Test): +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" diff --git a/social_core/tests/backends/test_open_id_connect.py b/social_core/tests/backends/test_open_id_connect.py index b9e998923..f57db5967 100644 --- a/social_core/tests/backends/test_open_id_connect.py +++ b/social_core/tests/backends/test_open_id_connect.py @@ -13,7 +13,7 @@ from ...exceptions import AuthTokenError from ...utils import parse_qs -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test sys.path.insert(0, "..") @@ -227,7 +227,7 @@ def test_invalid_kid(self): ) -class BaseOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): +class BaseOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.open_id_connect.OpenIdConnectAuth" issuer = "https://example.com" openid_config_body = json.dumps( diff --git a/social_core/tests/backends/test_openstreetmap_oauth2.py b/social_core/tests/backends/test_openstreetmap_oauth2.py index 7ff69ad6c..8c2a1292f 100644 --- a/social_core/tests/backends/test_openstreetmap_oauth2.py +++ b/social_core/tests/backends/test_openstreetmap_oauth2.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class OpenStreetMapOAuth2Test(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" diff --git a/social_core/tests/backends/test_orbi.py b/social_core/tests/backends/test_orbi.py index fac57928d..ae51573b4 100644 --- a/social_core/tests/backends/test_orbi.py +++ b/social_core/tests/backends/test_orbi.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class OrbiOAuth2Test(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" diff --git a/social_core/tests/backends/test_orcid.py b/social_core/tests/backends/test_orcid.py index 93bb4124d..984ee28c3 100644 --- a/social_core/tests/backends/test_orcid.py +++ b/social_core/tests/backends/test_orcid.py @@ -2,10 +2,10 @@ from social_core.backends.orcid import ORCIDOAuth2 -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class ORCIDOAuth2Test(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" diff --git a/social_core/tests/backends/test_osso.py b/social_core/tests/backends/test_osso.py index f720b3643..659b44aa7 100644 --- a/social_core/tests/backends/test_osso.py +++ b/social_core/tests/backends/test_osso.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class OssoOAuth2Test(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" diff --git a/social_core/tests/backends/test_patreon.py b/social_core/tests/backends/test_patreon.py index 0a0240a12..ff5ef25f1 100644 --- a/social_core/tests/backends/test_patreon.py +++ b/social_core/tests/backends/test_patreon.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class PatreonOAuth2Test(OAuth2Test): +class PatreonOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.patreon.PatreonOAuth2" user_data_url = ( "https://www.patreon.com/api/oauth2/v2/identity?" diff --git a/social_core/tests/backends/test_paypal.py b/social_core/tests/backends/test_paypal.py index 52da164ef..e752c70cc 100644 --- a/social_core/tests/backends/test_paypal.py +++ b/social_core/tests/backends/test_paypal.py @@ -2,10 +2,10 @@ from social_core.backends.paypal import PayPalOAuth2 -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class PayPalOAuth2Test(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" diff --git a/social_core/tests/backends/test_phabricator.py b/social_core/tests/backends/test_phabricator.py index f694be542..aa87ecb11 100644 --- a/social_core/tests/backends/test_phabricator.py +++ b/social_core/tests/backends/test_phabricator.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class PhabricatorOAuth2Test(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" @@ -37,7 +37,7 @@ def test_partial_pipeline(self): self.do_partial_pipeline() -class PhabricatorCustomDomainOAuth2Test(OAuth2Test): +class PhabricatorCustomDomainOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.phabricator.PhabricatorOAuth2" user_data_url = "https://example.com/api/user.whoami" expected_username = "user" diff --git a/social_core/tests/backends/test_pinterest.py b/social_core/tests/backends/test_pinterest.py index f1b8cbf51..333eb0c83 100644 --- a/social_core/tests/backends/test_pinterest.py +++ b/social_core/tests/backends/test_pinterest.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class PinterestOAuth2Test(OAuth2Test): +class PinterestOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.pinterest.PinterestOAuth2" user_data_url = "https://api.pinterest.com/v1/me/" expected_username = "foobar" @@ -24,7 +24,7 @@ def test_partial_pipeline(self): self.do_partial_pipeline() -class PinterestOAuth2BrokenServerResponseTest(OAuth2Test): +class PinterestOAuth2BrokenServerResponseTest(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.pinterest.PinterestOAuth2" user_data_url = "https://api.pinterest.com/v1/me/" expected_username = "foobar" diff --git a/social_core/tests/backends/test_podio.py b/social_core/tests/backends/test_podio.py index a81a05d24..ec59fc9e2 100644 --- a/social_core/tests/backends/test_podio.py +++ b/social_core/tests/backends/test_podio.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class PodioOAuth2Test(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" diff --git a/social_core/tests/backends/test_qiita.py b/social_core/tests/backends/test_qiita.py index 3b3d7d992..10ce5235c 100644 --- a/social_core/tests/backends/test_qiita.py +++ b/social_core/tests/backends/test_qiita.py @@ -2,10 +2,10 @@ from social_core.exceptions import AuthException -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class QiitaOAuth2Test(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" diff --git a/social_core/tests/backends/test_quizlet.py b/social_core/tests/backends/test_quizlet.py index f93245983..4f0ad6045 100644 --- a/social_core/tests/backends/test_quizlet.py +++ b/social_core/tests/backends/test_quizlet.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class QuizletOAuth2Test(OAuth2Test): +class QuizletOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.quizlet.QuizletOAuth2" expected_username = "foo_bar" diff --git a/social_core/tests/backends/test_readability.py b/social_core/tests/backends/test_readability.py index cec83579a..84d643b43 100644 --- a/social_core/tests/backends/test_readability.py +++ b/social_core/tests/backends/test_readability.py @@ -1,10 +1,10 @@ import json from urllib.parse import urlencode -from .oauth import OAuth1Test +from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test -class ReadabilityOAuth1Test(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" diff --git a/social_core/tests/backends/test_reddit.py b/social_core/tests/backends/test_reddit.py index e66e49e47..6d3db88e7 100644 --- a/social_core/tests/backends/test_reddit.py +++ b/social_core/tests/backends/test_reddit.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class RedditOAuth2Test(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" diff --git a/social_core/tests/backends/test_scistarter.py b/social_core/tests/backends/test_scistarter.py index dcafe8716..1b4c153c9 100644 --- a/social_core/tests/backends/test_scistarter.py +++ b/social_core/tests/backends/test_scistarter.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class ScistarterOAuth2Test(OAuth2Test): +class ScistarterOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.scistarter.SciStarterOAuth2" user_data_url = "https://scistarter.com/api/user_info" expected_username = "foobar" diff --git a/social_core/tests/backends/test_seznam.py b/social_core/tests/backends/test_seznam.py index d71f1e2f5..936b13121 100644 --- a/social_core/tests/backends/test_seznam.py +++ b/social_core/tests/backends/test_seznam.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class SeznamOAuth2Test(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" diff --git a/social_core/tests/backends/test_simplelogin.py b/social_core/tests/backends/test_simplelogin.py index ba2b9cb02..15a8d9903 100644 --- a/social_core/tests/backends/test_simplelogin.py +++ b/social_core/tests/backends/test_simplelogin.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class SimpleLoginOAuth2Test(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"}) diff --git a/social_core/tests/backends/test_sketchfab.py b/social_core/tests/backends/test_sketchfab.py index 973794596..b57b5a7c0 100644 --- a/social_core/tests/backends/test_sketchfab.py +++ b/social_core/tests/backends/test_sketchfab.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class SketchfabOAuth2Test(OAuth2Test): +class SketchfabOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.sketchfab.SketchfabOAuth2" user_data_url = "https://sketchfab.com/v2/users/me" expected_username = "foobar" diff --git a/social_core/tests/backends/test_skyrock.py b/social_core/tests/backends/test_skyrock.py index bdeca0c57..e33bf0659 100644 --- a/social_core/tests/backends/test_skyrock.py +++ b/social_core/tests/backends/test_skyrock.py @@ -1,10 +1,10 @@ import json from urllib.parse import urlencode -from .oauth import OAuth1Test +from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test -class SkyrockOAuth1Test(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" diff --git a/social_core/tests/backends/test_slack.py b/social_core/tests/backends/test_slack.py index 24b377ff8..1f47350bf 100644 --- a/social_core/tests/backends/test_slack.py +++ b/social_core/tests/backends/test_slack.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class SlackOAuth2Test(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"}) diff --git a/social_core/tests/backends/test_soundcloud.py b/social_core/tests/backends/test_soundcloud.py index 974bbc9be..28e0fd8f6 100644 --- a/social_core/tests/backends/test_soundcloud.py +++ b/social_core/tests/backends/test_soundcloud.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class SoundcloudOAuth2Test(OAuth2Test): +class SoundcloudOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.soundcloud.SoundcloudOAuth2" user_data_url = "https://api.soundcloud.com/me.json" expected_username = "foobar" diff --git a/social_core/tests/backends/test_spotify.py b/social_core/tests/backends/test_spotify.py index ebdf969ac..aed0f821b 100644 --- a/social_core/tests/backends/test_spotify.py +++ b/social_core/tests/backends/test_spotify.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class SpotifyOAuth2Test(OAuth2Test): +class SpotifyOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.spotify.SpotifyOAuth2" user_data_url = "https://api.spotify.com/v1/me" expected_username = "foobar" diff --git a/social_core/tests/backends/test_stackoverflow.py b/social_core/tests/backends/test_stackoverflow.py index 5cbbc29b5..e017ca928 100644 --- a/social_core/tests/backends/test_stackoverflow.py +++ b/social_core/tests/backends/test_stackoverflow.py @@ -1,10 +1,10 @@ import json from urllib.parse import urlencode -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class StackoverflowOAuth2Test(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" diff --git a/social_core/tests/backends/test_stocktwits.py b/social_core/tests/backends/test_stocktwits.py index c6404d920..ee75aae82 100644 --- a/social_core/tests/backends/test_stocktwits.py +++ b/social_core/tests/backends/test_stocktwits.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class StocktwitsOAuth2Test(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" diff --git a/social_core/tests/backends/test_strava.py b/social_core/tests/backends/test_strava.py index a7a962d5b..748d735ce 100644 --- a/social_core/tests/backends/test_strava.py +++ b/social_core/tests/backends/test_strava.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class StravaOAuthTest(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" diff --git a/social_core/tests/backends/test_stripe.py b/social_core/tests/backends/test_stripe.py index 0a36846db..fae311ab4 100644 --- a/social_core/tests/backends/test_stripe.py +++ b/social_core/tests/backends/test_stripe.py @@ -3,10 +3,10 @@ import requests from httpretty import HTTPretty -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class StripeOAuth2Test(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( diff --git a/social_core/tests/backends/test_taobao.py b/social_core/tests/backends/test_taobao.py index 9216b340e..ae67b9aef 100644 --- a/social_core/tests/backends/test_taobao.py +++ b/social_core/tests/backends/test_taobao.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class TaobaoOAuth2Test(OAuth2Test): +class TaobaoOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.taobao.TAOBAOAuth" user_data_url = "https://eco.taobao.com/router/rest" expected_username = "foobar" diff --git a/social_core/tests/backends/test_thisismyjam.py b/social_core/tests/backends/test_thisismyjam.py index 453a8b78d..6acf3b5c8 100644 --- a/social_core/tests/backends/test_thisismyjam.py +++ b/social_core/tests/backends/test_thisismyjam.py @@ -1,10 +1,10 @@ import json from urllib.parse import urlencode -from .oauth import OAuth1Test +from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test -class ThisIsMyJameOAuth1Test(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" diff --git a/social_core/tests/backends/test_tripit.py b/social_core/tests/backends/test_tripit.py index 52ed159c1..97a2c1aad 100644 --- a/social_core/tests/backends/test_tripit.py +++ b/social_core/tests/backends/test_tripit.py @@ -1,10 +1,10 @@ import json from urllib.parse import urlencode -from .oauth import OAuth1Test +from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test -class TripitOAuth1Test(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" diff --git a/social_core/tests/backends/test_tumblr.py b/social_core/tests/backends/test_tumblr.py index 69dcd6c7d..f20e86ec7 100644 --- a/social_core/tests/backends/test_tumblr.py +++ b/social_core/tests/backends/test_tumblr.py @@ -1,10 +1,10 @@ import json from urllib.parse import urlencode -from .oauth import OAuth1Test +from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test -class TumblrOAuth1Test(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" diff --git a/social_core/tests/backends/test_twitch.py b/social_core/tests/backends/test_twitch.py index b36e8e1c7..6ec4c5dbc 100644 --- a/social_core/tests/backends/test_twitch.py +++ b/social_core/tests/backends/test_twitch.py @@ -1,6 +1,6 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test from .test_open_id_connect import OpenIdConnectTestMixin @@ -53,7 +53,7 @@ class TwitchOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): ) -class TwitchOAuth2Test(OAuth2Test): +class TwitchOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.twitch.TwitchOAuth2" user_data_url = "https://api.twitch.tv/helix/users" expected_username = "test_user1" diff --git a/social_core/tests/backends/test_twitter.py b/social_core/tests/backends/test_twitter.py index 5911ef5aa..04c4e245e 100644 --- a/social_core/tests/backends/test_twitter.py +++ b/social_core/tests/backends/test_twitter.py @@ -1,10 +1,10 @@ import json from urllib.parse import urlencode -from .oauth import OAuth1Test +from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test -class TwitterOAuth1Test(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" @@ -125,7 +125,7 @@ def test_partial_pipeline(self): self.do_partial_pipeline() -class TwitterOAuth1IncludeEmailTest(OAuth1Test): +class TwitterOAuth1IncludeEmailTest(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.twitter.TwitterOAuth" user_data_url = ( "https://api.twitter.com/1.1/account/" diff --git a/social_core/tests/backends/test_twitter_oauth2.py b/social_core/tests/backends/test_twitter_oauth2.py index 5ed76abfd..8a40ed29f 100644 --- a/social_core/tests/backends/test_twitter_oauth2.py +++ b/social_core/tests/backends/test_twitter_oauth2.py @@ -2,7 +2,12 @@ from social_core.exceptions import AuthException -from .oauth import OAuth2PkcePlainTest, OAuth2PkceS256Test, OAuth2Test +from .oauth import ( + BaseAuthUrlTestMixin, + OAuth2PkcePlainTest, + OAuth2PkceS256Test, + OAuth2Test, +) class TwitterOAuth2Mixin: @@ -125,7 +130,7 @@ def test_partial_pipeline(self): self.assertEqual(social.extra_data["public_metrics"]["listed_count"], 7) -class TwitterOAuth2TestMissingOptionalValue(OAuth2Test): +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( diff --git a/social_core/tests/backends/test_uber.py b/social_core/tests/backends/test_uber.py index 524cfdc3b..fa01de08c 100644 --- a/social_core/tests/backends/test_uber.py +++ b/social_core/tests/backends/test_uber.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class UberOAuth2Test(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" diff --git a/social_core/tests/backends/test_udata.py b/social_core/tests/backends/test_udata.py index 001750a29..a95d412b0 100644 --- a/social_core/tests/backends/test_udata.py +++ b/social_core/tests/backends/test_udata.py @@ -1,10 +1,10 @@ import json from urllib.parse import urlencode -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class DatagouvfrOAuth2Test(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" diff --git a/social_core/tests/backends/test_universe.py b/social_core/tests/backends/test_universe.py index 639775064..fe0c0bcd0 100644 --- a/social_core/tests/backends/test_universe.py +++ b/social_core/tests/backends/test_universe.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class UniverseAuth2Test(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" diff --git a/social_core/tests/backends/test_upwork.py b/social_core/tests/backends/test_upwork.py index 1e85514d7..ba7823de6 100644 --- a/social_core/tests/backends/test_upwork.py +++ b/social_core/tests/backends/test_upwork.py @@ -1,10 +1,10 @@ import json from urllib.parse import urlencode -from .oauth import OAuth1Test +from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test -class UpworkOAuth1Test(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" diff --git a/social_core/tests/backends/test_vault.py b/social_core/tests/backends/test_vault.py index 9c3c3e385..54da6af39 100644 --- a/social_core/tests/backends/test_vault.py +++ b/social_core/tests/backends/test_vault.py @@ -2,13 +2,13 @@ from httpretty import HTTPretty -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test from .test_open_id_connect import OpenIdConnectTestMixin ROOT_URL = "https://vault.example.net:8200/" -class VaultOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): +class VaultOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.vault.VaultOpenIdConnect" issuer = f"{ROOT_URL}v1/identity/oidc/provider/default" openid_config_body = json.dumps( diff --git a/social_core/tests/backends/test_vk.py b/social_core/tests/backends/test_vk.py index f7bd9b7dc..8e9c69011 100644 --- a/social_core/tests/backends/test_vk.py +++ b/social_core/tests/backends/test_vk.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class VKOAuth2Test(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" diff --git a/social_core/tests/backends/test_wlcg.py b/social_core/tests/backends/test_wlcg.py index 08fcfa8dc..a4cde9178 100644 --- a/social_core/tests/backends/test_wlcg.py +++ b/social_core/tests/backends/test_wlcg.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class WLCGOAuth2Test(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" diff --git a/social_core/tests/backends/test_wunderlist.py b/social_core/tests/backends/test_wunderlist.py index c10216112..70e810950 100644 --- a/social_core/tests/backends/test_wunderlist.py +++ b/social_core/tests/backends/test_wunderlist.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class WunderlistOAuth2Test(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" diff --git a/social_core/tests/backends/test_xing.py b/social_core/tests/backends/test_xing.py index bb7dbee77..dc2edb677 100644 --- a/social_core/tests/backends/test_xing.py +++ b/social_core/tests/backends/test_xing.py @@ -1,10 +1,10 @@ import json from urllib.parse import urlencode -from .oauth import OAuth1Test +from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test -class XingOAuth1Test(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" diff --git a/social_core/tests/backends/test_yahoo.py b/social_core/tests/backends/test_yahoo.py index 185a727f3..4ecaeb88c 100644 --- a/social_core/tests/backends/test_yahoo.py +++ b/social_core/tests/backends/test_yahoo.py @@ -4,10 +4,10 @@ import requests from httpretty import HTTPretty -from .oauth import OAuth1Test +from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test -class YahooOAuth1Test(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" diff --git a/social_core/tests/backends/test_yammer.py b/social_core/tests/backends/test_yammer.py index 481b133be..8bba567fc 100644 --- a/social_core/tests/backends/test_yammer.py +++ b/social_core/tests/backends/test_yammer.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class YammerOAuth2Test(OAuth2Test): +class YammerOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.yammer.YammerOAuth2" expected_username = "foobar" access_token_body = json.dumps( diff --git a/social_core/tests/backends/test_yandex.py b/social_core/tests/backends/test_yandex.py index 5a879916b..d251219dc 100644 --- a/social_core/tests/backends/test_yandex.py +++ b/social_core/tests/backends/test_yandex.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class YandexOAuth2Test(OAuth2Test): +class YandexOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.yandex.YandexOAuth2" user_data_url = "https://login.yandex.ru/info" expected_username = "foobar" @@ -26,7 +26,7 @@ def test_partial_pipeline(self): self.do_partial_pipeline() -class YandexOAuth2TestEmptyEmail(OAuth2Test): +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. diff --git a/social_core/tests/backends/test_zoom.py b/social_core/tests/backends/test_zoom.py index ece905337..b20f8b1d4 100644 --- a/social_core/tests/backends/test_zoom.py +++ b/social_core/tests/backends/test_zoom.py @@ -1,9 +1,9 @@ import json -from .oauth import OAuth2Test +from .oauth import BaseAuthUrlTestMixin, OAuth2Test -class ZoomOAuth2Test(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" diff --git a/social_core/tests/backends/test_zotero.py b/social_core/tests/backends/test_zotero.py index e6b5cd0e7..4d35f1c75 100644 --- a/social_core/tests/backends/test_zotero.py +++ b/social_core/tests/backends/test_zotero.py @@ -1,9 +1,9 @@ from urllib.parse import urlencode -from .oauth import OAuth1Test +from .oauth import OAuth1AuthUrlTestMixin, OAuth1Test -class ZoteroOAuth1Test(OAuth1Test): +class ZoteroOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.zotero.ZoteroOAuth" expected_username = "FooBar" access_token_body = urlencode( diff --git a/social_core/utils.py b/social_core/utils.py index 71ad8186d..4b6bb2202 100644 --- a/social_core/utils.py +++ b/social_core/utils.py @@ -6,7 +6,7 @@ import time import unicodedata from urllib.parse import parse_qs as battery_parse_qs -from urllib.parse import urlencode, urlparse, urlunparse +from urllib.parse import unquote, urlencode, urlparse, urlunparse import requests from requests.adapters import HTTPAdapter @@ -65,13 +65,15 @@ def user_agent(): return "social-auth-" + social_core.__version__ -def url_add_parameters(url, params): +def url_add_parameters(url, params, _unquote_query=False): """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 From 49fbc7aefc727b39cd63778eac009813ab0cfc9b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 20:38:26 +0000 Subject: [PATCH 088/152] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.19.0 → v3.19.1](https://github.com/asottile/pyupgrade/compare/v3.19.0...v3.19.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 32317b3e2..ba5025946 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: isort args: [--profile=black] - repo: https://github.com/asottile/pyupgrade - rev: v3.19.0 + rev: v3.19.1 hooks: - id: pyupgrade args: [--py38-plus] From 6221412da65d97e822af714318ccb81f91149ccb Mon Sep 17 00:00:00 2001 From: Chris Rose Date: Mon, 6 Jan 2025 03:36:52 -0800 Subject: [PATCH 089/152] fix: LinkedIn OAuth API updates (#950) * fix linkedin userdetails endpoint * Fix tests for the linkedin OAuth updates Co-authored-by: Mohamed Ahmed --- social_core/backends/linkedin.py | 47 +++++---------------- social_core/tests/backends/test_linkedin.py | 25 +++++------ 2 files changed, 22 insertions(+), 50 deletions(-) diff --git a/social_core/backends/linkedin.py b/social_core/backends/linkedin.py index f402a191a..d416ff61f 100644 --- a/social_core/backends/linkedin.py +++ b/social_core/backends/linkedin.py @@ -48,14 +48,14 @@ 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/me?projection=({projection})" + 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~))" ) ACCESS_TOKEN_METHOD = "POST" REDIRECT_STATE = False - DEFAULT_SCOPE = ["r_liteprofile"] + DEFAULT_SCOPE = ["email", "profile", "openid"] EXTRA_DATA = [ ("id", "id"), ("expires_in", "expires"), @@ -66,14 +66,7 @@ class LinkedinOAuth2(BaseOAuth2): ] def user_details_url(self): - # use set() since LinkedIn fails when values are duplicated - fields_selectors = list( - set(["id", "firstName", "lastName"] + self.setting("FIELD_SELECTORS", [])) - ) - # user sort to ease the tests URL mocking - fields_selectors.sort() - fields_selectors = ",".join(fields_selectors) - return self.USER_DETAILS_URL.format(projection=fields_selectors) + return self.USER_DETAILS_URL def user_emails_url(self): return self.USER_EMAILS_URL @@ -83,10 +76,10 @@ def user_data(self, access_token, *args, **kwargs): self.user_details_url(), headers=self.user_data_headers(access_token) ) - if "emailAddress" in set(self.setting("FIELD_SELECTORS", [])): + if "email" in set(self.setting("FIELD_SELECTORS", [])): emails = self.email_data(access_token, *args, **kwargs) if emails: - response["emailAddress"] = emails[0] + response["email"] = emails[0] return response @@ -96,38 +89,20 @@ def email_data(self, access_token, *args, **kwargs): ) email_addresses = [] for element in response.get("elements", []): - email_address = element.get("handle~", {}).get("emailAddress") + 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""" - - def get_localized_name(name): - """ - FirstName & Last Name object - { - 'localized': { - 'en_US': 'Smith' - }, - 'preferredLocale': { - 'country': 'US', - 'language': 'en' - } - } - :return the localizedName from the lastName object - """ - locale = "{}_{}".format( - name["preferredLocale"]["language"], name["preferredLocale"]["country"] - ) - return name["localized"].get(locale, "") - + response = self.user_data(access_token=response["access_token"]) fullname, first_name, last_name = self.get_user_names( - first_name=get_localized_name(response["firstName"]), - last_name=get_localized_name(response["lastName"]), + first_name=response["given_name"], + last_name=response["family_name"], ) - email = response.get("emailAddress", "") + email = response.get("email", "") return { + "id": response.get("sub", ""), "username": first_name + last_name, "fullname": fullname, "first_name": first_name, diff --git a/social_core/tests/backends/test_linkedin.py b/social_core/tests/backends/test_linkedin.py index c2d2b49ce..c757c3afb 100644 --- a/social_core/tests/backends/test_linkedin.py +++ b/social_core/tests/backends/test_linkedin.py @@ -44,26 +44,23 @@ def test_invalid_nonce(self): class BaseLinkedinTest: - user_data_url = ( - "https://api.linkedin.com/v2/me" "?projection=(firstName,id,lastName)" - ) + 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://docs.microsoft.com/en-us/linkedin/consumer/integrations/self - # -serve/sign-in-with-linkedin?context=linkedin/consumer/context#api-request + # https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self + # -serve/sign-in-with-linkedin-v2#response-body-schema user_data_body = json.dumps( { - "id": "1010101010", - "firstName": { - "localized": {"en_US": "Foo"}, - "preferredLocale": {"country": "US", "language": "en"}, - }, - "lastName": { - "localized": {"en_US": "Bar"}, - "preferredLocale": {"country": "US", "language": "en"}, - }, + "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/", # noqa: E501 + "locale": "en-US", + "email": "doe@email.com", + "email_verified": True, } ) From c9e4b6b8aaa0b5d96414da02689e45598909b082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 13:33:56 +0100 Subject: [PATCH 090/152] chore: use ruff for linting and code formatting Using default configuration for now, but will be extended soon. --- .github/workflows/flake8.yml | 30 --------------------- .github/workflows/pre-commit.yml | 29 ++++++++++++++++++++ .pre-commit-config.yaml | 22 ++++----------- social_core/backends/keycloak.py | 4 +-- social_core/backends/loginradius.py | 2 +- social_core/backends/orcid.py | 4 +-- social_core/backends/scistarter.py | 2 +- social_core/backends/untappd.py | 2 +- social_core/backends/weibo.py | 1 + social_core/backends/weixin.py | 1 + social_core/pipeline/partial.py | 2 +- social_core/tests/backends/oauth.py | 1 - social_core/tests/backends/test_keycloak.py | 8 ++---- 13 files changed, 45 insertions(+), 63 deletions(-) delete mode 100644 .github/workflows/flake8.yml create mode 100644 .github/workflows/pre-commit.yml diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml deleted file mode 100644 index b70c68ba1..000000000 --- a/.github/workflows/flake8.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Flake8 - -on: - push: - pull_request: - -jobs: - flake8: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: 3.x - cache: pip - cache-dependency-path: requirements*.txt - - - name: Install pre-commit - run: | - python -m pip install --upgrade pip wheel - pip install -r requirements-dev.txt - - - name: Run flake8 - run: | - echo "::add-matcher::.github/matchers/flake8.json" - pre-commit run flake8 --all-files - echo "::remove-matcher owner=flake8::" diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 000000000..504e4008a --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,29 @@ +name: pre-commit check + +on: + push: + pull_request: + +jobs: + pre-commit: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - uses: astral-sh/setup-uv@v5 + + - name: pre-commit (uvx) + run: uvx pre-commit run --all + env: + RUFF_OUTPUT_FORMAT: github diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ba5025946..bc4fb63b6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,24 +9,12 @@ repos: - id: debug-statements - id: mixed-line-ending args: [--fix=lf] -- repo: https://github.com/pycqa/isort - rev: 5.13.2 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.6 hooks: - - id: isort - args: [--profile=black] -- repo: https://github.com/asottile/pyupgrade - rev: v3.19.1 - hooks: - - id: pyupgrade - args: [--py38-plus] -- repo: https://github.com/psf/black - rev: 24.10.0 - hooks: - - id: black -- repo: https://github.com/PyCQA/flake8 - rev: 7.1.1 - hooks: - - id: flake8 + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format - repo: meta hooks: - id: check-hooks-apply diff --git a/social_core/backends/keycloak.py b/social_core/backends/keycloak.py index 494ddabdb..8b87b5b3c 100644 --- a/social_core/backends/keycloak.py +++ b/social_core/backends/keycloak.py @@ -121,9 +121,7 @@ def public_key(self): ] ) - def user_data( - self, access_token, *args, **kwargs - ): # pylint: disable=unused-argument + 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 diff --git a/social_core/backends/loginradius.py b/social_core/backends/loginradius.py index b7e5b3b12..fe9f0240a 100644 --- a/social_core/backends/loginradius.py +++ b/social_core/backends/loginradius.py @@ -37,7 +37,7 @@ def request_access_token(self, *args, **kwargs): return self.get_json( params={"token": self.data.get("token"), "secret": self.setting("SECRET")}, *args, - **kwargs + **kwargs, ) def user_data(self, access_token, *args, **kwargs): diff --git a/social_core/backends/orcid.py b/social_core/backends/orcid.py index 8717fc003..f1f35eae8 100644 --- a/social_core/backends/orcid.py +++ b/social_core/backends/orcid.py @@ -1,6 +1,6 @@ """ - ORCID OAuth2 Application backend, docs at: - https://python-social-auth.readthedocs.io/en/latest/backends/orcid.html +ORCID OAuth2 Application backend, docs at: +https://python-social-auth.readthedocs.io/en/latest/backends/orcid.html """ from .oauth import BaseOAuth2 diff --git a/social_core/backends/scistarter.py b/social_core/backends/scistarter.py index 8a4990d69..c6b91ac4a 100644 --- a/social_core/backends/scistarter.py +++ b/social_core/backends/scistarter.py @@ -1,4 +1,4 @@ -""" SciStarter OAuth2 Auth """ +"""SciStarter OAuth2 Auth""" from .oauth import BaseOAuth2 diff --git a/social_core/backends/untappd.py b/social_core/backends/untappd.py index 924a9d89e..0057a4a73 100644 --- a/social_core/backends/untappd.py +++ b/social_core/backends/untappd.py @@ -76,7 +76,7 @@ def auth_complete(self, *args, **kwargs): response["response"]["access_token"], response=response["response"], *args, - **kwargs + **kwargs, ) def get_user_details(self, response): diff --git a/social_core/backends/weibo.py b/social_core/backends/weibo.py index b69125d5e..eb0cc5a6a 100644 --- a/social_core/backends/weibo.py +++ b/social_core/backends/weibo.py @@ -3,6 +3,7 @@ Weibo OAuth2 backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/weibo.html """ + from .oauth import BaseOAuth2 diff --git a/social_core/backends/weixin.py b/social_core/backends/weixin.py index 2aec528e2..b549b436b 100644 --- a/social_core/backends/weixin.py +++ b/social_core/backends/weixin.py @@ -2,6 +2,7 @@ """ Weixin OAuth2 backend """ + from urllib.parse import urlencode from requests import HTTPError diff --git a/social_core/pipeline/partial.py b/social_core/pipeline/partial.py index 8925ec699..661e63ae0 100644 --- a/social_core/pipeline/partial.py +++ b/social_core/pipeline/partial.py @@ -36,7 +36,7 @@ def wrapper(strategy, backend, pipeline_index, *args, **kwargs): pipeline_index=pipeline_index, current_partial=current_partial, *args, - **kwargs + **kwargs, ) or {} ) diff --git a/social_core/tests/backends/oauth.py b/social_core/tests/backends/oauth.py index f59a6f6ef..c4707f426 100644 --- a/social_core/tests/backends/oauth.py +++ b/social_core/tests/backends/oauth.py @@ -180,7 +180,6 @@ def do_login(self): class BaseAuthUrlTestMixin: - def check_parameters_in_authorization_url(self, auth_url_key="AUTHORIZATION_URL"): """ Check the parameters in authorization url diff --git a/social_core/tests/backends/test_keycloak.py b/social_core/tests/backends/test_keycloak.py index 9ef764f53..1d09d01d9 100644 --- a/social_core/tests/backends/test_keycloak.py +++ b/social_core/tests/backends/test_keycloak.py @@ -37,9 +37,7 @@ -----BEGIN RSA PRIVATE KEY----- {_PRIVATE_KEY_HEADERLESS} -----END RSA PRIVATE KEY----- -""".format( - _PRIVATE_KEY_HEADERLESS=_PRIVATE_KEY_HEADERLESS -) +""".format(_PRIVATE_KEY_HEADERLESS=_PRIVATE_KEY_HEADERLESS) _PUBLIC_KEY_HEADERLESS = """ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvyo2hx1L3ALHeUd/6xk/ @@ -55,9 +53,7 @@ -----BEGIN PUBLIC KEY----- {_PUBLIC_KEY_HEADERLESS} -----END PUBLIC KEY----- -""".format( - _PUBLIC_KEY_HEADERLESS=_PUBLIC_KEY_HEADERLESS -) +""".format(_PUBLIC_KEY_HEADERLESS=_PUBLIC_KEY_HEADERLESS) _KEY = "example" _SECRET = "1234abcd-1234-abcd-1234-abcd1234adcd" From 089e2627945bdc32df4e61efd22923b57193d9e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 13:43:38 +0100 Subject: [PATCH 091/152] Modify the dummy tests to be timezone-safe (#953) The dummy tests were failing on my non-UTC development machine, because the timestamps were generated with mktime(), which uses the locale settings from the host. Changing this to use the timezone-aware `now` sorted it. Co-authored-by: Chris Rose --- social_core/storage.py | 6 +++--- social_core/tests/backends/test_dummy.py | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/social_core/storage.py b/social_core/storage.py index ee3019ff8..392b98003 100644 --- a/social_core/storage.py +++ b/social_core/storage.py @@ -2,7 +2,6 @@ import base64 import re -import time import uuid import warnings from datetime import datetime, timedelta, timezone @@ -71,9 +70,10 @@ def expiration_timedelta(self): now = datetime.now(timezone.utc) # Detect if expires is a timestamp - if expires > time.mktime(now.timetuple()): + if expires > now.timestamp(): # expires is a datetime, return the remaining difference - return datetime.fromtimestamp(expires, tz=timezone.utc) - now + expiry_time = datetime.fromtimestamp(expires, tz=timezone.utc) + return expiry_time - now else: # expires is the time to live seconds since creation, # check against auth_time if present, otherwise return diff --git a/social_core/tests/backends/test_dummy.py b/social_core/tests/backends/test_dummy.py index cb16f5094..49e59923a 100644 --- a/social_core/tests/backends/test_dummy.py +++ b/social_core/tests/backends/test_dummy.py @@ -1,6 +1,5 @@ import datetime import json -import time from httpretty import HTTPretty @@ -121,9 +120,7 @@ class ExpirationTimeTest(DummyOAuth2Test): "first_name": "Foo", "last_name": "Bar", "email": "foo@bar.com", - "expires": time.mktime( - (datetime.datetime.now(datetime.timezone.utc) + DELTA).timetuple() - ), + "expires": (datetime.datetime.now() + DELTA).timestamp(), } ) From ec82b56a7c85504c613593346d664cb3794d9dbc Mon Sep 17 00:00:00 2001 From: Chris Rose Date: Mon, 6 Jan 2025 04:49:40 -0800 Subject: [PATCH 092/152] feat: Switch to pyproject.toml for project metadata (#951) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Switch to pyproject.toml for project metadata - import the metadata using pdm import - use importlib metadata for in-code version display * Install the dev dependencies with tox's own mechanism * Fix the httpretty UnmockedError This is due to https://github.com/gabrielfalcao/HTTPretty/issues/484 * Remove flake8 configuration Co-authored-by: Michal Čihař --- pyproject.toml | 81 ++++++++++++++++++++++ setup.cfg | 4 -- setup.py | 105 ----------------------------- social_core/__init__.py | 4 +- social_core/tests/requirements.txt | 4 -- tox.ini | 3 +- 6 files changed, 85 insertions(+), 116 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 social_core/tests/requirements.txt diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..2d237e71c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,81 @@ +[project] +name = "social-auth-core" +version = "4.5.4" +description = "Python social authentication made simple." +authors = [ + {name = "Matias Aguirre", email = "matiasaguirre@gmail.com"}, +] +dependencies = [ + "PyJWT>=2.7.0", + "cryptography>=1.4", + "defusedxml>=0.5.0", + "oauthlib>=1.0.3", + "python3-openid>=3.0.10", + "requests-oauthlib>=0.6.1", + "requests>=2.9.1", +] +requires-python = ">=3.8" +readme = "README.md" +license = {text = "BSD"} +keywords = ["openid", "oauth", "saml", "social auth"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Topic :: Internet", +] + +[tool.setuptools] +packages = ["social_core"] + +[project.urls] +Homepage = "https://github.com/python-social-auth/social-core" + +[project.optional-dependencies] +saml = [ + "python3-saml>=1.5.0", +] +azuread = [ + "cryptography>=2.1.1", +] +all = [ + "social-auth-core[saml]", + "social-auth-core[azuread]", +] +allpy3 = [ + "social-auth-core[all]", +] +# This is present until pip implements supports for PEP 735 +# see https://github.com/pypa/pip/issues/12963 +dev = [ + "flake8>=5.0.4", + "pytest>=4.5", + "httpretty~=1.1.0", + "coverage>=3.6", + "pytest-cov>=2.7.1", + # pinned because of https://github.com/gabrielfalcao/HTTPretty/issues/484 + "urllib3~=2.2.0", +] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[dependency-groups] +dev = [ + "flake8>=5.0.4", + "pytest>=4.5", + "httpretty~=1.1.0", + "coverage>=3.6", + "pytest-cov>=2.7.1", + # pinned because of https://github.com/gabrielfalcao/HTTPretty/issues/484 + "urllib3~=2.2.0", +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 8306b16dc..000000000 --- a/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-line-length = 120 -# Ignore some well known paths -exclude = .venv,.tox,dist,doc,build,*.egg,db/env.py,db/versions/*.py,site diff --git a/setup.py b/setup.py deleted file mode 100644 index df062bc1c..000000000 --- a/setup.py +++ /dev/null @@ -1,105 +0,0 @@ -import re -from os.path import dirname, join - -from setuptools import setup - -VERSION_RE = re.compile(r"__version__ = \"([\d\.]+)\"") - -LONG_DESCRIPTION = """ -Python Social Auth is an easy to setup social authentication/registration -mechanism with support for several frameworks and auth providers. - -It implements a common interface to define new authentication -providers from third parties. And to bring support for more frameworks -and ORMs. -""" - - -def long_description(): - try: - return open(join(dirname(__file__), "README.md")).read() - except OSError: - return None - - -def read_version(): - with open("social_core/__init__.py") as file: - version_line = [ - line for line in file.readlines() if line.startswith("__version__") - ][0] - return VERSION_RE.match(version_line).groups()[0] - - -def read_requirements(filename): - with open(filename) as file: - return [line for line in file.readlines() if not line.startswith("-")] - - -def read_tests_requirements(filename): - return read_requirements(f"social_core/tests/{filename}") - - -requirements = read_requirements("requirements-base.txt") -requirements_saml = read_requirements("requirements-saml.txt") -requirements_azuread = read_requirements("requirements-azuread.txt") - -tests_requirements = read_tests_requirements("requirements.txt") - -requirements_all = requirements_saml + requirements_azuread - -tests_requirements = tests_requirements + requirements_all - -setup( - name="social-auth-core", - version=read_version(), - author="Matias Aguirre", - author_email="matiasaguirre@gmail.com", - description="Python social authentication made simple.", - license="BSD", - keywords="openid, oauth, saml, social auth", - url="https://github.com/python-social-auth/social-core", - packages=[ - "social_core", - "social_core.backends", - "social_core.pipeline", - "social_core.tests", - "social_core.tests.actions", - "social_core.tests.backends", - "social_core.tests.backends.data", - ], - long_description=long_description() or LONG_DESCRIPTION, - long_description_content_type="text/markdown", - install_requires=requirements, - python_requires=">=3.8", - extras_require={ - "saml": [requirements_saml], - "azuread": [requirements_azuread], - "all": [requirements_all], - # Kept for compatibility - "allpy3": [requirements_all], - }, - classifiers=[ - "Development Status :: 4 - Beta", - "Topic :: Internet", - "License :: OSI Approved :: BSD License", - "Intended Audience :: Developers", - "Environment :: Web Environment", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - ], - package_data={ - "social_core/tests": [ - "social_core/tests/*.txt", - "social_core/tests/testkey.pem", - ] - }, - include_package_data=True, - tests_require=tests_requirements, - test_suite="social_core.tests", - zip_safe=False, -) diff --git a/social_core/__init__.py b/social_core/__init__.py index bb160d035..fb59b6839 100644 --- a/social_core/__init__.py +++ b/social_core/__init__.py @@ -1 +1,3 @@ -__version__ = "4.5.4" +import importlib.metadata + +__version__ = importlib.metadata.version("social-auth-core") diff --git a/social_core/tests/requirements.txt b/social_core/tests/requirements.txt deleted file mode 100644 index c21a1b7e5..000000000 --- a/social_core/tests/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -pytest>=4.5 -httpretty>=0.9.6 -coverage>=3.6 -pytest-cov>=2.7.1 diff --git a/tox.ini b/tox.ini index 58a4cc800..40bfa6e0e 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,6 @@ envlist = py38,py39,py310,py311,py312 [testenv] passenv = * deps = - py{38,39,310,311,312}: -rsocial_core/tests/requirements.txt + py{38,39,310,311,312}: -e .[dev,all] commands = - py{38,39,310,311,312}: pip install -e .[all] pytest {posargs:-v --cov=social_core} From 8fc823ba87e65020d398986fdf042998881a558b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 13:57:56 +0100 Subject: [PATCH 093/152] feat: modernize release process - use uv to build and publish packages - use trusted publishing instead for stored token - build and lint packages on every commit, not only on release time --- .github/workflows/release.yml | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 982bedbd7..7f4f862fa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,26 +3,20 @@ name: Release on: release: types: [published] + push: + pull_request: jobs: release: runs-on: ubuntu-latest + permissions: + id-token: write steps: - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.9' - cache: pip - cache-dependency-path: requirements*.txt - - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - pip install wheel twine + - uses: astral-sh/setup-uv@v5 - name: Verify tag is documented + if: github.event_name == 'release' run: | CURRENT_TAG=${GITHUB_REF#refs/tags/} CURRENT_VERSION=$(head -n1 social_core/__init__.py | awk '{print $3}' | sed 's/[^0-9\.]//g') @@ -34,9 +28,10 @@ jobs: fi - name: Build dist - run: python setup.py sdist bdist_wheel --python-tag py3 + run: uv build - name: Archive dist + if: github.event_name == 'release' uses: actions/upload-artifact@v4 with: name: dist @@ -45,10 +40,11 @@ jobs: dist/*.whl - name: Verify long description rendering - run: twine check dist/* + run: uvx twine check dist/* - name: Publish env: - PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} - run: | - twine upload --non-interactive -u __token__ -p "${PYPI_API_TOKEN}" dist/* + # TODO: remove once trusted publishing is configured + UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + if: github.event_name == 'release' && github.repository == 'python-social-auth/social-core' + run: uv publish From 10db9cced40c08663093c80b266ed0d6d1c3886a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 14:41:13 +0100 Subject: [PATCH 094/152] chore: drop support for Python 3.8 Fixes #946 --- .github/workflows/test.yml | 3 +-- pyproject.toml | 3 +-- tox.ini | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8bb7fb9aa..af51cff60 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,11 +4,10 @@ on: [push, pull_request] jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: python-version: - - '3.8' - '3.9' - '3.10' - '3.11' diff --git a/pyproject.toml b/pyproject.toml index 2d237e71c..364ee6282 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ "requests-oauthlib>=0.6.1", "requests>=2.9.1", ] -requires-python = ">=3.8" +requires-python = ">=3.9" readme = "README.md" license = {text = "BSD"} keywords = ["openid", "oauth", "saml", "social auth"] @@ -28,7 +28,6 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Topic :: Internet", ] diff --git a/tox.ini b/tox.ini index 40bfa6e0e..ce0f60ecf 100644 --- a/tox.ini +++ b/tox.ini @@ -4,11 +4,11 @@ # and then run "tox" from this directory. [tox] -envlist = py38,py39,py310,py311,py312 +envlist = py39,py310,py311,py312 [testenv] passenv = * deps = - py{38,39,310,311,312}: -e .[dev,all] + py{39,310,311,312}: -e .[dev,all] commands = pytest {posargs:-v --cov=social_core} From 28e9f739f9229462145bc78bee4afa75fa9fda63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 14:42:57 +0100 Subject: [PATCH 095/152] feat: add support for Python 3.13 --- .github/workflows/test.yml | 2 ++ pyproject.toml | 1 + tox.ini | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index af51cff60..d85563bd8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,12 +6,14 @@ jobs: test: runs-on: ubuntu-22.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 diff --git a/pyproject.toml b/pyproject.toml index 364ee6282..1045e3863 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ classifiers = [ "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", "Topic :: Internet", ] diff --git a/tox.ini b/tox.ini index ce0f60ecf..37311e7dc 100644 --- a/tox.ini +++ b/tox.ini @@ -4,11 +4,11 @@ # and then run "tox" from this directory. [tox] -envlist = py39,py310,py311,py312 +envlist = py39,py310,py311,py312,py313 [testenv] passenv = * deps = - py{39,310,311,312}: -e .[dev,all] + py{39,310,311,312,313}: -e .[dev,all] commands = pytest {posargs:-v --cov=social_core} From 90ebc0a450325cd8b029db8cc1173c1182ecb0fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 15:18:40 +0100 Subject: [PATCH 096/152] chore: remove not needed else statement --- social_core/backends/evernote.py | 3 +-- social_core/backends/facebook.py | 3 +-- social_core/backends/jawbone.py | 3 +-- social_core/backends/oauth.py | 9 ++++----- social_core/backends/open_id.py | 6 +++--- social_core/backends/twitter.py | 3 +-- social_core/backends/vk.py | 3 +-- social_core/backends/weixin.py | 6 ++---- social_core/pipeline/social_auth.py | 2 +- social_core/utils.py | 7 +++---- 10 files changed, 18 insertions(+), 27 deletions(-) diff --git a/social_core/backends/evernote.py b/social_core/backends/evernote.py index 7147e2dcc..10e0b13f7 100644 --- a/social_core/backends/evernote.py +++ b/social_core/backends/evernote.py @@ -53,8 +53,7 @@ def access_token(self, token): # Evernote returns a 401 error when AuthCanceled if err.response.status_code == 401: raise AuthCanceled(self, response=err.response) - else: - raise + raise def extra_data(self, user, uid, response, details=None, *args, **kwargs): data = super().extra_data(user, uid, response, details, *args, **kwargs) diff --git a/social_core/backends/facebook.py b/social_core/backends/facebook.py index c495b1d18..69bc53b96 100644 --- a/social_core/backends/facebook.py +++ b/social_core/backends/facebook.py @@ -204,8 +204,7 @@ def auth_complete(self, *args, **kwargs): if access_token is None: if self.data.get("error") == "access_denied": raise AuthCanceled(self) - else: - raise AuthException(self) + raise AuthException(self) return self.do_auth(access_token, response, *args, **kwargs) def auth_html(self): diff --git a/social_core/backends/jawbone.py b/social_core/backends/jawbone.py index ca77f42dd..89bdca05d 100644 --- a/social_core/backends/jawbone.py +++ b/social_core/backends/jawbone.py @@ -47,8 +47,7 @@ def process_error(self, data): if error: if error == "access_denied": raise AuthCanceled(self) - else: - raise AuthUnknownError(self, f"Jawbone error was {error}") + raise AuthUnknownError(self, f"Jawbone error was {error}") return super().process_error(data) def auth_complete_params(self, state=None): diff --git a/social_core/backends/oauth.py b/social_core/backends/oauth.py index 4d724ef9c..4668a373e 100644 --- a/social_core/backends/oauth.py +++ b/social_core/backends/oauth.py @@ -97,12 +97,11 @@ def validate_state(self): request_state = self.get_request_state() if not request_state: raise AuthMissingParameter(self, "state") - elif not state: + if not state: raise AuthStateMissing(self, "state") - elif not constant_time_compare(request_state, state): + if not constant_time_compare(request_state, state): raise AuthStateForbidden(self) - else: - return state + return state def get_redirect_uri(self, state=None): """Build redirect with redirect_state parameter.""" @@ -401,7 +400,7 @@ def process_error(self, data): 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"]) - elif "denied" in data: + if "denied" in data: raise AuthCanceled(self, data["denied"]) @handle_http_errors diff --git a/social_core/backends/open_id.py b/social_core/backends/open_id.py index a2533d4b8..064d37e3a 100644 --- a/social_core/backends/open_id.py +++ b/social_core/backends/open_id.py @@ -183,11 +183,11 @@ def auth_complete(self, *args, **kwargs): def process_error(self, data): if not data: raise AuthException(self, "OpenID relying party endpoint") - elif data.status == FAILURE: + if data.status == FAILURE: raise AuthFailed(self, data.message) - elif data.status == CANCEL: + if data.status == CANCEL: raise AuthCanceled(self) - elif data.status != SUCCESS: + if data.status != SUCCESS: raise AuthUnknownError(self, data.status) def setup_request(self, params=None): diff --git a/social_core/backends/twitter.py b/social_core/backends/twitter.py index 669c89f74..661608597 100644 --- a/social_core/backends/twitter.py +++ b/social_core/backends/twitter.py @@ -22,8 +22,7 @@ class TwitterOAuth(BaseOAuth1): def process_error(self, data): if "denied" in data: raise AuthCanceled(self) - else: - super().process_error(data) + super().process_error(data) def get_user_details(self, response): """Return user details from Twitter account""" diff --git a/social_core/backends/vk.py b/social_core/backends/vk.py index ff6ca4240..8eec688cb 100644 --- a/social_core/backends/vk.py +++ b/social_core/backends/vk.py @@ -123,8 +123,7 @@ def user_data(self, access_token, *args, **kwargs): msg = error.get("error_msg", "Unknown error") if error.get("error_code") == 5: raise AuthTokenRevoked(self, msg) - else: - raise AuthException(self, msg) + raise AuthException(self, msg) if data: data = data.get("response")[0] diff --git a/social_core/backends/weixin.py b/social_core/backends/weixin.py index b549b436b..e5873503c 100644 --- a/social_core/backends/weixin.py +++ b/social_core/backends/weixin.py @@ -94,8 +94,7 @@ def auth_complete(self, *args, **kwargs): except HTTPError as err: if err.response.status_code == 400: raise AuthCanceled(self, response=err.response) - else: - raise + raise except KeyError: raise AuthUnknownError(self) if "errcode" in response: @@ -166,8 +165,7 @@ def auth_complete(self, *args, **kwargs): except HTTPError as err: if err.response.status_code == 400: raise AuthCanceled(self) - else: - raise + raise except KeyError: raise AuthUnknownError(self) diff --git a/social_core/pipeline/social_auth.py b/social_core/pipeline/social_auth.py index 1c5940e75..247163371 100644 --- a/social_core/pipeline/social_auth.py +++ b/social_core/pipeline/social_auth.py @@ -20,7 +20,7 @@ def social_user(backend, uid, user=None, *args, **kwargs): if social: if user and social.user != user: raise AuthAlreadyAssociated(backend) - elif not user: + if not user: user = social.user return { "social": social, diff --git a/social_core/utils.py b/social_core/utils.py index 4b6bb2202..b89e0071e 100644 --- a/social_core/utils.py +++ b/social_core/utils.py @@ -256,12 +256,11 @@ def wrapper(*args, **kwargs): except requests.HTTPError as err: if err.response.status_code == 400: raise AuthCanceled(args[0], response=err.response) - elif err.response.status_code == 401: + if err.response.status_code == 401: raise AuthForbidden(args[0]) - elif err.response.status_code == 503: + if err.response.status_code == 503: raise AuthUnreachableProvider(args[0]) - else: - raise + raise return wrapper From e3332e8529819b0e3c967cc2d4863c31af12b76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 15:21:01 +0100 Subject: [PATCH 097/152] chore: use startswith with tuple This allows to join multiple lookups. --- social_core/tests/strategy.py | 2 +- social_core/utils.py | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/social_core/tests/strategy.py b/social_core/tests/strategy.py index 4ee4543df..089720153 100644 --- a/social_core/tests/strategy.py +++ b/social_core/tests/strategy.py @@ -84,7 +84,7 @@ def session_pop(self, name): def build_absolute_uri(self, path=None): """Build absolute URI with given (optional) path""" path = path or "" - if path.startswith("http://") or path.startswith("https://"): + if path.startswith(("http://", "https://")): return path return TEST_URI + path diff --git a/social_core/utils.py b/social_core/utils.py index b89e0071e..c5eec885b 100644 --- a/social_core/utils.py +++ b/social_core/utils.py @@ -214,7 +214,7 @@ def partial_pipeline_data(backend, user=None, partial_token=None, *args, **kwarg def build_absolute_uri(host_url, path=None): """Build absolute URI with given (optional) path""" path = path or "" - if path.startswith("http://") or path.startswith("https://"): + if path.startswith(("http://", "https://")): return path if host_url.endswith("/") and path.startswith("/"): path = path[1:] @@ -231,11 +231,7 @@ def constant_time_compare(val1, val2): def is_url(value): - return value and ( - value.startswith("http://") - or value.startswith("https://") - or value.startswith("/") - ) + return value and (value.startswith(("http://", "https://", "/"))) def setting_url(backend, *names): From f08c75fa81de330d6521aedef4013a2b8608ad77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 15:23:42 +0100 Subject: [PATCH 098/152] chore: remove not needed else after return --- social_core/backends/apple.py | 3 +-- social_core/backends/base.py | 3 +-- social_core/backends/google.py | 13 +++++-------- social_core/backends/mediawiki.py | 3 +-- social_core/backends/open_id.py | 5 ++--- social_core/backends/qiita.py | 3 +-- social_core/pipeline/social_auth.py | 2 +- social_core/storage.py | 18 ++++++++---------- social_core/store.py | 4 ++-- social_core/strategy.py | 12 +++++------- social_core/tests/pipeline.py | 6 ++---- social_core/tests/test_pipeline.py | 3 +-- social_core/utils.py | 10 ++++------ 13 files changed, 34 insertions(+), 51 deletions(-) diff --git a/social_core/backends/apple.py b/social_core/backends/apple.py index 2325a257f..7339ceb73 100644 --- a/social_core/backends/apple.py +++ b/social_core/backends/apple.py @@ -107,8 +107,7 @@ def get_apple_jwk(self, kid=None): if kid: return json.dumps([key for key in keys if key["kid"] == kid][0]) - else: - return (json.dumps(key) for key in keys) + return (json.dumps(key) for key in keys) def decode_id_token(self, id_token): """ diff --git a/social_core/backends/base.py b/social_core/backends/base.py index 8d3bd2b96..71a1c8bce 100644 --- a/social_core/backends/base.py +++ b/social_core/backends/base.py @@ -32,8 +32,7 @@ def setting(self, name, default=None): def start(self): if self.uses_redirect(): return self.strategy.redirect(self.auth_url()) - else: - return self.strategy.html(self.auth_html()) + return self.strategy.html(self.auth_html()) def complete(self, *args, **kwargs): return self.auth_complete(*args, **kwargs) diff --git a/social_core/backends/google.py b/social_core/backends/google.py index aafc2c761..a050ea2f0 100644 --- a/social_core/backends/google.py +++ b/social_core/backends/google.py @@ -14,10 +14,8 @@ def get_user_id(self, details, response): if self.setting("USE_UNIQUE_USER_ID", False): if "sub" in response: return response["sub"] - else: - return response["id"] - else: - return details["email"] + return response["id"] + return details["email"] def get_user_details(self, response): """Return user details from Google API account""" @@ -115,7 +113,7 @@ def auth_complete(self, *args, **kwargs): ) self.process_error(response) return self.do_auth(token, response=response, *args, **kwargs) - elif "code" in self.data: # Server-side workflow + if "code" in self.data: # Server-side workflow response = self.request_access_token( self.ACCESS_TOKEN_URL, data=self.auth_complete_params(), @@ -126,11 +124,10 @@ def auth_complete(self, *args, **kwargs): return self.do_auth( response["access_token"], response=response, *args, **kwargs ) - elif "id_token" in self.data: # Client-side workflow + if "id_token" in self.data: # Client-side workflow token = self.data.get("id_token") return self.do_auth(token, *args, **kwargs) - else: - raise AuthMissingParameter(self, "access_token, id_token, or code") + raise AuthMissingParameter(self, "access_token, id_token, or code") def user_data(self, access_token, *args, **kwargs): if "id_token" not in self.data: diff --git a/social_core/backends/mediawiki.py b/social_core/backends/mediawiki.py index 5ceb220b7..f6dd1a663 100644 --- a/social_core/backends/mediawiki.py +++ b/social_core/backends/mediawiki.py @@ -21,8 +21,7 @@ def force_unicode(value): """ if isinstance(value, str): return value - else: - return str(value, "unicode-escape") + return str(value, "unicode-escape") class MediaWiki(BaseOAuth1): diff --git a/social_core/backends/open_id.py b/social_core/backends/open_id.py index 064d37e3a..ed694abde 100644 --- a/social_core/backends/open_id.py +++ b/social_core/backends/open_id.py @@ -253,7 +253,6 @@ def openid_url(self): provider URL.""" if self.URL: return self.URL - elif OPENID_ID_FIELD in self.data: + if OPENID_ID_FIELD in self.data: return self.data[OPENID_ID_FIELD] - else: - raise AuthMissingParameter(self, OPENID_ID_FIELD) + raise AuthMissingParameter(self, OPENID_ID_FIELD) diff --git a/social_core/backends/qiita.py b/social_core/backends/qiita.py index dc8d5aa9b..1202a3727 100644 --- a/social_core/backends/qiita.py +++ b/social_core/backends/qiita.py @@ -83,5 +83,4 @@ def get_user_id(self, details, response): if user_id is not None: return str(user_id) - else: - raise AuthException("failed to get user id") + raise AuthException("failed to get user id") diff --git a/social_core/pipeline/social_auth.py b/social_core/pipeline/social_auth.py index 247163371..7ad0d20c8 100644 --- a/social_core/pipeline/social_auth.py +++ b/social_core/pipeline/social_auth.py @@ -74,7 +74,7 @@ def associate_by_email(backend, details, user=None, *args, **kwargs): users = list(backend.strategy.storage.user.get_users_by_email(email)) if len(users) == 0: return None - elif len(users) > 1: + if len(users) > 1: raise AuthException( backend, "The given email address is associated with another account" ) diff --git a/social_core/storage.py b/social_core/storage.py index 392b98003..9e3cb6276 100644 --- a/social_core/storage.py +++ b/social_core/storage.py @@ -74,16 +74,14 @@ def expiration_timedelta(self): # expires is a datetime, return the remaining difference expiry_time = datetime.fromtimestamp(expires, tz=timezone.utc) return expiry_time - now - else: - # 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 - else: - return timedelta(seconds=expires) + # 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) def expiration_datetime(self): # backward compatible alias diff --git a/social_core/store.py b/social_core/store.py index 6dfe9e072..cb6228b8b 100644 --- a/social_core/store.py +++ b/social_core/store.py @@ -30,8 +30,8 @@ def removeAssociation(self, server_url, handle): def expiresIn(self, assoc): if hasattr(assoc, "getExpiresIn"): return assoc.getExpiresIn() - else: # python3-openid 3.0.2 - return assoc.expiresIn + # python3-openid 3.0.2 + return assoc.expiresIn def getAssociation(self, server_url, handle=None): """Return stored association""" diff --git a/social_core/strategy.py b/social_core/strategy.py index 60ab1cd12..604942f1a 100644 --- a/social_core/strategy.py +++ b/social_core/strategy.py @@ -19,8 +19,7 @@ def render(self, tpl=None, html=None, context=None): context = context or {} if tpl: return self.render_template(tpl, context) - else: - return self.render_string(html, context) + return self.render_string(html, context) def render_template(self, tpl, context): raise NotImplementedError("Implement in subclass") @@ -137,13 +136,12 @@ def validate_email(self, email, code): verification_code = self.storage.code.get_code(code) if not verification_code or verification_code.code != code: return False - elif verification_code.email != email: + if verification_code.email != email: return False - elif verification_code.verified: + if verification_code.verified: return False - else: - verification_code.verify() - return True + verification_code.verify() + return True def render_html(self, tpl=None, html=None, context=None): """Render given template or raw html with given context""" diff --git a/social_core/tests/pipeline.py b/social_core/tests/pipeline.py index 7d8d8126a..94a91565f 100644 --- a/social_core/tests/pipeline.py +++ b/social_core/tests/pipeline.py @@ -5,16 +5,14 @@ def ask_for_password(strategy, *args, **kwargs): if strategy.session_get("password"): return {"password": strategy.session_get("password")} - else: - return strategy.redirect(strategy.build_absolute_uri("/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")} - else: - return strategy.redirect(strategy.build_absolute_uri("/slug")) + return strategy.redirect(strategy.build_absolute_uri("/slug")) def set_password(strategy, user, *args, **kwargs): diff --git a/social_core/tests/test_pipeline.py b/social_core/tests/test_pipeline.py index 0acd4f0fa..e92ddc50f 100644 --- a/social_core/tests/test_pipeline.py +++ b/social_core/tests/test_pipeline.py @@ -29,8 +29,7 @@ def get_social_auth(cls, provider, uid): if cls._called_times == 2: user = list(User.cache.values())[0] return IntegrityErrorUserSocialAuth(user, provider, uid) - else: - return super().get_social_auth(provider, uid) + return super().get_social_auth(provider, uid) class IntegrityErrorStorage(TestStorage): diff --git a/social_core/utils.py b/social_core/utils.py index c5eec885b..894e31274 100644 --- a/social_core/utils.py +++ b/social_core/utils.py @@ -207,8 +207,7 @@ def partial_pipeline_data(backend, user=None, partial_token=None, *args, **kwarg kwargs.setdefault("request", request_data) partial.extend_kwargs(kwargs) return partial - else: - backend.strategy.clean_partial_pipeline(partial_token) + backend.strategy.clean_partial_pipeline(partial_token) def build_absolute_uri(host_url, path=None): @@ -238,10 +237,9 @@ def setting_url(backend, *names): for name in names: if is_url(name): return name - else: - value = backend.setting(name) - if is_url(value): - return value + value = backend.setting(name) + if is_url(value): + return value def handle_http_errors(func): From 1548d6c8faf6c294e682bbd868f59db55f9843cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 15:28:09 +0100 Subject: [PATCH 099/152] fix: remove outdated Python 2 code Python 2 is not supported for years, this was forgotten. --- social_core/tests/test_utils.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/social_core/tests/test_utils.py b/social_core/tests/test_utils.py index 1b818b3ce..ea03398b0 100644 --- a/social_core/tests/test_utils.py +++ b/social_core/tests/test_utils.py @@ -1,4 +1,3 @@ -import sys import unittest from unittest.mock import Mock @@ -12,8 +11,6 @@ ) from .models import TestPartial -PY3 = sys.version_info[0] == 3 - class SanitizeRedirectTest(unittest.TestCase): def test_none_redirect(self): @@ -104,14 +101,9 @@ def is_active(self): class SlugifyTest(unittest.TestCase): def test_slugify_formats(self): - if PY3: - self.assertEqual(slugify("FooBar"), "foobar") - self.assertEqual(slugify("Foo Bar"), "foo-bar") - self.assertEqual(slugify("Foo (Bar)"), "foo-bar") - else: - self.assertEqual(slugify("FooBar".decode("utf-8")), "foobar") - self.assertEqual(slugify("Foo Bar".decode("utf-8")), "foo-bar") - self.assertEqual(slugify("Foo (Bar)".decode("utf-8")), "foo-bar") + self.assertEqual(slugify("FooBar"), "foobar") + self.assertEqual(slugify("Foo Bar"), "foo-bar") + self.assertEqual(slugify("Foo (Bar)"), "foo-bar") class BuildAbsoluteURITest(unittest.TestCase): From 9f04a63017275917587ab1628e071820a674a4ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 15:32:11 +0100 Subject: [PATCH 100/152] chore: use next() instead of getting first element from list --- social_core/backends/apple.py | 2 +- social_core/backends/clever.py | 2 +- social_core/tests/backends/oauth.py | 18 +++++++++--------- social_core/tests/test_pipeline.py | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/social_core/backends/apple.py b/social_core/backends/apple.py index 7339ceb73..6d58bc2a2 100644 --- a/social_core/backends/apple.py +++ b/social_core/backends/apple.py @@ -106,7 +106,7 @@ def get_apple_jwk(self, kid=None): raise AuthFailed(self, "Invalid jwk response") if kid: - return json.dumps([key for key in keys if key["kid"] == kid][0]) + return json.dumps(next(key for key in keys if key["kid"] == kid)) return (json.dumps(key) for key in keys) def decode_id_token(self, id_token): diff --git a/social_core/backends/clever.py b/social_core/backends/clever.py index bb65188a3..2f8d17236 100644 --- a/social_core/backends/clever.py +++ b/social_core/backends/clever.py @@ -21,7 +21,7 @@ def get_user_id(self, details, response): return response.get("data", {}).get("id") def get_user_type(self, data): - return list(data.get("data", {}).get("roles", {}).keys())[0] + return next(iter(data.get("data", {}).get("roles", {}).keys())) def get_user_details(self, response): """Return user details from Classlink account""" diff --git a/social_core/tests/backends/oauth.py b/social_core/tests/backends/oauth.py index c4707f426..722c0354a 100644 --- a/social_core/tests/backends/oauth.py +++ b/social_core/tests/backends/oauth.py @@ -118,7 +118,7 @@ def do_refresh_token(self): status=200, body=self.refresh_token_body, ) - user = list(User.cache.values())[0] + user = next(iter(User.cache.values())) social = user.social[0] social.refresh_token(strategy=self.strategy, **self.refresh_token_arguments()) return user, social @@ -136,17 +136,17 @@ def do_login(self): user = super().do_login() requests = latest_requests() - auth_request = [ + auth_request = next( r for r in requests if self.backend.authorization_url() in r.url - ][0] + ) code_challenge = auth_request.querystring.get("code_challenge")[0] code_challenge_method = auth_request.querystring.get("code_challenge_method")[0] self.assertIsNotNone(code_challenge) self.assertEqual(code_challenge_method, "plain") - auth_complete = [ + auth_complete = next( r for r in requests if self.backend.access_token_url() in r.url - ][0] + ) code_verifier = auth_complete.parsed_body.get("code_verifier")[0] self.assertEqual(code_challenge, code_verifier) @@ -159,17 +159,17 @@ def do_login(self): user = super().do_login() requests = latest_requests() - auth_request = [ + auth_request = next( r for r in requests if self.backend.authorization_url() in r.url - ][0] + ) code_challenge = auth_request.querystring.get("code_challenge")[0] code_challenge_method = auth_request.querystring.get("code_challenge_method")[0] self.assertIsNotNone(code_challenge) self.assertTrue(code_challenge_method in ["s256", "S256"]) - auth_complete = [ + auth_complete = next( r for r in requests if self.backend.access_token_url() in r.url - ][0] + ) code_verifier = auth_complete.parsed_body.get("code_verifier")[0] self.assertEqual( self.backend.generate_code_challenge(code_verifier, code_challenge_method), diff --git a/social_core/tests/test_pipeline.py b/social_core/tests/test_pipeline.py index e92ddc50f..77a43fe4e 100644 --- a/social_core/tests/test_pipeline.py +++ b/social_core/tests/test_pipeline.py @@ -27,7 +27,7 @@ def get_social_auth(cls, provider, uid): cls._called_times = 0 cls._called_times += 1 if cls._called_times == 2: - user = list(User.cache.values())[0] + user = next(iter(User.cache.values())) return IntegrityErrorUserSocialAuth(user, provider, uid) return super().get_social_auth(provider, uid) From 444937b152382aff40c94983c1557cfbf5841612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 15:42:56 +0100 Subject: [PATCH 101/152] chore: avoid using join on string literals Better to use f-strings or triple quoted strings. --- social_core/backends/base.py | 2 +- social_core/backends/lastfm.py | 4 +- social_core/backends/yahoo.py | 2 +- .../tests/backends/test_livejournal.py | 44 +++++++------------ social_core/tests/backends/test_ngpvan.py | 41 +++++++---------- .../tests/backends/test_open_id_connect.py | 2 +- social_core/tests/backends/test_steam.py | 44 ++++++++----------- 7 files changed, 56 insertions(+), 83 deletions(-) diff --git a/social_core/backends/base.py b/social_core/backends/base.py index 71a1c8bce..be8f5ec1c 100644 --- a/social_core/backends/base.py +++ b/social_core/backends/base.py @@ -190,7 +190,7 @@ def get_user_names(self, fullname="", first_name="", last_name=""): except ValueError: first_name = first_name or fullname or "" last_name = last_name or "" - fullname = fullname or " ".join((first_name, last_name)) + fullname = fullname or f"{first_name} {last_name}" return fullname.strip(), first_name.strip(), last_name.strip() def get_user(self, user_id): diff --git a/social_core/backends/lastfm.py b/social_core/backends/lastfm.py index 735d3d422..941c634fb 100644 --- a/social_core/backends/lastfm.py +++ b/social_core/backends/lastfm.py @@ -28,9 +28,7 @@ def auth_complete(self, *args, **kwargs): token = self.data["token"] signature = hashlib.md5( - "".join( - ("api_key", key, "methodauth.getSession", "token", token, secret) - ).encode() + f"api_key{key}methodauth.getSessiontoken{token}{secret}".encode() ).hexdigest() response = self.get_json( diff --git a/social_core/backends/yahoo.py b/social_core/backends/yahoo.py index a5ef64b7c..20c18da98 100644 --- a/social_core/backends/yahoo.py +++ b/social_core/backends/yahoo.py @@ -74,7 +74,7 @@ class YahooOAuth2(BaseOAuth2): def get_user_names(self, first_name, last_name): if first_name or last_name: - return " ".join((first_name, last_name)), first_name, last_name + return f"{first_name} {last_name}", first_name, last_name return None, None, None def get_user_details(self, response): diff --git a/social_core/tests/backends/test_livejournal.py b/social_core/tests/backends/test_livejournal.py index b25836613..aaba8790a 100644 --- a/social_core/tests/backends/test_livejournal.py +++ b/social_core/tests/backends/test_livejournal.py @@ -14,19 +14,15 @@ class LiveJournalOpenIdTest(OpenIdTest): backend_path = "social_core.backends.livejournal.LiveJournalOpenId" expected_username = "foobar" - discovery_body = "".join( - [ - '', - "", - '', - "http://specs.openid.net/auth/2.0/signon", - "http://www.livejournal.com/openid/server.bml", - "http://foobar.livejournal.com/", - "", - "", - "", - ] - ) + 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, @@ -44,20 +40,14 @@ class LiveJournalOpenIdTest(OpenIdTest): "openid.sig": "Z8MOozVPTOBhHG5ZS1NeGofxs1Q=", } ) - server_bml_body = "\n".join( - [ - "assoc_handle:1364935340:ZhruPQ7DJ9eGgUkeUA9A:27f8c32464", - "assoc_type:HMAC-SHA1", - "dh_server_public:WzsRyLomvAV3vwvGUrfzXDgfqnTF+m1l3JWb55fyHO7visPT4tmQ" - "iTjqFFnSVAtAOvQzoViMiZQisxNwnqSK4lYexoez1z6pP5ry3pqxJAEYj60vFGvRztict" - "Eo0brjhmO1SNfjK1ppjOymdykqLpZeaL5fsuLtMCwTnR/JQZVA=", - "enc_mac_key:LiOEVlLJSVUqfNvb5zPd76nEfvc=", - "expires_in:1207060", - "ns:http://specs.openid.net/auth/2.0", - "session_type:DH-SHA1", - "", - ] - ) + 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" diff --git a/social_core/tests/backends/test_ngpvan.py b/social_core/tests/backends/test_ngpvan.py index 9173ff6e6..66ca3b513 100644 --- a/social_core/tests/backends/test_ngpvan.py +++ b/social_core/tests/backends/test_ngpvan.py @@ -17,30 +17,23 @@ class NGPVANActionIDOpenIDTest(OpenIdTest): backend_path = "social_core.backends.ngpvan.ActionIDOpenID" expected_username = "testuser@user.local" - discovery_body = " ".join( - [ - '', - "', - "", - '', - "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", - "", - "", - "", - ] - ) + 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", diff --git a/social_core/tests/backends/test_open_id_connect.py b/social_core/tests/backends/test_open_id_connect.py index f57db5967..19c5d963e 100644 --- a/social_core/tests/backends/test_open_id_connect.py +++ b/social_core/tests/backends/test_open_id_connect.py @@ -166,7 +166,7 @@ def prepare_access_token_body( header, msg, sig = body["id_token"].split(".") id_token["sub"] = "1235" msg = base64.encodebytes(json.dumps(id_token).encode()).decode() - body["id_token"] = ".".join([header, msg, sig]) + body["id_token"] = f"{header}.{msg}.{sig}" return json.dumps(body) diff --git a/social_core/tests/backends/test_steam.py b/social_core/tests/backends/test_steam.py index 6798e453c..9f11afad5 100644 --- a/social_core/tests/backends/test_steam.py +++ b/social_core/tests/backends/test_steam.py @@ -14,32 +14,24 @@ class SteamOpenIdTest(OpenIdTest): backend_path = "social_core.backends.steam.SteamOpenId" expected_username = "foobar" - discovery_body = "".join( - [ - '', - '', - "", - '', - "http://specs.openid.net/auth/2.0/server", - "https://steamcommunity.com/openid/login", - "", - "", - "", - ] - ) - user_discovery_body = "".join( - [ - '', - '', - "", - '', - "http://specs.openid.net/auth/2.0/signon ", - "https://steamcommunity.com/openid/login", - "", - "", - "", - ] - ) + 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, From 4b315066a843088e0b7a55232474edcad17f9c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 15:47:09 +0100 Subject: [PATCH 102/152] chore: remove legacy code for no longer supported Python versions --- social_core/backends/azuread_b2c.py | 12 +----------- social_core/tests/backends/test_saml.py | 11 +---------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/social_core/backends/azuread_b2c.py b/social_core/backends/azuread_b2c.py index 63b9d4cde..e03e66fd5 100644 --- a/social_core/backends/azuread_b2c.py +++ b/social_core/backends/azuread_b2c.py @@ -34,17 +34,7 @@ from jwt import decode as jwt_decode from jwt import get_unverified_header -try: - from jwt.algorithms import RSAAlgorithm -except ImportError: - raise Exception( - # Python 3.3 is not supported because of compatibility in - # Cryptography package in Python3.3 You are welcome to patch - # and open a pull request. - "Cryptography library is required for this backend " - "(AzureADB2COAuth2) to work. Note that this backend is only " - "supported on Python 2 and Python 3.4+." - ) +from jwt.algorithms import RSAAlgorithm from ..exceptions import AuthException, AuthTokenError from .azuread import AzureADOAuth2 diff --git a/social_core/tests/backends/test_saml.py b/social_core/tests/backends/test_saml.py index 02d6af00c..de4925fde 100644 --- a/social_core/tests/backends/test_saml.py +++ b/social_core/tests/backends/test_saml.py @@ -1,5 +1,4 @@ import json -import os import re import sys import unittest @@ -10,11 +9,7 @@ import requests from httpretty import HTTPretty -try: - from onelogin.saml2.utils import OneLogin_Saml2_Utils -except ImportError: - # Only available for python 2.7 at the moment, so don't worry if this fails - pass +from onelogin.saml2.utils import OneLogin_Saml2_Utils from ...exceptions import AuthMissingParameter from .base import BaseBackendTest @@ -22,10 +17,6 @@ DATA_DIR = path.join(path.dirname(__file__), "data") -@unittest.skipIf( - "TRAVIS" in os.environ, - "Travis-ci segfaults probably due to a bad " "dependencies build", -) @unittest.skipIf( "__pypy__" in sys.builtin_module_names, "dm.xmlsec not compatible with pypy" ) From 21b1a0711fab1ebf6d802bc43c993e7624b9bc1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 15:51:38 +0100 Subject: [PATCH 103/152] chore: use dict literals --- social_core/tests/test_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/social_core/tests/test_utils.py b/social_core/tests/test_utils.py index ea03398b0..77a97a8f6 100644 --- a/social_core/tests/test_utils.py +++ b/social_core/tests/test_utils.py @@ -146,7 +146,7 @@ def test_returns_partial_when_uid_and_email_do_match(self): 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, *(), **dict([(key, val)])) + 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) @@ -155,14 +155,14 @@ 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, *(), **dict([(key, val)])) + 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, *(), **dict([(key, val)])) + 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) From 191bf03e69ba33fdb8112c6fde7251301937ba8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 15:52:56 +0100 Subject: [PATCH 104/152] fix: add explicit return None This makes it clear that the function intents to return None here. --- social_core/backends/gae.py | 1 + social_core/backends/oauth.py | 1 + social_core/backends/ping.py | 1 + social_core/pipeline/mail.py | 2 ++ social_core/pipeline/social_auth.py | 2 ++ social_core/storage.py | 2 ++ social_core/store.py | 1 + social_core/tests/models.py | 3 +++ social_core/tests/pipeline.py | 2 ++ social_core/utils.py | 4 ++++ 10 files changed, 19 insertions(+) diff --git a/social_core/backends/gae.py b/social_core/backends/gae.py index fc67fba1b..1fc3c1a5b 100644 --- a/social_core/backends/gae.py +++ b/social_core/backends/gae.py @@ -18,6 +18,7 @@ def get_user_id(self, details, response): 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).""" diff --git a/social_core/backends/oauth.py b/social_core/backends/oauth.py index 4668a373e..287629fdf 100644 --- a/social_core/backends/oauth.py +++ b/social_core/backends/oauth.py @@ -160,6 +160,7 @@ def revoke_token(self, token, uid): method=self.REVOKE_TOKEN_METHOD, ) return self.process_revoke_token_response(response) + return None class BaseOAuth1(OAuthAuth): diff --git a/social_core/backends/ping.py b/social_core/backends/ping.py index 8047de770..5d6b0d230 100644 --- a/social_core/backends/ping.py +++ b/social_core/backends/ping.py @@ -26,6 +26,7 @@ def find_valid_key(self, id_token): 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): """ diff --git a/social_core/pipeline/mail.py b/social_core/pipeline/mail.py index 239600d03..96a3a3fd6 100644 --- a/social_core/pipeline/mail.py +++ b/social_core/pipeline/mail.py @@ -18,6 +18,7 @@ def mail_validation(backend, details, is_new=False, *args, **kwargs): details["email"], data["verification_code"] ): raise InvalidEmail(backend) + return None else: current_partial = kwargs.get("current_partial") backend.strategy.send_email_validation( @@ -27,3 +28,4 @@ def mail_validation(backend, details, is_new=False, *args, **kwargs): return backend.strategy.redirect( backend.strategy.setting("EMAIL_VALIDATION_URL") ) + return None diff --git a/social_core/pipeline/social_auth.py b/social_core/pipeline/social_auth.py index 7ad0d20c8..4ab6e319a 100644 --- a/social_core/pipeline/social_auth.py +++ b/social_core/pipeline/social_auth.py @@ -51,6 +51,7 @@ def associate_user(backend, uid, user=None, social=None, *args, **kwargs): return result else: return {"social": social, "user": social.user, "new_association": True} + return None def associate_by_email(backend, details, user=None, *args, **kwargs): @@ -80,6 +81,7 @@ def associate_by_email(backend, details, user=None, *args, **kwargs): ) else: return {"user": users[0], "is_new": False} + return None def load_extra_data(backend, details, response, uid, user, *args, **kwargs): diff --git a/social_core/storage.py b/social_core/storage.py index 9e3cb6276..a77cbe2de 100644 --- a/social_core/storage.py +++ b/social_core/storage.py @@ -82,6 +82,7 @@ def expiration_timedelta(self): 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 @@ -108,6 +109,7 @@ def set_extra_data(self, extra_data=None): else: self.extra_data = extra_data return True + return None @classmethod def clean_username(cls, value): diff --git a/social_core/store.py b/social_core/store.py index cb6228b8b..eea1e4a6e 100644 --- a/social_core/store.py +++ b/social_core/store.py @@ -48,6 +48,7 @@ def getAssociation(self, server_url, handle=None): 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""" diff --git a/social_core/tests/models.py b/social_core/tests/models.py index bf6111fce..66774fd74 100644 --- a/social_core/tests/models.py +++ b/social_core/tests/models.py @@ -116,12 +116,14 @@ def get_user(cls, pk): for username, user in User.cache.items(): 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): @@ -216,6 +218,7 @@ def get_code(cls, code): for c in cls.cache.values(): if c.code == code: return c + return None class TestPartial(PartialMixin, BaseModel): diff --git a/social_core/tests/pipeline.py b/social_core/tests/pipeline.py index 94a91565f..b54003530 100644 --- a/social_core/tests/pipeline.py +++ b/social_core/tests/pipeline.py @@ -31,6 +31,7 @@ def remove_user(strategy, user, *args, **kwargs): def set_user_from_kwargs(strategy, *args, **kwargs): if strategy.session_get("attribute"): kwargs["user"].id + return None else: return strategy.redirect(strategy.build_absolute_uri("/attribute")) @@ -39,5 +40,6 @@ def set_user_from_kwargs(strategy, *args, **kwargs): def set_user_from_args(strategy, user, *args, **kwargs): if strategy.session_get("attribute"): user.id + return None else: return strategy.redirect(strategy.build_absolute_uri("/attribute")) diff --git a/social_core/utils.py b/social_core/utils.py index 894e31274..67bdaacf9 100644 --- a/social_core/utils.py +++ b/social_core/utils.py @@ -153,6 +153,7 @@ def first(func, items): for item in items: if func(item): return item + return None def parse_qs(value): @@ -208,6 +209,8 @@ def partial_pipeline_data(backend, user=None, partial_token=None, *args, **kwarg 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): @@ -240,6 +243,7 @@ def setting_url(backend, *names): value = backend.setting(name) if is_url(value): return value + return None def handle_http_errors(func): From 813822ae6ecfb3c57397e5080c25874f86a755ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 15:55:37 +0100 Subject: [PATCH 105/152] chore: use unpacking instead of manual concatenation This directly contructs the list instead of using an intermediate copy. --- social_core/actions.py | 15 +++++++++------ social_core/backends/mendeley.py | 3 ++- social_core/backends/oauth.py | 2 +- social_core/backends/odnoklassniki.py | 8 ++++++-- social_core/backends/professionali.py | 9 +++++++-- social_core/backends/vk.py | 3 ++- social_core/utils.py | 2 +- 7 files changed, 28 insertions(+), 14 deletions(-) diff --git a/social_core/actions.py b/social_core/actions.py index 4ac2b43ab..e9a0d0afc 100644 --- a/social_core/actions.py +++ b/social_core/actions.py @@ -24,8 +24,9 @@ def do_auth(backend, redirect_name="next"): # 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() + allowed_hosts = [ + *backend.setting("ALLOWED_REDIRECT_HOSTS", []), + backend.strategy.request_host(), ] redirect_uri = sanitize_redirect(allowed_hosts, redirect_uri) backend.strategy.session_set( @@ -105,8 +106,9 @@ def do_complete(backend, login, user=None, redirect_name="next", *args, **kwargs 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() + allowed_hosts = [ + *backend.setting("ALLOWED_REDIRECT_HOSTS", []), + backend.strategy.request_host(), ] url = sanitize_redirect(allowed_hosts, url) or backend.setting( "LOGIN_REDIRECT_URL" @@ -136,8 +138,9 @@ def do_disconnect( or backend.setting("LOGIN_REDIRECT_URL") ) if backend.setting("SANITIZE_REDIRECTS", True): - allowed_hosts = backend.setting("ALLOWED_REDIRECT_HOSTS", []) + [ - backend.strategy.request_host() + allowed_hosts = [ + *backend.setting("ALLOWED_REDIRECT_HOSTS", []), + backend.strategy.request_host(), ] url = ( sanitize_redirect(allowed_hosts, url) diff --git a/social_core/backends/mendeley.py b/social_core/backends/mendeley.py index 677351fba..b9e37641c 100644 --- a/social_core/backends/mendeley.py +++ b/social_core/backends/mendeley.py @@ -50,7 +50,8 @@ class MendeleyOAuth2(MendeleyMixin, BaseOAuth2): ACCESS_TOKEN_METHOD = "POST" DEFAULT_SCOPE = ["all"] REDIRECT_STATE = False - EXTRA_DATA = MendeleyMixin.EXTRA_DATA + [ + EXTRA_DATA = [ + *MendeleyMixin.EXTRA_DATA, ("refresh_token", "refresh_token"), ("expires_in", "expires_in"), ("token_type", "token_type"), diff --git a/social_core/backends/oauth.py b/social_core/backends/oauth.py index 287629fdf..71cc70185 100644 --- a/social_core/backends/oauth.py +++ b/social_core/backends/oauth.py @@ -239,7 +239,7 @@ def get_unauthorized_token(self): def set_unauthorized_token(self): token = self.unauthorized_token() name = self.name + self.UNATHORIZED_TOKEN_SUFIX - tokens = self.strategy.session_get(name, []) + [token] + tokens = [*self.strategy.session_get(name, []), token] self.strategy.session_set(name, tokens) return token diff --git a/social_core/backends/odnoklassniki.py b/social_core/backends/odnoklassniki.py index e744689fb..dbc6611de 100644 --- a/social_core/backends/odnoklassniki.py +++ b/social_core/backends/odnoklassniki.py @@ -77,8 +77,12 @@ def get_user_details(self, response): 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", () + fields = ( + "uid", + "first_name", + "last_name", + "name", + *self.setting("EXTRA_USER_DATA_LIST", ()), ) data = { "method": "users.getInfo", diff --git a/social_core/backends/professionali.py b/social_core/backends/professionali.py index 1ee10f35a..27ee01202 100644 --- a/social_core/backends/professionali.py +++ b/social_core/backends/professionali.py @@ -35,8 +35,13 @@ def user_data(self, access_token, response, *args, **kwargs): url = "https://api.professionali.ru/v6/users/get.json" fields = list( set( - ["firstname", "lastname", "avatar_big", "link"] - + self.setting("EXTRA_DATA", []) + [ + "firstname", + "lastname", + "avatar_big", + "link", + *self.setting("EXTRA_DATA", []), + ] ) ) params = { diff --git a/social_core/backends/vk.py b/social_core/backends/vk.py index 8eec688cb..4432e39a8 100644 --- a/social_core/backends/vk.py +++ b/social_core/backends/vk.py @@ -106,7 +106,8 @@ def user_data(self, access_token, *args, **kwargs): "screen_name", "nickname", "photo", - ] + self.setting("EXTRA_DATA", []) + *self.setting("EXTRA_DATA", []), + ] fields = ",".join(set(request_data)) data = vk_api( diff --git a/social_core/utils.py b/social_core/utils.py index 67bdaacf9..abd494456 100644 --- a/social_core/utils.py +++ b/social_core/utils.py @@ -83,7 +83,7 @@ def to_setting_name(*names): def setting_name(*names): - return to_setting_name(*((SETTING_PREFIX,) + names)) + return to_setting_name(*((SETTING_PREFIX, *names))) def sanitize_redirect(hosts, redirect_to): From 8a6be982c6cfb3e3f67b5b3948ddcc2b8c2c6921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 15:58:52 +0100 Subject: [PATCH 106/152] chore: remove not needed params from exception class --- social_core/pipeline/disconnect.py | 2 +- social_core/tests/test_pipeline.py | 4 ++-- social_core/tests/test_storage.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/social_core/pipeline/disconnect.py b/social_core/pipeline/disconnect.py index 4ac093d62..8e622ec9b 100644 --- a/social_core/pipeline/disconnect.py +++ b/social_core/pipeline/disconnect.py @@ -5,7 +5,7 @@ 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() + raise NotAllowedToDisconnect def get_entries( diff --git a/social_core/tests/test_pipeline.py b/social_core/tests/test_pipeline.py index 77a43fe4e..9545e0d1b 100644 --- a/social_core/tests/test_pipeline.py +++ b/social_core/tests/test_pipeline.py @@ -19,7 +19,7 @@ class UnknownError(Exception): class IntegrityErrorUserSocialAuth(TestUserSocialAuth): @classmethod def create_social_auth(cls, user, uid, provider): - raise IntegrityError() + raise IntegrityError @classmethod def get_social_auth(cls, provider, uid): @@ -44,7 +44,7 @@ def is_integrity_error(cls, exception): class UnknownErrorUserSocialAuth(TestUserSocialAuth): @classmethod def create_social_auth(cls, user, uid, provider): - raise UnknownError() + raise UnknownError class UnknownErrorStorage(IntegrityErrorStorage): diff --git a/social_core/tests/test_storage.py b/social_core/tests/test_storage.py index 8719979b7..3e328d041 100644 --- a/social_core/tests/test_storage.py +++ b/social_core/tests/test_storage.py @@ -30,7 +30,7 @@ class BrokenStrategy(BaseStrategy): class BrokenStrategyWithSettings(BrokenStrategy): def get_setting(self, name): - raise AttributeError() + raise AttributeError class BrokenStorage(BaseStorage): @@ -185,7 +185,7 @@ def test_random_string(self): def test_random_string_without_systemrandom(self): def SystemRandom(): - raise NotImplementedError() + raise NotImplementedError orig_random = getattr(random, "SystemRandom", None) random.SystemRandom = SystemRandom From 6051318c496a9974eb384e22d5f6a8299e1f3c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 16:01:54 +0100 Subject: [PATCH 107/152] chore: use f-strings instead of format() --- social_core/backends/linkedin.py | 4 +--- social_core/backends/zoom.py | 6 +----- social_core/tests/backends/test_facebook.py | 4 +--- social_core/tests/backends/test_keycloak.py | 8 ++++---- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/social_core/backends/linkedin.py b/social_core/backends/linkedin.py index d416ff61f..5aa3d7e57 100644 --- a/social_core/backends/linkedin.py +++ b/social_core/backends/linkedin.py @@ -117,9 +117,7 @@ def user_data_headers(self, access_token): headers["Accept-Language"] = ( lang if lang is not True else self.strategy.get_language() ) - headers["Authorization"] = "Bearer {access_token}".format( - access_token=access_token - ) + headers["Authorization"] = f"Bearer {access_token}" return headers def request_access_token(self, *args, **kwargs): diff --git a/social_core/backends/zoom.py b/social_core/backends/zoom.py index 8cb7eed69..1cefc7ceb 100644 --- a/social_core/backends/zoom.py +++ b/social_core/backends/zoom.py @@ -22,11 +22,7 @@ class ZoomOAuth2(BaseOAuth2): def user_data(self, access_token, *args, **kwargs): response = self.get_json( self.USER_DETAILS_URL, - headers={ - "Authorization": "Bearer {access_token}".format( - access_token=access_token - ) - }, + headers={"Authorization": f"Bearer {access_token}"}, ) return response diff --git a/social_core/tests/backends/test_facebook.py b/social_core/tests/backends/test_facebook.py index abdcabcd6..986e5a088 100644 --- a/social_core/tests/backends/test_facebook.py +++ b/social_core/tests/backends/test_facebook.py @@ -8,9 +8,7 @@ class FacebookOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): backend_path = "social_core.backends.facebook.FacebookOAuth2" - user_data_url = "https://graph.facebook.com/v{version}/me".format( - version=API_VERSION - ) + 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( diff --git a/social_core/tests/backends/test_keycloak.py b/social_core/tests/backends/test_keycloak.py index 1d09d01d9..48f587644 100644 --- a/social_core/tests/backends/test_keycloak.py +++ b/social_core/tests/backends/test_keycloak.py @@ -33,11 +33,11 @@ ey4gIBKESJF6X9fefiawCrI3+PC7x9x0ngP9R4t/OzDWVAYn9gmd """.strip() -_PRIVATE_KEY = """ +_PRIVATE_KEY = f""" -----BEGIN RSA PRIVATE KEY----- {_PRIVATE_KEY_HEADERLESS} -----END RSA PRIVATE KEY----- -""".format(_PRIVATE_KEY_HEADERLESS=_PRIVATE_KEY_HEADERLESS) +""" _PUBLIC_KEY_HEADERLESS = """ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvyo2hx1L3ALHeUd/6xk/ @@ -49,11 +49,11 @@ hwIDAQAB """.strip() -_PUBLIC_KEY = """ +_PUBLIC_KEY = f""" -----BEGIN PUBLIC KEY----- {_PUBLIC_KEY_HEADERLESS} -----END PUBLIC KEY----- -""".format(_PUBLIC_KEY_HEADERLESS=_PUBLIC_KEY_HEADERLESS) +""" _KEY = "example" _SECRET = "1234abcd-1234-abcd-1234-abcd1234adcd" From b3c7dc1d6ba2bc734fabb31fb1511655ab2d205d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 16:31:25 +0100 Subject: [PATCH 108/152] chore: use values when only dict values are needed --- social_core/tests/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social_core/tests/models.py b/social_core/tests/models.py index 66774fd74..8fa0b9f45 100644 --- a/social_core/tests/models.py +++ b/social_core/tests/models.py @@ -113,7 +113,7 @@ def create_user(cls, username, email=None, **extra_user_fields): @classmethod def get_user(cls, pk): - for username, user in User.cache.items(): + for user in User.cache.values(): if user.id == pk: return user return None From 70b6354c02a52e1b46329cb8723679d208410035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 16:32:59 +0100 Subject: [PATCH 109/152] chore: avoid import as when not renaming --- social_core/tests/backends/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/social_core/tests/backends/test_utils.py b/social_core/tests/backends/test_utils.py index d03fbe3bf..3b2ed24c6 100644 --- a/social_core/tests/backends/test_utils.py +++ b/social_core/tests/backends/test_utils.py @@ -1,4 +1,4 @@ -import unittest as unittest +import unittest from ...backends.github import GithubOAuth2 from ...backends.utils import get_backend, load_backends From c0ce8e2d001f7953cc3f777ce14d23426cff204d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 16:42:56 +0100 Subject: [PATCH 110/152] chore: use literals and comprehensions more effectively --- social_core/backends/mailru.py | 2 +- social_core/backends/open_id_connect.py | 2 +- social_core/backends/professionali.py | 16 +++++++--------- social_core/backends/vk.py | 2 +- .../tests/backends/test_open_id_connect.py | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/social_core/backends/mailru.py b/social_core/backends/mailru.py index 5968a4a99..610174e80 100644 --- a/social_core/backends/mailru.py +++ b/social_core/backends/mailru.py @@ -42,7 +42,7 @@ def user_data(self, access_token, *args, **kwargs): "app_id": key, "secure": "1", } - param_list = sorted(list(item + "=" + data[item] for item in data)) + param_list = sorted(f"{item}={value}" for item, value in data.values()) data["sig"] = md5(("".join(param_list) + secret).encode("utf-8")).hexdigest() return self.get_json("http://www.appsmail.ru/platform/api", params=data)[0] diff --git a/social_core/backends/open_id_connect.py b/social_core/backends/open_id_connect.py index e17b66440..af7701141 100644 --- a/social_core/backends/open_id_connect.py +++ b/social_core/backends/open_id_connect.py @@ -53,7 +53,7 @@ class OpenIdConnectAuth(BaseOAuth2): ID_KEY = "sub" USERNAME_KEY = "preferred_username" JWT_ALGORITHMS = ["RS256"] - JWT_DECODE_OPTIONS = dict() + JWT_DECODE_OPTIONS = {} # When these options are unspecified, server will choose via openid autoconfiguration ID_TOKEN_ISSUER = "" ACCESS_TOKEN_URL = "" diff --git a/social_core/backends/professionali.py b/social_core/backends/professionali.py index 27ee01202..55d970d81 100644 --- a/social_core/backends/professionali.py +++ b/social_core/backends/professionali.py @@ -34,15 +34,13 @@ def get_user_details(self, response): def user_data(self, access_token, response, *args, **kwargs): url = "https://api.professionali.ru/v6/users/get.json" fields = list( - set( - [ - "firstname", - "lastname", - "avatar_big", - "link", - *self.setting("EXTRA_DATA", []), - ] - ) + { + "firstname", + "lastname", + "avatar_big", + "link", + *self.setting("EXTRA_DATA", []), + } ) params = { "fields": ",".join(fields), diff --git a/social_core/backends/vk.py b/social_core/backends/vk.py index 4432e39a8..4bb395ce0 100644 --- a/social_core/backends/vk.py +++ b/social_core/backends/vk.py @@ -196,7 +196,7 @@ def vk_api(backend, method, data): data["method"] = method data["format"] = "json" url = "https://api.vk.com/api.php" - param_list = sorted(list(item + "=" + data[item] for item in data)) + param_list = sorted(item + "=" + data[item] for item in data) data["sig"] = md5(("".join(param_list) + secret).encode("utf-8")).hexdigest() else: url = "https://api.vk.com/method/" + method diff --git a/social_core/tests/backends/test_open_id_connect.py b/social_core/tests/backends/test_open_id_connect.py index 19c5d963e..5dacb96cb 100644 --- a/social_core/tests/backends/test_open_id_connect.py +++ b/social_core/tests/backends/test_open_id_connect.py @@ -159,7 +159,7 @@ def prepare_access_token_body( dict(self.key, iat=timegm(issue_datetime.timetuple()), nonce=nonce) ).key, algorithm="RS256", - headers=dict(kid=kid) if kid else None, + headers={"kid": kid} if kid else None, ) if tamper_message: From 9e7ca8b65180cccae341dba65849248167a8e678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 16:47:56 +0100 Subject: [PATCH 111/152] chore: simplify return statements - do not use intermediate variables - do not use useless else statements after return --- social_core/backends/cognito.py | 4 +--- social_core/backends/discourse.py | 3 +-- social_core/backends/loginradius.py | 3 +-- social_core/backends/oauth.py | 6 ++---- social_core/backends/open_id_connect.py | 3 +-- social_core/backends/orcid.py | 3 +-- social_core/backends/paypal.py | 3 +-- social_core/backends/untappd.py | 3 +-- social_core/backends/zoom.py | 3 +-- social_core/pipeline/mail.py | 17 ++++++++--------- social_core/pipeline/social_auth.py | 3 +-- social_core/pipeline/user.py | 4 ++-- social_core/storage.py | 3 +-- social_core/tests/pipeline.py | 6 ++---- 14 files changed, 24 insertions(+), 40 deletions(-) diff --git a/social_core/backends/cognito.py b/social_core/backends/cognito.py index 85402c9df..94a8cb410 100644 --- a/social_core/backends/cognito.py +++ b/social_core/backends/cognito.py @@ -43,11 +43,9 @@ def user_data(self, access_token, *args, **kwargs): headers={"Authorization": f"Bearer {access_token}"}, ) - user_data = { + return { "given_name": response.get("given_name"), "family_name": response.get("family_name"), "username": response.get("username"), "email": response.get("email"), } - - return user_data diff --git a/social_core/backends/discourse.py b/social_core/backends/discourse.py index e2baf024b..62dd91b02 100644 --- a/social_core/backends/discourse.py +++ b/social_core/backends/discourse.py @@ -39,7 +39,7 @@ def get_user_id(self, details, response): return response["email"] def get_user_details(self, response): - results = { + return { "username": response.get("username"), "email": response.get("email"), "name": response.get("name"), @@ -48,7 +48,6 @@ def get_user_details(self, response): or response.get("moderator") == "true", "is_superuser": response.get("admin") == "true", } - return results def add_nonce(self, nonce): self.strategy.storage.nonce.use(self.setting("SERVER_URL"), time.time(), nonce) diff --git a/social_core/backends/loginradius.py b/social_core/backends/loginradius.py index fe9f0240a..8e35a1b7e 100644 --- a/social_core/backends/loginradius.py +++ b/social_core/backends/loginradius.py @@ -58,14 +58,13 @@ def get_user_details(self, response): 'first_name': , 'last_name': } """ - profile = { + 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 "", } - return profile def get_user_id(self, details, response): """Return a unique ID for the current user, by default from server diff --git a/social_core/backends/oauth.py b/social_core/backends/oauth.py index 71cc70185..f139186f7 100644 --- a/social_core/backends/oauth.py +++ b/social_core/backends/oauth.py @@ -489,16 +489,14 @@ def create_code_verifier(self): def get_code_verifier(self): name = f"{self.name}_code_verifier" - code_verifier = self.strategy.session_get(name) - return 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) - code_challenge = encoded.decode().replace("=", "") # remove padding - return code_challenge + return encoded.decode().replace("=", "") # remove padding if method == "plain": return code_verifier raise AuthException("Unsupported code challenge method.") diff --git a/social_core/backends/open_id_connect.py b/social_core/backends/open_id_connect.py index af7701141..34fbde3d4 100644 --- a/social_core/backends/open_id_connect.py +++ b/social_core/backends/open_id_connect.py @@ -115,12 +115,11 @@ def oidc_config(self): @cache(ttl=86400) def get_jwks_keys(self): - keys = self.get_remote_jwks_keys() + 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'}) - return keys def get_remote_jwks_keys(self): response = self.request(self.jwks_uri()) diff --git a/social_core/backends/orcid.py b/social_core/backends/orcid.py index f1f35eae8..f1a8fb413 100644 --- a/social_core/backends/orcid.py +++ b/social_core/backends/orcid.py @@ -24,8 +24,7 @@ class ORCIDOAuth2(BaseOAuth2): ] def auth_params(self, state=None): - params = super().auth_params(state) - return params + return super().auth_params(state) def get_user_details(self, response): """Return user details from ORCID account""" diff --git a/social_core/backends/paypal.py b/social_core/backends/paypal.py index 3739bca41..0066e13ff 100644 --- a/social_core/backends/paypal.py +++ b/social_core/backends/paypal.py @@ -23,8 +23,7 @@ class PayPalOAuth2(BaseOAuth2): def user_data(self, access_token, *args, **kwargs): auth_header = {"Authorization": "Bearer %s" % access_token} - response = self.get_json(self.USER_DATA_URL, headers=auth_header) - return response + 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] diff --git a/social_core/backends/untappd.py b/social_core/backends/untappd.py index 0057a4a73..72432f2ca 100644 --- a/social_core/backends/untappd.py +++ b/social_core/backends/untappd.py @@ -30,12 +30,11 @@ class UntappdOAuth2(BaseOAuth2): def auth_params(self, state=None): client_id, client_secret = self.get_key_and_secret() - params = { + return { "client_id": client_id, "redirect_url": self.get_redirect_uri(), "response_type": self.RESPONSE_TYPE, } - return params def process_error(self, data): """ diff --git a/social_core/backends/zoom.py b/social_core/backends/zoom.py index 1cefc7ceb..d069262e9 100644 --- a/social_core/backends/zoom.py +++ b/social_core/backends/zoom.py @@ -20,11 +20,10 @@ class ZoomOAuth2(BaseOAuth2): EXTRA_DATA = [("expires_in", "expires")] def user_data(self, access_token, *args, **kwargs): - response = self.get_json( + return self.get_json( self.USER_DETAILS_URL, headers={"Authorization": f"Bearer {access_token}"}, ) - return response def get_user_details(self, response): username = response.get("id", "") diff --git a/social_core/pipeline/mail.py b/social_core/pipeline/mail.py index 96a3a3fd6..9aef64d2b 100644 --- a/social_core/pipeline/mail.py +++ b/social_core/pipeline/mail.py @@ -19,13 +19,12 @@ def mail_validation(backend, details, is_new=False, *args, **kwargs): ): raise InvalidEmail(backend) return None - else: - 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") - ) + 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 diff --git a/social_core/pipeline/social_auth.py b/social_core/pipeline/social_auth.py index 4ab6e319a..eb419ba0f 100644 --- a/social_core/pipeline/social_auth.py +++ b/social_core/pipeline/social_auth.py @@ -79,8 +79,7 @@ def associate_by_email(backend, details, user=None, *args, **kwargs): raise AuthException( backend, "The given email address is associated with another account" ) - else: - return {"user": users[0], "is_new": False} + return {"user": users[0], "is_new": False} return None diff --git a/social_core/pipeline/user.py b/social_core/pipeline/user.py index bb88d3c49..0139444d5 100644 --- a/social_core/pipeline/user.py +++ b/social_core/pipeline/user.py @@ -7,7 +7,7 @@ def get_username(strategy, details, backend, user=None, *args, **kwargs): if "username" not in backend.setting("USER_FIELDS", USER_FIELDS): - return + return None storage = strategy.storage if not user: @@ -68,7 +68,7 @@ def create_user(strategy, details, backend, user=None, *args, **kwargs): for name in backend.setting("USER_FIELDS", USER_FIELDS) } if not fields: - return + return None # Allow overriding the email field if desired by application specification if backend.setting("FORCE_EMAIL_LOWERCASE", False): diff --git a/social_core/storage.py b/social_core/storage.py index a77cbe2de..7116ca317 100644 --- a/social_core/storage.py +++ b/social_core/storage.py @@ -115,8 +115,7 @@ def set_extra_data(self, extra_data=None): def clean_username(cls, value): """Clean username removing any unsupported character""" value = NO_ASCII_REGEX.sub("", value) - value = NO_SPECIAL_REGEX.sub("", value) - return value + return NO_SPECIAL_REGEX.sub("", value) @classmethod def changed(cls, user): diff --git a/social_core/tests/pipeline.py b/social_core/tests/pipeline.py index b54003530..c414687d8 100644 --- a/social_core/tests/pipeline.py +++ b/social_core/tests/pipeline.py @@ -32,8 +32,7 @@ def set_user_from_kwargs(strategy, *args, **kwargs): if strategy.session_get("attribute"): kwargs["user"].id return None - else: - return strategy.redirect(strategy.build_absolute_uri("/attribute")) + return strategy.redirect(strategy.build_absolute_uri("/attribute")) @partial @@ -41,5 +40,4 @@ def set_user_from_args(strategy, user, *args, **kwargs): if strategy.session_get("attribute"): user.id return None - else: - return strategy.redirect(strategy.build_absolute_uri("/attribute")) + return strategy.redirect(strategy.build_absolute_uri("/attribute")) From 0feeb578b1aa05eb1730e4762f4ad4c5847a6db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 16:50:35 +0100 Subject: [PATCH 112/152] chore: bring back isort ordering --- social_core/backends/azuread_b2c.py | 4 +--- social_core/backends/azuread_tenant.py | 3 +-- social_core/tests/backends/test_saml.py | 1 - 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/social_core/backends/azuread_b2c.py b/social_core/backends/azuread_b2c.py index e03e66fd5..0fd27be84 100644 --- a/social_core/backends/azuread_b2c.py +++ b/social_core/backends/azuread_b2c.py @@ -30,10 +30,8 @@ import json from cryptography.hazmat.primitives import serialization -from jwt import DecodeError, ExpiredSignatureError +from jwt import DecodeError, ExpiredSignatureError, get_unverified_header from jwt import decode as jwt_decode -from jwt import get_unverified_header - from jwt.algorithms import RSAAlgorithm from ..exceptions import AuthException, AuthTokenError diff --git a/social_core/backends/azuread_tenant.py b/social_core/backends/azuread_tenant.py index 262ca1078..1e55edb3e 100644 --- a/social_core/backends/azuread_tenant.py +++ b/social_core/backends/azuread_tenant.py @@ -2,9 +2,8 @@ from cryptography.hazmat.backends import default_backend from cryptography.x509 import load_der_x509_certificate -from jwt import DecodeError, ExpiredSignatureError +from jwt import DecodeError, ExpiredSignatureError, get_unverified_header from jwt import decode as jwt_decode -from jwt import get_unverified_header from ..exceptions import AuthTokenError from .azuread import AzureADOAuth2 diff --git a/social_core/tests/backends/test_saml.py b/social_core/tests/backends/test_saml.py index de4925fde..538c0d136 100644 --- a/social_core/tests/backends/test_saml.py +++ b/social_core/tests/backends/test_saml.py @@ -8,7 +8,6 @@ import requests from httpretty import HTTPretty - from onelogin.saml2.utils import OneLogin_Saml2_Utils from ...exceptions import AuthMissingParameter From b9de505327683416960c89d81460a56016668db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 16:51:59 +0100 Subject: [PATCH 113/152] chore: remove not needed pass statements --- social_core/backends/base.py | 1 - social_core/backends/bungie.py | 1 - social_core/backends/saml.py | 1 - social_core/exceptions.py | 2 -- social_core/tests/backends/test_linkedin.py | 1 - 5 files changed, 6 deletions(-) diff --git a/social_core/backends/base.py b/social_core/backends/base.py index be8f5ec1c..4887314e0 100644 --- a/social_core/backends/base.py +++ b/social_core/backends/base.py @@ -52,7 +52,6 @@ def auth_complete(self, *args, **kwargs): def process_error(self, data): """Process data for errors, raise exception if needed. Call this method on any override of auth_complete.""" - pass def authenticate(self, *args, **kwargs): """Authenticate user using social credentials diff --git a/social_core/backends/bungie.py b/social_core/backends/bungie.py index e062dc280..f6fbe874f 100644 --- a/social_core/backends/bungie.py +++ b/social_core/backends/bungie.py @@ -23,7 +23,6 @@ class BungieOAuth2(BaseOAuth2): def auth_html(self): """Abstract Method Inclusion""" - pass def auth_headers(self): """Adds X-API-KEY and Origin""" diff --git a/social_core/backends/saml.py b/social_core/backends/saml.py index cb00da79a..9401b9412 100644 --- a/social_core/backends/saml.py +++ b/social_core/backends/saml.py @@ -386,4 +386,3 @@ def _check_entitlements(self, idp, attributes): be authenticated, or do nothing to allow the login pipeline to continue. """ - pass diff --git a/social_core/exceptions.py b/social_core/exceptions.py index c0900338c..dcf814e3a 100644 --- a/social_core/exceptions.py +++ b/social_core/exceptions.py @@ -1,8 +1,6 @@ class SocialAuthBaseException(ValueError): """Base class for pipeline exceptions.""" - pass - class WrongBackend(SocialAuthBaseException): def __init__(self, backend_name): diff --git a/social_core/tests/backends/test_linkedin.py b/social_core/tests/backends/test_linkedin.py index c757c3afb..d1483f5ac 100644 --- a/social_core/tests/backends/test_linkedin.py +++ b/social_core/tests/backends/test_linkedin.py @@ -40,7 +40,6 @@ class LinkedinOpenIdConnectTest( def test_invalid_nonce(self): """Skip the invalid nonce test as LinkedIn does not provide any nonce.""" - pass class BaseLinkedinTest: From ecec3f69bf7c804820dc6e7cea3c886bbdd81853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 16:52:52 +0100 Subject: [PATCH 114/152] chore: merge nested with statements --- social_core/tests/backends/oauth.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/social_core/tests/backends/oauth.py b/social_core/tests/backends/oauth.py index 722c0354a..4cbb2dccc 100644 --- a/social_core/tests/backends/oauth.py +++ b/social_core/tests/backends/oauth.py @@ -190,18 +190,20 @@ def check_parameters_in_authorization_url(self, auth_url_key="AUTHORIZATION_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", - ): - with patch.object( + 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() + ), + ): + # 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() From fca71b8751e1acd11c1573467b8dc3a4e524c6db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 15:27:15 +0100 Subject: [PATCH 115/152] feat(ci): extend ruff rulesets There are still many to address, but this is a good starting point. --- pyproject.toml | 83 ++++++++++++++++++++ social_core/backends/bitbucket_datacenter.py | 6 +- social_core/tests/backends/test_linkedin.py | 2 +- 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1045e3863..10ce2e349 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,89 @@ classifiers = [ [tool.setuptools] packages = ["social_core"] +[tool.ruff.lint] +extend-safe-fixes = [ + "D", + "TCH", + "FLY", + "SIM", + "ANN", + "FA102", + "UP" +] +select = ["ALL"] +ignore = [ + "COM", # CONFIG: No trailing commas + "D203", # CONFIG: incompatible with D211 + "D212", # CONFIG: incompatible with D213 + "E501", # WONTFIX: we accept long strings (rest is formatted by ruff) + 'ISC001', # CONFIG: formatter + "PT", # CONFIG: Not using pytest + "FIX002", # CONFIG: we use TODO + "TD002", # CONFIG: no detailed TODO documentation is required + "TD003", # CONFIG: no detailed TODO documentation is required + "ANN", # TODO: Missing type annotations + "D", # TODO: Missing documentation + "B904", # TODO: use raise from + "PLR2004", # TODO: Magic value used in comparison + "PLW2901", # TODO: loop variable overwritten by assignment target + "N", # TODO: Naming issues + "PTH", # TODO: Not using pathlib + "RUF012", # TODO: 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 + "TID252", # TODO: Prefer absolute imports over relative imports from parent modules + "FBT", # TODO: Boolean in function definition + "S105", # TODO: Possible hardcoded password assigned + "S113", # TODO: Probable use of `requests` call without timeout + "B018", # TODO: Found useless expression. + "A001", # TODO: Variable is shadowing a Python builtin + "A002", # TODO: Function argument is shadowing a Python builtin + "A004", # TODO: Import `ConnectionError` is shadowing a Python builtin + "ERA001", # TODO: Found commented-out code + "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 + "TRY003", # TODO: Avoid specifying long messages outside the exception class + "S101", # TODO: Use of `assert` detected + "DTZ001", # TODO: `datetime.datetime()` called without a `tzinfo` argument + "DTZ005", # TODO: `datetime.datetime.now()` called without a `tz` argument + "B006", # TODO: Do not use mutable data structures for argument defaults + "B026", # TODO: Star-arg unpacking after a keyword argument is strongly discouraged + "S311", # TODO: Standard pseudo-random generators are not suitable for cryptographic purposes + "S301", # TODO: `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue + "S324", # TODO: Probable use of insecure hash functions in `hashlib` + "S318", # TODO: Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents + "PLR1704", # TODO: Redefining argument with the local name + "PERF203", # WONTFIX: This rule is only enforced for Python versions prior to 3.11 + "ISC003", # TODO: Explicitly concatenated string should be implicitly concatenated + "B028", # TODO: No explicit `stacklevel` keyword argument found + "UP031", # TODO: Use format specifiers instead of percent format + "SIM910", # TODO [*] Use `fields.get("email")` instead of `fields.get("email", None)` + "RUF021", # TODO [*] Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear + "PLW0603", # TODO: Using the global statement to update `BACKENDSCACHE` is discouraged + "TRY301", # TODO: Abstract `raise` to an inner function + "SIM401", # TODO: [*] Use `attributes.get(key, None)` instead of an `if` block + "BLE001", # TODO: Do not catch blind exception: `Exception` + "S110", # TODO: `try`-`except`-`pass` detected, consider logging the exception + "RUF010", # TODO: [*] Use explicit conversion flag + "TRY300", # TODO: Consider moving this statement to an `else` block + "PIE804", # TODO: [*] Unnecessary `dict` kwargs + "G004", # TODO: Logging statement uses f-string +] + +[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.ruff.lint.mccabe] +max-complexity = 11 # TODO: should be lower + [project.urls] Homepage = "https://github.com/python-social-auth/social-core" diff --git a/social_core/backends/bitbucket_datacenter.py b/social_core/backends/bitbucket_datacenter.py index 419761486..01aa56a8f 100644 --- a/social_core/backends/bitbucket_datacenter.py +++ b/social_core/backends/bitbucket_datacenter.py @@ -20,7 +20,7 @@ class BitbucketDataCenterOAuth2(BaseOAuth2PKCE): 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 # noqa + # 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 = [ @@ -87,14 +87,14 @@ def user_data(self, access_token, *args, **kwargs) -> dict: # 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 # noqa + # 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 # noqa + # 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}", diff --git a/social_core/tests/backends/test_linkedin.py b/social_core/tests/backends/test_linkedin.py index d1483f5ac..2fcd417d9 100644 --- a/social_core/tests/backends/test_linkedin.py +++ b/social_core/tests/backends/test_linkedin.py @@ -56,7 +56,7 @@ class BaseLinkedinTest: "name": "FooBar", "given_name": "Foo", "family_name": "Bar", - "picture": "https://media.licdn-ei.com/dms/image/C5F03AQHqK8v7tB1HCQ/profile-displayphoto-shrink_100_100/0/", # noqa: E501 + "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, From 74e20208c7f9d21bad61dd2d5fe04bec4640de86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 17:08:28 +0100 Subject: [PATCH 116/152] chore: apply assorted ruff fixes --- pyproject.toml | 5 ----- social_core/backends/grafana.py | 2 +- social_core/backends/orcid.py | 2 +- social_core/backends/saml.py | 2 +- social_core/backends/slack.py | 2 +- social_core/backends/weibo.py | 2 +- social_core/pipeline/user.py | 2 +- social_core/pipeline/utils.py | 5 ++--- 8 files changed, 8 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 10ce2e349..244dee6c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,16 +95,11 @@ ignore = [ "ISC003", # TODO: Explicitly concatenated string should be implicitly concatenated "B028", # TODO: No explicit `stacklevel` keyword argument found "UP031", # TODO: Use format specifiers instead of percent format - "SIM910", # TODO [*] Use `fields.get("email")` instead of `fields.get("email", None)` - "RUF021", # TODO [*] Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear "PLW0603", # TODO: Using the global statement to update `BACKENDSCACHE` is discouraged "TRY301", # TODO: Abstract `raise` to an inner function - "SIM401", # TODO: [*] Use `attributes.get(key, None)` instead of an `if` block "BLE001", # TODO: Do not catch blind exception: `Exception` "S110", # TODO: `try`-`except`-`pass` detected, consider logging the exception - "RUF010", # TODO: [*] Use explicit conversion flag "TRY300", # TODO: Consider moving this statement to an `else` block - "PIE804", # TODO: [*] Unnecessary `dict` kwargs "G004", # TODO: Logging statement uses f-string ] diff --git a/social_core/backends/grafana.py b/social_core/backends/grafana.py index 1c74981b9..6fd70e004 100644 --- a/social_core/backends/grafana.py +++ b/social_core/backends/grafana.py @@ -25,5 +25,5 @@ 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}"}}, + headers={"Authorization": f"Bearer {access_token}"}, ) diff --git a/social_core/backends/orcid.py b/social_core/backends/orcid.py index f1a8fb413..3b27bc96d 100644 --- a/social_core/backends/orcid.py +++ b/social_core/backends/orcid.py @@ -120,7 +120,7 @@ def user_data(self, access_token, *args, **kwargs): self.USER_ID_URL, headers={ "Content-Type": "application/json", - "Authorization": f"Bearer {str(access_token)}", + "Authorization": f"Bearer {access_token!s}", }, ) diff --git a/social_core/backends/saml.py b/social_core/backends/saml.py index 9401b9412..a9f5c4ce5 100644 --- a/social_core/backends/saml.py +++ b/social_core/backends/saml.py @@ -74,7 +74,7 @@ def get_attr(self, attributes, conf_key, default_attribute): another attribute to use. """ key = self.conf.get(conf_key, default_attribute) - value = attributes[key] if key in attributes else None + value = attributes.get(key, None) if isinstance(value, list): value = value[0] if value else None return value diff --git a/social_core/backends/slack.py b/social_core/backends/slack.py index 46757cf94..deaebc3a0 100644 --- a/social_core/backends/slack.py +++ b/social_core/backends/slack.py @@ -33,7 +33,7 @@ def get_user_details(self, response): team = response.get("team") name = user["name"] email = user.get("email") - username = email and email.split("@", 1)[0] or name + 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: diff --git a/social_core/backends/weibo.py b/social_core/backends/weibo.py index eb0cc5a6a..33e7f4ab6 100644 --- a/social_core/backends/weibo.py +++ b/social_core/backends/weibo.py @@ -55,7 +55,7 @@ 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) + 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}, diff --git a/social_core/pipeline/user.py b/social_core/pipeline/user.py index 0139444d5..1fc080e23 100644 --- a/social_core/pipeline/user.py +++ b/social_core/pipeline/user.py @@ -72,7 +72,7 @@ def create_user(strategy, details, backend, user=None, *args, **kwargs): # Allow overriding the email field if desired by application specification if backend.setting("FORCE_EMAIL_LOWERCASE", False): - emailfield = fields.get("email", None) + emailfield = fields.get("email") if emailfield: fields["email"] = emailfield.lower() diff --git a/social_core/pipeline/utils.py b/social_core/pipeline/utils.py index 981c891b7..684266cc6 100644 --- a/social_core/pipeline/utils.py +++ b/social_core/pipeline/utils.py @@ -22,9 +22,8 @@ def partial_prepare( "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} + "user": (hasattr(user, "id") and user.id) or None, + "social": (social and {"provider": social.provider, "uid": social.uid}) or None, } ) From 54102e9dfbe09b8eda09594afa367ab5f17d74c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 17:14:58 +0100 Subject: [PATCH 117/152] chore: use f-strings where applicable --- pyproject.toml | 1 - social_core/backends/classlink.py | 2 +- social_core/backends/clever.py | 4 ++-- social_core/backends/digitalocean.py | 2 +- social_core/backends/discord.py | 10 +++++----- social_core/backends/docker.py | 4 ++-- social_core/backends/drip.py | 2 +- social_core/backends/fitbit.py | 2 +- social_core/backends/google.py | 2 +- social_core/backends/mineid.py | 2 +- social_core/backends/paypal.py | 4 ++-- social_core/backends/pocket.py | 2 +- social_core/backends/podio.py | 2 +- social_core/backends/shimmering.py | 2 +- social_core/backends/slack.py | 2 +- social_core/backends/twitch.py | 2 +- social_core/backends/twitter_oauth2.py | 2 +- 17 files changed, 23 insertions(+), 24 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 244dee6c0..6ec6d3f8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,6 @@ ignore = [ "PERF203", # WONTFIX: This rule is only enforced for Python versions prior to 3.11 "ISC003", # TODO: Explicitly concatenated string should be implicitly concatenated "B028", # TODO: No explicit `stacklevel` keyword argument found - "UP031", # TODO: Use format specifiers instead of percent format "PLW0603", # TODO: Using the global statement to update `BACKENDSCACHE` is discouraged "TRY301", # TODO: Abstract `raise` to an inner function "BLE001", # TODO: Do not catch blind exception: `Exception` diff --git a/social_core/backends/classlink.py b/social_core/backends/classlink.py index b08756adc..00d021577 100644 --- a/social_core/backends/classlink.py +++ b/social_core/backends/classlink.py @@ -37,7 +37,7 @@ def get_user_details(self, response): def user_data(self, token, *args, **kwargs): """Loads user data from service""" url = "https://nodeapi.classlink.com/v2/my/info" - auth_header = {"Authorization": "Bearer %s" % token} + auth_header = {"Authorization": f"Bearer {token}"} try: return self.get_json(url, headers=auth_header) except ValueError: diff --git a/social_core/backends/clever.py b/social_core/backends/clever.py index 2f8d17236..87887e25f 100644 --- a/social_core/backends/clever.py +++ b/social_core/backends/clever.py @@ -50,11 +50,11 @@ 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": "Bearer %s" % token} + 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 = "https://api.clever.com/v3.0/users/%s" % user_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 diff --git a/social_core/backends/digitalocean.py b/social_core/backends/digitalocean.py index f0f373150..6fcf5133c 100644 --- a/social_core/backends/digitalocean.py +++ b/social_core/backends/digitalocean.py @@ -36,7 +36,7 @@ def get_user_details(self, response): def user_data(self, token, *args, **kwargs): """Loads user data from service""" url = "https://api.digitalocean.com/v2/account" - auth_header = {"Authorization": "Bearer %s" % token} + auth_header = {"Authorization": f"Bearer {token}"} try: return self.get_json(url, headers=auth_header) except ValueError: diff --git a/social_core/backends/discord.py b/social_core/backends/discord.py index 023eb6318..38b040c2e 100644 --- a/social_core/backends/discord.py +++ b/social_core/backends/discord.py @@ -9,10 +9,10 @@ class DiscordOAuth2(BaseOAuth2): name = "discord" HOSTNAME = "discord.com" - AUTHORIZATION_URL = "https://%s/api/oauth2/authorize" % HOSTNAME - ACCESS_TOKEN_URL = "https://%s/api/oauth2/token" % HOSTNAME + AUTHORIZATION_URL = f"https://{HOSTNAME}/api/oauth2/authorize" + ACCESS_TOKEN_URL = f"https://{HOSTNAME}/api/oauth2/token" ACCESS_TOKEN_METHOD = "POST" - REVOKE_TOKEN_URL = "https://%s/api/oauth2/token/revoke" % HOSTNAME + REVOKE_TOKEN_URL = f"https://{HOSTNAME}/api/oauth2/token/revoke" REVOKE_TOKEN_METHOD = "GET" DEFAULT_SCOPE = ["identify"] SCOPE_SEPARATOR = "+" @@ -26,6 +26,6 @@ def get_user_details(self, response): } def user_data(self, access_token, *args, **kwargs): - url = "https://%s/api/users/@me" % self.HOSTNAME - auth_header = {"Authorization": "Bearer %s" % access_token} + url = f"https://{self.HOSTNAME}/api/users/@me" + auth_header = {"Authorization": f"Bearer {access_token}"} return self.get_json(url, headers=auth_header) diff --git a/social_core/backends/docker.py b/social_core/backends/docker.py index ffb5e394c..56f7ff2fa 100644 --- a/social_core/backends/docker.py +++ b/social_core/backends/docker.py @@ -42,6 +42,6 @@ def user_data(self, access_token, *args, **kwargs): """Grab user profile information from Docker Hub.""" username = kwargs["response"]["username"] return self.get_json( - "https://hub.docker.com/api/v1.1/users/%s/" % username, - headers={"Authorization": "Bearer %s" % access_token}, + f"https://hub.docker.com/api/v1.1/users/{username}/", + headers={"Authorization": f"Bearer {access_token}"}, ) diff --git a/social_core/backends/drip.py b/social_core/backends/drip.py index 7c163dbb1..4c0c33fe4 100644 --- a/social_core/backends/drip.py +++ b/social_core/backends/drip.py @@ -25,5 +25,5 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): return self.get_json( "https://api.getdrip.com/v2/user", - headers={"Authorization": "Bearer %s" % access_token}, + headers={"Authorization": f"Bearer {access_token}"}, ) diff --git a/social_core/backends/fitbit.py b/social_core/backends/fitbit.py index 40b77e789..aa9391735 100644 --- a/social_core/backends/fitbit.py +++ b/social_core/backends/fitbit.py @@ -54,7 +54,7 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - auth_header = {"Authorization": "Bearer %s" % access_token} + auth_header = {"Authorization": f"Bearer {access_token}"} return self.get_json( "https://api.fitbit.com/1/user/-/profile.json", headers=auth_header )["user"] diff --git a/social_core/backends/google.py b/social_core/backends/google.py index a050ea2f0..d73674be7 100644 --- a/social_core/backends/google.py +++ b/social_core/backends/google.py @@ -45,7 +45,7 @@ def user_data(self, access_token, *args, **kwargs): return self.get_json( "https://www.googleapis.com/oauth2/v3/userinfo", headers={ - "Authorization": "Bearer %s" % access_token, + "Authorization": f"Bearer {access_token}", }, ) diff --git a/social_core/backends/mineid.py b/social_core/backends/mineid.py index 9a1fc1687..77a522f5a 100644 --- a/social_core/backends/mineid.py +++ b/social_core/backends/mineid.py @@ -19,7 +19,7 @@ def user_data(self, access_token, *args, **kwargs): return self._user_data(access_token) def _user_data(self, access_token, path=None): - url = "%(scheme)s://%(host)s/api/user" % self.get_mineid_url_params() + url = "{scheme}://{host}/api/user".format(**self.get_mineid_url_params()) return self.get_json(url, params={"access_token": access_token}) @property diff --git a/social_core/backends/paypal.py b/social_core/backends/paypal.py index 0066e13ff..a18a9a899 100644 --- a/social_core/backends/paypal.py +++ b/social_core/backends/paypal.py @@ -22,7 +22,7 @@ class PayPalOAuth2(BaseOAuth2): REDIRECT_STATE = False def user_data(self, access_token, *args, **kwargs): - auth_header = {"Authorization": "Bearer %s" % access_token} + auth_header = {"Authorization": f"Bearer {access_token}"} return self.get_json(self.USER_DATA_URL, headers=auth_header) def get_user_details(self, response): @@ -49,7 +49,7 @@ def auth_complete_params(self, state=None): } def auth_headers(self): - auth = ("%s:%s" % self.get_key_and_secret()).encode() + auth = ("{}:{}".format(*self.get_key_and_secret())).encode() return {"Authorization": b"Basic " + base64.urlsafe_b64encode(auth)} def refresh_token_params(self, token, *args, **kwargs): diff --git a/social_core/backends/pocket.py b/social_core/backends/pocket.py index 05b4e98f2..bb1f0a991 100644 --- a/social_core/backends/pocket.py +++ b/social_core/backends/pocket.py @@ -33,7 +33,7 @@ def auth_url(self): token = self.get_json(self.REQUEST_TOKEN_URL, data=data)["code"] self.strategy.session_set("pocket_request_token", token) bits = (self.AUTHORIZATION_URL, token, self.redirect_uri) - return "%s?request_token=%s&redirect_uri=%s" % bits + return "{}?request_token={}&redirect_uri={}".format(*bits) @handle_http_errors def auth_complete(self, *args, **kwargs): diff --git a/social_core/backends/podio.py b/social_core/backends/podio.py index 55f19de6d..21c5ca5ed 100644 --- a/social_core/backends/podio.py +++ b/social_core/backends/podio.py @@ -28,7 +28,7 @@ def get_user_details(self, response): response["profile"]["name"] ) return { - "username": "user_%d" % response["user"]["user_id"], + "username": f"user_{response['user']['user_id']}", "email": response["user"]["mail"], "fullname": fullname, "first_name": first_name, diff --git a/social_core/backends/shimmering.py b/social_core/backends/shimmering.py index 1e9c2626d..358adcf10 100644 --- a/social_core/backends/shimmering.py +++ b/social_core/backends/shimmering.py @@ -31,7 +31,7 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" - headers = {"Authorization": "Bearer %s" % access_token} + headers = {"Authorization": f"Bearer {access_token}"} return self.get_json( "http://developers.shimmeringverify.com/user_info/", headers=headers ) diff --git a/social_core/backends/slack.py b/social_core/backends/slack.py index deaebc3a0..712ec2bd7 100644 --- a/social_core/backends/slack.py +++ b/social_core/backends/slack.py @@ -51,7 +51,7 @@ 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": "Bearer %s" % access_token}, + headers={"Authorization": f"Bearer {access_token}"}, ) if not response.get("id", None): response["id"] = response["user"]["id"] diff --git a/social_core/backends/twitch.py b/social_core/backends/twitch.py index 1d9b3ef1d..9c47df5a6 100644 --- a/social_core/backends/twitch.py +++ b/social_core/backends/twitch.py @@ -63,7 +63,7 @@ def get_user_details(self, response): def user_data(self, access_token, *args, **kwargs): client_id, _ = self.get_key_and_secret() auth_headers = { - "Authorization": "Bearer %s" % access_token, + "Authorization": f"Bearer {access_token}", "Client-Id": client_id, } url = "https://api.twitch.tv/helix/users" diff --git a/social_core/backends/twitter_oauth2.py b/social_core/backends/twitter_oauth2.py index c3695ea2c..778c36bf6 100644 --- a/social_core/backends/twitter_oauth2.py +++ b/social_core/backends/twitter_oauth2.py @@ -99,6 +99,6 @@ def user_data(self, access_token, *args, **kwargs): response = self.get_json( "https://api.twitter.com/2/users/me", params={"user.fields": ",".join(fields)}, - headers={"Authorization": "Bearer %s" % access_token}, + headers={"Authorization": f"Bearer {access_token}"}, ) return response["data"] From 3d95a292546286dcc68253b614d4e6fbdc994351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 6 Jan 2025 17:18:44 +0100 Subject: [PATCH 118/152] chore: build string directly instead of intermediate bits tuple --- social_core/backends/pocket.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/social_core/backends/pocket.py b/social_core/backends/pocket.py index bb1f0a991..6e4bc28cb 100644 --- a/social_core/backends/pocket.py +++ b/social_core/backends/pocket.py @@ -32,8 +32,7 @@ def auth_url(self): } token = self.get_json(self.REQUEST_TOKEN_URL, data=data)["code"] self.strategy.session_set("pocket_request_token", token) - bits = (self.AUTHORIZATION_URL, token, self.redirect_uri) - return "{}?request_token={}&redirect_uri={}".format(*bits) + return f"{self.AUTHORIZATION_URL}?request_token={token}&redirect_uri={self.redirect_uri}" @handle_http_errors def auth_complete(self, *args, **kwargs): From 2c4994c8b392e30e19465e87a8e9ed1b7722008b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Wed, 8 Jan 2025 09:34:19 +0100 Subject: [PATCH 119/152] fix(ci): correctly check for version info It is now stored in pyproject.toml. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f4f862fa..9256e94cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: if: github.event_name == 'release' run: | CURRENT_TAG=${GITHUB_REF#refs/tags/} - CURRENT_VERSION=$(head -n1 social_core/__init__.py | awk '{print $3}' | sed 's/[^0-9\.]//g') + CURRENT_VERSION=$(sed -n 's/version = "\(.*\)"/\1/p' pyproject.toml) if [ "${CURRENT_VERSION}" != "${CURRENT_TAG}" ]; then echo "========================================================================" echo "Error: tag '${CURRENT_TAG}' and version '${CURRENT_VERSION}' don't match" From f351427928360290e6ea2325d4c99a6b36e9d6c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Wed, 8 Jan 2025 09:54:26 +0100 Subject: [PATCH 120/152] Remve call for maintainers from README.md See #985 985 --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index 7c49cfec5..840dce4be 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,3 @@ -> ## THIS PROJECT IS OPEN FOR MAINTAINERS -> Development on the python-social-auth projects has been stagnated for a while, -> #445 was open a long time ago to discuss this matter and a plan (failed) was -> presented to fix the situation. For that reason, I'm opening the organization to -> new maintainers that will have the proper permissions to unstuck development. -> -> Those willing to join, contact me by email with the subject `[PSA Maintainer] -> ` and please let me know what motivates you to join in such role. - # Python Social Auth - Core ![Build Status](https://github.com/python-social-auth/social-core/workflows/Flake8/badge.svg) ![Build Status](https://github.com/python-social-auth/social-core/workflows/Tests/badge.svg) From e093a9a1a9e9237bbf0b6e753adc282dc178d88c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 08:50:19 +0100 Subject: [PATCH 121/152] chore: drop unused SSLHttpAdapter --- social_core/backends/base.py | 9 ++------- social_core/utils.py | 27 --------------------------- 2 files changed, 2 insertions(+), 34 deletions(-) diff --git a/social_core/backends/base.py b/social_core/backends/base.py index 4887314e0..4f312f2fb 100644 --- a/social_core/backends/base.py +++ b/social_core/backends/base.py @@ -3,7 +3,7 @@ from requests import ConnectionError, request from ..exceptions import AuthFailed -from ..utils import SSLHttpAdapter, module_member, parse_qs, user_agent +from ..utils import module_member, parse_qs, user_agent class BaseAuth: @@ -17,7 +17,6 @@ class BaseAuth: GET_ALL_EXTRA_DATA = False REQUIRES_EMAIL_VALIDATION = False SEND_USER_AGENT = False - SSL_PROTOCOL = None def __init__(self, strategy, redirect_uri=None): self.strategy = strategy @@ -234,11 +233,7 @@ def request(self, url, method="GET", *args, **kwargs): kwargs["headers"]["User-Agent"] = self.setting("USER_AGENT") or user_agent() try: - if self.SSL_PROTOCOL: - session = SSLHttpAdapter.ssl_adapter_session(self.SSL_PROTOCOL) - response = session.request(method, url, *args, **kwargs) - else: - response = request(method, url, *args, **kwargs) + response = request(method, url, *args, **kwargs) except ConnectionError as err: raise AuthFailed(self, str(err)) response.raise_for_status() diff --git a/social_core/utils.py b/social_core/utils.py index abd494456..3186aac43 100644 --- a/social_core/utils.py +++ b/social_core/utils.py @@ -9,8 +9,6 @@ from urllib.parse import unquote, urlencode, urlparse, urlunparse import requests -from requests.adapters import HTTPAdapter -from requests.packages.urllib3.poolmanager import PoolManager import social_core @@ -24,31 +22,6 @@ social_logger = logging.getLogger("social") -class SSLHttpAdapter(HTTPAdapter): - """ " - Transport adapter that allows to use any SSL protocol. Based on: - http://requests.rtfd.org/latest/user/advanced/#example-specific-ssl-version - """ - - def __init__(self, ssl_protocol): - self.ssl_protocol = ssl_protocol - super().__init__() - - def init_poolmanager(self, connections, maxsize, block=False): - self.poolmanager = PoolManager( - num_pools=connections, - maxsize=maxsize, - block=block, - ssl_version=self.ssl_protocol, - ) - - @classmethod - def ssl_adapter_session(cls, ssl_protocol): - session = requests.Session() - session.mount("https://", SSLHttpAdapter(ssl_protocol)) - return session - - def import_module(name): __import__(name) return sys.modules[name] From 3aac098218ce81cbef6b0ca82ba285b817546d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 09:00:24 +0100 Subject: [PATCH 122/152] chore(deps): update ruff to 0.9.0 --- .pre-commit-config.yaml | 2 +- social_core/backends/email.py | 1 + social_core/backends/facebook.py | 2 +- social_core/backends/saml.py | 6 +++--- social_core/backends/vk.py | 2 +- social_core/strategy.py | 4 +--- social_core/tests/backends/oauth.py | 2 +- social_core/tests/backends/test_behance.py | 9 +++------ social_core/tests/backends/test_disqus.py | 6 ++---- social_core/tests/backends/test_foursquare.py | 3 +-- social_core/tests/backends/test_mapmyfitness.py | 11 +++++------ social_core/tests/backends/test_ngpvan.py | 4 ++-- social_core/tests/backends/test_twitter.py | 5 ++--- social_core/tests/backends/test_yahoo.py | 2 +- social_core/tests/test_exceptions.py | 2 +- 15 files changed, 26 insertions(+), 35 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bc4fb63b6..5bbc51a27 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: mixed-line-ending args: [--fix=lf] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.6 + rev: v0.9.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/social_core/backends/email.py b/social_core/backends/email.py index bd34c24cb..c869a8865 100644 --- a/social_core/backends/email.py +++ b/social_core/backends/email.py @@ -1,3 +1,4 @@ +# noqa: A005 """ Legacy Email backend, docs at: https://python-social-auth.readthedocs.io/en/latest/backends/email.html diff --git a/social_core/backends/facebook.py b/social_core/backends/facebook.py index 69bc53b96..cbba9b760 100644 --- a/social_core/backends/facebook.py +++ b/social_core/backends/facebook.py @@ -146,7 +146,7 @@ def do_auth(self, access_token, response=None, *args, **kwargs): # 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" + self, "An error occurred while retrieving users Facebook data" ) data["access_token"] = access_token diff --git a/social_core/backends/saml.py b/social_core/backends/saml.py index a9f5c4ce5..3c18bb836 100644 --- a/social_core/backends/saml.py +++ b/social_core/backends/saml.py @@ -34,9 +34,9 @@ def __init__(self, name, **kwargs): 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)' + 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): diff --git a/social_core/backends/vk.py b/social_core/backends/vk.py index 4bb395ce0..64dac1e8e 100644 --- a/social_core/backends/vk.py +++ b/social_core/backends/vk.py @@ -151,7 +151,7 @@ def auth_complete(self, *args, **kwargs): "_".join([key, self.data.get("viewer_id"), secret]).encode("utf-8") ).hexdigest() if check_key != auth_key: - raise ValueError("VK.com authentication failed: invalid " "auth key") + raise ValueError("VK.com authentication failed: invalid auth key") user_check = self.setting("USERMODE") user_id = self.data.get("viewer_id") diff --git a/social_core/strategy.py b/social_core/strategy.py index 604942f1a..ef1ce5b63 100644 --- a/social_core/strategy.py +++ b/social_core/strategy.py @@ -29,9 +29,7 @@ def render_string(self, html, context): class BaseStrategy: - ALLOWED_CHARS = ( - "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789" - ) + ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" DEFAULT_TEMPLATE_STRATEGY = BaseTemplateStrategy def __init__(self, storage=None, tpl=None): diff --git a/social_core/tests/backends/oauth.py b/social_core/tests/backends/oauth.py index 4cbb2dccc..e91264fff 100644 --- a/social_core/tests/backends/oauth.py +++ b/social_core/tests/backends/oauth.py @@ -88,7 +88,7 @@ def do_start(self): class OAuth1Test(BaseOAuthTest): request_token_body = None - raw_complete_url = "/complete/{0}/?oauth_verifier=bazqux&" "oauth_token=foobar" + raw_complete_url = "/complete/{0}/?oauth_verifier=bazqux&oauth_token=foobar" def request_token_handler(self): HTTPretty.register_uri( diff --git a/social_core/tests/backends/test_behance.py b/social_core/tests/backends/test_behance.py index 357f9ce12..2452183d9 100644 --- a/social_core/tests/backends/test_behance.py +++ b/social_core/tests/backends/test_behance.py @@ -22,18 +22,15 @@ class BehanceOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): "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", + "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", + "78": "https://www.behance.net/assets/img/profile/no-image-78.jpg", }, "id": 1010101, "occupation": "Software Developer", diff --git a/social_core/tests/backends/test_disqus.py b/social_core/tests/backends/test_disqus.py index ffcba5e66..9cd7a2751 100644 --- a/social_core/tests/backends/test_disqus.py +++ b/social_core/tests/backends/test_disqus.py @@ -28,8 +28,7 @@ class DisqusOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): "reputation": 1.231755, "avatar": { "small": { - "permalink": "https://disqus.com/api/users/avatars/" - "foobar.jpg", + "permalink": "https://disqus.com/api/users/avatars/foobar.jpg", "cache": "https://securecdn.disqus.com/uploads/" "users/453/4556/avatar32.jpg?1285535379", }, @@ -38,8 +37,7 @@ class DisqusOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): "cache": "https://securecdn.disqus.com/uploads/users/453/" "4556/avatar92.jpg?1285535379", "large": { - "permalink": "https://disqus.com/api/users/avatars/" - "foobar.jpg", + "permalink": "https://disqus.com/api/users/avatars/foobar.jpg", "cache": "https://securecdn.disqus.com/uploads/users/" "453/4556/avatar92.jpg?1285535379", }, diff --git a/social_core/tests/backends/test_foursquare.py b/social_core/tests/backends/test_foursquare.py index 9773ed6db..3c09a773c 100644 --- a/social_core/tests/backends/test_foursquare.py +++ b/social_core/tests/backends/test_foursquare.py @@ -19,8 +19,7 @@ class FoursquareOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): }, "response": { "user": { - "photo": "https://is0.4sqi.net/userpix_thumbs/" - "BYKIT01VN4T4BISN.jpg", + "photo": "https://is0.4sqi.net/userpix_thumbs/BYKIT01VN4T4BISN.jpg", "pings": False, "homeCity": "Foo, Bar", "id": "1010101", diff --git a/social_core/tests/backends/test_mapmyfitness.py b/social_core/tests/backends/test_mapmyfitness.py index e4e605b95..5ed7bca24 100644 --- a/social_core/tests/backends/test_mapmyfitness.py +++ b/social_core/tests/backends/test_mapmyfitness.py @@ -36,12 +36,12 @@ class MapMyFitnessOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): "_links": { "stats": [ { - "href": "/v7.0/user_stats/112233/?" "aggregate_by_period=month", + "href": "/v7.0/user_stats/112233/?aggregate_by_period=month", "id": "112233", "name": "month", }, { - "href": "/v7.0/user_stats/112233/?" "aggregate_by_period=year", + "href": "/v7.0/user_stats/112233/?aggregate_by_period=year", "id": "112233", "name": "year", }, @@ -51,13 +51,12 @@ class MapMyFitnessOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): "name": "day", }, { - "href": "/v7.0/user_stats/112233/?" "aggregate_by_period=week", + "href": "/v7.0/user_stats/112233/?aggregate_by_period=week", "id": "112233", "name": "week", }, { - "href": "/v7.0/user_stats/112233/?" - "aggregate_by_period=lifetime", + "href": "/v7.0/user_stats/112233/?aggregate_by_period=lifetime", "id": "112233", "name": "lifetime", }, @@ -88,7 +87,7 @@ class MapMyFitnessOAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): ], "documentation": [{"href": "https://www.mapmyapi.com/docs/User"}], "workouts": [ - {"href": "/v7.0/workout/?user=112233&" "order_by=-start_datetime"} + {"href": "/v7.0/workout/?user=112233&order_by=-start_datetime"} ], "deactivation": [{"href": "/v7.0/user_deactivation/"}], "self": [{"href": "/v7.0/user/112233/", "id": "112233"}], diff --git a/social_core/tests/backends/test_ngpvan.py b/social_core/tests/backends/test_ngpvan.py index 66ca3b513..5f7122366 100644 --- a/social_core/tests/backends/test_ngpvan.py +++ b/social_core/tests/backends/test_ngpvan.py @@ -62,9 +62,9 @@ class NGPVANActionIDOpenIDTest(OpenIdTest): "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/firs" "t", + "openid.alias3.type.alias3": "http://openid.net/schema/namePerson/first", "openid.alias3.value.alias3": "John", - "openid.alias3.type.alias4": "http://openid.net/schema/namePerson/las" "t", + "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", diff --git a/social_core/tests/backends/test_twitter.py b/social_core/tests/backends/test_twitter.py index 04c4e245e..ebd6220a3 100644 --- a/social_core/tests/backends/test_twitter.py +++ b/social_core/tests/backends/test_twitter.py @@ -6,7 +6,7 @@ class TwitterOAuth1Test(OAuth1Test, OAuth1AuthUrlTestMixin): backend_path = "social_core.backends.twitter.TwitterOAuth" - user_data_url = "https://api.twitter.com/1.1/account/" "verify_credentials.json" + 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( @@ -128,8 +128,7 @@ def test_partial_pipeline(self): 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" + "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"}) diff --git a/social_core/tests/backends/test_yahoo.py b/social_core/tests/backends/test_yahoo.py index 4ecaeb88c..872bb1aa3 100644 --- a/social_core/tests/backends/test_yahoo.py +++ b/social_core/tests/backends/test_yahoo.py @@ -9,7 +9,7 @@ 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" + 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( diff --git a/social_core/tests/test_exceptions.py b/social_core/tests/test_exceptions.py index 5ed5811be..2f4336253 100644 --- a/social_core/tests/test_exceptions.py +++ b/social_core/tests/test_exceptions.py @@ -86,7 +86,7 @@ class AuthCanceledWithExtraMessageTest(BaseExceptionTestCase): class AuthUnknownErrorTest(BaseExceptionTestCase): exception = AuthUnknownError("foobar", "some error") - expected_message = "An unknown error happened while " "authenticating some error" + expected_message = "An unknown error happened while authenticating some error" class AuthStateForbiddenTest(BaseExceptionTestCase): From 3cce6f3d1120f8e5bc124ba8d7a686960c53448a Mon Sep 17 00:00:00 2001 From: Chris Rose Date: Fri, 10 Jan 2025 00:13:48 -0800 Subject: [PATCH 123/152] feat: Type Annotate the *heck* out of social-core (#986) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add py.typed and pyright to the build This enables typing in social_core and gives us the tools for testing * Many type annotation updates - ignore several errors that are endemic to mixin-type test strategies - in several cases assert that tests are set up correctly to give the type checker some help - for tidiness, mark some test-like class names as not-a-test * Most backends updated * Add type testing to the test action workflow * Ignore two very common type errors in the codebase * Restrict lxml to 5.2 https://github.com/xmlsec/python-xmlsec/issues/320 * Undo a TODO that is not actually a bug * Correctly use __future__ annotations in models.py Co-authored-by: Michal Čihař --- .github/workflows/test.yml | 37 +++++++++++++++++++ pyproject.toml | 5 +++ pyrightconfig.json | 4 ++ social_core/actions.py | 2 + social_core/backends/apple.py | 15 ++++++-- social_core/backends/auth0.py | 1 + social_core/backends/azuread_b2c.py | 5 ++- social_core/backends/azuread_tenant.py | 2 +- social_core/backends/base.py | 11 ++++-- social_core/backends/bitbucket.py | 6 +-- social_core/backends/clef.py | 2 +- social_core/backends/discogs.py | 2 +- social_core/backends/discourse.py | 2 +- social_core/backends/facebook.py | 1 + social_core/backends/five_hundred_px.py | 2 +- social_core/backends/gae.py | 2 +- social_core/backends/github.py | 2 +- social_core/backends/github_enterprise.py | 4 +- social_core/backends/google.py | 4 +- social_core/backends/khanacademy.py | 6 +-- social_core/backends/mediawiki.py | 7 ++-- social_core/backends/oauth.py | 28 ++++++++------ social_core/backends/okta.py | 2 +- social_core/backends/open_id.py | 7 +++- social_core/backends/open_id_connect.py | 4 +- social_core/backends/ping.py | 2 +- social_core/backends/qiita.py | 4 +- social_core/backends/rdio.py | 3 +- social_core/backends/saml.py | 6 ++- social_core/backends/scistarter.py | 4 +- social_core/backends/shopify.py | 2 +- social_core/backends/twitch.py | 2 + social_core/backends/udata.py | 1 + social_core/backends/uffd.py | 4 +- social_core/backends/yahoo.py | 2 +- social_core/py.typed | 0 social_core/storage.py | 18 +++++++-- social_core/tests/backends/base.py | 2 + social_core/tests/backends/legacy.py | 11 ++++-- social_core/tests/backends/oauth.py | 4 ++ social_core/tests/backends/open_id.py | 8 +++- social_core/tests/backends/test_apple.py | 1 + social_core/tests/backends/test_auth0.py | 1 + .../tests/backends/test_azuread_b2c.py | 3 +- .../backends/test_bitbucket_datacenter.py | 2 + social_core/tests/backends/test_cas.py | 2 + social_core/tests/backends/test_cognito.py | 3 ++ social_core/tests/backends/test_dummy.py | 2 + .../tests/backends/test_egi_checkin.py | 2 + social_core/tests/backends/test_etsy.py | 2 + social_core/tests/backends/test_google.py | 2 + social_core/tests/backends/test_linkedin.py | 2 + social_core/tests/backends/test_okta.py | 2 + .../tests/backends/test_open_id_connect.py | 6 ++- social_core/tests/backends/test_saml.py | 11 +++++- social_core/tests/backends/test_stripe.py | 2 + .../tests/backends/test_twitter_oauth2.py | 2 + social_core/tests/backends/test_vault.py | 2 + social_core/tests/backends/test_yahoo.py | 2 + social_core/tests/models.py | 30 +++++++++++++-- social_core/tests/strategy.py | 4 ++ social_core/tests/test_exceptions.py | 3 ++ social_core/tests/test_storage.py | 2 +- social_core/tests/test_utils.py | 1 + social_core/utils.py | 7 +++- tox.ini | 14 ++++++- 66 files changed, 272 insertions(+), 74 deletions(-) create mode 100644 pyrightconfig.json create mode 100644 social_core/py.typed diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d85563bd8..e9535f1c9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,43 @@ name: Tests on: [push, pull_request] jobs: + types: + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + python-version: + - '3.9' + - '3.13' + env: + PYTHON_VERSION: ${{ matrix.python-version }} + PYTHONUNBUFFERED: 1 + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: requirements*.txt + + - name: Install System dependencies + run: | + sudo apt-get update + sudo apt-get install -qq -y --no-install-recommends libxmlsec1-dev swig + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install tox + + - name: Type check with tox + run: tox -e "py${PYTHON_VERSION/\./}-pyright" + test: runs-on: ubuntu-22.04 strategy: diff --git a/pyproject.toml b/pyproject.toml index 6ec6d3f8f..b5fdf093e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,6 +119,8 @@ Homepage = "https://github.com/python-social-auth/social-core" [project.optional-dependencies] saml = [ "python3-saml>=1.5.0", + # pinned to 5.2 until a new wheel of xmlsec is released + "lxml~=5.2.1", ] azuread = [ "cryptography>=2.1.1", @@ -140,6 +142,7 @@ dev = [ "pytest-cov>=2.7.1", # pinned because of https://github.com/gabrielfalcao/HTTPretty/issues/484 "urllib3~=2.2.0", + "pyright>=1.1.391", ] [build-system] @@ -155,4 +158,6 @@ dev = [ "pytest-cov>=2.7.1", # pinned because of https://github.com/gabrielfalcao/HTTPretty/issues/484 "urllib3~=2.2.0", + "pyright>=1.1.391", + "pytest-xdist>=3.6.1", ] diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 000000000..00901b392 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,4 @@ +{ + "reportOptionalMemberAccess": "none", + "reportPossiblyUnboundVariable": "none" +} diff --git a/social_core/actions.py b/social_core/actions.py index e9a0d0afc..289da925f 100644 --- a/social_core/actions.py +++ b/social_core/actions.py @@ -101,6 +101,8 @@ def do_complete(backend, login, user=None, redirect_name="next", *args, **kwargs 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}" diff --git a/social_core/backends/apple.py b/social_core/backends/apple.py index 6d58bc2a2..297a78980 100644 --- a/social_core/backends/apple.py +++ b/social_core/backends/apple.py @@ -21,8 +21,11 @@ login """ +from __future__ import annotations + import json import time +from typing import TYPE_CHECKING import jwt from jwt.algorithms import RSAAlgorithm @@ -31,6 +34,9 @@ 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" @@ -96,7 +102,7 @@ def get_key_and_secret(self): client_secret = self.generate_client_secret() return client_id, client_secret - def get_apple_jwk(self, kid=None): + def get_apple_jwk(self, kid=None) -> str | JWKDict: """ Return requested Apple public key or all available. """ @@ -107,7 +113,9 @@ def get_apple_jwk(self, kid=None): if kid: return json.dumps(next(key for key in keys if key["kid"] == kid)) - return (json.dumps(key) for key in keys) + # 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): """ @@ -120,9 +128,10 @@ def decode_id_token(self, id_token): 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, + key=public_key, # type: ignore[reportArgumentType] audience=self.get_audience(), algorithms=["RS256"], ) diff --git a/social_core/backends/auth0.py b/social_core/backends/auth0.py index c2d188138..74a4dc9c1 100644 --- a/social_core/backends/auth0.py +++ b/social_core/backends/auth0.py @@ -61,6 +61,7 @@ def get_user_details(self, response): else: break else: + assert signature_error is not None # raise last esception found during iteration raise signature_error diff --git a/social_core/backends/azuread_b2c.py b/social_core/backends/azuread_b2c.py index 0fd27be84..bd4ad2a39 100644 --- a/social_core/backends/azuread_b2c.py +++ b/social_core/backends/azuread_b2c.py @@ -128,7 +128,10 @@ 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)) - return pub_key.public_bytes( + + # 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, ) diff --git a/social_core/backends/azuread_tenant.py b/social_core/backends/azuread_tenant.py index 1e55edb3e..ba9ec666a 100644 --- a/social_core/backends/azuread_tenant.py +++ b/social_core/backends/azuread_tenant.py @@ -97,7 +97,7 @@ def user_data(self, access_token, *args, **kwargs): return jwt_decode( id_token, - key=certificate.public_key(), + key=certificate.public_key(), # type: ignore[reportArgumentType] algorithms=["RS256"], audience=self.setting("KEY"), ) diff --git a/social_core/backends/base.py b/social_core/backends/base.py index 4f312f2fb..eee1b5daf 100644 --- a/social_core/backends/base.py +++ b/social_core/backends/base.py @@ -1,4 +1,5 @@ import time +from typing import Any from requests import ConnectionError, request @@ -118,7 +119,9 @@ def run_pipeline(self, pipeline, pipeline_index=0, *args, **kwargs): out.update(result) return out - def extra_data(self, user, uid, response, details=None, *args, **kwargs): + def extra_data( + self, user, uid, response, details=None, *args, **kwargs + ) -> dict[str, Any]: """Return default extra data to store in extra_data field""" data = { # store the last time authentication toke place @@ -137,9 +140,9 @@ def extra_data(self, user, uid, response, details=None, *args, **kwargs): size = len(entry) if size >= 1 and size <= 3: if size == 3: - name, alias, discard = entry + name, alias, discard = entry # type: ignore[reportAssignmentType] elif size == 2: - (name, alias), discard = entry, False + (name, alias), discard = entry, False # type: ignore[reportAssignmentType] elif size == 1: name = alias = entry[0] discard = False @@ -213,7 +216,7 @@ def auth_extra_arguments(self): ) return extra_arguments - def uses_redirect(self): + def uses_redirect(self) -> bool: """Return True if this provider uses redirect url method, otherwise return false.""" return True diff --git a/social_core/backends/bitbucket.py b/social_core/backends/bitbucket.py index bc65759ae..ca9ef9f3f 100644 --- a/social_core/backends/bitbucket.py +++ b/social_core/backends/bitbucket.py @@ -12,13 +12,13 @@ class BitbucketOAuthBase: def get_user_id(self, details, response): id_key = self.ID_KEY - if self.setting("USERNAME_AS_ID", False): + if self.setting("USERNAME_AS_ID", False): # type: ignore[reportAttributeAccessIssue] 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"]) + fullname, first_name, last_name = self.get_user_names(response["display_name"]) # type: ignore[reportAttributeAccessIssue] return { "username": response.get("username", ""), @@ -38,7 +38,7 @@ def user_data(self, access_token, *args, **kwargs): if address["is_primary"]: break - if self.setting("VERIFIED_EMAILS_ONLY", False) and not address["is_confirmed"]: + if self.setting("VERIFIED_EMAILS_ONLY", False) and not address["is_confirmed"]: # type: ignore[reportAttributeAccessIssue] raise AuthForbidden(self, "Bitbucket account has no verified email") user = self._get_user(access_token) diff --git a/social_core/backends/clef.py b/social_core/backends/clef.py index f6e792d0b..764a80035 100644 --- a/social_core/backends/clef.py +++ b/social_core/backends/clef.py @@ -24,7 +24,7 @@ def auth_params(self, *args, **kwargs): params["redirect_url"] = params.pop("redirect_uri") return params - def get_user_id(self, response, details): + def get_user_id(self, response, details): # type: ignore[reportIncompatibleMethodOverride] return details.get("info").get("id") def get_user_details(self, response): diff --git a/social_core/backends/discogs.py b/social_core/backends/discogs.py index dad46eefc..93de5e1e1 100644 --- a/social_core/backends/discogs.py +++ b/social_core/backends/discogs.py @@ -19,7 +19,7 @@ class DiscogsOAuth1(BaseOAuth1): 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): + def get_user_details(self, user_data): # type: ignore[reportIncompatibleMethodOverride] return { "username": user_data["username"], "id": user_data["id"], diff --git a/social_core/backends/discourse.py b/social_core/backends/discourse.py index 62dd91b02..8ebe99e29 100644 --- a/social_core/backends/discourse.py +++ b/social_core/backends/discourse.py @@ -53,7 +53,7 @@ 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) + return self.strategy.storage.nonce.get_nonce(self.setting("SERVER_URL"), nonce) def delete_nonce(self, nonce): self.strategy.storage.nonce.delete(nonce) diff --git a/social_core/backends/facebook.py b/social_core/backends/facebook.py index cbba9b760..9a628027b 100644 --- a/social_core/backends/facebook.py +++ b/social_core/backends/facebook.py @@ -191,6 +191,7 @@ def auth_complete(self, *args, **kwargs): 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) diff --git a/social_core/backends/five_hundred_px.py b/social_core/backends/five_hundred_px.py index 317701bce..dcc6426f8 100644 --- a/social_core/backends/five_hundred_px.py +++ b/social_core/backends/five_hundred_px.py @@ -14,7 +14,7 @@ class FiveHundredPxOAuth(BaseOAuth1): 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): + 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 { diff --git a/social_core/backends/gae.py b/social_core/backends/gae.py index 1fc3c1a5b..35436d75c 100644 --- a/social_core/backends/gae.py +++ b/social_core/backends/gae.py @@ -2,7 +2,7 @@ Google App Engine support using User API """ -from google.appengine.api import users +from google.appengine.api import users # type: ignore[reportMissingImports] from ..exceptions import AuthException from .base import BaseAuth diff --git a/social_core/backends/github.py b/social_core/backends/github.py index 14bb7931b..521678ec7 100644 --- a/social_core/backends/github.py +++ b/social_core/backends/github.py @@ -30,7 +30,7 @@ class GithubOAuth2(BaseOAuth2): ("refresh_token", "refresh_token"), ] - def api_url(self): + def api_url(self) -> str: return self.API_URL def get_user_details(self, response): diff --git a/social_core/backends/github_enterprise.py b/social_core/backends/github_enterprise.py index 907177a71..dd1a3e11c 100644 --- a/social_core/backends/github_enterprise.py +++ b/social_core/backends/github_enterprise.py @@ -11,7 +11,7 @@ class GithubEnterpriseMixin: def api_url(self): - return append_slash(self.setting("API_URL")) + return append_slash(self.setting("API_URL")) # type: ignore[reportAttributeAccessIssue] def authorization_url(self): return self._url("login/oauth/authorize") @@ -20,7 +20,7 @@ def access_token_url(self): return self._url("login/oauth/access_token") def _url(self, path): - return urljoin(append_slash(self.setting("URL")), path) + return urljoin(append_slash(self.setting("URL")), path) # type: ignore[reportAttributeAccessIssue] class GithubEnterpriseOAuth2(GithubEnterpriseMixin, GithubOAuth2): diff --git a/social_core/backends/google.py b/social_core/backends/google.py index d73674be7..c763a0f8d 100644 --- a/social_core/backends/google.py +++ b/social_core/backends/google.py @@ -3,12 +3,14 @@ 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: +class BaseGoogleAuth(BaseAuth): def get_user_id(self, details, response): """Use google email as unique id""" if self.setting("USE_UNIQUE_USER_ID", False): diff --git a/social_core/backends/khanacademy.py b/social_core/backends/khanacademy.py index 16c65cf9d..120dca4df 100644 --- a/social_core/backends/khanacademy.py +++ b/social_core/backends/khanacademy.py @@ -44,13 +44,13 @@ def unauthorized_token_request(self): callback_uri=self.get_redirect_uri(state), signature_method=SIGNATURE_HMAC, signature_type=SIGNATURE_TYPE_QUERY, - decoding=None, + decoding=None, # type: ignore[reportArgumentType] ) url = self.REQUEST_TOKEN_URL + "?" + urlencode(params) url, _, _ = auth.client.sign(url) return url - def oauth_auth(self, token=None, oauth_verifier=None): + def oauth_auth(self, token=None, oauth_verifier=None): # type: ignore[reportIncompatibleMethodOverride] key, secret = self.get_key_and_secret() oauth_verifier = oauth_verifier or self.data.get("oauth_verifier") token = token or {} @@ -64,7 +64,7 @@ def oauth_auth(self, token=None, oauth_verifier=None): verifier=oauth_verifier, signature_method=SIGNATURE_HMAC, signature_type=SIGNATURE_TYPE_QUERY, - decoding=None, + decoding=None, # type: ignore[reportArgumentType] ) diff --git a/social_core/backends/mediawiki.py b/social_core/backends/mediawiki.py index f6dd1a663..f027f345c 100644 --- a/social_core/backends/mediawiki.py +++ b/social_core/backends/mediawiki.py @@ -63,8 +63,7 @@ def oauth_authorization_request(self, token): """ if not isinstance(token, dict): token = parse_qs(token) - - oauth_token = token.get(self.OAUTH_TOKEN_PARAMETER_NAME)[0] + 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") @@ -93,8 +92,8 @@ def access_token(self, 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] - oauth_token_secret = credentials.get(b"oauth_token_secret")[0] + 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() diff --git a/social_core/backends/oauth.py b/social_core/backends/oauth.py index f139186f7..52d6d3de8 100644 --- a/social_core/backends/oauth.py +++ b/social_core/backends/oauth.py @@ -1,5 +1,8 @@ +from __future__ import annotations + import base64 import hashlib +from typing import TYPE_CHECKING, Any from urllib.parse import urlencode from oauthlib.oauth1 import SIGNATURE_TYPE_AUTH_HEADER @@ -23,6 +26,9 @@ ) from .base import BaseAuth +if TYPE_CHECKING: + from collections.abc import Mapping, MutableMapping + class OAuthAuth(BaseAuth): """OAuth authentication backend base class. @@ -124,23 +130,23 @@ def get_scope_argument(self): param[self.SCOPE_PARAMETER_NAME] = self.SCOPE_SEPARATOR.join(scope) return param - def user_data(self, access_token, *args, **kwargs): + 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): + def authorization_url(self) -> str: return self.AUTHORIZATION_URL - def access_token_url(self): + def access_token_url(self) -> str: return self.ACCESS_TOKEN_URL - def revoke_token_url(self, token, uid): + def revoke_token_url(self, token, uid) -> str | None: return self.REVOKE_TOKEN_URL - def revoke_token_params(self, token, uid): + def revoke_token_params(self, token, uid) -> dict[str, Any]: return {} - def revoke_token_headers(self, token, uid): + def revoke_token_headers(self, token, uid) -> dict[str, Any]: return {} def process_revoke_token_response(self, response): @@ -178,7 +184,7 @@ class BaseOAuth1(OAuthAuth): REDIRECT_URI_PARAMETER_NAME = "redirect_uri" UNATHORIZED_TOKEN_SUFIX = "unauthorized_token_name" - def auth_url(self): + def auth_url(self) -> str | bytes | None: """Return redirect url""" token = self.set_unauthorized_token() return self.oauth_authorization_request(token) @@ -334,10 +340,10 @@ class BaseOAuth2(OAuthAuth): STATE_PARAMETER = True USE_BASIC_AUTH = False - def use_basic_auth(self): + def use_basic_auth(self) -> bool: return self.USE_BASIC_AUTH - def auth_params(self, state=None): + def auth_params(self, state=None) -> MutableMapping[str, Any]: 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: @@ -346,7 +352,7 @@ def auth_params(self, state=None): params["response_type"] = self.RESPONSE_TYPE return params - def auth_url(self): + def auth_url(self) -> str | bytes | None: """Return redirect url""" state = self.get_or_create_state() params = self.auth_params(state) @@ -380,7 +386,7 @@ def auth_complete_credentials(self): return self.get_key_and_secret() return None - def auth_headers(self): + def auth_headers(self) -> Mapping[str, str | bytes]: return { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", diff --git a/social_core/backends/okta.py b/social_core/backends/okta.py index 0aee79b63..96b74be67 100644 --- a/social_core/backends/okta.py +++ b/social_core/backends/okta.py @@ -9,7 +9,7 @@ from .oauth import BaseOAuth2 -class OktaMixin: +class OktaMixin(BaseOAuth2): def api_url(self): return append_slash(self.setting("API_URL")) diff --git a/social_core/backends/open_id.py b/social_core/backends/open_id.py index ed694abde..ba0a28eb0 100644 --- a/social_core/backends/open_id.py +++ b/social_core/backends/open_id.py @@ -63,7 +63,8 @@ def values_from_response(self, response, sreg_names=None, ax_names=None): # Use Simple Registration attributes if provided if sreg_names: - resp = sreg.SRegResponse.fromSuccessResponse(response) + # 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 @@ -71,11 +72,13 @@ def values_from_response(self, response, sreg_names=None, ax_names=None): # Use Attribute Exchange attributes if provided if ax_names: - resp = ax.FetchResponse.fromSuccessResponse(response) + # 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): diff --git a/social_core/backends/open_id_connect.py b/social_core/backends/open_id_connect.py index 34fbde3d4..3c9108a19 100644 --- a/social_core/backends/open_id_connect.py +++ b/social_core/backends/open_id_connect.py @@ -141,7 +141,7 @@ def get_and_store_nonce(self, url, state): def get_nonce(self, nonce): try: - return self.strategy.storage.association.get( + return self.strategy.storage.association.get_association( server_url=self.authorization_url(), handle=nonce )[0] except IndexError: @@ -184,7 +184,7 @@ def find_valid_key(self, id_token): # 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() + self.get_jwks_keys.invalidate() # type: ignore[reportFunctionMemberAccess] keys = self.get_jwks_keys() for key in keys: diff --git a/social_core/backends/ping.py b/social_core/backends/ping.py index 5d6b0d230..5ef10a1bf 100644 --- a/social_core/backends/ping.py +++ b/social_core/backends/ping.py @@ -3,7 +3,7 @@ """ from jose import jwk, jwt -from jose.jwt import ExpiredSignatureError, JWTClaimsError, JWTError +from jose.exceptions import ExpiredSignatureError, JWTClaimsError, JWTError from jose.utils import base64url_decode from social_core.backends.open_id_connect import OpenIdConnectAuth diff --git a/social_core/backends/qiita.py b/social_core/backends/qiita.py index 1202a3727..cc14d56e3 100644 --- a/social_core/backends/qiita.py +++ b/social_core/backends/qiita.py @@ -43,7 +43,9 @@ class QiitaOAuth2(BaseOAuth2): ("image_monthly_upload_remaining", "image_monthly_upload_remaining"), ] - def auth_complete_params(self, state=None): + # 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"] diff --git a/social_core/backends/rdio.py b/social_core/backends/rdio.py index 8acf810c8..71735756d 100644 --- a/social_core/backends/rdio.py +++ b/social_core/backends/rdio.py @@ -47,7 +47,8 @@ def user_data(self, access_token, *args, **kwargs): "extras": "username,displayName,streamRegion", } request = self.oauth_request(access_token, RDIO_API, params, method="POST") - return self.get_json(request.url, method="POST", data=request.to_postdata())[ + # TODO: I don't think to_postdata exists. + return self.get_json(request.url, method="POST", data=request.to_postdata())[ # type: ignore[reportAttributeAccessIssue] "result" ] diff --git a/social_core/backends/saml.py b/social_core/backends/saml.py index 3c18bb836..c1e933669 100644 --- a/social_core/backends/saml.py +++ b/social_core/backends/saml.py @@ -10,8 +10,10 @@ import json -from onelogin.saml2.auth import OneLogin_Saml2_Auth -from onelogin.saml2.settings import OneLogin_Saml2_Settings +from onelogin.saml2.auth import OneLogin_Saml2_Auth # type: ignore reportMissingImports +from onelogin.saml2.settings import ( # type: ignore reportMissingImports + OneLogin_Saml2_Settings, +) from ..exceptions import AuthFailed, AuthMissingParameter from .base import BaseAuth diff --git a/social_core/backends/scistarter.py b/social_core/backends/scistarter.py index c6b91ac4a..14b04e60b 100644 --- a/social_core/backends/scistarter.py +++ b/social_core/backends/scistarter.py @@ -45,9 +45,11 @@ def user_data(self, access_token, *args, **kwards): 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), + auth=self.oauth_auth(token), # type: ignore[reportAttributeAccessIssue] method=self.ACCESS_TOKEN_METHOD, ) diff --git a/social_core/backends/shopify.py b/social_core/backends/shopify.py index f59333066..abef6d8f3 100644 --- a/social_core/backends/shopify.py +++ b/social_core/backends/shopify.py @@ -3,7 +3,7 @@ https://python-social-auth.readthedocs.io/en/latest/backends/shopify.html """ -import imp +import imp # type: ignore[reportMissingImports] from ..exceptions import AuthCanceled, AuthFailed from ..utils import handle_http_errors diff --git a/social_core/backends/twitch.py b/social_core/backends/twitch.py index 9c47df5a6..63a601753 100644 --- a/social_core/backends/twitch.py +++ b/social_core/backends/twitch.py @@ -25,6 +25,8 @@ def auth_params(self, state=None): 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"], diff --git a/social_core/backends/udata.py b/social_core/backends/udata.py index 2c56a7587..d1315f77f 100644 --- a/social_core/backends/udata.py +++ b/social_core/backends/udata.py @@ -15,6 +15,7 @@ class UdataBaseOAuth2(BaseOAuth2): REDIRECT_STATE = False DEFAULT_SCOPE = ["default"] ACCESS_TOKEN_METHOD = "POST" + USER_DATA_URL = None def get_user_details(self, response): """Return user details from Udata account.""" diff --git a/social_core/backends/uffd.py b/social_core/backends/uffd.py index 94c4ec083..3cf4543aa 100644 --- a/social_core/backends/uffd.py +++ b/social_core/backends/uffd.py @@ -1,3 +1,4 @@ +from typing import Any from urllib.parse import urlencode from .oauth import BaseOAuth2 @@ -39,7 +40,8 @@ def user_data(self, access_token, *args, **kwargs): """Loads user data from service""" url = self.userinfo_url() + "?" + urlencode({"access_token": access_token}) try: - return self.get_json(url) + user_data: dict[str, Any] = self.get_json(url) + return user_data except ValueError: return None diff --git a/social_core/backends/yahoo.py b/social_core/backends/yahoo.py index 20c18da98..fba780262 100644 --- a/social_core/backends/yahoo.py +++ b/social_core/backends/yahoo.py @@ -72,7 +72,7 @@ class YahooOAuth2(BaseOAuth2): ("token_type", "token_type"), ] - def get_user_names(self, first_name, last_name): + 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 diff --git a/social_core/py.typed b/social_core/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/social_core/storage.py b/social_core/storage.py index 7116ca317..af4663472 100644 --- a/social_core/storage.py +++ b/social_core/storage.py @@ -4,6 +4,7 @@ import re import uuid import warnings +from abc import abstractmethod from datetime import datetime, timedelta, timezone from openid.association import Association as OpenIdAssociation @@ -23,6 +24,9 @@ class UserMixin: uid = None extra_data = None + @abstractmethod + def save(self): ... + def get_backend(self, strategy): return strategy.get_backend_class(self.provider) @@ -200,7 +204,7 @@ def use(cls, server_url, timestamp, salt): raise NotImplementedError("Implement in subclass") @classmethod - def get(cls, server_url, salt): + def get_nonce(cls, server_url, salt): """Retrieve a Nonce instance""" raise NotImplementedError("Implement in subclass") @@ -226,7 +230,10 @@ def oids(cls, server_url, handle=None): if handle is not None: kwargs["handle"] = handle return sorted( - ((assoc.id, cls.openid_association(assoc)) for assoc in cls.get(**kwargs)), + ( + (assoc.id, cls.openid_association(assoc)) + for assoc in cls.get_association(**kwargs) + ), key=lambda x: x[1].issued, reverse=True, ) @@ -250,7 +257,7 @@ def store(cls, server_url, association): raise NotImplementedError("Implement in subclass") @classmethod - def get(cls, *args, **kwargs): + def get_association(cls, *args, **kwargs): """Get an Association instance""" raise NotImplementedError("Implement in subclass") @@ -265,6 +272,9 @@ class CodeMixin: code = "" verified = False + @abstractmethod + def save(self): ... + def verify(self): self.verified = True self.save() @@ -289,7 +299,7 @@ def get_code(cls, code): class PartialMixin: token = "" - data = "" + data = {} next_step = "" backend = "" diff --git a/social_core/tests/backends/base.py b/social_core/tests/backends/base.py index c64a70c11..286920cec 100644 --- a/social_core/tests/backends/base.py +++ b/social_core/tests/backends/base.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + import unittest import requests diff --git a/social_core/tests/backends/legacy.py b/social_core/tests/backends/legacy.py index 7453a3ae2..41d5f789f 100644 --- a/social_core/tests/backends/legacy.py +++ b/social_core/tests/backends/legacy.py @@ -24,21 +24,24 @@ def extra_settings(self): 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" + HTTPretty.register_uri( HTTPretty.GET, start_url, status=200, - body=self.form.format(self.complete_url), + body=self.form.format(complete_url), ) HTTPretty.register_uri( HTTPretty.POST, - self.complete_url, + complete_url, status=200, body=self.response_body, content_type="application/x-www-form-urlencoded", ) response = requests.get(start_url) - self.assertEqual(response.text, self.form.format(self.complete_url)) - response = requests.post(self.complete_url, data=parse_qs(self.response_body)) + self.assertEqual(response.text, self.form.format(complete_url)) + response = requests.post(complete_url, data=parse_qs(self.response_body)) self.strategy.set_request_data(parse_qs(response.text), self.backend) return self.backend.complete() diff --git a/social_core/tests/backends/oauth.py b/social_core/tests/backends/oauth.py index e91264fff..f98fb6526 100644 --- a/social_core/tests/backends/oauth.py +++ b/social_core/tests/backends/oauth.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + from unittest.mock import patch from urllib.parse import urlparse @@ -21,6 +23,7 @@ class BaseOAuthTest(BaseBackendTest): expected_username = "" 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", @@ -91,6 +94,7 @@ class OAuth1Test(BaseOAuthTest): 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" HTTPretty.register_uri( self._method(self.backend.REQUEST_TOKEN_METHOD), self.backend.REQUEST_TOKEN_URL, diff --git a/social_core/tests/backends/open_id.py b/social_core/tests/backends/open_id.py index 67885becb..b0723b5d2 100644 --- a/social_core/tests/backends/open_id.py +++ b/social_core/tests/backends/open_id.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + import sys from html.parser import HTMLParser @@ -92,10 +94,12 @@ def do_start(self): 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" HTTPretty.register_uri( - HTTPretty.POST, form.get("action"), status=200, body=self.server_response + HTTPretty.POST, action, status=200, body=self.server_response ) - response = requests.post(form.get("action"), data=inputs) + response = requests.post(action, data=inputs) self.strategy.set_request_data(parse_qs(response.content), self.backend) HTTPretty.register_uri( HTTPretty.POST, form.get("action"), status=200, body="is_valid:true\n" diff --git a/social_core/tests/backends/test_apple.py b/social_core/tests/backends/test_apple.py index c6471230a..bbb3b0765 100644 --- a/social_core/tests/backends/test_apple.py +++ b/social_core/tests/backends/test_apple.py @@ -30,6 +30,7 @@ class AppleIdTest(OAuth2Test, BaseAuthUrlTestMixin): 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", diff --git a/social_core/tests/backends/test_auth0.py b/social_core/tests/backends/test_auth0.py index e77723b0f..241ef26ae 100644 --- a/social_core/tests/backends/test_auth0.py +++ b/social_core/tests/backends/test_auth0.py @@ -56,6 +56,7 @@ class Auth0OAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): 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 diff --git a/social_core/tests/backends/test_azuread_b2c.py b/social_core/tests/backends/test_azuread_b2c.py index 709b7da0c..d87d646bb 100644 --- a/social_core/tests/backends/test_azuread_b2c.py +++ b/social_core/tests/backends/test_azuread_b2c.py @@ -107,7 +107,7 @@ class AzureADB2COAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): "access_token": "foobar", "token_type": "bearer", "id_token": jwt.encode( - key=RSAAlgorithm.from_jwk(json.dumps(RSA_PRIVATE_JWT_KEY)), + key=RSAAlgorithm.from_jwk(json.dumps(RSA_PRIVATE_JWT_KEY)), # type: ignore reportOperatorIssue headers={ "kid": RSA_PRIVATE_JWT_KEY["kid"], }, @@ -139,6 +139,7 @@ class AzureADB2COAuth2Test(OAuth2Test, BaseAuthUrlTestMixin): 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", diff --git a/social_core/tests/backends/test_bitbucket_datacenter.py b/social_core/tests/backends/test_bitbucket_datacenter.py index dda5fbb6f..a2b2050b6 100644 --- a/social_core/tests/backends/test_bitbucket_datacenter.py +++ b/social_core/tests/backends/test_bitbucket_datacenter.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + import json from httpretty import HTTPretty diff --git a/social_core/tests/backends/test_cas.py b/social_core/tests/backends/test_cas.py index 1d182140d..e3f4a6c10 100644 --- a/social_core/tests/backends/test_cas.py +++ b/social_core/tests/backends/test_cas.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + import json from httpretty import HTTPretty diff --git a/social_core/tests/backends/test_cognito.py b/social_core/tests/backends/test_cognito.py index b09563453..ef6a44780 100644 --- a/social_core/tests/backends/test_cognito.py +++ b/social_core/tests/backends/test_cognito.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test @@ -23,6 +25,7 @@ def user_data_url(self): 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, diff --git a/social_core/tests/backends/test_dummy.py b/social_core/tests/backends/test_dummy.py index 49e59923a..b4e9cc956 100644 --- a/social_core/tests/backends/test_dummy.py +++ b/social_core/tests/backends/test_dummy.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + import datetime import json diff --git a/social_core/tests/backends/test_egi_checkin.py b/social_core/tests/backends/test_egi_checkin.py index 3df87dca8..2fb34a614 100644 --- a/social_core/tests/backends/test_egi_checkin.py +++ b/social_core/tests/backends/test_egi_checkin.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + from .oauth import BaseAuthUrlTestMixin, OAuth2Test from .test_open_id_connect import OpenIdConnectTestMixin diff --git a/social_core/tests/backends/test_etsy.py b/social_core/tests/backends/test_etsy.py index f7d1e9cce..bd3ff49bc 100644 --- a/social_core/tests/backends/test_etsy.py +++ b/social_core/tests/backends/test_etsy.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + import json from .oauth import OAuth2PkceS256Test diff --git a/social_core/tests/backends/test_google.py b/social_core/tests/backends/test_google.py index d9c3ff5be..b990fc16a 100644 --- a/social_core/tests/backends/test_google.py +++ b/social_core/tests/backends/test_google.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + import json from urllib.parse import urlencode diff --git a/social_core/tests/backends/test_linkedin.py b/social_core/tests/backends/test_linkedin.py index 2fcd417d9..b145a63b3 100644 --- a/social_core/tests/backends/test_linkedin.py +++ b/social_core/tests/backends/test_linkedin.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + import json from .oauth import BaseAuthUrlTestMixin, OAuth2Test diff --git a/social_core/tests/backends/test_okta.py b/social_core/tests/backends/test_okta.py index a78e99827..9fb91cad5 100644 --- a/social_core/tests/backends/test_okta.py +++ b/social_core/tests/backends/test_okta.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + import json from httpretty import HTTPretty diff --git a/social_core/tests/backends/test_open_id_connect.py b/social_core/tests/backends/test_open_id_connect.py index 5dacb96cb..6bff4d0cb 100644 --- a/social_core/tests/backends/test_open_id_connect.py +++ b/social_core/tests/backends/test_open_id_connect.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + import base64 import datetime import json @@ -65,6 +67,8 @@ def setUp(self): self.key = JWK_KEY.copy() self.public_key = JWK_PUBLIC_KEY.copy() + assert self.openid_config_body, "openid_config_body must be set" + HTTPretty.register_uri( HTTPretty.GET, self.backend.oidc_endpoint() + "/.well-known/openid-configuration", @@ -156,7 +160,7 @@ def prepare_access_token_body( body["id_token"] = jwt.encode( id_token, key=jwt.PyJWK( - dict(self.key, iat=timegm(issue_datetime.timetuple()), nonce=nonce) + dict(self.key, iat=timegm(issue_datetime.timetuple()), nonce=nonce) # type: ignore reportCallIssue ).key, algorithm="RS256", headers={"kid": kid} if kid else None, diff --git a/social_core/tests/backends/test_saml.py b/social_core/tests/backends/test_saml.py index 538c0d136..9d43c644e 100644 --- a/social_core/tests/backends/test_saml.py +++ b/social_core/tests/backends/test_saml.py @@ -8,7 +8,15 @@ import requests from httpretty import HTTPretty -from onelogin.saml2.utils import OneLogin_Saml2_Utils + +try: + from onelogin.saml2.utils import ( # type: ignore reportMissingImports + OneLogin_Saml2_Utils, + ) + + SAML_MODULE_ENABLED = True +except ImportError: + SAML_MODULE_ENABLED = False from ...exceptions import AuthMissingParameter from .base import BaseBackendTest @@ -19,6 +27,7 @@ @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" diff --git a/social_core/tests/backends/test_stripe.py b/social_core/tests/backends/test_stripe.py index fae311ab4..ed3c3c126 100644 --- a/social_core/tests/backends/test_stripe.py +++ b/social_core/tests/backends/test_stripe.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + import json import requests diff --git a/social_core/tests/backends/test_twitter_oauth2.py b/social_core/tests/backends/test_twitter_oauth2.py index 8a40ed29f..35daf7373 100644 --- a/social_core/tests/backends/test_twitter_oauth2.py +++ b/social_core/tests/backends/test_twitter_oauth2.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + import json from social_core.exceptions import AuthException diff --git a/social_core/tests/backends/test_vault.py b/social_core/tests/backends/test_vault.py index 54da6af39..74646b039 100644 --- a/social_core/tests/backends/test_vault.py +++ b/social_core/tests/backends/test_vault.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + import json from httpretty import HTTPretty diff --git a/social_core/tests/backends/test_yahoo.py b/social_core/tests/backends/test_yahoo.py index 872bb1aa3..f12743011 100644 --- a/social_core/tests/backends/test_yahoo.py +++ b/social_core/tests/backends/test_yahoo.py @@ -1,3 +1,5 @@ +# pyright: reportAttributeAccessIssue=false + import json from urllib.parse import urlencode diff --git a/social_core/tests/models.py b/social_core/tests/models.py index 8fa0b9f45..2d05bc12b 100644 --- a/social_core/tests/models.py +++ b/social_core/tests/models.py @@ -1,4 +1,8 @@ +# pyright: reportAttributeAccessIssue=false +from __future__ import annotations + import base64 +from typing import TypeVar from ..storage import ( AssociationMixin, @@ -9,6 +13,8 @@ UserMixin, ) +ModelT = TypeVar("ModelT", bound="BaseModel") + class BaseModel: @classmethod @@ -17,7 +23,7 @@ def next_id(cls): return cls.NEXT_ID - 1 @classmethod - def get(cls, key): + def get(cls: type[ModelT], key) -> ModelT | None: return cls.cache.get(key) @classmethod @@ -57,6 +63,8 @@ def save(self): class TestUserSocialAuth(UserMixin, BaseModel): + __test__ = False + NEXT_ID = 1 cache = {} cache_by_uid = {} @@ -143,6 +151,8 @@ def get_users_by_email(cls, email): class TestNonce(NonceMixin, BaseModel): + __test__ = False + NEXT_ID = 1 cache = {} @@ -159,7 +169,7 @@ def use(cls, server_url, timestamp, salt): return nonce @classmethod - def get(cls, server_url, salt): + def get_nonce(cls, server_url, salt): return TestNonce.cache[server_url] @classmethod @@ -169,6 +179,8 @@ def delete(cls, nonce): class TestAssociation(AssociationMixin, BaseModel): + __test__ = False + NEXT_ID = 1 cache = {} @@ -192,7 +204,11 @@ def store(cls, server_url, association): assoc.save() @classmethod - def get(cls, server_url=None, handle=None): + def get_association( + cls: type[TestAssociation], + server_url=None, + handle=None, + ) -> list[AssociationMixin]: result = [] for assoc in TestAssociation.cache.values(): if server_url and assoc.server_url != server_url: @@ -210,6 +226,8 @@ def remove(cls, ids_to_delete): class TestCode(CodeMixin, BaseModel): + __test__ = False + NEXT_ID = 1 cache = {} @@ -222,6 +240,8 @@ def get_code(cls, code): class TestPartial(PartialMixin, BaseModel): + __test__ = False + NEXT_ID = 1 cache = {} @@ -238,6 +258,8 @@ def destroy(cls, token): class TestStorage(BaseStorage): + __test__ = False + user = TestUserSocialAuth nonce = TestNonce association = TestAssociation @@ -245,5 +267,5 @@ class TestStorage(BaseStorage): partial = TestPartial @classmethod - def is_integrity_error(cls, exception): + def is_integrity_error(cls, exception) -> bool | None: pass diff --git a/social_core/tests/strategy.py b/social_core/tests/strategy.py index 089720153..1a333e7ee 100644 --- a/social_core/tests/strategy.py +++ b/social_core/tests/strategy.py @@ -10,6 +10,8 @@ def __init__(self, url): class TestTemplateStrategy(BaseTemplateStrategy): + __test__ = False + def render_template(self, tpl, context): return tpl @@ -18,6 +20,8 @@ def render_string(self, html, context): class TestStrategy(BaseStrategy): + __test__ = False + DEFAULT_TEMPLATE_STRATEGY = TestTemplateStrategy def __init__(self, storage, tpl=None): diff --git a/social_core/tests/test_exceptions.py b/social_core/tests/test_exceptions.py index 2f4336253..22e451d37 100644 --- a/social_core/tests/test_exceptions.py +++ b/social_core/tests/test_exceptions.py @@ -28,6 +28,9 @@ class BaseExceptionTestCase(unittest.TestCase): def test_exception_message(self): if self.exception is None and self.expected_message == "": return + + if self.exception is None: + assert self.exception, "exception is not defined" try: raise self.exception except SocialAuthBaseException as err: diff --git a/social_core/tests/test_storage.py b/social_core/tests/test_storage.py index 3e328d041..e4f4ece7d 100644 --- a/social_core/tests/test_storage.py +++ b/social_core/tests/test_storage.py @@ -90,7 +90,7 @@ def test_store(self): def test_get(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): - self.association.get() + self.association.get_association() def test_remove(self): with self.assertRaisesRegex(NotImplementedError, NOT_IMPLEMENTED_MSG): diff --git a/social_core/tests/test_utils.py b/social_core/tests/test_utils.py index 77a97a8f6..cedef6388 100644 --- a/social_core/tests/test_utils.py +++ b/social_core/tests/test_utils.py @@ -130,6 +130,7 @@ def test_path_https(self): ) 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" ) diff --git a/social_core/utils.py b/social_core/utils.py index 3186aac43..74742a10c 100644 --- a/social_core/utils.py +++ b/social_core/utils.py @@ -275,7 +275,10 @@ def wrapped(this): cached_value = None if this.__class__ in self.cache: last_updated, cached_value = self.cache[this.__class__] - if not cached_value or now - last_updated > self.ttl: + + # 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 now - last_updated > self.ttl: # type: ignore[reportOperatorIssue] try: cached_value = fn(this) self.cache[this.__class__] = (now, cached_value) @@ -285,7 +288,7 @@ def wrapped(this): raise return cached_value - wrapped.invalidate = self._invalidate + wrapped.invalidate = self._invalidate # type: ignore[reportFunctionMemberAccess] return wrapped def _invalidate(self): diff --git a/tox.ini b/tox.ini index 37311e7dc..86b01f879 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py39,py310,py311,py312,py313 +envlist = py39,py310,py311,py312,py313,py39-pyright,p313-pyright [testenv] passenv = * @@ -12,3 +12,15 @@ deps = py{39,310,311,312,313}: -e .[dev,all] commands = pytest {posargs:-v --cov=social_core} + +[testenv:py39-pyright] +deps = + -e .[dev,all] +commands = + pyright + +[testenv:py313-pyright] +deps = + -e .[dev,all] +commands = + pyright From 72d4725d3691fd63383e17d549e03f4c9105716c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 09:34:16 +0100 Subject: [PATCH 124/152] feat(ci): extend pre-commit hooks Check more files using built-in rules. --- .pre-commit-config.yaml | 7 +++ pyproject.toml | 4 +- pyrightconfig.json | 4 +- .../tests/backends/data/saml_config.json | 46 +++++++++++-------- .../tests/backends/data/saml_response.txt | 2 +- .../backends/data/saml_response_legacy.txt | 2 +- .../data/saml_response_no_idp_name.txt | 2 +- .../data/saml_response_no_next_url.txt | 2 +- 8 files changed, 42 insertions(+), 27 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5bbc51a27..7f159901e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,11 +4,18 @@ 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.9.0 hooks: diff --git a/pyproject.toml b/pyproject.toml index b5fdf093e..c81fb466e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ ignore = [ "FBT", # TODO: Boolean in function definition "S105", # TODO: Possible hardcoded password assigned "S113", # TODO: Probable use of `requests` call without timeout - "B018", # TODO: Found useless expression. + "B018", # TODO: Found useless expression. "A001", # TODO: Variable is shadowing a Python builtin "A002", # TODO: Function argument is shadowing a Python builtin "A004", # TODO: Import `ConnectionError` is shadowing a Python builtin @@ -90,7 +90,7 @@ ignore = [ "S301", # TODO: `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue "S324", # TODO: Probable use of insecure hash functions in `hashlib` "S318", # TODO: Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents - "PLR1704", # TODO: Redefining argument with the local name + "PLR1704", # TODO: Redefining argument with the local name "PERF203", # WONTFIX: This rule is only enforced for Python versions prior to 3.11 "ISC003", # TODO: Explicitly concatenated string should be implicitly concatenated "B028", # TODO: No explicit `stacklevel` keyword argument found diff --git a/pyrightconfig.json b/pyrightconfig.json index 00901b392..388abdc8b 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,4 +1,4 @@ { - "reportOptionalMemberAccess": "none", - "reportPossiblyUnboundVariable": "none" + "reportOptionalMemberAccess": "none", + "reportPossiblyUnboundVariable": "none" } diff --git a/social_core/tests/backends/data/saml_config.json b/social_core/tests/backends/data/saml_config.json index 3f610107c..5dbcdcf9e 100644 --- a/social_core/tests/backends/data/saml_config.json +++ b/social_core/tests/backends/data/saml_config.json @@ -1,23 +1,31 @@ { - "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_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==" }, - "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" - } + "other": { + "entity_id": "https://unused.saml.example.com", + "url": "https://unused.saml.example.com/SAML2/Redirect/SSO" } + } } diff --git a/social_core/tests/backends/data/saml_response.txt b/social_core/tests/backends/data/saml_response.txt index 7f617611b..fce0761b1 100644 --- a/social_core/tests/backends/data/saml_response.txt +++ b/social_core/tests/backends/data/saml_response.txt @@ -1 +1 @@ -http://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== \ No newline at end of file +http://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== diff --git a/social_core/tests/backends/data/saml_response_legacy.txt b/social_core/tests/backends/data/saml_response_legacy.txt index 557bb59e8..e15c31a05 100644 --- a/social_core/tests/backends/data/saml_response_legacy.txt +++ b/social_core/tests/backends/data/saml_response_legacy.txt @@ -1 +1 @@ -http://myapp.com/?RelayState=testshib&SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL215YXBwLmNvbSIgSUQ9Il8yNTk2NTFlOTY3ZGIwOGZjYTQ4MjdkODI3YWY1M2RkMCIgSW5SZXNwb25zZVRvPSJURVNUX0lEIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDUtMDlUMDM6NTc6NDMuNzkyWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI%2BaHR0cHM6Ly9pZHAudGVzdHNoaWIub3JnL2lkcC9zaGliYm9sZXRoPC9zYW1sMjpJc3N1ZXI%2BPHNhbWwycDpTdGF0dXM%2BPHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM%2BPHNhbWwyOkVuY3J5cHRlZEFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIElkPSJfMGM0NzYzNzIyOWFkNmEzMTY1OGU0MDc2ZDNlYzBmNmQiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiLz48ZHM6S2V5SW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI%2BPHhlbmM6RW5jcnlwdGVkS2V5IElkPSJfYjZmNmU2YWZjMzYyNGI3NmM1N2JmOWZhODA5YzAzNmMiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDc0RDQ0FobWdBd0lCQWdJSkFPN0J3ZGpEWmNVV01BMEdDU3FHU0liM0RRRUJCUVVBTUVVeEN6QUpCZ05WQkFZVEFrTkJNUmt3CkZ3WURWUVFJRXhCQ2NtbDBhWE5vSUVOdmJIVnRZbWxoTVJzd0dRWURWUVFLRXhKd2VYUm9iMjR0YzI5amFXRnNMV0YxZEdnd0hoY04KTVRVd05UQTRNRGMxT0RRMldoY05NalV3TlRBM01EYzFPRFEyV2pCRk1Rc3dDUVlEVlFRR0V3SkRRVEVaTUJjR0ExVUVDQk1RUW5KcApkR2x6YUNCRGIyeDFiV0pwWVRFYk1Ca0dBMVVFQ2hNU2NIbDBhRzl1TFhOdlkybGhiQzFoZFhSb01JR2ZNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNEdOQURDQmlRS0JnUUNxM2cxQ2wrM3VSNXZDbk40SGJnalRnK20zbkhodGVFTXliKyt5Y1pZcmUyYnhVZnNzaEVSNngzM2wKMjN0SGNrUll3bTdNZEJicnAzTHJWb2lPQ2RQYmxUbWwxSWhFUFRDd0tNaEJLdnZXcVR2Z2ZjU1NuUnpBV2tMbFFZU3VzYXl5Wks0bgo5cWNZa1Y1TUZuaTFyYmp4K01yNWFPRW1iNXUzM2FtTUtMd1NUd0lEQVFBQm80R25NSUdrTUIwR0ExVWREZ1FXQkJSUmlCUjZ6UzY2CmZLVm9rcDB5SkhiZ3YzUlltakIxQmdOVkhTTUViakJzZ0JSUmlCUjZ6UzY2ZktWb2twMHlKSGJndjNSWW1xRkpwRWN3UlRFTE1Ba0cKQTFVRUJoTUNRMEV4R1RBWEJnTlZCQWdURUVKeWFYUnBjMmdnUTI5c2RXMWlhV0V4R3pBWkJnTlZCQW9URW5CNWRHaHZiaTF6YjJOcApZV3d0WVhWMGFJSUpBTzdCd2RqRFpjVVdNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ1lFQUp3c01VM1lTCmF5YlZqdUo4VVMwZlVobFBPbE00MFFGQ0dMNHZCM1RFYmIyNE1xOEhyalV3clUwSkZQR2xzOWEyT1l6TjJCM2UzNU5vck11eHMrZ3IKR3RyMnlQNkx2dVgrblY2QTkzd2I0b29HSG9HZkM3VkxseXhTU25zOTM3U1M1UjFwelE0Z1d6Wm1hMktHV0tJQ1dwaDV6UTBBUlZoTAo2Mzk2N21HTG1vST08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BTElQdkVNVUVGeXhrVHowQ2N4QVA5TjV4Y3NYT2V4aVV4cXBvR2VIeVFMV0R5RVBBUDVnZ1daL3NLZ1ViL2xWSk92bCtuQXhSdVhXUlc5dGxSWWx3R2orRVhIOWhIbmdEY1BWMDNqSUJMQnFJbElBL1RmMGw4cVliOHFKRy9ZM0RTS2RQNkwvUURtYXBtTXpFM29YOEJxMW5Ea3YrUWh4cmQwMGVGK2ZMYVQ0PTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BRVpUWDhHTkM0My9yWStTUVlBMXRudHlUTTVVNkN2dUNCaktsVEVlekZPRjBZZHhCWUdFQVVjYU8xNVNKOXBMemJ1L1h0WGxzTkVMZTdKdEx4RUpwYUxubWFENnIranNWczdLaTBLNHRTMGNBUERDWHV2R1FoMmFOVjVQOGJ3N1JWUGhLOGQwYlJ1RklGR09FOHMwTTZYOUpxWDN4S0MvL1lSbVVoeDlybnU3ZWlwMGh5ZitPaUZiVGR2SDY2NTB2LzQ3aVdKcDNZeFlUV0QyMHBNbVRJMUpwWUEwYjByWVFQRkR0RU93d0JxYktxanRJc3ZYVFJzeXJhQkxvbnFOeHN5dHpEWHEra0JsMXp3WGUvSE5QcUVQblczdnNxaFhZcDVGM3dkWThkKzNCOTRMZlpOdUd4a0p3VDNzdVR0OGY5VHRBSlI4VytBUmtzT2M4eDBVaVNsVG5BNHFHOTBLMTR5dkVoVHcvd2drZjFXV01RT3dpZDNpakFYbUV4MU5MbVZvYUxYb3p4VExkTjN6YnJ6VEJIRXc3R2J3ZEdrdU5pMlhZOW16YUgwaWtGRm51VUxjMHUwc0pycEdGdzlaK0VlUk44RzNVUVZ5MjhtS2g3ZFBwWU5KbzhyajIxZFFaK2JaeUtTUHZablU3REkyakdJRE5US1g2ZkVyVWFINGlOTzN4cUU2Vk90L2d4T3BMNE5VNUhLV0Q0bG93VzcwdUJjVEVQRmhwaThpYUovdTB6YzUvTEhvdVBjMzByc1RLZFc5cmJLL2NWaHNQUHErZzA5WHZpZ0QweTJvN2tOc1pVL25tRXFiSzBKOTBrazhCR3I5cXRSczY4bUJnSURtUHVwUkhwWjM4eXNnU2VZN3V0VlVaSG5tQ0dzTzZ2NDJ6OTVOK05Pb3RCTEVZbFd1ZEdzYnowQWc4VkRDSlY5ak95QW95MDZyL1AyUHBsOFhjdmJza2d2T1BMMWdDNnVYbVJJS1lmOEw4UDJCNXVjN0haK0dtUHNOWXRLS2VKRDFFUHovdCt2NlBIbXNVb3dsSDhSd3FMRHdtMUF4dlNLQTR3UXBlQ0dQd3A5YXRYS0lWMS84NUZzRWMzajVzNjd6VlRybThrVEpydXV2MDZEdFVRZDNMOFdwTkV4cWhQait6RUp6U3RxSG04ckhNMVhNQUVxdVozc0xycTVqLzFSNlpqS0dOdFJCbjhwOE5ERGtrWm0vWTV5TXlJNXJJS3U5bnA3bXdaaEVpeWVHeHdxblV3VVMvUzVDRjNnMHVidnd4eVVnalVvd1ZvTkNqYktBbkdtT2VCSW5abkh0eGdIVUhVOUVlTFdyd2pRc3JtUmpJV0R2RkZQa3l6SzJDL20yaitubmNxc2E1OGRLVXZxcGR1VTRJYnNPQng3UGpXdXRBNmY5bXd6YWxyRU1NK0lGR3VPdk9HMC93eUdzQjZLREV6bldjUC83NkQ4angzaHZFSlAzN3REbFgreGM4Qno5TXdKdkd6VG4xbTdCb2xoR0lzSXlCTys1ZXpXa3RDWVVIUURGVE9wbXA0MDlOWHp6ZUNTUGY1U2NDWG5YYjRPd01ULy9VM1JFUnRRbGMrNmU2WG1JRjhoRkJVc0taUUJsS2ppSDkwZHlzYWlsNmN2V3UyQW55Q3QxbWxXcHFLc0MzU2RTRVZDTG1qRjlUQUFUMEtFSGdZQjg3RjZtZUpTTysvOXkyZkRuYVVvUUlUVzdubnVuSCtkT3dWSGZMU0wyL2N5YTltNlQzR29TSVNMbGJPMVRzalhKclVkZW55OTcvM2tkNmhFQlphdGY1U3NETFQ3SjNsQUVJNDROeXJ0NkIxQWdod2JNdkpqd1JNTXRNdUJLc3ltUytKVzc4UFNEWXQ4MG9waDJQTTc1N0tBNCtUMTAvYnZaQkE5Vk1OdVpqNVV3NXRWMnFIS3dwS0t6ZVVETUFiQlBRaGpYcXlQZzFKa09rd2RQMUpnOHRITjJTelBZQTlmT1htV0pBZGJDS2tMb0F4ZTV6cDZBUzYzS3FXMmFmSUt6SHJ3RTJmS1VtamppeURvMnNuMkJHbWtBaTRzbnpiVzc2SUQvSVgwd044aDBaQ2VRc29vKzdtb1RCMEJxSnBkS1MycXlsUktoc3BSTC9henVQdmxaK1pwckJxdXpJdEZkNFVLMkpzQkp6VXcwZkpxcTV1bk9PZENzVWM3SUU3QTNmZ1NmZ3NBd1R3WFZJMEVoME5ySWZpMkFKV1Z2VFpEMys2eFZ3dS96WWhuVjc0VXkvMFE4Mi8yQWtpSGpFRjNJVGNLWHdTNTB6bWtLakxjZDJqa2h5TUFYMWRoQ0wwZElFMUJoN0RNamVvNC9YbjBqSlpPL3Rrbi9xZmYzc3RNb1BYVG9KTnBIU1RjR2ZheGtaMzJYNCt3Q0xPc0VBRWxlMVZSY0kwUkZyOFhHTSsxWU9BTjBodFdGcFMxaG9kSi9OczJqL1FnUVNEemNpQ1FZeUFDd3lFRWZDZjZybnR0VmJyTlJQZWlmSHhBM3B2UnZ5ZGRhNDE5cXl0ZXI0akJ3cmw3ZUpuVnJ2VEprR2VhU2FRbDdXWk5SQXBscXRnNnZPYmpiMHZDRWlFaFhKbmNzQUhxcXp5QTRGeWFUVGQ2R0FySU9adUNxRWVoWk51T01lOVlrMVpya0VkR3pIalJESWk3Q1BKQk12NEZ4ZHI3bnJvN0I1WEhKb0ZMNE1DSUtOWWU2aWZiTUtYOU5uN1FWdnphUmY2UXlaSW1BWENQZndvU1BkN2x6NXl3UDJLSUIyaGhFMWt5eVZ5YVc5T0praWpUY3dvUnZrSXhIU0RqMXFqeGxueXh0QzhVZ1pNWmlwcGgzQXJpcjRiekIzUDhIbGIzejZ0OW51KzZMemNiN2ZObVo0UHluaU50Vk9OQ0lHbEh4dTBSY3hQK3cwUXNsM1BtTzJLaHBpc2RIanhvSUJ1YVY1NXdoTlFFNmdNNFBrT0xINDc4Rzg4bUxkd2s2RFpkWVl4L2d6RWE3b3ZIL0pReFp2TzRLdFVTNmZjZHJxV2thTFg1cEhkNkdneFBGZ2NFc2Nad1ZqM2hCS0xFQmE5L0dodERINEhzRnNRbmpPZnNDQkNzN0tjRitmTi9oSUdUeHFqTVlKVHJRYmNtdWF5dk9xR3RQMDFPcXltR24rVm5FSVkzKytQcm95SFN3K0Q0b0JIVG1maFNXRmJLZCtuTlVFS3BhRVIxNkdCU256WktQRVRVSmdRWEw5QWJRQ3RXVjFHb0UzRWNnMDZYaVd2aHFHakpGNldtdEU4dHY4Q25rZmxMNm91TDRvNldpbmx2WnNEdkZrS0R6TDkwUTNsWC9NanBtRTFpWU9uYzdISXdEVGwraFRRcHdsYXJiTDVUNGNkZTg1akNwYU0xU3p1TStiQU5zMHlXVDA0ZXJUVFc2cnhlbXFDTHAra202TVVMTlZOcE1CazBiQjJpRU82UlRtc3VpRlhDUU1xdU5xZjdkWXUwTFFCZzQ0MkJzU1pBV1ZrWEVZblduOURLdTRSby8veEFsb2h5VHozWlZmSkhuWVBSdDloSUErRHVUL3c4T2ZzTURIWnlCelUvL0JEa1NiNkxjMHdraVA3QlhIdjBoNVdud2dNWUxlZDBPalR5UWI2aGxpVnQ5b0FjaDRFVy9EZUlBdkpaQ1BYVm1pUFFYTGVsOVJIRko2bXFiYVo0TCtaZG1ONmQwcFZNZ1FveXhmQTR3dEwwYVpiNnFZYkhibjJMd2VBQVZwL3M2TzVlMVExdnZpZDRTWHo0a2l3RW1LSStIeXZEQ1pnekpQQVN5Z1gvWDJFWEZ0NGV3SjVmUFQyVXZmWnhQWlpqMFZGSFpyUFQwWVd2VE16bjUva3hoT09oM2drVGdDSmNwNWVsZnp4cEFPNFl1a0NoNHJXdVNndDRqVUJyaWNYbFdWdWo5U3JSZVhUalNHTktLK202NWovUDllNHRHT0RkMk9BbjNKTVQ3Q3FuaDhreTZpZjVjbmpVMmU3UDhTZnBONGwxWEFiZEZEcGk5bVJYamEyTzR1RWFHNGNvNW4xcWNDT3ZNMWYyblFBY1ZGNUFoSXhueS96TWhmU2l2RXdOQ0Zyd2tBWDRyQVE0WldUNldFakFyUG5jb1Y4Z1VRclhxQVA4NDJmK1lNWWI5RHFncmFicEg1a3ZuMnQzcWRldGJHODJ0QWlTamhPcUxNYW9iU2F4cXdWa1lUOHRTMW9rUUt2MWZoZ2t6elpEOE5IQnVQQzdNVHdXS0VCS2tDRUUzRWRFMXhNQURLd1B1M3NSaGpSaExXZyszZ2srejJtdlU4cTBhTlc0Y3hObUdoekx4eEY0Q3NFNStMQ1cwOWFpUVJOM1VvWmg1aktBZzBiMlh3WHBLS3pycUVTY1BYdnI0L1dWUTMyMm5qRWRvQVdXR0t2WnBKMlRlREo0eDdiT21LVElFc2RHWU1UZzFVaEU2eFFQcnhqS3dWeGFJNVJyaVE4a0xpaGgwa0t0WHQvYTVsSDhzUjVwR0ZISGZ3dlNVb3liQTB1eUVDNnNRVitPbTVReUZmRmpqZHFCOGNpOGxQS1hLTHFCTHJ6bjNmUkh3TmQwbzFiRTg0aGllTkx5UlhZVmhrRCtFNEpGaVd3ZWt3U3VWM3BjQk9ybnRVU3RoWmx6M3hIUURUVGNJNWliOFJyQ2swZEZ6YTgvQmw3VUdtWlUwSXZ2UmdvVXF2TXNHT2dMY3pGWmRpZnJ5aGNiUTY4a2ZzZ3lCMHppdC9MN1BSV3V4RkdYdDFoTVZSVUZ3WXBJS04zVkI3cXVKZlgwamZsU1JaRndMaXdlK3VhYndmTVZ6c2doajUvOXZNNzcwK0JaMGtJcE45NzBTMG5BbHl6R0h0aW1nTUl1RXFhbUt5QTNTQlI1aHZIYmRyNENnTHFUbXIzbFFnWmpnSkNvN1FXYUJWTXdCR0RpdzVOVVhUUnBycWc4U3h2eDlnNWZwbXMrL0o2QjFEelNTM3ZRZzgxdHFRU1ZDWVJpc0Y3M2VqZlFuZk4zcUszd3RJRDkxQnRISmFvMEFaUUdKVFpKOXVsZ0kzV3hzdWR4ejB0VHVpNlJlSWpmSWsxekZRdFpwRExGMnB3NGpTQVdQTlJqNDBYdVIrRzFUVlI3OVFiME9FYkw4RDFoTU5zWmo3MTZNbUhSOTlKaUxNdm1FWHV5a1V4VGhGYjRMTzZVbW1kU3UwTlBpMXQ2NmNkYURpQWhMaVBFTGdUNkZsenA2T2FGSGNSNjRncEtyemtTNDJONEhJeFpNa2R6M0FsYkRhK2pOWHZPR1l3UWl5K0xNNENZWGtrTWtHR3ZTWis5R2xWQ0l5RXBJaXIzbEQ3bmdzZGk4emxGWDYvekNaczlQSUtwZFZlSGJGZi9GS20wV3AreHI0Ykd0R0RrVHR2Nk1Manh2YU8zanFHaUFWeERKVWFkTVBlS2VHSm5uempTdnpKbGdOVHV3c3grRnF5L2dPMkwxMGowWmhDWi92dE9NelVjNjl3cGhKZm9FNzU3V3lOeFJOcThJc0Y1Tkg5Y0x0b3UvbUNxOTc3YnZPSkRrSURCN3lKWEJ6YUhVQkJuSXJra1Qyemg3bGJmUm5SREJUSFZraVZMazVESUxqeC9XL1BSZEZpUUM2SzRmZGx4Y29JbzlMcnM4ZFVWZkt2TTNNYnJ6c1hGT3ZtVVh0K3NsZldvd3UyTC9ndG9mRFhvTUJZZnlEcWIvWlRaRWZ0MC83blliRm1relBEUlZacU5SR0F3YWZVNTU1UjB2SWtNbGR2VjdKUzhNT1BNYWlXQVBpelNLRG4yRzNvcys1MzRFQytaOGZnWmFPVWpZL0xLME9vME9RMmhvNUV6MGNMYWpwUjFINk9FNEhvUm1ydjQzZkFjdGpYc0hYdi81RXg3emdrWk1NZXZhTFNEdjZtcjFGcDk4QXR4L296VTFGVDBoMDUxcVcwR0g2VWpRRXk5aExSZDBBMnFkUTRMZXpReDNvbDFTblhsamt2MG4zTXFlaFozOC94bzZhdHFDdkJtQkc3amlUdXd6YnlVUngzRm1TM0NCNllOYnFON3hPYVRZRnlkOEZDL01nY0xGQmMwS3F4MXllQ2VUd1hucldQb0dvdllVQlYxYjA1cWtIa1d5V0RUaCsveXJFNzF0RjNxbUQvd3F6cUJyNE04NERtWWVuQkdFOWxtb3FIZEMyWnRpK09KVFZKcmlHZWxQQ3RjZnZRaUlQcHdDZ3BFNmg1ekZhRndLajRuZGtBUkRpTC95L1EwWTZxNU5rM1g5RURlTmdjY1pIcFdmOUpKQ3M2a29wdXRtYjdDczIrbVJYdER1S09DaGY5UVUyN3Bmb1NJaklYK3NGdHY1c0hhSms2aHBZMlpzUUhzaTBYbFowc3FMTnQ5ayszdTVnYnBSU1JCczlHaC9BaVY0dkNyYTRkOTh5U0dCdzRSR1FhSStpQ29RaG9YK3lxc3VrYkx6bXJUU3FXMVRXaXJReUlHZ1Q5VnFERE1mUzAxeGdQSlNFSTlIWlp6TGlFVXVGMm1CMi81Y2dqaEFUaWQrdGV1UVB4aldhN2NSc2t5YUhuTENjQURVUU9ESUFPVjJDWXROcnAwY29ZL091S3ZzaXlJT0lacVJ5dE1PMGVNZ1ZJWTBzWmdxeVEycXlubUx0NDBmWmd3SFVyV245Zm9TYTNtMkVRTy9uOS8yU2NuelJWdVZpVnNjM0tCSElQL3AzNlJlSWowTGlNcCtPQ0p3SHlLVW1UeDRBU1V0dXVhWktlRHl1QjlxcXJuUEFNWUVCeElsTGFvdXMzV1pHakIrcW9ub3QvNmk1UE40bUZjbHFDcUxhMGJHbks4ZnJxYy9yd2tuVGV0YUE0c2tXTEw1L21qNEd5MitFQkh3a0x3UXd2K0FKdmZTOXYvNDl1LzY0N1ZFYW15UzdZQ2ZEUHNBQUREQ1FFcWJNQ1h2Ui8xVmEwWi9YUWhoNlkrZUt0MEVpRDdpNmRZODJtQkFoNEJMRmRVV3VGZHVrdUVwaGZ2WXB3N2loVjNxTjB1NFM1NTRXU0dUa0ZsdlpYNG1hbkF4a1g2ekQxS0NWaEFMdEJnSDgzdkhxam9uc0lwOFMydHgwZ0tiYzEreHVaRVppVWlNVVlVdTByQVFsRFcrZHJoN3lVRHZqekFHSnBmTk01eThaMW45em93VzZ5YW5VZWFBNjhSZDd5TUxobFd0NVh6bGhBTVZDZmZYZ0pFelR1YzJEbENVOXNMLzVTVkRaV2N4R1E5aFM1cnJtK2VyQ1Jxd2FJQk1DNUtza0RCZHdOWmh2Q0FCdEpqS2Vla1FUSjd5MFp4SGNhbGVCaU1rbkYwZVRDZzFvUEhPUVZLQ3V3NE94cHRZUS9xS1V0TEFIWFZ2OTlLMGRWcWZDMmpVQWlHQmVYa0t3aGRYTGtJYlZxU0EyZmxraXBBeEhYNnByUEExQjF3eTVab3hPUFg4RVExOW92eXpBbFg1dHU0OXEwWC9PSExFN1o5T1cxenltRXR6ZFpyNXJZbWtFcVdtcHVSNU5jeHFwTWlZam93dUNXZWhubzIyeG5JM09IQ0xDZkFKaHRrcklhL1hPc0tZRFpCRzFJMGJsN2taR2R5cEtUQlhYdXl6WE5WUlU5L005ejhaVytwdG1oZ2NOUzBJS2VaaSs5bFl4cWRlS3lnbldTTTV3czdSYUpmNlRRZTNSaWJZUjFvNkhwRzB2VHpiTEtQZTZnRjJGODdiWlBJei9mcTNLWnZiM3UrSnhZcCtJVjBtQi9VN29YelhRRk1RK3VmWllpNzUxbkx6WlVxRE1ybU53TFJPVUFNUk8rVnJtblkwSVB1cFBVMXc0b0hBb1dnVGRnTk5pNk1uTFQ4V0pmUlhjT0pKMk1lbUc2K2ZNeHNZUU52UVJwa1RGY05vaFV6Y3ZjcHJ3NUV3WEVZQTJzbzczL2MvY3RIRGcreU05YlF4REppUlltRnFydkhYb29hS1JyekxnUjZLVWdoM3ltaWxaQ0lSSm9KbTE3aEtHM1pxTTE0Lzl5OUc5OE9BZjNkVTlqMDk3aUNlaEc3a2VxYXRJQ2hFWmJqbmQ4Y00rS3djN2FtVWp2ekQzQmNvMHl3MDJxT054OWF3OGhSblZiWDZhdkRJbGhySHZ6SU44MzFvUjljRHBwMG1DUEJXZFVDQlNqVGJ1RkZqRC90WElSbGxlT2JraFFKSUdSNlE2U1MxcXkzT29WT1VheFl6THY0U2s3dndrQUMwUitGREVIeVFZbFVhbVVkTWcyUmdwRUdhSVd1V3IxaGNnRm10QmREV2g3ZFBuWTF0U3VKOC95MXp4NkRvN2ZJYmNFenBBK2E0ODNtRG5vemdld3VmaFdqVCsvUS85WlEreFQ5UWJBT1pQSXhHV3VhSXVrVk8zSWxvZDhJM1NGZFJCTHY5ZXBDNzFLeXpSdVlpMktkOHJ5NVNINit1WnMxUHlZUlpRakdDK3Q4VzRtSE82Z1lFRWVXSkJ1UWhnSHdmV2xhZXlWb3hac0NBQVZKRUllT3hPZDZtNW45OHRCUDdHTmgxT1M0eDRCS2FVN1A0UVQzNVVIZW5meE84WWFQUThmbXlobUJhSVJVZklBTVN2ZTJZRFp5SWNNTTkrN0tNSVVabzJ0eXRvYzdCOGVvZzBNaUkrVkpFdFg0c29FRjFSWkhQZVV3NWlCTjI4OTh2MmVTcGNnVUJhWHFzOUN5VlZtTVJQMEtLUDJ1REt4MUdJcUhjS0ZCOXVQVWRkQS9vT3dNa0tVUWsraFZVVDVPbEVMdjd1a0FBUEE0eE4rZkczVmYxeUVKV0FiVGx5dWtGcThjNXBTRkY1cXVHbUgwVmVpQzVvVEFka1VES3Z6WGhWWUs5c3BRYjNVZ1Z0Qld6N1ZScnlOUVVST3BIZU5xeDlhZHA4YWREWCtRSHJUKytYblN4VVI3SVdGanlNTkZJRWlMWmkxdks1UVVrZlRDUU9qdjh2SHdiUi9MRHF3Z3M5bXdsT3pPY0RLdVBVK0dTb2lnVFdRejRWN0N2SHRaVDI3WUdKVG44RFFFM3IzdjB4aWxvODJ2U3VXSDg0WEU3VEJsTUpFb2R5eDNDRngwVUVkc3VhRHBPSEV3UjZYNlUyU0xseERYSXVZeEhlNXh2NjI4bXU0bDRMSnBYUjhkYmljTEZKQW55Q0FVeDJLb2dDamt1cmU4bXNUZktDbG8wamFlN1hNR05PSk15b0ZYbVlHZUh2eGhNUGMzTEtYLy9VY1p0c3p3dFJrQmNFdURXQysvQWNWZVBOSHVOWWI5MEpIcnRucGg1ZDlhL1lpTkpzY1N3QTFwUVZrdW1TQWtPQWdLdWRzcnl3c0N3Zkg1anNydVpHUTJDd1hKRXQzUU4wU2NLUlVnT1NCQ3FYa1BqZDVSVzJuOFZpamt4anovbWptakhCNmk0eHM5NEU2Nzk5STAyaldYNVd3UDZhTFRaTGt5TjhxNDUxT0RmeUZVZEY5WWsyZXQ5VUpsV1NzRFJMSWVCd0ZyQkEyZTdyRWsybWFLVUNCRW5PUWM2bUhVMXQvZ3gzK1VXVVFXbkpMZVUxbWUvbkFEdy96UGUwd3d0Vm9BaERZdDBoR1hQblJydjFoUHRGS01CeWtqckg3a0J5U0R3WDlQMi9XZkNkQlE5K1J4cHRsR2hvRmdpMUs0NVlOeEpEd05wTmd5MDV2WXUzVUtrMkpRYVNGUzcwK0Y1NzluRE5RenZpK0pPRlRsdDFmWDJGNXk5NEV2NHZobWRQSmRVOFVVRjU2Ymx0emxKREVFdmsySlFrOTM0aHpwTXJGZ1d3ZHUxUkxxSEhCN2h2T2hnaHNqV0ZGY01zNjZaRUtWcVhKUytxWWNVMHk0akwySVQrNlF2N2pvQ3BWbUdzUWtGY1FyblhxOUJiOTdaUS96UCtwaldmWTU0UmNRVlMydUU1YURObVVyVkdLK3E0d0xRcUhuRVViT2puSHFFeGlacUtxOVdRaUtUK2c3QS96bVlIQ2k0YzFTejRNVWhHb0t6U2l4aXoxYUNJUEJXdy9vczR2cUVqbXgzOGx6YnV0OWNWbElzeGNkTUpUTERRK3ZOZ0YyY1ZRaVcxRTQ0d3lWcnI3TUFaOE9KRVpFSzlEZWt5MzJQUkFuSkRUVXVqdGFscmJ0T2VOczhyS09uTjcvNFRqUEwvZmRlbEI4bjA4WXdSNXdmbU42VGpGWUhRSDFjbUZmK1AvNUxVMTI4Q1pEYjNQUStxMlFJazV3aE40eGwvcy9lb29pallmeWtDcm5aSEhHWkluTGhoU2pWbk5ISWdTL203VWV0NlhBTDdvZUl5UFRLeHVnbDJzRWtUQzNnZ0tjTnFZR0E5U3ZlYVlaQ00vWHNQRUtQbWs3QmlRNmprWFBKaE1yREd4Vkc0SW9aSDgrYjBrUWJYR2l0Mkw0L3hZdHh1bTVzcFNPSjdsTDltVFpRNnBxM2JOaTEwZU1mZ0ZWaDc3NU5JRlc0SEp3U1FtaTU0bk11blZTQjhxdjZKc0w3SGlsZ2N0ZHFSNThTTjVad1lCa2dOR1hzYjA1QXJWemVXbHh1Y21BSHNPT3dyczFnMzh6bTRZN2ZPZmducmFhV1kxanZZOFlEODZQZThkZzR4cE5paTg3UnNDZk5WK2NKVmMraktFdnpuZVY1Zzd0RmlxZCtsZHp4STlKemdSS2t0WUV6RUpRSVU5M2UvclJaN1lrVkZtNVV1cjVhMWYzcG83T0VtYkJUc2MrQ1FaOGNnYmIvbUphRXJoa3NyL3JURjBNcjNxeDl5SlJWSEJ6YWNWd0dScEFRaURPdnJkWU4xQXBVOTRyR1lrVFVzdWs1YjE1Wll2QVZxRlRzVlVMaS9HY29mbEljMm01Z2RFTFZOblRmdXY1Zlk5S1NlWHFoUU80S0pOYVZmbHAwQ0VKYWFFZFNLUXJJNXRaT2w1RkE4VXZlNmxTWVd5TVk0REl4a1RiT1JoWHVBdzR6b1RTMjgrN3d2TXhydVBkZnlKbUJCTkhQdCtEYmdKNHovcHJZWUhpTmFMTXNZamtQZE44ajNKZDczQXJFZk92Um52MzYxSVVVMFg1RDc1dlRSdlpkbzMzWERzanRlOU4weUo3K2lIQnF1a1FJY2pIVW9ic2RQN0hOajBVYWNSMHIvTmRlVTlGNFBNc1VLY2t6Tk4rZGhyMVI2d1J2R1VZb1pDRWJaWlJMWEt4QnA3SElUNEVQUktHakIvdW1xTFhhMXl6RWx2QW1WQUJhMDFZN3dGdk4wM2Ywb25FbUhTM2w1d1paRmV6cjVibnN5T01XVGxhMU5kaW1ZNXNVeE15VFliZmc4dzB2cXNEc28zWFAxYndLdzZ3M3VIRGQ1UHBSWnVDSnR0eWk0ZzJGeWI0Ymg1UU42ZkdORTI2ekRGN1Y4QmJwZXJLNkFKQ0xTWm5kaDZMMTlPUTBram4xUGpEMGk4c1BZcGFXOWxVeVJkZElPKzRWQS9LemxPUzJ4M2s5VUtUdElsTTBUSVdtZXFIS0dYUVpocGpvVGI2VlNKN203cjZaaVlQMnVsQVVvZmVWL0o2eCtzckxEQXkyQ2ZFNnFrREZ1OU9NWDBBSXVnN3loQUtOMDRyT3hVNk5tcGtjOUZ4bXUvVS9vR3hHdmIzeFVFTDYwdE1sSE9EaWtqY1I5RDJrKzRwbEc1WnV0d0FIY2kwRU02WHRrVEhQOU5QMlRTR1VFN1E5SGYvU0VEc2V0a25hZXhvWmhDczJLWDFMeU5JS0U0N2pkMkR3MTUreDRRVXV0VUFTbzU5Q1lHMVFBeW9BVVhrV3dtbXkzTGdTUWp5T3ZLV25qaE8veWpPd0FyWGd0NFBrSVVnZDQ1N05ReFpMbU41K0J4NVJoQ0FHdkUxYmxOZjlMek9keGJiaG5VZ2Z1RDM5MXVSRkhjS2RYREY3ZmVqb3gveThtaWZJcTRWVzQyajBHQnFOQUtkK0prMnJCMW9hOTRiT2hxcVVzanhqWnlRaGRXTzhNblR6T2tOaGVpZXU2blYxcW5yZ3JHU2huWTNJMlczb29GNFNnczRjZ3drZ2h2dHpFa0xUbU5OUm83RTdudVRuMkxJcmlGSnlvTmZQdUp0aWN0S0JtNzRGZytkWVBTMlIzTzNmOWxBZWxiVWZjbzZGNU9EL3hkS1VuRTh0V3FOMExVcDlWQUptWVZYZFVDaGJ4MjM4MWtDaStLNDJoRzUydFNQYU1hb1dTb0xQY2Zrb24rc1pYdjdEdEtwZi9HTzdhcUMza1pzRGpva29haHJGZGJWSlNTZWhrNGp5K3RzRHplQnJKSjBrMVZrUnJHN1NoVHZjTmd1cjVucVRUTEE5dlJMQmJNTTlhNlI1NEZ0Z1pQOWFKMU1aMEdCcUVpMnF6Ui8yd2tYQlhwcFhZdi9TcU1RV1dhbTVsSHBMVktxaDN4ZHRjNFdmck9mYldsbU1PNXA5Z0JUSFp1YUcxVGFkZXFRVVpKQmZBS01ENFdSR0NsMDFaeDRTVzE0YzZrdnFKdXExL080N215L3RsVHlLWndpYlBkQTNRMVVGd0I3R2Z4anEwaDN2ckxFbUNrS3Vsc0VBUkN6UnZNVjJSVnBVbFpUV240Y1Boc0hjcTNROElHSUYyKy9nOENFSU4vMU8xcVMvMkpXcXlDNmtIb0w4Y2R2R0VHbmkxSTNDTk1JcXhxaHhJL1V0R3REc2VwYmwrSHI0elh4MzZna3BCbXBoT2xkTFVYTHAzVEtibVVZRWJSWHcvZmRmeFQ3WDdZUFhHQ0hHVG1uTzk4WkxDOTA2Zmkvekd2b04rNlpzbCs3MkpWMGxJWEo0V3dZdWxFUmZHbkFDWGNoa0Yzei9ITWR3elcwTUFFaXptQmwvREo2ZUoyU01PSG1Uc25YbElGRDRlcFRrYnFBQ0dpZ2I1UExFdHdQRVRjYkNRckM5YUtTU1FnSTdEZXd1aWlxM2J0Y0RUWkIzeEI5WWxlbmhpU0FXNjIwcmwzc2ZjY3d3eGFSOHBDV2Rzd0x3dmFxcDhjM01PV3RCc2xPcmVTSkNEcWgvdzBYbm1WMFJVWFpNM2JvUmkwVXhsaHVUeDFlM1NTd09pbTlOczNYV3NoTmI4Lzc3VkhnUWhRVFlSUU1NRllYaWRmMElCKzBtSUpocWNoQTlUeUY3dGRjSDhrUUJUSHNEWS96bFpqK3EwNlFMd0JkbTkxc3IyK3VzZmxlaXB3WUMrcmdiNHROVnA3VU5rYkVqTnR6ZWZsTi9VRTlkbHZtT2x6V1dtZkh2NGVkUGkzMmJmeUNRS1d6SGJVVEV3NU0yVFpsZnpNaTFWUjVsaDBxQ1lqaDNITUlmL2MwcHBKd2I1b1lFTnBBenlxbnlmdmlTV3lBYzc2L1l1VWwvb2FVaysrYzBZc2d1TGo5ZGFQdVVvemhoZ3VjSytQRGlNckI0ODU1Mk83VWg0aHRwNmZ3S2dJa1JCTVFIUTd6MmV5WXovV1AwQm9ZZVhjOGc3aUprclhFNzA1bFo1bXhGU0poT3E1WlNleVJSb21pUm41K3VRemM5ZFdWQjBYb2JURXdOc0VRM2FIZ25JY29BczY2UGplUT09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDI6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg== \ No newline at end of file +http://myapp.com/?RelayState=testshib&SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL215YXBwLmNvbSIgSUQ9Il8yNTk2NTFlOTY3ZGIwOGZjYTQ4MjdkODI3YWY1M2RkMCIgSW5SZXNwb25zZVRvPSJURVNUX0lEIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDUtMDlUMDM6NTc6NDMuNzkyWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI%2BaHR0cHM6Ly9pZHAudGVzdHNoaWIub3JnL2lkcC9zaGliYm9sZXRoPC9zYW1sMjpJc3N1ZXI%2BPHNhbWwycDpTdGF0dXM%2BPHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM%2BPHNhbWwyOkVuY3J5cHRlZEFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIElkPSJfMGM0NzYzNzIyOWFkNmEzMTY1OGU0MDc2ZDNlYzBmNmQiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiLz48ZHM6S2V5SW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI%2BPHhlbmM6RW5jcnlwdGVkS2V5IElkPSJfYjZmNmU2YWZjMzYyNGI3NmM1N2JmOWZhODA5YzAzNmMiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDc0RDQ0FobWdBd0lCQWdJSkFPN0J3ZGpEWmNVV01BMEdDU3FHU0liM0RRRUJCUVVBTUVVeEN6QUpCZ05WQkFZVEFrTkJNUmt3CkZ3WURWUVFJRXhCQ2NtbDBhWE5vSUVOdmJIVnRZbWxoTVJzd0dRWURWUVFLRXhKd2VYUm9iMjR0YzI5amFXRnNMV0YxZEdnd0hoY04KTVRVd05UQTRNRGMxT0RRMldoY05NalV3TlRBM01EYzFPRFEyV2pCRk1Rc3dDUVlEVlFRR0V3SkRRVEVaTUJjR0ExVUVDQk1RUW5KcApkR2x6YUNCRGIyeDFiV0pwWVRFYk1Ca0dBMVVFQ2hNU2NIbDBhRzl1TFhOdlkybGhiQzFoZFhSb01JR2ZNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNEdOQURDQmlRS0JnUUNxM2cxQ2wrM3VSNXZDbk40SGJnalRnK20zbkhodGVFTXliKyt5Y1pZcmUyYnhVZnNzaEVSNngzM2wKMjN0SGNrUll3bTdNZEJicnAzTHJWb2lPQ2RQYmxUbWwxSWhFUFRDd0tNaEJLdnZXcVR2Z2ZjU1NuUnpBV2tMbFFZU3VzYXl5Wks0bgo5cWNZa1Y1TUZuaTFyYmp4K01yNWFPRW1iNXUzM2FtTUtMd1NUd0lEQVFBQm80R25NSUdrTUIwR0ExVWREZ1FXQkJSUmlCUjZ6UzY2CmZLVm9rcDB5SkhiZ3YzUlltakIxQmdOVkhTTUViakJzZ0JSUmlCUjZ6UzY2ZktWb2twMHlKSGJndjNSWW1xRkpwRWN3UlRFTE1Ba0cKQTFVRUJoTUNRMEV4R1RBWEJnTlZCQWdURUVKeWFYUnBjMmdnUTI5c2RXMWlhV0V4R3pBWkJnTlZCQW9URW5CNWRHaHZiaTF6YjJOcApZV3d0WVhWMGFJSUpBTzdCd2RqRFpjVVdNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ1lFQUp3c01VM1lTCmF5YlZqdUo4VVMwZlVobFBPbE00MFFGQ0dMNHZCM1RFYmIyNE1xOEhyalV3clUwSkZQR2xzOWEyT1l6TjJCM2UzNU5vck11eHMrZ3IKR3RyMnlQNkx2dVgrblY2QTkzd2I0b29HSG9HZkM3VkxseXhTU25zOTM3U1M1UjFwelE0Z1d6Wm1hMktHV0tJQ1dwaDV6UTBBUlZoTAo2Mzk2N21HTG1vST08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BTElQdkVNVUVGeXhrVHowQ2N4QVA5TjV4Y3NYT2V4aVV4cXBvR2VIeVFMV0R5RVBBUDVnZ1daL3NLZ1ViL2xWSk92bCtuQXhSdVhXUlc5dGxSWWx3R2orRVhIOWhIbmdEY1BWMDNqSUJMQnFJbElBL1RmMGw4cVliOHFKRy9ZM0RTS2RQNkwvUURtYXBtTXpFM29YOEJxMW5Ea3YrUWh4cmQwMGVGK2ZMYVQ0PTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BRVpUWDhHTkM0My9yWStTUVlBMXRudHlUTTVVNkN2dUNCaktsVEVlekZPRjBZZHhCWUdFQVVjYU8xNVNKOXBMemJ1L1h0WGxzTkVMZTdKdEx4RUpwYUxubWFENnIranNWczdLaTBLNHRTMGNBUERDWHV2R1FoMmFOVjVQOGJ3N1JWUGhLOGQwYlJ1RklGR09FOHMwTTZYOUpxWDN4S0MvL1lSbVVoeDlybnU3ZWlwMGh5ZitPaUZiVGR2SDY2NTB2LzQ3aVdKcDNZeFlUV0QyMHBNbVRJMUpwWUEwYjByWVFQRkR0RU93d0JxYktxanRJc3ZYVFJzeXJhQkxvbnFOeHN5dHpEWHEra0JsMXp3WGUvSE5QcUVQblczdnNxaFhZcDVGM3dkWThkKzNCOTRMZlpOdUd4a0p3VDNzdVR0OGY5VHRBSlI4VytBUmtzT2M4eDBVaVNsVG5BNHFHOTBLMTR5dkVoVHcvd2drZjFXV01RT3dpZDNpakFYbUV4MU5MbVZvYUxYb3p4VExkTjN6YnJ6VEJIRXc3R2J3ZEdrdU5pMlhZOW16YUgwaWtGRm51VUxjMHUwc0pycEdGdzlaK0VlUk44RzNVUVZ5MjhtS2g3ZFBwWU5KbzhyajIxZFFaK2JaeUtTUHZablU3REkyakdJRE5US1g2ZkVyVWFINGlOTzN4cUU2Vk90L2d4T3BMNE5VNUhLV0Q0bG93VzcwdUJjVEVQRmhwaThpYUovdTB6YzUvTEhvdVBjMzByc1RLZFc5cmJLL2NWaHNQUHErZzA5WHZpZ0QweTJvN2tOc1pVL25tRXFiSzBKOTBrazhCR3I5cXRSczY4bUJnSURtUHVwUkhwWjM4eXNnU2VZN3V0VlVaSG5tQ0dzTzZ2NDJ6OTVOK05Pb3RCTEVZbFd1ZEdzYnowQWc4VkRDSlY5ak95QW95MDZyL1AyUHBsOFhjdmJza2d2T1BMMWdDNnVYbVJJS1lmOEw4UDJCNXVjN0haK0dtUHNOWXRLS2VKRDFFUHovdCt2NlBIbXNVb3dsSDhSd3FMRHdtMUF4dlNLQTR3UXBlQ0dQd3A5YXRYS0lWMS84NUZzRWMzajVzNjd6VlRybThrVEpydXV2MDZEdFVRZDNMOFdwTkV4cWhQait6RUp6U3RxSG04ckhNMVhNQUVxdVozc0xycTVqLzFSNlpqS0dOdFJCbjhwOE5ERGtrWm0vWTV5TXlJNXJJS3U5bnA3bXdaaEVpeWVHeHdxblV3VVMvUzVDRjNnMHVidnd4eVVnalVvd1ZvTkNqYktBbkdtT2VCSW5abkh0eGdIVUhVOUVlTFdyd2pRc3JtUmpJV0R2RkZQa3l6SzJDL20yaitubmNxc2E1OGRLVXZxcGR1VTRJYnNPQng3UGpXdXRBNmY5bXd6YWxyRU1NK0lGR3VPdk9HMC93eUdzQjZLREV6bldjUC83NkQ4angzaHZFSlAzN3REbFgreGM4Qno5TXdKdkd6VG4xbTdCb2xoR0lzSXlCTys1ZXpXa3RDWVVIUURGVE9wbXA0MDlOWHp6ZUNTUGY1U2NDWG5YYjRPd01ULy9VM1JFUnRRbGMrNmU2WG1JRjhoRkJVc0taUUJsS2ppSDkwZHlzYWlsNmN2V3UyQW55Q3QxbWxXcHFLc0MzU2RTRVZDTG1qRjlUQUFUMEtFSGdZQjg3RjZtZUpTTysvOXkyZkRuYVVvUUlUVzdubnVuSCtkT3dWSGZMU0wyL2N5YTltNlQzR29TSVNMbGJPMVRzalhKclVkZW55OTcvM2tkNmhFQlphdGY1U3NETFQ3SjNsQUVJNDROeXJ0NkIxQWdod2JNdkpqd1JNTXRNdUJLc3ltUytKVzc4UFNEWXQ4MG9waDJQTTc1N0tBNCtUMTAvYnZaQkE5Vk1OdVpqNVV3NXRWMnFIS3dwS0t6ZVVETUFiQlBRaGpYcXlQZzFKa09rd2RQMUpnOHRITjJTelBZQTlmT1htV0pBZGJDS2tMb0F4ZTV6cDZBUzYzS3FXMmFmSUt6SHJ3RTJmS1VtamppeURvMnNuMkJHbWtBaTRzbnpiVzc2SUQvSVgwd044aDBaQ2VRc29vKzdtb1RCMEJxSnBkS1MycXlsUktoc3BSTC9henVQdmxaK1pwckJxdXpJdEZkNFVLMkpzQkp6VXcwZkpxcTV1bk9PZENzVWM3SUU3QTNmZ1NmZ3NBd1R3WFZJMEVoME5ySWZpMkFKV1Z2VFpEMys2eFZ3dS96WWhuVjc0VXkvMFE4Mi8yQWtpSGpFRjNJVGNLWHdTNTB6bWtLakxjZDJqa2h5TUFYMWRoQ0wwZElFMUJoN0RNamVvNC9YbjBqSlpPL3Rrbi9xZmYzc3RNb1BYVG9KTnBIU1RjR2ZheGtaMzJYNCt3Q0xPc0VBRWxlMVZSY0kwUkZyOFhHTSsxWU9BTjBodFdGcFMxaG9kSi9OczJqL1FnUVNEemNpQ1FZeUFDd3lFRWZDZjZybnR0VmJyTlJQZWlmSHhBM3B2UnZ5ZGRhNDE5cXl0ZXI0akJ3cmw3ZUpuVnJ2VEprR2VhU2FRbDdXWk5SQXBscXRnNnZPYmpiMHZDRWlFaFhKbmNzQUhxcXp5QTRGeWFUVGQ2R0FySU9adUNxRWVoWk51T01lOVlrMVpya0VkR3pIalJESWk3Q1BKQk12NEZ4ZHI3bnJvN0I1WEhKb0ZMNE1DSUtOWWU2aWZiTUtYOU5uN1FWdnphUmY2UXlaSW1BWENQZndvU1BkN2x6NXl3UDJLSUIyaGhFMWt5eVZ5YVc5T0praWpUY3dvUnZrSXhIU0RqMXFqeGxueXh0QzhVZ1pNWmlwcGgzQXJpcjRiekIzUDhIbGIzejZ0OW51KzZMemNiN2ZObVo0UHluaU50Vk9OQ0lHbEh4dTBSY3hQK3cwUXNsM1BtTzJLaHBpc2RIanhvSUJ1YVY1NXdoTlFFNmdNNFBrT0xINDc4Rzg4bUxkd2s2RFpkWVl4L2d6RWE3b3ZIL0pReFp2TzRLdFVTNmZjZHJxV2thTFg1cEhkNkdneFBGZ2NFc2Nad1ZqM2hCS0xFQmE5L0dodERINEhzRnNRbmpPZnNDQkNzN0tjRitmTi9oSUdUeHFqTVlKVHJRYmNtdWF5dk9xR3RQMDFPcXltR24rVm5FSVkzKytQcm95SFN3K0Q0b0JIVG1maFNXRmJLZCtuTlVFS3BhRVIxNkdCU256WktQRVRVSmdRWEw5QWJRQ3RXVjFHb0UzRWNnMDZYaVd2aHFHakpGNldtdEU4dHY4Q25rZmxMNm91TDRvNldpbmx2WnNEdkZrS0R6TDkwUTNsWC9NanBtRTFpWU9uYzdISXdEVGwraFRRcHdsYXJiTDVUNGNkZTg1akNwYU0xU3p1TStiQU5zMHlXVDA0ZXJUVFc2cnhlbXFDTHAra202TVVMTlZOcE1CazBiQjJpRU82UlRtc3VpRlhDUU1xdU5xZjdkWXUwTFFCZzQ0MkJzU1pBV1ZrWEVZblduOURLdTRSby8veEFsb2h5VHozWlZmSkhuWVBSdDloSUErRHVUL3c4T2ZzTURIWnlCelUvL0JEa1NiNkxjMHdraVA3QlhIdjBoNVdud2dNWUxlZDBPalR5UWI2aGxpVnQ5b0FjaDRFVy9EZUlBdkpaQ1BYVm1pUFFYTGVsOVJIRko2bXFiYVo0TCtaZG1ONmQwcFZNZ1FveXhmQTR3dEwwYVpiNnFZYkhibjJMd2VBQVZwL3M2TzVlMVExdnZpZDRTWHo0a2l3RW1LSStIeXZEQ1pnekpQQVN5Z1gvWDJFWEZ0NGV3SjVmUFQyVXZmWnhQWlpqMFZGSFpyUFQwWVd2VE16bjUva3hoT09oM2drVGdDSmNwNWVsZnp4cEFPNFl1a0NoNHJXdVNndDRqVUJyaWNYbFdWdWo5U3JSZVhUalNHTktLK202NWovUDllNHRHT0RkMk9BbjNKTVQ3Q3FuaDhreTZpZjVjbmpVMmU3UDhTZnBONGwxWEFiZEZEcGk5bVJYamEyTzR1RWFHNGNvNW4xcWNDT3ZNMWYyblFBY1ZGNUFoSXhueS96TWhmU2l2RXdOQ0Zyd2tBWDRyQVE0WldUNldFakFyUG5jb1Y4Z1VRclhxQVA4NDJmK1lNWWI5RHFncmFicEg1a3ZuMnQzcWRldGJHODJ0QWlTamhPcUxNYW9iU2F4cXdWa1lUOHRTMW9rUUt2MWZoZ2t6elpEOE5IQnVQQzdNVHdXS0VCS2tDRUUzRWRFMXhNQURLd1B1M3NSaGpSaExXZyszZ2srejJtdlU4cTBhTlc0Y3hObUdoekx4eEY0Q3NFNStMQ1cwOWFpUVJOM1VvWmg1aktBZzBiMlh3WHBLS3pycUVTY1BYdnI0L1dWUTMyMm5qRWRvQVdXR0t2WnBKMlRlREo0eDdiT21LVElFc2RHWU1UZzFVaEU2eFFQcnhqS3dWeGFJNVJyaVE4a0xpaGgwa0t0WHQvYTVsSDhzUjVwR0ZISGZ3dlNVb3liQTB1eUVDNnNRVitPbTVReUZmRmpqZHFCOGNpOGxQS1hLTHFCTHJ6bjNmUkh3TmQwbzFiRTg0aGllTkx5UlhZVmhrRCtFNEpGaVd3ZWt3U3VWM3BjQk9ybnRVU3RoWmx6M3hIUURUVGNJNWliOFJyQ2swZEZ6YTgvQmw3VUdtWlUwSXZ2UmdvVXF2TXNHT2dMY3pGWmRpZnJ5aGNiUTY4a2ZzZ3lCMHppdC9MN1BSV3V4RkdYdDFoTVZSVUZ3WXBJS04zVkI3cXVKZlgwamZsU1JaRndMaXdlK3VhYndmTVZ6c2doajUvOXZNNzcwK0JaMGtJcE45NzBTMG5BbHl6R0h0aW1nTUl1RXFhbUt5QTNTQlI1aHZIYmRyNENnTHFUbXIzbFFnWmpnSkNvN1FXYUJWTXdCR0RpdzVOVVhUUnBycWc4U3h2eDlnNWZwbXMrL0o2QjFEelNTM3ZRZzgxdHFRU1ZDWVJpc0Y3M2VqZlFuZk4zcUszd3RJRDkxQnRISmFvMEFaUUdKVFpKOXVsZ0kzV3hzdWR4ejB0VHVpNlJlSWpmSWsxekZRdFpwRExGMnB3NGpTQVdQTlJqNDBYdVIrRzFUVlI3OVFiME9FYkw4RDFoTU5zWmo3MTZNbUhSOTlKaUxNdm1FWHV5a1V4VGhGYjRMTzZVbW1kU3UwTlBpMXQ2NmNkYURpQWhMaVBFTGdUNkZsenA2T2FGSGNSNjRncEtyemtTNDJONEhJeFpNa2R6M0FsYkRhK2pOWHZPR1l3UWl5K0xNNENZWGtrTWtHR3ZTWis5R2xWQ0l5RXBJaXIzbEQ3bmdzZGk4emxGWDYvekNaczlQSUtwZFZlSGJGZi9GS20wV3AreHI0Ykd0R0RrVHR2Nk1Manh2YU8zanFHaUFWeERKVWFkTVBlS2VHSm5uempTdnpKbGdOVHV3c3grRnF5L2dPMkwxMGowWmhDWi92dE9NelVjNjl3cGhKZm9FNzU3V3lOeFJOcThJc0Y1Tkg5Y0x0b3UvbUNxOTc3YnZPSkRrSURCN3lKWEJ6YUhVQkJuSXJra1Qyemg3bGJmUm5SREJUSFZraVZMazVESUxqeC9XL1BSZEZpUUM2SzRmZGx4Y29JbzlMcnM4ZFVWZkt2TTNNYnJ6c1hGT3ZtVVh0K3NsZldvd3UyTC9ndG9mRFhvTUJZZnlEcWIvWlRaRWZ0MC83blliRm1relBEUlZacU5SR0F3YWZVNTU1UjB2SWtNbGR2VjdKUzhNT1BNYWlXQVBpelNLRG4yRzNvcys1MzRFQytaOGZnWmFPVWpZL0xLME9vME9RMmhvNUV6MGNMYWpwUjFINk9FNEhvUm1ydjQzZkFjdGpYc0hYdi81RXg3emdrWk1NZXZhTFNEdjZtcjFGcDk4QXR4L296VTFGVDBoMDUxcVcwR0g2VWpRRXk5aExSZDBBMnFkUTRMZXpReDNvbDFTblhsamt2MG4zTXFlaFozOC94bzZhdHFDdkJtQkc3amlUdXd6YnlVUngzRm1TM0NCNllOYnFON3hPYVRZRnlkOEZDL01nY0xGQmMwS3F4MXllQ2VUd1hucldQb0dvdllVQlYxYjA1cWtIa1d5V0RUaCsveXJFNzF0RjNxbUQvd3F6cUJyNE04NERtWWVuQkdFOWxtb3FIZEMyWnRpK09KVFZKcmlHZWxQQ3RjZnZRaUlQcHdDZ3BFNmg1ekZhRndLajRuZGtBUkRpTC95L1EwWTZxNU5rM1g5RURlTmdjY1pIcFdmOUpKQ3M2a29wdXRtYjdDczIrbVJYdER1S09DaGY5UVUyN3Bmb1NJaklYK3NGdHY1c0hhSms2aHBZMlpzUUhzaTBYbFowc3FMTnQ5ayszdTVnYnBSU1JCczlHaC9BaVY0dkNyYTRkOTh5U0dCdzRSR1FhSStpQ29RaG9YK3lxc3VrYkx6bXJUU3FXMVRXaXJReUlHZ1Q5VnFERE1mUzAxeGdQSlNFSTlIWlp6TGlFVXVGMm1CMi81Y2dqaEFUaWQrdGV1UVB4aldhN2NSc2t5YUhuTENjQURVUU9ESUFPVjJDWXROcnAwY29ZL091S3ZzaXlJT0lacVJ5dE1PMGVNZ1ZJWTBzWmdxeVEycXlubUx0NDBmWmd3SFVyV245Zm9TYTNtMkVRTy9uOS8yU2NuelJWdVZpVnNjM0tCSElQL3AzNlJlSWowTGlNcCtPQ0p3SHlLVW1UeDRBU1V0dXVhWktlRHl1QjlxcXJuUEFNWUVCeElsTGFvdXMzV1pHakIrcW9ub3QvNmk1UE40bUZjbHFDcUxhMGJHbks4ZnJxYy9yd2tuVGV0YUE0c2tXTEw1L21qNEd5MitFQkh3a0x3UXd2K0FKdmZTOXYvNDl1LzY0N1ZFYW15UzdZQ2ZEUHNBQUREQ1FFcWJNQ1h2Ui8xVmEwWi9YUWhoNlkrZUt0MEVpRDdpNmRZODJtQkFoNEJMRmRVV3VGZHVrdUVwaGZ2WXB3N2loVjNxTjB1NFM1NTRXU0dUa0ZsdlpYNG1hbkF4a1g2ekQxS0NWaEFMdEJnSDgzdkhxam9uc0lwOFMydHgwZ0tiYzEreHVaRVppVWlNVVlVdTByQVFsRFcrZHJoN3lVRHZqekFHSnBmTk01eThaMW45em93VzZ5YW5VZWFBNjhSZDd5TUxobFd0NVh6bGhBTVZDZmZYZ0pFelR1YzJEbENVOXNMLzVTVkRaV2N4R1E5aFM1cnJtK2VyQ1Jxd2FJQk1DNUtza0RCZHdOWmh2Q0FCdEpqS2Vla1FUSjd5MFp4SGNhbGVCaU1rbkYwZVRDZzFvUEhPUVZLQ3V3NE94cHRZUS9xS1V0TEFIWFZ2OTlLMGRWcWZDMmpVQWlHQmVYa0t3aGRYTGtJYlZxU0EyZmxraXBBeEhYNnByUEExQjF3eTVab3hPUFg4RVExOW92eXpBbFg1dHU0OXEwWC9PSExFN1o5T1cxenltRXR6ZFpyNXJZbWtFcVdtcHVSNU5jeHFwTWlZam93dUNXZWhubzIyeG5JM09IQ0xDZkFKaHRrcklhL1hPc0tZRFpCRzFJMGJsN2taR2R5cEtUQlhYdXl6WE5WUlU5L005ejhaVytwdG1oZ2NOUzBJS2VaaSs5bFl4cWRlS3lnbldTTTV3czdSYUpmNlRRZTNSaWJZUjFvNkhwRzB2VHpiTEtQZTZnRjJGODdiWlBJei9mcTNLWnZiM3UrSnhZcCtJVjBtQi9VN29YelhRRk1RK3VmWllpNzUxbkx6WlVxRE1ybU53TFJPVUFNUk8rVnJtblkwSVB1cFBVMXc0b0hBb1dnVGRnTk5pNk1uTFQ4V0pmUlhjT0pKMk1lbUc2K2ZNeHNZUU52UVJwa1RGY05vaFV6Y3ZjcHJ3NUV3WEVZQTJzbzczL2MvY3RIRGcreU05YlF4REppUlltRnFydkhYb29hS1JyekxnUjZLVWdoM3ltaWxaQ0lSSm9KbTE3aEtHM1pxTTE0Lzl5OUc5OE9BZjNkVTlqMDk3aUNlaEc3a2VxYXRJQ2hFWmJqbmQ4Y00rS3djN2FtVWp2ekQzQmNvMHl3MDJxT054OWF3OGhSblZiWDZhdkRJbGhySHZ6SU44MzFvUjljRHBwMG1DUEJXZFVDQlNqVGJ1RkZqRC90WElSbGxlT2JraFFKSUdSNlE2U1MxcXkzT29WT1VheFl6THY0U2s3dndrQUMwUitGREVIeVFZbFVhbVVkTWcyUmdwRUdhSVd1V3IxaGNnRm10QmREV2g3ZFBuWTF0U3VKOC95MXp4NkRvN2ZJYmNFenBBK2E0ODNtRG5vemdld3VmaFdqVCsvUS85WlEreFQ5UWJBT1pQSXhHV3VhSXVrVk8zSWxvZDhJM1NGZFJCTHY5ZXBDNzFLeXpSdVlpMktkOHJ5NVNINit1WnMxUHlZUlpRakdDK3Q4VzRtSE82Z1lFRWVXSkJ1UWhnSHdmV2xhZXlWb3hac0NBQVZKRUllT3hPZDZtNW45OHRCUDdHTmgxT1M0eDRCS2FVN1A0UVQzNVVIZW5meE84WWFQUThmbXlobUJhSVJVZklBTVN2ZTJZRFp5SWNNTTkrN0tNSVVabzJ0eXRvYzdCOGVvZzBNaUkrVkpFdFg0c29FRjFSWkhQZVV3NWlCTjI4OTh2MmVTcGNnVUJhWHFzOUN5VlZtTVJQMEtLUDJ1REt4MUdJcUhjS0ZCOXVQVWRkQS9vT3dNa0tVUWsraFZVVDVPbEVMdjd1a0FBUEE0eE4rZkczVmYxeUVKV0FiVGx5dWtGcThjNXBTRkY1cXVHbUgwVmVpQzVvVEFka1VES3Z6WGhWWUs5c3BRYjNVZ1Z0Qld6N1ZScnlOUVVST3BIZU5xeDlhZHA4YWREWCtRSHJUKytYblN4VVI3SVdGanlNTkZJRWlMWmkxdks1UVVrZlRDUU9qdjh2SHdiUi9MRHF3Z3M5bXdsT3pPY0RLdVBVK0dTb2lnVFdRejRWN0N2SHRaVDI3WUdKVG44RFFFM3IzdjB4aWxvODJ2U3VXSDg0WEU3VEJsTUpFb2R5eDNDRngwVUVkc3VhRHBPSEV3UjZYNlUyU0xseERYSXVZeEhlNXh2NjI4bXU0bDRMSnBYUjhkYmljTEZKQW55Q0FVeDJLb2dDamt1cmU4bXNUZktDbG8wamFlN1hNR05PSk15b0ZYbVlHZUh2eGhNUGMzTEtYLy9VY1p0c3p3dFJrQmNFdURXQysvQWNWZVBOSHVOWWI5MEpIcnRucGg1ZDlhL1lpTkpzY1N3QTFwUVZrdW1TQWtPQWdLdWRzcnl3c0N3Zkg1anNydVpHUTJDd1hKRXQzUU4wU2NLUlVnT1NCQ3FYa1BqZDVSVzJuOFZpamt4anovbWptakhCNmk0eHM5NEU2Nzk5STAyaldYNVd3UDZhTFRaTGt5TjhxNDUxT0RmeUZVZEY5WWsyZXQ5VUpsV1NzRFJMSWVCd0ZyQkEyZTdyRWsybWFLVUNCRW5PUWM2bUhVMXQvZ3gzK1VXVVFXbkpMZVUxbWUvbkFEdy96UGUwd3d0Vm9BaERZdDBoR1hQblJydjFoUHRGS01CeWtqckg3a0J5U0R3WDlQMi9XZkNkQlE5K1J4cHRsR2hvRmdpMUs0NVlOeEpEd05wTmd5MDV2WXUzVUtrMkpRYVNGUzcwK0Y1NzluRE5RenZpK0pPRlRsdDFmWDJGNXk5NEV2NHZobWRQSmRVOFVVRjU2Ymx0emxKREVFdmsySlFrOTM0aHpwTXJGZ1d3ZHUxUkxxSEhCN2h2T2hnaHNqV0ZGY01zNjZaRUtWcVhKUytxWWNVMHk0akwySVQrNlF2N2pvQ3BWbUdzUWtGY1FyblhxOUJiOTdaUS96UCtwaldmWTU0UmNRVlMydUU1YURObVVyVkdLK3E0d0xRcUhuRVViT2puSHFFeGlacUtxOVdRaUtUK2c3QS96bVlIQ2k0YzFTejRNVWhHb0t6U2l4aXoxYUNJUEJXdy9vczR2cUVqbXgzOGx6YnV0OWNWbElzeGNkTUpUTERRK3ZOZ0YyY1ZRaVcxRTQ0d3lWcnI3TUFaOE9KRVpFSzlEZWt5MzJQUkFuSkRUVXVqdGFscmJ0T2VOczhyS09uTjcvNFRqUEwvZmRlbEI4bjA4WXdSNXdmbU42VGpGWUhRSDFjbUZmK1AvNUxVMTI4Q1pEYjNQUStxMlFJazV3aE40eGwvcy9lb29pallmeWtDcm5aSEhHWkluTGhoU2pWbk5ISWdTL203VWV0NlhBTDdvZUl5UFRLeHVnbDJzRWtUQzNnZ0tjTnFZR0E5U3ZlYVlaQ00vWHNQRUtQbWs3QmlRNmprWFBKaE1yREd4Vkc0SW9aSDgrYjBrUWJYR2l0Mkw0L3hZdHh1bTVzcFNPSjdsTDltVFpRNnBxM2JOaTEwZU1mZ0ZWaDc3NU5JRlc0SEp3U1FtaTU0bk11blZTQjhxdjZKc0w3SGlsZ2N0ZHFSNThTTjVad1lCa2dOR1hzYjA1QXJWemVXbHh1Y21BSHNPT3dyczFnMzh6bTRZN2ZPZmducmFhV1kxanZZOFlEODZQZThkZzR4cE5paTg3UnNDZk5WK2NKVmMraktFdnpuZVY1Zzd0RmlxZCtsZHp4STlKemdSS2t0WUV6RUpRSVU5M2UvclJaN1lrVkZtNVV1cjVhMWYzcG83T0VtYkJUc2MrQ1FaOGNnYmIvbUphRXJoa3NyL3JURjBNcjNxeDl5SlJWSEJ6YWNWd0dScEFRaURPdnJkWU4xQXBVOTRyR1lrVFVzdWs1YjE1Wll2QVZxRlRzVlVMaS9HY29mbEljMm01Z2RFTFZOblRmdXY1Zlk5S1NlWHFoUU80S0pOYVZmbHAwQ0VKYWFFZFNLUXJJNXRaT2w1RkE4VXZlNmxTWVd5TVk0REl4a1RiT1JoWHVBdzR6b1RTMjgrN3d2TXhydVBkZnlKbUJCTkhQdCtEYmdKNHovcHJZWUhpTmFMTXNZamtQZE44ajNKZDczQXJFZk92Um52MzYxSVVVMFg1RDc1dlRSdlpkbzMzWERzanRlOU4weUo3K2lIQnF1a1FJY2pIVW9ic2RQN0hOajBVYWNSMHIvTmRlVTlGNFBNc1VLY2t6Tk4rZGhyMVI2d1J2R1VZb1pDRWJaWlJMWEt4QnA3SElUNEVQUktHakIvdW1xTFhhMXl6RWx2QW1WQUJhMDFZN3dGdk4wM2Ywb25FbUhTM2w1d1paRmV6cjVibnN5T01XVGxhMU5kaW1ZNXNVeE15VFliZmc4dzB2cXNEc28zWFAxYndLdzZ3M3VIRGQ1UHBSWnVDSnR0eWk0ZzJGeWI0Ymg1UU42ZkdORTI2ekRGN1Y4QmJwZXJLNkFKQ0xTWm5kaDZMMTlPUTBram4xUGpEMGk4c1BZcGFXOWxVeVJkZElPKzRWQS9LemxPUzJ4M2s5VUtUdElsTTBUSVdtZXFIS0dYUVpocGpvVGI2VlNKN203cjZaaVlQMnVsQVVvZmVWL0o2eCtzckxEQXkyQ2ZFNnFrREZ1OU9NWDBBSXVnN3loQUtOMDRyT3hVNk5tcGtjOUZ4bXUvVS9vR3hHdmIzeFVFTDYwdE1sSE9EaWtqY1I5RDJrKzRwbEc1WnV0d0FIY2kwRU02WHRrVEhQOU5QMlRTR1VFN1E5SGYvU0VEc2V0a25hZXhvWmhDczJLWDFMeU5JS0U0N2pkMkR3MTUreDRRVXV0VUFTbzU5Q1lHMVFBeW9BVVhrV3dtbXkzTGdTUWp5T3ZLV25qaE8veWpPd0FyWGd0NFBrSVVnZDQ1N05ReFpMbU41K0J4NVJoQ0FHdkUxYmxOZjlMek9keGJiaG5VZ2Z1RDM5MXVSRkhjS2RYREY3ZmVqb3gveThtaWZJcTRWVzQyajBHQnFOQUtkK0prMnJCMW9hOTRiT2hxcVVzanhqWnlRaGRXTzhNblR6T2tOaGVpZXU2blYxcW5yZ3JHU2huWTNJMlczb29GNFNnczRjZ3drZ2h2dHpFa0xUbU5OUm83RTdudVRuMkxJcmlGSnlvTmZQdUp0aWN0S0JtNzRGZytkWVBTMlIzTzNmOWxBZWxiVWZjbzZGNU9EL3hkS1VuRTh0V3FOMExVcDlWQUptWVZYZFVDaGJ4MjM4MWtDaStLNDJoRzUydFNQYU1hb1dTb0xQY2Zrb24rc1pYdjdEdEtwZi9HTzdhcUMza1pzRGpva29haHJGZGJWSlNTZWhrNGp5K3RzRHplQnJKSjBrMVZrUnJHN1NoVHZjTmd1cjVucVRUTEE5dlJMQmJNTTlhNlI1NEZ0Z1pQOWFKMU1aMEdCcUVpMnF6Ui8yd2tYQlhwcFhZdi9TcU1RV1dhbTVsSHBMVktxaDN4ZHRjNFdmck9mYldsbU1PNXA5Z0JUSFp1YUcxVGFkZXFRVVpKQmZBS01ENFdSR0NsMDFaeDRTVzE0YzZrdnFKdXExL080N215L3RsVHlLWndpYlBkQTNRMVVGd0I3R2Z4anEwaDN2ckxFbUNrS3Vsc0VBUkN6UnZNVjJSVnBVbFpUV240Y1Boc0hjcTNROElHSUYyKy9nOENFSU4vMU8xcVMvMkpXcXlDNmtIb0w4Y2R2R0VHbmkxSTNDTk1JcXhxaHhJL1V0R3REc2VwYmwrSHI0elh4MzZna3BCbXBoT2xkTFVYTHAzVEtibVVZRWJSWHcvZmRmeFQ3WDdZUFhHQ0hHVG1uTzk4WkxDOTA2Zmkvekd2b04rNlpzbCs3MkpWMGxJWEo0V3dZdWxFUmZHbkFDWGNoa0Yzei9ITWR3elcwTUFFaXptQmwvREo2ZUoyU01PSG1Uc25YbElGRDRlcFRrYnFBQ0dpZ2I1UExFdHdQRVRjYkNRckM5YUtTU1FnSTdEZXd1aWlxM2J0Y0RUWkIzeEI5WWxlbmhpU0FXNjIwcmwzc2ZjY3d3eGFSOHBDV2Rzd0x3dmFxcDhjM01PV3RCc2xPcmVTSkNEcWgvdzBYbm1WMFJVWFpNM2JvUmkwVXhsaHVUeDFlM1NTd09pbTlOczNYV3NoTmI4Lzc3VkhnUWhRVFlSUU1NRllYaWRmMElCKzBtSUpocWNoQTlUeUY3dGRjSDhrUUJUSHNEWS96bFpqK3EwNlFMd0JkbTkxc3IyK3VzZmxlaXB3WUMrcmdiNHROVnA3VU5rYkVqTnR6ZWZsTi9VRTlkbHZtT2x6V1dtZkh2NGVkUGkzMmJmeUNRS1d6SGJVVEV3NU0yVFpsZnpNaTFWUjVsaDBxQ1lqaDNITUlmL2MwcHBKd2I1b1lFTnBBenlxbnlmdmlTV3lBYzc2L1l1VWwvb2FVaysrYzBZc2d1TGo5ZGFQdVVvemhoZ3VjSytQRGlNckI0ODU1Mk83VWg0aHRwNmZ3S2dJa1JCTVFIUTd6MmV5WXovV1AwQm9ZZVhjOGc3aUprclhFNzA1bFo1bXhGU0poT3E1WlNleVJSb21pUm41K3VRemM5ZFdWQjBYb2JURXdOc0VRM2FIZ25JY29BczY2UGplUT09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDI6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg== diff --git a/social_core/tests/backends/data/saml_response_no_idp_name.txt b/social_core/tests/backends/data/saml_response_no_idp_name.txt index 2f57b07ae..5e7d736b9 100644 --- a/social_core/tests/backends/data/saml_response_no_idp_name.txt +++ b/social_core/tests/backends/data/saml_response_no_idp_name.txt @@ -1 +1 @@ -http://myapp.com/?RelayState=%7b%7d&SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL215YXBwLmNvbSIgSUQ9Il8yNTk2NTFlOTY3ZGIwOGZjYTQ4MjdkODI3YWY1M2RkMCIgSW5SZXNwb25zZVRvPSJURVNUX0lEIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDUtMDlUMDM6NTc6NDMuNzkyWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI%2BaHR0cHM6Ly9pZHAudGVzdHNoaWIub3JnL2lkcC9zaGliYm9sZXRoPC9zYW1sMjpJc3N1ZXI%2BPHNhbWwycDpTdGF0dXM%2BPHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM%2BPHNhbWwyOkVuY3J5cHRlZEFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIElkPSJfMGM0NzYzNzIyOWFkNmEzMTY1OGU0MDc2ZDNlYzBmNmQiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiLz48ZHM6S2V5SW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI%2BPHhlbmM6RW5jcnlwdGVkS2V5IElkPSJfYjZmNmU2YWZjMzYyNGI3NmM1N2JmOWZhODA5YzAzNmMiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDc0RDQ0FobWdBd0lCQWdJSkFPN0J3ZGpEWmNVV01BMEdDU3FHU0liM0RRRUJCUVVBTUVVeEN6QUpCZ05WQkFZVEFrTkJNUmt3CkZ3WURWUVFJRXhCQ2NtbDBhWE5vSUVOdmJIVnRZbWxoTVJzd0dRWURWUVFLRXhKd2VYUm9iMjR0YzI5amFXRnNMV0YxZEdnd0hoY04KTVRVd05UQTRNRGMxT0RRMldoY05NalV3TlRBM01EYzFPRFEyV2pCRk1Rc3dDUVlEVlFRR0V3SkRRVEVaTUJjR0ExVUVDQk1RUW5KcApkR2x6YUNCRGIyeDFiV0pwWVRFYk1Ca0dBMVVFQ2hNU2NIbDBhRzl1TFhOdlkybGhiQzFoZFhSb01JR2ZNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNEdOQURDQmlRS0JnUUNxM2cxQ2wrM3VSNXZDbk40SGJnalRnK20zbkhodGVFTXliKyt5Y1pZcmUyYnhVZnNzaEVSNngzM2wKMjN0SGNrUll3bTdNZEJicnAzTHJWb2lPQ2RQYmxUbWwxSWhFUFRDd0tNaEJLdnZXcVR2Z2ZjU1NuUnpBV2tMbFFZU3VzYXl5Wks0bgo5cWNZa1Y1TUZuaTFyYmp4K01yNWFPRW1iNXUzM2FtTUtMd1NUd0lEQVFBQm80R25NSUdrTUIwR0ExVWREZ1FXQkJSUmlCUjZ6UzY2CmZLVm9rcDB5SkhiZ3YzUlltakIxQmdOVkhTTUViakJzZ0JSUmlCUjZ6UzY2ZktWb2twMHlKSGJndjNSWW1xRkpwRWN3UlRFTE1Ba0cKQTFVRUJoTUNRMEV4R1RBWEJnTlZCQWdURUVKeWFYUnBjMmdnUTI5c2RXMWlhV0V4R3pBWkJnTlZCQW9URW5CNWRHaHZiaTF6YjJOcApZV3d0WVhWMGFJSUpBTzdCd2RqRFpjVVdNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ1lFQUp3c01VM1lTCmF5YlZqdUo4VVMwZlVobFBPbE00MFFGQ0dMNHZCM1RFYmIyNE1xOEhyalV3clUwSkZQR2xzOWEyT1l6TjJCM2UzNU5vck11eHMrZ3IKR3RyMnlQNkx2dVgrblY2QTkzd2I0b29HSG9HZkM3VkxseXhTU25zOTM3U1M1UjFwelE0Z1d6Wm1hMktHV0tJQ1dwaDV6UTBBUlZoTAo2Mzk2N21HTG1vST08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BTElQdkVNVUVGeXhrVHowQ2N4QVA5TjV4Y3NYT2V4aVV4cXBvR2VIeVFMV0R5RVBBUDVnZ1daL3NLZ1ViL2xWSk92bCtuQXhSdVhXUlc5dGxSWWx3R2orRVhIOWhIbmdEY1BWMDNqSUJMQnFJbElBL1RmMGw4cVliOHFKRy9ZM0RTS2RQNkwvUURtYXBtTXpFM29YOEJxMW5Ea3YrUWh4cmQwMGVGK2ZMYVQ0PTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BRVpUWDhHTkM0My9yWStTUVlBMXRudHlUTTVVNkN2dUNCaktsVEVlekZPRjBZZHhCWUdFQVVjYU8xNVNKOXBMemJ1L1h0WGxzTkVMZTdKdEx4RUpwYUxubWFENnIranNWczdLaTBLNHRTMGNBUERDWHV2R1FoMmFOVjVQOGJ3N1JWUGhLOGQwYlJ1RklGR09FOHMwTTZYOUpxWDN4S0MvL1lSbVVoeDlybnU3ZWlwMGh5ZitPaUZiVGR2SDY2NTB2LzQ3aVdKcDNZeFlUV0QyMHBNbVRJMUpwWUEwYjByWVFQRkR0RU93d0JxYktxanRJc3ZYVFJzeXJhQkxvbnFOeHN5dHpEWHEra0JsMXp3WGUvSE5QcUVQblczdnNxaFhZcDVGM3dkWThkKzNCOTRMZlpOdUd4a0p3VDNzdVR0OGY5VHRBSlI4VytBUmtzT2M4eDBVaVNsVG5BNHFHOTBLMTR5dkVoVHcvd2drZjFXV01RT3dpZDNpakFYbUV4MU5MbVZvYUxYb3p4VExkTjN6YnJ6VEJIRXc3R2J3ZEdrdU5pMlhZOW16YUgwaWtGRm51VUxjMHUwc0pycEdGdzlaK0VlUk44RzNVUVZ5MjhtS2g3ZFBwWU5KbzhyajIxZFFaK2JaeUtTUHZablU3REkyakdJRE5US1g2ZkVyVWFINGlOTzN4cUU2Vk90L2d4T3BMNE5VNUhLV0Q0bG93VzcwdUJjVEVQRmhwaThpYUovdTB6YzUvTEhvdVBjMzByc1RLZFc5cmJLL2NWaHNQUHErZzA5WHZpZ0QweTJvN2tOc1pVL25tRXFiSzBKOTBrazhCR3I5cXRSczY4bUJnSURtUHVwUkhwWjM4eXNnU2VZN3V0VlVaSG5tQ0dzTzZ2NDJ6OTVOK05Pb3RCTEVZbFd1ZEdzYnowQWc4VkRDSlY5ak95QW95MDZyL1AyUHBsOFhjdmJza2d2T1BMMWdDNnVYbVJJS1lmOEw4UDJCNXVjN0haK0dtUHNOWXRLS2VKRDFFUHovdCt2NlBIbXNVb3dsSDhSd3FMRHdtMUF4dlNLQTR3UXBlQ0dQd3A5YXRYS0lWMS84NUZzRWMzajVzNjd6VlRybThrVEpydXV2MDZEdFVRZDNMOFdwTkV4cWhQait6RUp6U3RxSG04ckhNMVhNQUVxdVozc0xycTVqLzFSNlpqS0dOdFJCbjhwOE5ERGtrWm0vWTV5TXlJNXJJS3U5bnA3bXdaaEVpeWVHeHdxblV3VVMvUzVDRjNnMHVidnd4eVVnalVvd1ZvTkNqYktBbkdtT2VCSW5abkh0eGdIVUhVOUVlTFdyd2pRc3JtUmpJV0R2RkZQa3l6SzJDL20yaitubmNxc2E1OGRLVXZxcGR1VTRJYnNPQng3UGpXdXRBNmY5bXd6YWxyRU1NK0lGR3VPdk9HMC93eUdzQjZLREV6bldjUC83NkQ4angzaHZFSlAzN3REbFgreGM4Qno5TXdKdkd6VG4xbTdCb2xoR0lzSXlCTys1ZXpXa3RDWVVIUURGVE9wbXA0MDlOWHp6ZUNTUGY1U2NDWG5YYjRPd01ULy9VM1JFUnRRbGMrNmU2WG1JRjhoRkJVc0taUUJsS2ppSDkwZHlzYWlsNmN2V3UyQW55Q3QxbWxXcHFLc0MzU2RTRVZDTG1qRjlUQUFUMEtFSGdZQjg3RjZtZUpTTysvOXkyZkRuYVVvUUlUVzdubnVuSCtkT3dWSGZMU0wyL2N5YTltNlQzR29TSVNMbGJPMVRzalhKclVkZW55OTcvM2tkNmhFQlphdGY1U3NETFQ3SjNsQUVJNDROeXJ0NkIxQWdod2JNdkpqd1JNTXRNdUJLc3ltUytKVzc4UFNEWXQ4MG9waDJQTTc1N0tBNCtUMTAvYnZaQkE5Vk1OdVpqNVV3NXRWMnFIS3dwS0t6ZVVETUFiQlBRaGpYcXlQZzFKa09rd2RQMUpnOHRITjJTelBZQTlmT1htV0pBZGJDS2tMb0F4ZTV6cDZBUzYzS3FXMmFmSUt6SHJ3RTJmS1VtamppeURvMnNuMkJHbWtBaTRzbnpiVzc2SUQvSVgwd044aDBaQ2VRc29vKzdtb1RCMEJxSnBkS1MycXlsUktoc3BSTC9henVQdmxaK1pwckJxdXpJdEZkNFVLMkpzQkp6VXcwZkpxcTV1bk9PZENzVWM3SUU3QTNmZ1NmZ3NBd1R3WFZJMEVoME5ySWZpMkFKV1Z2VFpEMys2eFZ3dS96WWhuVjc0VXkvMFE4Mi8yQWtpSGpFRjNJVGNLWHdTNTB6bWtLakxjZDJqa2h5TUFYMWRoQ0wwZElFMUJoN0RNamVvNC9YbjBqSlpPL3Rrbi9xZmYzc3RNb1BYVG9KTnBIU1RjR2ZheGtaMzJYNCt3Q0xPc0VBRWxlMVZSY0kwUkZyOFhHTSsxWU9BTjBodFdGcFMxaG9kSi9OczJqL1FnUVNEemNpQ1FZeUFDd3lFRWZDZjZybnR0VmJyTlJQZWlmSHhBM3B2UnZ5ZGRhNDE5cXl0ZXI0akJ3cmw3ZUpuVnJ2VEprR2VhU2FRbDdXWk5SQXBscXRnNnZPYmpiMHZDRWlFaFhKbmNzQUhxcXp5QTRGeWFUVGQ2R0FySU9adUNxRWVoWk51T01lOVlrMVpya0VkR3pIalJESWk3Q1BKQk12NEZ4ZHI3bnJvN0I1WEhKb0ZMNE1DSUtOWWU2aWZiTUtYOU5uN1FWdnphUmY2UXlaSW1BWENQZndvU1BkN2x6NXl3UDJLSUIyaGhFMWt5eVZ5YVc5T0praWpUY3dvUnZrSXhIU0RqMXFqeGxueXh0QzhVZ1pNWmlwcGgzQXJpcjRiekIzUDhIbGIzejZ0OW51KzZMemNiN2ZObVo0UHluaU50Vk9OQ0lHbEh4dTBSY3hQK3cwUXNsM1BtTzJLaHBpc2RIanhvSUJ1YVY1NXdoTlFFNmdNNFBrT0xINDc4Rzg4bUxkd2s2RFpkWVl4L2d6RWE3b3ZIL0pReFp2TzRLdFVTNmZjZHJxV2thTFg1cEhkNkdneFBGZ2NFc2Nad1ZqM2hCS0xFQmE5L0dodERINEhzRnNRbmpPZnNDQkNzN0tjRitmTi9oSUdUeHFqTVlKVHJRYmNtdWF5dk9xR3RQMDFPcXltR24rVm5FSVkzKytQcm95SFN3K0Q0b0JIVG1maFNXRmJLZCtuTlVFS3BhRVIxNkdCU256WktQRVRVSmdRWEw5QWJRQ3RXVjFHb0UzRWNnMDZYaVd2aHFHakpGNldtdEU4dHY4Q25rZmxMNm91TDRvNldpbmx2WnNEdkZrS0R6TDkwUTNsWC9NanBtRTFpWU9uYzdISXdEVGwraFRRcHdsYXJiTDVUNGNkZTg1akNwYU0xU3p1TStiQU5zMHlXVDA0ZXJUVFc2cnhlbXFDTHAra202TVVMTlZOcE1CazBiQjJpRU82UlRtc3VpRlhDUU1xdU5xZjdkWXUwTFFCZzQ0MkJzU1pBV1ZrWEVZblduOURLdTRSby8veEFsb2h5VHozWlZmSkhuWVBSdDloSUErRHVUL3c4T2ZzTURIWnlCelUvL0JEa1NiNkxjMHdraVA3QlhIdjBoNVdud2dNWUxlZDBPalR5UWI2aGxpVnQ5b0FjaDRFVy9EZUlBdkpaQ1BYVm1pUFFYTGVsOVJIRko2bXFiYVo0TCtaZG1ONmQwcFZNZ1FveXhmQTR3dEwwYVpiNnFZYkhibjJMd2VBQVZwL3M2TzVlMVExdnZpZDRTWHo0a2l3RW1LSStIeXZEQ1pnekpQQVN5Z1gvWDJFWEZ0NGV3SjVmUFQyVXZmWnhQWlpqMFZGSFpyUFQwWVd2VE16bjUva3hoT09oM2drVGdDSmNwNWVsZnp4cEFPNFl1a0NoNHJXdVNndDRqVUJyaWNYbFdWdWo5U3JSZVhUalNHTktLK202NWovUDllNHRHT0RkMk9BbjNKTVQ3Q3FuaDhreTZpZjVjbmpVMmU3UDhTZnBONGwxWEFiZEZEcGk5bVJYamEyTzR1RWFHNGNvNW4xcWNDT3ZNMWYyblFBY1ZGNUFoSXhueS96TWhmU2l2RXdOQ0Zyd2tBWDRyQVE0WldUNldFakFyUG5jb1Y4Z1VRclhxQVA4NDJmK1lNWWI5RHFncmFicEg1a3ZuMnQzcWRldGJHODJ0QWlTamhPcUxNYW9iU2F4cXdWa1lUOHRTMW9rUUt2MWZoZ2t6elpEOE5IQnVQQzdNVHdXS0VCS2tDRUUzRWRFMXhNQURLd1B1M3NSaGpSaExXZyszZ2srejJtdlU4cTBhTlc0Y3hObUdoekx4eEY0Q3NFNStMQ1cwOWFpUVJOM1VvWmg1aktBZzBiMlh3WHBLS3pycUVTY1BYdnI0L1dWUTMyMm5qRWRvQVdXR0t2WnBKMlRlREo0eDdiT21LVElFc2RHWU1UZzFVaEU2eFFQcnhqS3dWeGFJNVJyaVE4a0xpaGgwa0t0WHQvYTVsSDhzUjVwR0ZISGZ3dlNVb3liQTB1eUVDNnNRVitPbTVReUZmRmpqZHFCOGNpOGxQS1hLTHFCTHJ6bjNmUkh3TmQwbzFiRTg0aGllTkx5UlhZVmhrRCtFNEpGaVd3ZWt3U3VWM3BjQk9ybnRVU3RoWmx6M3hIUURUVGNJNWliOFJyQ2swZEZ6YTgvQmw3VUdtWlUwSXZ2UmdvVXF2TXNHT2dMY3pGWmRpZnJ5aGNiUTY4a2ZzZ3lCMHppdC9MN1BSV3V4RkdYdDFoTVZSVUZ3WXBJS04zVkI3cXVKZlgwamZsU1JaRndMaXdlK3VhYndmTVZ6c2doajUvOXZNNzcwK0JaMGtJcE45NzBTMG5BbHl6R0h0aW1nTUl1RXFhbUt5QTNTQlI1aHZIYmRyNENnTHFUbXIzbFFnWmpnSkNvN1FXYUJWTXdCR0RpdzVOVVhUUnBycWc4U3h2eDlnNWZwbXMrL0o2QjFEelNTM3ZRZzgxdHFRU1ZDWVJpc0Y3M2VqZlFuZk4zcUszd3RJRDkxQnRISmFvMEFaUUdKVFpKOXVsZ0kzV3hzdWR4ejB0VHVpNlJlSWpmSWsxekZRdFpwRExGMnB3NGpTQVdQTlJqNDBYdVIrRzFUVlI3OVFiME9FYkw4RDFoTU5zWmo3MTZNbUhSOTlKaUxNdm1FWHV5a1V4VGhGYjRMTzZVbW1kU3UwTlBpMXQ2NmNkYURpQWhMaVBFTGdUNkZsenA2T2FGSGNSNjRncEtyemtTNDJONEhJeFpNa2R6M0FsYkRhK2pOWHZPR1l3UWl5K0xNNENZWGtrTWtHR3ZTWis5R2xWQ0l5RXBJaXIzbEQ3bmdzZGk4emxGWDYvekNaczlQSUtwZFZlSGJGZi9GS20wV3AreHI0Ykd0R0RrVHR2Nk1Manh2YU8zanFHaUFWeERKVWFkTVBlS2VHSm5uempTdnpKbGdOVHV3c3grRnF5L2dPMkwxMGowWmhDWi92dE9NelVjNjl3cGhKZm9FNzU3V3lOeFJOcThJc0Y1Tkg5Y0x0b3UvbUNxOTc3YnZPSkRrSURCN3lKWEJ6YUhVQkJuSXJra1Qyemg3bGJmUm5SREJUSFZraVZMazVESUxqeC9XL1BSZEZpUUM2SzRmZGx4Y29JbzlMcnM4ZFVWZkt2TTNNYnJ6c1hGT3ZtVVh0K3NsZldvd3UyTC9ndG9mRFhvTUJZZnlEcWIvWlRaRWZ0MC83blliRm1relBEUlZacU5SR0F3YWZVNTU1UjB2SWtNbGR2VjdKUzhNT1BNYWlXQVBpelNLRG4yRzNvcys1MzRFQytaOGZnWmFPVWpZL0xLME9vME9RMmhvNUV6MGNMYWpwUjFINk9FNEhvUm1ydjQzZkFjdGpYc0hYdi81RXg3emdrWk1NZXZhTFNEdjZtcjFGcDk4QXR4L296VTFGVDBoMDUxcVcwR0g2VWpRRXk5aExSZDBBMnFkUTRMZXpReDNvbDFTblhsamt2MG4zTXFlaFozOC94bzZhdHFDdkJtQkc3amlUdXd6YnlVUngzRm1TM0NCNllOYnFON3hPYVRZRnlkOEZDL01nY0xGQmMwS3F4MXllQ2VUd1hucldQb0dvdllVQlYxYjA1cWtIa1d5V0RUaCsveXJFNzF0RjNxbUQvd3F6cUJyNE04NERtWWVuQkdFOWxtb3FIZEMyWnRpK09KVFZKcmlHZWxQQ3RjZnZRaUlQcHdDZ3BFNmg1ekZhRndLajRuZGtBUkRpTC95L1EwWTZxNU5rM1g5RURlTmdjY1pIcFdmOUpKQ3M2a29wdXRtYjdDczIrbVJYdER1S09DaGY5UVUyN3Bmb1NJaklYK3NGdHY1c0hhSms2aHBZMlpzUUhzaTBYbFowc3FMTnQ5ayszdTVnYnBSU1JCczlHaC9BaVY0dkNyYTRkOTh5U0dCdzRSR1FhSStpQ29RaG9YK3lxc3VrYkx6bXJUU3FXMVRXaXJReUlHZ1Q5VnFERE1mUzAxeGdQSlNFSTlIWlp6TGlFVXVGMm1CMi81Y2dqaEFUaWQrdGV1UVB4aldhN2NSc2t5YUhuTENjQURVUU9ESUFPVjJDWXROcnAwY29ZL091S3ZzaXlJT0lacVJ5dE1PMGVNZ1ZJWTBzWmdxeVEycXlubUx0NDBmWmd3SFVyV245Zm9TYTNtMkVRTy9uOS8yU2NuelJWdVZpVnNjM0tCSElQL3AzNlJlSWowTGlNcCtPQ0p3SHlLVW1UeDRBU1V0dXVhWktlRHl1QjlxcXJuUEFNWUVCeElsTGFvdXMzV1pHakIrcW9ub3QvNmk1UE40bUZjbHFDcUxhMGJHbks4ZnJxYy9yd2tuVGV0YUE0c2tXTEw1L21qNEd5MitFQkh3a0x3UXd2K0FKdmZTOXYvNDl1LzY0N1ZFYW15UzdZQ2ZEUHNBQUREQ1FFcWJNQ1h2Ui8xVmEwWi9YUWhoNlkrZUt0MEVpRDdpNmRZODJtQkFoNEJMRmRVV3VGZHVrdUVwaGZ2WXB3N2loVjNxTjB1NFM1NTRXU0dUa0ZsdlpYNG1hbkF4a1g2ekQxS0NWaEFMdEJnSDgzdkhxam9uc0lwOFMydHgwZ0tiYzEreHVaRVppVWlNVVlVdTByQVFsRFcrZHJoN3lVRHZqekFHSnBmTk01eThaMW45em93VzZ5YW5VZWFBNjhSZDd5TUxobFd0NVh6bGhBTVZDZmZYZ0pFelR1YzJEbENVOXNMLzVTVkRaV2N4R1E5aFM1cnJtK2VyQ1Jxd2FJQk1DNUtza0RCZHdOWmh2Q0FCdEpqS2Vla1FUSjd5MFp4SGNhbGVCaU1rbkYwZVRDZzFvUEhPUVZLQ3V3NE94cHRZUS9xS1V0TEFIWFZ2OTlLMGRWcWZDMmpVQWlHQmVYa0t3aGRYTGtJYlZxU0EyZmxraXBBeEhYNnByUEExQjF3eTVab3hPUFg4RVExOW92eXpBbFg1dHU0OXEwWC9PSExFN1o5T1cxenltRXR6ZFpyNXJZbWtFcVdtcHVSNU5jeHFwTWlZam93dUNXZWhubzIyeG5JM09IQ0xDZkFKaHRrcklhL1hPc0tZRFpCRzFJMGJsN2taR2R5cEtUQlhYdXl6WE5WUlU5L005ejhaVytwdG1oZ2NOUzBJS2VaaSs5bFl4cWRlS3lnbldTTTV3czdSYUpmNlRRZTNSaWJZUjFvNkhwRzB2VHpiTEtQZTZnRjJGODdiWlBJei9mcTNLWnZiM3UrSnhZcCtJVjBtQi9VN29YelhRRk1RK3VmWllpNzUxbkx6WlVxRE1ybU53TFJPVUFNUk8rVnJtblkwSVB1cFBVMXc0b0hBb1dnVGRnTk5pNk1uTFQ4V0pmUlhjT0pKMk1lbUc2K2ZNeHNZUU52UVJwa1RGY05vaFV6Y3ZjcHJ3NUV3WEVZQTJzbzczL2MvY3RIRGcreU05YlF4REppUlltRnFydkhYb29hS1JyekxnUjZLVWdoM3ltaWxaQ0lSSm9KbTE3aEtHM1pxTTE0Lzl5OUc5OE9BZjNkVTlqMDk3aUNlaEc3a2VxYXRJQ2hFWmJqbmQ4Y00rS3djN2FtVWp2ekQzQmNvMHl3MDJxT054OWF3OGhSblZiWDZhdkRJbGhySHZ6SU44MzFvUjljRHBwMG1DUEJXZFVDQlNqVGJ1RkZqRC90WElSbGxlT2JraFFKSUdSNlE2U1MxcXkzT29WT1VheFl6THY0U2s3dndrQUMwUitGREVIeVFZbFVhbVVkTWcyUmdwRUdhSVd1V3IxaGNnRm10QmREV2g3ZFBuWTF0U3VKOC95MXp4NkRvN2ZJYmNFenBBK2E0ODNtRG5vemdld3VmaFdqVCsvUS85WlEreFQ5UWJBT1pQSXhHV3VhSXVrVk8zSWxvZDhJM1NGZFJCTHY5ZXBDNzFLeXpSdVlpMktkOHJ5NVNINit1WnMxUHlZUlpRakdDK3Q4VzRtSE82Z1lFRWVXSkJ1UWhnSHdmV2xhZXlWb3hac0NBQVZKRUllT3hPZDZtNW45OHRCUDdHTmgxT1M0eDRCS2FVN1A0UVQzNVVIZW5meE84WWFQUThmbXlobUJhSVJVZklBTVN2ZTJZRFp5SWNNTTkrN0tNSVVabzJ0eXRvYzdCOGVvZzBNaUkrVkpFdFg0c29FRjFSWkhQZVV3NWlCTjI4OTh2MmVTcGNnVUJhWHFzOUN5VlZtTVJQMEtLUDJ1REt4MUdJcUhjS0ZCOXVQVWRkQS9vT3dNa0tVUWsraFZVVDVPbEVMdjd1a0FBUEE0eE4rZkczVmYxeUVKV0FiVGx5dWtGcThjNXBTRkY1cXVHbUgwVmVpQzVvVEFka1VES3Z6WGhWWUs5c3BRYjNVZ1Z0Qld6N1ZScnlOUVVST3BIZU5xeDlhZHA4YWREWCtRSHJUKytYblN4VVI3SVdGanlNTkZJRWlMWmkxdks1UVVrZlRDUU9qdjh2SHdiUi9MRHF3Z3M5bXdsT3pPY0RLdVBVK0dTb2lnVFdRejRWN0N2SHRaVDI3WUdKVG44RFFFM3IzdjB4aWxvODJ2U3VXSDg0WEU3VEJsTUpFb2R5eDNDRngwVUVkc3VhRHBPSEV3UjZYNlUyU0xseERYSXVZeEhlNXh2NjI4bXU0bDRMSnBYUjhkYmljTEZKQW55Q0FVeDJLb2dDamt1cmU4bXNUZktDbG8wamFlN1hNR05PSk15b0ZYbVlHZUh2eGhNUGMzTEtYLy9VY1p0c3p3dFJrQmNFdURXQysvQWNWZVBOSHVOWWI5MEpIcnRucGg1ZDlhL1lpTkpzY1N3QTFwUVZrdW1TQWtPQWdLdWRzcnl3c0N3Zkg1anNydVpHUTJDd1hKRXQzUU4wU2NLUlVnT1NCQ3FYa1BqZDVSVzJuOFZpamt4anovbWptakhCNmk0eHM5NEU2Nzk5STAyaldYNVd3UDZhTFRaTGt5TjhxNDUxT0RmeUZVZEY5WWsyZXQ5VUpsV1NzRFJMSWVCd0ZyQkEyZTdyRWsybWFLVUNCRW5PUWM2bUhVMXQvZ3gzK1VXVVFXbkpMZVUxbWUvbkFEdy96UGUwd3d0Vm9BaERZdDBoR1hQblJydjFoUHRGS01CeWtqckg3a0J5U0R3WDlQMi9XZkNkQlE5K1J4cHRsR2hvRmdpMUs0NVlOeEpEd05wTmd5MDV2WXUzVUtrMkpRYVNGUzcwK0Y1NzluRE5RenZpK0pPRlRsdDFmWDJGNXk5NEV2NHZobWRQSmRVOFVVRjU2Ymx0emxKREVFdmsySlFrOTM0aHpwTXJGZ1d3ZHUxUkxxSEhCN2h2T2hnaHNqV0ZGY01zNjZaRUtWcVhKUytxWWNVMHk0akwySVQrNlF2N2pvQ3BWbUdzUWtGY1FyblhxOUJiOTdaUS96UCtwaldmWTU0UmNRVlMydUU1YURObVVyVkdLK3E0d0xRcUhuRVViT2puSHFFeGlacUtxOVdRaUtUK2c3QS96bVlIQ2k0YzFTejRNVWhHb0t6U2l4aXoxYUNJUEJXdy9vczR2cUVqbXgzOGx6YnV0OWNWbElzeGNkTUpUTERRK3ZOZ0YyY1ZRaVcxRTQ0d3lWcnI3TUFaOE9KRVpFSzlEZWt5MzJQUkFuSkRUVXVqdGFscmJ0T2VOczhyS09uTjcvNFRqUEwvZmRlbEI4bjA4WXdSNXdmbU42VGpGWUhRSDFjbUZmK1AvNUxVMTI4Q1pEYjNQUStxMlFJazV3aE40eGwvcy9lb29pallmeWtDcm5aSEhHWkluTGhoU2pWbk5ISWdTL203VWV0NlhBTDdvZUl5UFRLeHVnbDJzRWtUQzNnZ0tjTnFZR0E5U3ZlYVlaQ00vWHNQRUtQbWs3QmlRNmprWFBKaE1yREd4Vkc0SW9aSDgrYjBrUWJYR2l0Mkw0L3hZdHh1bTVzcFNPSjdsTDltVFpRNnBxM2JOaTEwZU1mZ0ZWaDc3NU5JRlc0SEp3U1FtaTU0bk11blZTQjhxdjZKc0w3SGlsZ2N0ZHFSNThTTjVad1lCa2dOR1hzYjA1QXJWemVXbHh1Y21BSHNPT3dyczFnMzh6bTRZN2ZPZmducmFhV1kxanZZOFlEODZQZThkZzR4cE5paTg3UnNDZk5WK2NKVmMraktFdnpuZVY1Zzd0RmlxZCtsZHp4STlKemdSS2t0WUV6RUpRSVU5M2UvclJaN1lrVkZtNVV1cjVhMWYzcG83T0VtYkJUc2MrQ1FaOGNnYmIvbUphRXJoa3NyL3JURjBNcjNxeDl5SlJWSEJ6YWNWd0dScEFRaURPdnJkWU4xQXBVOTRyR1lrVFVzdWs1YjE1Wll2QVZxRlRzVlVMaS9HY29mbEljMm01Z2RFTFZOblRmdXY1Zlk5S1NlWHFoUU80S0pOYVZmbHAwQ0VKYWFFZFNLUXJJNXRaT2w1RkE4VXZlNmxTWVd5TVk0REl4a1RiT1JoWHVBdzR6b1RTMjgrN3d2TXhydVBkZnlKbUJCTkhQdCtEYmdKNHovcHJZWUhpTmFMTXNZamtQZE44ajNKZDczQXJFZk92Um52MzYxSVVVMFg1RDc1dlRSdlpkbzMzWERzanRlOU4weUo3K2lIQnF1a1FJY2pIVW9ic2RQN0hOajBVYWNSMHIvTmRlVTlGNFBNc1VLY2t6Tk4rZGhyMVI2d1J2R1VZb1pDRWJaWlJMWEt4QnA3SElUNEVQUktHakIvdW1xTFhhMXl6RWx2QW1WQUJhMDFZN3dGdk4wM2Ywb25FbUhTM2w1d1paRmV6cjVibnN5T01XVGxhMU5kaW1ZNXNVeE15VFliZmc4dzB2cXNEc28zWFAxYndLdzZ3M3VIRGQ1UHBSWnVDSnR0eWk0ZzJGeWI0Ymg1UU42ZkdORTI2ekRGN1Y4QmJwZXJLNkFKQ0xTWm5kaDZMMTlPUTBram4xUGpEMGk4c1BZcGFXOWxVeVJkZElPKzRWQS9LemxPUzJ4M2s5VUtUdElsTTBUSVdtZXFIS0dYUVpocGpvVGI2VlNKN203cjZaaVlQMnVsQVVvZmVWL0o2eCtzckxEQXkyQ2ZFNnFrREZ1OU9NWDBBSXVnN3loQUtOMDRyT3hVNk5tcGtjOUZ4bXUvVS9vR3hHdmIzeFVFTDYwdE1sSE9EaWtqY1I5RDJrKzRwbEc1WnV0d0FIY2kwRU02WHRrVEhQOU5QMlRTR1VFN1E5SGYvU0VEc2V0a25hZXhvWmhDczJLWDFMeU5JS0U0N2pkMkR3MTUreDRRVXV0VUFTbzU5Q1lHMVFBeW9BVVhrV3dtbXkzTGdTUWp5T3ZLV25qaE8veWpPd0FyWGd0NFBrSVVnZDQ1N05ReFpMbU41K0J4NVJoQ0FHdkUxYmxOZjlMek9keGJiaG5VZ2Z1RDM5MXVSRkhjS2RYREY3ZmVqb3gveThtaWZJcTRWVzQyajBHQnFOQUtkK0prMnJCMW9hOTRiT2hxcVVzanhqWnlRaGRXTzhNblR6T2tOaGVpZXU2blYxcW5yZ3JHU2huWTNJMlczb29GNFNnczRjZ3drZ2h2dHpFa0xUbU5OUm83RTdudVRuMkxJcmlGSnlvTmZQdUp0aWN0S0JtNzRGZytkWVBTMlIzTzNmOWxBZWxiVWZjbzZGNU9EL3hkS1VuRTh0V3FOMExVcDlWQUptWVZYZFVDaGJ4MjM4MWtDaStLNDJoRzUydFNQYU1hb1dTb0xQY2Zrb24rc1pYdjdEdEtwZi9HTzdhcUMza1pzRGpva29haHJGZGJWSlNTZWhrNGp5K3RzRHplQnJKSjBrMVZrUnJHN1NoVHZjTmd1cjVucVRUTEE5dlJMQmJNTTlhNlI1NEZ0Z1pQOWFKMU1aMEdCcUVpMnF6Ui8yd2tYQlhwcFhZdi9TcU1RV1dhbTVsSHBMVktxaDN4ZHRjNFdmck9mYldsbU1PNXA5Z0JUSFp1YUcxVGFkZXFRVVpKQmZBS01ENFdSR0NsMDFaeDRTVzE0YzZrdnFKdXExL080N215L3RsVHlLWndpYlBkQTNRMVVGd0I3R2Z4anEwaDN2ckxFbUNrS3Vsc0VBUkN6UnZNVjJSVnBVbFpUV240Y1Boc0hjcTNROElHSUYyKy9nOENFSU4vMU8xcVMvMkpXcXlDNmtIb0w4Y2R2R0VHbmkxSTNDTk1JcXhxaHhJL1V0R3REc2VwYmwrSHI0elh4MzZna3BCbXBoT2xkTFVYTHAzVEtibVVZRWJSWHcvZmRmeFQ3WDdZUFhHQ0hHVG1uTzk4WkxDOTA2Zmkvekd2b04rNlpzbCs3MkpWMGxJWEo0V3dZdWxFUmZHbkFDWGNoa0Yzei9ITWR3elcwTUFFaXptQmwvREo2ZUoyU01PSG1Uc25YbElGRDRlcFRrYnFBQ0dpZ2I1UExFdHdQRVRjYkNRckM5YUtTU1FnSTdEZXd1aWlxM2J0Y0RUWkIzeEI5WWxlbmhpU0FXNjIwcmwzc2ZjY3d3eGFSOHBDV2Rzd0x3dmFxcDhjM01PV3RCc2xPcmVTSkNEcWgvdzBYbm1WMFJVWFpNM2JvUmkwVXhsaHVUeDFlM1NTd09pbTlOczNYV3NoTmI4Lzc3VkhnUWhRVFlSUU1NRllYaWRmMElCKzBtSUpocWNoQTlUeUY3dGRjSDhrUUJUSHNEWS96bFpqK3EwNlFMd0JkbTkxc3IyK3VzZmxlaXB3WUMrcmdiNHROVnA3VU5rYkVqTnR6ZWZsTi9VRTlkbHZtT2x6V1dtZkh2NGVkUGkzMmJmeUNRS1d6SGJVVEV3NU0yVFpsZnpNaTFWUjVsaDBxQ1lqaDNITUlmL2MwcHBKd2I1b1lFTnBBenlxbnlmdmlTV3lBYzc2L1l1VWwvb2FVaysrYzBZc2d1TGo5ZGFQdVVvemhoZ3VjSytQRGlNckI0ODU1Mk83VWg0aHRwNmZ3S2dJa1JCTVFIUTd6MmV5WXovV1AwQm9ZZVhjOGc3aUprclhFNzA1bFo1bXhGU0poT3E1WlNleVJSb21pUm41K3VRemM5ZFdWQjBYb2JURXdOc0VRM2FIZ25JY29BczY2UGplUT09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDI6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg== \ No newline at end of file +http://myapp.com/?RelayState=%7b%7d&SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL215YXBwLmNvbSIgSUQ9Il8yNTk2NTFlOTY3ZGIwOGZjYTQ4MjdkODI3YWY1M2RkMCIgSW5SZXNwb25zZVRvPSJURVNUX0lEIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDUtMDlUMDM6NTc6NDMuNzkyWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI%2BaHR0cHM6Ly9pZHAudGVzdHNoaWIub3JnL2lkcC9zaGliYm9sZXRoPC9zYW1sMjpJc3N1ZXI%2BPHNhbWwycDpTdGF0dXM%2BPHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM%2BPHNhbWwyOkVuY3J5cHRlZEFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIElkPSJfMGM0NzYzNzIyOWFkNmEzMTY1OGU0MDc2ZDNlYzBmNmQiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiLz48ZHM6S2V5SW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI%2BPHhlbmM6RW5jcnlwdGVkS2V5IElkPSJfYjZmNmU2YWZjMzYyNGI3NmM1N2JmOWZhODA5YzAzNmMiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDc0RDQ0FobWdBd0lCQWdJSkFPN0J3ZGpEWmNVV01BMEdDU3FHU0liM0RRRUJCUVVBTUVVeEN6QUpCZ05WQkFZVEFrTkJNUmt3CkZ3WURWUVFJRXhCQ2NtbDBhWE5vSUVOdmJIVnRZbWxoTVJzd0dRWURWUVFLRXhKd2VYUm9iMjR0YzI5amFXRnNMV0YxZEdnd0hoY04KTVRVd05UQTRNRGMxT0RRMldoY05NalV3TlRBM01EYzFPRFEyV2pCRk1Rc3dDUVlEVlFRR0V3SkRRVEVaTUJjR0ExVUVDQk1RUW5KcApkR2x6YUNCRGIyeDFiV0pwWVRFYk1Ca0dBMVVFQ2hNU2NIbDBhRzl1TFhOdlkybGhiQzFoZFhSb01JR2ZNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNEdOQURDQmlRS0JnUUNxM2cxQ2wrM3VSNXZDbk40SGJnalRnK20zbkhodGVFTXliKyt5Y1pZcmUyYnhVZnNzaEVSNngzM2wKMjN0SGNrUll3bTdNZEJicnAzTHJWb2lPQ2RQYmxUbWwxSWhFUFRDd0tNaEJLdnZXcVR2Z2ZjU1NuUnpBV2tMbFFZU3VzYXl5Wks0bgo5cWNZa1Y1TUZuaTFyYmp4K01yNWFPRW1iNXUzM2FtTUtMd1NUd0lEQVFBQm80R25NSUdrTUIwR0ExVWREZ1FXQkJSUmlCUjZ6UzY2CmZLVm9rcDB5SkhiZ3YzUlltakIxQmdOVkhTTUViakJzZ0JSUmlCUjZ6UzY2ZktWb2twMHlKSGJndjNSWW1xRkpwRWN3UlRFTE1Ba0cKQTFVRUJoTUNRMEV4R1RBWEJnTlZCQWdURUVKeWFYUnBjMmdnUTI5c2RXMWlhV0V4R3pBWkJnTlZCQW9URW5CNWRHaHZiaTF6YjJOcApZV3d0WVhWMGFJSUpBTzdCd2RqRFpjVVdNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ1lFQUp3c01VM1lTCmF5YlZqdUo4VVMwZlVobFBPbE00MFFGQ0dMNHZCM1RFYmIyNE1xOEhyalV3clUwSkZQR2xzOWEyT1l6TjJCM2UzNU5vck11eHMrZ3IKR3RyMnlQNkx2dVgrblY2QTkzd2I0b29HSG9HZkM3VkxseXhTU25zOTM3U1M1UjFwelE0Z1d6Wm1hMktHV0tJQ1dwaDV6UTBBUlZoTAo2Mzk2N21HTG1vST08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BTElQdkVNVUVGeXhrVHowQ2N4QVA5TjV4Y3NYT2V4aVV4cXBvR2VIeVFMV0R5RVBBUDVnZ1daL3NLZ1ViL2xWSk92bCtuQXhSdVhXUlc5dGxSWWx3R2orRVhIOWhIbmdEY1BWMDNqSUJMQnFJbElBL1RmMGw4cVliOHFKRy9ZM0RTS2RQNkwvUURtYXBtTXpFM29YOEJxMW5Ea3YrUWh4cmQwMGVGK2ZMYVQ0PTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BRVpUWDhHTkM0My9yWStTUVlBMXRudHlUTTVVNkN2dUNCaktsVEVlekZPRjBZZHhCWUdFQVVjYU8xNVNKOXBMemJ1L1h0WGxzTkVMZTdKdEx4RUpwYUxubWFENnIranNWczdLaTBLNHRTMGNBUERDWHV2R1FoMmFOVjVQOGJ3N1JWUGhLOGQwYlJ1RklGR09FOHMwTTZYOUpxWDN4S0MvL1lSbVVoeDlybnU3ZWlwMGh5ZitPaUZiVGR2SDY2NTB2LzQ3aVdKcDNZeFlUV0QyMHBNbVRJMUpwWUEwYjByWVFQRkR0RU93d0JxYktxanRJc3ZYVFJzeXJhQkxvbnFOeHN5dHpEWHEra0JsMXp3WGUvSE5QcUVQblczdnNxaFhZcDVGM3dkWThkKzNCOTRMZlpOdUd4a0p3VDNzdVR0OGY5VHRBSlI4VytBUmtzT2M4eDBVaVNsVG5BNHFHOTBLMTR5dkVoVHcvd2drZjFXV01RT3dpZDNpakFYbUV4MU5MbVZvYUxYb3p4VExkTjN6YnJ6VEJIRXc3R2J3ZEdrdU5pMlhZOW16YUgwaWtGRm51VUxjMHUwc0pycEdGdzlaK0VlUk44RzNVUVZ5MjhtS2g3ZFBwWU5KbzhyajIxZFFaK2JaeUtTUHZablU3REkyakdJRE5US1g2ZkVyVWFINGlOTzN4cUU2Vk90L2d4T3BMNE5VNUhLV0Q0bG93VzcwdUJjVEVQRmhwaThpYUovdTB6YzUvTEhvdVBjMzByc1RLZFc5cmJLL2NWaHNQUHErZzA5WHZpZ0QweTJvN2tOc1pVL25tRXFiSzBKOTBrazhCR3I5cXRSczY4bUJnSURtUHVwUkhwWjM4eXNnU2VZN3V0VlVaSG5tQ0dzTzZ2NDJ6OTVOK05Pb3RCTEVZbFd1ZEdzYnowQWc4VkRDSlY5ak95QW95MDZyL1AyUHBsOFhjdmJza2d2T1BMMWdDNnVYbVJJS1lmOEw4UDJCNXVjN0haK0dtUHNOWXRLS2VKRDFFUHovdCt2NlBIbXNVb3dsSDhSd3FMRHdtMUF4dlNLQTR3UXBlQ0dQd3A5YXRYS0lWMS84NUZzRWMzajVzNjd6VlRybThrVEpydXV2MDZEdFVRZDNMOFdwTkV4cWhQait6RUp6U3RxSG04ckhNMVhNQUVxdVozc0xycTVqLzFSNlpqS0dOdFJCbjhwOE5ERGtrWm0vWTV5TXlJNXJJS3U5bnA3bXdaaEVpeWVHeHdxblV3VVMvUzVDRjNnMHVidnd4eVVnalVvd1ZvTkNqYktBbkdtT2VCSW5abkh0eGdIVUhVOUVlTFdyd2pRc3JtUmpJV0R2RkZQa3l6SzJDL20yaitubmNxc2E1OGRLVXZxcGR1VTRJYnNPQng3UGpXdXRBNmY5bXd6YWxyRU1NK0lGR3VPdk9HMC93eUdzQjZLREV6bldjUC83NkQ4angzaHZFSlAzN3REbFgreGM4Qno5TXdKdkd6VG4xbTdCb2xoR0lzSXlCTys1ZXpXa3RDWVVIUURGVE9wbXA0MDlOWHp6ZUNTUGY1U2NDWG5YYjRPd01ULy9VM1JFUnRRbGMrNmU2WG1JRjhoRkJVc0taUUJsS2ppSDkwZHlzYWlsNmN2V3UyQW55Q3QxbWxXcHFLc0MzU2RTRVZDTG1qRjlUQUFUMEtFSGdZQjg3RjZtZUpTTysvOXkyZkRuYVVvUUlUVzdubnVuSCtkT3dWSGZMU0wyL2N5YTltNlQzR29TSVNMbGJPMVRzalhKclVkZW55OTcvM2tkNmhFQlphdGY1U3NETFQ3SjNsQUVJNDROeXJ0NkIxQWdod2JNdkpqd1JNTXRNdUJLc3ltUytKVzc4UFNEWXQ4MG9waDJQTTc1N0tBNCtUMTAvYnZaQkE5Vk1OdVpqNVV3NXRWMnFIS3dwS0t6ZVVETUFiQlBRaGpYcXlQZzFKa09rd2RQMUpnOHRITjJTelBZQTlmT1htV0pBZGJDS2tMb0F4ZTV6cDZBUzYzS3FXMmFmSUt6SHJ3RTJmS1VtamppeURvMnNuMkJHbWtBaTRzbnpiVzc2SUQvSVgwd044aDBaQ2VRc29vKzdtb1RCMEJxSnBkS1MycXlsUktoc3BSTC9henVQdmxaK1pwckJxdXpJdEZkNFVLMkpzQkp6VXcwZkpxcTV1bk9PZENzVWM3SUU3QTNmZ1NmZ3NBd1R3WFZJMEVoME5ySWZpMkFKV1Z2VFpEMys2eFZ3dS96WWhuVjc0VXkvMFE4Mi8yQWtpSGpFRjNJVGNLWHdTNTB6bWtLakxjZDJqa2h5TUFYMWRoQ0wwZElFMUJoN0RNamVvNC9YbjBqSlpPL3Rrbi9xZmYzc3RNb1BYVG9KTnBIU1RjR2ZheGtaMzJYNCt3Q0xPc0VBRWxlMVZSY0kwUkZyOFhHTSsxWU9BTjBodFdGcFMxaG9kSi9OczJqL1FnUVNEemNpQ1FZeUFDd3lFRWZDZjZybnR0VmJyTlJQZWlmSHhBM3B2UnZ5ZGRhNDE5cXl0ZXI0akJ3cmw3ZUpuVnJ2VEprR2VhU2FRbDdXWk5SQXBscXRnNnZPYmpiMHZDRWlFaFhKbmNzQUhxcXp5QTRGeWFUVGQ2R0FySU9adUNxRWVoWk51T01lOVlrMVpya0VkR3pIalJESWk3Q1BKQk12NEZ4ZHI3bnJvN0I1WEhKb0ZMNE1DSUtOWWU2aWZiTUtYOU5uN1FWdnphUmY2UXlaSW1BWENQZndvU1BkN2x6NXl3UDJLSUIyaGhFMWt5eVZ5YVc5T0praWpUY3dvUnZrSXhIU0RqMXFqeGxueXh0QzhVZ1pNWmlwcGgzQXJpcjRiekIzUDhIbGIzejZ0OW51KzZMemNiN2ZObVo0UHluaU50Vk9OQ0lHbEh4dTBSY3hQK3cwUXNsM1BtTzJLaHBpc2RIanhvSUJ1YVY1NXdoTlFFNmdNNFBrT0xINDc4Rzg4bUxkd2s2RFpkWVl4L2d6RWE3b3ZIL0pReFp2TzRLdFVTNmZjZHJxV2thTFg1cEhkNkdneFBGZ2NFc2Nad1ZqM2hCS0xFQmE5L0dodERINEhzRnNRbmpPZnNDQkNzN0tjRitmTi9oSUdUeHFqTVlKVHJRYmNtdWF5dk9xR3RQMDFPcXltR24rVm5FSVkzKytQcm95SFN3K0Q0b0JIVG1maFNXRmJLZCtuTlVFS3BhRVIxNkdCU256WktQRVRVSmdRWEw5QWJRQ3RXVjFHb0UzRWNnMDZYaVd2aHFHakpGNldtdEU4dHY4Q25rZmxMNm91TDRvNldpbmx2WnNEdkZrS0R6TDkwUTNsWC9NanBtRTFpWU9uYzdISXdEVGwraFRRcHdsYXJiTDVUNGNkZTg1akNwYU0xU3p1TStiQU5zMHlXVDA0ZXJUVFc2cnhlbXFDTHAra202TVVMTlZOcE1CazBiQjJpRU82UlRtc3VpRlhDUU1xdU5xZjdkWXUwTFFCZzQ0MkJzU1pBV1ZrWEVZblduOURLdTRSby8veEFsb2h5VHozWlZmSkhuWVBSdDloSUErRHVUL3c4T2ZzTURIWnlCelUvL0JEa1NiNkxjMHdraVA3QlhIdjBoNVdud2dNWUxlZDBPalR5UWI2aGxpVnQ5b0FjaDRFVy9EZUlBdkpaQ1BYVm1pUFFYTGVsOVJIRko2bXFiYVo0TCtaZG1ONmQwcFZNZ1FveXhmQTR3dEwwYVpiNnFZYkhibjJMd2VBQVZwL3M2TzVlMVExdnZpZDRTWHo0a2l3RW1LSStIeXZEQ1pnekpQQVN5Z1gvWDJFWEZ0NGV3SjVmUFQyVXZmWnhQWlpqMFZGSFpyUFQwWVd2VE16bjUva3hoT09oM2drVGdDSmNwNWVsZnp4cEFPNFl1a0NoNHJXdVNndDRqVUJyaWNYbFdWdWo5U3JSZVhUalNHTktLK202NWovUDllNHRHT0RkMk9BbjNKTVQ3Q3FuaDhreTZpZjVjbmpVMmU3UDhTZnBONGwxWEFiZEZEcGk5bVJYamEyTzR1RWFHNGNvNW4xcWNDT3ZNMWYyblFBY1ZGNUFoSXhueS96TWhmU2l2RXdOQ0Zyd2tBWDRyQVE0WldUNldFakFyUG5jb1Y4Z1VRclhxQVA4NDJmK1lNWWI5RHFncmFicEg1a3ZuMnQzcWRldGJHODJ0QWlTamhPcUxNYW9iU2F4cXdWa1lUOHRTMW9rUUt2MWZoZ2t6elpEOE5IQnVQQzdNVHdXS0VCS2tDRUUzRWRFMXhNQURLd1B1M3NSaGpSaExXZyszZ2srejJtdlU4cTBhTlc0Y3hObUdoekx4eEY0Q3NFNStMQ1cwOWFpUVJOM1VvWmg1aktBZzBiMlh3WHBLS3pycUVTY1BYdnI0L1dWUTMyMm5qRWRvQVdXR0t2WnBKMlRlREo0eDdiT21LVElFc2RHWU1UZzFVaEU2eFFQcnhqS3dWeGFJNVJyaVE4a0xpaGgwa0t0WHQvYTVsSDhzUjVwR0ZISGZ3dlNVb3liQTB1eUVDNnNRVitPbTVReUZmRmpqZHFCOGNpOGxQS1hLTHFCTHJ6bjNmUkh3TmQwbzFiRTg0aGllTkx5UlhZVmhrRCtFNEpGaVd3ZWt3U3VWM3BjQk9ybnRVU3RoWmx6M3hIUURUVGNJNWliOFJyQ2swZEZ6YTgvQmw3VUdtWlUwSXZ2UmdvVXF2TXNHT2dMY3pGWmRpZnJ5aGNiUTY4a2ZzZ3lCMHppdC9MN1BSV3V4RkdYdDFoTVZSVUZ3WXBJS04zVkI3cXVKZlgwamZsU1JaRndMaXdlK3VhYndmTVZ6c2doajUvOXZNNzcwK0JaMGtJcE45NzBTMG5BbHl6R0h0aW1nTUl1RXFhbUt5QTNTQlI1aHZIYmRyNENnTHFUbXIzbFFnWmpnSkNvN1FXYUJWTXdCR0RpdzVOVVhUUnBycWc4U3h2eDlnNWZwbXMrL0o2QjFEelNTM3ZRZzgxdHFRU1ZDWVJpc0Y3M2VqZlFuZk4zcUszd3RJRDkxQnRISmFvMEFaUUdKVFpKOXVsZ0kzV3hzdWR4ejB0VHVpNlJlSWpmSWsxekZRdFpwRExGMnB3NGpTQVdQTlJqNDBYdVIrRzFUVlI3OVFiME9FYkw4RDFoTU5zWmo3MTZNbUhSOTlKaUxNdm1FWHV5a1V4VGhGYjRMTzZVbW1kU3UwTlBpMXQ2NmNkYURpQWhMaVBFTGdUNkZsenA2T2FGSGNSNjRncEtyemtTNDJONEhJeFpNa2R6M0FsYkRhK2pOWHZPR1l3UWl5K0xNNENZWGtrTWtHR3ZTWis5R2xWQ0l5RXBJaXIzbEQ3bmdzZGk4emxGWDYvekNaczlQSUtwZFZlSGJGZi9GS20wV3AreHI0Ykd0R0RrVHR2Nk1Manh2YU8zanFHaUFWeERKVWFkTVBlS2VHSm5uempTdnpKbGdOVHV3c3grRnF5L2dPMkwxMGowWmhDWi92dE9NelVjNjl3cGhKZm9FNzU3V3lOeFJOcThJc0Y1Tkg5Y0x0b3UvbUNxOTc3YnZPSkRrSURCN3lKWEJ6YUhVQkJuSXJra1Qyemg3bGJmUm5SREJUSFZraVZMazVESUxqeC9XL1BSZEZpUUM2SzRmZGx4Y29JbzlMcnM4ZFVWZkt2TTNNYnJ6c1hGT3ZtVVh0K3NsZldvd3UyTC9ndG9mRFhvTUJZZnlEcWIvWlRaRWZ0MC83blliRm1relBEUlZacU5SR0F3YWZVNTU1UjB2SWtNbGR2VjdKUzhNT1BNYWlXQVBpelNLRG4yRzNvcys1MzRFQytaOGZnWmFPVWpZL0xLME9vME9RMmhvNUV6MGNMYWpwUjFINk9FNEhvUm1ydjQzZkFjdGpYc0hYdi81RXg3emdrWk1NZXZhTFNEdjZtcjFGcDk4QXR4L296VTFGVDBoMDUxcVcwR0g2VWpRRXk5aExSZDBBMnFkUTRMZXpReDNvbDFTblhsamt2MG4zTXFlaFozOC94bzZhdHFDdkJtQkc3amlUdXd6YnlVUngzRm1TM0NCNllOYnFON3hPYVRZRnlkOEZDL01nY0xGQmMwS3F4MXllQ2VUd1hucldQb0dvdllVQlYxYjA1cWtIa1d5V0RUaCsveXJFNzF0RjNxbUQvd3F6cUJyNE04NERtWWVuQkdFOWxtb3FIZEMyWnRpK09KVFZKcmlHZWxQQ3RjZnZRaUlQcHdDZ3BFNmg1ekZhRndLajRuZGtBUkRpTC95L1EwWTZxNU5rM1g5RURlTmdjY1pIcFdmOUpKQ3M2a29wdXRtYjdDczIrbVJYdER1S09DaGY5UVUyN3Bmb1NJaklYK3NGdHY1c0hhSms2aHBZMlpzUUhzaTBYbFowc3FMTnQ5ayszdTVnYnBSU1JCczlHaC9BaVY0dkNyYTRkOTh5U0dCdzRSR1FhSStpQ29RaG9YK3lxc3VrYkx6bXJUU3FXMVRXaXJReUlHZ1Q5VnFERE1mUzAxeGdQSlNFSTlIWlp6TGlFVXVGMm1CMi81Y2dqaEFUaWQrdGV1UVB4aldhN2NSc2t5YUhuTENjQURVUU9ESUFPVjJDWXROcnAwY29ZL091S3ZzaXlJT0lacVJ5dE1PMGVNZ1ZJWTBzWmdxeVEycXlubUx0NDBmWmd3SFVyV245Zm9TYTNtMkVRTy9uOS8yU2NuelJWdVZpVnNjM0tCSElQL3AzNlJlSWowTGlNcCtPQ0p3SHlLVW1UeDRBU1V0dXVhWktlRHl1QjlxcXJuUEFNWUVCeElsTGFvdXMzV1pHakIrcW9ub3QvNmk1UE40bUZjbHFDcUxhMGJHbks4ZnJxYy9yd2tuVGV0YUE0c2tXTEw1L21qNEd5MitFQkh3a0x3UXd2K0FKdmZTOXYvNDl1LzY0N1ZFYW15UzdZQ2ZEUHNBQUREQ1FFcWJNQ1h2Ui8xVmEwWi9YUWhoNlkrZUt0MEVpRDdpNmRZODJtQkFoNEJMRmRVV3VGZHVrdUVwaGZ2WXB3N2loVjNxTjB1NFM1NTRXU0dUa0ZsdlpYNG1hbkF4a1g2ekQxS0NWaEFMdEJnSDgzdkhxam9uc0lwOFMydHgwZ0tiYzEreHVaRVppVWlNVVlVdTByQVFsRFcrZHJoN3lVRHZqekFHSnBmTk01eThaMW45em93VzZ5YW5VZWFBNjhSZDd5TUxobFd0NVh6bGhBTVZDZmZYZ0pFelR1YzJEbENVOXNMLzVTVkRaV2N4R1E5aFM1cnJtK2VyQ1Jxd2FJQk1DNUtza0RCZHdOWmh2Q0FCdEpqS2Vla1FUSjd5MFp4SGNhbGVCaU1rbkYwZVRDZzFvUEhPUVZLQ3V3NE94cHRZUS9xS1V0TEFIWFZ2OTlLMGRWcWZDMmpVQWlHQmVYa0t3aGRYTGtJYlZxU0EyZmxraXBBeEhYNnByUEExQjF3eTVab3hPUFg4RVExOW92eXpBbFg1dHU0OXEwWC9PSExFN1o5T1cxenltRXR6ZFpyNXJZbWtFcVdtcHVSNU5jeHFwTWlZam93dUNXZWhubzIyeG5JM09IQ0xDZkFKaHRrcklhL1hPc0tZRFpCRzFJMGJsN2taR2R5cEtUQlhYdXl6WE5WUlU5L005ejhaVytwdG1oZ2NOUzBJS2VaaSs5bFl4cWRlS3lnbldTTTV3czdSYUpmNlRRZTNSaWJZUjFvNkhwRzB2VHpiTEtQZTZnRjJGODdiWlBJei9mcTNLWnZiM3UrSnhZcCtJVjBtQi9VN29YelhRRk1RK3VmWllpNzUxbkx6WlVxRE1ybU53TFJPVUFNUk8rVnJtblkwSVB1cFBVMXc0b0hBb1dnVGRnTk5pNk1uTFQ4V0pmUlhjT0pKMk1lbUc2K2ZNeHNZUU52UVJwa1RGY05vaFV6Y3ZjcHJ3NUV3WEVZQTJzbzczL2MvY3RIRGcreU05YlF4REppUlltRnFydkhYb29hS1JyekxnUjZLVWdoM3ltaWxaQ0lSSm9KbTE3aEtHM1pxTTE0Lzl5OUc5OE9BZjNkVTlqMDk3aUNlaEc3a2VxYXRJQ2hFWmJqbmQ4Y00rS3djN2FtVWp2ekQzQmNvMHl3MDJxT054OWF3OGhSblZiWDZhdkRJbGhySHZ6SU44MzFvUjljRHBwMG1DUEJXZFVDQlNqVGJ1RkZqRC90WElSbGxlT2JraFFKSUdSNlE2U1MxcXkzT29WT1VheFl6THY0U2s3dndrQUMwUitGREVIeVFZbFVhbVVkTWcyUmdwRUdhSVd1V3IxaGNnRm10QmREV2g3ZFBuWTF0U3VKOC95MXp4NkRvN2ZJYmNFenBBK2E0ODNtRG5vemdld3VmaFdqVCsvUS85WlEreFQ5UWJBT1pQSXhHV3VhSXVrVk8zSWxvZDhJM1NGZFJCTHY5ZXBDNzFLeXpSdVlpMktkOHJ5NVNINit1WnMxUHlZUlpRakdDK3Q4VzRtSE82Z1lFRWVXSkJ1UWhnSHdmV2xhZXlWb3hac0NBQVZKRUllT3hPZDZtNW45OHRCUDdHTmgxT1M0eDRCS2FVN1A0UVQzNVVIZW5meE84WWFQUThmbXlobUJhSVJVZklBTVN2ZTJZRFp5SWNNTTkrN0tNSVVabzJ0eXRvYzdCOGVvZzBNaUkrVkpFdFg0c29FRjFSWkhQZVV3NWlCTjI4OTh2MmVTcGNnVUJhWHFzOUN5VlZtTVJQMEtLUDJ1REt4MUdJcUhjS0ZCOXVQVWRkQS9vT3dNa0tVUWsraFZVVDVPbEVMdjd1a0FBUEE0eE4rZkczVmYxeUVKV0FiVGx5dWtGcThjNXBTRkY1cXVHbUgwVmVpQzVvVEFka1VES3Z6WGhWWUs5c3BRYjNVZ1Z0Qld6N1ZScnlOUVVST3BIZU5xeDlhZHA4YWREWCtRSHJUKytYblN4VVI3SVdGanlNTkZJRWlMWmkxdks1UVVrZlRDUU9qdjh2SHdiUi9MRHF3Z3M5bXdsT3pPY0RLdVBVK0dTb2lnVFdRejRWN0N2SHRaVDI3WUdKVG44RFFFM3IzdjB4aWxvODJ2U3VXSDg0WEU3VEJsTUpFb2R5eDNDRngwVUVkc3VhRHBPSEV3UjZYNlUyU0xseERYSXVZeEhlNXh2NjI4bXU0bDRMSnBYUjhkYmljTEZKQW55Q0FVeDJLb2dDamt1cmU4bXNUZktDbG8wamFlN1hNR05PSk15b0ZYbVlHZUh2eGhNUGMzTEtYLy9VY1p0c3p3dFJrQmNFdURXQysvQWNWZVBOSHVOWWI5MEpIcnRucGg1ZDlhL1lpTkpzY1N3QTFwUVZrdW1TQWtPQWdLdWRzcnl3c0N3Zkg1anNydVpHUTJDd1hKRXQzUU4wU2NLUlVnT1NCQ3FYa1BqZDVSVzJuOFZpamt4anovbWptakhCNmk0eHM5NEU2Nzk5STAyaldYNVd3UDZhTFRaTGt5TjhxNDUxT0RmeUZVZEY5WWsyZXQ5VUpsV1NzRFJMSWVCd0ZyQkEyZTdyRWsybWFLVUNCRW5PUWM2bUhVMXQvZ3gzK1VXVVFXbkpMZVUxbWUvbkFEdy96UGUwd3d0Vm9BaERZdDBoR1hQblJydjFoUHRGS01CeWtqckg3a0J5U0R3WDlQMi9XZkNkQlE5K1J4cHRsR2hvRmdpMUs0NVlOeEpEd05wTmd5MDV2WXUzVUtrMkpRYVNGUzcwK0Y1NzluRE5RenZpK0pPRlRsdDFmWDJGNXk5NEV2NHZobWRQSmRVOFVVRjU2Ymx0emxKREVFdmsySlFrOTM0aHpwTXJGZ1d3ZHUxUkxxSEhCN2h2T2hnaHNqV0ZGY01zNjZaRUtWcVhKUytxWWNVMHk0akwySVQrNlF2N2pvQ3BWbUdzUWtGY1FyblhxOUJiOTdaUS96UCtwaldmWTU0UmNRVlMydUU1YURObVVyVkdLK3E0d0xRcUhuRVViT2puSHFFeGlacUtxOVdRaUtUK2c3QS96bVlIQ2k0YzFTejRNVWhHb0t6U2l4aXoxYUNJUEJXdy9vczR2cUVqbXgzOGx6YnV0OWNWbElzeGNkTUpUTERRK3ZOZ0YyY1ZRaVcxRTQ0d3lWcnI3TUFaOE9KRVpFSzlEZWt5MzJQUkFuSkRUVXVqdGFscmJ0T2VOczhyS09uTjcvNFRqUEwvZmRlbEI4bjA4WXdSNXdmbU42VGpGWUhRSDFjbUZmK1AvNUxVMTI4Q1pEYjNQUStxMlFJazV3aE40eGwvcy9lb29pallmeWtDcm5aSEhHWkluTGhoU2pWbk5ISWdTL203VWV0NlhBTDdvZUl5UFRLeHVnbDJzRWtUQzNnZ0tjTnFZR0E5U3ZlYVlaQ00vWHNQRUtQbWs3QmlRNmprWFBKaE1yREd4Vkc0SW9aSDgrYjBrUWJYR2l0Mkw0L3hZdHh1bTVzcFNPSjdsTDltVFpRNnBxM2JOaTEwZU1mZ0ZWaDc3NU5JRlc0SEp3U1FtaTU0bk11blZTQjhxdjZKc0w3SGlsZ2N0ZHFSNThTTjVad1lCa2dOR1hzYjA1QXJWemVXbHh1Y21BSHNPT3dyczFnMzh6bTRZN2ZPZmducmFhV1kxanZZOFlEODZQZThkZzR4cE5paTg3UnNDZk5WK2NKVmMraktFdnpuZVY1Zzd0RmlxZCtsZHp4STlKemdSS2t0WUV6RUpRSVU5M2UvclJaN1lrVkZtNVV1cjVhMWYzcG83T0VtYkJUc2MrQ1FaOGNnYmIvbUphRXJoa3NyL3JURjBNcjNxeDl5SlJWSEJ6YWNWd0dScEFRaURPdnJkWU4xQXBVOTRyR1lrVFVzdWs1YjE1Wll2QVZxRlRzVlVMaS9HY29mbEljMm01Z2RFTFZOblRmdXY1Zlk5S1NlWHFoUU80S0pOYVZmbHAwQ0VKYWFFZFNLUXJJNXRaT2w1RkE4VXZlNmxTWVd5TVk0REl4a1RiT1JoWHVBdzR6b1RTMjgrN3d2TXhydVBkZnlKbUJCTkhQdCtEYmdKNHovcHJZWUhpTmFMTXNZamtQZE44ajNKZDczQXJFZk92Um52MzYxSVVVMFg1RDc1dlRSdlpkbzMzWERzanRlOU4weUo3K2lIQnF1a1FJY2pIVW9ic2RQN0hOajBVYWNSMHIvTmRlVTlGNFBNc1VLY2t6Tk4rZGhyMVI2d1J2R1VZb1pDRWJaWlJMWEt4QnA3SElUNEVQUktHakIvdW1xTFhhMXl6RWx2QW1WQUJhMDFZN3dGdk4wM2Ywb25FbUhTM2w1d1paRmV6cjVibnN5T01XVGxhMU5kaW1ZNXNVeE15VFliZmc4dzB2cXNEc28zWFAxYndLdzZ3M3VIRGQ1UHBSWnVDSnR0eWk0ZzJGeWI0Ymg1UU42ZkdORTI2ekRGN1Y4QmJwZXJLNkFKQ0xTWm5kaDZMMTlPUTBram4xUGpEMGk4c1BZcGFXOWxVeVJkZElPKzRWQS9LemxPUzJ4M2s5VUtUdElsTTBUSVdtZXFIS0dYUVpocGpvVGI2VlNKN203cjZaaVlQMnVsQVVvZmVWL0o2eCtzckxEQXkyQ2ZFNnFrREZ1OU9NWDBBSXVnN3loQUtOMDRyT3hVNk5tcGtjOUZ4bXUvVS9vR3hHdmIzeFVFTDYwdE1sSE9EaWtqY1I5RDJrKzRwbEc1WnV0d0FIY2kwRU02WHRrVEhQOU5QMlRTR1VFN1E5SGYvU0VEc2V0a25hZXhvWmhDczJLWDFMeU5JS0U0N2pkMkR3MTUreDRRVXV0VUFTbzU5Q1lHMVFBeW9BVVhrV3dtbXkzTGdTUWp5T3ZLV25qaE8veWpPd0FyWGd0NFBrSVVnZDQ1N05ReFpMbU41K0J4NVJoQ0FHdkUxYmxOZjlMek9keGJiaG5VZ2Z1RDM5MXVSRkhjS2RYREY3ZmVqb3gveThtaWZJcTRWVzQyajBHQnFOQUtkK0prMnJCMW9hOTRiT2hxcVVzanhqWnlRaGRXTzhNblR6T2tOaGVpZXU2blYxcW5yZ3JHU2huWTNJMlczb29GNFNnczRjZ3drZ2h2dHpFa0xUbU5OUm83RTdudVRuMkxJcmlGSnlvTmZQdUp0aWN0S0JtNzRGZytkWVBTMlIzTzNmOWxBZWxiVWZjbzZGNU9EL3hkS1VuRTh0V3FOMExVcDlWQUptWVZYZFVDaGJ4MjM4MWtDaStLNDJoRzUydFNQYU1hb1dTb0xQY2Zrb24rc1pYdjdEdEtwZi9HTzdhcUMza1pzRGpva29haHJGZGJWSlNTZWhrNGp5K3RzRHplQnJKSjBrMVZrUnJHN1NoVHZjTmd1cjVucVRUTEE5dlJMQmJNTTlhNlI1NEZ0Z1pQOWFKMU1aMEdCcUVpMnF6Ui8yd2tYQlhwcFhZdi9TcU1RV1dhbTVsSHBMVktxaDN4ZHRjNFdmck9mYldsbU1PNXA5Z0JUSFp1YUcxVGFkZXFRVVpKQmZBS01ENFdSR0NsMDFaeDRTVzE0YzZrdnFKdXExL080N215L3RsVHlLWndpYlBkQTNRMVVGd0I3R2Z4anEwaDN2ckxFbUNrS3Vsc0VBUkN6UnZNVjJSVnBVbFpUV240Y1Boc0hjcTNROElHSUYyKy9nOENFSU4vMU8xcVMvMkpXcXlDNmtIb0w4Y2R2R0VHbmkxSTNDTk1JcXhxaHhJL1V0R3REc2VwYmwrSHI0elh4MzZna3BCbXBoT2xkTFVYTHAzVEtibVVZRWJSWHcvZmRmeFQ3WDdZUFhHQ0hHVG1uTzk4WkxDOTA2Zmkvekd2b04rNlpzbCs3MkpWMGxJWEo0V3dZdWxFUmZHbkFDWGNoa0Yzei9ITWR3elcwTUFFaXptQmwvREo2ZUoyU01PSG1Uc25YbElGRDRlcFRrYnFBQ0dpZ2I1UExFdHdQRVRjYkNRckM5YUtTU1FnSTdEZXd1aWlxM2J0Y0RUWkIzeEI5WWxlbmhpU0FXNjIwcmwzc2ZjY3d3eGFSOHBDV2Rzd0x3dmFxcDhjM01PV3RCc2xPcmVTSkNEcWgvdzBYbm1WMFJVWFpNM2JvUmkwVXhsaHVUeDFlM1NTd09pbTlOczNYV3NoTmI4Lzc3VkhnUWhRVFlSUU1NRllYaWRmMElCKzBtSUpocWNoQTlUeUY3dGRjSDhrUUJUSHNEWS96bFpqK3EwNlFMd0JkbTkxc3IyK3VzZmxlaXB3WUMrcmdiNHROVnA3VU5rYkVqTnR6ZWZsTi9VRTlkbHZtT2x6V1dtZkh2NGVkUGkzMmJmeUNRS1d6SGJVVEV3NU0yVFpsZnpNaTFWUjVsaDBxQ1lqaDNITUlmL2MwcHBKd2I1b1lFTnBBenlxbnlmdmlTV3lBYzc2L1l1VWwvb2FVaysrYzBZc2d1TGo5ZGFQdVVvemhoZ3VjSytQRGlNckI0ODU1Mk83VWg0aHRwNmZ3S2dJa1JCTVFIUTd6MmV5WXovV1AwQm9ZZVhjOGc3aUprclhFNzA1bFo1bXhGU0poT3E1WlNleVJSb21pUm41K3VRemM5ZFdWQjBYb2JURXdOc0VRM2FIZ25JY29BczY2UGplUT09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDI6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg== diff --git a/social_core/tests/backends/data/saml_response_no_next_url.txt b/social_core/tests/backends/data/saml_response_no_next_url.txt index 37b6e1c19..f89dc4151 100644 --- a/social_core/tests/backends/data/saml_response_no_next_url.txt +++ b/social_core/tests/backends/data/saml_response_no_next_url.txt @@ -1 +1 @@ -http://myapp.com/?RelayState=%7b%22idp%22%3a+%22testshib%22%7d&SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL215YXBwLmNvbSIgSUQ9Il8yNTk2NTFlOTY3ZGIwOGZjYTQ4MjdkODI3YWY1M2RkMCIgSW5SZXNwb25zZVRvPSJURVNUX0lEIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDUtMDlUMDM6NTc6NDMuNzkyWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI%2BaHR0cHM6Ly9pZHAudGVzdHNoaWIub3JnL2lkcC9zaGliYm9sZXRoPC9zYW1sMjpJc3N1ZXI%2BPHNhbWwycDpTdGF0dXM%2BPHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM%2BPHNhbWwyOkVuY3J5cHRlZEFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIElkPSJfMGM0NzYzNzIyOWFkNmEzMTY1OGU0MDc2ZDNlYzBmNmQiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiLz48ZHM6S2V5SW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI%2BPHhlbmM6RW5jcnlwdGVkS2V5IElkPSJfYjZmNmU2YWZjMzYyNGI3NmM1N2JmOWZhODA5YzAzNmMiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDc0RDQ0FobWdBd0lCQWdJSkFPN0J3ZGpEWmNVV01BMEdDU3FHU0liM0RRRUJCUVVBTUVVeEN6QUpCZ05WQkFZVEFrTkJNUmt3CkZ3WURWUVFJRXhCQ2NtbDBhWE5vSUVOdmJIVnRZbWxoTVJzd0dRWURWUVFLRXhKd2VYUm9iMjR0YzI5amFXRnNMV0YxZEdnd0hoY04KTVRVd05UQTRNRGMxT0RRMldoY05NalV3TlRBM01EYzFPRFEyV2pCRk1Rc3dDUVlEVlFRR0V3SkRRVEVaTUJjR0ExVUVDQk1RUW5KcApkR2x6YUNCRGIyeDFiV0pwWVRFYk1Ca0dBMVVFQ2hNU2NIbDBhRzl1TFhOdlkybGhiQzFoZFhSb01JR2ZNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNEdOQURDQmlRS0JnUUNxM2cxQ2wrM3VSNXZDbk40SGJnalRnK20zbkhodGVFTXliKyt5Y1pZcmUyYnhVZnNzaEVSNngzM2wKMjN0SGNrUll3bTdNZEJicnAzTHJWb2lPQ2RQYmxUbWwxSWhFUFRDd0tNaEJLdnZXcVR2Z2ZjU1NuUnpBV2tMbFFZU3VzYXl5Wks0bgo5cWNZa1Y1TUZuaTFyYmp4K01yNWFPRW1iNXUzM2FtTUtMd1NUd0lEQVFBQm80R25NSUdrTUIwR0ExVWREZ1FXQkJSUmlCUjZ6UzY2CmZLVm9rcDB5SkhiZ3YzUlltakIxQmdOVkhTTUViakJzZ0JSUmlCUjZ6UzY2ZktWb2twMHlKSGJndjNSWW1xRkpwRWN3UlRFTE1Ba0cKQTFVRUJoTUNRMEV4R1RBWEJnTlZCQWdURUVKeWFYUnBjMmdnUTI5c2RXMWlhV0V4R3pBWkJnTlZCQW9URW5CNWRHaHZiaTF6YjJOcApZV3d0WVhWMGFJSUpBTzdCd2RqRFpjVVdNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ1lFQUp3c01VM1lTCmF5YlZqdUo4VVMwZlVobFBPbE00MFFGQ0dMNHZCM1RFYmIyNE1xOEhyalV3clUwSkZQR2xzOWEyT1l6TjJCM2UzNU5vck11eHMrZ3IKR3RyMnlQNkx2dVgrblY2QTkzd2I0b29HSG9HZkM3VkxseXhTU25zOTM3U1M1UjFwelE0Z1d6Wm1hMktHV0tJQ1dwaDV6UTBBUlZoTAo2Mzk2N21HTG1vST08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BTElQdkVNVUVGeXhrVHowQ2N4QVA5TjV4Y3NYT2V4aVV4cXBvR2VIeVFMV0R5RVBBUDVnZ1daL3NLZ1ViL2xWSk92bCtuQXhSdVhXUlc5dGxSWWx3R2orRVhIOWhIbmdEY1BWMDNqSUJMQnFJbElBL1RmMGw4cVliOHFKRy9ZM0RTS2RQNkwvUURtYXBtTXpFM29YOEJxMW5Ea3YrUWh4cmQwMGVGK2ZMYVQ0PTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BRVpUWDhHTkM0My9yWStTUVlBMXRudHlUTTVVNkN2dUNCaktsVEVlekZPRjBZZHhCWUdFQVVjYU8xNVNKOXBMemJ1L1h0WGxzTkVMZTdKdEx4RUpwYUxubWFENnIranNWczdLaTBLNHRTMGNBUERDWHV2R1FoMmFOVjVQOGJ3N1JWUGhLOGQwYlJ1RklGR09FOHMwTTZYOUpxWDN4S0MvL1lSbVVoeDlybnU3ZWlwMGh5ZitPaUZiVGR2SDY2NTB2LzQ3aVdKcDNZeFlUV0QyMHBNbVRJMUpwWUEwYjByWVFQRkR0RU93d0JxYktxanRJc3ZYVFJzeXJhQkxvbnFOeHN5dHpEWHEra0JsMXp3WGUvSE5QcUVQblczdnNxaFhZcDVGM3dkWThkKzNCOTRMZlpOdUd4a0p3VDNzdVR0OGY5VHRBSlI4VytBUmtzT2M4eDBVaVNsVG5BNHFHOTBLMTR5dkVoVHcvd2drZjFXV01RT3dpZDNpakFYbUV4MU5MbVZvYUxYb3p4VExkTjN6YnJ6VEJIRXc3R2J3ZEdrdU5pMlhZOW16YUgwaWtGRm51VUxjMHUwc0pycEdGdzlaK0VlUk44RzNVUVZ5MjhtS2g3ZFBwWU5KbzhyajIxZFFaK2JaeUtTUHZablU3REkyakdJRE5US1g2ZkVyVWFINGlOTzN4cUU2Vk90L2d4T3BMNE5VNUhLV0Q0bG93VzcwdUJjVEVQRmhwaThpYUovdTB6YzUvTEhvdVBjMzByc1RLZFc5cmJLL2NWaHNQUHErZzA5WHZpZ0QweTJvN2tOc1pVL25tRXFiSzBKOTBrazhCR3I5cXRSczY4bUJnSURtUHVwUkhwWjM4eXNnU2VZN3V0VlVaSG5tQ0dzTzZ2NDJ6OTVOK05Pb3RCTEVZbFd1ZEdzYnowQWc4VkRDSlY5ak95QW95MDZyL1AyUHBsOFhjdmJza2d2T1BMMWdDNnVYbVJJS1lmOEw4UDJCNXVjN0haK0dtUHNOWXRLS2VKRDFFUHovdCt2NlBIbXNVb3dsSDhSd3FMRHdtMUF4dlNLQTR3UXBlQ0dQd3A5YXRYS0lWMS84NUZzRWMzajVzNjd6VlRybThrVEpydXV2MDZEdFVRZDNMOFdwTkV4cWhQait6RUp6U3RxSG04ckhNMVhNQUVxdVozc0xycTVqLzFSNlpqS0dOdFJCbjhwOE5ERGtrWm0vWTV5TXlJNXJJS3U5bnA3bXdaaEVpeWVHeHdxblV3VVMvUzVDRjNnMHVidnd4eVVnalVvd1ZvTkNqYktBbkdtT2VCSW5abkh0eGdIVUhVOUVlTFdyd2pRc3JtUmpJV0R2RkZQa3l6SzJDL20yaitubmNxc2E1OGRLVXZxcGR1VTRJYnNPQng3UGpXdXRBNmY5bXd6YWxyRU1NK0lGR3VPdk9HMC93eUdzQjZLREV6bldjUC83NkQ4angzaHZFSlAzN3REbFgreGM4Qno5TXdKdkd6VG4xbTdCb2xoR0lzSXlCTys1ZXpXa3RDWVVIUURGVE9wbXA0MDlOWHp6ZUNTUGY1U2NDWG5YYjRPd01ULy9VM1JFUnRRbGMrNmU2WG1JRjhoRkJVc0taUUJsS2ppSDkwZHlzYWlsNmN2V3UyQW55Q3QxbWxXcHFLc0MzU2RTRVZDTG1qRjlUQUFUMEtFSGdZQjg3RjZtZUpTTysvOXkyZkRuYVVvUUlUVzdubnVuSCtkT3dWSGZMU0wyL2N5YTltNlQzR29TSVNMbGJPMVRzalhKclVkZW55OTcvM2tkNmhFQlphdGY1U3NETFQ3SjNsQUVJNDROeXJ0NkIxQWdod2JNdkpqd1JNTXRNdUJLc3ltUytKVzc4UFNEWXQ4MG9waDJQTTc1N0tBNCtUMTAvYnZaQkE5Vk1OdVpqNVV3NXRWMnFIS3dwS0t6ZVVETUFiQlBRaGpYcXlQZzFKa09rd2RQMUpnOHRITjJTelBZQTlmT1htV0pBZGJDS2tMb0F4ZTV6cDZBUzYzS3FXMmFmSUt6SHJ3RTJmS1VtamppeURvMnNuMkJHbWtBaTRzbnpiVzc2SUQvSVgwd044aDBaQ2VRc29vKzdtb1RCMEJxSnBkS1MycXlsUktoc3BSTC9henVQdmxaK1pwckJxdXpJdEZkNFVLMkpzQkp6VXcwZkpxcTV1bk9PZENzVWM3SUU3QTNmZ1NmZ3NBd1R3WFZJMEVoME5ySWZpMkFKV1Z2VFpEMys2eFZ3dS96WWhuVjc0VXkvMFE4Mi8yQWtpSGpFRjNJVGNLWHdTNTB6bWtLakxjZDJqa2h5TUFYMWRoQ0wwZElFMUJoN0RNamVvNC9YbjBqSlpPL3Rrbi9xZmYzc3RNb1BYVG9KTnBIU1RjR2ZheGtaMzJYNCt3Q0xPc0VBRWxlMVZSY0kwUkZyOFhHTSsxWU9BTjBodFdGcFMxaG9kSi9OczJqL1FnUVNEemNpQ1FZeUFDd3lFRWZDZjZybnR0VmJyTlJQZWlmSHhBM3B2UnZ5ZGRhNDE5cXl0ZXI0akJ3cmw3ZUpuVnJ2VEprR2VhU2FRbDdXWk5SQXBscXRnNnZPYmpiMHZDRWlFaFhKbmNzQUhxcXp5QTRGeWFUVGQ2R0FySU9adUNxRWVoWk51T01lOVlrMVpya0VkR3pIalJESWk3Q1BKQk12NEZ4ZHI3bnJvN0I1WEhKb0ZMNE1DSUtOWWU2aWZiTUtYOU5uN1FWdnphUmY2UXlaSW1BWENQZndvU1BkN2x6NXl3UDJLSUIyaGhFMWt5eVZ5YVc5T0praWpUY3dvUnZrSXhIU0RqMXFqeGxueXh0QzhVZ1pNWmlwcGgzQXJpcjRiekIzUDhIbGIzejZ0OW51KzZMemNiN2ZObVo0UHluaU50Vk9OQ0lHbEh4dTBSY3hQK3cwUXNsM1BtTzJLaHBpc2RIanhvSUJ1YVY1NXdoTlFFNmdNNFBrT0xINDc4Rzg4bUxkd2s2RFpkWVl4L2d6RWE3b3ZIL0pReFp2TzRLdFVTNmZjZHJxV2thTFg1cEhkNkdneFBGZ2NFc2Nad1ZqM2hCS0xFQmE5L0dodERINEhzRnNRbmpPZnNDQkNzN0tjRitmTi9oSUdUeHFqTVlKVHJRYmNtdWF5dk9xR3RQMDFPcXltR24rVm5FSVkzKytQcm95SFN3K0Q0b0JIVG1maFNXRmJLZCtuTlVFS3BhRVIxNkdCU256WktQRVRVSmdRWEw5QWJRQ3RXVjFHb0UzRWNnMDZYaVd2aHFHakpGNldtdEU4dHY4Q25rZmxMNm91TDRvNldpbmx2WnNEdkZrS0R6TDkwUTNsWC9NanBtRTFpWU9uYzdISXdEVGwraFRRcHdsYXJiTDVUNGNkZTg1akNwYU0xU3p1TStiQU5zMHlXVDA0ZXJUVFc2cnhlbXFDTHAra202TVVMTlZOcE1CazBiQjJpRU82UlRtc3VpRlhDUU1xdU5xZjdkWXUwTFFCZzQ0MkJzU1pBV1ZrWEVZblduOURLdTRSby8veEFsb2h5VHozWlZmSkhuWVBSdDloSUErRHVUL3c4T2ZzTURIWnlCelUvL0JEa1NiNkxjMHdraVA3QlhIdjBoNVdud2dNWUxlZDBPalR5UWI2aGxpVnQ5b0FjaDRFVy9EZUlBdkpaQ1BYVm1pUFFYTGVsOVJIRko2bXFiYVo0TCtaZG1ONmQwcFZNZ1FveXhmQTR3dEwwYVpiNnFZYkhibjJMd2VBQVZwL3M2TzVlMVExdnZpZDRTWHo0a2l3RW1LSStIeXZEQ1pnekpQQVN5Z1gvWDJFWEZ0NGV3SjVmUFQyVXZmWnhQWlpqMFZGSFpyUFQwWVd2VE16bjUva3hoT09oM2drVGdDSmNwNWVsZnp4cEFPNFl1a0NoNHJXdVNndDRqVUJyaWNYbFdWdWo5U3JSZVhUalNHTktLK202NWovUDllNHRHT0RkMk9BbjNKTVQ3Q3FuaDhreTZpZjVjbmpVMmU3UDhTZnBONGwxWEFiZEZEcGk5bVJYamEyTzR1RWFHNGNvNW4xcWNDT3ZNMWYyblFBY1ZGNUFoSXhueS96TWhmU2l2RXdOQ0Zyd2tBWDRyQVE0WldUNldFakFyUG5jb1Y4Z1VRclhxQVA4NDJmK1lNWWI5RHFncmFicEg1a3ZuMnQzcWRldGJHODJ0QWlTamhPcUxNYW9iU2F4cXdWa1lUOHRTMW9rUUt2MWZoZ2t6elpEOE5IQnVQQzdNVHdXS0VCS2tDRUUzRWRFMXhNQURLd1B1M3NSaGpSaExXZyszZ2srejJtdlU4cTBhTlc0Y3hObUdoekx4eEY0Q3NFNStMQ1cwOWFpUVJOM1VvWmg1aktBZzBiMlh3WHBLS3pycUVTY1BYdnI0L1dWUTMyMm5qRWRvQVdXR0t2WnBKMlRlREo0eDdiT21LVElFc2RHWU1UZzFVaEU2eFFQcnhqS3dWeGFJNVJyaVE4a0xpaGgwa0t0WHQvYTVsSDhzUjVwR0ZISGZ3dlNVb3liQTB1eUVDNnNRVitPbTVReUZmRmpqZHFCOGNpOGxQS1hLTHFCTHJ6bjNmUkh3TmQwbzFiRTg0aGllTkx5UlhZVmhrRCtFNEpGaVd3ZWt3U3VWM3BjQk9ybnRVU3RoWmx6M3hIUURUVGNJNWliOFJyQ2swZEZ6YTgvQmw3VUdtWlUwSXZ2UmdvVXF2TXNHT2dMY3pGWmRpZnJ5aGNiUTY4a2ZzZ3lCMHppdC9MN1BSV3V4RkdYdDFoTVZSVUZ3WXBJS04zVkI3cXVKZlgwamZsU1JaRndMaXdlK3VhYndmTVZ6c2doajUvOXZNNzcwK0JaMGtJcE45NzBTMG5BbHl6R0h0aW1nTUl1RXFhbUt5QTNTQlI1aHZIYmRyNENnTHFUbXIzbFFnWmpnSkNvN1FXYUJWTXdCR0RpdzVOVVhUUnBycWc4U3h2eDlnNWZwbXMrL0o2QjFEelNTM3ZRZzgxdHFRU1ZDWVJpc0Y3M2VqZlFuZk4zcUszd3RJRDkxQnRISmFvMEFaUUdKVFpKOXVsZ0kzV3hzdWR4ejB0VHVpNlJlSWpmSWsxekZRdFpwRExGMnB3NGpTQVdQTlJqNDBYdVIrRzFUVlI3OVFiME9FYkw4RDFoTU5zWmo3MTZNbUhSOTlKaUxNdm1FWHV5a1V4VGhGYjRMTzZVbW1kU3UwTlBpMXQ2NmNkYURpQWhMaVBFTGdUNkZsenA2T2FGSGNSNjRncEtyemtTNDJONEhJeFpNa2R6M0FsYkRhK2pOWHZPR1l3UWl5K0xNNENZWGtrTWtHR3ZTWis5R2xWQ0l5RXBJaXIzbEQ3bmdzZGk4emxGWDYvekNaczlQSUtwZFZlSGJGZi9GS20wV3AreHI0Ykd0R0RrVHR2Nk1Manh2YU8zanFHaUFWeERKVWFkTVBlS2VHSm5uempTdnpKbGdOVHV3c3grRnF5L2dPMkwxMGowWmhDWi92dE9NelVjNjl3cGhKZm9FNzU3V3lOeFJOcThJc0Y1Tkg5Y0x0b3UvbUNxOTc3YnZPSkRrSURCN3lKWEJ6YUhVQkJuSXJra1Qyemg3bGJmUm5SREJUSFZraVZMazVESUxqeC9XL1BSZEZpUUM2SzRmZGx4Y29JbzlMcnM4ZFVWZkt2TTNNYnJ6c1hGT3ZtVVh0K3NsZldvd3UyTC9ndG9mRFhvTUJZZnlEcWIvWlRaRWZ0MC83blliRm1relBEUlZacU5SR0F3YWZVNTU1UjB2SWtNbGR2VjdKUzhNT1BNYWlXQVBpelNLRG4yRzNvcys1MzRFQytaOGZnWmFPVWpZL0xLME9vME9RMmhvNUV6MGNMYWpwUjFINk9FNEhvUm1ydjQzZkFjdGpYc0hYdi81RXg3emdrWk1NZXZhTFNEdjZtcjFGcDk4QXR4L296VTFGVDBoMDUxcVcwR0g2VWpRRXk5aExSZDBBMnFkUTRMZXpReDNvbDFTblhsamt2MG4zTXFlaFozOC94bzZhdHFDdkJtQkc3amlUdXd6YnlVUngzRm1TM0NCNllOYnFON3hPYVRZRnlkOEZDL01nY0xGQmMwS3F4MXllQ2VUd1hucldQb0dvdllVQlYxYjA1cWtIa1d5V0RUaCsveXJFNzF0RjNxbUQvd3F6cUJyNE04NERtWWVuQkdFOWxtb3FIZEMyWnRpK09KVFZKcmlHZWxQQ3RjZnZRaUlQcHdDZ3BFNmg1ekZhRndLajRuZGtBUkRpTC95L1EwWTZxNU5rM1g5RURlTmdjY1pIcFdmOUpKQ3M2a29wdXRtYjdDczIrbVJYdER1S09DaGY5UVUyN3Bmb1NJaklYK3NGdHY1c0hhSms2aHBZMlpzUUhzaTBYbFowc3FMTnQ5ayszdTVnYnBSU1JCczlHaC9BaVY0dkNyYTRkOTh5U0dCdzRSR1FhSStpQ29RaG9YK3lxc3VrYkx6bXJUU3FXMVRXaXJReUlHZ1Q5VnFERE1mUzAxeGdQSlNFSTlIWlp6TGlFVXVGMm1CMi81Y2dqaEFUaWQrdGV1UVB4aldhN2NSc2t5YUhuTENjQURVUU9ESUFPVjJDWXROcnAwY29ZL091S3ZzaXlJT0lacVJ5dE1PMGVNZ1ZJWTBzWmdxeVEycXlubUx0NDBmWmd3SFVyV245Zm9TYTNtMkVRTy9uOS8yU2NuelJWdVZpVnNjM0tCSElQL3AzNlJlSWowTGlNcCtPQ0p3SHlLVW1UeDRBU1V0dXVhWktlRHl1QjlxcXJuUEFNWUVCeElsTGFvdXMzV1pHakIrcW9ub3QvNmk1UE40bUZjbHFDcUxhMGJHbks4ZnJxYy9yd2tuVGV0YUE0c2tXTEw1L21qNEd5MitFQkh3a0x3UXd2K0FKdmZTOXYvNDl1LzY0N1ZFYW15UzdZQ2ZEUHNBQUREQ1FFcWJNQ1h2Ui8xVmEwWi9YUWhoNlkrZUt0MEVpRDdpNmRZODJtQkFoNEJMRmRVV3VGZHVrdUVwaGZ2WXB3N2loVjNxTjB1NFM1NTRXU0dUa0ZsdlpYNG1hbkF4a1g2ekQxS0NWaEFMdEJnSDgzdkhxam9uc0lwOFMydHgwZ0tiYzEreHVaRVppVWlNVVlVdTByQVFsRFcrZHJoN3lVRHZqekFHSnBmTk01eThaMW45em93VzZ5YW5VZWFBNjhSZDd5TUxobFd0NVh6bGhBTVZDZmZYZ0pFelR1YzJEbENVOXNMLzVTVkRaV2N4R1E5aFM1cnJtK2VyQ1Jxd2FJQk1DNUtza0RCZHdOWmh2Q0FCdEpqS2Vla1FUSjd5MFp4SGNhbGVCaU1rbkYwZVRDZzFvUEhPUVZLQ3V3NE94cHRZUS9xS1V0TEFIWFZ2OTlLMGRWcWZDMmpVQWlHQmVYa0t3aGRYTGtJYlZxU0EyZmxraXBBeEhYNnByUEExQjF3eTVab3hPUFg4RVExOW92eXpBbFg1dHU0OXEwWC9PSExFN1o5T1cxenltRXR6ZFpyNXJZbWtFcVdtcHVSNU5jeHFwTWlZam93dUNXZWhubzIyeG5JM09IQ0xDZkFKaHRrcklhL1hPc0tZRFpCRzFJMGJsN2taR2R5cEtUQlhYdXl6WE5WUlU5L005ejhaVytwdG1oZ2NOUzBJS2VaaSs5bFl4cWRlS3lnbldTTTV3czdSYUpmNlRRZTNSaWJZUjFvNkhwRzB2VHpiTEtQZTZnRjJGODdiWlBJei9mcTNLWnZiM3UrSnhZcCtJVjBtQi9VN29YelhRRk1RK3VmWllpNzUxbkx6WlVxRE1ybU53TFJPVUFNUk8rVnJtblkwSVB1cFBVMXc0b0hBb1dnVGRnTk5pNk1uTFQ4V0pmUlhjT0pKMk1lbUc2K2ZNeHNZUU52UVJwa1RGY05vaFV6Y3ZjcHJ3NUV3WEVZQTJzbzczL2MvY3RIRGcreU05YlF4REppUlltRnFydkhYb29hS1JyekxnUjZLVWdoM3ltaWxaQ0lSSm9KbTE3aEtHM1pxTTE0Lzl5OUc5OE9BZjNkVTlqMDk3aUNlaEc3a2VxYXRJQ2hFWmJqbmQ4Y00rS3djN2FtVWp2ekQzQmNvMHl3MDJxT054OWF3OGhSblZiWDZhdkRJbGhySHZ6SU44MzFvUjljRHBwMG1DUEJXZFVDQlNqVGJ1RkZqRC90WElSbGxlT2JraFFKSUdSNlE2U1MxcXkzT29WT1VheFl6THY0U2s3dndrQUMwUitGREVIeVFZbFVhbVVkTWcyUmdwRUdhSVd1V3IxaGNnRm10QmREV2g3ZFBuWTF0U3VKOC95MXp4NkRvN2ZJYmNFenBBK2E0ODNtRG5vemdld3VmaFdqVCsvUS85WlEreFQ5UWJBT1pQSXhHV3VhSXVrVk8zSWxvZDhJM1NGZFJCTHY5ZXBDNzFLeXpSdVlpMktkOHJ5NVNINit1WnMxUHlZUlpRakdDK3Q4VzRtSE82Z1lFRWVXSkJ1UWhnSHdmV2xhZXlWb3hac0NBQVZKRUllT3hPZDZtNW45OHRCUDdHTmgxT1M0eDRCS2FVN1A0UVQzNVVIZW5meE84WWFQUThmbXlobUJhSVJVZklBTVN2ZTJZRFp5SWNNTTkrN0tNSVVabzJ0eXRvYzdCOGVvZzBNaUkrVkpFdFg0c29FRjFSWkhQZVV3NWlCTjI4OTh2MmVTcGNnVUJhWHFzOUN5VlZtTVJQMEtLUDJ1REt4MUdJcUhjS0ZCOXVQVWRkQS9vT3dNa0tVUWsraFZVVDVPbEVMdjd1a0FBUEE0eE4rZkczVmYxeUVKV0FiVGx5dWtGcThjNXBTRkY1cXVHbUgwVmVpQzVvVEFka1VES3Z6WGhWWUs5c3BRYjNVZ1Z0Qld6N1ZScnlOUVVST3BIZU5xeDlhZHA4YWREWCtRSHJUKytYblN4VVI3SVdGanlNTkZJRWlMWmkxdks1UVVrZlRDUU9qdjh2SHdiUi9MRHF3Z3M5bXdsT3pPY0RLdVBVK0dTb2lnVFdRejRWN0N2SHRaVDI3WUdKVG44RFFFM3IzdjB4aWxvODJ2U3VXSDg0WEU3VEJsTUpFb2R5eDNDRngwVUVkc3VhRHBPSEV3UjZYNlUyU0xseERYSXVZeEhlNXh2NjI4bXU0bDRMSnBYUjhkYmljTEZKQW55Q0FVeDJLb2dDamt1cmU4bXNUZktDbG8wamFlN1hNR05PSk15b0ZYbVlHZUh2eGhNUGMzTEtYLy9VY1p0c3p3dFJrQmNFdURXQysvQWNWZVBOSHVOWWI5MEpIcnRucGg1ZDlhL1lpTkpzY1N3QTFwUVZrdW1TQWtPQWdLdWRzcnl3c0N3Zkg1anNydVpHUTJDd1hKRXQzUU4wU2NLUlVnT1NCQ3FYa1BqZDVSVzJuOFZpamt4anovbWptakhCNmk0eHM5NEU2Nzk5STAyaldYNVd3UDZhTFRaTGt5TjhxNDUxT0RmeUZVZEY5WWsyZXQ5VUpsV1NzRFJMSWVCd0ZyQkEyZTdyRWsybWFLVUNCRW5PUWM2bUhVMXQvZ3gzK1VXVVFXbkpMZVUxbWUvbkFEdy96UGUwd3d0Vm9BaERZdDBoR1hQblJydjFoUHRGS01CeWtqckg3a0J5U0R3WDlQMi9XZkNkQlE5K1J4cHRsR2hvRmdpMUs0NVlOeEpEd05wTmd5MDV2WXUzVUtrMkpRYVNGUzcwK0Y1NzluRE5RenZpK0pPRlRsdDFmWDJGNXk5NEV2NHZobWRQSmRVOFVVRjU2Ymx0emxKREVFdmsySlFrOTM0aHpwTXJGZ1d3ZHUxUkxxSEhCN2h2T2hnaHNqV0ZGY01zNjZaRUtWcVhKUytxWWNVMHk0akwySVQrNlF2N2pvQ3BWbUdzUWtGY1FyblhxOUJiOTdaUS96UCtwaldmWTU0UmNRVlMydUU1YURObVVyVkdLK3E0d0xRcUhuRVViT2puSHFFeGlacUtxOVdRaUtUK2c3QS96bVlIQ2k0YzFTejRNVWhHb0t6U2l4aXoxYUNJUEJXdy9vczR2cUVqbXgzOGx6YnV0OWNWbElzeGNkTUpUTERRK3ZOZ0YyY1ZRaVcxRTQ0d3lWcnI3TUFaOE9KRVpFSzlEZWt5MzJQUkFuSkRUVXVqdGFscmJ0T2VOczhyS09uTjcvNFRqUEwvZmRlbEI4bjA4WXdSNXdmbU42VGpGWUhRSDFjbUZmK1AvNUxVMTI4Q1pEYjNQUStxMlFJazV3aE40eGwvcy9lb29pallmeWtDcm5aSEhHWkluTGhoU2pWbk5ISWdTL203VWV0NlhBTDdvZUl5UFRLeHVnbDJzRWtUQzNnZ0tjTnFZR0E5U3ZlYVlaQ00vWHNQRUtQbWs3QmlRNmprWFBKaE1yREd4Vkc0SW9aSDgrYjBrUWJYR2l0Mkw0L3hZdHh1bTVzcFNPSjdsTDltVFpRNnBxM2JOaTEwZU1mZ0ZWaDc3NU5JRlc0SEp3U1FtaTU0bk11blZTQjhxdjZKc0w3SGlsZ2N0ZHFSNThTTjVad1lCa2dOR1hzYjA1QXJWemVXbHh1Y21BSHNPT3dyczFnMzh6bTRZN2ZPZmducmFhV1kxanZZOFlEODZQZThkZzR4cE5paTg3UnNDZk5WK2NKVmMraktFdnpuZVY1Zzd0RmlxZCtsZHp4STlKemdSS2t0WUV6RUpRSVU5M2UvclJaN1lrVkZtNVV1cjVhMWYzcG83T0VtYkJUc2MrQ1FaOGNnYmIvbUphRXJoa3NyL3JURjBNcjNxeDl5SlJWSEJ6YWNWd0dScEFRaURPdnJkWU4xQXBVOTRyR1lrVFVzdWs1YjE1Wll2QVZxRlRzVlVMaS9HY29mbEljMm01Z2RFTFZOblRmdXY1Zlk5S1NlWHFoUU80S0pOYVZmbHAwQ0VKYWFFZFNLUXJJNXRaT2w1RkE4VXZlNmxTWVd5TVk0REl4a1RiT1JoWHVBdzR6b1RTMjgrN3d2TXhydVBkZnlKbUJCTkhQdCtEYmdKNHovcHJZWUhpTmFMTXNZamtQZE44ajNKZDczQXJFZk92Um52MzYxSVVVMFg1RDc1dlRSdlpkbzMzWERzanRlOU4weUo3K2lIQnF1a1FJY2pIVW9ic2RQN0hOajBVYWNSMHIvTmRlVTlGNFBNc1VLY2t6Tk4rZGhyMVI2d1J2R1VZb1pDRWJaWlJMWEt4QnA3SElUNEVQUktHakIvdW1xTFhhMXl6RWx2QW1WQUJhMDFZN3dGdk4wM2Ywb25FbUhTM2w1d1paRmV6cjVibnN5T01XVGxhMU5kaW1ZNXNVeE15VFliZmc4dzB2cXNEc28zWFAxYndLdzZ3M3VIRGQ1UHBSWnVDSnR0eWk0ZzJGeWI0Ymg1UU42ZkdORTI2ekRGN1Y4QmJwZXJLNkFKQ0xTWm5kaDZMMTlPUTBram4xUGpEMGk4c1BZcGFXOWxVeVJkZElPKzRWQS9LemxPUzJ4M2s5VUtUdElsTTBUSVdtZXFIS0dYUVpocGpvVGI2VlNKN203cjZaaVlQMnVsQVVvZmVWL0o2eCtzckxEQXkyQ2ZFNnFrREZ1OU9NWDBBSXVnN3loQUtOMDRyT3hVNk5tcGtjOUZ4bXUvVS9vR3hHdmIzeFVFTDYwdE1sSE9EaWtqY1I5RDJrKzRwbEc1WnV0d0FIY2kwRU02WHRrVEhQOU5QMlRTR1VFN1E5SGYvU0VEc2V0a25hZXhvWmhDczJLWDFMeU5JS0U0N2pkMkR3MTUreDRRVXV0VUFTbzU5Q1lHMVFBeW9BVVhrV3dtbXkzTGdTUWp5T3ZLV25qaE8veWpPd0FyWGd0NFBrSVVnZDQ1N05ReFpMbU41K0J4NVJoQ0FHdkUxYmxOZjlMek9keGJiaG5VZ2Z1RDM5MXVSRkhjS2RYREY3ZmVqb3gveThtaWZJcTRWVzQyajBHQnFOQUtkK0prMnJCMW9hOTRiT2hxcVVzanhqWnlRaGRXTzhNblR6T2tOaGVpZXU2blYxcW5yZ3JHU2huWTNJMlczb29GNFNnczRjZ3drZ2h2dHpFa0xUbU5OUm83RTdudVRuMkxJcmlGSnlvTmZQdUp0aWN0S0JtNzRGZytkWVBTMlIzTzNmOWxBZWxiVWZjbzZGNU9EL3hkS1VuRTh0V3FOMExVcDlWQUptWVZYZFVDaGJ4MjM4MWtDaStLNDJoRzUydFNQYU1hb1dTb0xQY2Zrb24rc1pYdjdEdEtwZi9HTzdhcUMza1pzRGpva29haHJGZGJWSlNTZWhrNGp5K3RzRHplQnJKSjBrMVZrUnJHN1NoVHZjTmd1cjVucVRUTEE5dlJMQmJNTTlhNlI1NEZ0Z1pQOWFKMU1aMEdCcUVpMnF6Ui8yd2tYQlhwcFhZdi9TcU1RV1dhbTVsSHBMVktxaDN4ZHRjNFdmck9mYldsbU1PNXA5Z0JUSFp1YUcxVGFkZXFRVVpKQmZBS01ENFdSR0NsMDFaeDRTVzE0YzZrdnFKdXExL080N215L3RsVHlLWndpYlBkQTNRMVVGd0I3R2Z4anEwaDN2ckxFbUNrS3Vsc0VBUkN6UnZNVjJSVnBVbFpUV240Y1Boc0hjcTNROElHSUYyKy9nOENFSU4vMU8xcVMvMkpXcXlDNmtIb0w4Y2R2R0VHbmkxSTNDTk1JcXhxaHhJL1V0R3REc2VwYmwrSHI0elh4MzZna3BCbXBoT2xkTFVYTHAzVEtibVVZRWJSWHcvZmRmeFQ3WDdZUFhHQ0hHVG1uTzk4WkxDOTA2Zmkvekd2b04rNlpzbCs3MkpWMGxJWEo0V3dZdWxFUmZHbkFDWGNoa0Yzei9ITWR3elcwTUFFaXptQmwvREo2ZUoyU01PSG1Uc25YbElGRDRlcFRrYnFBQ0dpZ2I1UExFdHdQRVRjYkNRckM5YUtTU1FnSTdEZXd1aWlxM2J0Y0RUWkIzeEI5WWxlbmhpU0FXNjIwcmwzc2ZjY3d3eGFSOHBDV2Rzd0x3dmFxcDhjM01PV3RCc2xPcmVTSkNEcWgvdzBYbm1WMFJVWFpNM2JvUmkwVXhsaHVUeDFlM1NTd09pbTlOczNYV3NoTmI4Lzc3VkhnUWhRVFlSUU1NRllYaWRmMElCKzBtSUpocWNoQTlUeUY3dGRjSDhrUUJUSHNEWS96bFpqK3EwNlFMd0JkbTkxc3IyK3VzZmxlaXB3WUMrcmdiNHROVnA3VU5rYkVqTnR6ZWZsTi9VRTlkbHZtT2x6V1dtZkh2NGVkUGkzMmJmeUNRS1d6SGJVVEV3NU0yVFpsZnpNaTFWUjVsaDBxQ1lqaDNITUlmL2MwcHBKd2I1b1lFTnBBenlxbnlmdmlTV3lBYzc2L1l1VWwvb2FVaysrYzBZc2d1TGo5ZGFQdVVvemhoZ3VjSytQRGlNckI0ODU1Mk83VWg0aHRwNmZ3S2dJa1JCTVFIUTd6MmV5WXovV1AwQm9ZZVhjOGc3aUprclhFNzA1bFo1bXhGU0poT3E1WlNleVJSb21pUm41K3VRemM5ZFdWQjBYb2JURXdOc0VRM2FIZ25JY29BczY2UGplUT09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDI6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg== \ No newline at end of file +http://myapp.com/?RelayState=%7b%22idp%22%3a+%22testshib%22%7d&SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL215YXBwLmNvbSIgSUQ9Il8yNTk2NTFlOTY3ZGIwOGZjYTQ4MjdkODI3YWY1M2RkMCIgSW5SZXNwb25zZVRvPSJURVNUX0lEIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMDUtMDlUMDM6NTc6NDMuNzkyWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSI%2BaHR0cHM6Ly9pZHAudGVzdHNoaWIub3JnL2lkcC9zaGliYm9sZXRoPC9zYW1sMjpJc3N1ZXI%2BPHNhbWwycDpTdGF0dXM%2BPHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM%2BPHNhbWwyOkVuY3J5cHRlZEFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIElkPSJfMGM0NzYzNzIyOWFkNmEzMTY1OGU0MDc2ZDNlYzBmNmQiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiLz48ZHM6S2V5SW5mbyB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI%2BPHhlbmM6RW5jcnlwdGVkS2V5IElkPSJfYjZmNmU2YWZjMzYyNGI3NmM1N2JmOWZhODA5YzAzNmMiIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS1vYWVwLW1nZjFwIiB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiLz48L3hlbmM6RW5jcnlwdGlvbk1ldGhvZD48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDc0RDQ0FobWdBd0lCQWdJSkFPN0J3ZGpEWmNVV01BMEdDU3FHU0liM0RRRUJCUVVBTUVVeEN6QUpCZ05WQkFZVEFrTkJNUmt3CkZ3WURWUVFJRXhCQ2NtbDBhWE5vSUVOdmJIVnRZbWxoTVJzd0dRWURWUVFLRXhKd2VYUm9iMjR0YzI5amFXRnNMV0YxZEdnd0hoY04KTVRVd05UQTRNRGMxT0RRMldoY05NalV3TlRBM01EYzFPRFEyV2pCRk1Rc3dDUVlEVlFRR0V3SkRRVEVaTUJjR0ExVUVDQk1RUW5KcApkR2x6YUNCRGIyeDFiV0pwWVRFYk1Ca0dBMVVFQ2hNU2NIbDBhRzl1TFhOdlkybGhiQzFoZFhSb01JR2ZNQTBHQ1NxR1NJYjNEUUVCCkFRVUFBNEdOQURDQmlRS0JnUUNxM2cxQ2wrM3VSNXZDbk40SGJnalRnK20zbkhodGVFTXliKyt5Y1pZcmUyYnhVZnNzaEVSNngzM2wKMjN0SGNrUll3bTdNZEJicnAzTHJWb2lPQ2RQYmxUbWwxSWhFUFRDd0tNaEJLdnZXcVR2Z2ZjU1NuUnpBV2tMbFFZU3VzYXl5Wks0bgo5cWNZa1Y1TUZuaTFyYmp4K01yNWFPRW1iNXUzM2FtTUtMd1NUd0lEQVFBQm80R25NSUdrTUIwR0ExVWREZ1FXQkJSUmlCUjZ6UzY2CmZLVm9rcDB5SkhiZ3YzUlltakIxQmdOVkhTTUViakJzZ0JSUmlCUjZ6UzY2ZktWb2twMHlKSGJndjNSWW1xRkpwRWN3UlRFTE1Ba0cKQTFVRUJoTUNRMEV4R1RBWEJnTlZCQWdURUVKeWFYUnBjMmdnUTI5c2RXMWlhV0V4R3pBWkJnTlZCQW9URW5CNWRHaHZiaTF6YjJOcApZV3d0WVhWMGFJSUpBTzdCd2RqRFpjVVdNQXdHQTFVZEV3UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUZCUUFEZ1lFQUp3c01VM1lTCmF5YlZqdUo4VVMwZlVobFBPbE00MFFGQ0dMNHZCM1RFYmIyNE1xOEhyalV3clUwSkZQR2xzOWEyT1l6TjJCM2UzNU5vck11eHMrZ3IKR3RyMnlQNkx2dVgrblY2QTkzd2I0b29HSG9HZkM3VkxseXhTU25zOTM3U1M1UjFwelE0Z1d6Wm1hMktHV0tJQ1dwaDV6UTBBUlZoTAo2Mzk2N21HTG1vST08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BTElQdkVNVUVGeXhrVHowQ2N4QVA5TjV4Y3NYT2V4aVV4cXBvR2VIeVFMV0R5RVBBUDVnZ1daL3NLZ1ViL2xWSk92bCtuQXhSdVhXUlc5dGxSWWx3R2orRVhIOWhIbmdEY1BWMDNqSUJMQnFJbElBL1RmMGw4cVliOHFKRy9ZM0RTS2RQNkwvUURtYXBtTXpFM29YOEJxMW5Ea3YrUWh4cmQwMGVGK2ZMYVQ0PTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHM6S2V5SW5mbz48eGVuYzpDaXBoZXJEYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyI%2BPHhlbmM6Q2lwaGVyVmFsdWU%2BRVpUWDhHTkM0My9yWStTUVlBMXRudHlUTTVVNkN2dUNCaktsVEVlekZPRjBZZHhCWUdFQVVjYU8xNVNKOXBMemJ1L1h0WGxzTkVMZTdKdEx4RUpwYUxubWFENnIranNWczdLaTBLNHRTMGNBUERDWHV2R1FoMmFOVjVQOGJ3N1JWUGhLOGQwYlJ1RklGR09FOHMwTTZYOUpxWDN4S0MvL1lSbVVoeDlybnU3ZWlwMGh5ZitPaUZiVGR2SDY2NTB2LzQ3aVdKcDNZeFlUV0QyMHBNbVRJMUpwWUEwYjByWVFQRkR0RU93d0JxYktxanRJc3ZYVFJzeXJhQkxvbnFOeHN5dHpEWHEra0JsMXp3WGUvSE5QcUVQblczdnNxaFhZcDVGM3dkWThkKzNCOTRMZlpOdUd4a0p3VDNzdVR0OGY5VHRBSlI4VytBUmtzT2M4eDBVaVNsVG5BNHFHOTBLMTR5dkVoVHcvd2drZjFXV01RT3dpZDNpakFYbUV4MU5MbVZvYUxYb3p4VExkTjN6YnJ6VEJIRXc3R2J3ZEdrdU5pMlhZOW16YUgwaWtGRm51VUxjMHUwc0pycEdGdzlaK0VlUk44RzNVUVZ5MjhtS2g3ZFBwWU5KbzhyajIxZFFaK2JaeUtTUHZablU3REkyakdJRE5US1g2ZkVyVWFINGlOTzN4cUU2Vk90L2d4T3BMNE5VNUhLV0Q0bG93VzcwdUJjVEVQRmhwaThpYUovdTB6YzUvTEhvdVBjMzByc1RLZFc5cmJLL2NWaHNQUHErZzA5WHZpZ0QweTJvN2tOc1pVL25tRXFiSzBKOTBrazhCR3I5cXRSczY4bUJnSURtUHVwUkhwWjM4eXNnU2VZN3V0VlVaSG5tQ0dzTzZ2NDJ6OTVOK05Pb3RCTEVZbFd1ZEdzYnowQWc4VkRDSlY5ak95QW95MDZyL1AyUHBsOFhjdmJza2d2T1BMMWdDNnVYbVJJS1lmOEw4UDJCNXVjN0haK0dtUHNOWXRLS2VKRDFFUHovdCt2NlBIbXNVb3dsSDhSd3FMRHdtMUF4dlNLQTR3UXBlQ0dQd3A5YXRYS0lWMS84NUZzRWMzajVzNjd6VlRybThrVEpydXV2MDZEdFVRZDNMOFdwTkV4cWhQait6RUp6U3RxSG04ckhNMVhNQUVxdVozc0xycTVqLzFSNlpqS0dOdFJCbjhwOE5ERGtrWm0vWTV5TXlJNXJJS3U5bnA3bXdaaEVpeWVHeHdxblV3VVMvUzVDRjNnMHVidnd4eVVnalVvd1ZvTkNqYktBbkdtT2VCSW5abkh0eGdIVUhVOUVlTFdyd2pRc3JtUmpJV0R2RkZQa3l6SzJDL20yaitubmNxc2E1OGRLVXZxcGR1VTRJYnNPQng3UGpXdXRBNmY5bXd6YWxyRU1NK0lGR3VPdk9HMC93eUdzQjZLREV6bldjUC83NkQ4angzaHZFSlAzN3REbFgreGM4Qno5TXdKdkd6VG4xbTdCb2xoR0lzSXlCTys1ZXpXa3RDWVVIUURGVE9wbXA0MDlOWHp6ZUNTUGY1U2NDWG5YYjRPd01ULy9VM1JFUnRRbGMrNmU2WG1JRjhoRkJVc0taUUJsS2ppSDkwZHlzYWlsNmN2V3UyQW55Q3QxbWxXcHFLc0MzU2RTRVZDTG1qRjlUQUFUMEtFSGdZQjg3RjZtZUpTTysvOXkyZkRuYVVvUUlUVzdubnVuSCtkT3dWSGZMU0wyL2N5YTltNlQzR29TSVNMbGJPMVRzalhKclVkZW55OTcvM2tkNmhFQlphdGY1U3NETFQ3SjNsQUVJNDROeXJ0NkIxQWdod2JNdkpqd1JNTXRNdUJLc3ltUytKVzc4UFNEWXQ4MG9waDJQTTc1N0tBNCtUMTAvYnZaQkE5Vk1OdVpqNVV3NXRWMnFIS3dwS0t6ZVVETUFiQlBRaGpYcXlQZzFKa09rd2RQMUpnOHRITjJTelBZQTlmT1htV0pBZGJDS2tMb0F4ZTV6cDZBUzYzS3FXMmFmSUt6SHJ3RTJmS1VtamppeURvMnNuMkJHbWtBaTRzbnpiVzc2SUQvSVgwd044aDBaQ2VRc29vKzdtb1RCMEJxSnBkS1MycXlsUktoc3BSTC9henVQdmxaK1pwckJxdXpJdEZkNFVLMkpzQkp6VXcwZkpxcTV1bk9PZENzVWM3SUU3QTNmZ1NmZ3NBd1R3WFZJMEVoME5ySWZpMkFKV1Z2VFpEMys2eFZ3dS96WWhuVjc0VXkvMFE4Mi8yQWtpSGpFRjNJVGNLWHdTNTB6bWtLakxjZDJqa2h5TUFYMWRoQ0wwZElFMUJoN0RNamVvNC9YbjBqSlpPL3Rrbi9xZmYzc3RNb1BYVG9KTnBIU1RjR2ZheGtaMzJYNCt3Q0xPc0VBRWxlMVZSY0kwUkZyOFhHTSsxWU9BTjBodFdGcFMxaG9kSi9OczJqL1FnUVNEemNpQ1FZeUFDd3lFRWZDZjZybnR0VmJyTlJQZWlmSHhBM3B2UnZ5ZGRhNDE5cXl0ZXI0akJ3cmw3ZUpuVnJ2VEprR2VhU2FRbDdXWk5SQXBscXRnNnZPYmpiMHZDRWlFaFhKbmNzQUhxcXp5QTRGeWFUVGQ2R0FySU9adUNxRWVoWk51T01lOVlrMVpya0VkR3pIalJESWk3Q1BKQk12NEZ4ZHI3bnJvN0I1WEhKb0ZMNE1DSUtOWWU2aWZiTUtYOU5uN1FWdnphUmY2UXlaSW1BWENQZndvU1BkN2x6NXl3UDJLSUIyaGhFMWt5eVZ5YVc5T0praWpUY3dvUnZrSXhIU0RqMXFqeGxueXh0QzhVZ1pNWmlwcGgzQXJpcjRiekIzUDhIbGIzejZ0OW51KzZMemNiN2ZObVo0UHluaU50Vk9OQ0lHbEh4dTBSY3hQK3cwUXNsM1BtTzJLaHBpc2RIanhvSUJ1YVY1NXdoTlFFNmdNNFBrT0xINDc4Rzg4bUxkd2s2RFpkWVl4L2d6RWE3b3ZIL0pReFp2TzRLdFVTNmZjZHJxV2thTFg1cEhkNkdneFBGZ2NFc2Nad1ZqM2hCS0xFQmE5L0dodERINEhzRnNRbmpPZnNDQkNzN0tjRitmTi9oSUdUeHFqTVlKVHJRYmNtdWF5dk9xR3RQMDFPcXltR24rVm5FSVkzKytQcm95SFN3K0Q0b0JIVG1maFNXRmJLZCtuTlVFS3BhRVIxNkdCU256WktQRVRVSmdRWEw5QWJRQ3RXVjFHb0UzRWNnMDZYaVd2aHFHakpGNldtdEU4dHY4Q25rZmxMNm91TDRvNldpbmx2WnNEdkZrS0R6TDkwUTNsWC9NanBtRTFpWU9uYzdISXdEVGwraFRRcHdsYXJiTDVUNGNkZTg1akNwYU0xU3p1TStiQU5zMHlXVDA0ZXJUVFc2cnhlbXFDTHAra202TVVMTlZOcE1CazBiQjJpRU82UlRtc3VpRlhDUU1xdU5xZjdkWXUwTFFCZzQ0MkJzU1pBV1ZrWEVZblduOURLdTRSby8veEFsb2h5VHozWlZmSkhuWVBSdDloSUErRHVUL3c4T2ZzTURIWnlCelUvL0JEa1NiNkxjMHdraVA3QlhIdjBoNVdud2dNWUxlZDBPalR5UWI2aGxpVnQ5b0FjaDRFVy9EZUlBdkpaQ1BYVm1pUFFYTGVsOVJIRko2bXFiYVo0TCtaZG1ONmQwcFZNZ1FveXhmQTR3dEwwYVpiNnFZYkhibjJMd2VBQVZwL3M2TzVlMVExdnZpZDRTWHo0a2l3RW1LSStIeXZEQ1pnekpQQVN5Z1gvWDJFWEZ0NGV3SjVmUFQyVXZmWnhQWlpqMFZGSFpyUFQwWVd2VE16bjUva3hoT09oM2drVGdDSmNwNWVsZnp4cEFPNFl1a0NoNHJXdVNndDRqVUJyaWNYbFdWdWo5U3JSZVhUalNHTktLK202NWovUDllNHRHT0RkMk9BbjNKTVQ3Q3FuaDhreTZpZjVjbmpVMmU3UDhTZnBONGwxWEFiZEZEcGk5bVJYamEyTzR1RWFHNGNvNW4xcWNDT3ZNMWYyblFBY1ZGNUFoSXhueS96TWhmU2l2RXdOQ0Zyd2tBWDRyQVE0WldUNldFakFyUG5jb1Y4Z1VRclhxQVA4NDJmK1lNWWI5RHFncmFicEg1a3ZuMnQzcWRldGJHODJ0QWlTamhPcUxNYW9iU2F4cXdWa1lUOHRTMW9rUUt2MWZoZ2t6elpEOE5IQnVQQzdNVHdXS0VCS2tDRUUzRWRFMXhNQURLd1B1M3NSaGpSaExXZyszZ2srejJtdlU4cTBhTlc0Y3hObUdoekx4eEY0Q3NFNStMQ1cwOWFpUVJOM1VvWmg1aktBZzBiMlh3WHBLS3pycUVTY1BYdnI0L1dWUTMyMm5qRWRvQVdXR0t2WnBKMlRlREo0eDdiT21LVElFc2RHWU1UZzFVaEU2eFFQcnhqS3dWeGFJNVJyaVE4a0xpaGgwa0t0WHQvYTVsSDhzUjVwR0ZISGZ3dlNVb3liQTB1eUVDNnNRVitPbTVReUZmRmpqZHFCOGNpOGxQS1hLTHFCTHJ6bjNmUkh3TmQwbzFiRTg0aGllTkx5UlhZVmhrRCtFNEpGaVd3ZWt3U3VWM3BjQk9ybnRVU3RoWmx6M3hIUURUVGNJNWliOFJyQ2swZEZ6YTgvQmw3VUdtWlUwSXZ2UmdvVXF2TXNHT2dMY3pGWmRpZnJ5aGNiUTY4a2ZzZ3lCMHppdC9MN1BSV3V4RkdYdDFoTVZSVUZ3WXBJS04zVkI3cXVKZlgwamZsU1JaRndMaXdlK3VhYndmTVZ6c2doajUvOXZNNzcwK0JaMGtJcE45NzBTMG5BbHl6R0h0aW1nTUl1RXFhbUt5QTNTQlI1aHZIYmRyNENnTHFUbXIzbFFnWmpnSkNvN1FXYUJWTXdCR0RpdzVOVVhUUnBycWc4U3h2eDlnNWZwbXMrL0o2QjFEelNTM3ZRZzgxdHFRU1ZDWVJpc0Y3M2VqZlFuZk4zcUszd3RJRDkxQnRISmFvMEFaUUdKVFpKOXVsZ0kzV3hzdWR4ejB0VHVpNlJlSWpmSWsxekZRdFpwRExGMnB3NGpTQVdQTlJqNDBYdVIrRzFUVlI3OVFiME9FYkw4RDFoTU5zWmo3MTZNbUhSOTlKaUxNdm1FWHV5a1V4VGhGYjRMTzZVbW1kU3UwTlBpMXQ2NmNkYURpQWhMaVBFTGdUNkZsenA2T2FGSGNSNjRncEtyemtTNDJONEhJeFpNa2R6M0FsYkRhK2pOWHZPR1l3UWl5K0xNNENZWGtrTWtHR3ZTWis5R2xWQ0l5RXBJaXIzbEQ3bmdzZGk4emxGWDYvekNaczlQSUtwZFZlSGJGZi9GS20wV3AreHI0Ykd0R0RrVHR2Nk1Manh2YU8zanFHaUFWeERKVWFkTVBlS2VHSm5uempTdnpKbGdOVHV3c3grRnF5L2dPMkwxMGowWmhDWi92dE9NelVjNjl3cGhKZm9FNzU3V3lOeFJOcThJc0Y1Tkg5Y0x0b3UvbUNxOTc3YnZPSkRrSURCN3lKWEJ6YUhVQkJuSXJra1Qyemg3bGJmUm5SREJUSFZraVZMazVESUxqeC9XL1BSZEZpUUM2SzRmZGx4Y29JbzlMcnM4ZFVWZkt2TTNNYnJ6c1hGT3ZtVVh0K3NsZldvd3UyTC9ndG9mRFhvTUJZZnlEcWIvWlRaRWZ0MC83blliRm1relBEUlZacU5SR0F3YWZVNTU1UjB2SWtNbGR2VjdKUzhNT1BNYWlXQVBpelNLRG4yRzNvcys1MzRFQytaOGZnWmFPVWpZL0xLME9vME9RMmhvNUV6MGNMYWpwUjFINk9FNEhvUm1ydjQzZkFjdGpYc0hYdi81RXg3emdrWk1NZXZhTFNEdjZtcjFGcDk4QXR4L296VTFGVDBoMDUxcVcwR0g2VWpRRXk5aExSZDBBMnFkUTRMZXpReDNvbDFTblhsamt2MG4zTXFlaFozOC94bzZhdHFDdkJtQkc3amlUdXd6YnlVUngzRm1TM0NCNllOYnFON3hPYVRZRnlkOEZDL01nY0xGQmMwS3F4MXllQ2VUd1hucldQb0dvdllVQlYxYjA1cWtIa1d5V0RUaCsveXJFNzF0RjNxbUQvd3F6cUJyNE04NERtWWVuQkdFOWxtb3FIZEMyWnRpK09KVFZKcmlHZWxQQ3RjZnZRaUlQcHdDZ3BFNmg1ekZhRndLajRuZGtBUkRpTC95L1EwWTZxNU5rM1g5RURlTmdjY1pIcFdmOUpKQ3M2a29wdXRtYjdDczIrbVJYdER1S09DaGY5UVUyN3Bmb1NJaklYK3NGdHY1c0hhSms2aHBZMlpzUUhzaTBYbFowc3FMTnQ5ayszdTVnYnBSU1JCczlHaC9BaVY0dkNyYTRkOTh5U0dCdzRSR1FhSStpQ29RaG9YK3lxc3VrYkx6bXJUU3FXMVRXaXJReUlHZ1Q5VnFERE1mUzAxeGdQSlNFSTlIWlp6TGlFVXVGMm1CMi81Y2dqaEFUaWQrdGV1UVB4aldhN2NSc2t5YUhuTENjQURVUU9ESUFPVjJDWXROcnAwY29ZL091S3ZzaXlJT0lacVJ5dE1PMGVNZ1ZJWTBzWmdxeVEycXlubUx0NDBmWmd3SFVyV245Zm9TYTNtMkVRTy9uOS8yU2NuelJWdVZpVnNjM0tCSElQL3AzNlJlSWowTGlNcCtPQ0p3SHlLVW1UeDRBU1V0dXVhWktlRHl1QjlxcXJuUEFNWUVCeElsTGFvdXMzV1pHakIrcW9ub3QvNmk1UE40bUZjbHFDcUxhMGJHbks4ZnJxYy9yd2tuVGV0YUE0c2tXTEw1L21qNEd5MitFQkh3a0x3UXd2K0FKdmZTOXYvNDl1LzY0N1ZFYW15UzdZQ2ZEUHNBQUREQ1FFcWJNQ1h2Ui8xVmEwWi9YUWhoNlkrZUt0MEVpRDdpNmRZODJtQkFoNEJMRmRVV3VGZHVrdUVwaGZ2WXB3N2loVjNxTjB1NFM1NTRXU0dUa0ZsdlpYNG1hbkF4a1g2ekQxS0NWaEFMdEJnSDgzdkhxam9uc0lwOFMydHgwZ0tiYzEreHVaRVppVWlNVVlVdTByQVFsRFcrZHJoN3lVRHZqekFHSnBmTk01eThaMW45em93VzZ5YW5VZWFBNjhSZDd5TUxobFd0NVh6bGhBTVZDZmZYZ0pFelR1YzJEbENVOXNMLzVTVkRaV2N4R1E5aFM1cnJtK2VyQ1Jxd2FJQk1DNUtza0RCZHdOWmh2Q0FCdEpqS2Vla1FUSjd5MFp4SGNhbGVCaU1rbkYwZVRDZzFvUEhPUVZLQ3V3NE94cHRZUS9xS1V0TEFIWFZ2OTlLMGRWcWZDMmpVQWlHQmVYa0t3aGRYTGtJYlZxU0EyZmxraXBBeEhYNnByUEExQjF3eTVab3hPUFg4RVExOW92eXpBbFg1dHU0OXEwWC9PSExFN1o5T1cxenltRXR6ZFpyNXJZbWtFcVdtcHVSNU5jeHFwTWlZam93dUNXZWhubzIyeG5JM09IQ0xDZkFKaHRrcklhL1hPc0tZRFpCRzFJMGJsN2taR2R5cEtUQlhYdXl6WE5WUlU5L005ejhaVytwdG1oZ2NOUzBJS2VaaSs5bFl4cWRlS3lnbldTTTV3czdSYUpmNlRRZTNSaWJZUjFvNkhwRzB2VHpiTEtQZTZnRjJGODdiWlBJei9mcTNLWnZiM3UrSnhZcCtJVjBtQi9VN29YelhRRk1RK3VmWllpNzUxbkx6WlVxRE1ybU53TFJPVUFNUk8rVnJtblkwSVB1cFBVMXc0b0hBb1dnVGRnTk5pNk1uTFQ4V0pmUlhjT0pKMk1lbUc2K2ZNeHNZUU52UVJwa1RGY05vaFV6Y3ZjcHJ3NUV3WEVZQTJzbzczL2MvY3RIRGcreU05YlF4REppUlltRnFydkhYb29hS1JyekxnUjZLVWdoM3ltaWxaQ0lSSm9KbTE3aEtHM1pxTTE0Lzl5OUc5OE9BZjNkVTlqMDk3aUNlaEc3a2VxYXRJQ2hFWmJqbmQ4Y00rS3djN2FtVWp2ekQzQmNvMHl3MDJxT054OWF3OGhSblZiWDZhdkRJbGhySHZ6SU44MzFvUjljRHBwMG1DUEJXZFVDQlNqVGJ1RkZqRC90WElSbGxlT2JraFFKSUdSNlE2U1MxcXkzT29WT1VheFl6THY0U2s3dndrQUMwUitGREVIeVFZbFVhbVVkTWcyUmdwRUdhSVd1V3IxaGNnRm10QmREV2g3ZFBuWTF0U3VKOC95MXp4NkRvN2ZJYmNFenBBK2E0ODNtRG5vemdld3VmaFdqVCsvUS85WlEreFQ5UWJBT1pQSXhHV3VhSXVrVk8zSWxvZDhJM1NGZFJCTHY5ZXBDNzFLeXpSdVlpMktkOHJ5NVNINit1WnMxUHlZUlpRakdDK3Q4VzRtSE82Z1lFRWVXSkJ1UWhnSHdmV2xhZXlWb3hac0NBQVZKRUllT3hPZDZtNW45OHRCUDdHTmgxT1M0eDRCS2FVN1A0UVQzNVVIZW5meE84WWFQUThmbXlobUJhSVJVZklBTVN2ZTJZRFp5SWNNTTkrN0tNSVVabzJ0eXRvYzdCOGVvZzBNaUkrVkpFdFg0c29FRjFSWkhQZVV3NWlCTjI4OTh2MmVTcGNnVUJhWHFzOUN5VlZtTVJQMEtLUDJ1REt4MUdJcUhjS0ZCOXVQVWRkQS9vT3dNa0tVUWsraFZVVDVPbEVMdjd1a0FBUEE0eE4rZkczVmYxeUVKV0FiVGx5dWtGcThjNXBTRkY1cXVHbUgwVmVpQzVvVEFka1VES3Z6WGhWWUs5c3BRYjNVZ1Z0Qld6N1ZScnlOUVVST3BIZU5xeDlhZHA4YWREWCtRSHJUKytYblN4VVI3SVdGanlNTkZJRWlMWmkxdks1UVVrZlRDUU9qdjh2SHdiUi9MRHF3Z3M5bXdsT3pPY0RLdVBVK0dTb2lnVFdRejRWN0N2SHRaVDI3WUdKVG44RFFFM3IzdjB4aWxvODJ2U3VXSDg0WEU3VEJsTUpFb2R5eDNDRngwVUVkc3VhRHBPSEV3UjZYNlUyU0xseERYSXVZeEhlNXh2NjI4bXU0bDRMSnBYUjhkYmljTEZKQW55Q0FVeDJLb2dDamt1cmU4bXNUZktDbG8wamFlN1hNR05PSk15b0ZYbVlHZUh2eGhNUGMzTEtYLy9VY1p0c3p3dFJrQmNFdURXQysvQWNWZVBOSHVOWWI5MEpIcnRucGg1ZDlhL1lpTkpzY1N3QTFwUVZrdW1TQWtPQWdLdWRzcnl3c0N3Zkg1anNydVpHUTJDd1hKRXQzUU4wU2NLUlVnT1NCQ3FYa1BqZDVSVzJuOFZpamt4anovbWptakhCNmk0eHM5NEU2Nzk5STAyaldYNVd3UDZhTFRaTGt5TjhxNDUxT0RmeUZVZEY5WWsyZXQ5VUpsV1NzRFJMSWVCd0ZyQkEyZTdyRWsybWFLVUNCRW5PUWM2bUhVMXQvZ3gzK1VXVVFXbkpMZVUxbWUvbkFEdy96UGUwd3d0Vm9BaERZdDBoR1hQblJydjFoUHRGS01CeWtqckg3a0J5U0R3WDlQMi9XZkNkQlE5K1J4cHRsR2hvRmdpMUs0NVlOeEpEd05wTmd5MDV2WXUzVUtrMkpRYVNGUzcwK0Y1NzluRE5RenZpK0pPRlRsdDFmWDJGNXk5NEV2NHZobWRQSmRVOFVVRjU2Ymx0emxKREVFdmsySlFrOTM0aHpwTXJGZ1d3ZHUxUkxxSEhCN2h2T2hnaHNqV0ZGY01zNjZaRUtWcVhKUytxWWNVMHk0akwySVQrNlF2N2pvQ3BWbUdzUWtGY1FyblhxOUJiOTdaUS96UCtwaldmWTU0UmNRVlMydUU1YURObVVyVkdLK3E0d0xRcUhuRVViT2puSHFFeGlacUtxOVdRaUtUK2c3QS96bVlIQ2k0YzFTejRNVWhHb0t6U2l4aXoxYUNJUEJXdy9vczR2cUVqbXgzOGx6YnV0OWNWbElzeGNkTUpUTERRK3ZOZ0YyY1ZRaVcxRTQ0d3lWcnI3TUFaOE9KRVpFSzlEZWt5MzJQUkFuSkRUVXVqdGFscmJ0T2VOczhyS09uTjcvNFRqUEwvZmRlbEI4bjA4WXdSNXdmbU42VGpGWUhRSDFjbUZmK1AvNUxVMTI4Q1pEYjNQUStxMlFJazV3aE40eGwvcy9lb29pallmeWtDcm5aSEhHWkluTGhoU2pWbk5ISWdTL203VWV0NlhBTDdvZUl5UFRLeHVnbDJzRWtUQzNnZ0tjTnFZR0E5U3ZlYVlaQ00vWHNQRUtQbWs3QmlRNmprWFBKaE1yREd4Vkc0SW9aSDgrYjBrUWJYR2l0Mkw0L3hZdHh1bTVzcFNPSjdsTDltVFpRNnBxM2JOaTEwZU1mZ0ZWaDc3NU5JRlc0SEp3U1FtaTU0bk11blZTQjhxdjZKc0w3SGlsZ2N0ZHFSNThTTjVad1lCa2dOR1hzYjA1QXJWemVXbHh1Y21BSHNPT3dyczFnMzh6bTRZN2ZPZmducmFhV1kxanZZOFlEODZQZThkZzR4cE5paTg3UnNDZk5WK2NKVmMraktFdnpuZVY1Zzd0RmlxZCtsZHp4STlKemdSS2t0WUV6RUpRSVU5M2UvclJaN1lrVkZtNVV1cjVhMWYzcG83T0VtYkJUc2MrQ1FaOGNnYmIvbUphRXJoa3NyL3JURjBNcjNxeDl5SlJWSEJ6YWNWd0dScEFRaURPdnJkWU4xQXBVOTRyR1lrVFVzdWs1YjE1Wll2QVZxRlRzVlVMaS9HY29mbEljMm01Z2RFTFZOblRmdXY1Zlk5S1NlWHFoUU80S0pOYVZmbHAwQ0VKYWFFZFNLUXJJNXRaT2w1RkE4VXZlNmxTWVd5TVk0REl4a1RiT1JoWHVBdzR6b1RTMjgrN3d2TXhydVBkZnlKbUJCTkhQdCtEYmdKNHovcHJZWUhpTmFMTXNZamtQZE44ajNKZDczQXJFZk92Um52MzYxSVVVMFg1RDc1dlRSdlpkbzMzWERzanRlOU4weUo3K2lIQnF1a1FJY2pIVW9ic2RQN0hOajBVYWNSMHIvTmRlVTlGNFBNc1VLY2t6Tk4rZGhyMVI2d1J2R1VZb1pDRWJaWlJMWEt4QnA3SElUNEVQUktHakIvdW1xTFhhMXl6RWx2QW1WQUJhMDFZN3dGdk4wM2Ywb25FbUhTM2w1d1paRmV6cjVibnN5T01XVGxhMU5kaW1ZNXNVeE15VFliZmc4dzB2cXNEc28zWFAxYndLdzZ3M3VIRGQ1UHBSWnVDSnR0eWk0ZzJGeWI0Ymg1UU42ZkdORTI2ekRGN1Y4QmJwZXJLNkFKQ0xTWm5kaDZMMTlPUTBram4xUGpEMGk4c1BZcGFXOWxVeVJkZElPKzRWQS9LemxPUzJ4M2s5VUtUdElsTTBUSVdtZXFIS0dYUVpocGpvVGI2VlNKN203cjZaaVlQMnVsQVVvZmVWL0o2eCtzckxEQXkyQ2ZFNnFrREZ1OU9NWDBBSXVnN3loQUtOMDRyT3hVNk5tcGtjOUZ4bXUvVS9vR3hHdmIzeFVFTDYwdE1sSE9EaWtqY1I5RDJrKzRwbEc1WnV0d0FIY2kwRU02WHRrVEhQOU5QMlRTR1VFN1E5SGYvU0VEc2V0a25hZXhvWmhDczJLWDFMeU5JS0U0N2pkMkR3MTUreDRRVXV0VUFTbzU5Q1lHMVFBeW9BVVhrV3dtbXkzTGdTUWp5T3ZLV25qaE8veWpPd0FyWGd0NFBrSVVnZDQ1N05ReFpMbU41K0J4NVJoQ0FHdkUxYmxOZjlMek9keGJiaG5VZ2Z1RDM5MXVSRkhjS2RYREY3ZmVqb3gveThtaWZJcTRWVzQyajBHQnFOQUtkK0prMnJCMW9hOTRiT2hxcVVzanhqWnlRaGRXTzhNblR6T2tOaGVpZXU2blYxcW5yZ3JHU2huWTNJMlczb29GNFNnczRjZ3drZ2h2dHpFa0xUbU5OUm83RTdudVRuMkxJcmlGSnlvTmZQdUp0aWN0S0JtNzRGZytkWVBTMlIzTzNmOWxBZWxiVWZjbzZGNU9EL3hkS1VuRTh0V3FOMExVcDlWQUptWVZYZFVDaGJ4MjM4MWtDaStLNDJoRzUydFNQYU1hb1dTb0xQY2Zrb24rc1pYdjdEdEtwZi9HTzdhcUMza1pzRGpva29haHJGZGJWSlNTZWhrNGp5K3RzRHplQnJKSjBrMVZrUnJHN1NoVHZjTmd1cjVucVRUTEE5dlJMQmJNTTlhNlI1NEZ0Z1pQOWFKMU1aMEdCcUVpMnF6Ui8yd2tYQlhwcFhZdi9TcU1RV1dhbTVsSHBMVktxaDN4ZHRjNFdmck9mYldsbU1PNXA5Z0JUSFp1YUcxVGFkZXFRVVpKQmZBS01ENFdSR0NsMDFaeDRTVzE0YzZrdnFKdXExL080N215L3RsVHlLWndpYlBkQTNRMVVGd0I3R2Z4anEwaDN2ckxFbUNrS3Vsc0VBUkN6UnZNVjJSVnBVbFpUV240Y1Boc0hjcTNROElHSUYyKy9nOENFSU4vMU8xcVMvMkpXcXlDNmtIb0w4Y2R2R0VHbmkxSTNDTk1JcXhxaHhJL1V0R3REc2VwYmwrSHI0elh4MzZna3BCbXBoT2xkTFVYTHAzVEtibVVZRWJSWHcvZmRmeFQ3WDdZUFhHQ0hHVG1uTzk4WkxDOTA2Zmkvekd2b04rNlpzbCs3MkpWMGxJWEo0V3dZdWxFUmZHbkFDWGNoa0Yzei9ITWR3elcwTUFFaXptQmwvREo2ZUoyU01PSG1Uc25YbElGRDRlcFRrYnFBQ0dpZ2I1UExFdHdQRVRjYkNRckM5YUtTU1FnSTdEZXd1aWlxM2J0Y0RUWkIzeEI5WWxlbmhpU0FXNjIwcmwzc2ZjY3d3eGFSOHBDV2Rzd0x3dmFxcDhjM01PV3RCc2xPcmVTSkNEcWgvdzBYbm1WMFJVWFpNM2JvUmkwVXhsaHVUeDFlM1NTd09pbTlOczNYV3NoTmI4Lzc3VkhnUWhRVFlSUU1NRllYaWRmMElCKzBtSUpocWNoQTlUeUY3dGRjSDhrUUJUSHNEWS96bFpqK3EwNlFMd0JkbTkxc3IyK3VzZmxlaXB3WUMrcmdiNHROVnA3VU5rYkVqTnR6ZWZsTi9VRTlkbHZtT2x6V1dtZkh2NGVkUGkzMmJmeUNRS1d6SGJVVEV3NU0yVFpsZnpNaTFWUjVsaDBxQ1lqaDNITUlmL2MwcHBKd2I1b1lFTnBBenlxbnlmdmlTV3lBYzc2L1l1VWwvb2FVaysrYzBZc2d1TGo5ZGFQdVVvemhoZ3VjSytQRGlNckI0ODU1Mk83VWg0aHRwNmZ3S2dJa1JCTVFIUTd6MmV5WXovV1AwQm9ZZVhjOGc3aUprclhFNzA1bFo1bXhGU0poT3E1WlNleVJSb21pUm41K3VRemM5ZFdWQjBYb2JURXdOc0VRM2FIZ25JY29BczY2UGplUT09PC94ZW5jOkNpcGhlclZhbHVlPjwveGVuYzpDaXBoZXJEYXRhPjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDI6RW5jcnlwdGVkQXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg== From cd4396cfed7fc3b33f1c96209e8de9a3c8b60bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 09:36:54 +0100 Subject: [PATCH 125/152] feat(ci): pretty format toml files --- .pre-commit-config.yaml | 2 + pyproject.toml | 228 ++++++++++++++++++++-------------------- 2 files changed, 116 insertions(+), 114 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f159901e..eaa59357c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,3 +31,5 @@ repos: hooks: - id: pretty-format-yaml args: [--autofix, --indent, '2'] + - id: pretty-format-toml + args: [--autofix] diff --git a/pyproject.toml b/pyproject.toml index c81fb466e..d2e88516c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,40 +1,86 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = ["setuptools"] + +[dependency-groups] +dev = [ + "flake8>=5.0.4", + "pytest>=4.5", + "httpretty~=1.1.0", + "coverage>=3.6", + "pytest-cov>=2.7.1", + # pinned because of https://github.com/gabrielfalcao/HTTPretty/issues/484 + "urllib3~=2.2.0", + "pyright>=1.1.391", + "pytest-xdist>=3.6.1" +] + [project] -name = "social-auth-core" -version = "4.5.4" -description = "Python social authentication made simple." authors = [ - {name = "Matias Aguirre", email = "matiasaguirre@gmail.com"}, + {name = "Matias Aguirre", email = "matiasaguirre@gmail.com"} +] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "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", + "Topic :: Internet" ] dependencies = [ - "PyJWT>=2.7.0", - "cryptography>=1.4", - "defusedxml>=0.5.0", - "oauthlib>=1.0.3", - "python3-openid>=3.0.10", - "requests-oauthlib>=0.6.1", - "requests>=2.9.1", + "PyJWT>=2.7.0", + "cryptography>=1.4", + "defusedxml>=0.5.0", + "oauthlib>=1.0.3", + "python3-openid>=3.0.10", + "requests-oauthlib>=0.6.1", + "requests>=2.9.1" ] -requires-python = ">=3.9" -readme = "README.md" -license = {text = "BSD"} +description = "Python social authentication made simple." keywords = ["openid", "oauth", "saml", "social auth"] -classifiers = [ - "Development Status :: 4 - Beta", - "Environment :: Web Environment", - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Programming Language :: Python", - "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", - "Topic :: Internet", +license = {text = "BSD"} +name = "social-auth-core" +readme = "README.md" +requires-python = ">=3.9" +version = "4.5.4" + +[project.optional-dependencies] +all = [ + "social-auth-core[saml]", + "social-auth-core[azuread]" +] +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 = [ + "flake8>=5.0.4", + "pytest>=4.5", + "httpretty~=1.1.0", + "coverage>=3.6", + "pytest-cov>=2.7.1", + # pinned because of https://github.com/gabrielfalcao/HTTPretty/issues/484 + "urllib3~=2.2.0", + "pyright>=1.1.391" +] +saml = [ + "python3-saml>=1.5.0", + # pinned to 5.2 until a new wheel of xmlsec is released + "lxml~=5.2.1" ] -[tool.setuptools] -packages = ["social_core"] +[project.urls] +Homepage = "https://github.com/python-social-auth/social-core" [tool.ruff.lint] extend-safe-fixes = [ @@ -46,7 +92,6 @@ extend-safe-fixes = [ "FA102", "UP" ] -select = ["ALL"] ignore = [ "COM", # CONFIG: No trailing commas "D203", # CONFIG: incompatible with D211 @@ -59,48 +104,52 @@ ignore = [ "TD003", # CONFIG: no detailed TODO documentation is required "ANN", # TODO: Missing type annotations "D", # TODO: Missing documentation - "B904", # TODO: use raise from - "PLR2004", # TODO: Magic value used in comparison - "PLW2901", # TODO: loop variable overwritten by assignment target - "N", # TODO: Naming issues + "B904", # TODO: use raise from + "PLR2004", # TODO: Magic value used in comparison + "PLW2901", # TODO: loop variable overwritten by assignment target + "N", # TODO: Naming issues "PTH", # TODO: Not using pathlib - "RUF012", # TODO: Type annotations + "RUF012", # TODO: 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 - "TID252", # TODO: Prefer absolute imports over relative imports from parent modules + "ARG005", # TODO: Unused lambda argument + "TID252", # TODO: Prefer absolute imports over relative imports from parent modules "FBT", # TODO: Boolean in function definition "S105", # TODO: Possible hardcoded password assigned "S113", # TODO: Probable use of `requests` call without timeout - "B018", # TODO: Found useless expression. - "A001", # TODO: Variable is shadowing a Python builtin - "A002", # TODO: Function argument is shadowing a Python builtin - "A004", # TODO: Import `ConnectionError` is shadowing a Python builtin - "ERA001", # TODO: Found commented-out code - "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 - "TRY003", # TODO: Avoid specifying long messages outside the exception class - "S101", # TODO: Use of `assert` detected - "DTZ001", # TODO: `datetime.datetime()` called without a `tzinfo` argument - "DTZ005", # TODO: `datetime.datetime.now()` called without a `tz` argument - "B006", # TODO: Do not use mutable data structures for argument defaults - "B026", # TODO: Star-arg unpacking after a keyword argument is strongly discouraged - "S311", # TODO: Standard pseudo-random generators are not suitable for cryptographic purposes - "S301", # TODO: `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue - "S324", # TODO: Probable use of insecure hash functions in `hashlib` - "S318", # TODO: Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents - "PLR1704", # TODO: Redefining argument with the local name + "B018", # TODO: Found useless expression. + "A001", # TODO: Variable is shadowing a Python builtin + "A002", # TODO: Function argument is shadowing a Python builtin + "A004", # TODO: Import `ConnectionError` is shadowing a Python builtin + "ERA001", # TODO: Found commented-out code + "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 + "TRY003", # TODO: Avoid specifying long messages outside the exception class + "S101", # TODO: Use of `assert` detected + "DTZ001", # TODO: `datetime.datetime()` called without a `tzinfo` argument + "DTZ005", # TODO: `datetime.datetime.now()` called without a `tz` argument + "B006", # TODO: Do not use mutable data structures for argument defaults + "B026", # TODO: Star-arg unpacking after a keyword argument is strongly discouraged + "S311", # TODO: Standard pseudo-random generators are not suitable for cryptographic purposes + "S301", # TODO: `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue + "S324", # TODO: Probable use of insecure hash functions in `hashlib` + "S318", # TODO: Using `xml` to parse untrusted data is known to be vulnerable to XML attacks; use `defusedxml` equivalents + "PLR1704", # TODO: Redefining argument with the local name "PERF203", # WONTFIX: This rule is only enforced for Python versions prior to 3.11 - "ISC003", # TODO: Explicitly concatenated string should be implicitly concatenated - "B028", # TODO: No explicit `stacklevel` keyword argument found - "PLW0603", # TODO: Using the global statement to update `BACKENDSCACHE` is discouraged - "TRY301", # TODO: Abstract `raise` to an inner function - "BLE001", # TODO: Do not catch blind exception: `Exception` - "S110", # TODO: `try`-`except`-`pass` detected, consider logging the exception - "TRY300", # TODO: Consider moving this statement to an `else` block - "G004", # TODO: Logging statement uses f-string + "ISC003", # TODO: Explicitly concatenated string should be implicitly concatenated + "B028", # TODO: No explicit `stacklevel` keyword argument found + "PLW0603", # TODO: Using the global statement to update `BACKENDSCACHE` is discouraged + "TRY301", # TODO: Abstract `raise` to an inner function + "BLE001", # TODO: Do not catch blind exception: `Exception` + "S110", # TODO: `try`-`except`-`pass` detected, consider logging the exception + "TRY300", # TODO: Consider moving this statement to an `else` block + "G004" # TODO: Logging statement uses f-string ] +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"] @@ -110,54 +159,5 @@ ignore = [ max-args = 7 max-branches = 15 -[tool.ruff.lint.mccabe] -max-complexity = 11 # TODO: should be lower - -[project.urls] -Homepage = "https://github.com/python-social-auth/social-core" - -[project.optional-dependencies] -saml = [ - "python3-saml>=1.5.0", - # pinned to 5.2 until a new wheel of xmlsec is released - "lxml~=5.2.1", -] -azuread = [ - "cryptography>=2.1.1", -] -all = [ - "social-auth-core[saml]", - "social-auth-core[azuread]", -] -allpy3 = [ - "social-auth-core[all]", -] -# This is present until pip implements supports for PEP 735 -# see https://github.com/pypa/pip/issues/12963 -dev = [ - "flake8>=5.0.4", - "pytest>=4.5", - "httpretty~=1.1.0", - "coverage>=3.6", - "pytest-cov>=2.7.1", - # pinned because of https://github.com/gabrielfalcao/HTTPretty/issues/484 - "urllib3~=2.2.0", - "pyright>=1.1.391", -] - -[build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta" - -[dependency-groups] -dev = [ - "flake8>=5.0.4", - "pytest>=4.5", - "httpretty~=1.1.0", - "coverage>=3.6", - "pytest-cov>=2.7.1", - # pinned because of https://github.com/gabrielfalcao/HTTPretty/issues/484 - "urllib3~=2.2.0", - "pyright>=1.1.391", - "pytest-xdist>=3.6.1", -] +[tool.setuptools] +packages = ["social_core"] From 19e9ef09b149a84788d6e16a367f048a38e49393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 09:37:10 +0100 Subject: [PATCH 126/152] feat(ci): pretty format markdown files --- .pre-commit-config.yaml | 9 ++++++ CHANGELOG.md | 70 ++++++++++++++++++++++++++++++++++++----- CONTRIBUTING.md | 46 +++++++++++++-------------- README.md | 1 + 4 files changed, 96 insertions(+), 30 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eaa59357c..0b6a6f2c7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,3 +33,12 @@ repos: args: [--autofix, --indent, '2'] - id: pretty-format-toml args: [--autofix] +- repo: https://github.com/executablebooks/mdformat + rev: 0.7.21 + 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 58e8b3164..19a0bfdce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,16 +8,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Changed -- Handle case where user has not registered a `family-name` with ORCID + +- 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`. ## [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 @@ -26,23 +29,28 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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 @@ -53,6 +61,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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) @@ -67,6 +76,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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 @@ -74,12 +84,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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 @@ -87,6 +97,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 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` @@ -95,24 +106,25 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 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 @@ -120,6 +132,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 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 @@ -131,47 +144,53 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 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 +- 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 @@ -187,9 +206,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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 @@ -202,22 +223,26 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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 @@ -230,6 +255,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 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 @@ -250,10 +276,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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 @@ -273,10 +301,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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 @@ -289,6 +319,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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 @@ -300,6 +331,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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 @@ -310,6 +342,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 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 @@ -332,6 +365,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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 @@ -343,12 +377,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 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 @@ -356,11 +392,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 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 @@ -369,6 +407,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 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 @@ -379,6 +418,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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. @@ -388,6 +428,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 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. @@ -396,6 +437,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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 @@ -403,6 +445,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 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 @@ -412,9 +455,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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 @@ -422,11 +467,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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 @@ -435,11 +482,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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 toke place, use `auth_time` to determine if access token expired @@ -447,27 +496,32 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 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 toke 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. @@ -481,7 +535,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added better username characters clenup 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 +- 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) @@ -496,6 +550,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 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). @@ -532,5 +587,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 87d249f5c..d999c1e93 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,50 +3,50 @@ I like to encourage you to contribute to the repository. This should be as easy as possible for you but there are a few things -to consider when contributing. The following guidelines for +to consider when contributing. The following guidelines for contribution should be followed if you want to submit a pull request. ## How to prepare -* You need a [GitHub account](https://github.com/signup/free) -* Submit an [issue ticket](https://github.com/python-social-auth/social-core/issues) +- You need a [GitHub account](https://github.com/signup/free) +- Submit an [issue ticket](https://github.com/python-social-auth/social-core/issues) for your issue if there is no one yet. - * Describe the issue and include steps to reproduce if it's a bug. - * Ensure to mention the earliest version that you know is affected. -* If you are able and want to fix this, fork the repository on GitHub + - Describe the issue and include steps to reproduce if it's a bug. + - Ensure to mention the earliest version that you know is affected. +- If you are able and want to fix this, fork the repository on GitHub ## Make Changes -* In your forked repository, create a topic branch for your upcoming +- In your forked repository, create a topic branch for your upcoming patch. (e.g. `feature/new-backend` or `bug/auth-fails`) - * Usually this is based on the `master` branch. - * Create a branch based on master `git branch bug/auth-fails master` - then checkout the new branch with `git checkout bug/auth-fails`. - Please avoid working directly on the `master` branch. -* Make commits of logical units and describe them properly. -* Make sure you stick to [PEP8](https://www.python.org/dev/peps/pep-0008/) + - Usually this is based on the `master` branch. + - Create a branch based on master `git branch bug/auth-fails master` + then checkout the new branch with `git checkout bug/auth-fails`. + Please avoid working directly on the `master` branch. +- Make commits of logical units and describe them properly. +- Make sure you stick to [PEP8](https://www.python.org/dev/peps/pep-0008/) coding style that is used already. -* If possible, submit tests to your patch / new feature so it can be tested easily. -* Assure nothing is broken by running all the tests. -* Add a meaningful entry to the `CHANGELOG.md` document. +- If possible, submit tests to your patch / new feature so it can be tested easily. +- Assure nothing is broken by running all the tests. +- Add a meaningful entry to the `CHANGELOG.md` document. ## Submit Changes -* Push your changes to a topic branch in your fork of the repository. -* Open a pull request to the original repository and choose the right +- Push your changes to a topic branch in your fork of the repository. +- Open a pull request to the original repository and choose the right original branch you want to patch. -* If not done in commit messages (which you really should do) please +- If not done in commit messages (which you really should do) please reference and update your issue with the code changes. But _please do not close the issue yourself_. -* Even if you have write access to the repository, do not directly +- Even if you have write access to the repository, do not directly push or merge pull-requests. Let another team member review your pull request and approve. # Additional Resources -* [General GitHub documentation](http://help.github.com/) -* [GitHub pull request documentation](http://help.github.com/send-pull-requests/) -* [Read the Issue Guidelines by @necolas](https://github.com/necolas/issue-guidelines/blob/master/CONTRIBUTING.md) +- [General GitHub documentation](http://help.github.com/) +- [GitHub pull request documentation](http://help.github.com/send-pull-requests/) +- [Read the Issue Guidelines by @necolas](https://github.com/necolas/issue-guidelines/blob/master/CONTRIBUTING.md) for more details # Notes diff --git a/README.md b/README.md index 840dce4be..0cf2e4e09 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Python Social Auth - Core + ![Build Status](https://github.com/python-social-auth/social-core/workflows/Flake8/badge.svg) ![Build Status](https://github.com/python-social-auth/social-core/workflows/Tests/badge.svg) [![Build Status](https://travis-ci.org/python-social-auth/social-core.svg?branch=master)](https://travis-ci.org/python-social-auth/social-core) From 23f737d125a36723f278a28749ffe476552b3fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 09:37:49 +0100 Subject: [PATCH 127/152] feat(ci): add pyproject validation --- .pre-commit-config.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0b6a6f2c7..6865ff316 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,6 +33,10 @@ repos: args: [--autofix, --indent, '2'] - id: pretty-format-toml args: [--autofix] +- repo: https://github.com/abravalheri/validate-pyproject + rev: v0.23 + hooks: + - id: validate-pyproject - repo: https://github.com/executablebooks/mdformat rev: 0.7.21 hooks: From 2e91313df3d6824527d39485dccdb1e2351751e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 09:43:54 +0100 Subject: [PATCH 128/152] feat(ci): introduce codespell --- .pre-commit-config.yaml | 6 ++++++ CHANGELOG.md | 6 +++--- pyproject.toml | 4 ++++ social_core/backends/base.py | 2 +- social_core/backends/ngpvan.py | 2 +- social_core/backends/professionali.py | 4 ++-- social_core/backends/yahoo.py | 2 +- 7 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6865ff316..c34fd4d31 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,3 +46,9 @@ repos: - mdformat-ruff==0.1.3 - mdformat-shfmt==0.2.0 - mdformat_tables==1.0.0 +- repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + additional_dependencies: + - tomli diff --git a/CHANGELOG.md b/CHANGELOG.md index 19a0bfdce..2c8f6b95e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -490,7 +490,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - Store partial pipeline data in an storage class -- Store `auth_time` with the last time authentication toke place, use +- 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 @@ -504,7 +504,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### 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 toke place +- Added `auth_time` in extra data by default to store the time that the authentication took place ### Changed @@ -532,7 +532,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). 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 clenup rules, support for a configurable +- 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 diff --git a/pyproject.toml b/pyproject.toml index d2e88516c..67f730878 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,10 @@ saml = [ [project.urls] Homepage = "https://github.com/python-social-auth/social-core" +[tool.codespell] +count = true +ignore-words-list = "assertIn" + [tool.ruff.lint] extend-safe-fixes = [ "D", diff --git a/social_core/backends/base.py b/social_core/backends/base.py index eee1b5daf..ad0478811 100644 --- a/social_core/backends/base.py +++ b/social_core/backends/base.py @@ -124,7 +124,7 @@ def extra_data( ) -> dict[str, Any]: """Return default extra data to store in extra_data field""" data = { - # store the last time authentication toke place + # store the last time authentication took place "auth_time": int(time.time()) } extra_data_entries = [] diff --git a/social_core/backends/ngpvan.py b/social_core/backends/ngpvan.py index 51c4a3c3c..86b60577d 100644 --- a/social_core/backends/ngpvan.py +++ b/social_core/backends/ngpvan.py @@ -35,7 +35,7 @@ def setup_request(self, params=None): """ Setup the OpenID request - Because ActionID does not advertise the availiability of AX attributes + 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() """ diff --git a/social_core/backends/professionali.py b/social_core/backends/professionali.py index 55d970d81..945949645 100644 --- a/social_core/backends/professionali.py +++ b/social_core/backends/professionali.py @@ -1,7 +1,7 @@ """ -Professionaly OAuth 2.0 support. +Professionali OAuth 2.0 support. -This contribution adds support for professionaly.ru OAuth 2.0. +This contribution adds support for professionali.ru OAuth 2.0. Username is retrieved from the identity returned by server. """ diff --git a/social_core/backends/yahoo.py b/social_core/backends/yahoo.py index fba780262..ecb979315 100644 --- a/social_core/backends/yahoo.py +++ b/social_core/backends/yahoo.py @@ -47,7 +47,7 @@ def user_data(self, access_token, *args, **kwargs): def _get_guid(self, access_token): """ - Beause you have to provide GUID for every API request it's also + Because you have to provide GUID for every API request it's also returned during one of OAuth calls """ return self.get_json( From d43d442e82b9b01fdba25f24f092757877c3f98c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 09:51:33 +0100 Subject: [PATCH 129/152] feat: switch to renovate from dependabot This will give us fine-grained updates for pre-commit. --- .github/dependabot.yml | 12 ------------ .github/renovate.json | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 12 deletions(-) delete mode 100644 .github/dependabot.yml create mode 100644 .github/renovate.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 1ee4b451c..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2 -updates: -- package-ecosystem: github-actions - directory: / - schedule: - interval: daily - open-pull-requests-limit: 10 -- package-ecosystem: pip - directory: / - schedule: - interval: daily - open-pull-requests-limit: 10 diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 000000000..f234a6333 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ], + "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" + } + ] +} From 3f19af753f9592f2de20bdee34be697662a19a1c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 08:54:09 +0000 Subject: [PATCH 130/152] chore(deps): update python docker tag to v3.11.4 --- files/release/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/release/Dockerfile b/files/release/Dockerfile index 3960e177b..c27beecd3 100644 --- a/files/release/Dockerfile +++ b/files/release/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8.2-slim-buster +FROM python:3.11.4-slim-buster RUN apt-get update && \ apt-get install -y --no-install-recommends make gettext git curl && \ From 37d5ccda252fedb6b883e3074fc9131fcd0bd4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 09:51:33 +0100 Subject: [PATCH 131/152] feat: enable dependency dashboard for renovate --- .github/renovate.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index f234a6333..41c6edb7e 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,7 +1,8 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:recommended" + "config:recommended", + ":dependencyDashboard" ], "pre-commit": { "enabled": true From c77c4ed0e8c60c5881b7e062dcb02eb5637f2bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 10:01:10 +0100 Subject: [PATCH 132/152] chore: drop docker-compose based test and releaase workflows These are not used and maintained. --- .gitignore | 1 - docker-compose.yml | 26 -------------------------- files/local.env.template | 2 -- files/release/Dockerfile | 15 --------------- files/release/entrypoint.sh | 35 ----------------------------------- files/release/pypirc.template | 7 ------- files/tests/Dockerfile | 22 ---------------------- files/tests/entrypoint.sh | 10 ---------- files/tests/pyenv.sh | 20 -------------------- 9 files changed, 138 deletions(-) delete mode 100644 docker-compose.yml delete mode 100644 files/local.env.template delete mode 100644 files/release/Dockerfile delete mode 100755 files/release/entrypoint.sh delete mode 100644 files/release/pypirc.template delete mode 100644 files/tests/Dockerfile delete mode 100755 files/tests/entrypoint.sh delete mode 100755 files/tests/pyenv.sh diff --git a/.gitignore b/.gitignore index 81e5052ec..9377bf6e2 100644 --- a/.gitignore +++ b/.gitignore @@ -52,5 +52,4 @@ changelog.sh .pytest_cache/ -files/local.env .env diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 452793e81..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: '3.7' - -services: - social-release: - image: omab/social-core-release - build: - context: . - dockerfile: ./files/release/Dockerfile - environment: - - PROJECT_NAME=social-auth-core - env_file: - - ./files/local.env - volumes: - - .:/code - - social-tests: - image: omab/social-core-tests - build: - context: . - dockerfile: ./files/tests/Dockerfile - args: - - PYTHON_VERSIONS=3.8.17 3.9.17 3.10.12 3.11.4 3.12.0 - environment: - - PYTHON_VERSIONS=3.8.17 3.9.17 3.10.12 3.11.4 3.12.0 - volumes: - - .:/code diff --git a/files/local.env.template b/files/local.env.template deleted file mode 100644 index f3ecd73ea..000000000 --- a/files/local.env.template +++ /dev/null @@ -1,2 +0,0 @@ -PYPI_USERNAME= -PYPI_PASSWORD= diff --git a/files/release/Dockerfile b/files/release/Dockerfile deleted file mode 100644 index c27beecd3..000000000 --- a/files/release/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM python:3.11.4-slim-buster - -RUN apt-get update && \ - apt-get install -y --no-install-recommends make gettext git curl && \ - pip install -U pip && \ - pip install -U setuptools && \ - pip install -U twine - -COPY ./files/release/pypirc.template / -COPY ./files/release/entrypoint.sh / -ADD . /code - -WORKDIR /code - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/files/release/entrypoint.sh b/files/release/entrypoint.sh deleted file mode 100755 index 6885c3f6b..000000000 --- a/files/release/entrypoint.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash - -set -e - -if [ -z "${PYPI_USERNAME}" ] || [ -z "${PYPI_PASSWORD}" ]; then - echo "=====================================================================" - echo "Error: missing PYPI_USERNAME or PYPI_PASSWORD environment values" - echo "=====================================================================" - exit 1; -fi - -envsubst < /pypirc.template > ~/.pypirc - -# This will fail if tag doesn't exist -CURRENT_VERSION=$(head -n1 social_core/__init__.py | awk '{print $3}' | sed 's/[^0-9\.]//g') -CURRENT_TAG=$(git describe --tags --abbrev=0) - -if [ "${CURRENT_VERSION}" != "${CURRENT_TAG}" ]; then - echo "=====================================================================" - echo "Error: version '${CURRENT_VERSION}' not tagged" - echo "=====================================================================" - exit 1; -fi - -PYPI_URL="https://pypi.org/project/${PROJECT_NAME}/${CURRENT_VERSION}/" -VERSION_PAGE_STATUS=$(curl -s -I ${PYPI_URL} | head -n1 | awk '{print $2}') - -if [ "${VERSION_PAGE_STATUS}" == "200" ]; then - echo "=====================================================================" - echo "Error: version '${CURRENT_VERSION}' already exists" - echo "=====================================================================" - exit 1; -fi - -make clean build publish diff --git a/files/release/pypirc.template b/files/release/pypirc.template deleted file mode 100644 index 61a150414..000000000 --- a/files/release/pypirc.template +++ /dev/null @@ -1,7 +0,0 @@ -[distutils] -index-servers = - pypi - -[pypi] -username: ${PYPI_USERNAME} -password: ${PYPI_PASSWORD} diff --git a/files/tests/Dockerfile b/files/tests/Dockerfile deleted file mode 100644 index b66cf5550..000000000 --- a/files/tests/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM python:3.8.2-slim-buster - -ARG PYTHON_VERSIONS=${PYTHON_VERSIONS} -ENV PYTHON_VERSIONS=${PYTHON_VERSIONS} - -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - make git pkg-config ca-certificates wget curl llvm build-essential \ - python-openssl libssl-dev zlib1g-dev libbz2-dev libreadline-dev \ - libsqlite3-dev libncurses5-dev libncursesw5-dev xz-utils libxml2-dev \ - libxmlsec1-dev libffi-dev tk-dev liblzma-dev - -COPY ./files/tests/pyenv.sh / -RUN /pyenv.sh - -COPY ./files/tests/entrypoint.sh / - -ADD . /code - -WORKDIR /code - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/files/tests/entrypoint.sh b/files/tests/entrypoint.sh deleted file mode 100755 index 504f745be..000000000 --- a/files/tests/entrypoint.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -set -e - -export PATH=~/.pyenv/shims:~/.pyenv/bin:${PATH} -export PYENV_ROOT=~/.pyenv - -eval "$(pyenv init -)" - -tox diff --git a/files/tests/pyenv.sh b/files/tests/pyenv.sh deleted file mode 100755 index a9aca225c..000000000 --- a/files/tests/pyenv.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -set -e - -export PATH=~/.pyenv/shims:~/.pyenv/bin:${PATH} -export PYENV_ROOT=~/.pyenv -export PYTHON_VERSIONS=${PYTHON_VERSIONS} - -curl -L https://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash - -eval "$(pyenv init -)" - -for version in ${PYTHON_VERSIONS}; do - pyenv install "${version}" - pyenv local "${version}" - pip install --upgrade setuptools pip tox - pyenv local --unset -done - -pyenv local ${PYTHON_VERSIONS} From 12671781740975d2483e4bdd3fec6694f03665f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 10:05:25 +0100 Subject: [PATCH 133/152] chore: remove no longer used rerequirements-*.txt These are now inside pyproject.toml. --- .github/workflows/test.yml | 2 +- requirements-azuread.txt | 1 - requirements-base.txt | 7 ------- requirements-dev.txt | 1 - requirements-saml.txt | 1 - 5 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 requirements-azuread.txt delete mode 100644 requirements-base.txt delete mode 100644 requirements-dev.txt delete mode 100644 requirements-saml.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e9535f1c9..f6b868d29 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: with: python-version: ${{ matrix.python-version }} cache: pip - cache-dependency-path: requirements*.txt + cache-dependency-path: pyproject.toml - name: Install System dependencies run: | diff --git a/requirements-azuread.txt b/requirements-azuread.txt deleted file mode 100644 index 63081b67f..000000000 --- a/requirements-azuread.txt +++ /dev/null @@ -1 +0,0 @@ -cryptography>=2.1.1 diff --git a/requirements-base.txt b/requirements-base.txt deleted file mode 100644 index 46afbb6c4..000000000 --- a/requirements-base.txt +++ /dev/null @@ -1,7 +0,0 @@ -requests>=2.9.1 -oauthlib>=1.0.3 -requests-oauthlib>=0.6.1 -PyJWT>=2.7.0 -cryptography>=1.4 -defusedxml>=0.5.0 -python3-openid>=3.0.10 diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index e88d27155..000000000 --- a/requirements-dev.txt +++ /dev/null @@ -1 +0,0 @@ -pre-commit==4.0.1 diff --git a/requirements-saml.txt b/requirements-saml.txt deleted file mode 100644 index daf6ec32b..000000000 --- a/requirements-saml.txt +++ /dev/null @@ -1 +0,0 @@ -python3-saml>=1.5.0 From 4eb1656bcdafe63ca46dd2e5718978be1b7a6f88 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:03:54 +0000 Subject: [PATCH 134/152] chore(deps): update dependency ubuntu to v24 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f6b868d29..d77e72c02 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: types: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: @@ -41,7 +41,7 @@ jobs: run: tox -e "py${PYTHON_VERSION/\./}-pyright" test: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: From 86decc9f2d8008e8533cdb5de2f3ee878a564195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 11:27:21 +0100 Subject: [PATCH 135/152] chore: update shared files Automated update of shared files from the social-core repository, see https://github.com/python-social-auth/.github/blob/main/repo-sync.py --- .flake8 | 4 ---- CONTRIBUTING.md | 55 ------------------------------------------------- Makefile | 17 --------------- 3 files changed, 76 deletions(-) delete mode 100644 .flake8 delete mode 100644 CONTRIBUTING.md delete mode 100644 Makefile diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 44d893071..000000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -select = Q0 -avoid-escape = False -exclude = .git,.tox,.env,.github,.pytest_cache,__pycache__,files,dist,build,social_auth_core.egg-info diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index d999c1e93..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,55 +0,0 @@ -# How to contribute - -I like to encourage you to contribute to the repository. - -This should be as easy as possible for you but there are a few things -to consider when contributing. The following guidelines for -contribution should be followed if you want to submit a pull request. - -## How to prepare - -- You need a [GitHub account](https://github.com/signup/free) -- Submit an [issue ticket](https://github.com/python-social-auth/social-core/issues) - for your issue if there is no one yet. - - Describe the issue and include steps to reproduce if it's a bug. - - Ensure to mention the earliest version that you know is affected. -- If you are able and want to fix this, fork the repository on GitHub - -## Make Changes - -- In your forked repository, create a topic branch for your upcoming - patch. (e.g. `feature/new-backend` or `bug/auth-fails`) - - Usually this is based on the `master` branch. - - Create a branch based on master `git branch bug/auth-fails master` - then checkout the new branch with `git checkout bug/auth-fails`. - Please avoid working directly on the `master` branch. -- Make commits of logical units and describe them properly. -- Make sure you stick to [PEP8](https://www.python.org/dev/peps/pep-0008/) - coding style that is used already. -- If possible, submit tests to your patch / new feature so it can be tested easily. -- Assure nothing is broken by running all the tests. -- Add a meaningful entry to the `CHANGELOG.md` document. - -## Submit Changes - -- Push your changes to a topic branch in your fork of the repository. -- Open a pull request to the original repository and choose the right - original branch you want to patch. -- If not done in commit messages (which you really should do) please - reference and update your issue with the code changes. But _please - do not close the issue yourself_. -- Even if you have write access to the repository, do not directly - push or merge pull-requests. Let another team member review your - pull request and approve. - -# Additional Resources - -- [General GitHub documentation](http://help.github.com/) -- [GitHub pull request documentation](http://help.github.com/send-pull-requests/) -- [Read the Issue Guidelines by @necolas](https://github.com/necolas/issue-guidelines/blob/master/CONTRIBUTING.md) - for more details - -# Notes - -This documented is based in the work from [anselmh/CONTRIBUTING.md](https://github.com/anselmh/CONTRIBUTING.md), -licensed as [Creative Commons Attribution 3.0 Unported License](https://github.com/anselmh/CONTRIBUTING.md/blob/master/README.md#license). diff --git a/Makefile b/Makefile deleted file mode 100644 index e24219f30..000000000 --- a/Makefile +++ /dev/null @@ -1,17 +0,0 @@ -build: - @ python setup.py sdist - @ python setup.py bdist_wheel --python-tag py3 - -publish: - @ twine upload dist/* - -release: - @ docker-compose run social-release - -tests: - @ docker-compose run social-tests - -clean: - @ find . -name '*.py[co]' -delete - @ find . -name '__pycache__' -delete - @ rm -rf *.egg-info dist build From 810cbf250e2aeabfe196c89db914625ea869f60f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 11:45:27 +0100 Subject: [PATCH 136/152] feat: improve readme Issue #985 --- README.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0cf2e4e09..8b7a4407c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,5 @@ # Python Social Auth - Core -![Build Status](https://github.com/python-social-auth/social-core/workflows/Flake8/badge.svg) -![Build Status](https://github.com/python-social-auth/social-core/workflows/Tests/badge.svg) -[![Build Status](https://travis-ci.org/python-social-auth/social-core.svg?branch=master)](https://travis-ci.org/python-social-auth/social-core) -[![PyPI version](https://badge.fury.io/py/social-auth-core.svg)](https://badge.fury.io/py/social-auth-core) -[![Donate](https://img.shields.io/badge/Donate-PayPal-orange.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=matiasaguirre%40gmail%2ecom&lc=US&item_name=Python%20Social%20Auth&no_note=0¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_SM%2egif%3aNonHostedGuest) - Python Social Auth is an easy to setup social authentication/registration mechanism with support for several frameworks and auth providers. @@ -18,7 +12,7 @@ and storage solutions. ## Documentation -Project documentation is available at http://python-social-auth.readthedocs.org/. +Project documentation is available at https://python-social-auth.readthedocs.io/. ## Setup @@ -28,7 +22,11 @@ $ pip install social-auth-core ## Contributing -See the [CONTRIBUTING.md](CONTRIBUTING.md) document for details. +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 [https://github.com/python-social-auth/.github/blob/main/CONTRIBUTING.md](CONTRIBUTING.md) document for details. ## Versioning @@ -40,7 +38,7 @@ This project follows the BSD license. See the [LICENSE](LICENSE) for details. ## Donations -This project is maintained on my spare time, consider donating to keep -it improving. +This project welcomes donations to make the development sustainable, you can fund Python Social Auth on following platforms: -[![Donate](https://img.shields.io/badge/Donate-PayPal-orange.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=matiasaguirre%40gmail%2ecom&lc=US&item_name=Python%20Social%20Auth&no_note=0¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_SM%2egif%3aNonHostedGuest) +- [GitHub Sponsors](https://github.com/sponsors/python-social-auth/) +- [Open Collective](https://opencollective.com/python-social-auth) From 24231b3ab898dd1320cf330463834b2beb5a7624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 14:11:34 +0100 Subject: [PATCH 137/152] fix: use https link for semver --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b7a4407c..5aa0a0ef4 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ See the [https://github.com/python-social-auth/.github/blob/main/CONTRIBUTING.md ## Versioning -This project follows [Semantic Versioning 2.0.0](http://semver.org/spec/v2.0.0.html). +This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html). ## License From 6ac73254e854fc9c30b5fe9599dd32e7e589d8e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 14:29:11 +0100 Subject: [PATCH 138/152] feat(ci): turn pre-commit into reusable workflow See #984 --- .github/workflows/pre-commit-shared.yml | 27 +++++++++++++++++++++++++ .github/workflows/pre-commit.yml | 26 +++++------------------- 2 files changed, 32 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/pre-commit-shared.yml diff --git a/.github/workflows/pre-commit-shared.yml b/.github/workflows/pre-commit-shared.yml new file mode 100644 index 000000000..50268c149 --- /dev/null +++ b/.github/workflows/pre-commit-shared.yml @@ -0,0 +1,27 @@ +name: pre-commit check + +on: + workflow_call: + +jobs: + pre-commit: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml', 'requirements*.txt') }} + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + + - uses: astral-sh/setup-uv@v5 + + - run: uvx pre-commit run --all + env: + RUFF_OUTPUT_FORMAT: github diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 504e4008a..193d9812b 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -5,25 +5,9 @@ on: pull_request: jobs: + pre-commit-local: + uses: ./.github/workflows/pre-commit-shared.yml + if: github.repository == 'python-social-auth/core' pre-commit: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: actions/cache@v4 - with: - path: ~/.cache/pre-commit - key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: 3.x - - - uses: astral-sh/setup-uv@v5 - - - name: pre-commit (uvx) - run: uvx pre-commit run --all - env: - RUFF_OUTPUT_FORMAT: github + uses: python-social-auth/social-core/.github/workflows/pre-commit-shared.yml@master + if: github.repository != 'python-social-auth/core' From 834edb684a25cac1974904b214d1c108d6af9665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 14:35:17 +0100 Subject: [PATCH 139/152] fix: fix repo name --- .github/workflows/pre-commit.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 193d9812b..2dfd7aa3f 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -7,7 +7,7 @@ on: jobs: pre-commit-local: uses: ./.github/workflows/pre-commit-shared.yml - if: github.repository == 'python-social-auth/core' + if: github.repository == 'python-social-auth/social-core' pre-commit: uses: python-social-auth/social-core/.github/workflows/pre-commit-shared.yml@master - if: github.repository != 'python-social-auth/core' + if: github.repository != 'python-social-auth/social-core' From 773c729740b30abda935d97e171e210c97550cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 14:37:54 +0100 Subject: [PATCH 140/152] chore: use logging native formatting --- pyproject.toml | 3 +-- social_core/backends/cas.py | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 67f730878..f21e8c2d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,8 +147,7 @@ ignore = [ "TRY301", # TODO: Abstract `raise` to an inner function "BLE001", # TODO: Do not catch blind exception: `Exception` "S110", # TODO: `try`-`except`-`pass` detected, consider logging the exception - "TRY300", # TODO: Consider moving this statement to an `else` block - "G004" # TODO: Logging statement uses f-string + "TRY300" # TODO: Consider moving this statement to an `else` block ] select = ["ALL"] diff --git a/social_core/backends/cas.py b/social_core/backends/cas.py index 875e13b5c..1d9c69a6e 100644 --- a/social_core/backends/cas.py +++ b/social_core/backends/cas.py @@ -32,12 +32,12 @@ class CASOpenIdConnectAuth(OpenIdConnectAuth): def oidc_endpoint(self): endpoint = self.setting("OIDC_ENDPOINT", self.OIDC_ENDPOINT) - logger.debug(f"backend: CAS, endpoint: {endpoint}") + logger.debug("backend: CAS, endpoint: %s", endpoint) return endpoint def get_user_id(self, details, response): logger.debug( - f"backend: CAS, method: get_user_id, details: {details}, {response}" + "backend: CAS, method: get_user_id, details: %s, %s", details, response ) return details.get("username") @@ -45,12 +45,12 @@ def user_data(self, access_token, *args, **kwargs): data = self.get_json( self.userinfo_url(), headers={"Authorization": f"Bearer {access_token}"} ) - logger.debug(f"backend: CAS, user_data: {data}") + 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(f"backend: CAS, username_key: {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), From 9fe73f4902f8d00a03909b44c85e9d3f38e04ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 14:43:29 +0100 Subject: [PATCH 141/152] chore: drop 10 years deprecated interface I think 10 years of depreciation and getting a warning should be more than enough. --- CHANGELOG.md | 1 + pyproject.toml | 1 - social_core/storage.py | 6 ------ 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c8f6b95e..460f96ada 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - 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. ## [4.5.4](https://github.com/python-social-auth/social-core/releases/tag/4.5.4) - 2024-04-25 diff --git a/pyproject.toml b/pyproject.toml index f21e8c2d3..76f270926 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -142,7 +142,6 @@ ignore = [ "PLR1704", # TODO: Redefining argument with the local name "PERF203", # WONTFIX: This rule is only enforced for Python versions prior to 3.11 "ISC003", # TODO: Explicitly concatenated string should be implicitly concatenated - "B028", # TODO: No explicit `stacklevel` keyword argument found "PLW0603", # TODO: Using the global statement to update `BACKENDSCACHE` is discouraged "TRY301", # TODO: Abstract `raise` to an inner function "BLE001", # TODO: Do not catch blind exception: `Exception` diff --git a/social_core/storage.py b/social_core/storage.py index af4663472..979acab35 100644 --- a/social_core/storage.py +++ b/social_core/storage.py @@ -3,7 +3,6 @@ import base64 import re import uuid -import warnings from abc import abstractmethod from datetime import datetime, timedelta, timezone @@ -41,11 +40,6 @@ def access_token(self): """Return access_token stored in extra_data or None""" return self.extra_data.get("access_token") - @property - def tokens(self): - warnings.warn("tokens is deprecated, use access_token instead") - return self.access_token - def refresh_token(self, strategy, *args, **kwargs): token = self.extra_data.get("refresh_token") or self.extra_data.get( "access_token" From 5217ad9abb37efe1662bc6ed760973b92f4b7410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 15:04:08 +0100 Subject: [PATCH 142/152] chore: move the logic to the sync script --- .github/workflows/pre-commit.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 2dfd7aa3f..7d3e79f8c 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -5,9 +5,6 @@ on: pull_request: jobs: - pre-commit-local: + pre-commit: uses: ./.github/workflows/pre-commit-shared.yml if: github.repository == 'python-social-auth/social-core' - pre-commit: - uses: python-social-auth/social-core/.github/workflows/pre-commit-shared.yml@master - if: github.repository != 'python-social-auth/social-core' From c8e60d70a1ecb978c0a168d8cb50a4b57867d006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 15:07:55 +0100 Subject: [PATCH 143/152] fix: remove no longer needed condition --- .github/workflows/pre-commit.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 7d3e79f8c..1ad48b985 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -7,4 +7,3 @@ on: jobs: pre-commit: uses: ./.github/workflows/pre-commit-shared.yml - if: github.repository == 'python-social-auth/social-core' From 71bfcfea3792047e540cb9e158b678d0488ce9a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Fri, 10 Jan 2025 15:26:57 +0100 Subject: [PATCH 144/152] feat: make release flow reusable Fixes #984 --- .github/workflows/release-shared.yml | 56 ++++++++++++++++++++++++++++ .github/workflows/release.yml | 41 ++------------------ 2 files changed, 59 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/release-shared.yml diff --git a/.github/workflows/release-shared.yml b/.github/workflows/release-shared.yml new file mode 100644 index 000000000..7a7f91804 --- /dev/null +++ b/.github/workflows/release-shared.yml @@ -0,0 +1,56 @@ +name: Release + +on: + workflow_call: + inputs: + github_event_name: + required: true + type: string + +jobs: + release: + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + + - name: Verify tag is documented + if: inputs.github_event_name == 'release' + run: | + CURRENT_TAG=${GITHUB_REF#refs/tags/} + CURRENT_VERSION=$(sed -n 's/version = "\(.*\)"/\1/p' pyproject.toml) + if [ "${CURRENT_VERSION}" != "${CURRENT_TAG}" ]; then + echo "========================================================================" + echo "Error: tag '${CURRENT_TAG}' and version '${CURRENT_VERSION}' don't match" + echo "========================================================================" + exit 1; + fi + + - 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@v4 + if: inputs.github_event_name == 'release' + with: + name: dist + path: | + dist/*.tar.gz + dist/*.whl + + - run: uvx twine check dist/* + + - if: inputs.github_event_name == 'release' + run: uv publish --trusted-publishing always diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9256e94cf..c09e94150 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,43 +8,8 @@ on: jobs: release: - runs-on: ubuntu-latest + uses: ./.github/workflows/release-shared.yml + with: + github_event_name: ${{ github.event_name }} permissions: id-token: write - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 - - - name: Verify tag is documented - if: github.event_name == 'release' - run: | - CURRENT_TAG=${GITHUB_REF#refs/tags/} - CURRENT_VERSION=$(sed -n 's/version = "\(.*\)"/\1/p' pyproject.toml) - if [ "${CURRENT_VERSION}" != "${CURRENT_TAG}" ]; then - echo "========================================================================" - echo "Error: tag '${CURRENT_TAG}' and version '${CURRENT_VERSION}' don't match" - echo "========================================================================" - exit 1; - fi - - - name: Build dist - run: uv build - - - name: Archive dist - if: github.event_name == 'release' - uses: actions/upload-artifact@v4 - with: - name: dist - path: | - dist/*.tar.gz - dist/*.whl - - - name: Verify long description rendering - run: uvx twine check dist/* - - - name: Publish - env: - # TODO: remove once trusted publishing is configured - UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }} - if: github.event_name == 'release' && github.repository == 'python-social-auth/social-core' - run: uv publish From 43b8b7ea9ff4933e0f00d815a20ddfc9a93f9a6f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 21:48:01 +0000 Subject: [PATCH 145/152] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.1 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c34fd4d31..f4249737f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: pretty-format-json args: [--no-sort-keys, --autofix, --no-ensure-ascii] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.0 + rev: v0.9.1 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 138c2f15eb74265d553df3ba5a11aebd24e65fb6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Jan 2025 16:25:57 +0000 Subject: [PATCH 146/152] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.2 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f4249737f..b76edc69d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: pretty-format-json args: [--no-sort-keys, --autofix, --no-ensure-ascii] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.1 + rev: v0.9.2 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From b010c81852dc3cfb704a405f9508aac350b94981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 16 Jan 2025 19:48:00 +0100 Subject: [PATCH 147/152] feat(ci): enable dep updates automerge --- .github/renovate.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/renovate.json b/.github/renovate.json index 41c6edb7e..19a79f8b7 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -4,6 +4,10 @@ "config:recommended", ":dependencyDashboard" ], + "automerge": true, + "automergeType": "pr", + "automergeStrategy": "rebase", + "platformAutomerge": true, "pre-commit": { "enabled": true }, From d27f63c064e56164114676e04fd23415ea033bc6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 20:21:52 +0000 Subject: [PATCH 148/152] chore(deps): update pre-commit hook codespell-project/codespell to v2.4.0 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b76edc69d..7830be380 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: - mdformat-shfmt==0.2.0 - mdformat_tables==1.0.0 - repo: https://github.com/codespell-project/codespell - rev: v2.3.0 + rev: v2.4.0 hooks: - id: codespell additional_dependencies: From 8fdc85b9030f8e335583b083e2a79b02e066fbb9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jan 2025 23:12:07 +0000 Subject: [PATCH 149/152] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.3 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7830be380..9768cadc2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - id: pretty-format-json args: [--no-sort-keys, --autofix, --no-ensure-ascii] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.2 + rev: v0.9.3 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 67abf8f327d01f3c753c29f454c61f246a6feaf8 Mon Sep 17 00:00:00 2001 From: Dennis McGregor Date: Tue, 28 Jan 2025 02:20:09 +1300 Subject: [PATCH 150/152] feat: Log `HTTPError` response text (#1008) * Log HTTP error response * Add more detail to logged response Co-authored-by: Dennis McGregor --- social_core/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/social_core/utils.py b/social_core/utils.py index 74742a10c..c41b52f59 100644 --- a/social_core/utils.py +++ b/social_core/utils.py @@ -225,6 +225,12 @@ 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: From 7c6fa64398dca949d82463b7bb62648caa05ff2f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 21:02:16 +0000 Subject: [PATCH 151/152] chore(deps): update pre-commit hook codespell-project/codespell to v2.4.1 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9768cadc2..52128a3a6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: - mdformat-shfmt==0.2.0 - mdformat_tables==1.0.0 - repo: https://github.com/codespell-project/codespell - rev: v2.4.0 + rev: v2.4.1 hooks: - id: codespell additional_dependencies: From 5772e9bde00c518d76739b5da59b48b1663f4789 Mon Sep 17 00:00:00 2001 From: Rub21 Date: Wed, 29 Jan 2025 11:40:50 -0500 Subject: [PATCH 152/152] Replace openstreetmap -> openhistoricalmap --- social_core/backends/openstreetmap_oauth2.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/social_core/backends/openstreetmap_oauth2.py b/social_core/backends/openstreetmap_oauth2.py index 6df409273..c38993c32 100644 --- a/social_core/backends/openstreetmap_oauth2.py +++ b/social_core/backends/openstreetmap_oauth2.py @@ -16,8 +16,8 @@ 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" + AUTHORIZATION_URL = "https://www.openhistoricalmap.org/oauth2/authorize" + ACCESS_TOKEN_URL = "https://www.openhistoricalmap.org/oauth2/token" ACCESS_TOKEN_METHOD = "POST" SCOPE_SEPARATOR = " " STATE_PARAMETER = True @@ -45,7 +45,7 @@ def user_data(self, access_token, *args, **kwargs): headers = {"Authorization": f"Bearer {access_token}"} response = self.get_json( - url="https://api.openstreetmap.org/api/0.6/user/details.json", + url="https://api.openhistoricalmap.org/api/0.6/user/details.json", headers=headers, )