{"record":{"id":"dffebce163b01d47","repo":"elsa-workflows/elsa-core","slug":"the-external-authentication-session-is-no-longer-valid","errorCode":null,"errorMessage":"The external authentication session is no longer valid.","messagePattern":"The external authentication session is no longer valid\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.ExternalAuthentication/Services/DefaultExternalAuthenticationTokenIssuer.cs","lineNumber":47,"sourceCode":"        session.CurrentRefreshTokenHash = Hash(refreshToken);\n        await sessionStore.SaveAsync(session, cancellationToken);\n        return await IssueResponseAsync(session, refreshToken, cancellationToken);\n    }\n\n    public async ValueTask<ExternalTokenResponse> RefreshAsync(string clientId, SensitiveString refreshToken, CancellationToken cancellationToken = default)\n    {\n        var rawToken = refreshToken.Reveal();\n        var separator = rawToken.IndexOf('.', StringComparison.Ordinal);\n        if (separator <= 0 || separator == rawToken.Length - 1)\n            throw new InvalidOperationException(\"The external refresh token is invalid.\");\n        var sessionId = rawToken[..separator];\n        var currentHash = Hash(rawToken);\n        var session = await sessionStore.FindByIdAsync(sessionId, cancellationToken);\n        if (session is null || !string.Equals(session.AuthenticationClientId, clientId, StringComparison.Ordinal))\n            throw new InvalidOperationException(\"The external refresh token is invalid.\");\n        var connection = await connectionRegistry.FindByKeyAsync(session.TenantId, session.ConnectionKey, cancellationToken);\n        if (session.RevokedAt != null || session.ExpiresAt <= clock.UtcNow || connection is null || connection.IsShadowed || !connection.Connection.IsEnabled || connection.Connection.ArchivedAt is not null || !string.Equals(connection.Connection.MaterialRevision, session.ConnectionMaterialRevision, StringComparison.Ordinal))\n            throw new InvalidOperationException(\"The external authentication session is no longer valid.\");\n        if (!string.Equals(session.SecretGenerationFingerprint, await GetSecretFingerprintAsync(connection.Connection.SecretBindings, cancellationToken), StringComparison.Ordinal))\n            throw new InvalidOperationException(\"The external authentication session secrets changed.\");\n\n        var nextToken = CreateRefreshToken(session.Id);\n        var rotation = await sessionStore.TryRotateRefreshTokenAsync(session.Id, currentHash, session.RefreshGeneration, Hash(nextToken), clock.UtcNow, cancellationToken);\n        if (rotation is not ExternalAuthenticationSessionRotationResult.Rotated { Session: var rotated })\n            throw new InvalidOperationException(\"The external refresh token cannot be used.\");\n\n        return await IssueResponseAsync(rotated, nextToken, cancellationToken);\n    }\n\n    private async ValueTask<ExternalTokenResponse> IssueResponseAsync(ExternalAuthenticationSession session, string refreshToken, CancellationToken cancellationToken)\n    {\n        using var tenantContext = tenantAccessor.PushContext(new()\n            { Id = session.TenantId, Name = session.TenantId });\n        var user = await userProvider.FindAsync(new()\n                       { Id = session.UserId }, cancellationToken)\n            ?? throw new InvalidOperationException(\"The external authentication session user no longer exists.\");","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.ExternalAuthentication/Services/DefaultExternalAuthenticationTokenIssuer.cs#L29-L65","documentation":"After parsing the token and loading the session, RefreshAsync validates that the session is still live and that its backing external connection is unchanged. This is thrown when the session is revoked, expired, the connection no longer exists, is shadowed/disabled/archived, or the connection's MaterialRevision differs from the revision recorded at sign-in — i.e. the session's snapshot of the external connection is stale and refresh must not proceed.","triggerScenarios":"Refreshing a token for a session where: session.RevokedAt is set; session.ExpiresAt <= now; connectionRegistry.FindByKeyAsync returns null; connection.IsShadowed is true; connection.Connection.IsEnabled is false; connection.Connection.ArchivedAt is set; or connection.Connection.MaterialRevision != session.ConnectionMaterialRevision (connection edited/re-imported since sign-in).","commonSituations":"An admin disabled, archived, or edited the external connection while users hold live sessions; a re-deployment re-created the connection with a new material revision; a refresh-replay test reused an already-rotated/revoked token; long-lived sessions exceeding the session expiry window; the tenant's connection was deleted.","solutions":["Treat this as a terminal auth failure: discard the stored tokens and redirect the user through a fresh IssueAsync (sign-in) flow.","If it appeared after a deployment, check whether the external connection was recreated or edited — its MaterialRevision changed, so all prior sessions are intentionally invalidated.","If the connection should still be valid, verify it is enabled, not archived, not shadowed, and still registered under the session's TenantId + ConnectionKey.","If revocation was unintended, investigate what called the revoke path (rotation replay or admin action) before re-issuing."],"exampleFix":"// before\ntry { return await issuer.RefreshAsync(token, clientId); }\ncatch (InvalidOperationException) { throw; }\n\n// after\ntry\n{\n    return await issuer.RefreshAsync(token, clientId);\n}\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"no longer valid\"))\n{\n    // session/connection is dead by design — force full re-authentication\n    await signInManager.SignOutAsync();\n    return Results.Challenge();\n}","handlingStrategy":"try-catch","validationCode":"// clients cannot inspect session/connection state directly; before refresh you can only check expiry locally\nif ( DateTimeOffset.UtcNow >= storedSessionExpiresAt )\n    return await ReAuthenticateAsync(); // skip doomed refresh call","typeGuard":null,"tryCatchPattern":"try\n{\n    return await issuer.RefreshAsync(refreshToken, clientId);\n}\ncatch (InvalidOperationException ex) when (ex.Message == \"The external authentication session is no longer valid.\")\n{\n    ClearStoredTokens();\n    return await ReAuthenticateAsync();\n}","preventionTips":["Treat external connection edits (disable/archive/revision change) as a sign-out event for affected users.","Track session expiry client-side and re-authenticate proactively before expiry.","Notify users when a connection they depend on is disabled or re-imported.","Never assume refresh tokens remain valid across deployments that touch connection definitions."],"tags":["authentication","session-expired","token-refresh","connection-state"],"backgroundTag":"jwt-token-expired","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}