From 1e178f59a6473fd3e99e0c269ddc525ffee54101 Mon Sep 17 00:00:00 2001 From: "Raphael J. Sandor" <45048465+R-Sandor@users.noreply.github.com> Date: Tue, 13 May 2025 04:46:34 -0400 Subject: [PATCH 1/8] basic oauth setup --- .../security/config/SecSecurityConfig.java | 39 ++++++++++++++----- .../handlers/Oauth2LoginSuccessHandler.java | 26 +++++++++++++ .../jwt/service/OauthUserService.java | 35 +++++++++++++++++ .../main/resources/application-dev.properties | 7 ++++ 4 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 server/src/main/java/dev/findfirst/security/jwt/handlers/Oauth2LoginSuccessHandler.java create mode 100644 server/src/main/java/dev/findfirst/security/jwt/service/OauthUserService.java diff --git a/server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java b/server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java index 21117c6f..f9ee59ea 100644 --- a/server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java +++ b/server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java @@ -1,10 +1,13 @@ package dev.findfirst.security.config; +import static org.springframework.security.config.Customizer.withDefaults; + import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import dev.findfirst.security.filters.CookieAuthenticationFilter; import dev.findfirst.security.jwt.AuthEntryPointJwt; +import dev.findfirst.security.jwt.handlers.Oauth2LoginSuccessHandler; import dev.findfirst.security.userauth.service.UserDetailsServiceImpl; import com.nimbusds.jose.jwk.JWK; @@ -33,6 +36,7 @@ import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @EnableWebSecurity @@ -50,6 +54,8 @@ public class SecSecurityConfig { private final AuthEntryPointJwt unauthorizedHandler; + private final Oauth2LoginSuccessHandler oauth2Success; + @Bean public CookieAuthenticationFilter cookieJWTAuthFilter() { return new CookieAuthenticationFilter(); @@ -78,18 +84,31 @@ public DaoAuthenticationProvider authenticationProvider() { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/user/**").permitAll() - .anyRequest().authenticated()); - http.csrf(csrf -> csrf.disable()) - .httpBasic(httpBasicCustomizer -> httpBasicCustomizer - .authenticationEntryPoint(unauthorizedHandler)) - .oauth2ResourceServer(rs -> rs.jwt(jwt -> jwt.decoder(jwtDecoder()))) - .sessionManagement( - session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(unauthorizedHandler) - .accessDeniedHandler(new BearerTokenAccessDeniedHandler())); + http.oauth2Login(withDefaults()); + + http.formLogin(withDefaults()); + http.authorizeHttpRequests( + authorize -> authorize.requestMatchers("/**").permitAll().anyRequest().authenticated()); + + http.csrf(csrf -> csrf.disable()); + + http.httpBasic( + httpBasicCustomizer -> httpBasicCustomizer.authenticationEntryPoint(unauthorizedHandler)); + + http.oauth2ResourceServer(rs -> rs.jwt(jwt -> jwt.decoder(jwtDecoder()))).sessionManagement( + session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + http.exceptionHandling(exceptions -> exceptions + .defaultAuthenticationEntryPointFor(unauthorizedHandler, + new AntPathRequestMatcher("/user/signin")) + .accessDeniedHandler(new BearerTokenAccessDeniedHandler())); + http.authenticationProvider(authenticationProvider()); + + // filters http.addFilterBefore(cookieJWTAuthFilter(), UsernamePasswordAuthenticationFilter.class); + + // wrap it all up. return http.build(); } diff --git a/server/src/main/java/dev/findfirst/security/jwt/handlers/Oauth2LoginSuccessHandler.java b/server/src/main/java/dev/findfirst/security/jwt/handlers/Oauth2LoginSuccessHandler.java new file mode 100644 index 00000000..d76bee03 --- /dev/null +++ b/server/src/main/java/dev/findfirst/security/jwt/handlers/Oauth2LoginSuccessHandler.java @@ -0,0 +1,26 @@ +package dev.findfirst.security.jwt.handlers; + + + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class Oauth2LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws ServletException, IOException { + System.out.println("auth success"); + } + +} diff --git a/server/src/main/java/dev/findfirst/security/jwt/service/OauthUserService.java b/server/src/main/java/dev/findfirst/security/jwt/service/OauthUserService.java new file mode 100644 index 00000000..d8505f5d --- /dev/null +++ b/server/src/main/java/dev/findfirst/security/jwt/service/OauthUserService.java @@ -0,0 +1,35 @@ +package dev.findfirst.security.jwt.service; + + + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class OauthUserService implements OAuth2UserService { + // private final UserRepo userRepo; + + @Transactional + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + System.out.println("signing user in"); + OAuth2UserService oAuth2UserService = + new DefaultOAuth2UserService(); + OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest); + log.debug(oAuth2User.toString()); + + + return oAuth2User; + } + + +} diff --git a/server/src/main/resources/application-dev.properties b/server/src/main/resources/application-dev.properties index 7194011b..22f42a3a 100644 --- a/server/src/main/resources/application-dev.properties +++ b/server/src/main/resources/application-dev.properties @@ -28,5 +28,12 @@ spring.mail.port=1025 spring.mail.username=findfirst@localmail.dev # Dev tools +spring.devtools.restart.enabled=true spring.devtools.restart.pollInterval=10s spring.flyway.locations=classpath:db/migration,classpath:db/dev + + +# spring.security.oauth2.client.registration.github.client-secret.github.redirectUri=localhost:9000/user/oauth/code/github +spring + +logging.level.org.springframework.security=TRACE From 1b3263c9ae36609373ede9f816cb04ee69524915 Mon Sep 17 00:00:00 2001 From: "Raphael J. Sandor" <45048465+R-Sandor@users.noreply.github.com> Date: Wed, 14 May 2025 06:13:16 -0400 Subject: [PATCH 2/8] add oauth2 client dependency --- server/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/server/build.gradle b/server/build.gradle index 7f605f43..3743e29a 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -85,6 +85,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.jsoup:jsoup:1.17.2' From 1a81be4109674e450755509a9be7749a2d2f2986 Mon Sep 17 00:00:00 2001 From: "Raphael J. Sandor" <45048465+R-Sandor@users.noreply.github.com> Date: Sat, 17 May 2025 05:10:53 -0400 Subject: [PATCH 3/8] Multiple security chain support added build.gradle: removed Spring Data Rest. OAuthClientsCondition: if there are oauth2 clients. SecSecurityConfig: added support for multiple security filters if oauth2 clients are listed. --- server/build.gradle | 1 - .../conditions/OAuthClientsCondition.java | 23 +++++++++ .../security/config/SecSecurityConfig.java | 49 +++++++++++++------ 3 files changed, 56 insertions(+), 17 deletions(-) create mode 100644 server/src/main/java/dev/findfirst/security/conditions/OAuthClientsCondition.java diff --git a/server/build.gradle b/server/build.gradle index 3743e29a..1a0f7640 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -79,7 +79,6 @@ repositories { dependencies { compileOnly 'org.projectlombok:lombok' - implementation 'org.springframework.boot:spring-boot-starter-data-rest' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' diff --git a/server/src/main/java/dev/findfirst/security/conditions/OAuthClientsCondition.java b/server/src/main/java/dev/findfirst/security/conditions/OAuthClientsCondition.java new file mode 100644 index 00000000..f0505479 --- /dev/null +++ b/server/src/main/java/dev/findfirst/security/conditions/OAuthClientsCondition.java @@ -0,0 +1,23 @@ +package dev.findfirst.security.conditions; + +import java.util.Collections; +import java.util.Map; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +public class OAuthClientsCondition implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + Binder binder = Binder.get(context.getEnvironment()); + Map properties = binder + .bind("spring.security.oauth2.client.registration", Bindable.mapOf(String.class, String.class)) + .orElse(Collections.emptyMap()); + return !properties.isEmpty(); + } + +} diff --git a/server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java b/server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java index f9ee59ea..735bb1c7 100644 --- a/server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java +++ b/server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java @@ -5,6 +5,7 @@ import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import dev.findfirst.security.conditions.OAuthClientsCondition; import dev.findfirst.security.filters.CookieAuthenticationFilter; import dev.findfirst.security.jwt.AuthEntryPointJwt; import dev.findfirst.security.jwt.handlers.Oauth2LoginSuccessHandler; @@ -19,7 +20,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; @@ -83,35 +86,49 @@ public DaoAuthenticationProvider authenticationProvider() { } @Bean + @Order(1) public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http.oauth2Login(withDefaults()); - http.formLogin(withDefaults()); - http.authorizeHttpRequests( - authorize -> authorize.requestMatchers("/**").permitAll().anyRequest().authenticated()); + http.securityMatcher("/user/**", "/api/**") // Include /login + .authorizeHttpRequests(auth -> auth.requestMatchers("/").denyAll()) + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/user/**").permitAll() + .anyRequest().authenticated()); - http.csrf(csrf -> csrf.disable()); + // stateless cookie app + http.csrf(csrf -> csrf.disable()) + .sessionManagement( + session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .oauth2ResourceServer(rs -> rs.jwt(jwt -> jwt.decoder(jwtDecoder()))); http.httpBasic( - httpBasicCustomizer -> httpBasicCustomizer.authenticationEntryPoint(unauthorizedHandler)); + httpBasicCustomizer -> httpBasicCustomizer.authenticationEntryPoint(unauthorizedHandler)) - http.oauth2ResourceServer(rs -> rs.jwt(jwt -> jwt.decoder(jwtDecoder()))).sessionManagement( - session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + // use this exeception only for /user/signin + .exceptionHandling(exceptions -> exceptions + .defaultAuthenticationEntryPointFor(unauthorizedHandler, + new AntPathRequestMatcher("/user/signin")) + .accessDeniedHandler(new BearerTokenAccessDeniedHandler())) - http.exceptionHandling(exceptions -> exceptions - .defaultAuthenticationEntryPointFor(unauthorizedHandler, - new AntPathRequestMatcher("/user/signin")) - .accessDeniedHandler(new BearerTokenAccessDeniedHandler())); + .authenticationProvider(authenticationProvider()) - http.authenticationProvider(authenticationProvider()); - - // filters - http.addFilterBefore(cookieJWTAuthFilter(), UsernamePasswordAuthenticationFilter.class); + // filters + .addFilterBefore(cookieJWTAuthFilter(), UsernamePasswordAuthenticationFilter.class); // wrap it all up. return http.build(); } + @Bean + @Order(2) + @Conditional(OAuthClientsCondition.class) + public SecurityFilterChain oauth2ClientsFilterChain(HttpSecurity http) throws Exception { + http.securityMatcher("/oauth2/**", "/login/**", "/error/**", "/*") // Apply only for OAuth paths + .oauth2Login(withDefaults()) + .formLogin(withDefaults()); + return http.build(); + } + @Bean JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withPublicKey(this.key).build(); From 1fe9f3aee3f3ccb9d7c3e02d4826ecc2662b0575 Mon Sep 17 00:00:00 2001 From: "Raphael J. Sandor" <45048465+R-Sandor@users.noreply.github.com> Date: Sun, 18 May 2025 10:40:02 -0400 Subject: [PATCH 4/8] automatic registration for Oauth2 users --- .../security/config/SecSecurityConfig.java | 2 +- .../handlers/Oauth2LoginSuccessHandler.java | 2 +- .../jwt/service/OauthUserService.java | 35 ----------- .../oauth2client/OauthUserService.java | 60 +++++++++++++++++++ 4 files changed, 62 insertions(+), 37 deletions(-) delete mode 100644 server/src/main/java/dev/findfirst/security/jwt/service/OauthUserService.java create mode 100644 server/src/main/java/dev/findfirst/security/oauth2client/OauthUserService.java diff --git a/server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java b/server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java index 735bb1c7..e4669710 100644 --- a/server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java +++ b/server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java @@ -124,7 +124,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti @Conditional(OAuthClientsCondition.class) public SecurityFilterChain oauth2ClientsFilterChain(HttpSecurity http) throws Exception { http.securityMatcher("/oauth2/**", "/login/**", "/error/**", "/*") // Apply only for OAuth paths - .oauth2Login(withDefaults()) + .oauth2Login(oauth -> oauth.successHandler(oauth2Success)) .formLogin(withDefaults()); return http.build(); } diff --git a/server/src/main/java/dev/findfirst/security/jwt/handlers/Oauth2LoginSuccessHandler.java b/server/src/main/java/dev/findfirst/security/jwt/handlers/Oauth2LoginSuccessHandler.java index d76bee03..7176fb12 100644 --- a/server/src/main/java/dev/findfirst/security/jwt/handlers/Oauth2LoginSuccessHandler.java +++ b/server/src/main/java/dev/findfirst/security/jwt/handlers/Oauth2LoginSuccessHandler.java @@ -20,7 +20,7 @@ public class Oauth2LoginSuccessHandler extends SavedRequestAwareAuthenticationSu @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { - System.out.println("auth success"); + log.debug("successful login"); } } diff --git a/server/src/main/java/dev/findfirst/security/jwt/service/OauthUserService.java b/server/src/main/java/dev/findfirst/security/jwt/service/OauthUserService.java deleted file mode 100644 index d8505f5d..00000000 --- a/server/src/main/java/dev/findfirst/security/jwt/service/OauthUserService.java +++ /dev/null @@ -1,35 +0,0 @@ -package dev.findfirst.security.jwt.service; - - - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -@Slf4j -public class OauthUserService implements OAuth2UserService { - // private final UserRepo userRepo; - - @Transactional - @Override - public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - System.out.println("signing user in"); - OAuth2UserService oAuth2UserService = - new DefaultOAuth2UserService(); - OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest); - log.debug(oAuth2User.toString()); - - - return oAuth2User; - } - - -} diff --git a/server/src/main/java/dev/findfirst/security/oauth2client/OauthUserService.java b/server/src/main/java/dev/findfirst/security/oauth2client/OauthUserService.java new file mode 100644 index 00000000..03912b69 --- /dev/null +++ b/server/src/main/java/dev/findfirst/security/oauth2client/OauthUserService.java @@ -0,0 +1,60 @@ +package dev.findfirst.security.oauth2client; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.rmi.UnexpectedException; +import java.util.UUID; + +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import dev.findfirst.security.userauth.models.payload.request.SignupRequest; +import dev.findfirst.users.exceptions.EmailAlreadyRegisteredException; +import dev.findfirst.users.exceptions.UserNameTakenException; +import dev.findfirst.users.model.user.User; +import dev.findfirst.users.repository.UserRepo; +import dev.findfirst.users.service.UserManagementService; + +@Service +@RequiredArgsConstructor +@Slf4j +public class OauthUserService implements OAuth2UserService { + + final UserRepo userRepo; + final UserManagementService ums; + + @Transactional + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2UserService oAuth2UserService = new DefaultOAuth2UserService(); + OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest); + + // user exists in database by email + var attrs = oAuth2User.getAttributes(); + var email = (String) attrs.get("email"); + var username = (String) attrs.get("login"); + if (email != null && !email.isEmpty()) { + log.debug("attempt login with email {}", email); + } else if (username != null && !username.isEmpty()) { + log.debug("looking up if user exist with username {}", username); + var oauth2PlaceholderEmail = username + userRequest.getClientRegistration().getClientId(); + if (userRepo.findByUsername(username).isEmpty()) { + try { + ums.createNewUserAccount(new SignupRequest(username, oauth2PlaceholderEmail, UUID.randomUUID().toString())); + } catch (UnexpectedException | UserNameTakenException | EmailAlreadyRegisteredException e) { + // TODO Auto-generated catch block + log.debug("errors occured"); + } + } + } + + return oAuth2User; + } + +} From 1a4095aa775bad251a08c71eb85f09ba3f395c07 Mon Sep 17 00:00:00 2001 From: "Raphael J. Sandor" <45048465+R-Sandor@users.noreply.github.com> Date: Tue, 20 May 2025 06:02:27 -0400 Subject: [PATCH 5/8] more refactoring and adding support for oauth2 --- .../security/config/SecSecurityConfig.java | 2 +- .../handlers/Oauth2LoginSuccessHandler.java | 26 ------ .../security/jwt/service/TokenService.java | 80 +++++++++++++++++++ .../oauth2client/OauthUserService.java | 18 +++-- .../handlers/Oauth2LoginSuccessHandler.java | 51 ++++++++++++ .../users/controller/UserController.java | 4 +- .../users/service/UserManagementService.java | 22 ++--- 7 files changed, 155 insertions(+), 48 deletions(-) delete mode 100644 server/src/main/java/dev/findfirst/security/jwt/handlers/Oauth2LoginSuccessHandler.java create mode 100644 server/src/main/java/dev/findfirst/security/jwt/service/TokenService.java create mode 100644 server/src/main/java/dev/findfirst/security/oauth2client/handlers/Oauth2LoginSuccessHandler.java diff --git a/server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java b/server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java index e4669710..70d8a40a 100644 --- a/server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java +++ b/server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java @@ -8,7 +8,7 @@ import dev.findfirst.security.conditions.OAuthClientsCondition; import dev.findfirst.security.filters.CookieAuthenticationFilter; import dev.findfirst.security.jwt.AuthEntryPointJwt; -import dev.findfirst.security.jwt.handlers.Oauth2LoginSuccessHandler; +import dev.findfirst.security.oauth2client.handlers.Oauth2LoginSuccessHandler; import dev.findfirst.security.userauth.service.UserDetailsServiceImpl; import com.nimbusds.jose.jwk.JWK; diff --git a/server/src/main/java/dev/findfirst/security/jwt/handlers/Oauth2LoginSuccessHandler.java b/server/src/main/java/dev/findfirst/security/jwt/handlers/Oauth2LoginSuccessHandler.java deleted file mode 100644 index 7176fb12..00000000 --- a/server/src/main/java/dev/findfirst/security/jwt/handlers/Oauth2LoginSuccessHandler.java +++ /dev/null @@ -1,26 +0,0 @@ -package dev.findfirst.security.jwt.handlers; - - - -import java.io.IOException; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; -import org.springframework.stereotype.Component; - -@Component -@Slf4j -public class Oauth2LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { - - @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, - Authentication authentication) throws ServletException, IOException { - log.debug("successful login"); - } - -} diff --git a/server/src/main/java/dev/findfirst/security/jwt/service/TokenService.java b/server/src/main/java/dev/findfirst/security/jwt/service/TokenService.java new file mode 100644 index 00000000..fb1ca324 --- /dev/null +++ b/server/src/main/java/dev/findfirst/security/jwt/service/TokenService.java @@ -0,0 +1,80 @@ +package dev.findfirst.security.jwt.service; + +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.time.Instant; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.JwtEncoderParameters; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.stereotype.Service; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; + +import dev.findfirst.users.repository.UserRepo; +import dev.findfirst.users.model.user.URole; +import dev.findfirst.security.userauth.utils.Constants; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class TokenService { + + @Value("${findfirst.app.jwtExpirationMs}") + private int jwtExpirationMs; + + @Value("${jwt.public.key}") + private RSAPublicKey key; + + @Value("${jwt.private.key}") + private RSAPrivateKey priv; + + private final UserRepo userRepo; + + + public String generateTokenFromUser(int userId) { + Instant now = Instant.now(); + var user = userRepo.findById(userId).orElseThrow(); + String email = user.getEmail(); + Integer roleId = user.getRole().getId(); + var roleName = URole.values()[roleId].toString(); + JwtClaimsSet claims = JwtClaimsSet.builder().issuer("self").issuedAt(Instant.now()) + .expiresAt(now.plusSeconds(jwtExpirationMs)).subject(email).claim("scope", email) + .claim(Constants.ROLE_ID_CLAIM, roleId).claim(Constants.ROLE_NAME_CLAIM, roleName) + .claim("userId", userId).build(); + return jwtEncoder().encode(JwtEncoderParameters.from(claims)).getTokenValue(); + } + + + JwtEncoder jwtEncoder() { + JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build(); + JWKSource jwks = new ImmutableJWKSet<>(new JWKSet(jwk)); + return new NimbusJwtEncoder(jwks); + } + + // private String extractUserId(Authentication authentication) { + // if (authentication.getPrincipal() instanceof UserDetails) { + // String details = ((UserDetails) authentication.getPrincipal()).getUsername(); + // System.out.println("If details " + details); + // return details; + // } else if (authentication.getPrincipal() instanceof DefaultOAuth2User) { + // DefaultOAuth2User oAuth2User = (DefaultOAuth2User) + // authentication.getPrincipal(); + // String details = oAuth2User.getAttribute("id"); + // System.out.println("Else details " + details); + // return details; + // } + // return null; + // } +} diff --git a/server/src/main/java/dev/findfirst/security/oauth2client/OauthUserService.java b/server/src/main/java/dev/findfirst/security/oauth2client/OauthUserService.java index 03912b69..e93a7d95 100644 --- a/server/src/main/java/dev/findfirst/security/oauth2client/OauthUserService.java +++ b/server/src/main/java/dev/findfirst/security/oauth2client/OauthUserService.java @@ -17,9 +17,9 @@ import dev.findfirst.security.userauth.models.payload.request.SignupRequest; import dev.findfirst.users.exceptions.EmailAlreadyRegisteredException; import dev.findfirst.users.exceptions.UserNameTakenException; -import dev.findfirst.users.model.user.User; import dev.findfirst.users.repository.UserRepo; import dev.findfirst.users.service.UserManagementService; +import dev.findfirst.users.model.user.User; @Service @RequiredArgsConstructor @@ -34,6 +34,7 @@ public class OauthUserService implements OAuth2UserService oAuth2UserService = new DefaultOAuth2UserService(); OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest); + User user = null; // user exists in database by email var attrs = oAuth2User.getAttributes(); @@ -41,18 +42,25 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic var username = (String) attrs.get("login"); if (email != null && !email.isEmpty()) { log.debug("attempt login with email {}", email); + // user = userRepo.findByEmail(email).or() } else if (username != null && !username.isEmpty()) { log.debug("looking up if user exist with username {}", username); + var userOpt = userRepo.findByUsername(username); var oauth2PlaceholderEmail = username + userRequest.getClientRegistration().getClientId(); - if (userRepo.findByUsername(username).isEmpty()) { + if (userOpt.isEmpty()) { try { - ums.createNewUserAccount(new SignupRequest(username, oauth2PlaceholderEmail, UUID.randomUUID().toString())); + log.debug("creating a new user for oauth2"); + user = ums.createNewUserAccount(new SignupRequest(username, oauth2PlaceholderEmail, UUID.randomUUID().toString())); } catch (UnexpectedException | UserNameTakenException | EmailAlreadyRegisteredException e) { - // TODO Auto-generated catch block - log.debug("errors occured"); + log.debug("errors occured: {}", e.getMessage()); } + } else { + user = userOpt.get(); } } + if (user.getUserId() != null) { + oAuth2User.getAttributes().put("id", user.getUserId()); + } return oAuth2User; } diff --git a/server/src/main/java/dev/findfirst/security/oauth2client/handlers/Oauth2LoginSuccessHandler.java b/server/src/main/java/dev/findfirst/security/oauth2client/handlers/Oauth2LoginSuccessHandler.java new file mode 100644 index 00000000..43e7d926 --- /dev/null +++ b/server/src/main/java/dev/findfirst/security/oauth2client/handlers/Oauth2LoginSuccessHandler.java @@ -0,0 +1,51 @@ +package dev.findfirst.security.oauth2client.handlers; + + + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import dev.findfirst.security.jwt.service.TokenService; +import dev.findfirst.security.jwt.service.RefreshTokenService; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + + +import dev.findfirst.users.service.UserManagementService; + +@Component +@Slf4j +@RequiredArgsConstructor +public class Oauth2LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { + + @Value("${findfirst.app.frontend-url}") + private String redirectURL; + + private final TokenService ts; + private final RefreshTokenService rt; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws ServletException, IOException { + log.debug("successful login"); + // ResponseCookie cookie = ResponseCookie.from("findfirst", tkns.jwt()).secure(secure).path("/") + // .domain(domain).httpOnly(true).build(); + // + // return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()) + // .body(new TokenRefreshResponse(tkns.refreshToken())); + + + // var signinTokens = new SigninTokens(jwt, refreshToken.getToken()); + + getRedirectStrategy().sendRedirect(request, response, redirectURL); + + } + +} diff --git a/server/src/main/java/dev/findfirst/users/controller/UserController.java b/server/src/main/java/dev/findfirst/users/controller/UserController.java index aeed1ff0..cd74f0d0 100644 --- a/server/src/main/java/dev/findfirst/users/controller/UserController.java +++ b/server/src/main/java/dev/findfirst/users/controller/UserController.java @@ -115,7 +115,7 @@ public ResponseEntity resetPassword(@RequestParam @Email String email) { } } - @GetMapping("changePassword") + @GetMapping("/changePassword") public ResponseEntity frontendPasswordWithToken(@RequestParam("token") String token) throws URISyntaxException { @@ -130,7 +130,7 @@ public ResponseEntity frontendPasswordWithToken(@RequestParam("token") S return new ResponseEntity<>(httpHeaders, HttpStatus.SEE_OTHER); } - @PostMapping("changePassword") + @PostMapping("/changePassword") public ResponseEntity passwordChange(@RequestBody TokenPassword tokenPassword) { try { pwdService.changePassword(tokenPassword); diff --git a/server/src/main/java/dev/findfirst/users/service/UserManagementService.java b/server/src/main/java/dev/findfirst/users/service/UserManagementService.java index 8ddbcae8..6dc717b1 100644 --- a/server/src/main/java/dev/findfirst/users/service/UserManagementService.java +++ b/server/src/main/java/dev/findfirst/users/service/UserManagementService.java @@ -31,6 +31,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.jdbc.core.mapping.AggregateReference; +import dev.findfirst.security.jwt.service.TokenService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.jwt.JwtClaimsSet; import org.springframework.security.oauth2.jwt.JwtEncoder; @@ -48,10 +49,7 @@ public class UserManagementService { private final PasswordTokenRepository passwordTokenRepository; private final RefreshTokenService refreshTokenService; private final PasswordEncoder passwdEncoder; - private final JwtEncoder encoder; - - @Value("${findfirst.app.jwtExpirationMs}") - private int jwtExpirationMs; + private final TokenService ts; @Value("${findfirst.upload.location}") private String uploadLocation; @@ -188,6 +186,7 @@ public User createNewUserAccount(SignupRequest signupRequest) // create a new tenant try { + log.debug("new user - saving user"); return saveUser(user); } catch (Exception e) { // If any exception occurs we should delete the records that were just made. @@ -197,17 +196,12 @@ public User createNewUserAccount(SignupRequest signupRequest) } } + /** + * Wrapper for the Token Service. + * @param userId the userId for token generation. + */ public String generateTokenFromUser(int userId) { - Instant now = Instant.now(); - var user = userRepo.findById(userId).orElseThrow(); - String email = user.getEmail(); - Integer roleId = user.getRole().getId(); - var roleName = URole.values()[roleId].toString(); - JwtClaimsSet claims = JwtClaimsSet.builder().issuer("self").issuedAt(Instant.now()) - .expiresAt(now.plusSeconds(jwtExpirationMs)).subject(email).claim("scope", email) - .claim(Constants.ROLE_ID_CLAIM, roleId).claim(Constants.ROLE_NAME_CLAIM, roleName) - .claim("userId", userId).build(); - return encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue(); + return ts.generateTokenFromUser(userId); } public SigninTokens signinUser(String authorization) throws NoUserFoundException { From c64b4fdf45a21bc8a2ef4468223325d47ce3dc74 Mon Sep 17 00:00:00 2001 From: "Raphael J. Sandor" <45048465+R-Sandor@users.noreply.github.com> Date: Wed, 21 May 2025 05:57:28 -0400 Subject: [PATCH 6/8] TODO: oauth2Successhandler to attach cookie --- .../oauth2client/OauthUserService.java | 40 ++++++++++++++++--- .../handlers/Oauth2LoginSuccessHandler.java | 15 +++++-- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/dev/findfirst/security/oauth2client/OauthUserService.java b/server/src/main/java/dev/findfirst/security/oauth2client/OauthUserService.java index e93a7d95..eff8ab00 100644 --- a/server/src/main/java/dev/findfirst/security/oauth2client/OauthUserService.java +++ b/server/src/main/java/dev/findfirst/security/oauth2client/OauthUserService.java @@ -4,12 +4,16 @@ import lombok.extern.slf4j.Slf4j; import java.rmi.UnexpectedException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,7 +23,10 @@ import dev.findfirst.users.exceptions.UserNameTakenException; import dev.findfirst.users.repository.UserRepo; import dev.findfirst.users.service.UserManagementService; +import dev.findfirst.users.model.user.URole; import dev.findfirst.users.model.user.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; @Service @RequiredArgsConstructor @@ -40,29 +47,52 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic var attrs = oAuth2User.getAttributes(); var email = (String) attrs.get("email"); var username = (String) attrs.get("login"); + String registrationId = userRequest.getClientRegistration().getClientId(); if (email != null && !email.isEmpty()) { log.debug("attempt login with email {}", email); // user = userRepo.findByEmail(email).or() } else if (username != null && !username.isEmpty()) { log.debug("looking up if user exist with username {}", username); var userOpt = userRepo.findByUsername(username); - var oauth2PlaceholderEmail = username + userRequest.getClientRegistration().getClientId(); + + var oauth2PlaceholderEmail = username + registrationId; if (userOpt.isEmpty()) { try { log.debug("creating a new user for oauth2"); - user = ums.createNewUserAccount(new SignupRequest(username, oauth2PlaceholderEmail, UUID.randomUUID().toString())); + user = ums + .createNewUserAccount(new SignupRequest(username, oauth2PlaceholderEmail, UUID.randomUUID().toString())); } catch (UnexpectedException | UserNameTakenException | EmailAlreadyRegisteredException e) { log.debug("errors occured: {}", e.getMessage()); } - } else { + } else { user = userOpt.get(); } } - if (user.getUserId() != null) { - oAuth2User.getAttributes().put("id", user.getUserId()); + if (user.getUserId() != null) { + GrantedAuthority authority = new SimpleGrantedAuthority(URole.values()[user.getRole().getId()].toString()); + String userNameAttributeName = userRequest + .getClientRegistration() + .getProviderDetails() + .getUserInfoEndpoint() + .getUserNameAttributeName(); + log.debug("USER ATTRIBUTE NAME: {}", userNameAttributeName); + var attributes = customAttribute(attrs, userNameAttributeName, user.getUserId(), registrationId); + return new DefaultOAuth2User(Collections.singletonList(authority), attributes, userNameAttributeName); } return oAuth2User; } + private Map customAttribute( + Map attributes, + String userNameAttributeName, + int userID, + String registrationId) { + Map customAttribute = new HashMap<>(); + customAttribute.put(userNameAttributeName, attributes.get(userNameAttributeName)); + customAttribute.put("provider", registrationId); + customAttribute.put("userID", userID); + return customAttribute; + } + } diff --git a/server/src/main/java/dev/findfirst/security/oauth2client/handlers/Oauth2LoginSuccessHandler.java b/server/src/main/java/dev/findfirst/security/oauth2client/handlers/Oauth2LoginSuccessHandler.java index 43e7d926..ac6a0959 100644 --- a/server/src/main/java/dev/findfirst/security/oauth2client/handlers/Oauth2LoginSuccessHandler.java +++ b/server/src/main/java/dev/findfirst/security/oauth2client/handlers/Oauth2LoginSuccessHandler.java @@ -12,12 +12,14 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; + import dev.findfirst.security.jwt.service.TokenService; import dev.findfirst.security.jwt.service.RefreshTokenService; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.stereotype.Component; - +import dev.findfirst.users.model.user.SigninTokens; import dev.findfirst.users.service.UserManagementService; @Component @@ -40,9 +42,14 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo // // return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()) // .body(new TokenRefreshResponse(tkns.refreshToken())); - - - // var signinTokens = new SigninTokens(jwt, refreshToken.getToken()); + authentication.getPrincipal(); + DefaultOAuth2User principal = (DefaultOAuth2User) authentication.getPrincipal(); + var userID = (Integer) principal.getAttributes().get("userID"); + log.debug("userID {}", userID); + ts.generateTokenFromUser(userID); + + // var signinTokens = new SigninTokens(jwt, refreshToken.getToken()); + // getRedirectStrategy().sendRedirect(request, response, redirectURL); From fae7013fe1e7714270087564df0d23ba32a3d3f7 Mon Sep 17 00:00:00 2001 From: "Raphael J. Sandor" <45048465+R-Sandor@users.noreply.github.com> Date: Thu, 22 May 2025 06:05:25 -0400 Subject: [PATCH 7/8] server support for Oauth2 --- .../jwt/service/RefreshTokenService.java | 8 ++-- .../handlers/Oauth2LoginSuccessHandler.java | 47 ++++++++++--------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/server/src/main/java/dev/findfirst/security/jwt/service/RefreshTokenService.java b/server/src/main/java/dev/findfirst/security/jwt/service/RefreshTokenService.java index a1357509..f733cc47 100644 --- a/server/src/main/java/dev/findfirst/security/jwt/service/RefreshTokenService.java +++ b/server/src/main/java/dev/findfirst/security/jwt/service/RefreshTokenService.java @@ -31,12 +31,14 @@ public Optional findByToken(String token) { } public RefreshToken createRefreshToken(User user) { + return createRefreshToken(user.getUserId()); + } - RefreshToken refreshToken = new RefreshToken(null, AggregateReference.to(user.getUserId()), + public RefreshToken createRefreshToken(int userID) { + RefreshToken refreshToken = new RefreshToken(null, AggregateReference.to(userID), UUID.randomUUID().toString(), Instant.now().plusMillis(refreshTokenDurationMs)); - refreshToken = refreshTokenRepository.save(refreshToken); - return refreshToken; + return refreshTokenRepository.save(refreshToken); } public RefreshToken verifyExpiration(RefreshToken token) { diff --git a/server/src/main/java/dev/findfirst/security/oauth2client/handlers/Oauth2LoginSuccessHandler.java b/server/src/main/java/dev/findfirst/security/oauth2client/handlers/Oauth2LoginSuccessHandler.java index ac6a0959..1f08e786 100644 --- a/server/src/main/java/dev/findfirst/security/oauth2client/handlers/Oauth2LoginSuccessHandler.java +++ b/server/src/main/java/dev/findfirst/security/oauth2client/handlers/Oauth2LoginSuccessHandler.java @@ -1,56 +1,57 @@ package dev.findfirst.security.oauth2client.handlers; - - import java.io.IOException; import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseCookie; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; -import dev.findfirst.security.jwt.service.TokenService; import dev.findfirst.security.jwt.service.RefreshTokenService; +import dev.findfirst.security.jwt.service.TokenService; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.stereotype.Component; -import dev.findfirst.users.model.user.SigninTokens; -import dev.findfirst.users.service.UserManagementService; - @Component @Slf4j @RequiredArgsConstructor public class Oauth2LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { @Value("${findfirst.app.frontend-url}") - private String redirectURL; + private String redirectURL; + + @Value("${findfirst.app.domain}") + private String domain; + + @Value("${findfirst.secure-cookies:true}") + private boolean secure; private final TokenService ts; - private final RefreshTokenService rt; + private final RefreshTokenService rt; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { - log.debug("successful login"); - // ResponseCookie cookie = ResponseCookie.from("findfirst", tkns.jwt()).secure(secure).path("/") - // .domain(domain).httpOnly(true).build(); - // - // return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString()) - // .body(new TokenRefreshResponse(tkns.refreshToken())); - authentication.getPrincipal(); - DefaultOAuth2User principal = (DefaultOAuth2User) authentication.getPrincipal(); - var userID = (Integer) principal.getAttributes().get("userID"); - log.debug("userID {}", userID); - ts.generateTokenFromUser(userID); - - // var signinTokens = new SigninTokens(jwt, refreshToken.getToken()); - // - + + DefaultOAuth2User principal = (DefaultOAuth2User) authentication.getPrincipal(); + var userID = (Integer) principal.getAttributes().get("userID"); + + var jwt = ts.generateTokenFromUser(userID); + + ResponseCookie cookie = ResponseCookie.from("findfirst", jwt).secure(secure).path("/") + .domain(domain).httpOnly(true).build(); + + response.addHeader("Set-Cookie", cookie.toString()); + response.getWriter().write(""" + { refreshToken: %s} + """.formatted(rt.createRefreshToken(userID))); getRedirectStrategy().sendRedirect(request, response, redirectURL); } From 41a2648979ecef301e2729f12992240970090373 Mon Sep 17 00:00:00 2001 From: "Raphael J. Sandor" <45048465+R-Sandor@users.noreply.github.com> Date: Thu, 29 May 2025 04:43:13 -0400 Subject: [PATCH 8/8] Attempt to fix chron job mapping --- .../findfirst/core/repository/jdbc/BookmarkJDBCRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/dev/findfirst/core/repository/jdbc/BookmarkJDBCRepository.java b/server/src/main/java/dev/findfirst/core/repository/jdbc/BookmarkJDBCRepository.java index 27048e74..78f0df16 100644 --- a/server/src/main/java/dev/findfirst/core/repository/jdbc/BookmarkJDBCRepository.java +++ b/server/src/main/java/dev/findfirst/core/repository/jdbc/BookmarkJDBCRepository.java @@ -23,7 +23,7 @@ public interface BookmarkJDBCRepository public Page findAllByUserId(int userId, Pageable pageable); - @Query("SELECT b FROM Bookmark b WHERE b.screenshotUrl IS NULL OR TRIM(b.screenshotUrl)=''") + @Query("SELECT * FROM Bookmark b WHERE b.screenshot_url IS NULL OR TRIM(b.screenshot_url)=''") List findBookmarksWithEmptyOrBlankScreenShotUrl(); @Query("select * from bookmark where to_tsvector(title) @@ to_tsquery(:keywords) AND bookmark.user_id = :userID")