540 lines
23 KiB
C#
540 lines
23 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using AutoMapper;
|
|
using Fido2NetLib;
|
|
using Fido2NetLib.Objects;
|
|
using gehGassi.Common.Data;
|
|
using gehGassi.Core.Interfaces;
|
|
using gehGassi.Domain.Users;
|
|
using gehGassi.Web.Helper;
|
|
using gehGassi.Web.Models;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Caching.Distributed;
|
|
using Microsoft.Extensions.Localization;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Newtonsoft.Json;
|
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
|
|
|
namespace gehGassi.Web.Controllers
|
|
{
|
|
/// <summary>
|
|
/// Controller für die Verwaltung von Fido-Credentials usw.
|
|
/// </summary>
|
|
[Authorize]
|
|
public class FidoController : BaseController
|
|
{
|
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
|
private readonly IUserService _userService;
|
|
private readonly Fido2 _fidoLib;
|
|
private readonly IFidoService _fidoService;
|
|
private readonly ILogger<FidoController> _logger;
|
|
private readonly IStringLocalizer<FidoController> _localizer;
|
|
private readonly IMapper _mapper;
|
|
private readonly UserManager<ApplicationUser> _userManager;
|
|
private readonly IOptions<Fido2Options> _fidoOptions;
|
|
private readonly IDistributedCache _memoryCache;
|
|
private readonly IOptions<SessionSettings> _sessionSettings;
|
|
|
|
/// <summary>
|
|
/// Erstellt eine Instanz
|
|
/// </summary>
|
|
/// <param name="fidoService">Instanz eins IFidoService</param>
|
|
/// <param name="logger">Instanz eines ILogger</param>
|
|
/// <param name="localizer">Instanz eines IStringLocalizer</param>
|
|
/// <param name="mapper">Instanz eines IMapper</param>
|
|
/// <param name="userManager">Instanz eines UserManager</param>
|
|
/// <param name="fidoOptions">Instanz eines Fido2Options</param>
|
|
/// <param name="memoryCache">Instanz eines IDistributedCache</param>
|
|
/// <param name="signInManager">Instanz eines SignInManager</param>
|
|
/// <param name="userService">Instanz eines IUserService</param>
|
|
/// <param name="sessionSettings">Instanz eines IOptions SessionSettings</param>
|
|
public FidoController(IFidoService fidoService, ILogger<FidoController> logger, IStringLocalizer<FidoController> localizer, IMapper mapper, UserManager<ApplicationUser> userManager,
|
|
IOptions<Fido2Options> fidoOptions, IDistributedCache memoryCache, SignInManager<ApplicationUser> signInManager, IUserService userService,
|
|
IOptions<SessionSettings> sessionSettings)
|
|
{
|
|
_fidoService = fidoService;
|
|
_logger = logger;
|
|
_localizer = localizer;
|
|
_mapper = mapper;
|
|
_userManager = userManager;
|
|
_fidoOptions = fidoOptions;
|
|
_memoryCache = memoryCache;
|
|
_signInManager = signInManager;
|
|
_userService = userService;
|
|
_sessionSettings = sessionSettings;
|
|
|
|
_fidoLib = new Fido2(new Fido2Configuration()
|
|
{
|
|
ServerDomain = _fidoOptions.Value.ServerDomain,
|
|
ServerName = _fidoOptions.Value.ServerName,
|
|
Origin = _fidoOptions.Value.Origin,
|
|
TimestampDriftTolerance = _fidoOptions.Value.TimestampDriftTolerance
|
|
});
|
|
}
|
|
|
|
#region ByUser
|
|
|
|
/// <summary>
|
|
/// Gibt einen View für die Fido-Credentials-Verwaltung zurück
|
|
/// </summary>
|
|
/// <returns>View</returns>
|
|
public IActionResult Index()
|
|
{
|
|
ViewBag.PageInfo = _localizer["Fido_PageInfo"];
|
|
return View();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gibt eine Liste von Entitäten basierend auf Abfragekriterien zurück
|
|
/// </summary>
|
|
/// <param name="dm">Abfragekriterien</param>
|
|
/// <returns>Liste von gefundenen Entitäten</returns>
|
|
[HttpPost]
|
|
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
|
public IActionResult GetCredentials([FromBody] DataManager dm)
|
|
{
|
|
if (dm != null)
|
|
{
|
|
var propList = new List<ComplexProperty>();
|
|
dm.SetComplexProperties(propList);
|
|
}
|
|
|
|
var resultList = _fidoService.Filter(dm?.SearchValue ?? "", User.Identity.Name);
|
|
|
|
//Sortierung
|
|
resultList = dm.ApplySorting(resultList);
|
|
//Filter
|
|
resultList = dm.ApplyFiltering(resultList, out var countFiltered);
|
|
//Paging
|
|
resultList = dm.ApplyPaging(resultList);
|
|
|
|
var resultListVm = resultList.ToList().Select(item => _mapper.Map<FidoStoredCredentialListVm>(item)).ToList();
|
|
|
|
//FilterPreview?
|
|
if (!dm.RequiresCounts)
|
|
return Json(resultListVm);
|
|
|
|
return Json(new { result = resultListVm, count = countFiltered });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Anlegen eines Credentials für Plattform
|
|
/// </summary>
|
|
/// <returns>PartialView</returns>
|
|
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
|
public IActionResult CreatePlatform()
|
|
{
|
|
var model = new CreateFidoCredentialVm()
|
|
{
|
|
Username = User.Identity.Name,
|
|
AttType = "none",
|
|
AuthType = "platform",
|
|
UserVerification = "required",
|
|
RequireResidentKey = false,
|
|
Name = string.Empty,
|
|
CredentialType = FidoCredentialTypeVm.Platform
|
|
};
|
|
return PartialView("_CreatePlatform", model);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Anlegen eines Credentials für CrossPlattform
|
|
/// </summary>
|
|
/// <returns>PartialView</returns>
|
|
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
|
public IActionResult CreateCrossPlatform()
|
|
{
|
|
var model = new CreateFidoCredentialVm()
|
|
{
|
|
Username = User.Identity.Name,
|
|
AttType = "none",
|
|
AuthType = "cross-platform",
|
|
UserVerification = "required",
|
|
RequireResidentKey = false,
|
|
Name = string.Empty,
|
|
CredentialType = FidoCredentialTypeVm.Crossplatform
|
|
};
|
|
return PartialView("_CreateCrossPlatform", model);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bearbeiten eines Credentials
|
|
/// </summary>
|
|
/// <param name="id">Id des Credentials</param>
|
|
/// <returns>PartialView</returns>
|
|
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
|
public async Task<IActionResult> Edit(long id)
|
|
{
|
|
var item = await _fidoService.GetAsync(id);
|
|
if (item != null)
|
|
{
|
|
var model = _mapper.Map<EditFidoCredentialVm>(item);
|
|
return PartialView("_Edit", model);
|
|
}
|
|
return PartialView("_Error");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bearbeiten eines Credentials
|
|
/// </summary>
|
|
/// <param name="model">Model</param>
|
|
/// <returns>Json</returns>
|
|
[ValidateAntiForgeryToken]
|
|
[HttpPost]
|
|
public async Task<IActionResult> Edit(EditFidoCredentialVm model)
|
|
{
|
|
var result = new ResponseVm { Success = false };
|
|
|
|
if (ModelState.IsValid)
|
|
{
|
|
var item = await _fidoService.GetAsync(model.Id);
|
|
if (item != null)
|
|
{
|
|
_mapper.Map(model, item);
|
|
await _fidoService.CommitAsync(User.Identity.Name);
|
|
|
|
result.Data = item.ToCamelCaseJson();
|
|
result.Success = true;
|
|
result.Html = string.Empty;
|
|
return Json(new { result.Success, result.Html, result.Data });
|
|
}
|
|
}
|
|
result.Html = await PartialView("_Edit", model).ToStringAsync(ControllerContext);
|
|
return Json(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Löschen eines Credentials
|
|
/// </summary>
|
|
/// <param name="id">Id der Entitäten</param>
|
|
/// <returns>Json</returns>
|
|
[HttpPost]
|
|
public async Task<IActionResult> Delete(long id)
|
|
{
|
|
var result = new ResponseVm { Success = false };
|
|
var item = await _fidoService.GetAsync(id);
|
|
if (item != null)
|
|
{
|
|
_fidoService.Remove(item);
|
|
await _fidoService.CommitAsync(User.Identity.Name);
|
|
result.Data = item.ToCamelCaseJson();
|
|
result.Success = true;
|
|
}
|
|
return Json(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gibt zurück ob ein Benutzer über Fido2-Credentials verfügt
|
|
/// </summary>
|
|
/// <param name="userName">Benutzername</param>
|
|
/// <returns>JSON</returns>
|
|
[AllowAnonymous]
|
|
[HttpPost]
|
|
public async Task<IActionResult> HasFidoCredentials(string userName)
|
|
{
|
|
var hasCredentials = await _fidoService.HasFidoCredentialsAsync(userName);
|
|
return Json(hasCredentials);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hilfsmethode die einen angemeldeten Benutzer (Fido) zur richtigen Startseite weiterleitet
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public async Task<IActionResult> RedirectFido()
|
|
{
|
|
if (User.Identity.IsAuthenticated)
|
|
{
|
|
var user = await _userService.GetByUsernameAsync(User.Identity.Name);
|
|
CultureInfo.CurrentCulture = new CultureInfo(user.PreferredLanguage);
|
|
var roles = await _userManager.GetRolesAsync(user);
|
|
|
|
if (roles.Contains("Administrator"))
|
|
return RedirectToAction("Index", "Home", new { culture = CultureInfo.CurrentCulture.TwoLetterISOLanguageName });
|
|
else if (roles.Contains("Tenant"))
|
|
return RedirectToAction("IndexTenant", "Home", new { culture = CultureInfo.CurrentCulture.TwoLetterISOLanguageName });
|
|
else if (roles.Contains("Customer"))
|
|
return RedirectToAction("IndexCustomer", "Home", new { culture = CultureInfo.CurrentCulture.TwoLetterISOLanguageName });
|
|
else if (roles.Contains("Instructor"))
|
|
return RedirectToAction("IndexInstructor", "Home", new { culture = CultureInfo.CurrentCulture.TwoLetterISOLanguageName });
|
|
else if (roles.Contains("Student"))
|
|
return RedirectToAction("IndexStudent", "Home", new { culture = CultureInfo.CurrentCulture.TwoLetterISOLanguageName });
|
|
else
|
|
return RedirectToAction("Status401", "Home", new { culture = CultureInfo.CurrentCulture.TwoLetterISOLanguageName });
|
|
}
|
|
return RedirectToAction("Login", "Account", new { culture = CultureInfo.CurrentCulture.TwoLetterISOLanguageName });
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region FIDO2 Specific Register
|
|
|
|
/// <summary>
|
|
/// FIDO2 Options erstellen
|
|
/// </summary>
|
|
/// <param name="model"></param>
|
|
/// <returns>JSON</returns>
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<JsonResult> MakeCredentialOptions(CreateFidoCredentialVm model)
|
|
{
|
|
try
|
|
{
|
|
var user = new Fido2User
|
|
{
|
|
DisplayName = model.Username,
|
|
Name = model.Username,
|
|
Id = Encoding.UTF8.GetBytes(model.Username) // byte representation of userID is required
|
|
};
|
|
|
|
// 2. Get user existing keys by username
|
|
var items = await _fidoService.GetCredentialsAsync(model.Username);
|
|
var existingKeys = new List<PublicKeyCredentialDescriptor>();
|
|
foreach (var publicKeyCredentialDescriptor in items)
|
|
{
|
|
existingKeys.Add(publicKeyCredentialDescriptor.Descriptor);
|
|
}
|
|
|
|
// 3. Create options
|
|
var authenticatorSelection = new AuthenticatorSelection
|
|
{
|
|
RequireResidentKey = model.RequireResidentKey,
|
|
UserVerification = model.UserVerification.ToEnum<UserVerificationRequirement>()
|
|
};
|
|
|
|
if (!string.IsNullOrEmpty(model.AuthType))
|
|
authenticatorSelection.AuthenticatorAttachment = model.AuthType.ToEnum<AuthenticatorAttachment>();
|
|
|
|
var exts = new AuthenticationExtensionsClientInputs() { Extensions = true, UserVerificationIndex = true, Location = true, UserVerificationMethod = true, BiometricAuthenticatorPerformanceBounds = new AuthenticatorBiometricPerfBounds { FAR = float.MaxValue, FRR = float.MaxValue } };
|
|
|
|
var options = _fidoLib.RequestNewCredential(user, existingKeys, authenticatorSelection, model.AttType.ToEnum<AttestationConveyancePreference>(), exts);
|
|
|
|
// 4. Temporarily store options, session/in-memory cache/redis/db
|
|
await _memoryCache.SetStringAsync($"{model.Username}.fido2.attestationOptions", options.ToJson(), new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) });
|
|
await _memoryCache.SetStringAsync($"{model.Username}.fido2.model", JsonSerializer.Serialize(model), new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) });
|
|
//HttpContext.Session.SetString("fido2.attestationOptions", options.ToJson());
|
|
|
|
// 5. return options to client
|
|
var settings = new JsonSerializerSettings
|
|
{
|
|
NullValueHandling = NullValueHandling.Ignore,
|
|
DateFormatHandling = DateFormatHandling.IsoDateFormat
|
|
};
|
|
return Json(options, settings);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return Json(new CredentialCreateOptions { Status = "error", ErrorMessage = FormatException(e) });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// FIDO2 Credentials erstellen und Speichern
|
|
/// </summary>
|
|
/// <param name="attestationResponse"></param>
|
|
/// <returns>JSON</returns>
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<JsonResult> MakeCredential([FromBody] AuthenticatorAttestationRawResponse attestationResponse)
|
|
{
|
|
try
|
|
{
|
|
// 1. get the options we sent the client
|
|
var jsonOptions = await _memoryCache.GetStringAsync($"{User.Identity.Name}.fido2.attestationOptions");
|
|
var options = CredentialCreateOptions.FromJson(jsonOptions);
|
|
|
|
var json = await _memoryCache.GetStringAsync($"{User.Identity.Name}.fido2.model");
|
|
var model = JsonSerializer.Deserialize<CreateFidoCredentialVm>(json);
|
|
|
|
// 2. Create callback so that lib can verify credential id is unique to this user
|
|
async Task<bool> Callback(IsCredentialIdUniqueToUserParams args)
|
|
{
|
|
var users = await _fidoService.GetUsersByCredentialIdAsync(args.CredentialId);
|
|
if (users.Count > 0) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
// 2. Verify and make the credentials
|
|
var success = await _fidoLib.MakeNewCredentialAsync(attestationResponse, options, (IsCredentialIdUniqueToUserAsyncDelegate)Callback);
|
|
|
|
// 3. Store the credentials in db
|
|
var credential = _fidoService.Create();
|
|
credential.UserId = options.User.Id;
|
|
credential.Name = model.Name;
|
|
credential.CredentialType = (FidoCredentialType)model.CredentialType;
|
|
credential.Username = options.User.Name;
|
|
credential.Descriptor = new PublicKeyCredentialDescriptor(success.Result.CredentialId);
|
|
credential.PublicKey = success.Result.PublicKey;
|
|
credential.UserHandle = success.Result.User.Id;
|
|
credential.SignatureCounter = success.Result.Counter;
|
|
credential.CredType = success.Result.CredType;
|
|
credential.RegDate = DateTimeOffset.UtcNow;
|
|
credential.AaGuid = success.Result.Aaguid;
|
|
|
|
_fidoService.Add(credential);
|
|
await _fidoService.CommitAsync(User.Identity.Name);
|
|
|
|
return Json(success);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return Json(new Fido2.CredentialMakeResult { Status = "error", ErrorMessage = FormatException(e) });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hilfs-Funktion zum formatieren einer Fehlermeldung
|
|
/// </summary>
|
|
/// <param name="e">Fehler</param>
|
|
/// <returns>Formatierte Fehlermeldung</returns>
|
|
private string FormatException(Exception e)
|
|
{
|
|
return $"{e.Message}{(e.InnerException != null ? " (" + e.InnerException.Message + ")" : "")}";
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region FIDO2 Specific Login
|
|
|
|
/// <summary>
|
|
/// Fido-Options für Login erstellen
|
|
/// </summary>
|
|
/// <param name="username"></param>
|
|
/// <param name="userVerification"></param>
|
|
/// <returns>JSON</returns>
|
|
[AllowAnonymous]
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<ActionResult> AssertionOptionsPost([FromForm] string username, [FromForm] string userVerification)
|
|
{
|
|
try
|
|
{
|
|
var existingCredentials = new List<PublicKeyCredentialDescriptor>();
|
|
|
|
if (!string.IsNullOrEmpty(username))
|
|
{
|
|
var identityUser = await _userManager.FindByNameAsync(username);
|
|
if(identityUser == null)
|
|
return Json(new AssertionOptions { Status = "error", ErrorMessage = "unf" });
|
|
|
|
var user = new Fido2User
|
|
{
|
|
DisplayName = identityUser.UserName,
|
|
Name = identityUser.UserName,
|
|
Id = Encoding.UTF8.GetBytes(identityUser.UserName) // byte representation of userID is required
|
|
};
|
|
|
|
if (user == null) throw new ArgumentException("Username was not registered");
|
|
|
|
// 2. Get registered credentials from database
|
|
var items = await _fidoService.GetCredentialsAsync(identityUser.UserName);
|
|
existingCredentials = items.Select(c => c.Descriptor).ToList();
|
|
}
|
|
|
|
var exts = new AuthenticationExtensionsClientInputs() { SimpleTransactionAuthorization = "FIDO", GenericTransactionAuthorization = new TxAuthGenericArg { ContentType = "text/plain", Content = new byte[] { 0x46, 0x49, 0x44, 0x4F } }, UserVerificationIndex = true, Location = true, UserVerificationMethod = true };
|
|
|
|
// 3. Create options
|
|
var uv = string.IsNullOrEmpty(userVerification) ? UserVerificationRequirement.Discouraged : userVerification.ToEnum<UserVerificationRequirement>();
|
|
var options = _fidoLib.GetAssertionOptions(
|
|
existingCredentials,
|
|
uv,
|
|
exts
|
|
);
|
|
|
|
// 4. Temporarily store options, session/in-memory cache/redis/db
|
|
await _memoryCache.SetStringAsync($"{username}.fido2.assertionOptions", options.ToJson(), new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) });
|
|
|
|
// 5. Return options to client
|
|
var settings = new JsonSerializerSettings
|
|
{
|
|
NullValueHandling = NullValueHandling.Ignore,
|
|
DateFormatHandling = DateFormatHandling.IsoDateFormat
|
|
};
|
|
return Json(options, settings);
|
|
}
|
|
|
|
catch (Exception e)
|
|
{
|
|
return Json(new AssertionOptions { Status = "error", ErrorMessage = FormatException(e) });
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fido Login prüfen
|
|
/// </summary>
|
|
/// <param name="clientResponse"></param>
|
|
/// <param name="username"></param>
|
|
/// <returns>JSON</returns>
|
|
[AllowAnonymous]
|
|
[HttpPost]
|
|
[ValidateAntiForgeryToken]
|
|
public async Task<JsonResult> MakeAssertion([FromBody] AuthenticatorAssertionRawResponse clientResponse, [FromQuery] string username)
|
|
{
|
|
try
|
|
{
|
|
// 1. Get the assertion options we sent the client
|
|
var jsonOptions = await _memoryCache.GetStringAsync($"{username}.fido2.assertionOptions");
|
|
var options = AssertionOptions.FromJson(jsonOptions);
|
|
|
|
// 2. Get registered credential from database
|
|
var creds = await _fidoService.GetByIdAsync(clientResponse.Id);
|
|
|
|
if (creds == null)
|
|
{
|
|
throw new Exception("Unknown credentials");
|
|
}
|
|
|
|
// 3. Get credential counter from database
|
|
var storedCounter = creds.SignatureCounter;
|
|
|
|
// 4. Create callback to check if userhandle owns the credentialId
|
|
async Task<bool> Callback(IsUserHandleOwnerOfCredentialIdParams args)
|
|
{
|
|
var storedCreds = await _fidoService.GetCredentialsByUserHandleAsync(args.UserHandle);
|
|
return storedCreds.Exists(c => c.Descriptor.Id.SequenceEqual(args.CredentialId));
|
|
}
|
|
|
|
// 5. Make the assertion
|
|
var res = await _fidoLib.MakeAssertionAsync(clientResponse, options, creds.PublicKey, storedCounter, Callback);
|
|
|
|
// 6. Store the updated counter
|
|
await _fidoService.UpdateCounterAsync(res.CredentialId, res.Counter);
|
|
|
|
var user = await _userService.GetByUsernameAsync(creds.Username);
|
|
if (user == null)
|
|
{
|
|
throw new InvalidOperationException($"Unable to load user.");
|
|
}
|
|
|
|
if (await _userManager.IsLockedOutAsync(user))
|
|
{
|
|
throw new InvalidOperationException($"Locked out.");
|
|
}
|
|
|
|
user.LastLoginDate = DateTimeOffset.UtcNow;
|
|
await _userService.CommitAsync(user.UserName);
|
|
|
|
await _signInManager.SignInAsync(user, isPersistent: _sessionSettings.Value.PersistentCookie);
|
|
|
|
CultureInfo.CurrentCulture = new CultureInfo(user.PreferredLanguage);
|
|
|
|
// 7. return OK to client
|
|
return Json(res);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return Json(new AssertionVerificationResult { Status = "error", ErrorMessage = FormatException(e) });
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|