Protect your Blog Pages By a Temporary Token Using Cloudflare Worker | My Note

Protect your Blog Pages By a Temporary Token Using Cloudflare Worker

Updated: Oct 20th, 2023


When building a blog system, we often want to privately share certain pages with others. This means that only individuals with the link can view those pages.

This feature is commonly found in mature products like Google Docs, Notion, and Yuque. However, it can be challenging to implement when creating our own blog systems.

Today, I would like to share an easy method for protecting your public blog page using a temporary token.

Here is an example :

What I Want to Achieve

  1. Per-page token: Each page should have a unique token.
  2. Token expiration: Once the token expires, users should no longer be able to access the content.
  3. Invalidating tokens: To invalidate a token, simply change the secret in the worker.
  4. Practicality: It would be helpful to have an easy way to generate tokens using local scripts.
    1. Using Obsidian: I have discovered a quick method for adding sharable url with token to front matter in Obsidian.

Prerequisite

  • Your blog system is proxied by Cloudflare.
  • You have a basic understanding of JavaScript programming.
    • For instance, although I haven’t formally learned JavaScript, I do have experience in programming with C++. Additionally, I am familiar with using Google and Stack Overflow to assist me in my coding. This knowledge is sufficient for me to write JavaScript code and implement the desired feature.

Understand the cloudflare worker

If your blog page is proxied by Cloudflare, you have access to various tools that allow you to control incoming requests and add customized logic to implement desired features in your blog system. For example, in Cloudflare, the traffic sequence is shown in the Cloudflare document.

image.png

Cloudflare Worker is a powerful tool provided by Cloudflare, and it is free for up to 100,000 requests per day. To get started, you can test the Hello World worker on Cloudflare.

image.png

We will now proceed step by step to create a stronger worker capable of safeguarding our blog using temporary tokens.

Level 1 : Protect Your Blog with a Static Password

Now we use a simple worker to help you understand how workers works.

Using Cloudflare Worker: A Simple Example

To help you understand how workers work, we will use a simple example.

For each incoming request to your blog page, we can route it to our Cloudflare worker. The worker is a piece of JavaScript code that allows us to implement various features. For instance, if you want to protect your page with a static password, you can write a JavaScript worker. In the code, you can extract the password from the URL parameters and compare it with your private password. If they match, the worker can grant access by turning on a green light for this request. Otherwise, the worker can block the request and return a 404 error to the user.

https://0x10.sh/test-page?password=user_provided_password

export default {
  async fetch(request) {
    const private_password = "testpassword";
    const url_404 = "https://0x10.sh/404"
    const url = new URL(request.url);

    if (!url.searchParams.has("password")) {
      // do not provide password
      return Response.redirect(url_404, 301);
    }

    // extract password from https://0x10.sh/test-page?password=user_provided_password
    let get_password = url.searchParams.get("password")
    if (private_password == get_password){
       // console.log("ok")
       return fetch(request);
    }
    // the password is wrong
    return Response.redirect(url_404, 301);
  },
};

Add a route to make your worker works

To ensure that only certain pages are protected, you can utilize the route feature. By creating a route rule with a specific prefix, you can direct the worker to check the password for URLs that match this prefix. For instance, in this example, we make all pages prefixed with 0x10.sh/p/ protected by this worker.

image.png

Level 2: Use a per-page Token

Using only a static password is a naive method because if the password is leaked or shared with someone, it puts all your private content at risk.

To enhance security, we can utilize a token generated by a hash algorithm.

Here’s what I do: I combine a secret string with the pathname and generate a digest using the sha256 algorithm. Each page has a unique digest, which acts as a per-page token. If this token is leaked, only the content of that specific page will be compromised. The sha256 algorithm cannot retrieve the secret string.

To implement this, we can include a token parameter in the URL, like http://0x10.sh/test-token?t=09d0e4ba80684e670a. The corresponding worker code is provided below. All necessary details are included within the code and no further explanation is required.

export default {
  async fetch(request) {
    // https://developers.cloudflare.com/workers/examples/signing-requests/
    const secret = "YOUR SECRET";
    const url_404 = "https://0x10.sh/404"
    const url = new URL(request.url);
    if (!url.searchParams.has("t")) {
      return Response.redirect(url_404, 301);
    }
    let token = url.searchParams.get("t")
    const all_content = secret + url.pathname;
    // console.log("all_content : ", all_content)

    const my_hash_content = new TextEncoder().encode(all_content)
    const digest = await crypto.subtle.digest({
        name: 'SHA-256',
      },
      my_hash_content
    );
    // Turn it into a hex string
    const hexString = [...new Uint8Array(digest)]
      .map(b => b.toString(16).padStart(2, '0'))
      .join('')
    // console.log("digest (hexString) : ", hexString)

    if (token == hexString){
      // console.log("ok")
      return fetch(request);
    }
    // Incorrect key supplied. Reject the request.
    return Response.redirect(url_404, 301);
  },
};

Level 3: Use a temporary token with a expiry date

If the token is valid permanently, it becomes difficult to control the page and make it private again. To address this issue, we can set an expiry date for the token.

To make that happen we can include an “expiry” parameter in the URL:

http://0x10.sh/test-token?t=09d0e4ba80684e670a&expiry=1697644998

The “expiry” parameter represents a Unix timestamp in seconds.

To implement our system, we use the sha256 algorithm to generate a unique token for each expiry date. This process involves a secret string, pathname, and expiry date. Users must provide both the expiry date and its corresponding token in order to access protected content. It is not possible for users to modify either of these values to bypass security measures. Only individuals with knowledge of the secret can generate valid tokens.

export default {
  async fetch(request) {
    // https://developers.cloudflare.com/workers/examples/signing-requests/
    const secret = "YOUR SECRET";
    const url_404 = "https://0x10.sh/404"
    const url = new URL(request.url);

    // Make sure you have the minimum necessary query parameters.
    if (!url.searchParams.has("t") || !url.searchParams.has("expiry")) {
       return Response.redirect(url_404, 301);
    }

    let token = url.searchParams.get("t")

    // The Date.now() static method returns the number of milliseconds elapsed since the epoch, 
    // which is defined as the midnight at the beginning of January 1, 1970, UTC.
    const expiry = Number(url.searchParams.get("expiry"));
    var now_seconds = Math.round(Date.now() / 1000);
    if (Number.isNaN(expiry)){
      // console.log("expire is invalid")
      const body = `expire is invalid`;
      return Response.redirect(url_404, 301);
    }
    if (now_seconds > expiry) {
      // console.log("is expiried")
      return Response.redirect(url_404, 301);
    }

    const all_content = secret + url.pathname + expiry;

    const my_hash_content = new TextEncoder().encode(all_content)
    const digest = await crypto.subtle.digest(
      {
        name: 'SHA-256',
      },
      my_hash_content
    );
    // Turn it into a hex string
    const hexString = [...new Uint8Array(digest)]
      .map(b => b.toString(16).padStart(2, '0'))
      .join('')

    if (token == hexString){
      // console.log("ok")
      return fetch(request);
    }
    // Incorrect key supplied. Reject the request.
    return Response.redirect(url_404, 301);
  },
};

Level 4: Concealing the Expiry Date Within the Token

It is strange that the URL is excessively long and contains too many parameters.

We can conceal the expiry parameters within the token parameters and only use part of the sha256 digest (which is also safe too), thereby shortening the token.

We no longer need to include the expiry parameter in the URL. Instead, we can simply append the expiry unix timestamp to the token.

Generating Token on the Client Side

To generate a valid token, you can run a script on the client side and share the URL with people you trust. The specific method may vary depending on your preferences. Personally, I use the Obsidian Plugin called Templater and write some JavaScript code to generate the token. This code then automatically appends the valid URL to the front matter of my note. Are you interested in how it is implemented?

no title picture

In the reading view, the URL can be clicked.

image.png

Conclusion

It is done !

By the way, there are other methods to protect your page, such as Cloudflare Zero Trust or leveraging the OAuth API of platforms like GitHub and Google. Tutorials for these methods will be covered in future blog posts.


Instead of authenticating the giscus application, you can also comment directly on GitHub.


Notes mentioning this note