diff --git a/identity-server/CHANGELOG.md b/identity-server/CHANGELOG.md
index f145cf4c9..26fbc2ab9 100644
--- a/identity-server/CHANGELOG.md
+++ b/identity-server/CHANGELOG.md
@@ -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
diff --git a/identity-server/src/Directory.Build.props b/identity-server/src/Directory.Build.props
index fdddb3731..e7c2fe3f6 100644
--- a/identity-server/src/Directory.Build.props
+++ b/identity-server/src/Directory.Build.props
@@ -10,7 +10,7 @@
Duende IdentityServer
true
is-
- 7.0
+ 8.0
true
diff --git a/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/UserInteractionOptions.cs b/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/UserInteractionOptions.cs
index f2c649097..c59f4819f 100644
--- a/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/UserInteractionOptions.cs
+++ b/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/UserInteractionOptions.cs
@@ -141,13 +141,4 @@ public class UserInteractionOptions
/// handle those values.
///
public ICollection PromptValuesSupported { get; set; } = new HashSet(Constants.SupportedPromptModes);
-
- ///
- /// 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)
- ///
- public bool UseHttp303Redirects { get; set; }
}
diff --git a/identity-server/src/IdentityServer/Endpoints/Results/AuthorizeInteractionPageResult.cs b/identity-server/src/IdentityServer/Endpoints/Results/AuthorizeInteractionPageResult.cs
index 8ba0cd40e..4b3757250 100644
--- a/identity-server/src/IdentityServer/Endpoints/Results/AuthorizeInteractionPageResult.cs
+++ b/identity-server/src/IdentityServer/Endpoints/Results/AuthorizeInteractionPageResult.cs
@@ -126,6 +126,7 @@ internal class AuthorizeInteractionPageHttpWriter : IHttpResponseWriter
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
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 CreateErrorMessage(AuthorizeResponse response, HttpContext context)
diff --git a/identity-server/src/IdentityServer/Endpoints/Results/EndSessionResult.cs b/identity-server/src/IdentityServer/Endpoints/Results/EndSessionResult.cs
index cda630d5e..2a98f5132 100644
--- a/identity-server/src/IdentityServer/Endpoints/Results/EndSessionResult.cs
+++ b/identity-server/src/IdentityServer/Endpoints/Results/EndSessionResult.cs
@@ -83,6 +83,7 @@ internal class EndSessionHttpWriter : IHttpResponseWriter
redirect = redirect.AddQueryString(_options.UserInteraction.LogoutIdParameter, id);
}
- context.Response.RedirectWithStatusCode(redirect, _options.UserInteraction.UseHttp303Redirects);
+ context.Response.StatusCode = StatusCodes.Status303SeeOther;
+ context.Response.Headers.Location = redirect;
}
}
diff --git a/identity-server/src/IdentityServer/Extensions/HttpResponseExtensions.cs b/identity-server/src/IdentityServer/Extensions/HttpResponseExtensions.cs
index 630bbeafd..29b4d42dd 100644
--- a/identity-server/src/IdentityServer/Extensions/HttpResponseExtensions.cs
+++ b/identity-server/src/IdentityServer/Extensions/HttpResponseExtensions.cs
@@ -109,25 +109,4 @@ public static class HttpResponseExtensions
headers.Append("X-Content-Security-Policy", cspHeader);
}
}
-
- ///
- /// 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.
- ///
- /// The HTTP response.
- /// The URL to redirect to.
- /// If true, uses HTTP 303; otherwise uses HTTP 302.
- 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);
- }
- }
}
diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Authorize/Http303RedirectTests.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Authorize/Http303RedirectTests.cs
index 31409bf29..0f6e61209 100644
--- a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Authorize/Http303RedirectTests.cs
+++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Authorize/Http303RedirectTests.cs
@@ -8,11 +8,6 @@ using Duende.IdentityServer.Test;
namespace Duende.IdentityServer.IntegrationTests.Endpoints.Authorize;
-///
-/// 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.
-///
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;