<commit_before>#
# Copyright (c) 2016 NORDUnet A/S
# All rights reserved.
#
#   Redistribution and use in source and binary forms, with or
#   without modification, are permitted provided that the following
#   conditions are met:
#
#     1. Redistributions of source code must retain the above copyright
#        notice, this list of conditions and the following disclaimer.
#     2. Redistributions in binary form must reproduce the above
#        copyright notice, this list of conditions and the following
#        disclaimer in the documentation and/or other materials provided
#        with the distribution.
#     3. Neither the name of the NORDUnet nor the names of its
#        contributors may be used to endorse or promote products derived
#        from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#

from saml2 import BINDING_HTTP_REDIRECT
from saml2.ident import decode
from saml2.client import Saml2Client
from saml2.response import LogoutResponse
from saml2.metadata import entity_descriptor
from werkzeug.exceptions import Forbidden
from flask import request, session, redirect, abort, make_response
from flask import current_app, Blueprint

from eduid_common.api.decorators import MarshalWith
from eduid_common.authn.utils import get_location
from eduid_common.authn.loa import get_loa
from eduid_common.authn.eduid_saml2 import get_authn_request, get_authn_response
from eduid_common.authn.eduid_saml2 import authenticate
from eduid_common.authn.cache import IdentityCache, StateCache
from eduid_webapp.authn.acs_registry import get_action, schedule_action
from eduid_webapp.authn.helpers import verify_auth_token, verify_relay_state


authn_views = Blueprint('authn', __name__)


@authn_views.route('/login')
def login():
    """
    login view, redirects to SAML2 IdP
    """
    return _authn('login-action')


@authn_views.route('/chpass')
def chpass():
    """
    Reauthn view, sends a SAML2 reauthn request to the IdP.
    """
    return _authn('change-password-action', force_authn=True)


@authn_views.route('/terminate')
def terminate():
    """
    Reauthn view, sends a SAML2 reauthn request to the IdP.
    """
    return _authn('terminate-account-action', force_authn=True)


def _authn(action, force_authn=False):
    redirect_url = current_app.config.get('SAML2_LOGIN_REDIRECT_URL', '/')
    relay_state = verify_relay_state(request.args.get('next', redirect_url), redirect_url)
    idps = current_app.saml2_config.getattr('idp')
    assert len(idps) == 1
    idp = idps.keys()[0]
    idp = request.args.get('idp', idp)
    loa = request.args.get('required_loa', None)
    authn_request = get_authn_request(current_app.config, session,
                                      relay_state, idp, required_loa=loa,
                                      force_authn=force_authn)
    schedule_action(action)
    current_app.logger.info('Redirecting the user to the IdP for ' + action)
    return redirect(get_location(authn_request))


@authn_views.route('/saml2-acs', methods=['POST'])
def assertion_consumer_service():
    """
    Assertion consumer service, receives POSTs from SAML2 IdP's
    """

    if 'SAMLResponse' not in request.form:
        abort(400)

    xmlstr = request.form['SAMLResponse']
    session_info = get_authn_response(current_app.config, session, xmlstr)
    current_app.logger.debug('Trying to locate the user authenticated by the IdP')
    user = authenticate(current_app, session_info)

    if user is None:
        current_app.logger.error('Could not find the user identified by the IdP')
        raise Forbidden("Access not authorized")

    action = get_action()
    return action(session_info, user)


def _get_name_id(session):
    """
    Get the SAML2 NameID of the currently logged in user.
    :param session: The current session object
    :return: NameID
    :rtype: saml2.saml.NameID | None
    """
    try:
        return decode(session['_saml2_session_name_id'])
    except KeyError:
        return None


@authn_views.route('/logout', methods=['GET'])
def logout():
    """
    SAML Logout Request initiator.
    This view initiates the SAML2 Logout request
    using the pysaml2 library to create the LogoutRequest.
    """
    eppn = session.get('user_eppn')

    if eppn is None:
        current_app.logger.info('Session cookie has expired, no logout action needed')
        location = current_app.config.get('SAML2_LOGOUT_REDIRECT_URL')
        return redirect(location)

    user = current_app.central_userdb.get_user_by_eppn(eppn)

    current_app.logger.debug('Logout process started for user {}'.format(user))
    state = StateCache(session)
    identity = IdentityCache(session)

    client = Saml2Client(current_app.saml2_config,
                         state_cache=state,
                         identity_cache=identity)

    subject_id = _get_name_id(session)
    if subject_id is None:
        current_app.logger.warning(
            'The session does not contain '
            'the subject id for user {}'.format(user))
        location = current_app.config.get('SAML2_LOGOUT_REDIRECT_URL')

    else:
        logouts = client.global_logout(subject_id)
        loresponse = logouts.values()[0]
        # loresponse is a dict for REDIRECT binding, and LogoutResponse for SOAP binding
        if isinstance(loresponse, LogoutResponse):
            if loresponse.status_ok():
                current_app.logger.debug('Performing local logout for {}'.format(user))
                session.clear()
                location = current_app.config.get('SAML2_LOGOUT_REDIRECT_URL')
                location = verify_relay_state(request.form.get('RelayState', location), location)
                return redirect(location)
            else:
                abort(500)
        headers_tuple = loresponse[1]['headers']
        location = headers_tuple[0][1]<commit_msg>Clear the session before redirecting the user<commit_after>#
# Copyright (c) 2016 NORDUnet A/S
# All rights reserved.
#
#   Redistribution and use in source and binary forms, with or
#   without modification, are permitted provided that the following
#   conditions are met:
#
#     1. Redistributions of source code must retain the above copyright
#        notice, this list of conditions and the following disclaimer.
#     2. Redistributions in binary form must reproduce the above
#        copyright notice, this list of conditions and the following
#        disclaimer in the documentation and/or other materials provided
#        with the distribution.
#     3. Neither the name of the NORDUnet nor the names of its
#        contributors may be used to endorse or promote products derived
#        from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#

from saml2 import BINDING_HTTP_REDIRECT
from saml2.ident import decode
from saml2.client import Saml2Client
from saml2.response import LogoutResponse
from saml2.metadata import entity_descriptor
from werkzeug.exceptions import Forbidden
from flask import request, session, redirect, abort, make_response
from flask import current_app, Blueprint

from eduid_common.api.decorators import MarshalWith
from eduid_common.authn.utils import get_location
from eduid_common.authn.loa import get_loa
from eduid_common.authn.eduid_saml2 import get_authn_request, get_authn_response
from eduid_common.authn.eduid_saml2 import authenticate
from eduid_common.authn.cache import IdentityCache, StateCache
from eduid_webapp.authn.acs_registry import get_action, schedule_action
from eduid_webapp.authn.helpers import verify_auth_token, verify_relay_state


authn_views = Blueprint('authn', __name__)


@authn_views.route('/login')
def login():
    """
    login view, redirects to SAML2 IdP
    """
    return _authn('login-action')


@authn_views.route('/chpass')
def chpass():
    """
    Reauthn view, sends a SAML2 reauthn request to the IdP.
    """
    return _authn('change-password-action', force_authn=True)


@authn_views.route('/terminate')
def terminate():
    """
    Reauthn view, sends a SAML2 reauthn request to the IdP.
    """
    return _authn('terminate-account-action', force_authn=True)


def _authn(action, force_authn=False):
    redirect_url = current_app.config.get('SAML2_LOGIN_REDIRECT_URL', '/')
    relay_state = verify_relay_state(request.args.get('next', redirect_url), redirect_url)
    idps = current_app.saml2_config.getattr('idp')
    assert len(idps) == 1
    idp = idps.keys()[0]
    idp = request.args.get('idp', idp)
    loa = request.args.get('required_loa', None)
    authn_request = get_authn_request(current_app.config, session,
                                      relay_state, idp, required_loa=loa,
                                      force_authn=force_authn)
    schedule_action(action)
    current_app.logger.info('Redirecting the user to the IdP for ' + action)
    return redirect(get_location(authn_request))


@authn_views.route('/saml2-acs', methods=['POST'])
def assertion_consumer_service():
    """
    Assertion consumer service, receives POSTs from SAML2 IdP's
    """

    if 'SAMLResponse' not in request.form:
        abort(400)

    xmlstr = request.form['SAMLResponse']
    session_info = get_authn_response(current_app.config, session, xmlstr)
    current_app.logger.debug('Trying to locate the user authenticated by the IdP')
    user = authenticate(current_app, session_info)

    if user is None:
        current_app.logger.error('Could not find the user identified by the IdP')
        raise Forbidden("Access not authorized")

    action = get_action()
    return action(session_info, user)


def _get_name_id(session):
    """
    Get the SAML2 NameID of the currently logged in user.
    :param session: The current session object
    :return: NameID
    :rtype: saml2.saml.NameID | None
    """
    try:
        return decode(session['_saml2_session_name_id'])
    except KeyError:
        return None


@authn_views.route('/logout', methods=['GET'])
def logout():
    """
    SAML Logout Request initiator.
    This view initiates the SAML2 Logout request
    using the pysaml2 library to create the LogoutRequest.
    """
    eppn = session.get('user_eppn')

    if eppn is None:
        current_app.logger.info('Session cookie has expired, no logout action needed')
        location = current_app.config.get('SAML2_LOGOUT_REDIRECT_URL')
        return redirect(location)

    user = current_app.central_userdb.get_user_by_eppn(eppn)

    current_app.logger.debug('Logout process started for user {}'.format(user))
    state = StateCache(session)
    identity = IdentityCache(session)

    client = Saml2Client(current_app.saml2_config,
                         state_cache=state,
                         identity_cache=identity)

    # The user doesn't have a SAML2 NameID in the session
    # after a token login from Signup into Dashboard.
    subject_id = _get_name_id(session)
    if subject_id is None:
        current_app.logger.warning(
            'The session does not contain '
            'the subject id for user {}'.format(user))
        session.clear()
        location = current_app.config.get('SAML2_LOGOUT_REDIRECT_URL')

    else:
        logouts = client.global_logout(subject_id)
        loresponse = logouts.values()[0]
        # loresponse is a dict for REDIRECT binding, and LogoutResponse for SOAP binding
        if isinstance(loresponse, LogoutResponse):
            if loresponse.status_ok():
                current_app.logger.debug('Performing local logout for {}'.format(user))
                session.clear()
                location = current_app.config.get('SAML2_LOGOUT_REDIRECT_URL')
                location = verify_relay_state(request.form.get('RelayState', location), location)
                return redirect(location)
            else:
                abort(500)
        headers_tuple = loresponse[1]['headers']
        location = headers_tuple[0][1]
{'fraction_non_alphanumeric': 0.08054439982150825, 'fraction_numerical': 0.006619068868064852, 'mean_word_length': 3.4674418604651165, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 4}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1187344', 'n_tokens_mistral': 4050, 'n_tokens_neox': 3857, 'n_words': 1204}