This example depends on compiled JS SDK found in the auth-policy-refactor branch.
Make sure to compile this version of ChatEngine in the sibling directory:
/chat-engine (auth-policy-refactor)
/chat-engine-scratch
These are updated PubNub functions that implement swap-able auth policies that work with 3rd party services.
Set them up like so:
rest-gateway = on request - chat-engine-gateway
rest-server = on request - chat-engine-server
before-publish = before publish - chat-engine-validateNote that the original 'on-presence' function that appears in chat-engine-setup is missing here. It is still required. However, most PubNub accounts only allow 3 functions at the moment.
The Facebook JS SDK provides a method to login (and a GUI element too!). When Facebook tells us we're logged in, we boot ChatEngine with the authentication information that get from Facebook. This includes a user object, an id, and the Facebook Access Token.
Check out the docs for Facebook Access Tokens and the properties for ChatEngine.connect().
function buildChatEngine(fbMe, fbAccessToken) {
ChatEngine.connect(fbMe.id, fbMe, fbAccessToken);
};Authentication information is stored in memory on the client and sent with every single ChatEngine request.
ChatEngine Server Function receives the request and immediately proxies it to the Gateway Function. It will not continue unless it receives { allow: true } from the Gateway.
The token and user information is forwarded to the Gateway function.
return authPolicy().then((res) => {
console.log(res)
let b = {allow: false};
try {
b = JSON.parse(res.body);
} catch(err) {
response.status = 500;
return response.send('could not parse auth json');
}
if(b.allow) {
return controllers[route][method]();
} else {
response.status = 401;
return response.send(res.body);
}
}).catch((res) => {
response.status = 200;
return response.send();
});The Gateway Function returns "allow" true or false depending on the policy. Authentication policies are defined on a per route basis. In general, a policy is in charge of "who" can do "what."
As soon as ChatEngine connect is called, it makes a request to /login. This is the only open route in our authorization policy.
The login route should take the user's information and return a successful response if it is valid. The Gateway function will then store this information in it's own cache, so that 3rd party servers do not have to be contacted in the future.
In this example, notice how login needs to validateFBToken. See below for what that does.
Notice how the 'grant' policy calls a different function called isOverAge. This asks Facebook to validate that the user is over 13 before giving them access to any new chat (the /grant endpoint).
export default (request, response) => {
if (route == 'login') {
return validateFBToken(request.body.uuid, request.body.authKey, request.body.authData)
.then(done).catch(die);
} else if (route == 'grant') {
return isOverAge(request.body.uuid, request.body.authKey, 13)
.then(done).catch(die);
} else {
return done({
allow: true
});
}
};All the information from the original request is available in the policy gateway.
request = request.body && JSON.parse(request.body);This function validates the supplied uuid and authToken to grant the user access to ChatEngine. It uses the xhr module to call Facebook's debug endpoint and validate that the user token is legitimate and that it matches who the user claims to be.
let validateFBToken = (uuid, authKey, authData) => {
return new Promise((resolve, reject) => {
return xhr.fetch(`https://graph.facebook.com/debug_token?access_token=${myFBToken}|${myFBSecret}&input_token=${authKey}`)
.then((x) => x.json()).then((x) => {
if(x.data.is_valid && x.data.user_id == uuid) {
resolve({
allow: true
});
} else {
resolve({
allow: false,
text: 'Could not validate auth token.'
});
}
});
});
};If the token is valid on /login, the Server Function stores the authKey in the db keyed by uuid. This acts as a session cache, allowing us to validate the authToken ourselves in future requests; preventing us from hitting Facebook's servers every time we need to check if the user is valid (which is every time).
controllers.login.post = () => {
return db.set(['valid', body.uuid].join(':'), body.authKey).then((worked) => {
response.status = 200;
return response.send('it worked');
}).catch((err) => {
response.status = 401;
return response.send('it did not work');
});
}If this were the /grant endpoint, the server would grant the user access to some chat. And if it were a invite request, it might send someone else a chat invite.
By comparing the uuid and the cached authToken, we can validate that the user is who they say they are. This is because Facebook told us so, and we cached that in the db.
All other ChatEngine requests will use this method rather than asking Facebook every time.
export default (request, response) => {
const db = require('kvstore');
console.log('request made');
if(request.channels[0].indexOf('ce#') === 0) {
const url = `https://pubsub.pubnub.com/v1/blocks/sub-key/${request.subkey}/chat-engine-server?route=validate&uuid=${request.message.params.uuid}&authKey=${request.message.params.auth}`;
return xhr.fetch(url, httpOptions).then((res) => {
if (res.status === 200) {
console.log('validate route says were ok');
return resolve(res);
} else {
return reject(res);
}
}).catch((err) => {
return reject(err);
});
}
return request.ok();
};With cached session validation and protected routes, we have a secure way allowing access to chat rooms and other functions.
Here is a policy that restricts people from getting grants unless Facebook tells us they're over the age of 13.
let isOverAge = (uuid, authKey, minAge = 13) => {
return new Promise((resolve, reject) => {
let url = `https://graph.facebook.com/${uuid}?&access_token=${authKey}&fields=age_range`;
return xhr.fetch(url)
.then((x) => x.json()).then((x) => {
if(x.age_range.min > minAge) {
return resolve({
allow: true,
});
} else {
return resolve({
allow: false,
text: 'Not old enough'
});
}
});
});
};