1
+ import { fromByteArray } from "base64-js" ;
1
2
import "formdata-polyfill" ;
2
3
import $ from "jquery" ;
3
4
import "weakmap-polyfill" ;
4
- import "webauthn-polyfills" ;
5
5
6
6
import {
7
7
type AuthenticatorValidationChallenge ,
@@ -257,9 +257,47 @@ class AutosubmitStage extends Stage<AutosubmitChallenge> {
257
257
}
258
258
}
259
259
260
+ export interface Assertion {
261
+ id : string ;
262
+ rawId : string ;
263
+ type : string ;
264
+ registrationClientExtensions : string ;
265
+ response : {
266
+ clientDataJSON : string ;
267
+ attestationObject : string ;
268
+ } ;
269
+ }
270
+
271
+ export interface AuthAssertion {
272
+ id : string ;
273
+ rawId : string ;
274
+ type : string ;
275
+ assertionClientExtensions : string ;
276
+ response : {
277
+ clientDataJSON : string ;
278
+ authenticatorData : string ;
279
+ signature : string ;
280
+ userHandle : string | null ;
281
+ } ;
282
+ }
283
+
260
284
class AuthenticatorValidateStage extends Stage < AuthenticatorValidationChallenge > {
261
285
deviceChallenge ?: DeviceChallenge ;
262
286
287
+ b64enc ( buf : Uint8Array ) : string {
288
+ return fromByteArray ( buf ) . replace ( / \+ / g, "-" ) . replace ( / \/ / g, "_" ) . replace ( / = / g, "" ) ;
289
+ }
290
+
291
+ b64RawEnc ( buf : Uint8Array ) : string {
292
+ return fromByteArray ( buf ) . replace ( / \+ / g, "-" ) . replace ( / \/ / g, "_" ) ;
293
+ }
294
+
295
+ u8arr ( input : string ) : Uint8Array {
296
+ return Uint8Array . from ( atob ( input . replace ( / _ / g, "/" ) . replace ( / - / g, "+" ) ) , ( c ) =>
297
+ c . charCodeAt ( 0 ) ,
298
+ ) ;
299
+ }
300
+
263
301
checkWebAuthnSupport ( ) : boolean {
264
302
if ( "credentials" in navigator ) {
265
303
return true ;
@@ -272,6 +310,98 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
272
310
return false ;
273
311
}
274
312
313
+ /**
314
+ * Transforms items in the credentialCreateOptions generated on the server
315
+ * into byte arrays expected by the navigator.credentials.create() call
316
+ */
317
+ transformCredentialCreateOptions (
318
+ credentialCreateOptions : PublicKeyCredentialCreationOptions ,
319
+ userId : string ,
320
+ ) : PublicKeyCredentialCreationOptions {
321
+ const user = credentialCreateOptions . user ;
322
+ // Because json can't contain raw bytes, the server base64-encodes the User ID
323
+ // So to get the base64 encoded byte array, we first need to convert it to a regular
324
+ // string, then a byte array, re-encode it and wrap that in an array.
325
+ const stringId = decodeURIComponent ( window . atob ( userId ) ) ;
326
+ user . id = this . u8arr ( this . b64enc ( this . u8arr ( stringId ) ) ) ;
327
+ const challenge = this . u8arr ( credentialCreateOptions . challenge . toString ( ) ) ;
328
+
329
+ return Object . assign ( { } , credentialCreateOptions , {
330
+ challenge,
331
+ user,
332
+ } ) ;
333
+ }
334
+
335
+ /**
336
+ * Transforms the binary data in the credential into base64 strings
337
+ * for posting to the server.
338
+ * @param {PublicKeyCredential } newAssertion
339
+ */
340
+ transformNewAssertionForServer ( newAssertion : PublicKeyCredential ) : Assertion {
341
+ const attObj = new Uint8Array (
342
+ ( newAssertion . response as AuthenticatorAttestationResponse ) . attestationObject ,
343
+ ) ;
344
+ const clientDataJSON = new Uint8Array ( newAssertion . response . clientDataJSON ) ;
345
+ const rawId = new Uint8Array ( newAssertion . rawId ) ;
346
+
347
+ const registrationClientExtensions = newAssertion . getClientExtensionResults ( ) ;
348
+ return {
349
+ id : newAssertion . id ,
350
+ rawId : this . b64enc ( rawId ) ,
351
+ type : newAssertion . type ,
352
+ registrationClientExtensions : JSON . stringify ( registrationClientExtensions ) ,
353
+ response : {
354
+ clientDataJSON : this . b64enc ( clientDataJSON ) ,
355
+ attestationObject : this . b64enc ( attObj ) ,
356
+ } ,
357
+ } ;
358
+ }
359
+
360
+ transformCredentialRequestOptions (
361
+ credentialRequestOptions : PublicKeyCredentialRequestOptions ,
362
+ ) : PublicKeyCredentialRequestOptions {
363
+ const challenge = this . u8arr ( credentialRequestOptions . challenge . toString ( ) ) ;
364
+
365
+ const allowCredentials = ( credentialRequestOptions . allowCredentials || [ ] ) . map (
366
+ ( credentialDescriptor ) => {
367
+ const id = this . u8arr ( credentialDescriptor . id . toString ( ) ) ;
368
+ return Object . assign ( { } , credentialDescriptor , { id } ) ;
369
+ } ,
370
+ ) ;
371
+
372
+ return Object . assign ( { } , credentialRequestOptions , {
373
+ challenge,
374
+ allowCredentials,
375
+ } ) ;
376
+ }
377
+
378
+ /**
379
+ * Encodes the binary data in the assertion into strings for posting to the server.
380
+ * @param {PublicKeyCredential } newAssertion
381
+ */
382
+ transformAssertionForServer ( newAssertion : PublicKeyCredential ) : AuthAssertion {
383
+ const response = newAssertion . response as AuthenticatorAssertionResponse ;
384
+ const authData = new Uint8Array ( response . authenticatorData ) ;
385
+ const clientDataJSON = new Uint8Array ( response . clientDataJSON ) ;
386
+ const rawId = new Uint8Array ( newAssertion . rawId ) ;
387
+ const sig = new Uint8Array ( response . signature ) ;
388
+ const assertionClientExtensions = newAssertion . getClientExtensionResults ( ) ;
389
+
390
+ return {
391
+ id : newAssertion . id ,
392
+ rawId : this . b64enc ( rawId ) ,
393
+ type : newAssertion . type ,
394
+ assertionClientExtensions : JSON . stringify ( assertionClientExtensions ) ,
395
+
396
+ response : {
397
+ clientDataJSON : this . b64RawEnc ( clientDataJSON ) ,
398
+ signature : this . b64RawEnc ( sig ) ,
399
+ authenticatorData : this . b64RawEnc ( authData ) ,
400
+ userHandle : null ,
401
+ } ,
402
+ } ;
403
+ }
404
+
275
405
render ( ) {
276
406
if ( this . challenge . deviceChallenges . length === 1 ) {
277
407
this . deviceChallenge = this . challenge . deviceChallenges [ 0 ] ;
@@ -375,18 +505,24 @@ class AuthenticatorValidateStage extends Stage<AuthenticatorValidationChallenge>
375
505
` ) ;
376
506
navigator . credentials
377
507
. get ( {
378
- publicKey : PublicKeyCredential . parseRequestOptionsFromJSON (
379
- this . deviceChallenge ?. challenge as PublicKeyCredentialRequestOptionsJSON ,
508
+ publicKey : this . transformCredentialRequestOptions (
509
+ this . deviceChallenge ?. challenge as PublicKeyCredentialRequestOptions ,
380
510
) ,
381
511
} )
382
512
. then ( ( assertion ) => {
383
513
if ( ! assertion ) {
384
514
throw new Error ( "No assertion" ) ;
385
515
}
386
516
try {
517
+ // we now have an authentication assertion! encode the byte arrays contained
518
+ // in the assertion data as strings for posting to the server
519
+ const transformedAssertionForServer = this . transformAssertionForServer (
520
+ assertion as PublicKeyCredential ,
521
+ ) ;
522
+
387
523
// post the assertion to the server for verification.
388
524
this . executor . submit ( {
389
- webauthn : ( assertion as PublicKeyCredential ) . toJSON ( ) ,
525
+ webauthn : transformedAssertionForServer ,
390
526
} ) ;
391
527
} catch ( err ) {
392
528
throw new Error ( `Error when validating assertion on server: ${ err } ` ) ;
0 commit comments