Skip to content

Use relative URLs in /login redirects #14714

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down
104 changes: 104 additions & 0 deletions docs/modules/ROOT/pages/migration-7/web.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
= Web Migrations

== Favor Relative URIs

When redirecting to a login endpoint, Spring Security has favored absolute URIs in the past.
For example, if you set your login page like so:

[tabs]
======
Java::
+
[source,java,role="primary"]
----
http
// ...
.formLogin((form) -> form.loginPage("/my-login"))
// ...
----

Kotlin::
+
[source,kotlin,role="secondary"]
----
http {
formLogin {
loginPage = "/my-login"
}
}
----

Xml::
+
[source,kotlin,role="secondary"]
----
<http ...>
<form-login login-page="/my-login"/>
</http>
----
======

then when redirecting to `/my-login` Spring Security would use a `Location:` like the following:

[source]
----
302 Found
// ...
Location: https://myapp.example.org/my-login
----

However, this is no longer necessary given that the RFC is was based on is now obsolete.

In Spring Security 7, this is changed to use a relative URI like so:

[source]
----
302 Found
// ...
Location: /my-login
----

Most applications will not notice a difference.
However, in the event that this change causes problems, you can switch back to the Spring Security 6 behavior by setting the `favorRelativeUrls` value:

[tabs]
======
Java::
+
[source,java,role="primary"]
----
LoginUrlAuthenticationEntryPoint entryPoint = new LoginUrlAuthenticationEntryPoint("/my-login");
entryPoint.setFavorRelativeUris(false);
http
// ...
.exceptionHandling((exceptions) -> exceptions.authenticaitonEntryPoint(entryPoint))
// ...
----

Kotlin::
+
[source,kotlin,role="secondary"]
----
LoginUrlAuthenticationEntryPoint entryPoint = LoginUrlAuthenticationEntryPoint("/my-login")
entryPoint.setFavorRelativeUris(false)

http {
exceptionHandling {
authenticationEntryPoint = entryPoint
}
}
----

Xml::
+
[source,kotlin,role="secondary"]
----
<http entry-point-ref="myEntryPoint">
<!-- ... -->
</http>

<b:bean id="myEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<b:property name="favorRelativeUris" value="true"/>
</b:bean>
----
======
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
* @author colin sampaleanu
* @author Omri Spector
* @author Luke Taylor
* @author Michal Okosy
* @since 3.0
*/
public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {
Expand All @@ -77,6 +78,8 @@ public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoin

private boolean useForward = false;

private boolean favorRelativeUris = false;

private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

/**
Expand Down Expand Up @@ -146,27 +149,38 @@ protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpSer
if (UrlUtils.isAbsoluteUrl(loginForm)) {
return loginForm;
}
if (requiresRewrite(request)) {
return httpsUri(request, loginForm);
}
return this.favorRelativeUris ? loginForm : absoluteUri(request, loginForm).getUrl();
}

private boolean requiresRewrite(HttpServletRequest request) {
return this.forceHttps && "http".equals(request.getScheme());
}

private String httpsUri(HttpServletRequest request, String path) {
int serverPort = this.portResolver.getServerPort(request);
String scheme = request.getScheme();
Integer httpsPort = this.portMapper.lookupHttpsPort(serverPort);
if (httpsPort == null) {
logger.warn(LogMessage.format("Unable to redirect to HTTPS as no port mapping found for HTTP port %s",
serverPort));
return this.favorRelativeUris ? path : absoluteUri(request, path).getUrl();
}
RedirectUrlBuilder builder = absoluteUri(request, path);
builder.setScheme("https");
builder.setPort(httpsPort);
return builder.getUrl();
}

private RedirectUrlBuilder absoluteUri(HttpServletRequest request, String path) {
RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();
urlBuilder.setScheme(scheme);
urlBuilder.setScheme(request.getScheme());
urlBuilder.setServerName(request.getServerName());
urlBuilder.setPort(serverPort);
urlBuilder.setPort(this.portResolver.getServerPort(request));
urlBuilder.setContextPath(request.getContextPath());
urlBuilder.setPathInfo(loginForm);
if (this.forceHttps && "http".equals(scheme)) {
Integer httpsPort = this.portMapper.lookupHttpsPort(serverPort);
if (httpsPort != null) {
// Overwrite scheme and port in the redirect URL
urlBuilder.setScheme("https");
urlBuilder.setPort(httpsPort);
}
else {
logger.warn(LogMessage.format("Unable to redirect to HTTPS as no port mapping found for HTTP port %s",
serverPort));
}
}
return urlBuilder.getUrl();
urlBuilder.setPathInfo(path);
return urlBuilder;
}

/**
Expand Down Expand Up @@ -244,4 +258,18 @@ protected boolean isUseForward() {
return this.useForward;
}

/**
* Favor using relative URIs when formulating a redirect.
*
* <p>
* Note that a relative redirect is not always possible. For example, when redirecting
* from {@code http} to {@code https}, the URL needs to be absolute.
* </p>
* @param favorRelativeUris whether to favor relative URIs or not
* @since 6.5
*/
public void setFavorRelativeUris(boolean favorRelativeUris) {
this.favorRelativeUris = favorRelativeUris;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ public void testHttpsOperationFromOriginalHttpsUrl() throws Exception {
ep.setPortResolver(new MockPortResolver(8080, 8443));
ep.commence(request, response, null);
assertThat(response.getRedirectedUrl()).isEqualTo("https://www.example.com:8443/bigWebApp/hello");
// access to https via http port
request.setServerPort(8080);
response = new MockHttpServletResponse();
ep.setPortResolver(new MockPortResolver(8080, 8443));
ep.commence(request, response, null);
assertThat(response.getRedirectedUrl()).isEqualTo("https://www.example.com:8443/bigWebApp/hello");
}

@Test
Expand Down Expand Up @@ -231,4 +237,54 @@ public void absoluteLoginFormUrlCantBeUsedWithForwarding() throws Exception {
assertThatIllegalArgumentException().isThrownBy(ep::afterPropertiesSet);
}

@Test
public void commenceWhenFavorRelativeUrisThenHttpsSchemeNotIncluded() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("/some_path");
request.setScheme("https");
request.setServerName("www.example.com");
request.setContextPath("/bigWebApp");
request.setServerPort(443);
MockHttpServletResponse response = new MockHttpServletResponse();
LoginUrlAuthenticationEntryPoint ep = new LoginUrlAuthenticationEntryPoint("/hello");
ep.setFavorRelativeUris(true);
ep.setPortMapper(new PortMapperImpl());
ep.setForceHttps(true);
ep.setPortMapper(new PortMapperImpl());
ep.setPortResolver(new MockPortResolver(80, 443));
ep.afterPropertiesSet();
ep.commence(request, response, null);
assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello");
request.setServerPort(8443);
response = new MockHttpServletResponse();
ep.setPortResolver(new MockPortResolver(8080, 8443));
ep.commence(request, response, null);
assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello");
// access to https via http port
request.setServerPort(8080);
response = new MockHttpServletResponse();
ep.setPortResolver(new MockPortResolver(8080, 8443));
ep.commence(request, response, null);
assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello");
}

@Test
public void commenceWhenFavorRelativeUrisThenHttpSchemeNotIncluded() throws Exception {
LoginUrlAuthenticationEntryPoint ep = new LoginUrlAuthenticationEntryPoint("/hello");
ep.setFavorRelativeUris(true);
ep.setPortMapper(new PortMapperImpl());
ep.setPortResolver(new MockPortResolver(80, 443));
ep.afterPropertiesSet();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("/some_path");
request.setContextPath("/bigWebApp");
request.setScheme("http");
request.setServerName("localhost");
request.setContextPath("/bigWebApp");
request.setServerPort(80);
MockHttpServletResponse response = new MockHttpServletResponse();
ep.commence(request, response, null);
assertThat(response.getRedirectedUrl()).isEqualTo("/bigWebApp/hello");
}

}
Loading