Skip to content

Implement Cloudflare Zero Trust Access Control for Notehost with JWT Verification #45

@jonatw

Description

@jonatw

I was looking for a way to add access control using Cloudflare Zero Trust to restrict who can see my note via its Cloudflare Access Gateway.

I found a solution, but unfortunately, I am not very proficient in writing TypeScript or JavaScript. So I asked AI for help and developed my own implementation. The purpose of this issue is to see if someone can make a PR or to leave it here in case anyone else needs this solution.

However, this solution is not perfect for a workspace with many pages. If you don't include all the pages in the slugToPage section, there is a chance that your original Notion domain prefix might be revealed in the URL path. You could potentially use random strings or a UUID-like string to obscure the original Notion site domain, as it is still available to everyone by design.

Step 1

Follow the Readme instructions, using npx to generate your own code from the template.

Step 2

Continue to follow the instructions in the Readme to edit wrangler.toml and site-config.ts.

Step 3

Add the jose package to package.json to decode Cloudflare Access JWT tokens.

  "dependencies": {
    "notehost": "^1.0.33",
    "jose": "^5.8.0"
  },

After adding this package to package.json, run npm install to install it.

Step 4

Modify src/index.ts and adapt the code. Here is an example. Replace YOUR_TEAM_DOMAIN_PREFIX with your own Cloudflare team domain prefix.

import { initializeReverseProxy, reverseProxy } from 'notehost'
import { jwtVerify } from 'jose';
import { SITE_CONFIG } from './site-config'

initializeReverseProxy(SITE_CONFIG)

async function getPublicKey(kid: string): Promise<Uint8Array | null> {
  const response = await fetch('http://YOUR_TEAM_DOMAIN_PREFIX.cloudflareaccess.com/cdn-cgi/access/certs');
  const { keys } = await response.json();

  // Find the key that matches the "kid" (Key ID) in the JWT header
  const key = keys.find((k: any) => k.kid === kid);

  if (!key) return null;

  return new Uint8Array(Buffer.from(key.x5c[0], 'base64'));
}

async function verifyJWT(jwt: string): Promise<boolean> {
  try {
    const { header } = jwtVerify(jwt, async (header) => {
      const publicKey = await getPublicKey(header.kid);
      if (!publicKey) throw new Error('Public key not found');
      return publicKey;
    });

    // If no error is thrown, the JWT is valid
    return true;
  } catch (error) {
    console.error('JWT verification failed:', error);
    return false;
  }
}

export default {
  async fetch(request: Request): Promise<Response> {
    const jwt = request.headers.get('Cf-Access-Jwt-Assertion');

    if (!jwt) {
      return new Response('Unauthorized', { status: 401 });
    }

    const isValid = await verifyJWT(jwt);
    if (!isValid) {
      return new Response('Unauthorized', { status: 401 });
    }

    return await reverseProxy(request)
  },
}
Screenshot 2024-09-01 at 8 26 12 PM

Step 5

Deploy it to Cloudflare Workers, setup an self hosted application in zero trust using on the custom domain you deployed and verify if it works.

Screenshot 2024-09-01 at 8 31 28 PM

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions