Skip to content

Commit

Permalink
tests: add tests for basic auth
Browse files Browse the repository at this point in the history
  • Loading branch information
SharzyL committed Apr 12, 2024
1 parent d2609b8 commit f64741b
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 87 deletions.
55 changes: 24 additions & 31 deletions src/auth.js
Original file line number Diff line number Diff line change
@@ -1,58 +1,51 @@
import { WorkerError } from "./common.js";
import { WorkerError } from "./common.js"

function parseBasicAuth(request) {
const Authorization = request.headers.get('Authorization');

const [scheme, encoded] = Authorization.split(' ');

// The Authorization header must start with Basic, followed by a space.
if (!encoded || scheme !== 'Basic') {
throw new WorkerError(400, 'malformed authorization header');
}

const buffer = Uint8Array.from(atob(encoded), character => character.charCodeAt(0))
const decoded = new TextDecoder().decode(buffer).normalize();

const index = decoded.indexOf(':');
// Encoding function
export function encodeBasicAuth(username, password) {
const credentials = `${username}:${password}`
const encodedCredentials = Buffer.from(credentials).toString("base64")
return `Basic ${encodedCredentials}`
}

if (index === -1 || /[\0-\x1F\x7F]/.test(decoded)) {
throw WorkerError(400, 'invalid authorization value');
// Decoding function
export function decodeBasicAuth(encodedString) {
const [scheme, encodedCredentials] = encodedString.split(" ")
if (scheme !== "Basic") {
throw new WorkerError(400, "Invalid authentication scheme")
}

return {
user: decoded.substring(0, index),
pass: decoded.substring(index + 1),
};
const credentials = Buffer.from(encodedCredentials, "base64").toString("utf-8")
const [username, password] = credentials.split(":")
return { username, password }
}

// return true if auth passes or is not required,
// return auth page if auth is required
// throw WorkerError if auth failed
export function verifyAuth(request, env) {
// pass auth if 'BASIC_AUTH' is not present
if (!('BASIC_AUTH' in env)) return null
if (!env.BASIC_AUTH) return null

const passwdMap = new Map(Object.entries(env['BASIC_AUTH']))
const passwdMap = new Map(Object.entries(env.BASIC_AUTH))

// pass auth if 'BASIC_AUTH' is empty
if (passwdMap.size === 0) return null

if (request.headers.has('Authorization')) {
const { user, pass } = parseBasicAuth(request)
if (passwdMap.get(user) === undefined) {
if (request.headers.has("Authorization")) {
const { username, password } = decodeBasicAuth(request.headers.get("Authorization"))
if (passwdMap.get(username) === undefined) {
throw new WorkerError(401, "user not found for basic auth")
} else if (passwdMap.get(user) !== pass) {
} else if (passwdMap.get(username) !== password) {
throw new WorkerError(401, "incorrect passwd for basic auth")
} else {
return null
}
} else {
return new Response('HTTP basic auth is required', {
return new Response("HTTP basic auth is required", {
status: 401,
headers: {
// Prompts the user for credentials.
'WWW-Authenticate': 'Basic realm="my scope", charset="UTF-8"',
"WWW-Authenticate": "Basic charset=\"UTF-8\"",
},
});
})
}
}
82 changes: 82 additions & 0 deletions test/basicAuth.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { expect, test } from "vitest"
import { areBlobsEqual, BASE_URL, createFormData, randomBlob, staticPages, workerFetchWithAuth } from "./testUtils.js"
import { encodeBasicAuth, decodeBasicAuth } from "../src/auth.js"

test("basic auth encode and decode", async () => {
const userPasswdPairs = [
["user1", "passwd1"],
["あおい", "まなか"],
["1234#", "اهلا"],
]
for (const [user, passwd] of userPasswdPairs) {
const encoded = encodeBasicAuth(user, passwd)
const decoded = decodeBasicAuth(encoded)
expect(decoded.username).toStrictEqual(user)
expect(decoded.password).toStrictEqual(passwd)
}
})

test("basic auth", async () => {
const usersKv = {
"user1": "passwd1",
"user2": "passwd2",
}

// access index
for (const page of staticPages) {
expect((await workerFetchWithAuth(usersKv, `${BASE_URL}/${page}`, {})).status).toStrictEqual(401)
}
expect((await workerFetchWithAuth(usersKv, BASE_URL, {
headers: { "Authorization": encodeBasicAuth("user1", usersKv["user1"]) },
})).status).toStrictEqual(200)

// upload with no auth
const blob1 = randomBlob(1024)
const uploadResp = await workerFetchWithAuth(usersKv, BASE_URL, {
method: "POST",
body: createFormData({ c: blob1 }),
})
expect(uploadResp.status).toStrictEqual(401)

// upload with true auth
const uploadResp1 = await workerFetchWithAuth(usersKv, BASE_URL, {
method: "POST",
body: createFormData({ c: blob1 }),
headers: { "Authorization": encodeBasicAuth("user2", usersKv["user2"]) },
})
expect(uploadResp1.status).toStrictEqual(200)

// upload with wrong auth
const uploadResp2 = await workerFetchWithAuth(usersKv, BASE_URL, {
method: "POST",
body: createFormData({ c: blob1 }),
headers: { "Authorization": encodeBasicAuth("user1", "wrong-password") },
})
expect(uploadResp2.status).toStrictEqual(401)

// revisit without auth
const uploadJson = JSON.parse(await uploadResp1.text())
const url = uploadJson["url"]
const revisitResp = await workerFetchWithAuth(usersKv, url)
expect(revisitResp.status).toStrictEqual(200)
expect(areBlobsEqual(await revisitResp.blob(), blob1)).toBeTruthy()

// update with no auth
const blob2 = randomBlob(1024)
const admin = uploadJson["admin"]
const updateResp = await workerFetchWithAuth(usersKv, admin, {
method: "PUT",
body: createFormData({ c: blob2 }),
})
expect(updateResp.status).toStrictEqual(200)
const revisitUpdatedResp = await workerFetchWithAuth(usersKv, url)
expect(revisitUpdatedResp.status).toStrictEqual(200)
expect(areBlobsEqual(await revisitUpdatedResp.blob(), blob2)).toBeTruthy()

// delete with no auth
const deleteResp = await workerFetchWithAuth(usersKv, admin, {
method: "DELETE",
})
expect(deleteResp.status).toStrictEqual(200)
expect((await workerFetchWithAuth(usersKv, url)).status).toStrictEqual(404)
})
2 changes: 1 addition & 1 deletion test/formdata.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ test("basic formdata", async () => {
// compare "s"
const parsedSecret = new TextDecoder().decode(parts.get("s").content)
expect(parsedSecret).toStrictEqual(secret)
})
})
72 changes: 17 additions & 55 deletions test/integration.spec.js
Original file line number Diff line number Diff line change
@@ -1,57 +1,13 @@
import { SELF, env, ctx } from "cloudflare:test"
import { env } from "cloudflare:test"
import { test, expect } from "vitest"

import { params, genRandStr } from "../src/common.js"

import * as crypto from "crypto"

// for auto reload
import worker from "../src/index.js"

const BASE_URL = env["BASE_URL"]
const RAND_NAME_REGEX = /^[ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678]+$/

async function workerFetch(req, options) {
// we are not using SELF.fetch since it sometimes do not print worker log to console
// return await SELF.fetch(req, options)
return await worker.fetch(new Request(req, options), env, ctx)
}

async function upload(kv) {
const uploadResponse = await workerFetch(new Request(BASE_URL, {
method: "POST",
body: createFormData(kv),
}))
expect(uploadResponse.status).toStrictEqual(200)
expect(uploadResponse.headers.get("Content-Type")).toStrictEqual("application/json;charset=UTF-8")
return JSON.parse(await uploadResponse.text())
}

function createFormData(kv) {
const fd = new FormData()
Object.entries(kv).forEach(([k, v]) => {
if ((v === Object(v)) && "filename" in v && "value" in v) {
fd.set(k, new File([v.value], v.filename))
} else {
fd.set(k, v)
}
})
return fd
}

function randomBlob(len) {
const buf = Buffer.alloc(len)
return new Blob([crypto.randomFillSync(buf, 0, len)])
}

async function areBlobsEqual(blob1, blob2) {
return Buffer.from(await blob1.arrayBuffer()).compare(
Buffer.from(await blob2.arrayBuffer()),
) === 0
}
import {
randomBlob, areBlobsEqual, createFormData, workerFetch, upload,
BASE_URL, RAND_NAME_REGEX, staticPages,
} from "./testUtils.js"

test("static page", async () => {
const staticPages = ["", "index.html", "index", "tos", "tos.html", "api", "api.html"]
for (const page of staticPages) {
expect((await workerFetch(`${BASE_URL}/${page}`)).status).toStrictEqual(200)
}
Expand Down Expand Up @@ -133,7 +89,7 @@ test("basic", async () => {

// check delete
const deleteResponse = await workerFetch(new Request(admin, { method: "DELETE" }))
expect(putResponse.status).toStrictEqual(200)
expect(deleteResponse.status).toStrictEqual(200)

// check visit modified
const revisitDeletedResponse = await workerFetch(url)
Expand Down Expand Up @@ -181,7 +137,7 @@ test("expire", async () => {
await testExpireParse("1M", 18144000)
await testExpireParse("100 m", 6000)

const testFailParse = async (expire, expireSecs) => {
const testFailParse = async (expire) => {
const uploadResponse = await workerFetch(new Request(BASE_URL, {
method: "POST",
body: createFormData({ "c": blob1, "e": expire }),
Expand Down Expand Up @@ -375,10 +331,18 @@ test("cache control", async () => {
const uploadResp = await upload(({ "c": randomBlob(1024) }))
const url = uploadResp["url"]
const resp = await workerFetch(url)
expect(resp.headers.get("Cache-Control")).toStrictEqual(`public, max-age=${env.CACHE_PASTE_AGE}`)
if ("CACHE_PASTE_AGE" in env) {
expect(resp.headers.get("Cache-Control")).toStrictEqual(`public, max-age=${env.CACHE_PASTE_AGE}`)
} else {
expect(resp.headers.get("Cache-Control")).toBeUndefined()
}

const indexResp = await workerFetch(BASE_URL)
expect(indexResp.headers.get("Cache-Control")).toStrictEqual(`public, max-age=${env.CACHE_STATIC_PAGE_AGE}`)
if ("CACHE_STATIC_PAGE_AGE" in env) {
expect(indexResp.headers.get("Cache-Control")).toStrictEqual(`public, max-age=${env.CACHE_STATIC_PAGE_AGE}`)
} else {
expect(indexResp.headers.get("Cache-Control")).toBeUndefined()
}

const staleResp = await workerFetch(url, {
headers: {
Expand All @@ -388,6 +352,4 @@ test("cache control", async () => {
expect(staleResp.status).toStrictEqual(304)
})

// TODO: add tests for basic auth

// TODO: add tests for CORS
55 changes: 55 additions & 0 deletions test/testUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { env, ctx } from "cloudflare:test"

import { expect } from "vitest"
import crypto from "crypto"
import worker from "../src/index.js"

export const BASE_URL = env["BASE_URL"]
export const RAND_NAME_REGEX = /^[ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678]+$/

export const staticPages = ["", "index.html", "index", "tos", "tos.html", "api", "api.html"]

export async function workerFetch(req, options) {
// we are not using SELF.fetch since it sometimes do not print worker log to console
// return await SELF.fetch(req, options)
return await worker.fetch(new Request(req, options), env, ctx)
}

export async function workerFetchWithAuth(usersKv, req, options) {
const newEnv = Object.assign({ BASIC_AUTH: usersKv }, env)
return await worker.fetch(new Request(req, options), newEnv, ctx)
}

export async function upload(kv) {
const uploadResponse = await workerFetch(new Request(BASE_URL, {
method: "POST",
body: createFormData(kv),
}))
expect(uploadResponse.status).toStrictEqual(200)
expect(uploadResponse.headers.get("Content-Type")).toStrictEqual("application/json;charset=UTF-8")
return JSON.parse(await uploadResponse.text())
}

export function createFormData(kv) {
const fd = new FormData()
Object.entries(kv).forEach(([k, v]) => {
if ((v === Object(v)) && "filename" in v && "value" in v) {
fd.set(k, new File([v.value], v.filename))
} else {
fd.set(k, v)
}
})
return fd
}

export function randomBlob(len) {
const buf = Buffer.alloc(len)
return new Blob([crypto.randomFillSync(buf, 0, len)])
}

export async function areBlobsEqual(blob1, blob2) {
return Buffer.from(await blob1.arrayBuffer()).compare(
Buffer.from(await blob2.arrayBuffer()),
) === 0
}

0 comments on commit f64741b

Please sign in to comment.