Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Mediator;
using OpenAlprWebhookProcessor.Data.Repositories;
using Org.BouncyCastle.Asn1.Ocsp;
using System.Threading;
using System.Threading.Tasks;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Mediator;
using OpenAlprWebhookProcessor.Data.Repositories;
using OpenAlprWebhookProcessor.Features.Ignores.Commands.UpdateIgnore;
using System;
using System.Threading;
using System.Threading.Tasks;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Mediator;
using System;
using System.IO;

namespace OpenAlprWebhookProcessor.Features.ImageRelay.WebsocketSnapshotRelay
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ public class UpdatePlateNumberCommand : ICommand
}






Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ public async ValueTask<Unit> Handle(UpdatePlateNumberCommand request, Cancellati
}






Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Mediator;
using Microsoft.EntityFrameworkCore;
using OpenAlprWebhookProcessor.Data.Repositories;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public async Task<List<LicensePlateTrainingData>> ExtractTrainingDataAsync(
var nextTime = DateTimeOffset.FromUnixTimeMilliseconds(next.ReceivedOnEpoch).DateTime;
var hoursUntilNext = (float)(nextTime - currentTime).TotalHours;

if (hoursUntilNext > 0 && hoursUntilNext < 96) // Less than 4 days - filter out irregular visitors
if (hoursUntilNext > 0 && hoursUntilNext < 96) // Include intervals less than 4 days (96 hours)
{
var features = ExtractFeatures(current, sightings.Take(i + 1).ToList());
features.HoursUntilNextSeen = hoursUntilNext;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Mediator;
using OpenAlprWebhookProcessor.Data.Repositories;
using OpenAlprWebhookProcessor.Features.Webhooks.WebhookProcessor.OpenAlprWebsocket;
using System;
using System.Threading;
using System.Threading.Tasks;

Expand Down
99 changes: 99 additions & 0 deletions OpenAlprWebhookProcessor.Server/Features/Users/AuthController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
using OpenAlprWebhookProcessor.Features.Users.Commands.Logout;
using OpenAlprWebhookProcessor.Features.Users.Queries.GetCurrentUser;
using OpenAlprWebhookProcessor.Features.Users.Queries.CanRegister;
using OpenAlprWebhookProcessor.Features.Users.Commands.RegisterPasskey;
using OpenAlprWebhookProcessor.Features.Users.Commands.CompletePasskeyRegistration;
using OpenAlprWebhookProcessor.Features.Users.Commands.AuthenticatePasskey;
using OpenAlprWebhookProcessor.Features.Users.Commands.CompletePasskeyAuthentication;
using OpenAlprWebhookProcessor.Features.Users.Commands.DeletePasskey;
using OpenAlprWebhookProcessor.Features.Users.Queries.GetUserPasskeys;

namespace OpenAlprWebhookProcessor.Features.Users
{
Expand Down Expand Up @@ -101,5 +107,98 @@ public async Task<IActionResult> GetCurrentUser(CancellationToken cancellationTo

return Ok(user);
}

[HttpPost("passkey/register")]
public async Task<IActionResult> RegisterPasskey([FromBody] RegisterPasskeyRequest request, CancellationToken cancellationToken)
{
try
{
var command = new RegisterPasskeyCommand(User, request.Name);
var response = await _mediator.Send(command, cancellationToken);
return Ok(response);
}
catch (AppException ex)
{
return BadRequest(new { message = ex.Message });
}
}

[HttpPost("passkey/complete-registration")]
public async Task<IActionResult> CompletePasskeyRegistration([FromBody] CompletePasskeyRegistrationRequest request, CancellationToken cancellationToken)
{
try
{
var command = new CompletePasskeyRegistrationCommand(User, request.AttestationResponse, request.Name);
var response = await _mediator.Send(command, cancellationToken);
return Ok(response);
}
catch (AppException ex)
{
return BadRequest(new { message = ex.Message });
}
}

[AllowAnonymous]
[HttpPost("passkey/authenticate")]
public async Task<IActionResult> AuthenticatePasskey([FromBody] AuthenticatePasskeyRequest request, CancellationToken cancellationToken)
{
try
{
var command = new AuthenticatePasskeyCommand(request.Username);
var response = await _mediator.Send(command, cancellationToken);
return Ok(response);
}
catch (AppException ex)
{
return BadRequest(new { message = ex.Message });
}
}

[AllowAnonymous]
[HttpPost("passkey/complete-authentication")]
public async Task<IActionResult> CompletePasskeyAuthentication([FromBody] CompletePasskeyAuthenticationRequest request, CancellationToken cancellationToken)
{
try
{
var command = new CompletePasskeyAuthenticationCommand(request.Username, request.AssertionResponse, request.RememberMe);
var response = await _mediator.Send(command, cancellationToken);
return Ok(response);
}
catch (AppException ex)
{
return BadRequest(new { message = ex.Message });
}
}

[HttpGet("passkey/list")]
public async Task<IActionResult> GetPasskeys(CancellationToken cancellationToken)
{
try
{
var query = new GetUserPasskeysQuery(User);
var response = await _mediator.Send(query, cancellationToken);
return Ok(response);
}
catch (AppException ex)
{
return BadRequest(new { message = ex.Message });
}
}

[HttpDelete("passkey/{passkeyId}")]
public async Task<IActionResult> DeletePasskey(int passkeyId, CancellationToken cancellationToken)
{
try
{
var command = new DeletePasskeyCommand(User, passkeyId);
var response = await _mediator.Send(command, cancellationToken);
return Ok(response);
}
catch (AppException ex)
{
return BadRequest(new { message = ex.Message });
}
}
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Mediator;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using OpenAlprWebhookProcessor.Features.Users.Data;
using OpenAlprWebhookProcessor.Features.Users.Queries.GetAllUsers;
using System.Threading;
Expand All @@ -11,13 +12,16 @@ public class AuthenticateCommandHandler : IQueryHandler<AuthenticateCommand, Use
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UsersContext _context;

public AuthenticateCommandHandler(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager)
SignInManager<ApplicationUser> signInManager,
UsersContext context)
{
_userManager = userManager;
_signInManager = signInManager;
_context = context;
}

public async ValueTask<UserDto> Handle(AuthenticateCommand request, CancellationToken cancellationToken)
Expand All @@ -37,11 +41,16 @@ public async ValueTask<UserDto> Handle(AuthenticateCommand request, Cancellation
if (!result.Succeeded)
throw new AppException("Username or password is incorrect");

// Check if user has passkeys
var hasPasskeys = await _context.PasskeyCredentials
.AnyAsync(p => p.UserId == user.Id, cancellationToken);

var authUser = new UserDto
{
FirstName = user.FirstName,
Id = user.Id,
TwoFactorEnabled = false,
HasPasskeys = hasPasskeys,
LastName = user.LastName,
Username = user.UserName,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Mediator;

namespace OpenAlprWebhookProcessor.Features.Users.Commands.AuthenticatePasskey
{
public record AuthenticatePasskeyCommand(string Username) : IQuery<AuthenticatePasskeyResponse>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Mediator;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using OpenAlprWebhookProcessor.Features.Users.Data;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Fido2NetLib;
using Fido2NetLib.Objects;

namespace OpenAlprWebhookProcessor.Features.Users.Commands.AuthenticatePasskey
{
public class AuthenticatePasskeyCommandHandler : IQueryHandler<AuthenticatePasskeyCommand, AuthenticatePasskeyResponse>
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IFido2 _fido2;
private readonly UsersContext _context;
private readonly IMemoryCache _cache;

public AuthenticatePasskeyCommandHandler(
UserManager<ApplicationUser> userManager,
IFido2 fido2,
UsersContext context,
IMemoryCache cache)
{
_userManager = userManager;
_fido2 = fido2;
_context = context;
_cache = cache;
}

public async ValueTask<AuthenticatePasskeyResponse> Handle(AuthenticatePasskeyCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
throw new AppException("User not found");

var existingCredentials = await _context.PasskeyCredentials
.Where(c => c.UserId == user.Id)
.Select(c => new PublicKeyCredentialDescriptor(Convert.FromBase64String(c.CredentialId)))
.ToListAsync(cancellationToken);

if (!existingCredentials.Any())
throw new AppException("No passkeys registered for this user");

var options = _fido2.GetAssertionOptions(new GetAssertionOptionsParams
{
AllowedCredentials = existingCredentials,
UserVerification = UserVerificationRequirement.Preferred
});

var cacheKey = $"passkey_authentication_{user.Id}";
_cache.Set(cacheKey, options, TimeSpan.FromMinutes(5));

return new AuthenticatePasskeyResponse(options);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace OpenAlprWebhookProcessor.Features.Users.Commands.AuthenticatePasskey
{
public record AuthenticatePasskeyRequest(string Username);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Fido2NetLib;

namespace OpenAlprWebhookProcessor.Features.Users.Commands.AuthenticatePasskey
{
public record AuthenticatePasskeyResponse(AssertionOptions Options);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Mediator;
using OpenAlprWebhookProcessor.Features.Users.Queries.GetAllUsers;

namespace OpenAlprWebhookProcessor.Features.Users.Commands.CompletePasskeyAuthentication
{
public record CompletePasskeyAuthenticationCommand(
string Username,
string AssertionResponse,
bool RememberMe = false) : IQuery<UserDto>;
}
Loading
Loading