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

  1. Restart the authorization flow and use a fresh, unused authorization code or refresh token.
  2. Verify the client sends the code (or refresh_token) correctly in the token request to the same tenant that issued it.
  3. Check that server encryption/signing certificates in App_Data have not been deleted or replaced since the token was issued.
  4. 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

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


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)