Skip to content

Latest commit

 

History

History
158 lines (132 loc) · 4.53 KB

README.md

File metadata and controls

158 lines (132 loc) · 4.53 KB

FireFlare

Firebase authentication for Cloudflare workers with no dependencies. Google public keys are cached with Workers KV to speed up authentication.

Installation

npm i fireflare

https://www.npmjs.com/package/fireflare

Usage

import {auth} from "fireflare"

// Taken from https://github.com/cloudflare/worker-template-router
async function handleRequest(request: Request) {
  const r = new Router()

  r.get('/', async (request) => {
    if(!(await auth('projectId', KV_NAMESPACE, request.headers.get('Authorization')?.replace("Bearer ", "") ?? null))) {
      // Not authenticated
      return new Response("Unauthorized");
    }

    return new Response("Authorized");
  }) // return a default message for the root route

  const resp = await r.route(request)
  return resp
}

Additional/custom claims validation

if (!(await auth('projectId', env.KV_NAMESPACE, request.headers.get('Authorization')?.replace("Bearer ", "") ?? null, [
  // Use a helper claims check function
  Equals('custom-claim', 'expected-value'),
  InPast('issued'),
  InFuture('exp'),
  NotEmpty('sub'),
  // Validate claims with custom implementation
  (claims) => {
    const value = claims['custom-claim'];
    // Return false to reject
    return (typeof value === 'string' || value instanceof String) && value === "custom-value";
  }
]))) {
  // Not authenticated
  return new Response("Unauthorized");
}

Example authentication using itty-router

// Auth middleware
const requireUser = async (request: Request, env: Env) => {
  const token = request.headers.get('Authorization')?.replace('Bearer ', '');

  return auth(env.FIREBASE_PROJECT_ID, env.KV_NAMESPACE, token ?? null).then((success) => {
    if (!success) {
      return new Response("Not authenticated", { status: 401 });
    }
  }).catch((err) => {
    return new Response("Not authenticated", { status: 401 });
  });
};

router.get('/hello-auth', requireUser, async (request, env) => {
  return new Response("Authenticated", { status: 200 });
});

Bonus: Full example (including CORS)

export interface Env {
  FIREBASE_PROJECT_ID: string,

  KV_NAMESPACE: KVNamespace;
}

const CorsHeaders = {
  "Access-Control-Allow-Origin": "https://www.example.com",
  "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS,DELETE",
  "Access-Control-Max-Age": "86400",
}

export default {
  async fetch (request: Request, env: Env, context: ExecutionContext) {
    if (request.method === "OPTIONS") {
      return handleOptions(request)
    } else {
      const response = await router.handle(request, env);

      if (response instanceof Response) {
        for (const [key, value] of Object.entries(CorsHeaders)) {
          response.headers.set(key, value);
        }
      }

      return response;
    }
  }
};

const router = Router();

const requireUser = async (request: Request, env: Env) => {
  const token = request.headers.get('Authorization')?.replace('Bearer ', '');

  return auth(env.FIREBASE_PROJECT_ID, env.KV_NAMESPACE, token ?? null).then((success) => {
    if (!success) {
      return new Response("Not authenticated", { status: 401 });
    }
  }).catch((err) => {
    return new Response("Not authenticated", { status: 401 });
  });
};

router.get('/hello-auth', requireUser, async (request: IttyRequest, env: Env) => {
  return new Response("Authenticated", { status: 200 });
});


router.all('*', () => new Response('Not Found.', { status: 404 }));

// Cors header stuff
function handleOptions (request: Request) {
  // Make sure the necessary headers are present
  // for this to be a valid pre-flight request
  let headers = request.headers
  if (
    headers.get("Origin") !== null &&
    headers.get("Access-Control-Request-Method") !== null &&
    headers.get("Access-Control-Request-Headers") !== null
  ) {
    // Handle CORS pre-flight request.
    // If you want to check or reject the requested method + headers
    // you can do that here.
    let respHeaders = {
      ...CorsHeaders,
      // Allow all future content Request headers to go back to browser
      // such as Authorization (Bearer) or X-Client-Name-Version
      "Access-Control-Allow-Headers": request.headers.get("Access-Control-Request-Headers") as string,
    }
    return new Response(null, {
      headers: respHeaders,
    })
  } else {
    // Handle standard OPTIONS request.
    // If you want to allow other HTTP Methods, you can do that here.
    return new Response(null, {
      headers: {
        Allow: "GET, HEAD, POST, OPTIONS, DELETE",
      },
    })
  }
}