示例#1
0
    def test_get_single_auth_policy_pe(self, mock_get_policy_definitions):
        """
        verify that (more specific) policy which refers to a client is selected

        this is the first one of a series to test that policy evaluation
        supports the filtering by client, now focusing on the function
              get_single_auth_policy
        which is used by the QRToken and the PushToken to retrieve the pairing
        and callback urls from the policy defintions

        """

        mock_get_policy_definitions.return_value = {
            'authentication': {
                'qrtoken_pairing_callback_url': {
                    'type': 'str'
                }
            }
        }
        with patch.object(
            linotp.lib.policy, '_get_client', autospec=True) \
            as mock_get_client:

            # ------------------------------------------------------------------ --

            # call the get_policies function which must be mocked

            with patch.object(
                linotp.lib.policy.processing, 'get_policies',  autospec=True) \
                as mock_get_policies:

                # ---------------------------------------------------------- --

                # setup the to be called  mocked functions

                mock_get_policies.side_effect = m_get_policies
                mock_get_client.side_effect = m_get_client_match

                action_value = get_single_auth_policy(
                    'qrtoken_pairing_callback_url', realms=['*'])

                assert 'client' in action_value

                mock_get_client.side_effect = m_get_client_no_match
                action_value = get_single_auth_policy(
                    'qrtoken_pairing_callback_url', realms=['*'])

                assert 'client' not in action_value
    def test_get_single_auth_policy_new_pe(self):
        """
        verify that (more specific) policy which refers to a client is selected

        this is the first one of a series to test that policy evaluation
        supports the filtering by client, now focusing on the function
              get_single_auth_policy
        which is used by the QRToken and the PushToken to retrieve the pairing
        and callback urls from the policy defintions

        """

        with patch.object(
            linotp.lib.policy, '_get_client', autospec=True) \
            as mock_get_client:

            # ------------------------------------------------------------------ --

            # the new policy engine is by defining this in the config

            mocked_context['Config']['NewPolicyEvaluation'] = True

            # and calls the get_policies function which must be mocked

            with patch.object(
                linotp.lib.policy.processing, 'get_policies',  autospec=True) \
                as mock_get_policies:

                # ---------------------------------------------------------- --

                # setup the to be called  mocked functions

                mock_get_policies.side_effect = m_get_policies
                mock_get_client.side_effect = m_get_client_match

                action_value = get_single_auth_policy(
                    'qrtoken_pairing_callback_url', realms=['*'])

                assert 'client' in action_value

                mock_get_client.side_effect = m_get_client_no_match
                action_value = get_single_auth_policy(
                    'qrtoken_pairing_callback_url', realms=['*'])

                assert 'client' not in action_value
示例#3
0
    def getInitDetail(self, params, user=None):

        """
        returns initialization details in the enrollment process
        (gets called after update method). used here to pass the
        pairing url to the user

        :param params: parameters provided by the client

        :param user: (unused)

        :raises TokenStateError: If token state is not 'initialized'

        :returns: a dict consisting of a 'pairing_url' entry, containing
            the pairing url and a 'pushtoken_pairing_url' entry containing
            a data structure used in the manage frontend in the enrollment
            process
        """

        _ = context['translate']
        response_detail = {}

        self.ensure_state('initialized')

        # ------------------------------------------------------------------- --

        # collect data used for generating the pairing url

        serial = self.getSerial()

        # ------------------------------------------------------------------- --

        owner = get_token_owner(self)
        if owner and owner.login and owner.realm:
            realms = [owner.realm]
        else:
            realms = self.getRealms()

        # it is guaranteed, that cb_url has a value
        # because we checked it in the update method

        cb_url = get_single_auth_policy('pushtoken_pairing_callback_url',
                                        user=owner, realms=realms)

        # --------------------------------------------------------------- --

        partition = self.getFromTokenInfo('partition')

        # FIXME: certificate usage

        pairing_url = generate_pairing_url(token_type='push',
                                           partition=partition,
                                           serial=serial,
                                           callback_url=cb_url,
                                           use_cert=False)

        # --------------------------------------------------------------- --

        self.addToInfo('pairing_url', pairing_url)
        response_detail['pairing_url'] = pairing_url

        # --------------------------------------------------------------- --

        # add response tabs (used in the manage view on enrollment)

        response_detail['lse_qr_url'] = {
            'description': _('Pairing URL'),
            'img': create_img(pairing_url, width=250),
            'order': 0,
            'value': pairing_url}

        response_detail['serial'] = self.getSerial()

        # ------------------------------------------------------------------ --

        self.change_state('unpaired')

        return response_detail
示例#4
0
    def update(self, params):

        """
        initialization entry hook for the enrollment process.

        :param params: parameters provided by the client

        :raises Exception: If the client supplied unrecognized
            configuration parameters for this token type

        :raises Exception: If the policy 'pushtoken_pairing_callback_url'
            was not set.

        :raises TokenStateError: If token state is not None
            (default pre-enrollment state)
        """

        param_keys = set(params.keys())
        init_rollout_state_keys = set(['type', 'serial', '::scope::',
                                       'user.login', 'description',
                                       'user.realm', 'session', 'key_size',
                                       'resConf', 'user', 'realm',
                                       'pin'])

        # ------------------------------------------------------------------- --

        if not param_keys.issubset(init_rollout_state_keys):

            # make sure the call aborts, if request
            # type wasn't recognized

            raise Exception('Unknown request type for token type pushtoken')

        # if param keys are in above set, the token is
        # initialized for the first time. this is e.g. done on the
        # manage web ui. since the token doesn't exist in the database
        # yet, its rollout state must be None (that is: the data for
        # the rollout state doesn't exist yet)

        self.ensure_state(None)

        # --------------------------------------------------------------- --

        # we check if callback policies are set. this must be done here
        # because the token gets saved directly after the update method
        # in the TokenHandler

        _ = context['translate']

        owner = get_token_owner(self)
        if owner and owner.login and owner.realm:
            realms = [owner.realm]
        else:
            realms = self.getRealms()

        cb_url = get_single_auth_policy('pushtoken_pairing_callback_url',
                                        user=owner, realms=realms)

        if not cb_url:
            raise Exception(_('Policy pushtoken_pairing_callback_url must '
                              'have a value'))

        partition = get_partition(realms, owner)
        self.addToTokenInfo('partition', partition)

        # --------------------------------------------------------------- --

        # we set the the active state of the token to False, because
        # it should not be allowed to use it for validation before the
        # pairing process is done

        self.token.LinOtpIsactive = False

        # -------------------------------------------------------------- --

        TokenClass.update(self, params, reset_failcount=True)

        # -------------------------------------------------------------- --

        self.change_state('initialized')
示例#5
0
    def createChallenge(self, transaction_id, options):

        """
        entry hook for the challenge logic. when this function is called
        a challenge with an transaction was created.

        :param transaction_id: A unique transaction id used to identity
            the challenge object

        :param options: additional options as a dictionary

        :raises TokenStateError: If token state is not 'active' or
            'pairing_response_received'

        :returns: A tuple (success, message, data, attributes)
            with success being a boolean indicating if the call
            to this method was successful, message being a string
            that is passed to the user, attributes being additional
            output data (unused in here)
        """

        valid_states = ['pairing_response_received',
                        'active']

        self.ensure_state_is_in(valid_states)

        # ------------------------------------------------------------------- --

        # inside the challenge url we sent a callback url for the client
        # which is defined by an authentication policy

        owner = get_token_owner(self)
        if owner and owner.login and owner.realm:
            realms = [owner.realm]
        else:
            realms = self.getRealms()

        callback_policy_name = 'pushtoken_challenge_callback_url'
        callback_url = get_single_auth_policy(callback_policy_name,
                                              user=owner, realms=realms)

        if not callback_url:
            raise Exception(_('Policy pushtoken_challenge_callback_url must '
                              'have a value'))

        # ------------------------------------------------------------------- --

        # load and configure provider

        # the realm logic was taken from the
        # provider loading in the smstoken class
        # TODO: refactor & centralize logic

        realm = None
        if realms:
            realm = realms[0]

        push_provider = loadProviderFromPolicy(provider_type='push',
                                               realm=realm,
                                               user=owner)

        # ------------------------------------------------------------------- --

        if self.current_state == 'pairing_response_received':

            content_type = CONTENT_TYPE_PAIRING

            message = ''
            challenge_url, sig_base = self.create_challenge_url(transaction_id,
                                                                content_type,
                                                                callback_url)

        else:

            content_type_as_str = options.get('content_type')

            try:

                # pylons silently converts all ints in json
                # to unicode :(

                content_type = int(content_type_as_str)

            except:

                raise ValueError('Unrecognized content type: %s'
                                 % content_type_as_str)

            # --------------------------------------------------------------- --

            if content_type == CONTENT_TYPE_SIGNREQ:

                message = options.get('data')
                challenge_url, sig_base = self.create_challenge_url(
                                                transaction_id, content_type,
                                                callback_url, message=message)

            # --------------------------------------------------------------- --

            elif content_type == CONTENT_TYPE_LOGIN:

                message = options.get('data')
                login, __, host = message.partition('@')

                challenge_url, sig_base = self.create_challenge_url(
                                                transaction_id, content_type,
                                                callback_url, login=login,
                                                host=host)

            else:

                raise ValueError('Unrecognized content type: %s' % content_type)

        # ------------------------------------------------------------------- --

        # send the challenge_url to the push notification proxy

        token_info = self.getTokenInfo()
        gda = token_info['gda']

        log.debug("pushing notification: %r : %r", challenge_url, gda)

        success, response = push_provider.push_notification(challenge_url, gda)

        if not success:
            raise Exception('push mechanism failed. response was %r'
                            % response)

        # ------------------------------------------------------------------- --

        # we save sig_base in the challenge data, because we need it in
        # checkOtp to verify the signature

        b64_sig_base = b64encode(sig_base)
        data = {'sig_base': b64_sig_base}

        if self.current_state == 'pairing_response_received':
            self.change_state('pairing_challenge_sent')

        # ------------------------------------------------------------------- --

        # don't pass the challenge_url as message to the user

        return (True, '', data, {})
示例#6
0
    def createChallenge(self, transaction_id, options):
        """
        """
        _ = context['translate']

        valid_states = ['pairing_response_received', 'pairing_complete']
        self.ensure_state_is_in(valid_states)

        # ------------------------------------------------------------------- --

        if self.current_state == 'pairing_response_received':
            content_type = CONTENT_TYPE_PAIRING
            reset_url = True
        else:

            content_type_as_str = options.get('content_type')
            reset_url = False

            if content_type_as_str is None:
                content_type = None
            else:
                try:
                    # pylons silently converts all ints in json
                    # to unicode :(
                    content_type = int(content_type_as_str)
                except:
                    raise ValueError('Unrecognized content type: %s' %
                                     content_type_as_str)

        # ------------------------------------------------------------------- --

        message = options.get('data')

        # ------------------------------------------------------------------- --

        owner = get_token_owner(self)
        if owner and owner.login and owner.realm:
            realms = [owner.realm]
        else:
            realms = self.getRealms()

        callback_policies = [
            'qrtoken_challenge_callback_url', 'qrtoken_challenge_callback_sms'
        ]
        callback_url = get_single_auth_policy(callback_policies[0],
                                              user=owner,
                                              realms=realms)
        callback_sms = get_single_auth_policy(callback_policies[1],
                                              user=owner,
                                              realms=realms)

        if not callback_url and not callback_sms:
            raise Exception(
                _('Policy %s must have a value') %
                _(" or ").join(callback_policies))

        # TODO: get from policy/config
        compression = False

        # ------------------------------------------------------------------- --

        challenge_url, user_sig = self.create_challenge_url(
            transaction_id, content_type, message, callback_url, callback_sms,
            compression, reset_url)

        data = {'message': message, 'user_sig': user_sig}

        if self.current_state == 'pairing_response_received':
            self.change_state('pairing_challenge_sent')

        return (True, challenge_url, data, {})
示例#7
0
    def getInitDetail(self, params, user=None):

        _ = context['translate']
        response_detail = {}

        param_keys = set(params.keys())
        init_rollout_state_keys = set([
            'type', 'hashlib', 'serial', '::scope::', 'key_size', 'user.login',
            'description', 'user.realm', 'session', 'otplen', 'pin', 'resConf',
            'user', 'realm', 'qr'
        ])

        # ------------------------------------------------------------------- --

        if param_keys.issubset(init_rollout_state_keys):

            # collect data used for generating the pairing url

            serial = self.getSerial()
            # for qrtoken hashlib is ignored
            hash_algorithm = None
            otp_pin_length = int(self.getOtpLen())

            owner = get_token_owner(self)
            if owner and owner.login and owner.realm:
                realms = [owner.realm]
                user = owner
            else:
                realms = self.getRealms()

            pairing_policies = [
                'qrtoken_pairing_callback_url', 'qrtoken_pairing_callback_sms'
            ]

            # it is guaranteed, that either cb_url or cb_sms has a value
            # because we checked it in the update method

            cb_url = get_single_auth_policy(pairing_policies[0],
                                            user=owner,
                                            realms=realms)
            cb_sms = get_single_auth_policy(pairing_policies[1],
                                            user=owner,
                                            realms=realms)

            # --------------------------------------------------------------- --

            partition = self.getFromTokenInfo('partition')

            # FIXME: certificate usage

            pairing_url = generate_pairing_url(token_type='qr',
                                               partition=partition,
                                               serial=serial,
                                               callback_url=cb_url,
                                               callback_sms_number=cb_sms,
                                               otp_pin_length=otp_pin_length,
                                               hash_algorithm=hash_algorithm,
                                               use_cert=False)

            # --------------------------------------------------------------- --

            self.addToInfo('pairing_url', pairing_url)
            response_detail['pairing_url'] = pairing_url

            # create response tabs
            response_detail['lse_qr_url'] = {
                'description': _('QRToken Pairing Url'),
                'img': create_img(pairing_url, width=250),
                'order': 0,
                'value': pairing_url
            }
            response_detail['lse_qr_cert'] = {
                'description': _('QRToken Certificate'),
                'img': create_img(pairing_url, width=250),
                'order': 1,
                'value': pairing_url
            }

            response_detail['serial'] = self.getSerial()

        # ------------------------------------------------------------------ --

        else:

            # make sure the call aborts, if request
            # type wasn't recognized

            raise Exception('Unknown request type for token type qr')

        # ------------------------------------------------------------------- --

        self.change_state('pairing_url_sent')

        return response_detail
示例#8
0
    def update(self, params):

        param_keys = set(params.keys())
        init_rollout_state_keys = set([
            'type', 'hashlib', 'serial', '::scope::', 'key_size', 'user.login',
            'description', 'user.realm', 'session', 'otplen', 'resConf',
            'user', 'realm', 'qr', 'pin'
        ])

        # ------------------------------------------------------------------- --

        if not param_keys.issubset(init_rollout_state_keys):

            # make sure the call aborts, if request
            # type wasn't recognized

            raise Exception('Unknown request type for token type qr')

        # if param keys are in {'type', 'hashlib'} the token is
        # initialized for the first time. this is e.g. done on the
        # manage web ui. since the token doesn't exist in the database
        # yet, its rollout state must be None (that is: they data for
        # the rollout state doesn't exist yet)

        self.ensure_state(None)

        # --------------------------------------------------------------- --

        # we check if callback policies are set. this must be done here
        # because the token gets saved directly after the update method
        # in the TokenHandler

        _ = context['translate']

        owner = get_token_owner(self)
        if owner and owner.login and owner.realm:
            realms = [owner.realm]
        else:
            realms = self.getRealms()

        pairing_policies = [
            'qrtoken_pairing_callback_url', 'qrtoken_pairing_callback_sms'
        ]

        cb_url = get_single_auth_policy(pairing_policies[0],
                                        user=owner,
                                        realms=realms)
        cb_sms = get_single_auth_policy(pairing_policies[1],
                                        user=owner,
                                        realms=realms)

        if not cb_url and not cb_sms:
            raise Exception(
                _('Policy %s must have a value') %
                _(" or ").join(pairing_policies))

        challenge_policies = [
            'qrtoken_challenge_callback_url', 'qrtoken_challenge_callback_sms'
        ]

        cb_url = get_single_auth_policy(challenge_policies[0],
                                        user=owner,
                                        realms=realms)
        cb_sms = get_single_auth_policy(challenge_policies[1],
                                        user=owner,
                                        realms=realms)

        if not cb_url and not cb_sms:
            raise Exception(
                _('Policy %s must have a value') %
                _(" or ").join(challenge_policies))

        partition = get_partition(realms, owner)
        self.addToTokenInfo('partition', partition)

        # --------------------------------------------------------------- --

        # we set the the active state of the token to False, because
        # it should not be allowed to use it for validation before the
        # pairing process is done

        self.token.LinOtpIsactive = False

        # --------------------------------------------------------------- --

        if 'otplen' not in params:
            params['otplen'] = getFromConfig("QRTokenOtpLen", 8)

        # -------------------------------------------------------------- --

        TokenClass.update(self, params, reset_failcount=True)
示例#9
0
    def createChallenge(self, transaction_id, options):
        """
        create a challenge - either for pairing or challenges when the token is
        activated.

        we support re-activation by the means that if we are in the state
        'pairing_challenge_sent' the activation challenge could be triggered
        again

        :param transaction_id: scope of the challenge, will become part of the
                               challenge url code
        :param options: the request optional parameters
        :return: tuple with (True, challenge_url, data, {})
                 whereby the data is a dict with
                   {'message': message, 'user_sig': user_sig}
        """

        _ = context['translate']

        valid_states = [
            'pairing_response_received', 'pairing_challenge_sent',
            'pairing_complete'
        ]
        self.ensure_state_is_in(valid_states)

        # ------------------------------------------------------------------- --

        if self.current_state in [
                'pairing_challenge_sent', 'pairing_response_received'
        ]:

            content_type = CONTENT_TYPE_PAIRING
            reset_url = True

        elif self.current_state == 'pairing_complete':

            content_type_as_str = options.get('content_type')
            reset_url = False

            if content_type_as_str is None:
                content_type = None
            else:
                try:
                    # pylons silently converts all ints in json
                    # to unicode :(
                    content_type = int(content_type_as_str)
                except:
                    raise ValueError('Unrecognized content type: %s' %
                                     content_type_as_str)

        # ------------------------------------------------------------------- --

        message = options.get('data')

        # ------------------------------------------------------------------- --

        owner = get_token_owner(self)
        if owner and owner.login and owner.realm:
            realms = [owner.realm]
        else:
            realms = self.getRealms()

        callback_policies = [
            'qrtoken_challenge_callback_url', 'qrtoken_challenge_callback_sms'
        ]
        callback_url = get_single_auth_policy(callback_policies[0],
                                              user=owner,
                                              realms=realms)
        callback_sms = get_single_auth_policy(callback_policies[1],
                                              user=owner,
                                              realms=realms)

        if not callback_url and not callback_sms:
            raise Exception(
                _('Policy %s must have a value') %
                _(" or ").join(callback_policies))

        # TODO: get from policy/config
        compression = False

        # ------------------------------------------------------------------- --

        challenge_url, user_sig = self.create_challenge_url(
            transaction_id, content_type, message, callback_url, callback_sms,
            compression, reset_url)

        data = {'message': message, 'user_sig': user_sig}

        if self.current_state == 'pairing_response_received':
            self.change_state('pairing_challenge_sent')

        return (True, challenge_url, data, {})
示例#10
0
    def getInitDetail(self, params, user=None):

        """
        returns initialization details in the enrollment process
        (gets called after update method). used here to pass the
        pairing url to the user

        :param params: parameters provided by the client

        :param user: (unused)

        :raises TokenStateError: If token state is not 'initialized'

        :returns: a dict consisting of a 'pairing_url' entry, containing
            the pairing url and a 'pushtoken_pairing_url' entry containing
            a data structure used in the manage frontend in the enrollment
            process
        """

        _ = context['translate']
        response_detail = {}

        self.ensure_state('initialized')

        # ------------------------------------------------------------------- --

        # collect data used for generating the pairing url

        serial = self.getSerial()

        # ------------------------------------------------------------------- --

        owner = get_token_owner(self)
        if owner and owner.login and owner.realm:
            realms = [owner.realm]
        else:
            realms = self.getRealms()

        # it is guaranteed, that cb_url has a value
        # because we checked it in the update method

        cb_url = get_single_auth_policy('pushtoken_pairing_callback_url',
                                        user=owner, realms=realms)

        # --------------------------------------------------------------- --

        partition = self.getFromTokenInfo('partition')

        # FIXME: certificate usage

        pairing_url = generate_pairing_url(token_type='push',
                                           partition=partition,
                                           serial=serial,
                                           callback_url=cb_url,
                                           use_cert=False)

        # --------------------------------------------------------------- --

        self.addToInfo('pairing_url', pairing_url)
        response_detail['pairing_url'] = pairing_url

        # --------------------------------------------------------------- --

        # add response tabs (used in the manage view on enrollment)

        response_detail['lse_qr_url'] = {
            'description': _('Pairing URL'),
            'img': create_img(pairing_url, width=250),
            'order': 0,
            'value': pairing_url}

        response_detail['serial'] = self.getSerial()

        # ------------------------------------------------------------------ --

        self.change_state('unpaired')

        return response_detail
示例#11
0
    def update(self, params):

        """
        initialization entry hook for the enrollment process.

        :param params: parameters provided by the client

        :raises Exception: If the client supplied unrecognized
            configuration parameters for this token type

        :raises Exception: If the policy 'pushtoken_pairing_callback_url'
            was not set.

        :raises TokenStateError: If token state is not None
            (default pre-enrollment state)
        """

        param_keys = set(params.keys())
        init_rollout_state_keys = set(['type', 'serial', '::scope::',
                                       'user.login', 'description',
                                       'user.realm', 'session', 'key_size',
                                       'resConf', 'user', 'realm',
                                       'pin'])

        # ------------------------------------------------------------------- --

        if not param_keys.issubset(init_rollout_state_keys):

            # make sure the call aborts, if request
            # type wasn't recognized

            raise Exception('Unknown request type for token type pushtoken')

        # if param keys are in above set, the token is
        # initialized for the first time. this is e.g. done on the
        # manage web ui. since the token doesn't exist in the database
        # yet, its rollout state must be None (that is: the data for
        # the rollout state doesn't exist yet)

        self.ensure_state(None)

        # --------------------------------------------------------------- --

        # we check if callback policies are set. this must be done here
        # because the token gets saved directly after the update method
        # in the TokenHandler

        _ = context['translate']

        owner = get_token_owner(self)
        if owner and owner.login and owner.realm:
            realms = [owner.realm]
        else:
            realms = self.getRealms()

        cb_url = get_single_auth_policy('pushtoken_pairing_callback_url',
                                        user=owner, realms=realms)

        if not cb_url:
            raise Exception(_('Policy pushtoken_pairing_callback_url must '
                              'have a value'))

        partition = get_partition(realms, owner)
        self.addToTokenInfo('partition', partition)

        # --------------------------------------------------------------- --

        # we set the the active state of the token to False, because
        # it should not be allowed to use it for validation before the
        # pairing process is done

        self.token.LinOtpIsactive = False

        # -------------------------------------------------------------- --

        TokenClass.update(self, params, reset_failcount=True)

        # -------------------------------------------------------------- --

        self.change_state('initialized')
示例#12
0
    def createChallenge(self, transaction_id, options):

        """
        entry hook for the challenge logic. when this function is called
        a challenge with an transaction was created.

        :param transaction_id: A unique transaction id used to identity
            the challenge object

        :param options: additional options as a dictionary

        :raises TokenStateError: If token state is not 'active' or
            'pairing_response_received'

        :returns: A tuple (success, message, data, attributes)
            with success being a boolean indicating if the call
            to this method was successful, message being a string
            that is passed to the user, attributes being additional
            output data (unused in here)
        """
        _ = context['translate']

        valid_states = ['active',
                        'pairing_response_received',

                        # we support re activation
                        # as long as the token is not active
                        'pairing_challenge_sent',
                        ]

        self.ensure_state_is_in(valid_states)

        # ------------------------------------------------------------------- --

        # inside the challenge url we sent a callback url for the client
        # which is defined by an authentication policy

        owner = get_token_owner(self)
        if owner and owner.login and owner.realm:
            realms = [owner.realm]
        else:
            realms = self.getRealms()

        callback_policy_name = 'pushtoken_challenge_callback_url'
        callback_url = get_single_auth_policy(callback_policy_name,
                                              user=owner, realms=realms)

        if not callback_url:
            raise Exception(_('Policy pushtoken_challenge_callback_url must '
                              'have a value'))

        # ------------------------------------------------------------------- --

        # load and configure provider

        # the realm logic was taken from the
        # provider loading in the smstoken class
        # TODO: refactor & centralize logic

        realm = None
        if realms:
            realm = realms[0]

        push_provider = loadProviderFromPolicy(provider_type='push',
                                               realm=realm,
                                               user=owner)

        # ------------------------------------------------------------------- --

        if self.current_state in ['pairing_response_received',
                                  'pairing_challenge_sent']:

            content_type = CONTENT_TYPE_PAIRING

            message = ''
            challenge_url, sig_base = self.create_challenge_url(transaction_id,
                                                                content_type,
                                                                callback_url)

        elif self.current_state in ['active']:

            content_type_as_str = options.get(
                'content_type', CONTENT_TYPE_SIGNREQ)

            try:

                # pylons silently converts all ints in json
                # to unicode :(

                content_type = int(content_type_as_str)

            except:

                raise ValueError('Unrecognized content type: %s'
                                 % content_type_as_str)

            # --------------------------------------------------------------- --

            if content_type == CONTENT_TYPE_SIGNREQ:

                message = options.get('data')
                challenge_url, sig_base = self.create_challenge_url(
                                                transaction_id, content_type,
                                                callback_url, message=message)

            # --------------------------------------------------------------- --

            elif content_type == CONTENT_TYPE_LOGIN:

                message = options.get('data')
                login, __, host = message.partition('@')

                challenge_url, sig_base = self.create_challenge_url(
                                                transaction_id, content_type,
                                                callback_url, login=login,
                                                host=host)

            else:

                raise ValueError('Unrecognized content type: %s' % content_type)

        # ------------------------------------------------------------------- --

        # send the challenge_url to the push notification proxy

        token_info = self.getTokenInfo()
        gda = token_info['gda']

        log.debug("pushing notification: %r : %r", challenge_url, gda)

        success, response = push_provider.push_notification(
                                                challenge_url,
                                                gda,
                                                transaction_id)

        if not success:
            raise Exception('push mechanism failed. response was %r'
                            % response)

        # ------------------------------------------------------------------- --

        # we save sig_base in the challenge data, because we need it in
        # checkOtp to verify the signature

        b64_sig_base = b64encode(sig_base)
        data = {'sig_base': b64_sig_base}

        if self.current_state == 'pairing_response_received':
            self.change_state('pairing_challenge_sent')

        # ------------------------------------------------------------------- --

        # don't pass the challenge_url as message to the user

        return (True, '', data, {})