package com.aet.timesheets.common.security.config;
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.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
@EnableWebSecurity
public class SecurityFilterChainConfig {
@Autowired
private JwtFilter jwtFilter;
@Value("#{'${security.exclude.paths}'.split(',')}")
private List<String> excludedPaths;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
excludedPaths = excludedPaths.stream().map(String::trim).toList();
http
.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.authorizeHttpRequests(auth -> auth.requestMatchers(excludedPaths.toArray(new String[0])).permitAll()
.anyRequest().authenticated())
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
//config.setAllowedOriginPatterns(List.of("*"));
config.setAllowedOrigins(List.of(
"https://app.dev.timemate-nonprod.aeturnum.net",
"https://app.qa.timemate-nonprod.aeturnum.net",
"https://app.stg.timemate-nonprod.aeturnum.net",
"https://app.timemate.aeturnum.com",
"http://localhost:3000"
));
config.setAllowedMethods(List.of("GET", "POST", "DELETE", "PUT", "PATCH", "HEAD", "OPTIONS"));
config.setAllowedHeaders(List.of(
"Access-Control-Allow-Headers", "Origin", "Accept", "X-Requested-With",
"Content-Type", "Access-Control-Request-Method", "Access-Control-Request-Headers",
"Access-Control-Allow-Headers", "Authorization", "TENANT_ID"));
config.setAllowCredentials(true); // Optional: enable if cookies are used
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
package com.aet.timesheets.common.security.config;
import com.aet.timesheets.common.dto.response.CommonErrorResponseDTO;
import com.aet.timesheets.common.exception.UnauthorizedException;
import com.aet.timesheets.common.security.service.dto.AuthUserDetails;
import com.aet.timesheets.common.security.service.impl.InternalServiceImpl;
import com.aet.timesheets.common.security.service.impl.JwtManagerServiceImpl;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import static com.aet.timesheets.common.utils.Constant.*;
@Component
@Slf4j
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private JwtManagerServiceImpl jwtManagerService;
@Autowired
private InternalServiceImpl internalService;
@Value("#{'${security.exclude.paths}'.split(',')}")
private List<String> excludedPaths;
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
request.setAttribute(USE_LOGIN_USER_TOKEN, TRUE);
request.setAttribute(TENANT_ID, request.getHeader(TENANT_ID));
request.setAttribute(X_REQUEST_ID, request.getHeader(X_REQUEST_ID));
request.setAttribute(APP_ID, UUID.randomUUID().toString());
String path = request.getServletPath();
boolean shouldSkip = excludedPaths.stream()
.anyMatch(excludedPath -> new AntPathMatcher().match(excludedPath.trim(), path));
log.debug("JwtFilter - shouldNotFilter for path '{}': {}", path, shouldSkip);
return shouldSkip;
}
@Override
protected void doFilterInternal(
final @NotNull HttpServletRequest request,
final @NotNull HttpServletResponse response,
final @NotNull FilterChain filterChain) throws IOException {
try {
String token = extractJwt(request);
String tenantId = request.getHeader(TENANT_ID);
final AuthUserDetails authUserDetails = jwtManagerService.validateToken(token);
final UserDetails userDetails = internalService.handleAuthUserDetails(authUserDetails, tenantId);
final UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails, token, userDetails.getAuthorities());
request.setAttribute(TENANT_ID, tenantId);
request.setAttribute(TOKEN, token);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
log.info("Request: method=JwtFilter.doFilterInternal, requestId={}, appId={}, user={}",
request.getAttribute(X_REQUEST_ID),
request.getAttribute(APP_ID),
authUserDetails.getEmail());
filterChain.doFilter(request, response);
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
CommonErrorResponseDTO repose = new CommonErrorResponseDTO();
repose.setTimestamp(System.currentTimeMillis());
repose.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
repose.setErrors(List.of("User Not Authorized.Error: " + e));
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(repose));
}
}
private String extractJwt(final HttpServletRequest request) {
final String authHeader = request.getHeader(AUTHORIZATION);
String jwt = null;
if (authHeader != null && authHeader.startsWith(BEARER)) {
jwt = authHeader.replaceFirst(BEARER, "").trim();
}
if (jwt == null || jwt.isBlank()) {
throw new UnauthorizedException("Auth Token Not Found");
}
return jwt;
}
}
..
package com.aet.timesheets.common.security.config;
import com.aet.timesheets.common.exception.InternalErrorException;
import com.nimbusds.jose.JWSAlgorithm;
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.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.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
@Configuration
public class JwtManagementSecurityConfig {
private static final String INTERNAL_SIGNING_KEY_ID = "gateway-signing-key";
@Value("${jwt.signing.key.private}")
private String privateKeyPem;
@Value("${jwt.signing.key.public}")
private String publicKeyPem;
/**
* Provides access to the internal signing key for JWT processor configuration.
*
* @return The internal RSA signing key
*/
@Bean("internalSigningKey")
public RSAKey internalSigningKey() {
// Load from PEM format
final RSAPrivateKey privateKey = loadPrivateKeyFromPem(privateKeyPem);
final RSAPublicKey publicKey = loadPublicKeyFromPem(publicKeyPem);
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(INTERNAL_SIGNING_KEY_ID)
.build();
}
/**
* In the core module extra security flow has been handled.
* Currently, this bean is used only in core module.
*
* @return Configured JWT processor for internal tokens
*/
@Bean
public ConfigurableJWTProcessor<SecurityContext> configurableJWTProcessor(
final RSAKey internalSigningKey) {
try {
final JWKSource<SecurityContext> keySource =
new ImmutableJWKSet<>(new JWKSet(internalSigningKey));
final JWSKeySelector<SecurityContext> keySelector =
new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, keySource);
final ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(keySelector);
return jwtProcessor;
} catch (Exception e) {
throw new InternalErrorException("Failed to configure JWT processor", e);
}
}
private RSAPrivateKey loadPrivateKeyFromPem(String privateKeyPem) {
try {
final String normalizedPrivateKey = privateKeyPem.replace("\\n", "\n");
final String cleanKey = normalizedPrivateKey
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace("-----BEGIN RSA PRIVATE KEY-----", "")
.replace("-----END RSA PRIVATE KEY-----", "")
.replaceAll("\\s", "");
final byte[] keyBytes = Base64.getDecoder().decode(cleanKey);
final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
final KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new InternalErrorException("Failed to load private key from PEM", e);
}
}
private RSAPublicKey loadPublicKeyFromPem(String publicKeyPem) {
try {
final String normalizedPublicKey = publicKeyPem.replace("\\n", "\n");
final String cleanKey = normalizedPublicKey
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replace("-----BEGIN RSA PUBLIC KEY-----", "")
.replace("-----END RSA PUBLIC KEY-----", "")
.replaceAll("\\s", "");
final byte[] keyBytes = Base64.getDecoder().decode(cleanKey);
final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
final KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPublicKey) keyFactory.generatePublic(keySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new InternalErrorException("Failed to load public key from PEM", e);
}
}
}
package com.aet.timesheets.common.security.service.dto;
import com.aet.timesheets.common.utils.enums.Region;
import com.aet.timesheets.common.utils.enums.Status;
import com.aet.timesheets.common.utils.enums.UserRole;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@Builder
@ToString
public class AuthUserDetails implements UserDetails {
private String username;
private String password;
private String subId;
private String id; // time mate ID
private String email;
private String firstName;
private String lastName;
private String designation;
private UserRole role;
private Status status;
private Region region;
private String azureADId;
private String tenantId;
private String tenantName;
private boolean isInternalIssuer;
// private List<String> cognitoGroups;
public AuthUserDetails(String username, String password, UserRole role) {
this.username = username;
this.password = password;
this.role = role;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(role);
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
,,
Comments
Post a Comment