Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions EssentialCSharp.Web.Tests/McpApiTokenServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ public async Task CreateTokenAsync_WithExpiryBeyondSixMonths_Throws()

using var scope = factory.Services.CreateScope();
var tokenService = scope.ServiceProvider.GetRequiredService<McpApiTokenService>();
DateTime requestedExpiry = McpApiTokenService.GetDefaultExpirationUtc(DateTime.UtcNow).AddDays(1);
DateTime requestedExpiry = McpApiTokenService.GetDefaultExpirationUtc(DateTime.UtcNow).AddDays(2);

await Assert.That(() => tokenService.CreateTokenAsync(userId, "too-long", requestedExpiry))
.Throws<ArgumentOutOfRangeException>()
.WithMessageContaining("6 months");
.WithMessageContaining(McpApiTokenService.MaxExpiryValidationMessage);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@page
@model McpAccessModel
@using EssentialCSharp.Web.Models
@using EssentialCSharp.Web.Services
@{
ViewData["Title"] = "MCP Access";
ViewData["ActivePage"] = ManageNavPages.McpAccess;
Expand Down Expand Up @@ -61,7 +62,7 @@
<label asp-for="ExpiresOn" class="form-label">Expiry Date</label>
<input asp-for="ExpiresOn" type="date" class="form-control" max="@Model.MaxExpiresOn.ToString("yyyy-MM-dd")" />
<span asp-validation-for="ExpiresOn" class="text-danger"></span>
<div class="form-text">MCP tokens default to 6 months and cannot exceed 6 months from today. The token expires at end of day (23:59:59) UTC on the selected date.</div>
<div class="form-text">MCP tokens expire after @McpApiTokenService.DefaultLifetimeMonths months by default, with a maximum lifetime of @McpApiTokenService.DefaultLifetimeMonths months from today. The token expires at end of day (UTC) on the selected date.</div>
</div>
<button type="submit" class="btn btn-primary">Create Token</button>
</form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ public class McpAccessModel(
public async Task<IActionResult> OnGetAsync()
{
DisableCaching();
InitializeExpiryBounds();
DateTime nowUtc = DateTime.UtcNow;
InitializeExpiryBounds(nowUtc);
ApplyDefaultExpiryIfMissing();
string? userId = userManager.GetUserId(User);
if (userId is null) return Challenge();
Expand All @@ -44,7 +45,9 @@ public async Task<IActionResult> OnGetAsync()
public async Task<IActionResult> OnPostCreateAsync()
{
DisableCaching();
InitializeExpiryBounds();
DateTime nowUtc = DateTime.UtcNow;
DateOnly todayUtc = DateOnly.FromDateTime(nowUtc);
InitializeExpiryBounds(nowUtc);
ApplyDefaultExpiryIfMissing();
string? userId = userManager.GetUserId(User);
Comment on lines +48 to 52
if (userId is null) return Challenge();
Expand All @@ -54,7 +57,7 @@ public async Task<IActionResult> OnPostCreateAsync()

if (ExpiresOn.HasValue)
{
if (ExpiresOn.Value < DateOnly.FromDateTime(DateTime.UtcNow))
if (ExpiresOn.Value < todayUtc)
ModelState.AddModelError(nameof(ExpiresOn), "Expiry date must be today or in the future.");
if (ExpiresOn.Value > MaxExpiresOn)
ModelState.AddModelError(nameof(ExpiresOn), McpApiTokenService.MaxExpiryValidationMessage);
Expand Down Expand Up @@ -97,9 +100,9 @@ private void DisableCaching()
Response.Headers.Expires = "0";
}

private void InitializeExpiryBounds()
private void InitializeExpiryBounds(DateTime nowUtc)
{
MaxExpiresOn = McpApiTokenService.GetDefaultExpiryDate();
MaxExpiresOn = McpApiTokenService.GetDefaultExpiryDate(nowUtc);
}

private void ApplyDefaultExpiryIfMissing()
Expand Down
6 changes: 4 additions & 2 deletions EssentialCSharp.Web/Controllers/McpTokenController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ public async Task<IActionResult> CreateToken(
if (name.Length > 256)
return BadRequest(new { Error = "Token name must be 256 characters or fewer." });

DateTime nowUtc = DateTime.UtcNow;
DateOnly todayUtc = DateOnly.FromDateTime(nowUtc);
DateTime? expiresAt = null;
DateOnly maxExpiresOn = McpApiTokenService.GetDefaultExpiryDate();
DateOnly maxExpiresOn = McpApiTokenService.GetDefaultExpiryDate(nowUtc);
if (request?.ExpiresOn is DateOnly expiresOn)
Comment on lines +29 to 33
{
if (expiresOn < DateOnly.FromDateTime(DateTime.UtcNow))
if (expiresOn < todayUtc)
return BadRequest(new { Error = "ExpiresOn must be today or in the future." });
if (expiresOn > maxExpiresOn)
return BadRequest(new { Error = McpApiTokenService.MaxExpiryValidationMessage });
Expand Down
2 changes: 1 addition & 1 deletion EssentialCSharp.Web/Services/McpApiTokenService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace EssentialCSharp.Web.Services;
public class McpApiTokenService(EssentialCSharpWebContext db)
{
public const int DefaultLifetimeMonths = 6;
public const string MaxExpiryValidationMessage = "MCP tokens can expire at most 6 months from today.";
public static readonly string MaxExpiryValidationMessage = $"MCP tokens can expire at most {DefaultLifetimeMonths} months from today.";

public sealed record ResolvedMcpApiToken(Guid TokenId, string UserId);

Expand Down
Loading