@@ -512,6 +512,148 @@ describe('v2/view-builder/views/webauthn/EnrollWebauthnView', function() {
512512 . catch ( done . fail ) ;
513513 } ) ;
514514
515+ // OKTA-1184438: okta-core emits profile.transports as a comma separated string
516+ // so the entire profile serializes as Map<String,String>; tolerate both shapes.
517+ it ( 'excludeCredentials parses comma separated string profile.transports' , function ( done ) {
518+ const newCredential = {
519+ response : {
520+ clientDataJSON : 123 ,
521+ attestationObject : 234 ,
522+ getTransports : function ( ) { return [ 'internal' ] ; } ,
523+ } ,
524+ getClientExtensionResults : function ( ) { return { } ; } ,
525+ } ;
526+ spyOn ( webauthn , 'isNewApiAvailable' ) . and . callFake ( ( ) => true ) ;
527+ spyOn ( navigator . credentials , 'create' ) . and . returnValue ( Promise . resolve ( newCredential ) ) ;
528+ spyOn ( BaseForm . prototype , 'saveForm' ) ;
529+
530+ const enrollmentsWithStringTransports = {
531+ value : [
532+ {
533+ displayName : 'Touch ID' ,
534+ type : 'security_key' ,
535+ key : 'webauthn' ,
536+ id : 'autwa6eD9o02iBbtv0g3' ,
537+ authenticatorId : 'fwftheidkwh282hv8g3' ,
538+ credentialId : '7Ag2iWUqfz0SanWDj-ZZ2fpDsgiEDt_08O1VSSRZHpgkUS1zhLSyWYDrxXXB5VE_w1iiqSvPaRgXcmG5rPwB-w' ,
539+ profile : { transports : 'usb,nfc' } ,
540+ } ,
541+ ] ,
542+ } ;
543+
544+ testContext . init ( EnrollWebauthnResponse . currentAuthenticator . value , enrollmentsWithStringTransports ) ;
545+ testContext . view . $ ( '.webauthn-setup' ) . click ( ) ;
546+
547+ Expect . waitForSpyCall ( testContext . view . form . saveForm )
548+ . then ( ( ) => {
549+ const calledWith = navigator . credentials . create . calls . mostRecent ( ) . args [ 0 ] ;
550+ expect ( calledWith . publicKey . excludeCredentials ) . toEqual ( [
551+ {
552+ type : 'public-key' ,
553+ id : CryptoUtil . strToBin (
554+ '7Ag2iWUqfz0SanWDj-ZZ2fpDsgiEDt_08O1VSSRZHpgkUS1zhLSyWYDrxXXB5VE_w1iiqSvPaRgXcmG5rPwB-w'
555+ ) ,
556+ transports : [ 'usb' , 'nfc' ] ,
557+ } ,
558+ ] ) ;
559+ done ( ) ;
560+ } )
561+ . catch ( done . fail ) ;
562+ } ) ;
563+
564+ it ( 'excludeCredentials parses single string profile.transports' , function ( done ) {
565+ const newCredential = {
566+ response : {
567+ clientDataJSON : 123 ,
568+ attestationObject : 234 ,
569+ getTransports : function ( ) { return [ 'internal' ] ; } ,
570+ } ,
571+ getClientExtensionResults : function ( ) { return { } ; } ,
572+ } ;
573+ spyOn ( webauthn , 'isNewApiAvailable' ) . and . callFake ( ( ) => true ) ;
574+ spyOn ( navigator . credentials , 'create' ) . and . returnValue ( Promise . resolve ( newCredential ) ) ;
575+ spyOn ( BaseForm . prototype , 'saveForm' ) ;
576+
577+ const enrollmentsWithStringTransports = {
578+ value : [
579+ {
580+ displayName : 'Touch ID' ,
581+ type : 'security_key' ,
582+ key : 'webauthn' ,
583+ id : 'autwa6eD9o02iBbtv0g3' ,
584+ authenticatorId : 'fwftheidkwh282hv8g3' ,
585+ credentialId : '7Ag2iWUqfz0SanWDj-ZZ2fpDsgiEDt_08O1VSSRZHpgkUS1zhLSyWYDrxXXB5VE_w1iiqSvPaRgXcmG5rPwB-w' ,
586+ profile : { transports : 'usb' } ,
587+ } ,
588+ ] ,
589+ } ;
590+
591+ testContext . init ( EnrollWebauthnResponse . currentAuthenticator . value , enrollmentsWithStringTransports ) ;
592+ testContext . view . $ ( '.webauthn-setup' ) . click ( ) ;
593+
594+ Expect . waitForSpyCall ( testContext . view . form . saveForm )
595+ . then ( ( ) => {
596+ const calledWith = navigator . credentials . create . calls . mostRecent ( ) . args [ 0 ] ;
597+ expect ( calledWith . publicKey . excludeCredentials ) . toEqual ( [
598+ {
599+ type : 'public-key' ,
600+ id : CryptoUtil . strToBin (
601+ '7Ag2iWUqfz0SanWDj-ZZ2fpDsgiEDt_08O1VSSRZHpgkUS1zhLSyWYDrxXXB5VE_w1iiqSvPaRgXcmG5rPwB-w'
602+ ) ,
603+ transports : [ 'usb' ] ,
604+ } ,
605+ ] ) ;
606+ done ( ) ;
607+ } )
608+ . catch ( done . fail ) ;
609+ } ) ;
610+
611+ it ( 'excludeCredentials omits transports when profile.transports is not a valid string' , function ( done ) {
612+ const newCredential = {
613+ response : {
614+ clientDataJSON : 123 ,
615+ attestationObject : 234 ,
616+ getTransports : function ( ) { return [ 'internal' ] ; } ,
617+ } ,
618+ getClientExtensionResults : function ( ) { return { } ; } ,
619+ } ;
620+ spyOn ( webauthn , 'isNewApiAvailable' ) . and . callFake ( ( ) => true ) ;
621+ spyOn ( navigator . credentials , 'create' ) . and . returnValue ( Promise . resolve ( newCredential ) ) ;
622+ spyOn ( BaseForm . prototype , 'saveForm' ) ;
623+
624+ const enrollmentsWithBadTransports = {
625+ value : [
626+ {
627+ displayName : 'Touch ID' ,
628+ type : 'security_key' ,
629+ key : 'webauthn' ,
630+ id : 'autwa6eD9o02iBbtv0g3' ,
631+ authenticatorId : 'fwftheidkwh282hv8g3' ,
632+ credentialId : '7Ag2iWUqfz0SanWDj-ZZ2fpDsgiEDt_08O1VSSRZHpgkUS1zhLSyWYDrxXXB5VE_w1iiqSvPaRgXcmG5rPwB-w' ,
633+ profile : { transports : 123 } ,
634+ } ,
635+ ] ,
636+ } ;
637+
638+ testContext . init ( EnrollWebauthnResponse . currentAuthenticator . value , enrollmentsWithBadTransports ) ;
639+ testContext . view . $ ( '.webauthn-setup' ) . click ( ) ;
640+
641+ Expect . waitForSpyCall ( testContext . view . form . saveForm )
642+ . then ( ( ) => {
643+ const calledWith = navigator . credentials . create . calls . mostRecent ( ) . args [ 0 ] ;
644+ expect ( calledWith . publicKey . excludeCredentials ) . toEqual ( [
645+ {
646+ type : 'public-key' ,
647+ id : CryptoUtil . strToBin (
648+ '7Ag2iWUqfz0SanWDj-ZZ2fpDsgiEDt_08O1VSSRZHpgkUS1zhLSyWYDrxXXB5VE_w1iiqSvPaRgXcmG5rPwB-w'
649+ ) ,
650+ } ,
651+ ] ) ;
652+ done ( ) ;
653+ } )
654+ . catch ( done . fail ) ;
655+ } ) ;
656+
515657 it ( 'credentials.get should be called with empty excludeCredentials when requreResidentKey=true' , function ( done ) {
516658 const newCredential = {
517659 response : {
0 commit comments