diff --git a/src/main/java/com/eactive/eai/authserver/custom/BearerTokenContoller.java b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenContoller.java new file mode 100644 index 0000000..bda62ca --- /dev/null +++ b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenContoller.java @@ -0,0 +1,125 @@ +package com.eactive.eai.authserver.custom; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.json.simple.JSONObject; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException; +import com.eactive.eai.authserver.service.OAuth2Manager; +import com.eactive.eai.common.util.Logger; + +@RestController +@RequestMapping("/auth/oauth/v2") +public class BearerTokenContoller { + + public static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); + private final BearerTokenService tokenService; + + public BearerTokenContoller(BearerTokenService tokenService) { + this.tokenService = tokenService; + } + + // 기본 CA Bearer Token 발급 + @PostMapping("/token") + public ResponseEntity issueCAToken(@RequestParam MultiValueMap req) throws JwtAuthException { + JSONObject resObject = new JSONObject(); + + try { + String clientId = req.getFirst("client_id"); // .get("client_id"); + String grantType = req.getFirst("grant_type"); + if (!StringUtils.equals(grantType, "client_credentials") ) { + throw new JwtAuthException("unsupported_grant_type", "Unsupported grant type"); + } + + ClientDetails clientDetails = OAuth2Manager.getInstance().getClientDeatilsStore().get(clientId); + if (clientDetails == null) { + throw new JwtAuthException("invalid_client", "Bad client credentials(not found)"); + } + + String tokenType = "Bearer"; + String clientSecret = req.getFirst("client_secret"); + String scopes = req.getFirst("scope"); + Long expiresIn = Long.valueOf(clientDetails.getAccessTokenValiditySeconds()); + + String[] reqScopeArr = org.springframework.util.StringUtils.tokenizeToStringArray(scopes, " "); + Set scopeSet = new HashSet(); + for (String scope : reqScopeArr) { + scopeSet.add(scope); + } + veryfyClient(clientDetails, clientSecret, scopeSet); + + String token = tokenService.generateCAToken(clientId, expiresIn, scopeSet); + + Map tokenMap = new HashMap<>(); + tokenMap.put("token_type", tokenType); + tokenMap.put("access_token", token); + tokenMap.put("expires_in", expiresIn); + tokenMap.put("scope", scopes); + // ... and so on for all key-value pairs + resObject.putAll(tokenMap); + + return ResponseEntity.ok(resObject); + } catch (JwtAuthException e) { + logger.error(e); + + JSONObject errorJson = new JSONObject(); + errorJson.put("error", e.getCode()); + errorJson.put("error_description", e.getMessage()); + resObject.putAll(errorJson); + return ResponseEntity.status(401).body(resObject); + } catch (Exception e) { + logger.error(e); + + JSONObject errorJson = new JSONObject(); + errorJson.put("error", "invalid_request"); + errorJson.put("error_description", e.getMessage()); + resObject.putAll(errorJson); + return ResponseEntity.status(500).body(resObject); + } + } + +// // 서비스용 Bearer Token 발급 +// @PostMapping("/service") +// public ResponseEntity issueFileToken(@RequestBody Map req) { +// String fileId = req.get("fileid"); +// +// String token = tokenService.generateFileToken(fileId); +// +// Map tokenMap = new HashMap<>(); +// tokenMap.put("token_type", "Bearer"); +// tokenMap.put("file_token", token); +// tokenMap.put("expires_in", "599"); +// tokenMap.put("expires_on", "1575593514"); +// tokenMap.put("resource", fileId); +// +// return ResponseEntity.ok(tokenMap); +// } + private void veryfyClient(ClientDetails clientDetails, String clientSecret, Set scopeSet) throws JwtAuthException { + if (!StringUtils.equals(clientDetails.getClientSecret(), clientSecret)) { + throw new JwtAuthException("invalid_client", "Bad client credentials(client_secret is empty or not match)"); + } + + if (scopeSet.isEmpty()) { + throw new JwtAuthException("invalid_client", "Bad client credentials(Scope is empty)"); + } + + if (!clientDetails.getScope().containsAll(scopeSet)) { + throw new JwtAuthException("invalid_client", "Bad client credentials(Include unacceptable scope)"); + } + } +} + diff --git a/src/main/java/com/eactive/eai/authserver/custom/BearerTokenFilter.java b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenFilter.java new file mode 100644 index 0000000..d993d30 --- /dev/null +++ b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenFilter.java @@ -0,0 +1,85 @@ +package com.eactive.eai.authserver.custom; + +import java.io.IOException; +import java.util.Arrays; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.json.simple.JSONObject; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException; +import com.eactive.eai.common.util.Logger; + +@Component +public class BearerTokenFilter extends OncePerRequestFilter{ + + public static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); + private final BearerTokenService tokenService; + + public static final String ERROR_AUTHENTICATION_FAIL = "E.AUTHENTICATION_FAIL"; + public static final String ERROR_AUTHORIZATION_FAIL = "E.AUTHORIZATION_FAIL"; + public static final String ERROR_TOKEN_EXPIRED = "E.TOKEN_EXPIRED"; + + public BearerTokenFilter(BearerTokenService tokenService) { + this.tokenService = tokenService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException { + JSONObject resObject = new JSONObject(); + + try { + String auth = request.getHeader("Authorization"); + + if (auth != null && auth.startsWith("Bearer ")) { + String token = auth.substring(7); + + String clientId = null; + String fileId = null; + + if (tokenService.validateCA(token)) { + clientId = tokenService.getClientIdFromCA(token); + } else { + throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Access token is invalid or expired"); + } + +// if (fileId != null) { +// UsernamePasswordAuthenticationToken authentication = +// new UsernamePasswordAuthenticationToken(fileId, null, Arrays.asList()); +// SecurityContextHolder.getContext().setAuthentication(authentication); +// } + } + + filterChain.doFilter(request, response); + } catch (JwtAuthException jae) { + logger.debug(jae.getMessage()); + + JSONObject errorJson = new JSONObject(); + errorJson.put("error", jae.getCode()); + errorJson.put("error_description", jae.getMessage()); + resObject.putAll(errorJson); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 설정 + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(resObject.toJSONString()); + } catch (Exception e) { + logger.error(e.getMessage()); + JSONObject errorJson = new JSONObject(); + errorJson.put("error", "invalid_request"); + errorJson.put("error_description", e.getMessage()); + resObject.putAll(errorJson); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); // 500 설정 + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(resObject.toJSONString()); + } + } +} + diff --git a/src/main/java/com/eactive/eai/authserver/custom/BearerTokenInfo.java b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenInfo.java new file mode 100644 index 0000000..e5a37fb --- /dev/null +++ b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenInfo.java @@ -0,0 +1,49 @@ +package com.eactive.eai.authserver.custom; + +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class BearerTokenInfo { + @JsonProperty("client_id") + private String clientId; + private Long expiresIn; + private Long expiresAt; + private Set scopeSet; + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public Long getExpiresIn() { + return expiresIn; + } + + public void setExpiresIn(Long expiresIn) { + this.expiresIn = expiresIn; + } + + public Long getExpiresAt() { + return expiresAt; + } + + public void setExpiresAt(Long expiresIn) { + this.expiresAt = System.currentTimeMillis() + (expiresIn * 1000); + } + + public Set getScopeSet() { + return scopeSet; + } + + public void setScopeSet(Set scopeSet) { + this.scopeSet = scopeSet; + } + + public boolean isExpired() { + return System.currentTimeMillis() > expiresAt; + } +} diff --git a/src/main/java/com/eactive/eai/authserver/custom/BearerTokenService.java b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenService.java new file mode 100644 index 0000000..3b01368 --- /dev/null +++ b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenService.java @@ -0,0 +1,97 @@ +package com.eactive.eai.authserver.custom; + +import java.security.SecureRandom; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.springframework.stereotype.Service; + +@Service +public class BearerTokenService { + + // CA 토큰 저장소 + private final Map CATokenStore = new ConcurrentHashMap<>(); + + // File 토큰 저장소 + private final Map fileTokenStore = new ConcurrentHashMap<>(); + +// // FileTransfer 토큰 저장소 +// private final Map fileTransferTokenStore = new ConcurrentHashMap<>(); + + public String generateCAToken(String clientId, Long expiresIn, Set scopeSet) { + String token = UUID.randomUUID().toString(); + + BearerTokenInfo CATokenInfo = new BearerTokenInfo(); + CATokenInfo.setClientId(clientId); + CATokenInfo.setExpiresIn(expiresIn); + CATokenInfo.setExpiresAt(expiresIn); + CATokenInfo.setScopeSet(scopeSet); + + CATokenStore.put(token, CATokenInfo); + + return token; + } + + public String generateFileToken(String fileId) { + //String token = UUID.randomUUID().toString().replace("-", ""); + String token = generateTokenString(987); + fileTokenStore.put(token, fileId); + return token; + } + +// public String generateFileTransferToken(String fileId) { +// //String token = UUID.randomUUID().toString().replace("-", ""); +// String token = generateTokenString(654); +// fileTransferTokenStore.put(token, fileId); +// return token; +// } + + public boolean validateCA(String token) { + BearerTokenInfo CATokenInfo = CATokenStore.get(token); + if (CATokenInfo == null) return false; + + if (CATokenInfo.isExpired()) { + CATokenStore.remove(token); + return false; + } + return CATokenStore.containsKey(token); + } + + public boolean validateFile(String token) { + return fileTokenStore.containsKey(token); + } + +// public boolean validateFileTransfer(String token) { +// return fileTransferTokenStore.containsKey(token); +// } + + public String getClientIdFromCA(String token) { + BearerTokenInfo CATokenInfo = CATokenStore.get(token); + return CATokenInfo.getClientId(); + } + + public String getFileIdFromFile(String token) { + return fileTokenStore.get(token); + } + +// public String getFileIdFromFileTransfer(String token) { +// return fileTransferTokenStore.get(token); +// } + + private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + private static final SecureRandom secureRandom = new SecureRandom(); + + public static String generateTokenString(int length) { + // Java 8의 IntStream과 Collectors.joining을 사용한 효율적인 방법 + return IntStream.range(0, length) + .map(i -> secureRandom.nextInt(CHARACTERS.length())) + .mapToObj(CHARACTERS::charAt) + .map(String::valueOf) + .collect(Collectors.joining()); + } +} + diff --git a/src/main/java/com/eactive/eai/authserver/custom/SecurityConfig.java b/src/main/java/com/eactive/eai/authserver/custom/SecurityConfig.java new file mode 100644 index 0000000..f2a6183 --- /dev/null +++ b/src/main/java/com/eactive/eai/authserver/custom/SecurityConfig.java @@ -0,0 +1,58 @@ +package com.eactive.eai.authserver.custom; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +// +//@Configuration +//@EnableWebSecurity +//public class SecurityConfig { +// +// private final BearerTokenFilter tokenFilter; +// +// public SecurityConfig(BearerTokenFilter tokenFilter) { +// this.tokenFilter = tokenFilter; +// } +// +// @Bean +// public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { +// +// http +// .csrf().disable() +// .authorizeRequests() +// .antMatchers("/auth/token/**").permitAll() +// .anyRequest().authenticated() +// .and() +// .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class); +// +// return http.build(); +// } +//} +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + private final BearerTokenFilter tokenFilter; + + @Autowired + public SecurityConfig(BearerTokenFilter tokenFilter) { + this.tokenFilter = tokenFilter; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + + http + .csrf().disable() + .authorizeRequests() + .antMatchers("/auth/token/**").permitAll() + .anyRequest().authenticated() + .and() + .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class); + } +} \ No newline at end of file