Recommendations for incrementally authorising new capabilities

Ably's auth uses a system of immutable tokens. A token is needed for a client to connect to the Ably service, which it obtains from your auth server. Each token has a fixed set of capabilities, listing the channels or namespaces that that client can access and what they can do with them. See documentation/general/authentication for more info and an introduction to authentication in Ably.

For most usecases, this simple approach is preferable. For one thing, it means that your servers are always the single source of truth for all permission information. A client can only ever do something with a token generated by (or a token request signed by) your server, and all tokens are time-limited. If permissions were set and revoked by requests to Ably, then permissions could get out of sync between your servers and Ably (e.g. if a request to revoke a privilege fails) — if permissions are entirely controlled by your servers, this isn't possible.

However, in some usecases, your server may not know in advance every capability a client should have. For example, a client may want access to another channel it didn't previously know it would need to access.

Ably handles this by allowing clients to reauthorize. That is, once they've obtained a new token from your auth server, they can upgrade the connection to use this token, by calling auth#authorize. (This upgrade is now done without any disruption to the connection; in older 0.8 client libraries, it would briefly disconnect, reconnect with the new token and then recover the state).
 
In most use-cases, the client will know when it needs to re-authorize. There are also usecases where the server would want to instruct the client to re-authorize, for example to revoke some permission. There's no special mechanism for this; you can use whatever mechanism you normally use to communicate with the client to tell it to call authorize(), such as a message sent over an Ably channel that the client is listening on. If all else fails, you can wait until the token expires, at which point the client will be forced to seek a new token from your auth server. (You can specify how long the token is valid for at creation time; the default is one hour).
Note: We have also recently added a token revocation API. Do reach out if you are interested in enabling this feature.

Reauthorization strategies


There are several different ways you can handle reauthorization with your auth server.

Capabilities entirely determined by your server


In many cases, your auth server is able to keep state on each client. For example, if you have an app which clients log in to, and your server keeps track of sessions. Your server may know the complete list of what capabilities each client should have at any time. Then when the client requests a new token (eg using `authUrl`), you just generate a token request with that canonical list.

Capabilities stored on your server, client can request changes


Sometimes your server may not know the complete list — for example, if a client should be able to request an additional capability that the server wouldn't know about beforehand. In that case, the client can tell the server about that when it requests the token, either (with `authUrl`) by using the `authParams` or `authHeaders` attributes of `authOptions` to send some data as either a param or a header, or for more flexibility by using the `authCallback` feature, which lets you construct whatever kind of request to your server you like (e.g. a POST with data in the body). See https://www.ably.io/documentation/realtime/authentication#auth-options for more information. Either way, when your server gets that request, it can check that the client should be allowed that new capability, add it to the total list of capabilities that it's keeping track of, and generate a token request with that list.

Capabilities stored on the client, server reauthorizes them all for every change


In a few cases, your auth server cannot keep any state on active clients (e.g. you're not using sessions etc.). In this case, the client needs to keep a list of all capabilities it needs itself. When it needs a new capability, it would give the server all those capabilities (in the same ways as in the previous paragraph). The server would check the client should have access to each of those capabilities, then generate a token request.

Capabilities stored on the client, server incrementally authorizes new ones


Very rarely, the server cannot keep state on clients, but also cannot re-check that the client should have access to all capabilities each time the client requests a change - for example, if checking permissions is a very expensive operation. One possible solution for this case is to store already-authorized channels clientside, as in the previous option, but signed with a secret kept serverside, to avoid having to trust the client. For example, an auth server could set a cookie with both stringified capabilities and a SHA256 hmac digest of "<clientId>:<stringified-capabilities>". Then when the client requests a new token with some new extra capability, the auth server would verify the hmac digest and know that it had previously authorized that set of capabilities. Then parse it and add the new capability, create both a new token request and a new hmac digest with the result, and update the cookie in the token request response.

Handling authentication failures


In most cases, the client knows exactly what capabilities it has, and so you can reauthorize if you decides to do something you don't currently have permission to do. In some cases it does not, and has no way of knowing it doesn't have permission to do something until it tries and fails. Ably will respond to a request which fails for lack of capabilities with an ErrorInfo with code 40160. It's possible to catch that and reauth at that stage. An example of this in node.js:
const Ably = require('ably');
var rest = new Ably.Rest({key: ABLY_KEY});

const getSignedTokenRequest = (channels, callback) => {
    var tokenParams = {};

    // In real life, we would first check that this client should have access to

    // these channels, rather than taking it at its word

   tokenParams.capability = channels.reduce((acc, channel) => {

   acc[channel] = ['*'];

   return acc;

 }, {});

  rest.auth.createTokenRequest(tokenParams, (err, tokenRequest) => {

      callback(err, tokenRequest);

  });

};

var channelsINeedAccessTo = ['initial'];

var authCallback = (_tokenParams, callback) => {

  // This would really be making an http request to your server with the

  // list of channels and anything you need for your server to be sure of the

  // identity of the client

  getSignedTokenRequest(channelsINeedAccessTo, callback);

};

var realtime = newAbly.Realtime({authCallback:authCallback});
var channel = realtime.channels.get('channelName');

channel.attach((err) => {
  if(err && err.code === 40160) {
    console.log("Saw that channel was denied access due to capabilities, requesting a token with access to a new channel and reauthing");

    channelsINeedAccessTo.push(channel.name);

  realtime.auth.authorize((err) => {
    console.log('Reauth', err);
    if(!err) {
        channel.attach((err) => {
            console.log('Second attempt channel attach', err);
        })

    }  

 })
}

})