diff --git a/src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs b/src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs index bd8542e8bf..5c337cb9e3 100644 --- a/src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs +++ b/src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs @@ -26,7 +26,7 @@ public class UserDecryptionOptions : ResponseModel /// Gets or sets the WebAuthn PRF decryption keys. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public WebAuthnPrfDecryptionOption? WebAuthnPrfOption { get; set; } + public WebAuthnPrfDecryptionOption[]? WebAuthnPrfOptions { get; set; } /// /// Gets or sets information regarding this users trusted device decryption setup. @@ -45,13 +45,19 @@ public class WebAuthnPrfDecryptionOption { public string EncryptedPrivateKey { get; } public string EncryptedUserKey { get; } + public string CredentialId { get; } + public string[] Transports { get; } public WebAuthnPrfDecryptionOption( string encryptedPrivateKey, - string encryptedUserKey) + string encryptedUserKey, + string credentialId, + string[]? transports = null) { EncryptedPrivateKey = encryptedPrivateKey; EncryptedUserKey = encryptedUserKey; + CredentialId = credentialId; + Transports = transports ?? []; } } diff --git a/src/Identity/IdentityServer/IUserDecryptionOptionsBuilder.cs b/src/Identity/IdentityServer/IUserDecryptionOptionsBuilder.cs index fece7b10b4..4013b75295 100644 --- a/src/Identity/IdentityServer/IUserDecryptionOptionsBuilder.cs +++ b/src/Identity/IdentityServer/IUserDecryptionOptionsBuilder.cs @@ -11,6 +11,6 @@ public interface IUserDecryptionOptionsBuilder IUserDecryptionOptionsBuilder ForUser(User user); IUserDecryptionOptionsBuilder WithDevice(Device device); IUserDecryptionOptionsBuilder WithSso(SsoConfig ssoConfig); - IUserDecryptionOptionsBuilder WithWebAuthnLoginCredential(WebAuthnCredential credential); + IUserDecryptionOptionsBuilder WithWebAuthnLoginCredentials(IEnumerable credentials); Task BuildAsync(); } diff --git a/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs b/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs index e679c48433..4c8eafab74 100644 --- a/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs @@ -30,6 +30,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator _assertionOptionsDataProtector; private readonly IAssertWebAuthnLoginCredentialCommand _assertWebAuthnLoginCredentialCommand; private readonly IDeviceValidator _deviceValidator; + private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository; public WebAuthnGrantValidator( UserManager userManager, @@ -50,7 +51,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator "webauthn"; @@ -98,7 +101,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator return this; } - public IUserDecryptionOptionsBuilder WithWebAuthnLoginCredential(WebAuthnCredential credential) + public IUserDecryptionOptionsBuilder WithWebAuthnLoginCredentials(IEnumerable credentials) { - if (credential.GetPrfStatus() == WebAuthnPrfStatus.Enabled) + var prfEnabledCredentials = credentials + .Where(c => c.GetPrfStatus() == WebAuthnPrfStatus.Enabled) + .Select(c => new WebAuthnPrfDecryptionOption( + c.EncryptedPrivateKey, + c.EncryptedUserKey, + c.CredentialId, + [] // Stored credentials currently lack Transports, just send an empty array for now + )) + .ToArray(); + + if (prfEnabledCredentials.Length > 0) { - _options.WebAuthnPrfOption = new WebAuthnPrfDecryptionOption(credential.EncryptedPrivateKey, credential.EncryptedUserKey); + _options.WebAuthnPrfOptions = prfEnabledCredentials; } return this; } diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index 5a4f83818a..e7a0c4d3cf 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -585,7 +585,7 @@ public class BaseRequestValidatorTests _userDecryptionOptionsBuilder.ForUser(Arg.Any()).Returns(_userDecryptionOptionsBuilder); _userDecryptionOptionsBuilder.WithDevice(Arg.Any()).Returns(_userDecryptionOptionsBuilder); _userDecryptionOptionsBuilder.WithSso(Arg.Any()).Returns(_userDecryptionOptionsBuilder); - _userDecryptionOptionsBuilder.WithWebAuthnLoginCredential(Arg.Any()).Returns(_userDecryptionOptionsBuilder); + _userDecryptionOptionsBuilder.WithWebAuthnLoginCredentials(Arg.Any>()).Returns(_userDecryptionOptionsBuilder); _userDecryptionOptionsBuilder.BuildAsync().Returns(Task.FromResult(new UserDecryptionOptions { HasMasterPassword = false, @@ -626,7 +626,7 @@ public class BaseRequestValidatorTests _userDecryptionOptionsBuilder.ForUser(Arg.Any()).Returns(_userDecryptionOptionsBuilder); _userDecryptionOptionsBuilder.WithDevice(Arg.Any()).Returns(_userDecryptionOptionsBuilder); _userDecryptionOptionsBuilder.WithSso(Arg.Any()).Returns(_userDecryptionOptionsBuilder); - _userDecryptionOptionsBuilder.WithWebAuthnLoginCredential(Arg.Any()).Returns(_userDecryptionOptionsBuilder); + _userDecryptionOptionsBuilder.WithWebAuthnLoginCredentials(Arg.Any>()).Returns(_userDecryptionOptionsBuilder); _userDecryptionOptionsBuilder.BuildAsync().Returns(Task.FromResult(new UserDecryptionOptions { HasMasterPassword = true, diff --git a/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs b/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs index b44dfe8d5f..59201e014e 100644 --- a/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs +++ b/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs @@ -41,7 +41,7 @@ public class UserDecryptionOptionsBuilderTests [BitAutoData(true, false, true)] // EncryptedPrivateKey and EncryptedUserKey are non-null, EncryptedPublicKey is null [BitAutoData(true, true, false)] // EncryptedPrivateKey and EncryptedPublicKey are non-null, EncryptedUserKey is null [BitAutoData(false, true, true)] // EncryptedPublicKey and EncryptedUserKey are non-null, EncryptedPrivateKey is null - public async Task WithWebAuthnLoginCredential_VariousKeyCombinations_ShouldReturnCorrectPrfOption( + public async Task WithWebAuthnLoginCredentials_VariousKeyCombinations_ShouldReturnCorrectPrfOption( bool hasEncryptedPrivateKey, bool hasEncryptedPublicKey, bool hasEncryptedUserKey, @@ -51,17 +51,19 @@ public class UserDecryptionOptionsBuilderTests credential.EncryptedPublicKey = hasEncryptedPublicKey ? "encryptedPublicKey" : null; credential.EncryptedUserKey = hasEncryptedUserKey ? "encryptedUserKey" : null; - var result = await _builder.WithWebAuthnLoginCredential(credential).BuildAsync(); + var result = await _builder.WithWebAuthnLoginCredentials([credential]).BuildAsync(); if (credential.GetPrfStatus() == WebAuthnPrfStatus.Enabled) { - Assert.NotNull(result.WebAuthnPrfOption); - Assert.Equal(credential.EncryptedPrivateKey, result.WebAuthnPrfOption!.EncryptedPrivateKey); - Assert.Equal(credential.EncryptedUserKey, result.WebAuthnPrfOption!.EncryptedUserKey); + Assert.NotNull(result.WebAuthnPrfOptions); + Assert.Single(result.WebAuthnPrfOptions); + Assert.Equal(credential.EncryptedPrivateKey, result.WebAuthnPrfOptions![0].EncryptedPrivateKey); + Assert.Equal(credential.CredentialId, result.WebAuthnPrfOptions![0].CredentialId); + Assert.Equal(credential.EncryptedUserKey, result.WebAuthnPrfOptions![0].EncryptedUserKey); } else { - Assert.Null(result.WebAuthnPrfOption); + Assert.Null(result.WebAuthnPrfOptions); } }