Esempio n. 1
0
 def _get_form_action(self, soup):
     LOG.debug('Looking for the form action')
     form = soup.find('form')
     if form is None:
         alohomora.die('Expected form not found, please make sure Duo is set up properly.{}Please check: {}'
                       .format(os.linesep, self.idp_url))
     LOG.debug('Found form action %s', form['action'])
     return form['action']
Esempio n. 2
0
def to_seconds(tstr):
    """Takes string in form of 3h/25m/13s/13 and returns integer seconds.
    No unit specified implies seconds."""
    try:
        val, suffix = re.match("^([0-9]+)([HhMmSs]?)$", tstr).groups()
    except:
        alohomora.die("Can't parse duration '%s'" % tstr)
    scale = {'h': 3600, 'm': 60}.get(suffix.lower(), 1)

    return int(val) * scale
Esempio n. 3
0
    def main(self):
        """Run the program."""

        if self.options.version:
            print('Version:', alohomora.__version__)
            print('Python: ', sys.version.replace('\n', ' ').replace('\r', ''))
            print('README: ', alohomora.__url__)
            return

        # Validate options
        duration = to_seconds(self._get_config('duration', '1h'))

        if not DURATION_MIN <= duration <= DURATION_MAX:
            alohomora.die(
                "Duration of '%s' not in the range of %s-%s seconds" %
                (self._get_config('duration',
                                  None), DURATION_MIN, DURATION_MAX))

        #
        # Get the user's credentials
        #
        username = self._get_config('username', os.getenv("USER"))
        if not username:
            alohomora.die("Oops, don't forget to provide a username")

        password = getpass.getpass()

        idp_url = self._get_config('idp-url', None)
        if not idp_url:
            alohomora.die("Oops, don't forget to provide an idp-url")

        auth_method = self._get_config('auth-method', None)
        auth_device = self._get_config('auth-device', None)

        #
        # Authenticate the user
        #
        provider = alohomora.req.DuoRequestsProvider(idp_url, auth_method)
        (okay, response) = provider.login_one_factor(username, password)
        assertion = None

        if not okay:
            # we need to 2FA
            LOG.info('We need to two-factor')
            (okay, response) = provider.login_two_factor(response, auth_device)
            if not okay:
                alohomora.die('Error doing two-factor, sorry.')
            assertion = response
        else:
            LOG.info('One-factor OK')
            assertion = response

        awsroles = alohomora.saml.get_roles(assertion)

        # If I have more than one role, ask the user which one they want,
        # otherwise just proceed
        if len(awsroles) == 0:
            print('You are not authorized for any AWS roles.')
            sys.exit(0)
        elif len(awsroles) == 1:
            role_arn = awsroles[0].split(',')[0]
            principal_arn = awsroles[0].split(',')[1]
        elif len(awsroles) > 1:
            # arn:{{ partition }}:iam::{{ accountid }}:role/{{ role_name }}
            account_id = self._get_config('account', None)
            role_name = self._get_config('role-name', None)
            idp_name = self._get_config('idp-name', DEFAULT_IDP_NAME)

            # If the user has specified a partition, use it; otherwise, try autodiscovery
            partition = self._get_config('aws-partition', None)
            if partition is None:
                partition = awsroles[0].split(':')[1]

            if account_id is not None and role_name is not None and idp_name is not None:
                role_arn = "arn:%s:iam::%s:role/%s" % (partition, account_id,
                                                       role_name)
                principal_arn = "arn:%s:iam::%s:saml-provider/%s" % (
                    partition, account_id, idp_name)
            else:
                account_map = {}
                try:
                    accounts = self.config.options('account_map')
                    for account in accounts:
                        account_map[account] = self.config.get(
                            'account_map', account)
                except Exception:
                    pass
                selectedrole = alohomora._prompt_for_a_thing(
                    "Please choose the role you would like to assume:",
                    awsroles,
                    lambda s: format_role(s.split(',')[0], account_map))

                role_arn = selectedrole.split(',')[0]
                principal_arn = selectedrole.split(',')[1]

        token = alohomora.keys.get(role_arn, principal_arn, assertion,
                                   duration)
        alohomora.keys.save(token,
                            profile=self._get_config('aws-profile',
                                                     DEFAULT_AWS_PROFILE))
Esempio n. 4
0
    def login_two_factor(self, response_1fa, auth_device=None):
        """Log in with the second factor, borrowing first factor data if necessary"""

        soup_1fa = BeautifulSoup(response_1fa.text, 'html.parser')
        duo_host = None
        sig_request = None
        # post_action = None
        for iframe in soup_1fa.find_all('iframe'):
            duo_host = iframe.get('data-host')
            sig_request = iframe.get('data-sig-request')

        sigs = sig_request.split(':')
        duo_sig = sigs[0]
        app_sig = sigs[1]

        # Pulling the iframe into the page
        frame_url = 'https://%s/frame/web/v1/auth?tx=%s&parent=%s&v=2.3' % \
            (duo_host, duo_sig, response_1fa.url)
        LOG.info('Getting Duo iframe')
        # if the duo integration has an allowed origin list, we must
        # pass the page URL as a Referer header in addition to using
        # the `parent` query parameter in the frame URL
        (response, soup) = self._do_get(frame_url,
                                        headers={
                                            'Referer': response_1fa.url,
                                        })

        payload = {}
        for inputtag in soup.find_all('input'):
            name = inputtag.get('name', '')
            value = inputtag.get('value', '')
            # Populate all parameters with the existing value (picks up hidden fields too)
            payload[name] = value

        # Post data to emulate the plugin determination
        LOG.info('Posting plugin information to Duo')
        (response, soup) = self._do_post(frame_url, data=payload)

        sid = unquote(urlparse.urlparse(response.request.url).query[4:])
        new_action = self._get_form_action(soup)
        device = self._get_duo_device(soup, auth_device)
        factor = self._get_auth_factor(soup, device)

        # Finally send the POST request for an auth to Duo
        payload = {
            'sid':
            sid,
            'device':
            device.value if
            (device.name != "Security Key (U2F)"
             and not device.value.startswith('WA')) else "u2f_token",
            'factor':
            factor.name if
            (device.name != "Security Key (U2F)"
             and not device.value.startswith('WA')) else "U2F Token",
            'out_of_date':
            ''
        }
        if factor.name == "Passcode":
            payload['passcode'] = factor.value
        headers = {'Referer': response.url}
        (status, _) = self._do_post('https://%s%s' % (duo_host, new_action),
                                    data=payload,
                                    headers=headers,
                                    soup=False)

        # Response is of form
        # {"stat": "OK", "response": {"txid": "f95cbacc-151c-43a6-b462-b33420e72633"}}
        txid = json.loads(status.text)['response']['txid']
        LOG.debug("Received transaction ID %s", txid)

        # Initial call will NOT block
        (status, _) = self._do_post('https://%s/frame/status' % duo_host,
                                    data={
                                        'sid': sid,
                                        'txid': txid
                                    },
                                    soup=False)
        # text from this will be something like
        # {
        #   "stat": "OK",
        #   "response": {
        #       "status_code": "pushed",
        #       "status": "Pushed a login request to your device..."
        #   }
        # }
        # if U2F enrolled, text from this will be something like
        # {
        #   "stat": "OK",
        #   "response": {
        #       "status_code": "u2f_sent",
        #       "status": "Use your Security Key to log in.",
        #       "u2f_sign_request": [{"keyHandle": "..."}, {...}]
        #   }
        # }
        status_data = json.loads(status.text)
        LOG.info(str(status_data))
        if status_data['stat'] != 'OK':
            LOG.error("Returned from inital status call: %s", status.text)
            alohomora.die("Sorry, there was a problem talking to Duo.")
        print(status_data['response']['status'])
        allowed = status_data['response']['status_code'] == 'allow'

        # there should never be a case where `allowed` is True if the user picked Security Key
        if device.name == "Security Key (U2F)" or device.value.startswith(
                'WA'):
            challenges = [
                r for r in status_data['response']['u2f_sign_request']
                if self._validate_u2f_request(duo_host, r)
            ]
            resp = self._get_u2f_response(challenges)
            # pull the first challenge's sessionId since they all match
            # the challenges list should not be empty here, as the device would not be presented
            # to the user without a corresponding challenge
            resp['sessionId'] = challenges[0]['sessionId']
            # include the session ID as passed to us earlier
            payload['sid'] = sid
            # u2f_token and u2f_finish are magic strings here
            payload['device'] = 'u2f_token'
            payload['factor'] = 'u2f_finish'
            # these are a copy/paste from the duo integration's POST data
            payload['out_of_date'] = None
            payload['days_to_block'] = 'None'
            # finally, the response data itself needs to be a JSON string
            payload['response_data'] = json.dumps(resp)

            (status,
             _) = self._do_post('https://%s%s' % (duo_host, new_action),
                                data=payload,
                                headers=headers,
                                soup=False)
            status_data = json.loads(status.text)
            # Response is of form
            # {"stat": "OK", "response": {"txid": "f95cbacc-151c-43a6-b462-b33420e72633"}}
            txid = json.loads(status.text)['response']['txid']
            LOG.debug("Received transaction ID %s", txid)

            # Initial call will NOT block
            (status, _) = self._do_post('https://%s/frame/status' % duo_host,
                                        data={
                                            'sid': sid,
                                            'txid': txid
                                        },
                                        soup=False)
            status_data = json.loads(status.text)
            LOG.info(str(status_data))
            if status_data['stat'] != 'OK':
                LOG.error("Returned from inital status call: %s", status.text)
                alohomora.die("Sorry, there was a problem talking to Duo.")
            print(status_data['response']['status'])
            allowed = status_data['response']['status_code'] == 'allow'
            if not allowed:
                alohomora.die(
                    "Sorry, there was a problem with your security key, try again."
                )

        while not allowed:
            # call again to get status of request
            # for a push notification, this will hang until the user approves/denies
            # for a phone call, you need to keep polling until the user approves/denies
            (status, _) = self._do_post('https://%s/frame/status' % duo_host,
                                        data={
                                            'sid': sid,
                                            'txid': txid
                                        },
                                        soup=False)
            status_data = json.loads(status.text)

            if status_data['stat'] != 'OK':
                LOG.error("Returned from second status call: %s", status.text)
                alohomora.die("Sorry, there was a problem talking to Duo.")
            if status_data['response']['status_code'] == 'allow':
                LOG.info("Login allowed!")
                allowed = True
            elif status_data['response']['status_code'] == 'deny':
                LOG.error("Login disallowed: %s", status.text)
                alohomora.die("The login was blocked!")
            else:
                LOG.info("Still waiting... (%s)",
                         status_data['response']['status_code'])
                LOG.debug(str(status_data))
                time.sleep(2)

        signed_auth = ''
        if 'result_url' in status_data['response']:
            # We have to specifically ask Duo for the signed auth string;
            # this doesn't come for free anymore
            (postresult, _) = self._do_post(
                'https://%s%s' %
                (duo_host, status_data['response']['result_url']),
                data={'sid': sid},
                soup=False)
            postresult_data = json.loads(postresult.text)
            signed_auth = postresult_data['response']['cookie']
        elif 'cookie' in status_data['response']:
            # Leaving this option in here, in case Duo treats different users differently
            signed_auth = status_data['response']['cookie']
        else:
            raise Exception(
                "Unable to find signed token from successful Duo auth")

        payload = {
            '_eventId_proceed': 'transition',
            'sig_response': '%s:%s' % (signed_auth, app_sig)
        }
        (response, soup) = self._do_post(response_1fa.url, data=payload)

        assertion = self._get_assertion(soup)
        return (True, assertion)
Esempio n. 5
0
    def login_one_factor(self, username, password):
        self.session = requests.Session()

        (response, soup) = self._do_get(self.idp_url)
        payload = {}

        for inputtag in soup.find_all('input'):
            name = inputtag.get('name', '')
            # value = inputtag.get('value', '')
            if "user" in name.lower():
                # Make an educated guess that this is the right field for the username
                payload[name] = username
            elif "email" in name.lower():
                # Some IdPs also label the username field as 'email'
                payload[name] = username
            elif "pass" in name.lower():
                # Make an educated guess that this is the right field for the password
                payload[name] = password
            else:
                # Populate the parameter with the existing value (picks up hidden fields as well)
                # payload[name] = value
                pass
        payload['_eventId_proceed'] = ''
        # Omit the password from the debug output...
        payload_debugger = {}
        for key in payload:
            if "pass" in key.lower():
                payload_debugger[key] = '****'
            else:
                payload_debugger[key] = payload[key]
        LOG.debug(payload_debugger)
        if username not in payload.values():
            alohomora.die("Couldn't find right form field for username!")
        elif password not in payload.values():
            alohomora.die("Couldn't find right form field for password!")

        # Some IdPs don't explicitly set a form action, but if one is set we should
        # build the idpauthformsubmiturl by combining the scheme and hostname
        # from the entry url with the form action target
        # If the action tag doesn't exist, we just stick with the
        # idpauthformsubmiturl above
        for inputtag in soup.find_all(re.compile('form', re.IGNORECASE)):
            action = inputtag.get('action')
            if action:
                parsedurl = urlparse.urlparse(self.idp_url)
                idpauthformsubmiturl = parsedurl.scheme + "://" + parsedurl.netloc + action

        post_headers = {
            'Referer': response.url,
            'Content-Type': 'application/x-www-form-urlencoded'
        }
        (response, soup) = self._do_post(idpauthformsubmiturl,
                                         data=payload,
                                         headers=post_headers)

        # We need to check if the user actually got logged in
        # See if we have anything classed 'form-error'
        for tag in soup.find_all(
                lambda x: x.has_attr('class') and 'form-error' in x['class']):
            alohomora.die(tag.get_text())

        # Look for the SAMLResponse attribute of the input tag (determined by
        # analyzing the debug print lines above)
        assertion = ''
        for inputtag in soup.find_all('input'):
            if inputtag.get('name') == 'SAMLResponse':
                # print(inputtag.get('value'))
                assertion = inputtag.get('value')
        if assertion != '':
            return (True, assertion)
        return (False, response)
Esempio n. 6
0
    def login_two_factor(self, response_1fa):
        """Log in with the second factor, borrowing first factor data if necessary"""

        soup_1fa = BeautifulSoup(response_1fa.text, 'html.parser')
        duo_host = None
        sig_request = None
        # post_action = None
        for iframe in soup_1fa.find_all('iframe'):
            duo_host = iframe.get('data-host')
            sig_request = iframe.get('data-sig-request')

        sigs = sig_request.split(':')
        duo_sig = sigs[0]
        app_sig = sigs[1]

        # Pulling the iframe into the page
        frame_url = 'https://%s/frame/web/v1/auth?tx=%s&parent=%s&v=2.3' % \
            (duo_host, duo_sig, response_1fa.url)
        LOG.info('Getting Duo iframe')
        (response, soup) = self._do_get(frame_url)

        payload = {}
        for inputtag in soup.find_all('input'):
            name = inputtag.get('name', '')
            value = inputtag.get('value', '')
            # Populate all parameters with the existing value (picks up hidden fields too)
            payload[name] = value

        # Post data to emulate the plugin determination
        LOG.info('Posting plugin information to Duo')
        (response, soup) = self._do_post(frame_url, data=payload)

        sid = unquote(urlparse.urlparse(response.request.url).query[4:])
        new_action = self._get_form_action(soup)
        device = self._get_duo_device(soup)
        factor = self._get_auth_factor(soup, device)

        # Finally send the POST request for an auth to Duo
        payload = {
            'sid': sid,
            'device': device.value,
            'factor': factor.name,
            'out_of_date': ''
        }
        if factor.name == "Passcode":
            payload['passcode'] = factor.value
        headers = {'Referer': response.url}
        (status, _) = self._do_post('https://%s%s' % (duo_host, new_action),
                                    data=payload,
                                    headers=headers,
                                    soup=False)

        # Response is of form
        # {"stat": "OK", "response": {"txid": "f95cbacc-151c-43a6-b462-b33420e72633"}}
        txid = json.loads(status.text)['response']['txid']
        LOG.debug("Received transaction ID %s", txid)

        # Initial call will NOT block
        (status, _) = self._do_post('https://%s/frame/status' % duo_host,
                                    data={
                                        'sid': sid,
                                        'txid': txid
                                    },
                                    soup=False)
        # text from this will be something like
        # {
        #   "stat": "OK",
        #   "response": {
        #       "status_code": "pushed",
        #       "status": "Pushed a login request to your device..."
        #   }
        # }
        status_data = json.loads(status.text)
        LOG.info(str(status_data))
        if status_data['stat'] != 'OK':
            LOG.error("Returned from inital status call: %s", status.text)
            alohomora.die("Sorry, there was a problem talking to Duo.")
        print(status_data['response']['status'])
        allowed = status_data['response']['status_code'] == 'allow'

        while not allowed:
            # call again to get status of request
            # for a push notification, this will hang until the user approves/denies
            # for a phone call, you need to keep polling until the user approves/denies
            (status, _) = self._do_post('https://%s/frame/status' % duo_host,
                                        data={
                                            'sid': sid,
                                            'txid': txid
                                        },
                                        soup=False)
            status_data = json.loads(status.text)

            if status_data['stat'] != 'OK':
                LOG.error("Returned from second status call: %s", status.text)
                alohomora.die("Sorry, there was a problem talking to Duo.")
            if status_data['response']['status_code'] == 'allow':
                LOG.info("Login allowed!")
                allowed = True
            elif status_data['response']['status_code'] == 'deny':
                LOG.error("Login disallowed: %s", status.text)
                alohomora.die("The login was blocked!")
            else:
                LOG.info("Still waiting... (%s)",
                         status_data['response']['status_code'])
                LOG.debug(str(status_data))
                time.sleep(2)

        signed_auth = ''
        if 'result_url' in status_data['response']:
            # We have to specifically ask Duo for the signed auth string; this doesn't come for free anymore
            (postresult, _) = self._do_post(
                'https://%s%s' %
                (duo_host, status_data['response']['result_url']),
                data={'sid': sid},
                soup=False)
            postresult_data = json.loads(postresult.text)
            signed_auth = postresult_data['response']['cookie']
        elif 'cookie' in status_data['response']:
            # Leaving this option in here, in case Duo treats different users differently
            signed_auth = status_data['response']['cookie']
        else:
            raise Exception(
                "Unable to find signed token from successful Duo auth")

        payload = {
            '_eventId_proceed': 'transition',
            'sig_response': '%s:%s' % (signed_auth, app_sig)
        }
        (response, soup) = self._do_post(response_1fa.url, data=payload)

        assertion = self._get_assertion(soup)
        return (True, assertion)