Exemplo n.º 1
0
    def setUp(self):

        # Use a the gateway definition for configuring the provider
        identifier = "mySmppGW"
        id = set_smsgateway(identifier, self.provider_module, description="test",
                            options=self.config)
        self.assertTrue(id > 0)
        self.provider = create_sms_instance(identifier=identifier)
        self.assertEqual(type(self.provider), SmppSMSProvider)
Exemplo n.º 2
0
    def test_00_errors(self):
        # No smsgateway defined
        s = SmppSMSProvider()
        self.assertRaises(SMSError, s.submit_message, "phone", "message")

        # No host defined
        set_smsgateway("missing_host", self.provider_module,
                       options={"SMSC_PORT": "1234"})
        p = create_sms_instance(identifier="missing_host")
        self.assertRaises(SMSError, p.submit_message, "phone", "message")
        delete_smsgateway("missing_host")

        # No port defined
        set_smsgateway("missing_port", self.provider_module,
                       options={"SMSC_HOST": "1.1.1.1"})
        p = create_sms_instance(identifier="missing_port")
        self.assertRaises(SMSError, p.submit_message, "phone", "message")
        delete_smsgateway("missing_port")
Exemplo n.º 3
0
    def _send_sms(self, message="<otp>"):
        """
        send sms

        :param message: the sms submit message - could contain placeholders
            like <otp> or <serial>
        :type message: string

        :return: submitted message
        :rtype: string
        """
        ret = None

        phone = self.get_tokeninfo("phone")
        otp = self.get_otp()[2]
        serial = self.get_serial()

        message = message.replace("<otp>", otp)
        message = message.replace("<serial>", serial)
        log.debug("sending SMS to phone number {0!s} ".format(phone))

        # First we try to get the new SMS gateway config style
        sms_gateway_identifier = get_from_config("sms.identifier")

        if sms_gateway_identifier:
            # New style
            sms = create_sms_instance(sms_gateway_identifier)

        else:
            # Old style
            (SMSProvider, SMSProviderClass) = self._get_sms_provider()
            log.debug("smsprovider: {0!s}, class: {1!s}".format(SMSProvider,
                                                      SMSProviderClass))

            try:
                sms = get_sms_provider_class(SMSProvider, SMSProviderClass)()
            except Exception as exc:
                log.error("Failed to load SMSProvider: {0!r}".format(exc))
                log.debug("{0!s}".format(traceback.format_exc()))
                raise exc

            try:
                # now we need the config from the env
                log.debug("loading SMS configuration for class {0!s}".format(sms))
                config = self._get_sms_provider_config()
                log.debug("config: {0!r}".format(config))
                sms.load_config(config)
            except Exception as exc:
                log.error("Failed to load sms.providerConfig: {0!r}".format(exc))
                log.debug("{0!s}".format(traceback.format_exc()))
                raise Exception("Failed to load sms.providerConfig: {0!r}".format(exc))

        log.debug("submitMessage: {0!r}, to phone {1!r}".format(message, phone))
        ret = sms.submit_message(phone, message)
        return ret, message
Exemplo n.º 4
0
 def test_08_smsgateway_success(self):
     responses.add(responses.POST,
                   self.url)
     identifier = "mySMS"
     provider_module = "privacyidea.lib.smsprovider.SipgateSMSProvider" \
                       ".SipgateSMSProvider"
     id = set_smsgateway(identifier, provider_module, description="test",
                         options=self.config)
     self.assertTrue(id > 0)
     sms = create_sms_instance(identifier)
     r = sms.submit_message("123456", "Hello")
     self.assertTrue(r)
Exemplo n.º 5
0
    def test_03_create_instance_by_identifier(self):
        # SMS gateway definition
        identifier = "myGW"
        provider_module = "privacyidea.lib.smsprovider.HttpSMSProvider" \
                          ".HttpSMSProvider"
        id = set_smsgateway(identifier, provider_module, description="test",
                            options={"HTTP_METHOD": "POST",
                                     "URL": "example.com"})
        self.assertTrue(id > 0)

        sms = create_sms_instance(identifier)

        self.assertEqual(sms.smsgateway.option_dict.get("URL"), "example.com")
        self.assertEqual(sms.smsgateway.option_dict.get("HTTP_METHOD"),
                         "POST")
Exemplo n.º 6
0
    def test_08_smsgateway_success(self):
        r = add_smtpserver("myServer", "1.2.3.4", sender="*****@*****.**")
        self.assertTrue(r > 0)
        smtpmock.setdata(response={"*****@*****.**": (200, "OK")})

        identifier = "myMail"
        provider_module = "privacyidea.lib.smsprovider.SmtpSMSProvider" \
                          ".SmtpSMSProvider"
        id = set_smsgateway(identifier, provider_module, description="test",
                            options={"SMTPIDENTIFIER": "myServer",
                                     "MAILTO": "*****@*****.**",
                                     "SUBJECT": "{phone}",
                                     "BODY": "{otp}"})
        self.assertTrue(id > 0)
        sms = create_sms_instance(identifier)
        r = sms.submit_message("123456", "Halo")
        self.assertTrue(r)
Exemplo n.º 7
0
    def test_10_new_smsgateway(self):
        identifier = "myGW"
        provider_module = "privacyidea.lib.smsprovider.HttpSMSProvider" \
                          ".HttpSMSProvider"
        id = set_smsgateway(identifier, provider_module, description="test",
                            options={"HTTP_METHOD": "POST",
                                     "URL": "http://example.com",
                                     "RETURN_SUCCESS": "ID",
                                     "text": "{otp}",
                                     "phone": "{phone}"})
        self.assertTrue(id > 0)

        sms = create_sms_instance(identifier)

        responses.add(responses.POST,
                      "http://example.com",
                      body=self.success_body)
        # Here we need to send the SMS
        r = sms.submit_message("123456", "Hello")
        self.assertTrue(r)

        delete_smsgateway(identifier)
Exemplo n.º 8
0
    def test_03_create_instance_by_identifier(self):
        # SMS gateway definition
        identifier = "myGW"
        provider_module = "privacyidea.lib.smsprovider.HttpSMSProvider" \
                          ".HttpSMSProvider"
        id = set_smsgateway(identifier,
                            provider_module,
                            description="test",
                            options={
                                "HTTP_METHOD": "POST",
                                "URL": "example.com"
                            },
                            headers={"Authorization": "QWERTZ"})
        self.assertTrue(id > 0)

        sms = create_sms_instance(identifier)

        self.assertEqual(sms.smsgateway.option_dict.get("URL"), "example.com")
        self.assertEqual(sms.smsgateway.option_dict.get("HTTP_METHOD"), "POST")
        self.assertEqual(sms.smsgateway.header_dict.get("Authorization"),
                         "QWERTZ")
        self.assertEqual(sms.smsgateway.option_dict.get("Authorization"), None)
Exemplo n.º 9
0
    def create_challenge(self, transactionid=None, options=None):
        """
        This method creates a challenge, which is submitted to the user.
        The submitted challenge will be preserved in the challenge
        database.

        If no transaction id is given, the system will create a transaction
        id and return it, so that the response can refer to this transaction.

        :param transactionid: the id of this challenge
        :param options: the request context parameters / data
        :type options: dict
        :return: tuple of (bool, message, transactionid, attributes)
        :rtype: tuple

        The return tuple builds up like this:
        ``bool`` if submit was successful;
        ``message`` which is displayed in the JSON response;
        additional challenge ``reply_dict``, which are displayed in the JSON challenges response.
        """
        options = options or {}
        message = get_action_values_from_options(
            SCOPE.AUTH, ACTION.CHALLENGETEXT,
            options) or DEFAULT_CHALLENGE_TEXT

        reply_dict = {}
        data = None
        # Initially we assume there is no error from Firebase
        res = True
        fb_identifier = self.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG)
        if fb_identifier:
            challenge = b32encode_and_unicode(geturandom())
            if fb_identifier != POLL_ONLY:
                # We only push to Firebase if this tokens does NOT POLL_ONLY.
                fb_gateway = create_sms_instance(fb_identifier)
                registration_url = get_action_values_from_options(
                    SCOPE.ENROLL,
                    PUSH_ACTION.REGISTRATION_URL,
                    options=options)
                pem_privkey = self.get_tokeninfo(PRIVATE_KEY_SERVER)
                smartphone_data = _build_smartphone_data(
                    self.token.serial, challenge, registration_url,
                    pem_privkey, options)
                res = fb_gateway.submit_message(
                    self.get_tokeninfo("firebase_token"), smartphone_data)

            # Create the challenge in the challenge table if either the message
            # was successfully submitted to the Firebase API or if polling is
            # allowed in general or for this specific token.
            allow_polling = get_action_values_from_options(
                SCOPE.AUTH, PUSH_ACTION.ALLOW_POLLING,
                options=options) or PushAllowPolling.ALLOW
            if ((allow_polling == PushAllowPolling.ALLOW or
                 (allow_polling == PushAllowPolling.TOKEN and is_true(
                     self.get_tokeninfo(POLLING_ALLOWED, default='True'))))
                    or res):
                validity = int(
                    get_from_config('DefaultChallengeValidityTime', 120))
                tokentype = self.get_tokentype().lower()
                # Maybe there is a PushChallengeValidityTime...
                lookup_for = tokentype.capitalize() + 'ChallengeValidityTime'
                validity = int(get_from_config(lookup_for, validity))

                # Create the challenge in the database
                db_challenge = Challenge(self.token.serial,
                                         transaction_id=transactionid,
                                         challenge=challenge,
                                         data=data,
                                         session=options.get("session"),
                                         validitytime=validity)
                db_challenge.save()
                self.challenge_janitor()

            # If sending the Push message failed, we still raise an error and a warning.
            if not res:
                log.warning(
                    u"Failed to submit message to Firebase service for token {0!s}."
                    .format(self.token.serial))
                raise ValidateError(
                    "Failed to submit message to Firebase service.")
        else:
            log.warning(u"The token {0!s} has no tokeninfo {1!s}. "
                        u"The message could not be sent.".format(
                            self.token.serial, PUSH_ACTION.FIREBASE_CONFIG))
            raise ValidateError(
                "The token has no tokeninfo. Can not send via Firebase service."
            )

        return True, message, db_challenge.transaction_id, reply_dict
Exemplo n.º 10
0
    def create_challenge(self, transactionid=None, options=None):
        """
        This method creates a challenge, which is submitted to the user.
        The submitted challenge will be preserved in the challenge
        database.

        If no transaction id is given, the system will create a transaction
        id and return it, so that the response can refer to this transaction.

        :param transactionid: the id of this challenge
        :param options: the request context parameters / data
        :type options: dict
        :return: tuple of (bool, message, transactionid, attributes)
        :rtype: tuple

        The return tuple builds up like this:
        ``bool`` if submit was successful;
        ``message`` which is displayed in the JSON response;
        additional ``attributes``, which are displayed in the JSON response.
        """
        options = options or {}
        message = get_action_values_from_options(
            SCOPE.AUTH, ACTION.CHALLENGETEXT,
            options) or DEFAULT_CHALLENGE_TEXT

        attributes = None
        data = None
        fb_identifier = self.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG)
        if fb_identifier:
            challenge = b32encode_and_unicode(geturandom())
            fb_gateway = create_sms_instance(fb_identifier)
            pem_privkey = self.get_tokeninfo(PRIVATE_KEY_SERVER)
            smartphone_data = _build_smartphone_data(self.token.serial,
                                                     challenge, fb_gateway,
                                                     pem_privkey, options)
            res = fb_gateway.submit_message(
                self.get_tokeninfo("firebase_token"), smartphone_data)
            if not res:
                raise ValidateError(
                    "Failed to submit message to firebase service.")
        else:
            log.warning(u"The token {0!s} has no tokeninfo {1!s}. "
                        u"The message could not be sent.".format(
                            self.token.serial, PUSH_ACTION.FIREBASE_CONFIG))
            raise ValidateError(
                "The token has no tokeninfo. Can not send via firebase service."
            )

        validity = int(get_from_config('DefaultChallengeValidityTime', 120))
        tokentype = self.get_tokentype().lower()
        # Maybe there is a PushChallengeValidityTime...
        lookup_for = tokentype.capitalize() + 'ChallengeValidityTime'
        validity = int(get_from_config(lookup_for, validity))

        # Create the challenge in the database
        db_challenge = Challenge(self.token.serial,
                                 transaction_id=transactionid,
                                 challenge=challenge,
                                 data=data,
                                 session=options.get("session"),
                                 validitytime=validity)
        db_challenge.save()
        self.challenge_janitor()
        return True, message, db_challenge.transaction_id, attributes
Exemplo n.º 11
0
    def api_endpoint(cls, request, g):
        """
        This provides a function which is called by the API endpoint
        ``/ttype/push`` which is defined in :doc:`../../api/ttype`

        The method returns a tuple ``("json", {})``

        This endpoint provides several functionalities:

        - It is used for the 2nd enrollment step of the smartphone.
          It accepts the following parameters:

            .. sourcecode:: http

              POST /ttype/push HTTP/1.1
              Host: https://yourprivacyideaserver

              serial=<token serial>
              fbtoken=<firebase token>
              pubkey=<public key>

        - It is also used when the smartphone sends the signed response
          to the challenge during authentication. The following parameters ar accepted:

            .. sourcecode:: http

              POST /ttype/push HTTP/1.1
              Host: https://yourprivacyideaserver

              serial=<token serial>
              nonce=<the actual challenge>
              signature=<the signed nonce>

        - And it also acts as an endpoint for polling challenges:

            .. sourcecode:: http

              GET /ttype/push HTTP/1.1
              Host: https://yourprivacyideaserver

              serial=<tokenserial>
              timestamp=<timestamp>
              signature=SIGNATURE(<tokenserial>|<timestamp>)

          More on polling can be found here: https://github.com/privacyidea/privacyidea/wiki/concept%3A-pushtoken-poll

        :param request: The Flask request
        :param g: The Flask global object g
        :return: The json string representing the result dictionary
        :rtype: tuple("json", str)
        """
        details = {}
        result = False

        if request.method == 'POST':
            serial = getParam(request.all_data, "serial", optional=False)
            if serial and "fbtoken" in request.all_data and "pubkey" in request.all_data:
                log.debug("Do the 2nd step of the enrollment.")
                try:
                    token_obj = get_one_token(serial=serial,
                                              tokentype="push",
                                              rollout_state="clientwait")
                    token_obj.update(request.all_data)
                except ResourceNotFoundError:
                    raise ResourceNotFoundError(
                        "No token with this serial number "
                        "in the rollout state 'clientwait'.")
                init_detail_dict = request.all_data

                details = token_obj.get_init_detail(init_detail_dict)
                result = True
            elif serial and "nonce" in request.all_data and "signature" in request.all_data:
                log.debug(
                    "Handling the authentication response from the smartphone."
                )
                challenge = getParam(request.all_data, "nonce")
                serial = getParam(request.all_data, "serial")
                signature = getParam(request.all_data, "signature")

                # get the token_obj for the given serial:
                token_obj = get_one_token(serial=serial, tokentype="push")
                pubkey_obj = _build_verify_object(
                    token_obj.get_tokeninfo(PUBLIC_KEY_SMARTPHONE))
                # Do the 2nd step of the authentication
                # Find valid challenges
                challengeobject_list = get_challenges(serial=serial,
                                                      challenge=challenge)

                if challengeobject_list:
                    # There are valid challenges, so we check this signature
                    for chal in challengeobject_list:
                        # verify the signature of the nonce
                        sign_data = u"{0!s}|{1!s}".format(challenge, serial)
                        try:
                            pubkey_obj.verify(b32decode(signature),
                                              sign_data.encode("utf8"),
                                              padding.PKCS1v15(),
                                              hashes.SHA256())
                            # The signature was valid
                            log.debug(
                                "Found matching challenge {0!s}.".format(chal))
                            chal.set_otp_status(True)
                            chal.save()
                            result = True
                        except InvalidSignature as _e:
                            pass

            else:
                raise ParameterError("Missing parameters!")
        elif request.method == 'GET':
            # This is only used for polling
            # By default we allow polling if the policy is not set.
            allow_polling = get_action_values_from_options(
                SCOPE.AUTH, PUSH_ACTION.ALLOW_POLLING,
                options={'g': g}) or PushAllowPolling.ALLOW
            if allow_polling == PushAllowPolling.DENY:
                raise PolicyError('Polling not allowed!')
            serial = getParam(request.all_data, "serial", optional=False)
            timestamp = getParam(request.all_data, 'timestamp', optional=False)
            signature = getParam(request.all_data, 'signature', optional=False)
            # first check if the timestamp is in the required span
            try:
                ts = isoparse(timestamp)
            except (ValueError, TypeError) as _e:
                log.debug('{0!s}'.format(traceback.format_exc()))
                raise privacyIDEAError(
                    'Could not parse timestamp {0!s}. '
                    'ISO-Format required.'.format(timestamp))
            # TODO: make time delta configurable
            td = timedelta(minutes=POLL_TIME_WINDOW)
            # We don't know if the passed timestamp is timezone aware. If no
            # timezone is passed, we assume UTC
            if ts.tzinfo:
                now = datetime.now(utc)
            else:
                now = datetime.utcnow()
            if not (now - td <= ts <= now + td):
                raise privacyIDEAError(
                    'Timestamp {0!s} not in valid range.'.format(timestamp))
            # now check the signature
            # first get the token
            try:
                tok = get_one_token(serial=serial,
                                    tokentype=cls.get_class_type())
                # If the push_allow_polling policy is set to "token" we also
                # need to check the POLLING_ALLOWED tokeninfo. If it evaluated
                # to 'False', polling is not allowed for this token. If the
                # tokeninfo value evaluates to 'True' or is not set at all,
                # polling is allowed for this token.
                if allow_polling == PushAllowPolling.TOKEN:
                    if not is_true(
                            tok.get_tokeninfo(POLLING_ALLOWED,
                                              default='True')):
                        log.debug(
                            'Polling not allowed for pushtoken {0!s} due to '
                            'tokeninfo.'.format(serial))
                        raise PolicyError('Polling not allowed!')

                pubkey_obj = _build_verify_object(
                    tok.get_tokeninfo(PUBLIC_KEY_SMARTPHONE))
                sign_data = u"{serial}|{timestamp}".format(**request.all_data)
                pubkey_obj.verify(b32decode(signature),
                                  sign_data.encode("utf8"), padding.PKCS1v15(),
                                  hashes.SHA256())
                # The signature was valid now check for an open challenge
                # we need the private server key to sign the smartphone data
                pem_privkey = tok.get_tokeninfo(PRIVATE_KEY_SERVER)
                # we also need the FirebaseGateway for this token
                fb_identifier = tok.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG)
                if not fb_identifier:
                    raise ResourceNotFoundError(
                        'The pushtoken {0!s} has no Firebase configuration '
                        'assigned.'.format(serial))
                fb_gateway = create_sms_instance(fb_identifier)
                options = {'g': g}
                challenges = []
                challengeobject_list = get_challenges(serial=serial)
                for chal in challengeobject_list:
                    # check if the challenge is active and not already answered
                    _cnt, answered = chal.get_otp_status()
                    if not answered and chal.is_valid():
                        # then return the necessary smartphone data to answer
                        # the challenge
                        sp_data = _build_smartphone_data(
                            serial, chal.challenge, fb_gateway, pem_privkey,
                            options)
                        challenges.append(sp_data)
                # return the challenges as a list in the result value
                result = challenges
            except (ResourceNotFoundError, ParameterError, InvalidSignature,
                    ConfigAdminError, BinasciiError) as e:
                # to avoid disclosing information we always fail with an invalid
                # signature error even if the token with the serial could not be found
                log.debug('{0!s}'.format(traceback.format_exc()))
                log.info('The following error occurred during the signature '
                         'check: "{0!r}"'.format(e))
                raise privacyIDEAError('Could not verify signature!')

        else:
            raise privacyIDEAError(
                'Method {0!s} not allowed in \'api_endpoint\' '
                'for push token.'.format(request.method))

        return "json", prepare_result(result, details=details)
Exemplo n.º 12
0
    def create_challenge(self, transactionid=None, options=None):
        """
        This method creates a challenge, which is submitted to the user.
        The submitted challenge will be preserved in the challenge
        database.

        If no transaction id is given, the system will create a transaction
        id and return it, so that the response can refer to this transaction.

        :param transactionid: the id of this challenge
        :param options: the request context parameters / data
        :type options: dict
        :return: tuple of (bool, message, transactionid, attributes)
        :rtype: tuple

        The return tuple builds up like this:
        ``bool`` if submit was successful;
        ``message`` which is displayed in the JSON response;
        additional ``attributes``, which are displayed in the JSON response.
        """
        res = False
        options = options or {}
        message = get_action_values_from_options(
            SCOPE.AUTH, ACTION.CHALLENGETEXT,
            options) or DEFAULT_CHALLENGE_TEXT

        sslverify = get_action_values_from_options(
            SCOPE.AUTH, PUSH_ACTION.SSL_VERIFY, options) or "1"
        sslverify = getParam({"sslverify": sslverify},
                             "sslverify",
                             allowed_values=["0", "1"],
                             default="1")

        attributes = None
        data = None
        challenge = b32encode_and_unicode(geturandom())
        fb_identifier = self.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG)
        if fb_identifier:
            # We send the challenge to the Firebase service
            fb_gateway = create_sms_instance(fb_identifier)
            url = fb_gateway.smsgateway.option_dict.get(
                FIREBASE_CONFIG.REGISTRATION_URL)
            message_on_mobile = get_action_values_from_options(
                SCOPE.AUTH, PUSH_ACTION.MOBILE_TEXT,
                options) or DEFAULT_MOBILE_TEXT
            title = get_action_values_from_options(
                SCOPE.AUTH, PUSH_ACTION.MOBILE_TITLE, options) or "privacyIDEA"
            smartphone_data = {
                "nonce": challenge,
                "question": message_on_mobile,
                "serial": self.token.serial,
                "title": title,
                "sslverify": sslverify,
                "url": url
            }
            # Create the signature.
            # value to string
            sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format(
                **smartphone_data)

            pem_privkey = self.get_tokeninfo(PRIVATE_KEY_SERVER)
            privkey_obj = serialization.load_pem_private_key(
                to_bytes(pem_privkey), None, default_backend())

            # Sign the data with PKCS1 padding. Not all Androids support PSS padding.
            signature = privkey_obj.sign(sign_string.encode("utf8"),
                                         padding.PKCS1v15(), hashes.SHA256())
            smartphone_data["signature"] = b32encode_and_unicode(signature)

            res = fb_gateway.submit_message(
                self.get_tokeninfo("firebase_token"), smartphone_data)
            if not res:
                raise ValidateError(
                    "Failed to submit message to firebase service.")
        else:
            log.warning(u"The token {0!s} has no tokeninfo {1!s}. "
                        u"The message could not be sent.".format(
                            self.token.serial, PUSH_ACTION.FIREBASE_CONFIG))
            raise ValidateError(
                "The token has no tokeninfo. Can not send via firebase service."
            )

        validity = int(get_from_config('DefaultChallengeValidityTime', 120))
        tokentype = self.get_tokentype().lower()
        # Maybe there is a PushChallengeValidityTime...
        lookup_for = tokentype.capitalize() + 'ChallengeValidityTime'
        validity = int(get_from_config(lookup_for, validity))

        # Create the challenge in the database
        db_challenge = Challenge(self.token.serial,
                                 transaction_id=transactionid,
                                 challenge=challenge,
                                 data=data,
                                 session=options.get("session"),
                                 validitytime=validity)
        db_challenge.save()
        self.challenge_janitor()
        return True, message, db_challenge.transaction_id, attributes
Exemplo n.º 13
0
    def _send_sms(self, message="<otp>"):
        """
        send sms

        :param message: the sms submit message - could contain placeholders
            like <otp> or <serial>
        :type message: string

        :return: submitted message
        :rtype: string
        """
        if is_true(self.get_tokeninfo("dynamic_phone")):
            phone = self.user.get_user_phone("mobile")
            if type(phone) == list and phone:
                # if there is a non-empty list, we use the first entry
                phone = phone[0]
        else:
            phone = self.get_tokeninfo("phone")
        if not phone:  # pragma: no cover
            log.warning("Token {0!s} does not have a phone number!".format(
                self.token.serial))
        otp = self.get_otp()[2]
        serial = self.get_serial()

        message = message.replace("<otp>", otp)
        message = message.replace("<serial>", serial)
        log.debug("sending SMS to phone number {0!s} ".format(phone))

        # First we try to get the new SMS gateway config style
        # The token specific identifier has priority over the system wide identifier
        sms_gateway_identifier = self.get_tokeninfo(
            "sms.identifier") or get_from_config("sms.identifier")

        if sms_gateway_identifier:
            # New style
            sms = create_sms_instance(sms_gateway_identifier)

        else:
            # Old style
            (SMSProvider, SMSProviderClass) = self._get_sms_provider()
            log.debug("smsprovider: {0!s}, class: {1!s}".format(
                SMSProvider, SMSProviderClass))

            try:
                sms = get_sms_provider_class(SMSProvider, SMSProviderClass)()
            except Exception as exc:
                log.error("Failed to load SMSProvider: {0!r}".format(exc))
                log.debug("{0!s}".format(traceback.format_exc()))
                raise exc

            try:
                # now we need the config from the env
                log.debug(
                    "loading SMS configuration for class {0!s}".format(sms))
                config = self._get_sms_provider_config()
                log.debug("config: {0!r}".format(config))
                sms.load_config(config)
            except Exception as exc:
                log.error(
                    "Failed to load sms.providerConfig: {0!r}".format(exc))
                log.debug("{0!s}".format(traceback.format_exc()))
                raise Exception(
                    "Failed to load sms.providerConfig: {0!r}".format(exc))

        log.debug("submitMessage: {0!r}, to phone {1!r}".format(
            message, phone))
        ret = sms.submit_message(phone, message)
        return ret, message
Exemplo n.º 14
0
    def _send_sms(self, message="<otp>"):
        """
        send sms

        :param message: the sms submit message - could contain placeholders
            like <otp> or <serial>
        :type message: string

        :return: submitted message
        :rtype: string
        """
        if is_true(self.get_tokeninfo("dynamic_phone")):
            phone = self.user.get_user_phone("mobile")
            if type(phone) == list and phone:
                # if there is a non-empty list, we use the first entry
                phone = phone[0]
        else:
            phone = self.get_tokeninfo("phone")
        if not phone:  # pragma: no cover
            log.warning("Token {0!s} does not have a phone number!".format(self.token.serial))
        otp = self.get_otp()[2]
        serial = self.get_serial()

        message = message.replace("<otp>", otp)
        message = message.replace("<serial>", serial)
        log.debug("sending SMS to phone number {0!s} ".format(phone))

        # First we try to get the new SMS gateway config style
        # The token specific identifier has priority over the system wide identifier
        sms_gateway_identifier = self.get_tokeninfo("sms.identifier") or get_from_config("sms.identifier")

        if sms_gateway_identifier:
            # New style
            sms = create_sms_instance(sms_gateway_identifier)

        else:
            # Old style
            (SMSProvider, SMSProviderClass) = self._get_sms_provider()
            log.debug("smsprovider: {0!s}, class: {1!s}".format(SMSProvider,
                                                      SMSProviderClass))

            try:
                sms = get_sms_provider_class(SMSProvider, SMSProviderClass)()
            except Exception as exc:
                log.error("Failed to load SMSProvider: {0!r}".format(exc))
                log.debug("{0!s}".format(traceback.format_exc()))
                raise exc

            try:
                # now we need the config from the env
                log.debug("loading SMS configuration for class {0!s}".format(sms))
                config = self._get_sms_provider_config()
                log.debug("config: {0!r}".format(config))
                sms.load_config(config)
            except Exception as exc:
                log.error("Failed to load sms.providerConfig: {0!r}".format(exc))
                log.debug("{0!s}".format(traceback.format_exc()))
                raise Exception("Failed to load sms.providerConfig: {0!r}".format(exc))

        log.debug("submitMessage: {0!r}, to phone {1!r}".format(message, phone))
        ret = sms.submit_message(phone, message)
        return ret, message
Exemplo n.º 15
0
    def _api_endpoint_get(cls, g, request_data):
        """ Handle all GET requests to the api endpoint.

        Currently this is only used for polling.
        :param g: The Flask context
        :param request_data: Dictionary containing the parameters of the request
        :type request_data: dict
        :returns: Result of the polling operation, 'True' if an unanswered and
                  matching challenge exists, 'False' otherwise.
        :rtype: bool
        """
        # By default we allow polling if the policy is not set.
        allow_polling = get_action_values_from_options(
            SCOPE.AUTH, PUSH_ACTION.ALLOW_POLLING,
            options={'g': g}) or PushAllowPolling.ALLOW
        if allow_polling == PushAllowPolling.DENY:
            raise PolicyError('Polling not allowed!')
        serial = getParam(request_data, "serial", optional=False)
        timestamp = getParam(request_data, 'timestamp', optional=False)
        signature = getParam(request_data, 'signature', optional=False)
        # first check if the timestamp is in the required span
        cls._check_timestamp_in_range(timestamp, POLL_TIME_WINDOW)
        # now check the signature
        # first get the token
        try:
            tok = get_one_token(serial=serial, tokentype=cls.get_class_type())
            # If the push_allow_polling policy is set to "token" we also
            # need to check the POLLING_ALLOWED tokeninfo. If it evaluated
            # to 'False', polling is not allowed for this token. If the
            # tokeninfo value evaluates to 'True' or is not set at all,
            # polling is allowed for this token.
            if allow_polling == PushAllowPolling.TOKEN:
                if not is_true(
                        tok.get_tokeninfo(POLLING_ALLOWED, default='True')):
                    log.debug('Polling not allowed for pushtoken {0!s} due to '
                              'tokeninfo.'.format(serial))
                    raise PolicyError('Polling not allowed!')

            pubkey_obj = _build_verify_object(
                tok.get_tokeninfo(PUBLIC_KEY_SMARTPHONE))
            sign_data = u"{serial}|{timestamp}".format(**request_data)
            pubkey_obj.verify(b32decode(signature), sign_data.encode("utf8"),
                              padding.PKCS1v15(), hashes.SHA256())
            # The signature was valid now check for an open challenge
            # we need the private server key to sign the smartphone data
            pem_privkey = tok.get_tokeninfo(PRIVATE_KEY_SERVER)
            # we also need the FirebaseGateway for this token
            fb_identifier = tok.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG)
            if not fb_identifier:
                raise ResourceNotFoundError(
                    'The pushtoken {0!s} has no Firebase configuration '
                    'assigned.'.format(serial))
            fb_gateway = create_sms_instance(fb_identifier)
            options = {'g': g}
            challenges = []
            challengeobject_list = get_challenges(serial=serial)
            for chal in challengeobject_list:
                # check if the challenge is active and not already answered
                _cnt, answered = chal.get_otp_status()
                if not answered and chal.is_valid():
                    # then return the necessary smartphone data to answer
                    # the challenge
                    sp_data = _build_smartphone_data(serial, chal.challenge,
                                                     fb_gateway, pem_privkey,
                                                     options)
                    challenges.append(sp_data)
            # return the challenges as a list in the result value
            result = challenges
        except (ResourceNotFoundError, ParameterError, InvalidSignature,
                ConfigAdminError, BinasciiError) as e:
            # to avoid disclosing information we always fail with an invalid
            # signature error even if the token with the serial could not be found
            log.debug('{0!s}'.format(traceback.format_exc()))
            log.info('The following error occurred during the signature '
                     'check: "{0!r}"'.format(e))
            raise privacyIDEAError('Could not verify signature!')

        return result
Exemplo n.º 16
0
    def create_challenge(self, transactionid=None, options=None):
        """
        This method creates a challenge, which is submitted to the user.
        The submitted challenge will be preserved in the challenge
        database.

        If no transaction id is given, the system will create a transaction
        id and return it, so that the response can refer to this transaction.

        :param transactionid: the id of this challenge
        :param options: the request context parameters / data
        :type options: dict
        :return: tuple of (bool, message, transactionid, attributes)
        :rtype: tuple

        The return tuple builds up like this:
        ``bool`` if submit was successful;
        ``message`` which is displayed in the JSON response;
        additional ``attributes``, which are displayed in the JSON response.
        """
        res = False
        options = options or {}
        message = get_action_values_from_options(SCOPE.AUTH,
                                                 ACTION.CHALLENGETEXT,
                                                 options) or DEFAULT_CHALLENGE_TEXT

        sslverify = get_action_values_from_options(SCOPE.AUTH,
                                                   PUSH_ACTION.SSL_VERIFY,
                                                   options) or "1"
        sslverify = getParam({"sslverify": sslverify}, "sslverify", allowed_values=["0", "1"], default="1")

        attributes = None
        data = None
        challenge = b32encode_and_unicode(geturandom())
        fb_identifier = self.get_tokeninfo(PUSH_ACTION.FIREBASE_CONFIG)
        if fb_identifier:
            # We send the challenge to the Firebase service
            fb_gateway = create_sms_instance(fb_identifier)
            url = fb_gateway.smsgateway.option_dict.get(FIREBASE_CONFIG.REGISTRATION_URL)
            message_on_mobile = get_action_values_from_options(SCOPE.AUTH,
                                                   PUSH_ACTION.MOBILE_TEXT,
                                                   options) or DEFAULT_MOBILE_TEXT
            title = get_action_values_from_options(SCOPE.AUTH,
                                                   PUSH_ACTION.MOBILE_TITLE,
                                                   options) or "privacyIDEA"
            smartphone_data = {"nonce": challenge,
                               "question": message_on_mobile,
                               "serial": self.token.serial,
                               "title": title,
                               "sslverify": sslverify,
                               "url": url}
            # Create the signature.
            # value to string
            sign_string = u"{nonce}|{url}|{serial}|{question}|{title}|{sslverify}".format(**smartphone_data)

            pem_privkey = self.get_tokeninfo(PRIVATE_KEY_SERVER)
            privkey_obj = serialization.load_pem_private_key(to_bytes(pem_privkey), None, default_backend())

            # Sign the data with PKCS1 padding. Not all Androids support PSS padding.
            signature = privkey_obj.sign(sign_string.encode("utf8"),
                                         padding.PKCS1v15(),
                                         hashes.SHA256())
            smartphone_data["signature"] = b32encode_and_unicode(signature)

            res = fb_gateway.submit_message(self.get_tokeninfo("firebase_token"), smartphone_data)
            if not res:
                raise ValidateError("Failed to submit message to firebase service.")
        else:
            log.warning(u"The token {0!s} has no tokeninfo {1!s}. "
                        u"The message could not be sent.".format(self.token.serial,
                                                                 PUSH_ACTION.FIREBASE_CONFIG))
            raise ValidateError("The token has no tokeninfo. Can not send via firebase service.")

        validity = int(get_from_config('DefaultChallengeValidityTime', 120))
        tokentype = self.get_tokentype().lower()
        # Maybe there is a PushChallengeValidityTime...
        lookup_for = tokentype.capitalize() + 'ChallengeValidityTime'
        validity = int(get_from_config(lookup_for, validity))

        # Create the challenge in the database
        db_challenge = Challenge(self.token.serial,
                                 transaction_id=transactionid,
                                 challenge=challenge,
                                 data=data,
                                 session=options.get("session"),
                                 validitytime=validity)
        db_challenge.save()
        self.challenge_janitor()
        return True, message, db_challenge.transaction_id, attributes