The next generation of the Teknik Services. Written in ASP.NET. https://www.teknik.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ManageController.cs 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using IdentityServer4;
  7. using IdentityServer4.EntityFramework.DbContexts;
  8. using IdentityServer4.EntityFramework.Entities;
  9. using IdentityServer4.EntityFramework.Mappers;
  10. using IdentityServer4.Models;
  11. using IdentityServer4.Stores;
  12. using Microsoft.AspNetCore.Authorization;
  13. using Microsoft.AspNetCore.Identity;
  14. using Microsoft.AspNetCore.Mvc;
  15. using Microsoft.EntityFrameworkCore;
  16. using Microsoft.EntityFrameworkCore.Internal;
  17. using Microsoft.Extensions.Logging;
  18. using Teknik.Configuration;
  19. using Teknik.IdentityServer.Models;
  20. using Teknik.IdentityServer.Models.Manage;
  21. using Teknik.Logging;
  22. using Teknik.Utilities;
  23. namespace Teknik.IdentityServer.Controllers
  24. {
  25. [Authorize(Policy = "Internal", AuthenticationSchemes = "Bearer")]
  26. [Route("[controller]/[action]")]
  27. [ApiController]
  28. public class ManageController : DefaultController
  29. {
  30. private readonly UserManager<ApplicationUser> _userManager;
  31. private readonly SignInManager<ApplicationUser> _signInManager;
  32. public ManageController(
  33. ILogger<Logger> logger,
  34. Config config,
  35. UserManager<ApplicationUser> userManager,
  36. SignInManager<ApplicationUser> signInManager) : base(logger, config)
  37. {
  38. _userManager = userManager;
  39. _signInManager = signInManager;
  40. }
  41. [HttpPost]
  42. public async Task<IActionResult> CreateUser(NewUserModel model)
  43. {
  44. if (string.IsNullOrEmpty(model.Username))
  45. return new JsonResult(new { success = false, message = "Username is required" });
  46. if (string.IsNullOrEmpty(model.Password))
  47. return new JsonResult(new { success = false, message = "Password is required" });
  48. var identityUser = new ApplicationUser(model.Username)
  49. {
  50. Id = Guid.NewGuid().ToString(),
  51. UserName = model.Username,
  52. AccountStatus = model.AccountStatus,
  53. AccountType = model.AccountType,
  54. Email = model.RecoveryEmail,
  55. EmailConfirmed = model.RecoveryVerified,
  56. PGPPublicKey = model.PGPPublicKey
  57. };
  58. var result = await _userManager.CreateAsync(identityUser, model.Password);
  59. if (result.Succeeded)
  60. {
  61. return new JsonResult(new { success = true });
  62. }
  63. return new JsonResult(new { success = false, message = "Unable to create user.", identityErrors = result.Errors });
  64. }
  65. [HttpPost]
  66. public async Task<IActionResult> DeleteUser(DeleteUserModel model, [FromServices] ConfigurationDbContext configContext)
  67. {
  68. if (string.IsNullOrEmpty(model.Username))
  69. return new JsonResult(new { success = false, message = "Username is required" });
  70. var foundUser = await _userManager.FindByNameAsync(model.Username);
  71. if (foundUser != null)
  72. {
  73. // Find this user's clients
  74. var foundClients = configContext.Clients.Where(c =>
  75. c.Properties.Exists(p =>
  76. p.Key == "username" &&
  77. p.Value.ToLower() == model.Username.ToLower())
  78. ).ToList();
  79. if (foundClients != null)
  80. {
  81. configContext.Clients.RemoveRange(foundClients);
  82. configContext.SaveChanges();
  83. }
  84. var result = await _userManager.DeleteAsync(foundUser);
  85. if (result.Succeeded)
  86. return new JsonResult(new { success = true });
  87. else
  88. return new JsonResult(new { success = false, message = "Unable to delete user.", identityErrors = result.Errors });
  89. }
  90. return new JsonResult(new { success = false, message = "User does not exist." });
  91. }
  92. [HttpGet]
  93. public async Task<IActionResult> UserExists(string username)
  94. {
  95. if (string.IsNullOrEmpty(username))
  96. return new JsonResult(new { success = false, message = "Username is required" });
  97. var foundUser = await _userManager.FindByNameAsync(username);
  98. return new JsonResult(new { success = true, data = foundUser != null });
  99. }
  100. [HttpGet]
  101. public async Task<IActionResult> GetUserInfo(string username)
  102. {
  103. if (string.IsNullOrEmpty(username))
  104. return new JsonResult(new { success = false, message = "Username is required" });
  105. var foundUser = await _userManager.FindByNameAsync(username);
  106. if (foundUser != null)
  107. {
  108. return new JsonResult(new { success = true, data = foundUser.ToJson() });
  109. }
  110. return new JsonResult(new { success = false, message = "User does not exist." });
  111. }
  112. [HttpPost]
  113. public async Task<IActionResult> CheckPassword(CheckPasswordModel model)
  114. {
  115. if (string.IsNullOrEmpty(model.Username))
  116. return new JsonResult(new { success = false, message = "Username is required" });
  117. if (string.IsNullOrEmpty(model.Password))
  118. return new JsonResult(new { success = false, message = "Password is required" });
  119. var foundUser = await _userManager.FindByNameAsync(model.Username);
  120. if (foundUser != null)
  121. {
  122. bool valid = await _userManager.CheckPasswordAsync(foundUser, model.Password);
  123. return new JsonResult(new { success = true, data = valid });
  124. }
  125. return new JsonResult(new { success = false, message = "User does not exist." });
  126. }
  127. [HttpPost]
  128. public async Task<IActionResult> GeneratePasswordResetToken(GeneratePasswordResetTokenModel model)
  129. {
  130. if (string.IsNullOrEmpty(model.Username))
  131. return new JsonResult(new { success = false, message = "Username is required" });
  132. var foundUser = await _userManager.FindByNameAsync(model.Username);
  133. if (foundUser != null)
  134. {
  135. string token = await _userManager.GeneratePasswordResetTokenAsync(foundUser);
  136. return new JsonResult(new { success = true, data = token });
  137. }
  138. return new JsonResult(new { success = false, message = "User does not exist." });
  139. }
  140. [HttpPost]
  141. public async Task<IActionResult> ResetPassword(ResetPasswordModel model)
  142. {
  143. if (string.IsNullOrEmpty(model.Username))
  144. return new JsonResult(new { success = false, message = "Username is required" });
  145. if (string.IsNullOrEmpty(model.Token))
  146. return new JsonResult(new { success = false, message = "Token is required" });
  147. if (string.IsNullOrEmpty(model.Password))
  148. return new JsonResult(new { success = false, message = "Password is required" });
  149. var foundUser = await _userManager.FindByNameAsync(model.Username);
  150. if (foundUser != null)
  151. {
  152. var result = await _userManager.ResetPasswordAsync(foundUser, model.Token, model.Password);
  153. if (result.Succeeded)
  154. return new JsonResult(new { success = true });
  155. else
  156. return new JsonResult(new { success = false, message = "Unable to reset password.", identityErrors = result.Errors });
  157. }
  158. return new JsonResult(new { success = false, message = "User does not exist." });
  159. }
  160. [HttpPost]
  161. public async Task<IActionResult> UpdatePassword(UpdatePasswordModel model)
  162. {
  163. if (string.IsNullOrEmpty(model.Username))
  164. return new JsonResult(new { success = false, message = "Username is required" });
  165. if (string.IsNullOrEmpty(model.CurrentPassword))
  166. return new JsonResult(new { success = false, message = "Current Password is required" });
  167. if (string.IsNullOrEmpty(model.NewPassword))
  168. return new JsonResult(new { success = false, message = "New Password is required" });
  169. var foundUser = await _userManager.FindByNameAsync(model.Username);
  170. if (foundUser != null)
  171. {
  172. var result = await _userManager.ChangePasswordAsync(foundUser, model.CurrentPassword, model.NewPassword);
  173. if (result.Succeeded)
  174. return new JsonResult(new { success = true });
  175. else
  176. return new JsonResult(new { success = false, message = "Unable to update password.", identityErrors = result.Errors });
  177. }
  178. return new JsonResult(new { success = false, message = "User does not exist." });
  179. }
  180. [HttpPost]
  181. public async Task<IActionResult> UpdateEmail(UpdateEmailModel model)
  182. {
  183. if (string.IsNullOrEmpty(model.Username))
  184. return new JsonResult(new { success = false, message = "Username is required" });
  185. var foundUser = await _userManager.FindByNameAsync(model.Username);
  186. if (foundUser != null)
  187. {
  188. var result = await _userManager.SetEmailAsync(foundUser, model.Email);
  189. if (result.Succeeded)
  190. {
  191. var token = await _userManager.GenerateEmailConfirmationTokenAsync(foundUser);
  192. return new JsonResult(new { success = true, data = token });
  193. }
  194. else
  195. return new JsonResult(new { success = false, message = "Unable to update email address.", identityErrors = result.Errors });
  196. }
  197. return new JsonResult(new { success = false, message = "User does not exist." });
  198. }
  199. [HttpPost]
  200. public async Task<IActionResult> VerifyEmail(VerifyEmailModel model)
  201. {
  202. if (string.IsNullOrEmpty(model.Username))
  203. return new JsonResult(new { success = false, message = "Username is required" });
  204. if (string.IsNullOrEmpty(model.Token))
  205. return new JsonResult(new { success = false, message = "Token is required" });
  206. var foundUser = await _userManager.FindByNameAsync(model.Username);
  207. if (foundUser != null)
  208. {
  209. var result = await _userManager.ConfirmEmailAsync(foundUser, model.Token);
  210. if (result.Succeeded)
  211. return new JsonResult(new { success = true });
  212. else
  213. return new JsonResult(new { success = false, message = "Unable to verify email address.", identityErrors = result.Errors });
  214. }
  215. return new JsonResult(new { success = false, message = "User does not exist." });
  216. }
  217. [HttpPost]
  218. public async Task<IActionResult> UpdateAccountStatus(UpdateAccountStatusModel model)
  219. {
  220. if (string.IsNullOrEmpty(model.Username))
  221. return new JsonResult(new { success = false, message = "Username is required" });
  222. var foundUser = await _userManager.FindByNameAsync(model.Username);
  223. if (foundUser != null)
  224. {
  225. foundUser.AccountStatus = model.AccountStatus;
  226. var result = await _userManager.UpdateAsync(foundUser);
  227. if (result.Succeeded)
  228. return new JsonResult(new { success = true });
  229. else
  230. return new JsonResult(new { success = false, message = "Unable to update account status.", identityErrors = result.Errors });
  231. }
  232. return new JsonResult(new { success = false, message = "User does not exist." });
  233. }
  234. [HttpPost]
  235. public async Task<IActionResult> UpdateAccountType(UpdateAccountTypeModel model)
  236. {
  237. if (string.IsNullOrEmpty(model.Username))
  238. return new JsonResult(new { success = false, message = "Username is required" });
  239. var foundUser = await _userManager.FindByNameAsync(model.Username);
  240. if (foundUser != null)
  241. {
  242. foundUser.AccountType = model.AccountType;
  243. var result = await _userManager.UpdateAsync(foundUser);
  244. if (result.Succeeded)
  245. return new JsonResult(new { success = true });
  246. else
  247. return new JsonResult(new { success = false, message = "Unable to update account type.", identityErrors = result.Errors });
  248. }
  249. return new JsonResult(new { success = false, message = "User does not exist." });
  250. }
  251. [HttpPost]
  252. public async Task<IActionResult> UpdatePGPPublicKey(UpdatePGPPublicKeyModel model)
  253. {
  254. if (string.IsNullOrEmpty(model.Username))
  255. return new JsonResult(new { success = false, message = "Username is required" });
  256. var foundUser = await _userManager.FindByNameAsync(model.Username);
  257. if (foundUser != null)
  258. {
  259. foundUser.PGPPublicKey = model.PGPPublicKey;
  260. var result = await _userManager.UpdateAsync(foundUser);
  261. if (result.Succeeded)
  262. return new JsonResult(new { success = true });
  263. else
  264. return new JsonResult(new { success = false, message = "Unable to update pgp public key.", identityErrors = result.Errors });
  265. }
  266. return new JsonResult(new { success = false, message = "User does not exist." });
  267. }
  268. [HttpGet]
  269. public async Task<IActionResult> Get2FAKey(string username)
  270. {
  271. if (string.IsNullOrEmpty(username))
  272. return new JsonResult(new { success = false, message = "Username is required" });
  273. var foundUser = await _userManager.FindByNameAsync(username);
  274. if (foundUser != null)
  275. {
  276. string unformattedKey = await _userManager.GetAuthenticatorKeyAsync(foundUser);
  277. return new JsonResult(new { success = true, data = FormatKey(unformattedKey) });
  278. }
  279. return new JsonResult(new { success = false, message = "User does not exist." });
  280. }
  281. [HttpPost]
  282. public async Task<IActionResult> Reset2FAKey(Reset2FAKeyModel model)
  283. {
  284. if (string.IsNullOrEmpty(model.Username))
  285. return new JsonResult(new { success = false, message = "Username is required" });
  286. var foundUser = await _userManager.FindByNameAsync(model.Username);
  287. if (foundUser != null)
  288. {
  289. await _userManager.ResetAuthenticatorKeyAsync(foundUser);
  290. string unformattedKey = await _userManager.GetAuthenticatorKeyAsync(foundUser);
  291. return new JsonResult(new { success = true, data = FormatKey(unformattedKey) });
  292. }
  293. return new JsonResult(new { success = false, message = "User does not exist." });
  294. }
  295. [HttpPost]
  296. public async Task<IActionResult> Enable2FA(Enable2FAModel model)
  297. {
  298. if (string.IsNullOrEmpty(model.Username))
  299. return new JsonResult(new { success = false, message = "Username is required" });
  300. if (string.IsNullOrEmpty(model.Code))
  301. return new JsonResult(new { success = false, message = "Code is required" });
  302. var foundUser = await _userManager.FindByNameAsync(model.Username);
  303. if (foundUser != null)
  304. {
  305. // Strip spaces and hypens
  306. var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
  307. var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
  308. foundUser, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
  309. if (is2faTokenValid)
  310. {
  311. var result = await _userManager.SetTwoFactorEnabledAsync(foundUser, true);
  312. if (result.Succeeded)
  313. {
  314. var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(foundUser, 10);
  315. return new JsonResult(new { success = true, data = recoveryCodes.ToArray() });
  316. }
  317. else
  318. return new JsonResult(new { success = false, message = "Unable to set Two-Factor Authentication.", identityErrors = result.Errors });
  319. }
  320. return new JsonResult(new { success = false, message = "Verification code is invalid." });
  321. }
  322. return new JsonResult(new { success = false, message = "User does not exist." });
  323. }
  324. [HttpPost]
  325. public async Task<IActionResult> Disable2FA(Disable2FAModel model)
  326. {
  327. if (string.IsNullOrEmpty(model.Username))
  328. return new JsonResult(new { success = false, message = "Username is required" });
  329. var foundUser = await _userManager.FindByNameAsync(model.Username);
  330. if (foundUser != null)
  331. {
  332. var result = await _userManager.SetTwoFactorEnabledAsync(foundUser, false);
  333. if (result.Succeeded)
  334. return new JsonResult(new { success = true });
  335. else
  336. return new JsonResult(new { success = false, message = "Unable to disable Two-Factor Authentication.", identityErrors = result.Errors });
  337. }
  338. return new JsonResult(new { success = false, message = "User does not exist." });
  339. }
  340. [HttpPost]
  341. public async Task<IActionResult> GenerateRecoveryCodes(GenerateRecoveryCodesModel model)
  342. {
  343. if (string.IsNullOrEmpty(model.Username))
  344. return new JsonResult(new { success = false, message = "Username is required" });
  345. var foundUser = await _userManager.FindByNameAsync(model.Username);
  346. if (foundUser != null)
  347. {
  348. if (foundUser.TwoFactorEnabled)
  349. {
  350. var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(foundUser, 10);
  351. return new JsonResult(new { success = true, data = recoveryCodes.ToArray() });
  352. }
  353. return new JsonResult(new { success = false, message = "Two-Factor Authentication is not enabled." });
  354. }
  355. return new JsonResult(new { success = false, message = "User does not exist." });
  356. }
  357. [HttpGet]
  358. public async Task<IActionResult> GetClient(string username, string clientId, [FromServices] IClientStore clientStore, [FromServices] ConfigurationDbContext configContext)
  359. {
  360. if (string.IsNullOrEmpty(username))
  361. return new JsonResult(new { success = false, message = "Username is required" });
  362. if (string.IsNullOrEmpty(clientId))
  363. return new JsonResult(new { success = false, message = "Client Id is required" });
  364. var client = configContext.Clients.FirstOrDefault(c =>
  365. c.ClientId == clientId &&
  366. c.Properties.Exists(p =>
  367. p.Key == "username" &&
  368. p.Value.ToLower() == username.ToLower())
  369. );
  370. if (client != null)
  371. {
  372. var foundClient = await clientStore.FindClientByIdAsync(client.ClientId);
  373. return new JsonResult(new { success = true, data = foundClient });
  374. }
  375. return new JsonResult(new { success = false, message = "Client does not exist." });
  376. }
  377. [HttpGet]
  378. public async Task<IActionResult> GetClients(string username, [FromServices] IClientStore clientStore, [FromServices] ConfigurationDbContext configContext)
  379. {
  380. if (string.IsNullOrEmpty(username))
  381. return new JsonResult(new { success = false, message = "Username is required" });
  382. var foundClientIds = configContext.Clients.Where(c =>
  383. c.Properties.Exists(p =>
  384. p.Key == "username" &&
  385. p.Value.ToLower() == username.ToLower())
  386. ).Select(c => c.ClientId);
  387. var clients = new List<IdentityServer4.Models.Client>();
  388. foreach (var clientId in foundClientIds)
  389. {
  390. var foundClient = await clientStore.FindClientByIdAsync(clientId);
  391. if (foundClient != null)
  392. clients.Add(foundClient);
  393. }
  394. return new JsonResult(new { success = true, data = clients });
  395. }
  396. [HttpPost]
  397. public IActionResult CreateClient(CreateClientModel model, [FromServices] ConfigurationDbContext configContext)
  398. {
  399. // Generate a unique client ID
  400. var clientId = StringHelper.RandomString(20, "abcdefghjkmnpqrstuvwxyz1234567890");
  401. while (configContext.Clients.Where(c => c.ClientId == clientId).FirstOrDefault() != null)
  402. {
  403. clientId = StringHelper.RandomString(20, "abcdefghjkmnpqrstuvwxyz1234567890");
  404. }
  405. var clientSecret = StringHelper.RandomString(40, "abcdefghjkmnpqrstuvwxyz1234567890");
  406. // Generate the origin for the callback
  407. Uri redirect = new Uri(model.CallbackUrl);
  408. string origin = redirect.Scheme + "://" + redirect.Host;
  409. var client = new IdentityServer4.Models.Client
  410. {
  411. Properties = new Dictionary<string, string>()
  412. {
  413. { "username", model.Username }
  414. },
  415. ClientId = clientId,
  416. ClientName = model.Name,
  417. ClientUri = model.HomepageUrl,
  418. LogoUri = model.LogoUrl,
  419. AllowedGrantTypes = new List<string>()
  420. {
  421. GrantType.AuthorizationCode,
  422. GrantType.ClientCredentials
  423. },
  424. ClientSecrets =
  425. {
  426. new IdentityServer4.Models.Secret(clientSecret.Sha256())
  427. },
  428. RequireConsent = true,
  429. RedirectUris =
  430. {
  431. model.CallbackUrl
  432. },
  433. AllowedCorsOrigins =
  434. {
  435. origin
  436. },
  437. AllowedScopes = model.AllowedScopes,
  438. AllowOfflineAccess = true
  439. };
  440. configContext.Clients.Add(client.ToEntity());
  441. configContext.SaveChanges();
  442. return new JsonResult(new { success = true, data = new { id = clientId, secret = clientSecret } });
  443. }
  444. [HttpPost]
  445. public IActionResult EditClient(EditClientModel model, [FromServices] ConfigurationDbContext configContext)
  446. {
  447. // Validate it's an actual client
  448. var foundClient = configContext.Clients.Where(c => c.ClientId == model.ClientId).FirstOrDefault();
  449. if (foundClient != null)
  450. {
  451. foundClient.ClientName = model.Name;
  452. foundClient.ClientUri = model.HomepageUrl;
  453. foundClient.LogoUri = model.LogoUrl;
  454. configContext.Entry(foundClient).State = EntityState.Modified;
  455. // Update the redirect URL for this client
  456. var results = configContext.Set<ClientRedirectUri>().Where(c => c.ClientId == foundClient.Id).ToList();
  457. if (results != null)
  458. {
  459. configContext.RemoveRange(results);
  460. }
  461. var newUri = new ClientRedirectUri();
  462. newUri.Client = foundClient;
  463. newUri.ClientId = foundClient.Id;
  464. newUri.RedirectUri = model.CallbackUrl;
  465. configContext.Add(newUri);
  466. // Generate the origin for the callback
  467. Uri redirect = new Uri(model.CallbackUrl);
  468. string origin = redirect.Scheme + "://" + redirect.Host;
  469. // Update the allowed origin for this client
  470. var corsOrigins = configContext.Set<ClientCorsOrigin>().Where(c => c.ClientId == foundClient.Id).ToList();
  471. if (corsOrigins != null)
  472. {
  473. configContext.RemoveRange(corsOrigins);
  474. }
  475. var newOrigin = new ClientCorsOrigin();
  476. newOrigin.Client = foundClient;
  477. newOrigin.ClientId = foundClient.Id;
  478. newOrigin.Origin = origin;
  479. configContext.Add(newUri);
  480. // Save all the changed
  481. configContext.SaveChanges();
  482. return new JsonResult(new { success = true });
  483. }
  484. return new JsonResult(new { success = false, message = "Client does not exist." });
  485. }
  486. [HttpPost]
  487. public IActionResult DeleteClient(DeleteClientModel model, [FromServices] ConfigurationDbContext configContext)
  488. {
  489. var foundClient = configContext.Clients.Where(c => c.ClientId == model.ClientId).FirstOrDefault();
  490. if (foundClient != null)
  491. {
  492. configContext.Clients.Remove(foundClient);
  493. configContext.SaveChanges();
  494. return new JsonResult(new { success = true });
  495. }
  496. return new JsonResult(new { success = false, message = "Client does not exist." });
  497. }
  498. private string FormatKey(string unformattedKey)
  499. {
  500. var result = new StringBuilder();
  501. int currentPosition = 0;
  502. while (currentPosition + 4 < unformattedKey.Length)
  503. {
  504. result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
  505. currentPosition += 4;
  506. }
  507. if (currentPosition < unformattedKey.Length)
  508. {
  509. result.Append(unformattedKey.Substring(currentPosition));
  510. }
  511. return result.ToString().ToLowerInvariant();
  512. }
  513. }
  514. }