Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user authentication and the possibility to trigger/send a message… #207

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python: [2.7, 3.6, 3.7, 3.8, "3.10"]
python: [3.6, 3.7, 3.8, "3.10"]

name: Python ${{ matrix.python }} Test

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ var/

pip-log.txt
pip-delete-this-directory.txt

.idea
64 changes: 63 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ In order to use this library, you need to have a free account on <http://pusher.

## Supported Platforms

* Python - supports Python versions 2.7, 3.6 and above
* Python - supports Python version 3.6 and above

## Features

Expand All @@ -23,11 +23,13 @@ In order to use this library, you need to have a free account on <http://pusher.
- [Getting started](#getting-started)
- [Configuration](#configuration)
- [Triggering Events](#triggering-events)
- [Send a message to a specific user](#send-a-message-to-a-specific-user)
- [Querying Application State](#querying-application-state)
- [Getting Information For All Channels](#getting-information-for-all-channels)
- [Getting Information For A Specific Channel](#getting-information-for-a-specific-channel)
- [Getting User Information For A Presence Channel](#getting-user-information-for-a-presence-channel)
- [Authenticating Channel Subscription](#authenticating-channel-subscription)
- [Authenticating User](#authenticating-user)
- [Terminating User Connections](#terminating-user-connections)
- [End-to-end Encryption](#end-to-end-encryption)
- [Receiving Webhooks](#receiving-webhooks)
Expand Down Expand Up @@ -164,6 +166,30 @@ pusher_client.trigger_batch([
])
```

### Send a message to a specific user

#### `Pusher::send_to_user`

|Argument |Description |
|:-:|:-:|
|user_id `String` |**Required** <br> The user id |
|event `String`| **Required** <br> The name of the event you wish to trigger. |
|data `JSONable data` | **Required** <br> The event's payload |

|Return Values |Description |
|:-:|:-:|
|buffered_events `Dict` | A parsed response that includes the event_id for each event published to a channel. See example. |

`Pusher::trigger` will throw a `TypeError` if called with parameters of the wrong type; or a `ValueError` if called on more than 100 channels, with an event name longer than 200 characters, or with more than 10240 characters of data (post JSON serialisation).

##### Example

This call will send a message to the user with id `'123'`.

```python
pusher_client.send_to_user( u'123', u'some_event', {u'message': u'hello worlds'})
```

## Querying Application State

### Getting Information For All Channels
Expand Down Expand Up @@ -288,6 +314,40 @@ auth = pusher_client.authenticate(
# return `auth` as a response
```

## Authenticating User

#### `Pusher::authenticate_user`

To authenticate users on Pusher Channels on your application, you can use the authenticate_user function:

|Argument |Description |
|:-:|:-:|
|socket_id `String` | **Required**<br> The channel's socket_id, also sent to you in the POST request |
|user_data `Dict` |**Required for presence channels** <br> This will be a dictionary containing the data you want associated with a user. An `"id"` key is *required* |

|Return Values |Description |
|:-:|:-:|
|response `Dict` | A dictionary to send as a response to the authentication request.|

For more information see:
* [authenticating users](https://pusher.com/docs/channels/server_api/authenticating-users/)
* [auth-signatures](https://pusher.com/docs/channels/library_auth_reference/auth-signatures/)

##### Example

###### User Authentication

```python
auth = pusher_client.authenticate_user(
socket_id=u"1234.12",
user_data = {
u'id': u'123',
u'name': u'John Smith'
}
)
# return `auth` as a response
```

## Terminating user connections

TIn order to terminate a user's connections, the user must have been authenticated. Check the [Server user authentication docs](http://pusher.com/docs/authenticating_users) for the information on how to create a user authentication endpoint.
Expand Down Expand Up @@ -406,9 +466,11 @@ Feature | Supported
-------------------------------------------| :-------:
Trigger event on single channel | *&#10004;*
Trigger event on multiple channels | *&#10004;*
Trigger event to a specifc user | *&#10004;*
Excluding recipients from events | *&#10004;*
Authenticating private channels | *&#10004;*
Authenticating presence channels | *&#10004;*
Authenticating users | *&#10004;*
Get the list of channels in an application | *&#10004;*
Get the state of a single channel | *&#10004;*
Get a list of users in a presence channel | *&#10004;*
Expand Down
58 changes: 39 additions & 19 deletions pusher/authentication_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
ensure_binary,
validate_channel,
validate_socket_id,
validate_user_data,
channel_name_re
)
)

from pusher.client import Client
from pusher.http import GET, POST, Request, request_method
Expand All @@ -31,21 +32,21 @@

class AuthenticationClient(Client):
def __init__(
self,
app_id,
key,
secret,
ssl=True,
host=None,
port=None,
timeout=5,
cluster=None,
encryption_master_key=None,
encryption_master_key_base64=None,
json_encoder=None,
json_decoder=None,
backend=None,
**backend_options):
self,
app_id,
key,
secret,
ssl=True,
host=None,
port=None,
timeout=5,
cluster=None,
encryption_master_key=None,
encryption_master_key_base64=None,
json_encoder=None,
json_decoder=None,
backend=None,
**backend_options):

super(AuthenticationClient, self).__init__(
app_id,
Expand All @@ -63,7 +64,6 @@ def __init__(
backend,
**backend_options)


def authenticate(self, channel, socket_id, custom_data=None):
"""Used to generate delegated client subscription token.
Expand All @@ -89,7 +89,7 @@ def authenticate(self, channel, socket_id, custom_data=None):
signature = sign(self.secret, string_to_sign)

auth = "%s:%s" % (self.key, signature)
response_payload = { "auth": auth }
response_payload = {"auth": auth}

if is_encrypted_channel(channel):
shared_secret = generate_shared_secret(
Expand All @@ -102,6 +102,25 @@ def authenticate(self, channel, socket_id, custom_data=None):

return response_payload

def authenticate_user(self, socket_id, user_data=None):
"""Creates a user authentication signature.
:param socket_id: id of the socket that requires authorization
:param user_data: used to provide user info
"""
validate_user_data(user_data)
socket_id = validate_socket_id(socket_id)
andersonrocha0 marked this conversation as resolved.
Show resolved Hide resolved

user_data_encoded = json.dumps(user_data, cls=self._json_encoder)

string_to_sign = "%s::user::%s" % (socket_id, user_data_encoded)

signature = sign(self.secret, string_to_sign)

auth_response = "%s:%s" % (self.key, signature)
response_payload = {"auth": auth_response, 'user_data': user_data_encoded}

return response_payload

def validate_webhook(self, key, signature, body):
"""Used to validate incoming webhook messages. When used it guarantees
Expand Down Expand Up @@ -131,7 +150,8 @@ def validate_webhook(self, key, signature, body):
if not time_ms:
return None

if abs(time.time()*1000 - time_ms) > 300000:
if abs(time.time() * 1000 - time_ms) > 300000:
return None

return body_data

46 changes: 26 additions & 20 deletions pusher/pusher.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from pusher.util import (
ensure_text,
pusher_url_re,
doc_string)
doc_string, validate_user_id)

from pusher.pusher_client import PusherClient
from pusher.authentication_client import AuthenticationClient
Expand Down Expand Up @@ -45,21 +45,21 @@ class Pusher(object):
:param backend_options: additional backend
"""
def __init__(
self,
app_id,
key,
secret,
ssl=True,
host=None,
port=None,
timeout=5,
cluster=None,
encryption_master_key=None,
encryption_master_key_base64=None,
json_encoder=None,
json_decoder=None,
backend=None,
**backend_options):
self,
app_id,
key,
secret,
ssl=True,
host=None,
port=None,
timeout=5,
cluster=None,
encryption_master_key=None,
encryption_master_key_base64=None,
json_encoder=None,
json_decoder=None,
backend=None,
**backend_options):

self._pusher_client = PusherClient(
app_id,
Expand Down Expand Up @@ -93,7 +93,6 @@ def __init__(
backend,
**backend_options)


@classmethod
def from_url(cls, url, **options):
"""Alternative constructor that extracts the information from a URL.
Expand Down Expand Up @@ -123,7 +122,6 @@ def from_url(cls, url, **options):

return cls(**options_)


@classmethod
def from_env(cls, env='PUSHER_URL', **options):
"""Alternative constructor that extracts the information from an URL
Expand All @@ -143,12 +141,16 @@ def from_env(cls, env='PUSHER_URL', **options):

return cls.from_url(val, **options)


@doc_string(PusherClient.trigger.__doc__)
def trigger(self, channels, event_name, data, socket_id=None):
return self._pusher_client.trigger(
channels, event_name, data, socket_id)

@doc_string(PusherClient.trigger.__doc__)
def send_to_user(self, user_id, event_name, data):
validate_user_id(user_id)
user_server_string = "#server-to-user-%s" % user_id
return self._pusher_client.trigger([user_server_string], event_name, data)

@doc_string(PusherClient.trigger_batch.__doc__)
def trigger_batch(self, batch=[], already_encoded=False):
Expand All @@ -158,7 +160,6 @@ def trigger_batch(self, batch=[], already_encoded=False):
def channels_info(self, prefix_filter=None, attributes=[]):
return self._pusher_client.channels_info(prefix_filter, attributes)


@doc_string(PusherClient.channel_info.__doc__)
def channel_info(self, channel, attributes=[]):
return self._pusher_client.channel_info(channel, attributes)
Expand All @@ -176,6 +177,11 @@ def authenticate(self, channel, socket_id, custom_data=None):
return self._authentication_client.authenticate(
channel, socket_id, custom_data)

@doc_string(AuthenticationClient.authenticate_user.__doc__)
def authenticate_user(self, socket_id, user_data=None):
return self._authentication_client.authenticate_user(
socket_id, user_data
)

@doc_string(AuthenticationClient.validate_webhook.__doc__)
def validate_webhook(self, key, signature, body):
Expand Down
Loading