diff --git a/vaadin-spring-tests/test-spring-security-fusion-contextpath/src/main/java/com/vaadin/flow/spring/fusionsecurity/endpoints/BalanceEndpoint.java b/vaadin-spring-tests/test-spring-security-fusion-contextpath/src/main/java/com/vaadin/flow/spring/fusionsecurity/endpoints/BalanceEndpoint.java index f4e006dc9..45d6ae810 100644 --- a/vaadin-spring-tests/test-spring-security-fusion-contextpath/src/main/java/com/vaadin/flow/spring/fusionsecurity/endpoints/BalanceEndpoint.java +++ b/vaadin-spring-tests/test-spring-security-fusion-contextpath/src/main/java/com/vaadin/flow/spring/fusionsecurity/endpoints/BalanceEndpoint.java @@ -1,13 +1,12 @@ package com.vaadin.flow.spring.fusionsecurity.endpoints; +import javax.annotation.security.PermitAll; import java.math.BigDecimal; -import javax.annotation.security.PermitAll; +import org.springframework.beans.factory.annotation.Autowired; -import com.vaadin.fusion.Endpoint; import com.vaadin.flow.spring.fusionsecurity.service.BankService; - -import org.springframework.beans.factory.annotation.Autowired; +import com.vaadin.fusion.Endpoint; @Endpoint @PermitAll diff --git a/vaadin-spring-tests/test-spring-security-fusion-jwt/pom.xml b/vaadin-spring-tests/test-spring-security-fusion-jwt/pom.xml index 0e92eca80..59d901117 100644 --- a/vaadin-spring-tests/test-spring-security-fusion-jwt/pom.xml +++ b/vaadin-spring-tests/test-spring-security-fusion-jwt/pom.xml @@ -62,6 +62,10 @@ org.springframework.security spring-security-oauth2-jose + + org.springframework.security + spring-security-oauth2-resource-server + com.vaadin vaadin-button-testbench diff --git a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/Application.java b/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/Application.java index 1ea07161e..4c0297d8a 100644 --- a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/Application.java +++ b/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/Application.java @@ -1,19 +1,18 @@ package com.vaadin.flow.spring.fusionsecurityjwt; -import com.vaadin.flow.spring.fusionsecurity.SecurityConfig; - import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Import; -@SpringBootApplication -@ComponentScan(basePackages = {"com.vaadin.flow.flow.fusionsecurity"}, - excludeFilters = - {@ComponentScan.Filter(type=FilterType.ASSIGNABLE_TYPE, value = SecurityConfig.class), - @ComponentScan.Filter(type=FilterType.REGEX, - pattern="com\\.vaadin\\.flow\\.flow\\.fusionsecurity\\.endpoints\\..*")}) -public class Application extends com.vaadin.flow.spring.fusionsecurity.Application { +@SpringBootApplication() +@ComponentScan(basePackages = { + "com.vaadin.flow.spring.fusionsecurity" }, excludeFilters = { + @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com\\.vaadin\\.flow\\.spring\\.fusionsecurity\\.endpoints\\..*") }) +@Import(JwtSecurityUtils.class) +public class Application + extends com.vaadin.flow.spring.fusionsecurity.Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); diff --git a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/auth/JwtSplitCookieBearerTokenConverterFilter.java b/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/auth/JwtSplitCookieBearerTokenConverterFilter.java deleted file mode 100644 index 59ca69e60..000000000 --- a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/auth/JwtSplitCookieBearerTokenConverterFilter.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.vaadin.flow.spring.fusionsecurityjwt.auth; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import java.io.IOException; - -public class JwtSplitCookieBearerTokenConverterFilter implements Filter { - - @Override - public void doFilter(ServletRequest request, ServletResponse response, - FilterChain chain) throws IOException, ServletException { - final String tokenFromSplitCookies = JwtSplitCookieUtils - .getTokenFromSplitCookies((HttpServletRequest) request); - if (tokenFromSplitCookies != null) { - HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper( - (HttpServletRequest) request) { - @Override - public String getHeader(String headerName) { - if ("Authorization".equals(headerName)) { - return "Bearer " + tokenFromSplitCookies; - } - return super.getHeader(headerName); - } - }; - chain.doFilter(requestWrapper, response); - } else { - chain.doFilter(request, response); - } - } - -} diff --git a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/auth/JwtSplitCookieManagementFilter.java b/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/auth/JwtSplitCookieManagementFilter.java deleted file mode 100644 index 2c0407ca6..000000000 --- a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/auth/JwtSplitCookieManagementFilter.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.vaadin.flow.spring.fusionsecurityjwt.auth; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; - -public class JwtSplitCookieManagementFilter implements Filter { - - @Override - public void doFilter(ServletRequest request, ServletResponse response, - FilterChain chain) throws IOException, ServletException { - Authentication authentication = SecurityContextHolder.getContext() - .getAuthentication(); - - if (authentication == null || - authentication instanceof AnonymousAuthenticationToken) { - // Token authentication failed — remove the cookies - JwtSplitCookieUtils - .removeJwtSplitCookies((HttpServletRequest) request, - (HttpServletResponse) response); - } else { - JwtSplitCookieUtils - .setJwtSplitCookiesIfNecessary((HttpServletRequest) request, - (HttpServletResponse) response, authentication); - } - - chain.doFilter(request, response); - } - -} diff --git a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/auth/JwtSplitCookieUtils.java b/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/auth/JwtSplitCookieUtils.java deleted file mode 100644 index b8842c2c0..000000000 --- a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/auth/JwtSplitCookieUtils.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.vaadin.flow.spring.fusionsecurityjwt.auth; - -import javax.servlet.ServletContext; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.nio.charset.StandardCharsets; -import java.util.Date; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.JWSHeader; -import com.nimbusds.jose.JWSSigner; -import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.JWKMatcher; -import com.nimbusds.jose.jwk.JWKSelector; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.proc.SecurityContext; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.SignedJWT; -import org.springframework.core.ResolvableType; -import org.springframework.security.core.Authentication; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.context.support.WebApplicationContextUtils; -import org.springframework.web.util.WebUtils; - -public class JwtSplitCookieUtils { - public static final String JWT_HEADER_AND_PAYLOAD_COOKIE_NAME = "jwt.headerAndPayload"; - public static final String JWT_SIGNATURE_COOKIE_NAME = "jwt.signature"; - - public static String getTokenFromSplitCookies(HttpServletRequest request) { - Cookie[] cookies = request.getCookies(); - if (cookies == null) { - return null; - } - - Cookie jwtHeaderAndPayload = WebUtils - .getCookie(request, JWT_HEADER_AND_PAYLOAD_COOKIE_NAME); - if (jwtHeaderAndPayload == null) { - return null; - } - - Cookie jwtSignature = WebUtils - .getCookie(request, JWT_SIGNATURE_COOKIE_NAME); - if (jwtSignature == null) { - return null; - } - - return jwtHeaderAndPayload.getValue() + "." + jwtSignature.getValue(); - } - - public static void setJwtSplitCookiesIfNecessary(HttpServletRequest request, - HttpServletResponse response, Authentication authentication) { - ServletContext servletContext = request.getServletContext(); - WebApplicationContext webApplicationContext = WebApplicationContextUtils - .getWebApplicationContext(servletContext); - - JWKSource jwkSource = (JWKSource) webApplicationContext - .getBean(webApplicationContext.getBeanNamesForType( - ResolvableType.forClassWithGenerics(JWKSource.class, - SecurityContext.class))[0]); - - final long EXPIRES_IN = 3600L; - - final Date now = new Date(); - - final String rolePrefix = "ROLE_"; - final String scope = authentication.getAuthorities().stream() - .map(Objects::toString).filter(a -> a.startsWith(rolePrefix)) - .map(a -> a.substring(rolePrefix.length())) - .collect(Collectors.joining(" ")); - - SignedJWT signedJWT; - try { - JWSAlgorithm jwsAlgorithm = JWSAlgorithm.HS256; - JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256); - JWKSelector jwkSelector = new JWKSelector( - JWKMatcher.forJWSHeader(jwsHeader)); - - List jwks = jwkSource.get(jwkSelector, null); - JWK jwk = jwks.get(0); - - JWSSigner signer = new DefaultJWSSignerFactory() - .createJWSSigner(jwk, jwsAlgorithm); - JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() - .subject(authentication.getName()).issuer("statelessapp") - .issueTime(now) - .expirationTime(new Date(now.getTime() + EXPIRES_IN * 1000)) - .claim("scope", scope).build(); - signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), - claimsSet); - signedJWT.sign(signer); - - Cookie headerAndPayload = new Cookie( - JWT_HEADER_AND_PAYLOAD_COOKIE_NAME, - new String(signedJWT.getSigningInput(), - StandardCharsets.UTF_8)); - headerAndPayload.setSecure(true); - headerAndPayload.setHttpOnly(false); - headerAndPayload.setPath(request.getContextPath() + "/"); - headerAndPayload.setMaxAge((int) EXPIRES_IN - 1); - response.addCookie(headerAndPayload); - - Cookie signature = new Cookie(JWT_SIGNATURE_COOKIE_NAME, - signedJWT.getSignature().toString()); - signature.setHttpOnly(true); - signature.setSecure(true); - signature.setPath(request.getContextPath() + "/"); - signature.setMaxAge((int) EXPIRES_IN - 1); - response.addCookie(signature); - } catch (JOSEException e) { - e.printStackTrace(); - } - - } - - public static void removeJwtSplitCookies(HttpServletRequest request, - HttpServletResponse response) { - Cookie jwtHeaderAndPayloadRemove = new Cookie( - JWT_HEADER_AND_PAYLOAD_COOKIE_NAME, null); - jwtHeaderAndPayloadRemove.setPath(request.getContextPath() + "/"); - jwtHeaderAndPayloadRemove.setMaxAge(0); - jwtHeaderAndPayloadRemove.setSecure(request.isSecure()); - jwtHeaderAndPayloadRemove.setHttpOnly(false); - response.addCookie(jwtHeaderAndPayloadRemove); - - Cookie jwtSignatureRemove = new Cookie(JWT_SIGNATURE_COOKIE_NAME, null); - jwtSignatureRemove.setPath(request.getContextPath() + "/"); - jwtSignatureRemove.setMaxAge(0); - jwtSignatureRemove.setSecure(request.isSecure()); - jwtSignatureRemove.setHttpOnly(true); - response.addCookie(jwtSignatureRemove); - } -} diff --git a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/config/SecurityConfiguration.java b/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/config/SecurityConfiguration.java deleted file mode 100644 index 2b8b11bbc..000000000 --- a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/config/SecurityConfiguration.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.vaadin.flow.spring.fusionsecurityjwt.config; - -import java.util.stream.Collectors; - -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.OctetSequenceKey; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.proc.SecurityContext; -import com.nimbusds.jose.util.Base64URL; -import com.vaadin.flow.spring.fusionsecurity.data.UserInfo; -import com.vaadin.flow.spring.fusionsecurity.data.UserInfoRepository; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.core.annotation.Order; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -@EnableWebSecurity -@Order(10) -public class SecurityConfiguration extends VaadinStatelessWebSecurityConfig { - - public static String ROLE_USER = "user"; - public static String ROLE_ADMIN = "admin"; - - @Autowired - private UserInfoRepository userInfoRepository; - - @Override - protected void configure(HttpSecurity http) throws Exception { - // @formatter:off - // Public access - http.authorizeRequests().antMatchers("/").permitAll(); - // Admin only access - http.authorizeRequests().antMatchers("/admin-only/**") - .hasAnyRole(ROLE_ADMIN); - super.configure(http); - - http - .sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.STATELESS); - - setJwtSplitCookieAuthentication(http, "statelessapp", 3600, - JWSAlgorithm.HS256); - setLoginView(http, "/login"); - // @formatter:on - } - - @Bean - JWKSource jwkSource() { - OctetSequenceKey key = new OctetSequenceKey.Builder( - Base64URL.from("I72kIcB1UrUQVHVUAzgweE+BLc0bF8mLv9SmrgKsQAk=")) - .algorithm(JWSAlgorithm.HS256).build(); - JWKSet jwkSet = new JWKSet(key); - return (jwkSelector, context) -> jwkSelector.select(jwkSet); - } - - @Override - public void configure(WebSecurity web) throws Exception { - super.configure(web); - web.ignoring().antMatchers("/public/**"); - } - - @Override - protected void configure(AuthenticationManagerBuilder auth) - throws Exception { - auth.userDetailsService(username -> { - UserInfo userInfo = userInfoRepository.findByUsername(username); - if (userInfo == null) { - throw new UsernameNotFoundException( - "No user present with username: " + username); - } else { - return new User(userInfo.getUsername(), - userInfo.getEncodedPassword(), - userInfo.getRoles().stream() - .map(role -> new SimpleGrantedAuthority( - "ROLE_" + role)) - .collect(Collectors.toList())); - } - }); - } -} diff --git a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/config/VaadinStatelessSavedRequestAwareAuthenticationSuccessHandler.java b/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/config/VaadinStatelessSavedRequestAwareAuthenticationSuccessHandler.java deleted file mode 100644 index 9e07fef5e..000000000 --- a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/config/VaadinStatelessSavedRequestAwareAuthenticationSuccessHandler.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.vaadin.flow.spring.fusionsecurityjwt.config; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.vaadin.flow.spring.fusionsecurityjwt.auth.JwtSplitCookieUtils; -import com.vaadin.flow.spring.security.VaadinSavedRequestAwareAuthenticationSuccessHandler; - -import org.springframework.security.core.Authentication; - -public class VaadinStatelessSavedRequestAwareAuthenticationSuccessHandler extends VaadinSavedRequestAwareAuthenticationSuccessHandler { - - @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, - Authentication authentication) throws ServletException, IOException { - JwtSplitCookieUtils.setJwtSplitCookiesIfNecessary(request, response, authentication); - super.onAuthenticationSuccess(request, response, authentication); - - } -} diff --git a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/config/VaadinStatelessWebSecurityConfig.java b/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/config/VaadinStatelessWebSecurityConfig.java deleted file mode 100644 index de4aa5af3..000000000 --- a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/config/VaadinStatelessWebSecurityConfig.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.vaadin.flow.spring.fusionsecurityjwt.config; - -import java.util.HashSet; -import java.util.Set; - -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.proc.JWSKeySelector; -import com.nimbusds.jose.proc.JWSVerificationKeySelector; -import com.nimbusds.jose.proc.SecurityContext; -import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; -import com.nimbusds.jwt.proc.DefaultJWTProcessor; -import org.springframework.context.annotation.Bean; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; -import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; -import org.springframework.security.web.authentication.switchuser.SwitchUserFilter; -import org.springframework.security.web.context.SecurityContextPersistenceFilter; - -import com.vaadin.flow.spring.fusionsecurityjwt.auth.JwtSplitCookieBearerTokenConverterFilter; -import com.vaadin.flow.spring.fusionsecurityjwt.auth.JwtSplitCookieManagementFilter; -import com.vaadin.flow.spring.fusionsecurityjwt.auth.JwtSplitCookieUtils; - -import com.vaadin.flow.spring.security.VaadinWebSecurityConfigurerAdapter; - -public class VaadinStatelessWebSecurityConfig - extends VaadinWebSecurityConfigurerAdapter { - protected void setJwtSplitCookieAuthentication(HttpSecurity http, - String issuer, long expires_in, JWSAlgorithm algorithm) - throws Exception { - // @formatter:off - http - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) - .addFilterAfter(new JwtSplitCookieBearerTokenConverterFilter(), - SecurityContextPersistenceFilter.class) - .addFilterAfter(new JwtSplitCookieManagementFilter(), - SwitchUserFilter.class); - // @formatter:on - } - - @Override - protected void setLoginView(HttpSecurity http, String fusionLoginViewPath, String logoutUrl) throws Exception { - super.setLoginView(http, fusionLoginViewPath, logoutUrl); - http.formLogin().successHandler(new VaadinStatelessSavedRequestAwareAuthenticationSuccessHandler()); - http.logout().addLogoutHandler((request, response, authentication) -> { - JwtSplitCookieUtils.removeJwtSplitCookies(request, response); - }); - } - - @Bean - JwtAuthenticationConverter jwtAuthenticationConverter() { - // Converter from "scope" claims in JWT into ROLE_ prefixed authorities. - JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); - grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); - - JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); - jwtAuthenticationConverter - .setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); - return jwtAuthenticationConverter; - } - - @Bean - JwtDecoder jwtDecoder(JWKSource jwkSource) { - Set jwsAlgorithmSet = new HashSet<>(); - jwsAlgorithmSet.addAll(JWSAlgorithm.Family.RSA); - jwsAlgorithmSet.addAll(JWSAlgorithm.Family.EC); - jwsAlgorithmSet.addAll(JWSAlgorithm.Family.HMAC_SHA); - JWSKeySelector jwsKeySelector = new JWSVerificationKeySelector<>( - jwsAlgorithmSet, jwkSource); - ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); - jwtProcessor.setJWSKeySelector(jwsKeySelector); - jwtProcessor.setJWTClaimsSetVerifier((claimsSet, context) -> { - }); - return new NimbusJwtDecoder(jwtProcessor); - } -} diff --git a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/resources/application.properties b/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/resources/application.properties new file mode 100644 index 000000000..38bff441a --- /dev/null +++ b/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/resources/application.properties @@ -0,0 +1,5 @@ +server.port=9999 +logging.level.org.springframework.security=DEBUG +logging.level.com.vaadin.flow.server.connect.auth=DEBUG +logging.level.com.vaadin.flow.server.auth=DEBUG +springSecurityTestApp.security.stateless = true \ No newline at end of file diff --git a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/test/java/com/vaadin/flow/spring/fusionsecurityjwt/SecurityIT.java b/vaadin-spring-tests/test-spring-security-fusion-jwt/src/test/java/com/vaadin/flow/spring/fusionsecurityjwt/SecurityIT.java index fc3a21901..b6df6a815 100644 --- a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/test/java/com/vaadin/flow/spring/fusionsecurityjwt/SecurityIT.java +++ b/vaadin-spring-tests/test-spring-security-fusion-jwt/src/test/java/com/vaadin/flow/spring/fusionsecurityjwt/SecurityIT.java @@ -1,5 +1,56 @@ package com.vaadin.flow.spring.fusionsecurityjwt; -public class SecurityIT extends com.vaadin.flow.spring.fusionsecurity.SecurityIT { +import java.util.Base64; +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.Cookie; + +import elemental.json.Json; +import elemental.json.JsonObject; + +public class SecurityIT + extends com.vaadin.flow.spring.fusionsecurity.SecurityIT { + + @Test + public void cookie_set_for_user() { + openLogin(); + loginUser(); + checkJwtUsername("john"); + } + + @Test + public void cookie_set_for_admin() { + openLogin(); + loginAdmin(); + checkJwtUsername("emma"); + } + + @Test + public void cookie_reset_on_logout() { + openLogin(); + Assert.assertNull(getJwtCookie()); + loginUser(); + logout(); + Assert.assertNull(getJwtCookie()); + } + + private void openLogin() { + getDriver().get(getRootURL() + "/login"); + } + + private Cookie getJwtCookie() { + return getDriver().manage().getCookieNamed("jwt" + + ".headerAndPayload"); + } + + private void checkJwtUsername(String expectedUsername) { + Cookie jwtCookie = getJwtCookie(); + Assert.assertNotNull(jwtCookie); + + String payload = jwtCookie.getValue().split("\\.")[1]; + JsonObject payloadJson = Json.parse( + new String(Base64.getUrlDecoder().decode(payload))); + Assert.assertEquals(expectedUsername, payloadJson.getString("sub")); + } } diff --git a/vaadin-spring-tests/test-spring-security-fusion/pom.xml b/vaadin-spring-tests/test-spring-security-fusion/pom.xml index eea68ecea..6d8e6916c 100644 --- a/vaadin-spring-tests/test-spring-security-fusion/pom.xml +++ b/vaadin-spring-tests/test-spring-security-fusion/pom.xml @@ -48,6 +48,10 @@ com.h2database h2 + + org.springframework.security + spring-security-oauth2-jose + com.vaadin diff --git a/vaadin-spring-tests/test-spring-security-fusion/src/main/java/com/vaadin/flow/spring/fusionsecurity/SecurityConfig.java b/vaadin-spring-tests/test-spring-security-fusion/src/main/java/com/vaadin/flow/spring/fusionsecurity/SecurityConfig.java index 8baf8ddd6..52afb92d3 100644 --- a/vaadin-spring-tests/test-spring-security-fusion/src/main/java/com/vaadin/flow/spring/fusionsecurity/SecurityConfig.java +++ b/vaadin-spring-tests/test-spring-security-fusion/src/main/java/com/vaadin/flow/spring/fusionsecurity/SecurityConfig.java @@ -1,25 +1,27 @@ package com.vaadin.flow.spring.fusionsecurity; +import javax.crypto.spec.SecretKeySpec; +import java.util.Base64; import java.util.stream.Collectors; -import com.vaadin.flow.spring.fusionsecurity.data.UserInfo; -import com.vaadin.flow.spring.fusionsecurity.data.UserInfoRepository; -import com.vaadin.flow.spring.security.VaadinWebSecurityConfigurerAdapter; -import com.vaadin.flow.spring.security.VaadinSavedRequestAwareAuthenticationSuccessHandler; - import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; + +import com.vaadin.flow.spring.fusionsecurity.data.UserInfo; +import com.vaadin.flow.spring.fusionsecurity.data.UserInfoRepository; +import com.vaadin.flow.spring.security.VaadinWebSecurityConfigurerAdapter; @EnableWebSecurity @Configuration @@ -31,6 +33,9 @@ public class SecurityConfig extends VaadinWebSecurityConfigurerAdapter { @Autowired private UserInfoRepository userInfoRepository; + @Value("${springSecurityTestApp.security.stateless:false}") + private boolean stateless; + @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); @@ -48,6 +53,13 @@ protected void configure(HttpSecurity http) throws Exception { super.configure(http); setLoginView(http, "/login"); + + if (stateless) { + setStatelessAuthentication(http, new SecretKeySpec( + Base64.getUrlDecoder() + .decode("I72kIcB1UrUQVHVUAzgweE-BLc0bF8mLv9SmrgKsQAk"), + JwsAlgorithms.HS256), "statelessapp"); + } } @Override diff --git a/vaadin-spring-tests/test-spring-security-fusion/src/test/java/com/vaadin/flow/spring/fusionsecurity/SecurityIT.java b/vaadin-spring-tests/test-spring-security-fusion/src/test/java/com/vaadin/flow/spring/fusionsecurity/SecurityIT.java index 668e21872..95762ef9d 100644 --- a/vaadin-spring-tests/test-spring-security-fusion/src/test/java/com/vaadin/flow/spring/fusionsecurity/SecurityIT.java +++ b/vaadin-spring-tests/test-spring-security-fusion/src/test/java/com/vaadin/flow/spring/fusionsecurity/SecurityIT.java @@ -4,16 +4,17 @@ import java.util.List; import java.util.stream.Collectors; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + import com.vaadin.flow.component.button.testbench.ButtonElement; import com.vaadin.flow.component.login.testbench.LoginFormElement; import com.vaadin.flow.component.login.testbench.LoginOverlayElement; import com.vaadin.flow.testutil.ChromeBrowserTest; +import com.vaadin.testbench.ElementQuery; import com.vaadin.testbench.TestBenchElement; -import org.junit.After; -import org.junit.Assert; -import org.junit.Test; - public class SecurityIT extends ChromeBrowserTest { private static final String ROOT_PAGE_HEADER_TEXT = "Welcome to the TypeScript Bank of Vaadin"; @@ -36,12 +37,18 @@ public void tearDown() { private void checkForBrowserErrors() { checkLogsForErrors(msg -> { return msg.contains( - "admin-only/secret.txt - Failed to load resource: the server responded with a status of 403"); + "admin-only/secret.txt - Failed to load resource: the " + + "server responded with a status of 403") || + msg.contains("webpack-internal://"); }); } - private void logout() { - if (!$(ButtonElement.class).attribute("id", "logout").exists()) { + protected void logout() { + ElementQuery mainViewQuery = $("*").attribute("id", + "main-view"); + if (!mainViewQuery.exists() || + !mainViewQuery.get(0).$(ButtonElement.class) + .attribute("id", "logout").exists()) { open(""); assertRootPageShown(); } @@ -274,11 +281,11 @@ private void assertPathShown(String path) { .equals(getRootURL() + "/" + path)); } - private void loginUser() { + protected void loginUser() { login("john", "john"); } - private void loginAdmin() { + protected void loginAdmin() { login("emma", "emma"); } @@ -307,7 +314,7 @@ private void assertPageContains(String contents) { Assert.assertTrue(pageSource.contains(contents)); } - private List getMenuItems() { + protected List getMenuItems() { List anchors = getMainView().$("vaadin-tabs").first() .$("a").all(); diff --git a/vaadin-spring/pom.xml b/vaadin-spring/pom.xml index 1f6199c04..104e248d5 100644 --- a/vaadin-spring/pom.xml +++ b/vaadin-spring/pom.xml @@ -155,6 +155,16 @@ ${vaadin.flow.version} test + + org.springframework.security + spring-security-oauth2-jose + true + + + org.springframework.security + spring-security-oauth2-resource-server + true + diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSecurityContextRepository.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSecurityContextRepository.java new file mode 100644 index 000000000..d9fa0fbf2 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSecurityContextRepository.java @@ -0,0 +1,225 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.spring.security; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKMatcher; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.JWSKeySelector; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtValidators; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.security.web.context.HttpRequestResponseHolder; +import org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper; +import org.springframework.security.web.context.SecurityContextRepository; + +/** + * A {@link SecurityContextRepository} implementation that stores the + * authentication using a signed JWT persisted in cookies. + */ +class JwtSecurityContextRepository implements SecurityContextRepository { + private static final String ROLES_CLAIM = "roles"; + private static final String ROLE_AUTHORITY_PREFIX = "ROLE_"; + private final Log logger = LogFactory.getLog(this.getClass()); + private final SerializedJwtSplitCookieRepository serializedJwtSplitCookieRepository = new SerializedJwtSplitCookieRepository(); + private final JwtAuthenticationConverter jwtAuthenticationConverter; + private String issuer; + private long expiresIn = 1800L; + private JWKSource jwkSource; + private JWSAlgorithm jwsAlgorithm; + private JwtDecoder jwtDecoder; + private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + + JwtSecurityContextRepository() { + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + grantedAuthoritiesConverter.setAuthorityPrefix(ROLE_AUTHORITY_PREFIX); + grantedAuthoritiesConverter.setAuthoritiesClaimName(ROLES_CLAIM); + + jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter( + grantedAuthoritiesConverter); + } + + void setJwkSource( + JWKSource jwkSource) { + this.jwkSource = jwkSource; + } + + void setJwsAlgorithm(JWSAlgorithm jwsAlgorithm) { + this.jwsAlgorithm = jwsAlgorithm; + } + + void setExpiresIn(long expiresIn) { + this.expiresIn = expiresIn; + this.serializedJwtSplitCookieRepository.setExpiresIn(expiresIn); + } + + void setIssuer(String issuer) { + this.issuer = issuer; + } + + public void setTrustResolver(AuthenticationTrustResolver trustResolver) { + this.trustResolver = trustResolver; + } + + private JwtDecoder getJwtDecoder() { + if (jwtDecoder != null) { + return jwtDecoder; + } + + DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + jwtProcessor.setJWTClaimsSetVerifier((claimsSet, context) -> { + // No-op, Spring Security’s NimbusJwtDecoder uses its own validator + }); + + JWSKeySelector jwsKeySelector = new JWSVerificationKeySelector<>( + jwsAlgorithm, jwkSource); + jwtProcessor.setJWSKeySelector(jwsKeySelector); + NimbusJwtDecoder nimbusJwtDecoder = new NimbusJwtDecoder(jwtProcessor); + nimbusJwtDecoder.setJwtValidator( + issuer != null ? JwtValidators.createDefaultWithIssuer(issuer) + : JwtValidators.createDefault()); + this.jwtDecoder = nimbusJwtDecoder; + return jwtDecoder; + } + + private String encodeJwt(Authentication authentication) + throws JOSEException { + if (authentication == null || + trustResolver.isAnonymous(authentication)) { + return null; + } + + final Date now = new Date(); + + final List roles = authentication.getAuthorities().stream() + .map(Objects::toString) + .filter(a -> a.startsWith(ROLE_AUTHORITY_PREFIX)) + .map(a -> a.substring(ROLE_AUTHORITY_PREFIX.length())) + .collect(Collectors.toList()); + + SignedJWT signedJWT; + JWSHeader jwsHeader = new JWSHeader(jwsAlgorithm); + JWKSelector jwkSelector = new JWKSelector( + JWKMatcher.forJWSHeader(jwsHeader)); + + List jwks = jwkSource.get(jwkSelector, null); + JWK jwk = jwks.get(0); + + JWSSigner signer = new DefaultJWSSignerFactory().createJWSSigner(jwk, + jwsAlgorithm); + JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().subject( + authentication.getName()).issuer(issuer).issueTime(now) + .expirationTime(new Date(now.getTime() + expiresIn * 1000)) + .claim(ROLES_CLAIM, roles).build(); + signedJWT = new SignedJWT(jwsHeader, claimsSet); + signedJWT.sign(signer); + + return signedJWT.serialize(); + } + + private Jwt decodeJwt(HttpServletRequest request) { + String serializedJwt = serializedJwtSplitCookieRepository.loadSerializedJwt( + request); + if (serializedJwt == null) { + return null; + } + + return getJwtDecoder().decode(serializedJwt); + } + + @Override + public SecurityContext loadContext( + HttpRequestResponseHolder requestResponseHolder) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + + HttpServletRequest request = requestResponseHolder.getRequest(); + Jwt jwt = decodeJwt(request); + if (jwt != null) { + Authentication authentication = jwtAuthenticationConverter.convert( + jwt); + context.setAuthentication(authentication); + } + + requestResponseHolder.setResponse(new UpdateJwtResponseWrapper(request, + requestResponseHolder.getResponse())); + return context; + } + + @Override + public void saveContext(SecurityContext context, HttpServletRequest request, + HttpServletResponse response) { + String serializedJwt; + try { + serializedJwt = encodeJwt(context.getAuthentication()); + } catch (JOSEException e) { + logger.warn("Cannot serialize SecurityContext as JWT", e); + serializedJwt = null; + } + serializedJwtSplitCookieRepository.saveSerializedJwt(serializedJwt, + request, response); + } + + @Override + public boolean containsContext(HttpServletRequest request) { + return serializedJwtSplitCookieRepository.containsSerializedJwt( + request); + } + + private final class UpdateJwtResponseWrapper + extends SaveContextOnUpdateOrErrorResponseWrapper { + private final HttpServletRequest request; + + private UpdateJwtResponseWrapper(HttpServletRequest request, + HttpServletResponse response) { + super(response, true); + this.request = request; + } + + @Override + protected void saveContext(SecurityContext context) { + JwtSecurityContextRepository.this.saveContext(context, this.request, + this); + } + } +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/SerializedJwtSplitCookieRepository.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/SerializedJwtSplitCookieRepository.java new file mode 100644 index 000000000..a5f9837a6 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/SerializedJwtSplitCookieRepository.java @@ -0,0 +1,148 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.spring.security; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.util.WebUtils; + +/** + * Persists the signed and serialized JWT using a pair of cookies: + * "jwt.headerAndPayload" (JS-readable), and "jwt.signature" (HTTP-only). + */ +class SerializedJwtSplitCookieRepository { + private static final String JWT_HEADER_AND_PAYLOAD_COOKIE_NAME = "jwt.headerAndPayload"; + private static final String JWT_SIGNATURE_COOKIE_NAME = "jwt.signature"; + + private long expiresIn = 1800L; + + SerializedJwtSplitCookieRepository() { + } + + /** + * Sets max-age limit for cookies. + * + * @param expiresIn max age (seconds), the default is 30 min + */ + void setExpiresIn(long expiresIn) { + this.expiresIn = expiresIn; + } + + /** + * Reads the serialized JWT from the request cookies. + * + * @param request the request to read the token from + * @return serialized token + */ + String loadSerializedJwt(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + return null; + } + + Cookie jwtHeaderAndPayload = WebUtils.getCookie(request, + JWT_HEADER_AND_PAYLOAD_COOKIE_NAME); + if (jwtHeaderAndPayload == null) { + return null; + } + + Cookie jwtSignature = WebUtils.getCookie(request, + JWT_SIGNATURE_COOKIE_NAME); + if (jwtSignature == null) { + return null; + } + + return jwtHeaderAndPayload.getValue() + "." + jwtSignature.getValue(); + } + + /** + * Saves the serialized JWT string using response cookies. If the serialized + * JWT is null, the cookies are removed. + * + * @param serializedJwt the serialized JWT + * @param request the request + * @param response the response to send the cookies + */ + void saveSerializedJwt(String serializedJwt, HttpServletRequest request, + HttpServletResponse response) { + if (serializedJwt == null) { + this.removeJwtSplitCookies(request, response); + } else { + this.setJwtSplitCookies(serializedJwt, request, response); + } + } + + /** + * Checks the presence of JWT cookies in request. + * + * @param request request for checking + * @return true when both the JWT cookies are present + */ + boolean containsSerializedJwt(HttpServletRequest request) { + Cookie jwtHeaderAndPayload = WebUtils.getCookie(request, + JWT_HEADER_AND_PAYLOAD_COOKIE_NAME); + Cookie jwtSignature = WebUtils.getCookie(request, + JWT_SIGNATURE_COOKIE_NAME); + return (jwtHeaderAndPayload != null) && (jwtSignature != null); + } + + private void setJwtSplitCookies(String serializedJwt, + HttpServletRequest request, HttpServletResponse response) { + final String[] parts = serializedJwt.split("\\."); + final String jwtHeaderAndPayload = parts[0] + "." + parts[1]; + final String jwtSignature = parts[2]; + + Cookie headerAndPayload = new Cookie(JWT_HEADER_AND_PAYLOAD_COOKIE_NAME, + jwtHeaderAndPayload); + headerAndPayload.setHttpOnly(false); + headerAndPayload.setSecure(request.isSecure()); + headerAndPayload.setPath(getRequestContextPath(request)); + headerAndPayload.setMaxAge((int) expiresIn - 1); + response.addCookie(headerAndPayload); + + Cookie signature = new Cookie(JWT_SIGNATURE_COOKIE_NAME, jwtSignature); + signature.setHttpOnly(true); + signature.setSecure(request.isSecure()); + signature.setPath(getRequestContextPath(request)); + signature.setMaxAge((int) expiresIn - 1); + response.addCookie(signature); + } + + private void removeJwtSplitCookies(HttpServletRequest request, + HttpServletResponse response) { + Cookie jwtHeaderAndPayloadRemove = new Cookie( + JWT_HEADER_AND_PAYLOAD_COOKIE_NAME, null); + jwtHeaderAndPayloadRemove.setPath(getRequestContextPath(request)); + jwtHeaderAndPayloadRemove.setMaxAge(0); + jwtHeaderAndPayloadRemove.setSecure(request.isSecure()); + jwtHeaderAndPayloadRemove.setHttpOnly(false); + response.addCookie(jwtHeaderAndPayloadRemove); + + Cookie jwtSignatureRemove = new Cookie(JWT_SIGNATURE_COOKIE_NAME, null); + jwtSignatureRemove.setPath(getRequestContextPath(request)); + jwtSignatureRemove.setMaxAge(0); + jwtSignatureRemove.setSecure(request.isSecure()); + jwtSignatureRemove.setHttpOnly(true); + response.addCookie(jwtSignatureRemove); + } + + private String getRequestContextPath(HttpServletRequest request) { + final String contextPath = request.getContextPath(); + return "".equals(contextPath) ? "/" : contextPath; + } +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinDefaultRequestCache.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinDefaultRequestCache.java index 0fe664ee8..6f2b240c7 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinDefaultRequestCache.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinDefaultRequestCache.java @@ -1,3 +1,18 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ package com.vaadin.flow.spring.security; import javax.servlet.http.HttpServletRequest; @@ -6,22 +21,26 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.security.web.savedrequest.SavedRequest; import org.springframework.stereotype.Component; /** * A default request cache implementation which aims to ignore requests that are * not for routes. - * + * * Using this class helps with redirecting the user to the correct route after * login instead of redirecting to some internal URL like a service worker or * some data the service worker has fetched. */ @Component -public class VaadinDefaultRequestCache extends HttpSessionRequestCache { +public class VaadinDefaultRequestCache implements RequestCache { @Autowired private RequestUtil requestUtil; + private RequestCache delegateRequestCache = new HttpSessionRequestCache(); + @Override public void saveRequest(HttpServletRequest request, HttpServletResponse response) { @@ -38,12 +57,30 @@ public void saveRequest(HttpServletRequest request, LoggerFactory.getLogger(getClass()) .debug("Saving request to " + request.getRequestURI()); - super.saveRequest(request, response); + delegateRequestCache.saveRequest(request, response); + } + + @Override + public SavedRequest getRequest(HttpServletRequest request, + HttpServletResponse response) { + return delegateRequestCache.getRequest(request, response); + } + + @Override + public HttpServletRequest getMatchingRequest(HttpServletRequest request, + HttpServletResponse response) { + return delegateRequestCache.getMatchingRequest(request, response); + } + + @Override + public void removeRequest(HttpServletRequest request, + HttpServletResponse response) { + delegateRequestCache.removeRequest(request, response); } /** * Checks if the request is initiated by a service worker. - * + * * NOTE This method can never be used for security purposes as the "Referer" * header is easy to fake. */ @@ -52,4 +89,7 @@ private boolean isServiceWorkerInitiated(HttpServletRequest request) { return referer != null && referer.endsWith("sw.js"); } + void setDelegateRequestCache(RequestCache delegateRequestCache) { + this.delegateRequestCache = delegateRequestCache; + } } \ No newline at end of file diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinStatelessSecurityConfigurer.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinStatelessSecurityConfigurer.java new file mode 100644 index 000000000..d3066973b --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinStatelessSecurityConfigurer.java @@ -0,0 +1,167 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.spring.security; + +import javax.crypto.SecretKey; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.OctetSequenceKey; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; +import org.springframework.security.config.annotation.web.configurers.SecurityContextConfigurer; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.security.web.csrf.LazyCsrfTokenRepository; +import org.springframework.security.web.savedrequest.CookieRequestCache; +import org.springframework.security.web.savedrequest.RequestCache; + +class VaadinStatelessSecurityConfigurer> + extends + AbstractHttpConfigurer, H> { + private long expiresIn = 1800L; + + private String issuer; + + private SecretKeyConfigurer secretKeyConfigurer; + + @Override + @SuppressWarnings("unchecked") + public void init(H http) { + JwtSecurityContextRepository jwtSecurityContextRepository = new JwtSecurityContextRepository(); + SecurityContextConfigurer securityContext = http.getConfigurer( + SecurityContextConfigurer.class); + if (securityContext != null) { + securityContext.securityContextRepository( + jwtSecurityContextRepository); + } else { + http.setSharedObject(SecurityContextRepository.class, + jwtSecurityContextRepository); + } + } + + @Override + @SuppressWarnings("unchecked") + public void configure(H http) { + SecurityContextRepository securityContextRepository = http.getSharedObject( + SecurityContextRepository.class); + + if (securityContextRepository instanceof JwtSecurityContextRepository) { + JwtSecurityContextRepository jwtSecurityContextRepository = (JwtSecurityContextRepository) securityContextRepository; + + jwtSecurityContextRepository.setJwsAlgorithm( + secretKeyConfigurer.getAlgorithm()); + jwtSecurityContextRepository.setJwkSource( + secretKeyConfigurer.getJWKSource()); + jwtSecurityContextRepository.setIssuer(issuer); + jwtSecurityContextRepository.setExpiresIn(expiresIn); + + AuthenticationTrustResolver trustResolver = http.getSharedObject( + AuthenticationTrustResolver.class); + if (trustResolver == null) { + trustResolver = new AuthenticationTrustResolverImpl(); + } + jwtSecurityContextRepository.setTrustResolver(trustResolver); + } + + RequestCache requestCache = http.getSharedObject(RequestCache.class); + if (requestCache instanceof VaadinDefaultRequestCache) { + ((VaadinDefaultRequestCache) requestCache).setDelegateRequestCache( + new CookieRequestCache()); + } + + CsrfConfigurer csrf = http.getConfigurer(CsrfConfigurer.class); + if (csrf != null) { + // Use cookie for storing CSRF token, as it does not require a + // session (double-submit cookie pattern) + CsrfTokenRepository csrfTokenRepository = new LazyCsrfTokenRepository( + CookieCsrfTokenRepository.withHttpOnlyFalse()); + csrf.csrfTokenRepository(csrfTokenRepository); + } + } + + public VaadinStatelessSecurityConfigurer expiresIn(long expiresIn) { + this.expiresIn = expiresIn; + return this; + } + + public VaadinStatelessSecurityConfigurer issuer(String issuer) { + this.issuer = issuer; + return this; + } + + public SecretKeyConfigurer withSecretKey() { + if (this.secretKeyConfigurer == null) { + this.secretKeyConfigurer = new SecretKeyConfigurer(); + } + return this.secretKeyConfigurer; + } + + public SecretKeyConfigurer withSecretKey( + Customizer customizer) { + if (this.secretKeyConfigurer == null) { + this.secretKeyConfigurer = new SecretKeyConfigurer(); + } + customizer.customize(secretKeyConfigurer); + return this.secretKeyConfigurer; + } + + public class SecretKeyConfigurer { + private SecretKey secretKey; + + private JwsAlgorithm jwsAlgorithm; + + private SecretKeyConfigurer() { + } + + public SecretKeyConfigurer secretKey(SecretKey secretKey) { + this.secretKey = secretKey; + if (this.jwsAlgorithm == null) { + this.jwsAlgorithm = MacAlgorithm.from(secretKey.getAlgorithm()); + } + return this; + } + + public SecretKeyConfigurer algorithm(MacAlgorithm algorithm) { + this.jwsAlgorithm = algorithm; + return this; + } + + public VaadinStatelessSecurityConfigurer and() { + return VaadinStatelessSecurityConfigurer.this; + } + + JWKSource getJWKSource() { + OctetSequenceKey key = new OctetSequenceKey.Builder( + secretKey).algorithm(getAlgorithm()).build(); + JWKSet jwkSet = new JWKSet(key); + return (jwkSelector, context) -> jwkSelector.select(jwkSet); + } + + JWSAlgorithm getAlgorithm() { + return JWSAlgorithm.parse(jwsAlgorithm.getName()); + } + } +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurityConfigurerAdapter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurityConfigurerAdapter.java index 2f01684b0..4ec6f2d72 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurityConfigurerAdapter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurityConfigurerAdapter.java @@ -15,17 +15,11 @@ */ package com.vaadin.flow.spring.security; +import javax.crypto.SecretKey; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; -import com.vaadin.flow.component.Component; -import com.vaadin.flow.internal.AnnotationReader; -import com.vaadin.flow.router.Route; -import com.vaadin.flow.router.internal.RouteUtil; -import com.vaadin.flow.server.HandlerHelper; -import com.vaadin.flow.server.auth.ViewAccessChecker; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; @@ -33,10 +27,19 @@ import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.internal.AnnotationReader; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.internal.RouteUtil; +import com.vaadin.flow.server.HandlerHelper; +import com.vaadin.flow.server.auth.ViewAccessChecker; + /** * Provides basic Vaadin security configuration for the project. *

@@ -55,10 +58,9 @@ @EnableWebSecurity @Configuration public class MySecurityConfigurerAdapter extends VaadinWebSecurityConfigurerAdapter { - -} + +} * - * */ public abstract class VaadinWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @@ -157,7 +159,7 @@ public static RequestMatcher getDefaultWebSecurityIgnoreMatcher() { *

* This is used when your application uses a Fusion based login view * available at the given path. - * + * * @param http * the http security from {@link #configure(HttpSecurity)} * @param fusionLoginViewPath @@ -176,7 +178,7 @@ protected void setLoginView(HttpSecurity http, String fusionLoginViewPath) *

* This is used when your application uses a Fusion based login view * available at the given path. - * + * * @param http * the http security from {@link #configure(HttpSecurity)} * @param fusionLoginViewPath @@ -191,14 +193,14 @@ protected void setLoginView(HttpSecurity http, String fusionLoginViewPath, FormLoginConfigurer formLogin = http.formLogin(); formLogin.loginPage(fusionLoginViewPath).permitAll(); formLogin.successHandler( - new VaadinSavedRequestAwareAuthenticationSuccessHandler()); + getVaadinSavedRequestAwareAuthenticationSuccessHandler(http)); http.logout().logoutSuccessUrl(logoutUrl); viewAccessChecker.setLoginView(fusionLoginViewPath); } /** * Sets up login for the application using the given Flow login view. - * + * * @param http * the http security from {@link #configure(HttpSecurity)} * @param flowLoginView @@ -213,14 +215,14 @@ protected void setLoginView(HttpSecurity http, /** * Sets up login for the application using the given Flow login view. - * + * * @param http * the http security from {@link #configure(HttpSecurity)} * @param flowLoginView * the login view to use * @param logoutUrl * the URL to redirect the user to after logging out - * + * * @throws Exception * if something goes wrong */ @@ -245,10 +247,63 @@ protected void setLoginView(HttpSecurity http, FormLoginConfigurer formLogin = http.formLogin(); formLogin.loginPage(loginPath).permitAll(); formLogin.successHandler( - new VaadinSavedRequestAwareAuthenticationSuccessHandler()); + getVaadinSavedRequestAwareAuthenticationSuccessHandler(http)); http.csrf().ignoringAntMatchers(loginPath); http.logout().logoutSuccessUrl(logoutUrl); viewAccessChecker.setLoginView(flowLoginView); } + /** + * Sets up stateless JWT authentication using cookies. + * + * @param http + * the http security from {@link #configure(HttpSecurity)} + * @param secretKey + * the secret key for encoding and decoding JWTs, must use a + * {@link MacAlgorithm} algorithm name + * @param issuer + * the issuer JWT claim + * @throws Exception + * if something goes wrong + */ + protected void setStatelessAuthentication(HttpSecurity http, + SecretKey secretKey, String issuer) throws Exception { + setStatelessAuthentication(http, secretKey, issuer, 1800L); + } + + /** + * Sets up stateless JWT authentication using cookies. + * + * @param http + * the http security from {@link #configure(HttpSecurity)} + * @param secretKey + * the secret key for encoding and decoding JWTs, must use a + * {@link MacAlgorithm} algorithm name + * @param issuer + * the issuer JWT claim + * @param expiresIn + * lifetime of the JWT and cookies, in seconds + * @throws Exception + * if something goes wrong + */ + protected void setStatelessAuthentication(HttpSecurity http, + SecretKey secretKey, String issuer, long expiresIn) + throws Exception { + VaadinStatelessSecurityConfigurer vaadinStatelessSecurityConfigurer = new VaadinStatelessSecurityConfigurer<>(); + http.apply(vaadinStatelessSecurityConfigurer); + + vaadinStatelessSecurityConfigurer.withSecretKey().secretKey(secretKey) + .and().issuer(issuer).expiresIn(expiresIn); + } + + private VaadinSavedRequestAwareAuthenticationSuccessHandler getVaadinSavedRequestAwareAuthenticationSuccessHandler( + HttpSecurity http) { + VaadinSavedRequestAwareAuthenticationSuccessHandler vaadinSavedRequestAwareAuthenticationSuccessHandler = new VaadinSavedRequestAwareAuthenticationSuccessHandler(); + RequestCache requestCache = http.getSharedObject(RequestCache.class); + if (requestCache != null) { + vaadinSavedRequestAwareAuthenticationSuccessHandler.setRequestCache( + requestCache); + } + return vaadinSavedRequestAwareAuthenticationSuccessHandler; + } } diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java index bae363d28..499ee013d 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java @@ -88,11 +88,17 @@ protected Stream getExcludedPatterns() { "com\\.vaadin\\.flow\\.spring\\.scopes\\.VaadinSessionScope", "com\\.vaadin\\.flow\\.spring\\.scopes\\.AbstractScope", "com\\.vaadin\\.flow\\.spring\\.scopes\\.VaadinUIScope", + "com\\.vaadin\\.flow\\.spring\\.security\\.JwtSecurityContextRepository", + "com\\.vaadin\\.flow\\.spring\\.security\\.JwtSecurityContextRepository\\$UpdateJwtResponseWrapper", + "com\\.vaadin\\.flow\\.spring\\.security\\.SerializedJwtSplitCookieRepository", "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinAwareSecurityContextHolderStrategy", "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinWebSecurityConfigurerAdapter", "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinDefaultRequestCache", "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinSavedRequestAwareAuthenticationSuccessHandler", "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinSavedRequestAwareAuthenticationSuccessHandler\\$RedirectStrategy", + "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinStatelessSecurityConfigurer", + "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinStatelessSecurityConfigurer\\$SecretKeyConfigurer", + "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinWebSecurityConfigurerAdapter\\$BearerTokenAuthentiationFilterPostProcessor", "com\\.vaadin\\.flow\\.spring\\.VaadinServletContextInitializer\\$ClassPathScanner", "com\\.vaadin\\.flow\\.spring\\.VaadinServletContextInitializer\\$CustomResourceLoader"), super.getExcludedPatterns()); diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/SerializedJwtSplitCookieRepositoryTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/SerializedJwtSplitCookieRepositoryTest.java new file mode 100644 index 000000000..30cc2aeab --- /dev/null +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/SerializedJwtSplitCookieRepositoryTest.java @@ -0,0 +1,222 @@ +package com.vaadin.flow.spring.security; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +public class SerializedJwtSplitCookieRepositoryTest { + private static final int DEFAULT_MAX_AGE = 1800; + private static final int CUSTOM_MAX_AGE = 3600; + private static final String JWT_HEADER_AND_PAYLOAD = "foo.bar"; + private static final String JWT_SIGNATURE = "baz"; + private static final String JWT = + JWT_HEADER_AND_PAYLOAD + "." + JWT_SIGNATURE; + private static final String JWT_SIGNATURE_NAME = "jwt.signature"; + private static final String JWT_HEADER_AND_PAYLOAD_NAME = "jwt.headerAndPayload"; + private static final Cookie JWT_HEADER_AND_PAYLOAD_COOKIE; + private static final Cookie JWT_SIGNATURE_COOKIE; + private static final String CONTEXT_PATH = "/context-path/"; + + static { + JWT_HEADER_AND_PAYLOAD_COOKIE = new Cookie(JWT_HEADER_AND_PAYLOAD_NAME, + JWT_HEADER_AND_PAYLOAD); + JWT_SIGNATURE_COOKIE = new Cookie(JWT_SIGNATURE_NAME, JWT_SIGNATURE); + } + + private SerializedJwtSplitCookieRepository serializedJwtSplitCookieRepository; + private HttpServletRequest request; + private HttpServletResponse response; + + @Before + public void setup() { + serializedJwtSplitCookieRepository = new SerializedJwtSplitCookieRepository(); + request = Mockito.mock(HttpServletRequest.class); + Mockito.doReturn(CONTEXT_PATH).when(request).getContextPath(); + Mockito.doReturn(true).when(request).isSecure(); + response = Mockito.mock(HttpServletResponse.class); + } + + @Test + public void containsSerializedJwt_true_when_bothCookiesPreset() { + Mockito.doReturn(new Cookie[] { JWT_HEADER_AND_PAYLOAD_COOKIE, + JWT_SIGNATURE_COOKIE }).when(request).getCookies(); + Assert.assertTrue( + serializedJwtSplitCookieRepository.containsSerializedJwt( + request)); + } + + @Test + public void containsSerializedJwt_false_when_signatureCookieMissing() { + Mockito.doReturn(new Cookie[] { JWT_HEADER_AND_PAYLOAD_COOKIE }) + .when(request).getCookies(); + Assert.assertFalse( + serializedJwtSplitCookieRepository.containsSerializedJwt( + request)); + } + + @Test + public void containsSerializedJwt_false_when_headerAndPayloadCookieMissing() { + Mockito.doReturn(new Cookie[] { JWT_SIGNATURE_COOKIE }).when(request) + .getCookies(); + Assert.assertFalse( + serializedJwtSplitCookieRepository.containsSerializedJwt( + request)); + } + + @Test + public void containsSerializedJwt_false_when_bothCookiesMissing() { + Mockito.doReturn(new Cookie[] {}).when(request).getCookies(); + Assert.assertFalse( + serializedJwtSplitCookieRepository.containsSerializedJwt( + request)); + } + + @Test + public void containsSerializedJwt_false_when_cookiesNull() { + Mockito.doReturn(null).when(request).getCookies(); + Assert.assertFalse( + serializedJwtSplitCookieRepository.containsSerializedJwt( + request)); + } + + @Test + public void loadSerializedJwt_returnsString_when_cookiesPresent() { + Mockito.doReturn(new Cookie[] { JWT_HEADER_AND_PAYLOAD_COOKIE, + JWT_SIGNATURE_COOKIE }).when(request).getCookies(); + + String serializedJwt = serializedJwtSplitCookieRepository.loadSerializedJwt( + request); + Assert.assertEquals(JWT, serializedJwt); + } + + @Test + public void loadSerializedJwt_returnsNull_when_headerAndPayloadCookieMissing() { + Mockito.doReturn(new Cookie[] { JWT_SIGNATURE_COOKIE }).when(request) + .getCookies(); + + String serializedJwt; + serializedJwt = serializedJwtSplitCookieRepository.loadSerializedJwt( + request); + Assert.assertNull(JWT, serializedJwt); + } + + @Test + public void loadSerializedJwt_returnsNull_when_signatureCookieMissing() { + Mockito.doReturn(new Cookie[] { JWT_HEADER_AND_PAYLOAD_COOKIE }) + .when(request).getCookies(); + + String serializedJwt; + serializedJwt = serializedJwtSplitCookieRepository.loadSerializedJwt( + request); + Assert.assertNull(JWT, serializedJwt); + } + + @Test + public void loadSerializedJwt_returnsNull_when_bothCookiesMissing() { + Mockito.doReturn(new Cookie[] {}).when(request).getCookies(); + + String serializedJwt; + serializedJwt = serializedJwtSplitCookieRepository.loadSerializedJwt( + request); + Assert.assertNull(JWT, serializedJwt); + } + + + @Test + public void loadSerializedJwt_returnsNull_when_cookiesNull() { + Mockito.doReturn(null).when(request).getCookies(); + + String serializedJwt; + serializedJwt = serializedJwtSplitCookieRepository.loadSerializedJwt( + request); + Assert.assertNull(JWT, serializedJwt); + } + + @Test + public void saveSerializedJwt_sets_cookiePair() { + serializedJwtSplitCookieRepository.saveSerializedJwt(JWT, request, + response); + checkResponseCookiePair(JWT_HEADER_AND_PAYLOAD, JWT_SIGNATURE, true, + DEFAULT_MAX_AGE - 1, CONTEXT_PATH); + } + + @Test + public void saveSerializedJwt_resets_cookiePair() { + serializedJwtSplitCookieRepository.saveSerializedJwt(null, request, + response); + checkResponseCookiePair(null, null, true, 0, CONTEXT_PATH); + } + + @Test + public void saveSerializedJwt_setsWithMaxAge_after_setExpireIn() { + serializedJwtSplitCookieRepository.setExpiresIn(CUSTOM_MAX_AGE); + serializedJwtSplitCookieRepository.saveSerializedJwt(JWT, request, + response); + checkResponseCookiePair(JWT_HEADER_AND_PAYLOAD, JWT_SIGNATURE, true, + CUSTOM_MAX_AGE - 1, CONTEXT_PATH); + } + + @Test + public void saveSerializedJwt_resetsWithoutMaxAge_after_setExpireIn() { + serializedJwtSplitCookieRepository.setExpiresIn(CUSTOM_MAX_AGE); + serializedJwtSplitCookieRepository.saveSerializedJwt(null, request, + response); + checkResponseCookiePair(null, null, true, 0, CONTEXT_PATH); + } + + @Test + public void saveSerializedJwt_sets_withNonSecure_request() { + Mockito.doReturn(false).when(request).isSecure(); + serializedJwtSplitCookieRepository.saveSerializedJwt(JWT, request, + response); + checkResponseCookiePair(JWT_HEADER_AND_PAYLOAD, JWT_SIGNATURE, false, + DEFAULT_MAX_AGE - 1, CONTEXT_PATH); + } + + @Test + public void saveSerializedJwt_sets_withEmptyContextPath() { + Mockito.doReturn("").when(request).getContextPath(); + serializedJwtSplitCookieRepository.saveSerializedJwt(JWT, request, + response); + checkResponseCookiePair(JWT_HEADER_AND_PAYLOAD, JWT_SIGNATURE, true, + DEFAULT_MAX_AGE - 1, "/"); + } + + private void checkResponseCookiePair(String expectedHeaderAndPayload, + String expectedSignature, boolean expectedIsSecure, int maxAge, + String expectedPath) { + ArgumentCaptor cookieArgumentCaptor = ArgumentCaptor.forClass( + Cookie.class); + Mockito.verify(response, Mockito.times(2)) + .addCookie(cookieArgumentCaptor.capture()); + List cookieList = cookieArgumentCaptor.getAllValues(); + + Cookie headerAndPayloadCookie = cookieList.get(0); + Assert.assertNotNull(headerAndPayloadCookie); + Assert.assertEquals(JWT_HEADER_AND_PAYLOAD_NAME, + headerAndPayloadCookie.getName()); + Assert.assertEquals(expectedHeaderAndPayload, + headerAndPayloadCookie.getValue()); + Assert.assertFalse(headerAndPayloadCookie.isHttpOnly()); + Assert.assertEquals(expectedIsSecure, + headerAndPayloadCookie.getSecure()); + Assert.assertEquals(expectedPath, headerAndPayloadCookie.getPath()); + Assert.assertEquals(maxAge, headerAndPayloadCookie.getMaxAge()); + + Cookie signatureCookie = cookieList.get(1); + Assert.assertNotNull(signatureCookie); + Assert.assertEquals(JWT_SIGNATURE_NAME, signatureCookie.getName()); + Assert.assertEquals(expectedSignature, signatureCookie.getValue()); + Assert.assertTrue(signatureCookie.isHttpOnly()); + Assert.assertEquals(expectedIsSecure, signatureCookie.getSecure()); + Assert.assertEquals(expectedPath, signatureCookie.getPath()); + Assert.assertEquals(maxAge, signatureCookie.getMaxAge()); + } +} \ No newline at end of file diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/VaadinDefaultRequestCacheTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/VaadinDefaultRequestCacheTest.java index 3b6a0f49d..4c0b25214 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/VaadinDefaultRequestCacheTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/VaadinDefaultRequestCacheTest.java @@ -1,18 +1,9 @@ package com.vaadin.flow.spring.security; -import java.lang.reflect.Method; -import java.util.Collections; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; - -import com.vaadin.flow.server.HandlerHelper.RequestType; -import com.vaadin.fusion.Endpoint; -import com.vaadin.fusion.EndpointRegistry; -import com.vaadin.fusion.FusionControllerConfiguration; -import com.vaadin.fusion.FusionEndpointProperties; -import com.vaadin.flow.spring.SpringBootAutoConfiguration; -import com.vaadin.flow.spring.SpringSecurityAutoConfiguration; +import java.lang.reflect.Method; +import java.util.Collections; import org.junit.Assert; import org.junit.Test; @@ -20,9 +11,20 @@ import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.security.web.savedrequest.SavedRequest; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; +import com.vaadin.flow.server.HandlerHelper.RequestType; +import com.vaadin.flow.spring.SpringBootAutoConfiguration; +import com.vaadin.flow.spring.SpringSecurityAutoConfiguration; +import com.vaadin.fusion.Endpoint; +import com.vaadin.fusion.EndpointRegistry; +import com.vaadin.fusion.FusionControllerConfiguration; +import com.vaadin.fusion.FusionEndpointProperties; + @RunWith(SpringRunner.class) @SpringBootTest(classes = { FusionEndpointProperties.class }) @ContextConfiguration(classes = { FusionControllerConfiguration.class, @@ -89,6 +91,73 @@ public void endpointRequestNotSaved() throws Exception { Assert.assertNull(cache.getRequest(request, response)); } + @Test + public void getRequest_uses_delegateRequestCache() throws Exception { + HttpServletRequest request = RequestUtilTest.createRequest( + "/hello-world", null); + HttpServletResponse response = createResponse(); + SavedRequest expectedSavedRequest = Mockito.mock(SavedRequest.class); + RequestCache delegateRequestCache = Mockito.mock(RequestCache.class); + Mockito.doReturn(expectedSavedRequest).when(delegateRequestCache) + .getRequest(request, response); + cache.setDelegateRequestCache(delegateRequestCache); + + SavedRequest actualSavedRequest = cache.getRequest(request, response); + Mockito.verify(delegateRequestCache).getRequest(request, response); + Assert.assertEquals(expectedSavedRequest, actualSavedRequest); + + cache.setDelegateRequestCache(new HttpSessionRequestCache()); + } + + @Test + public void getMatchingRequest_uses_delegateRequestCache() + throws Exception { + HttpServletRequest request = RequestUtilTest.createRequest( + "/hello-world", null); + HttpServletResponse response = createResponse(); + HttpServletRequest expectedMachingRequest = RequestUtilTest.createRequest( + "", null); + RequestCache delegateRequestCache = Mockito.mock(RequestCache.class); + Mockito.doReturn(expectedMachingRequest).when(delegateRequestCache) + .getMatchingRequest(request, response); + cache.setDelegateRequestCache(delegateRequestCache); + + HttpServletRequest actualMatchingRequest = cache.getMatchingRequest( + request, response); + Mockito.verify(delegateRequestCache).getMatchingRequest(request, response); + Assert.assertEquals(expectedMachingRequest, actualMatchingRequest); + + cache.setDelegateRequestCache(new HttpSessionRequestCache()); + } + + @Test + public void saveRequest_uses_delegateRequestCache() throws Exception { + HttpServletRequest request = RequestUtilTest.createRequest( + "/hello-world", null); + HttpServletResponse response = createResponse(); + RequestCache delegateRequestCache = Mockito.mock(RequestCache.class); + cache.setDelegateRequestCache(delegateRequestCache); + + cache.saveRequest(request, response); + Mockito.verify(delegateRequestCache).saveRequest(request, response); + + cache.setDelegateRequestCache(new HttpSessionRequestCache()); + } + + @Test + public void removeRequest_uses_delegateRequestCache() throws Exception { + HttpServletRequest request = RequestUtilTest.createRequest( + "/hello-world", null); + HttpServletResponse response = createResponse(); + RequestCache delegateRequestCache = Mockito.mock(RequestCache.class); + cache.setDelegateRequestCache(delegateRequestCache); + + cache.removeRequest(request, response); + Mockito.verify(delegateRequestCache).removeRequest(request, response); + + cache.setDelegateRequestCache(new HttpSessionRequestCache()); + } + private HttpServletResponse createResponse() { return Mockito.mock(HttpServletResponse.class); }