Exemplo n.º 1
0
def _do_action():
    action_type = session.get('current_plugin')
    if not action_type:
        abort(403)

    plugin_obj = current_app.plugins[action_type]()
    old_format = 'user_oid' in session['current_action']
    action = Action(data=session['current_action'], old_format=old_format)
    try:
        data = plugin_obj.perform_step(action)
    except plugin_obj.ActionError as exc:
        return _aborted(action, exc)
    except plugin_obj.ValidationError as exc:
        errors = exc.args[0]
        current_app.logger.info(
            'Validation error {} for step {} of action {}'.format(
                errors, session['current_step'], action))
        session['current_step'] -= 1
        return error_response(payload={'errors': errors},
                              message=CommonMsg.form_errors)

    eppn = session.get('user_eppn')
    if session['total_steps'] == session['current_step']:
        current_app.logger.info(
            'Finished pre-login action {} for eppn {}'.format(
                action.action_type, eppn))
        return success_response(payload=dict(data=data),
                                message=ActionsMsg.action_completed)

    current_app.logger.info(
        'Performed step {} for action {} for eppn {}'.format(
            action.action_type, session['current_step'], eppn))
    session['current_step'] += 1
    return success_response(payload={'data': data}, message=None)
Exemplo n.º 2
0
def _do_action():
    action_type = session.get('current_plugin')
    if not action_type:
        abort(403)

    plugin_obj = current_app.plugins[action_type]()
    old_format = 'user_oid' in session['current_action']
    action = Action(data=session['current_action'], old_format=old_format)
    try:
        data = plugin_obj.perform_step(action)
    except plugin_obj.ActionError as exc:
        return _aborted(action, exc)
    except plugin_obj.ValidationError as exc:
        errors = exc.args[0]
        current_app.logger.info('Validation error {} for step {} of action {}'.format(
            errors, session['current_step'], action))
        session['current_step'] -= 1
        return {
            '_status': 'error',
            'errors': errors,
        }

    eppn = session.get('user_eppn')
    if session['total_steps'] == session['current_step']:
        current_app.logger.info('Finished pre-login action {} for eppn {}'.format(action.action_type, eppn))
        return {
            'message': 'actions.action-completed',
            'data': data,
        }

    current_app.logger.info('Performed step {} for action {} for eppn {}'.format(
        action.action_type, session['current_step'], eppn))
    session['current_step'] += 1
    return {'data': data}
Exemplo n.º 3
0
def verify_token(user, credential_id):
    current_app.logger.debug('verify-token called with credential_id: {}'.format(credential_id))
    redirect_url = urlappend(current_app.config['DASHBOARD_URL'], 'security')

    # Check if requested key id is a mfa token and if the user used that to log in
    token_to_verify = user.credentials.filter(FidoCredential).find(credential_id)
    if not token_to_verify:
        return redirect_with_msg(redirect_url, ':ERROR:eidas.token_not_found')
    if token_to_verify.key not in session.get('eduidIdPCredentialsUsed', []):
        # If token was not used for login, reauthn the user
        current_app.logger.info('Token {} not used for login, redirecting to idp'.format(token_to_verify.key))
        ts_url = current_app.config.get('TOKEN_SERVICE_URL')
        reauthn_url = urlappend(ts_url, 'reauthn')
        next_url = url_for('eidas.verify_token', credential_id=credential_id, _external=True)
        # Add idp arg to next_url if set
        idp = request.args.get('idp')
        if idp:
            next_url = '{}?idp={}'.format(next_url, idp)
        return redirect('{}?next={}'.format(reauthn_url, next_url))

    # Set token key id in session
    session['verify_token_action_credential_id'] = credential_id

    # Request a authentication from idp
    required_loa = 'loa3'
    return _authn('token-verify-action', required_loa, force_authn=True)
Exemplo n.º 4
0
def verify_token(user, credential_id) -> Union[FluxData, WerkzeugResponse]:
    current_app.logger.debug('verify-token called with credential_id: {}'.format(credential_id))
    redirect_url = current_app.config.token_verify_redirect_url

    # Check if requested key id is a mfa token and if the user used that to log in
    token_to_verify = user.credentials.filter(FidoCredential).find(credential_id)
    if not token_to_verify:
        return redirect_with_msg(redirect_url, EidasMsg.token_not_found)
    if token_to_verify.key not in session.get('eduidIdPCredentialsUsed', []):
        # If token was not used for login, reauthn the user
        current_app.logger.info('Token {} not used for login, redirecting to idp'.format(token_to_verify.key))
        ts_url = current_app.config.token_service_url
        reauthn_url = urlappend(ts_url, 'reauthn')
        next_url = url_for('eidas.verify_token', credential_id=credential_id, _external=True)
        # Add idp arg to next_url if set
        idp = request.args.get('idp')
        if idp:
            next_url = '{}?idp={}'.format(next_url, idp)
        return redirect('{}?next={}'.format(reauthn_url, next_url))

    # Set token key id in session
    session['verify_token_action_credential_id'] = credential_id

    # Request a authentication from idp
    required_loa = 'loa3'
    return _authn('token-verify-action', required_loa, force_authn=True)
Exemplo n.º 5
0
def get_actions():
    user = current_app.central_userdb.get_user_by_eppn(session.get('user_eppn'))
    actions = get_next_action(user)
    if not actions['action']:
        return json.dumps({'action': False,
                           'url': actions['idp_url'],
                           'payload': {
                               'csrf_token': session.new_csrf_token()
                               }
                           })
    action_type = session['current_plugin']
    plugin_obj = current_app.plugins[action_type]()
    old_format = 'user_oid' in session['current_action']
    action = Action(data=session['current_action'], old_format=old_format)
    current_app.logger.info('Starting pre-login action {} '
                            'for user {}'.format(action.action_type, user))
    try:
        url = plugin_obj.get_url_for_bundle(action)
        return json.dumps({'action': True,
                           'url': url,
                           'payload': {
                               'csrf_token': session.new_csrf_token()
                               }})
    except plugin_obj.ActionError as exc:
        _aborted(action, exc)
        abort(500)
Exemplo n.º 6
0
def get_actions():
    user = current_app.central_userdb.get_user_by_eppn(
        session.get('user_eppn'))
    actions = get_next_action(user)
    if not actions['action']:
        return json.dumps({
            'action': False,
            'url': actions['idp_url'],
            'payload': {
                'csrf_token': session.new_csrf_token()
            }
        })
    action_type = session['current_plugin']
    plugin_obj = current_app.plugins[action_type]()
    old_format = 'user_oid' in session['current_action']
    action = Action(data=session['current_action'], old_format=old_format)
    current_app.logger.info('Starting pre-login action {} '
                            'for user {}'.format(action.action_type, user))
    try:
        url = plugin_obj.get_url_for_bundle(action)
        return json.dumps({
            'action': True,
            'url': url,
            'payload': {
                'csrf_token': session.new_csrf_token()
            }
        })
    except plugin_obj.ActionError as exc:
        _aborted(action, exc)
        abort(500)
Exemplo n.º 7
0
def logout_service():
    """SAML Logout Response endpoint
    The IdP will send the logout response to this view,
    which will process it with pysaml2 help and log the user
    out.
    Note that the IdP can request a logout even when
    we didn't initiate the process as a single logout
    request started by another SP.
    """
    current_app.logger.debug('Logout service started')

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

    logout_redirect_url = current_app.config.saml2_logout_redirect_url
    next_page = session.get('next', logout_redirect_url)
    next_page = request.args.get('next', next_page)
    next_page = request.form.get('RelayState', next_page)
    next_page = verify_relay_state(next_page, logout_redirect_url)

    if 'SAMLResponse' in request.form:  # we started the logout
        current_app.logger.debug('Receiving a logout response from the IdP')
        response = client.parse_logout_request_response(
            request.form['SAMLResponse'], BINDING_HTTP_REDIRECT)
        state.sync()
        if response and response.status_ok():
            session.clear()
            return redirect(next_page)
        else:
            current_app.logger.error('Unknown error during the logout')
            abort(400)

    # logout started by the IdP
    elif 'SAMLRequest' in request.form:
        current_app.logger.debug('Receiving a logout request from the IdP')
        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 {0} '
                'Performing local logout'.format(
                    session['eduPersonPrincipalName']))
            session.clear()
            return redirect(next_page)
        else:
            http_info = client.handle_logout_request(
                request.form['SAMLRequest'],
                subject_id,
                BINDING_HTTP_REDIRECT,
                relay_state=request.form['RelayState'])
            state.sync()
            location = get_location(http_info)
            session.clear()
            return redirect(location)
    current_app.logger.error('No SAMLResponse or SAMLRequest parameter found')
    abort(400)
Exemplo n.º 8
0
def account_terminated(user):
    """
    The account termination action,
    removes all credentials for the terminated account
    from the VCCS service,
    flags the account as terminated,
    sends an email to the address in the terminated account,
    and logs out the session.

    :type user: eduid_userdb.user.User
    """
    security_user = SecurityUser.from_user(user, current_app.private_userdb)
    authn_ts = session.get('reauthn-for-termination', None)
    if authn_ts is None:
        abort(400)

    now = datetime.utcnow()
    delta = now - datetime.fromtimestamp(authn_ts)

    if int(delta.total_seconds()) > 600:
        return error_response(message=SecurityMsg.stale_reauthn)

    del session['reauthn-for-termination']

    # revoke all user passwords
    revoke_all_credentials(current_app.config.vccs_url, security_user)
    # Skip removing old passwords from the user at this point as a password reset will do that anyway.
    # This fixes the problem with loading users for a password reset as users without passwords triggers
    # the UserHasNotCompletedSignup check in eduid-userdb.
    # TODO: Needs a decision on how to handle unusable user passwords
    # for p in security_user.credentials.filter(Password).to_list():
    #    security_user.passwords.remove(p.key)

    # flag account as terminated
    security_user.terminated = True
    try:
        save_and_sync_user(security_user)
    except UserOutOfSync:
        return error_response(message=CommonMsg.out_of_sync)

    current_app.stats.count(name='security_account_terminated', value=1)
    current_app.logger.info('Terminated user account')

    # email the user
    try:
        send_termination_mail(security_user)
    except MsgTaskFailed as e:
        current_app.logger.error(
            f'Failed to send account termination mail: {e}')
        current_app.logger.error(
            'Account will be terminated successfully anyway.')

    current_app.logger.debug(f'Logging out (terminated) user {user}')
    return redirect(
        f'{current_app.config.logout_endpoint}?next={current_app.config.termination_redirect_url}'
    )
Exemplo n.º 9
0
def _aborted(action, exc):
    eppn = session.get('user_eppn')
    current_app.logger.info(u'Aborted pre-login action {} for eppn {}, '
                            u'reason: {}'.format(action.action_type, eppn,
                                                 exc.args[0]))
    if exc.remove_action:
        aid = action.action_id
        msg = 'Removing faulty action with id '
        current_app.logger.info(msg + str(aid))
        current_app.actions_db.remove_action_by_id(aid)
    return error_response(message=exc.args[0])
Exemplo n.º 10
0
def account_terminated(user):
    """
    The account termination action,
    removes all credentials for the terminated account
    from the VCCS service,
    flags the account as terminated,
    sends an email to the address in the terminated account,
    and logs out the session.

    :type user: eduid_userdb.user.User
    """
    security_user = SecurityUser.from_user(user, current_app.private_userdb)
    authn_ts = session.get('reauthn-for-termination', None)
    if authn_ts is None:
        abort(400)

    now = datetime.utcnow()
    delta = now - datetime.fromtimestamp(authn_ts)

    if int(delta.total_seconds()) > 600:
        return error_message('security.stale_authn_info')

    del session['reauthn-for-termination']

    # revoke all user passwords
    revoke_all_credentials(current_app.config.get('VCCS_URL'), security_user)
    # Skip removing old passwords from the user at this point as a password reset will do that anyway.
    # This fixes the problem with loading users for a password reset as users without passwords triggers
    # the UserHasNotCompletedSignup check in eduid-userdb.
    # TODO: Needs a decision on how to handle unusable user passwords
    #for p in security_user.credentials.filter(Password).to_list():
    #    security_user.passwords.remove(p.key)

    # flag account as terminated
    security_user.terminated = True
    try:
        save_and_sync_user(security_user)
    except UserOutOfSync:
        return error_message('user-out-of-sync')

    current_app.stats.count(name='security_account_terminated', value=1)
    current_app.logger.info('Terminated user account')

    # email the user
    send_termination_mail(security_user)

    session.invalidate()
    current_app.logger.info('Invalidated session for user')

    site_url = current_app.config.get("EDUID_SITE_URL")
    current_app.logger.info('Redirection user to user {}'.format(site_url))
    # TODO: Add a account termination completed view to redirect to
    return redirect(site_url)
Exemplo n.º 11
0
def _aborted(action, exc):
    eppn = session.get('user_eppn')
    current_app.logger.info(u'Aborted pre-login action {} for eppn {}, '
                            u'reason: {}'.format(action.action_type,
                                                 eppn, exc.args[0]))
    if exc.remove_action:
        aid = action.action_id
        msg = 'Removing faulty action with id '
        current_app.logger.info(msg + str(aid))
        current_app.actions_db.remove_action_by_id(aid)
    return {
            '_status': 'error',
            'message': exc.args[0]
            }
Exemplo n.º 12
0
def compile_credential_list(user: ResetPasswordUser) -> list:
    """
    :return: List of augmented credentials
    """
    credentials = []
    authn_info = current_app.authninfo_db.get_authn_info(user)
    credentials_used = session.get('eduidIdPCredentialsUsed', list())
    # In the development environment credentials_used gets set to None
    if credentials_used is None:
        credentials_used = []
    for credential in user.credentials.to_list():
        credential_dict = credential.to_dict()
        credential_dict['key'] = credential.key
        if credential.key in credentials_used:
            credential_dict['used_for_login'] = True
        if credential.is_verified:
            credential_dict['verified'] = True
        credential_dict.update(authn_info[credential.key])
        credentials.append(credential_dict)
    return credentials
Exemplo n.º 13
0
def change_password(user, old_password, new_password):
    """
    View to change the password
    """
    security_user = SecurityUser.from_user(user, current_app.private_userdb)
    authn_ts = session.get('reauthn-for-chpass', None)
    if authn_ts is None:
        return error_message('chpass.no_reauthn')

    now = datetime.utcnow()
    delta = now - datetime.fromtimestamp(authn_ts)
    timeout = current_app.config.get('CHPASS_TIMEOUT', 600)
    if int(delta.total_seconds()) > timeout:
        return error_message('chpass.stale_reauthn')

    vccs_url = current_app.config.get('VCCS_URL')
    added = add_credentials(vccs_url, old_password, new_password, security_user, source='security')

    if not added:
        current_app.logger.debug('Problem verifying the old credentials for {}'.format(user))
        return error_message('chpass.unable-to-verify-old-password')

    security_user.terminated = False
    try:
        save_and_sync_user(security_user)
    except UserOutOfSync:
        return error_message('user-out-of-sync')

    del session['reauthn-for-chpass']

    current_app.stats.count(name='security_password_changed', value=1)
    current_app.logger.info('Changed password for user {}'.format(security_user.eppn))

    next_url = current_app.config.get('DASHBOARD_URL', '/profile')
    credentials = {
        'next_url': next_url,
        'credentials': compile_credential_list(security_user),
        'message': 'chpass.password-changed'
        }

    return CredentialList().dump(credentials).data
Exemplo n.º 14
0
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')

    next = request.args.get('next', '')
    location = next or current_app.config.saml2_logout_redirect_url

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

    user = current_app.central_userdb.get_user_by_eppn(eppn)

    current_app.logger.debug('Logout process started for user {}'.format(user))

    return saml_logout(current_app, user, location)
Exemplo n.º 15
0
def compile_credential_list(security_user):
    """
    :param security_user: User
    :type security_user: eduid_userdb.security.SecurityUser
    :return: List of augmented credentials
    :rtype: list
    """
    credentials = []
    authn_info = current_app.authninfo_db.get_authn_info(security_user)
    credentials_used = session.get('eduidIdPCredentialsUsed', list())
    # In the development environment credentials_used gets set to None
    if credentials_used is None:
        credentials_used = []
    for credential in security_user.credentials.to_list():
        credential_dict = credential.to_dict()
        credential_dict['key'] = credential.key
        if credential.key in credentials_used:
            credential_dict['used_for_login'] = True
        if credential.is_verified:
            credential_dict['verified'] = True
        credential_dict.update(authn_info[credential.key])
        credentials.append(credential_dict)
    return credentials
Exemplo n.º 16
0
def compile_credential_list(security_user):
    """
    :param security_user: User
    :type security_user: eduid_userdb.security.SecurityUser
    :return: List of augmented credentials
    :rtype: list
    """
    credentials = []
    authn_info = current_app.authninfo_db.get_authn_info(security_user)
    credentials_used = session.get('eduidIdPCredentialsUsed', list())
    # In the development environment credentials_used gets set to None
    if credentials_used is None:
        credentials_used = []
    for credential in security_user.credentials.to_list():
        credential_dict = credential.to_dict()
        credential_dict['key'] = credential.key
        if credential.key in credentials_used:
            credential_dict['used_for_login'] = True
        if credential.is_verified:
            credential_dict['verified'] = True
        credential_dict.update(authn_info[credential.key])
        credentials.append(credential_dict)
    return credentials
Exemplo n.º 17
0
    def perform_step(self, action):
        current_app.logger.debug('Performing MFA step')
        if current_app.config.mfa_testing:
            current_app.logger.debug('Test mode is on, faking authentication')
            return {
                'success': True,
                'testing': True,
            }

        if action.old_format:
            userid = action.user_id
            user = current_app.central_userdb.get_user_by_id(
                userid, raise_on_missing=False)
        else:
            eppn = action.eppn
            user = current_app.central_userdb.get_user_by_eppn(
                eppn, raise_on_missing=False)
        current_app.logger.debug(
            'Loaded User {} from db (in perform_action)'.format(user))

        # Third party service MFA
        if session.mfa_action.success is True:  # Explicit check that success is the boolean True
            issuer = session.mfa_action.issuer
            authn_instant = session.mfa_action.authn_instant
            authn_context = session.mfa_action.authn_context
            current_app.logger.info(
                'User {} logged in using external mfa service {}'.format(
                    user, issuer))
            action.result = {
                'success': True,
                'issuer': issuer,
                'authn_instant': authn_instant,
                'authn_context': authn_context,
            }
            current_app.actions_db.update_action(action)
            # Clear mfa_action from session
            del session.mfa_action
            return action.result

        req_json = request.get_json()
        if not req_json:
            current_app.logger.error(
                'No data in request to authn {}'.format(user))
            raise self.ActionError(ActionsMsg.no_data)

        # Process POSTed data
        if 'tokenResponse' in req_json:
            # CTAP1/U2F
            token_response = request.get_json().get('tokenResponse', '')
            current_app.logger.debug(
                'U2F token response: {}'.format(token_response))

            challenge = session.get(self.PACKAGE_NAME + '.u2f.challenge')
            current_app.logger.debug('Challenge: {!r}'.format(challenge))

            result = fido_tokens.verify_u2f(user, challenge, token_response)

            if result is not None:
                action.result = result
                current_app.actions_db.update_action(action)
                return action.result

        elif 'authenticatorData' in req_json:
            # CTAP2/Webauthn
            try:
                result = fido_tokens.verify_webauthn(user, req_json,
                                                     self.PACKAGE_NAME)
            except fido_tokens.VerificationProblem as exc:
                raise self.ActionError(exc.msg)

            action.result = result
            current_app.actions_db.update_action(action)
            return action.result

        else:
            current_app.logger.error(
                'Neither U2F nor Webauthn data in request to authn {}'.format(
                    user))
            current_app.logger.debug('Request: {}'.format(req_json))
            raise self.ActionError(ActionsMsg.no_response)

        raise self.ActionError(ActionsMsg.unknown_token)
Exemplo n.º 18
0
    def perform_step(self, action):
        current_app.logger.debug('Performing MFA step')
        if current_app.config['MFA_TESTING']:
            current_app.logger.debug('Test mode is on, faking authentication')
            return {
                'success': True,
                'testing': True,
            }

        if action.old_format:
            userid = action.user_id
            user = current_app.central_userdb.get_user_by_id(
                userid, raise_on_missing=False)
        else:
            eppn = action.eppn
            user = current_app.central_userdb.get_user_by_eppn(
                eppn, raise_on_missing=False)
        current_app.logger.debug(
            'Loaded User {} from db (in perform_action)'.format(user))

        # Third party service MFA
        if session.mfa_action.success is True:  # Explicit check that success is the boolean True
            issuer = session.mfa_action.issuer
            authn_instant = session.mfa_action.authn_instant
            authn_context = session.mfa_action.authn_context
            current_app.logger.info(
                'User {} logged in using external mfa service {}'.format(
                    user, issuer))
            action.result = {
                'success': True,
                'issuer': issuer,
                'authn_instant': authn_instant,
                'authn_context': authn_context
            }
            current_app.actions_db.update_action(action)
            return action.result

        req_json = request.get_json()
        if not req_json:
            current_app.logger.error(
                'No data in request to authn {}'.format(user))
            raise self.ActionError('mfa.no-request-data')

        # Process POSTed data
        if 'tokenResponse' in req_json:
            # CTAP1/U2F
            token_response = request.get_json().get('tokenResponse', '')
            current_app.logger.debug(
                'U2F token response: {}'.format(token_response))

            challenge = session.get(self.PACKAGE_NAME + '.u2f.challenge')
            current_app.logger.debug('Challenge: {!r}'.format(challenge))

            device, counter, touch = complete_authentication(
                challenge, token_response,
                current_app.config['U2F_VALID_FACETS'])
            current_app.logger.debug('U2F authentication data: {}'.format({
                'keyHandle':
                device['keyHandle'],
                'touch':
                touch,
                'counter':
                counter,
            }))

            for this in user.credentials.filter(U2F).to_list():
                if this.keyhandle == device['keyHandle']:
                    current_app.logger.info(
                        'User {} logged in using U2F token {} (touch: {}, counter {})'
                        .format(user, this, touch, counter))
                    action.result = {
                        'success': True,
                        'touch': touch,
                        'counter': counter,
                        RESULT_CREDENTIAL_KEY_NAME: this.key,
                    }
                    current_app.actions_db.update_action(action)
                    return action.result
        elif 'authenticatorData' in req_json:
            # CTAP2/Webauthn
            req = {}
            for this in [
                    'credentialId', 'clientDataJSON', 'authenticatorData',
                    'signature'
            ]:
                try:
                    req_json[this] += ('=' * (len(req_json[this]) % 4))
                    req[this] = base64.urlsafe_b64decode(req_json[this])
                except:
                    current_app.logger.error(
                        'Failed to find/b64decode Webauthn parameter {}: {}'.
                        format(this, req_json.get(this)))
                    raise self.ActionError(
                        'mfa.bad-token-response'
                    )  # XXX add bad-token-response to frontend
            current_app.logger.debug(
                'Webauthn request after decoding:\n{}'.format(
                    pprint.pformat(req)))
            client_data = ClientData(req['clientDataJSON'])
            auth_data = AuthenticatorData(req['authenticatorData'])

            credentials = _get_user_credentials(user)
            fido2state = json.loads(session[self.PACKAGE_NAME +
                                            '.webauthn.state'])

            rp_id = current_app.config['FIDO2_RP_ID']
            fido2rp = RelyingParty(rp_id, 'eduID')
            fido2server = _get_fido2server(credentials, fido2rp)
            matching_credentials = [
                (v['webauthn'], k) for k, v in credentials.items()
                if v['webauthn'].credential_id == req['credentialId']
            ]

            if not matching_credentials:
                current_app.logger.error(
                    'Could not find webauthn credential {} on user {}'.format(
                        req['credentialId'], user))
                raise self.ActionError('mfa.unknown-token')

            authn_cred = fido2server.authenticate_complete(
                fido2state,
                [mc[0] for mc in matching_credentials],
                req['credentialId'],
                client_data,
                auth_data,
                req['signature'],
            )
            current_app.logger.debug(
                'Authenticated Webauthn credential: {}'.format(authn_cred))

            cred_key = [mc[1] for mc in matching_credentials][0]

            touch = auth_data.flags
            counter = auth_data.counter
            current_app.logger.info(
                'User {} logged in using Webauthn token {} (touch: {}, counter {})'
                .format(user, cred_key, touch, counter))
            action.result = {
                'success':
                True,
                'touch':
                auth_data.is_user_present() or auth_data.is_user_verified(),
                'user_present':
                auth_data.is_user_present(),
                'user_verified':
                auth_data.is_user_verified(),
                'counter':
                counter,
                RESULT_CREDENTIAL_KEY_NAME:
                cred_key,
            }
            current_app.actions_db.update_action(action)
            return action.result

        else:
            current_app.logger.error(
                'Neither U2F nor Webauthn data in request to authn {}'.format(
                    user))
            current_app.logger.debug('Request: {}'.format(req_json))
            raise self.ActionError('mfa.no-token-response')

        raise self.ActionError('mfa.unknown-token')
Exemplo n.º 19
0
def change_password(user):
    """
    View to change the password
    """
    security_user = SecurityUser.from_user(user, current_app.private_userdb)
    min_entropy = current_app.config.password_entropy
    schema = ChangePasswordSchema(zxcvbn_terms=get_zxcvbn_terms(
        security_user.eppn),
                                  min_entropy=int(min_entropy))

    if not request.data:
        return error_response(message='chpass.no-data')

    try:
        form = schema.load(json.loads(request.data))
        current_app.logger.debug(form)
    except ValidationError as e:
        current_app.logger.error(e)
        return error_response(message='chpass.weak-password')
    else:
        old_password = form.get('old_password')
        new_password = form.get('new_password')

    if session.get_csrf_token() != form['csrf_token']:
        return error_response(message='csrf.try_again')

    authn_ts = session.get('reauthn-for-chpass', None)
    if authn_ts is None:
        return error_response(message='chpass.no_reauthn')

    now = datetime.utcnow()
    delta = now - datetime.fromtimestamp(authn_ts)
    timeout = current_app.config.chpass_timeout
    if int(delta.total_seconds()) > timeout:
        return error_response(message='chpass.stale_reauthn')

    vccs_url = current_app.config.vccs_url
    added = add_credentials(vccs_url,
                            old_password,
                            new_password,
                            security_user,
                            source='security')

    if not added:
        current_app.logger.debug(
            'Problem verifying the old credentials for {}'.format(user))
        return error_response(message='chpass.unable-to-verify-old-password')

    security_user.terminated = False
    try:
        save_and_sync_user(security_user)
    except UserOutOfSync:
        return error_response(message='user-out-of-sync')

    del session['reauthn-for-chpass']

    current_app.stats.count(name='security_password_changed', value=1)
    current_app.logger.info('Changed password for user {}'.format(
        security_user.eppn))

    next_url = current_app.config.dashboard_url
    credentials = {
        'next_url': next_url,
        'credentials': compile_credential_list(security_user),
        'message': 'chpass.password-changed',
    }

    return credentials
Exemplo n.º 20
0
    def perform_step(self, action):
        current_app.logger.debug('Performing MFA step')
        if current_app.config['MFA_TESTING']:
            current_app.logger.debug('Test mode is on, faking authentication')
            return {
                'success': True,
                'testing': True,
            }

        if action.old_format:
            userid = action.user_id
            user = current_app.central_userdb.get_user_by_id(userid, raise_on_missing=False)
        else:
            eppn = action.eppn
            user = current_app.central_userdb.get_user_by_eppn(eppn, raise_on_missing=False)
        current_app.logger.debug('Loaded User {} from db (in perform_action)'.format(user))

        # Third party service MFA
        if session.mfa_action.success is True:  # Explicit check that success is the boolean True
            issuer = session.mfa_action.issuer
            authn_instant = session.mfa_action.authn_instant
            authn_context = session.mfa_action.authn_context
            current_app.logger.info('User {} logged in using external mfa service {}'.format(user, issuer))
            action.result = {
                'success': True,
                'issuer': issuer,
                'authn_instant': authn_instant,
                'authn_context': authn_context
            }
            current_app.actions_db.update_action(action)
            return action.result

        req_json = request.get_json()
        if not req_json:
            current_app.logger.error('No data in request to authn {}'.format(user))
            raise self.ActionError('mfa.no-request-data')

        # Process POSTed data
        if 'tokenResponse' in req_json:
            # CTAP1/U2F
            token_response = request.get_json().get('tokenResponse', '')
            current_app.logger.debug('U2F token response: {}'.format(token_response))

            challenge = session.get(self.PACKAGE_NAME + '.u2f.challenge')
            current_app.logger.debug('Challenge: {!r}'.format(challenge))

            device, counter, touch = complete_authentication(challenge, token_response,
                                                             current_app.config['U2F_VALID_FACETS'])
            current_app.logger.debug('U2F authentication data: {}'.format({
                'keyHandle': device['keyHandle'],
                'touch': touch,
                'counter': counter,
            }))

            for this in user.credentials.filter(U2F).to_list():
                if this.keyhandle == device['keyHandle']:
                    current_app.logger.info('User {} logged in using U2F token {} (touch: {}, counter {})'.format(
                        user, this, touch, counter))
                    action.result = {'success': True,
                                     'touch': touch,
                                     'counter': counter,
                                     RESULT_CREDENTIAL_KEY_NAME: this.key,
                                     }
                    current_app.actions_db.update_action(action)
                    return action.result
        elif 'authenticatorData' in req_json:
            # CTAP2/Webauthn
            req = {}
            for this in ['credentialId', 'clientDataJSON', 'authenticatorData', 'signature']:
                try:
                    req_json[this] += ('=' * (len(req_json[this]) % 4))
                    req[this] = base64.urlsafe_b64decode(req_json[this])
                except:
                    current_app.logger.error('Failed to find/b64decode Webauthn parameter {}: {}'.format(
                        this, req_json.get(this)))
                    raise self.ActionError('mfa.bad-token-response')  # XXX add bad-token-response to frontend
            current_app.logger.debug('Webauthn request after decoding:\n{}'.format(pprint.pformat(req)))
            client_data = ClientData(req['clientDataJSON'])
            auth_data = AuthenticatorData(req['authenticatorData'])

            credentials = _get_user_credentials(user)
            fido2state = json.loads(session[self.PACKAGE_NAME + '.webauthn.state'])

            rp_id = current_app.config['FIDO2_RP_ID']
            fido2rp = RelyingParty(rp_id, 'eduID')
            fido2server = _get_fido2server(credentials, fido2rp)
            matching_credentials = [(v['webauthn'], k) for k,v in credentials.items()
                                    if v['webauthn'].credential_id == req['credentialId']]

            if not matching_credentials:
                current_app.logger.error('Could not find webauthn credential {} on user {}'.format(
                    req['credentialId'], user))
                raise self.ActionError('mfa.unknown-token')

            authn_cred = fido2server.authenticate_complete(
                fido2state,
                [mc[0] for mc in matching_credentials],
                req['credentialId'],
                client_data,
                auth_data,
                req['signature'],
            )
            current_app.logger.debug('Authenticated Webauthn credential: {}'.format(authn_cred))

            cred_key = [mc[1] for mc in matching_credentials][0]

            touch = auth_data.flags
            counter = auth_data.counter
            current_app.logger.info('User {} logged in using Webauthn token {} (touch: {}, counter {})'.format(
                user, cred_key, touch, counter))
            action.result = {'success': True,
                             'touch': auth_data.is_user_present() or auth_data.is_user_verified(),
                             'user_present': auth_data.is_user_present(),
                             'user_verified': auth_data.is_user_verified(),
                             'counter': counter,
                             RESULT_CREDENTIAL_KEY_NAME: cred_key,
                             }
            current_app.actions_db.update_action(action)
            return action.result

        else:
            current_app.logger.error('Neither U2F nor Webauthn data in request to authn {}'.format(user))
            current_app.logger.debug('Request: {}'.format(req_json))
            raise self.ActionError('mfa.no-token-response')

        raise self.ActionError('mfa.unknown-token')
Exemplo n.º 21
0
def change_password_view(user: User, old_password: str,
                         new_password: str) -> FluxData:
    """
    View to change the password
    """
    if not old_password or not new_password:
        return error_response(message=ResetPwMsg.chpass_no_data)

    min_entropy = current_app.config.password_entropy
    try:
        is_valid_password(new_password,
                          user_info=get_zxcvbn_terms(user.eppn),
                          min_entropy=min_entropy)
    except ValueError:
        return error_response(message=ResetPwMsg.chpass_weak)

    authn_ts = session.get('reauthn-for-chpass', None)
    if authn_ts is None:
        return error_response(message=ResetPwMsg.no_reauthn)

    now = datetime.utcnow()
    delta = now - datetime.fromtimestamp(authn_ts)
    timeout = current_app.config.chpass_timeout
    if int(delta.total_seconds()) > timeout:
        return error_response(message=ResetPwMsg.stale_reauthn)

    hashed = session.reset_password.generated_password_hash
    if check_password(new_password, hashed):
        is_generated = True
        current_app.stats.count(name='change_password_generated_password_used')
    else:
        is_generated = False
        current_app.stats.count(name='change_password_custom_password_used')

    resetpw_user = ResetPasswordUser.from_user(user,
                                               current_app.private_userdb)

    vccs_url = current_app.config.vccs_url
    added = change_password(resetpw_user, new_password, old_password,
                            'reset-password', is_generated, vccs_url)

    if not added:
        current_app.logger.debug(
            f'Problem verifying the old credentials for {user}')
        return error_response(message=ResetPwMsg.unrecognized_pw)

    resetpw_user.terminated = False
    try:
        save_and_sync_user(resetpw_user)
    except UserOutOfSync:
        return error_response(message=CommonMsg.out_of_sync)

    del session['reauthn-for-chpass']

    current_app.stats.count(name='security_password_changed', value=1)
    current_app.logger.info(f'Changed password for user {resetpw_user.eppn}')

    next_url = current_app.config.dashboard_url
    return success_response(
        payload={
            'next_url': next_url,
            'credentials': compile_credential_list(resetpw_user),
            'message': ResetPwMsg.chpass_password_changed,
        },
        message=ResetPwMsg.chpass_password_changed,
    )
Exemplo n.º 22
0
def set_new_pw_extra_security_token(
    code: str,
    password: str,
    tokenResponse: Optional[str] = None,
    authenticatorData: Optional[str] = None,
    clientDataJSON: Optional[str] = None,
    credentialId: Optional[str] = None,
    signature: Optional[str] = None,
) -> FluxData:
    """
    View that receives an emailed reset password code, hw token data,
    and a password, and sets the password as credential for the user, with
    extra security.

    Preconditions required for the call to succeed:
    * A PasswordResetEmailAndTokenState object in the password_reset_state_db
      keyed by the received code.
    * A flag in said state object indicating that the emailed code has already
      been verified.

    As side effects, this view will:
    * Compare the received password with the hash in the session to mark
      it accordingly (as suggested or as custom);
    * Revoke all password credentials the user had;

    This operation may fail due to:
    * The codes do not correspond to a valid state in the db;
    * Any of the codes have expired;
    * No valid user corresponds to the eppn stored in the state;
    * Communication problems with the VCCS backend;
    * Synchronization problems with the central user db.
    """
    data = _load_data(code, password)
    if data.error:
        return error_response(message=data.error)

    # Process POSTed data
    success = False
    if tokenResponse:
        # CTAP1/U2F
        token_response = request.get_json().get('tokenResponse', '')
        current_app.logger.debug(f'U2F token response: {token_response}')

        _challenge = session.get(SESSION_PREFIX + '.u2f.challenge')
        if not isinstance(_challenge, bytes):
            raise TypeError(
                f'U2F challenge in session is not bytes {repr(_challenge)}')
        current_app.logger.debug(f'Challenge: {_challenge!r}')

        result = fido_tokens.verify_u2f(data.user, _challenge, token_response)

        if result is not None:
            success = result['success']

    elif not success and authenticatorData:
        # CTAP2/Webauthn
        try:
            result = fido_tokens.verify_webauthn(
                data.user,
                dict(
                    credentialId=credentialId,
                    clientDataJSON=clientDataJSON,
                    authenticatorData=authenticatorData,
                    signature=signature,
                ),
                SESSION_PREFIX,
            )
        except fido_tokens.VerificationProblem:
            pass
        else:
            success = result['success']

    else:
        current_app.logger.error(
            f'Neither U2F nor Webauthn data in request to authn {data.user}')

    if not success:
        return error_response(message=ResetPwMsg.fido_token_fail)

    current_app.logger.info(f'Resetting password for user {data.user}')
    reset_user_password(data.user, data.state, password)
    current_app.logger.info(
        f'Password reset done, removing state for {data.user}')
    current_app.password_reset_state_db.remove_state(data.state)
    return success_response(message=ResetPwMsg.pw_resetted)