fullstackhero/dotnet-starter-kit · error · UnauthorizedException

tenant validity has expired

Error message

tenant {tenant.Id} validity has expired

What it means

ValidateTenantStatus (IdentityService) throws UnauthorizedException when the current UTC time is past the tenant's ValidUpto date plus the configured billing grace period. It blocks authentication (login and refresh-token flows) for tenants whose subscription has fully lapsed. The grace period deliberately lets a lapsed tenant keep authenticating for a few days before hard cutoff.

Solutions

  1. Renew the tenant subscription so tenant.ValidUpto is extended past today
  2. Check the tenant's ValidUpto value in the tenants table and compare with server UTC time
  3. Verify _gracePeriodDays configuration — increase it if the business intent is a longer grace window
  4. Fix server clock/NTP skew if the server clock is ahead

Example fix

// before (seed data)
ValidUpto = DateTime.UtcNow.AddDays(-30)
// after
ValidUpto = DateTime.UtcNow.AddYears(1)
Defensive patterns

Strategy: validation

Validate before calling

var tenant = await db.Tenants.FirstOrDefaultAsync(t => t.Id == tenantId, ct);
if (tenant is null || DateTime.UtcNow > tenant.ValidUpto.AddDays(gracePeriodDays))
    return Results.Redirect("/subscription-renewed-required");

Try / catch

try
{
    await authService.LoginAsync(request, ct);
}
catch (UnauthorizedException ex) when (ex.Message.Contains("validity has expired"))
{
    return Results.Problem(statusCode: 402, detail: "Tenant subscription expired — renew to continue.");
}

Prevention

When it happens

Trigger: Calling login (ValidateCredentialsAsync) or token refresh (ValidateRefreshTokenAsync) for a user whose tenant.ValidUpto + _gracePeriodDays is earlier than TimeProvider.GetUtcNow().

Common situations: Expired subscription/billing lapse in staging or production; system clock skew (server clock ahead of DB dates); seed/test data with stale ValidUpto values; grace period configured to 0 days.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/d838ceaec4ff22bf. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Services/IdentityService.cs:293

    }

    private void ValidateTenantStatus(AppTenantInfo tenant)
    {
        if (tenant.Id == MultitenancyConstants.Root.Id)
        {
            return;
        }

        if (!tenant.IsActive)
        {
            throw new UnauthorizedException($"tenant {tenant.Id} is deactivated");
        }

        // Honor the billing grace period: a lapsed tenant can still authenticate until
        // ValidUpto + grace (matching the request-time guard in MultitenancyModule).
        if (_timeProvider.GetUtcNow().UtcDateTime > tenant.ValidUpto.AddDays(_gracePeriodDays))
        {
            throw new UnauthorizedException($"tenant {tenant.Id} validity has expired");
        }
    }

    private async Task<List<Claim>> BuildUserClaimsAsync(FshUser user, string tenantId, CancellationToken ct)
    {
        var claims = CreateBasicClaims(user, tenantId);
        await AddRoleClaimsAsync(claims, user, ct);
        return claims;
    }

    private static List<Claim> CreateBasicClaims(FshUser user, string tenantId)
    {
        var fullName = $"{user.FirstName} {user.LastName}".Trim();
        return
        [
            new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            // RFC 7519 short-form sub/name/email emitted alongside legacy ClaimTypes.* so JWT consumers read them per spec.
            // `name` is published explicitly because the default outbound map turns ClaimTypes.Name into `unique_name`, not `name`.

View on GitHub (pinned to 3f2959e683)