From ba3bc2783f0180df939d51591e95658a6d2a4690 Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Tue, 14 Sep 2021 14:35:31 +0300 Subject: [PATCH 01/20] feat(fusion): add stateless option to VaadinWebSecurityConfigurerAdapter Fixes #807 --- ...SplitCookieBearerTokenConverterFilter.java | 36 ---- .../auth/JwtSplitCookieUtils.java | 138 ------------- .../config/SecurityConfiguration.java | 37 ++-- ...uestAwareAuthenticationSuccessHandler.java | 23 --- .../VaadinStatelessWebSecurityConfig.java | 79 -------- vaadin-spring/pom.xml | 8 + .../JwtSplitCookieManagementFilter.java | 13 +- .../security/JwtSplitCookieService.java | 125 ++++++++++++ ...uestAwareAuthenticationSuccessHandler.java | 14 ++ .../VaadinWebSecurityConfigurerAdapter.java | 191 ++++++++++++++++-- .../spring/SpringClassesSerializableTest.java | 3 + 11 files changed, 346 insertions(+), 321 deletions(-) delete mode 100644 vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/auth/JwtSplitCookieBearerTokenConverterFilter.java delete mode 100644 vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/auth/JwtSplitCookieUtils.java delete mode 100644 vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/config/VaadinStatelessSavedRequestAwareAuthenticationSuccessHandler.java delete mode 100644 vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/config/VaadinStatelessWebSecurityConfig.java rename {vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/auth => vaadin-spring/src/main/java/com/vaadin/flow/spring/security}/JwtSplitCookieManagementFilter.java (80%) create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSplitCookieService.java 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/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 index 2b8b11bbc..5173b91e0 100644 --- 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 @@ -1,18 +1,10 @@ package com.vaadin.flow.spring.fusionsecurityjwt.config; +import javax.crypto.spec.SecretKeySpec; +import java.util.Base64; 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; @@ -22,17 +14,22 @@ 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.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 @Order(10) -public class SecurityConfiguration extends VaadinStatelessWebSecurityConfig { +public class SecurityConfiguration extends VaadinWebSecurityConfigurerAdapter { 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 @@ -47,21 +44,15 @@ protected void configure(HttpSecurity http) throws Exception { .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); - setJwtSplitCookieAuthentication(http, "statelessapp", 3600, - JWSAlgorithm.HS256); + setJwtSplitCookieAuthentication(http, + new SecretKeySpec(Base64.getUrlDecoder().decode( + "I72kIcB1UrUQVHVUAzgweE+BLc0bF8mLv9SmrgKsQAk="), + JwsAlgorithms.HS256), + "statelessapp"); 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); 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/pom.xml b/vaadin-spring/pom.xml index 1f6199c04..7c5cad4fe 100644 --- a/vaadin-spring/pom.xml +++ b/vaadin-spring/pom.xml @@ -155,6 +155,14 @@ ${vaadin.flow.version} test + + org.springframework.security + spring-security-oauth2-jose + + + org.springframework.security + spring-security-oauth2-resource-server + 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/src/main/java/com/vaadin/flow/spring/security/JwtSplitCookieManagementFilter.java similarity index 80% rename from vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/auth/JwtSplitCookieManagementFilter.java rename to vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSplitCookieManagementFilter.java index 2c0407ca6..12f365fc1 100644 --- a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/auth/JwtSplitCookieManagementFilter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSplitCookieManagementFilter.java @@ -1,4 +1,4 @@ -package com.vaadin.flow.spring.fusionsecurityjwt.auth; +package com.vaadin.flow.spring.security; import javax.servlet.Filter; import javax.servlet.FilterChain; @@ -15,6 +15,13 @@ public class JwtSplitCookieManagementFilter implements Filter { + private JwtSplitCookieService jwtSplitCookieService; + + public JwtSplitCookieManagementFilter( + JwtSplitCookieService jwtSplitCookieService) { + this.jwtSplitCookieService = jwtSplitCookieService; + } + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { @@ -24,11 +31,11 @@ public void doFilter(ServletRequest request, ServletResponse response, if (authentication == null || authentication instanceof AnonymousAuthenticationToken) { // Token authentication failed — remove the cookies - JwtSplitCookieUtils + jwtSplitCookieService .removeJwtSplitCookies((HttpServletRequest) request, (HttpServletResponse) response); } else { - JwtSplitCookieUtils + jwtSplitCookieService .setJwtSplitCookiesIfNecessary((HttpServletRequest) request, (HttpServletResponse) response, authentication); } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSplitCookieService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSplitCookieService.java new file mode 100644 index 000000000..85f50690e --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSplitCookieService.java @@ -0,0 +1,125 @@ +package com.vaadin.flow.spring.security; + +import javax.crypto.SecretKey; +import javax.servlet.ServletException; +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.OctetSequenceKey; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.web.util.WebUtils; + +public class JwtSplitCookieService { + public static final String JWT_HEADER_AND_PAYLOAD_COOKIE_NAME = "jwt.headerAndPayload"; + public static final String JWT_SIGNATURE_COOKIE_NAME = "jwt.signature"; + + private static final String ROLES_CLAIM = "roles"; + private static final String ROLE_AUTHORITY_PREFIX = "ROLE_"; + + private String issuer; + + private long expiresIn; + + private JWK jwk; + + private BearerTokenResolver bearerTokenResolver = (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 JwtSplitCookieService(SecretKey secretKey, String issuer, long expiresIn) { + this.jwk = new OctetSequenceKey.Builder(secretKey) + .algorithm(JWSAlgorithm.parse(secretKey.getAlgorithm())) + .build(); + this.issuer = issuer; + this.expiresIn = expiresIn; + } + + public BearerTokenResolver getBearerTokenResolver() { + return bearerTokenResolver; + } + + public void setJwtSplitCookiesIfNecessary(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws ServletException { + 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; + try { + JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(jwk.getAlgorithm().getName()); + JWSHeader jwsHeader = new JWSHeader(jwsAlgorithm); + + 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); + + Cookie headerAndPayload = new Cookie(JWT_HEADER_AND_PAYLOAD_COOKIE_NAME, + new String(signedJWT.getSigningInput(), StandardCharsets.UTF_8)); + headerAndPayload.setHttpOnly(false); + headerAndPayload.setSecure(request.isSecure()); + headerAndPayload.setPath(request.getContextPath() + "/"); + headerAndPayload.setMaxAge((int) expiresIn - 1); + response.addCookie(headerAndPayload); + + Cookie signature = new Cookie(JWT_SIGNATURE_COOKIE_NAME, signedJWT.getSignature().toString()); + signature.setHttpOnly(true); + signature.setSecure(request.isSecure()); + signature.setPath(request.getContextPath() + "/"); + signature.setMaxAge((int) expiresIn - 1); + response.addCookie(signature); + } catch (JOSEException e) { + throw new ServletException("Unable to issue a new JWT", e); + } + } + + public 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/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java index 0504cce67..3510d3998 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java @@ -84,6 +84,11 @@ public class VaadinSavedRequestAwareAuthenticationSuccessHandler */ private static final String SPRING_CSRF_TOKEN = "Spring-CSRF-token"; + /** + * Optional service to persist authentication in JWT cookies. + */ + private JwtSplitCookieService jwtSplitCookieService; + /** * Redirect strategy used by * {@link VaadinSavedRequestAwareAuthenticationSuccessHandler}. @@ -158,6 +163,11 @@ public void onAuthenticationSuccess(HttpServletRequest request, determineTargetUrl(request, response)); } + if (jwtSplitCookieService != null) { + jwtSplitCookieService.setJwtSplitCookiesIfNecessary(request, + response, authentication); + } + super.onAuthenticationSuccess(request, response, authentication); } @@ -188,4 +198,8 @@ public void setRequestCache(RequestCache requestCache) { super.setRequestCache(requestCache); this.requestCache = requestCache; } + + void setJwtSplitCookieService(JwtSplitCookieService jwtSplitCookieService) { + this.jwtSplitCookieService = jwtSplitCookieService; + } } 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..60156756a 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,28 +15,46 @@ */ 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.ObjectPostProcessor; 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.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +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.JwtAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter; +import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +import org.springframework.security.web.authentication.switchuser.SwitchUserFilter; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfAuthenticationStrategy; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.security.web.csrf.LazyCsrfTokenRepository; 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 +73,10 @@ @EnableWebSecurity @Configuration public class MySecurityConfigurerAdapter extends VaadinWebSecurityConfigurerAdapter { - -} + +} * - * + * */ public abstract class VaadinWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @@ -72,6 +90,8 @@ public abstract class VaadinWebSecurityConfigurerAdapter @Autowired private ViewAccessChecker viewAccessChecker; + private VaadinSavedRequestAwareAuthenticationSuccessHandler authenticationSuccessHandler; + /** * The paths listed as "ignoring" in this method are handled without any * Spring Security involvement. They have no access to any security context @@ -157,7 +177,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 +196,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 @@ -190,15 +210,14 @@ protected void setLoginView(HttpSecurity http, String fusionLoginViewPath, String logoutUrl) throws Exception { FormLoginConfigurer formLogin = http.formLogin(); formLogin.loginPage(fusionLoginViewPath).permitAll(); - formLogin.successHandler( - new VaadinSavedRequestAwareAuthenticationSuccessHandler()); + formLogin.successHandler(getAuthenticationSuccessHandler()); 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 +232,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 */ @@ -244,11 +263,145 @@ protected void setLoginView(HttpSecurity http, // Actually set it up FormLoginConfigurer formLogin = http.formLogin(); formLogin.loginPage(loginPath).permitAll(); - formLogin.successHandler( - new VaadinSavedRequestAwareAuthenticationSuccessHandler()); + formLogin.successHandler(getAuthenticationSuccessHandler()); 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 + */ + protected void setJwtSplitCookieAuthentication(HttpSecurity http, + SecretKey secretKey, String issuer) throws Exception { + setJwtSplitCookieAuthentication(http, secretKey, issuer, 3600L); + } + + /** + * 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 + */ + @SuppressWarnings("unchecked") + protected void setJwtSplitCookieAuthentication(HttpSecurity http, + SecretKey secretKey, String issuer, long expiresIn) + throws Exception { + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); + grantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); + + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter + .setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); + + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(secretKey) + .build(); + jwtDecoder + .setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer)); + + JwtSplitCookieService jwtSplitCookieService = new JwtSplitCookieService( + secretKey, issuer, expiresIn); + + getAuthenticationSuccessHandler().setJwtSplitCookieService(jwtSplitCookieService); + + // @formatter:off + http + .oauth2ResourceServer(oAuth2ResourceServer -> + customizeOAuth2ResourceServer(oAuth2ResourceServer, + jwtSplitCookieService.getBearerTokenResolver(), + jwtDecoder, jwtAuthenticationConverter)) + .addFilterAfter( + new JwtSplitCookieManagementFilter(jwtSplitCookieService), + SwitchUserFilter.class); + // @formatter:on + + http.logout().addLogoutHandler( + (request, response, authentication) -> jwtSplitCookieService + .removeJwtSplitCookies(request, response)); + + registerCsrfAuthenticationStrategy(http); + } + + private VaadinSavedRequestAwareAuthenticationSuccessHandler getAuthenticationSuccessHandler() { + if (authenticationSuccessHandler == null) { + authenticationSuccessHandler = new VaadinSavedRequestAwareAuthenticationSuccessHandler(); + } + return authenticationSuccessHandler; + } + + private void registerCsrfAuthenticationStrategy(HttpSecurity http) { + 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()); + CsrfAuthenticationStrategy csrfAuthenticationStrategy = new CsrfAuthenticationStrategy( + csrfTokenRepository); + csrf.csrfTokenRepository(csrfTokenRepository) + .sessionAuthenticationStrategy( + (authentication, request, response) -> { + if (!(authentication instanceof JwtAuthenticationToken)) { + csrfAuthenticationStrategy + .onAuthentication(authentication, + request, response); + } + }); + } + } + + private void customizeOAuth2ResourceServer( + OAuth2ResourceServerConfigurer oAuth2ResourceServer, + BearerTokenResolver bearerTokenResolver, JwtDecoder jwtDecoder, + JwtAuthenticationConverter jwtAuthenticationConverter) { + // OAuth2ResourceServerConfigurer configures a CSRF protection bypass + // when request contains bearer token, and does not provide a way to + // re-enable CSRF protection for such requests. However, having JWT in + // cookies requires keeping CSRF protection, so here is a workaround: + // set the cookie-based bearer token resolver directly on the + // BearerTokenAuthenticationFilter using a post processor. + oAuth2ResourceServer.bearerTokenResolver(request -> null); + oAuth2ResourceServer.withObjectPostProcessor( + new BearerTokenAuthentiationFilterPostProcessor( + bearerTokenResolver)); + + oAuth2ResourceServer.jwt().decoder(jwtDecoder) + .jwtAuthenticationConverter(jwtAuthenticationConverter); + } + + private static class BearerTokenAuthentiationFilterPostProcessor + implements ObjectPostProcessor { + BearerTokenResolver bearerTokenResolver; + + BearerTokenAuthentiationFilterPostProcessor( + BearerTokenResolver bearerTokenResolver) { + this.bearerTokenResolver = bearerTokenResolver; + } + + @Override + public F postProcess( + F filter) { + filter.setBearerTokenResolver(bearerTokenResolver); + return filter; + } + } } 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..6ef74938b 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,14 @@ 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\\.JwtSplitCookieManagementFilter", + "com\\.vaadin\\.flow\\.spring\\.security\\.JwtSplitCookieService", "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\\.VaadinWebSecurityConfigurerAdapter\\$BearerTokenAuthentiationFilterPostProcessor", "com\\.vaadin\\.flow\\.spring\\.VaadinServletContextInitializer\\$ClassPathScanner", "com\\.vaadin\\.flow\\.spring\\.VaadinServletContextInitializer\\$CustomResourceLoader"), super.getExcludedPatterns()); From f3c3ac6248740fb5ecd77305304947d4326acb88 Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Tue, 14 Sep 2021 15:21:42 +0300 Subject: [PATCH 02/20] chore: remove unused private field --- .../VaadinSavedRequestAwareAuthenticationSuccessHandler.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java index 3510d3998..4eb42c5d2 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java @@ -58,9 +58,6 @@ public class VaadinSavedRequestAwareAuthenticationSuccessHandler /** This header contains 'ok' if login was successful. */ private static final String RESULT_HEADER = "Result"; - /** This header contains the Vaadin CSRF token. */ - private static final String VAADIN_CSRF_HEADER = "Vaadin-CSRF"; - /** * This header contains the URL defined as the default URL to redirect to * after login. From 7b3de0f20e14db383cc18b34f9ae564f18f861d8 Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Mon, 20 Sep 2021 17:36:04 +0300 Subject: [PATCH 03/20] refactor: reimplement stateless security with JwtSecurityContextRepository and security configurer --- .../JwtSecurityContextRepository.java | 179 ++++++++++++++++++ .../JwtSplitCookieManagementFilter.java | 46 ----- .../security/JwtSplitCookieService.java | 125 ------------ .../SerializedJwtSplitCookieRepository.java | 99 ++++++++++ ...uestAwareAuthenticationSuccessHandler.java | 14 -- .../VaadinStatelessSecurityConfigurer.java | 144 ++++++++++++++ .../VaadinWebSecurityConfigurerAdapter.java | 127 +------------ .../spring/SpringClassesSerializableTest.java | 6 +- 8 files changed, 434 insertions(+), 306 deletions(-) create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSecurityContextRepository.java delete mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSplitCookieManagementFilter.java delete mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSplitCookieService.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/security/SerializedJwtSplitCookieRepository.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinStatelessSecurityConfigurer.java 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..56b173b23 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSecurityContextRepository.java @@ -0,0 +1,179 @@ +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.SecurityContextRepository; + +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 String issuer; + private long expiresIn = 1800L; + private JWKSource jwkSource; + private JWSAlgorithm jwsAlgorithm; + private JwtDecoder jwtDecoder; + private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + final private SerializedJwtSplitCookieRepository serializedJwtSplitCookieRepository = new SerializedJwtSplitCookieRepository(); + final private JwtAuthenticationConverter jwtAuthenticationConverter; + + 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) -> { + }); + + JWSKeySelector jwsKeySelector = new JWSVerificationKeySelector( + jwsAlgorithm, jwkSource); + jwtProcessor.setJWSKeySelector(jwsKeySelector); + NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(jwtProcessor); + jwtDecoder.setJwtValidator( + issuer != null ? JwtValidators.createDefaultWithIssuer(issuer) + : JwtValidators.createDefault()); + this.jwtDecoder = jwtDecoder; + return jwtDecoder; + } + + private String encodeJwt(HttpServletRequest request, + HttpServletResponse response, 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(); + } + + @Override + public SecurityContext loadContext( + HttpRequestResponseHolder requestResponseHolder) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + + String serializedJwt = serializedJwtSplitCookieRepository.loadSerializedJwt( + requestResponseHolder.getRequest()); + Jwt jwt = getJwtDecoder().decode(serializedJwt); + if (jwt != null) { + Authentication authentication = jwtAuthenticationConverter.convert( + jwt); + context.setAuthentication(authentication); + } + + return context; + } + + @Override + public void saveContext(SecurityContext context, HttpServletRequest request, + HttpServletResponse response) { + String serializedJwt; + try { + serializedJwt = encodeJwt(request, response, + 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); + } +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSplitCookieManagementFilter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSplitCookieManagementFilter.java deleted file mode 100644 index 12f365fc1..000000000 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSplitCookieManagementFilter.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.vaadin.flow.spring.security; - -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 { - - private JwtSplitCookieService jwtSplitCookieService; - - public JwtSplitCookieManagementFilter( - JwtSplitCookieService jwtSplitCookieService) { - this.jwtSplitCookieService = jwtSplitCookieService; - } - - @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 - jwtSplitCookieService - .removeJwtSplitCookies((HttpServletRequest) request, - (HttpServletResponse) response); - } else { - jwtSplitCookieService - .setJwtSplitCookiesIfNecessary((HttpServletRequest) request, - (HttpServletResponse) response, authentication); - } - - chain.doFilter(request, response); - } - -} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSplitCookieService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSplitCookieService.java deleted file mode 100644 index 85f50690e..000000000 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSplitCookieService.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.vaadin.flow.spring.security; - -import javax.crypto.SecretKey; -import javax.servlet.ServletException; -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.OctetSequenceKey; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.SignedJWT; -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; -import org.springframework.web.util.WebUtils; - -public class JwtSplitCookieService { - public static final String JWT_HEADER_AND_PAYLOAD_COOKIE_NAME = "jwt.headerAndPayload"; - public static final String JWT_SIGNATURE_COOKIE_NAME = "jwt.signature"; - - private static final String ROLES_CLAIM = "roles"; - private static final String ROLE_AUTHORITY_PREFIX = "ROLE_"; - - private String issuer; - - private long expiresIn; - - private JWK jwk; - - private BearerTokenResolver bearerTokenResolver = (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 JwtSplitCookieService(SecretKey secretKey, String issuer, long expiresIn) { - this.jwk = new OctetSequenceKey.Builder(secretKey) - .algorithm(JWSAlgorithm.parse(secretKey.getAlgorithm())) - .build(); - this.issuer = issuer; - this.expiresIn = expiresIn; - } - - public BearerTokenResolver getBearerTokenResolver() { - return bearerTokenResolver; - } - - public void setJwtSplitCookiesIfNecessary(HttpServletRequest request, HttpServletResponse response, - Authentication authentication) throws ServletException { - 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; - try { - JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(jwk.getAlgorithm().getName()); - JWSHeader jwsHeader = new JWSHeader(jwsAlgorithm); - - 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); - - Cookie headerAndPayload = new Cookie(JWT_HEADER_AND_PAYLOAD_COOKIE_NAME, - new String(signedJWT.getSigningInput(), StandardCharsets.UTF_8)); - headerAndPayload.setHttpOnly(false); - headerAndPayload.setSecure(request.isSecure()); - headerAndPayload.setPath(request.getContextPath() + "/"); - headerAndPayload.setMaxAge((int) expiresIn - 1); - response.addCookie(headerAndPayload); - - Cookie signature = new Cookie(JWT_SIGNATURE_COOKIE_NAME, signedJWT.getSignature().toString()); - signature.setHttpOnly(true); - signature.setSecure(request.isSecure()); - signature.setPath(request.getContextPath() + "/"); - signature.setMaxAge((int) expiresIn - 1); - response.addCookie(signature); - } catch (JOSEException e) { - throw new ServletException("Unable to issue a new JWT", e); - } - } - - public 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/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..21bc62452 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/SerializedJwtSplitCookieRepository.java @@ -0,0 +1,99 @@ +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; + +class SerializedJwtSplitCookieRepository { + public static final String JWT_HEADER_AND_PAYLOAD_COOKIE_NAME = "jwt.headerAndPayload"; + public static final String JWT_SIGNATURE_COOKIE_NAME = "jwt.signature"; + + private long expiresIn = 1800L; + + SerializedJwtSplitCookieRepository() { + } + + void setExpiresIn(long expiresIn) { + this.expiresIn = expiresIn; + } + + 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(); + } + + void saveSerializedJwt(String jwt, HttpServletRequest request, + HttpServletResponse response) { + if (jwt == null) { + this.removeJwtSplitCookies(request, response); + } else { + this.setJwtSplitCookies(jwt, request, response); + } + } + + 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 jwt, HttpServletRequest request, + HttpServletResponse response) { + final String[] parts = jwt.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(request.getContextPath() + "/"); + 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(request.getContextPath() + "/"); + 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(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/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java index 4eb42c5d2..112e40b69 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java @@ -81,11 +81,6 @@ public class VaadinSavedRequestAwareAuthenticationSuccessHandler */ private static final String SPRING_CSRF_TOKEN = "Spring-CSRF-token"; - /** - * Optional service to persist authentication in JWT cookies. - */ - private JwtSplitCookieService jwtSplitCookieService; - /** * Redirect strategy used by * {@link VaadinSavedRequestAwareAuthenticationSuccessHandler}. @@ -160,11 +155,6 @@ public void onAuthenticationSuccess(HttpServletRequest request, determineTargetUrl(request, response)); } - if (jwtSplitCookieService != null) { - jwtSplitCookieService.setJwtSplitCookiesIfNecessary(request, - response, authentication); - } - super.onAuthenticationSuccess(request, response, authentication); } @@ -195,8 +185,4 @@ public void setRequestCache(RequestCache requestCache) { super.setRequestCache(requestCache); this.requestCache = requestCache; } - - void setJwtSplitCookieService(JwtSplitCookieService jwtSplitCookieService) { - this.jwtSplitCookieService = jwtSplitCookieService; - } } 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..5cc1c4c14 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinStatelessSecurityConfigurer.java @@ -0,0 +1,144 @@ +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.builders.HttpSecurity; +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; + +public class VaadinStatelessSecurityConfigurer> + extends + AbstractHttpConfigurer, H> { + private long expiresIn = 1800L; + + private String issuer; + + private SecretKeyConfigurer secretKeyConfigurer; + + @Override + 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 + 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); + } + + 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 60156756a..cd2ae185e 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 @@ -21,29 +21,13 @@ import java.util.stream.Stream; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.config.annotation.ObjectPostProcessor; 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.WebSecurityConfigurerAdapter; -import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; -import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.jose.jws.MacAlgorithm; -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.JwtAuthenticationToken; -import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; -import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter; -import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; -import org.springframework.security.web.authentication.switchuser.SwitchUserFilter; -import org.springframework.security.web.csrf.CookieCsrfTokenRepository; -import org.springframework.security.web.csrf.CsrfAuthenticationStrategy; -import org.springframework.security.web.csrf.CsrfTokenRepository; -import org.springframework.security.web.csrf.LazyCsrfTokenRepository; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -76,7 +60,6 @@ public class MySecurityConfigurerAdapter extends VaadinWebSecurityConfigurerAdap } * - * */ public abstract class VaadinWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @@ -90,8 +73,6 @@ public abstract class VaadinWebSecurityConfigurerAdapter @Autowired private ViewAccessChecker viewAccessChecker; - private VaadinSavedRequestAwareAuthenticationSuccessHandler authenticationSuccessHandler; - /** * The paths listed as "ignoring" in this method are handled without any * Spring Security involvement. They have no access to any security context @@ -210,7 +191,7 @@ protected void setLoginView(HttpSecurity http, String fusionLoginViewPath, String logoutUrl) throws Exception { FormLoginConfigurer formLogin = http.formLogin(); formLogin.loginPage(fusionLoginViewPath).permitAll(); - formLogin.successHandler(getAuthenticationSuccessHandler()); + formLogin.successHandler(new VaadinSavedRequestAwareAuthenticationSuccessHandler()); http.logout().logoutSuccessUrl(logoutUrl); viewAccessChecker.setLoginView(fusionLoginViewPath); } @@ -263,7 +244,7 @@ protected void setLoginView(HttpSecurity http, // Actually set it up FormLoginConfigurer formLogin = http.formLogin(); formLogin.loginPage(loginPath).permitAll(); - formLogin.successHandler(getAuthenticationSuccessHandler()); + formLogin.successHandler(new VaadinSavedRequestAwareAuthenticationSuccessHandler()); http.csrf().ignoringAntMatchers(loginPath); http.logout().logoutSuccessUrl(logoutUrl); viewAccessChecker.setLoginView(flowLoginView); @@ -280,6 +261,7 @@ protected void setLoginView(HttpSecurity http, * @param issuer * the issuer JWT claim * @throws Exception + * if something goes wrong */ protected void setJwtSplitCookieAuthentication(HttpSecurity http, SecretKey secretKey, String issuer) throws Exception { @@ -299,109 +281,16 @@ protected void setJwtSplitCookieAuthentication(HttpSecurity http, * @param expiresIn * lifetime of the JWT and cookies, in seconds * @throws Exception + * if something goes wrong */ @SuppressWarnings("unchecked") protected void setJwtSplitCookieAuthentication(HttpSecurity http, SecretKey secretKey, String issuer, long expiresIn) throws Exception { - JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); - grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); - grantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); - - JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); - jwtAuthenticationConverter - .setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); - - NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(secretKey) - .build(); - jwtDecoder - .setJwtValidator(JwtValidators.createDefaultWithIssuer(issuer)); - - JwtSplitCookieService jwtSplitCookieService = new JwtSplitCookieService( - secretKey, issuer, expiresIn); + VaadinStatelessSecurityConfigurer vaadinStatelessSecurityConfigurer = new VaadinStatelessSecurityConfigurer<>(); + http.apply(vaadinStatelessSecurityConfigurer); - getAuthenticationSuccessHandler().setJwtSplitCookieService(jwtSplitCookieService); - - // @formatter:off - http - .oauth2ResourceServer(oAuth2ResourceServer -> - customizeOAuth2ResourceServer(oAuth2ResourceServer, - jwtSplitCookieService.getBearerTokenResolver(), - jwtDecoder, jwtAuthenticationConverter)) - .addFilterAfter( - new JwtSplitCookieManagementFilter(jwtSplitCookieService), - SwitchUserFilter.class); - // @formatter:on - - http.logout().addLogoutHandler( - (request, response, authentication) -> jwtSplitCookieService - .removeJwtSplitCookies(request, response)); - - registerCsrfAuthenticationStrategy(http); - } - - private VaadinSavedRequestAwareAuthenticationSuccessHandler getAuthenticationSuccessHandler() { - if (authenticationSuccessHandler == null) { - authenticationSuccessHandler = new VaadinSavedRequestAwareAuthenticationSuccessHandler(); - } - return authenticationSuccessHandler; - } - - private void registerCsrfAuthenticationStrategy(HttpSecurity http) { - 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()); - CsrfAuthenticationStrategy csrfAuthenticationStrategy = new CsrfAuthenticationStrategy( - csrfTokenRepository); - csrf.csrfTokenRepository(csrfTokenRepository) - .sessionAuthenticationStrategy( - (authentication, request, response) -> { - if (!(authentication instanceof JwtAuthenticationToken)) { - csrfAuthenticationStrategy - .onAuthentication(authentication, - request, response); - } - }); - } - } - - private void customizeOAuth2ResourceServer( - OAuth2ResourceServerConfigurer oAuth2ResourceServer, - BearerTokenResolver bearerTokenResolver, JwtDecoder jwtDecoder, - JwtAuthenticationConverter jwtAuthenticationConverter) { - // OAuth2ResourceServerConfigurer configures a CSRF protection bypass - // when request contains bearer token, and does not provide a way to - // re-enable CSRF protection for such requests. However, having JWT in - // cookies requires keeping CSRF protection, so here is a workaround: - // set the cookie-based bearer token resolver directly on the - // BearerTokenAuthenticationFilter using a post processor. - oAuth2ResourceServer.bearerTokenResolver(request -> null); - oAuth2ResourceServer.withObjectPostProcessor( - new BearerTokenAuthentiationFilterPostProcessor( - bearerTokenResolver)); - - oAuth2ResourceServer.jwt().decoder(jwtDecoder) - .jwtAuthenticationConverter(jwtAuthenticationConverter); - } - - private static class BearerTokenAuthentiationFilterPostProcessor - implements ObjectPostProcessor { - BearerTokenResolver bearerTokenResolver; - - BearerTokenAuthentiationFilterPostProcessor( - BearerTokenResolver bearerTokenResolver) { - this.bearerTokenResolver = bearerTokenResolver; - } - - @Override - public F postProcess( - F filter) { - filter.setBearerTokenResolver(bearerTokenResolver); - return filter; - } + vaadinStatelessSecurityConfigurer.withSecretKey().secretKey(secretKey) + .and().issuer(issuer).expiresIn(expiresIn); } } 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 6ef74938b..95adfbbf0 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,13 +88,15 @@ 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\\.JwtSplitCookieManagementFilter", - "com\\.vaadin\\.flow\\.spring\\.security\\.JwtSplitCookieService", + "com\\.vaadin\\.flow\\.spring\\.security\\.JwtSecurityContextRepository", + "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"), From 411d0bd469b175ec8541c709303306f43d1cb169 Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Wed, 22 Sep 2021 13:28:56 +0300 Subject: [PATCH 04/20] Fix SecurityIT with JWT --- .../endpoints/BalanceEndpoint.java | 7 +- .../spring/fusionsecurityjwt/Application.java | 15 ++-- .../config/SecurityConfiguration.java | 80 ------------------- .../src/main/resources/application.properties | 5 ++ .../spring/fusionsecurity/SecurityConfig.java | 24 ++++-- .../JwtSecurityContextRepository.java | 22 +++-- .../SerializedJwtSplitCookieRepository.java | 41 ++++++++-- 7 files changed, 83 insertions(+), 111 deletions(-) delete mode 100644 vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/config/SecurityConfiguration.java create mode 100644 vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/resources/application.properties 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/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..c807c1c76 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,16 @@ 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; -@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\\..*") }) +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/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 5173b91e0..000000000 --- a/vaadin-spring-tests/test-spring-security-fusion-jwt/src/main/java/com/vaadin/flow/spring/fusionsecurityjwt/config/SecurityConfiguration.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.vaadin.flow.spring.fusionsecurityjwt.config; - -import javax.crypto.spec.SecretKeySpec; -import java.util.Base64; -import java.util.stream.Collectors; - -import org.springframework.beans.factory.annotation.Autowired; -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; -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 -@Order(10) -public class SecurityConfiguration extends VaadinWebSecurityConfigurerAdapter { - - 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, - new SecretKeySpec(Base64.getUrlDecoder().decode( - "I72kIcB1UrUQVHVUAzgweE+BLc0bF8mLv9SmrgKsQAk="), - JwsAlgorithms.HS256), - "statelessapp"); - setLoginView(http, "/login"); - // @formatter:on - } - - @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/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/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..2ec87e473 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) { + setJwtSplitCookieAuthentication(http, new SecretKeySpec( + Base64.getUrlDecoder() + .decode("I72kIcB1UrUQVHVUAzgweE-BLc0bF8mLv9SmrgKsQAk"), + JwsAlgorithms.HS256), "statelessapp"); + } } @Override 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 index 56b173b23..38ba11a5a 100644 --- 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 @@ -37,18 +37,22 @@ import org.springframework.security.web.context.HttpRequestResponseHolder; 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()); + final private SerializedJwtSplitCookieRepository serializedJwtSplitCookieRepository = new SerializedJwtSplitCookieRepository(); + final private JwtAuthenticationConverter jwtAuthenticationConverter; private String issuer; private long expiresIn = 1800L; private JWKSource jwkSource; private JWSAlgorithm jwsAlgorithm; private JwtDecoder jwtDecoder; private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); - final private SerializedJwtSplitCookieRepository serializedJwtSplitCookieRepository = new SerializedJwtSplitCookieRepository(); - final private JwtAuthenticationConverter jwtAuthenticationConverter; JwtSecurityContextRepository() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); @@ -87,7 +91,6 @@ private JwtDecoder getJwtDecoder() { return jwtDecoder; } - DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); jwtProcessor.setJWTClaimsSetVerifier((claimsSet, context) -> { }); @@ -139,14 +142,21 @@ private String encodeJwt(HttpServletRequest request, 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(); - String serializedJwt = serializedJwtSplitCookieRepository.loadSerializedJwt( - requestResponseHolder.getRequest()); - Jwt jwt = getJwtDecoder().decode(serializedJwt); + Jwt jwt = decodeJwt(requestResponseHolder.getRequest()); if (jwt != null) { Authentication authentication = jwtAuthenticationConverter.convert( jwt); 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 index 21bc62452..bdc1062e0 100644 --- 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 @@ -6,6 +6,10 @@ 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 { public static final String JWT_HEADER_AND_PAYLOAD_COOKIE_NAME = "jwt.headerAndPayload"; public static final String JWT_SIGNATURE_COOKIE_NAME = "jwt.signature"; @@ -15,10 +19,21 @@ class SerializedJwtSplitCookieRepository { 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) { @@ -40,15 +55,29 @@ String loadSerializedJwt(HttpServletRequest request) { return jwtHeaderAndPayload.getValue() + "." + jwtSignature.getValue(); } - void saveSerializedJwt(String jwt, HttpServletRequest request, + /** + * 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 (jwt == null) { + if (serializedJwt == null) { this.removeJwtSplitCookies(request, response); } else { - this.setJwtSplitCookies(jwt, request, response); + 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); @@ -57,9 +86,9 @@ boolean containsSerializedJwt(HttpServletRequest request) { return (jwtHeaderAndPayload != null) && (jwtSignature != null); } - private void setJwtSplitCookies(String jwt, HttpServletRequest request, - HttpServletResponse response) { - final String[] parts = jwt.split("\\."); + 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]; From 310e227d4a9479415335abc96ccf6a0bbb98643d Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Thu, 23 Sep 2021 13:25:49 +0300 Subject: [PATCH 05/20] Fix loading the JWT user in IT --- .../com/vaadin/flow/spring/fusionsecurityjwt/Application.java | 2 ++ 1 file changed, 2 insertions(+) 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 c807c1c76..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 @@ -4,11 +4,13 @@ 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.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 { From d36d01248cb94108d3350660cb2d57f783f93daa Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Mon, 27 Sep 2021 19:37:58 +0300 Subject: [PATCH 06/20] Add ITs for JWT cookies --- .../spring/fusionsecurityjwt/SecurityIT.java | 53 ++++++++++++++++++- .../spring/fusionsecurity/SecurityIT.java | 34 ++++++------ .../JwtSecurityContextRepository.java | 26 ++++++++- .../security/VaadinDefaultRequestCache.java | 33 ++++++++++-- .../VaadinStatelessSecurityConfigurer.java | 14 +++-- .../VaadinWebSecurityConfigurerAdapter.java | 19 +++++-- 6 files changed, 149 insertions(+), 30 deletions(-) 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/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..d063ee533 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(); } @@ -108,13 +115,6 @@ public void navigate_to_private_view_prevented() { assertLoginViewShown(); } - @Test - public void navigate_to_admin_view_prevented() { - open(""); - navigateTo("admin", false); - assertLoginViewShown(); - } - @Test public void redirect_to_private_view_after_login() { open("private"); @@ -274,11 +274,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 +307,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/src/main/java/com/vaadin/flow/spring/security/JwtSecurityContextRepository.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/JwtSecurityContextRepository.java index 38ba11a5a..d13c5c035 100644 --- 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 @@ -35,6 +35,7 @@ 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; /** @@ -143,7 +144,8 @@ private String encodeJwt(HttpServletRequest request, } private Jwt decodeJwt(HttpServletRequest request) { - String serializedJwt = serializedJwtSplitCookieRepository.loadSerializedJwt(request); + String serializedJwt = serializedJwtSplitCookieRepository.loadSerializedJwt( + request); if (serializedJwt == null) { return null; } @@ -156,13 +158,16 @@ public SecurityContext loadContext( HttpRequestResponseHolder requestResponseHolder) { SecurityContext context = SecurityContextHolder.createEmptyContext(); - Jwt jwt = decodeJwt(requestResponseHolder.getRequest()); + 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; } @@ -186,4 +191,21 @@ 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/VaadinDefaultRequestCache.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinDefaultRequestCache.java index 0fe664ee8..6e11c3eae 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 @@ -6,22 +6,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 +42,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 +74,7 @@ private boolean isServiceWorkerInitiated(HttpServletRequest request) { return referer != null && referer.endsWith("sw.js"); } + public 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 index 5cc1c4c14..5be8565ee 100644 --- 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 @@ -11,7 +11,6 @@ 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.builders.HttpSecurity; 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; @@ -21,6 +20,8 @@ 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; public class VaadinStatelessSecurityConfigurer> extends @@ -32,6 +33,7 @@ public class VaadinStatelessSecurityConfigurer> private SecretKeyConfigurer secretKeyConfigurer; @Override + @SuppressWarnings("unchecked") public void init(H http) { JwtSecurityContextRepository jwtSecurityContextRepository = new JwtSecurityContextRepository(); SecurityContextConfigurer securityContext = http.getConfigurer( @@ -46,6 +48,7 @@ public void init(H http) { } @Override + @SuppressWarnings("unchecked") public void configure(H http) { SecurityContextRepository securityContextRepository = http.getSharedObject( SecurityContextRepository.class); @@ -68,8 +71,13 @@ public void configure(H http) { jwtSecurityContextRepository.setTrustResolver(trustResolver); } - CsrfConfigurer csrf = http.getConfigurer( - CsrfConfigurer.class); + 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) 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 cd2ae185e..d93def264 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 @@ -28,6 +28,7 @@ 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; @@ -191,7 +192,8 @@ protected void setLoginView(HttpSecurity http, String fusionLoginViewPath, String logoutUrl) throws Exception { FormLoginConfigurer formLogin = http.formLogin(); formLogin.loginPage(fusionLoginViewPath).permitAll(); - formLogin.successHandler(new VaadinSavedRequestAwareAuthenticationSuccessHandler()); + formLogin.successHandler( + getVaadinSavedRequestAwareAuthenticationSuccessHandler(http)); http.logout().logoutSuccessUrl(logoutUrl); viewAccessChecker.setLoginView(fusionLoginViewPath); } @@ -244,7 +246,8 @@ protected void setLoginView(HttpSecurity http, // Actually set it up FormLoginConfigurer formLogin = http.formLogin(); formLogin.loginPage(loginPath).permitAll(); - formLogin.successHandler(new VaadinSavedRequestAwareAuthenticationSuccessHandler()); + formLogin.successHandler( + getVaadinSavedRequestAwareAuthenticationSuccessHandler(http)); http.csrf().ignoringAntMatchers(loginPath); http.logout().logoutSuccessUrl(logoutUrl); viewAccessChecker.setLoginView(flowLoginView); @@ -283,7 +286,6 @@ protected void setJwtSplitCookieAuthentication(HttpSecurity http, * @throws Exception * if something goes wrong */ - @SuppressWarnings("unchecked") protected void setJwtSplitCookieAuthentication(HttpSecurity http, SecretKey secretKey, String issuer, long expiresIn) throws Exception { @@ -293,4 +295,15 @@ protected void setJwtSplitCookieAuthentication(HttpSecurity http, 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; + } } From 52dff85aee40298f100369adfd44cd85fc1c6e19 Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Tue, 28 Sep 2021 15:38:12 +0300 Subject: [PATCH 07/20] Fix SpringClassesSerializableTest --- .../com/vaadin/flow/spring/SpringClassesSerializableTest.java | 1 + 1 file changed, 1 insertion(+) 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 95adfbbf0..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 @@ -89,6 +89,7 @@ protected Stream getExcludedPatterns() { "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", From 78fe1bec904cdd440750ab4dcf26897c7de998ce Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Tue, 28 Sep 2021 15:38:33 +0300 Subject: [PATCH 08/20] Add SerializedJwtSplitCookieRepositoryTest --- .../SerializedJwtSplitCookieRepository.java | 13 +- ...erializedJwtSplitCookieRepositoryTest.java | 222 ++++++++++++++++++ 2 files changed, 231 insertions(+), 4 deletions(-) create mode 100644 vaadin-spring/src/test/java/com/vaadin/flow/spring/security/SerializedJwtSplitCookieRepositoryTest.java 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 index bdc1062e0..b354752ed 100644 --- 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 @@ -96,14 +96,14 @@ private void setJwtSplitCookies(String serializedJwt, jwtHeaderAndPayload); headerAndPayload.setHttpOnly(false); headerAndPayload.setSecure(request.isSecure()); - headerAndPayload.setPath(request.getContextPath() + "/"); + 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(request.getContextPath() + "/"); + signature.setPath(getRequestContextPath(request)); signature.setMaxAge((int) expiresIn - 1); response.addCookie(signature); } @@ -112,17 +112,22 @@ private void removeJwtSplitCookies(HttpServletRequest request, HttpServletResponse response) { Cookie jwtHeaderAndPayloadRemove = new Cookie( JWT_HEADER_AND_PAYLOAD_COOKIE_NAME, null); - jwtHeaderAndPayloadRemove.setPath(request.getContextPath() + "/"); + 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(request.getContextPath() + "/"); + 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 contextPath.equals("") ? "/" : contextPath; + } } 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 From 28eb973b292766730b33f1883d6417f3eadbf91f Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Wed, 29 Sep 2021 10:16:34 +0300 Subject: [PATCH 09/20] Test delegateRequestCache in VaadinDefaultRequestCache --- .../VaadinDefaultRequestCacheTest.java | 91 ++++++++++++++++--- 1 file changed, 80 insertions(+), 11 deletions(-) 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); } From b4de07a82cb69de9320859578824dc3662edd8b4 Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Wed, 29 Sep 2021 11:04:41 +0300 Subject: [PATCH 10/20] Fix logout button check in SecurityIT --- .../java/com/vaadin/flow/spring/fusionsecurity/SecurityIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d063ee533..664f3920a 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 @@ -47,7 +47,7 @@ protected void logout() { ElementQuery mainViewQuery = $("*").attribute("id", "main-view"); if (!mainViewQuery.exists() || - mainViewQuery.get(0).$(ButtonElement.class) + !mainViewQuery.get(0).$(ButtonElement.class) .attribute("id", "logout").exists()) { open(""); assertRootPageShown(); From 1db07ce52d37670b9ba4a0ce08815f25938218fb Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Wed, 29 Sep 2021 11:07:15 +0300 Subject: [PATCH 11/20] Revert accidentally removed test --- .../com/vaadin/flow/spring/fusionsecurity/SecurityIT.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 664f3920a..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 @@ -115,6 +115,13 @@ public void navigate_to_private_view_prevented() { assertLoginViewShown(); } + @Test + public void navigate_to_admin_view_prevented() { + open(""); + navigateTo("admin", false); + assertLoginViewShown(); + } + @Test public void redirect_to_private_view_after_login() { open("private"); From 76340c889eb0818b995160ccc9e2fd8d85c588b3 Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Wed, 29 Sep 2021 11:18:42 +0300 Subject: [PATCH 12/20] Make stateless Spring Security dependencies optional --- vaadin-spring-tests/test-spring-security-fusion-jwt/pom.xml | 4 ++++ vaadin-spring/pom.xml | 2 ++ 2 files changed, 6 insertions(+) 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/pom.xml b/vaadin-spring/pom.xml index 7c5cad4fe..104e248d5 100644 --- a/vaadin-spring/pom.xml +++ b/vaadin-spring/pom.xml @@ -158,10 +158,12 @@ org.springframework.security spring-security-oauth2-jose + true org.springframework.security spring-security-oauth2-resource-server + true From fbe7e656bb68d20e65a2848f6bd476322cc1d99b Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Wed, 29 Sep 2021 11:23:26 +0300 Subject: [PATCH 13/20] Add license headers --- .../security/JwtSecurityContextRepository.java | 15 +++++++++++++++ .../SerializedJwtSplitCookieRepository.java | 15 +++++++++++++++ .../security/VaadinDefaultRequestCache.java | 15 +++++++++++++++ .../VaadinStatelessSecurityConfigurer.java | 15 +++++++++++++++ 4 files changed, 60 insertions(+) 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 index d13c5c035..390cad736 100644 --- 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 @@ -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; 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 index b354752ed..80fa35280 100644 --- 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 @@ -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.Cookie; 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 6e11c3eae..ca2daeb63 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; 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 index 5be8565ee..a9c1c80d5 100644 --- 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 @@ -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.crypto.SecretKey; From edfbaa21bda65dd61016b17ee6447bd55a4c2a23 Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Wed, 29 Sep 2021 11:27:07 +0300 Subject: [PATCH 14/20] Make cookie names private --- .../spring/security/SerializedJwtSplitCookieRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 80fa35280..dd4e86260 100644 --- 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 @@ -26,8 +26,8 @@ * "jwt.headerAndPayload" (JS-readable), and "jwt.signature" (HTTP-only). */ class SerializedJwtSplitCookieRepository { - public static final String JWT_HEADER_AND_PAYLOAD_COOKIE_NAME = "jwt.headerAndPayload"; - public static final String JWT_SIGNATURE_COOKIE_NAME = "jwt.signature"; + 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; From b4e81c216f5fda20ae8b9193add324861c817e07 Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Wed, 29 Sep 2021 11:31:59 +0300 Subject: [PATCH 15/20] Make setDelegateRequestCache package-private for now --- .../vaadin/flow/spring/security/VaadinDefaultRequestCache.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ca2daeb63..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 @@ -89,7 +89,7 @@ private boolean isServiceWorkerInitiated(HttpServletRequest request) { return referer != null && referer.endsWith("sw.js"); } - public void setDelegateRequestCache(RequestCache delegateRequestCache) { + void setDelegateRequestCache(RequestCache delegateRequestCache) { this.delegateRequestCache = delegateRequestCache; } } \ No newline at end of file From 14ecfc2e70a7ba94c15d3ef3f7159b113cfd1d26 Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Wed, 29 Sep 2021 11:41:44 +0300 Subject: [PATCH 16/20] Make VaadinStatelessSecurityConfigurer package-private for now --- .../flow/spring/security/VaadinStatelessSecurityConfigurer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index a9c1c80d5..d3066973b 100644 --- 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 @@ -38,7 +38,7 @@ import org.springframework.security.web.savedrequest.CookieRequestCache; import org.springframework.security.web.savedrequest.RequestCache; -public class VaadinStatelessSecurityConfigurer> +class VaadinStatelessSecurityConfigurer> extends AbstractHttpConfigurer, H> { private long expiresIn = 1800L; From 4e922bc34029c30a2d90b8e99b0c687884efea0f Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Wed, 29 Sep 2021 11:45:00 +0300 Subject: [PATCH 17/20] Rename stateless security config entrypoint method --- .../vaadin/flow/spring/fusionsecurity/SecurityConfig.java | 2 +- .../spring/security/VaadinWebSecurityConfigurerAdapter.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 2ec87e473..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 @@ -55,7 +55,7 @@ protected void configure(HttpSecurity http) throws Exception { setLoginView(http, "/login"); if (stateless) { - setJwtSplitCookieAuthentication(http, new SecretKeySpec( + setStatelessAuthentication(http, new SecretKeySpec( Base64.getUrlDecoder() .decode("I72kIcB1UrUQVHVUAzgweE-BLc0bF8mLv9SmrgKsQAk"), JwsAlgorithms.HS256), "statelessapp"); 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 d93def264..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 @@ -266,9 +266,9 @@ protected void setLoginView(HttpSecurity http, * @throws Exception * if something goes wrong */ - protected void setJwtSplitCookieAuthentication(HttpSecurity http, + protected void setStatelessAuthentication(HttpSecurity http, SecretKey secretKey, String issuer) throws Exception { - setJwtSplitCookieAuthentication(http, secretKey, issuer, 3600L); + setStatelessAuthentication(http, secretKey, issuer, 1800L); } /** @@ -286,7 +286,7 @@ protected void setJwtSplitCookieAuthentication(HttpSecurity http, * @throws Exception * if something goes wrong */ - protected void setJwtSplitCookieAuthentication(HttpSecurity http, + protected void setStatelessAuthentication(HttpSecurity http, SecretKey secretKey, String issuer, long expiresIn) throws Exception { VaadinStatelessSecurityConfigurer vaadinStatelessSecurityConfigurer = new VaadinStatelessSecurityConfigurer<>(); From 7e84353009d0d12e5ec88884289c3fe71154a018 Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Wed, 29 Sep 2021 11:53:45 +0300 Subject: [PATCH 18/20] Revert change in SuccessHandler --- .../VaadinSavedRequestAwareAuthenticationSuccessHandler.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java index 112e40b69..0504cce67 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSavedRequestAwareAuthenticationSuccessHandler.java @@ -58,6 +58,9 @@ public class VaadinSavedRequestAwareAuthenticationSuccessHandler /** This header contains 'ok' if login was successful. */ private static final String RESULT_HEADER = "Result"; + /** This header contains the Vaadin CSRF token. */ + private static final String VAADIN_CSRF_HEADER = "Vaadin-CSRF"; + /** * This header contains the URL defined as the default URL to redirect to * after login. From cb05b239029928957e3ffea39616a9aa9c65980c Mon Sep 17 00:00:00 2001 From: haijian Date: Wed, 29 Sep 2021 12:55:37 +0300 Subject: [PATCH 19/20] add oauth2-jose dep to fusion security test --- vaadin-spring-tests/test-spring-security-fusion/pom.xml | 4 ++++ 1 file changed, 4 insertions(+) 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 From c4d590f929021e8c86ffaf795487cec330fe8596 Mon Sep 17 00:00:00 2001 From: Anton Platonov Date: Thu, 30 Sep 2021 11:46:06 +0300 Subject: [PATCH 20/20] Fix analysis findings --- .../JwtSecurityContextRepository.java | 19 +++++++++---------- .../SerializedJwtSplitCookieRepository.java | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) 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 index 390cad736..d9fa0fbf2 100644 --- 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 @@ -61,8 +61,8 @@ 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()); - final private SerializedJwtSplitCookieRepository serializedJwtSplitCookieRepository = new SerializedJwtSplitCookieRepository(); - final private JwtAuthenticationConverter jwtAuthenticationConverter; + private final SerializedJwtSplitCookieRepository serializedJwtSplitCookieRepository = new SerializedJwtSplitCookieRepository(); + private final JwtAuthenticationConverter jwtAuthenticationConverter; private String issuer; private long expiresIn = 1800L; private JWKSource jwkSource; @@ -109,21 +109,21 @@ private JwtDecoder getJwtDecoder() { DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); jwtProcessor.setJWTClaimsSetVerifier((claimsSet, context) -> { + // No-op, Spring Security’s NimbusJwtDecoder uses its own validator }); - JWSKeySelector jwsKeySelector = new JWSVerificationKeySelector( + JWSKeySelector jwsKeySelector = new JWSVerificationKeySelector<>( jwsAlgorithm, jwkSource); jwtProcessor.setJWSKeySelector(jwsKeySelector); - NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(jwtProcessor); - jwtDecoder.setJwtValidator( + NimbusJwtDecoder nimbusJwtDecoder = new NimbusJwtDecoder(jwtProcessor); + nimbusJwtDecoder.setJwtValidator( issuer != null ? JwtValidators.createDefaultWithIssuer(issuer) : JwtValidators.createDefault()); - this.jwtDecoder = jwtDecoder; + this.jwtDecoder = nimbusJwtDecoder; return jwtDecoder; } - private String encodeJwt(HttpServletRequest request, - HttpServletResponse response, Authentication authentication) + private String encodeJwt(Authentication authentication) throws JOSEException { if (authentication == null || trustResolver.isAnonymous(authentication)) { @@ -191,8 +191,7 @@ public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { String serializedJwt; try { - serializedJwt = encodeJwt(request, response, - context.getAuthentication()); + serializedJwt = encodeJwt(context.getAuthentication()); } catch (JOSEException e) { logger.warn("Cannot serialize SecurityContext as JWT", e); serializedJwt = null; 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 index dd4e86260..a5f9837a6 100644 --- 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 @@ -143,6 +143,6 @@ private void removeJwtSplitCookies(HttpServletRequest request, private String getRequestContextPath(HttpServletRequest request) { final String contextPath = request.getContextPath(); - return contextPath.equals("") ? "/" : contextPath; + return "".equals(contextPath) ? "/" : contextPath; } }