Skip to content

Commit

Permalink
Support usage of library with bundlers (#6)
Browse files Browse the repository at this point in the history
* improving 'scripts/build-all.js' tool to build both 'index.js' (for non-bundler) and 'only.index.js' (for bundler), and to minify src/* scripts

* adding bundler fields to 'package.json', and improve docs

* more tweaks/fixes

* removing outdated sodium.ready stuff

* adding plugins for vite and webpack, lots of rearranging, updating docs

* README: fixing links to bundler/non-bundler guides

* fixing a few typos in BUNDLERS guide

* fixing symlink destination locations in 'postinstall' script

* fixing 'build-gh-pages.js' script to have the right 'dist/' directory

* more fixes

* package.json: removing some likely unnecessary fields

* more fixes, improving build script and externals import

* more fixes to scripts and testbed

* fixing missing 'exports' for bundler plugins in 'package.json', docs tweaks

* changing build tool to output 'bundlers/walc.mjs' instead of '.js' extension, updating docs/etc

* adding more BUNDLERS docs about issues with top-level-await and SSR HTML injection failing, tweaks to code/tools
  • Loading branch information
getify authored Mar 26, 2024
1 parent bf84580 commit eeca156
Show file tree
Hide file tree
Showing 14 changed files with 583 additions and 115 deletions.
123 changes: 123 additions & 0 deletions BUNDLERS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Deploying WebAuthn-Local-Client WITH A Bundler

This project has non-ESM dependencies, which unfortunately cannot be *bundled* in with your other app code. Modern bundlers unfortunately don't out-of-the-box support configurations that can handle such a situation.

As such, this project provides plugins for Vite and Webpack, to take care of the various steps needed to get these non-ESM dependencies into an otherwise bundled web app built by those tools.

## Bundler Plugins

The plugins for Vite and Webpack are included in the `bundler-plugins/` directory. They should handle all necessary steps to load the dependencies.

**Note:** You should not need to manually copy any files out of the `dist/bundlers/` directory, as the plugins access the `webauthn-local-client` dependency (in `node_modules`) directly to pull the files needed. But for reference, the files these plugins access are:

* `dist/bundlers/walc.mjs`

ESM library module that's suitable for bundling and `import`ing into your web app.

**Note:** this is *not* the same as `dist/auto/walc.js`, which is only intended [for web application projects WITHOUT a bundler](NON-BUNDLERS.md)

* `dist/bundlers/walc-external-bundle.js`

Non-ESM (plain global .js) bundle of dependencies that must be loaded separately from (and prior to) your app's bundle. Includes the concatenated contents of these individual dependencies:

- `dist/auto/external/libsodium.js`
- `dist/auto/external/libsodium-wrappers.js`
- `dist/auto/external/cbor.js`
- `dist/auto/external/asn1.all.min.js`

**Note:** The [`ASN1` dependency](https://github.com/yoursunny/asn1.js) is [licensed under MPL 2.0](https://www.mozilla.org/en-US/MPL/2.0/), which is generally compatible with this library's [MIT license](LICENSE.txt). However, MPL 2.0 specifically requires preservation of the copyright/license header (block comment at top of `asn1.all.min.js`). To comply with this licensing requirement, ensure your tooling does not remove this comment from the bundle file.

### Vite Plugin

If using Vite 5+, it's strongly suggested to import this library's Vite-plugin to manage the loading of its non-ESM dependencies. Add something like the following to your `vite.config.js` file:

```js
import { defineConfig } from "vite";
import WALC from "webauthn-local-client/bundlers/vite";

export default defineConfig({
// ..

plugins: [ WALC() ],

build: {
// WALC uses "top-level await", which is ES2022+
target: "es2022"
},

// ..
});
```

This plugin works for the `vite dev` (dev-server), `vite preview` (also dev-server), and `vite build` modes. In all cases, it copies the `dist/bundlers/walc-external-bundle.js` file into the `public/` directory of your project root. It also injects a `<script src="/walc-external-bundle.js"></script>` tag into the markup of the `index.html` file that Vite produces for your app.

**Note:** At present, this plugin is not configurable in any way (i.e., calling `WALC()` above with no arguments). If something about its behavior is not compatible with your Vite project setup -- which can vary widely and be quite complex to predict or support by a basic plugin -- it's recommended you simply copy over the `webauthn-local-client/bundler-plugins/vite.mjs` plugin and make necessary changes.

#### Top-level `await`

This library uses ["top-level `await`"](https://github.com/tc39/proposal-top-level-await), a feature added to JS in ES2022. The current default target for Vite seems to be browsers older than this, so the above config explicitly sets the `build.target` to `"es2022"`.

You may experience issues where your tooling/configuration either ignores this setting, or otherwise breaks with it set. This may variously result in seeing an error about the top-level `await`s in this library being incompatible with the built-target, or an error about `await` needing to only be in `async function`s or the top-level of a module (which it is!).

Those types of errors generally indicate that you may need to configure Vite to skip trying to optimize the `walc.mjs` file during bundling, something like:

```js
export default defineConfig({

// ..

optimizeDeps: {
exclude: [ "webauthn-local-client" ]
}

// ..
});
```

#### SSR Breakage

An unfortunate gotcha of tools that wrap Vite (e.g., Astro, Nuxt, etc) and do SSR (server-side rendering) is that they *break* a key assumption/behavior of this module's Vite plugin: the HTML injection of `<script src="/walc-external-bundle.js"></script>`.

As such, you'll likely need to manually add that `<script>` tag to your HTML pages/templates. The Vite plugin still copies that file into the `public/` folder for you, so it should load once the tag is added to your HTML.

### Webpack Plugin

If using Webpack 5+, make sure you're already using the [HTML Webpack Plugin](https://github.com/jantimon/html-webpack-plugin/) to manage building your `index.html` (and/or other HTML pages).

Then import this library's Webpack-plugin to manage the loading of its non-ESM dependencies. Add something like the following to your `webpack.config.js`:

```js
// 'HtmlWebpackPlugin' is a required dependency of the
// webauthn-local-client Webpack plugin
import HtmlWebpackPlugin from "html-webpack-plugin";
import WALC from "webauthn-local-client/bundlers/webpack";

export default {
// ..

plugins: [
// required WALC dependency
new HtmlWebpackPlugin({
// ..
}),

WALC()
],

// ..
};
```

This plugin copies the `dist/bundlers/walc-external-bundle.js` file into the build root (default `dist/`), along with the other bundled files. It also injects a `<script src="walc-external-bundle.js"></script>` tag into the markup of the `index.html` file (and any other HTML files) that Webpack produces for your app.

**Note:** At present, this plugin is not configurable in any way (i.e., calling `WALC()` above with no arguments). If something about its behavior is not compatible with your Webpack project setup -- which can vary widely and be quite complex to predict or support by a basic plugin -- it's recommended you simply copy over the `webauthn-local-client/bundler-plugins/webpack.mjs` plugin and make necessary changes.

## Import/Usage

To import and use **webauthn-local-client** in a *bundled* browser app:

```js
import { register, auth } from "webauthn-local-client";
```

When `import`ed like this, both Vite and Webpack should (via these plugins) properly find and bundle the `dist/bundlers/walc.mjs` ESM library module with the rest of your app code, hopefully without any further steps necessary.
49 changes: 49 additions & 0 deletions NON-BUNDLERS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Deploying WebAuthn-Local-Client WITHOUT A Bundler

To use this library directly -- i.e., in a classic/vanilla web project without a modern bundler tool -- make a directory for it (e.g., `webauthn-local-client/`) in your browser app's JS assets directory.

Then copy over all `dist/auto/*` contents, as-is:

* `dist/auto/walc.js`

**Note:** this is *not* the same as `dist/bundlers/walc.mjs`, which is only intended [for web application projects WITH a bundler](BUNDLERS.md)

* `dist/auto/external.js`

This is an *auto-loader* that dynamically loads the rest of the `external/*` dependencies via `<script>`-element injection into the DOM. `dist/auto/walc.js` imports and activates this loader automatically.

* `dist/auto/external/*` (preserve the whole `external/` sub-directory):
- `libsodium.js`
- `libsodium-wrappers.js`
- `cbor.js`
- `asn1.all.min.js`

## Import/Usage

To import and use **webauthn-local-client** in a *non-bundled* browser app:

```js
import { register, auth } from "/path/to/js-assets/webauthn-local-client/walc.js";
```

The library's dependencies will be auto-loaded (via `external.js`).

## Using Import Map

If your **non-bundled** browser app has an [Import Map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap), you can improve the `import` by adding an entry for this library:

```html
<script type="importmap">
{
"imports": {
"webauthn-local-client": "/path/to/js-assets/webauthn-local-client/walc.js"
}
}
</script>
```

Then you'll be able to `import` the library in a more friendly/readable way:

```js
import { register, auth } from "webauthn-local-client";
```
52 changes: 14 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,46 +17,22 @@ However, the intended use-case for **WebAuthn-Local-Client** is to allow [Local-

**Note:** This package *may* be used in combination with a traditional FIDO2 server application architecture, but does not include any specific functionality for that purpose. For server integration with `WebAuthn`, you may instead consider alternative libraries, like [this one](https://github.com/passwordless-id/webauthn) or [this one](https://github.com/raohwork/webauthn-client).

## Usage
## Deployment / Import

The [**webauthn-local-client** npm package](https://npmjs.com/package/webauthn-local-client) ships with a `dist/` directory with all files you need to deploy.
The [**webauthn-local-client** npm package](https://npmjs.com/package/webauthn-local-client) includes a `dist/` directory with all files you need to deploy **webauthn-local-client** (and its dependencies) into your application/project.

Make a directory (e.g. `webauthn-local-client/`) in your browser app's JS assets, and copy all files from `dist/` (including `dist/external/*` files) as-is into it.
If you obtain this library via git instead of npm, you'll need to [build `dist/` manually](#re-building-dist) before deployment.

Then import the library in an ESM module in your browser app:
* **USING A WEB BUNDLER?** (vite, webpack, etc) Use the `dist/bundlers/*` files and see [Bundler Deployment](BUNDLERS.md) for instructions.

```js
import { register, auth } from "/path/to/webauthn-local-client/index.js";
```

**Note:** This library exposes its API in modern ESM format, but it relies on dependencies that are non-ESM (UMD), which automatically add themselves to the global namespace; it cannot use `import` to load its own dependencies. Instead, the included `external.js` module manages loading the dependencies via `<script>` element injection into the page. If your development/deployment processes include bundling (webpack, rollup, etc), please configure your tool(s) to skip bundling this library and its dependencies, and just copy them over as indicated above. Alternately, before/during build, you'll need to make sure the `import "./external.js"` line at the top of `index.js` is removed/commented out, to ensure that module is skipped in the bundle.

### Loading via Import Map

If your app uses an [Import Map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap), you can and an entry for this library:

```html
<script type="importmap">
{
"imports": {
"webauthn-local-client": "/path/to/webauthn-local-client/index.js"
}
}
</script>
```

Then you can `import` it in a more friendly/readable way:

```js
import { register, auth } from "webauthn-local-client";
```
* Otherwise, use the `dist/auto/*` files and see [Non-Bundler Deployment](NON-BUNDLERS.md) for instructions.

### Supported?
## `WebAuthn` Supported?

To check if `WebAuthn` is supported on the device:
To check if `WebAuthn` API and functionality is supported on the device:

```js
import { supportsWebAuthn } from "webauthn-local-client";
import { supportsWebAuthn } from "...";

if (supportsWebAuthn) {
// welcome to the future, without passwords!
Expand All @@ -70,7 +46,7 @@ else {
To check if [passkey autofill (aka "Conditional Mediation")](https://web.dev/articles/passkey-form-autofill) is supported on the device:

```js
import { supportsConditionalMediation } from "webauthn-local-client";
import { supportsConditionalMediation } from "...";

if (supportsConditionalMediation) {
// provide an <input> and UX for user to
Expand All @@ -90,7 +66,7 @@ else {
To register a new credential in a `WebAuthn`-exposed authenticator, use `register()`:

```js
import { register, regDefaults } from "..";
import { register, regDefaults } from "...";

// optional:
var regOptions = regDefaults({
Expand Down Expand Up @@ -157,7 +133,7 @@ Typically, though, [web applications *assume*](https://medium.com/webauthnworks/
To authenticate (i.e., [perform an assertion](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API/Attestation_and_Assertion#assertion)) with an existing credential via a `WebAuthn`-exposed authenticator, use `auth()`:

```js
import { auth, authDefaults } from "..";
import { auth, authDefaults } from "...";

// optional:
var authOptions = authDefaults({
Expand Down Expand Up @@ -190,7 +166,7 @@ Typical `auth()` configuration options:
For certain UX flows, such as switching from the conditional-mediation to another authentication approach, you will need to cancel (via `signal`) a previous call to `auth()` before invoking an `auth()` call with different options. But calling `abort()` causes that pending `auth()` to throw an exception. To suppress this exception when resetting, pass the `resetAbortReason` value:

```js
import { resetAbortReason, authDefaults, auth } from "..";
import { resetAbortReason, authDefaults, auth } from "...";

var cancelToken = new AbortController();
var authResult = await auth({ /* .. */ , signal: cancelToken.signal });
Expand Down Expand Up @@ -231,7 +207,7 @@ If `auth()` completes completes successfully, the return value (`authResult` abo
To verify an authentication response (from `auth()`), use `verifyAuthResponse()`:

```js
import { verifyAuthResponse, } from "..";
import { verifyAuthResponse, } from "...";

var publicKey = ... // aka, regResult.response.publicKey

Expand All @@ -246,7 +222,7 @@ var verified = await verifyAuthResponse(
If you used `packPublicKeyJSON()` on the original `publicKey` value to store/transmit it, you'll need to use `unpackPublicKeyJSON()` before passing it to `verifyAuthResponse()`:

```js
import { verifyAuthResponse, unpackPublicKeyJSON } from "..";
import { verifyAuthResponse, unpackPublicKeyJSON } from "...";

var packedPublicKey = ... // result from previous packPublicKeyJSON()

Expand Down
99 changes: 99 additions & 0 deletions bundler-plugins/vite.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import path from "node:path";
import fs from "node:fs";
import fsp from "node:fs/promises";


export default WALC;


// ********************************

function WALC() {
var config;
var walcSrcPath;
var externalBundleSrcPath;
var externalBundleDestPath;
var externalBundleCopied = false;

return {
name: "vite-plugin-walc",
enforce: "pre",

async configResolved(cfg) {
config = cfg;
var bundlersDir = path.join(config.root,"node_modules","webauthn-local-client","dist","bundlers");
walcSrcPath = path.join(bundlersDir,"walc.mjs");
externalBundleSrcPath = path.join(bundlersDir,"walc-external-bundle.js");
externalBundleDestPath = (
config.command == "build" ?
path.join(config.root,config.build.outDir,path.basename(externalBundleSrcPath)) :

config.command == "serve" ?
path.join(config.publicDir,path.basename(externalBundleSrcPath)) :

null
);
return copyExternalBundle();
},
resolveId(source) {
// NOTE: this should never be `import`ed
if (source == "webauthn-local-client/bundlers/walc-external-bundle.js") {
// ...but if found, mark it as "external" because
// the contents are non-ESM compatible
return { id: source, external: true, };
}
},
load(id,opts) {
if (id == "webauthn-local-client") {
return fs.readFileSync(walcSrcPath,{ encoding: "utf8", });
}
},

buildEnd() {
externalBundleCopied = false;
},
transformIndexHtml(html) {
return [
{
tag: "script",
injectTo: "head-prepend",
attrs: {
src: `/${path.basename(externalBundleDestPath)}`,
},
},
];
},

// NOTE: ensuring the external-bundle is copied (in case the
// dest directory hasn't been created earlier in the lifecyle)
writeBundle: copyExternalBundle,
buildStart: copyExternalBundle,
};


// ****************************

async function copyExternalBundle() {
if (
// need to copy the external bundle?
!externalBundleCopied &&

// bundle output path set properly?
externalBundleDestPath &&

// bundle file exists?
fs.existsSync(externalBundleSrcPath) &&

// destination directory exists?
fs.existsSync(path.dirname(externalBundleDestPath))
) {
try {
await fsp.copyFile(externalBundleSrcPath,externalBundleDestPath);
externalBundleCopied = true;
}
catch (err) {
console.log(err);
}
}
}
}
Loading

0 comments on commit eeca156

Please sign in to comment.