From 19cf073677c536ff5bb0b373ca0c2996d30b95fd Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Fri, 19 Jun 2026 01:36:08 +0100 Subject: [PATCH] Regenerate the secret --- .vscode/settings.json | 2 +- Makefile | 2 +- doc/Dictionnaire.md | 1 - .../Administration/ClientController.cs | 99 +++++++++++++++++++ src/Yavsc.Org/Views/Client/Details.cshtml | 1 + .../Views/Client/RegenerateSecret.cshtml | 39 ++++++++ src/Yavsc.Org/Views/Client/ShowSecret.cshtml | 53 ++++++++++ src/Yavsc.Org/appsettings-org.json | 5 +- test/yavscTests/WebServerFixture.cs | 10 -- test/yavscTests/appsettings.json | 3 +- 10 files changed, 199 insertions(+), 16 deletions(-) create mode 100644 src/Yavsc.Org/Views/Client/RegenerateSecret.cshtml create mode 100644 src/Yavsc.Org/Views/Client/ShowSecret.cshtml diff --git a/.vscode/settings.json b/.vscode/settings.json index 6e19d926..b2c7c06e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,5 +26,5 @@ "dotnet test": true }, "makefile.configureOnOpen": false, - "chat.disableAIFeatures": false + "chat.disableAIFeatures": true } diff --git a/Makefile b/Makefile index 4ef730d6..a4922a3d 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ src/Yavsc/bin/output/wwwroot: dotnet --project src/Yavsc.Org/Yavsc.Org.csproj publish test: - ASPNETCORE_ENVIRONMENT=Development dotnet test -e SLNDIR=$(SLNDIR) + ASPNETCORE_ENVIRONMENT=Development dotnet test watch: dotnet watch -p:Configuration=$(CONFIG) --project src/Yavsc/Yavsc.csproj diff --git a/doc/Dictionnaire.md b/doc/Dictionnaire.md index 33302c7e..970ed8c7 100644 --- a/doc/Dictionnaire.md +++ b/doc/Dictionnaire.md @@ -37,4 +37,3 @@ Ainsi, les termes juridiques sont toujours disponibles, quel que soit le domaine du projet. Ce dictionnaire juridique est simplement fondamental, par construction, c'est la base de tous les dictionnaires. - diff --git a/src/Yavsc.Org/Controllers/Administration/ClientController.cs b/src/Yavsc.Org/Controllers/Administration/ClientController.cs index 431a2517..459d1857 100644 --- a/src/Yavsc.Org/Controllers/Administration/ClientController.cs +++ b/src/Yavsc.Org/Controllers/Administration/ClientController.cs @@ -194,5 +194,104 @@ namespace Yavsc.Controllers await dbContext.SaveChangesAsync(); return RedirectToAction("Index"); } + + // GET: Client/RegenerateSecret/5 + [ActionName("RegenerateSecret")] + public async Task RegenerateSecretGet(int id) + { + Client client = await dbContext.Clients + .Include(c => c.ClientSecrets) + .SingleOrDefaultAsync(m => m.Id == id); + if (client == null) + { + return NotFound(); + } + return View("RegenerateSecret", client); + } + + // POST: Client/RegenerateSecret/5 + [HttpPost, ActionName("RegenerateSecret")] + [ValidateAntiForgeryToken] + public async Task RegenerateSecretConfirmed(int id) + { + Client client = await dbContext.Clients + .Include(c => c.ClientSecrets) + .SingleOrDefaultAsync(m => m.Id == id); + if (client == null) + { + return NotFound(); + } + + // Generate a fresh secret in clear text. We display it to the admin + // once, then IdentityServer will hash it on SaveChanges. + var newSecret = GenerateRawClientSecret(); + var now = DateTime.UtcNow; + var expiration = now.AddDays(90); + + // Replace: drop existing secrets and add the freshly generated one. + if (client.ClientSecrets != null && client.ClientSecrets.Count > 0) + { + dbContext.ClientSecrets.RemoveRange(client.ClientSecrets); + } + + client.ClientSecrets = new List + { + new ClientSecret + { + ClientId = client.Id, + Client = client, + Type = "SharedSecret", + Value = newSecret, + Description = $"Regenerated on {now:yyyy-MM-dd HH:mm:ss} UTC", + Created = now, + Expiration = expiration + } + }; + + dbContext.Update(client); + await dbContext.SaveChangesAsync(User.GetUserId()); + + // Flash the secret through TempData so the next request can render + // it exactly once, then it is gone forever (IdentityServer stores + // it hashed). + TempData["NewClientSecret"] = newSecret; + TempData["NewClientSecretExpiresAt"] = expiration.ToString("u"); + TempData["NewClientSecretClientName"] = client.ClientName ?? client.ClientId; + + return RedirectToAction("ShowSecret", new { id = client.Id }); + } + + // GET: Client/ShowSecret/5 + public IActionResult ShowSecret(int id) + { + var secret = TempData["NewClientSecret"] as string; + if (string.IsNullOrEmpty(secret)) + { + // The one-shot window is closed. Refuse to render anything + // sensitive and bounce back to the details page. + return RedirectToAction("Details", new { id }); + } + + // TempData.Keep would persist to the next request; we deliberately + // do NOT keep it so the value cannot be replayed. + + ViewBag.NewClientSecret = secret; + ViewBag.NewClientSecretExpiresAt = TempData["NewClientSecretExpiresAt"] as string; + ViewBag.NewClientSecretClientName = TempData["NewClientSecretClientName"] as string; + ViewBag.ClientId = id; + return View(); + } + + private static string GenerateRawClientSecret() + { + // 32 bytes => 43 url-safe base64 chars without padding. Enough entropy + // for a client secret; readable enough to copy/paste once. + var bytes = new byte[32]; + System.Security.Cryptography.RandomNumberGenerator.Fill(bytes); + return Convert.ToBase64String(bytes) + .TrimEnd('=') + .Replace('+', '-') + .Replace('/', '_'); + } } } diff --git a/src/Yavsc.Org/Views/Client/Details.cshtml b/src/Yavsc.Org/Views/Client/Details.cshtml index b86615a4..188f4269 100644 --- a/src/Yavsc.Org/Views/Client/Details.cshtml +++ b/src/Yavsc.Org/Views/Client/Details.cshtml @@ -66,5 +66,6 @@

@Localizer["Edit"] | + @Localizer["Regenerate secret"] | @Localizer["Back to List"]

diff --git a/src/Yavsc.Org/Views/Client/RegenerateSecret.cshtml b/src/Yavsc.Org/Views/Client/RegenerateSecret.cshtml new file mode 100644 index 00000000..b0fd554d --- /dev/null +++ b/src/Yavsc.Org/Views/Client/RegenerateSecret.cshtml @@ -0,0 +1,39 @@ +@model Client + +

@Localizer["Regenerate client secret"]

+ + + +
+

@Model.ClientName

+
+
+
@Html.DisplayNameFor(model => model.ClientId)
+
@Html.DisplayFor(model => model.ClientId)
+
@Localizer["Current secrets"]
+
+ @if (Model.ClientSecrets != null && Model.ClientSecrets.Count > 0) + { +
    + @foreach (var secret in Model.ClientSecrets) + { +
  • @secret.Description (created @secret.Created.ToString("u"))
  • + } +
+ } + else + { + @Localizer["No secret currently set."] + } +
+
+
+ +
+ @Html.AntiForgeryToken() + + @Localizer["Cancel"] +
diff --git a/src/Yavsc.Org/Views/Client/ShowSecret.cshtml b/src/Yavsc.Org/Views/Client/ShowSecret.cshtml new file mode 100644 index 00000000..5256e327 --- /dev/null +++ b/src/Yavsc.Org/Views/Client/ShowSecret.cshtml @@ -0,0 +1,53 @@ +@{ + var secret = ViewBag.NewClientSecret as string; + var expiresAt = ViewBag.NewClientSecretExpiresAt as string; + var clientName = ViewBag.NewClientSecretClientName as string; + var clientId = ViewBag.ClientId; +} + +

@Localizer["New client secret"]

+ + + +
+

@clientName

+
+
+
@Localizer["Client identifier"]
+
@clientId
+
@Localizer["Expires at"]
+
@expiresAt
+
@Localizer["Secret value"]
+
+
@secret
+ + +
+
+
+ +

+ @Localizer["I have saved the secret — return to client"] +

+ + diff --git a/src/Yavsc.Org/appsettings-org.json b/src/Yavsc.Org/appsettings-org.json index 53f5432c..706d70c5 100644 --- a/src/Yavsc.Org/appsettings-org.json +++ b/src/Yavsc.Org/appsettings-org.json @@ -82,7 +82,10 @@ "Kestrel": { "Endpoints": { "Http": { - "Url": "http://localhost:3002" + "Url": "http://localhost:3003" + }, + "Https": { + "Url": "https://localhost:3004" } } } diff --git a/test/yavscTests/WebServerFixture.cs b/test/yavscTests/WebServerFixture.cs index bb9ece19..586309c2 100644 --- a/test/yavscTests/WebServerFixture.cs +++ b/test/yavscTests/WebServerFixture.cs @@ -119,18 +119,8 @@ namespace yavscTests.ServerFixtures public async Task SetupHost() { - var builder = WebApplication.CreateBuilder(); - var slnDir = builder.Configuration.GetValue("SLNDIR"); - Assert.NotNull(slnDir); - - var yavscOrgPath = Path.GetFullPath( - Path.Combine(slnDir, - "src/Yavsc.Org")); - - - Directory.SetCurrentDirectory(yavscOrgPath); builder.AddConfiguration("org").AddInMemoryCollection(new Dictionary { [$"ConnectionStrings:{YavscConstants.YavscConnectionStringName}"] = "InMemory" diff --git a/test/yavscTests/appsettings.json b/test/yavscTests/appsettings.json index 6a214b5a..d49b688b 100644 --- a/test/yavscTests/appsettings.json +++ b/test/yavscTests/appsettings.json @@ -63,7 +63,6 @@ "UserName": "fakeuser", "Password": "f/\\kePassw0rd" } - }, - "SLNDIR": "/home/paul/Workspace/yavsc" + } }