Esempio n. 1
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)
Esempio n. 2
0
    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)
Esempio n. 3
0
    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)
Esempio 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)
Esempio n. 5
0
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)
Esempio n. 6
0
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),
    }
Esempio n. 7
0
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)
Esempio n. 8
0
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')
Esempio n. 9
0
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)
Esempio n. 10
0
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
Esempio n. 11
0
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}')
Esempio n. 12
0
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)
Esempio n. 13
0
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
Esempio n. 14
0
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
Esempio n. 15
0
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)
Esempio n. 16
0
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
Esempio n. 17
0
    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)
Esempio n. 18
0
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)
Esempio n. 19
0
 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)
Esempio n. 20
0
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)
Esempio n. 21
0
    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)
Esempio n. 22
0
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
Esempio n. 23
0
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
Esempio n. 24
0
    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)
Esempio n. 25
0
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
    }
Esempio n. 26
0
    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'))
Esempio n. 27
0
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)
Esempio n. 28
0
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')
Esempio n. 29
0
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)
Esempio n. 30
0
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')
Esempio n. 31
0
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)