diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 210ce937..549657fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: services: postgres: - image: postgres:9.6 + image: postgres:13.4 ports: - 5432:5432 options: >- diff --git a/auth_backends/suomifi.py b/auth_backends/suomifi.py index dc7497dc..88a769b7 100644 --- a/auth_backends/suomifi.py +++ b/auth_backends/suomifi.py @@ -317,12 +317,13 @@ def create_logout_redirect(self, social_user, token=''): Token is used for tracking state.""" idp = self.get_idp('suomifi') auth = self._create_saml_auth(idp=idp) + extra_data = json.loads(social_user.extra_data) redirect = auth.logout(return_to=token, nq=idp.entity_id, - name_id=social_user.extra_data['name_id'], + name_id=extra_data['name_id'], name_id_format='urn:oasis:names:tc:SAML:2.0:nameid-format:transient', spnq=self.setting('SP_ENTITY_ID'), - session_index=social_user.extra_data['session_index']) + session_index=extra_data['session_index']) social_user.extra_data = {} social_user.save() return self.strategy.redirect(redirect) diff --git a/auth_backends/tests/conftest.py b/auth_backends/tests/conftest.py index fd2f345f..0514ebf9 100644 --- a/auth_backends/tests/conftest.py +++ b/auth_backends/tests/conftest.py @@ -33,10 +33,10 @@ class DummyOidcBackchannelLogoutBackend( def create_backend_logout_token(backend, **kwargs): kwargs.setdefault('iss', backend.oidc_config().get('issuer')) - kwargs.setdefault('sub', get_random_string()) + kwargs.setdefault('sub', get_random_string(15)) kwargs.setdefault('aud', backend.setting('KEY')) kwargs.setdefault('iat', int(time.time()) - 10) - kwargs.setdefault('jti', get_random_string()) + kwargs.setdefault('jti', get_random_string(15)) kwargs.setdefault('events', { 'http://schemas.openid.net/event/backchannel-logout': {}, }) diff --git a/auth_backends/tests/test_oidc_backchannel_logout.py b/auth_backends/tests/test_oidc_backchannel_logout.py index 678bf3b0..4d17cf42 100644 --- a/auth_backends/tests/test_oidc_backchannel_logout.py +++ b/auth_backends/tests/test_oidc_backchannel_logout.py @@ -227,7 +227,7 @@ def test_logout_token_extra_nonce( ): logout_token = logout_token_factory( backend, - nonce=get_random_string(), + nonce=get_random_string(15), ) backend.strategy.logout_token = logout_token @@ -245,7 +245,7 @@ def test_logout_token_no_social_auth( ): logout_token = logout_token_factory( backend, - sub=get_random_string(), + sub=get_random_string(15), ) backend.strategy.logout_token = logout_token @@ -269,7 +269,7 @@ def test_backchannel_logout_not_implemented_in_backend( reload_social_django_utils() - password = get_random_string() + password = get_random_string(15) user = user_factory(password=password) usersocialauth_factory(provider='dummyoidcbackend', user=user) @@ -310,7 +310,7 @@ def test_backchannel_successful_logout( reload_social_django_utils() - password = get_random_string() + password = get_random_string(15) user = user_factory(password=password) backend = DummyOidcBackchannelLogoutBackend() @@ -367,7 +367,7 @@ def test_backchannel_logout_no_social_auth( reload_social_django_utils() - password = get_random_string() + password = get_random_string(15) user = user_factory(password=password) backend = DummyOidcBackchannelLogoutBackend() @@ -416,9 +416,9 @@ def test_backchannel_successful_logout_other_session_unaffected( reload_social_django_utils() - password = get_random_string() + password = get_random_string(15) user = user_factory(password=password) - password2 = get_random_string() + password2 = get_random_string(15) user2 = user_factory(password=password2) backend = DummyOidcBackchannelLogoutBackend() diff --git a/docker-compose.yml b/docker-compose.yml index c2a30604..bc8d1033 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.7' services: postgres: - image: postgres:9.6-alpine + image: postgres:13.4-alpine environment: POSTGRES_USER: tunnistamo POSTGRES_PASSWORD: tunnistamo diff --git a/oidc_apis/scopes.py b/oidc_apis/scopes.py index e7be36d3..99b92041 100644 --- a/oidc_apis/scopes.py +++ b/oidc_apis/scopes.py @@ -1,3 +1,4 @@ +import json import re from django.utils.translation import gettext_lazy as _ @@ -142,6 +143,10 @@ def create_response_dic(self): social_user = UserSocialAuth.objects.get(user=self.user, provider='suomifi') except UserSocialAuth.DoesNotExist: return dic + + if isinstance(social_user.extra_data, str): + social_user.extra_data = json.loads(social_user.extra_data) + for level in SuomiFiAccessLevel.objects.all(): scope = 'suomifi_' + level.shorthand if scope in self.scopes: diff --git a/requirements-dev.txt b/requirements-dev.txt index 5a4f6004..35208bc8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,8 +8,6 @@ argparse==1.4.0 # via unittest2 -attrs==21.2.0 - # via pytest build==1.0.3 # via pip-tools click==8.1.7 @@ -38,8 +36,9 @@ linecache2==1.0.0 # via traceback2 mccabe==0.6.1 # via flake8 -packaging==21.0 +packaging==24.1 # via + # -c requirements.txt # build # pytest pip-tools==7.4.0 @@ -50,20 +49,18 @@ pycodestyle==2.8.0 # via flake8 pyflakes==2.4.0 # via flake8 -pyparsing==2.4.7 - # via packaging pyproject-hooks==1.0.0 # via # build # pip-tools -pytest==7.2.0 +pytest==7.4.4 # via # -r requirements-dev.in # pytest-cov # pytest-django pytest-cov==3.0.0 # via -r requirements-dev.in -pytest-django==4.4.0 +pytest-django==4.8.0 # via -r requirements-dev.in python-dateutil==2.8.2 # via diff --git a/requirements.in b/requirements.in index 1497f11b..850d7a4d 100644 --- a/requirements.in +++ b/requirements.in @@ -1,15 +1,15 @@ -django<4.0 +django<5.0 django-multiselectfield -django-oauth-toolkit +django-oauth-toolkit<=1.6.0 django-parler>2.1 django-cors-headers # Use our own fork of django-oidc-provider as long as the token extraction PR is not merged # https://github.com/juanifioren/django-oidc-provider/pull/389 -git+https://github.com/City-of-Helsinki/django-oidc-provider.git@745b7ebfabd568acc282ec0e8ac098f54ee933f9 +git+https://github.com/City-of-Helsinki/django-oidc-provider.git@f25cf7665eef59d15f14a8b2a8276ec955b5b73b djangorestframework>=3.10 django-helusers django-bootstrap3 -psycopg2 +psycopg2>2.8.3 --no-binary psycopg2 raven PyJWT[crypto] diff --git a/requirements.txt b/requirements.txt index 55426549..dda0802d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,8 +6,10 @@ # --no-binary psycopg2 -asgiref==3.4.1 +asgiref==3.8.1 # via django +cachetools==5.3.3 + # via django-helusers certifi==2023.7.22 # via requests cffi==1.14.4 @@ -18,7 +20,7 @@ coreapi==2.3.3 # via -r requirements.in coreschema==0.0.4 # via coreapi -cryptography==41.0.3 +cryptography==42.0.8 # via # -r requirements.in # jwcrypto @@ -29,26 +31,27 @@ defusedxml==0.5.0 # python3-openid # python3-saml # social-auth-core -deprecated==1.2.13 - # via jwcrypto -django==3.2.21 +deprecation==2.1.0 + # via django-helusers +django==4.2.13 # via # -r requirements.in # django-appconf + # django-bootstrap3 # django-cors-headers # django-crequest # django-filter # django-helusers # django-multiselectfield # django-oauth-toolkit + # django-parler # django-translation-checker # djangorestframework - # drf-oidc-auth django-appconf==1.0.5 # via django-compressor -django-bootstrap3==10.0.1 +django-bootstrap3==23.6 # via -r requirements.in -django-compressor==2.4.1 +django-compressor==3.0 # via -r requirements.in django-cors-headers==3.6.0 # via -r requirements.in @@ -58,7 +61,7 @@ django-environ==0.4.5 # via -r requirements.in django-filter==2.4.0 # via -r requirements.in -django-helusers==0.4.2 +django-helusers==0.13.0 # via -r requirements.in django-ipware==2.1.0 # via -r requirements.in @@ -66,24 +69,20 @@ django-multiselectfield==0.1.12 # via -r requirements.in django-npm==1.0.0 # via -r requirements.in -django-oauth-toolkit==1.2.0 +django-oauth-toolkit==1.6.0 # via -r requirements.in -django-oidc-provider @ git+https://github.com/City-of-Helsinki/django-oidc-provider.git@745b7ebfabd568acc282ec0e8ac098f54ee933f9 +django-oidc-provider @ git+https://github.com/City-of-Helsinki/django-oidc-provider.git@f25cf7665eef59d15f14a8b2a8276ec955b5b73b # via -r requirements.in -django-parler==2.2 +django-parler==2.3 # via # -r requirements.in # django-translation-checker -django-sass-processor==1.1 +django-sass-processor==1.4.1 # via -r requirements.in django-translation-checker @ git+https://github.com/City-of-Helsinki/django-translation-checker.git@master # via -r requirements.in -djangorestframework==3.12.4 - # via - # -r requirements.in - # drf-oidc-auth -drf-oidc-auth==0.9 - # via django-helusers +djangorestframework==3.15.2 + # via -r requirements.in ecdsa==0.14.1 # via python-jose future==0.18.3 @@ -98,8 +97,10 @@ itypes==1.1.0 # via coreapi jinja2==2.11.3 # via coreschema -jwcrypto==1.4.2 - # via -r requirements.in +jwcrypto==1.5.6 + # via + # -r requirements.in + # django-oauth-toolkit libsass==0.21.0 # via -r requirements.in lxml==4.9.1 @@ -108,16 +109,18 @@ markupsafe==1.1.0 # via jinja2 maxminddb==1.5.1 # via geoip2 -oauthlib==2.1.0 +oauthlib==3.2.2 # via # django-oauth-toolkit # requests-oauthlib # social-auth-core -pillow==9.5.0 +packaging==24.1 + # via deprecation +pillow==10.4.0 # via -r requirements.in polib==1.1.0 # via django-translation-checker -psycopg2==2.8.3 +psycopg2==2.9.9 # via -r requirements.in pyasn1==0.4.5 # via @@ -128,9 +131,7 @@ pycparser==2.18 pycryptodomex==3.15.0 # via pyjwkest pyjwkest==1.4.0 - # via - # django-oidc-provider - # drf-oidc-auth + # via django-oidc-provider pyjwt==2.4.0 # via # -r requirements.in @@ -138,6 +139,7 @@ pyjwt==2.4.0 python-jose==3.3.0 # via # -r requirements.in + # django-helusers # social-auth-core python3-openid==3.1.0 # via social-auth-core @@ -145,11 +147,9 @@ python3-saml==1.9.0 # via # -r requirements.in # social-auth-core -pytz==2018.4 - # via django raven==6.9.0 # via -r requirements.in -rcssmin==1.0.6 +rcssmin==1.1.0 # via django-compressor requests==2.31.0 # via @@ -163,7 +163,7 @@ requests==2.31.0 # social-auth-core requests-oauthlib==1.0.0 # via social-auth-core -rjsmin==1.1.0 +rjsmin==1.2.0 # via django-compressor rsa==4.7.2 # via python-jose @@ -173,24 +173,25 @@ ruamel-yaml-clib==0.2.6 # via ruamel-yaml six==1.16.0 # via - # django-compressor # ecdsa # isodate # libsass # pyjwkest -social-auth-app-django==5.0.0 +social-auth-app-django==5.1.0 # via -r requirements.in social-auth-core==4.1.0 # via # -r requirements.in # social-auth-app-django -sqlparse==0.4.4 +sqlparse==0.5.0 # via django +typing-extensions==4.12.2 + # via + # asgiref + # jwcrypto uritemplate==3.0.0 # via coreapi -urllib3==1.26.6 +urllib3==2.2.2 # via requests -wrapt==1.14.1 - # via deprecated xmlsec==1.3.10 # via python3-saml diff --git a/tunnistamo/api_common.py b/tunnistamo/api_common.py index 43385093..3894f4ed 100644 --- a/tunnistamo/api_common.py +++ b/tunnistamo/api_common.py @@ -1,8 +1,8 @@ import datetime import json import logging +from zoneinfo import ZoneInfo -import pytz from django.conf import settings from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured @@ -18,7 +18,7 @@ User = get_user_model() logger = logging.getLogger(__name__) -local_tz = pytz.timezone(settings.TIME_ZONE) +local_tz = ZoneInfo(settings.TIME_ZONE) def parse_scope(scope): diff --git a/tunnistamo/middleware.py b/tunnistamo/middleware.py index 91e925b2..5a788eec 100644 --- a/tunnistamo/middleware.py +++ b/tunnistamo/middleware.py @@ -1,14 +1,13 @@ import json import logging from datetime import timedelta -from urllib.parse import parse_qsl, urlencode, urlsplit +from urllib.parse import parse_qsl, quote, urlencode, urlsplit from django.conf import settings from django.http import HttpResponseForbidden, HttpResponseRedirect from django.middleware.locale import LocaleMiddleware as DjangoLocaleMiddleware from django.urls import reverse from django.utils import timezone, translation -from django.utils.http import quote from django.utils.translation import gettext_lazy as _ from oidc_provider.lib.errors import BearerTokenError from social_core.exceptions import AuthException diff --git a/tunnistamo/settings.py b/tunnistamo/settings.py index caba73dd..ff595fc8 100644 --- a/tunnistamo/settings.py +++ b/tunnistamo/settings.py @@ -157,8 +157,6 @@ 'users', 'oidc_provider', - 'django.contrib.admin', - 'social_django', 'rest_framework', @@ -168,7 +166,8 @@ 'crequest', 'django_filters', - 'helusers', + 'helusers.apps.HelusersConfig', + 'helusers.apps.HelusersAdminConfig', 'yletunnus', 'hkijwt', diff --git a/tunnistamo/social_auth_urls.py b/tunnistamo/social_auth_urls.py index 3a5542aa..d6f284c2 100644 --- a/tunnistamo/social_auth_urls.py +++ b/tunnistamo/social_auth_urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import re_path +from django.urls import re_path from social_django import views app_name = 'social' diff --git a/tunnistamo/test_settings.py b/tunnistamo/test_settings.py index a1e6c85f..28812991 100644 --- a/tunnistamo/test_settings.py +++ b/tunnistamo/test_settings.py @@ -1,6 +1,6 @@ from .settings import * -ALLOWED_HOSTS = '*' +ALLOWED_HOSTS = ['*'] ### # Suomi.fi test configuration diff --git a/tunnistamo/tests/conftest.py b/tunnistamo/tests/conftest.py index 693144aa..9274fddd 100644 --- a/tunnistamo/tests/conftest.py +++ b/tunnistamo/tests/conftest.py @@ -80,7 +80,7 @@ def social_login(settings, test_client=None, trust_loa=True): 'backend': DummyFixedOidcBackend.name }) - state_value = get_random_string() + state_value = get_random_string(15) session = test_client.session session[f'{DummyFixedOidcBackend.name}_state'] = state_value session.save() @@ -97,7 +97,7 @@ def create_oidc_clients_and_api(): oidc_client = OidcClient.objects.create( name='Test Client', client_id='test_client', - client_secret=get_random_string(), + client_secret=get_random_string(15), require_consent=False, _scope='profile token_introspection' ) @@ -153,7 +153,7 @@ def get_tokens(test_client, oidc_client, response_type, scopes=None, fetch_token 'scope': ' '.join(scopes), 'response_type': response_type, 'response_mode': 'form_post', - 'nonce': get_random_string(), + 'nonce': get_random_string(15), } response = test_client.get(authorize_url, authorize_request_data, follow=False) diff --git a/tunnistamo/tests/test_auth_error_redirect.py b/tunnistamo/tests/test_auth_error_redirect.py index 3da38685..292315e1 100644 --- a/tunnistamo/tests/test_auth_error_redirect.py +++ b/tunnistamo/tests/test_auth_error_redirect.py @@ -43,7 +43,7 @@ def test_redirect_to_client_after_social_auth_error( django_client = CancelExampleComRedirectClient(backend_name=HelsinkiTunnistus.name) # Start the OIDC flow. - state = get_random_string() + state = get_random_string(15) oidc_client = start_oidc_authorize( django_client, oidcclient_factory, diff --git a/tunnistamo/tests/test_oidc_provider_idp_hint.py b/tunnistamo/tests/test_oidc_provider_idp_hint.py index 98a694d4..8fbb13e8 100644 --- a/tunnistamo/tests/test_oidc_provider_idp_hint.py +++ b/tunnistamo/tests/test_oidc_provider_idp_hint.py @@ -27,7 +27,7 @@ def test_idp_hint_is_kept_when_redirecting_to_login_view(client, user, is_authen 'scope': 'openid', 'response_type': response_type, 'response_mode': 'form_post', - 'nonce': get_random_string(), + 'nonce': get_random_string(15), 'idp_hint': idp_hint_value, } if prompt: diff --git a/tunnistamo/tests/test_oidc_provider_pkce.py b/tunnistamo/tests/test_oidc_provider_pkce.py index 1679bfcc..4640d854 100644 --- a/tunnistamo/tests/test_oidc_provider_pkce.py +++ b/tunnistamo/tests/test_oidc_provider_pkce.py @@ -11,7 +11,7 @@ @pytest.mark.django_db @pytest.mark.parametrize('code_verifier,should_succeed', ( ('', False), - pytest.param(get_random_string(), True, id='random_string'), + pytest.param(get_random_string(15), True, id='random_string'), )) def test_token_endpoint_requires_code_verifier( client, @@ -32,7 +32,7 @@ def test_token_endpoint_requires_code_verifier( tunnistamo_session = tunnistamosession_factory(user=user) oidc_client = oidcclient_factory(redirect_uris=['https://example.com/callback']) - nonce = get_random_string() + nonce = get_random_string(15) code_challenge = urlsafe_b64encode( hashlib.sha256(code_verifier.encode('ascii')).digest() ).decode('utf-8').replace('=', '') diff --git a/tunnistamo/tests/test_suomifi_authentication.py b/tunnistamo/tests/test_suomifi_authentication.py index 0322db9c..9d85910a 100644 --- a/tunnistamo/tests/test_suomifi_authentication.py +++ b/tunnistamo/tests/test_suomifi_authentication.py @@ -202,9 +202,10 @@ def test_suomifi_login_response(django_client, django_user_model): assert user.first_name == 'Teppo' assert user.last_name == 'Testi' social_user = UserSocialAuth.objects.get(user=user) - assert social_user.extra_data['suomifi_attributes']['nationalIdentificationNumber'] == '010101-0101' - assert social_user.extra_data['suomifi_attributes']['cn'] == 'Testi Teppo' - assert social_user.extra_data['suomifi_attributes']['KotikuntaKuntaS'] == 'Muuala' + extra_data = json.loads(social_user.extra_data) + assert extra_data['suomifi_attributes']['nationalIdentificationNumber'] == '010101-0101' + assert extra_data['suomifi_attributes']['cn'] == 'Testi Teppo' + assert extra_data['suomifi_attributes']['KotikuntaKuntaS'] == 'Muuala' @pytest.mark.django_db diff --git a/tunnistamo/tests/test_tunnistamo_session_checks.py b/tunnistamo/tests/test_tunnistamo_session_checks.py index 62eb7521..8fd70b26 100644 --- a/tunnistamo/tests/test_tunnistamo_session_checks.py +++ b/tunnistamo/tests/test_tunnistamo_session_checks.py @@ -42,7 +42,7 @@ def test_authorize_endpoint(user, response_type, ended): 'scope': 'openid profile', 'response_type': response_type, 'response_mode': 'form_post', - 'nonce': get_random_string(), + 'nonce': get_random_string(15), } response = django_test_client.get(authorize_url, authorize_request_data, follow=False) diff --git a/users/migrations/0020_populate_cors_allowed_origins.py b/users/migrations/0020_populate_cors_allowed_origins.py index 23535eca..c6a64f95 100644 --- a/users/migrations/0020_populate_cors_allowed_origins.py +++ b/users/migrations/0020_populate_cors_allowed_origins.py @@ -17,6 +17,7 @@ class Migration(migrations.Migration): dependencies = [ ('users', '0019_allowedorigin'), + ('users', '0034_application_algorithm_and_more'), ] operations = [ diff --git a/users/migrations/0034_application_algorithm_and_more.py b/users/migrations/0034_application_algorithm_and_more.py new file mode 100644 index 00000000..996f0741 --- /dev/null +++ b/users/migrations/0034_application_algorithm_and_more.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.13 on 2024-07-04 13:39 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0019_allowedorigin'), # this needs to be run before 0020_populate_cors_allowed_origins.py + ] + + operations = [ + migrations.AddField( + model_name="application", + name="algorithm", + field=models.CharField( + blank=True, + choices=[ + ("", "No OIDC support"), + ("RS256", "RSA with SHA-2 256"), + ("HS256", "HMAC with SHA-2 256"), + ], + default="", + max_length=5, + ), + ), + migrations.AlterField( + model_name="application", + name="authorization_grant_type", + field=models.CharField( + choices=[ + ("authorization-code", "Authorization code"), + ("implicit", "Implicit"), + ("password", "Resource owner password-based"), + ("client-credentials", "Client credentials"), + ("openid-hybrid", "OpenID connect hybrid"), + ], + max_length=32, + ), + ), + migrations.AlterField( + model_name="application", + name="user", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(app_label)s_%(class)s", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/users/tests/conftest.py b/users/tests/conftest.py index 2c909982..be6b4c28 100644 --- a/users/tests/conftest.py +++ b/users/tests/conftest.py @@ -56,9 +56,9 @@ def user_factory(): User = get_user_model() # NOQA def make_instance(**kwargs): - kwargs.setdefault('username', get_random_string()) - kwargs.setdefault('password', get_random_string()) - kwargs.setdefault('email', u'{}@example.com'.format(get_random_string())) + kwargs.setdefault('username', get_random_string(15)) + kwargs.setdefault('password', get_random_string(15)) + kwargs.setdefault('email', u'{}@example.com'.format(get_random_string(15))) instance = User.objects.create(**kwargs) instance.set_password(kwargs.pop('password')) @@ -72,7 +72,7 @@ def make_instance(**kwargs): @pytest.fixture() def usersocialauth_factory(): def make_instance(**kwargs): - kwargs.setdefault('uid', get_random_string()) + kwargs.setdefault('uid', get_random_string(15)) return UserSocialAuth.objects.create(**kwargs) @@ -82,8 +82,8 @@ def make_instance(**kwargs): @pytest.fixture() def application_factory(): def make_instance(**kwargs): - kwargs.setdefault('name', get_random_string()) - kwargs.setdefault('client_id', get_random_string()) + kwargs.setdefault('name', get_random_string(15)) + kwargs.setdefault('client_id', get_random_string(15)) kwargs.setdefault('user', None) kwargs.setdefault('redirect_uris', '') kwargs.setdefault('client_type', Application.CLIENT_PUBLIC) @@ -97,9 +97,9 @@ def make_instance(**kwargs): @pytest.fixture() def oidcclient_factory(): def make_instance(**kwargs): - kwargs.setdefault('name', get_random_string()) + kwargs.setdefault('name', get_random_string(15)) kwargs.setdefault('client_type', 'public') - kwargs.setdefault('client_id', get_random_string()) + kwargs.setdefault('client_id', get_random_string(15)) kwargs.setdefault('redirect_uris', list()) response_types = kwargs.pop('response_types', ['id_token token']) @@ -126,7 +126,7 @@ def make_instance(**kwargs): def loginmethod_factory(): def make_instance(**kwargs): kwargs.setdefault('provider_id', None) - kwargs.setdefault('name', get_random_string()) + kwargs.setdefault('name', get_random_string(15)) kwargs.setdefault('order', 1) return LoginMethod.objects.create(**kwargs) @@ -221,15 +221,15 @@ class DummyOidcBackend2(DummyOidcBackendBase): def create_id_token(backend, **kwargs): kwargs.setdefault('iss', backend.oidc_config().get('issuer')) - kwargs.setdefault('sub', get_random_string()) + kwargs.setdefault('sub', get_random_string(15)) kwargs.setdefault('aud', backend.setting('KEY')) kwargs.setdefault('azp', backend.setting('KEY')) kwargs.setdefault('exp', int(time.time()) + 60 * 5) kwargs.setdefault('iat', int(time.time()) - 10) - kwargs.setdefault('jti', get_random_string()) - kwargs.setdefault('name', get_random_string()) - kwargs.setdefault('given_name', get_random_string()) - kwargs.setdefault('family_name', get_random_string()) + kwargs.setdefault('jti', get_random_string(15)) + kwargs.setdefault('name', get_random_string(15)) + kwargs.setdefault('given_name', get_random_string(15)) + kwargs.setdefault('family_name', get_random_string(15)) keys = [] for rsakey in RSAKey.objects.all(): @@ -269,7 +269,7 @@ def user_data(self, access_token, *args, **kwargs): def get_json(self, url, *args, **kwargs): if url == self.oidc_config()['token_endpoint']: - nonce = self.get_and_store_nonce(self.authorization_url(), get_random_string()) + nonce = self.get_and_store_nonce(self.authorization_url(), get_random_string(15)) id_token = create_id_token( self, nonce=nonce, diff --git a/users/tests/test_login_view.py b/users/tests/test_login_view.py index 09b480ea..11bcfa5f 100644 --- a/users/tests/test_login_view.py +++ b/users/tests/test_login_view.py @@ -71,7 +71,7 @@ def test_login_view_ignore_unknown_app(client, loginmethod_factory, application_ loginmethod_factory(provider_id='facebook') params = { - "next": "http://example.com/?client_id={}".format(get_random_string()), + "next": "http://example.com/?client_id={}".format(get_random_string(15)), } response = client.get('/login/', params) diff --git a/users/tests/test_logout_view.py b/users/tests/test_logout_view.py index 1cf88e11..dcfce467 100644 --- a/users/tests/test_logout_view.py +++ b/users/tests/test_logout_view.py @@ -12,7 +12,7 @@ def link_to_url_found_in_response(response, url): @pytest.mark.django_db def test_logout(client, user_factory): - password = get_random_string() + password = get_random_string(15) user = user_factory(password=password) client.login(username=user.username, password=password) @@ -54,7 +54,7 @@ def test_logout_redirect_next(client, user_factory, _next, expected, application @pytest.mark.parametrize('_next', ( None, '', - pytest.param(get_random_string(), id='random_string'), + pytest.param(get_random_string(15), id='random_string'), 12345, '//example.com', '/foo', @@ -83,7 +83,7 @@ def test_logout_redirect_next_authenticated(client, user_factory, application_fa app = application_factory(post_logout_redirect_uris='http://example.com/', redirect_uris=['http://example.com/']) app.save() - password = get_random_string() + password = get_random_string(15) user = user_factory(password=password) client.login(username=user.username, password=password) @@ -109,7 +109,7 @@ def test_logout_redirect_to_third_party_oidc_end_session( ) settings.SOCIAL_AUTH_DUMMYOIDCBACKEND_REDIRECT_LOGOUT_TO_END_SESSION = redirect_enabled_in_backend - password = get_random_string() + password = get_random_string(15) user = user_factory(password=password) usersocialauth_factory(provider='dummyoidcbackend', user=user) @@ -140,7 +140,7 @@ def test_logout_redirect_to_third_party_oidc_end_session_with_id_token_hint( ) settings.SOCIAL_AUTH_DUMMYOIDCBACKEND_REDIRECT_LOGOUT_TO_END_SESSION = True - password = get_random_string() + password = get_random_string(15) user = user_factory(password=password) usersocialauth_factory( @@ -179,7 +179,7 @@ def test_logout_redirect_to_third_party_oidc_end_session_retain_post_logout_redi redirect_uris=['https://example.com/'] ) - password = get_random_string() + password = get_random_string(15) user = user_factory(password=password) usersocialauth_factory(provider='dummyoidcbackend', user=user) @@ -221,7 +221,7 @@ def test_logout_redirect_to_two_third_party_oidc_end_sessions( redirect_uris=['https://example.com/'] ) - password = get_random_string() + password = get_random_string(15) user = user_factory(password=password) usersocialauth_factory(provider='dummyoidcbackend', user=user) diff --git a/users/tests/test_models.py b/users/tests/test_models.py index ecccd5f9..3de8d6ed 100644 --- a/users/tests/test_models.py +++ b/users/tests/test_models.py @@ -10,7 +10,7 @@ from django.urls import reverse from django.utils.crypto import get_random_string from django.utils.timezone import now -from oauth2_provider.admin import Grant +from oauth2_provider.models import Grant from oidc_provider.models import Code, RSAKey from services.models import Service diff --git a/users/tests/test_pipeline.py b/users/tests/test_pipeline.py index ac599898..76830c84 100644 --- a/users/tests/test_pipeline.py +++ b/users/tests/test_pipeline.py @@ -26,7 +26,7 @@ def dummy_backend(settings): def request_social_complete(client): - state_value = get_random_string() + state_value = get_random_string(15) session = client.session session[f'{DummyFixedOidcBackend.name}_state'] = state_value session.save() diff --git a/users/tests/test_tunnistamo_authorize_view.py b/users/tests/test_tunnistamo_authorize_view.py index 59a969f5..8c15afd1 100644 --- a/users/tests/test_tunnistamo_authorize_view.py +++ b/users/tests/test_tunnistamo_authorize_view.py @@ -161,7 +161,7 @@ def test_original_client_id_is_passed_to_helsinki_tunnistus_authentication_servi settings.SOCIAL_AUTH_HELTUNNISTUSSUOMIFI_OIDC_ENDPOINT = 'https://heltunnistussuomifi.example.com' django_client = CancelExampleComRedirectClient() - state = get_random_string() + state = get_random_string(15) oidc_client = start_oidc_authorize( django_client, oidcclient_factory, @@ -182,7 +182,7 @@ def test_ui_locales_parameter_of_authorize_request_is_passed_to_helsinki_tunnist settings.SOCIAL_AUTH_HELTUNNISTUSSUOMIFI_OIDC_ENDPOINT = 'https://heltunnistussuomifi.example.com' django_client = CancelExampleComRedirectClient() - state = get_random_string() + state = get_random_string(15) ui_locales = "this can be whatever" start_oidc_authorize( django_client, @@ -242,7 +242,7 @@ def test_kc_action_is_passed_to_helsinki_tunnistus_authentication_service( settings.SOCIAL_AUTH_HELTUNNISTUSSUOMIFI_OIDC_ENDPOINT = 'https://heltunnistussuomifi.example.com' django_client = CancelExampleComRedirectClient() - state = get_random_string() + state = get_random_string(15) kc_action = "UPDATE_PASSWORD" start_oidc_authorize( django_client, diff --git a/users/tests/test_tunnistamo_session.py b/users/tests/test_tunnistamo_session.py index b272d3c4..90e94c5f 100644 --- a/users/tests/test_tunnistamo_session.py +++ b/users/tests/test_tunnistamo_session.py @@ -150,8 +150,8 @@ def test_tunnistamo_session_add_element_cannot_add_non_model(user, tunnistamoses def _create_request_with_anonymous_user(rf): request = rf.get('/') - SessionMiddleware().process_request(request) - AuthenticationMiddleware().process_request(request) + SessionMiddleware(get_response=lambda response: response).process_request(request) + AuthenticationMiddleware(get_response=lambda response: response).process_request(request) request.session.save() return request diff --git a/users/views.py b/users/views.py index 07d32a07..2c1ae53b 100644 --- a/users/views.py +++ b/users/views.py @@ -7,7 +7,7 @@ from django.conf import settings from django.db.models import Case, Value, When from django.http import HttpResponse, JsonResponse -from django.shortcuts import redirect +from django.shortcuts import redirect, resolve_url from django.urls import reverse from django.utils import translation from django.views.decorators.http import require_http_methods @@ -243,6 +243,38 @@ def _validate_client_uri(self, uri): return uri in (u for uri_text in uri_texts for u in _process_uris(uri_text)) + def get_default_redirect_url(self): + """Return the default redirect URL from GET or POST params or settings. + + Since django 4.1 there were new mixin class for LogoutView and also + get method was overwritten with post method so this new logic will handle + the redirect urls. + """ + if self.next_page: + return resolve_url(self.next_page) + elif settings.LOGOUT_REDIRECT_URL: + return resolve_url(settings.LOGOUT_REDIRECT_URL) + else: + url_with_logout_redirect = self._get_url_with_logout_redirect_uri() + + return url_with_logout_redirect or self.request.path + + def _get_url_with_logout_redirect_uri(self): + # If no backend, there should be no redirecting. + if not self.backend: + return self.request.get_full_path() + + # Get the redirect uri from the request for the response. + logout_redirect_uri = self.request.GET.get( + "post_logout_redirect_uri", + self.request.POST.get("post_logout_redirect_uri", None), + ) + + if logout_redirect_uri: + params = {"post_logout_redirect_uri": logout_redirect_uri} + encoded_params = urlencode(params) + return f"{self.request.path}?{encoded_params}" + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['post_logout_redirect_uri'] = self.post_logout_redirect_uri @@ -290,7 +322,7 @@ def get_active_social_users(self, user): return UserSocialAuth.objects.none() return UserSocialAuth.objects.filter(user=user).order_by('-modified') - def get_oidc_backends_end_session_url(self, request, social_users): + def get_oidc_backends_end_session_url(self, request, social_users): # noqa: C901 """Return end session url of the first OIDC backend the user has a social auth entry on and hasn't been redirected yet. @@ -338,6 +370,8 @@ def get_oidc_backends_end_session_url(self, request, social_users): # Add id_token_hint to the end session url if the id_token is available # in the extra_data + if isinstance(social_user.extra_data, str): + social_user.extra_data = json.loads(social_user.extra_data) id_token = social_user.extra_data.get('id_token') if id_token: end_session_parameters['id_token_hint'] = id_token