Skip to content

Add 6 month default and max MCP token expiry#1043

Merged
BenjaminMichaelis merged 3 commits intomainfrom
copilot/add-default-max-mcp-token-expiration
Apr 30, 2026
Merged

Add 6 month default and max MCP token expiry#1043
BenjaminMichaelis merged 3 commits intomainfrom
copilot/add-default-max-mcp-token-expiration

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 29, 2026

  • Inspect MCP token creation flow and current expiry behavior
  • Run existing tests to capture baseline issues
  • Add shared 6-month default and maximum MCP token expiration enforcement
  • Update MCP API and account management UI messaging/validation to reflect the 6-month default and max
  • Add or update tests covering default and maximum MCP token expiration behavior
  • Run targeted validation for the affected projects
  • Run final review/security validation and open a PR

Copilot AI and others added 3 commits April 29, 2026 16:02
Agent-Logs-Url: https://github.com/IntelliTect/EssentialCSharp.Web/sessions/29b86c94-db59-4104-8285-197ae2428813

Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com>
Agent-Logs-Url: https://github.com/IntelliTect/EssentialCSharp.Web/sessions/29b86c94-db59-4104-8285-197ae2428813

Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com>
Agent-Logs-Url: https://github.com/IntelliTect/EssentialCSharp.Web/sessions/29b86c94-db59-4104-8285-197ae2428813

Co-authored-by: BenjaminMichaelis <22186029+BenjaminMichaelis@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enforces a 6-month default and maximum expiration window for MCP API tokens across the service layer, API endpoint, and account management UI.

Changes:

  • Added shared default/max expiry logic to McpApiTokenService and applied it during token creation.
  • Updated MCP token creation validation/messaging in both the API controller and Identity management UI.
  • Added integration tests covering default expiry, custom expiry within bounds, and rejection beyond the max.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
EssentialCSharp.Web/Views/McpSetup/Index.cshtml Updates setup guide wording to reflect the new default expiry behavior.
EssentialCSharp.Web/Services/McpApiTokenService.cs Centralizes 6-month default/max expiry calculation and enforces it in CreateTokenAsync.
EssentialCSharp.Web/Controllers/McpTokenController.cs Adds max-expiry validation for the API token creation endpoint.
EssentialCSharp.Web/Areas/Identity/Pages/Account/Manage/McpAccess.cshtml.cs Adds server-side expiry bounds/defaulting and validation in the account management flow.
EssentialCSharp.Web/Areas/Identity/Pages/Account/Manage/McpAccess.cshtml Updates UI constraints (max attribute) and messaging for the new expiry rules.
EssentialCSharp.Web.Tests/McpApiTokenServiceTests.cs Adds tests for default expiry, valid custom expiry, and max-expiry enforcement.

Comment on lines +48 to +52
DateTime requestedExpiry = McpApiTokenService.GetDefaultExpirationUtc(DateTime.UtcNow).AddDays(1);

await Assert.That(() => tokenService.CreateTokenAsync(userId, "too-long", requestedExpiry))
.Throws<ArgumentOutOfRangeException>()
.WithMessageContaining("6 months");
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test can be flaky around a UTC midnight boundary: requestedExpiry is computed from DateTime.UtcNow, but CreateTokenAsync computes createdAt from a separate UtcNow. If the date rolls over between those calls, the service's computed max can advance by a day and the requestedExpiry may no longer be beyond the max, causing intermittent failures. Consider making requestedExpiry unambiguously beyond any possible max (e.g., more than 1 day past), or refactoring token creation to accept/inject a reference time so tests can be deterministic.

Copilot uses AI. Check for mistakes.
public class McpApiTokenService(EssentialCSharpWebContext db)
{
public const int DefaultLifetimeMonths = 6;
public const string MaxExpiryValidationMessage = "MCP tokens can expire at most 6 months from today.";
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MaxExpiryValidationMessage hard-codes "6 months" while DefaultLifetimeMonths is the actual source of truth. If the lifetime ever changes, the validation message (and any consumers that display it) will become inaccurate; consider formatting the message from DefaultLifetimeMonths (and optionally include the computed max date for clarity).

Suggested change
public const string MaxExpiryValidationMessage = "MCP tokens can expire at most 6 months from today.";
public static string MaxExpiryValidationMessage => $"MCP tokens can expire at most {DefaultLifetimeMonths} months from today.";

Copilot uses AI. Check for mistakes.
Comment on lines 29 to 33
DateTime? expiresAt = null;
DateOnly maxExpiresOn = McpApiTokenService.GetDefaultExpiryDate();
if (request?.ExpiresOn is DateOnly expiresOn)
{
if (expiresOn < DateOnly.FromDateTime(DateTime.UtcNow))
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DateTime.UtcNow is evaluated multiple times during validation (GetDefaultExpiryDate() and the expiresOn < DateOnly.FromDateTime(DateTime.UtcNow) check). If the request crosses a UTC midnight boundary between calls, the computed maxExpiresOn and "today" could disagree, causing inconsistent accept/reject behavior. Capture a single nowUtc at the top of the action and derive both bounds from it (you can pass it into GetDefaultExpiryDate(nowUtc)).

Suggested change
DateTime? expiresAt = null;
DateOnly maxExpiresOn = McpApiTokenService.GetDefaultExpiryDate();
if (request?.ExpiresOn is DateOnly expiresOn)
{
if (expiresOn < DateOnly.FromDateTime(DateTime.UtcNow))
DateTime nowUtc = DateTime.UtcNow;
DateOnly todayUtc = DateOnly.FromDateTime(nowUtc);
DateTime? expiresAt = null;
DateOnly maxExpiresOn = McpApiTokenService.GetDefaultExpiryDate(nowUtc);
if (request?.ExpiresOn is DateOnly expiresOn)
{
if (expiresOn < todayUtc)

Copilot uses AI. Check for mistakes.
Comment on lines 46 to +60
DisableCaching();
InitializeExpiryBounds();
ApplyDefaultExpiryIfMissing();
string? userId = userManager.GetUserId(User);
if (userId is null) return Challenge();

if (string.IsNullOrWhiteSpace(TokenName))
ModelState.AddModelError(nameof(TokenName), "Token name is required.");

if (ExpiresOn.HasValue && ExpiresOn.Value < DateOnly.FromDateTime(DateTime.UtcNow))
ModelState.AddModelError(nameof(ExpiresOn), "Expiry date must be today or in the future.");
if (ExpiresOn.HasValue)
{
if (ExpiresOn.Value < DateOnly.FromDateTime(DateTime.UtcNow))
ModelState.AddModelError(nameof(ExpiresOn), "Expiry date must be today or in the future.");
if (ExpiresOn.Value > MaxExpiresOn)
ModelState.AddModelError(nameof(ExpiresOn), McpApiTokenService.MaxExpiryValidationMessage);
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InitializeExpiryBounds() (via GetDefaultExpiryDate()) and the "today" check each call DateTime.UtcNow separately. Around a UTC date rollover, this can yield inconsistent validation (e.g., MaxExpiresOn computed from one day but the comparison uses the next day). Capture a single nowUtc per request and use it for both MaxExpiresOn and the minimum-date validation (e.g., DateOnly.FromDateTime(nowUtc)).

Copilot uses AI. Check for mistakes.
<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">Leave blank for a non-expiring token. The token expires at end of day (23:59:59) UTC on the selected date.</div>
<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>
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The help text says the token expires at end of day "(23:59:59) UTC", but the code persists TimeOnly.MaxValue (23:59:59.9999999). To avoid inaccurate UX/docs, consider changing the text to just "end of day (UTC)" or align it with the actual persisted precision.

Suggested change
<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 default to 6 months and cannot exceed 6 months from today. The token expires at end of day (UTC) on the selected date.</div>

Copilot uses AI. Check for mistakes.
@BenjaminMichaelis BenjaminMichaelis merged commit 2d3ffbe into main Apr 30, 2026
13 checks passed
@BenjaminMichaelis BenjaminMichaelis deleted the copilot/add-default-max-mcp-token-expiration branch April 30, 2026 07:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants