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)
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}
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)
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)
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)
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)
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)
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}' )
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])
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)
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] }
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
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
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)
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
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)
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')
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
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')
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, )
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)