Static sites
A site with no server can sign people in from the browser. @masks/client runs the sign-in with
PKCE and keeps the tokens in the browser tab. The site names itself to masks with a small JSON file
it serves, so there is no client to register and no secret to keep.
Setting up
Section titled “Setting up”-
Add the plugin to
vite.config.js:import { defineConfig } from "vite";import { svelte } from "@sveltejs/vite-plugin-svelte";import { masks } from "@masks/client/vite";export default defineConfig({plugins: [svelte(),masks({name: "Catalog",scope: ["openid", "profile", "offline_access"],}),],}); -
Sign people in from the page:
import { createBrowserClient } from "@masks/client";export const auth = createBrowserClient({issuer: "https://acme.auth.example.com",scope: ["openid", "profile", "offline_access"],});const actor = await auth.ready();if (!actor) await auth.authorize(); -
Build with the site’s public address:
Terminal window MASKS_ORIGIN=https://catalog.example.com npm run build -
Upload the build to any host that serves files, such as Cloudflare Pages.
The tenant must allow dynamic registration. A manager turns it on in /manage, under Settings,
then General, then Dynamic registration.
How it works
Section titled “How it works”- The plugin writes
oauth/client.jsoninto the build. The file’s address is the site’s client ID, such ashttps://catalog.example.com/oauth/client.json. masks reads the file the first time somebody signs in. See a client named by its metadata document. - masks sends people back to the site’s home page,
/.ready()finishes the sign-in there, puts the browser back on the page it started from, and returns the signed-in person’s claims. It returnsnullwhen nobody is signed in. - The tokens are kept in
sessionStorage, so a reload keeps the person signed in. A new tab signs in again, which is quick while the person is still signed in to masks. auth.fetch(url)calls an API with the access token, and refreshes it first when it is about to expire.auth.logout()signs the person out of the site.auth.logout({ everywhere: true })also signs them out of masks.- With
dpop: true, tokens are bound to a key the page cannot export. The key is kept in IndexedDB, so a reload can still use the tokens.
MASKS_ORIGIN must be the address people visit. A build for https://catalog.example.com does not
sign anyone in on a preview address such as https://abc123.catalog.pages.dev, since the file names
only the first.
Requirements
Section titled “Requirements”-
The page must be served over
https, or fromlocalhost. On any otherhttppage the browser hides the cryptography a sign-in needs, andcreateBrowserClientthrowsinsecure_context. -
masks must be able to fetch the file. A masks on the internet cannot reach
localhost, so for development, register a public client forhttp://localhost:5173/in/manageand pass its ID while the site runs on the dev server:createBrowserClient({issuer: "https://acme.auth.example.com",clientId: import.meta.env.DEV ? "the client ID from /manage" : undefined,});
Reference
Section titled “Reference”masks(options) from @masks/client/vite takes:
| Option | Default | |
|---|---|---|
name |
The name masks shows when it asks the person to approve the site. | |
origin |
MASKS_ORIGIN |
The site’s public address. The build stops when neither is set. |
scope |
The most a sign-in may ask for. | |
redirectUris |
the home page | Where masks may send people back to. |
postLogoutRedirectUris |
the home page | Where masks may send people after signing out everywhere. |
logoUri |
A logo masks shows beside the name. | |
dpop |
false |
Whether masks binds every token to a DPoP key. |
clientMetadata(options) from @masks/client returns the same file as an object, for a build that
does not use Vite. createBrowserClient takes clientId and redirectUri for a site that does not
use the file. See @masks/client for every method.