{"record":{"id":"d838ceaec4ff22bf","repo":"fullstackhero/dotnet-starter-kit","slug":"tenant-tenant-id-validity-has-expired","errorCode":null,"errorMessage":"tenant {tenant.Id} validity has expired","messagePattern":"tenant (.+?) validity has expired","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Services/IdentityService.cs","lineNumber":293,"sourceCode":"    }\n\n    private void ValidateTenantStatus(AppTenantInfo tenant)\n    {\n        if (tenant.Id == MultitenancyConstants.Root.Id)\n        {\n            return;\n        }\n\n        if (!tenant.IsActive)\n        {\n            throw new UnauthorizedException($\"tenant {tenant.Id} is deactivated\");\n        }\n\n        // Honor the billing grace period: a lapsed tenant can still authenticate until\n        // ValidUpto + grace (matching the request-time guard in MultitenancyModule).\n        if (_timeProvider.GetUtcNow().UtcDateTime > tenant.ValidUpto.AddDays(_gracePeriodDays))\n        {\n            throw new UnauthorizedException($\"tenant {tenant.Id} validity has expired\");\n        }\n    }\n\n    private async Task<List<Claim>> BuildUserClaimsAsync(FshUser user, string tenantId, CancellationToken ct)\n    {\n        var claims = CreateBasicClaims(user, tenantId);\n        await AddRoleClaimsAsync(claims, user, ct);\n        return claims;\n    }\n\n    private static List<Claim> CreateBasicClaims(FshUser user, string tenantId)\n    {\n        var fullName = $\"{user.FirstName} {user.LastName}\".Trim();\n        return\n        [\n            new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),\n            // RFC 7519 short-form sub/name/email emitted alongside legacy ClaimTypes.* so JWT consumers read them per spec.\n            // `name` is published explicitly because the default outbound map turns ClaimTypes.Name into `unique_name`, not `name`.","sourceCodeStart":275,"sourceCodeEnd":311,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Services/IdentityService.cs#L275-L311","documentation":"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.","triggerScenarios":"Calling login (ValidateCredentialsAsync) or token refresh (ValidateRefreshTokenAsync) for a user whose tenant.ValidUpto + _gracePeriodDays is earlier than TimeProvider.GetUtcNow().","commonSituations":"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.","solutions":["Renew the tenant subscription so tenant.ValidUpto is extended past today","Check the tenant's ValidUpto value in the tenants table and compare with server UTC time","Verify _gracePeriodDays configuration — increase it if the business intent is a longer grace window","Fix server clock/NTP skew if the server clock is ahead"],"exampleFix":"// before (seed data)\nValidUpto = DateTime.UtcNow.AddDays(-30)\n// after\nValidUpto = DateTime.UtcNow.AddYears(1)","handlingStrategy":"validation","validationCode":"var tenant = await db.Tenants.FirstOrDefaultAsync(t => t.Id == tenantId, ct);\nif (tenant is null || DateTime.UtcNow > tenant.ValidUpto.AddDays(gracePeriodDays))\n    return Results.Redirect(\"/subscription-renewed-required\");","typeGuard":null,"tryCatchPattern":"try\n{\n    await authService.LoginAsync(request, ct);\n}\ncatch (UnauthorizedException ex) when (ex.Message.Contains(\"validity has expired\"))\n{\n    return Results.Problem(statusCode: 402, detail: \"Tenant subscription expired — renew to continue.\");\n}","preventionTips":["Monitor tenant ValidUpto dates and alert before expiry","Keep the billing grace period deliberately configured (not 0) for lapsed tenants","Sync server clocks with NTP to avoid false expiry","Seed test environments with future ValidUpto values"],"tags":["auth","multitenancy","billing","expired"],"backgroundTag":"authentication-required","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}