Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PAS-556 | Add support for credential hints to the register flow #670

Merged
merged 4 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/AdminConsole/AdminConsole.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.FeatureManagement.AspNetCore" Version="3.5.0" />
<PackageReference Include="Passwordless.AspNetCore" Version="2.0.0-beta10" />
<PackageReference Include="Passwordless.AspNetCore" Version="2.1.0-beta.1" />
<PackageReference Include="Stripe.net" Version="41.*" />
</ItemGroup>

Expand Down
10 changes: 8 additions & 2 deletions src/AdminConsole/Pages/App/Playground/NewAccount.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
var requestToken = Antiforgery.GetAndStoreTokens(Model.HttpContext).RequestToken;
}


<div>
<div class="flex flex-1 flex-col justify-center lg:flex-non bg-gray-200 rounded-md p-6 max-w-fit">
<div class="w-full max-w-sm lg:w-96">
Expand Down Expand Up @@ -85,6 +84,13 @@
}
</div>

<div>
<label for="hints" class="block text-sm font-medium leading-6 text-gray-900">Credential hints (optional)</label>
<div class="mt-2">
<input asp-for="Hints" type="text" name="hints" id="hints" class="text-input">
<span asp-validation-for="Hints"></span>
</div>
</div>

<div>
<button id="register-btn" class="btn-primary" type="button">Register</button>
Expand Down Expand Up @@ -127,7 +133,7 @@ const createNewAccount = async (e) => {
if (req.ok) {
const { token } = await req.json();
const nicknameForDevice = data.get("nickname");
const { error } = await p.register(token , nicknameForDevice);
const { error } = await p.register(token, nicknameForDevice);

if (error) {
console.error(error);
Expand Down
34 changes: 12 additions & 22 deletions src/AdminConsole/Pages/App/Playground/NewAccount.cshtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,28 @@

namespace Passwordless.AdminConsole.Pages.App.Playground;

public class NewAccountModel : PageModel
public class NewAccountModel(IScopedPasswordlessClient passwordlessClient)
: PageModel
{
private readonly ILogger<IndexModel> _logger;
private readonly IScopedPasswordlessClient _passwordlessClient;
[MaxLength(64)]
public string Nickname { get; set; } = "";

public NewAccountModel(ILogger<IndexModel> logger, IScopedPasswordlessClient passwordlessClient)
{
_logger = logger;
this._passwordlessClient = passwordlessClient;
}
public string Attestation { get; set; } = "none";

public void OnGet()
{
public string Hints { get; set; } = "";

}

public async Task<IActionResult> OnPostToken(string name, string email, string attestation)
public async Task<IActionResult> OnPostToken(string name, string email, string attestation, string hints)
{
try
{
var userId = Guid.NewGuid().ToString();
var token = await _passwordlessClient.CreateRegisterTokenAsync(new RegisterOptions(userId, $"Playground: {email}")
var token = await passwordlessClient.CreateRegisterTokenAsync(new RegisterOptions(userId, $"Playground: {email}")
{
DisplayName = name,
Aliases = new HashSet<string>(1) { email },
Aliases = [email],
AliasHashing = false,
Attestation = attestation
Attestation = attestation,
Hints = hints.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
});

return new JsonResult(token);
Expand All @@ -44,12 +39,7 @@ public async Task<IActionResult> OnPostToken(string name, string email, string a

public async Task<IActionResult> OnPost(string token)
{
var res = await _passwordlessClient.VerifyAuthenticationTokenAsync(token);
var res = await passwordlessClient.VerifyAuthenticationTokenAsync(token);
return new JsonResult(res);
}

[MaxLength(64)]
public string Nickname { get; set; }

public string Attestation { get; set; } = "none";
}
4 changes: 3 additions & 1 deletion src/Service/Fido2Service.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public async Task<string> CreateRegisterTokenAsync(RegisterToken tokenProps)
if (string.IsNullOrEmpty(tokenProps.Attestation)) tokenProps.Attestation = "none";
TokenValidator.ValidateAttestation(tokenProps, features);

// check if aliases is available
// Check if aliases is available
if (tokenProps.Aliases != null)
{
var hashedAliases = tokenProps.Aliases.Select(alias => HashAlias(alias, _tenantProvider.Tenant));
Expand Down Expand Up @@ -152,6 +152,8 @@ public async Task<SessionResponse<CredentialCreateOptions>> RegisterBeginAsync(F
CredProps = true
});

options.Hints = token.Hints;

var session = await _tokenService.EncodeTokenAsync(
new RegisterSession
{
Expand Down
14 changes: 12 additions & 2 deletions src/Service/Models/RegisterToken.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Fido2NetLib.Objects;
using MessagePack;
using Passwordless.Common.Validation;

Expand All @@ -20,17 +21,26 @@ public class RegisterToken : Token

[MessagePack.Key(13)]
public string Attestation { get; set; } = "None";

[MessagePack.Key(14)]
public string AuthenticatorType { get; set; }

[MessagePack.Key(15)]
public bool Discoverable { get; set; } = true;

[MessagePack.Key(16)]
public string UserVerification { get; set; } = "Preferred";

[MessagePack.Key(17)]
[MaxLength(10), MaxLengthCollection(250), RequiredCollection(AllowEmptyStrings = false)]
public HashSet<string> Aliases { get; set; }
public HashSet<string>? Aliases { get; set; }

[MessagePack.Key(18)] public bool AliasHashing { get; set; } = true;
[MessagePack.Key(18)]
public bool AliasHashing { get; set; } = true;

[MessagePack.Key(19)]
[MaxLength(3)]
public IReadOnlyList<PublicKeyCredentialHint> Hints { get; set; } = [];
}

[MessagePackObject]
Expand Down
22 changes: 8 additions & 14 deletions src/Service/TokenService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,31 +149,25 @@ private async Task<Tuple<Key, int>> GetRandomKeyAsync()

public async Task<string> EncodeTokenAsync<T>(T token, string prefix, bool contractless = false)
{
byte[] msgpack;
if (contractless)
{
msgpack = MessagePackSerializer.Serialize(token, ContractlessStandardResolver.Options);
}
else
{
msgpack = MessagePackSerializer.Serialize(token);
}
var msgpack = contractless
? MessagePackSerializer.Serialize(token, ContractlessStandardResolver.Options)
: MessagePackSerializer.Serialize(token);

(Key key, int keyId) = await GetRandomKeyAsync();
(Key key, var keyId) = await GetRandomKeyAsync();

_log.LogInformation("Encoding using keyId={keyId}", keyId);
var mac = CreateMac(key, msgpack);

var envelope = new MacEnvelope { Mac = mac, Token = msgpack, KeyId = keyId };
var envelop_binary = MessagePackSerializer.Serialize(envelope);
var envelop_binary_b64 = Base64Url.Encode(envelop_binary);
var envelopeBinary = MessagePackSerializer.Serialize(envelope);
var envelopeBinaryB64 = Base64Url.Encode(envelopeBinary);

if (!string.IsNullOrEmpty(prefix))
{
return prefix + envelop_binary_b64;
return prefix + envelopeBinaryB64;
}

return envelop_binary_b64;
return envelopeBinaryB64;
}

/// <summary>
Expand Down
16 changes: 14 additions & 2 deletions tests/Service.Tests/Implementations/Fido2ServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public async Task CreateRegisterToken_Throws_ApiException_WhenMaxUsersExceededFo
.ReturnsAsync("test_token");
_mockFeatureContextProvider.Setup(x => x.UseContext()).ReturnsAsync(new FeaturesContext(false, 0, null, 10000, false, true, true));
_mockTenantStorage.Setup(x => x.GetUsersCount()).ReturnsAsync(10000);
_mockTenantStorage.Setup(x => x.GetCredentialsByUserIdAsync(It.Is<string>(p => p == "test"))).ReturnsAsync(new List<StoredCredential>(0));
_mockTenantStorage.Setup(x => x.GetCredentialsByUserIdAsync(It.Is<string>(p => p == "test"))).ReturnsAsync([]);

// act
var actual = await Assert.ThrowsAsync<ApiException>(async () =>
Expand Down Expand Up @@ -104,7 +104,19 @@ public async Task CreateRegisterToken_Works_WhenMaxUsersExceededForExistingUser(
_mockFeatureContextProvider.Setup(x => x.UseContext()).ReturnsAsync(new FeaturesContext(false, 0, null, 10000, false, true, true));
_mockTenantStorage.Setup(x => x.GetUsersCount()).ReturnsAsync(10000);
_mockTenantStorage.Setup(x => x.GetCredentialsByUserIdAsync(It.Is<string>(p => p == "test"))).ReturnsAsync(
new List<StoredCredential>(1) { new() { UserHandle = "test"u8.ToArray(), Descriptor = null!, Origin = null!, AttestationFmt = null!, CreatedAt = DateTime.UtcNow, PublicKey = null!, SignatureCounter = 123, RPID = null! } });
[
new StoredCredential
{
UserHandle = "test"u8.ToArray(),
Descriptor = null!,
Origin = null!,
AttestationFmt = null!,
CreatedAt = DateTime.UtcNow,
PublicKey = null!,
SignatureCounter = 123,
RPID = null!
}
]);

// act
var actual = await _sut.CreateRegisterTokenAsync(new RegisterToken
Expand Down
Loading