Skip to content

Commit 8b047d1

Browse files
authored
feat: Add the Authentication Context bean to the Vaadin Web Security (#15129)
- AuthenticationContext is a concrete class that gets access to the authenticated user and allows performing logout integrated with Spring Security - AuthenticationContext is an injectable managed bean - Developers can plug additional LogoutHandlers in VaadinWebSecurity.addLogoutHandlers() - VaadinWebSecurity configures a LogoutSuccessHandler with a redirect strategy that can handle also XHR UIDL requests (UidlRedirectStrategy) Fixes #14958
1 parent 1927930 commit 8b047d1

File tree

12 files changed

+614
-45
lines changed

12 files changed

+614
-45
lines changed

flow-tests/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/SecurityConfig.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
1717
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
1818

19+
import com.vaadin.flow.component.UI;
20+
import com.vaadin.flow.internal.UrlUtil;
21+
import com.vaadin.flow.server.VaadinServletRequest;
1922
import com.vaadin.flow.spring.RootMappedCondition;
2023
import com.vaadin.flow.spring.VaadinConfigurationProperties;
2124
import com.vaadin.flow.spring.flowsecurity.data.UserInfo;
@@ -63,6 +66,11 @@ public void configure(HttpSecurity http) throws Exception {
6366
.permitAll();
6467
super.configure(http);
6568
setLoginView(http, LoginView.class, getLogoutSuccessUrl());
69+
http.logout().addLogoutHandler((request, response, authentication) -> {
70+
UI ui = UI.getCurrent();
71+
ui.accessSynchronously(() -> ui.getPage().setLocation(UrlUtil
72+
.getServletPathRelative(getLogoutSuccessUrl(), request)));
73+
});
6674
}
6775

6876
@Bean
Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
package com.vaadin.flow.spring.flowsecurity;
22

3+
import java.util.Optional;
4+
35
import org.springframework.beans.factory.annotation.Autowired;
4-
import org.springframework.security.core.context.SecurityContext;
5-
import org.springframework.security.core.context.SecurityContextHolder;
66
import org.springframework.security.core.userdetails.UserDetails;
7-
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
87
import org.springframework.stereotype.Component;
98

10-
import com.vaadin.flow.component.UI;
11-
import com.vaadin.flow.internal.UrlUtil;
12-
import com.vaadin.flow.server.VaadinServletRequest;
139
import com.vaadin.flow.spring.flowsecurity.data.UserInfo;
1410
import com.vaadin.flow.spring.flowsecurity.service.UserInfoService;
11+
import com.vaadin.flow.spring.security.AuthenticationContext;
1512

1613
@Component
1714
public class SecurityUtils {
@@ -20,38 +17,20 @@ public class SecurityUtils {
2017
private UserInfoService userInfoService;
2118
@Autowired
2219
private SecurityConfig securityConfig;
23-
24-
public UserDetails getAuthenticatedUser() {
25-
SecurityContext context = SecurityContextHolder.getContext();
26-
if (context == null) {
27-
throw new IllegalStateException("No security context available");
28-
}
29-
if (context.getAuthentication() == null) {
30-
return null;
31-
}
32-
Object principal = context.getAuthentication().getPrincipal();
33-
if (principal instanceof UserDetails) {
34-
return (UserDetails) context.getAuthentication().getPrincipal();
35-
}
36-
// Anonymous or no authentication.
37-
return null;
38-
}
20+
@Autowired
21+
private AuthenticationContext authenticationContext;
3922

4023
public UserInfo getAuthenticatedUserInfo() {
41-
UserDetails details = getAuthenticatedUser();
42-
if (details == null) {
24+
Optional<UserDetails> userDetails = authenticationContext
25+
.getAuthenticatedUser(UserDetails.class);
26+
if (userDetails.isEmpty()) {
4327
return null;
4428
}
45-
return userInfoService.findByUsername(details.getUsername());
29+
return userInfoService.findByUsername(userDetails.get().getUsername());
4630
}
4731

4832
public void logout() {
49-
SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
50-
logoutHandler.setInvalidateHttpSession(false);
51-
VaadinServletRequest request = VaadinServletRequest.getCurrent();
52-
logoutHandler.logout(request, null, null);
53-
UI.getCurrent().getPage().setLocation(UrlUtil.getServletPathRelative(
54-
securityConfig.getLogoutSuccessUrl(), request));
33+
authenticationContext.logout();
5534
}
5635

5736
}

flow-tests/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/service/BankService.java

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import java.math.BigDecimal;
44
import java.util.Optional;
55

6-
import com.vaadin.flow.spring.flowsecurity.SecurityUtils;
76
import com.vaadin.flow.spring.flowsecurity.data.Account;
7+
import com.vaadin.flow.spring.security.AuthenticationContext;
88

99
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.security.core.userdetails.UserDetails;
1011
import org.springframework.stereotype.Service;
1112

1213
@Service
@@ -16,7 +17,7 @@ public class BankService {
1617
private AccountService accountService;
1718

1819
@Autowired
19-
private SecurityUtils utils;
20+
private AuthenticationContext authenticationContext;
2021

2122
public void applyForLoan() {
2223
applyForLoan(10000);
@@ -26,14 +27,19 @@ public void applyForHugeLoan() {
2627
applyForLoan(1000000);
2728
try {
2829
Thread.sleep(3000);
29-
} catch (InterruptedException e) {
30+
} catch (InterruptedException ignored) {
3031
}
3132
}
3233

3334
private void applyForLoan(int amount) {
34-
String name = utils.getAuthenticatedUser().getUsername();
35-
Optional<Account> acc = accountService.findByOwner(name);
36-
if (!acc.isPresent()) {
35+
Optional<UserDetails> authenticatedUser = authenticationContext
36+
.getAuthenticatedUser(UserDetails.class);
37+
if (authenticatedUser.isEmpty()) {
38+
return;
39+
}
40+
Optional<Account> acc = accountService
41+
.findByOwner(authenticatedUser.get().getUsername());
42+
if (acc.isEmpty()) {
3743
return;
3844
}
3945
Account account = acc.get();
@@ -42,9 +48,13 @@ private void applyForLoan(int amount) {
4248
}
4349

4450
public BigDecimal getBalance() {
45-
String name = utils.getAuthenticatedUser().getUsername();
46-
return accountService.findByOwner(name).map(Account::getBalance)
47-
.orElse(null);
51+
Optional<UserDetails> authenticatedUser = authenticationContext
52+
.getAuthenticatedUser(UserDetails.class);
53+
if (authenticatedUser.isEmpty()) {
54+
return null;
55+
}
56+
return accountService.findByOwner(authenticatedUser.get().getUsername())
57+
.map(Account::getBalance).orElse(null);
4858
}
4959

5060
}

vaadin-spring/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@
109109
<artifactId>spring-test</artifactId>
110110
<scope>test</scope>
111111
</dependency>
112+
<dependency>
113+
<groupId>org.springframework.security</groupId>
114+
<artifactId>spring-security-test</artifactId>
115+
<scope>test</scope>
116+
</dependency>
112117

113118
<dependency>
114119
<groupId>org.springframework.boot</groupId>
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright 2000-2022 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.spring.security;
17+
18+
import java.io.IOException;
19+
import java.io.Serializable;
20+
import java.util.List;
21+
import java.util.Optional;
22+
23+
import jakarta.servlet.ServletException;
24+
import jakarta.servlet.http.HttpServletRequest;
25+
import jakarta.servlet.http.HttpServletResponse;
26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
28+
import org.springframework.security.authentication.AnonymousAuthenticationToken;
29+
import org.springframework.security.core.Authentication;
30+
import org.springframework.security.core.context.SecurityContext;
31+
import org.springframework.security.core.context.SecurityContextHolder;
32+
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
33+
import org.springframework.security.web.authentication.logout.LogoutHandler;
34+
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
35+
36+
import com.vaadin.flow.component.UI;
37+
import com.vaadin.flow.server.VaadinServletRequest;
38+
import com.vaadin.flow.server.VaadinServletResponse;
39+
40+
/**
41+
* The authentication context of the application.
42+
* <p>
43+
* An instance of this class is available for injection as bean in view and
44+
* layout classes.
45+
*
46+
* It allows to access authenticated user information and to initiate the logout
47+
* process.
48+
*
49+
* @author Vaadin Ltd
50+
* @since 23.3
51+
*/
52+
public class AuthenticationContext implements Serializable {
53+
54+
private static final Logger LOGGER = LoggerFactory
55+
.getLogger(AuthenticationContext.class);
56+
57+
private transient LogoutSuccessHandler logoutSuccessHandler;
58+
59+
private transient CompositeLogoutHandler logoutHandler;
60+
61+
/**
62+
* Gets an {@link Optional} with an instance of the current user if it has
63+
* been authenticated, or empty if the user is not authenticated.
64+
*
65+
* Anonymous users are considered not authenticated.
66+
*
67+
* @param <U>
68+
* the type parameter of the expected user instance
69+
* @param userType
70+
* the type of the expected user instance
71+
* @return an {@link Optional} with the current authenticated user, or empty
72+
* if none available
73+
* @throws ClassCastException
74+
* if the current user instance does not match the given
75+
* {@code userType}.
76+
*/
77+
public <U> Optional<U> getAuthenticatedUser(Class<U> userType) {
78+
return getAuthentication().map(Authentication::getPrincipal)
79+
.map(userType::cast);
80+
}
81+
82+
/**
83+
* Indicates whether a user is currently authenticated.
84+
*
85+
* Anonymous users are considered not authenticated.
86+
*
87+
* @return {@literal true} if a user is currently authenticated, otherwise
88+
* {@literal false}
89+
*/
90+
public boolean isAuthenticated() {
91+
return getAuthentication().map(Authentication::isAuthenticated)
92+
.orElse(false);
93+
}
94+
95+
/**
96+
* Initiates the logout process of the current authenticated user by
97+
* invalidating the local session and then notifying
98+
* {@link org.springframework.security.web.authentication.logout.LogoutHandler}.
99+
*/
100+
public void logout() {
101+
HttpServletRequest request = VaadinServletRequest.getCurrent()
102+
.getHttpServletRequest();
103+
HttpServletResponse response = VaadinServletResponse.getCurrent()
104+
.getHttpServletResponse();
105+
Authentication auth = SecurityContextHolder.getContext()
106+
.getAuthentication();
107+
108+
final UI ui = UI.getCurrent();
109+
logoutHandler.logout(request, response, auth);
110+
ui.accessSynchronously(() -> {
111+
try {
112+
logoutSuccessHandler.onLogoutSuccess(request, response, auth);
113+
} catch (IOException | ServletException e) {
114+
// Raise a warning log message about the failure.
115+
LOGGER.warn(
116+
"There was an error notifying the logout handler about the user logout",
117+
e);
118+
}
119+
});
120+
}
121+
122+
/**
123+
* Sets component to handle logout process.
124+
*
125+
* This method should be invoked after deserialization to refresh required
126+
* transient fields.
127+
*
128+
* @param logoutSuccessHandler
129+
* {@link LogoutSuccessHandler} instance, not {@literal null}.
130+
* @param logoutHandlers
131+
* {@link LogoutHandler}s list, not {@literal null}.
132+
*/
133+
void setLogoutHandlers(LogoutSuccessHandler logoutSuccessHandler,
134+
List<LogoutHandler> logoutHandlers) {
135+
this.logoutSuccessHandler = logoutSuccessHandler;
136+
this.logoutHandler = new CompositeLogoutHandler(logoutHandlers);
137+
}
138+
139+
private static Optional<Authentication> getAuthentication() {
140+
return Optional.of(SecurityContextHolder.getContext())
141+
.map(SecurityContext::getAuthentication)
142+
.filter(auth -> !(auth instanceof AnonymousAuthenticationToken));
143+
}
144+
145+
/* For testing purposes */
146+
LogoutSuccessHandler getLogoutSuccessHandler() {
147+
return logoutSuccessHandler;
148+
}
149+
150+
/* For testing purposes */
151+
CompositeLogoutHandler getLogoutHandler() {
152+
return logoutHandler;
153+
}
154+
155+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.vaadin.flow.spring.security;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import jakarta.servlet.http.HttpServletResponse;
5+
import java.io.IOException;
6+
7+
import org.slf4j.LoggerFactory;
8+
import org.springframework.security.web.DefaultRedirectStrategy;
9+
10+
import com.vaadin.flow.component.UI;
11+
import com.vaadin.flow.server.HandlerHelper;
12+
13+
/**
14+
* A strategy to handle redirects which is aware of UIDL requests.
15+
*
16+
* @author Vaadin Ltd
17+
* @since 1.0
18+
*/
19+
public class UidlRedirectStrategy extends DefaultRedirectStrategy {
20+
21+
@Override
22+
public void sendRedirect(HttpServletRequest request,
23+
HttpServletResponse response, String url) throws IOException {
24+
final var servletMapping = request.getHttpServletMapping().getPattern();
25+
if (HandlerHelper.isFrameworkInternalRequest(servletMapping, request)) {
26+
UI ui = UI.getCurrent();
27+
if (ui != null) {
28+
ui.getPage().setLocation(url);
29+
} else {
30+
LoggerFactory.getLogger(UidlRedirectStrategy.class).warn(
31+
"A redirect to {} was request during a Vaadin request, "
32+
+ "but it was not possible to get the UI instance to perform the action.",
33+
url);
34+
}
35+
} else {
36+
super.sendRedirect(request, response, url);
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)