mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 09:28:24 +00:00
Make HTTP 303 redirects unconditional, remove UseHttp303Redirects opt-in flag
HTTP 303 (See Other) is now the mandatory redirect status code for all authorization and end-session redirects. The UserInteractionOptions.UseHttp303Redirects flag and the RedirectWithStatusCode helper have been removed. The four call sites in AuthorizeResult, AuthorizeInteractionPageResult, and EndSessionResult now set the status code and Location header directly. MinVer bumped to 8.0 to signal the breaking change.
This commit is contained in:
parent
30373631c7
commit
cf51c4f7b2
8 changed files with 21 additions and 162 deletions
|
|
@ -1,5 +1,10 @@
|
|||
# IdentityServer Changelog
|
||||
|
||||
# 8.0.0
|
||||
|
||||
## Breaking Changes
|
||||
- HTTP 303 (See Other) is now the unconditional redirect status code for all authorization and end-session redirects. The `UserInteractionOptions.UseHttp303Redirects` opt-in flag has been removed. This aligns IdentityServer with the FAPI 2.0 Security Profile (Section 5.3.2.2, item 11).
|
||||
|
||||
# 7.4.0-preview.1
|
||||
|
||||
## Breaking Changes
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<Product>Duende IdentityServer</Product>
|
||||
<DisableImplicitNamespaceImports>true</DisableImplicitNamespaceImports>
|
||||
<MinVerTagPrefix>is-</MinVerTagPrefix>
|
||||
<MinVerMinimumMajorMinor>7.0</MinVerMinimumMajorMinor>
|
||||
<MinVerMinimumMajorMinor>8.0</MinVerMinimumMajorMinor>
|
||||
<IsIdSrvProject>true</IsIdSrvProject>
|
||||
</PropertyGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -141,13 +141,4 @@ public class UserInteractionOptions
|
|||
/// handle those values.
|
||||
/// </summary>
|
||||
public ICollection<string> PromptValuesSupported { get; set; } = new HashSet<string>(Constants.SupportedPromptModes);
|
||||
|
||||
/// <summary>
|
||||
/// When enabled, uses HTTP 303 (See Other) status code for redirects instead of HTTP 302 (Found).
|
||||
/// This is recommended by the FAPI 2.0 Security Profile (Section 5.3.2.2, item 11) to prevent
|
||||
/// user agents from resubmitting POST data when following redirects.
|
||||
/// See: https://openid.net/specs/fapi-security-profile-2_0-final.html
|
||||
/// Default: false (uses HTTP 302 for backward compatibility)
|
||||
/// </summary>
|
||||
public bool UseHttp303Redirects { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ internal class AuthorizeInteractionPageHttpWriter : IHttpResponseWriter<Authoriz
|
|||
}
|
||||
|
||||
url = url.AddQueryString(result.ReturnUrlParameterName, returnUrl);
|
||||
context.Response.RedirectWithStatusCode(_urls.GetAbsoluteUrl(url), _options.UserInteraction.UseHttp303Redirects);
|
||||
context.Response.StatusCode = StatusCodes.Status303SeeOther;
|
||||
context.Response.Headers.Location = _urls.GetAbsoluteUrl(url);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@ public class AuthorizeHttpWriter : IHttpResponseWriter<AuthorizeResult>
|
|||
response.Request.ResponseMode == OidcConstants.ResponseModes.Fragment)
|
||||
{
|
||||
context.Response.SetNoCache();
|
||||
context.Response.RedirectWithStatusCode(BuildRedirectUri(response), _options.UserInteraction.UseHttp303Redirects);
|
||||
context.Response.StatusCode = StatusCodes.Status303SeeOther;
|
||||
context.Response.Headers.Location = BuildRedirectUri(response);
|
||||
}
|
||||
else if (response.Request.ResponseMode == OidcConstants.ResponseModes.FormPost)
|
||||
{
|
||||
|
|
@ -231,7 +232,8 @@ public class AuthorizeHttpWriter : IHttpResponseWriter<AuthorizeResult>
|
|||
var errorUrl = _options.UserInteraction.ErrorUrl;
|
||||
|
||||
var url = errorUrl.AddQueryString(_options.UserInteraction.ErrorIdParameter, id);
|
||||
context.Response.RedirectWithStatusCode(_urls.GetAbsoluteUrl(url), _options.UserInteraction.UseHttp303Redirects);
|
||||
context.Response.StatusCode = StatusCodes.Status303SeeOther;
|
||||
context.Response.Headers.Location = _urls.GetAbsoluteUrl(url);
|
||||
}
|
||||
|
||||
protected virtual Task<ErrorMessage> CreateErrorMessage(AuthorizeResponse response, HttpContext context)
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ internal class EndSessionHttpWriter : IHttpResponseWriter<EndSessionResult>
|
|||
redirect = redirect.AddQueryString(_options.UserInteraction.LogoutIdParameter, id);
|
||||
}
|
||||
|
||||
context.Response.RedirectWithStatusCode(redirect, _options.UserInteraction.UseHttp303Redirects);
|
||||
context.Response.StatusCode = StatusCodes.Status303SeeOther;
|
||||
context.Response.Headers.Location = redirect;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,25 +109,4 @@ public static class HttpResponseExtensions
|
|||
headers.Append("X-Content-Security-Policy", cspHeader);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redirects the response to the specified URL using the appropriate HTTP status code.
|
||||
/// When useHttp303 is true, uses HTTP 303 (See Other) instead of HTTP 302 (Found).
|
||||
/// HTTP 303 is recommended by FAPI 2.0 Security Profile to prevent POST data resubmission.
|
||||
/// </summary>
|
||||
/// <param name="response">The HTTP response.</param>
|
||||
/// <param name="url">The URL to redirect to.</param>
|
||||
/// <param name="useHttp303">If true, uses HTTP 303; otherwise uses HTTP 302.</param>
|
||||
public static void RedirectWithStatusCode(this HttpResponse response, string url, bool useHttp303)
|
||||
{
|
||||
if (useHttp303)
|
||||
{
|
||||
response.StatusCode = StatusCodes.Status303SeeOther;
|
||||
response.Headers.Location = url;
|
||||
}
|
||||
else
|
||||
{
|
||||
response.Redirect(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,6 @@ using Duende.IdentityServer.Test;
|
|||
|
||||
namespace Duende.IdentityServer.IntegrationTests.Endpoints.Authorize;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for HTTP 303 redirect behavior per FAPI 2.0 Security Profile.
|
||||
/// When UseHttp303Redirects is enabled, the server should use HTTP 303 (See Other)
|
||||
/// instead of HTTP 302 (Found) for authorization redirects.
|
||||
/// </summary>
|
||||
public class Http303RedirectTests
|
||||
{
|
||||
private const string Category = "HTTP 303 Redirect Tests";
|
||||
|
|
@ -56,47 +51,14 @@ public class Http303RedirectTests
|
|||
Username = "bob",
|
||||
IsActive = true
|
||||
});
|
||||
}
|
||||
|
||||
private void InitializeWithHttp303(bool enableHttp303)
|
||||
{
|
||||
_pipeline.OnPostConfigureServices += services => { };
|
||||
_pipeline.OnPreConfigure += app => { };
|
||||
_pipeline.OnPostConfigure += app => { };
|
||||
_pipeline.Initialize();
|
||||
_pipeline.Options.UserInteraction.UseHttp303Redirects = enableHttp303;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", Category)]
|
||||
public async Task Authorize_WithHttp303Disabled_Returns302Redirect()
|
||||
public async Task Authorize_Returns303Redirect()
|
||||
{
|
||||
InitializeWithHttp303(enableHttp303: false);
|
||||
|
||||
await _pipeline.LoginAsync("bob");
|
||||
_pipeline.BrowserClient.AllowAutoRedirect = false;
|
||||
|
||||
var url = _pipeline.CreateAuthorizeUrl(
|
||||
clientId: "code_client",
|
||||
responseType: "code",
|
||||
scope: "openid",
|
||||
redirectUri: "https://client/callback",
|
||||
state: "123_state",
|
||||
nonce: "123_nonce");
|
||||
|
||||
var response = await _pipeline.BrowserClient.GetAsync(url);
|
||||
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.Redirect);
|
||||
response.Headers.Location.ShouldNotBeNull();
|
||||
response.Headers.Location!.ToString().ShouldStartWith("https://client/callback");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", Category)]
|
||||
public async Task Authorize_WithHttp303Enabled_Returns303Redirect()
|
||||
{
|
||||
InitializeWithHttp303(enableHttp303: true);
|
||||
|
||||
await _pipeline.LoginAsync("bob");
|
||||
_pipeline.BrowserClient.AllowAutoRedirect = false;
|
||||
|
||||
|
|
@ -117,10 +79,8 @@ public class Http303RedirectTests
|
|||
|
||||
[Fact]
|
||||
[Trait("Category", Category)]
|
||||
public async Task Authorize_WithHttp303Enabled_ImplicitFlow_Returns303Redirect()
|
||||
public async Task Authorize_ImplicitFlow_Returns303Redirect()
|
||||
{
|
||||
InitializeWithHttp303(enableHttp303: true);
|
||||
|
||||
await _pipeline.LoginAsync("bob");
|
||||
_pipeline.BrowserClient.AllowAutoRedirect = false;
|
||||
|
||||
|
|
@ -141,34 +101,8 @@ public class Http303RedirectTests
|
|||
|
||||
[Fact]
|
||||
[Trait("Category", Category)]
|
||||
public async Task AuthorizeRedirectToLogin_WithHttp303Disabled_Returns302Redirect()
|
||||
public async Task Authorize_RedirectToLogin_Returns303Redirect()
|
||||
{
|
||||
InitializeWithHttp303(enableHttp303: false);
|
||||
|
||||
// User is not logged in, so authorize should redirect to login page
|
||||
_pipeline.BrowserClient.AllowAutoRedirect = false;
|
||||
|
||||
var url = _pipeline.CreateAuthorizeUrl(
|
||||
clientId: "code_client",
|
||||
responseType: "code",
|
||||
scope: "openid",
|
||||
redirectUri: "https://client/callback",
|
||||
state: "123_state",
|
||||
nonce: "123_nonce");
|
||||
|
||||
var response = await _pipeline.BrowserClient.GetAsync(url);
|
||||
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.Redirect);
|
||||
response.Headers.Location.ShouldNotBeNull();
|
||||
response.Headers.Location!.ToString().ShouldContain("/account/login");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", Category)]
|
||||
public async Task AuthorizeRedirectToLogin_WithHttp303Enabled_Returns303Redirect()
|
||||
{
|
||||
InitializeWithHttp303(enableHttp303: true);
|
||||
|
||||
// User is not logged in, so authorize should redirect to login page
|
||||
_pipeline.BrowserClient.AllowAutoRedirect = false;
|
||||
|
||||
|
|
@ -189,10 +123,8 @@ public class Http303RedirectTests
|
|||
|
||||
[Fact]
|
||||
[Trait("Category", Category)]
|
||||
public async Task Authorize_ErrorRedirectToErrorPage_WithHttp303Enabled_Returns303Redirect()
|
||||
public async Task Authorize_ErrorRedirectToErrorPage_Returns303Redirect()
|
||||
{
|
||||
InitializeWithHttp303(enableHttp303: true);
|
||||
|
||||
await _pipeline.LoginAsync("bob");
|
||||
_pipeline.BrowserClient.AllowAutoRedirect = false;
|
||||
|
||||
|
|
@ -209,32 +141,13 @@ public class Http303RedirectTests
|
|||
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.SeeOther);
|
||||
response.Headers.Location.ShouldNotBeNull();
|
||||
var location = response.Headers.Location!.ToString();
|
||||
location.ShouldContain("/home/error");
|
||||
response.Headers.Location!.ToString().ShouldContain("/home/error");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", Category)]
|
||||
public async Task EndSession_WithHttp303Disabled_Returns302Redirect()
|
||||
public async Task EndSession_Returns303Redirect()
|
||||
{
|
||||
InitializeWithHttp303(enableHttp303: false);
|
||||
|
||||
await _pipeline.LoginAsync("bob");
|
||||
_pipeline.BrowserClient.AllowAutoRedirect = false;
|
||||
|
||||
var response = await _pipeline.BrowserClient.GetAsync(IdentityServerPipeline.EndSessionEndpoint);
|
||||
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.Redirect);
|
||||
response.Headers.Location.ShouldNotBeNull();
|
||||
response.Headers.Location!.ToString().ShouldContain("/account/logout");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", Category)]
|
||||
public async Task EndSession_WithHttp303Enabled_Returns303Redirect()
|
||||
{
|
||||
InitializeWithHttp303(enableHttp303: true);
|
||||
|
||||
await _pipeline.LoginAsync("bob");
|
||||
_pipeline.BrowserClient.AllowAutoRedirect = false;
|
||||
|
||||
|
|
@ -247,41 +160,8 @@ public class Http303RedirectTests
|
|||
|
||||
[Fact]
|
||||
[Trait("Category", Category)]
|
||||
public async Task EndSession_WithIdTokenHint_WithHttp303Disabled_Returns302Redirect()
|
||||
public async Task EndSession_WithIdTokenHint_Returns303Redirect()
|
||||
{
|
||||
InitializeWithHttp303(enableHttp303: false);
|
||||
|
||||
await _pipeline.LoginAsync("bob");
|
||||
_pipeline.BrowserClient.AllowAutoRedirect = false;
|
||||
|
||||
// First get an id_token
|
||||
var url = _pipeline.CreateAuthorizeUrl(
|
||||
clientId: "implicit_client",
|
||||
responseType: "id_token",
|
||||
scope: "openid",
|
||||
redirectUri: "https://implicit/callback",
|
||||
state: "123_state",
|
||||
nonce: "123_nonce");
|
||||
|
||||
var authorizeResponse = await _pipeline.BrowserClient.GetAsync(url);
|
||||
var authorization = new Duende.IdentityModel.Client.AuthorizeResponse(authorizeResponse.Headers.Location!.ToString());
|
||||
var idToken = authorization.IdentityToken;
|
||||
|
||||
// Now call end session with the id_token_hint
|
||||
var response = await _pipeline.BrowserClient.GetAsync(
|
||||
IdentityServerPipeline.EndSessionEndpoint + "?id_token_hint=" + idToken);
|
||||
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.Redirect);
|
||||
response.Headers.Location.ShouldNotBeNull();
|
||||
response.Headers.Location!.ToString().ShouldContain("/account/logout");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", Category)]
|
||||
public async Task EndSession_WithIdTokenHint_WithHttp303Enabled_Returns303Redirect()
|
||||
{
|
||||
InitializeWithHttp303(enableHttp303: true);
|
||||
|
||||
await _pipeline.LoginAsync("bob");
|
||||
_pipeline.BrowserClient.AllowAutoRedirect = false;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue