passkeys-smoke.html

5.5 KB · raw

1<!doctype html>2<html lang="en">3  <head>4    <meta charset="utf-8" />5    <meta name="viewport" content="width=device-width, initial-scale=1" />6    <title>Peach Passkey Smoke</title>7    <style>8      :root {9        color-scheme: dark;10        font-family: ui-sans-serif, system-ui, sans-serif;11      }1213      body {14        margin: 0;15        min-height: 100vh;16        display: grid;17        place-items: center;18        background:19          radial-gradient(circle at top, rgba(217, 104, 48, 0.2), transparent 30%),20          #111009;21        color: #f0e8d8;22      }2324      main {25        width: min(560px, calc(100vw - 40px));26        padding: 28px;27        border-radius: 18px;28        background: rgba(255, 255, 255, 0.03);29        border: 1px solid rgba(240, 232, 216, 0.08);30        box-shadow: 0 20px 70px rgba(0, 0, 0, 0.35);31      }3233      h1 {34        margin: 0 0 8px;35        font-size: 28px;36      }3738      p {39        margin: 0 0 16px;40        color: rgba(240, 232, 216, 0.72);41      }4243      .actions {44        display: flex;45        gap: 12px;46        margin-bottom: 18px;47      }4849      button {50        border: 0;51        border-radius: 999px;52        padding: 11px 16px;53        font: inherit;54        cursor: pointer;55        color: #111009;56        background: #f0e8d8;57      }5859      button.primary {60        background: #d96830;61        color: #fff7ed;62      }6364      .status {65        font-family: ui-monospace, SFMono-Regular, monospace;66        font-size: 13px;67        padding: 12px 14px;68        border-radius: 12px;69        background: rgba(255, 255, 255, 0.04);70        white-space: pre-wrap;71      }7273      .status[data-state="ok"] {74        outline: 1px solid rgba(94, 229, 94, 0.35);75      }7677      .status[data-state="error"] {78        outline: 1px solid rgba(229, 85, 85, 0.35);79      }80    </style>81  </head>82  <body>83    <main>84      <h1>Peach Passkey Smoke</h1>85      <p>Exercises WebAuthn registration and authentication against the local Peach extension provider.</p>86      <div class="actions">87        <button id="create" class="primary">Create passkey</button>88        <button id="get">Authenticate</button>89      </div>90      <div id="create-status" class="status" data-state="idle">Create idle</div>91      <div id="get-status" class="status" data-state="idle" style="margin-top: 12px">Get idle</div>92    </main>9394    <script>95      const state = {96        credentialJson: null,97      };9899      function bytes(...values) {100        return Uint8Array.from(values);101      }102103      function base64urlToBytes(input) {104        const padded = input.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(input.length / 4) * 4, "=");105        const binary = atob(padded);106        const out = new Uint8Array(binary.length);107        for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);108        return out;109      }110111      function setStatus(id, stateName, value) {112        const el = document.getElementById(id);113        el.dataset.state = stateName;114        el.textContent = value;115      }116117      async function createPasskey() {118        setStatus("create-status", "pending", "Creating passkey...");119        try {120          const credential = await navigator.credentials.create({121            publicKey: {122              challenge: bytes(1, 2, 3, 4, 5, 6, 7, 8),123              rp: { id: location.hostname, name: "Peach Passkey Smoke" },124              user: {125                id: bytes(10, 20, 30, 40, 50, 60, 70, 80),126                name: "[email protected]",127                displayName: "June Local",128              },129              pubKeyCredParams: [{ type: "public-key", alg: -7 }],130              timeout: 60_000,131              attestation: "none",132              authenticatorSelection: {133                residentKey: "required",134                userVerification: "required",135              },136            },137          });138139          state.credentialJson = credential.toJSON();140          setStatus(141            "create-status",142            "ok",143            `Created ${state.credentialJson.id}\n${JSON.stringify(state.credentialJson, null, 2)}`144          );145        } catch (error) {146          setStatus("create-status", "error", `Create failed: ${error.name}: ${error.message}`);147        }148      }149150      async function authenticate() {151        if (!state.credentialJson) {152          setStatus("get-status", "error", "Authenticate failed: missing created credential");153          return;154        }155156        setStatus("get-status", "pending", "Authenticating...");157        try {158          const assertion = await navigator.credentials.get({159            publicKey: {160              challenge: bytes(8, 7, 6, 5, 4, 3, 2, 1),161              rpId: location.hostname,162              allowCredentials: [163                {164                  type: "public-key",165                  id: base64urlToBytes(state.credentialJson.rawId),166                },167              ],168              userVerification: "required",169              timeout: 60_000,170            },171          });172173          const assertionJson = assertion.toJSON();174          setStatus(175            "get-status",176            "ok",177            `Authenticated ${assertionJson.id}\n${JSON.stringify(assertionJson, null, 2)}`178          );179        } catch (error) {180          setStatus("get-status", "error", `Get failed: ${error.name}: ${error.message}`);181        }182      }183184      document.getElementById("create").addEventListener("click", () => {185        void createPasskey();186      });187      document.getElementById("get").addEventListener("click", () => {188        void authenticate();189      });190    </script>191  </body>192</html>