diff --git a/.pylintrc b/.pylintrc index 9b4a084..6d4e262 100644 --- a/.pylintrc +++ b/.pylintrc @@ -60,97 +60,24 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". -disable=apply-builtin, - backtick, - bad-inline-option, - bad-python3-import, - basestring-builtin, - buffer-builtin, - cmp-builtin, - cmp-method, - coerce-builtin, - coerce-method, - comprehension-escape, - delslice-method, - deprecated-itertools-function, - deprecated-operator-function, +disable=bad-inline-option, deprecated-pragma, - deprecated-str-translate-call, - deprecated-string-function, - deprecated-sys-function, - deprecated-types-field, - deprecated-urllib-function, - dict-items-not-iterating, - dict-iter-method, - dict-keys-not-iterating, - dict-values-not-iterating, - dict-view-method, - div-method, - eq-without-hash, - exception-escape, - exception-message-attribute, - execfile-builtin, - file-builtin, file-ignored, - filter-builtin-not-iterating, - getslice-method, - hex-method, - idiv-method, - import-star-module-level, - indexing-exception, - input-builtin, - intern-builtin, - invalid-str-codec, locally-disabled, - long-builtin, - long-suffix, - map-builtin-not-iterating, - metaclass-assignment, missing-class-docstring, missing-function-docstring, missing-module-docstring, - next-method-called, - next-method-defined, - no-absolute-import, - non-ascii-bytes-literal, - nonzero-method, - oct-method, - old-division, - old-ne-operator, - old-octal-literal, - old-raise-syntax, - parameter-unpacking, - raising-string, - range-builtin-not-iterating, raw-checker-failed, - raw_input-builtin, - rdiv-method, - reduce-builtin, - reload-builtin, - round-builtin, - setslice-method, - standarderror-builtin, suppressed-message, - sys-max-int, too-few-public-methods, - unichr-builtin, - unicode-builtin, - unpacking-in-except, use-symbolic-message-instead, useless-suppression, - using-cmp-argument, - xrange-builtin, - xreadlines-attribute, - zip-builtin-not-iterating, - # aka Wrong hanging indentation: Pylint disagrees with black, as seen in https://github.com/psf/black/issues/48 - C0330, # these below are supposed to be dealt with at some point... abstract-method, arguments-differ, broad-except, fixme, invalid-name, - no-self-use, protected-access, super-with-arguments, too-many-arguments, @@ -379,13 +306,6 @@ max-line-length=120 # Maximum number of lines in a module. max-module-lines=1000 -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no @@ -604,5 +524,5 @@ min-public-methods=2 # Exceptions that will emit a warning when being caught. Defaults to # "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception +overgeneral-exceptions=builtins.BaseException, + builtins.Exception diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dd81942 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,38 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) + + +## [4.3] - 2025-04-04 +### Added +- `get_nonce` method on `AuthenticationViewMixin` that can be overridden to provide custom nonce +- `login` method on `AuthenticationViewMixin` that allows to invoke django `login` while suppressing +session key rotation. It is recommended to use this method to avoid `Invalid authentication session` error +on mobile devices with Smart ID app running on same device as the browser running your webapp; however, +you need to consider security risks. +- Changelog + +### Changed +- Upgrade `esteid-helper` to version [0.6.0](https://github.com/thorgate/esteid-helper/releases/tag/0.6.0) +that supports retry on network error +- Persist authentication session state after session completion, including authentication result details. +This allows to execute multiple `patch` requests within the same session, which is useful for example in case +of a network error that causes the FE to never receive the finalization response from BE. +- Persist signing session state after session completion (but not the temporary containers), for same purpose +as with authentication sessions above. + +### Fixed +- Use unreleased version of `oscrypto`, dependency of pyasice, that fixes compatibility with some OpenSSL versions, +for tests. If you are getting `oscrypto.errors.LibraryNotFoundError: Error detecting the version of libcrypto` +in your project you will need to fix your dependencies as well. +- Coverage report including legacy compatibility code scheduled for removal (pragma: no cover not applying properly) +- Exception messages being discarded and default exception message always being used + +## [4.3] - 2024-12-31 +### Added +- Allow to provide custom random bytes to authenticate + +[4.3]: https://github.com/olivierlacan/keep-a-changelog/compare/v4.2...v4.3 +[4.2]: https://github.com/olivierlacan/keep-a-changelog/compare/v4.1...v4.2 diff --git a/esteid/actions.py b/esteid/actions.py index 06a211e..2c52329 100644 --- a/esteid/actions.py +++ b/esteid/actions.py @@ -1,4 +1,3 @@ -# pragma: no cover import base64 import binascii import logging diff --git a/esteid/authentication/authenticator.py b/esteid/authentication/authenticator.py index e90ed21..39e6585 100644 --- a/esteid/authentication/authenticator.py +++ b/esteid/authentication/authenticator.py @@ -2,7 +2,7 @@ from time import time from typing import Dict, Optional, Type -from esteid.authentication.types import AuthenticationResult, SessionData +from esteid.authentication.types import AuthenticationResult, SessionData, Status from esteid.exceptions import EsteidError, SigningSessionDoesNotExist, SigningSessionExists @@ -31,6 +31,10 @@ class Authenticator: AUTHENTICATION_METHODS: Dict[str, Type["Authenticator"]] = {} + # For some authentication mechanisms that involve talking to the backend, django session needs + # needs to be set before the authentication process starts. + DJANGO_SESSION_IS_NEEDED = False + _SESSION_KEY = f"{__name__}.session" # timeout in seconds, after which a fresh session can be started even if old session data is present. @@ -73,28 +77,35 @@ def setup(self, initial_data: dict = None): # Session management. # NOTE: this part is largely a copy-paste from signing. - def save_session_data(self, *, session_id, hash_value_b64): + @classmethod + def clean_session_data(cls): """ - Saves the session data between initialization and polling requests + Creates a new session data object. """ - session_data = self.session_data or SessionData() - - session_data.session_id = session_id - session_data.hash_value_b64 = hash_value_b64 - session_data.timestamp = int(time()) + return SessionData(status=Status.PENDING, result=None) - self.session[self._SESSION_KEY] = dict(session_data) + def save_session_data(self): + """ + Saves the session data between initialization and polling requests + """ + self.session_data.timestamp = int(time()) + self.session[self._SESSION_KEY] = dict(self.session_data) def load_session_data(self, session) -> SessionData: try: session_data = session[self._SESSION_KEY] except KeyError: - session_data = {} + return self.clean_session_data() try: session_data = SessionData(session_data) - except TypeError: - session_data = SessionData() + if getattr(session_data, "result", None) is not None: + session_data.result = AuthenticationResult(session_data.result) + session_data.result.is_valid() + session_data.is_valid() + except (ValueError, TypeError): + logging.exception("Invalid session data %r found, cleaning it up.", dict(session_data)) + session_data = self.clean_session_data() self._cleanup_session(session) # Not doing session data validation here, because @@ -116,11 +127,11 @@ def __init__(self, session, initial=False, origin=None): except AttributeError: timestamp = 0 - if time() < timestamp + self.SESSION_VALIDITY_TIMEOUT: + if time() < timestamp + self.SESSION_VALIDITY_TIMEOUT and session_data.status == Status.PENDING: raise SigningSessionExists("Another authentication session already in progress") - # session expired => create a fresh data store - session_data = SessionData() + # session expired or is complete => create a fresh data store + session_data = self.clean_session_data() # wipe the old data from session. self._cleanup_session(session) diff --git a/esteid/authentication/tests/test_authenticator.py b/esteid/authentication/tests/test_authenticator.py index d30bb1a..ca982e6 100644 --- a/esteid/authentication/tests/test_authenticator.py +++ b/esteid/authentication/tests/test_authenticator.py @@ -4,13 +4,18 @@ import pytest from esteid.authentication import Authenticator -from esteid.authentication.types import SessionData +from esteid.authentication.types import SessionData, Status from esteid.exceptions import EsteidError, SigningSessionDoesNotExist, SigningSessionExists @pytest.fixture() def test_session_data(): - return SessionData(timestamp=11111, session_id="test", hash_value_b64="MQ==") # a very old timestamp + return SessionData( + timestamp=11111, # a very old timestamp + session_id="test", + hash_value_b64="MQ==", + status=Status.PENDING, + ) @patch.object(Authenticator, "AUTHENTICATION_METHODS", {}) @@ -45,7 +50,7 @@ def test_authenticator_init__initial_true(test_session_data): session = {} authenticator = Authenticator(session, initial=True) - assert authenticator.session_data == {} + assert authenticator.session_data == authenticator.clean_session_data() assert authenticator.session is session assert session == {} @@ -55,7 +60,7 @@ def test_authenticator_init__initial_true(test_session_data): session = {Authenticator._SESSION_KEY: wrong_data} authenticator = Authenticator(session, initial=True) - assert authenticator.session_data == {} + assert authenticator.session_data == authenticator.clean_session_data() assert authenticator.session is session assert session == {} @@ -63,20 +68,50 @@ def test_authenticator_init__initial_true(test_session_data): session = {Authenticator._SESSION_KEY: {**test_session_data}} authenticator = Authenticator(session, initial=True) - assert authenticator.session_data == {} + assert authenticator.session_data == authenticator.clean_session_data() assert authenticator.session is session assert session == {} - # Some (unvalidated) session data present, not expired => error - session = {Authenticator._SESSION_KEY: {"timestamp": int(time()), "key": "value"}} - with pytest.raises(SigningSessionExists): - Authenticator(session, initial=True) + # Some invalid session data present, session is reset + session = { + Authenticator._SESSION_KEY: { + "timestamp": int(time()), + "key": "value", + } + } + authenticator = Authenticator(session, initial=True) + assert authenticator.session_data == authenticator.clean_session_data() + assert authenticator.session is session + assert session == {} # Correct session data present, not expired => error - session = {Authenticator._SESSION_KEY: {**test_session_data, "timestamp": int(time()), "key": "value"}} + session = { + Authenticator._SESSION_KEY: { + "timestamp": int(time()), + "status": Status.PENDING, + "session_id": "test", + "result": None, + "hash_value_b64": "hash", + } + } with pytest.raises(SigningSessionExists): Authenticator(session, initial=True) + # Correct session data present, not expired but not pending anymore => success, session is reset + session = { + Authenticator._SESSION_KEY: { + "timestamp": int(time()), + "status": Status.SUCCESS, + "session_id": "test", + "result": None, + "hash_value_b64": "hash", + } + } + authenticator = Authenticator(session, initial=True) + assert authenticator.session_data == authenticator.clean_session_data() + assert authenticator.session is session + assert session == {} + def test_authenticator_init__initial_false(test_session_data): # Wrong data: empty diff --git a/esteid/authentication/types.py b/esteid/authentication/types.py index d3450b0..a39a31d 100644 --- a/esteid/authentication/types.py +++ b/esteid/authentication/types.py @@ -1,6 +1,17 @@ +import typing as t +from http import HTTPStatus + from esteid.types import PredictableDict +class AuthenticationResult(PredictableDict): + country: str + id_code: str + given_name: str + surname: str + certificate_b64: str + + class SessionData(PredictableDict): """ Wrapper for temporary data stored between authentication polling requests. @@ -12,11 +23,21 @@ class SessionData(PredictableDict): timestamp: int session_id: str hash_value_b64: str + status: str + result: t.Optional[AuthenticationResult] -class AuthenticationResult(PredictableDict): - country: str - id_code: str - given_name: str - surname: str - certificate_b64: str +class Status: + ERROR = "error" + PENDING = "pending" + SUCCESS = "success" + CANCELLED = "cancelled" + + @classmethod + def http_status_for_status(cls, status: str) -> HTTPStatus: + return { + cls.ERROR: HTTPStatus.GONE, + cls.PENDING: HTTPStatus.ACCEPTED, + cls.SUCCESS: HTTPStatus.OK, + cls.CANCELLED: HTTPStatus.CONFLICT, + }.get(status, HTTPStatus.INTERNAL_SERVER_ERROR) diff --git a/esteid/authentication/views.py b/esteid/authentication/views.py index 2d2ea20..2c0d8b3 100644 --- a/esteid/authentication/views.py +++ b/esteid/authentication/views.py @@ -1,6 +1,8 @@ import logging -from typing import Type, TYPE_CHECKING +from http import HTTPStatus +from typing import Optional, Type, TYPE_CHECKING +from django.contrib.auth import HASH_SESSION_KEY, login, SESSION_KEY from django.http import HttpRequest, JsonResponse from esteid.exceptions import ActionInProgress @@ -41,6 +43,10 @@ class AuthenticationViewMixin(SessionViewMixin): authentication_method: str = None authenticator: Type[Authenticator] = None + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._authenticator_instance: Optional[Authenticator] = None + def on_auth_success(self, request, data: AuthenticationResult): """ A hook to make use of the authentication data once the process is complete. @@ -49,6 +55,24 @@ def on_auth_success(self, request, data: AuthenticationResult): """ pass + @classmethod + def login(cls, request, user, backend=None): + # This should prevent session key rotation in login. Key rotation must be prevented, as in some cases + # the request where authentication actually happens will never be delivered to FE due to network error. + # + # esteid-helper retries in this case, but if session cookie was changed and lost there is nothing + # we can do. + # + # See condition in login() + if request.session.get(SESSION_KEY) is None: + request.session[SESSION_KEY] = user.pk + session_auth_hash = "" + if hasattr(user, "get_session_auth_hash"): + session_auth_hash = user.get_session_auth_hash() + if session_auth_hash: + request.session[HASH_SESSION_KEY] = session_auth_hash + login(request, user, backend) + def success_response(self, request, data: AuthenticationResult): """Customizable response on success""" return JsonResponse({**data, "status": self.Status.SUCCESS}) @@ -58,56 +82,96 @@ def select_authenticator_class(self) -> Type["Authenticator"]: return self.authenticator return Authenticator.select_authenticator(self.authentication_method) + def dispatch(self, request, *args, **kwargs): + try: + if request.session.session_key is None and self.select_authenticator_class().DJANGO_SESSION_IS_NEEDED: + return JsonResponse( + { + "status": self.Status.ERROR, + "error": "DjangoSessionHasChanged", + # This error message is unlikely to reach the end user and is more for a developer, + # esteid-helper will retry on Gone status + "message": "Unable to log you in, likely due to network error. Please try again", + # If you are a developer reading this, you need to check login() method and possibly + # override it to ensure that the session key doesn't get cycled. + # + # This happens when key is cycled but due to network error updated session cookie is + # not delivered to the FE and FE keeps using the old session key. + }, + status=HTTPStatus.GONE, + ) + except AttributeError: + pass + + try: + return super().dispatch(request, *args, **kwargs) + finally: + if self._authenticator_instance is not None: + if self._authenticator_instance.session_data.is_valid(raise_exception=False): + self._authenticator_instance.save_session_data() + + def handle_user_cancel(self): + if self._authenticator_instance is not None: + self._authenticator_instance.session_data.status = self.Status.CANCELLED + + def handle_error(self): + if self._authenticator_instance is not None: + self._authenticator_instance.session_data.status = self.Status.ERROR + + def get_nonce(self, request) -> Optional[bytes]: + return None + def start_session(self, request: "RequestType", *args, **kwargs): """ Initiates an authentication session. """ auth_class = self.select_authenticator_class() - authenticator = auth_class.start_session(request.session, request.data, origin=get_origin(request)) - - do_cleanup = True + self._authenticator_instance = auth_class.start_session( + request.session, request.data, origin=get_origin(request) + ) try: - result = authenticator.authenticate() - + self._authenticator_instance.session_data.result = self._authenticator_instance.authenticate( + random_bytes=self.get_nonce(request) + ) except ActionInProgress as e: - do_cleanup = False # return SUCCESS to indicate that the upstream service successfully accepted the request return JsonResponse({"status": self.Status.SUCCESS, **e.data}, status=e.status) - else: - # Handle a theoretical case of immediate authentication - self.on_auth_success(request, result) - return JsonResponse({**result, "status": self.Status.SUCCESS}) - - finally: - if do_cleanup: - authenticator.cleanup() + # Handle a theoretical case of immediate authentication + self.on_auth_success(request, self._authenticator_instance.session_data.result) + self._authenticator_instance.session_data.status = self.Status.SUCCESS + return self.success_response(request, self._authenticator_instance.session_data.result) def finish_session(self, request: "RequestType", *args, **kwargs): """ Checks the status of an authentication session """ authenticator_class = self.select_authenticator_class() - authenticator = authenticator_class.load_session(request.session, origin=get_origin(request)) - - do_cleanup = True + self._authenticator_instance = authenticator_class.load_session(request.session, origin=get_origin(request)) + + if ( + self._authenticator_instance.session_data.status != self.Status.PENDING + and self._authenticator_instance.session_data.result is not None + ): + # Return cached data, if available + return JsonResponse( + { + "status": self._authenticator_instance.session_data.status, + **self._authenticator_instance.session_data.result, + }, + status=self.Status.http_status_for_status(self._authenticator_instance.session_data.status), + ) try: - result = authenticator.poll(request.data) - + self._authenticator_instance.session_data.result = self._authenticator_instance.poll(request.data) except ActionInProgress as e: - do_cleanup = False return JsonResponse({"status": self.Status.PENDING, **e.data}, status=e.status) - else: - self.on_auth_success(request, result) - return self.success_response(request, result) - - finally: - if do_cleanup: - authenticator.cleanup() + self.on_auth_success(request, self._authenticator_instance.session_data.result) + self._authenticator_instance.session_data.status = self.Status.SUCCESS + return self.success_response(request, self._authenticator_instance.session_data.result) def handle_delete_request(self, request): authenticator_class = self.select_authenticator_class() diff --git a/esteid/exceptions.py b/esteid/exceptions.py index d6db526..da31bcd 100644 --- a/esteid/exceptions.py +++ b/esteid/exceptions.py @@ -25,11 +25,12 @@ class EsteidError(Exception): kwargs: dict def __init__(self, message=None, **kwargs): + self.message = message or self.default_message super().__init__(message) self.kwargs = kwargs def get_message(self): - return str(self.default_message).format(**self.kwargs) + return str(self.message).format(**self.kwargs) def get_user_error(self): return { diff --git a/esteid/flowtest/signer.py b/esteid/flowtest/signer.py index 4d7a87a..3a9fee8 100644 --- a/esteid/flowtest/signer.py +++ b/esteid/flowtest/signer.py @@ -11,7 +11,9 @@ def prepare(self, container: pyasice.Container = None, files: List[DataFile] = N container = self.open_container(container, files) xml_sig = pyasice.XmlSignature.create() - self.save_session_data(digest=b"test", container=container, xml_sig=xml_sig) + self.set_container(container=container, xml_sig=xml_sig) + self.session_data.digest = b"test" + self.save_session_data() return {"verification_code": "1234"} diff --git a/esteid/flowtest/tests/test_authentication_flow.py b/esteid/flowtest/tests/test_authentication_flow.py index b75f011..0045b71 100644 --- a/esteid/flowtest/tests/test_authentication_flow.py +++ b/esteid/flowtest/tests/test_authentication_flow.py @@ -9,7 +9,7 @@ from django.test import Client from django.urls import reverse -from esteid.authentication.types import AuthenticationResult +from esteid.authentication.types import AuthenticationResult, Status from esteid.flowtest.views import AuthTestView from esteid.mobileid.i18n import TranslatedMobileIDService from esteid.mobileid.types import AuthenticateResult as MobileIdAuthInitResult @@ -133,7 +133,7 @@ def test_auth_flow_smartid( **auth_result, } - assert SmartIdAuthenticator._SESSION_KEY not in session, "Failed to clean up session" + assert session[SmartIdAuthenticator._SESSION_KEY]["status"] != Status.PENDING, "Failed to finalize session" @pytest.mark.parametrize( @@ -180,4 +180,4 @@ def test_auth_flow_mobileid( **auth_result, } - assert SmartIdAuthenticator._SESSION_KEY not in session, "Failed to clean up session" + assert session[SmartIdAuthenticator._SESSION_KEY]["status"] != Status.PENDING, "Failed to finalize session" diff --git a/esteid/flowtest/tests/test_signing_flow.py b/esteid/flowtest/tests/test_signing_flow.py index 12463a7..3eb8338 100644 --- a/esteid/flowtest/tests/test_signing_flow.py +++ b/esteid/flowtest/tests/test_signing_flow.py @@ -11,6 +11,7 @@ from django.test import Client from django.urls import reverse +from esteid.authentication.types import Status from esteid.flowtest.views import SigningTestView from esteid.signing import Signer @@ -56,7 +57,7 @@ def test_my_signing_flow(urlconf, content_type, datafiles): "status": SigningTestView.Status.SUCCESS, } - assert Signer._SESSION_KEY not in session, "Failed to clean up session" + assert session[Signer._SESSION_KEY]["status"] != Status.PENDING, "Failed to finalize session" assert not os.path.exists(temp_container_file), "Failed to clean up files" @@ -101,5 +102,5 @@ def test_my_post_signing_flow(urlconf, content_type, datafiles): assert response.json() == { "status": SigningTestView.Status.SUCCESS, } - assert Signer._SESSION_KEY not in session, "Failed to clean up session" + assert session[Signer._SESSION_KEY]["status"] != Status.PENDING, "Failed to finalize session" assert not os.path.exists(temp_container_file), "Failed to clean up files" diff --git a/esteid/idcard/authenticator.py b/esteid/idcard/authenticator.py index c58d975..0f9d9d4 100644 --- a/esteid/idcard/authenticator.py +++ b/esteid/idcard/authenticator.py @@ -49,10 +49,9 @@ def authenticate(self, random_bytes=None): hash_value = generate_hash(self.hash_type, random_bytes) hash_value_b64 = base64.b64encode(hash_value).decode() - self.save_session_data( - session_id=uuid.uuid4().hex, - hash_value_b64=hash_value_b64, - ) + self.session_data.session_id = uuid.uuid4().hex + self.session_data.hash_value_b64 = hash_value_b64 + self.save_session_data() raise ActionInProgress( data={ diff --git a/esteid/idcard/signer.py b/esteid/idcard/signer.py index 22170cc..3ea8e80 100644 --- a/esteid/idcard/signer.py +++ b/esteid/idcard/signer.py @@ -63,7 +63,9 @@ def prepare(self, container: pyasice.Container = None, files: List[DataFile] = N # Note: uses default digest algorithm (sha256) signed_digest = xml_sig.digest() - self.save_session_data(digest=signed_digest, container=container, xml_sig=xml_sig) + self.set_container(container=container, xml_sig=xml_sig) + self.session_data.digest = signed_digest + self.save_session_data() return { # hex-encoded digest to be consumed by the web-eid.js library diff --git a/esteid/idcard/tests/test_idcardsigner.py b/esteid/idcard/tests/test_idcardsigner.py index bc40910..2f9a3ac 100644 --- a/esteid/idcard/tests/test_idcardsigner.py +++ b/esteid/idcard/tests/test_idcardsigner.py @@ -6,6 +6,7 @@ import pytest +from esteid.authentication.types import Status from esteid.exceptions import InvalidParameter from esteid.idcard import IdCardSigner from esteid.idcard import signer as signer_module @@ -15,7 +16,9 @@ def idcardsigner(): signer = IdCardSigner({}, initial=True) mock_container = Mock(name="mock_container") - with patch.object(signer, "open_container", return_value=mock_container), patch.object(signer, "save_session_data"): + with patch.object(signer, "open_container", return_value=mock_container), patch.object( + signer, "save_session_data" + ), patch.object(signer, "set_container"): mock_container.prepare_signature.return_value = mock_xml_sig = Mock(name="mock_xml_sig") mock_xml_sig.digest.return_value = b"some binary digest" @@ -32,6 +35,7 @@ def idcard_session_data(): "temp_signature_file": f.name, "temp_container_file": "...", "timestamp": int(time()), + "status": Status.SUCCESS, } ) os.remove(f.name) @@ -66,11 +70,9 @@ def test_idcardsigner_prepare(idcardsigner, static_certificate): xml_sig = idcardsigner.open_container().prepare_signature(...) assert xml_sig._mock_name == "mock_xml_sig" - idcardsigner.save_session_data.assert_called_once_with( - digest=xml_sig.digest(), - container=container, - xml_sig=xml_sig, - ) + idcardsigner.set_container.assert_called_once_with(container=container, xml_sig=xml_sig) + idcardsigner.save_session_data.assert_called_once_with() + assert idcardsigner.session_data.digest == xml_sig.digest() assert result == {"digest": base64.b64encode(xml_sig.digest()).decode()} diff --git a/esteid/mixins.py b/esteid/mixins.py index eafd62f..d353d1a 100644 --- a/esteid/mixins.py +++ b/esteid/mixins.py @@ -7,6 +7,7 @@ from django.http import Http404, HttpRequest, JsonResponse, QueryDict from django.utils.translation import gettext +from esteid.authentication.types import Status from esteid.exceptions import CanceledByUser, EsteidError, InvalidParameters @@ -37,11 +38,8 @@ class SessionViewMixin: Also does common error handling. """ - class Status: - ERROR = "error" - PENDING = "pending" - SUCCESS = "success" - CANCELLED = "cancelled" + class Status(Status): + pass start_session: Callable finish_session: Callable @@ -52,6 +50,9 @@ def report_error(self, e: EsteidError): def handle_user_cancel(self): pass + def handle_error(self): + pass + def handle_errors(self, e: Exception, stage="start"): if isinstance(e, EsteidError): if isinstance(e, CanceledByUser): @@ -66,6 +67,7 @@ def handle_errors(self, e: Exception, stage="start"): raise e if isinstance(e, DjangoValidationError): + self.handle_error() return JsonResponse( {"status": self.Status.ERROR, "error": e.__class__.__name__, "message": str(e)}, status=HTTPStatus.CONFLICT, diff --git a/esteid/mobileid/authenticator.py b/esteid/mobileid/authenticator.py index 19f8c3c..e283d75 100644 --- a/esteid/mobileid/authenticator.py +++ b/esteid/mobileid/authenticator.py @@ -18,6 +18,8 @@ class MobileIdAuthenticator(Authenticator): id_code: str language: str + DJANGO_SESSION_IS_NEEDED = True + def setup(self, initial_data: dict = None): """ Receives user input via POST: `id_code`, `phone_number`, `language` @@ -46,9 +48,9 @@ def authenticate(self, random_bytes=None): self.id_code, self.phone_number, language=self.language, random_bytes=random_bytes ) - self.save_session_data( - session_id=auth_initial_result.session_id, hash_value_b64=auth_initial_result.hash_value_b64 - ) + self.session_data.session_id = auth_initial_result.session_id + self.session_data.hash_value_b64 = auth_initial_result.hash_value_b64 + self.save_session_data() raise ActionInProgress( data={ diff --git a/esteid/mobileid/signer.py b/esteid/mobileid/signer.py index 6170f6f..d7452cf 100644 --- a/esteid/mobileid/signer.py +++ b/esteid/mobileid/signer.py @@ -52,13 +52,10 @@ def prepare(self, container=None, files: List[DataFile] = None) -> dict: xml_sig = container.prepare_signature(certificate) sign_session = service.sign(self.id_code, self.phone_number, xml_sig.signed_data(), language=self.language) - - self.save_session_data( - digest=sign_session.digest, - container=container, - xml_sig=xml_sig, - session_id=sign_session.session_id, - ) + self.session_data.digest = sign_session.digest + self.session_data.session_id = sign_session.session_id + self.set_container(container=container, xml_sig=xml_sig) + self.save_session_data() return { "verification_code": sign_session.verification_code, @@ -90,9 +87,3 @@ def finalize(self, data: dict = None) -> Container: container.add_signature(xml_sig) return container - - def save_session_data(self, *, digest: bytes, container: Container, xml_sig: XmlSignature, session_id: str): - data_obj = self.session_data - data_obj.session_id = session_id - - super().save_session_data(digest=digest, container=container, xml_sig=xml_sig) diff --git a/esteid/mobileid/tests/test_mobileidsigner.py b/esteid/mobileid/tests/test_mobileidsigner.py index 1291f30..4093da1 100644 --- a/esteid/mobileid/tests/test_mobileidsigner.py +++ b/esteid/mobileid/tests/test_mobileidsigner.py @@ -2,20 +2,28 @@ import os from tempfile import NamedTemporaryFile from time import time -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest from esteid.mobileid import MobileIdSigner from esteid.mobileid import signer as signer_module +from ...authentication.types import Status from ...exceptions import InvalidIdCode, InvalidParameter, InvalidParameters, SigningSessionDoesNotExist +from ..types import SignResult @pytest.fixture() def mobileidsigner(): signer = MobileIdSigner({}, initial=True) - with patch.object(signer, "open_container"), patch.object(signer, "save_session_data"): + with patch.object(signer, "open_container") as open_container, patch.object(signer, "save_session_data"): + open_container().prepare_signature().signed_data = Mock(return_value=b"signature signature") + open_container().prepare_signature().dump = Mock(return_value=b"") + open_container().finalize().getbuffer = Mock(return_value=b"buffer buffer") + open_container().prepare_signature().reset_mock() + open_container().finalize().reset_mock() + open_container.reset_mock() yield signer @@ -29,6 +37,7 @@ def mobileid_session_data(): "temp_container_file": "...", "session_id": "...", "timestamp": int(time()), + "status": Status.SUCCESS, } os.remove(f.name) @@ -36,6 +45,18 @@ def mobileid_session_data(): @pytest.fixture() def mobileidservice(): with patch.object(signer_module, "TranslatedMobileIDService") as service_cls: + service_cls.get_instance().sign = Mock( + return_value=SignResult( + "session_id", + b"digest", + 1234, + ) + ) + service_cls.get_instance().get_certificate = Mock( + return_value=b"certificate certificate", + ) + service_cls.get_instance().reset_mock() + service_cls.reset_mock() yield service_cls @@ -99,6 +120,7 @@ def test_mobileidsigner_setup(data, error): "temp_container_file": "b", "session_id": "c", "timestamp": int(time()), + "status": Status.SUCCESS, }, None, id="Good session data", @@ -141,12 +163,11 @@ def test_mobileidsigner_prepare(mobileidsigner, MID_DEMO_PHONE_EE_OK, MID_DEMO_P sign_session = service.sign(...) - mobileidsigner.save_session_data.assert_called_once_with( - digest=sign_session.digest, - container=container, - xml_sig=xml_sig, - session_id=sign_session.session_id, - ) + mobileidsigner.save_session_data.assert_called_once_with() + assert mobileidsigner.session_data.digest == sign_session.digest + assert mobileidsigner.session_data.session_id == sign_session.session_id + assert mobileidsigner.session_data.temp_signature_file + assert mobileidsigner.session_data.temp_container_file assert result["verification_code"] == sign_session.verification_code diff --git a/esteid/signing/signer.py b/esteid/signing/signer.py index ad05769..37e549a 100644 --- a/esteid/signing/signer.py +++ b/esteid/signing/signer.py @@ -13,6 +13,7 @@ from esteid.exceptions import EsteidError, SigningSessionDoesNotExist, SigningSessionExists from esteid.util import get_request_session_method +from ..authentication.types import Status from .types import DataFile, InterimSessionData @@ -95,26 +96,30 @@ def setup(self, initial_data: dict = None): """Customize this to receive and check any data prior to `prepare()`""" pass - def save_session_data(self, *, digest: bytes, container: Container, xml_sig: XmlSignature): + @classmethod + def clean_session_data(cls): """ - Saves the interim session data along with a timestamp that is used to determine session validity. - - Can be extended to accept additional arguments + Creates a new session data object. """ - data_obj = self.session_data - - data_obj.digest = digest - data_obj.timestamp = int(time()) + return cls.SessionData(status=Status.PENDING) + def set_container(self, *, container: Container, xml_sig: XmlSignature): with NamedTemporaryFile(delete=False) as temp_signature_file: temp_signature_file.write(xml_sig.dump()) - data_obj.temp_signature_file = temp_signature_file.name + self.session_data.temp_signature_file = temp_signature_file.name with NamedTemporaryFile("wb", delete=False) as temp_container_file: temp_container_file.write(container.finalize().getbuffer()) - data_obj.temp_container_file = temp_container_file.name + self.session_data.temp_container_file = temp_container_file.name - self.session[self._SESSION_KEY] = dict(data_obj) + def save_session_data(self): + """ + Saves the interim session data along with a timestamp that is used to determine session validity. + + Can be extended to accept additional arguments + """ + self.session_data.timestamp = int(time()) + self.session[self._SESSION_KEY] = dict(self.session_data) # Methods that probably do not need overriding @@ -122,12 +127,13 @@ def load_session_data(self, session) -> InterimSessionData: try: session_data = session[self._SESSION_KEY] except KeyError: - session_data = {} + session_data = self.clean_session_data() try: session_data = self.SessionData(session_data) - except TypeError: - session_data = self.SessionData() + session_data.is_valid() + except (ValueError, TypeError): + session_data = self.clean_session_data() self._cleanup_session(session) # Not doing session data validation here, because @@ -149,11 +155,11 @@ def __init__(self, session, initial=False): except AttributeError: timestamp = 0 - if time() < timestamp + self.SESSION_VALIDITY_TIMEOUT: + if time() < timestamp + self.SESSION_VALIDITY_TIMEOUT and session_data.status == Status.PENDING: raise SigningSessionExists("Another signing session already in progress") # session expired => create a fresh data store - session_data = self.SessionData() + session_data = self.clean_session_data() # clear the old session data. This incurs no DB overhead: # Django issues the actual DB query only in the process_response phase. @@ -173,11 +179,11 @@ def __init__(self, session, initial=False): self.session = session self.session_data = session_data - def cleanup(self): + def cleanup(self, *, delete_session=True): """ Cleans temporary signing session data and files. """ - return self._cleanup_session(self.session) + return self._cleanup_session(self.session, delete_session=delete_session) @classmethod def start_session(cls, session, initial_data) -> "Signer": @@ -196,8 +202,12 @@ def load_session(cls, session) -> "Signer": return cls(session, initial=False) @classmethod - def _cleanup_session(cls, session): - data = session.pop(cls._SESSION_KEY, None) + def _cleanup_session(cls, session, *, delete_session=True): + data = session.get(cls._SESSION_KEY, None) + + if delete_session: + session.pop(cls._SESSION_KEY, None) + if not data: return diff --git a/esteid/signing/tests/test_signer.py b/esteid/signing/tests/test_signer.py index ccae8c9..d839523 100644 --- a/esteid/signing/tests/test_signer.py +++ b/esteid/signing/tests/test_signer.py @@ -3,6 +3,7 @@ import pytest +from esteid.authentication.types import Status from esteid.exceptions import EsteidError, SigningSessionDoesNotExist, SigningSessionExists from esteid.signing import Signer @@ -14,6 +15,7 @@ def test_session_data(): timestamp=11111, temp_container_file="temp_container_file", temp_signature_file="temp_signature_file", + status=Status.PENDING, ) @@ -49,7 +51,7 @@ def test_signer_init__initial_true(test_session_data): session = {} signer = Signer(session, initial=True) - assert signer.session_data == {} + assert signer.session_data == Signer.clean_session_data() assert signer.session is session assert session == {} @@ -59,7 +61,7 @@ def test_signer_init__initial_true(test_session_data): session = {Signer._SESSION_KEY: wrong_data} signer = Signer(session, initial=True) - assert signer.session_data == {} + assert signer.session_data == Signer.clean_session_data() assert signer.session is session assert session == {} @@ -67,20 +69,32 @@ def test_signer_init__initial_true(test_session_data): session = {Signer._SESSION_KEY: dict(test_session_data)} signer = Signer(session, initial=True) - assert signer.session_data == {} + assert signer.session_data == Signer.clean_session_data() assert signer.session is session assert session == {} - # Some (unvalidated) session data present, not expired => error + # Some (unvalidated) session data present, session is reset session = {Signer._SESSION_KEY: {"timestamp": int(time()), "key": "value"}} - with pytest.raises(SigningSessionExists): - Signer(session, initial=True) + signer = Signer(session, initial=True) + assert signer.session_data == Signer.clean_session_data() + assert signer.session is session + assert session == {} # Correct session data present, not expired => error - session = {Signer._SESSION_KEY: {**test_session_data, "timestamp": int(time()), "key": "value"}} + session = {Signer._SESSION_KEY: {**test_session_data, "timestamp": int(time())}} with pytest.raises(SigningSessionExists): Signer(session, initial=True) + # Correct session data present, not expired but not pending anymore => success, session is reset + session_data = Signer.SessionData(**test_session_data) + session_data.status = Status.SUCCESS + session_data.timestamp = int(time()) + session = {Signer._SESSION_KEY: session_data} + signer = Signer(session, initial=True) + assert signer.session_data == Signer.clean_session_data() + assert signer.session is session + assert session == {} + def test_signer_init__initial_false(test_session_data): # Wrong data: empty session diff --git a/esteid/signing/tests/test_types.py b/esteid/signing/tests/test_types.py index 90432af..6c496b8 100644 --- a/esteid/signing/tests/test_types.py +++ b/esteid/signing/tests/test_types.py @@ -5,6 +5,7 @@ from django.core.files import File +from ...authentication.types import Status from ..types import DataFile, InterimSessionData @@ -36,12 +37,24 @@ def test_txt(tmp_path): id="empty dict", ), pytest.param( - {"digest_b64": "", "temp_signature_file": "", "temp_container_file": "", "timestamp": 0}, + { + "digest_b64": "", + "temp_signature_file": "", + "temp_container_file": "", + "timestamp": 0, + "status": Status.PENDING, + }, None, id="empty values", ), pytest.param( - {"digest_b64": "dGVzdA==", "temp_signature_file": "asdf", "temp_container_file": "asdf", "timestamp": 0}, + { + "digest_b64": "dGVzdA==", + "temp_signature_file": "asdf", + "temp_container_file": "asdf", + "timestamp": 0, + "status": Status.PENDING, + }, None, id="non-empty values", ), diff --git a/esteid/signing/tests/test_views.py b/esteid/signing/tests/test_views.py index 080263e..eb28b66 100644 --- a/esteid/signing/tests/test_views.py +++ b/esteid/signing/tests/test_views.py @@ -16,6 +16,7 @@ UpstreamServiceError, UserNotRegistered, ) +from esteid.signing.signer import Signer from esteid.types import Signer as SignerData from ..views import SignViewMixin @@ -34,6 +35,8 @@ def signer_class(): signer_class = Mock(name="signer class") signer_class.start_session.return_value = signer_class() signer_class.load_session.return_value = signer_class() + signer_class().session_data = Signer.clean_session_data() + signer_class.reset_mock() return signer_class diff --git a/esteid/signing/types.py b/esteid/signing/types.py index 0bd1815..29db705 100644 --- a/esteid/signing/types.py +++ b/esteid/signing/types.py @@ -62,6 +62,7 @@ class InterimSessionData(PredictableDict): temp_container_file: str temp_signature_file: str timestamp: int + status: str @property def digest(self): diff --git a/esteid/signing/views.py b/esteid/signing/views.py index 3c1fef8..6bdcc5f 100644 --- a/esteid/signing/views.py +++ b/esteid/signing/views.py @@ -1,5 +1,5 @@ import logging -from typing import BinaryIO, Type, TYPE_CHECKING, Union +from typing import BinaryIO, Optional, Type, TYPE_CHECKING, Union from django.http import HttpRequest, JsonResponse @@ -38,6 +38,10 @@ class SignViewMixin(SessionViewMixin): signer_class: Type[Signer] = None signing_method: str = None + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._signer_instance: Optional[Signer] = None + def get_container(self, *args, **kwargs) -> Union[str, BinaryIO, pyasice.Container]: """ Returns [path to|file handle of] the container to sign, if it exists prior to signing @@ -103,13 +107,31 @@ def select_signer_class(self) -> Type["Signer"]: return self.signer_class return Signer.select_signer(self.signing_method) + def dispatch(self, request, *args, **kwargs): + try: + return super().dispatch(request, *args, **kwargs) + finally: + if self._signer_instance is not None and self._signer_instance.session_data.is_valid(raise_exception=False): + self._signer_instance.save_session_data() + + def handle_user_cancel(self): + if self._signer_instance is not None: + self._signer_instance.session_data.status = self.Status.CANCELLED + + def handle_error(self): + if self._signer_instance is not None: + self._signer_instance.session_data.status = self.Status.ERROR + + def get_nonce(self, request) -> Optional[bytes]: + return None + def start_session(self, request: "RequestType", *args, **kwargs): """ Initiates a signing session """ signer_class = self.select_signer_class() - signer = signer_class.start_session(request.session, request.data) + self._signer_instance = signer_class.start_session(request.session, request.data) try: container = self.get_container(*args, **kwargs) @@ -126,9 +148,9 @@ def start_session(self, request: "RequestType", *args, **kwargs): else: container = pyasice.Container(container) - self.check_eligibility(signer, container) + self.check_eligibility(self._signer_instance, container) - response_to_user = signer.prepare(container, files_to_sign) + response_to_user = self._signer_instance.prepare(container, files_to_sign) return JsonResponse({**response_to_user, "status": self.Status.SUCCESS}) @@ -137,21 +159,31 @@ def finish_session(self, request: "RequestType", *args, **kwargs): Checks the status of a signing session and attempts to finalize signing """ signer_class = self.select_signer_class() - signer = signer_class.load_session(request.session) + self._signer_instance = signer_class.load_session(request.session) + + if self._signer_instance.session_data.status != self.Status.PENDING: + # Return cached data, if available + return JsonResponse( + {"status": self._signer_instance.session_data.status}, + status=self.Status.http_status_for_status(self._signer_instance.session_data.status), + ) do_cleanup = True try: - container = signer.finalize(getattr(request, "data", None)) + container = self._signer_instance.finalize(getattr(request, "data", None)) self.save_container(container, *args, **kwargs) except ActionInProgress as e: do_cleanup = False return JsonResponse({"status": self.Status.PENDING, **e.data}, status=e.status) + else: + self._signer_instance.session_data.status = self.Status.SUCCESS + finally: if do_cleanup: - signer.cleanup() + self._signer_instance.cleanup(delete_session=False) return self.get_success_response(*args, **kwargs) diff --git a/esteid/smartid/authenticator.py b/esteid/smartid/authenticator.py index 53aa0b1..bccd9cb 100644 --- a/esteid/smartid/authenticator.py +++ b/esteid/smartid/authenticator.py @@ -17,6 +17,8 @@ class SmartIdAuthenticator(Authenticator): id_code: str country: str + DJANGO_SESSION_IS_NEEDED = True + def setup(self, initial_data: dict = None): """ Receives user input via POST: `id_code`, `country` @@ -41,10 +43,9 @@ def authenticate(self, random_bytes=None): service = TranslatedSmartIDService.get_instance() auth_initial_result = service.authenticate(self.id_code, self.country, random_bytes=random_bytes) - - self.save_session_data( - session_id=auth_initial_result.session_id, hash_value_b64=auth_initial_result.hash_value_b64 - ) + self.session_data.session_id = auth_initial_result.session_id + self.session_data.hash_value_b64 = auth_initial_result.hash_value_b64 + self.save_session_data() raise ActionInProgress( data={ diff --git a/esteid/smartid/signer.py b/esteid/smartid/signer.py index 12bc117..240c21f 100644 --- a/esteid/smartid/signer.py +++ b/esteid/smartid/signer.py @@ -2,7 +2,7 @@ from typing import List import pyasice -from pyasice import Container, XmlSignature +from pyasice import Container from esteid.exceptions import ActionInProgress, InvalidIdCode, InvalidParameters from esteid.signing import DataFile, Signer @@ -52,12 +52,10 @@ def prepare(self, container: Container = None, files: List[DataFile] = None) -> sign_session = service.sign_by_document_number(document_number, xml_sig.signed_data()) - self.save_session_data( - digest=sign_session.digest, - container=container, - xml_sig=xml_sig, - session_id=sign_session.session_id, - ) + self.session_data.digest = sign_session.digest + self.session_data.session_id = sign_session.session_id + self.set_container(container=container, xml_sig=xml_sig) + self.save_session_data() return { "verification_code": sign_session.verification_code, @@ -90,9 +88,3 @@ def finalize(self, data: dict = None) -> Container: container.add_signature(xml_sig) return container - - def save_session_data(self, *, digest: bytes, container: Container, xml_sig: XmlSignature, session_id: str): - data_obj = self.session_data - data_obj.session_id = session_id - - super().save_session_data(digest=digest, container=container, xml_sig=xml_sig) diff --git a/esteid/static/esteid-helper/Esteid.main.web.js b/esteid/static/esteid-helper/Esteid.main.web.js index 73e7188..53dd88b 100644 --- a/esteid/static/esteid-helper/Esteid.main.web.js +++ b/esteid/static/esteid-helper/Esteid.main.web.js @@ -232,7 +232,7 @@ var IdCardManager_default = IdCardManager; // IdentificationManager.js - var request = async (url, data, method = "POST") => { + var request = async (url, data, method = "POST", retries = 3) => { const headers = { "Content-Type": "application/json" }; @@ -241,24 +241,38 @@ headers["X-CSRFToken"] = data.csrfmiddlewaretoken; body = JSON.stringify(data || {}); } + const onError = async (err) => { + const retriesRemaining = retries - 1; + if (retriesRemaining > 0) { + console.log(`Error fetching ${url}: ${err}, waiting for 1000ms before retrying.`); + await new Promise((resolve) => setTimeout(resolve, 1e3)); + console.log(`Retrying ${url}, ${retriesRemaining} tries remaining.`); + return await request(url, data, method, retriesRemaining); + } + console.log(err); + return {}; + }; try { const response = await fetch(url, { method, headers, body }); - const responseText = await response.text(); - try { - const data2 = JSON.parse(responseText); - data2.success = data2.status === "success"; - data2.pending = `${response.status}` === "202"; - return { - data: data2, - ok: response.ok - }; - } catch (err) { - console.log("Failed to parse response as JSON", responseText); - return {}; + if (`${response.status}` !== "410") { + const responseText = await response.text(); + try { + const data2 = JSON.parse(responseText); + data2.success = data2.status === "success"; + data2.pending = `${response.status}` === "202"; + return { + data: data2, + ok: response.ok + }; + } catch (err) { + console.log("Failed to parse response as JSON", responseText); + return {}; + } + } else { + return await onError(new Error("The session is gone, we need to try and refresh the page.")); } } catch (err) { - console.log(err); - return {}; + return await onError(err); } }; var IdentificationManager = class { diff --git a/esteid/static/esteid-helper/Esteid.main.web.min.js b/esteid/static/esteid-helper/Esteid.main.web.min.js index d5c4128..e54e653 100644 --- a/esteid/static/esteid-helper/Esteid.main.web.min.js +++ b/esteid/static/esteid-helper/Esteid.main.web.min.js @@ -1 +1 @@ -(()=>{var o="EST",d="ENG",l="RUS",c="LIT",R=[o,d,l,c],k={user_cancel:{[o]:"Allkirjastamine katkestati",[d]:"Signing was cancelled",[c]:"Pasira\u0161ymas nutrauktas",[l]:"\u041F\u043E\u0434\u043F\u0438\u0441\u044C \u0431\u044B\u043B\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430"},no_certificates:{[o]:"Sertifikaate ei leitud",[d]:"Certificate not found",[c]:"Nerastas sertifikatas",[l]:"\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D"},invalid_argument:{[o]:"Vigane sertifikaadi identifikaator",[d]:"Invalid certificate identifier",[c]:"Neteisingas sertifikato identifikatorius",[l]:"\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u0430"},no_implementation:{[o]:"Vajalik tarkvara on puudu",[d]:"Unable to find software",[c]:"Nerasta programin\u0117s \u012Franga",[l]:"\u041E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E\u0435 \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u043E\u0435 \u043E\u0431\u0435\u0441\u043F\u0435\u0447\u0435\u043D\u0438\u0435"},version_mismatch:{[o]:"Allkirjastamise tarkvara ja brauseri laienduse versioonid ei \xFChti. Palun uuendage oma id-kaardi tarkvara.",[d]:"The versions of the signing software and browser extension do not match. Please update your ID card software.",[c]:"Parakst\u012B\u0161anas programmas un p\u0101rl\u016Bka papla\u0161in\u0101juma versijas nesakr\u012Bt. L\u016Bdzu, atjauniniet savu ID kartes programmat\u016Bru.",[l]:"\u0412\u0435\u0440\u0441\u0438\u0438 \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u044B \u0434\u043B\u044F \u043F\u043E\u0434\u043F\u0438\u0441\u0430\u043D\u0438\u044F \u0438 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430 \u043D\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u044E\u0442. \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u0435 \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u043E\u0435 \u043E\u0431\u0435\u0441\u043F\u0435\u0447\u0435\u043D\u0438\u0435 \u0434\u043B\u044F \u0432\u0430\u0448\u0435\u0439 \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u043E\u043D\u043D\u043E\u0439 \u043A\u0430\u0440\u0442\u044B."},technical_error:{[o]:"Tehniline viga",[d]:"Technical error",[c]:"Technin\u0117 klaida",[l]:"\u0422\u0435\u0445\u043D\u0438\u0447\u0435\u0441\u043A\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430"},not_allowed:{[o]:"Veebis allkirjastamise k\xE4ivitamine on v\xF5imalik vaid https aadressilt",[d]:"Web signing is allowed only from https:// URL",[c]:"Web signing is allowed only from https:// URL",[l]:"\u041F\u043E\u0434\u043F\u0438\u0441\u044C \u0432 \u0438\u043D\u0442\u0435\u0440\u043D\u0435\u0442\u0435 \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u0430 \u0442\u043E\u043B\u044C\u043A\u043E \u0441 URL-\u043E\u0432, \u043D\u0430\u0447\u0438\u043D\u0430\u044E\u0449\u0438\u0445\u0441\u044F \u0441 https://"}},m=class{constructor(e){this.language=e||o,this.certificate=null,this.supportedSignatureAlgorithms=null,this.signatureAlgorithm=null}initializeIdCard(){return new Promise(function(e,t){typeof window.webeid<"u"?e("web-eid"):typeof window.hwcrypto<"u"&&window.hwcrypto.use("auto")?e("hwcrypto"):t("Backend selection failed")})}getCertificate(){return new Promise((e,t)=>{let i={lang:this.language};window.webeid.getSigningCertificate(i).then(({certificate:n,supportedSignatureAlgorithms:s})=>{this.certificate=n,this.supportedSignatureAlgorithms=s,e(n)},n=>{t(n)})})}signHexData(e,t="SHA-256"){return new Promise((i,n)=>{let s={lang:this.language};window.webeid.sign(this.certificate,e,t,s).then(r=>{this.signatureAlgorithm=r.signatureAlgorithm,i(r.signature)},r=>{n(r)})})}authenticate(e,t){let i={lang:this.language,...t};return new Promise((n,s)=>window.webeid.authenticate(e,i).then(r=>{n(r)},r=>{s(r)}))}get language(){return this._language}set language(e){R.indexOf(e)!==-1&&(this._language=e)}getWebeidErrorMapping(e){switch((e?e.code:null)||null){case"ERR_WEBEID_CONTEXT_INSECURE":return"not_allowed";case"ERR_WEBEID_ACTION_TIMEOUT":return"technical_error";case"ERR_WEBEID_USER_CANCELLED":case"ERR_WEBEID_USER_TIMEOUT":return"user_cancel";case"ERR_WEBEID_VERSION_MISMATCH":case"ERR_WEBEID_VERSION_INVALID":return"version_mismatch";case"ERR_WEBEID_EXTENSION_UNAVAILABLE":case"ERR_WEBEID_NATIVE_UNAVAILABLE":return"no_implementation";case"ERR_WEBEID_NATIVE_FATAL":return e.message.includes("https")?"not_allowed":"technical_error";default:case"ERR_WEBEID_UNKNOWN_ERROR":case"ERR_WEBEID_NATIVE_INVALID_ARGUMENT":case"ERR_WEBEID_ACTION_PENDING":case"ERR_WEBEID_MISSING_PARAMETER":return"technical_error"}}getError(e){let t;return typeof k[e]>"u"?t=this.getWebeidErrorMapping(e)||"technical_error":t=e,{error_code:t,message:k[t][this.language],raw:e}}},_=m;var g=async(E,e,t="POST")=>{let i={"Content-Type":"application/json"},n=null;t!=="GET"&&(i["X-CSRFToken"]=e.csrfmiddlewaretoken,n=JSON.stringify(e||{}));try{let s=await fetch(E,{method:t,headers:i,body:n}),r=await s.text();try{let a=JSON.parse(r);return a.success=a.status==="success",a.pending=`${s.status}`=="202",{data:a,ok:s.ok}}catch{return console.log("Failed to parse response as JSON",r),{}}}catch(s){return console.log(s),{}}},p=class{constructor({language:e,idUrl:t,mobileIdUrl:i,smartIdUrl:n,csrfToken:s,pollInterval:r}){this.idCardManager=new _(e),this.idUrl=t,this.mobileIdUrl=i,this.smartIdUrl=n,this.csrfToken=s,this.language=e,this.pollInterval=r||3e3}checkStatus(e,t,i){let n=this.pollInterval,s=this.csrfToken,r=()=>{g(e,{csrfmiddlewaretoken:s},"PATCH").then(({ok:a,data:h})=>{a&&h.pending?setTimeout(()=>r(),n):a&&h.success?t(h):i(h)}).catch(a=>{console.log("Status error",a)})};return r()}signWithIdCard(){return new Promise((e,t)=>{this.__signHandleIdCard(e,t)})}signWithMobileId({idCode:e,phoneNumber:t}){return new Promise((i,n)=>{this.__signHandleMid(e,t,i,n)})}signWithSmartId({idCode:e,country:t}){return new Promise((i,n)=>{this.__signHandleSmartid(e,t,i,n)})}__signHandleIdCard(e,t){this.idCardManager.initializeIdCard().then(()=>{this.idCardManager.getCertificate().then(i=>{g(this.idUrl,{csrfmiddlewaretoken:this.csrfToken,certificate:i}).then(({ok:n,data:s})=>{n&&s.success?this.__doSign(s.digest,e,t):t(s)})},t)},t)}__doSign(e,t,i){this.idCardManager.signHexData(e).then(n=>{g(this.idUrl,{csrfmiddlewaretoken:this.csrfToken,signature_value:n},"PATCH").then(({ok:s,data:r})=>{s&&r.success?t(r):i(r)})},i)}__signHandleMid(e,t,i,n){g(this.mobileIdUrl,{id_code:e,phone_number:t,language:this.language,csrfmiddlewaretoken:this.csrfToken}).then(({ok:s,data:r})=>{s&&r.success?i(r):n(r)})}midStatus(){return new Promise((e,t)=>{this.checkStatus(this.mobileIdUrl,e,t)})}__signHandleSmartid(e,t,i,n){g(this.smartIdUrl,{id_code:e,country:t,csrfmiddlewaretoken:this.csrfToken}).then(({ok:s,data:r})=>{s&&r.success?i(r):n(r)})}smartidStatus(){return new Promise((e,t)=>{this.checkStatus(this.smartIdUrl,e,t)})}authenticateWithIdCard(e){return new Promise((t,i)=>{g(this.idUrl,{csrfmiddlewaretoken:this.csrfToken},"POST").then(({ok:n,data:s})=>{if(n&&s.pending)return this.idCardManager.initializeIdCard().then(()=>this.idCardManager.authenticate(s.nonce,e||{}).then(r=>g(this.idUrl,{csrfmiddlewaretoken:this.csrfToken,...r},"PATCH").then(({ok:a,data:h})=>{a&&h.success?t(h):i(h)},i),r=>{if(r.code==="ERR_WEBEID_USER_CANCELLED")return g(this.idUrl,{csrfmiddlewaretoken:this.csrfToken},"DELETE").then(()=>{i(r)},i);i(r)}),i);i(s)})})}getError(e){return this.idCardManager.getError(e)}},I=p;function f(E,e){let t=Object.entries(e).map(([i,n])=>`${encodeURIComponent(i)}=${encodeURIComponent(n)}`).join("&");return fetch(E,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:t}).then(i=>i.json().then(n=>({data:n,ok:i.ok})),i=>(console.log(i),{}))}var u=class{constructor(e){let t={language:null,idEndpoints:{start:null,finish:null,finalize:null},midEndpoints:{start:null,status:null,finalize:null},smartidEndpoints:{start:null,status:null,finalize:null},...e};this.idCardManager=new _(t.language),this.idEndpoints=t.idEndpoints,this.midEndpoints=t.midEndpoints,this.smartidEndpoints=t.smartidEndpoints}checkStatus(e,t,i,n){let s=()=>{f(e,t).then(({ok:r,data:a})=>{r&&a.pending?setTimeout(()=>s(),1e3):r&&a.success?i(a):n(a)})};return s}signWithIdCard(e){return new Promise((t,i)=>{this.__signHandleId(e,t,i)})}signWithMobileId(e){return new Promise((t,i)=>{this.__signHandleMid(e,t,i)})}signWithSmartId(e){return new Promise((t,i)=>{this.__signHandleSmartid(e,t,i)})}sign(e,t){if(e===u.SIGN_ID)return this.signWithIdCard(t);if(e===u.SIGN_MOBILE)return this.signWithMobileId(t);if(e===u.SIGN_SMARTID)return this.signWithSmartId(t);throw new TypeError("LegacyIdentificationManager: Bad signType")}__signHandleId(e,t,i){this.idCardManager.initializeIdCard().then(()=>{this.idCardManager.getCertificate().then(n=>{f(this.idEndpoints.start,{...e,certificate:n}).then(({ok:s,data:r})=>{s&&r.success?this.__doSign(r.digest,e,t,i):i(r)})},i)},i)}__doSign(e,t,i,n){this.idCardManager.signHexData(e).then(s=>{f(this.idEndpoints.finish,{...t,signature_value:s}).then(({ok:r,data:a})=>{r&&a.success?i(a):n(a)})},n)}__signHandleMid(e,t,i){f(this.midEndpoints.start,e).then(({ok:n,data:s})=>{n&&s.success?t(s):i(s)})}midStatus(e){return new Promise((t,i)=>{this.checkStatus(this.midEndpoints.status,e,t,i)()})}__signHandleSmartid(e,t,i){f(this.smartidEndpoints.start,e).then(({ok:n,data:s})=>{n&&s.success?t(s):i(s)})}smartidStatus(e){return new Promise((t,i)=>{this.checkStatus(this.smartidEndpoints.status,e,t,i)()})}getError(e){return this.idCardManager.getError(e)}};u.SIGN_ID="id";u.SIGN_MOBILE="mid";u.SIGN_SMARTID="smartid";var w=u;var T={ET:o,EN:d,RU:l,LT:c};var S={IdentificationManager:I,LegacyIdentificationManager:w,Languages:T};var A=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{};A.Esteid=S;var O=S;})(); +(()=>{var d="EST",l="ENG",c="RUS",u="LIT",A=[d,l,c,u],k={user_cancel:{[d]:"Allkirjastamine katkestati",[l]:"Signing was cancelled",[u]:"Pasira\u0161ymas nutrauktas",[c]:"\u041F\u043E\u0434\u043F\u0438\u0441\u044C \u0431\u044B\u043B\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430"},no_certificates:{[d]:"Sertifikaate ei leitud",[l]:"Certificate not found",[u]:"Nerastas sertifikatas",[c]:"\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D"},invalid_argument:{[d]:"Vigane sertifikaadi identifikaator",[l]:"Invalid certificate identifier",[u]:"Neteisingas sertifikato identifikatorius",[c]:"\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u0430"},no_implementation:{[d]:"Vajalik tarkvara on puudu",[l]:"Unable to find software",[u]:"Nerasta programin\u0117s \u012Franga",[c]:"\u041E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E\u0435 \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u043E\u0435 \u043E\u0431\u0435\u0441\u043F\u0435\u0447\u0435\u043D\u0438\u0435"},version_mismatch:{[d]:"Allkirjastamise tarkvara ja brauseri laienduse versioonid ei \xFChti. Palun uuendage oma id-kaardi tarkvara.",[l]:"The versions of the signing software and browser extension do not match. Please update your ID card software.",[u]:"Parakst\u012B\u0161anas programmas un p\u0101rl\u016Bka papla\u0161in\u0101juma versijas nesakr\u012Bt. L\u016Bdzu, atjauniniet savu ID kartes programmat\u016Bru.",[c]:"\u0412\u0435\u0440\u0441\u0438\u0438 \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u044B \u0434\u043B\u044F \u043F\u043E\u0434\u043F\u0438\u0441\u0430\u043D\u0438\u044F \u0438 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430 \u043D\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u044E\u0442. \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u0435 \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u043E\u0435 \u043E\u0431\u0435\u0441\u043F\u0435\u0447\u0435\u043D\u0438\u0435 \u0434\u043B\u044F \u0432\u0430\u0448\u0435\u0439 \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u043E\u043D\u043D\u043E\u0439 \u043A\u0430\u0440\u0442\u044B."},technical_error:{[d]:"Tehniline viga",[l]:"Technical error",[u]:"Technin\u0117 klaida",[c]:"\u0422\u0435\u0445\u043D\u0438\u0447\u0435\u0441\u043A\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430"},not_allowed:{[d]:"Veebis allkirjastamise k\xE4ivitamine on v\xF5imalik vaid https aadressilt",[l]:"Web signing is allowed only from https:// URL",[u]:"Web signing is allowed only from https:// URL",[c]:"\u041F\u043E\u0434\u043F\u0438\u0441\u044C \u0432 \u0438\u043D\u0442\u0435\u0440\u043D\u0435\u0442\u0435 \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u0430 \u0442\u043E\u043B\u044C\u043A\u043E \u0441 URL-\u043E\u0432, \u043D\u0430\u0447\u0438\u043D\u0430\u044E\u0449\u0438\u0445\u0441\u044F \u0441 https://"}},p=class{constructor(e){this.language=e||d,this.certificate=null,this.supportedSignatureAlgorithms=null,this.signatureAlgorithm=null}initializeIdCard(){return new Promise(function(e,t){typeof window.webeid<"u"?e("web-eid"):typeof window.hwcrypto<"u"&&window.hwcrypto.use("auto")?e("hwcrypto"):t("Backend selection failed")})}getCertificate(){return new Promise((e,t)=>{let i={lang:this.language};window.webeid.getSigningCertificate(i).then(({certificate:n,supportedSignatureAlgorithms:r})=>{this.certificate=n,this.supportedSignatureAlgorithms=r,e(n)},n=>{t(n)})})}signHexData(e,t="SHA-256"){return new Promise((i,n)=>{let r={lang:this.language};window.webeid.sign(this.certificate,e,t,r).then(s=>{this.signatureAlgorithm=s.signatureAlgorithm,i(s.signature)},s=>{n(s)})})}authenticate(e,t){let i={lang:this.language,...t};return new Promise((n,r)=>window.webeid.authenticate(e,i).then(s=>{n(s)},s=>{r(s)}))}get language(){return this._language}set language(e){A.indexOf(e)!==-1&&(this._language=e)}getWebeidErrorMapping(e){switch((e?e.code:null)||null){case"ERR_WEBEID_CONTEXT_INSECURE":return"not_allowed";case"ERR_WEBEID_ACTION_TIMEOUT":return"technical_error";case"ERR_WEBEID_USER_CANCELLED":case"ERR_WEBEID_USER_TIMEOUT":return"user_cancel";case"ERR_WEBEID_VERSION_MISMATCH":case"ERR_WEBEID_VERSION_INVALID":return"version_mismatch";case"ERR_WEBEID_EXTENSION_UNAVAILABLE":case"ERR_WEBEID_NATIVE_UNAVAILABLE":return"no_implementation";case"ERR_WEBEID_NATIVE_FATAL":return e.message.includes("https")?"not_allowed":"technical_error";default:case"ERR_WEBEID_UNKNOWN_ERROR":case"ERR_WEBEID_NATIVE_INVALID_ARGUMENT":case"ERR_WEBEID_ACTION_PENDING":case"ERR_WEBEID_MISSING_PARAMETER":return"technical_error"}}getError(e){let t;return typeof k[e]>"u"?t=this.getWebeidErrorMapping(e)||"technical_error":t=e,{error_code:t,message:k[t][this.language],raw:e}}},_=p;var g=async(f,e,t="POST",i=3)=>{let n={"Content-Type":"application/json"},r=null;t!=="GET"&&(n["X-CSRFToken"]=e.csrfmiddlewaretoken,r=JSON.stringify(e||{}));let s=async a=>{let o=i-1;return o>0?(console.log(`Error fetching ${f}: ${a}, waiting for 1000ms before retrying.`),await new Promise(E=>setTimeout(E,1e3)),console.log(`Retrying ${f}, ${o} tries remaining.`),await g(f,e,t,o)):(console.log(a),{})};try{let a=await fetch(f,{method:t,headers:n,body:r});if(`${a.status}`!="410"){let o=await a.text();try{let E=JSON.parse(o);return E.success=E.status==="success",E.pending=`${a.status}`=="202",{data:E,ok:a.ok}}catch{return console.log("Failed to parse response as JSON",o),{}}}else return await s(new Error("The session is gone, we need to try and refresh the page."))}catch(a){return await s(a)}},I=class{constructor({language:e,idUrl:t,mobileIdUrl:i,smartIdUrl:n,csrfToken:r,pollInterval:s}){this.idCardManager=new _(e),this.idUrl=t,this.mobileIdUrl=i,this.smartIdUrl=n,this.csrfToken=r,this.language=e,this.pollInterval=s||3e3}checkStatus(e,t,i){let n=this.pollInterval,r=this.csrfToken,s=()=>{g(e,{csrfmiddlewaretoken:r},"PATCH").then(({ok:a,data:o})=>{a&&o.pending?setTimeout(()=>s(),n):a&&o.success?t(o):i(o)}).catch(a=>{console.log("Status error",a)})};return s()}signWithIdCard(){return new Promise((e,t)=>{this.__signHandleIdCard(e,t)})}signWithMobileId({idCode:e,phoneNumber:t}){return new Promise((i,n)=>{this.__signHandleMid(e,t,i,n)})}signWithSmartId({idCode:e,country:t}){return new Promise((i,n)=>{this.__signHandleSmartid(e,t,i,n)})}__signHandleIdCard(e,t){this.idCardManager.initializeIdCard().then(()=>{this.idCardManager.getCertificate().then(i=>{g(this.idUrl,{csrfmiddlewaretoken:this.csrfToken,certificate:i}).then(({ok:n,data:r})=>{n&&r.success?this.__doSign(r.digest,e,t):t(r)})},t)},t)}__doSign(e,t,i){this.idCardManager.signHexData(e).then(n=>{g(this.idUrl,{csrfmiddlewaretoken:this.csrfToken,signature_value:n},"PATCH").then(({ok:r,data:s})=>{r&&s.success?t(s):i(s)})},i)}__signHandleMid(e,t,i,n){g(this.mobileIdUrl,{id_code:e,phone_number:t,language:this.language,csrfmiddlewaretoken:this.csrfToken}).then(({ok:r,data:s})=>{r&&s.success?i(s):n(s)})}midStatus(){return new Promise((e,t)=>{this.checkStatus(this.mobileIdUrl,e,t)})}__signHandleSmartid(e,t,i,n){g(this.smartIdUrl,{id_code:e,country:t,csrfmiddlewaretoken:this.csrfToken}).then(({ok:r,data:s})=>{r&&s.success?i(s):n(s)})}smartidStatus(){return new Promise((e,t)=>{this.checkStatus(this.smartIdUrl,e,t)})}authenticateWithIdCard(e){return new Promise((t,i)=>{g(this.idUrl,{csrfmiddlewaretoken:this.csrfToken},"POST").then(({ok:n,data:r})=>{if(n&&r.pending)return this.idCardManager.initializeIdCard().then(()=>this.idCardManager.authenticate(r.nonce,e||{}).then(s=>g(this.idUrl,{csrfmiddlewaretoken:this.csrfToken,...s},"PATCH").then(({ok:a,data:o})=>{a&&o.success?t(o):i(o)},i),s=>{if(s.code==="ERR_WEBEID_USER_CANCELLED")return g(this.idUrl,{csrfmiddlewaretoken:this.csrfToken},"DELETE").then(()=>{i(s)},i);i(s)}),i);i(r)})})}getError(e){return this.idCardManager.getError(e)}},w=I;function m(f,e){let t=Object.entries(e).map(([i,n])=>`${encodeURIComponent(i)}=${encodeURIComponent(n)}`).join("&");return fetch(f,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:t}).then(i=>i.json().then(n=>({data:n,ok:i.ok})),i=>(console.log(i),{}))}var h=class{constructor(e){let t={language:null,idEndpoints:{start:null,finish:null,finalize:null},midEndpoints:{start:null,status:null,finalize:null},smartidEndpoints:{start:null,status:null,finalize:null},...e};this.idCardManager=new _(t.language),this.idEndpoints=t.idEndpoints,this.midEndpoints=t.midEndpoints,this.smartidEndpoints=t.smartidEndpoints}checkStatus(e,t,i,n){let r=()=>{m(e,t).then(({ok:s,data:a})=>{s&&a.pending?setTimeout(()=>r(),1e3):s&&a.success?i(a):n(a)})};return r}signWithIdCard(e){return new Promise((t,i)=>{this.__signHandleId(e,t,i)})}signWithMobileId(e){return new Promise((t,i)=>{this.__signHandleMid(e,t,i)})}signWithSmartId(e){return new Promise((t,i)=>{this.__signHandleSmartid(e,t,i)})}sign(e,t){if(e===h.SIGN_ID)return this.signWithIdCard(t);if(e===h.SIGN_MOBILE)return this.signWithMobileId(t);if(e===h.SIGN_SMARTID)return this.signWithSmartId(t);throw new TypeError("LegacyIdentificationManager: Bad signType")}__signHandleId(e,t,i){this.idCardManager.initializeIdCard().then(()=>{this.idCardManager.getCertificate().then(n=>{m(this.idEndpoints.start,{...e,certificate:n}).then(({ok:r,data:s})=>{r&&s.success?this.__doSign(s.digest,e,t,i):i(s)})},i)},i)}__doSign(e,t,i,n){this.idCardManager.signHexData(e).then(r=>{m(this.idEndpoints.finish,{...t,signature_value:r}).then(({ok:s,data:a})=>{s&&a.success?i(a):n(a)})},n)}__signHandleMid(e,t,i){m(this.midEndpoints.start,e).then(({ok:n,data:r})=>{n&&r.success?t(r):i(r)})}midStatus(e){return new Promise((t,i)=>{this.checkStatus(this.midEndpoints.status,e,t,i)()})}__signHandleSmartid(e,t,i){m(this.smartidEndpoints.start,e).then(({ok:n,data:r})=>{n&&r.success?t(r):i(r)})}smartidStatus(e){return new Promise((t,i)=>{this.checkStatus(this.smartidEndpoints.status,e,t,i)()})}getError(e){return this.idCardManager.getError(e)}};h.SIGN_ID="id";h.SIGN_MOBILE="mid";h.SIGN_SMARTID="smartid";var S=h;var R={ET:d,EN:l,RU:c,LT:u};var T={IdentificationManager:w,LegacyIdentificationManager:S,Languages:R};var N=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{};N.Esteid=T;var y=T;})(); diff --git a/poetry.lock b/poetry.lock index 89c3f8c..00fb1e9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. [[package]] name = "asgiref" @@ -6,6 +6,7 @@ version = "3.8.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, @@ -23,6 +24,7 @@ version = "1.5.1" description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" optional = false python-versions = "*" +groups = ["main", "dev"] files = [ {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, @@ -34,6 +36,7 @@ version = "2.15.8" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.7.2" +groups = ["dev"] files = [ {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"}, {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"}, @@ -53,6 +56,7 @@ version = "24.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, @@ -72,6 +76,8 @@ version = "0.2.1" description = "Backport of the standard library zoneinfo module" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] +markers = "python_version < \"3.9\"" files = [ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, @@ -100,6 +106,7 @@ version = "24.3.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, @@ -146,6 +153,7 @@ version = "5.5.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, @@ -157,6 +165,7 @@ version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, @@ -168,6 +177,8 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -247,6 +258,7 @@ version = "5.2.0" description = "Universal encoding detector for Python 3" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, @@ -258,6 +270,7 @@ version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, @@ -359,6 +372,7 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -373,6 +387,7 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -384,6 +399,7 @@ version = "6.5.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, @@ -449,6 +465,7 @@ version = "3.3.1" description = "Show coverage stats online via coveralls.io" optional = false python-versions = ">= 3.5" +groups = ["dev"] files = [ {file = "coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"}, {file = "coveralls-3.3.1.tar.gz", hash = "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea"}, @@ -468,6 +485,7 @@ version = "43.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, @@ -517,6 +535,7 @@ version = "0.3.9" description = "serialize all of Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, @@ -532,6 +551,7 @@ version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, @@ -543,6 +563,7 @@ version = "4.2.17" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "Django-4.2.17-py3-none-any.whl", hash = "sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0"}, {file = "Django-4.2.17.tar.gz", hash = "sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc"}, @@ -564,6 +585,7 @@ version = "0.22" description = "An SSL-enabled development server for Django" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "django_sslserver-0.22-py3-none-any.whl", hash = "sha256:c598a363d2ccdc2421c08ddb3d8b0973f80e8e47a3a5b74e4a2896f21c2947c5"}, ] @@ -577,6 +599,7 @@ version = "3.15.2" description = "Web APIs for Django, made easy." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, @@ -592,6 +615,7 @@ version = "0.6.2" description = "Pythonic argument parser, that will make you smile" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] @@ -602,6 +626,7 @@ version = "1.0.2" description = "Certificates for Estonian e-identity services" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "esteid_certificates-1.0.2-py2.py3-none-any.whl", hash = "sha256:ab15fd07f43fc455800457d24d2daaa84a1d9eb94d61f9f9b61c9544a140a06a"}, {file = "esteid_certificates-1.0.2.tar.gz", hash = "sha256:abdd88ffc5d2c5b52ac7a51e1b94b17a0572f9edd42613a3b8b47ceb30b239a2"}, @@ -613,6 +638,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -627,6 +654,7 @@ version = "3.16.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, @@ -643,6 +671,7 @@ version = "5.0.4" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.6.1" +groups = ["dev"] files = [ {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, @@ -659,6 +688,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -673,6 +703,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -684,6 +715,7 @@ version = "5.12.0" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, @@ -701,6 +733,7 @@ version = "1.10.0" description = "A fast and thorough lazy object proxy." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, @@ -747,6 +780,7 @@ version = "4.9.4" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +groups = ["main"] files = [ {file = "lxml-4.9.4-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e214025e23db238805a600f1f37bf9f9a15413c7bf5f9d6ae194f84980c78722"}, {file = "lxml-4.9.4-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ec53a09aee61d45e7dbe7e91252ff0491b6b5fee3d85b2d45b173d8ab453efc1"}, @@ -855,6 +889,7 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -866,6 +901,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -877,20 +913,26 @@ version = "1.3.0" description = "TLS (SSL) sockets, key generation, encryption, decryption, signing, verification and KDFs using the OS crypto libraries. Does not require a compiler, and relies on the OS for patching. Works on Windows, OS X and Linux/BSD." optional = false python-versions = "*" -files = [ - {file = "oscrypto-1.3.0-py2.py3-none-any.whl", hash = "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085"}, - {file = "oscrypto-1.3.0.tar.gz", hash = "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4"}, -] +groups = ["main", "dev"] +files = [] +develop = false [package.dependencies] asn1crypto = ">=1.5.1" +[package.source] +type = "git" +url = "https://github.com/wbond/oscrypto.git" +reference = "d5f3437ed24257895ae1edd9e503cfb352e635a8" +resolved_reference = "d5f3437ed24257895ae1edd9e503cfb352e635a8" + [[package]] name = "packaging" version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -902,6 +944,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -913,6 +956,7 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -929,6 +973,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -944,6 +989,7 @@ version = "1.2.0" description = "Manipulate ASiC-E containers and XAdES/eIDAS signatures for Estonian e-identity services" optional = false python-versions = "<4.0,>=3.7" +groups = ["main"] files = [ {file = "pyasice-1.2.0-py3-none-any.whl", hash = "sha256:a99b8407124dd6f69017885f17b352ca4ed30674d7320bde9b9ad90a0de9cd94"}, {file = "pyasice-1.2.0.tar.gz", hash = "sha256:2ffbc7b101b6571590715dc2863324f010f0ff827ad9f06c7f6cb3e4f92ce260"}, @@ -962,6 +1008,7 @@ version = "2.9.1" description = "Python style guide checker" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, @@ -973,6 +1020,8 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -984,6 +1033,7 @@ version = "2.5.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, @@ -995,6 +1045,7 @@ version = "2.16.4" description = "python code static checker" optional = false python-versions = ">=3.7.2" +groups = ["dev"] files = [ {file = "pylint-2.16.4-py3-none-any.whl", hash = "sha256:4a770bb74fde0550fa0ab4248a2ad04e7887462f9f425baa0cd8d3c1d098eaee"}, {file = "pylint-2.16.4.tar.gz", hash = "sha256:8841f26a0dbc3503631b6a20ee368b3f5e0e5461a1d95cf15d103dab748a0db3"}, @@ -1024,6 +1075,7 @@ version = "24.3.0" description = "Python wrapper module around the OpenSSL library" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pyOpenSSL-24.3.0-py3-none-any.whl", hash = "sha256:e474f5a473cd7f92221cc04976e48f4d11502804657a08a989fb3be5514c904a"}, {file = "pyopenssl-24.3.0.tar.gz", hash = "sha256:49f7a019577d834746bc55c5fce6ecbcec0f2b4ec5ce1cf43a9a173b8138bb36"}, @@ -1042,6 +1094,7 @@ version = "1.8.0" description = "API to interact with the python pyproject.toml based projects" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pyproject_api-1.8.0-py3-none-any.whl", hash = "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228"}, {file = "pyproject_api-1.8.0.tar.gz", hash = "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496"}, @@ -1061,6 +1114,7 @@ version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, @@ -1083,6 +1137,7 @@ version = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, @@ -1101,6 +1156,7 @@ version = "4.9.0" description = "A Django plugin for pytest." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99"}, {file = "pytest_django-4.9.0.tar.gz", hash = "sha256:8bf7bc358c9ae6f6fc51b6cebb190fe20212196e6807121f11bd6a3b03428314"}, @@ -1119,6 +1175,7 @@ version = "2024.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, @@ -1130,6 +1187,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -1151,6 +1209,7 @@ version = "1.12.1" description = "Mock out responses from the requests package" optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, @@ -1168,6 +1227,7 @@ version = "0.5.3" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, @@ -1183,6 +1243,8 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -1224,6 +1286,7 @@ version = "0.13.2" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, @@ -1235,6 +1298,7 @@ version = "4.23.2" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "tox-4.23.2-py3-none-any.whl", hash = "sha256:452bc32bb031f2282881a2118923176445bac783ab97c874b8770ab4c3b76c38"}, {file = "tox-4.23.2.tar.gz", hash = "sha256:86075e00e555df6e82e74cfc333917f91ecb47ffbc868dcafbd2672e332f4a2c"}, @@ -1262,6 +1326,8 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -1273,6 +1339,8 @@ version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["main", "dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, @@ -1284,6 +1352,7 @@ version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, @@ -1301,6 +1370,7 @@ version = "20.28.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, @@ -1321,6 +1391,7 @@ version = "1.17.0" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, @@ -1390,6 +1461,6 @@ files = [ ] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.8.0" -content-hash = "5fbeb74e75b639061ff564029e7a8592032a95930e7b08b44d3ed0574d78df17" +content-hash = "b865acc77831c80a21fbb34bd4cedfd2787fcc9905287741a05ff6bc198d05ce" diff --git a/pyproject.toml b/pyproject.toml index 1d970c2..c23ccbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "django-esteid" -version = "4.2" +version = "4.3" description = "Django-esteid is a package that provides Esteid based authentication for your Django applications." readme = "README.md" license = "BSD" @@ -92,6 +92,9 @@ pytest-cov = ">=2.8.1" pytest-django = ">=3.5.1" pytest = ">=4.6.5" requests-mock = "*" +# See https://github.com/wbond/oscrypto/issues/78 +oscrypto = { git = "https://github.com/wbond/oscrypto.git", rev = "d5f3437ed24257895ae1edd9e503cfb352e635a8" } + tox = ">=1.7.0" [build-system] diff --git a/setup.cfg b/setup.cfg index cce866c..a7c3a4d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,3 +41,4 @@ omit = esteid/ocsp.py esteid/urls.py esteid/views.py + esteid/actions.py