Skip to content

Commit

Permalink
More updates!
Browse files Browse the repository at this point in the history
  • Loading branch information
johnpangalos committed Aug 3, 2023
1 parent 2a13bec commit 1f170c8
Show file tree
Hide file tree
Showing 14 changed files with 1,534 additions and 2,144 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pnpm-lock.yaml binary
4 changes: 2 additions & 2 deletions .github/actions/install-deps/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ runs:
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20

- uses: pnpm/action-setup@v2.2.2
with:
version: 7
version: 8
run_install: false

- name: Get pnpm store directory
Expand Down
Binary file not shown.
12 changes: 6 additions & 6 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@
"description": "",
"main": "index.js",
"scripts": {
"dev": "wrangler dev --config wrangler-dev.toml",
"dev": "wrangler dev --config wrangler-dev.toml --remote",
"type-check": "tsc"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@cloudflare/workers-types": "^3.19.0",
"@types/uuid": "^8.3.4",
"wrangler": "^2.19.0"
"@types/uuid": "^9.0.2",
"wrangler": "^3.4.0"
},
"dependencies": {
"@ssttevee/multipart-parser": "^0.1.9",
"hono": "^2.7.8",
"toucan-js": "^2.7.0",
"uuid": "^8.3.2"
"hono": "^3.3.4",
"toucan-js": "^3.2.1",
"uuid": "^9.0.0"
}
}
11 changes: 6 additions & 5 deletions api/src/db/posts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Identity, PluginData } from "@/middleware/auth";
import { Identity } from "@/middleware/auth";
import { Account, Post, WigglesContext } from "@/types";
import { DAY, generateSignedUrl, ImageSize, unixTime } from "@/utils";

Expand Down Expand Up @@ -92,8 +92,9 @@ export async function createPosts(c: WigglesContext, postList: Post[]) {
metadata: post,
},
{
key: `post-account-${post.accountId}-${MAX -
Number.parseInt(post.timestamp)}`,
key: `post-account-${post.accountId}-${
MAX - Number.parseInt(post.timestamp)
}`,
value: "",
metadata: post,
},
Expand All @@ -120,8 +121,8 @@ export async function deletePosts(c: WigglesContext, orderKeys: string[]) {
for (const key of orderKeys) {
const feedKey = `post-feed-${key}`;

const access = c.get<PluginData["cloudflareAccess"]>("cloudflareAccess");
const identity: Identity | undefined = await access.JWT.getIdentity();
const access = c.get("JWT");
const identity: Identity | undefined = await access.getIdentity();
if (identity === undefined) throw new Error("Identity not found");
const accountKey = `post-account-${identity.email}-${key}`;

Expand Down
6 changes: 3 additions & 3 deletions api/src/handlers/account.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Identity, PluginData } from "@/middleware/auth";
import { Identity } from "@/middleware/auth";
import { Account, WigglesContext } from "@/types";

export async function GetMe(c: WigglesContext) {
const access = c.get<PluginData["cloudflareAccess"]>("cloudflareAccess");
const identity: Identity | undefined = await access.JWT.getIdentity();
const access = c.get("JWT");
const identity: Identity | undefined = await access.getIdentity();
if (identity === undefined) throw new Error("Identity not found");

const meStr = await c.env.WIGGLES.get(`account-${identity.email}`);
Expand Down
16 changes: 8 additions & 8 deletions api/src/handlers/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ import { v4 as uuidv4 } from "uuid";
import { createPosts, deletePosts, readPosts } from "@/db";
import { Post, WigglesContext } from "@/types";
import { parseFormDataRequest } from "@/utils";
import { Identity, PluginData } from "@/middleware/auth";
import { Identity } from "@/middleware/auth";

export async function GetPosts(c: WigglesContext) {
let size: string | null = c.req.query("size");
let size: string | undefined = c.req.query("size");
if (size === null) size = "WRPost";
if ("WRPost" !== size && "WRThumbnail" !== size)
throw new Error("Invalid query param.");

const cursor: string | null = c.req.query("cursor");
const cursor: string | undefined = c.req.query("cursor");

let limitStr: string | null = c.req.query("limit");
if (limitStr === null) limitStr = "10";
let limitStr: string | undefined = c.req.query("limit");
if (limitStr === undefined) limitStr = "10";
const limit = Number.parseInt(limitStr);

const email: string = c.req.query("email");
const email: string | undefined = c.req.query("email");

return c.json(await readPosts(c, { size, cursor, limit, email }));
}
Expand Down Expand Up @@ -62,8 +62,8 @@ export async function PostUpload(c: WigglesContext) {
const idList = await Promise.all(promises);
const timestamp = +new Date();

const access = c.get<PluginData["cloudflareAccess"]>("cloudflareAccess");
const identity: Identity | undefined = await access.JWT.getIdentity();
const access = c.get("JWT");
const identity: Identity | undefined = await access.getIdentity();
if (identity === undefined) throw new Error("Identity not found");

const postList: Post[] = idList.map((id, idx) => ({
Expand Down
191 changes: 90 additions & 101 deletions api/src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MiddlewareHandler, WigglesContext } from "@/types";
import { Next } from "hono";
import { HonoRequest, Next } from "hono";
import { getCookie } from "hono/cookie";

export const getIdentity = async ({
jwt,
Expand Down Expand Up @@ -82,24 +83,12 @@ export type PluginArgs = {
domain: string;
};

export type PluginData = {
cloudflareAccess: {
JWT: {
payload: JWTPayload;
getIdentity: () => Promise<undefined | Identity>;
};
};
};

const base64URLDecode = (s: string) => {
s = s
.replace(/-/g, "+")
.replace(/_/g, "/")
.replace(/\s/g, "");
s = s.replace(/-/g, "+").replace(/_/g, "/").replace(/\s/g, "");
return new Uint8Array(
(Array.prototype.map.call(atob(s), (c: string) =>
Array.prototype.map.call(atob(s), (c: string) =>
c.charCodeAt(0)
) as unknown) as ArrayBufferLike
) as unknown as ArrayBufferLike
);
};

Expand All @@ -121,105 +110,105 @@ type CertsResponse = {
public_certs: { kid: string; cert: string }[];
};

const generateValidator = (c: WigglesContext) => async (
request: Request
): Promise<{
jwt: string;
payload: object;
}> => {
const jwt = request.cookie("CF_Authorization");

if (jwt === undefined) throw new Error("JWT not on request");
const parts = jwt.split(".");
if (parts.length !== 3) {
throw new Error("JWT does not have three parts.");
}
const [header, payload, signature] = parts;

const textDecoder = new TextDecoder("utf-8");
const { kid, alg } = JSON.parse(textDecoder.decode(base64URLDecode(header)));
if (alg !== "RS256") {
throw new Error("Unknown JWT type or algorithm.");
}
const generateValidator =
(c: WigglesContext) =>
async (): Promise<{
jwt: string;
payload: object;
}> => {
const jwt = getCookie(c, "CF_Authorization");

if (jwt === undefined) throw new Error("JWT not on request");
const parts = jwt.split(".");
if (parts.length !== 3) {
throw new Error("JWT does not have three parts.");
}
const [header, payload, signature] = parts;

const textDecoder = new TextDecoder("utf-8");
const { kid, alg } = JSON.parse(
textDecoder.decode(base64URLDecode(header))
);
if (alg !== "RS256") {
throw new Error("Unknown JWT type or algorithm.");
}

const certsURL = new URL("/cdn-cgi/access/certs", c.env.DOMAIN);
const certsURL = new URL("/cdn-cgi/access/certs", c.env.DOMAIN);

const certsResponse = await fetch(certsURL.toString(), {
cf: {
cacheTtl: 5 * 60,
cacheEverything: true,
},
});
const { keys } = (await certsResponse.json()) as CertsResponse;
if (!keys) {
throw new Error("Could not fetch signing keys.");
}
const jwk = keys.find((key) => key.kid === kid);
if (!jwk) {
throw new Error("Could not find matching signing key.");
}
if (jwk.kty !== "RSA" || jwk.alg !== "RS256") {
throw new Error("Unknown key type of algorithm.");
}
// c.env.WIGGLES.put(jwkKey, JSON.stringify(jwk));
// }

const certsResponse = await fetch(certsURL.toString(), {
cf: {
cacheTtl: 5 * 60,
cacheEverything: true,
},
});
const { keys } = (await certsResponse.json()) as CertsResponse;
if (!keys) {
throw new Error("Could not fetch signing keys.");
}
const jwk = keys.find((key) => key.kid === kid);
if (!jwk) {
throw new Error("Could not find matching signing key.");
}
if (jwk.kty !== "RSA" || jwk.alg !== "RS256") {
throw new Error("Unknown key type of algorithm.");
}
// c.env.WIGGLES.put(jwkKey, JSON.stringify(jwk));
// }

const key = await crypto.subtle.importKey(
"jwk",
jwk,
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
false,
["verify"]
);
const key = await crypto.subtle.importKey(
"jwk",
jwk,
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
false,
["verify"]
);

const unroundedSecondsSinceEpoch = Date.now() / 1000;
const unroundedSecondsSinceEpoch = Date.now() / 1000;

const payloadObj = JSON.parse(textDecoder.decode(base64URLDecode(payload)));
const payloadObj = JSON.parse(textDecoder.decode(base64URLDecode(payload)));

if (payloadObj.iss && payloadObj.iss !== certsURL.origin) {
throw new Error("JWT issuer is incorrect.");
}
if (payloadObj.aud && payloadObj.aud[0] !== c.env.AUDIENCE) {
throw new Error("JWT audience is incorrect.");
}
if (
payloadObj.exp &&
Math.floor(unroundedSecondsSinceEpoch) >= payloadObj.exp
) {
throw new Error("JWT has expired.");
}
if (
payloadObj.nbf &&
Math.ceil(unroundedSecondsSinceEpoch) < payloadObj.nbf
) {
throw new Error("JWT is not yet valid.");
}
if (payloadObj.iss && payloadObj.iss !== certsURL.origin) {
throw new Error("JWT issuer is incorrect.");
}
if (payloadObj.aud && payloadObj.aud[0] !== c.env.AUDIENCE) {
throw new Error("JWT audience is incorrect.");
}
if (
payloadObj.exp &&
Math.floor(unroundedSecondsSinceEpoch) >= payloadObj.exp
) {
throw new Error("JWT has expired.");
}
if (
payloadObj.nbf &&
Math.ceil(unroundedSecondsSinceEpoch) < payloadObj.nbf
) {
throw new Error("JWT is not yet valid.");
}

const verified = await crypto.subtle.verify(
"RSASSA-PKCS1-v1_5",
key,
base64URLDecode(signature),
asciiToUint8Array(`${header}.${payload}`)
);
if (!verified) {
throw new Error("Could not verify JWT.");
}
const verified = await crypto.subtle.verify(
"RSASSA-PKCS1-v1_5",
key,
base64URLDecode(signature),
asciiToUint8Array(`${header}.${payload}`)
);
if (!verified) {
throw new Error("Could not verify JWT.");
}

return { jwt, payload: payloadObj };
};
return { jwt, payload: payloadObj };
};

export function auth(): MiddlewareHandler<Response | undefined> {
return async (c: WigglesContext, next: Next) => {
try {
const validator = generateValidator(c);

const { jwt, payload } = await validator(c.req);
const { jwt, payload } = await validator();

c.set("cloudflareAccess", {
JWT: {
payload,
getIdentity: () => getIdentity({ jwt, domain: c.env.DOMAIN }),
},
c.set("JWT", {
payload,
getIdentity: () => getIdentity({ jwt, domain: c.env.DOMAIN }),
});

await next();
Expand Down
5 changes: 3 additions & 2 deletions api/src/middleware/sentry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MiddlewareHandler } from "@/types";
import Toucan, { Options as ToucanOptions } from "toucan-js";
import { Toucan, Options as ToucanOptions } from "toucan-js";

declare module "hono" {
interface ContextVariableMap {
Expand All @@ -12,8 +12,9 @@ export function sentry(
): MiddlewareHandler<void> {
return async (c, next) => {
if (c.env.ENV !== "production") return await next();

const sentry = new Toucan({
request: c.req,
request: c.req.raw,
context: c.executionCtx,
release: c.env.RELEASE,
environment: c.env.ENV,
Expand Down
2 changes: 1 addition & 1 deletion api/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export type WigglesEnv = {
};
};
};
export type WigglesContext = Context<string, WigglesEnv>;
export type WigglesContext = Context<WigglesEnv>;

export type MiddlewareHandler<T> = (
c: WigglesContext,
Expand Down

0 comments on commit 1f170c8

Please sign in to comment.