Skip to content

Commit 47a7958

Browse files
committed
Implement OTT
1 parent e35c0b7 commit 47a7958

File tree

5 files changed

+151
-17
lines changed

5 files changed

+151
-17
lines changed

spring-security-advanced-authentication-ui-demo/src/main/java/software/xdev/security/MainWebSecurity.java

+13
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import org.springframework.security.config.Customizer;
1515
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
1616
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
17+
import org.springframework.security.core.userdetails.UserDetailsService;
18+
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
1719
import org.springframework.security.web.SecurityFilterChain;
1820
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter;
1921
import org.springframework.security.web.savedrequest.NullRequestCache;
@@ -65,13 +67,24 @@ public SecurityFilterChain configure(
6567
.referrerPolicy(r -> r.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.SAME_ORIGIN))
6668
.contentSecurityPolicy(csp -> csp.policyDirectives(this.getCSP())))
6769
.formLogin(Customizer.withDefaults())
70+
.oneTimeTokenLogin(c -> c.tokenGenerationSuccessHandler(
71+
(request, response, oneTimeToken) -> {
72+
// Do nothing - Dummy
73+
}))
6874
.oauth2Login(c -> c.defaultSuccessUrl("/"))
6975
.authorizeHttpRequests(urlRegistry -> urlRegistry.anyRequest().authenticated())
7076
.requestCache(c -> c.requestCache(new NullRequestCache()));
7177

7278
return http.build();
7379
}
7480

81+
// Required for OTT
82+
@Bean
83+
public UserDetailsService userDetailsService()
84+
{
85+
return new InMemoryUserDetailsManager();
86+
}
87+
7588
// Example CSP
7689
protected String getCSP()
7790
{

spring-security-advanced-authentication-ui/src/main/java/software/xdev/spring/security/web/authentication/ui/advanced/filters/AdvancedLoginPageGeneratingFilter.java

+102-11
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ public class AdvancedLoginPageGeneratingFilter
5959

6060
protected String formLoginSignInText = "Sign in";
6161

62+
protected String oneTimeTokenHeaderText = "Request a One-Time Token";
63+
64+
protected String oneTimeTokenUsernameParameter = "username";
65+
66+
protected String oneTimeTokenUsernameText = "Username";
67+
68+
protected String oneTimeTokenSendTokenText = "Send token";
69+
6270
protected String ssoLoginHeaderText = "Login with";
6371

6472
protected String footer = "";
@@ -156,6 +164,30 @@ public AdvancedLoginPageGeneratingFilter formLoginSignInText(final String formLo
156164
return this;
157165
}
158166

167+
public AdvancedLoginPageGeneratingFilter oneTimeTokenHeaderText(final String oneTimeTokenHeaderText)
168+
{
169+
this.oneTimeTokenHeaderText = oneTimeTokenHeaderText;
170+
return this;
171+
}
172+
173+
public AdvancedLoginPageGeneratingFilter oneTimeTokenUsernameText(final String oneTimeTokenUsernameText)
174+
{
175+
this.oneTimeTokenUsernameText = oneTimeTokenUsernameText;
176+
return this;
177+
}
178+
179+
public AdvancedLoginPageGeneratingFilter oneTimeTokenUsernameParameter(final String oneTimeTokenUsernameParameter)
180+
{
181+
this.oneTimeTokenUsernameParameter = oneTimeTokenUsernameParameter;
182+
return this;
183+
}
184+
185+
public AdvancedLoginPageGeneratingFilter oneTimeTokenSendTokenText(final String oneTimeTokenSendTokenText)
186+
{
187+
this.oneTimeTokenSendTokenText = oneTimeTokenSendTokenText;
188+
return this;
189+
}
190+
159191
public AdvancedLoginPageGeneratingFilter ssoLoginHeaderText(final String ssoLoginHeaderText)
160192
{
161193
this.ssoLoginHeaderText = ssoLoginHeaderText;
@@ -208,10 +240,11 @@ protected String generateBody(
208240
return " " + this.createBodyElement()
209241
+ " " + this.createContainerElement()
210242
+ " " + this.createMainElement()
211-
+ this.createError(loginError, errorMsg)
212-
+ this.createLogoutSuccess(logoutSuccess)
243+
+ this.renderError(loginError, errorMsg)
244+
+ this.renderLogoutSuccess(logoutSuccess)
213245
+ this.header
214246
+ this.createFormLogin(request, contextPath)
247+
+ this.createOneTimeTokenLogin(request, contextPath)
215248
+ (this.hasSSOLogin() ? this.createHeaderLoginWith() : "")
216249
+ this.createOAuth2LoginPagePart(contextPath)
217250
+ this.createSaml2LoginPagePart(contextPath)
@@ -266,6 +299,18 @@ protected String createMainElement()
266299
+ "'>";
267300
}
268301

302+
@Override
303+
protected String renderError(final boolean isError, final String message)
304+
{
305+
if(!isError)
306+
{
307+
return "";
308+
}
309+
return "<div class=\"alert alert-danger\" role=\"alert\">" + HtmlUtils.htmlEscape(message) + "</div>";
310+
}
311+
312+
// region Render FormLogin
313+
269314
@SuppressWarnings("java:S1192")
270315
protected String createFormLogin(final HttpServletRequest request, final String contextPath)
271316
{
@@ -290,24 +335,63 @@ protected String createFormLogin(final HttpServletRequest request, final String
290335
+ "</form>";
291336
}
292337

293-
protected String createFormLoginSignInButton()
294-
{
295-
return "<button class=\"btn btn-block btn-primary w-100\" type=\"submit\">"
296-
+ this.formLoginSignInText
297-
+ "</button>";
298-
}
299-
300-
@Override
301338
protected String createRememberMe(final String paramName)
302339
{
303-
return "<div class=\"form-check text-start my-2\">"
340+
return "<div class=\"form-check text-start mt-1\">"
304341
+ "<label for='remember-me' class=\"form-check-label\">"
305342
+ this.formLoginRememberMeText
306343
+ "</label>"
307344
+ "<input class=\"form-check-input\" type='checkbox' name='" + paramName + "' id='remember-me'/>"
308345
+ "</div>";
309346
}
310347

348+
protected String createFormLoginSignInButton()
349+
{
350+
return "<button class=\"btn btn-block btn-primary w-100 mt-1\" type=\"submit\">"
351+
+ this.formLoginSignInText
352+
+ "</button>";
353+
}
354+
355+
// endregion
356+
// region Render OTT
357+
358+
protected String createOneTimeTokenLogin(final HttpServletRequest request, final String contextPath)
359+
{
360+
if(!this.oneTimeTokenEnabled)
361+
{
362+
return "";
363+
}
364+
365+
return this.createOneTimeTokenLoginHeader()
366+
+ "<form id=\"ott-form\" class=\"mb-3\" method=\"post\" action=\"" + contextPath
367+
+ this.generateOneTimeTokenUrl + "\">"
368+
+ "<div class='form-floating'>"
369+
+ " <input type='text' class='form-control' name=\"" + this.oneTimeTokenUsernameParameter + "\""
370+
+ " id='ott-username' placeholder=\"" + this.oneTimeTokenUsernameText + "\" required>"
371+
+ " <label for='username'>" + this.oneTimeTokenUsernameParameter + "</label>"
372+
+ "</div>"
373+
+ this.renderHiddenInputs(request)
374+
+ this.createOneTimeTokenLoginSignInButton()
375+
+ "</form>";
376+
}
377+
378+
protected String createOneTimeTokenLoginHeader()
379+
{
380+
return "<h5 class=\"h5 mb-2 fw-normal\">"
381+
+ this.oneTimeTokenHeaderText
382+
+ "</h5>";
383+
}
384+
385+
protected String createOneTimeTokenLoginSignInButton()
386+
{
387+
return "<button class=\"btn btn-block btn-primary w-100 mt-1\" type=\"submit\">"
388+
+ this.oneTimeTokenSendTokenText
389+
+ "</button>";
390+
}
391+
392+
// endregion
393+
// region Render SSO
394+
311395
protected boolean hasSSOLogin()
312396
{
313397
return this.oauth2LoginEnabled || this.saml2LoginEnabled;
@@ -379,6 +463,13 @@ protected String createSSOLoginPagePart(
379463
+ "</table>";
380464
}
381465

466+
// endregion
467+
468+
protected String renderHiddenInputs(final HttpServletRequest request)
469+
{
470+
return this.renderHiddenInputs(this.resolveHiddenInputs.apply(request).entrySet());
471+
}
472+
382473
protected record ButtonBuildingData(
383474
String url,
384475
String name,

spring-security-advanced-authentication-ui/src/main/java/software/xdev/spring/security/web/authentication/ui/advanced/filters/AdvancedLogoutPageGeneratingFilter.java

+7
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,11 @@ protected String generateBody(final HttpServletRequest request)
106106
+ " </div>"
107107
+ " </body>";
108108
}
109+
110+
// Don't use template engine (with Regex) to improve performance
111+
@Override
112+
protected String renderHiddenInputs(final HttpServletRequest request)
113+
{
114+
return this.renderHiddenInputs(this.resolveHiddenInputs.apply(request).entrySet());
115+
}
109116
}

spring-security-advanced-authentication-ui/src/main/java/software/xdev/spring/security/web/authentication/ui/advanced/filters/AdvancedSharedPageGeneratingFilter.java

+18-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.List;
1919
import java.util.Map;
2020
import java.util.Optional;
21+
import java.util.Set;
2122
import java.util.stream.Collectors;
2223

2324
import software.xdev.spring.security.web.authentication.ui.extendable.filters.ExtendableDefaultPageGeneratingFilter;
@@ -47,9 +48,9 @@ public interface AdvancedSharedPageGeneratingFilter<S extends AdvancedSharedPage
4748
S addHeaderMeta(String name, String content);
4849

4950
default String generateHeader(
50-
Map<String, String> headerMetas,
51-
String pageTitle,
52-
List<String> headerElements)
51+
final Map<String, String> headerMetas,
52+
final String pageTitle,
53+
final List<String> headerElements)
5354
{
5455
return " <head>"
5556
+ " <meta charset='utf-8'>"
@@ -61,4 +62,18 @@ default String generateHeader(
6162
+ headerElements.stream().map(s -> " " + s).collect(Collectors.joining())
6263
+ " </head>";
6364
}
65+
66+
default String renderHiddenInputs(final Set<Map.Entry<String, String>> entries)
67+
{
68+
final StringBuilder sb = new StringBuilder(50);
69+
for(final Map.Entry<String, String> input : entries)
70+
{
71+
sb.append("<input name=\"");
72+
sb.append(input.getKey());
73+
sb.append("\" type=\"hidden\" value=\"");
74+
sb.append(input.getValue());
75+
sb.append("\" />\n");
76+
}
77+
return sb.toString();
78+
}
6479
}

spring-security-advanced-authentication-ui/src/main/java/software/xdev/spring/security/web/authentication/ui/extendable/filters/ExtendableDefaultLoginPageGeneratingFilter.java

+11-3
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,12 @@ public void setResolveHeaders(final Function<HttpServletRequest, Map<String, Str
137137
@Override
138138
public boolean isEnabled()
139139
{
140-
return this.formLoginEnabled || this.oauth2LoginEnabled || this.saml2LoginEnabled;
140+
// Improvement: OTT and Passkeys are missing!!!
141+
return this.formLoginEnabled
142+
|| this.oneTimeTokenEnabled
143+
|| this.oauth2LoginEnabled
144+
|| this.saml2LoginEnabled
145+
|| this.passkeysEnabled;
141146
}
142147

143148
@Override
@@ -384,8 +389,11 @@ protected String renderFormLogin(
384389
}
385390

386391
protected String renderOneTimeTokenLogin(
387-
final HttpServletRequest request, final boolean loginError, final boolean logoutSuccess,
388-
final String contextPath, final String errorMsg)
392+
final HttpServletRequest request,
393+
final boolean loginError,
394+
final boolean logoutSuccess,
395+
final String contextPath,
396+
final String errorMsg)
389397
{
390398
if(!this.oneTimeTokenEnabled)
391399
{

0 commit comments

Comments
 (0)