{"record":{"id":"223ef84d449e8cd2","repo":"elsa-workflows/elsa-core","slug":"redirectrejected","errorCode":"RedirectRejected","errorMessage":"ProviderHttpException(ProviderHttpFailure.RedirectRejected)","messagePattern":"ProviderHttpException\\(ProviderHttpFailure\\.RedirectRejected\\)","errorType":"error_code","errorClass":"ProviderHttpException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.ExternalAuthentication/Services/ProviderHttpClientFactory.cs","lineNumber":100,"sourceCode":"    private async ValueTask<ProviderHttpResponse> SendAsync(Uri uri, ProviderResponseKind kind, Func<Uri, HttpRequestMessage> createRequest, CancellationToken cancellationToken)\n    {\n        var redirects = 0;\n        var current = uri;\n        using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);\n        timeout.CancelAfter(options.Value.ProviderEgress.RequestTimeout);\n\n        try\n        {\n            while (true)\n            {\n                await destinationValidator.ValidateAsync(current, timeout.Token);\n                using var request = createRequest(current);\n                using var response = await invoker.SendAsync(request, timeout.Token);\n\n                if (IsRedirect(response.StatusCode))\n                {\n                    if (kind is ProviderResponseKind.Token or ProviderResponseKind.UserInfo || response.Headers.Location is null || redirects++ >= options.Value.ProviderEgress.MaximumRedirects)\n                        throw new ProviderHttpException(ProviderHttpFailure.RedirectRejected);\n\n                    current = new(current, response.Headers.Location);\n                    continue;\n                }\n\n                if (!response.IsSuccessStatusCode)\n                    return new(response.StatusCode, []);\n\n                return new(response.StatusCode, await ReadResponseBodyAsync(response, kind, timeout.Token));\n            }\n        }\n        catch (OutboundDestinationException)\n        {\n            throw new ProviderHttpException(ProviderHttpFailure.DestinationRejected);\n        }\n        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)\n        {\n            throw new ProviderHttpException(ProviderHttpFailure.Timeout);","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.ExternalAuthentication/Services/ProviderHttpClientFactory.cs#L82-L118","documentation":"ProviderHttpClient deliberately disables automatic redirects (AllowAutoRedirect = false) so every hop can be revalidated against the outbound destination policy. This ProviderHttpException(ProviderHttpFailure.RedirectRejected) is thrown when a redirect is received but cannot be followed safely: the response kind is Token or UserInfo (security-sensitive endpoints must never redirect), the Location header is missing, or the maximum redirect count configured in ProviderEgress.MaximumRedirects has been exceeded.","triggerScenarios":"Calling IProviderHttpClient.GetAsync/PostFormAsync where the provider responds 301/302/303/307/308 and any of: kind is ProviderResponseKind.Token or ProviderResponseKind.UserInfo; the response has no Location header; more than ProviderEgress.MaximumRedirects redirects are returned; or a subsequent hop fails destination validation (which itself becomes DestinationRejected, not this error).","commonSituations":"Provider moving its token endpoint behind a redirect (common with provider migrations) — token/userinfo redirects are rejected outright; http→https or trailing-slash redirects on discovery documents causing redirect loops that exhaust MaximumRedirects; a misconfigured authority URL pointing at a redirecting landing page instead of the OIDC discovery document.","solutions":["Point your configuration at the final, direct URL for the endpoint (authority, token endpoint, userinfo endpoint) so no redirect is needed — especially for Token and UserInfo calls, which are always rejected on redirect.","If redirects are legitimate for Discovery/SigningKeys requests, raise ProviderEgress.MaximumRedirects in ExternalAuthenticationOptions.","Check for a redirect loop: the same host responding redirect→redirect means a server-side configuration problem; fetch the URL with curl -I to see the chain and fix the provider or the configured URL.","Verify the Location target would pass outbound-destination validation; if the hop is to a disallowed host, allowlist that host or correct the provider's redirect target."],"exampleFix":"// before — authority URL redirects to /.well-known/openid-configuration\noptions.Authority = new Uri(\"https://idp.example.com\");\n\n// after — use the final non-redirecting discovery root\noptions.Authority = new Uri(\"https://idp.example.com/realms/main\");","handlingStrategy":"try-catch","validationCode":"// pre-flight: detect redirects (and loops) for discovery URLs before using them\ncurl -sIL -o /dev/null -w \"%{http_code} %{num_redirects} %{url_effective}\" https://idp.example.com/.well-known/openid-configuration\n// in C#: use HttpClientHandler(AllowAutoRedirect=false) to probe; reject if any 3xx appears for Token/UserInfo endpoints","typeGuard":"static bool IsRedirectRejectedCase(ProviderHttpException ex, ProviderResponseKind kind) =>\n    ex.Failure == ProviderHttpFailure.RedirectRejected &&\n    kind is ProviderResponseKind.Token or ProviderResponseKind.UserInfo;","tryCatchPattern":"try\n{\n    var response = await client.GetAsync(tokenEndpoint, ProviderResponseKind.Token, ct);\n}\ncatch (ProviderHttpException ex) when (ex.Failure == ProviderHttpFailure.RedirectRejected)\n{\n    logger.LogWarning(\"Provider endpoint attempted a redirect; reconfigure to the final URL. Kind: {Kind}\", kind);\n    throw; // never auto-follow redirects for token/userinfo — that is the security boundary\n}","preventionTips":["Always configure the final, non-redirecting URL for token and userinfo endpoints — redirects are never followed for these kinds.","Probe discovery URLs with curl -I (HEAD) after any provider change to catch new redirect behavior early.","Keep ProviderEgress.MaximumRedirects small (1–2) so loops fail fast instead of silently eating redirects.","Monitor/logs for RedirectRejected — a sudden appearance usually means the provider changed its endpoint layout."],"tags":["http","security","oauth","redirect"],"backgroundTag":"http-error-response","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}