@@ -69,9 +69,7 @@ public async Task Assertion_Fails_WhenCredentialIdIsNotBase64UrlEncoded()
6969 test . CredentialJson . TransformAsJsonObject ( credentialJson =>
7070 {
7171 var base64UrlCredentialId = ( string ) credentialJson [ "id" ] ! ;
72- var rawCredentialId = Base64Url . DecodeFromChars ( base64UrlCredentialId ) ;
73- var base64CredentialId = Convert . ToBase64String ( rawCredentialId ) + "==" ;
74- credentialJson [ "id" ] = base64CredentialId ;
72+ credentialJson [ "id" ] = GetInvalidBase64UrlValue ( base64UrlCredentialId ) ;
7573 } ) ;
7674
7775 var result = await test . RunAsync ( ) ;
@@ -164,6 +162,58 @@ public async Task Assertion_Fails_WhenCredentialResponseIsNotAnObject(string jso
164162 Assert . StartsWith ( "The assertion credential JSON had an invalid format" , result . Failure . Message ) ;
165163 }
166164
165+ [ Fact ]
166+ public async Task Assertion_Fails_WhenOriginalOptionsChallengeIsMissing ( )
167+ {
168+ var test = new AssertionTest ( ) ;
169+ test . OriginalOptionsJson . TransformAsJsonObject ( originalOptionsJson =>
170+ {
171+ Assert . True ( originalOptionsJson . Remove ( "challenge" ) ) ;
172+ } ) ;
173+
174+ var result = await test . RunAsync ( ) ;
175+
176+ Assert . False ( result . Succeeded ) ;
177+
178+ Assert . StartsWith ( "The original passkey request options had an invalid format" , result . Failure . Message ) ;
179+ Assert . Contains ( "was missing required properties including: 'challenge'" , result . Failure . Message ) ;
180+ }
181+
182+ [ Fact ]
183+ public async Task Assertion_Fails_WhenOriginalOptionsChallengeIsNotBase64UrlEncoded ( )
184+ {
185+ var test = new AssertionTest ( ) ;
186+ test . OriginalOptionsJson . TransformAsJsonObject ( originalOptionsJson =>
187+ {
188+ var base64UrlChallenge = ( string ) originalOptionsJson [ "challenge" ] ! ;
189+ originalOptionsJson [ "challenge" ] = GetInvalidBase64UrlValue ( base64UrlChallenge ) ;
190+ } ) ;
191+
192+ var result = await test . RunAsync ( ) ;
193+
194+ Assert . False ( result . Succeeded ) ;
195+ Assert . StartsWith ( "The original passkey request options had an invalid format" , result . Failure . Message ) ;
196+ Assert . Contains ( "base64url string" , result . Failure . Message ) ;
197+ }
198+
199+ [ Theory ]
200+ [ InlineData ( "42" ) ]
201+ [ InlineData ( "null" ) ]
202+ [ InlineData ( "{}" ) ]
203+ public async Task Assertion_Fails_WhenOriginalOptionsChallengeIsNotString ( string jsonValue )
204+ {
205+ var test = new AssertionTest ( ) ;
206+ test . OriginalOptionsJson . TransformAsJsonObject ( originalOptionsJson =>
207+ {
208+ originalOptionsJson [ "challenge" ] = JsonNode . Parse ( jsonValue ) ;
209+ } ) ;
210+
211+ var result = await test . RunAsync ( ) ;
212+
213+ Assert . False ( result . Succeeded ) ;
214+ Assert . StartsWith ( "The original passkey request options had an invalid format" , result . Failure . Message ) ;
215+ }
216+
167217 [ Fact ]
168218 public async Task Assertion_Fails_WhenClientDataJsonIsMissing ( )
169219 {
@@ -256,9 +306,7 @@ public async Task Assertion_Fails_WhenAuthenticatorDataIsNotBase64UrlEncoded()
256306 test . CredentialJson . TransformAsJsonObject ( credentialJson =>
257307 {
258308 var base64UrlAuthenticatorData = ( string ) credentialJson [ "response" ] ! [ "authenticatorData" ] ! ;
259- var rawAuthenticatorData = Base64Url . DecodeFromChars ( base64UrlAuthenticatorData ) ;
260- var base64AuthenticatorData = Convert . ToBase64String ( rawAuthenticatorData ) + "==" ;
261- credentialJson [ "response" ] ! [ "authenticatorData" ] = base64AuthenticatorData ;
309+ credentialJson [ "response" ] ! [ "authenticatorData" ] = GetInvalidBase64UrlValue ( base64UrlAuthenticatorData ) ;
262310 } ) ;
263311
264312 var result = await test . RunAsync ( ) ;
@@ -325,9 +373,7 @@ public async Task Assertion_Fails_WhenResponseSignatureIsNotBase64UrlEncoded()
325373 test . CredentialJson . TransformAsJsonObject ( credentialJson =>
326374 {
327375 var base64UrlSignature = ( string ) credentialJson [ "response" ] ! [ "signature" ] ! ;
328- var rawSignature = Base64Url . DecodeFromChars ( base64UrlSignature ) ;
329- var base64Signature = Convert . ToBase64String ( rawSignature ) + "==" ;
330- credentialJson [ "response" ] ! [ "signature" ] = base64Signature ;
376+ credentialJson [ "response" ] ! [ "signature" ] = GetInvalidBase64UrlValue ( base64UrlSignature ) ;
331377 } ) ;
332378
333379 var result = await test . RunAsync ( ) ;
@@ -401,6 +447,25 @@ public async Task Assertion_Fails_WhenResponseUserHandleIsNull()
401447 Assert . StartsWith ( "The authenticator response was missing a user handle" , result . Failure . Message ) ;
402448 }
403449
450+ [ Fact ]
451+ public async Task Assertion_Fails_WhenResponseUserHandleDoesNotMatchUserId ( )
452+ {
453+ var test = new AssertionTest
454+ {
455+ IsUserIdentified = true ,
456+ } ;
457+ test . CredentialJson . TransformAsJsonObject ( credentialJson =>
458+ {
459+ var newUserId = test . User . Id [ ..^ 1 ] ;
460+ credentialJson [ "response" ] ! [ "userHandle" ] = Base64Url . EncodeToString ( Encoding . UTF8 . GetBytes ( newUserId ) ) ;
461+ } ) ;
462+
463+ var result = await test . RunAsync ( ) ;
464+
465+ Assert . False ( result . Succeeded ) ;
466+ Assert . StartsWith ( "The provided user handle" , result . Failure . Message ) ;
467+ }
468+
404469 [ Fact ]
405470 public async Task Assertion_Fails_WhenClientDataJsonTypeIsMissing ( )
406471 {
@@ -509,9 +574,7 @@ public async Task Assertion_Fails_WhenClientDataJsonChallengeIsNotBase64UrlEncod
509574 test . ClientDataJson . TransformAsJsonObject ( clientDataJson =>
510575 {
511576 var base64UrlChallenge = ( string ) clientDataJson [ "challenge" ] ! ;
512- var rawChallenge = Base64Url . DecodeFromChars ( base64UrlChallenge ) ;
513- var base64Challenge = Convert . ToBase64String ( rawChallenge ) + "==" ;
514- clientDataJson [ "challenge" ] = base64Challenge ;
577+ clientDataJson [ "challenge" ] = GetInvalidBase64UrlValue ( base64UrlChallenge ) ;
515578 } ) ;
516579
517580 var result = await test . RunAsync ( ) ;
@@ -803,7 +866,7 @@ public async Task Assertion_Succeeds_WhenSignCountIsZero()
803866 var test = new AssertionTest ( ) ;
804867 test . AuthenticatorDataArgs . Transform ( args => args with
805868 {
806- SignCount = 0 , // Normally 1
869+ SignCount = 0 , // Usually 1 by default
807870 } ) ;
808871
809872 var result = await test . RunAsync ( ) ;
@@ -1057,6 +1120,53 @@ public async Task Assertion_Fails_WhenAuthenticatorDataIsBackupEligibleButStored
10571120 result . Failure . Message ) ;
10581121 }
10591122
1123+ [ Fact ]
1124+ public async Task Assertion_Fails_WhenProvidedCredentialIsNotInAllowedCredentials ( )
1125+ {
1126+ var test = new AssertionTest ( ) ;
1127+ var allowedCredentialId = test . CredentialId . ToArray ( ) ;
1128+ allowedCredentialId [ 0 ] ++ ;
1129+ test . AddAllowedCredential ( allowedCredentialId ) ;
1130+
1131+ var result = await test . RunAsync ( ) ;
1132+
1133+ Assert . False ( result . Succeeded ) ;
1134+ Assert . StartsWith (
1135+ "The provided credential ID was not in the list of allowed credentials" ,
1136+ result . Failure . Message ) ;
1137+ }
1138+
1139+ [ Fact ]
1140+ public async Task Assertion_Succeeds_WhenProvidedCredentialIsInAllowedCredentials ( )
1141+ {
1142+ var test = new AssertionTest ( ) ;
1143+ var otherAllowedCredentialId = test . CredentialId . ToArray ( ) ;
1144+ otherAllowedCredentialId [ 0 ] ++ ;
1145+ test . AddAllowedCredential ( test . CredentialId ) ;
1146+ test . AddAllowedCredential ( otherAllowedCredentialId ) ;
1147+
1148+ var result = await test . RunAsync ( ) ;
1149+
1150+ Assert . True ( result . Succeeded ) ;
1151+ }
1152+
1153+ [ Theory ]
1154+ [ InlineData ( false ) ]
1155+ [ InlineData ( true ) ]
1156+ public async Task Assertion_Fails_WhenCredentialDoesNotExistOnTheUser ( bool isUserIdentified )
1157+ {
1158+ var test = new AssertionTest
1159+ {
1160+ IsUserIdentified = isUserIdentified ,
1161+ DoesCredentialExistOnUser = false
1162+ } ;
1163+
1164+ var result = await test . RunAsync ( ) ;
1165+
1166+ Assert . False ( result . Succeeded ) ;
1167+ Assert . StartsWith ( "The provided credential does not belong to the specified user" , result . Failure . Message ) ;
1168+ }
1169+
10601170 private sealed class AssertionTest : PasskeyTestBase < PasskeyAssertionResult < PocoUser > >
10611171 {
10621172 private static readonly byte [ ] _defaultChallenge = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ] ;
@@ -1075,6 +1185,7 @@ private sealed class AssertionTest : PasskeyTestBase<PasskeyAssertionResult<Poco
10751185 public bool IsUserIdentified { get ; set ; }
10761186 public bool IsStoredPasskeyBackupEligible { get ; set ; }
10771187 public bool IsStoredPasskeyBackedUp { get ; set ; }
1188+ public bool DoesCredentialExistOnUser { get ; set ; } = true ;
10781189 public COSEAlgorithmIdentifier Algorithm { get ; set ; } = COSEAlgorithmIdentifier . ES256 ;
10791190 public ReadOnlyMemory < byte > Challenge { get ; set ; } = _defaultChallenge ;
10801191 public ReadOnlyMemory < byte > CredentialId { get ; set ; } = _defaultCredentialId ;
@@ -1087,11 +1198,11 @@ private sealed class AssertionTest : PasskeyTestBase<PasskeyAssertionResult<Poco
10871198 public ComputedJsonObject CredentialJson { get ; } = new ( ) ;
10881199 public ComputedValue < UserPasskeyInfo > StoredPasskey { get ; } = new ( ) ;
10891200
1090- public void AddAllowCredentials ( string userId )
1201+ public void AddAllowedCredential ( ReadOnlyMemory < byte > credentialId )
10911202 {
10921203 _allowCredentials . Add ( new ( )
10931204 {
1094- Id = BufferSource . FromString ( userId ) ,
1205+ Id = BufferSource . FromBytes ( credentialId ) ,
10951206 Type = "public-key" ,
10961207 Transports = [ "internal" ] ,
10971208 } ) ;
@@ -1119,6 +1230,7 @@ protected override async Task<PasskeyAssertionResult<PocoUser>> RunCoreAsync()
11191230 {
11201231 RpIdHash = SHA256 . HashData ( Encoding . UTF8 . GetBytes ( RpId ?? string . Empty ) ) ,
11211232 Flags = AuthenticatorDataFlags . UserPresent ,
1233+ SignCount = 1 ,
11221234 } ) ;
11231235 var authenticatorData = AuthenticatorData . Compute ( MakeAuthenticatorData ( authenticatorDataArgs ) ) ;
11241236 var clientDataJson = ClientDataJson . Compute ( $$ """
@@ -1171,7 +1283,7 @@ protected override async Task<PasskeyAssertionResult<PocoUser>> RunCoreAsync()
11711283 userManager
11721284 . Setup ( m => m . GetPasskeyAsync ( It . IsAny < PocoUser > ( ) , It . IsAny < byte [ ] > ( ) ) )
11731285 . Returns ( ( PocoUser user , byte [ ] credentialId ) => Task . FromResult (
1174- user == User && CredentialId . Span . SequenceEqual ( credentialId )
1286+ DoesCredentialExistOnUser && user == User && CredentialId . Span . SequenceEqual ( credentialId )
11751287 ? storedPasskey
11761288 : null ) ) ;
11771289
0 commit comments