|
@@ -18,9 +18,12 @@ package org.springframework.security.web.authentication.ui;
|
|
|
|
|
|
import java.io.IOException;
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
+import java.util.Collection;
|
|
|
import java.util.Collections;
|
|
|
+import java.util.List;
|
|
|
import java.util.Map;
|
|
|
import java.util.function.Function;
|
|
|
+import java.util.function.Predicate;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
import jakarta.servlet.FilterChain;
|
|
@@ -31,10 +34,14 @@ import jakarta.servlet.http.HttpServletRequest;
|
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
|
import org.jspecify.annotations.Nullable;
|
|
|
|
|
|
+import org.springframework.security.core.Authentication;
|
|
|
+import org.springframework.security.core.context.SecurityContextHolder;
|
|
|
+import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
|
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
|
|
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
|
|
|
import org.springframework.util.Assert;
|
|
|
import org.springframework.web.filter.GenericFilterBean;
|
|
|
+import org.springframework.web.util.UriComponentsBuilder;
|
|
|
|
|
|
/**
|
|
|
* For internal use with namespace configuration in the case where a user doesn't
|
|
@@ -52,6 +59,9 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|
|
|
|
|
public static final String ERROR_PARAMETER_NAME = "error";
|
|
|
|
|
|
+ private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
|
|
|
+ .getContextHolderStrategy();
|
|
|
+
|
|
|
private @Nullable String loginPageUrl;
|
|
|
|
|
|
private @Nullable String logoutSuccessUrl;
|
|
@@ -78,6 +88,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|
|
|
|
|
private @Nullable String rememberMeParameter;
|
|
|
|
|
|
+ private final String factorParameter = "factor";
|
|
|
+
|
|
|
+ private final Collection<String> allowedParameters = List.of(this.factorParameter);
|
|
|
+
|
|
|
@SuppressWarnings("NullAway.Init")
|
|
|
private Map<String, String> oauth2AuthenticationUrlToClientName;
|
|
|
|
|
@@ -109,6 +123,18 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Use this {@link SecurityContextHolderStrategy} to retrieve authenticated users.
|
|
|
+ * <p>
|
|
|
+ * Uses {@link SecurityContextHolder#getContextHolderStrategy()} by default.
|
|
|
+ * @param securityContextHolderStrategy the strategy to use
|
|
|
+ * @since 7.0
|
|
|
+ */
|
|
|
+ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
|
|
|
+ Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
|
|
|
+ this.securityContextHolderStrategy = securityContextHolderStrategy;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Sets a Function used to resolve a Map of the hidden inputs where the key is the
|
|
|
* name of the input and the value is the value of the input. Typically this is used
|
|
@@ -223,16 +249,43 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|
|
String errorMsg = "Invalid credentials";
|
|
|
String contextPath = request.getContextPath();
|
|
|
|
|
|
- return HtmlTemplates.fromTemplate(LOGIN_PAGE_TEMPLATE)
|
|
|
+ HtmlTemplates.Builder builder = HtmlTemplates.fromTemplate(LOGIN_PAGE_TEMPLATE)
|
|
|
.withRawHtml("contextPath", contextPath)
|
|
|
- .withRawHtml("javaScript", renderJavaScript(request, contextPath))
|
|
|
- .withRawHtml("formLogin", renderFormLogin(request, loginError, logoutSuccess, contextPath, errorMsg))
|
|
|
- .withRawHtml("oneTimeTokenLogin",
|
|
|
- renderOneTimeTokenLogin(request, loginError, logoutSuccess, contextPath, errorMsg))
|
|
|
- .withRawHtml("oauth2Login", renderOAuth2Login(loginError, logoutSuccess, errorMsg, contextPath))
|
|
|
- .withRawHtml("saml2Login", renderSaml2Login(loginError, logoutSuccess, errorMsg, contextPath))
|
|
|
- .withRawHtml("passkeyLogin", renderPasskeyLogin())
|
|
|
- .render();
|
|
|
+ .withRawHtml("javaScript", "")
|
|
|
+ .withRawHtml("formLogin", "")
|
|
|
+ .withRawHtml("oneTimeTokenLogin", "")
|
|
|
+ .withRawHtml("oauth2Login", "")
|
|
|
+ .withRawHtml("saml2Login", "")
|
|
|
+ .withRawHtml("passkeyLogin", "");
|
|
|
+
|
|
|
+ Predicate<String> wantsAuthority = wantsAuthority(request);
|
|
|
+ if (wantsAuthority.test("webauthn")) {
|
|
|
+ builder.withRawHtml("javaScript", renderJavaScript(request, contextPath))
|
|
|
+ .withRawHtml("passkeyLogin", renderPasskeyLogin());
|
|
|
+ }
|
|
|
+ if (wantsAuthority.test("password")) {
|
|
|
+ builder.withRawHtml("formLogin",
|
|
|
+ renderFormLogin(request, loginError, logoutSuccess, contextPath, errorMsg));
|
|
|
+ }
|
|
|
+ if (wantsAuthority.test("ott")) {
|
|
|
+ builder.withRawHtml("oneTimeTokenLogin",
|
|
|
+ renderOneTimeTokenLogin(request, loginError, logoutSuccess, contextPath, errorMsg));
|
|
|
+ }
|
|
|
+ if (wantsAuthority.test("authorization_code")) {
|
|
|
+ builder.withRawHtml("oauth2Login", renderOAuth2Login(loginError, logoutSuccess, errorMsg, contextPath));
|
|
|
+ }
|
|
|
+ if (wantsAuthority.test("saml_response")) {
|
|
|
+ builder.withRawHtml("saml2Login", renderSaml2Login(loginError, logoutSuccess, errorMsg, contextPath));
|
|
|
+ }
|
|
|
+ return builder.render();
|
|
|
+ }
|
|
|
+
|
|
|
+ private Predicate<String> wantsAuthority(HttpServletRequest request) {
|
|
|
+ String[] authorities = request.getParameterValues(this.factorParameter);
|
|
|
+ if (authorities == null) {
|
|
|
+ return (authority) -> true;
|
|
|
+ }
|
|
|
+ return List.of(authorities)::contains;
|
|
|
}
|
|
|
|
|
|
private String renderJavaScript(HttpServletRequest request, String contextPath) {
|
|
@@ -271,6 +324,13 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|
|
return "";
|
|
|
}
|
|
|
|
|
|
+ String username = getUsername();
|
|
|
+ String usernameInput = ((username != null)
|
|
|
+ ? HtmlTemplates.fromTemplate(FORM_READONLY_USERNAME_INPUT).withValue("username", username)
|
|
|
+ : HtmlTemplates.fromTemplate(FORM_USERNAME_INPUT))
|
|
|
+ .withValue("usernameParameter", this.usernameParameter)
|
|
|
+ .render();
|
|
|
+
|
|
|
String hiddenInputs = this.resolveHiddenInputs.apply(request)
|
|
|
.entrySet()
|
|
|
.stream()
|
|
@@ -281,7 +341,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|
|
.withValue("loginUrl", contextPath + this.authenticationUrl)
|
|
|
.withRawHtml("errorMessage", renderError(loginError, errorMsg))
|
|
|
.withRawHtml("logoutMessage", renderSuccess(logoutSuccess))
|
|
|
- .withValue("usernameParameter", this.usernameParameter)
|
|
|
+ .withRawHtml("usernameInput", usernameInput)
|
|
|
.withValue("passwordParameter", this.passwordParameter)
|
|
|
.withRawHtml("rememberMeInput", renderRememberMe(this.rememberMeParameter))
|
|
|
.withRawHtml("hiddenInputs", hiddenInputs)
|
|
@@ -301,11 +361,17 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|
|
.map((inputKeyValue) -> renderHiddenInput(inputKeyValue.getKey(), inputKeyValue.getValue()))
|
|
|
.collect(Collectors.joining("\n"));
|
|
|
|
|
|
+ String username = getUsername();
|
|
|
+ String usernameInput = (username != null)
|
|
|
+ ? HtmlTemplates.fromTemplate(ONE_TIME_READONLY_USERNAME_INPUT).withValue("username", username).render()
|
|
|
+ : ONE_TIME_USERNAME_INPUT;
|
|
|
+
|
|
|
return HtmlTemplates.fromTemplate(ONE_TIME_TEMPLATE)
|
|
|
.withValue("generateOneTimeTokenUrl", contextPath + this.generateOneTimeTokenUrl)
|
|
|
.withRawHtml("errorMessage", renderError(loginError, errorMsg))
|
|
|
.withRawHtml("logoutMessage", renderSuccess(logoutSuccess))
|
|
|
.withRawHtml("hiddenInputs", hiddenInputs)
|
|
|
+ .withRawHtml("usernameInput", usernameInput)
|
|
|
.render();
|
|
|
}
|
|
|
|
|
@@ -374,6 +440,14 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|
|
.render();
|
|
|
}
|
|
|
|
|
|
+ private @Nullable String getUsername() {
|
|
|
+ Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
|
|
|
+ if (authentication != null && authentication.isAuthenticated()) {
|
|
|
+ return authentication.getName();
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
private boolean isLogoutSuccess(HttpServletRequest request) {
|
|
|
return this.logoutSuccessUrl != null && matches(request, this.logoutSuccessUrl);
|
|
|
}
|
|
@@ -413,10 +487,19 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|
|
if (request.getQueryString() != null) {
|
|
|
uri += "?" + request.getQueryString();
|
|
|
}
|
|
|
+ UriComponentsBuilder addAllowed = UriComponentsBuilder.fromUriString(url);
|
|
|
+ for (String parameter : this.allowedParameters) {
|
|
|
+ String[] values = request.getParameterValues(parameter);
|
|
|
+ if (values != null) {
|
|
|
+ for (String value : values) {
|
|
|
+ addAllowed.queryParam(parameter, value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
if ("".equals(request.getContextPath())) {
|
|
|
- return uri.equals(url);
|
|
|
+ return uri.equals(addAllowed.toUriString());
|
|
|
}
|
|
|
- return uri.equals(request.getContextPath() + url);
|
|
|
+ return uri.equals(request.getContextPath() + addAllowed.toUriString());
|
|
|
}
|
|
|
|
|
|
private static final String CSRF_HEADERS = """
|
|
@@ -466,7 +549,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|
|
{{errorMessage}}{{logoutMessage}}
|
|
|
<p>
|
|
|
<label for="username" class="screenreader">Username</label>
|
|
|
- <input type="text" id="username" name="{{usernameParameter}}" placeholder="Username" required autofocus>
|
|
|
+ {{usernameInput}}
|
|
|
</p>
|
|
|
<p>
|
|
|
<label for="password" class="screenreader">Password</label>
|
|
@@ -477,6 +560,14 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|
|
<button type="submit" class="primary">Sign in</button>
|
|
|
</form>""";
|
|
|
|
|
|
+ private static final String FORM_READONLY_USERNAME_INPUT = """
|
|
|
+ <input type="text" id="username" name="{{usernameParameter}}" value="{{username}}" placeholder="Username" required readonly>
|
|
|
+ """;
|
|
|
+
|
|
|
+ private static final String FORM_USERNAME_INPUT = """
|
|
|
+ <input type="text" id="username" name="{{usernameParameter}}" placeholder="Username" required autofocus>
|
|
|
+ """;
|
|
|
+
|
|
|
private static final String HIDDEN_HTML_INPUT_TEMPLATE = """
|
|
|
<input name="{{name}}" type="hidden" value="{{value}}" />
|
|
|
""";
|
|
@@ -509,11 +600,19 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|
|
{{errorMessage}}{{logoutMessage}}
|
|
|
<p>
|
|
|
<label for="ott-username" class="screenreader">Username</label>
|
|
|
- <input type="text" id="ott-username" name="username" placeholder="Username" required>
|
|
|
+ {{usernameInput}}
|
|
|
</p>
|
|
|
{{hiddenInputs}}
|
|
|
<button class="primary" type="submit" form="ott-form">Send Token</button>
|
|
|
</form>
|
|
|
""";
|
|
|
|
|
|
+ private static final String ONE_TIME_READONLY_USERNAME_INPUT = """
|
|
|
+ <input type="text" id="ott-username" name="username" value="{{username}}" placeholder="Username" required readonly>
|
|
|
+ """;
|
|
|
+
|
|
|
+ private static final String ONE_TIME_USERNAME_INPUT = """
|
|
|
+ <input type="text" id="ott-username" name="username" placeholder="Username" required>
|
|
|
+ """;
|
|
|
+
|
|
|
}
|