Skip to content

Commit bfa27cd

Browse files
authored
Chronjob error (#377)
* basic oauth setup * add oauth2 client dependency * 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. * automatic registration for Oauth2 users * more refactoring and adding support for oauth2 * TODO: oauth2Successhandler to attach cookie * server support for Oauth2 * Attempt to fix chron job mapping
1 parent 988b6e9 commit bfa27cd

File tree

11 files changed

+329
-30
lines changed

11 files changed

+329
-30
lines changed

server/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,12 @@ repositories {
7979
dependencies {
8080
compileOnly 'org.projectlombok:lombok'
8181

82-
implementation 'org.springframework.boot:spring-boot-starter-data-rest'
8382
implementation 'org.springframework.boot:spring-boot-starter-validation'
8483
implementation 'org.springframework.boot:spring-boot-starter-web'
8584
implementation 'org.springframework.boot:spring-boot-starter-webflux'
8685
implementation 'org.springframework.boot:spring-boot-starter-security'
8786
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
87+
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
8888
implementation 'org.springframework.boot:spring-boot-starter-mail'
8989

9090
implementation 'org.jsoup:jsoup:1.17.2'

server/src/main/java/dev/findfirst/core/repository/jdbc/BookmarkJDBCRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public interface BookmarkJDBCRepository
2323

2424
public Page<BookmarkJDBC> findAllByUserId(int userId, Pageable pageable);
2525

26-
@Query("SELECT b FROM Bookmark b WHERE b.screenshotUrl IS NULL OR TRIM(b.screenshotUrl)=''")
26+
@Query("SELECT * FROM Bookmark b WHERE b.screenshot_url IS NULL OR TRIM(b.screenshot_url)=''")
2727
List<BookmarkJDBC> findBookmarksWithEmptyOrBlankScreenShotUrl();
2828

2929
@Query("select * from bookmark where to_tsvector(title) @@ to_tsquery(:keywords) AND bookmark.user_id = :userID")
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package dev.findfirst.security.conditions;
2+
3+
import java.util.Collections;
4+
import java.util.Map;
5+
6+
import org.springframework.boot.context.properties.bind.Bindable;
7+
import org.springframework.boot.context.properties.bind.Binder;
8+
import org.springframework.context.annotation.Condition;
9+
import org.springframework.context.annotation.ConditionContext;
10+
import org.springframework.core.type.AnnotatedTypeMetadata;
11+
12+
public class OAuthClientsCondition implements Condition {
13+
14+
@Override
15+
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
16+
Binder binder = Binder.get(context.getEnvironment());
17+
Map<String, String> properties = binder
18+
.bind("spring.security.oauth2.client.registration", Bindable.mapOf(String.class, String.class))
19+
.orElse(Collections.emptyMap());
20+
return !properties.isEmpty();
21+
}
22+
23+
}

server/src/main/java/dev/findfirst/security/config/SecSecurityConfig.java

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package dev.findfirst.security.config;
22

3+
import static org.springframework.security.config.Customizer.withDefaults;
4+
35
import java.security.interfaces.RSAPrivateKey;
46
import java.security.interfaces.RSAPublicKey;
57

8+
import dev.findfirst.security.conditions.OAuthClientsCondition;
69
import dev.findfirst.security.filters.CookieAuthenticationFilter;
710
import dev.findfirst.security.jwt.AuthEntryPointJwt;
11+
import dev.findfirst.security.oauth2client.handlers.Oauth2LoginSuccessHandler;
812
import dev.findfirst.security.userauth.service.UserDetailsServiceImpl;
913

1014
import com.nimbusds.jose.jwk.JWK;
@@ -16,7 +20,9 @@
1620
import lombok.RequiredArgsConstructor;
1721
import org.springframework.beans.factory.annotation.Value;
1822
import org.springframework.context.annotation.Bean;
23+
import org.springframework.context.annotation.Conditional;
1924
import org.springframework.context.annotation.Configuration;
25+
import org.springframework.core.annotation.Order;
2026
import org.springframework.security.authentication.AuthenticationManager;
2127
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
2228
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
@@ -33,6 +39,7 @@
3339
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
3440
import org.springframework.security.web.SecurityFilterChain;
3541
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
42+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
3643

3744
@Configuration
3845
@EnableWebSecurity
@@ -50,6 +57,8 @@ public class SecSecurityConfig {
5057

5158
private final AuthEntryPointJwt unauthorizedHandler;
5259

60+
private final Oauth2LoginSuccessHandler oauth2Success;
61+
5362
@Bean
5463
public CookieAuthenticationFilter cookieJWTAuthFilter() {
5564
return new CookieAuthenticationFilter();
@@ -77,19 +86,46 @@ public DaoAuthenticationProvider authenticationProvider() {
7786
}
7887

7988
@Bean
89+
@Order(1)
8090
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
81-
http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/user/**").permitAll()
82-
.anyRequest().authenticated());
91+
92+
http.securityMatcher("/user/**", "/api/**") // Include /login
93+
.authorizeHttpRequests(auth -> auth.requestMatchers("/").denyAll())
94+
.authorizeHttpRequests(authorize -> authorize
95+
.requestMatchers("/user/**").permitAll()
96+
.anyRequest().authenticated());
97+
98+
// stateless cookie app
8399
http.csrf(csrf -> csrf.disable())
84-
.httpBasic(httpBasicCustomizer -> httpBasicCustomizer
85-
.authenticationEntryPoint(unauthorizedHandler))
86-
.oauth2ResourceServer(rs -> rs.jwt(jwt -> jwt.decoder(jwtDecoder())))
87100
.sessionManagement(
88101
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
89-
.exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(unauthorizedHandler)
90-
.accessDeniedHandler(new BearerTokenAccessDeniedHandler()));
91-
http.authenticationProvider(authenticationProvider());
92-
http.addFilterBefore(cookieJWTAuthFilter(), UsernamePasswordAuthenticationFilter.class);
102+
.oauth2ResourceServer(rs -> rs.jwt(jwt -> jwt.decoder(jwtDecoder())));
103+
104+
http.httpBasic(
105+
httpBasicCustomizer -> httpBasicCustomizer.authenticationEntryPoint(unauthorizedHandler))
106+
107+
// use this exeception only for /user/signin
108+
.exceptionHandling(exceptions -> exceptions
109+
.defaultAuthenticationEntryPointFor(unauthorizedHandler,
110+
new AntPathRequestMatcher("/user/signin"))
111+
.accessDeniedHandler(new BearerTokenAccessDeniedHandler()))
112+
113+
.authenticationProvider(authenticationProvider())
114+
115+
// filters
116+
.addFilterBefore(cookieJWTAuthFilter(), UsernamePasswordAuthenticationFilter.class);
117+
118+
// wrap it all up.
119+
return http.build();
120+
}
121+
122+
@Bean
123+
@Order(2)
124+
@Conditional(OAuthClientsCondition.class)
125+
public SecurityFilterChain oauth2ClientsFilterChain(HttpSecurity http) throws Exception {
126+
http.securityMatcher("/oauth2/**", "/login/**", "/error/**", "/*") // Apply only for OAuth paths
127+
.oauth2Login(oauth -> oauth.successHandler(oauth2Success))
128+
.formLogin(withDefaults());
93129
return http.build();
94130
}
95131

server/src/main/java/dev/findfirst/security/jwt/service/RefreshTokenService.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ public Optional<RefreshToken> findByToken(String token) {
3131
}
3232

3333
public RefreshToken createRefreshToken(User user) {
34+
return createRefreshToken(user.getUserId());
35+
}
3436

35-
RefreshToken refreshToken = new RefreshToken(null, AggregateReference.to(user.getUserId()),
37+
public RefreshToken createRefreshToken(int userID) {
38+
RefreshToken refreshToken = new RefreshToken(null, AggregateReference.to(userID),
3639
UUID.randomUUID().toString(), Instant.now().plusMillis(refreshTokenDurationMs));
3740

38-
refreshToken = refreshTokenRepository.save(refreshToken);
39-
return refreshToken;
41+
return refreshTokenRepository.save(refreshToken);
4042
}
4143

4244
public RefreshToken verifyExpiration(RefreshToken token) {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package dev.findfirst.security.jwt.service;
2+
3+
import java.security.interfaces.RSAPrivateKey;
4+
import java.security.interfaces.RSAPublicKey;
5+
import java.time.Instant;
6+
7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
10+
import org.springframework.security.oauth2.jwt.JwtDecoder;
11+
import org.springframework.security.oauth2.jwt.JwtEncoder;
12+
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
13+
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
14+
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
15+
import org.springframework.stereotype.Service;
16+
17+
import com.nimbusds.jose.jwk.JWK;
18+
import com.nimbusds.jose.jwk.JWKSet;
19+
import com.nimbusds.jose.jwk.RSAKey;
20+
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
21+
import com.nimbusds.jose.jwk.source.JWKSource;
22+
import com.nimbusds.jose.proc.SecurityContext;
23+
24+
import dev.findfirst.users.repository.UserRepo;
25+
import dev.findfirst.users.model.user.URole;
26+
import dev.findfirst.security.userauth.utils.Constants;
27+
28+
import lombok.RequiredArgsConstructor;
29+
30+
@Service
31+
@RequiredArgsConstructor
32+
public class TokenService {
33+
34+
@Value("${findfirst.app.jwtExpirationMs}")
35+
private int jwtExpirationMs;
36+
37+
@Value("${jwt.public.key}")
38+
private RSAPublicKey key;
39+
40+
@Value("${jwt.private.key}")
41+
private RSAPrivateKey priv;
42+
43+
private final UserRepo userRepo;
44+
45+
46+
public String generateTokenFromUser(int userId) {
47+
Instant now = Instant.now();
48+
var user = userRepo.findById(userId).orElseThrow();
49+
String email = user.getEmail();
50+
Integer roleId = user.getRole().getId();
51+
var roleName = URole.values()[roleId].toString();
52+
JwtClaimsSet claims = JwtClaimsSet.builder().issuer("self").issuedAt(Instant.now())
53+
.expiresAt(now.plusSeconds(jwtExpirationMs)).subject(email).claim("scope", email)
54+
.claim(Constants.ROLE_ID_CLAIM, roleId).claim(Constants.ROLE_NAME_CLAIM, roleName)
55+
.claim("userId", userId).build();
56+
return jwtEncoder().encode(JwtEncoderParameters.from(claims)).getTokenValue();
57+
}
58+
59+
60+
JwtEncoder jwtEncoder() {
61+
JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build();
62+
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
63+
return new NimbusJwtEncoder(jwks);
64+
}
65+
66+
// private String extractUserId(Authentication authentication) {
67+
// if (authentication.getPrincipal() instanceof UserDetails) {
68+
// String details = ((UserDetails) authentication.getPrincipal()).getUsername();
69+
// System.out.println("If details " + details);
70+
// return details;
71+
// } else if (authentication.getPrincipal() instanceof DefaultOAuth2User) {
72+
// DefaultOAuth2User oAuth2User = (DefaultOAuth2User)
73+
// authentication.getPrincipal();
74+
// String details = oAuth2User.getAttribute("id");
75+
// System.out.println("Else details " + details);
76+
// return details;
77+
// }
78+
// return null;
79+
// }
80+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package dev.findfirst.security.oauth2client;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
6+
import java.rmi.UnexpectedException;
7+
import java.util.Collections;
8+
import java.util.HashMap;
9+
import java.util.Map;
10+
import java.util.UUID;
11+
12+
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
13+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
14+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
15+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
16+
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
17+
import org.springframework.security.oauth2.core.user.OAuth2User;
18+
import org.springframework.stereotype.Service;
19+
import org.springframework.transaction.annotation.Transactional;
20+
21+
import dev.findfirst.security.userauth.models.payload.request.SignupRequest;
22+
import dev.findfirst.users.exceptions.EmailAlreadyRegisteredException;
23+
import dev.findfirst.users.exceptions.UserNameTakenException;
24+
import dev.findfirst.users.repository.UserRepo;
25+
import dev.findfirst.users.service.UserManagementService;
26+
import dev.findfirst.users.model.user.URole;
27+
import dev.findfirst.users.model.user.User;
28+
import org.springframework.security.core.GrantedAuthority;
29+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
30+
31+
@Service
32+
@RequiredArgsConstructor
33+
@Slf4j
34+
public class OauthUserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
35+
36+
final UserRepo userRepo;
37+
final UserManagementService ums;
38+
39+
@Transactional
40+
@Override
41+
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
42+
OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
43+
OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest);
44+
User user = null;
45+
46+
// user exists in database by email
47+
var attrs = oAuth2User.getAttributes();
48+
var email = (String) attrs.get("email");
49+
var username = (String) attrs.get("login");
50+
String registrationId = userRequest.getClientRegistration().getClientId();
51+
if (email != null && !email.isEmpty()) {
52+
log.debug("attempt login with email {}", email);
53+
// user = userRepo.findByEmail(email).or()
54+
} else if (username != null && !username.isEmpty()) {
55+
log.debug("looking up if user exist with username {}", username);
56+
var userOpt = userRepo.findByUsername(username);
57+
58+
var oauth2PlaceholderEmail = username + registrationId;
59+
if (userOpt.isEmpty()) {
60+
try {
61+
log.debug("creating a new user for oauth2");
62+
user = ums
63+
.createNewUserAccount(new SignupRequest(username, oauth2PlaceholderEmail, UUID.randomUUID().toString()));
64+
} catch (UnexpectedException | UserNameTakenException | EmailAlreadyRegisteredException e) {
65+
log.debug("errors occured: {}", e.getMessage());
66+
}
67+
} else {
68+
user = userOpt.get();
69+
}
70+
}
71+
if (user.getUserId() != null) {
72+
GrantedAuthority authority = new SimpleGrantedAuthority(URole.values()[user.getRole().getId()].toString());
73+
String userNameAttributeName = userRequest
74+
.getClientRegistration()
75+
.getProviderDetails()
76+
.getUserInfoEndpoint()
77+
.getUserNameAttributeName();
78+
log.debug("USER ATTRIBUTE NAME: {}", userNameAttributeName);
79+
var attributes = customAttribute(attrs, userNameAttributeName, user.getUserId(), registrationId);
80+
return new DefaultOAuth2User(Collections.singletonList(authority), attributes, userNameAttributeName);
81+
}
82+
83+
return oAuth2User;
84+
}
85+
86+
private Map<String, Object> customAttribute(
87+
Map<String, Object> attributes,
88+
String userNameAttributeName,
89+
int userID,
90+
String registrationId) {
91+
Map<String, Object> customAttribute = new HashMap<>();
92+
customAttribute.put(userNameAttributeName, attributes.get(userNameAttributeName));
93+
customAttribute.put("provider", registrationId);
94+
customAttribute.put("userID", userID);
95+
return customAttribute;
96+
}
97+
98+
}

0 commit comments

Comments
 (0)