コード例 #1
0
 def __init__(self, subject_id, transport):
     super(ServiceClient, self).__init__('svc', subject_id, transport)
     self.x_iov_jwt_service = XiovJWTService(self._transport, self._subject)
コード例 #2
0
 def setUp(self):
     self._transport = MagicMock(spec=JOSETransport)
     self.x_iov_jwt_service = XiovJWTService(self._transport, "subject")
     self._headers = {"X-IOV-JWT": "jwt", "Other Header": "jwt"}
コード例 #3
0
class ServiceClient(BaseClient):
    """Service Client for interacting with Serive endpoints"""
    def __init__(self, subject_id, transport):
        super(ServiceClient, self).__init__('svc', subject_id, transport)
        self.x_iov_jwt_service = XiovJWTService(self._transport, self._subject)

    @api_call
    def authorize(self,
                  user,
                  context=None,
                  policy=None,
                  title=None,
                  ttl=None,
                  push_title=None,
                  push_body=None):
        """
        Authorize a transaction for the provided user. This
        get_service_service method would be utilized if you are
        using this as a secondary factor for user login or authorizing a
        single transaction within your application.
        This will NOT begin a user session.
        :param user: LaunchKey Username, User Push ID, or Directory User ID
        for the End User
        :param context: Arbitrary string of data up to 400 characters to be
        presented to the End User during
        authorization to provide context regarding the individual request
        :param policy: Authorization policy override for this authorization.
        The policy can only increase the security level any existing policy
        in the Service Profile. It can never reduce the security level of
        the Service Profile's policy.
        :param title: String of data up to 200 characters to be presented to
        the End User during authorization as the title of the individual
        authorization request
        :param ttl: Time for this authorization request to be valid. If no
        value is provided, the system default will be used.
        :param push_title: Title that will appear in the mobile authenticator's
        push message. This feature is only available for Directory Services
        that have push credentials configured.
        :param push_body: Body that will appear in the mobile authenticator's
        push message. This feature is only available for Directory Services
        that have push credentials configured.
        :raise: launchkey.exceptions.InvalidParameters - Input parameters were
        not correct
        :raise: launchkey.exceptions.InvalidPolicyInput - Input policy was not
        valid
        :raise: launchkey.exceptions.PolicyFailure - Auth creation failed due
        to user not passing policy
        :raise: launchkey.exceptions.EntityNotFound - Username was invalid or
        the user does not have any valid devices
        :raise: launchkey.exceptions.RateLimited - Too many authorization
        requests have been created for this user
        :raise: launchkey.exceptions.InvalidPolicy - The input policy is not
        valid. It should be a
        launchkey.clients.service.AuthPolicy.
        Please wait and try again.
        :return: String - Unique identifier for tracking status of the
        authorization request
        """
        warnings.warn(
            'This method has been deprecated and will be removed'
            ' in a future major release!', DeprecationWarning)
        auth = self.authorization_request(user, context, policy, title, ttl,
                                          push_title, push_body)
        return auth.auth_request

    @api_call
    def authorization_request(self,
                              user,
                              context=None,
                              policy=None,
                              title=None,
                              ttl=None,
                              push_title=None,
                              push_body=None,
                              denial_reasons=None):
        """
        Authorize a transaction for the provided user. This get_service_service
        method would be utilized if you are using this as a secondary factor
        for user login or authorizing a single transaction within your
        application. This will NOT begin a user session.
        :param user: LaunchKey Username, User Push ID, or Directory User ID
        for the End User
        :param context: Arbitrary string of data up to 400 characters to be
        presented to the End User during authorization to provide context
        regarding the individual request
        :param policy: Authorization policy override for this authorization.
        The policy can only increase the security
        level any existing policy in the Service Profile. It can never reduce
        the security level of the Service Profile's policy.
        :param title: String of data up to 200 characters to be presented to
        the End User during authorization as the title of the individual
        authorization request
        :param ttl: Time for this authorization request to be valid. If no
        value is provided, the system default will be used.
        :param push_title: Title that will appear in the mobile authenticator's
        push message. This feature is only available for Directory Services
        that have push credentials configured.
        :param push_body: Body that will appear in the mobile authenticator's
        push message. This feature is only available for Directory Services
        that have push credentials configured.
        :param denial_reasons: List of denial reasons to present to the user if
        they deny the request. This list must include at least two items. At
        least one of the items must have a fraud value of false and at least
        one of the items must have a fraud value of true. If no denial_reasons
        are given the defaults will be used. If a list is provided and denial
        context inquiry is not enabled for the Directory, this request will
        error. This feature is only available for Directory Services.
        :raise: launchkey.exceptions.InvalidParameters - Input parameters were
        not correct
        :raise: launchkey.exceptions.InvalidPolicyInput - Input policy was not
        valid
        :raise: launchkey.exceptions.PolicyFailure - Auth creation failed due
        to user not passing policy
        :raise: launchkey.exceptions.EntityNotFound - Username was invalid or
        the user does not have any valid devices
        :raise: launchkey.exceptions.RateLimited - Too many authorization
        requests have been created for this user
        :raise: launchkey.exceptions.InvalidPolicy - The input policy is not
        valid. It should be a launchkey.clients.service.AuthPolicy.
        Please wait and try again.
        :raise: launchkey.exceptions.AuthorizationInProgress - Authorization
        request already exists for the requesting user. That request either
        needs to be responded to, expire out, or be canceled with
        cancel_authorization_request().
        :return AuthorizationResponse: Unique identifier for tracking status
        of the authorization request
        """
        kwargs = {'username': user}
        if context is not None:
            kwargs['context'] = context
        if title is not None:
            kwargs['title'] = title
        if ttl is not None:
            kwargs['ttl'] = ttl
        if push_title is not None:
            kwargs['push_title'] = push_title
        if push_body is not None:
            kwargs['push_body'] = push_body
        if policy is not None:
            if not isinstance(policy, AuthPolicy):
                raise InvalidParameters(
                    "Please verify the input policy is a "
                    "launchkey.entities.service.AuthPolicy class")
            kwargs['policy'] = policy.get_policy()
        if denial_reasons is not None:
            if not isinstance(denial_reasons, (list, set)):
                raise InvalidParameters(
                    "Please ensure that input denial_reasons are a list of "
                    "launchkey.entities.service.DenialReason classes.")
            parsed_reasons = []
            for reason in denial_reasons:
                if not isinstance(reason, DenialReason):
                    raise InvalidParameters(
                        "Please verify that denial_reasons are "
                        "launchkey.entities.service.DenialReason classes.")
                parsed_reasons.append({
                    "id": reason.denial_id,
                    "reason": reason.reason,
                    "fraud": reason.fraud
                })
            kwargs['denial_reasons'] = parsed_reasons

        response = self._transport.post("/service/v3/auths", self._subject,
                                        **kwargs)
        data = self._validate_response(response, AuthorizeValidator)
        return AuthorizationRequest(data.get('auth_request'),
                                    data.get('push_package'),
                                    data.get('device_ids'))

    @api_call
    def get_advanced_authorization_response(self, authorization_request_id):
        """
        Request the response for a previous authorization call.
        :param authorization_request_id: Unique identifier returned by
        authorization_request()
        :raise: launchkey.exceptions.InvalidParameters - Input parameters were
        not correct
        :raise: launchkey.exceptions.RequestTimedOut - The authorization
        request has not been responded to before the
        timeout period (5 minutes)
        :raise: launchkey.exceptions.AuthorizationRequestCanceled - The
        authorization request has been canceled so a response cannot be
        retrieved.
        :return: None if the user has not responded otherwise a
        launchkey.entities.service.AdvancedAuthorizationResponse object
                 with the user's response
        in it
        """
        response = self._transport.get(
            "/service/v3/auths/%s" % authorization_request_id, self._subject)

        if response.status_code == 204:
            authorization_response = None
        else:
            data = self._validate_response(response,
                                           AuthorizationResponseValidator)
            authorization_response = AdvancedAuthorizationResponse(
                data, self._transport)

        return authorization_response

    @deprecated
    def get_authorization_response(self, authorization_request_id):
        """
        NOTE: This method is being deprecated. Use
        `get_advanced_authorization_response` instead!

        Request the response for a previous authorization call.
        :param authorization_request_id: Unique identifier returned by
        authorization_request()
        :raise: launchkey.exceptions.InvalidParameters - Input parameters were
        not correct
        :raise: launchkey.exceptions.RequestTimedOut - The authorization
        request has not been responded to before the
        timeout period (5 minutes)
        :raise: launchkey.exceptions.AuthorizationRequestCanceled - The
        authorization request has been canceled so a response cannot be
        retrieved.
        :return: None if the user has not responded otherwise a
        launchkey.entities.service.AuthorizationResponse object
                 with the user's response
        in it
        """
        advanced_authorization_response = \
            self.get_advanced_authorization_response(authorization_request_id)

        if not advanced_authorization_response:
            return None

        return AuthorizationResponse(advanced_authorization_response.data,
                                     advanced_authorization_response.transport)

    @api_call
    def cancel_authorization_request(self, authorization_request_id):
        """
        Request to cancel an authorization request for the End User
        :param authorization_request_id: Unique identifier returned by
        authorization_request()
        :raise: launchkey.exceptions.InvalidParameters - Input parameters were
        not correct
        :raise: launchkey.exceptions.EntityNotFound - The authorization
        request does not exist.
        :raise: launchkey.exceptions.AuthorizationRequestCanceled - The
        authorization request has already been canceled.
        :raise: launchkey.exceptions.AuthorizationResponseExists - The
        authorization request has already been responded to so it cannot be
        canceled.
        """
        self._transport.delete(
            "/service/v3/auths/%s" % authorization_request_id, self._subject)

    @api_call
    def session_start(self, user, authorization_request_id):
        """
        Request to start a Service Session for the End User which was derived
        from a authorization request
        :param user: LaunchKey Username, User Push ID, or Directory User ID for
        the End User
        :param authorization_request_id: Unique identifier returned by
        authorization_request()
        :raise: launchkey.exceptions.InvalidParameters - Input parameters were
        not correct
        :raise: launchkey.exceptions.EntityNotFound - The input username was
        not valid
        """
        self._transport.post("/service/v3/sessions",
                             self._subject,
                             username=user,
                             auth_request=authorization_request_id)

    @api_call
    def session_end(self, user):
        """
        Request to end a Service Session for the End User
        :param user: LaunchKey Username, User Push ID, or Directory User ID for
        the End User
        :raise: launchkey.exceptions.InvalidParameters - Input parameters were
        not correct
        :raise: launchkey.exceptions.EntityNotFound - The input username was
        not valid
        """
        self._transport.delete("/service/v3/sessions",
                               self._subject,
                               username=user)

    @api_call
    def verify_totp(self, user, otp):
        """
        Verifies a given TOTP is valid for a given user.
        :param user: Unique value identifying the End User in your
        system. This value was used to create the Directory User and Link
        Device.
        :param otp: 6-8 digit OTP code for to verify.
        :return: Boolean stating whether the given OTP code is valid.
        :raise: launchkey.exceptions.EntityNotFound - Unable to find TOTP
        configuration for given user.
        """
        response = self._transport.post("/service/v3/totp",
                                        self._subject,
                                        identifier=user,
                                        otp=otp)
        data = self._validate_response(response,
                                       ServiceTOTPVerificationValidator)

        return data["valid"]

    def handle_advanced_webhook(self, body, headers, method=None, path=None):
        """
        Handle an advanced webhook callback
        In the event of a Logout webhook, be sure to call session_end() when
        you complete the process of ending the user's session in your
        implementation.  This will remove the corresponding Application from
        the authorization list on all of the the user's mobile devices.
        :param body: The raw body that was send in the POST content
        :param headers: A generic map of response headers. These will be used
        to access and validate authorization
        :param path:  The path of the request
        :param method: The HTTP method of the request
        :return: launchkey.entities.service.SessionEndRequest or
        launchkey.entities.service.AdvancedAuthorizationResponse
        :raises launchkey.exceptions.UnexpectedWebhookRequest: when the
        request or its cannot be parsed or fails
        validation.
        :raises launchkey.exceptions.UnableToDecryptWebhookRequest: when the
        request is an authorization response webhook and the request body
        cannot be decrypted
        :raises launchkey.exceptions.UnexpectedAuthorizationResponse: when the
        decrypted auth package is missing required data. This error is
        indicative of a non webhook request being sent to the method.
        :raises launchkey.exceptions.UnexpectedKeyID: when the auth package in
        an authorization response webhook request body is decrypted using a
        public key whose private key is not known by the client. This can be
        a configuration issue.
        :raises launchkey.exceptions.UnexpectedDeviceResponse: when the auth
        package received from the device is invalid. This error is
        indicative of a man in the middle (MITM) attack.
        :raises launchkey.exceptions.WebhookAuthorizationError: when the
        "Authorization" header in the headers.
        """
        if method is None:
            warnings.warn(
                "Not passing a valid request method string is "
                "deprecated and will be required in the next "
                "major version", PendingDeprecationWarning)

        if path is None:
            warnings.warn(
                "Not passing a valid request path string is "
                "deprecated and will be required in the next "
                "major version", PendingDeprecationWarning)
        try:
            if "service_user_hash" in "%s" % body:
                body = self.x_iov_jwt_service.verify_jwt_request(
                    body, headers, method, path)
                try:
                    body = self._validate_response(loads(body),
                                                   AuthorizeSSEValidator)
                except UnexpectedAPIResponse as reason:
                    raise UnexpectedWebhookRequest(reason=reason)
                result = SessionEndRequest(
                    body['service_user_hash'],
                    self._transport.parse_api_time(body['api_time']))
            else:
                try:
                    decrypted_body = self.x_iov_jwt_service.decrypt_jwe(
                        body, headers, method, path)
                    auth_response = loads(decrypted_body)
                    result = AdvancedAuthorizationResponse(
                        auth_response, self._transport)
                except XiovJWTDecryptionFailure as reason:
                    raise UnableToDecryptWebhookRequest(reason=reason)
                except KeyError as reason:
                    raise UnexpectedAuthorizationResponse(reason=reason)
        except XiovJWTValidationFailure as reason:
            raise UnexpectedWebhookRequest(reason)

        return result

    @deprecated
    def handle_webhook(self, body, headers, method=None, path=None):
        """
        NOTE: This method is being deprecated. Use `handle_advanced_webhook`
        instead!

        Handle a webhook callback
        In the event of a Logout webhook, be sure to call session_end() when
        you complete the process of ending the user's session in your
        implementation.  This will remove the corresponding Application from
        the authorization list on all of the the user's mobile devices.
        :param body: The raw body that was send in the POST content
        :param headers: A generic map of response headers. These will be used
        to access and validate authorization
        :param path:  The path of the request
        :param method: The HTTP method of the request
        :return: launchkey.entities.service.SessionEndRequest or
        launchkey.entities.service.AuthorizationResponse
        :raises launchkey.exceptions.UnexpectedWebhookRequest: when the
        request or its cannot be parsed or fails
        validation.
        :raises launchkey.exceptions.UnableToDecryptWebhookRequest: when the
        request is an authorization response webhook and the request body
        cannot be decrypted
        :raises launchkey.exceptions.UnexpectedAuthorizationResponse: when the
        decrypted auth package is missing required data. This error is
        indicative of a non webhook request being sent to the method.
        :raises launchkey.exceptions.UnexpectedKeyID: when the auth package in
        an authorization response webhook request body is decrypted using a
        public key whose private key is not known by the client. This can be
        a configuration issue.
        :raises launchkey.exceptions.UnexpectedDeviceResponse: when the auth
        package received from the device is invalid. This error is
        indicative of a man in the middle (MITM) attack.
        :raises launchkey.exceptions.WebhookAuthorizationError: when the
        "Authorization" header in the headers.
        """
        advanced_authorization_response = self.handle_advanced_webhook(
            body, headers, method, path)

        if isinstance(advanced_authorization_response, SessionEndRequest):
            return advanced_authorization_response

        return AuthorizationResponse(advanced_authorization_response.data,
                                     advanced_authorization_response.transport)
コード例 #4
0
class TestXiovJwtService(unittest.TestCase):
    def setUp(self):
        self._transport = MagicMock(spec=JOSETransport)
        self.x_iov_jwt_service = XiovJWTService(self._transport, "subject")
        self._headers = {"X-IOV-JWT": "jwt", "Other Header": "jwt"}

    def test_verify_jwt_request_returns_body_on_success(self):
        response = self.x_iov_jwt_service.verify_jwt_request(
            "body", self._headers, "method", "path")
        self.assertEqual(response, "body")

    def test_verify_jwt_request_returns_decoded_body_when_given_as_bytes(self):
        response = self.x_iov_jwt_service.verify_jwt_request(
            b"body", self._headers, "method", "path")
        self.assertEqual(response, u"body")

    def test_verify_jwt_missing_x_iov_jwt_raises_webhook_authorization_error(
            self):
        with self.assertRaises(WebhookAuthorizationError):
            self.x_iov_jwt_service.verify_jwt_request("body", {}, "method",
                                                      "path")

    def test_transport_verify_jwt_request_called_with_correct_params(self):
        self.x_iov_jwt_service.verify_jwt_request("body", self._headers,
                                                  "method", "path")
        self._transport.verify_jwt_request.assert_called_with(
            "jwt", "subject", "method", "path", "body")

    def test_transport_verify_jwt_request_jwt_validation_failure_raises_x_iov_jwt_validation_failure(
            self):
        self._transport.verify_jwt_request.side_effect = JWTValidationFailure
        with self.assertRaises(XiovJWTValidationFailure):
            self.x_iov_jwt_service.verify_jwt_request("body", self._headers,
                                                      "method", "path")

    def test_transport_verify_jwt_request_invalid_jwt_response_raises_x_iov_jwt_validation_failure(
            self):
        self._transport.verify_jwt_request.side_effect = InvalidJWTResponse
        with self.assertRaises(XiovJWTValidationFailure):
            self.x_iov_jwt_service.verify_jwt_request("body", self._headers,
                                                      "method", "path")

    def test_decrypt_jwe_calls_verify_jwt_request(self):
        patched = patch.object(self.x_iov_jwt_service, 'verify_jwt_request')
        verify_jwt_request_patch = patched.start()
        self.addCleanup(patched.stop)

        self.x_iov_jwt_service.decrypt_jwe("body", self._headers, "method",
                                           "path")
        verify_jwt_request_patch.assert_called_with("body", self._headers,
                                                    "method", "path")

    def test_decrypt_jwe_transport_decrtypt_response_params(self):
        patched = patch.object(self.x_iov_jwt_service, 'verify_jwt_request')
        verify_jwt_request_patch = patched.start()
        self.addCleanup(patched.stop)

        self.x_iov_jwt_service.decrypt_jwe("body", self._headers, "method",
                                           "path")
        self._transport.decrypt_response.assert_called_with(
            verify_jwt_request_patch.return_value)

    def test_decrypt_jwe_jwekest_exception_raises_x_iov_jwt_decryption_failure(
            self):
        self._transport.decrypt_response.side_effect = JWKESTException
        with self.assertRaises(XiovJWTDecryptionFailure):
            self.x_iov_jwt_service.decrypt_jwe("body", self._headers, "method",
                                               "path")