using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using IdentityServer4; using IdentityServer4.EntityFramework.DbContexts; using IdentityServer4.EntityFramework.Entities; using IdentityServer4.EntityFramework.Mappers; using IdentityServer4.Models; using IdentityServer4.Stores; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.Logging; using Teknik.Configuration; using Teknik.IdentityServer.Models; using Teknik.IdentityServer.Models.Manage; using Teknik.Logging; using Teknik.Utilities; namespace Teknik.IdentityServer.Controllers { [Authorize(Policy = "Internal", AuthenticationSchemes = "Bearer")] [Route("[controller]/[action]")] [ApiController] public class ManageController : DefaultController { private readonly UserManager _userManager; private readonly SignInManager _signInManager; public ManageController( ILogger logger, Config config, UserManager userManager, SignInManager signInManager) : base(logger, config) { _userManager = userManager; _signInManager = signInManager; } [HttpPost] public async Task CreateUser(NewUserModel model) { if (string.IsNullOrEmpty(model.Username)) return new JsonResult(new { success = false, message = "Username is required" }); if (string.IsNullOrEmpty(model.Password)) return new JsonResult(new { success = false, message = "Password is required" }); var identityUser = new ApplicationUser(model.Username) { Id = Guid.NewGuid().ToString(), UserName = model.Username, AccountStatus = model.AccountStatus, AccountType = model.AccountType, Email = model.RecoveryEmail, EmailConfirmed = model.RecoveryVerified, PGPPublicKey = model.PGPPublicKey }; var result = await _userManager.CreateAsync(identityUser, model.Password); if (result.Succeeded) { return new JsonResult(new { success = true }); } return new JsonResult(new { success = false, message = "Unable to create user.", identityErrors = result.Errors }); } [HttpPost] public async Task DeleteUser(DeleteUserModel model) { if (string.IsNullOrEmpty(model.Username)) return new JsonResult(new { success = false, message = "Username is required" }); var foundUser = await _userManager.FindByNameAsync(model.Username); if (foundUser != null) { var result = await _userManager.DeleteAsync(foundUser); if (result.Succeeded) return new JsonResult(new { success = true }); else return new JsonResult(new { success = false, message = "Unable to delete user.", identityErrors = result.Errors }); } return new JsonResult(new { success = false, message = "User does not exist." }); } [HttpGet] public async Task UserExists(string username) { if (string.IsNullOrEmpty(username)) return new JsonResult(new { success = false, message = "Username is required" }); var foundUser = await _userManager.FindByNameAsync(username); return new JsonResult(new { success = true, data = foundUser != null }); } [HttpGet] public async Task GetUserInfo(string username) { if (string.IsNullOrEmpty(username)) return new JsonResult(new { success = false, message = "Username is required" }); var foundUser = await _userManager.FindByNameAsync(username); if (foundUser != null) { return new JsonResult(new { success = true, data = foundUser.ToJson() }); } return new JsonResult(new { success = false, message = "User does not exist." }); } [HttpPost] public async Task CheckPassword(CheckPasswordModel model) { if (string.IsNullOrEmpty(model.Username)) return new JsonResult(new { success = false, message = "Username is required" }); if (string.IsNullOrEmpty(model.Password)) return new JsonResult(new { success = false, message = "Password is required" }); var foundUser = await _userManager.FindByNameAsync(model.Username); if (foundUser != null) { bool valid = await _userManager.CheckPasswordAsync(foundUser, model.Password); return new JsonResult(new { success = true, data = valid }); } return new JsonResult(new { success = false, message = "User does not exist." }); } [HttpPost] public async Task GeneratePasswordResetToken(GeneratePasswordResetTokenModel model) { if (string.IsNullOrEmpty(model.Username)) return new JsonResult(new { success = false, message = "Username is required" }); var foundUser = await _userManager.FindByNameAsync(model.Username); if (foundUser != null) { string token = await _userManager.GeneratePasswordResetTokenAsync(foundUser); return new JsonResult(new { success = true, data = token }); } return new JsonResult(new { success = false, message = "User does not exist." }); } [HttpPost] public async Task ResetPassword(ResetPasswordModel model) { if (string.IsNullOrEmpty(model.Username)) return new JsonResult(new { success = false, message = "Username is required" }); if (string.IsNullOrEmpty(model.Token)) return new JsonResult(new { success = false, message = "Token is required" }); if (string.IsNullOrEmpty(model.Password)) return new JsonResult(new { success = false, message = "Password is required" }); var foundUser = await _userManager.FindByNameAsync(model.Username); if (foundUser != null) { var result = await _userManager.ResetPasswordAsync(foundUser, model.Token, model.Password); if (result.Succeeded) return new JsonResult(new { success = true }); else return new JsonResult(new { success = false, message = "Unable to reset password.", identityErrors = result.Errors }); } return new JsonResult(new { success = false, message = "User does not exist." }); } [HttpPost] public async Task UpdatePassword(UpdatePasswordModel model) { if (string.IsNullOrEmpty(model.Username)) return new JsonResult(new { success = false, message = "Username is required" }); if (string.IsNullOrEmpty(model.CurrentPassword)) return new JsonResult(new { success = false, message = "Current Password is required" }); if (string.IsNullOrEmpty(model.NewPassword)) return new JsonResult(new { success = false, message = "New Password is required" }); var foundUser = await _userManager.FindByNameAsync(model.Username); if (foundUser != null) { var result = await _userManager.ChangePasswordAsync(foundUser, model.CurrentPassword, model.NewPassword); if (result.Succeeded) return new JsonResult(new { success = true }); else return new JsonResult(new { success = false, message = "Unable to update password.", identityErrors = result.Errors }); } return new JsonResult(new { success = false, message = "User does not exist." }); } [HttpPost] public async Task UpdateEmail(UpdateEmailModel model) { if (string.IsNullOrEmpty(model.Username)) return new JsonResult(new { success = false, message = "Username is required" }); var foundUser = await _userManager.FindByNameAsync(model.Username); if (foundUser != null) { var result = await _userManager.SetEmailAsync(foundUser, model.Email); if (result.Succeeded) { var token = await _userManager.GenerateEmailConfirmationTokenAsync(foundUser); return new JsonResult(new { success = true, data = token }); } else return new JsonResult(new { success = false, message = "Unable to update email address.", identityErrors = result.Errors }); } return new JsonResult(new { success = false, message = "User does not exist." }); } [HttpPost] public async Task VerifyEmail(VerifyEmailModel model) { if (string.IsNullOrEmpty(model.Username)) return new JsonResult(new { success = false, message = "Username is required" }); if (string.IsNullOrEmpty(model.Token)) return new JsonResult(new { success = false, message = "Token is required" }); var foundUser = await _userManager.FindByNameAsync(model.Username); if (foundUser != null) { var result = await _userManager.ConfirmEmailAsync(foundUser, model.Token); if (result.Succeeded) return new JsonResult(new { success = true }); else return new JsonResult(new { success = false, message = "Unable to verify email address.", identityErrors = result.Errors }); } return new JsonResult(new { success = false, message = "User does not exist." }); } [HttpPost] public async Task UpdateAccountStatus(UpdateAccountStatusModel model) { if (string.IsNullOrEmpty(model.Username)) return new JsonResult(new { success = false, message = "Username is required" }); var foundUser = await _userManager.FindByNameAsync(model.Username); if (foundUser != null) { foundUser.AccountStatus = model.AccountStatus; var result = await _userManager.UpdateAsync(foundUser); if (result.Succeeded) return new JsonResult(new { success = true }); else return new JsonResult(new { success = false, message = "Unable to update account status.", identityErrors = result.Errors }); } return new JsonResult(new { success = false, message = "User does not exist." }); } [HttpPost] public async Task UpdateAccountType(UpdateAccountTypeModel model) { if (string.IsNullOrEmpty(model.Username)) return new JsonResult(new { success = false, message = "Username is required" }); var foundUser = await _userManager.FindByNameAsync(model.Username); if (foundUser != null) { foundUser.AccountType = model.AccountType; var result = await _userManager.UpdateAsync(foundUser); if (result.Succeeded) return new JsonResult(new { success = true }); else return new JsonResult(new { success = false, message = "Unable to update account type.", identityErrors = result.Errors }); } return new JsonResult(new { success = false, message = "User does not exist." }); } [HttpPost] public async Task UpdatePGPPublicKey(UpdatePGPPublicKeyModel model) { if (string.IsNullOrEmpty(model.Username)) return new JsonResult(new { success = false, message = "Username is required" }); var foundUser = await _userManager.FindByNameAsync(model.Username); if (foundUser != null) { foundUser.PGPPublicKey = model.PGPPublicKey; var result = await _userManager.UpdateAsync(foundUser); if (result.Succeeded) return new JsonResult(new { success = true }); else return new JsonResult(new { success = false, message = "Unable to update pgp public key.", identityErrors = result.Errors }); } return new JsonResult(new { success = false, message = "User does not exist." }); } [HttpGet] public async Task Get2FAKey(string username) { if (string.IsNullOrEmpty(username)) return new JsonResult(new { success = false, message = "Username is required" }); var foundUser = await _userManager.FindByNameAsync(username); if (foundUser != null) { string unformattedKey = await _userManager.GetAuthenticatorKeyAsync(foundUser); return new JsonResult(new { success = true, data = FormatKey(unformattedKey) }); } return new JsonResult(new { success = false, message = "User does not exist." }); } [HttpPost] public async Task Reset2FAKey(Reset2FAKeyModel model) { if (string.IsNullOrEmpty(model.Username)) return new JsonResult(new { success = false, message = "Username is required" }); var foundUser = await _userManager.FindByNameAsync(model.Username); if (foundUser != null) { await _userManager.ResetAuthenticatorKeyAsync(foundUser); string unformattedKey = await _userManager.GetAuthenticatorKeyAsync(foundUser); return new JsonResult(new { success = true, data = FormatKey(unformattedKey) }); } return new JsonResult(new { success = false, message = "User does not exist." }); } [HttpPost] public async Task Enable2FA(Enable2FAModel model) { if (string.IsNullOrEmpty(model.Username)) return new JsonResult(new { success = false, message = "Username is required" }); if (string.IsNullOrEmpty(model.Code)) return new JsonResult(new { success = false, message = "Code is required" }); var foundUser = await _userManager.FindByNameAsync(model.Username); if (foundUser != null) { // Strip spaces and hypens var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty); var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( foundUser, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); if (is2faTokenValid) { var result = await _userManager.SetTwoFactorEnabledAsync(foundUser, true); if (result.Succeeded) { var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(foundUser, 10); return new JsonResult(new { success = true, data = recoveryCodes.ToArray() }); } else return new JsonResult(new { success = false, message = "Unable to set Two-Factor Authentication.", identityErrors = result.Errors }); } return new JsonResult(new { success = false, message = "Verification code is invalid." }); } return new JsonResult(new { success = false, message = "User does not exist." }); } [HttpPost] public async Task Disable2FA(Disable2FAModel model) { if (string.IsNullOrEmpty(model.Username)) return new JsonResult(new { success = false, message = "Username is required" }); var foundUser = await _userManager.FindByNameAsync(model.Username); if (foundUser != null) { var result = await _userManager.SetTwoFactorEnabledAsync(foundUser, false); if (result.Succeeded) return new JsonResult(new { success = true }); else return new JsonResult(new { success = false, message = "Unable to disable Two-Factor Authentication.", identityErrors = result.Errors }); } return new JsonResult(new { success = false, message = "User does not exist." }); } [HttpPost] public async Task GenerateRecoveryCodes(GenerateRecoveryCodesModel model) { if (string.IsNullOrEmpty(model.Username)) return new JsonResult(new { success = false, message = "Username is required" }); var foundUser = await _userManager.FindByNameAsync(model.Username); if (foundUser != null) { if (foundUser.TwoFactorEnabled) { var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(foundUser, 10); return new JsonResult(new { success = true, data = recoveryCodes.ToArray() }); } return new JsonResult(new { success = false, message = "Two-Factor Authentication is not enabled." }); } return new JsonResult(new { success = false, message = "User does not exist." }); } [HttpGet] public async Task GetClient(string username, string clientId, [FromServices] IClientStore clientStore, [FromServices] ConfigurationDbContext configContext) { if (string.IsNullOrEmpty(username)) return new JsonResult(new { success = false, message = "Username is required" }); if (string.IsNullOrEmpty(clientId)) return new JsonResult(new { success = false, message = "Client Id is required" }); var client = configContext.Clients.FirstOrDefault(c => c.ClientId == clientId && c.Properties.Exists(p => p.Key == "username" && p.Value.ToLower() == username.ToLower()) ); if (client != null) { var foundClient = await clientStore.FindClientByIdAsync(client.ClientId); return new JsonResult(new { success = true, data = foundClient }); } return new JsonResult(new { success = false, message = "Client does not exist." }); } [HttpGet] public async Task GetClients(string username, [FromServices] IClientStore clientStore, [FromServices] ConfigurationDbContext configContext) { if (string.IsNullOrEmpty(username)) return new JsonResult(new { success = false, message = "Username is required" }); var foundClientIds = configContext.Clients.Where(c => c.Properties.Exists(p => p.Key == "username" && p.Value.ToLower() == username.ToLower()) ).Select(c => c.ClientId); var clients = new List(); foreach (var clientId in foundClientIds) { var foundClient = await clientStore.FindClientByIdAsync(clientId); if (foundClient != null) clients.Add(foundClient); } return new JsonResult(new { success = true, data = clients }); } [HttpPost] public IActionResult CreateClient(CreateClientModel model, [FromServices] ConfigurationDbContext configContext) { // Generate a unique client ID var clientId = StringHelper.RandomString(20, "abcdefghjkmnpqrstuvwxyz1234567890"); while (configContext.Clients.Where(c => c.ClientId == clientId).FirstOrDefault() != null) { clientId = StringHelper.RandomString(20, "abcdefghjkmnpqrstuvwxyz1234567890"); } var clientSecret = StringHelper.RandomString(40, "abcdefghjkmnpqrstuvwxyz1234567890"); var client = new IdentityServer4.Models.Client { Properties = new Dictionary() { { "username", model.Username } }, ClientId = clientId, ClientName = model.Name, ClientUri = model.HomepageUrl, LogoUri = model.LogoUrl, AllowedGrantTypes = new List() { GrantType.AuthorizationCode, GrantType.ClientCredentials }, ClientSecrets = { new IdentityServer4.Models.Secret(clientSecret.Sha256()) }, RequireConsent = true, RedirectUris = { model.CallbackUrl }, AllowedScopes = model.AllowedScopes, AllowOfflineAccess = true }; configContext.Clients.Add(client.ToEntity()); configContext.SaveChanges(); return new JsonResult(new { success = true, data = new { id = clientId, secret = clientSecret } }); } [HttpPost] public IActionResult EditClient(EditClientModel model, [FromServices] ConfigurationDbContext configContext) { // Validate it's an actual client var foundClient = configContext.Clients.Where(c => c.ClientId == model.ClientId).FirstOrDefault(); if (foundClient != null) { foundClient.ClientName = model.Name; foundClient.ClientUri = model.HomepageUrl; foundClient.LogoUri = model.LogoUrl; configContext.Entry(foundClient).State = EntityState.Modified; // Update the redirect URL for this client var results = configContext.Set().Where(c => c.ClientId == foundClient.Id).ToList(); if (results != null) { configContext.RemoveRange(results); } var newUri = new ClientRedirectUri(); newUri.Client = foundClient; newUri.ClientId = foundClient.Id; newUri.RedirectUri = model.CallbackUrl; configContext.Add(newUri); // Save all the changed configContext.SaveChanges(); return new JsonResult(new { success = true }); } return new JsonResult(new { success = false, message = "Client does not exist." }); } [HttpPost] public IActionResult DeleteClient(DeleteClientModel model, [FromServices] ConfigurationDbContext configContext) { var foundClient = configContext.Clients.Where(c => c.ClientId == model.ClientId).FirstOrDefault(); if (foundClient != null) { configContext.Clients.Remove(foundClient); configContext.SaveChanges(); return new JsonResult(new { success = true }); } return new JsonResult(new { success = false, message = "Client does not exist." }); } private string FormatKey(string unformattedKey) { var result = new StringBuilder(); int currentPosition = 0; while (currentPosition + 4 < unformattedKey.Length) { result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" "); currentPosition += 4; } if (currentPosition < unformattedKey.Length) { result.Append(unformattedKey.Substring(currentPosition)); } return result.ToString().ToLowerInvariant(); } } }