<commit_before>#
# Copyright (c) 2013, 2014, 2016 NORDUnet A/S. All rights reserved.
# Copyright 2012 <NAME>. All rights reserved.
#
# See the file eduid-IdP/LICENSE.txt for license statement.
#
# Author : <NAME> <<EMAIL>>
#          <NAME>
#

"""
Code handling Single Sign On logins.
"""

import hmac
import pprint
import time
from dataclasses import replace
from hashlib import sha256
from html import escape, unescape
from typing import Mapping, Optional

from defusedxml import ElementTree as DefusedElementTree
from flask import make_response, redirect, render_template, request
from flask_babel import gettext as _
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, TooManyRequests
from werkzeug.wrappers import Response as WerkzeugResponse

from eduid_common.api import exceptions
from eduid_common.session import session
from eduid_common.session.logindata import SSOLoginData
from eduid_userdb.idp import IdPUser
from eduid_userdb.idp.user import SAMLAttributeSettings

from eduid_webapp.idp import assurance, mischttp
from eduid_webapp.idp.app import current_idp_app as current_app
from eduid_webapp.idp.assurance import AssuranceException, MissingMultiFactor, WrongMultiFactor
from eduid_webapp.idp.idp_actions import check_for_pending_actions
from eduid_webapp.idp.idp_saml import (
    AuthnInfo,
    IdP_SAMLRequest,
    ResponseArgs,
    SAMLParseError,
    SAMLValidationError,
    gen_key,
)
from eduid_webapp.idp.service import Service
from eduid_webapp.idp.sso_session import SSOSession
from eduid_webapp.idp.util import b64encode, get_requested_authn_context


class MustAuthenticate(Exception):
    """
    This exception is raised in special circumstances when the IdP decides
    that a user really must authenticate again, even though there exist an
    SSO session.
    """


# -----------------------------------------------------------------------------
# === Single log in ====
# -----------------------------------------------------------------------------


class SSO(Service):
    """
    Single Sign On service.

    :param sso_session: SSO session
    :param context: IdP context
    """

    def __init__(self, sso_session: SSOSession):
        super().__init__(sso_session)

    def perform_login(self, ticket: SSOLoginData) -> WerkzeugResponse:
        """
        Validate request, and then proceed with creating an AuthnResponse and
        invoking the 'outgoing' SAML2 binding.

        :param ticket: Login process state
        :return: Response
        """
        current_app.logger.debug("\n\n---\n\n")
        current_app.logger.debug("--- In SSO.perform_login() ---")

        if not isinstance(self.sso_session, SSOSession):
            raise RuntimeError(f'self.sso_session is not of type {SSOSession} ({type(self.sso_session)})')

        user = self.sso_session.idp_user

        resp_args = self._validate_login_request(ticket)

        session['user_eppn'] = user.eppn

        action_response = check_for_pending_actions(user, ticket, self.sso_session)
        if action_response:
            return action_response

        # We won't get here until the user has completed all login actions

        response_authn = self._get_login_response_authn(ticket, user)

        saml_response = self._make_saml_response(response_authn, resp_args, user, ticket, self.sso_session)

        binding_out = resp_args['binding_out']
        destination = resp_args['destination']
        http_args = ticket.saml_req.apply_binding(resp_args, ticket.RelayState, str(saml_response))

        # INFO-Log the SSO session id and the AL and destination
        current_app.logger.info(f'{ticket.key}: response authn={response_authn}, dst={destination}')
        self._fticks_log(
            relying_party=resp_args.get('sp_entity_id', destination),
            authn_method=response_authn.class_ref,
            user_id=str(user.user_id),
        )

        return mischttp.create_html_response(binding_out, http_args)

    def _make_saml_response(
        self, response_authn: AuthnInfo, resp_args: ResponseArgs, user: IdPUser, ticket: SSOLoginData, sso_session
    ):
        """
        Create the SAML response using pysaml2 create_authn_response().

        :param resp_args: pysaml2 response arguments
        :param user: IdP user
        :param ticket: Login process state

        :return: SAML response (string)
        """
        saml_attribute_settings = SAMLAttributeSettings(
            default_eppn_scope=current_app.config.default_eppn_scope,
            default_country=current_app.config.default_country,
            default_country_code=current_app.config.default_country_code,
            default_scoped_affiliation=current_app.config.default_scoped_affiliation,<commit_msg>use SamlResponse type<commit_after>#
# Copyright (c) 2013, 2014, 2016 NORDUnet A/S. All rights reserved.
# Copyright 2012 <NAME>. All rights reserved.
#
# See the file eduid-IdP/LICENSE.txt for license statement.
#
# Author : <NAME> <<EMAIL>>
#          <NAME>
#

"""
Code handling Single Sign On logins.
"""

import hmac
import pprint
import time
from dataclasses import replace
from hashlib import sha256
from html import escape, unescape
from typing import Mapping, Optional

from defusedxml import ElementTree as DefusedElementTree
from flask import make_response, redirect, render_template, request
from flask_babel import gettext as _
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, TooManyRequests
from werkzeug.wrappers import Response as WerkzeugResponse

from eduid_common.api import exceptions
from eduid_common.session import session
from eduid_common.session.logindata import SSOLoginData
from eduid_userdb.idp import IdPUser
from eduid_userdb.idp.user import SAMLAttributeSettings

from eduid_webapp.idp import assurance, mischttp
from eduid_webapp.idp.app import current_idp_app as current_app
from eduid_webapp.idp.assurance import AssuranceException, MissingMultiFactor, WrongMultiFactor
from eduid_webapp.idp.idp_actions import check_for_pending_actions
from eduid_webapp.idp.idp_saml import (
    AuthnInfo,
    IdP_SAMLRequest,
    ResponseArgs,
    SAMLParseError,
    SamlResponse,
    SAMLValidationError,
    gen_key,
)
from eduid_webapp.idp.service import Service
from eduid_webapp.idp.sso_session import SSOSession
from eduid_webapp.idp.util import b64encode, get_requested_authn_context


class MustAuthenticate(Exception):
    """
    This exception is raised in special circumstances when the IdP decides
    that a user really must authenticate again, even though there exist an
    SSO session.
    """


# -----------------------------------------------------------------------------
# === Single log in ====
# -----------------------------------------------------------------------------


class SSO(Service):
    """
    Single Sign On service.

    :param sso_session: SSO session
    :param context: IdP context
    """

    def __init__(self, sso_session: SSOSession):
        super().__init__(sso_session)

    def perform_login(self, ticket: SSOLoginData) -> WerkzeugResponse:
        """
        Validate request, and then proceed with creating an AuthnResponse and
        invoking the 'outgoing' SAML2 binding.

        :param ticket: Login process state
        :return: Response
        """
        current_app.logger.debug("\n\n---\n\n")
        current_app.logger.debug("--- In SSO.perform_login() ---")

        if not isinstance(self.sso_session, SSOSession):
            raise RuntimeError(f'self.sso_session is not of type {SSOSession} ({type(self.sso_session)})')

        user = self.sso_session.idp_user

        resp_args = self._validate_login_request(ticket)

        session['user_eppn'] = user.eppn

        action_response = check_for_pending_actions(user, ticket, self.sso_session)
        if action_response:
            return action_response

        # We won't get here until the user has completed all login actions

        response_authn = self._get_login_response_authn(ticket, user)

        saml_response = self._make_saml_response(response_authn, resp_args, user, ticket, self.sso_session)

        binding_out = resp_args['binding_out']
        destination = resp_args['destination']
        http_args = ticket.saml_req.apply_binding(resp_args, ticket.RelayState, saml_response)

        # INFO-Log the SSO session id and the AL and destination
        current_app.logger.info(f'{ticket.key}: response authn={response_authn}, dst={destination}')
        self._fticks_log(
            relying_party=resp_args.get('sp_entity_id', destination),
            authn_method=response_authn.class_ref,
            user_id=str(user.user_id),
        )

        return mischttp.create_html_response(binding_out, http_args)

    def _make_saml_response(
        self,
        response_authn: AuthnInfo,
        resp_args: ResponseArgs,
        user: IdPUser,
        ticket: SSOLoginData,
        sso_session: SSOSession,
    ) -> SamlResponse:
        """
        Create the SAML response using pysaml2 create_authn_response().

        :param resp_args: pysaml2 response arguments
        :param user: IdP user
        :param ticket: Login process state

        :return: SAML response (string)
        """
        saml_attribute_settings = SAMLAttributeSettings(
            default_eppn_scope=current_app.config.default_eppn_scope,
            default_country=current_app.config.default_country,
            default_country_code=current_app.config.default_country_code,
            default_scoped_affiliation=current_app.config.default_scoped_affiliation,
{'fraction_non_alphanumeric': 0.10357951038880889, 'fraction_numerical': 0.0051429746965644925, 'mean_word_length': 3.1339285714285716, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 16, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1529261', 'n_tokens_mistral': 2907, 'n_tokens_neox': 2816, 'n_words': 718}