Skip to content

Commit b598b50

Browse files
vaadin-bottaefi
andauthored
fix: Fix navigation to secured view with oauth2 without router-ignore (#14523) (#14653)
This is to fix the problem of navigating to a secured view from a public view before login in while using the OAuth2 external login page. Prior to this, it was needed to set "router-ignore" attribute on the links from public pages to skip vaadin-router and let the spring-security forward the user to the external login page. Fixes #14253 Co-authored-by: Soroosh Taefi <taefi.soroosh@gmail.com>
1 parent 673baa7 commit b598b50

6 files changed

Lines changed: 290 additions & 41 deletions

File tree

flow-server/src/main/java/com/vaadin/flow/server/auth/ViewAccessChecker.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import javax.annotation.security.DenyAll;
2222
import javax.annotation.security.PermitAll;
2323
import javax.annotation.security.RolesAllowed;
24+
import javax.servlet.http.HttpServletRequest;
2425
import javax.servlet.http.HttpSession;
2526

2627
import com.vaadin.flow.component.Component;

flow-server/src/test/java/com/vaadin/flow/server/auth/AccessAnnotationCheckerTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public String getName() {
5757
return "John Doe";
5858
}
5959
};
60+
static final String REQUEST_URL = "http://localhost:8080/myapp/";
6061

6162
@Rule
6263
public ExpectedException exception = ExpectedException.none();
@@ -364,6 +365,8 @@ static HttpServletRequest createRequest(Principal userPrincipal,
364365
.thenAnswer(query -> {
365366
return roleSet.contains(query.getArguments()[0]);
366367
});
368+
Mockito.when(request.getRequestURL())
369+
.thenReturn(new StringBuffer(REQUEST_URL));
367370
return request;
368371
}
369372

flow-server/src/test/java/com/vaadin/flow/server/auth/ViewAccessCheckerTest.java

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
package com.vaadin.flow.server.auth;
22

3+
import javax.servlet.http.HttpServletRequest;
4+
import javax.servlet.http.HttpSession;
35
import java.lang.reflect.Field;
46
import java.security.Principal;
57
import java.util.ArrayList;
68
import java.util.HashMap;
79
import java.util.Map;
810
import java.util.Optional;
911

10-
import javax.servlet.http.HttpServletRequest;
11-
import javax.servlet.http.HttpSession;
12+
import org.junit.Assert;
13+
import org.junit.Before;
14+
import org.junit.Test;
15+
import org.mockito.Mockito;
1216

1317
import com.vaadin.flow.component.Component;
1418
import com.vaadin.flow.component.UI;
@@ -34,24 +38,19 @@
3438
import com.vaadin.flow.server.VaadinSession;
3539
import com.vaadin.flow.server.auth.AccessControlTestClasses.AnonymousAllowedView;
3640
import com.vaadin.flow.server.auth.AccessControlTestClasses.DenyAllView;
41+
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationAnonymousAllowedByGrandParentView;
42+
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationAnonymousAllowedByParentView;
43+
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationDenyAllAsInterfacesIgnoredView;
44+
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationDenyAllByGrandParentView;
45+
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationPermitAllByGrandParentAsInterfacesIgnoredView;
46+
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationPermitAllByGrandParentView;
47+
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationRolesAllowedAdminByGrandParentView;
48+
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationRolesAllowedUserByGrandParentView;
3749
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationView;
3850
import com.vaadin.flow.server.auth.AccessControlTestClasses.PermitAllView;
3951
import com.vaadin.flow.server.auth.AccessControlTestClasses.RolesAllowedAdminView;
4052
import com.vaadin.flow.server.auth.AccessControlTestClasses.RolesAllowedUserView;
4153
import com.vaadin.flow.server.auth.AccessControlTestClasses.TestLoginView;
42-
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationAnonymousAllowedByParentView;
43-
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationAnonymousAllowedByGrandParentView;
44-
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationPermitAllByGrandParentView;
45-
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationDenyAllByGrandParentView;
46-
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationRolesAllowedUserByGrandParentView;
47-
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationRolesAllowedAdminByGrandParentView;
48-
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationDenyAllAsInterfacesIgnoredView;
49-
import com.vaadin.flow.server.auth.AccessControlTestClasses.NoAnnotationPermitAllByGrandParentAsInterfacesIgnoredView;
50-
51-
import org.junit.Assert;
52-
import org.junit.Before;
53-
import org.junit.Test;
54-
import org.mockito.Mockito;
5554

5655
public class ViewAccessCheckerTest {
5756

@@ -233,6 +232,11 @@ public void loginViewAccessAlwaysAllowed() {
233232
public void redirectUrlStoredForAnonymousUsers() {
234233
Result result = checkAccess(RolesAllowedAdminView.class, null);
235234
Assert.assertFalse(result.wasTargetViewRendered());
235+
Assert.assertEquals(
236+
AccessAnnotationCheckerTest.REQUEST_URL
237+
+ getRoute(RolesAllowedAdminView.class),
238+
result.sessionAttributes.get(
239+
ViewAccessChecker.SESSION_STORED_REDIRECT_ABSOLUTE));
236240
Assert.assertEquals(getRoute(RolesAllowedAdminView.class),
237241
result.sessionAttributes
238242
.get(ViewAccessChecker.SESSION_STORED_REDIRECT));
@@ -245,6 +249,8 @@ public void redirectUrlNotStoredForLoggedInUsers() {
245249
Assert.assertFalse(result.wasTargetViewRendered());
246250
Assert.assertNull(result.sessionAttributes
247251
.get(ViewAccessChecker.SESSION_STORED_REDIRECT));
252+
Assert.assertNull(result.sessionAttributes
253+
.get(ViewAccessChecker.SESSION_STORED_REDIRECT_ABSOLUTE));
248254
}
249255

250256
@Test
@@ -658,14 +664,14 @@ private Result setupRequest(Class navigationTarget, User user,
658664
Mockito.when(vaadinServletRequest.getHttpServletRequest())
659665
.thenReturn(httpServletRequest);
660666
Mockito.when(vaadinServletRequest.getUserPrincipal())
661-
.thenAnswer(anasert -> httpServletRequest.getUserPrincipal());
667+
.thenAnswer(answer -> httpServletRequest.getUserPrincipal());
668+
Mockito.when(vaadinServletRequest.getSession())
669+
.thenAnswer(answer -> httpServletRequest.getSession());
662670
Mockito.when(vaadinServletRequest.isUserInRole(Mockito.any()))
663671
.thenAnswer(answer -> httpServletRequest
664672
.isUserInRole(answer.getArgument(0)));
665-
Mockito.when(vaadinServletRequest.getSession())
666-
.thenAnswer(answer -> httpServletRequest.getSession());
667-
Mockito.when(vaadinServletRequest.getRequestURL())
668-
.thenReturn(new StringBuffer("http://localhost:8080/"));
673+
Mockito.when(vaadinServletRequest.getRequestURL()).thenReturn(
674+
new StringBuffer(AccessAnnotationCheckerTest.REQUEST_URL));
669675

670676
CurrentInstance.set(VaadinRequest.class, vaadinServletRequest);
671677

vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java

Lines changed: 94 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@
2222
import javax.servlet.http.HttpServletResponse;
2323
import javax.servlet.http.HttpSession;
2424

25+
import com.vaadin.flow.router.BeforeEnterEvent;
2526
import com.vaadin.flow.server.auth.ViewAccessChecker;
2627

28+
import org.springframework.core.log.LogMessage;
2729
import org.springframework.security.core.Authentication;
2830
import org.springframework.security.web.DefaultRedirectStrategy;
2931
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
3032
import org.springframework.security.web.csrf.CsrfToken;
3133
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
3234
import org.springframework.security.web.savedrequest.RequestCache;
3335
import org.springframework.security.web.savedrequest.SavedRequest;
36+
import org.springframework.util.StringUtils;
3437

3538
/**
3639
* A version of {@link SavedRequestAwareAuthenticationSuccessHandler} that
@@ -89,16 +92,9 @@ public static class RedirectStrategy extends DefaultRedirectStrategy {
8992
@Override
9093
public void sendRedirect(HttpServletRequest request,
9194
HttpServletResponse response, String url) throws IOException {
92-
String redirectUrl;
93-
String savedRedirectUrl = response.getHeader(SAVED_URL_HEADER);
94-
if (savedRedirectUrl != null) {
95-
redirectUrl = savedRedirectUrl;
96-
} else {
97-
redirectUrl = url;
98-
}
9995

10096
if (!isTypescriptLogin(request)) {
101-
super.sendRedirect(request, response, redirectUrl);
97+
super.sendRedirect(request, response, url);
10298
return;
10399
}
104100

@@ -126,33 +122,111 @@ public void sendRedirect(HttpServletRequest request,
126122
*/
127123
public VaadinSavedRequestAwareAuthenticationSuccessHandler() {
128124
setRedirectStrategy(new RedirectStrategy());
125+
setTargetUrlParameter(SAVED_URL_HEADER);
129126
}
130127

128+
/**
129+
* Called when a user has been successfully authenticated and finds out
130+
* whether it should redirect the user back to a default success url or the
131+
* originally requested url before the authentication.
132+
* <p>
133+
* As the user might have initiated the request to a restricted resource in
134+
* different ways, this method is responsible for extracting the final
135+
* target for redirection of the user and to set it on the response header,
136+
* so that it can be used by the redirection strategy in a unified way. See
137+
* {@link RedirectStrategy} and
138+
* {@link VaadinSavedRequestAwareAuthenticationSuccessHandler#determineTargetUrl(HttpServletRequest, HttpServletResponse)}
139+
* <p>
140+
* If the redirection to the login page for authentication is initiated by
141+
* spring security (such as entering some URI manually into the address bar
142+
* and not navigating via Vaadin application), then a SavedRequest object
143+
* containing the originally requested path is pushed to the request cache
144+
* by the Spring Security so the redirect target url would be extracted from
145+
* that.
146+
* <p>
147+
* Contrarily, navigating via Vaadin application router (e.g. via menus or
148+
* the links within the application) will result in requests being sent to
149+
* "/" or "/{app-context-root}", so the Spring Security will not intercept
150+
* and the SavedRequest will be null. In this case, the target redirect url
151+
* can be extracted from the session. See
152+
* {@link ViewAccessChecker#beforeEnter(BeforeEnterEvent)}
153+
*
154+
* @param request
155+
* the request which caused the successful authentication
156+
* @param response
157+
* the response
158+
* @param authentication
159+
* the <tt>Authentication</tt> object which was created during
160+
* the authentication process.
161+
*/
131162
@Override
132163
public void onAuthenticationSuccess(HttpServletRequest request,
133164
HttpServletResponse response, Authentication authentication)
134165
throws ServletException, IOException {
135-
SavedRequest savedRequest = this.requestCache.getRequest(request,
136-
response);
137-
String storedServerNavigation = getStoredServerNavigation(request);
138-
if (storedServerNavigation != null) {
139-
response.setHeader(SAVED_URL_HEADER, storedServerNavigation);
140-
} else if (savedRequest != null) {
141-
/*
142-
* This is here instead of in sendRedirect as we do not want to
143-
* fallback to the default URL but instead send that separately.
144-
*/
145-
response.setHeader(SAVED_URL_HEADER, savedRequest.getRedirectUrl());
146-
}
147166

148167
if (isTypescriptLogin(request)) {
149168
response.setHeader(DEFAULT_URL_HEADER,
150169
determineTargetUrl(request, response));
151170
}
152171

172+
SavedRequest savedRequest = this.requestCache.getRequest(request,
173+
response);
174+
String fullySavedRequestUrl = getStoredServerNavigation(request);
175+
176+
if (savedRequest != null) {
177+
String targetUrlParameter = this.getTargetUrlParameter();
178+
if (!this.isAlwaysUseDefaultTargetUrl()
179+
&& (targetUrlParameter == null || !StringUtils.hasText(
180+
request.getParameter(targetUrlParameter)))) {
181+
this.clearAuthenticationAttributes(request);
182+
String targetUrl = savedRequest.getRedirectUrl();
183+
response.setHeader(SAVED_URL_HEADER, targetUrl);
184+
this.getRedirectStrategy().sendRedirect(request, response,
185+
targetUrl);
186+
return;
187+
} else {
188+
this.requestCache.removeRequest(request, response);
189+
}
190+
} else if (fullySavedRequestUrl != null) {
191+
response.setHeader(SAVED_URL_HEADER, fullySavedRequestUrl);
192+
}
193+
153194
super.onAuthenticationSuccess(request, response, authentication);
154195
}
155196

197+
/**
198+
* Determines the originally requested path by the user before
199+
* authentication by reading the target redirect url from the response
200+
* header.
201+
* <p>
202+
* Note that if a defaultSuccessUrl has been configured on the http security
203+
* configurer, or the value of {@code targetUrlParameter} is {@code null},
204+
* it will fall back to the default super class implementation.
205+
*
206+
* @param request
207+
* the http servlet request instance
208+
* @param response
209+
* the http servlet response instance
210+
* @return the original requested path by the user before authentication.
211+
*/
212+
@Override
213+
protected String determineTargetUrl(HttpServletRequest request,
214+
HttpServletResponse response) {
215+
if (!isAlwaysUseDefaultTargetUrl()
216+
&& this.getTargetUrlParameter() != null) {
217+
String targetUrl = response.getHeader(this.getTargetUrlParameter());
218+
if (StringUtils.hasText(targetUrl)) {
219+
if (this.logger.isTraceEnabled()) {
220+
this.logger.trace(LogMessage.format(
221+
"Using url %s from response header %s", targetUrl,
222+
this.getTargetUrlParameter()));
223+
}
224+
return targetUrl;
225+
}
226+
}
227+
return super.determineTargetUrl(request, response);
228+
}
229+
156230
/**
157231
* Gets the target URL potentially stored by the server side view access
158232
* control.

vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurity.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import javax.servlet.http.HttpServletResponse;
2929

3030
import org.springframework.beans.factory.annotation.Autowired;
31+
import org.springframework.beans.factory.annotation.Value;
3132
import org.springframework.context.annotation.Bean;
3233
import org.springframework.http.HttpStatus;
3334
import org.springframework.security.access.AccessDeniedException;
@@ -96,6 +97,9 @@ public abstract class VaadinWebSecurity {
9697
@Autowired
9798
private ViewAccessChecker viewAccessChecker;
9899

100+
@Value("#{servletContext.contextPath}")
101+
private String servletContextPath;
102+
99103
/**
100104
* Registers default {@link SecurityFilterChain} bean.
101105
* <p>
@@ -383,6 +387,27 @@ protected void setLoginView(HttpSecurity http,
383387
viewAccessChecker.setLoginView(flowLoginView);
384388
}
385389

390+
/**
391+
* Sets up the login page URI of the OAuth2 provider on the specified
392+
* HttpSecurity instance.
393+
*
394+
* @param http
395+
* the http security from {@link #filterChain(HttpSecurity)}
396+
* @param oauth2LoginPage
397+
* the login page of the OAuth2 provider. This Specifies the URL
398+
* to send users to if login is required.
399+
* @throws Exception
400+
* Re-throws the possible exceptions while activating
401+
* OAuth2LoginConfigurer
402+
*/
403+
protected void setOAuth2LoginPage(HttpSecurity http, String oauth2LoginPage)
404+
throws Exception {
405+
http.oauth2Login().loginPage(oauth2LoginPage).successHandler(
406+
getVaadinSavedRequestAwareAuthenticationSuccessHandler(http))
407+
.permitAll();
408+
viewAccessChecker.setLoginView(servletContextPath + oauth2LoginPage);
409+
}
410+
386411
/**
387412
* Sets up stateless JWT authentication using cookies.
388413
*

0 commit comments

Comments
 (0)