elsa-workflows/elsa-core · error · InvalidOperationException
The external authentication session user no longer exists.
Error message
The external authentication session user no longer exists.
What it means
IssueResponseAsync resolves the user recorded on the external authentication session via userProvider.FindAsync before issuing the token response. This is thrown when the user no longer exists — the session outlived its user (deleted or removed from the user store), so Elsa cannot re-evaluate roles and must refuse to issue tokens.
Solutions
- Force re-authentication: the user must sign in again (or be re-provisioned) before any token can be issued.
- Check whether a user cleanup/deactivation job deleted accounts that still hold sessions; consider cascading session revocation on user delete.
- Verify the user provider is pointed at the correct store/database and the user's Id is present.
- Confirm the tenant context matches: the lookup runs under session.TenantId, so a user moved across tenants will not resolve.
Example fix
// before: assume user always exists at refresh
var resp = await issuer.RefreshAsync(token, clientId);
// after
try
{
return await issuer.RefreshAsync(token, clientId);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("user no longer exists"))
{
await revokeStoredSessionAsync();
return Results.Challenge(); // user deleted — full re-auth (or account re-provisioning) required
} Defensive patterns
Strategy: try-catch
Try / catch
try
{
return await issuer.RefreshAsync(refreshToken, clientId);
}
catch (InvalidOperationException ex) when (ex.Message == "The external authentication session user no longer exists.")
{
// session outlived its user — cannot be recovered by retry
ClearStoredTokens();
return Results.Unauthorized(); // or route to sign-in / account restoration
} Prevention
- Revoke external authentication sessions when deleting or deactivating users (cascade on user delete).
- Verify the user provider points at the same store that issued the session (no environment drift).
- Ensure users are not moved across tenants while holding sessions, since lookup is tenant-scoped.
- Handle this as terminal: only re-provisioning the user or fresh sign-in can recover.
When it happens
Trigger: Calling IssueAsync for a session whose UserId does not resolve in the user provider, or calling RefreshAsync: the refresh path reaches IssueResponseAsync after successful rotation and throws if the session's user was deleted between sign-in and refresh.
Common situations: An admin or cleanup job deleted/deactivated the user while the user still held a refresh token; user store switched providers or databases and the user id is absent there; tenant-scoped lookup misses the user because the session's TenantId no longer matches where the user lives.
Understand the failure class
Background: "User not found", "Invalid user", and "does not exist": what missing-user lookup errors mean across Rocket.Chat, LiteLLM, Phabricator, rustfs, and pnpm — this error's family across 10 libraries.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- The external refresh token is invalid.
- The external authentication session is no longer valid.
- The external authentication session secrets changed.
- The external refresh token cannot be used.
- The identity provider rejected the authentication request.
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/412860ab80589deb.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.ExternalAuthentication/Services/DefaultExternalAuthenticationTokenIssuer.cs:65
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.");
var roles = (await roleProvider.FindByIdsAsync(user.Roles, cancellationToken)).ToArray();
// Role permissions go through the same deployment boundary as the external grants beside them. They
// used to be concatenated raw, which let a permission the boundary had just excluded during grant
// resolution reappear here from the same roles -- making the deny list unenforceable for anything a
// role happened to carry, and ElsaRolePermissionGrantSource's own filtering pointless. Re-applying it
// at issuance also picks up a boundary that changed since sign-in, because refreshing reissues.
// With no boundary configured, which is the default, every well-formed permission passes and nothing
// about this changes.
var boundary = new PermissionGrantBoundary(options.Value.PermissionGrants);
var permissions = roles.SelectMany(x => x.Permissions)
.Concat(session.ExternalGrants.Select(x => x.Permission))
.Where(boundary.Allows)
.Distinct(StringComparer.Ordinal)
.ToArray();
var accessToken = await tokenService.IssueAccessTokenAsync(new(user, roles.Select(x => x.Name).ToArray(), permissions, [], session.Id), cancellationToken);
var now = clock.UtcNow;
return new(
accessToken.Token,View on GitHub (pinned to fe9217bdfa)