Authenticating users
Application security is very important so Channels provides a mechanism for authenticating a user’s access to a channel at the point of subscription.
To do so you must expose an authentication endpoint from your own servers that is called by the Channels WebSocket libraries. Your server can then respond to either allow or deny the subscription.
This can be used both to restrict access to private channels, and in the case of presence channels notify subscribers of who else is also subscribed via presence events.
- Setting the channel authentication endpoint
- Setting the channel authentication transport (JS only)
- Implementing authentication endpoints
- Batching auth requests
- The authentication process
Setting the Channel Authentication endpoint
The destination of the authentication request can be configured.
new Pusher('app_key', { authEndpoint: '/pusher_auth.php' });
The default value for this is: /pusher/auth
If the authentication endpoint is protected by a CSRF filter, then you can pass in a CSRF token via the auth
hash under headers
.
var pusher = new Pusher('app_key', {
authEndpoint: '/pusher_auth.php',
auth: {
headers: {
'X-CSRF-Token': "SOME_CSRF_TOKEN"
}
}
});
Note that you should change the name of the CSRF token key to the convention you prefer.
As an example, in Rails, you can inject the CSRF token into Javacript like this using ERB
<script>
var pusher = new Pusher('app_key', {
authEndpoint: '/pusher/auth',
auth: {
headers: {
'X-CSRF-Token': "<%= form_authenticity_token %>"
}
}
});
</script>
In order to connect to a private or presence channel using libPusher, you first need to configure your server authorisation URL.
_client.authorizationURL = [NSURL URLWithString:@"http://www.yourserver.com/authorise"];
When you attempt to connect to a private or presence channel, libPusher will make a form-encoded POST request to the above URL, passing along the socket_id
and channel_name
as parameters. Prior to sending the request, the Channels delegate will be notified, passing in the NSMutableURLRequest instance that will be sent.
Its up to you to configure the request to handle whatever authentication mechanism you are using. In this example, we set a custom header with a token which the server will use to authenticate the user before proceeding with authorisation.
- (void)pusher:(PTPusher *)pusher willAuthorizeChannel:(PTPusherChannel *)channel withRequest:(NSMutableURLRequest *)request
{
[request setValue:@"some-authentication-token" forHTTPHeaderField:@"X-MyCustom-AuthTokenHeader"];
}
HttpAuthorizer authorizer = new HttpAuthorizer("http://example.com/some_auth_endpoint");
PusherOptions options = new PusherOptions().setAuthorizer(authorizer);
Pusher pusher = new Pusher(YOUR_APP_KEY, options);
Setting the Channel Authentication transport
Channel authentication transport is only applicable to the Channels JavaScript library.
The authentication transport can be configured by passing an option to the Pusher
constructor.
new Pusher('app_key', { authTransport: 'jsonp' });
The default value for this is: ajax
.
‘ajax’ authentication transport
The HTTP POST
request that is made to the authentication endpoint when a subscription takes place contains the following request parameters:
socket_id
- A unique identifier for the specific client connection to Channels
channel_name
- The name of the channel being subscribed to
‘jsonp’ authentication transport
JSONp authentication can be used when the authentication endpoint is on a different domain to the web application. An additional parameter called callback
will also be passed identifying the JavaScript function to be called from the script generated by the authentication endpoint.
Note: To use JSONp support, you must use Channels client version 1.6 or above.
As described above you need to set the authTransport
Channels constructor option to jsonp
and optionally pass authEndpoint
value.
When using JSONp the authentication the following parameters will be passed as query parameters (a GET
request):
socket_id
- A unique identifier for the specific client connection to Channels
channel_name
- The name of the channel being subscribed to
callback
- The name of the function to be called in the script to be generated by the authentication endpoint
As discussed, the duty of the server is to check the user has permission to subscribe to the supplied channel. If the user does have permission then an authentication HTTP response must be returned in a JSON format with an authentication signature. We have a number of server libraries that make doing this really simple.
Note: If you don’t want to use one of the existing libraries, or there isn’t one in the technology you want to use, please see authenticating signatures or get in touch.
Implementing authentication endpoints
The following sections show how this is achieved for the different types of channels.
General notes
In all cases, the format of the response is very similar:
- Unsuccessful responses from an authentication endpoint should serve a
403 Forbidden
HTTP status. - Successful responses from an authentication endpoint should carry a
200 OK
HTTP status and a body of the form
{
"auth": $AUTHORIZATION_STRING
}
Details of $AUTHORIZATION_STRING
described below.
Types of channel
- Private channels - used to control access to a source of information
- Presence channels - used to control access, but also allows info about the user to be distributed to other users.
Implementing the auth endpoint for a private channel
Here are some examples of private channel authentication for different languages:
Note: These examples assume that your Channels client is already configured. You can find details on how to configure your Channels client here.
class PusherController < ApplicationController
def auth
if current_user
response = pusher_client.authenticate(params[:channel_name], params[:socket_id])
render json: response
else
render text: 'Forbidden', status: '403'
end
end
end
global $user;
if ($user->uid)
{
echo $pusher->socket_auth($_POST['channel_name'], $_POST['socket_id']);
}
else
{
header('', true, 403);
echo "Forbidden";
}
if ( is_user_logged_in() )
{
echo $pusher->socket_auth($_POST['channel_name'], $_POST['socket_id']);
}
else
{
header('', true, 403);
echo "Forbidden";
}
var express = require('express');
var bodyParser = require('body-parser');
var app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.post('/pusher/auth', function(req, res) {
var socketId = req.body.socket_id;
var channel = req.body.channel_name;
var auth = pusher.authenticate(socketId, channel);
res.send(auth);
});
var port = process.env.PORT || 5000;
app.listen(port);
using PusherServer;
public class MyController : Controller
{
public ActionResult Auth(string channel_name, string socket_id)
{
var auth = pusher.Authenticate( channel_name, socketId );
var json = auth.ToJson();
return new ContentResult { Content = json, ContentType = "application/json" };
}
}
@app.route("/pusher/auth", methods=['POST'])
def pusher_authentication():
auth = pusher.authenticate(
channel=request.form['channel_name'],
socket_id=request.form['socket_id']
)
return json.dumps(auth)
func pusherAuth(res http.ResponseWriter, req *http.Request) {
params, _ := ioutil.ReadAll(req.Body)
response, err := client.AuthenticatePrivateChannel(params)
if err != nil {
panic(err)
}
fmt.Fprintf(res, string(response))
}
func main() {
http.HandleFunc("/pusher/auth", pusherAuth)
http.ListenAndServe(":5000", nil)
}
#This will authorise _all_ users. Only use for debugging!
pusher generate auth-server --app-id APP_ID
The generated JSON should look as follows:
{
"auth":"278d425bdf160c739803:a99e78e7cd40dcd0d4ae06be0a5395b6cd3c085764229fd40b39ce92c39af33e"
}
Implementing the auth endpoint for a presence channel
Authentication of a presence channel is performed in exactly the same way as a private channel but the JSON response must have a channel_data
property containing information that you wish to share about the current user.
Here are some examples of presence channel authentication for different languages:
Note: These examples assume that your Channels client is already configured. You can find details on how to configure your Channels client here.
class PusherController < ApplicationController
def auth
if current_user
response = pusher_client.authenticate(params[:channel_name], params[:socket_id], {
user_id: current_user.id, # => required
user_info: { # => optional - for example
name: current_user.name,
email: current_user.email
}
})
render json: response
else
render text: 'Forbidden', status: '403'
end
end
end
global $user;
if ($user->uid)
{
$presence_data = array('name' => $user->name);
echo $pusher->presence_auth($_POST['channel_name'], $_POST['socket_id'], $user->uid, $presence_data);
}
else
{
header('', true, 403);
echo( "Forbidden" );
}
if ( is_user_logged_in() )
{
global $current_user;
get_currentuserinfo();
$presence_data = array('name' => $current_user->display_name);
echo $pusher->presence_auth($_POST['channel_name'], $_POST['socket_id'], $current_user->ID, $presence_data);
}
else
{
header('', true, 403);
echo( "Forbidden" );
}
using PusherServer;
public class MyController : Controller
{
public ActionResult Auth(string channel_name, string socket_id)
{
var channelData = new PresenceChannelData() {
user_id: "unique_user_id",
user_info: new {
name = "Mr Channels",
twitter_id = "@pusher"
}
};
var auth = pusher.Authenticate( channelName, socketId, channelData );
var json = auth.ToJson();
return new ContentResult { Content = json, ContentType = "application/json" };
}
}
var express = require('express');
var bodyParser = require('body-parser');
var app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.post('/pusher/auth', function(req, res) {
var socketId = req.body.socket_id;
var channel = req.body.channel_name;
var presenceData = {
user_id: 'unique_user_id',
user_info: {
name: 'Mr Channels',
twitter_id: '@pusher'
}
};
var auth = pusher.authenticate(socketId, channel, presenceData);
res.send(auth);
});
var port = process.env.PORT || 5000;
app.listen(port);
@app.route("/pusher/auth", methods=['POST'])
def pusher_authentication():
auth = pusher.authenticate(
channel=request.form['channel_name'],
socket_id=request.form['socket_id'],
custom_data={
u'user_id': u'1',
u'user_info': {
u'twitter': u'@pusher'
}
}
)
return json.dumps(auth)
params, _ := ioutil.ReadAll(req.Body)
presenceData := pusher.MemberData{
UserId: "1",
UserInfo: map[string]string{
"twitter": "pusher",
},
}
response, err := client.AuthenticatePresenceChannel(params, presenceData)
if err != nil {
panic(err)
}
fmt.Fprintf(res, response)
The generated JSON should look similar to the private channel JSON with an additional channel_data
property which should be valid JSON encoded as a string:
{
"auth":"49e26cb8e9dde3dfc009:a8cf1d3deefbb1bdc6a9d1547640d49d94b4b512320e2597c257a740edd1788f",
"channel_data":"{\"user_id\":\"Phil Leggetter\",\"user_info\":{\"name\":\"Phil Leggetter\",\"imageUrl\":\"http:\\\/\\\/www.gravatar.com\\\/avatar\\\/ecc56977271e781991b6172c16248459?s=80&d=mm&r=g\"}}"
}
JSONp authentication endpoints
JSONp auth endpoints are only applicable to the Channels JavaScript library.
As discussed above the authTransport
should be set to jsonp
in order to use JSONp authentication. If JSONp authentication is being used then it is highly likely that the endpoint is on a different domain so the authEndpoint
should also be set.
<script src="//js.pusher.com/4.2/pusher.min.js"></script>
<script>
var pusher = new Pusher('MY_PUSHER_KEY', {
authTransport: 'jsonp',
authEndpoint: 'http://myserver.com/pusher_jsonp_auth'
});
</script>
Then you would subscribe to a private or presence channel, as needed.
<script>
var channel = pusher.subscribe('private-my-channel');
channel.bind('greet', function(data) {
alert(data.greeting);
});
</script>
As demonstrated in the sequence diagram the server would then receive a call. Authentication works in much the same way as it does with an AJAX request. However, the request parameters are passed in the query string, and the authentication response must be a string which represents a JavaScript function call which indicates the authentication response. The function to be called is identified by the callback
parameter in the query string. Because the JavaScript code returned in the response is executed by the browser, the response needs to have a content type of application/javascript
.
The following examples demonstrate how private channels are authenticated and how the response is wrapped in a callback. Presence channels are authenticated as demonstrated in the Presence endpoint section and the callbacks wrapped in exactly the same way as shown here.
class PusherController < ApplicationController
def auth
if current_user
auth = pusher_client.authenticate(params[:channel_name], params[:socket_id])
render(
text: params[:callback] + "(" + auth.to_json + ")",
content_type: 'application/javascript'
)
else
render text: 'Forbidden', status: '403'
end
end
end
// Express.js setup
// http://expressjs.com/
...
app.get("/pusher/auth", function(req, res) {
var query = req.query;
var socketId = query.socket_id;
var channel = query.channel_name;
var callback = query.callback;
var presenceData = {
user_id: "some_id",
user_info: {
name: "John Smith"
}
};
var auth = JSON.stringify(pusher.authenticate(socketId, channel, presenceData));
var cb = callback.replace(/\"/g,"") + "(" + auth + ");";
res.set({
"Content-Type": "application/javascript"
});
res.send(cb);
});
global $user;
if ($user->uid)
{
$pusher = new Pusher(APP_KEY, APP_SECRET, APP_ID);
$auth = $pusher->socket_auth($_GET['channel_name'], $_GET['socket_id']);
$callback = str_replace('\\', '', $_GET['callback']);
header('Content-Type: application/javascript');
echo($callback . '(' . $auth . ');');
}
else
{
header('', true, 403);
echo "Forbidden";
}
if ( is_user_logged_in() )
{
$pusher = new Pusher(APP_KEY, APP_SECRET, APP_ID);
$auth = $pusher->socket_auth($_GET['channel_name'], $_GET['socket_id']);
$callback = str_replace('\\', '', $_GET['callback']);
header('Content-Type: application/javascript');
echo($callback . '(' . $auth . ');');
}
else
{
header('', true, 403);
echo "Forbidden";
}
Example auth project
If you’d like to start experimenting with private or presence channels right away, take a look at the pusher-channels-auth-example repo. It’s a small server with an auth endpoint that approves every request, it can be quickly deployed to heroku for convenience.
Batching auth requests (aka multi-auth)
Currently, pusher-js itself does not support authenticating multiple channels in one HTTP request. However, thanks to Dirk Bonhomme you can use the pusher-js-auth plugin that buffers subscription requests and sends auth requests to your endpoint in batches.
The authentication process
The basic mechanism for doing this is broadly the same for both scenarios. The following sequence diagram shows the signing process:
- When a new instance of the
Pusher
object is created a new WebSocket object is created. - The WebSocket object connects to the Channels WebSocket endpoint.
-
Once the connection has been established a universally unique
socket_id
is returned to the Channels JavaScript library. - A subscription is made to a
private-
orpresence-
channel. - The
private-
orpresence-
prefix identifies the channel as requiring authentication so a request is made to an authentication endpoint via either AJAX or JSONp. For more information see channel authentication transports. - If successful your application returns an authorisation string (see generating the authentication signature) to the Channels JavaScript library signed with your Channels secret.
- The
channel_name
and authentication signature is sent to Channels over the WebSocket, which completes the authorisation if the authorisation string has been correctly signed.
For example, in both the subscribe
calls below an authentication request will take place.
var pusher = new Pusher('APP_KEY');
var privateChannel = pusher.subscribe('private-channel');
var presenceChannel = pusher.subscribe('presence-channel');
The purpose of the authentication callback is so that you can check that the current user of your application has permission to access the channel that they are trying to subscribe to. How the user is authenticated depends on the decisions you make and the system you are developing.
Have you tried using the search to find what you’re after? If you still have a question then get in touch with us and let us help you out.