Regenerate the secret
This commit is contained in:
parent
3508f0a55d
commit
19cf073677
10 changed files with 199 additions and 16 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -26,5 +26,5 @@
|
|||
"dotnet test": true
|
||||
},
|
||||
"makefile.configureOnOpen": false,
|
||||
"chat.disableAIFeatures": false
|
||||
"chat.disableAIFeatures": true
|
||||
}
|
||||
|
|
|
|||
2
Makefile
2
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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -194,5 +194,104 @@ namespace Yavsc.Controllers
|
|||
await dbContext.SaveChangesAsync();
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
// GET: Client/RegenerateSecret/5
|
||||
[ActionName("RegenerateSecret")]
|
||||
public async Task<IActionResult> 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<IActionResult> 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<ClientSecret>
|
||||
{
|
||||
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('/', '_');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,5 +66,6 @@
|
|||
</div>
|
||||
<p>
|
||||
<a asp-action="Edit" asp-route-id="@Model.Id">@Localizer["Edit"]</a> |
|
||||
<a asp-action="RegenerateSecret" asp-route-id="@Model.Id">@Localizer["Regenerate secret"]</a> |
|
||||
<a asp-action="Index">@Localizer["Back to List"]</a>
|
||||
</p>
|
||||
|
|
|
|||
39
src/Yavsc.Org/Views/Client/RegenerateSecret.cshtml
Normal file
39
src/Yavsc.Org/Views/Client/RegenerateSecret.cshtml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
@model Client
|
||||
|
||||
<h2>@Localizer["Regenerate client secret"]</h2>
|
||||
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<strong>@Localizer["Warning."]</strong>
|
||||
@Localizer["Regenerating the secret will immediately invalidate the current one. Any client using the existing secret will be unable to authenticate until it is updated with the new value. The new secret will be displayed exactly once and cannot be recovered afterwards."]
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>@Model.ClientName</h4>
|
||||
<hr />
|
||||
<dl class="dl-horizontal">
|
||||
<dt>@Html.DisplayNameFor(model => model.ClientId)</dt>
|
||||
<dd>@Html.DisplayFor(model => model.ClientId)</dd>
|
||||
<dt>@Localizer["Current secrets"]</dt>
|
||||
<dd>
|
||||
@if (Model.ClientSecrets != null && Model.ClientSecrets.Count > 0)
|
||||
{
|
||||
<ul>
|
||||
@foreach (var secret in Model.ClientSecrets)
|
||||
{
|
||||
<li>@secret.Description <small class="text-muted">(created @secret.Created.ToString("u"))</small></li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
else
|
||||
{
|
||||
<em>@Localizer["No secret currently set."]</em>
|
||||
}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<form asp-action="RegenerateSecret" asp-route-id="@Model.Id" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<button type="submit" class="btn btn-danger">@Localizer["Regenerate now"]</button>
|
||||
<a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-default">@Localizer["Cancel"]</a>
|
||||
</form>
|
||||
53
src/Yavsc.Org/Views/Client/ShowSecret.cshtml
Normal file
53
src/Yavsc.Org/Views/Client/ShowSecret.cshtml
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
||||
<h2>@Localizer["New client secret"]</h2>
|
||||
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<strong>@Localizer["This is the only time this secret will be displayed."]</strong>
|
||||
@Localizer["Copy it now and store it in a safe place. After you leave this page, the value cannot be recovered — only its hash is stored on the server."]
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>@clientName</h4>
|
||||
<hr />
|
||||
<dl class="dl-horizontal">
|
||||
<dt>@Localizer["Client identifier"]</dt>
|
||||
<dd>@clientId</dd>
|
||||
<dt>@Localizer["Expires at"]</dt>
|
||||
<dd>@expiresAt</dd>
|
||||
<dt>@Localizer["Secret value"]</dt>
|
||||
<dd>
|
||||
<pre id="newSecretValue" class="pre-scrollable" style="user-select: all;">@secret</pre>
|
||||
<button type="button" class="btn btn-default" onclick="copySecret()">@Localizer["Copy"]</button>
|
||||
<span id="copyFeedback" class="text-success" style="display:none;">@Localizer["Copied!"]</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<a asp-action="Details" asp-route-id="@clientId" class="btn btn-primary">@Localizer["I have saved the secret — return to client"]</a>
|
||||
</p>
|
||||
|
||||
<script>
|
||||
function copySecret() {
|
||||
var text = document.getElementById('newSecretValue').innerText;
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(function () {
|
||||
var fb = document.getElementById('copyFeedback');
|
||||
fb.style.display = 'inline';
|
||||
setTimeout(function () { fb.style.display = 'none'; }, 2000);
|
||||
});
|
||||
} else {
|
||||
// Fallback for browsers without clipboard API
|
||||
var range = document.createRange();
|
||||
range.selectNode(document.getElementById('newSecretValue'));
|
||||
window.getSelection().removeAllRanges();
|
||||
window.getSelection().addRange(range);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -82,7 +82,10 @@
|
|||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"Http": {
|
||||
"Url": "http://localhost:3002"
|
||||
"Url": "http://localhost:3003"
|
||||
},
|
||||
"Https": {
|
||||
"Url": "https://localhost:3004"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,18 +119,8 @@ namespace yavscTests.ServerFixtures
|
|||
|
||||
public async Task SetupHost()
|
||||
{
|
||||
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
|
||||
var slnDir = builder.Configuration.GetValue<string>("SLNDIR");
|
||||
Assert.NotNull(slnDir);
|
||||
|
||||
var yavscOrgPath = Path.GetFullPath(
|
||||
Path.Combine(slnDir,
|
||||
"src/Yavsc.Org"));
|
||||
|
||||
|
||||
Directory.SetCurrentDirectory(yavscOrgPath);
|
||||
builder.AddConfiguration("org").AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
[$"ConnectionStrings:{YavscConstants.YavscConnectionStringName}"] = "InMemory"
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@
|
|||
"UserName": "fakeuser",
|
||||
"Password": "f/\\kePassw0rd"
|
||||
}
|
||||
},
|
||||
"SLNDIR": "/home/paul/Workspace/yavsc"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue