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 static_url(filename): url = app.config.get('STATIC_URL') if url: return urlappend(url, filename) # If STATIC_URL is not set use Flask default return url_for('static', filename=filename)
def __init__(self, name: str, config: dict, **kwargs): # Initialise type of self.config before any parent class sets a precedent to mypy self.config = SupportConfig.init_config(ns='webapp', app_name=name, test_config=config) super().__init__(name, **kwargs) # cast self.config because sometimes mypy thinks it is a FlaskConfig after super().__init__() self.config: SupportConfig = cast(SupportConfig, self.config) # type: ignore if self.config.token_service_url_logout is None: self.config.token_service_url_logout = urlappend( self.config.token_service_url, 'logout') from eduid_webapp.support.views import support_views self.register_blueprint(support_views) self.support_user_db = db.SupportUserDB(self.config.mongo_uri) self.support_authn_db = db.SupportAuthnInfoDB(self.config.mongo_uri) self.support_proofing_log_db = db.SupportProofingLogDB( self.config.mongo_uri) self.support_signup_db = db.SupportSignupUserDB(self.config.mongo_uri) self.support_actions_db = db.SupportActionsDB(self.config.mongo_uri) self.support_letter_proofing_db = db.SupportLetterProofingDB( self.config.mongo_uri) self.support_oidc_proofing_db = db.SupportOidcProofingDB( self.config.mongo_uri) self.support_email_proofing_db = db.SupportEmailProofingDB( self.config.mongo_uri) self.support_phone_proofing_db = db.SupportPhoneProofingDB( self.config.mongo_uri) register_template_funcs(self)
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 authorize(user): if user.orcid is None: proofing_state = current_app.proofing_statedb.get_state_by_eppn(user.eppn, raise_on_missing=False) if not proofing_state: current_app.logger.debug('No proofing state found for user {!s}. Initializing new proofing state.'.format( user)) proofing_state = OrcidProofingState({'eduPersonPrincipalName': user.eppn, 'state': get_unique_hash(), 'nonce': get_unique_hash()}) current_app.proofing_statedb.save(proofing_state) claims_request = ClaimsRequest(userinfo=Claims(id=None)) oidc_args = { 'client_id': current_app.oidc_client.client_id, 'response_type': 'code', 'scope': 'openid', 'claims': claims_request.to_json(), 'redirect_uri': url_for('orcid.authorization_response', _external=True), 'state': proofing_state.state, 'nonce': proofing_state.nonce, } authorization_url = '{}?{}'.format(current_app.oidc_client.authorization_endpoint, urlencode(oidc_args)) current_app.logger.debug('Authorization url: {!s}'.format(authorization_url)) current_app.stats.count(name='authn_request') return redirect(authorization_url) # Orcid already connected to user url = urlappend(current_app.config['DASHBOARD_URL'], 'accountlinking') scheme, netloc, path, query_string, fragment = urlsplit(url) new_query_string = urlencode({'msg': ':ERROR:orc.already_connected'}) url = urlunsplit((scheme, netloc, path, new_query_string, fragment)) return redirect(url)
def send_letter(request, user, nin): settings = request.registry.settings letter_url = settings.get('letter_service_url') send_letter_url = urlappend(letter_url, 'send-letter') data = {'eppn': user.eppn, 'nin': nin} response = requests.post(send_letter_url, data=data) result = 'error' msg = _('There was a problem with the letter service. ' 'Please try again later.') if response.status_code == 200: logger.info("Letter sent to user {!r}.".format(user)) # This log line moved here from letter_status function expires = response.json()['letter_expires'] expires = datetime.utcfromtimestamp(int(expires)) expires = expires.strftime('%Y-%m-%d') result = 'success' msg = _('A letter with a verification code has been sent to your ' 'official postal address. Please return to this page once you receive it.' ' The code will be valid until ${expires}.', mapping={'expires': expires}) return { 'result': result, 'message': get_localizer(request).translate(msg), }
def terminate_account(context, request): ''' Terminate account view. It receives a POST request, checks the csrf token, schedules the account termination action, and redirects to the IdP. ''' settings = request.registry.settings # check csrf csrf = sanitize_post_key(request, 'csrf') if csrf != request.session.get_csrf_token(): return HTTPBadRequest() ts_url = request.registry.settings.get('token_service_url') terminate_url = urlappend(ts_url, 'terminate') next_url = request.route_url('account-terminated') params = {'next': next_url} url_parts = list(urlparse.urlparse(terminate_url)) query = urlparse.parse_qs(url_parts[4]) query.update(params) url_parts[4] = urlencode(query) location = urlparse.urlunparse(url_parts) return HTTPFound(location=location)
def nin_verify_action(session_info, user): """ Use a Sweden Connect federation IdP assertion to verify a users identity. :param session_info: the SAML session info :param user: Central db user :type session_info: dict :type user: eduid_userdb.User :return: redirect response :rtype: Response """ redirect_url = urlappend(current_app.config['DASHBOARD_URL'], 'nins') if not is_required_loa(session_info, 'loa3'): return redirect_with_msg(redirect_url, ':ERROR:eidas.authn_context_mismatch') if not is_valid_reauthn(session_info): return redirect_with_msg(redirect_url, ':ERROR:eidas.reauthn_expired') proofing_user = ProofingUser.from_user(user, current_app.private_userdb) asserted_nin = get_saml_attribute(session_info, 'personalIdentityNumber')[0] if proofing_user.nins.verified.count != 0: current_app.logger.error('User already has a verified NIN') current_app.logger.debug('Primary NIN: {}. Asserted NIN: {}'.format(proofing_user.nins.primary.number, asserted_nin)) return redirect_with_msg(redirect_url, ':ERROR:eidas.nin_already_verified') # Create a proofing log issuer = session_info['issuer'] authn_context = get_authn_ctx(session_info) try: user_address = current_app.msg_relay.get_postal_address(asserted_nin) except MsgTaskFailed as e: current_app.logger.error('Navet lookup failed: {}'.format(e)) current_app.stats.count('navet_error') return redirect_with_msg(redirect_url, ':ERROR:error_navet_task') proofing_log_entry = SwedenConnectProofing(user=proofing_user, created_by='eduid-eidas', nin=asserted_nin, issuer=issuer, authn_context_class=authn_context, user_postal_address=user_address, proofing_version='2018v1') # Verify NIN for user try: nin_element = NinProofingElement(number=asserted_nin, application='eduid-eidas', verified=False) proofing_state = NinProofingState({'eduPersonPrincipalName': user.eppn, 'nin': nin_element.to_dict()}) verify_nin_for_user(user, proofing_state, proofing_log_entry) except AmTaskFailed as e: current_app.logger.error('Verifying NIN for user failed') current_app.logger.error('{}'.format(e)) return redirect_with_msg(redirect_url, ':ERROR:Temporary technical problems') current_app.stats.count(name='nin_verified') return redirect_with_msg(redirect_url, 'eidas.nin_verify_success')
def mfa_authentication_action(session_info: Mapping[str, Any], user: User) -> WerkzeugResponse: relay_state = request.form.get('RelayState') current_app.logger.debug('RelayState: {}'.format(relay_state)) redirect_url = None if 'eidas_redirect_urls' in session: redirect_url = session['eidas_redirect_urls'].pop(relay_state, None) if not redirect_url: # With no redirect url just redirect the user to dashboard for a new try to log in # TODO: This will result in a error 400 until we put the authentication in the session current_app.logger.error('Missing redirect url for mfa authentication') return redirect_with_msg(current_app.config.action_url, EidasMsg.no_redirect_url) # We get the mfa authentication views "next" argument as base64 to avoid our request sanitation # to replace all & to & redirect_url = base64.b64decode(redirect_url).decode('utf-8') # TODO: Rename verify_relay_state to verify_redirect_url redirect_url = verify_relay_state(redirect_url) if not is_required_loa(session_info, 'loa3'): return redirect_with_msg(redirect_url, EidasMsg.authn_context_mismatch) if not is_valid_reauthn(session_info): return redirect_with_msg(redirect_url, EidasMsg.reauthn_expired) # Check that a verified NIN is equal to the asserted attribute personalIdentityNumber _personal_idns = get_saml_attribute(session_info, 'personalIdentityNumber') if _personal_idns is None: current_app.logger.error( 'Got no personalIdentityNumber attributes. pysaml2 without the right attribute_converter?' ) # TODO: change to reasonable redirect_with_msg when the ENUM work for that is merged raise RuntimeError('Got no personalIdentityNumber') asserted_nin = _personal_idns[0] user_nin = user.nins.verified.find(asserted_nin) if not user_nin: current_app.logger.error('Asserted NIN not matching user verified nins') current_app.logger.debug('Asserted NIN: {}'.format(asserted_nin)) current_app.stats.count(name='mfa_auth_nin_not_matching') return redirect_with_msg(redirect_url, EidasMsg.nin_not_matching) session.mfa_action.success = True session.mfa_action.issuer = session_info['issuer'] session.mfa_action.authn_instant = session_info['authn_info'][0][2] session.mfa_action.authn_context = get_authn_ctx(session_info) current_app.stats.count(name='mfa_auth_success') current_app.stats.count(name=f'mfa_auth_{session_info["issuer"]}_success') # Redirect back to action app but to the redirect-action view resp = redirect_with_msg(redirect_url, EidasMsg.action_completed, error=False) scheme, netloc, path, query_string, fragment = urlsplit(resp.location) new_path = urlappend(path, 'redirect-action') new_url = urlunsplit((scheme, netloc, new_path, query_string, fragment)) current_app.logger.debug(f'Redirecting to: {new_url}') return redirect(new_url)
def support_init_app(name, config): """ Create an instance of an eduid support app. First, it will load the configuration from support.settings.common then any settings given in the `config` param. Then, the app instance will be updated with common stuff by `eduid_init_app`, and finally all needed blueprints will be registered with it. :param name: The name of the instance, it will affect the configuration loaded. :type name: str :param config: any additional configuration settings. Specially useful in test cases :type config: dict :return: the flask app :rtype: flask.Flask """ app = eduid_init_app(name, config) app.config.update(config) if app.config.get('TOKEN_SERVICE_URL_LOGOUT') is None: app.config['TOKEN_SERVICE_URL_LOGOUT'] = urlappend( app.config['TOKEN_SERVICE_URL'], 'logout') from eduid_webapp.support.views import support_views app.register_blueprint(support_views) app.support_user_db = db.SupportUserDB(app.config['MONGO_URI']) app.support_authn_db = db.SupportAuthnInfoDB(app.config['MONGO_URI']) app.support_proofing_log_db = db.SupportProofingLogDB( app.config['MONGO_URI']) app.support_signup_db = db.SupportSignupUserDB(app.config['MONGO_URI']) app.support_actions_db = db.SupportActionsDB(app.config['MONGO_URI']) app.support_letter_proofing_db = db.SupportLetterProofingDB( app.config['MONGO_URI']) app.support_oidc_proofing_db = db.SupportOidcProofingDB( app.config['MONGO_URI']) app.support_email_proofing_db = db.SupportEmailProofingDB( app.config['MONGO_URI']) app.support_phone_proofing_db = db.SupportPhoneProofingDB( app.config['MONGO_URI']) app = register_template_funcs(app) app.logger.info('Init {} app...'.format(name)) return app
def send_password_reset_mail(email_address: str): """ :param email_address: User input for password reset """ try: user = current_app.central_userdb.get_user_by_mail(email_address) except UserHasNotCompletedSignup: # Old bug where incomplete signup users where written to the central db current_app.logger.info( f"Cannot reset a password with the following " f"email address: {email_address}: incomplete user") raise BadCode(ResetPwMsg.invalid_user) except DocumentDoesNotExist: current_app.logger.info(f"Cannot reset a password with the following " f"unknown email address: {email_address}.") raise BadCode(ResetPwMsg.user_not_found) state = ResetPasswordEmailState(eppn=user.eppn, email_address=email_address, email_code=get_unique_hash()) current_app.password_reset_state_db.save(state) text_template = 'reset_password_email.txt.jinja2' html_template = 'reset_password_email.html.jinja2' to_addresses = [ address.email for address in user.mail_addresses.verified.to_list() ] pwreset_timeout = current_app.config.email_code_timeout // 60 // 60 # seconds to hours # We must send the user to an url that does not correspond to a flask view, # but to a js bundle (i.e. a flask view in a *different* app) resetpw_link = urlappend(current_app.config.password_reset_link, f"code/{state.email_code.code}") context = { 'reset_password_link': resetpw_link, 'password_reset_timeout': pwreset_timeout } subject = _('Reset password') try: send_mail(subject, to_addresses, text_template, html_template, current_app, context, state.reference) except MailTaskFailed as error: current_app.logger.error(f'Sending password reset e-mail for ' f'{email_address} failed: {error}') raise BadCode(ResetPwMsg.send_pw_failure) current_app.logger.info(f'Sent password reset email to user {user}') current_app.logger.debug(f'Mail addresses: {to_addresses}')
def verify_link(user): """ Used for verifying an e-mail address when the user clicks the link in the verification mail. """ proofing_user = ProofingUser.from_user(user, current_app.private_userdb) code = request.args.get('code') email = request.args.get('email') if code and email: current_app.logger.debug('Trying to save email address {} as verified for user {}'.format(email, proofing_user)) url = urlappend(current_app.config['DASHBOARD_URL'], 'emails') scheme, netloc, path, query_string, fragment = urlsplit(url) try: state = current_app.proofing_statedb.get_state_by_eppn_and_email(proofing_user.eppn, email) timeout = current_app.config.get('EMAIL_VERIFICATION_TIMEOUT', 24) if state.is_expired(timeout): current_app.logger.info("Verification code is expired. Removing the state") current_app.logger.debug("Proofing state: {}".format(state)) current_app.proofing_statedb.remove_state(state) new_query_string = urlencode({'msg': ':ERROR:emails.code_invalid_or_expired'}) url = urlunsplit((scheme, netloc, path, new_query_string, fragment)) return redirect(url) except DocumentDoesNotExist: current_app.logger.info('Could not find proofing state for email {}'.format(email)) new_query_string = urlencode({'msg': ':ERROR:emails.unknown_email'}) url = urlunsplit((scheme, netloc, path, new_query_string, fragment)) return redirect(url) if code == state.verification.verification_code: try: verify_mail_address(state, proofing_user) current_app.logger.info('Email successfully verified') current_app.logger.debug('Email address: {}'.format(email)) new_query_string = urlencode({'msg': 'emails.verification-success'}) url = urlunsplit((scheme, netloc, path, new_query_string, fragment)) return redirect(url) except UserOutOfSync: current_app.logger.info('Could not confirm email, data out of sync') current_app.logger.debug('Mail address: {}'.format(email)) new_query_string = urlencode({'msg': ':ERROR:user-out-of-sync'}) url = urlunsplit((scheme, netloc, path, new_query_string, fragment)) return redirect(url) current_app.logger.info("Invalid verification code") current_app.logger.debug("Email address: {}".format(state.verification.email)) new_query_string = urlencode({'msg': ':ERROR:emails.code_invalid_or_expired'}) url = urlunsplit((scheme, netloc, path, new_query_string, fragment)) return redirect(url) abort(400)
def no_authn_views(app, paths): """ :param app: Flask app :type app: flask.Flask :param paths: Paths that does not require authentication :type paths: list :return: Flask app :rtype: flask.Flask """ app_root = app.config.get('APPLICATION_ROOT') if app_root is None: app_root = '' for path in paths: no_auth_regex = '^{!s}$'.format(urlappend(app_root, path)) if no_auth_regex not in app.config['NO_AUTHN_URLS']: app.config['NO_AUTHN_URLS'].append(no_auth_regex) return app
def no_authn_views(app, paths): """ :param app: Flask app :type app: flask.Flask :param paths: Paths that does not require authentication :type paths: list :return: Flask app :rtype: flask.Flask """ app_root = app.config.get('APPLICATION_ROOT') if app_root is None: app_root = '' for path in paths: no_auth_regex = '^{!s}$'.format(urlappend(app_root, path)) if no_auth_regex not in app.config['NO_AUTHN_URLS']: app.config['NO_AUTHN_URLS'].append(no_auth_regex) return app
def mfa_authentication_action(session_info, user): relay_state = request.form.get('RelayState') current_app.logger.debug('RelayState: {}'.format(relay_state)) redirect_url = None if 'eidas_redirect_urls' in session: redirect_url = session['eidas_redirect_urls'].pop(relay_state, None) if not redirect_url: # With no redirect url just redirect the user to dashboard for a new try to log in # TODO: This will result in a error 400 until we put the authentication in the session current_app.logger.error('Missing redirect url for mfa authentication') return redirect_with_msg(current_app.config['ACTION_URL'], ':ERROR:eidas.no_redirect_url') # We get the mfa authentication views "next" argument as base64 to avoid our request sanitation # to replace all & to & redirect_url = base64.b64decode(redirect_url).decode('utf-8') # TODO: Rename verify_relay_state to verify_redirect_url redirect_url = verify_relay_state(redirect_url) if not is_required_loa(session_info, 'loa3'): return redirect_with_msg(redirect_url, ':ERROR:eidas.authn_context_mismatch') if not is_valid_reauthn(session_info): return redirect_with_msg(redirect_url, ':ERROR:eidas.reauthn_expired') # Check that a verified NIN is equal to the asserted attribute personalIdentityNumber asserted_nin = get_saml_attribute(session_info, 'personalIdentityNumber')[0] user_nin = user.nins.verified.find(asserted_nin) if not user_nin: current_app.logger.error('Asserted NIN not matching user verified nins') current_app.logger.debug('Asserted NIN: {}'.format(asserted_nin)) return redirect_with_msg(redirect_url, ':ERROR:eidas.nin_not_matching') session.mfa_action.success = True session.mfa_action.issuer = session_info['issuer'] session.mfa_action.authn_instant = session_info['authn_info'][0][2] session.mfa_action.authn_context = get_authn_ctx(session_info) # Redirect back to action app but to the redirect-action view resp = redirect_with_msg(redirect_url, 'actions.action-completed') scheme, netloc, path, query_string, fragment = urlsplit(resp.location) new_path = urlappend(path, 'redirect-action') new_url = urlunsplit((scheme, netloc, new_path, query_string, fragment)) current_app.logger.debug(f'Redirecting to: {new_url}') return redirect(new_url)
def support_init_app(name, config): """ Create an instance of an eduid support app. First, it will load the configuration from support.settings.common then any settings given in the `config` param. Then, the app instance will be updated with common stuff by `eduid_init_app`, and finally all needed blueprints will be registered with it. :param name: The name of the instance, it will affect the configuration loaded. :type name: str :param config: any additional configuration settings. Specially useful in test cases :type config: dict :return: the flask app :rtype: flask.Flask """ app = eduid_init_app(name, config) app.config.update(config) if app.config.get('TOKEN_SERVICE_URL_LOGOUT') is None: app.config['TOKEN_SERVICE_URL_LOGOUT'] = urlappend(app.config['TOKEN_SERVICE_URL'], 'logout') from eduid_webapp.support.views import support_views app.register_blueprint(support_views) app.support_user_db = db.SupportUserDB(app.config['MONGO_URI']) app.support_authn_db = db.SupportAuthnInfoDB(app.config['MONGO_URI']) app.support_proofing_log_db = db.SupportProofingLogDB(app.config['MONGO_URI']) app.support_signup_db = db.SupportSignupUserDB(app.config['MONGO_URI']) app.support_actions_db = db.SupportActionsDB(app.config['MONGO_URI']) app.support_letter_proofing_db = db.SupportLetterProofingDB(app.config['MONGO_URI']) app.support_oidc_proofing_db = db.SupportOidcProofingDB(app.config['MONGO_URI']) app.support_email_proofing_db = db.SupportEmailProofingDB(app.config['MONGO_URI']) app.support_phone_proofing_db = db.SupportPhoneProofingDB(app.config['MONGO_URI']) app = register_template_funcs(app) app.logger.info('Init {} app...'.format(name)) return app
def check_authn(request): settings = registry.settings cookie_name = settings.get('session.key') if ((cookie_name in request.cookies and 'eduPersonPrincipalName' in request.session) or ('eppn' in request.params and 'token' in request.params and 'nonce' in request.params)): try: remember(request, request.session['eduPersonPrincipalName']) except KeyError: # we have just signed up, there is no eppn in the session, # we must have it as a request.param eppn = request.params['eppn'] remember(request, eppn) request.session['eduPersonPrincipalName'] = eppn return handler(request) try: reset_password_url = request.route_url('reset-password') except KeyError: pass else: if reset_password_url in request.url: return handler(request) login_url = urlappend(settings['token_service_url'], 'login') next_url = request.url params = {'next': next_url} url_parts = list(urlparse.urlparse(login_url)) query = urlparse.parse_qs(url_parts[4]) query.update(params) url_parts[4] = urlencode(query) location = urlparse.urlunparse(url_parts) request.session.persist() request.session.set_cookie() return HTTPFound(location=location)
def start_password_change(context, request): ''' ''' # check csrf csrf = sanitize_post_key(request, 'csrf') if csrf != request.session.get_csrf_token(): return HTTPBadRequest() ts_url = request.registry.settings.get('token_service_url') chpass_url = urlappend(ts_url, 'chpass') next_url = request.route_url('password-change') params = {'next': next_url} url_parts = list(urlparse.urlparse(chpass_url)) query = urlparse.parse_qs(url_parts[4]) query.update(params) url_parts[4] = urlencode(query) location = urlparse.urlunparse(url_parts) return HTTPFound(location=location)
def test_change_language_with_invalid_host(self): self.set_logged(email = '*****@*****.**') host = self.settings['dashboard_hostname'] dashboard_baseurl = self.settings['dashboard_baseurl'] referer = 'http://{hostname}/'.format(hostname=host) invalid_host = 'attacker.controlled.site' from eduid_common.api.utils import urlappend url = urlappend(referer, '/set_language/?lang=sv') response = self.testapp.get(url, extra_environ={ 'HTTP_REFERER': referer, 'HTTP_HOST': invalid_host }, status=302) # the semantics checked by this test have changed. # now, if the invalid_host does not coincide with the # cookie domain, the authn cookie is not sent in the request, # and therefore the request is taken as unauthn and # redirected to the authn service. cookies = self.testapp.cookies self.assertIsNone(cookies.get('lang', None)) self.assertTrue(self.settings['token_service_url'] in response.location)
def start_password_change(context, request): ''' ''' # check csrf csrf = sanitize_post_key(request, 'csrf') if csrf != request.session.get_csrf_token(): return HTTPBadRequest() ts_url = request.registry.settings.get('token_service_url') chpass_url = urlappend(ts_url, 'chpass') next_url = request.route_url('password-change') params = {'next': next_url} url_parts = list(urlparse.urlparse(chpass_url)) query = urlparse.parse_qs(url_parts[4]) query.update(params) url_parts[4] = urlencode(query) location = urlparse.urlunparse(url_parts) return HTTPFound(location=location)
def check_authn(request): settings = registry.settings cookie_name = settings.get('session.key') if ((cookie_name in request.cookies and 'eduPersonPrincipalName' in request.session) or ('eppn' in request.params and 'token' in request.params and 'nonce' in request.params)): try: remember(request, request.session['eduPersonPrincipalName']) except KeyError: # we have just signed up, there is no eppn in the session, # we must have it as a request.param eppn = request.params['eppn'] remember(request, eppn) request.session['eduPersonPrincipalName'] = eppn return handler(request) try: reset_password_url = request.route_url('reset-password') except KeyError: pass else: if reset_password_url in request.url: return handler(request) login_url = urlappend(settings['token_service_url'], 'login') next_url = request.url params = {'next': next_url} url_parts = list(urlparse.urlparse(login_url)) query = urlparse.parse_qs(url_parts[4]) query.update(params) url_parts[4] = urlencode(query) location = urlparse.urlunparse(url_parts) request.session.persist() request.session.set_cookie() return HTTPFound(location=location)
def delete_account(user): """ Terminate account view. It receives a POST request, checks the csrf token, schedules the account termination action, and redirects to the IdP. """ current_app.logger.debug('Initiating account termination for user {}'.format(user)) ts_url = current_app.config.get('TOKEN_SERVICE_URL') terminate_url = urlappend(ts_url, 'terminate') next_url = url_for('security.account_terminated') params = {'next': next_url} url_parts = list(urlparse(terminate_url)) query = parse_qs(url_parts[4]) query.update(params) url_parts[4] = urlencode(query) location = urlunparse(url_parts) return RedirectSchema().dump({'location': location}).data
def delete_account(user): """ Terminate account view. It receives a POST request, checks the csrf token, schedules the account termination action, and redirects to the IdP. """ current_app.logger.debug('Initiating account termination for user {}'.format(user)) ts_url = current_app.config.get('TOKEN_SERVICE_URL') terminate_url = urlappend(ts_url, 'terminate') next_url = url_for('security.account_terminated') params = {'next': next_url} url_parts = list(urlparse.urlparse(terminate_url)) query = urlparse.parse_qs(url_parts[4]) query.update(params) url_parts[4] = urlencode(query) location = urlparse.urlunparse(url_parts) return RedirectSchema().dump({'location': location}).data
def get_url_for_bundle(self, action: Action) -> str: """ Return the url for the bundle that contains the front-end javascript side of the plugin. To be injected into an index.html file. If there is some error in the process, raise ActionError. :param action: the action as retrieved from the eduid_actions db :returns: the url :raise: ActionPlugin.ActionError """ path = current_app.config.bundles_path version = current_app.config.bundles_version feature_cookie = request.cookies.get(current_app.config.bundles_feature_cookie) if feature_cookie and feature_cookie in current_app.config.bundles_feature_version: version = current_app.config.bundles_feature_version[feature_cookie] bundle_name = 'eduid_action.{}.js' env = current_app.config.environment if env == 'dev': bundle_name = 'eduid_action.{}-bundle.dev.js' elif env == 'staging': bundle_name = 'eduid_action.{}.staging.js' base = urlappend(path, bundle_name.format(self.PLUGIN_NAME)) return get_static_url_for(base, version=version)
def letter_status(request, user, nin): settings = request.registry.settings letter_url = settings.get('letter_service_url') state_url = urlappend(letter_url, 'get-state') data = {'eppn': user.eppn} response = requests.post(state_url, data=data) sent, result = False, 'error' msg = _('There was a problem with the letter service. ' 'Please try again later.') if response.status_code == 200: state = response.json() if 'letter_sent' in state: sent = True result = 'success' expires = datetime.utcfromtimestamp(int(state['letter_expires'])) expires = expires.strftime('%Y-%m-%d') msg = _('A letter has already been sent to your official postal address. ' 'The code enclosed will expire on ${expires}. ' 'After that date you can restart the process if the letter was lost.', mapping={'expires': expires}) else: sent = False result = 'success' msg = _('When you click on the "Send" button a letter with a ' 'verification code will be sent to your official postal address.') logger.info("Asking user {!r} if they want to send a letter.".format(user)) else: logger.error('Error getting status from the letter service. Status code {!r}, msg "{}"'.format( response.status_code, response.text)) return { 'result': result, 'message': get_localizer(request).translate(msg), 'sent': sent }
def finish_letter_action(self, data, post_data): """ Contact the eduid-idproofing-letter service and give it the code the user supplied. If the letter proofing service approves of the code, this code does the following: * Put together some LetterProofing data with information about the user, the vetting, the users registered address etc. (Kantara requirement) * Log what the letter proofing service returned on the user (we put it there for now...) * Upgrade the NIN in question to verified=True * Mark the verification code as used :returns: status, message in a dict :rtype: dict """ nin, index = data.split() index = int(index) settings = self.request.registry.settings letter_url = settings.get('letter_service_url') verify_letter_url = urlappend(letter_url, 'verify-code') code = post_data['verification_code'] self.user = get_session_user(self.request) # small helper function to make rest of the function more readable def make_result(result, msg): return dict(result = result, message = msg) data = {'eppn': self.user.eppn, 'verification_code': code} logger.info("Posting letter verification code for user {!r}.".format(self.user)) response = requests.post(verify_letter_url, data=data) logger.info("Received response from idproofing-letter after posting verification code " "for user {!r}.".format(self.user)) if response.status_code != 200: # Do nothing, just return above error message and log microservice return code logger.info("Received status code {!s} from idproofing-letter after posting verification code " "for user {!r}.".format(response.status_code, self.user)) return make_result('error', _('There was a problem with the letter service. ' 'Please try again later.')) rdata = response.json().get('data', {}) if not (rdata.get('verified', False) and nin == rdata.get('number', None)): log.info('User {!r} supplied wrong letter verification code or nin did not match.'.format( self.user)) log.debug('NIN in dashboard: {!s}, NIN in idproofing-letter: {!s}'.format( nin, rdata.get('number', None))) return make_result('error', _('Your verification code seems to be wrong, please try again.')) # Save data from successful verification call for later addition to user proofing collection. # Convert self.user to a DashboardUser manually instead of letting save_dashboard_user do # it to get access to add_letter_proofing_data(). user = DashboardUser(data = self.user.to_dict()) rdata['created_ts'] = datetime.utcfromtimestamp(int(rdata['created_ts'])) rdata['verified_ts'] = datetime.utcfromtimestamp(int(rdata['verified_ts'])) user.add_letter_proofing_data(rdata) # Look up users official address at the time of verification per Kantara requirements logger.info("Looking up address via Navet for user {!r}.".format(self.user)) user_postal_address = self.request.msgrelay.get_full_postal_address(rdata['number']) logger.info("Finished looking up address via Navet for user {!r}.".format(self.user)) proofing_data = LetterProofing(self.user, rdata['number'], rdata['official_address'], rdata['transaction_id'], user_postal_address) # Log verification event and fail if that goes wrong logger.info("Logging proofing data for user {!r}.".format(self.user)) if not self.request.idproofinglog.log_verification(proofing_data): log.error('Logging of letter proofing data for user {!r} failed.'.format(self.user)) return make_result('error', _('Sorry, we are experiencing temporary technical ' 'problems, please try again later.')) logger.info("Finished logging proofing data for user {!r}.".format(self.user)) # This is a hack to reuse the existing proofing functionality, the users code has # already been verified by the micro service but we decided the dashboard could # continue 'upgrading' the users until we've made the planned proofing consumer set_nin_verified(self.request, user, nin) try: self.request.context.save_dashboard_user(user) except UserOutOfSync: log.error("Verified norEduPersonNIN NOT saved for user {!r}. User out of sync.".format( self.user)) return self.sync_user() self.user = user # Finally mark the verification as used save_as_verified(self.request, 'norEduPersonNIN', self.user, nin) logger.info("Verified NIN by physical letter saved for user {!r}.".format( self.user)) return make_result('success', _('You have successfully verified your identity'))
def logout(context, request): settings = request.registry.settings authn_url = settings.get('token_service_url') logout_url = urlappend(authn_url, 'logout') return HTTPFound(location=logout_url)
def includeme(config): # DB setup settings = config.registry.settings mongodb = MongoDB(db_uri=settings['mongo_uri']) authninfodb = MongoDB(db_uri=settings['mongo_uri']) config.registry.settings['mongodb'] = mongodb config.registry.settings['authninfodb'] = authninfodb config.registry.settings['db_conn'] = mongodb.get_connection config.set_request_property(lambda x: x.registry.settings['mongodb']. get_database('eduid_dashboard'), 'db', reify=True) config.set_request_property(lambda x: x.registry.settings['authninfodb']. get_database('eduid_idp_authninfo'), 'authninfodb', reify=True) # Create userdb instance and store it in our config, # and make a getter lambda for pyramid to retreive it _userdb = UserDBWrapper(config.registry.settings['mongo_uri']) config.registry.settings['userdb'] = _userdb config.add_request_method(lambda x: x.registry.settings['userdb'], 'userdb', reify=True) # same DB using new style users config.registry.settings['userdb_new'] = UserDB( config.registry.settings['mongo_uri'], db_name='eduid_am') config.add_request_method(lambda x: x.registry.settings['userdb_new'], 'userdb_new', reify=True) # Set up handle to Dashboards private UserDb (DashboardUserDB) _dashboard_userdb = DashboardUserDB(config.registry.settings['mongo_uri']) config.registry.settings['dashboard_userdb'] = _dashboard_userdb config.add_request_method( lambda x: x.registry.settings['dashboard_userdb'], 'dashboard_userdb', reify=True) config.registry.settings['msgrelay'] = MsgRelay(config.registry.settings) config.add_request_method(lambda x: x.registry.settings['msgrelay'], 'msgrelay', reify=True) config.registry.settings['lookuprelay'] = LookupMobileRelay( config.registry.settings) config.add_request_method(lambda x: x.registry.settings['lookuprelay'], 'lookuprelay', reify=True) config.registry.settings['idproofinglog'] = IDProofingLog( config.registry.settings) config.add_request_method(lambda x: x.registry.settings['idproofinglog'], 'idproofinglog', reify=True) config.registry.settings['amrelay'] = AmRelay(config.registry.settings) config.add_request_method(lambda x: x.registry.settings['amrelay'], 'amrelay', reify=True) config.set_request_property(is_logged, 'is_logged', reify=True) config.registry.settings['stats'] = get_stats_instance(settings, log) # Make the result of the lambda available as request.stats config.set_request_property(lambda x: x.registry.settings['stats'], 'stats', reify=True) # # Route setups # config.add_route('home', '/', factory=HomeFactory) if settings['workmode'] == 'personal': config.include(profile_urls, route_prefix='/profile/') config.include(disabled_admin_urls, route_prefix='/admin/{userid}/') else: config.include(profile_urls, route_prefix='/users/{userid}/') config.include(admin_urls, route_prefix='/admin/{userid}/') config.add_route('token-login', '/tokenlogin/') config.add_route('logout', '/logout') settings['token_service_url_logout'] = urlappend( settings['token_service_url'], 'logout') if settings['workmode'] == 'personal': config.add_route('verifications', '/verificate/{model}/{code}/', factory=VerificationsFactory) else: config.add_route('verifications', '/verificate/{model}/{code}/', factory=ForbiddenFactory) config.add_route('help', '/help/', factory=HelpFactory) # Seems unused -- ft@ 2016-01-14 #config.add_route('session-reload', '/session-reload/', # factory=PersonFactory) config.add_route('set_language', '/set_language/') config.add_route('error500test', '/error500test/') config.add_route('error500', '/error500/') config.add_route('error404', '/error404/') if not settings.get('testing', False): config.add_view(context=Exception, view='eduiddashboard.views.portal.exception_view', renderer='templates/error500.jinja2') config.add_view(context=HTTPNotFound, view='eduiddashboard.views.portal.not_found_view', renderer='templates/error404.jinja2') # Favicon config.add_route('favicon', '/favicon.ico') config.add_view('eduiddashboard.views.static.favicon_view', route_name='favicon')
def verify_link(user): """ Used for verifying an e-mail address when the user clicks the link in the verification mail. """ proofing_user = ProofingUser.from_user(user, current_app.private_userdb) code = request.args.get('code') email = request.args.get('email') if code and email: current_app.logger.debug( 'Trying to save email address {} as verified for user {}'.format( email, proofing_user)) url = urlappend(current_app.config['DASHBOARD_URL'], 'emails') scheme, netloc, path, query_string, fragment = urlparse.urlsplit(url) try: state = current_app.proofing_statedb.get_state_by_eppn_and_email( proofing_user.eppn, email) timeout = current_app.config.get('EMAIL_VERIFICATION_TIMEOUT', 24) if state.is_expired(timeout): current_app.logger.info( "Verification code is expired. Removing the state") current_app.logger.debug("Proofing state: {}".format(state)) current_app.proofing_statedb.remove_state(state) new_query_string = urlencode( {'msg': ':ERROR:emails.code_invalid_or_expired'}) url = urlparse.urlunsplit( (scheme, netloc, path, new_query_string, fragment)) return redirect(url) except DocumentDoesNotExist: current_app.logger.info( 'Could not find proofing state for email {}'.format(email)) new_query_string = urlencode( {'msg': ':ERROR:emails.unknown_email'}) url = urlparse.urlunsplit( (scheme, netloc, path, new_query_string, fragment)) return redirect(url) if code == state.verification.verification_code: try: verify_mail_address(state, proofing_user) current_app.logger.info('Email successfully verified') current_app.logger.debug('Email address: {}'.format(email)) new_query_string = urlencode( {'msg': 'emails.verification-success'}) url = urlparse.urlunsplit( (scheme, netloc, path, new_query_string, fragment)) return redirect(url) except UserOutOfSync: current_app.logger.info( 'Could not confirm email, data out of sync') current_app.logger.debug('Mail address: {}'.format(email)) new_query_string = urlencode( {'msg': ':ERROR:user-out-of-sync'}) url = urlparse.urlunsplit( (scheme, netloc, path, new_query_string, fragment)) return redirect(url) current_app.logger.info("Invalid verification code") current_app.logger.debug("Email address: {}".format( state.verification.email)) new_query_string = urlencode( {'msg': ':ERROR:emails.code_invalid_or_expired'}) url = urlparse.urlunsplit( (scheme, netloc, path, new_query_string, fragment)) return redirect(url) abort(400)
def token_verify_action(session_info, user): """ Use a Sweden Connect federation IdP assertion to verify a users MFA token and, if necessary, the users identity. :param session_info: the SAML session info :param user: Central db user :type session_info: dict :type user: eduid_userdb.User :return: redirect response :rtype: Response """ redirect_url = urlappend(current_app.config['DASHBOARD_URL'], 'security') if not is_required_loa(session_info, 'loa3'): return redirect_with_msg(redirect_url, ':ERROR:eidas.authn_context_mismatch') if not is_valid_reauthn(session_info): return redirect_with_msg(redirect_url, ':ERROR:eidas.reauthn_expired') proofing_user = ProofingUser.from_user(user, current_app.private_userdb) token_to_verify = proofing_user.credentials.filter(FidoCredential).find( session['verify_token_action_credential_id']) # Check (again) if token was used to authenticate this session if token_to_verify.key not in session['eduidIdPCredentialsUsed']: return redirect_with_msg(redirect_url, ':ERROR:eidas.token_not_in_credentials_used') # Verify asserted NIN for user if there are no verified NIN if proofing_user.nins.verified.count == 0: nin_verify_action(session_info) user = current_app.central_userdb.get_user_by_eppn(user.eppn) proofing_user = ProofingUser.from_user(user, current_app.private_userdb) token_to_verify = proofing_user.credentials.filter(FidoCredential).find( session['verify_token_action_credential_id']) # Check that a verified NIN is equal to the asserted attribute personalIdentityNumber asserted_nin = get_saml_attribute(session_info, 'personalIdentityNumber')[0] user_nin = proofing_user.nins.verified.find(asserted_nin) if not user_nin: current_app.logger.error('Asserted NIN not matching user verified nins') current_app.logger.debug('Asserted NIN: {}'.format(asserted_nin)) return redirect_with_msg(redirect_url, ':ERROR:eidas.nin_not_matching') # Create a proofing log issuer = session_info['issuer'] current_app.logger.debug('Issuer: {}'.format(issuer)) authn_context = get_authn_ctx(session_info) current_app.logger.debug('Authn context: {}'.format(authn_context)) try: user_address = current_app.msg_relay.get_postal_address(user_nin.number) except MsgTaskFailed as e: current_app.logger.error('Navet lookup failed: {}'.format(e)) current_app.stats.count('navet_error') return redirect_with_msg(redirect_url, ':ERROR:error_navet_task') proofing_log_entry = MFATokenProofing(user=proofing_user, created_by='eduid-eidas', nin=user_nin.number, issuer=issuer, authn_context_class=authn_context, key_id=token_to_verify.key, user_postal_address=user_address, proofing_version='2018v1') # Set token as verified token_to_verify.is_verified = True token_to_verify.proofing_method = 'SWAMID_AL2_MFA_HI' token_to_verify.proofing_version = '2018v1' # Save proofing log entry and save user if current_app.proofing_log.save(proofing_log_entry): current_app.logger.info('Recorded MFA token verification in the proofing log') try: save_and_sync_user(proofing_user) except AmTaskFailed as e: current_app.logger.error('Verifying token for user failed') current_app.logger.error('{}'.format(e)) return redirect_with_msg(redirect_url, ':ERROR:Temporary technical problems') current_app.stats.count(name='fido_token_verified') return redirect_with_msg(redirect_url, 'eidas.token_verify_success')
def authorization_response(user): # Redirect url for user feedback url = urlappend(current_app.config['DASHBOARD_URL'], 'accountlinking') scheme, netloc, path, query_string, fragment = urlsplit(url) current_app.stats.count(name='authn_response') # parse authentication response query_string = request.query_string.decode('utf-8') current_app.logger.debug('query_string: {!s}'.format(query_string)) authn_resp = current_app.oidc_client.parse_response(AuthorizationResponse, info=query_string, sformat='urlencoded') current_app.logger.debug('Authorization response received: {!s}'.format(authn_resp)) if authn_resp.get('error'): current_app.logger.error('AuthorizationError {!s} - {!s} ({!s})'.format(request.host, authn_resp['error'], authn_resp.get('error_message'), authn_resp.get('error_description'))) new_query_string = urlencode({'msg': ':ERROR:orc.authorization_fail'}) url = urlunsplit((scheme, netloc, path, new_query_string, fragment)) return redirect(url) user_oidc_state = authn_resp['state'] proofing_state = current_app.proofing_statedb.get_state_by_oidc_state(user_oidc_state, raise_on_missing=False) if not proofing_state: current_app.logger.error('The \'state\' parameter ({!s}) does not match a user state.'.format(user_oidc_state)) new_query_string = urlencode({'msg': ':ERROR:orc.unknown_state'}) url = urlunsplit((scheme, netloc, path, new_query_string, fragment)) return redirect(url) # do token request args = { 'code': authn_resp['code'], 'redirect_uri': url_for('orcid.authorization_response', _external=True), } current_app.logger.debug('Trying to do token request: {!s}'.format(args)) token_resp = current_app.oidc_client.do_access_token_request(scope='openid', state=authn_resp['state'], request_args=args, authn_method='client_secret_basic') current_app.logger.debug('token response received: {!s}'.format(token_resp)) id_token = token_resp['id_token'] if id_token['nonce'] != proofing_state.nonce: current_app.logger.error('The \'nonce\' parameter does not match for user') new_query_string = urlencode({'msg': ':ERROR:orc.unknown_nonce'}) url = urlunsplit((scheme, netloc, path, new_query_string, fragment)) return redirect(url) current_app.logger.info('ORCID authorized for user') # do userinfo request current_app.logger.debug('Trying to do userinfo request:') userinfo = current_app.oidc_client.do_user_info_request(method=current_app.config['USERINFO_ENDPOINT_METHOD'], state=authn_resp['state']) current_app.logger.debug('userinfo received: {!s}'.format(userinfo)) if userinfo['sub'] != id_token['sub']: current_app.logger.error('The \'sub\' of userinfo does not match \'sub\' of ID Token for user {!s}.'.format( proofing_state.eppn)) new_query_string = urlencode({'msg': ':ERROR:orc.sub_mismatch'}) url = urlunsplit((scheme, netloc, path, new_query_string, fragment)) return redirect(url) # Save orcid and oidc data to user current_app.logger.info('Saving ORCID data for user') proofing_user = ProofingUser.from_user(user, current_app.private_userdb) oidc_id_token = OidcIdToken(iss=id_token['iss'], sub=id_token['sub'], aud=id_token['aud'], exp=id_token['exp'], iat=id_token['iat'], nonce=id_token['nonce'], auth_time=id_token['auth_time'], application='orcid') oidc_authz = OidcAuthorization(access_token=token_resp['access_token'], token_type=token_resp['token_type'], id_token=oidc_id_token, expires_in=token_resp['expires_in'], refresh_token=token_resp['refresh_token'], application='orcid') orcid_element = Orcid(id=userinfo['id'], name=userinfo['name'], given_name=userinfo['given_name'], family_name=userinfo['family_name'], verified=True, oidc_authz=oidc_authz, application='orcid') orcid_proofing = OrcidProofing(proofing_user, created_by='orcid', orcid=orcid_element.id, issuer=orcid_element.oidc_authz.id_token.iss, audience=orcid_element.oidc_authz.id_token.aud, proofing_method='oidc', proofing_version='2018v1') if current_app.proofing_log.save(orcid_proofing): current_app.logger.info('ORCID proofing data saved to log') proofing_user.orcid = orcid_element save_and_sync_user(proofing_user) current_app.logger.info('ORCID proofing data saved to user') new_query_string = urlencode({'msg': 'orc.authorization_success'}) else: current_app.logger.info('ORCID proofing data NOT saved, failed to save proofing log') new_query_string = urlencode({'msg': ':ERROR:Temporary technical problems'}) # Clean up current_app.logger.info('Removing proofing state') current_app.proofing_statedb.remove_state(proofing_state) url = urlunsplit((scheme, netloc, path, new_query_string, fragment)) return redirect(url)