aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-06-04 11:47:20 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2024-06-04 11:47:20 -0400
commit1da3e983599b81dc9587f060131385a9a63ef1c6 (patch)
tree7fe25c46d0b53fbf18c4eb5be2dc614339c2611c
parente8548467d945ccb286da595a02c816abb596439d (diff)
parent451091e93b5feee7a5e01d3a81f5d63efa7ea8be (diff)
save latest dev updates
-rw-r--r--lib/vnlib.browser/src/mfa/fido.ts20
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/FidoEndpoint.cs53
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/CoseEncodings.cs11
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/FidoAuthenticatorResponse.cs2
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/FidoDecoder.cs87
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/FidoDeviceCredential.cs2
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/UserFidoMfaExtensions.cs2
7 files changed, 161 insertions, 16 deletions
diff --git a/lib/vnlib.browser/src/mfa/fido.ts b/lib/vnlib.browser/src/mfa/fido.ts
index 5eaa166..8d58616 100644
--- a/lib/vnlib.browser/src/mfa/fido.ts
+++ b/lib/vnlib.browser/src/mfa/fido.ts
@@ -46,6 +46,7 @@ export interface IFidoDevice{
interface FidoRegistration{
readonly id: string;
+ readonly friendlyName: string;
readonly publicKey?: string;
readonly publicKeyAlgorithm: number;
readonly clientDataJSON: string;
@@ -114,18 +115,19 @@ export const useFidoApi = (endpoint: MaybeRef<string>, axiosConfig?: MaybeRef<Ax
return data.getResultOrThrow();
}
- const registerCredential = async (registration: RegistrationResponseJSON, commonName: string): Promise<WebMessage> => {
+ const registerCredential = async (reg: RegistrationResponseJSON, commonName: string): Promise<WebMessage> => {
- const response: FidoRegistration = {
- id: registration.id,
- publicKey: registration.response.publicKey,
- publicKeyAlgorithm: registration.response.publicKeyAlgorithm!,
- clientDataJSON: registration.response.clientDataJSON,
- authenticatorData: registration.response.authenticatorData,
- attestationObject: registration.response.attestationObject
+ const registration: FidoRegistration = {
+ id: reg.id,
+ publicKey: reg.response.publicKey,
+ publicKeyAlgorithm: reg.response.publicKeyAlgorithm!,
+ clientDataJSON: reg.response.clientDataJSON,
+ authenticatorData: reg.response.authenticatorData,
+ attestationObject: reg.response.attestationObject,
+ friendlyName: commonName
}
- const { data } = await axios.value.post<WebMessage>(ep(), { response, commonName });
+ const { data } = await axios.value.post<WebMessage>(ep(), { registration });
return data;
}
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/FidoEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/FidoEndpoint.cs
index 779d8c9..1627d8b 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/FidoEndpoint.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/FidoEndpoint.cs
@@ -31,6 +31,7 @@ using FluentValidation;
using VNLib.Utils;
using VNLib.Utils.Memory;
using VNLib.Hashing;
+using VNLib.Utils.Logging;
using VNLib.Plugins.Essentials.Users;
using VNLib.Plugins.Essentials.Endpoints;
using VNLib.Plugins.Extensions.Loading;
@@ -99,6 +100,11 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
return VirtualClose(entity, webm, HttpStatusCode.NotFound);
}
+ if(webm.Assert(user.FidoCanAddKey(), "You cannot add another key to this account. You must delete an existing one first"))
+ {
+ return VirtualOk(entity, webm);
+ }
+
//TODO: Store challenge in user session
string challenge = RandomHash.GetRandomBase64(16);
@@ -138,7 +144,12 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
return VirtualClose(entity, webm, HttpStatusCode.BadRequest);
}
- if(doc.RootElement.TryGetProperty("response", out JsonElement deviceResponse))
+ /*
+ * Handle a registration response from the client that is used to
+ * register a new credential to the user's account
+ */
+
+ if (doc.RootElement.TryGetProperty("registration", out JsonElement deviceResponse))
{
//complete registation of new device
FidoAuthenticatorResponse? res = deviceResponse.Deserialize<FidoAuthenticatorResponse>();
@@ -185,7 +196,33 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
return VirtualClose(entity, webm, HttpStatusCode.UnprocessableEntity);
}
- return VirtualOk(entity);
+ FidoDeviceCredential? cred = FidoDecoder.FromResponse(response);
+
+ if (webm.Assert(cred != null, "Your device did not send valid public key data"))
+ {
+ return VirtualClose(entity, webm, HttpStatusCode.BadRequest);
+ }
+
+ Log.Information("Adding new credential\n {cred}", cred);
+
+ using IUser? user = await _users.GetUserFromIDAsync(entity.Session.UserID, entity.EventCancellation);
+
+ if(webm.Assert(user != null, "User not found"))
+ {
+ return VirtualClose(entity, webm, HttpStatusCode.NotFound);
+ }
+
+ if (webm.Assert(user.FidoCanAddKey(), "You cannot add another key to your account, you must delete an existing one"))
+ {
+ return VirtualOk(entity, webm);
+ }
+
+ //user.FidoAddCredential(cred);
+
+ webm.Result = "Your fido device was successfully added to your account";
+ webm.Success = true;
+
+ return VirtualOk(entity, webm);
}
}
@@ -232,6 +269,12 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
.WithMessage("Fido 'device_id' must be provided")
.MaximumLength(256);
+ RuleFor(c => c.DeviceName)
+ .NotEmpty()
+ .Matches(@"^[a-zA-Z0-9\s]+$")
+ .WithMessage("Your device name contains invalid characters")
+ .MaximumLength(64);
+
RuleFor(c => c.Base64PublicKey)
.NotEmpty()
.WithMessage("Fido 'public_key' must be provided");
@@ -265,18 +308,18 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
{
RuleFor(c => c.Base64Challenge)
.NotEmpty()
- .WithMessage("Fido 'challenge' must be provided")
+ .WithMessage("Fido 'challenge' is required")
.MaximumLength(4096);
RuleFor(c => c.Origin)
.NotEmpty()
- .WithMessage("Fido 'origin' must be provided")
+ .WithMessage("Fido 'origin' is required")
.MaximumLength(1024);
RuleFor(c => c.Type)
.NotEmpty()
.WithMessage("Fido 'type' must be provided")
- .MaximumLength(64);
+ .Matches("webauthn.create");
}
}
} \ No newline at end of file
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/CoseEncodings.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/CoseEncodings.cs
index e0160e1..4b94c91 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/CoseEncodings.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/CoseEncodings.cs
@@ -58,5 +58,16 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
_ => string.Empty
};
}
+
+ public static int GetPublicKeySizeForAlg(int code)
+ {
+ return code switch
+ {
+ -7 => 64,
+ -35 => 96,
+ -36 => 132,
+ _ => -1
+ };
+ }
}
} \ No newline at end of file
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/FidoAuthenticatorResponse.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/FidoAuthenticatorResponse.cs
index 8f0ac7f..863c1e7 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/FidoAuthenticatorResponse.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/FidoAuthenticatorResponse.cs
@@ -46,5 +46,7 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA.Fido
[JsonPropertyName("attestationObject")]
public string? Base64Attestation { get; set; }
+ [JsonPropertyName("friendlyName")]
+ public string? DeviceName { get; set; }
}
}
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/FidoDecoder.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/FidoDecoder.cs
new file mode 100644
index 0000000..df54f8c
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/FidoDecoder.cs
@@ -0,0 +1,87 @@
+/*
+* Copyright (c) 2024 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Accounts
+* File: UserFidoMfaExtensions.cs
+*
+* UserFidoMfaExtensions.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Buffers.Binary;
+using System.Formats.Cbor;
+
+using VNLib.Utils;
+using VNLib.Utils.Memory;
+
+namespace VNLib.Plugins.Essentials.Accounts.MFA.Fido
+{
+ internal static class FidoDecoder
+ {
+ public static FidoDeviceCredential? FromResponse(FidoAuthenticatorResponse response)
+ {
+ //Make sure the response has a public key and a valid algorithm
+ if (!response.CoseAlgorithmNumber.HasValue || string.IsNullOrWhiteSpace(response.Base64PublicKey))
+ {
+ return null;
+ }
+
+ if(!VerifyKeySizes(response.CoseAlgorithmNumber.Value, response.Base64PublicKey))
+ {
+ return null;
+ }
+
+ return new FidoDeviceCredential
+ {
+ Base64UrlId = response.DeviceId,
+ CoseAlgId = response.CoseAlgorithmNumber.Value,
+ Base64PublicKey = response.Base64PublicKey,
+ Name = response.DeviceName ?? string.Empty
+ };
+ }
+
+
+ private static bool VerifyKeySizes(int algCode, string pubkey)
+ {
+ using UnsafeMemoryHandle<byte> binBuffer = MemoryUtil.UnsafeAlloc<byte>(pubkey.Length + 16, true);
+
+ ERRNO decoded = VnEncoding.Base64UrlDecode(pubkey, binBuffer.Span);
+
+ if(!decoded)
+ {
+ return false;
+ }
+
+ Span<byte> guid = binBuffer.AsSpan(0, 16);
+ Span<byte> lenBin = binBuffer.AsSpan(16, 2);
+
+ ushort idLen = BinaryPrimitives.ReadUInt16LittleEndian(lenBin);
+
+ //Id length is outside the size of the buffer
+ if(idLen + 18 > decoded)
+ {
+ return false;
+ }
+
+ Span<byte> key = binBuffer.AsSpan(18, idLen);
+ Span<byte> pubKey = binBuffer.AsSpan(18 + idLen); //Finally the actual public key length
+
+ return pubKey.Length == CoseEncodings.GetPublicKeySizeForAlg(algCode);
+ }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/FidoDeviceCredential.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/FidoDeviceCredential.cs
index 2d7e01e..bd87af3 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/FidoDeviceCredential.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/FidoDeviceCredential.cs
@@ -27,7 +27,7 @@ using System.Text.Json.Serialization;
namespace VNLib.Plugins.Essentials.Accounts.MFA.Fido
{
- public sealed class FidoDeviceCredential
+ public sealed record class FidoDeviceCredential
{
[JsonPropertyName("n")]
public string Name { get; set; } = string.Empty;
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/UserFidoMfaExtensions.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/UserFidoMfaExtensions.cs
index f6bb748..e0c84e0 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/UserFidoMfaExtensions.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Fido/UserFidoMfaExtensions.cs
@@ -24,7 +24,7 @@
using System;
using System.Linq;
-
+using System.Buffers;
using VNLib.Plugins.Essentials.Users;
namespace VNLib.Plugins.Essentials.Accounts.MFA.Fido