Skip to content

Commit

Permalink
Drop js dependencies and build tools
Browse files Browse the repository at this point in the history
This adds a few lines of JavaScript to handle base64url encoding and
decoding of the array buffers that the browser returns/expects, so we
can convert to the json format that the server expects.

Previously this used a third-party library, but that requires bringing in
Yarn and Packer to manage this, which results in a transitive dependency
on almost 600k lines of JavaScript (excluding blank lines and comments)
and another 9k lines of TypeScript.

By implementing the conversion by hand, we reduce the footprint of the
example by:

 * Dropping the 64 MB node_modules directory.
 * Reducing the size of the dev dependencies Nix closure by 100 MB.
 * Removing the 6.4 seconds required to run Yarn initially.
 * Removing the 4.3 seconds required to run Parcel initially.
  • Loading branch information
ruuda committed Jun 14, 2020
1 parent 0bfb7a3 commit 551e19a
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 5,279 deletions.
1 change: 0 additions & 1 deletion default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ then haskellPackages.fido2.env.overrideAttrs (
x: {
buildInputs = x.buildInputs ++ [
pkgs.entr
pkgs.yarn
pkgs.cabal-install
pkgs.cabal2nix
pkgs.fd
Expand Down
9 changes: 8 additions & 1 deletion server/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,14 @@ type Users = (Map Fido2.UserId User, Map Fido2.CredentialId Fido2.UserId)

app :: TVar Sessions -> TVar Users -> ScottyM ()
app sessions users = do
Scotty.middleware (staticPolicy (addBase "dist"))
Scotty.get "/index.html" $ do
Scotty.setHeader "content-type" "text/html; charset=utf-8"
Scotty.file "index.html"

Scotty.get "/index.js" $ do
Scotty.setHeader "content-type" "application/javascript"
Scotty.file "index.js"

Scotty.get "/register/begin" $ do
(sessionId, session) <- getSessionScotty sessions
-- NOTE: We currently do not support multiple credentials per user.
Expand Down
64 changes: 54 additions & 10 deletions server/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,47 @@
import {supported, get, create} from "@github/webauthn-json";
function base64Decode(str) {
// Convert base64url to the base64 dialect understood by atob,
// then convert the resulting string to an ArrayBuffer.
let padded;
if (str.length % 4 === 0) padded = str;
if (str.length % 4 === 2) padded = str + "==";
if (str.length % 4 === 3) padded = str + "=";
let base64 = padded.replace(/-/g, "+").replace(/_/g, "/");
return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
}

function base64Encode(buffer) {
// We get an ArrayBuffer, but btoa expects a string (with only code points in
// the range 0-255). I expected that we could do
//
// btoa(new TextDecoder("latin1").decode(buffer))
//
// but then btoa complains that the input contains characters outside of the
// Latin-1 range! So instead we manually map each byte with fromCharCode.
const binaryString = Array.from(new Uint8Array(buffer), b => String.fromCharCode(b)).join("");

// Encode the buffer in the base64 dialect returned by btoa,
// then convert that to base64url that is required by the spec.
return btoa(binaryString).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}

function deepBase64Encode(obj) {
if (obj instanceof ArrayBuffer) {
return base64Encode(obj);
}
if (obj instanceof Array) {
return obj.map(deepBase64Encode);
}
if (obj instanceof Object) {
let result = {};
for (key in obj) {
if (!(obj[key] instanceof Function)) {
result[key] = deepBase64Encode(obj[key]);
}
}
return result;
}
return obj;
}

const SERVER = "http://localhost:8080";
window.addEventListener("load", () => {
Expand All @@ -12,25 +55,24 @@ window.addEventListener("load", () => {

const publicKey = {
rp: params.rp,
challenge: params.challenge,
challenge: base64Decode(params.challenge),
pubKeyCredParams: params.pubKeyCredParams,
user: {
name: "john.doe",
displayName: "John Doe",
id: params.user.id,
id: base64Decode(params.user.id),
},
authenticatorSelection: params.authenticatorSelection,
};

const credentialCreationOptions = { publicKey };

const credential = await create(credentialCreationOptions);
const credential = await navigator.credentials.create(credentialCreationOptions);
console.log("credential", credential);

const result = await fetch(`${SERVER}/register/complete`, {
method: "POST",
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credential),
body: JSON.stringify(deepBase64Encode(credential)),
credentials: "include"
});

Expand All @@ -41,16 +83,18 @@ window.addEventListener("load", () => {
loginForm.addEventListener("submit", async (e) => {
e.preventDefault();
const response = await fetch(`${SERVER}/login/begin`, { credentials: "include" });
const params = await response.json();
const publicKey = await response.json();

publicKey.challenge = base64Decode(publicKey.challenge);
publicKey.allowCredentials.forEach(cred => cred.id = base64Decode(cred.id));

const publicKey = params;
const credentialRequestOptions = { publicKey };
const credential = await get(credentialRequestOptions);
const credential = await navigator.credentials.get(credentialRequestOptions);

const result = await fetch(`${SERVER}/login/complete`, {
method: "POST",
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credential),
body: JSON.stringify(deepBase64Encode(credential)),
credentials: "include"
});

Expand Down
15 changes: 0 additions & 15 deletions server/package.json

This file was deleted.

2 changes: 0 additions & 2 deletions server/run.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
#!/usr/bin/env bash
yarn
yarn parcel build index.html
git ls-files .. | entr -r cabal run server
Loading

0 comments on commit 551e19a

Please sign in to comment.