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
{
///
/// Controller für die Verwaltung von Fido-Credentials usw.
///
[Authorize]
public class FidoController : BaseController
{
private readonly SignInManager _signInManager;
private readonly IUserService _userService;
private readonly Fido2 _fidoLib;
private readonly IFidoService _fidoService;
private readonly ILogger _logger;
private readonly IStringLocalizer _localizer;
private readonly IMapper _mapper;
private readonly UserManager _userManager;
private readonly IOptions _fidoOptions;
private readonly IDistributedCache _memoryCache;
private readonly IOptions _sessionSettings;
///
/// Erstellt eine Instanz
///
/// Instanz eins IFidoService
/// Instanz eines ILogger
/// Instanz eines IStringLocalizer
/// Instanz eines IMapper
/// Instanz eines UserManager
/// Instanz eines Fido2Options
/// Instanz eines IDistributedCache
/// Instanz eines SignInManager
/// Instanz eines IUserService
/// Instanz eines IOptions SessionSettings
public FidoController(IFidoService fidoService, ILogger logger, IStringLocalizer localizer, IMapper mapper, UserManager userManager,
IOptions fidoOptions, IDistributedCache memoryCache, SignInManager signInManager, IUserService userService,
IOptions 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
///
/// Gibt einen View für die Fido-Credentials-Verwaltung zurück
///
/// View
public IActionResult Index()
{
ViewBag.PageInfo = _localizer["Fido_PageInfo"];
return View();
}
///
/// Gibt eine Liste von Entitäten basierend auf Abfragekriterien zurück
///
/// Abfragekriterien
/// Liste von gefundenen Entitäten
[HttpPost]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult GetCredentials([FromBody] DataManager dm)
{
if (dm != null)
{
var propList = new List();
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(item)).ToList();
//FilterPreview?
if (!dm.RequiresCounts)
return Json(resultListVm);
return Json(new { result = resultListVm, count = countFiltered });
}
///
/// Anlegen eines Credentials für Plattform
///
/// PartialView
[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);
}
///
/// Anlegen eines Credentials für CrossPlattform
///
/// PartialView
[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);
}
///
/// Bearbeiten eines Credentials
///
/// Id des Credentials
/// PartialView
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public async Task Edit(long id)
{
var item = await _fidoService.GetAsync(id);
if (item != null)
{
var model = _mapper.Map(item);
return PartialView("_Edit", model);
}
return PartialView("_Error");
}
///
/// Bearbeiten eines Credentials
///
/// Model
/// Json
[ValidateAntiForgeryToken]
[HttpPost]
public async Task 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);
}
///
/// Löschen eines Credentials
///
/// Id der Entitäten
/// Json
[HttpPost]
public async Task 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);
}
///
/// Gibt zurück ob ein Benutzer über Fido2-Credentials verfügt
///
/// Benutzername
/// JSON
[AllowAnonymous]
[HttpPost]
public async Task HasFidoCredentials(string userName)
{
var hasCredentials = await _fidoService.HasFidoCredentialsAsync(userName);
return Json(hasCredentials);
}
///
/// Hilfsmethode die einen angemeldeten Benutzer (Fido) zur richtigen Startseite weiterleitet
///
///
public async Task 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
///
/// FIDO2 Options erstellen
///
///
/// JSON
[HttpPost]
[ValidateAntiForgeryToken]
public async Task 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();
foreach (var publicKeyCredentialDescriptor in items)
{
existingKeys.Add(publicKeyCredentialDescriptor.Descriptor);
}
// 3. Create options
var authenticatorSelection = new AuthenticatorSelection
{
RequireResidentKey = model.RequireResidentKey,
UserVerification = model.UserVerification.ToEnum()
};
if (!string.IsNullOrEmpty(model.AuthType))
authenticatorSelection.AuthenticatorAttachment = model.AuthType.ToEnum();
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(), 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) });
}
}
///
/// FIDO2 Credentials erstellen und Speichern
///
///
/// JSON
[HttpPost]
[ValidateAntiForgeryToken]
public async Task 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(json);
// 2. Create callback so that lib can verify credential id is unique to this user
async Task 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) });
}
}
///
/// Hilfs-Funktion zum formatieren einer Fehlermeldung
///
/// Fehler
/// Formatierte Fehlermeldung
private string FormatException(Exception e)
{
return $"{e.Message}{(e.InnerException != null ? " (" + e.InnerException.Message + ")" : "")}";
}
#endregion
#region FIDO2 Specific Login
///
/// Fido-Options für Login erstellen
///
///
///
/// JSON
[AllowAnonymous]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task AssertionOptionsPost([FromForm] string username, [FromForm] string userVerification)
{
try
{
var existingCredentials = new List();
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();
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) });
}
}
///
/// Fido Login prüfen
///
///
///
/// JSON
[AllowAnonymous]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task 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 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
}
}