package com.icontrols.oauth.controller; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; import java.time.Duration; import java.util.Base64; import java.util.Base64.Encoder; import java.util.List; import java.util.Random; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestHeader; 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 org.springframework.web.client.RestTemplate; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.RedirectView; import com.icontrols.oauth.constants.ERROR; import com.icontrols.oauth.model.ClientInfo; import com.icontrols.oauth.model.ComplexInfo; import com.icontrols.oauth.model.JWTInfo; import com.icontrols.oauth.model.Token; import com.icontrols.oauth.model.WallpadCode; import com.icontrols.oauth.repo.ClientInfoRepository; import com.icontrols.oauth.repo.ComplexInfoRepository; import com.icontrols.oauth.utils.AES256Util; import com.icontrols.oauth.utils.DanziAuthUtils; import com.icontrols.oauth.utils.JWTUtils; import com.sun.net.httpserver.HttpServer; @RestController @RequestMapping(value = "/api/oauth2") public class OAuthController { private static final Logger logger = LoggerFactory.getLogger(OAuthController.class); @Autowired ClientInfoRepository clientInfoRepo; @Autowired ComplexInfoRepository complexInfoRepo; @Autowired RedisTemplate redisTemplate; // http://127.0.0.1:8080/api/oauth2/authorize?client_id=clientid&redirect_uri=http://127.0.0.1:8080/api/oauth2/redirect&scope=scope&response_type=code&state=state @RequestMapping(value = "/authorize", method = RequestMethod.GET) public ModelAndView Authorize(@RequestHeader HttpHeaders headers, @RequestParam(value = "response_type", required = true) String responseType, @RequestParam(value = "client_id", required = true) String clientId, @RequestParam(value = "state", required = true) String state, @RequestParam(value = "scope") String scope, @RequestParam(value = "redirect_uri", required = true) String redirectUri, HttpSession httpSession, Model model) { logger.info("**************************************************************"); logger.info("**************************************************************"); logger.info("[STEP #1] /api/oauth2/authorize"); logger.info("**************************************************************"); logger.info("**************************************************************"); logger.info("*[parameters]"); logger.info("*1. response_type: " + responseType); logger.info("*2. client_id: " + clientId); logger.info("*3. state: " + state); // logger.info("*4. scope: " + scope); logger.info("*5. redirect_uri: " + redirectUri); // 월패드 인증코드 시간이 만료되어 history.go(-1)로 해당 페이지에 들어오는 경우에 // 세션에 저장되었던 사용자 정보를 지움. if (httpSession.getAttribute("wallpadCode") != null) httpSession.removeAttribute("wallpadCode"); if (httpSession.getAttribute("homeInfo") != null) httpSession.removeAttribute("homeInfo"); ModelAndView mav = new ModelAndView(); ClientInfo clientInfo = clientInfoRepo.findByClientId(clientId); if (clientInfo == null) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, ERROR.INVALID_CLIENT); } else { logger.info("*[clientinfo]"); logger.info("*" + clientInfo.toString()); if (!responseType.equals("code")) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.UNSUPPORTED_GRANT_TYPE); } else { // 클라이언트에 따라 endpoint를 변경해야해서 세션에 클라이언트 정보를 저장. httpSession.setAttribute("clientId", clientId); httpSession.setAttribute("redirectUri", redirectUri); httpSession.setAttribute("state", state); // if (scope != null) // httpSession.setAttribute("scope", scope); List complexInfos = complexInfoRepo.findAll(); model.addAttribute("complexInfos", complexInfos); // \src\main\webapp\WEB-INF\jsp\InsertUserInfo.jsp mav.setViewName("InsertUserInfo"); } } return mav; } @RequestMapping(value = "/user/info/submit", method = RequestMethod.GET) public ModelAndView sendCode(HttpSession httpSession, @RequestParam(value = "complex", required = true) String complex, @RequestParam(value = "dong", required = true) String dong, @RequestParam(value = "ho", required = true) String ho) throws IOException, NoSuchAlgorithmException, GeneralSecurityException { logger.info("**************************************************************"); logger.info("**************************************************************"); logger.info("[STEP #2] /api/oauth2/user/submit"); logger.info("**************************************************************"); logger.info("**************************************************************"); logger.info("*[parameters]"); logger.info("*1. complex: " + complex); logger.info("*2. dong: " + dong); logger.info("*3. ho: " + ho); String clientId = httpSession.getAttribute("clientId").toString(); // wallpad 코드 생성 Random generator = new Random(); String wallpadCode = ""; for (int i = 0; i < 6; i++) { wallpadCode += Integer.toString(generator.nextInt(10)); } logger.info("*[wallpadCode]"); logger.info("*code: " + wallpadCode); // 세대정보 인코딩 String homeInfo = complex + "/" + dong + "/" + ho + "/" + clientId; // 2019.05.02 DB저장에서 세션 저장 방식으로 변경 httpSession.setAttribute("wallpadCode", wallpadCode); httpSession.setAttribute("homeInfo", homeInfo); logger.info("*[redis]"); logger.info("*(key,value): (" + wallpadCode + "," + homeInfo + ")"); // 단지서버로 월패드인증번호 전송 WallpadCode body = new WallpadCode(dong, ho, wallpadCode); logger.info(body.toString()); ClientInfo clientInfo = clientInfoRepo.findByClientId(clientId); String endPoint = clientInfo.getEndPoint(); // String url = "http://" + complex + ":8002/kakao/auth"; String url = "http://" + complex + endPoint + "/auth"; logger.info(url); // TODO 예외처리 단지통신 HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); factory.setReadTimeout(3000); factory.setConnectTimeout(3000); RestTemplate template = new RestTemplate(factory); HttpEntity entity = new HttpEntity(body); try { String answer = template.postForObject(url, entity, String.class); logger.info(answer); } catch (Exception e) { new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.INVALID_HOMEINFO); } ModelAndView mav = new ModelAndView(); mav.setViewName("InsertCode"); return mav; } // InsertCode.jsp에서 월패드 인증번호를 입력받아 ajax로 실행하는 부분, // 사용자가 입력한 인증번호의 유효성을 검증함. @RequestMapping(value = "/validate", method = RequestMethod.GET) public @ResponseBody Boolean validate(HttpSession httpSession, @RequestParam(value = "code", required = true) String wallpadCode) { logger.info("**************************************************************"); logger.info("**************************************************************"); logger.info("[STEP #3] /api/oauth2/auth"); logger.info("**************************************************************"); logger.info("**************************************************************"); logger.info("*[parameters]"); logger.info("*1. Input code: " + wallpadCode); // ValueOperations vop = redisTemplate.opsForValue(); // if (vop.get(wallpadCode) == null) { // 없으면 null // // return false; // } // httpSession.setAttribute("homeId", vop.get(wallpadCode)); // 2019.05.02 DB저장에서 세션 저장 방식으로 변경 logger.info(httpSession.getAttributeNames().toString()); if (httpSession.getAttribute("wallpadCode") == null) { logger.info("null"); return false; } else { if (!httpSession.getAttribute("wallpadCode").toString().equals(wallpadCode)) { logger.info(httpSession.getAttribute("wallpadCode").toString()); return false; } } logger.info(httpSession.getAttribute("wallpadCode").toString()); return true; } // 월패드 인증이 실행된 경우, 인증 코드를 발급해 리다이렉트하는 부분 @RequestMapping(value = "/code/generate", method = RequestMethod.GET) public RedirectView validateCode(HttpSession httpSession) { logger.info("**************************************************************"); logger.info("**************************************************************"); logger.info("[STEP #4] /code/generate"); logger.info("**************************************************************"); logger.info("**************************************************************"); logger.info("*[parameters]"); logger.info("*1. home id: " + httpSession.getAttribute("homeInfo")); String code = ""; String homeInfo = ""; ValueOperations vop = redisTemplate.opsForValue(); // 코드 발급 homeInfo = httpSession.getAttribute("homeInfo").toString(); logger.info("*[home info]"); logger.info("*1. homeInfo: " + homeInfo); code = ""; Random rnd = new Random(); for (int i = 0; i < 10; i++) { code += String.valueOf((char) ((int) (rnd.nextInt(26)) + 65)); } logger.info("*[code]"); logger.info("*1. code: " + code); Duration d = Duration.ofSeconds(180); while (!vop.setIfAbsent(code, homeInfo, d)) { code = ""; for (int i = 0; i < 10; i++) { code += String.valueOf((char) ((int) (rnd.nextInt(26)) + 65)); } } String redirectUri = (String) httpSession.getAttribute("redirectUri"); redirectUri += "?state=" + httpSession.getAttribute("state"); redirectUri += "&code=" + code; RedirectView redirectView = new RedirectView(); redirectView.setUrl(redirectUri); return redirectView; } // 토큰 발급하는 부분 @RequestMapping(value = "/token", method = RequestMethod.POST, produces = "application/json") public @ResponseBody String Token(@RequestParam(value = "grant_type", required = true) String grantType, @RequestParam(value = "code", required = false) String code, @RequestParam(value = "refresh_token", required = false) String refreshToken, HttpSession httpSession, @RequestHeader HttpHeaders headers, HttpServletResponse response) throws NoSuchAlgorithmException, UnsupportedEncodingException, GeneralSecurityException { response.setStatus(HttpServletResponse.SC_OK); logger.info("[STEP #5] /api/oauth2/token"); // TODO Code유효성 검사 // TODO token 발급 logger.info("grant_type: " + grantType); ValueOperations vop = redisTemplate.opsForValue(); String newAccessToken = ""; // 생성해야 함. String newRefreshToken = ""; // 생성해야 함. String answer = null; // 단지서버에 토큰 관련요청 후 수신하는 응답 Token token = new Token(); if (grantType.equals("authorization_code")) { // TODO 코드 검사 if (vop.get(code) == null) { // 인증코드가 없는 경우 // 에러페이지 리턴 throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.INVALID_GRANT); } else { AES256Util aes = new AES256Util(); String homeInfo = vop.get(code).toString(); String jwt_homeInfo = homeInfo.split("/")[1] + "/" + homeInfo.split("/")[2]; String encodedHomeInfo = aes.encrypt(jwt_homeInfo); logger.info("encodedHomeInfo =" + encodedHomeInfo); logger.info("encoded = " + encodedHomeInfo); String complexIp = homeInfo.split("/")[0]; Encoder encoder = Base64.getEncoder(); complexIp = new String(encoder.encode(complexIp.getBytes())); JWTInfo jwtinfo = new JWTInfo(complexIp, encodedHomeInfo, homeInfo.split("/")[3]); newAccessToken = JWTUtils.generateToken(jwtinfo, "accessToken"); newRefreshToken = JWTUtils.generateToken(jwtinfo, "refreshToken"); token.setAccess_token(newAccessToken); token.setRefresh_token(newRefreshToken); String clientId = JWTUtils.getClientInfoFromToken(newAccessToken); ClientInfo clientInfo = clientInfoRepo.findByClientId(clientId); String url = clientInfo.getEndPoint(); answer = DanziAuthUtils.create(token, url); } } else if (grantType.equals("refresh_token")) { if (refreshToken != null) { // 1. 정상적으로 생성되었는지 확인 if (!JWTUtils.validateToken(refreshToken, "refreshToken")) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.INVALID_REFRESHTOKEN); // client정보검사 if (headers.get("Authorization").get(0) == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, ERROR.INVALID_CLIENT); // if (!validateHeaderAuth(headers.get("Authorization").get(0))) // throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, ERROR.INVALID_CLIENT); // 단지에 갱신토큰 유효성 확인 String clientId = JWTUtils.getClientInfoFromToken(refreshToken); ClientInfo clientInfo = clientInfoRepo.findByClientId(clientId); String url = clientInfo.getEndPoint(); String isValidToken = DanziAuthUtils.get(url, refreshToken); logger.info(isValidToken); JSONObject isValidTokenJson = new JSONObject(isValidToken); logger.info(isValidTokenJson.toString()); if (!isValidTokenJson.get("result").toString().equalsIgnoreCase("success")) { if (isValidTokenJson.getJSONObject("error").getInt("code") == -200) // 서버에러인경우 throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); logger.info(isValidTokenJson.get("result").toString()); JSONObject error = new JSONObject(); error.put("error", "invalid_grant"); error.put("error_description", "invalid token"); return error.toString(); } // 유효한 토큰인 경우 토큰 생성 JWTInfo jwtinfo = JWTUtils.getJWTInfoFromToken(refreshToken); newAccessToken = JWTUtils.generateToken(jwtinfo, "accessToken"); newRefreshToken = JWTUtils.generateToken(jwtinfo, "refreshToken"); token.setAccess_token(newAccessToken); token.setRefresh_token(newRefreshToken); // 단지서버에 갱신된 토큰 전달 answer = DanziAuthUtils.refresh(url, token, refreshToken); logger.info(answer); } } else { throw new ResponseStatusException(HttpStatus.BAD_REQUEST); } // 갱신된 토큰 전달에 대한 응답처리 logger.info(answer); if (answer == null) { throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR); } else { JSONObject obj = new JSONObject(answer); logger.info(obj.get("result").toString()); if (!obj.get("result").toString().equalsIgnoreCase("success")) { if (obj.getJSONObject("error").getInt("code") == -200) // 서버에러인경우 throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); JSONObject error = new JSONObject(); error.put("error", "invalid_grant"); error.put("error_description", "invalid token"); return error.toString(); } } // class 만들어서 사용하는 것으로 변경해야함. logger.info(token.toString()); // TODO 단지서버로 전송하고 정상 응답 받은 경우에만 정상리턴 -> 아닌경우에는 500 // 토큰 생성인 경우와 갱신인 경우 구분해서 처리 create, refresh JSONObject tokenResult = new JSONObject(); tokenResult.put("access_token", token.getAccess_token()); tokenResult.put("refresh_token", token.getRefresh_token()); tokenResult.put("token_type", token.getToken_type()); tokenResult.put("expires_in", token.getExpires_in()); return tokenResult.toString(); } public boolean validateHeaderAuth(String authorization) { String splitStr[] = authorization.split(" "); // splitStr length 가 2가 아니면 유효하지 않은 authorization 값 if (splitStr.length != 2) return false; // splitStr이 Basic이 아니어도 에러 if (!splitStr[0].equals("Basic")) return false; // Base64 디코드 String decodeStr = new String(Base64.getDecoder().decode(splitStr[1].trim())); splitStr = decodeStr.split(":"); // splitStr length 가 2가 아니면 유효하지 않은 authorization 값 if (splitStr.length != 2) return false; ClientInfo clientInfo = clientInfoRepo.findByClientId(splitStr[0]); if (clientInfo == null) return false; if (!clientInfo.getClientSecret().equals(splitStr[1])) return false; return true; } // ((Test)) Redirect Destination // 여기는 리다이렉트 받는 URl임 테스트용. Client를 대신함. @RequestMapping(value = "/redirect", method = RequestMethod.GET) public @ResponseBody String RedirectTester(HttpSession httpSession, @RequestParam(value = "code", required = true) String code) { logger.info("/api/oauth2/redirect"); String redirectUri = "http://61.33.215.56:8003/api/oauth2/token?grant_type=authorization_code&code=" + code; RedirectView redirectView = new RedirectView(); redirectView.setUrl(redirectUri); return code; } @RequestMapping(value = "/aws/healthcheck", method = RequestMethod.GET) public void healthCheck(HttpEntity httpEntity) { return; } }