Beispiel #1
0
def external_first_login_authenticate(user, response):
    """
    Create a special unauthenticated session for user login through external identity provider for the first time.

    :param user: the user with external credential
    :param response: the response to return
    :return: the response
    """

    data = session.data if session._get_current_object() else {}
    data.update({
        'auth_user_external_id_provider': user['external_id_provider'],
        'auth_user_external_id': user['external_id'],
        'auth_user_fullname': user['fullname'],
        'auth_user_access_token': user['access_token'],
        'auth_user_external_first_login': True,
        'service_url': user['service_url'],
    })
    user_identity = '{}#{}'.format(user['external_id_provider'],
                                   user['external_id'])
    print_cas_log(
        f'Finalizing first-time login from external IdP - data updated: user=[{user_identity}]',
        LogLevel.INFO,
    )
    response = create_session(response, data=data)
    print_cas_log(
        f'Finalizing first-time login from external IdP - anonymous session created: user=[{user_identity}]',
        LogLevel.INFO,
    )
    return response
Beispiel #2
0
 def _parse_service_validation(self, xml):
     resp = CasResponse()
     doc = etree.fromstring(xml)
     auth_doc = doc.xpath('/cas:serviceResponse/*[1]',
                          namespaces=doc.nsmap)[0]
     resp.status = str(auth_doc.xpath('local-name()'))
     if (resp.status == 'authenticationSuccess'):
         print_cas_log(
             f'Service validation succeeded with status: ["{resp.status}"]',
             LogLevel.INFO)
         resp.authenticated = True
         resp.user = str(
             auth_doc.xpath('string(./cas:user)', namespaces=doc.nsmap))
         attributes = auth_doc.xpath('./cas:attributes/*',
                                     namespaces=doc.nsmap)
         for attribute in attributes:
             resp.attributes[str(attribute.xpath('local-name()'))] = str(
                 attribute.text)
         scopes = resp.attributes.get('accessTokenScope')
         resp.attributes['accessTokenScope'] = set(
             scopes.split(' ') if scopes else [])
     else:
         print_cas_log(
             f'Service validation failed with status: ["{resp.status}"]',
             LogLevel.ERROR)
         resp.authenticated = False
     resp_attributes = [
         f'({key}: {val})' for key, val in resp.attributes.items()
     ]
     print_cas_log(f'Parsed CAS response: attributes=[{resp_attributes}]',
                   LogLevel.INFO)
     return resp
Beispiel #3
0
    def service_validate(self, ticket, service_url):
        """
        Send request to CAS to validate ticket.

        :param str ticket: CAS service ticket
        :param str service_url: Service URL from which the authentication request originates
        :rtype: CasResponse
        :raises: CasError if an unexpected response is returned
        """

        url = furl.furl(self.BASE_URL)
        url.path.segments.extend((
            'p3',
            'serviceValidate',
        ))
        url.args['ticket'] = ticket
        url.args['service'] = service_url

        print_cas_log(f'Validating service ticket ["{ticket}"]', LogLevel.INFO)
        resp = requests.get(url.url)
        if resp.status_code == 200:
            print_cas_log(
                f'Service ticket validation response: ticket=[{ticket}], status=[{resp.status_code}]',
                LogLevel.INFO,
            )
            return self._parse_service_validation(resp.content)
        else:
            print_cas_log(
                f'Service ticket validation failed: ticket=[{ticket}], status=[{resp.status_code}]',
                LogLevel.ERROR,
            )
            self._handle_error(resp)
Beispiel #4
0
def get_user_from_cas_resp(cas_resp):
    """
    Given a CAS service validation response, attempt to retrieve user information and next action.
    The `user` in `cas_resp` is the unique GUID of the user. Please do not use the primary key `id`
    or the email `username`. This holds except for the first step of ORCiD login.

    :param cas_resp: the cas service validation response
    :return: the user, the external_credential, and the next action
    """
    from osf.models import OSFUser
    if cas_resp.user:
        user = OSFUser.load(cas_resp.user)
        # cas returns a valid OSF user id
        if user:
            return user, None, 'authenticate'
        # cas does not return a valid OSF user id
        else:
            external_credential = validate_external_credential(cas_resp.user)
            # invalid cas response
            if not external_credential:
                print_cas_log(
                    'CAS response error - missing user or external identity',
                    LogLevel.ERROR)
                return None, None, None
            # cas returns a valid external credential
            user = get_user(
                external_id_provider=external_credential['provider'],
                external_id=external_credential['id'])
            # existing user found
            if user:
                return user, external_credential, 'authenticate'
            # user first time login through external identity provider
            else:
                return None, external_credential, 'external_first_login'
    print_cas_log('CAS response error - `cas_resp.user` is empty',
                  LogLevel.ERROR)
    return None, None, None
Beispiel #5
0
def authenticate(user, access_token, response, user_updates=None):
    data = session.data if session._get_current_object() else {}
    data.update({
        'auth_user_username': user.username,
        'auth_user_id': user._primary_key,
        'auth_user_fullname': user.fullname,
        'auth_user_access_token': access_token,
    })
    print_cas_log(
        f'Finalizing authentication - data updated: user=[{user._id}]',
        LogLevel.INFO)
    enqueue_task(
        update_user_from_activity.s(user._id,
                                    timezone.now().timestamp(),
                                    cas_login=True,
                                    updates=user_updates))
    print_cas_log(
        f'Finalizing authentication - user update queued: user=[{user._id}]',
        LogLevel.INFO)
    response = create_session(response, data=data)
    print_cas_log(
        f'Finalizing authentication - session created: user=[{user._id}]',
        LogLevel.INFO)
    return response
Beispiel #6
0
def make_response_from_ticket(ticket, service_url):
    """
    Given a CAS ticket and service URL, attempt to validate the user and return a proper redirect response.

    :param str ticket: CAS service ticket
    :param str service_url: Service URL from which the authentication request originates
    :return: redirect response
    """

    service_furl = furl.furl(service_url)
    # `service_url` is guaranteed to be removed of `ticket` parameter, which has been pulled off in
    # `framework.sessions.before_request()`.
    if 'ticket' in service_furl.args:
        service_furl.args.pop('ticket')
    client = get_client()
    cas_resp = client.service_validate(ticket, service_furl.url)
    if cas_resp.authenticated:
        user, external_credential, action = get_user_from_cas_resp(cas_resp)
        user_updates = {}  # serialize updates to user to be applied async
        # user found and authenticated
        if user and action == 'authenticate':
            print_cas_log(
                f'CAS response - authenticating user: user=[{user._id}], '
                f'external=[{external_credential}], action=[{action}]',
                LogLevel.INFO,
            )
            # If users check the TOS consent checkbox via CAS, CAS sets the attribute `termsOfServiceChecked` to `true`
            # and then release it to OSF among other authentication attributes. When OSF receives it, it trusts CAS and
            # updates the user object if this is THE FINAL STEP of the login flow. DON'T update TOS consent status when
            # `external_credential == true` (i.e. w/ `action == 'authenticate'` or `action == 'external_first_login'`)
            # since neither is the final step of a login flow.
            tos_checked_via_cas = cas_resp.attributes.get(
                'termsOfServiceChecked', 'false') == 'true'
            if tos_checked_via_cas:
                user_updates['accepted_terms_of_service'] = timezone.now()
                print_cas_log(
                    f'CAS TOS consent checked: {user.guids.first()._id}, {user.username}',
                    LogLevel.INFO)
            # if we successfully authenticate and a verification key is present, invalidate it
            if user.verification_key:
                user_updates['verification_key'] = None

            # if user is authenticated by external IDP, ask CAS to authenticate user for a second time
            # this extra step will guarantee that 2FA are enforced
            # current CAS session created by external login must be cleared first before authentication
            if external_credential:
                user.verification_key = generate_verification_key()
                user.save()
                print_cas_log(
                    f'CAS response - redirect existing external IdP login to verification key login: user=[{user._id}]',
                    LogLevel.INFO)
                return redirect(
                    get_logout_url(
                        get_login_url(service_url,
                                      username=user.username,
                                      verification_key=user.verification_key)))

            # if user is authenticated by CAS
            # TODO [CAS-27]: Remove Access Token From Service Validation
            print_cas_log(
                f'CAS response - finalizing authentication: user=[{user._id}]',
                LogLevel.INFO)
            return authenticate(user,
                                cas_resp.attributes.get('accessToken', ''),
                                redirect(service_furl.url), user_updates)
        # first time login from external identity provider
        if not user and external_credential and action == 'external_first_login':
            print_cas_log(
                f'CAS response - first login from external IdP: '
                f'external=[{external_credential}], action=[{action}]',
                LogLevel.INFO,
            )
            from website.util import web_url_for
            # orcid attributes can be marked private and not shared, default to orcid otherwise
            fullname = u'{} {}'.format(
                cas_resp.attributes.get('given-names', ''),
                cas_resp.attributes.get('family-name', '')).strip()
            # TODO [CAS-27]: Remove Access Token From Service Validation
            user = {
                'external_id_provider': external_credential['provider'],
                'external_id': external_credential['id'],
                'fullname': fullname,
                'access_token': cas_resp.attributes.get('accessToken', ''),
                'service_url': service_furl.url,
            }
            print_cas_log(
                f'CAS response - creating anonymous session: external=[{external_credential}]',
                LogLevel.INFO)
            return external_first_login_authenticate(
                user, redirect(web_url_for('external_login_email_get')))
    # Unauthorized: ticket could not be validated, or user does not exist.
    print_cas_log(
        'Ticket validation failed or user does not exist. Redirect back to service URL (logged out).',
        LogLevel.ERROR)
    return redirect(service_furl.url)