Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ 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'
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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public interface BookmarkJDBCRepository

public Page<BookmarkJDBC> 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<BookmarkJDBC> findBookmarksWithEmptyOrBlankScreenShotUrl();

@Query("select * from bookmark where to_tsvector(title) @@ to_tsquery(:keywords) AND bookmark.user_id = :userID")
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> properties = binder
.bind("spring.security.oauth2.client.registration", Bindable.mapOf(String.class, String.class))
.orElse(Collections.emptyMap());
return !properties.isEmpty();
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
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.conditions.OAuthClientsCondition;
import dev.findfirst.security.filters.CookieAuthenticationFilter;
import dev.findfirst.security.jwt.AuthEntryPointJwt;
import dev.findfirst.security.oauth2client.handlers.Oauth2LoginSuccessHandler;
import dev.findfirst.security.userauth.service.UserDetailsServiceImpl;

import com.nimbusds.jose.jwk.JWK;
Expand All @@ -16,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;
Expand All @@ -33,6 +39,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
Expand All @@ -50,6 +57,8 @@ public class SecSecurityConfig {

private final AuthEntryPointJwt unauthorizedHandler;

private final Oauth2LoginSuccessHandler oauth2Success;

@Bean
public CookieAuthenticationFilter cookieJWTAuthFilter() {
return new CookieAuthenticationFilter();
Expand Down Expand Up @@ -77,19 +86,46 @@ public DaoAuthenticationProvider authenticationProvider() {
}

@Bean
@Order(1)
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/user/**").permitAll()
.anyRequest().authenticated());

http.securityMatcher("/user/**", "/api/**") // Include /login
.authorizeHttpRequests(auth -> auth.requestMatchers("/").denyAll())
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/user/**").permitAll()
.anyRequest().authenticated());

// stateless cookie app
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.authenticationProvider(authenticationProvider());
http.addFilterBefore(cookieJWTAuthFilter(), UsernamePasswordAuthenticationFilter.class);
.oauth2ResourceServer(rs -> rs.jwt(jwt -> jwt.decoder(jwtDecoder())));

http.httpBasic(
httpBasicCustomizer -> httpBasicCustomizer.authenticationEntryPoint(unauthorizedHandler))

// use this exeception only for /user/signin
.exceptionHandling(exceptions -> exceptions
.defaultAuthenticationEntryPointFor(unauthorizedHandler,
new AntPathRequestMatcher("/user/signin"))
.accessDeniedHandler(new BearerTokenAccessDeniedHandler()))

.authenticationProvider(authenticationProvider())

// 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(oauth -> oauth.successHandler(oauth2Success))
.formLogin(withDefaults());
return http.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ public Optional<RefreshToken> 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SecurityContext> 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;
// }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package dev.findfirst.security.oauth2client;

import lombok.RequiredArgsConstructor;
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;

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.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
@Slf4j
public class OauthUserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

final UserRepo userRepo;
final UserManagementService ums;

@Transactional
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest);
User user = null;

// user exists in database by email
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 + registrationId;
if (userOpt.isEmpty()) {
try {
log.debug("creating a new user for oauth2");
user = ums
.createNewUserAccount(new SignupRequest(username, oauth2PlaceholderEmail, UUID.randomUUID().toString()));
} catch (UnexpectedException | UserNameTakenException | EmailAlreadyRegisteredException e) {
log.debug("errors occured: {}", e.getMessage());
}
} else {
user = userOpt.get();
}
}
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<String, Object> customAttribute(
Map<String, Object> attributes,
String userNameAttributeName,
int userID,
String registrationId) {
Map<String, Object> customAttribute = new HashMap<>();
customAttribute.put(userNameAttributeName, attributes.get(userNameAttributeName));
customAttribute.put("provider", registrationId);
customAttribute.put("userID", userID);
return customAttribute;
}

}
Loading
Loading