{"record":{"id":"67558786b2524381","repo":"OrchardCMS/OrchardCore","slug":"the-user-principal-cannot-be-resolved","errorCode":null,"errorMessage":"The user principal cannot be resolved.","messagePattern":"The user principal cannot be resolved\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/OrchardCore.Modules/OrchardCore.OpenId/Controllers/AccessController.cs","lineNumber":597,"sourceCode":"        var authorization = authorizations.FirstOrDefault();\n        authorization ??= await _authorizationManager.CreateAsync(\n            identity: identity,\n            subject: identity.GetUserIdentifier(),\n            client: await _applicationManager.GetIdAsync(application),\n            type: AuthorizationTypes.Permanent,\n            scopes: identity.GetScopes());\n\n        identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));\n        identity.SetDestinations(GetDestinations);\n\n        return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);\n    }\n\n    private async Task<IActionResult> ExchangeAuthorizationCodeOrRefreshTokenGrantType(OpenIddictRequest request)\n    {\n        // Retrieve the claims principal stored in the authorization code/refresh token.\n        var info = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme) ??\n            throw new InvalidOperationException(\"The user principal cannot be resolved.\");\n\n        if (request.IsRefreshTokenGrantType())\n        {\n            var type = info.Principal.FindFirst(OpenIdConstants.Claims.EntityType)?.Value;\n            if (!string.Equals(type, OpenIdConstants.EntityTypes.User, StringComparison.Ordinal))\n            {\n                return Forbid(new AuthenticationProperties(new Dictionary<string, string>\n                {\n                    [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.UnauthorizedClient,\n                    [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =\n                        \"The refresh token grant type is not allowed for refresh tokens retrieved using the client credentials flow.\",\n                }), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);\n            }\n        }\n\n        // By default, re-use the principal stored in the authorization code/refresh token.\n        var principal = info.Principal;\n","sourceCodeStart":579,"sourceCodeEnd":615,"githubUrl":"https://github.com/OrchardCMS/OrchardCore/blob/4306c0717fe573f6fca1b4955909ddab6a192807/src/OrchardCore.Modules/OrchardCore.OpenId/Controllers/AccessController.cs#L579-L615","documentation":"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.').","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: reusing a redeemed code\nPOST /connect/token  code=<already-used-code>\n// after: obtain a new code via the authorize endpoint, then exchange it once\nPOST /connect/token  grant_type=authorization_code&code=<fresh-code>&redirect_uri=...","handlingStrategy":"retry","validationCode":"// Before exchanging, confirm the code/token is present and not yet redeemed\nif (string.IsNullOrEmpty(request.Code) && string.IsNullOrEmpty(request.RefreshToken))\n    throw new InvalidOperationException(\"Nothing to exchange: code and refresh_token are both missing.\");","typeGuard":"bool CanExchange(OpenIddictRequest r) =>\n    r != null && (!string.IsNullOrEmpty(r.Code) || !string.IsNullOrEmpty(r.RefreshToken));","tryCatchPattern":"try\n{\n    token = await ExchangeCodeAsync(code, redirectUri);\n}\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"user principal cannot be resolved\"))\n{\n    // code likely expired or already redeemed — restart the flow once\n    code = await StartAuthorizationFlowAsync();\n    token = await ExchangeCodeAsync(code, redirectUri);\n}","preventionTips":["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."],"tags":["openid","oauth","token-exchange","authorization-code","refresh-token"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"4306c0717fe573f6fca1b4955909ddab6a192807","analyzedAt":"2026-09-13T17:41:05.024Z","contentChangedAt":"2026-09-13T17:41:05.024Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}