add spring security and token management

This commit is contained in:
dirgantarasiahaan 2023-05-23 19:42:51 +07:00
parent 597d4062c7
commit c76c01a174
35 changed files with 1095 additions and 53 deletions

67
pom.xml
View File

@ -17,6 +17,59 @@
<java.version>11</java.version> <java.version>11</java.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.5.4</version>
<exclusions>
<exclusion>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.5.2</version>
<exclusions>
<exclusion>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.3.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.3.9.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.19</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId> <artifactId>spring-boot-starter-data-jpa</artifactId>
@ -34,20 +87,16 @@
<artifactId>brave</artifactId> <artifactId>brave</artifactId>
<version>5.16.0</version> <version>5.16.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>
<version>3.12.0</version> <version>3.12.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId> <artifactId>spring-boot-devtools</artifactId>

View File

@ -0,0 +1,33 @@
package com.iconplus.smartproc.configuration;
import com.iconplus.smartproc.model.projection.UserRoleView;
import com.iconplus.smartproc.repository.RolesRepository;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final RolesRepository rolesRepository;
public CustomUserDetailsService(RolesRepository rolesRepository) {
this.rolesRepository = rolesRepository;
}
@Override
public UserDetails loadUserByUsername(String username) {
List<SimpleGrantedAuthority> authorities = null;
Optional<UserRoleView> userRole = rolesRepository.getUserRoleByUserId(username);
if (userRole.isPresent()) {
authorities=List.of(new SimpleGrantedAuthority(userRole.get().getRole()));
}
return new org.springframework.security.core.userdetails.User(username, username, authorities);
}
}

View File

@ -0,0 +1,39 @@
package com.iconplus.smartproc.configuration;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.iconplus.smartproc.exception.ErrorResponse;
import com.iconplus.smartproc.util.Constants;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@Log4j2
public class JwtAuthenticationAccessDenied implements AccessDeniedHandler {
private static final String DEFAULT_CODE = "30000";
private static final String DEFAULT_MESSAGE = "Access denied";
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException {
log.error("Access denied, {}", e.getMessage());
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setCode(DEFAULT_CODE);
errorResponse.setTitle(Constants.TITLE_INVALID_NEXT_STEP);
errorResponse.setMessage(DEFAULT_MESSAGE);
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpServletResponse.getOutputStream()
.println(new ObjectMapper().writeValueAsString(errorResponse));
}
}

View File

@ -0,0 +1,54 @@
package com.iconplus.smartproc.configuration;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.iconplus.smartproc.exception.ErrorResponse;
import com.iconplus.smartproc.util.Constants;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
ErrorResponse errorResponse = new ErrorResponse();
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
var exception = (Exception) request.getAttribute("exception");
String message;
if (exception != null) {
if (exception.getCause() != null) {
message = exception.getCause().toString() + " " + exception.getMessage();
} else {
message = exception.getMessage();
}
errorResponse.setCode(Constants.ERR_CODE_40051);
errorResponse.setTitle(Constants.TITLE_INVALID_NEXT_STEP);
errorResponse.setMessage(message);
response.getOutputStream()
.println(new ObjectMapper().writeValueAsString(errorResponse));
} else {
errorResponse.setCode(Constants.ERR_CODE_80007);
errorResponse.setTitle(Constants.TITLE_INVALID_NEXT_STEP);
errorResponse.setMessage("Invalid Access Token");
response.getOutputStream()
.println(new ObjectMapper().writeValueAsString(errorResponse));
}
}
}

View File

@ -0,0 +1,79 @@
package com.iconplus.smartproc.configuration;
import com.iconplus.smartproc.exception.BusinessException;
import com.iconplus.smartproc.model.request.PostAccessTokenRequest;
import com.iconplus.smartproc.service.authentication.PostCheckAccessTokenService;
import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
private final JwtTokenUtil jwtTokenUtil;
private final PostCheckAccessTokenService postCheckAccessTokenService;
public JwtRequestFilter(JwtTokenUtil jwtTokenUtil,
PostCheckAccessTokenService postCheckAccessTokenService) {
this.jwtTokenUtil = jwtTokenUtil;
this.postCheckAccessTokenService = postCheckAccessTokenService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
try {
String jwtToken = extractJwtFromRequest(request);
if (StringUtils.hasText(jwtToken) && jwtTokenUtil.validateTokenOnly(jwtToken)) {
isValidToken(request, jwtToken);
UserDetails userDetails = new org.springframework.security.core.userdetails.User(jwtTokenUtil.getUsernameFromToken(jwtToken), "",
jwtTokenUtil.getRolesFromToken(jwtToken));
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
} catch(ExpiredJwtException | BadCredentialsException ex)
{
request.setAttribute("exception", ex);
}
chain.doFilter(request, response);
}
private void isValidToken(HttpServletRequest request, String jwtToken) {
String requestUrl = request.getRequestURI();
String refreshTokenUrl = "/authentication-service/authentication/v1/refresh-token";
if (!refreshTokenUrl.equals(requestUrl)) {
var isValid = isValidAuthenticateToken(jwtToken);
if (!isValid) {
throw new BusinessException(HttpStatus.UNAUTHORIZED, "Invalid Access Token");
}
}
}
private boolean isValidAuthenticateToken(String jwtToken) {
return postCheckAccessTokenService.execute(PostAccessTokenRequest.builder()
.accessToken(jwtToken)
.build()).getIsValid();
}
private String extractJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}

View File

@ -0,0 +1,91 @@
package com.iconplus.smartproc.configuration;
import com.iconplus.smartproc.model.token.TokenContent;
import io.jsonwebtoken.*;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.security.KeyPair;
import java.util.*;
import java.util.function.Function;
@Component
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -2550185165626007488L;
@Autowired
private KeyPair keyPair;
//retrieve expiration date from jwt token
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
//for retrieveing any information from token we will need the secret key
public Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(keyPair.getPublic()).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public String generateToken(String username, TokenContent tokenContent, Integer expirationInMs, String channel, String scopeType) {
Map<String, Object> claims = new HashMap<>();
claims.put("authorities", tokenContent.getAccessMenu());
claims.put("fullname", tokenContent.getFullname());
claims.put("username", tokenContent.getUsername());
claims.put("user_id", tokenContent.getUserId());
claims.put("role", tokenContent.getRole());
return doGenerateToken(claims, username, expirationInMs);
}
public String doGenerateToken(Map<String, Object> claims, String subject, Integer expirationInMs) {
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationInMs))
.signWith(SignatureAlgorithm.RS256, keyPair.getPrivate()).compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
public boolean validateTokenOnly(String authToken) {
try {
Jwts.parser().setSigningKey(keyPair.getPublic()).parseClaimsJws(authToken);
return true;
} catch (SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException ex) {
throw new BadCredentialsException("INVALID_CREDENTIALS", ex);
} catch (ExpiredJwtException ex) {
throw ex;
}
}
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser().setSigningKey(keyPair.getPublic()).parseClaimsJws(token).getBody();
return claims.getSubject();
}
@SuppressWarnings("squid:S4834")
public List<SimpleGrantedAuthority> getRolesFromToken(String token) {
Claims claims = Jwts.parser().setSigningKey(keyPair.getPublic()).parseClaimsJws(token).getBody();
List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
ArrayList<String> authorities = claims.get("authorities", ArrayList.class);
authorities.forEach(c-> authorityList.add(new SimpleGrantedAuthority(c)));
return authorityList;
}
}

View File

@ -0,0 +1,30 @@
package com.iconplus.smartproc.configuration;
import com.iconplus.smartproc.util.RSAUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
@Configuration
public class KeyConfiguration {
@Value("${jwt.private-key}")
private String privateKey;
@Value("${jwt.public-key}")
private String publicKey;
@Bean
public KeyPair keypairBean() {
PublicKey pubKey = RSAUtil.getPublicKey(publicKey);
PrivateKey privKey = RSAUtil.getPrivateKey(privateKey);
return new KeyPair(pubKey, privKey);
}
}

View File

@ -0,0 +1,57 @@
package com.iconplus.smartproc.configuration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private JwtAuthenticationAccessDenied jwtAuthenticationAccessDenied;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.sessionFixation().none().and()
.csrf().disable();
httpSecurity.authorizeRequests()
.antMatchers("/api*/**").permitAll()
.antMatchers("/actuator/health").permitAll()
.antMatchers("/swagger*/**").permitAll()
.antMatchers("/v2*/**").permitAll()
.antMatchers("/token/jwks.json").permitAll()
.anyRequest().authenticated().and()
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAuthenticationAccessDenied);
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/v2/api-docs",
"/configuration/ui",
"/swagger-resources/**",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**");
}
}

View File

@ -45,7 +45,7 @@ public class UsersController {
.orElseThrow(() -> new ResourceNotFoundException("Users not exist with id :" + id)); .orElseThrow(() -> new ResourceNotFoundException("Users not exist with id :" + id));
users.setUsername(usersDetails.getUsername()); users.setUsername(usersDetails.getUsername());
users.setNama(usersDetails.getNama()); users.setFullname(usersDetails.getFullname());
users.setEmail(usersDetails.getEmail()); users.setEmail(usersDetails.getEmail());
users.setPassword(usersDetails.getPassword()); users.setPassword(usersDetails.getPassword());
// users.setInstansi(usersDetails.getInstansi()); // users.setInstansi(usersDetails.getInstansi());

View File

@ -1,28 +0,0 @@
package com.iconplus.smartproc.exception;
import java.util.Date;
public class ErrorDetails {
private Date timestamp;
private String message;
private String details;
public ErrorDetails(Date timestamp, String message, String details) {
super();
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
public Date getTimestamp() {
return timestamp;
}
public String getMessage() {
return message;
}
public String getDetails() {
return details;
}
}

View File

@ -5,6 +5,7 @@ import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext; import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity; import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.context.Context;
import org.apache.velocity.runtime.resource.loader.StringResourceLoader; import org.apache.velocity.runtime.resource.loader.StringResourceLoader;
import org.apache.velocity.runtime.resource.util.StringResourceRepository; import org.apache.velocity.runtime.resource.util.StringResourceRepository;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -88,7 +89,7 @@ public class ErrorHelper {
.getApplicationAttribute(StringResourceLoader.REPOSITORY_NAME_DEFAULT); .getApplicationAttribute(StringResourceLoader.REPOSITORY_NAME_DEFAULT);
resourceRepository.putStringResource(RAW_TEMPLATE, rawNotificationTemplate); resourceRepository.putStringResource(RAW_TEMPLATE, rawNotificationTemplate);
return new VelocityContext(parameters); return new VelocityContext((Context) parameters);
} }
private void addVelocityProperties() { private void addVelocityProperties() {

View File

@ -0,0 +1,53 @@
package com.iconplus.smartproc.helper.service;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.Payload;
import lombok.extern.log4j.Log4j2;
import net.minidev.json.JSONObject;
import org.apache.commons.lang3.StringUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Log4j2
public class TokenUtils {
private TokenUtils() {
}
public static Map<String, String> decodeToken(String jwtToken) {
Map<String, String> body = new HashMap<>();
try {
if (jwtToken != null) {
jwtToken = jwtToken.replace("Bearer", "");
jwtToken = jwtToken.trim();
}
if (StringUtils.isBlank(jwtToken)) {
return body;
}
JWSObject token = JWSObject.parse(jwtToken);
Payload tokenPayload = token.getPayload();
JSONObject tokenBody = tokenPayload.toJSONObject();
tokenBody.forEach((key, value) -> {
if (Objects.isNull(value)) {
value = "";
}
body.put(key, value.toString());
});
} catch (Exception e) {
log.error("Failed to parse JWT Token. Error: {}", e.getMessage());
}
return body;
}
public static Object getValueByParam(String param, String token) {
return decodeToken(token).get(param);
}
}

View File

@ -0,0 +1,52 @@
package com.iconplus.smartproc.model.entity;
import com.iconplus.smartproc.helper.base.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.GeneratorType;
import org.hibernate.annotations.Type;
import javax.persistence.*;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "permission")
public class Menu extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "subMenu")
private String subMenu;
@Column(name = "menu")
private String menu;
// @Column(name = "can_view")
// @Type(type = "org.hibernate.type.NumericBooleanType")
// private Boolean canView;
//
// @Column(name = "can_read")
// @Type(type = "org.hibernate.type.NumericBooleanType")
// private Boolean canRead;
//
// @Column(name = "can_create")
// @Type(type = "org.hibernate.type.NumericBooleanType")
// private Boolean canCreate;
//
// @Column(name = "can_delete")
// @Type(type = "org.hibernate.type.NumericBooleanType")
// private Boolean canDelete;
@Column(name = "deleted")
@Type(type = "org.hibernate.type.NumericBooleanType")
private Boolean deleted;
}

View File

@ -0,0 +1,41 @@
package com.iconplus.smartproc.model.entity;
import com.iconplus.smartproc.helper.base.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Type;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "permission")
public class Permission extends BaseEntity {
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(
name = "UUID",
strategy = "org.hibernate.id.UUIDGenerator"
)
@Column(name = "id")
private String id;
@Column(name = "role_id")
private Long roleId;
@Column(name = "role_id")
private Long menuId;
@Column(name = "deleted")
@Type(type = "org.hibernate.type.NumericBooleanType")
private Boolean deleted;
}

View File

@ -0,0 +1,51 @@
package com.iconplus.smartproc.model.entity;
import com.iconplus.smartproc.helper.base.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Type;
import javax.persistence.*;
import java.sql.Timestamp;
@Data
@Builder
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "token_management")
public class TokenManagement extends BaseEntity {
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(
name = "UUID",
strategy = "org.hibernate.id.UUIDGenerator"
)
@Column(name = "id")
private String id;
@Column(name = "user_id")
private String userId;
@Lob
@Column(name = "access_token")
private String accessToken;
@Lob
@Column(name = "refresh_token")
private String refreshToken;
@Column(name = "issued_time")
private Timestamp issuedTime;
@Column(name = "expired_time")
private Timestamp expiredTime;
@Column(name = "deleted")
@Type(type = "org.hibernate.type.NumericBooleanType")
private Boolean deleted;
}

View File

@ -20,8 +20,8 @@ public class Users extends BaseEntity {
@Column(name = "username") @Column(name = "username")
private String username; private String username;
@Column(name = "nama") @Column(name = "fullname")
private String nama; private String fullname;
@Column(name = "email") @Column(name = "email")
private String email; private String email;

View File

@ -0,0 +1,16 @@
package com.iconplus.smartproc.model.projection;
import java.sql.Clob;
public interface TokenManagementView {
String getId();
void setId(String id);
String getUserId();
void setUserId(String userId);
Clob getAccessToken();
void setAccessToken(Clob accessToken);
}

View File

@ -0,0 +1,19 @@
package com.iconplus.smartproc.model.projection;
public interface UserRoleView {
Long getId();
String getUsername();
String getPassword();
Long getRoleId();
String getRole();
Boolean getDeleted();
void setId(Long id);
void setUsername(String username);
void setPassword(String password);
void setRoleId(Long roleId);
void setRole(String role);
void setDeleted(Boolean deleted);
}

View File

@ -0,0 +1,17 @@
package com.iconplus.smartproc.model.request;
import com.iconplus.smartproc.helper.base.BaseRequest;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginRequest extends BaseRequest {
private String email;
private String password;
}

View File

@ -0,0 +1,16 @@
package com.iconplus.smartproc.model.request;
import com.iconplus.smartproc.helper.base.BaseRequest;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PostAccessTokenRequest extends BaseRequest {
private String accessToken;
}

View File

@ -0,0 +1,26 @@
package com.iconplus.smartproc.model.response;
import com.iconplus.smartproc.helper.base.BaseResponse;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Set;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class LoginResponse extends BaseResponse {
private String accessToken;
private String validity;
private String refreshToken;
private String username;
private String email;
private Long roleId;
private String role;
Set<String> accessMenu;
}

View File

@ -0,0 +1,16 @@
package com.iconplus.smartproc.model.response;
import com.iconplus.smartproc.helper.base.BaseResponse;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PostAccessTokenResponse extends BaseResponse {
private Boolean isValid;
}

View File

@ -0,0 +1,16 @@
package com.iconplus.smartproc.model.token;
import lombok.Builder;
import lombok.Data;
import java.util.Set;
@Builder
@Data
public class TokenContent {
private String userId;
private String role;
private Set<String> accessMenu;
private String fullname;
private String username;
}

View File

@ -1,10 +1,26 @@
package com.iconplus.smartproc.repository; package com.iconplus.smartproc.repository;
import com.iconplus.smartproc.model.entity.Roles; import com.iconplus.smartproc.model.entity.Roles;
import com.iconplus.smartproc.model.projection.UserRoleView;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository @Repository
public interface RolesRepository extends JpaRepository<Roles, Long> { public interface RolesRepository extends JpaRepository<Roles, Long> {
@Query(value = "SELECT u.id as id, " +
"u.username as username, " +
"u.password as password, " +
"u.roleId as roleId, " +
"r.role as role " +
"FROM Users u " +
"join Roles r on r.id=u.roleId " +
"WHERE u.deleted = false " +
"AND r.deleted = false " +
"AND u.username = :userName ")
Optional<UserRoleView> getUserRoleByUserId(String userName);
} }

View File

@ -0,0 +1,19 @@
package com.iconplus.smartproc.repository;
import com.iconplus.smartproc.model.entity.TokenManagement;
import com.iconplus.smartproc.model.projection.TokenManagementView;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface TokenManagementRepository extends JpaRepository<TokenManagement, String> {
@Query(value = "SELECT access_token as accessToken FROM token_management " +
"WHERE user_id = :userId " +
"AND deleted = false " +
"AND expired_time >= SYSDATE", nativeQuery = true)
List<TokenManagementView> findAccessTokenByUserIdAndDeletedFalse(String userId);
}

View File

@ -4,7 +4,11 @@ import com.iconplus.smartproc.model.entity.Users;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository @Repository
public interface UsersRepository extends JpaRepository<Users, Long> { public interface UsersRepository extends JpaRepository<Users, Long> {
Optional<Users> findByEmailAndDeletedFalse(String email);
} }

View File

@ -0,0 +1,47 @@
package com.iconplus.smartproc.service.authentication;
import com.iconplus.smartproc.exception.BusinessException;
import com.iconplus.smartproc.helper.service.BaseService;
import com.iconplus.smartproc.model.request.LoginRequest;
import com.iconplus.smartproc.model.response.LoginResponse;
import com.iconplus.smartproc.repository.UsersRepository;
import com.iconplus.smartproc.util.Constants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class LoginService implements BaseService<LoginRequest, LoginResponse> {
private final UsersRepository usersRepository;
public LoginService(UsersRepository userRepository) {
this.usersRepository = userRepository;
}
@Override
public LoginResponse execute(LoginRequest input) {
var user= usersRepository.findByEmailAndDeletedFalse(input.getEmail())
.orElseThrow(() -> new BusinessException(HttpStatus.CONFLICT,
Constants.ERR_CODE_10003,
Constants.ERR_TTL_10003,
String.format(Constants.ERR_MSG_10003, input.getEmail())));
if (!StringUtils.equalsIgnoreCase(input.getPassword(), user.getPassword())) {
throw new BusinessException(HttpStatus.CONFLICT,
Constants.ERR_CODE_10004,
Constants.ERR_TTL_10004,
Constants.ERR_MSG_10004);
}
return LoginResponse.builder()
.build();
}
}

View File

@ -0,0 +1,55 @@
package com.iconplus.smartproc.service.authentication;
import com.iconplus.smartproc.helper.service.BaseService;
import com.iconplus.smartproc.helper.service.TokenUtils;
import com.iconplus.smartproc.model.projection.TokenManagementView;
import com.iconplus.smartproc.model.request.PostAccessTokenRequest;
import com.iconplus.smartproc.model.response.PostAccessTokenResponse;
import com.iconplus.smartproc.repository.TokenManagementRepository;
import com.iconplus.smartproc.util.CommonUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Slf4j
public class PostCheckAccessTokenService implements BaseService<PostAccessTokenRequest, PostAccessTokenResponse> {
private TokenManagementRepository tokenManagementRepository;
public PostCheckAccessTokenService(TokenManagementRepository tokenManagementRepository) {
this.tokenManagementRepository = tokenManagementRepository;
}
@Override
public PostAccessTokenResponse execute(PostAccessTokenRequest input) {
var decodeToken = TokenUtils.decodeToken(input.getAccessToken());
String userId = decodeToken.get("user_id");
List<TokenManagementView> tokenManagementViews = tokenManagementRepository.findAccessTokenByUserIdAndDeletedFalse(userId);
if (tokenManagementViews.isEmpty()) {
log.error("access token not found in db");
return PostAccessTokenResponse.builder()
.isValid(false)
.build();
}
List<String> userTokenList = tokenManagementViews.stream()
.map(c-> CommonUtil.clobToString(c.getAccessToken()))
.collect(Collectors.toList());
boolean isMatch = userTokenList.stream().anyMatch(s -> s.equals(input.getAccessToken()));
if (isMatch) {
return PostAccessTokenResponse.builder()
.isValid(true)
.build();
}
return PostAccessTokenResponse.builder()
.isValid(false)
.build();
}
}

View File

@ -0,0 +1,25 @@
package com.iconplus.smartproc.util;
import com.iconplus.smartproc.exception.TechnicalException;
import java.sql.Clob;
import java.sql.SQLException;
public class CommonUtil {
private CommonUtil() {
}
public static String clobToString(Clob input) {
try {
if (input != null) {
return input.getSubString(1, (int) input.length());
} else {
return null;
}
} catch (SQLException exception) {
throw new TechnicalException(Constants.ERR_TTL_40041,
Constants.ERR_TTL_40041,
Constants.ERR_MSG_40041);
}
}
}

View File

@ -12,4 +12,21 @@ public class Constants {
public static final String ERR_TTL_10002 = "Data tersedia"; public static final String ERR_TTL_10002 = "Data tersedia";
public static final String ERR_MSG_10002 = "Jenis Anggaran dengan id : %s sudah tersedia"; public static final String ERR_MSG_10002 = "Jenis Anggaran dengan id : %s sudah tersedia";
public static final String ERR_CODE_10003 = "10003";
public static final String ERR_TTL_10003 = "Data tidak tersedia";
public static final String ERR_MSG_10003 = "User dengan email : %s tidak ditemukan";
public static final String ERR_CODE_10004 = "10004";
public static final String ERR_TTL_10004 = "Gagal Authentikasi User";
public static final String ERR_MSG_10004 = "Silahkan Periksa kembali Email dan Password Anda";
public static final String ERR_CODE_40041 = "40041";
public static final String ERR_TTL_40041 = "Terjadi Gangguan";
public static final String ERR_MSG_40041 = "Masalah Koneksi System";
public static final String ERR_CODE_40051 = "40051";
public static final String ERR_CODE_80007 = "80007";
public static final String TITLE_INVALID_NEXT_STEP = "Proses tidak dapat dilanjutkan";
} }

View File

@ -0,0 +1,109 @@
package com.iconplus.smartproc.util;
import com.iconplus.smartproc.exception.TechnicalException;
import lombok.extern.log4j.Log4j2;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
@Log4j2
public class RSAUtil {
private RSAUtil(){
}
public static PrivateKey getPrivateKey(String base64PrivateKey){
PrivateKey privateKey = null;
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(base64PrivateKey.getBytes()));
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
privateKey = keyFactory.generatePrivate(keySpec);
} catch (InvalidKeySpecException e) {
log.error("InvalidKeySpecException : "+e.getMessage());
throw new TechnicalException("80000", "InvalidKeySpecException : "+e.getMessage());
} catch (NoSuchAlgorithmException e) {
log.error("NoSuchAlgorithmException : "+e.getMessage());
throw new TechnicalException("80000", "NoSuchAlgorithmException : "+e.getMessage());
}
return privateKey;
}
public static PublicKey getPublicKey(String base64PublicKey){
PublicKey publicKey = null;
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(base64PublicKey.getBytes()));
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
publicKey = keyFactory.generatePublic(keySpec);
} catch (InvalidKeySpecException e) {
log.error("InvalidKeySpecException : "+e.getMessage());
throw new TechnicalException("80000", "InvalidKeySpecException : "+e.getMessage());
} catch (NoSuchAlgorithmException e) {
log.error("NoSuchAlgorithmException : "+e.getMessage());
throw new TechnicalException("80000", "NoSuchAlgorithmException : "+e.getMessage());
}
return publicKey;
}
private static String decryptChipper(byte[] data, PrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPPadding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(cipher.doFinal(data));
} catch (NoSuchAlgorithmException e) {
log.error("NoSuchAlgorithmException : "+e.getMessage());
throw new TechnicalException("80000", "NoSuchAlgorithmException : "+e.getMessage());
} catch (NoSuchPaddingException e) {
log.error("NoSuchPaddingException : "+e.getMessage());
throw new TechnicalException("80000", "NoSuchPaddingException : "+e.getMessage());
} catch (InvalidKeyException e) {
log.error("InvalidKeyException : "+e.getMessage());
throw new TechnicalException("80000", "InvalidKeyException : "+e.getMessage());
} catch (IllegalBlockSizeException e) {
log.error("IllegalBlockSizeException : "+e.getMessage());
throw new TechnicalException("80000", "IllegalBlockSizeException : "+e.getMessage());
} catch (BadPaddingException e) {
log.error("BadPaddingException : "+e.getMessage());
throw new TechnicalException("80000", "BadPaddingException : "+e.getMessage());
}
}
private static byte[] encryptChipper(byte[] data, PublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPPadding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data);
} catch (NoSuchAlgorithmException e) {
log.error("NoSuchAlgorithmException : "+e.getMessage());
throw new TechnicalException("80000", "NoSuchAlgorithmException : "+e.getMessage());
} catch (NoSuchPaddingException e) {
log.error("NoSuchPaddingException : "+e.getMessage());
throw new TechnicalException("80000", "NoSuchPaddingException : "+e.getMessage());
} catch (InvalidKeyException e) {
log.error("InvalidKeyException : "+e.getMessage());
throw new TechnicalException("80000", "InvalidKeyException : "+e.getMessage());
} catch (IllegalBlockSizeException e) {
log.error("IllegalBlockSizeException : "+e.getMessage());
throw new TechnicalException("80000", "IllegalBlockSizeException : "+e.getMessage());
} catch (BadPaddingException e) {
log.error("BadPaddingException : "+e.getMessage());
throw new TechnicalException("80000", "BadPaddingException : "+e.getMessage());
}
}
public static String decrypt(String data, String base64PrivateKey) {
return decryptChipper(Base64.getDecoder().decode(data.getBytes()), getPrivateKey(base64PrivateKey));
}
public static String encrypt(String data, String base64PublicKey) {
return Base64.getEncoder().encodeToString(encryptChipper(data.getBytes(), getPublicKey(base64PublicKey)));
}
}

View File

@ -0,0 +1,22 @@
#Configuration for database
jwt:
public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx3MRA7zxvaWKrtmPl2hRJLFiyryvj0ZUlmWw9OZIgqwJUDBTsg5yFX4hCQrANV1yy5ibTqAn2APdNCdhGgp8R2YLWrUR2vVGbmnKXXzEDsFpT6cgo+/a+lWaUq8aYEhzVg8Xjmy9oG1s521LklEz/jKD2xNE4OgY2Y1SgfUH+bexs84ZEyUhcSpCrcZenjCns7Ubp9zsWPrXa/j6kr7ZuFWH7nXN/i+oYF0HFhc+hDaVr2R9Q7s56wAQNE8XfI2Q+h4iRI1hWa5Vva9ha1DnCN9McJDLBHoGy1coIUEoQKkDhCNQmdHlubIJYDoFIyfDTrSQIXw2gzrrC9sOgYU64wIDAQAB
private-key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHcxEDvPG9pYqu2Y+XaFEksWLKvK+PRlSWZbD05kiCrAlQMFOyDnIVfiEJCsA1XXLLmJtOoCfYA900J2EaCnxHZgtatRHa9UZuacpdfMQOwWlPpyCj79r6VZpSrxpgSHNWDxeObL2gbWznbUuSUTP+MoPbE0Tg6BjZjVKB9Qf5t7GzzhkTJSFxKkKtxl6eMKeztRun3OxY+tdr+PqSvtm4VYfudc3+L6hgXQcWFz6ENpWvZH1DuznrABA0Txd8jZD6HiJEjWFZrlW9r2FrUOcI30xwkMsEegbLVyghQShAqQOEI1CZ0eW5sglgOgUjJ8NOtJAhfDaDOusL2w6BhTrjAgMBAAECggEAL8SPPqXx/8m1/Up96XVCeddCIewu39GoUJzqVL0SgLlTQbFRWkO7bwpWPyQkBKPs6nYSPDJ/WG0UfXkw+FuqiC8YaREUxawYwjj0Do/jJOWNo1YTqy+28l2uKD9nwceYV/QlYCcLA3Exs/upLdHk8eyHR+DFjlgIG5KNQDK/Rj8cLdQGJjAZjHUGzaRq/HhDi+0LVkOoUvzbzle8RSiiZnNrxCt0UV1CambQzvyxXzqLgtGe4s+qItwk/zzkvXDRnRju3+/AyaFaToA2Wnp9gdV2rT4cRGHhj0Dqh0vwrBH4RiPtEfLQjIa9DhTPHfpr6ACGe6GF96fJ889WIEl4gQKBgQDrGgYKGwIhY9j2V7tu6eHmLxYFGzxSPOtfBZPzSLUyQXrcd6wFlb+Fsae2bgBFZJzilIv4lG0wNaGSFjAjteyXMoqVCTIUKJCjzWs+g3qjuA5fu9IAeWEEaXuKSVvkV5tJrnwhmqYdH1jQx5xhpO434W2a4wuEgOnU6SzNjvS/TwKBgQDZLb3Q9KRwlMcyt/rgjeTOf2SD1jvlKD2t+YRuaxlzBs7j99hCEP3GLaTz6/rIptfkutwk10FM/piO5PCyoPXNc2A1Z73ukmsirMRy2G06PfqKBQrV/B9vtIqPW1Agz7KUpXARrHL8Bb6T2ljKl2HU+kC4Pgx2GZQgYnbEvNBGLQKBgQCNf3grFN5PYlzuxxbURofjlmtWX3IKvQechSrqvdPwj2B22L/8DIjc2nPqZIJdQZT6+hTUY+DjpyO7XQdUNuZSieTGlmZBo7iKHdRyJ4fkiZ59F/notyUhFqt4K3bXE7MuwYmMYBuiY399COaiDjYiA9eH5SbFdSFHN5/ziBreGQKBgGZHNG2DyS1745PvMScvq+HT/PJZojt5iBK2v7eAmKujOSwDPMVgWyNJu7VkHOcCLAp8NdDjzs0D2bTx/KkjRJ9NBrIf+UKxkeLymlG7uzUCm0sEtOWxptxkmhyJVGMfbWqzvuOT41LtIaNf4REH2fsDIBekoRm9UhUuSeC9SxjBAoGAP6pdT7HlMsr++XJFqs9+dj7tRsX+a6Coi3T8BFaf2Nl9YxMFFaSBbNOO4QfZWyCUcfbkLrcdEka5VTVt/HwckDKzckxWqH3hMn2+kAKJnHhAvB1M38KAyvdpA54vNgVyID+vL5VbqpHk8wsUbVWZc8F/ERELfuHwzcD0rt6Opnc=
expired-time :
service-account:
access-token: 480
refresh-token: 720
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/smartproc
username: postgres
password: postgre
jackson:
default-property-inclusion: NON_NULL
jpa:
hibernate:
ddl-auto: update
database-platform: org.hibernate.dialect.PostgreSQLDialect
show-sql: true

View File

@ -1,12 +0,0 @@
spring.datasource.url=jdbc:postgresql://localhost:5432/smartproc
spring.datasource.username=postgres
spring.datasource.password=postgre
spring.jpa.show-sql=true
## Hibernate Properties
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect
# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update
server.port=9090

View File

@ -0,0 +1,7 @@
spring:
application:
name: "smartproc-service"
profiles:
active: local
server:
port: 9090

View File

@ -0,0 +1,5 @@
spring.application.name=smartproc
spring.profiles.active=@spring.profiles.active@
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=ALWAYS
management.metrics.tags.application=${spring.application.name}