dauth-context-react 6.5.0 → 6.6.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/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +289 -287
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +289 -287
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/DauthProfileModal.tsx +113 -291
- package/src/api/dauth.api.ts +54 -31
- package/src/api/interfaces/dauth.api.responses.ts +18 -0
- package/src/api/utils/config.ts +3 -9
- package/src/index.tsx +7 -0
- package/src/initialDauthState.ts +1 -0
- package/src/interfaces.ts +3 -6
- package/src/reducer/dauth.actions.ts +85 -28
- package/src/webauthn.ts +62 -32
package/src/api/dauth.api.ts
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
IPasskeyRegistrationFinishResponse,
|
|
10
10
|
IDeletePasskeyResponse,
|
|
11
11
|
IUploadAvatarResponse,
|
|
12
|
+
IPasskeyAuthStartResponse,
|
|
13
|
+
IPasskeyAuthFinishResponse,
|
|
12
14
|
} from './interfaces/dauth.api.responses';
|
|
13
15
|
|
|
14
16
|
function getCsrfToken(): string {
|
|
@@ -94,14 +96,11 @@ export async function deleteAccountAPI(
|
|
|
94
96
|
export async function getPasskeyCredentialsAPI(
|
|
95
97
|
basePath: string
|
|
96
98
|
): Promise<IPasskeyCredentialsResponse> {
|
|
97
|
-
const response = await fetch(
|
|
98
|
-
|
|
99
|
-
{
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
credentials: 'include',
|
|
103
|
-
}
|
|
104
|
-
);
|
|
99
|
+
const response = await fetch(`${basePath}/passkey/credentials`, {
|
|
100
|
+
method: 'GET',
|
|
101
|
+
headers: { 'X-CSRF-Token': getCsrfToken() },
|
|
102
|
+
credentials: 'include',
|
|
103
|
+
});
|
|
105
104
|
const data = await response.json();
|
|
106
105
|
return { response, data };
|
|
107
106
|
}
|
|
@@ -109,17 +108,14 @@ export async function getPasskeyCredentialsAPI(
|
|
|
109
108
|
export async function startPasskeyRegistrationAPI(
|
|
110
109
|
basePath: string
|
|
111
110
|
): Promise<IPasskeyRegistrationStartResponse> {
|
|
112
|
-
const response = await fetch(
|
|
113
|
-
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
credentials: 'include',
|
|
121
|
-
}
|
|
122
|
-
);
|
|
111
|
+
const response = await fetch(`${basePath}/passkey/register/start`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
'X-CSRF-Token': getCsrfToken(),
|
|
116
|
+
},
|
|
117
|
+
credentials: 'include',
|
|
118
|
+
});
|
|
123
119
|
const data = await response.json();
|
|
124
120
|
return { response, data };
|
|
125
121
|
}
|
|
@@ -128,18 +124,45 @@ export async function finishPasskeyRegistrationAPI(
|
|
|
128
124
|
basePath: string,
|
|
129
125
|
body: { credential: unknown; name?: string }
|
|
130
126
|
): Promise<IPasskeyRegistrationFinishResponse> {
|
|
131
|
-
const response = await fetch(
|
|
132
|
-
|
|
133
|
-
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
127
|
+
const response = await fetch(`${basePath}/passkey/register/finish`, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: {
|
|
130
|
+
'Content-Type': 'application/json',
|
|
131
|
+
'X-CSRF-Token': getCsrfToken(),
|
|
132
|
+
},
|
|
133
|
+
credentials: 'include',
|
|
134
|
+
body: JSON.stringify(body),
|
|
135
|
+
});
|
|
136
|
+
const data = await response.json();
|
|
137
|
+
return { response, data };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function startPasskeyAuthAPI(
|
|
141
|
+
basePath: string
|
|
142
|
+
): Promise<IPasskeyAuthStartResponse> {
|
|
143
|
+
const response = await fetch(`${basePath}/passkey/auth-start`, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: {
|
|
146
|
+
'Content-Type': 'application/json',
|
|
147
|
+
},
|
|
148
|
+
credentials: 'include',
|
|
149
|
+
});
|
|
150
|
+
const data = await response.json();
|
|
151
|
+
return { response, data };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function finishPasskeyAuthAPI(
|
|
155
|
+
basePath: string,
|
|
156
|
+
body: { credential: unknown; sessionId: string }
|
|
157
|
+
): Promise<IPasskeyAuthFinishResponse> {
|
|
158
|
+
const response = await fetch(`${basePath}/passkey/auth-finish`, {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: {
|
|
161
|
+
'Content-Type': 'application/json',
|
|
162
|
+
},
|
|
163
|
+
credentials: 'include',
|
|
164
|
+
body: JSON.stringify(body),
|
|
165
|
+
});
|
|
143
166
|
const data = await response.json();
|
|
144
167
|
return { response, data };
|
|
145
168
|
}
|
|
@@ -70,6 +70,24 @@ export interface IDeletePasskeyResponse {
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
export interface IPasskeyAuthStartResponse {
|
|
74
|
+
response: Response;
|
|
75
|
+
data: {
|
|
76
|
+
status: string;
|
|
77
|
+
options?: Record<string, unknown>;
|
|
78
|
+
sessionId?: string;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface IPasskeyAuthFinishResponse {
|
|
83
|
+
response: Response;
|
|
84
|
+
data: {
|
|
85
|
+
status: string;
|
|
86
|
+
redirect?: string;
|
|
87
|
+
message?: string;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
export interface IUploadAvatarResponse {
|
|
74
92
|
response: Response;
|
|
75
93
|
data: {
|
package/src/api/utils/config.ts
CHANGED
|
@@ -12,15 +12,9 @@ function checkIsLocalhost(): boolean {
|
|
|
12
12
|
return (
|
|
13
13
|
hostname === 'localhost' ||
|
|
14
14
|
hostname === '[::1]' ||
|
|
15
|
-
/^127(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/.test(
|
|
16
|
-
|
|
17
|
-
)
|
|
18
|
-
/^192\.168(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){2}$/.test(
|
|
19
|
-
hostname
|
|
20
|
-
) ||
|
|
21
|
-
/^10(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/.test(
|
|
22
|
-
hostname
|
|
23
|
-
)
|
|
15
|
+
/^127(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/.test(hostname) ||
|
|
16
|
+
/^192\.168(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){2}$/.test(hostname) ||
|
|
17
|
+
/^10(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/.test(hostname)
|
|
24
18
|
);
|
|
25
19
|
}
|
|
26
20
|
|
package/src/index.tsx
CHANGED
|
@@ -146,6 +146,11 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
|
|
|
146
146
|
[ctx]
|
|
147
147
|
);
|
|
148
148
|
|
|
149
|
+
const loginWithPasskey = useCallback(
|
|
150
|
+
() => action.loginWithPasskeyAction(ctx),
|
|
151
|
+
[ctx]
|
|
152
|
+
);
|
|
153
|
+
|
|
149
154
|
const memoProvider = useMemo(
|
|
150
155
|
() => ({
|
|
151
156
|
...dauthState,
|
|
@@ -157,6 +162,7 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
|
|
|
157
162
|
registerPasskey,
|
|
158
163
|
deletePasskeyCredential,
|
|
159
164
|
uploadAvatar,
|
|
165
|
+
loginWithPasskey,
|
|
160
166
|
}),
|
|
161
167
|
[
|
|
162
168
|
dauthState,
|
|
@@ -168,6 +174,7 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
|
|
|
168
174
|
registerPasskey,
|
|
169
175
|
deletePasskeyCredential,
|
|
170
176
|
uploadAvatar,
|
|
177
|
+
loginWithPasskey,
|
|
171
178
|
]
|
|
172
179
|
);
|
|
173
180
|
|
package/src/initialDauthState.ts
CHANGED
|
@@ -19,6 +19,7 @@ const initialDauthState: IDauthState = {
|
|
|
19
19
|
registerPasskey: () => Promise.resolve(null),
|
|
20
20
|
deletePasskeyCredential: () => Promise.resolve(false),
|
|
21
21
|
uploadAvatar: () => Promise.resolve(false),
|
|
22
|
+
loginWithPasskey: () => Promise.resolve(false),
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
export default initialDauthState;
|
package/src/interfaces.ts
CHANGED
|
@@ -86,13 +86,10 @@ export interface IDauthState {
|
|
|
86
86
|
updateUser: (fields: Partial<IDauthUser>) => Promise<boolean>;
|
|
87
87
|
deleteAccount: () => Promise<boolean>;
|
|
88
88
|
getPasskeyCredentials: () => Promise<IPasskeyCredential[]>;
|
|
89
|
-
registerPasskey: (
|
|
90
|
-
|
|
91
|
-
) => Promise<IPasskeyCredential | null>;
|
|
92
|
-
deletePasskeyCredential: (
|
|
93
|
-
credentialId: string
|
|
94
|
-
) => Promise<boolean>;
|
|
89
|
+
registerPasskey: (name?: string) => Promise<IPasskeyCredential | null>;
|
|
90
|
+
deletePasskeyCredential: (credentialId: string) => Promise<boolean>;
|
|
95
91
|
uploadAvatar: (file: File) => Promise<boolean>;
|
|
92
|
+
loginWithPasskey: () => Promise<boolean>;
|
|
96
93
|
}
|
|
97
94
|
|
|
98
95
|
export interface DauthProfileModalProps {
|
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
finishPasskeyRegistrationAPI,
|
|
10
10
|
deletePasskeyCredentialAPI,
|
|
11
11
|
uploadAvatarAPI,
|
|
12
|
+
startPasskeyAuthAPI,
|
|
13
|
+
finishPasskeyAuthAPI,
|
|
12
14
|
} from '../api/dauth.api';
|
|
13
15
|
import type { IPasskeyCredential } from '../api/interfaces/dauth.api.responses';
|
|
14
|
-
import { createPasskeyCredential } from '../webauthn';
|
|
16
|
+
import { createPasskeyCredential, getPasskeyCredential } from '../webauthn';
|
|
15
17
|
import { IDauthDomainState, IDauthUser } from '../interfaces';
|
|
16
18
|
import * as DauthTypes from './dauth.types';
|
|
17
19
|
|
|
@@ -182,38 +184,31 @@ export async function registerPasskeyAction(
|
|
|
182
184
|
const { authProxyPath, onError } = ctx;
|
|
183
185
|
try {
|
|
184
186
|
// Step 1: Get registration options from server
|
|
185
|
-
const startResult =
|
|
186
|
-
await startPasskeyRegistrationAPI(authProxyPath);
|
|
187
|
+
const startResult = await startPasskeyRegistrationAPI(authProxyPath);
|
|
187
188
|
if (startResult.response.status !== 200) {
|
|
188
189
|
onError(new Error('Failed to start passkey registration'));
|
|
189
190
|
return null;
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
// Step 2: Execute WebAuthn ceremony in the browser
|
|
193
|
-
const credential = await createPasskeyCredential(
|
|
194
|
-
startResult.data
|
|
195
|
-
);
|
|
194
|
+
const credential = await createPasskeyCredential(startResult.data);
|
|
196
195
|
|
|
197
196
|
// Step 3: Send the credential back to the server
|
|
198
|
-
const finishResult = await finishPasskeyRegistrationAPI(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
);
|
|
197
|
+
const finishResult = await finishPasskeyRegistrationAPI(authProxyPath, {
|
|
198
|
+
credential,
|
|
199
|
+
name,
|
|
200
|
+
});
|
|
202
201
|
if (
|
|
203
202
|
finishResult.response.status === 200 ||
|
|
204
203
|
finishResult.response.status === 201
|
|
205
204
|
) {
|
|
206
205
|
return finishResult.data.credential;
|
|
207
206
|
}
|
|
208
|
-
onError(
|
|
209
|
-
new Error('Failed to finish passkey registration')
|
|
210
|
-
);
|
|
207
|
+
onError(new Error('Failed to finish passkey registration'));
|
|
211
208
|
return null;
|
|
212
209
|
} catch (error) {
|
|
213
210
|
onError(
|
|
214
|
-
error instanceof Error
|
|
215
|
-
? error
|
|
216
|
-
: new Error('Passkey registration error')
|
|
211
|
+
error instanceof Error ? error : new Error('Passkey registration error')
|
|
217
212
|
);
|
|
218
213
|
return null;
|
|
219
214
|
}
|
|
@@ -231,11 +226,7 @@ export async function deletePasskeyCredentialAction(
|
|
|
231
226
|
);
|
|
232
227
|
return result.response.status === 200;
|
|
233
228
|
} catch (error) {
|
|
234
|
-
onError(
|
|
235
|
-
error instanceof Error
|
|
236
|
-
? error
|
|
237
|
-
: new Error('Delete passkey error')
|
|
238
|
-
);
|
|
229
|
+
onError(error instanceof Error ? error : new Error('Delete passkey error'));
|
|
239
230
|
return false;
|
|
240
231
|
}
|
|
241
232
|
}
|
|
@@ -254,18 +245,84 @@ export async function uploadAvatarAction(
|
|
|
254
245
|
});
|
|
255
246
|
return true;
|
|
256
247
|
}
|
|
257
|
-
onError(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
248
|
+
onError(new Error('Avatar upload error: ' + result.data.message));
|
|
249
|
+
return false;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
onError(error instanceof Error ? error : new Error('Avatar upload error'));
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function loginWithPasskeyAction(
|
|
257
|
+
ctx: ActionContext
|
|
258
|
+
): Promise<boolean> {
|
|
259
|
+
const { dispatch, authProxyPath, onError } = ctx;
|
|
260
|
+
dispatch({
|
|
261
|
+
type: DauthTypes.SET_IS_LOADING,
|
|
262
|
+
payload: { isLoading: true },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
// Step 1: Get authentication options
|
|
267
|
+
const startResult = await startPasskeyAuthAPI(authProxyPath);
|
|
268
|
+
if (startResult.data.status !== 'success' || !startResult.data.options) {
|
|
269
|
+
dispatch({
|
|
270
|
+
type: DauthTypes.SET_IS_LOADING,
|
|
271
|
+
payload: { isLoading: false },
|
|
272
|
+
});
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Step 2: Run WebAuthn ceremony
|
|
277
|
+
const credential = await getPasskeyCredential(
|
|
278
|
+
startResult.data.options as Record<string, any>
|
|
261
279
|
);
|
|
280
|
+
|
|
281
|
+
// Step 3: Verify credential with server
|
|
282
|
+
const finishResult = await finishPasskeyAuthAPI(authProxyPath, {
|
|
283
|
+
credential,
|
|
284
|
+
sessionId: startResult.data.sessionId!,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
if (
|
|
288
|
+
finishResult.data.status === 'login-success' &&
|
|
289
|
+
finishResult.data.redirect
|
|
290
|
+
) {
|
|
291
|
+
// The redirect URL contains ?code=... for the auth code exchange
|
|
292
|
+
const code = new URL(finishResult.data.redirect).searchParams.get('code');
|
|
293
|
+
if (code) {
|
|
294
|
+
const exchangeResult = await exchangeCodeAPI(authProxyPath, code);
|
|
295
|
+
if (exchangeResult.response.status === 200) {
|
|
296
|
+
dispatch({
|
|
297
|
+
type: DauthTypes.LOGIN,
|
|
298
|
+
payload: {
|
|
299
|
+
user: exchangeResult.data.user,
|
|
300
|
+
domain: exchangeResult.data.domain,
|
|
301
|
+
isAuthenticated: true,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
dispatch({
|
|
305
|
+
type: DauthTypes.SET_IS_LOADING,
|
|
306
|
+
payload: { isLoading: false },
|
|
307
|
+
});
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
dispatch({
|
|
314
|
+
type: DauthTypes.SET_IS_LOADING,
|
|
315
|
+
payload: { isLoading: false },
|
|
316
|
+
});
|
|
262
317
|
return false;
|
|
263
318
|
} catch (error) {
|
|
264
319
|
onError(
|
|
265
|
-
error instanceof Error
|
|
266
|
-
? error
|
|
267
|
-
: new Error('Avatar upload error')
|
|
320
|
+
error instanceof Error ? error : new Error('Passkey authentication error')
|
|
268
321
|
);
|
|
322
|
+
dispatch({
|
|
323
|
+
type: DauthTypes.SET_IS_LOADING,
|
|
324
|
+
payload: { isLoading: false },
|
|
325
|
+
});
|
|
269
326
|
return false;
|
|
270
327
|
}
|
|
271
328
|
}
|
package/src/webauthn.ts
CHANGED
|
@@ -3,12 +3,8 @@
|
|
|
3
3
|
* Uses the raw Web Credentials API — no external dependencies.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export function base64urlToBuffer(
|
|
7
|
-
base64url
|
|
8
|
-
): ArrayBuffer {
|
|
9
|
-
const base64 = base64url
|
|
10
|
-
.replace(/-/g, '+')
|
|
11
|
-
.replace(/_/g, '/');
|
|
6
|
+
export function base64urlToBuffer(base64url: string): ArrayBuffer {
|
|
7
|
+
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
|
|
12
8
|
const pad = base64.length % 4;
|
|
13
9
|
const padded = pad ? base64 + '='.repeat(4 - pad) : base64;
|
|
14
10
|
const binary = atob(padded);
|
|
@@ -19,9 +15,7 @@ export function base64urlToBuffer(
|
|
|
19
15
|
return bytes.buffer;
|
|
20
16
|
}
|
|
21
17
|
|
|
22
|
-
export function bufferToBase64url(
|
|
23
|
-
buffer: ArrayBuffer
|
|
24
|
-
): string {
|
|
18
|
+
export function bufferToBase64url(buffer: ArrayBuffer): string {
|
|
25
19
|
const bytes = new Uint8Array(buffer);
|
|
26
20
|
let binary = '';
|
|
27
21
|
for (let i = 0; i < bytes.length; i++) {
|
|
@@ -44,17 +38,16 @@ export async function createPasskeyCredential(
|
|
|
44
38
|
): Promise<Record<string, any>> {
|
|
45
39
|
const publicKey = {
|
|
46
40
|
...options,
|
|
41
|
+
rp: { ...options.rp, id: window.location.hostname },
|
|
47
42
|
challenge: base64urlToBuffer(options.challenge),
|
|
48
43
|
user: {
|
|
49
44
|
...options.user,
|
|
50
45
|
id: base64urlToBuffer(options.user.id),
|
|
51
46
|
},
|
|
52
|
-
excludeCredentials: (options.excludeCredentials ?? []).map(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
})
|
|
57
|
-
),
|
|
47
|
+
excludeCredentials: (options.excludeCredentials ?? []).map((c: any) => ({
|
|
48
|
+
...c,
|
|
49
|
+
id: base64urlToBuffer(c.id),
|
|
50
|
+
})),
|
|
58
51
|
} as PublicKeyCredentialCreationOptions;
|
|
59
52
|
|
|
60
53
|
const credential = (await navigator.credentials.create({
|
|
@@ -65,34 +58,26 @@ export async function createPasskeyCredential(
|
|
|
65
58
|
throw new Error('Passkey registration was cancelled');
|
|
66
59
|
}
|
|
67
60
|
|
|
68
|
-
const attestation =
|
|
69
|
-
credential.response as AuthenticatorAttestationResponse;
|
|
61
|
+
const attestation = credential.response as AuthenticatorAttestationResponse;
|
|
70
62
|
|
|
71
63
|
return {
|
|
72
64
|
id: credential.id,
|
|
73
65
|
rawId: bufferToBase64url(credential.rawId),
|
|
74
66
|
type: credential.type,
|
|
75
67
|
response: {
|
|
76
|
-
clientDataJSON: bufferToBase64url(
|
|
77
|
-
|
|
78
|
-
),
|
|
79
|
-
attestationObject: bufferToBase64url(
|
|
80
|
-
attestation.attestationObject
|
|
81
|
-
),
|
|
68
|
+
clientDataJSON: bufferToBase64url(attestation.clientDataJSON),
|
|
69
|
+
attestationObject: bufferToBase64url(attestation.attestationObject),
|
|
82
70
|
...(attestation.getTransports
|
|
83
71
|
? { transports: attestation.getTransports() }
|
|
84
72
|
: {}),
|
|
85
73
|
...(attestation.getPublicKeyAlgorithm
|
|
86
74
|
? {
|
|
87
|
-
publicKeyAlgorithm:
|
|
88
|
-
attestation.getPublicKeyAlgorithm(),
|
|
75
|
+
publicKeyAlgorithm: attestation.getPublicKeyAlgorithm(),
|
|
89
76
|
}
|
|
90
77
|
: {}),
|
|
91
78
|
...(attestation.getPublicKey
|
|
92
79
|
? {
|
|
93
|
-
publicKey: bufferToBase64url(
|
|
94
|
-
attestation.getPublicKey()!
|
|
95
|
-
),
|
|
80
|
+
publicKey: bufferToBase64url(attestation.getPublicKey()!),
|
|
96
81
|
}
|
|
97
82
|
: {}),
|
|
98
83
|
...(attestation.getAuthenticatorData
|
|
@@ -103,9 +88,54 @@ export async function createPasskeyCredential(
|
|
|
103
88
|
}
|
|
104
89
|
: {}),
|
|
105
90
|
},
|
|
106
|
-
clientExtensionResults:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
91
|
+
clientExtensionResults: credential.getClientExtensionResults(),
|
|
92
|
+
authenticatorAttachment: credential.authenticatorAttachment ?? undefined,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Converts server-provided authentication options (base64url strings)
|
|
98
|
+
* into the format expected by navigator.credentials.get(),
|
|
99
|
+
* executes the WebAuthn ceremony, and serializes the response
|
|
100
|
+
* back to JSON with base64url-encoded ArrayBuffers.
|
|
101
|
+
*/
|
|
102
|
+
export async function getPasskeyCredential(
|
|
103
|
+
options: Record<string, any>
|
|
104
|
+
): Promise<Record<string, any>> {
|
|
105
|
+
const publicKey: PublicKeyCredentialRequestOptions = {
|
|
106
|
+
challenge: base64urlToBuffer(options.challenge),
|
|
107
|
+
timeout: options.timeout,
|
|
108
|
+
rpId: window.location.hostname,
|
|
109
|
+
userVerification: options.userVerification || 'preferred',
|
|
110
|
+
allowCredentials: (options.allowCredentials || []).map(
|
|
111
|
+
(c: { id: string; type: string; transports?: string[] }) => ({
|
|
112
|
+
...c,
|
|
113
|
+
id: base64urlToBuffer(c.id),
|
|
114
|
+
})
|
|
115
|
+
),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const credential = (await navigator.credentials.get({
|
|
119
|
+
publicKey,
|
|
120
|
+
})) as PublicKeyCredential;
|
|
121
|
+
|
|
122
|
+
if (!credential) throw new Error('No credential returned');
|
|
123
|
+
|
|
124
|
+
const assertion = credential.response as AuthenticatorAssertionResponse;
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
id: credential.id,
|
|
128
|
+
rawId: bufferToBase64url(credential.rawId),
|
|
129
|
+
type: credential.type,
|
|
130
|
+
response: {
|
|
131
|
+
clientDataJSON: bufferToBase64url(assertion.clientDataJSON),
|
|
132
|
+
authenticatorData: bufferToBase64url(assertion.authenticatorData),
|
|
133
|
+
signature: bufferToBase64url(assertion.signature),
|
|
134
|
+
userHandle: assertion.userHandle
|
|
135
|
+
? bufferToBase64url(assertion.userHandle)
|
|
136
|
+
: null,
|
|
137
|
+
},
|
|
138
|
+
clientExtensionResults: credential.getClientExtensionResults(),
|
|
139
|
+
authenticatorAttachment: credential.authenticatorAttachment,
|
|
110
140
|
};
|
|
111
141
|
}
|