OrchardCMS/OrchardCore · error · InvalidOperationException
The user principal cannot be resolved.
Error message
The user principal cannot be resolved.
What it means
ExchangeAuthorizationCodeOrRefreshTokenGrantType authenticates the incoming request with the OpenIddict server scheme to recover the principal embedded in the authorization code or refresh token. If HttpContext.AuthenticateAsync returns null (no valid ticket), the controller throws InvalidOperationException('The user principal cannot be resolved.').
Solutions
- Restart the authorization flow and use a fresh, unused authorization code or refresh token.
- Verify the client sends the code (or refresh_token) correctly in the token request to the same tenant that issued it.
- Check that server encryption/signing certificates in App_Data have not been deleted or replaced since the token was issued.
- Confirm the token endpoint, client_id, and client_secret match the application record and that clocks are synchronized.
Example fix
// before: reusing a redeemed code POST /connect/token code=<already-used-code> // after: obtain a new code via the authorize endpoint, then exchange it once POST /connect/token grant_type=authorization_code&code=<fresh-code>&redirect_uri=...
Defensive patterns
Strategy: retry
Validate before calling
// Before exchanging, confirm the code/token is present and not yet redeemed
if (string.IsNullOrEmpty(request.Code) && string.IsNullOrEmpty(request.RefreshToken))
throw new InvalidOperationException("Nothing to exchange: code and refresh_token are both missing."); Type guard
bool CanExchange(OpenIddictRequest r) =>
r != null && (!string.IsNullOrEmpty(r.Code) || !string.IsNullOrEmpty(r.RefreshToken)); Try / catch
try
{
token = await ExchangeCodeAsync(code, redirectUri);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("user principal cannot be resolved"))
{
// code likely expired or already redeemed — restart the flow once
code = await StartAuthorizationFlowAsync();
token = await ExchangeCodeAsync(code, redirectUri);
} Prevention
- Never reuse authorization codes; request a new one after any exchange failure.
- Do not delete or rotate App_Data certificates while outstanding tokens exist.
- Keep server clocks synchronized (NTP) to avoid token validity issues.
- Send the exchange request to the same tenant that issued the code/token.
When it happens
Trigger: Token exchange with a malformed, tampered, expired, or already-redeemed authorization code / refresh token, or a request sent without the code/token so OpenIddict cannot authenticate the ticket; encryption/signing keys changed server-side (App_Data certs replaced) invalidating outstanding tokens.
Common situations: Client retries a code that was already redeemed (codes are single-use); server rotated encryption certificates between issuing and exchanging; clock skew making the code/token invalid; token issued by a different tenant or environment.
Related errors
- The application details cannot be found.
- The specified grant type is not supported.
- The application was concurrently updated and cannot be…
- The authorization was concurrently updated and cannot be…
- The scope was concurrently updated and cannot be persisted…
AI-assisted analysis of OrchardCMS/OrchardCore@4306c0717f (2026-09-13).
Data as JSON: /api/errors/67558786b2524381.
Report an issue: GitHub.
Appendix: source
Thrown at src/OrchardCore.Modules/OrchardCore.OpenId/Controllers/AccessController.cs:597
var authorization = authorizations.FirstOrDefault();
authorization ??= await _authorizationManager.CreateAsync(
identity: identity,
subject: identity.GetUserIdentifier(),
client: await _applicationManager.GetIdAsync(application),
type: AuthorizationTypes.Permanent,
scopes: identity.GetScopes());
identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
identity.SetDestinations(GetDestinations);
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
private async Task<IActionResult> ExchangeAuthorizationCodeOrRefreshTokenGrantType(OpenIddictRequest request)
{
// Retrieve the claims principal stored in the authorization code/refresh token.
var info = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme) ??
throw new InvalidOperationException("The user principal cannot be resolved.");
if (request.IsRefreshTokenGrantType())
{
var type = info.Principal.FindFirst(OpenIdConstants.Claims.EntityType)?.Value;
if (!string.Equals(type, OpenIdConstants.EntityTypes.User, StringComparison.Ordinal))
{
return Forbid(new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.UnauthorizedClient,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The refresh token grant type is not allowed for refresh tokens retrieved using the client credentials flow.",
}), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
}
// By default, re-use the principal stored in the authorization code/refresh token.
var principal = info.Principal;
View on GitHub (pinned to 4306c0717f)