elsa-workflows/elsa-core · error · InvalidOperationException
The external authentication session is no longer valid.
Error message
The external authentication session is no longer valid.
What it means
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.
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.
Example fix
// before
try { return await issuer.RefreshAsync(token, clientId); }
catch (InvalidOperationException) { throw; }
// after
try
{
return await issuer.RefreshAsync(token, clientId);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("no longer valid"))
{
// session/connection is dead by design — force full re-authentication
await signInManager.SignOutAsync();
return Results.Challenge();
} Defensive patterns
Strategy: try-catch
Validate before calling
// clients cannot inspect session/connection state directly; before refresh you can only check expiry locally
if ( DateTimeOffset.UtcNow >= storedSessionExpiresAt )
return await ReAuthenticateAsync(); // skip doomed refresh call Try / catch
try
{
return await issuer.RefreshAsync(refreshToken, clientId);
}
catch (InvalidOperationException ex) when (ex.Message == "The external authentication session is no longer valid.")
{
ClearStoredTokens();
return await ReAuthenticateAsync();
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- The external authentication session secrets changed.
- The external refresh token cannot be used.
- The external authentication session user no longer exists.
- The identity provider rejected the authentication request.
- The identity provider callback could not be correlated.
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/dffebce163b01d47.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.ExternalAuthentication/Services/DefaultExternalAuthenticationTokenIssuer.cs:47
session.CurrentRefreshTokenHash = Hash(refreshToken);
await sessionStore.SaveAsync(session, cancellationToken);
return await IssueResponseAsync(session, refreshToken, cancellationToken);
}
public async ValueTask<ExternalTokenResponse> RefreshAsync(string clientId, SensitiveString refreshToken, CancellationToken cancellationToken = default)
{
var rawToken = refreshToken.Reveal();
var separator = rawToken.IndexOf('.', StringComparison.Ordinal);
if (separator <= 0 || separator == rawToken.Length - 1)
throw new InvalidOperationException("The external refresh token is invalid.");
var sessionId = rawToken[..separator];
var currentHash = Hash(rawToken);
var session = await sessionStore.FindByIdAsync(sessionId, cancellationToken);
if (session is null || !string.Equals(session.AuthenticationClientId, clientId, StringComparison.Ordinal))
throw new InvalidOperationException("The external refresh token is invalid.");
var connection = await connectionRegistry.FindByKeyAsync(session.TenantId, session.ConnectionKey, cancellationToken);
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))
throw new InvalidOperationException("The external authentication session is no longer valid.");
if (!string.Equals(session.SecretGenerationFingerprint, await GetSecretFingerprintAsync(connection.Connection.SecretBindings, cancellationToken), StringComparison.Ordinal))
throw new InvalidOperationException("The external authentication session secrets changed.");
var nextToken = CreateRefreshToken(session.Id);
var rotation = await sessionStore.TryRotateRefreshTokenAsync(session.Id, currentHash, session.RefreshGeneration, Hash(nextToken), clock.UtcNow, cancellationToken);
if (rotation is not ExternalAuthenticationSessionRotationResult.Rotated { Session: var rotated })
throw new InvalidOperationException("The external refresh token cannot be used.");
return await IssueResponseAsync(rotated, nextToken, cancellationToken);
}
private async ValueTask<ExternalTokenResponse> IssueResponseAsync(ExternalAuthenticationSession session, string refreshToken, CancellationToken cancellationToken)
{
using var tenantContext = tenantAccessor.PushContext(new()
{ Id = session.TenantId, Name = session.TenantId });
var user = await userProvider.FindAsync(new()
{ Id = session.UserId }, cancellationToken)
?? throw new InvalidOperationException("The external authentication session user no longer exists.");View on GitHub (pinned to fe9217bdfa)