Netlify Identity protects Ably apps from hacks
Bruce Thomas
Posted on May 25, 2022
A few weeks ago I saw this message in our internal support channel on Slack, and it made my gears grind. So I stopped to take a look, since the last thing we want is a customer taken advantage of.
The customer has responded to 703762824
Ticket Name: xxxxxxxxxxxxx - API Key Security breach
Priority: Ticket
Description: Ticket raised as a result of a security report as indicated by ... to contact client to let them know and take action as their API Key was exposed in the Wayback Machine, and is still live.
There are people out there on the internet cheeky enough to freeload off your account, and use up your monthly quotas. What's more: you may not know that it is happening.
How to ensure your Ably app gets hacked
Log in to Ably, copy your app's API key and paste it into your code, like this:
const ably = new
Ably.Realtime('aBCdeFg.ABcDEfG:abc123def456....789xyz');
Then commit this line of code to a git repository, deploy it to production and the wayback machine will freeze it in amber. It is that simple. There it will wait until an eager little Hobbit finds it in the dark. Your precioussssss API key will be in the wrong hands.
Luckily, at Ably, we monitor authentication key leaks and contact the client.
Basic authentication is like a cat flap -- any old cat can come into your house. Token authentication is like one of those with a chip reader: only the cool cats that match the chip's programmed VIP guest list can pass the flap test and get in.
Using Netlify functions to protect your app
This article shows you how to set up token authentication with very little effort. We will make an endpoint using a Netlify function that lets you do this.
const authUrl = ".netlify/functions/ably-jwt?id={user-id}";
const ably = new Ably.Realtime({ authUrl });
That looks great, no API anywhere, but what\'s stopping someone on the internet from taking the authentication URL and using it elsewhere? Well nothing is stopping them. Token authentication will hide the API key, but that alone is not enough.
We also need a way to issue the token to valid users only. This is where Netfliy comes into the picture, we can use their Identity services in conjunction with a serverless function, and that is included in the free tier plan (when this article was written).
TL;DR
If you just want to get into the guts of setting up your own Netlify app without all the explanations, then head over to the ably-labs Github repo, where the README is a condensed version of this article.
Reminder: you will need accounts on these platforms:
Ably, Github and Netlify.
If you are only interested in going through the implementation steps, then jump down to Okay, okay, enough ... let's ship it!
What is an "Ably" JWT token?
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.
(Taken from https://jwt.io/introduction/ )
Typically an encoded JWT token looks like this:
Try it here https://jwt.io/ - crafted by the folks at auth0
Token authentication is secure for two reasons: Ably JWT tokens are digitally signed and they expire regularly. Additionally you can monitor who is using your app whenever the token refreshes. When the JWT expires, the user is routed via your validation server and you choose whether or not to reissue their token.
An Ably JWT is not strictly an Ably construct, rather it is a JWT which has been constructed to be compatible with Ably. If you want to know in detail how it works, our best practice guide will cover everything.
Netlify serverless functions & identity workflow
In our example app, a new user needs to register and confirm their email address to activate themselves before they can log in. At login, we validate them with Netlify Identity and check that they have not been flagged as Banned. Bad actors are not issued with a JWT token. Valid users are issued a token, to authenticate with Ably, and the auth URL carries their unique ID within it.
(1) User logs-in (2) Check user identity - valid users get JWT token and continue to the app, and invalid users are rejected (3) use the token, wait for it to expire then repeat.
The Netlify Identity allows us to administer users by editing metadata associated with their account. Flagging a bad actor is a matter of assigning them a role via the Netlify dashboard.
The JWT token as a serverless function
The key part of our JWT endpoint is this code snippet. Independent of what platform you are using, this part would be the same. This is the anatomy of the Ably JWT token, which differs from a standard JWT because we require additional key/values in the data payload.
function generateAblyJWT(props) {
const { apiKey, clientId, capability, ttlSeconds } = props;
const [appId, keyId, keySecret] = apiKey.split(/[\.\:]/g);
const keyName = `${appId}.${keyId}`;
const typ = "JWT"; // type
const alg = "HS256"; // algorithm sha256
const kid = keyName; // appId + keyId
const currentTime = Math.floor(Date.now() / 1000);
const iat = currentTime; // initiated at (seconds)
const exp = currentTime + ttlSeconds; // expire after
const header = { typ, alg, kid };
const claims = {
iat,
exp,
"x-ably-capability": capability,
"x-ably-clientId": clientId,
};
const base64Header = encryptObject(header);
const base64Claims = encryptObject(claims);
const token = `${base64Header}.${base64Claims}`;
const signature = b64(SHA256(token, keySecret));
const jwt = `${token}.${signature}`;
console.log({ header, claims, signature, jwt });
return jwt;
}
The generateAblyJWT
function is almost identical to the one you will find when you read through the Ably documentation on authentication methods. We chose Netlify as the platform for this article but the same idea can be applied to any platform that exposes a serverless endpoint. For example you could do the same thing with a Cloudflare function, a Runkit endpoint, or a Heroku dyno ...etc. Essentially, you can use any service that "wakes-up", runs code and goes dormant after the response payload has been delivered.
Using Netlify Identity to validate our users
This next function is responsible for two things. First it is the endpoint itself, so our authentication URL will execute this function. Secondly, it connects with Netlify Identity to validate that the user, calling the endpoint, is trustworthy.
I'd like to draw your attention to the line of code highlighted below. That is the Netlify endpoint which we will use to get the user with that id. The value of id is coming from the querystring, which we use to "bake" into our Ably JWT, as the clientId
property. This means that both Ably and Netlify will have the same id in their respective audit logs.
The great thing about Netlify Identity is that once you activate it, the second argument in the endpoint handler has a populated user context Object, and that is how we connect the frontend client with the underlying backend.
const axios = require("axios");
const generateAblyJWT = require("./generate-ably-jwt.js");
exports.handler = async function (event, context) {
const { queryStringParameters } = event;
const { id } = queryStringParameters || {};
const { clientContext } = context || {};
console.log({ queryStringParameters });
console.log({ clientContext });
const { identity } = context.clientContext || {};
const { token, url } = identity || {};
const userUrl = `${url}/admin/users/${id}`; <<<<<
const Authorization = `Bearer ${token}`;
console.log({ identity, id, token, url, userUrl, Authorization });
let response;
/*
We Use client context and querystring ID value
to check the user exists by retrieving their
User Identity object.
Then we inspect that accounts metadata for role flags,
which are added via the Identity dashboard,
if it contains "Banned" we do not reissue the token
*/
await axios
.get(userUrl, { headers: { Authorization } })
.then(({ data }) => {
console.log("Success! User identity", data);
const banned = /^banned/i;
const { roles = [] } = data.app_metadata;
const reject = roles.some((item) => banned.test(item));
// delegate the error message to the catch clause.
if (reject) throw new Error(`User with id [${id}] has been banned`);
const settings = {
clientId: id,
apiKey: process.env.ABLY_APIKEY,
capability: process.env.ABLY_CAPABILITY,
ttlSeconds: Number(process.env.ABLY_TTLSECONDS),
};
response = {
statusCode: 200,
body: generateAblyJWT(settings),
headers: { "Content-Type": "application/jwt" },
};
})
.catch((error) => {
console.log("Failed to get user!");
response = {
statusCode: 500,
body: `Internal Error: ${error}`,
headers: { "Content-Type": "text/plain" },
};
});
console.log("response payload", response);
return response;
};
The exported handler function is a modified version of Netlify's "hello world" tutorial. The only major changes are: it's integrated with Identity and the return (aka response) has the header Content-Type: application/jwt
, the example body was originally a (JSON object), now it is a String.
Apart from these two main functions there is additional boilerplate code that loads an encryption library, CryptoJS. Oh, and be sure that expiry time is cast as Number(). If anything is incorrect the Ably realtime network will provide a diagnostic error message.
A summary of the front-end setup
The modal pop-up, that does registration, log-in and password reminder actions, is governed by a single div and a JavaScript from Netlify: Identity widget.
<main>
<div>
<a href="https://ably.com/" target="_blank">
<img src="images/motif-red.svg?netlify-identity-jwt"
alt="Ably logo" /></a>
<h1>
Ably JWT Authentication <br />
using Netlify Identity and <br />
serverless functions
</h1>
<div data-netlify-identity-menu></div> <<<<<<<<<<<
<span class="container">
<button onclick="go(this)">authenticate</button>
<strong class="message"></strong>
</span>
</div>
</main>
And the AUTHENTICATE button executes this function ...
function go(el) {
/*
get the user id from localstorage.
This is only created once the user logs in
*/
const user = localStorage.getItem("gotrue.user") || null;
if (!user) {
showMessage("Can't access user ID, please log in first.");
return null;
}
/*
Add the User's identity ID to the authURL
to ensure the Ably token refresh has that credential
we are binding the authURL and the user
*/
const { id } = JSON.parse(user);
const { origin } = window.location;
const authUrl = `${origin}/.netlify/functions/ably-jwt?id=${id}`;
console.log({ authUrl });
window.ably = new Ably.Realtime({ authUrl });
window.ably.connection.on(handleConnection(el));
}
Once again the URL is highlighted. This value plus the user's ID is used to connect the client to Ably realtime. The moment that happens a countdown timer begins, according to the TTL value set in our environment variables. When it expires, the URL (now stored in Ably realtime) will be used to request an updated token.
Okay, okay, enough ... let's ship it!
Preparation
The first thing we need to do is prepare the respective platforms. Here we login to our Ably account and create a new app, for which we will need the API key later. Then we fork the repo from Ably Labs to your own repository. We need to do this so that Netlify can import our project in the next step.
Deploy and setup the Netlify services
Now we can import the source code from Github to Netlify, and deploy the app. When the app is up and running we will encounter some errors which we need to correct. As we fix these you will learn a little more about how Netlify works.
To moderate who is allowed to use our app we need a user registration process. Netlify provides a very good widget to create users, log them on and even send password reminders. In addition, you can use SSO log-in services like Google, Github etc. For this project I went with basic email registration. Before you can start registration you need to activate the Netlify Identity services in your dashboard.
A user is only considered valid once they have confirmed their email address. This integrity check can be bypassed if you want. Once a user has completed the confirmation you will see that identity appear in the dashboard, and you will be able to edit their metadata.
So we registered a user and logged in with their credentials, and still we are seeing an error when we try to connect to Ably realtime. This is because we have not added the API key.
Add the environment variables
We finish up the process by adding our environment variables, re-deploying the app and test the validity of the user account. At this stage you should finally see the authenticate button turn green, which indicates that this user was issued a JWT token, and was successfully connected to the Ably realtime network.
Key | Type | Description |
---|---|---|
ABLY_APIKEY | String | The API key of your Ably App |
ABLY_CAPABILITY | String or Null | JSON string of permissions e.g. {"channel-name":["subscribe"]}
|
ABLY_TTLSECONDS | Number | Refresh rate in seconds e.g. 3600 (60sec) |
Capability is related to the Ably APP permissions.
You might want to limit operations to subscribe only. To refine permissions please refer to our documentation: capabilities explained. In this example {"channel-name": ["subscribe", "publish"]}
means the status channel has publish and subscribe permission.
This demo application doesn't use a channel, it's purpose is to test authentication, however the Ably JWT needs to have that value assigned when the JWT is generated.
User moderation
In this last section we will pretend that our main user is no longer welcome, edit their metadata in the Identity section and add "Banned" to the role property. From that point, our app will refuse to re-issue the JWT to that particular user.
Conclusion
Now you know a little more about how Ably works internally and that our team actively checks for API key leaks. External flags do pop up and we will reach out if something looks odd, or potentially dangerous. We take security seriously and can't recommend token authentication enough.
In this article, we covered how to set up an authentication server using Netlify's free tier offering, how to integrate with Identity services, and we illustrated how straightforward an authentication endpoint is. The security and moderation control really is worth the effort.
With Netlify, the entire developer experience was smooth, from onboarding, to linking Github, importing a project and deploying to production. My only slow down was moving the dashboard settings (the UI values) into a netlify.toml to ensure anyone who uses this repo gets an equally easy experience.
The TOML bootloads my defaults, which identifies where the functions are and where the website files serve from. There was one other hiccup; figuring how to do application/jwt content type for the response header. Originally I used plain text (that is what a JWT string is), but in this case the header must identify the type as a "JWT".
And in closing, a word about the development of this demo. The majority of the work was done using the Netlify CLI, which is a superb piece of dev tooling. It simulates the serverless environment perfectly, but using Identify requires that you complete your development online. I got struck with a bug regarding the context object supplied to the endpoint handler, which was always missing user context. This is highlighted on the Netlify documentation for Identity and they clearly state Identity requires HTTPS, but I missed that and had egg on my face for the greater part of a day. (blush) Read the documentation!
Cleanup and teardown
When you are done with the demo please be sure to remove it securely, either by key revocation or by deleting the app.?
In this last GIF we go over three things: revoking an API key (in the event that you have been compromised) this preserves the app but destroys the key. Delete the app entirely which permanently destroys everything associated with that app, and finally how to delete the Netlify instance.
Ably in a nutshell
Ably provides APIs to implement pub/sub messaging for realtime features in your apps. You also get a globally-distributed scalable infrastructure out-of-the-box, along with a suite of services.
These include features like presence 窶? which shows the online/offline status of various participants, automatic re-connection and resumption of messages in case of intermittent network issues, message ordering and guaranteed delivery, as well as easy ways to integrate with third-party APIs.Ably enables pub/sub messaging, primarily over WebSockets.
The concept of channels allows you to categorize the data and decide which components have access to which channels. You can also specify capabilities for various participants on these channels like publish-only, subscribe-only, message history, etc.
Learn more about the Ably platform
Tools to limit your exposure
gitguardian keeps secrets out of your source code. It scans your source code to detect API keys, passwords, certificates, encryption keys and other sensitive data in realtime
GitHub Actions - is great for environment protection rules and environment secrets (beta)
GitHub Secret Scanning Partners - lists supported secrets and the partners that GitHub works with to prevent fraudulent use of secrets that were committed accidentally.
Global .gitignore - project .gitignore files are well known, the lesser known more important .gitignore
file can be set up on your machine and applied to every git repository you track. git config --global core.excludesfile ~/.gitignore_global
The JSON web token (JWT) and the testing utility.
Posted on May 25, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.