Skip to content

Commit

Permalink
feat: basic auth (#84)
Browse files Browse the repository at this point in the history
  • Loading branch information
keroxp authored Mar 5, 2020
1 parent 7df3e4d commit 324f4b4
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 5 deletions.
46 changes: 46 additions & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2019 Yusuke Sakurai. All rights reserved. MIT license.
import { HttpHandler } from "./router.ts";
import { RoutingError } from "./error.ts";
import { Sha1 } from "./vendor/https/deno.land/std/ws/sha1.ts"
import { assert } from "./vendor/https/deno.land/std/testing/asserts.ts";

/** Deny request with 404 if method doesn't match */
export const methodFilter = (...method: string[]): HttpHandler => async req => {
Expand All @@ -18,3 +20,47 @@ export const contentTypeFilter = (
}
throw new RoutingError(400, `Invalid content type`);
};

function timeSafeCompare(secret: string, other: string): boolean {
const a = new Sha1();
const b = new Sha1();
a.update(secret);
b.update(other);
return a.toString() === b.toString();
}

/** Basic Auth middleware */
export function basicAuth({username, password, message}: {
username: string,
password: string,
message?: string
}): HttpHandler {
assert(username, "username must be defined and not be empty");
assert(password, "password must be defined and not be ampty");
// WWW-Authenticate: Basic realm="SECRET AREA"
return function basicAuth(req) {
const authorization = req.headers.get("authorization");
if (!authorization) {
return req.respond({
status: 401,
headers: new Headers({
"www-authenticate": "Basic realm=\"RECRET AREA\""
}),
body: message ?? "Authentication Required"
})
} else {
const unauthorized = () => req.respond({ status: 401, body: "Unauthorized" });
let m = authorization.match(/^Basic (.+?)$/);
if (!m) {
return unauthorized();
}
const [u,p] = atob(m[1]).split(":");
if (u == null || p == null) {
return unauthorized();
}
if (!timeSafeCompare(username, u) || !timeSafeCompare(password, p)) {
return unauthorized();
}
}
}
}
51 changes: 49 additions & 2 deletions middleware_test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { methodFilter } from "./middleware.ts";
import { methodFilter, basicAuth } from "./middleware.ts";
import { it } from "./test_util.ts";
import {
assertEquals,
Expand All @@ -7,7 +7,7 @@ import {
import { createRecorder } from "./testing.ts";
import { RoutingError } from "./error.ts";
it("middleware", t => {
t.run("basic", async () => {
t.run("methodFilter", async () => {
const filter = methodFilter("POST");
const req = createRecorder({
url: "/",
Expand All @@ -17,4 +17,51 @@ it("middleware", t => {
await filter(req);
}, RoutingError);
});
t.run("basicAuth", async () => {
const auth = basicAuth({
username: "deno",
password: "land",
message: "hello"
});
let req = createRecorder({
url: "/",
method: "GET"
});
await auth(req);
let resp = await req.response();
assertEquals(resp.status, 401);
assertEquals(resp.headers.has("www-authenticate"), true);
assertEquals(await resp.body?.text(), "hello");
const up = btoa("deno:land");
req = createRecorder({
url: "/",
method: "GET",
headers: new Headers({
"authorization": `Basic ${up}`
})
});
await auth(req);
assertEquals(req.isResponded(), false);
});
t.run("basicAuth failed", async () => {
const patterns = ["Basic hoge", `Basic ${btoa("deno:js")}`, `Basic ${btoa("deno:")}`, "Basic"];
const auth = basicAuth({
username: "deno",
password: "land",
message: "hello"
});
for (const pat of patterns) {
let req = createRecorder({
url: "/",
method: "GET",
headers: new Headers({
"authorization": pat
})
});
await auth(req);
assertEquals(req.isResponded(), true);
const resp = await req.response();
assertEquals(resp.status, 401);
}
})
});
3 changes: 2 additions & 1 deletion modules-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"/media_types/mod.ts",
"/mime/multipart.ts",
"/util/async.ts",
"/ws/mod.ts"
"/ws/mod.ts",
"/ws/sha1.ts"
]
},
"https://dev.jspm.io/react": {
Expand Down
3 changes: 2 additions & 1 deletion modules.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"/media_types/mod.ts",
"/mime/multipart.ts",
"/util/async.ts",
"/ws/mod.ts"
"/ws/mod.ts",
"/ws/sha1.ts"
]
},
"https://dev.jspm.io/react": {
Expand Down
1 change: 1 addition & 0 deletions site/components/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const Content: FC = ({ children }) => (
<SideBarLink href={"/testing-handler"}>Testing Handler</SideBarLink>
<SideBarLink href={"/manage-cookie"}>Manage Cookie</SideBarLink>
<SideBarLink href={"/handle-ws"}>Handle WebSocket</SideBarLink>
<SideBarLink href={"/basic-auth"}>Basic Auth</SideBarLink>
</SideBarSection>
</SideBar>
{children}
Expand Down
39 changes: 39 additions & 0 deletions site/pages/basic-auth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Article } from "../components/article.tsx";
import { Code } from "../components/code.tsx";
import React from "../../vendor/https/dev.jspm.io/react/index.js";
import { DFC } from "../../jsx.ts";
import { fetchExample } from "../content.ts";
import { Content } from "../components/content.tsx";
import { Q } from "../components/common.tsx";

const BasicAuth: DFC<{ codes: { [key: string]: string } }> = ({ codes }) => (
<Content>
<Article>
<section id={"basic-auth"}>
<h2>Basic Auth</h2>
<p>
Servest provides Basic Aauth (
<a href="https://tools.ietf.org/html/rfc7617" target="_blank">
RFC7617
</a>
) middleware by official. Add <Q>hasicAuth()</Q> middleware into your
router or routes.
</p>
<Code href={"/example/basic_auth.ts"} code={codes["basic_auth.ts"]} />
</section>
</Article>
</Content>
);

BasicAuth.getInitialProps = async () => {
const codes = Object.fromEntries(
await Promise.all(
["basic_auth.ts"].map(async v => {
return [v, await fetchExample(v)];
})
)
);
return { codes };
};

export default BasicAuth;
21 changes: 21 additions & 0 deletions site/public/example/basic_auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2019 Yusuke Sakurai. All rights reserved. MIT license.
import { createRouter } from "../../../router.ts";
import { basicAuth } from "../../../middleware.ts";
const router = createRouter();
// Add global auth middleware
router.use(
basicAuth({
username: "deno",
password: "deno is nice"
})
);
router.get("/", async req => {
await req.respond({
status: 200,
headers: new Headers({
"content-type": "text/plain"
}),
body: "Hello, Servest!"
});
});
router.listen(":8899");
2 changes: 1 addition & 1 deletion site/public/example/handle_ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ function handleHandshake(sock: WebSocket) {
}
const router = createRouter();
router.ws("/ws", handleHandshake);
router.listen(":8899");
router.listen(":8899");
1 change: 1 addition & 0 deletions vendor/https/deno.land/std/ws/sha1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "https://deno.land/[email protected]/ws/sha1.ts";

0 comments on commit 324f4b4

Please sign in to comment.