@excom/web-authn 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/chunked-rush-logs/web-authn.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/web-authn.build_docs.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/web-authn.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_docs/all.log +1 -0
- package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_docs/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +4 -0
- package/config/rig.json +5 -0
- package/index.ts +17 -0
- package/package.json +44 -0
- package/rush-logs/web-authn.apply-exports.cache.log +1 -0
- package/rush-logs/web-authn.apply-exports.log +1 -0
- package/rush-logs/web-authn.build_docs.cache.log +1 -0
- package/rush-logs/web-authn.build_docs.log +1 -0
- package/rush-logs/web-authn.build_package-metas.cache.log +1 -0
- package/rush-logs/web-authn.build_package-metas.log +1 -0
- package/support/custom-elements.json +156 -0
- package/support/demos/authenticate.html +24 -0
- package/support/demos/register.html +24 -0
- package/support/dist-docs/web-authn.md +205 -0
- package/support/docs/README.md +81 -0
- package/support/package-meta.json +240 -0
- package/support/tests/authenticate.view.test.ts +28 -0
- package/support/tests/register.view.test.ts +29 -0
- package/support/tests/web-authn.test.ts +566 -0
- package/tsconfig.json +5 -0
- package/web-authn.ts +219 -0
package/web-authn.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { FetchableElement } from "@excom/fetchable-element";
|
|
2
|
+
import { Neutron, TEvent } from "@excom/neutron";
|
|
3
|
+
|
|
4
|
+
export type FetchArgs = [url: string, requestInit: RequestInit];
|
|
5
|
+
|
|
6
|
+
export type WebAuthnSubmitEvent = TEvent & {
|
|
7
|
+
type: "web-authn-submit";
|
|
8
|
+
detail: FetchArgs;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Native form `submit` this element intercepts, not a Neutron emit. */
|
|
12
|
+
export type WebAuthnNativeSubmitEvent = SubmitEvent & {
|
|
13
|
+
type: "submit";
|
|
14
|
+
bubbles: true;
|
|
15
|
+
cancelable: true;
|
|
16
|
+
composed: false;
|
|
17
|
+
};
|
|
18
|
+
import {
|
|
19
|
+
startAuthentication,
|
|
20
|
+
startRegistration,
|
|
21
|
+
} from "@simplewebauthn/browser";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* WebAuthn (passkey) registration or authentication, wired to a plain
|
|
25
|
+
* `<form>`. On submit: fetches ceremony options from `options-url`,
|
|
26
|
+
* passes them to the browser's WebAuthn prompt (via
|
|
27
|
+
* `@simplewebauthn/browser`'s `startRegistration` / `startAuthentication`
|
|
28
|
+
* — installed as a dependency of this package, no extra setup needed),
|
|
29
|
+
* then posts the resulting credential to `verify-url`. Composes
|
|
30
|
+
* `FetchableElement`, so `is-loading` / `is-success` / `is-error` / `provision`
|
|
31
|
+
* and every request-building attribute are inherited — see that
|
|
32
|
+
* package's docs for the full list.
|
|
33
|
+
*
|
|
34
|
+
* Chain a next step off `web-authn-success` — e.g. redirect, or trigger
|
|
35
|
+
* a follow-up `<super-form>` — the same way you'd react to any
|
|
36
|
+
* `{tag}-success` event.
|
|
37
|
+
*
|
|
38
|
+
* @summary Passkey register / authenticate wired to a `<form>`.
|
|
39
|
+
*
|
|
40
|
+
* @fires web-authn-submit - Internal — dispatched once the browser
|
|
41
|
+
* ceremony (register or authenticate) resolves, just before the
|
|
42
|
+
* verify-url fetch runs. The credential is the request body. Default
|
|
43
|
+
* action calls `doFetch()`.
|
|
44
|
+
* @type WebAuthnSubmitEvent
|
|
45
|
+
* @listens submit - The default action of the `<form>` matched by
|
|
46
|
+
* `form-ref`; prevented, then starts the ceremony.
|
|
47
|
+
* @type WebAuthnNativeSubmitEvent
|
|
48
|
+
* @command --submit - Starts the ceremony programmatically (`<button
|
|
49
|
+
* command="--submit" commandfor="…">`) — the only option when `form-ref`
|
|
50
|
+
* points to a form that isn't a descendant.
|
|
51
|
+
* @default-action web-authn-submit - Calls `doFetch(url, requestInit)`
|
|
52
|
+
* with the event's detail (the verify-url request).
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* <web-authn options-url="/api/webauthn/register/options"
|
|
56
|
+
* verify-url="/api/webauthn/register/verify" start-method="register">
|
|
57
|
+
* <form><input name="username"><button type="submit">Register</button></form>
|
|
58
|
+
* </web-authn>
|
|
59
|
+
*/
|
|
60
|
+
export const WebAuthn = Neutron.compose([
|
|
61
|
+
FetchableElement,
|
|
62
|
+
Neutron({
|
|
63
|
+
tag: "web-authn",
|
|
64
|
+
props: {
|
|
65
|
+
// options
|
|
66
|
+
/**
|
|
67
|
+
* @option
|
|
68
|
+
* CSS selector for the `<form>` to intercept. Must be a
|
|
69
|
+
* descendant to be heard directly — point elsewhere and invoke
|
|
70
|
+
* the `--submit` command instead.
|
|
71
|
+
* @default :scope form
|
|
72
|
+
* @values <CSS Selector>
|
|
73
|
+
*/
|
|
74
|
+
formRef: {
|
|
75
|
+
type: String,
|
|
76
|
+
// Non-descendant form: this element won't hear `submit`; invoke `--submit`
|
|
77
|
+
defaultValue: () => ":scope form",
|
|
78
|
+
},
|
|
79
|
+
/**
|
|
80
|
+
* @option
|
|
81
|
+
* HTTP method used for both the `options-url` and `verify-url`
|
|
82
|
+
* requests.
|
|
83
|
+
* @default POST
|
|
84
|
+
*/
|
|
85
|
+
apiMethod: {
|
|
86
|
+
type: String,
|
|
87
|
+
defaultValue: () => "POST",
|
|
88
|
+
},
|
|
89
|
+
/**
|
|
90
|
+
* @option
|
|
91
|
+
* Endpoint that returns the WebAuthn ceremony options JSON (from
|
|
92
|
+
* your server's `generateRegistrationOptions` /
|
|
93
|
+
* `generateAuthenticationOptions`). Fetched first, before the
|
|
94
|
+
* browser prompt appears.
|
|
95
|
+
* @values <URL>
|
|
96
|
+
*/
|
|
97
|
+
optionsUrl: String,
|
|
98
|
+
/**
|
|
99
|
+
* @option
|
|
100
|
+
* Which WebAuthn ceremony to run.
|
|
101
|
+
* @values register | authenticate
|
|
102
|
+
*/
|
|
103
|
+
startMethod: String,
|
|
104
|
+
/**
|
|
105
|
+
* @option
|
|
106
|
+
* Endpoint that verifies the credential produced by the browser
|
|
107
|
+
* prompt (your server's `verifyRegistrationResponse` /
|
|
108
|
+
* `verifyAuthenticationResponse`). Receives the credential as the
|
|
109
|
+
* request body.
|
|
110
|
+
* @values <URL>
|
|
111
|
+
*/
|
|
112
|
+
verifyUrl: String,
|
|
113
|
+
// private
|
|
114
|
+
optionsPromise: Promise,
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
])
|
|
118
|
+
.defineMethods({
|
|
119
|
+
doSubmit: ({
|
|
120
|
+
verifyUrl,
|
|
121
|
+
optionsUrl,
|
|
122
|
+
startMethod,
|
|
123
|
+
apiMethod,
|
|
124
|
+
getFetchArgs,
|
|
125
|
+
getFormElement,
|
|
126
|
+
}) => {
|
|
127
|
+
const formElement = getFormElement();
|
|
128
|
+
if (!verifyUrl || !optionsUrl || !startMethod || !formElement) {
|
|
129
|
+
console.error("Missing required attributes for WebAuthn");
|
|
130
|
+
return {
|
|
131
|
+
emit: [
|
|
132
|
+
"error",
|
|
133
|
+
{ detail: "Missing required attributes for WebAuthn" },
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
isLoading: true,
|
|
140
|
+
optionsPromise: doAuth({
|
|
141
|
+
fetchArgs: getFetchArgs([optionsUrl, { method: apiMethod }]),
|
|
142
|
+
startMethod,
|
|
143
|
+
}),
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
.onEvent("submit", (_, e) => {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
return { doSubmit: [] };
|
|
150
|
+
})
|
|
151
|
+
.onCommand("--submit", () => ({ doSubmit: [] }))
|
|
152
|
+
.onEventDefault("web-authn-submit", (_, { detail }) => ({
|
|
153
|
+
// `detail` is the `[url, requestInit]` tuple (`onPromiseResolved`
|
|
154
|
+
// below). `doFetch` takes that tuple as one arg.
|
|
155
|
+
doFetch: [detail],
|
|
156
|
+
}))
|
|
157
|
+
.onPromiseResolved(
|
|
158
|
+
"optionsPromise",
|
|
159
|
+
({ verifyUrl, apiMethod, getFetchArgs }, result) => ({
|
|
160
|
+
emit: [
|
|
161
|
+
"web-authn-submit",
|
|
162
|
+
{
|
|
163
|
+
detail: getFetchArgs([
|
|
164
|
+
verifyUrl,
|
|
165
|
+
{ method: apiMethod, body: result.optionsPromise },
|
|
166
|
+
]),
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
})
|
|
170
|
+
)
|
|
171
|
+
.onPromiseRejected("optionsPromise", (_, result) => {
|
|
172
|
+
console.error("WebAuthn error:", result.optionsPromise);
|
|
173
|
+
return {
|
|
174
|
+
emit: [
|
|
175
|
+
"error",
|
|
176
|
+
{
|
|
177
|
+
detail: {
|
|
178
|
+
message:
|
|
179
|
+
result.optionsPromise instanceof Error
|
|
180
|
+
? result.optionsPromise.message
|
|
181
|
+
: "WebAuthn operation failed",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
async function doAuth({
|
|
189
|
+
fetchArgs,
|
|
190
|
+
startMethod,
|
|
191
|
+
}: {
|
|
192
|
+
fetchArgs: [string, RequestInit];
|
|
193
|
+
startMethod: string;
|
|
194
|
+
}) {
|
|
195
|
+
// 1. Call the optionsUrl endpoint to get the options
|
|
196
|
+
const response = await fetch(fetchArgs[0], {
|
|
197
|
+
...fetchArgs[1],
|
|
198
|
+
body: JSON.stringify(fetchArgs[1].body),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
throw new Error(`Failed to fetch options: ${response.statusText}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const options = await response.json();
|
|
206
|
+
|
|
207
|
+
let payload;
|
|
208
|
+
if (startMethod === "register") {
|
|
209
|
+
payload = await startRegistration({ optionsJSON: options });
|
|
210
|
+
} else if (startMethod === "authenticate") {
|
|
211
|
+
payload = await startAuthentication({ optionsJSON: options });
|
|
212
|
+
} else {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`Invalid startMethod: ${startMethod}. Must be 'register' or 'authenticate'`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return payload;
|
|
219
|
+
}
|