Tuesday, 19 June, 2018 UTC


Summary


Photo by NESA by Makers / Unsplash
At Cloudflare, we believe that getting new products and features into the hands of customers as soon as possible is the best way to get great feedback. The thing about releasing products early and often is that sometimes they might not be initially ready for your entire user base. You might want to provide access to only particular sets of customers that may be: power users, those who have expressed interest participating in a beta, or customers in need of a new feature the most.
As I have been meeting with many of the users who were in our own Workers beta program, I’ve seen (somewhat unsurprisingly) that many of our users share the same belief that they should be getting feedback from their own users early and often.
However, I was surprised to learn about the difficulty that many beta program members had in creating the necessary controls to quickly and securely gate new or deprecated features when testing and releasing updates.
Below are some ideas and recipes I’ve seen implemented inside of Cloudflare Workers to ensure the appropriate customers have access to the correct features.

How Workers Work

First, a brief primer on how Workers work.
As soon as a Worker is deployed, it is available and ready to run at every one of Cloudflare’s 155+ data centers in response to a request made to your website, application or API. Workers are able to modify anything about both the request to and response from your origin server. They also have the ability to make subrequests to other endpoints in response to the initial request.
Workers are able to make their own subrequests using the available fetch method. We’ll be relying on this as well as the fact that requests made via fetch are also cacheable by Cloudflare to make sure that gating of features is not just secure but also quick.

How to Securely Cache User Permissions

Let’s say you have an endpoint on your origin that allows us to securely pull the permissions for a particular user.
https://api.yoursite.com/user/{uid}
From a Cloudflare Worker we can securely fetch this permission information using a token and have it returned either as JSON or as part of the headers.
// Create Request
 var permissionRequest = new Request(permissionsURL, {
      method: 'GET', 
      headers: new Headers({
        'X-Auth-Token': 'super-secret-token'
      })
    });
// Make the request and wait for the response
var permissionResponse = await fetch(permissionRequest, { cf: { cacheTtl: 14400 } });

// Getting Permissions returned in the Headers
var newFeatureAvailable = permissionResponse.headers.get('X-YourSite-NewFeature');

// Getting Permissions returned as JSON
var jsonPermissions = await permissionResponse.json();

As I wrote earlier, the fetch request actually caches the responses generated when using it. So, subsequent Workers calls can grab user permissions without having to go back to the origin’s endpoint.
While the default cache TTL of 4 hours might work for many applications, fetch will also allow you to set an arbitrary TTL to ensure that your users are not granted permissions any longer than necessary. To set a TTL of 300 seconds (note: the free plan has a lower TTL limit of 2 hours or 7200 seconds) you would change the fetch above to be:

var permissionResponse = await fetch(permissionRequest, { cf: { cacheTtl: 300 } });

A Note about Caching Sensitive Objects

If you are storing sensitive information (like user permissions) in Cloudflare’s cache, it is always important to keep in mind that the url should never be publicly accessible, but rather only from within a Worker.
The Worker set to run in front of api.yoursite.com/user/{uid} should either block all requests to the path from outside of a Cloudflare Worker or check to ensure the request has a valid secret key.

A Note about Using “Super-Secret-Tokens”

Tokens should be provided in your Worker when uploaded to Cloudflare and verified by your origin on each request. Extremely security conscious readers might be nervous about storing credentials in code, but note that Cloudflare strongly encourages 2FA as well as restricts Worker access to specific accounts. We are also exploring better ways of passing secrets to Workers.

Common Ways of Gating New Features

Now that you have quickly fetched the user permissions from cache, it’s time to do something with them! There are endless things you could do, but for this post I will cover some of the more common ones including: restricting paths, A/B Testing, and custom routing between origins.

Restricting Paths

Let’s say you’re releasing v2 of your current API. You want all users to still be able to send GET and POST requests to v1, but since you’re still performance tuning some new v2 features, only authorized users should be able to POST while everyone can GET. Continuing from the example before, this can be done with Cloudflare using the following code:

const apiV2 = jsonPermissions['apiV2'];

// Check to see if user in allowed to test the v2 API
if (apiV2) {
    // They're allowed to test v2 so pass everything through. 
    return fetch(request);
} else {
    // If they aren't specifically allowed to test v2 then we
    // only allow GETs everything else returns a 403 from the edge.
    if (request.method !== 'GET') {
        return new Response('Sorry, this page is not available.',
            { status: 403, statusText: 'Forbidden' });
    }
    return fetch(request);
}

A/B Testing



When releasing a new API version you might also want to update your documentation with a new design, but before rolling out anything it’s important to run a test to make sure it improves (or doesn’t harm) your relevant metrics. A/B testing different versions of the documentation amongst users who have access to V2 of the API can be easily done with Cloudflare Workers:

const apiV2 = jsonPermissions['apiV2'];
const group = jsonPermissions['testingGroup'];

// Here we'll use a variable set in the JSON returned from
// the  user API to determine the users test group, but you 
// could also do this randomly by assigning a cookie to send back.
// Example: https://developers.cloudflare.com/workers/recipes/a-b-testing/

// Make sure the user is allowed to see API V2
if (apiV2) {
    let url = new URL(request.url);
    
    // Append the user's test group to the forwared request
    // Hidden from user: /docs/v2/ -> /group-1/docs/v2/
    url.pathname = `/${group}${url.pathname}`;
    
    const modifiedRequest = new Request(url, {
        method: request.method,
        headers: request.headers
    });
    const response = await fetch(modifiedRequest);

    return response;
} else {
    // User shouldn't be allowed to see V2 docs
    return new Response('Sorry, this page is not yet available.',
        { status: 403, statusText: 'Forbidden' });
}

Custom Routing Between Origins

Spinning up a new version of an API or Application sometimes requires spinning up an entirely new origin server. Cloudflare Workers can easily route API calls to separate origins based on paths, headers, or anything else in the request. Here we’ll make sure the user has permission to access v2 of the API and then route the request to the dedicated origin:

const apiV2Allowed = jsonPermissions['apiV2Allowed'];

const v1origin = 'https://prod-v1-api.yoursite.com';
const v2origin = 'https://beta-v2-api.yoursite.com';

// Original URL: https://api.yoursite.com/v2/endpoint
const originalURL = new URL(request.url);
const originalPath = originalURL.pathname;
const apiVersion = originalPath.split('/')[1];
const endpoint = originalPath.split('/').splice(2).join('/');


if (apiVersion === 'v2') {
    if (apiV2Allowed) {
        let newUrl = new URL(v2origin);
        newUrl.pathname = endpoint;
        const modifiedRequest = new Request(newUrl, {
            method: request.method,
            headers: request.headers
        });
        return fetch(modifiedRequest);
    } else {
        return new Response('Sorry, this API version is not available.',
            { status: 403, statusText: 'Forbidden' });
    }
} else {
    let newUrl = new URL(v1origin);
    newUrl.pathname = endpoint;
    const modifiedRequest = new Request(newUrl, {
        method: request.method,
        headers: request.headers
    });
    return fetch(modifiedRequest);
}

Think I should have included another way of gating features? Make sure to share it on our Cloudflare Community recipe exchange.