isitme 0.0.1
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/README.md +104 -0
- package/dist/browser.d.ts +23 -0
- package/dist/browser.js +400 -0
- package/dist/index.d.ts +314 -0
- package/dist/index.js +15243 -0
- package/dist/types.d.ts +129 -0
- package/dist/types.js +0 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# isitme
|
|
2
|
+
|
|
3
|
+
Passkey authentication for Express. One line of code, no database, no passwords.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm install isitme
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import express from "express";
|
|
13
|
+
import cookieParser from "cookie-parser";
|
|
14
|
+
import { isitme } from "isitme";
|
|
15
|
+
|
|
16
|
+
const app = express();
|
|
17
|
+
app.use(cookieParser());
|
|
18
|
+
app.use(isitme());
|
|
19
|
+
|
|
20
|
+
app.get("/", (req, res) => {
|
|
21
|
+
res.send("You are authenticated!");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
app.listen(3000);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The first visitor registers a passkey with their fingerprint or face. After that, every request requires biometric authentication.
|
|
28
|
+
|
|
29
|
+
## Options
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
app.use(isitme({
|
|
33
|
+
secret: "your-jwt-secret", // default: random (sessions lost on restart)
|
|
34
|
+
storage: "./auth.json", // default: "./auth.json", false for in-memory
|
|
35
|
+
publicPaths: ["/", "/about"], // routes that skip auth
|
|
36
|
+
prefix: "/auth", // auth endpoint prefix
|
|
37
|
+
sessionExpiryDays: 7, // session duration
|
|
38
|
+
page: { title: "My App" }, // customize login page, or false to disable
|
|
39
|
+
}));
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Public paths and conditional auth
|
|
43
|
+
|
|
44
|
+
The middleware sets `req.isAuthenticated` on every request, even on public paths:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
app.use(isitme({ publicPaths: ["/"] }));
|
|
48
|
+
|
|
49
|
+
app.get("/", (req, res) => {
|
|
50
|
+
if (req.isAuthenticated) {
|
|
51
|
+
res.send("Welcome back!");
|
|
52
|
+
} else {
|
|
53
|
+
res.send("Sign in to continue.");
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Browser client
|
|
59
|
+
|
|
60
|
+
For custom login pages, import the browser helpers:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
import { register, login, getAuthStatus } from "isitme/browser";
|
|
64
|
+
|
|
65
|
+
const status = await getAuthStatus();
|
|
66
|
+
|
|
67
|
+
if (!status.isSetup) {
|
|
68
|
+
await register();
|
|
69
|
+
} else {
|
|
70
|
+
await login();
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Custom storage
|
|
75
|
+
|
|
76
|
+
Implement the `StorageAdapter` interface to use any backend:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
import { isitme } from "isitme";
|
|
80
|
+
|
|
81
|
+
app.use(isitme({
|
|
82
|
+
storage: {
|
|
83
|
+
isSetupComplete() { /* ... */ },
|
|
84
|
+
getUser() { /* ... */ },
|
|
85
|
+
getCredentials() { /* ... */ },
|
|
86
|
+
getCredentialById(id) { /* ... */ },
|
|
87
|
+
saveUserAndCredential(user, credential) { /* ... */ },
|
|
88
|
+
updateCredentialCounter(id, counter) { /* ... */ },
|
|
89
|
+
getAuthDataJson() { /* ... */ },
|
|
90
|
+
}
|
|
91
|
+
}));
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## How it works
|
|
95
|
+
|
|
96
|
+
1. `isitme()` adds auth routes (`/auth/*`) and a guard middleware to your app
|
|
97
|
+
2. Unauthenticated visitors see a built-in login page
|
|
98
|
+
3. Registration creates a WebAuthn passkey tied to the device's biometrics
|
|
99
|
+
4. Login verifies the passkey and sets a JWT session cookie
|
|
100
|
+
5. `req.isAuthenticated` is available on every request
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface AuthStatus {
|
|
2
|
+
setupComplete: boolean;
|
|
3
|
+
isAuthenticated: boolean;
|
|
4
|
+
}
|
|
5
|
+
interface RegisterResult {
|
|
6
|
+
verified: boolean;
|
|
7
|
+
authData?: string;
|
|
8
|
+
}
|
|
9
|
+
interface LoginResult {
|
|
10
|
+
verified: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface IsitmeBrowserOptions {
|
|
13
|
+
/** Auth API prefix. Default: `/auth` */
|
|
14
|
+
prefix?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare function register(opts?: IsitmeBrowserOptions): Promise<RegisterResult>;
|
|
18
|
+
|
|
19
|
+
declare function login(opts?: IsitmeBrowserOptions): Promise<LoginResult>;
|
|
20
|
+
|
|
21
|
+
declare function getAuthStatus(opts?: IsitmeBrowserOptions): Promise<AuthStatus>;
|
|
22
|
+
|
|
23
|
+
export { type AuthStatus, type IsitmeBrowserOptions, type LoginResult, type RegisterResult, getAuthStatus, login, register };
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
// ../../node_modules/.pnpm/@simplewebauthn+browser@11.0.0/node_modules/@simplewebauthn/browser/dist/bundle/index.js
|
|
2
|
+
function bufferToBase64URLString(buffer) {
|
|
3
|
+
const bytes = new Uint8Array(buffer);
|
|
4
|
+
let str = "";
|
|
5
|
+
for (const charCode of bytes) {
|
|
6
|
+
str += String.fromCharCode(charCode);
|
|
7
|
+
}
|
|
8
|
+
const base64String = btoa(str);
|
|
9
|
+
return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
10
|
+
}
|
|
11
|
+
function base64URLStringToBuffer(base64URLString) {
|
|
12
|
+
const base64 = base64URLString.replace(/-/g, "+").replace(/_/g, "/");
|
|
13
|
+
const padLength = (4 - base64.length % 4) % 4;
|
|
14
|
+
const padded = base64.padEnd(base64.length + padLength, "=");
|
|
15
|
+
const binary = atob(padded);
|
|
16
|
+
const buffer = new ArrayBuffer(binary.length);
|
|
17
|
+
const bytes = new Uint8Array(buffer);
|
|
18
|
+
for (let i = 0; i < binary.length; i++) {
|
|
19
|
+
bytes[i] = binary.charCodeAt(i);
|
|
20
|
+
}
|
|
21
|
+
return buffer;
|
|
22
|
+
}
|
|
23
|
+
function browserSupportsWebAuthn() {
|
|
24
|
+
return window?.PublicKeyCredential !== void 0 && typeof window.PublicKeyCredential === "function";
|
|
25
|
+
}
|
|
26
|
+
function toPublicKeyCredentialDescriptor(descriptor) {
|
|
27
|
+
const { id } = descriptor;
|
|
28
|
+
return {
|
|
29
|
+
...descriptor,
|
|
30
|
+
id: base64URLStringToBuffer(id),
|
|
31
|
+
transports: descriptor.transports
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function isValidDomain(hostname) {
|
|
35
|
+
return hostname === "localhost" || /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i.test(hostname);
|
|
36
|
+
}
|
|
37
|
+
var WebAuthnError = class extends Error {
|
|
38
|
+
constructor({ message, code, cause, name }) {
|
|
39
|
+
super(message, { cause });
|
|
40
|
+
this.name = name ?? cause.name;
|
|
41
|
+
this.code = code;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
function identifyRegistrationError({ error, options }) {
|
|
45
|
+
const { publicKey } = options;
|
|
46
|
+
if (!publicKey) {
|
|
47
|
+
throw Error("options was missing required publicKey property");
|
|
48
|
+
}
|
|
49
|
+
if (error.name === "AbortError") {
|
|
50
|
+
if (options.signal instanceof AbortSignal) {
|
|
51
|
+
return new WebAuthnError({
|
|
52
|
+
message: "Registration ceremony was sent an abort signal",
|
|
53
|
+
code: "ERROR_CEREMONY_ABORTED",
|
|
54
|
+
cause: error
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
} else if (error.name === "ConstraintError") {
|
|
58
|
+
if (publicKey.authenticatorSelection?.requireResidentKey === true) {
|
|
59
|
+
return new WebAuthnError({
|
|
60
|
+
message: "Discoverable credentials were required but no available authenticator supported it",
|
|
61
|
+
code: "ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT",
|
|
62
|
+
cause: error
|
|
63
|
+
});
|
|
64
|
+
} else if (options.mediation === "conditional" && publicKey.authenticatorSelection?.userVerification === "required") {
|
|
65
|
+
return new WebAuthnError({
|
|
66
|
+
message: "User verification was required during automatic registration but it could not be performed",
|
|
67
|
+
code: "ERROR_AUTO_REGISTER_USER_VERIFICATION_FAILURE",
|
|
68
|
+
cause: error
|
|
69
|
+
});
|
|
70
|
+
} else if (publicKey.authenticatorSelection?.userVerification === "required") {
|
|
71
|
+
return new WebAuthnError({
|
|
72
|
+
message: "User verification was required but no available authenticator supported it",
|
|
73
|
+
code: "ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT",
|
|
74
|
+
cause: error
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
} else if (error.name === "InvalidStateError") {
|
|
78
|
+
return new WebAuthnError({
|
|
79
|
+
message: "The authenticator was previously registered",
|
|
80
|
+
code: "ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED",
|
|
81
|
+
cause: error
|
|
82
|
+
});
|
|
83
|
+
} else if (error.name === "NotAllowedError") {
|
|
84
|
+
return new WebAuthnError({
|
|
85
|
+
message: error.message,
|
|
86
|
+
code: "ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",
|
|
87
|
+
cause: error
|
|
88
|
+
});
|
|
89
|
+
} else if (error.name === "NotSupportedError") {
|
|
90
|
+
const validPubKeyCredParams = publicKey.pubKeyCredParams.filter((param) => param.type === "public-key");
|
|
91
|
+
if (validPubKeyCredParams.length === 0) {
|
|
92
|
+
return new WebAuthnError({
|
|
93
|
+
message: 'No entry in pubKeyCredParams was of type "public-key"',
|
|
94
|
+
code: "ERROR_MALFORMED_PUBKEYCREDPARAMS",
|
|
95
|
+
cause: error
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return new WebAuthnError({
|
|
99
|
+
message: "No available authenticator supported any of the specified pubKeyCredParams algorithms",
|
|
100
|
+
code: "ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG",
|
|
101
|
+
cause: error
|
|
102
|
+
});
|
|
103
|
+
} else if (error.name === "SecurityError") {
|
|
104
|
+
const effectiveDomain = window.location.hostname;
|
|
105
|
+
if (!isValidDomain(effectiveDomain)) {
|
|
106
|
+
return new WebAuthnError({
|
|
107
|
+
message: `${window.location.hostname} is an invalid domain`,
|
|
108
|
+
code: "ERROR_INVALID_DOMAIN",
|
|
109
|
+
cause: error
|
|
110
|
+
});
|
|
111
|
+
} else if (publicKey.rp.id !== effectiveDomain) {
|
|
112
|
+
return new WebAuthnError({
|
|
113
|
+
message: `The RP ID "${publicKey.rp.id}" is invalid for this domain`,
|
|
114
|
+
code: "ERROR_INVALID_RP_ID",
|
|
115
|
+
cause: error
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
} else if (error.name === "TypeError") {
|
|
119
|
+
if (publicKey.user.id.byteLength < 1 || publicKey.user.id.byteLength > 64) {
|
|
120
|
+
return new WebAuthnError({
|
|
121
|
+
message: "User ID was not between 1 and 64 characters",
|
|
122
|
+
code: "ERROR_INVALID_USER_ID_LENGTH",
|
|
123
|
+
cause: error
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
} else if (error.name === "UnknownError") {
|
|
127
|
+
return new WebAuthnError({
|
|
128
|
+
message: "The authenticator was unable to process the specified options, or could not create a new credential",
|
|
129
|
+
code: "ERROR_AUTHENTICATOR_GENERAL_ERROR",
|
|
130
|
+
cause: error
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return error;
|
|
134
|
+
}
|
|
135
|
+
var BaseWebAuthnAbortService = class {
|
|
136
|
+
createNewAbortSignal() {
|
|
137
|
+
if (this.controller) {
|
|
138
|
+
const abortError = new Error("Cancelling existing WebAuthn API call for new one");
|
|
139
|
+
abortError.name = "AbortError";
|
|
140
|
+
this.controller.abort(abortError);
|
|
141
|
+
}
|
|
142
|
+
const newController = new AbortController();
|
|
143
|
+
this.controller = newController;
|
|
144
|
+
return newController.signal;
|
|
145
|
+
}
|
|
146
|
+
cancelCeremony() {
|
|
147
|
+
if (this.controller) {
|
|
148
|
+
const abortError = new Error("Manually cancelling existing WebAuthn API call");
|
|
149
|
+
abortError.name = "AbortError";
|
|
150
|
+
this.controller.abort(abortError);
|
|
151
|
+
this.controller = void 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
var WebAuthnAbortService = new BaseWebAuthnAbortService();
|
|
156
|
+
var attachments = ["cross-platform", "platform"];
|
|
157
|
+
function toAuthenticatorAttachment(attachment) {
|
|
158
|
+
if (!attachment) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (attachments.indexOf(attachment) < 0) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
return attachment;
|
|
165
|
+
}
|
|
166
|
+
async function startRegistration(options) {
|
|
167
|
+
const { optionsJSON, useAutoRegister = false } = options;
|
|
168
|
+
if (!browserSupportsWebAuthn()) {
|
|
169
|
+
throw new Error("WebAuthn is not supported in this browser");
|
|
170
|
+
}
|
|
171
|
+
const publicKey = {
|
|
172
|
+
...optionsJSON,
|
|
173
|
+
challenge: base64URLStringToBuffer(optionsJSON.challenge),
|
|
174
|
+
user: {
|
|
175
|
+
...optionsJSON.user,
|
|
176
|
+
id: base64URLStringToBuffer(optionsJSON.user.id)
|
|
177
|
+
},
|
|
178
|
+
excludeCredentials: optionsJSON.excludeCredentials?.map(toPublicKeyCredentialDescriptor)
|
|
179
|
+
};
|
|
180
|
+
const createOptions = {};
|
|
181
|
+
if (useAutoRegister) {
|
|
182
|
+
createOptions.mediation = "conditional";
|
|
183
|
+
}
|
|
184
|
+
createOptions.publicKey = publicKey;
|
|
185
|
+
createOptions.signal = WebAuthnAbortService.createNewAbortSignal();
|
|
186
|
+
let credential;
|
|
187
|
+
try {
|
|
188
|
+
credential = await navigator.credentials.create(createOptions);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
throw identifyRegistrationError({ error: err, options: createOptions });
|
|
191
|
+
}
|
|
192
|
+
if (!credential) {
|
|
193
|
+
throw new Error("Registration was not completed");
|
|
194
|
+
}
|
|
195
|
+
const { id, rawId, response, type } = credential;
|
|
196
|
+
let transports = void 0;
|
|
197
|
+
if (typeof response.getTransports === "function") {
|
|
198
|
+
transports = response.getTransports();
|
|
199
|
+
}
|
|
200
|
+
let responsePublicKeyAlgorithm = void 0;
|
|
201
|
+
if (typeof response.getPublicKeyAlgorithm === "function") {
|
|
202
|
+
try {
|
|
203
|
+
responsePublicKeyAlgorithm = response.getPublicKeyAlgorithm();
|
|
204
|
+
} catch (error) {
|
|
205
|
+
warnOnBrokenImplementation("getPublicKeyAlgorithm()", error);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
let responsePublicKey = void 0;
|
|
209
|
+
if (typeof response.getPublicKey === "function") {
|
|
210
|
+
try {
|
|
211
|
+
const _publicKey = response.getPublicKey();
|
|
212
|
+
if (_publicKey !== null) {
|
|
213
|
+
responsePublicKey = bufferToBase64URLString(_publicKey);
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
warnOnBrokenImplementation("getPublicKey()", error);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
let responseAuthenticatorData;
|
|
220
|
+
if (typeof response.getAuthenticatorData === "function") {
|
|
221
|
+
try {
|
|
222
|
+
responseAuthenticatorData = bufferToBase64URLString(response.getAuthenticatorData());
|
|
223
|
+
} catch (error) {
|
|
224
|
+
warnOnBrokenImplementation("getAuthenticatorData()", error);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
id,
|
|
229
|
+
rawId: bufferToBase64URLString(rawId),
|
|
230
|
+
response: {
|
|
231
|
+
attestationObject: bufferToBase64URLString(response.attestationObject),
|
|
232
|
+
clientDataJSON: bufferToBase64URLString(response.clientDataJSON),
|
|
233
|
+
transports,
|
|
234
|
+
publicKeyAlgorithm: responsePublicKeyAlgorithm,
|
|
235
|
+
publicKey: responsePublicKey,
|
|
236
|
+
authenticatorData: responseAuthenticatorData
|
|
237
|
+
},
|
|
238
|
+
type,
|
|
239
|
+
clientExtensionResults: credential.getClientExtensionResults(),
|
|
240
|
+
authenticatorAttachment: toAuthenticatorAttachment(credential.authenticatorAttachment)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function warnOnBrokenImplementation(methodName, cause) {
|
|
244
|
+
console.warn(`The browser extension that intercepted this WebAuthn API call incorrectly implemented ${methodName}. You should report this error to them.
|
|
245
|
+
`, cause);
|
|
246
|
+
}
|
|
247
|
+
function browserSupportsWebAuthnAutofill() {
|
|
248
|
+
if (!browserSupportsWebAuthn()) {
|
|
249
|
+
return new Promise((resolve) => resolve(false));
|
|
250
|
+
}
|
|
251
|
+
const globalPublicKeyCredential = window.PublicKeyCredential;
|
|
252
|
+
if (globalPublicKeyCredential.isConditionalMediationAvailable === void 0) {
|
|
253
|
+
return new Promise((resolve) => resolve(false));
|
|
254
|
+
}
|
|
255
|
+
return globalPublicKeyCredential.isConditionalMediationAvailable();
|
|
256
|
+
}
|
|
257
|
+
function identifyAuthenticationError({ error, options }) {
|
|
258
|
+
const { publicKey } = options;
|
|
259
|
+
if (!publicKey) {
|
|
260
|
+
throw Error("options was missing required publicKey property");
|
|
261
|
+
}
|
|
262
|
+
if (error.name === "AbortError") {
|
|
263
|
+
if (options.signal instanceof AbortSignal) {
|
|
264
|
+
return new WebAuthnError({
|
|
265
|
+
message: "Authentication ceremony was sent an abort signal",
|
|
266
|
+
code: "ERROR_CEREMONY_ABORTED",
|
|
267
|
+
cause: error
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
} else if (error.name === "NotAllowedError") {
|
|
271
|
+
return new WebAuthnError({
|
|
272
|
+
message: error.message,
|
|
273
|
+
code: "ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",
|
|
274
|
+
cause: error
|
|
275
|
+
});
|
|
276
|
+
} else if (error.name === "SecurityError") {
|
|
277
|
+
const effectiveDomain = window.location.hostname;
|
|
278
|
+
if (!isValidDomain(effectiveDomain)) {
|
|
279
|
+
return new WebAuthnError({
|
|
280
|
+
message: `${window.location.hostname} is an invalid domain`,
|
|
281
|
+
code: "ERROR_INVALID_DOMAIN",
|
|
282
|
+
cause: error
|
|
283
|
+
});
|
|
284
|
+
} else if (publicKey.rpId !== effectiveDomain) {
|
|
285
|
+
return new WebAuthnError({
|
|
286
|
+
message: `The RP ID "${publicKey.rpId}" is invalid for this domain`,
|
|
287
|
+
code: "ERROR_INVALID_RP_ID",
|
|
288
|
+
cause: error
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
} else if (error.name === "UnknownError") {
|
|
292
|
+
return new WebAuthnError({
|
|
293
|
+
message: "The authenticator was unable to process the specified options, or could not create a new assertion signature",
|
|
294
|
+
code: "ERROR_AUTHENTICATOR_GENERAL_ERROR",
|
|
295
|
+
cause: error
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return error;
|
|
299
|
+
}
|
|
300
|
+
async function startAuthentication(options) {
|
|
301
|
+
const { optionsJSON, useBrowserAutofill = false, verifyBrowserAutofillInput = true } = options;
|
|
302
|
+
if (!browserSupportsWebAuthn()) {
|
|
303
|
+
throw new Error("WebAuthn is not supported in this browser");
|
|
304
|
+
}
|
|
305
|
+
let allowCredentials;
|
|
306
|
+
if (optionsJSON.allowCredentials?.length !== 0) {
|
|
307
|
+
allowCredentials = optionsJSON.allowCredentials?.map(toPublicKeyCredentialDescriptor);
|
|
308
|
+
}
|
|
309
|
+
const publicKey = {
|
|
310
|
+
...optionsJSON,
|
|
311
|
+
challenge: base64URLStringToBuffer(optionsJSON.challenge),
|
|
312
|
+
allowCredentials
|
|
313
|
+
};
|
|
314
|
+
const getOptions = {};
|
|
315
|
+
if (useBrowserAutofill) {
|
|
316
|
+
if (!await browserSupportsWebAuthnAutofill()) {
|
|
317
|
+
throw Error("Browser does not support WebAuthn autofill");
|
|
318
|
+
}
|
|
319
|
+
const eligibleInputs = document.querySelectorAll("input[autocomplete$='webauthn']");
|
|
320
|
+
if (eligibleInputs.length < 1 && verifyBrowserAutofillInput) {
|
|
321
|
+
throw Error('No <input> with "webauthn" as the only or last value in its `autocomplete` attribute was detected');
|
|
322
|
+
}
|
|
323
|
+
getOptions.mediation = "conditional";
|
|
324
|
+
publicKey.allowCredentials = [];
|
|
325
|
+
}
|
|
326
|
+
getOptions.publicKey = publicKey;
|
|
327
|
+
getOptions.signal = WebAuthnAbortService.createNewAbortSignal();
|
|
328
|
+
let credential;
|
|
329
|
+
try {
|
|
330
|
+
credential = await navigator.credentials.get(getOptions);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
throw identifyAuthenticationError({ error: err, options: getOptions });
|
|
333
|
+
}
|
|
334
|
+
if (!credential) {
|
|
335
|
+
throw new Error("Authentication was not completed");
|
|
336
|
+
}
|
|
337
|
+
const { id, rawId, response, type } = credential;
|
|
338
|
+
let userHandle = void 0;
|
|
339
|
+
if (response.userHandle) {
|
|
340
|
+
userHandle = bufferToBase64URLString(response.userHandle);
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
id,
|
|
344
|
+
rawId: bufferToBase64URLString(rawId),
|
|
345
|
+
response: {
|
|
346
|
+
authenticatorData: bufferToBase64URLString(response.authenticatorData),
|
|
347
|
+
clientDataJSON: bufferToBase64URLString(response.clientDataJSON),
|
|
348
|
+
signature: bufferToBase64URLString(response.signature),
|
|
349
|
+
userHandle
|
|
350
|
+
},
|
|
351
|
+
type,
|
|
352
|
+
clientExtensionResults: credential.getClientExtensionResults(),
|
|
353
|
+
authenticatorAttachment: toAuthenticatorAttachment(credential.authenticatorAttachment)
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ../browser/dist/index.js
|
|
358
|
+
async function register(opts = {}) {
|
|
359
|
+
const prefix = opts.prefix || "/auth";
|
|
360
|
+
const optRes = await fetch(`${prefix}/register/options`, { method: "POST" });
|
|
361
|
+
if (!optRes.ok) throw new Error("Failed to get registration options");
|
|
362
|
+
const { options, nonce, userId } = await optRes.json();
|
|
363
|
+
const registration = await startRegistration({ optionsJSON: options });
|
|
364
|
+
const verifyRes = await fetch(`${prefix}/register/verify`, {
|
|
365
|
+
method: "POST",
|
|
366
|
+
headers: { "Content-Type": "application/json" },
|
|
367
|
+
body: JSON.stringify({ nonce, userId, response: registration })
|
|
368
|
+
});
|
|
369
|
+
const result = await verifyRes.json();
|
|
370
|
+
if (!verifyRes.ok) throw new Error(result.error || "Registration failed");
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
async function login(opts = {}) {
|
|
374
|
+
const prefix = opts.prefix || "/auth";
|
|
375
|
+
const optRes = await fetch(`${prefix}/login/options`, { method: "POST" });
|
|
376
|
+
if (!optRes.ok) throw new Error("Failed to get login options");
|
|
377
|
+
const { options, nonce } = await optRes.json();
|
|
378
|
+
const authentication = await startAuthentication({ optionsJSON: options });
|
|
379
|
+
const verifyRes = await fetch(`${prefix}/login/verify`, {
|
|
380
|
+
method: "POST",
|
|
381
|
+
headers: { "Content-Type": "application/json" },
|
|
382
|
+
body: JSON.stringify({ nonce, response: authentication })
|
|
383
|
+
});
|
|
384
|
+
if (!verifyRes.ok) {
|
|
385
|
+
const data = await verifyRes.json();
|
|
386
|
+
throw new Error(data.error || "Authentication failed");
|
|
387
|
+
}
|
|
388
|
+
return verifyRes.json();
|
|
389
|
+
}
|
|
390
|
+
async function getAuthStatus(opts = {}) {
|
|
391
|
+
const prefix = opts.prefix || "/auth";
|
|
392
|
+
const res = await fetch(`${prefix}/status`);
|
|
393
|
+
if (!res.ok) throw new Error("Failed to fetch auth status");
|
|
394
|
+
return res.json();
|
|
395
|
+
}
|
|
396
|
+
export {
|
|
397
|
+
getAuthStatus,
|
|
398
|
+
login,
|
|
399
|
+
register
|
|
400
|
+
};
|