diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 8b8acde..bbc7654 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -12,4 +12,9 @@ + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 7d21bf7..8806c7b 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,5 +4,5 @@ - + \ No newline at end of file diff --git a/server/build.gradle b/server/build.gradle index 915df2e..35e5a49 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -6,7 +6,8 @@ plugins { group = 'com.yogit' version = '0.0.1-SNAPSHOT' -sourceCompatibility = '11' +sourceCompatibility = '17' +targetCompatibility = '17' configurations { compileOnly { @@ -37,8 +38,26 @@ dependencies { // JWT implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1' + + // apple login + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // 테스트 용 + implementation 'org.apache.httpcomponents:httpclient' + implementation 'com.nimbusds:nimbus-jose-jwt:3.10' + implementation "io.jsonwebtoken:jjwt:0.9.1" + implementation "org.bouncycastle:bcpkix-jdk15on:1.50" + implementation "org.apache.httpcomponents:httpclient:4.5.13" + implementation fileTree(dir: 'libs', include: '*.jar') } tasks.named('test') { useJUnitPlatform() } + +// apple login +tasks.withType(JavaCompile){ + options.compilerArgs.addAll([ + "--add-exports=java.base/sun.security.pkcs=ALL-UNNAMED", + "--add-exports=java.base/sun.security.util=ALL-UNNAMED", + "--add-exports=java.base/sun.security.x509=ALL-UNNAMED" + ]) +} diff --git a/server/src/main/java/com/yogit/server/applelogin/controller/AppleController.java b/server/src/main/java/com/yogit/server/applelogin/controller/AppleController.java new file mode 100644 index 0000000..75940a2 --- /dev/null +++ b/server/src/main/java/com/yogit/server/applelogin/controller/AppleController.java @@ -0,0 +1,147 @@ +package com.yogit.server.applelogin.controller; + +import com.yogit.server.applelogin.model.AppsResponse; +import com.yogit.server.applelogin.model.ServicesResponse; +import com.yogit.server.applelogin.model.TokenResponse; +import com.yogit.server.applelogin.service.AppleService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.*; + +import java.security.NoSuchAlgorithmException; +import java.util.Map; + +@Controller +public class AppleController { + + private Logger logger = LoggerFactory.getLogger(AppleController.class); + + @Autowired + AppleService appleService; + + /** + * Sign in with Apple - JS Page (index.html) + * + * @param model + * @return + */ + @GetMapping(value = "/") + public String appleLoginPage(ModelMap model) { + + Map metaInfo = appleService.getLoginMetaInfo(); + + model.addAttribute("client_id", metaInfo.get("CLIENT_ID")); + model.addAttribute("redirect_uri", metaInfo.get("REDIRECT_URI")); + model.addAttribute("nonce", metaInfo.get("NONCE")); + + System.out.println(model.getAttribute("client_id")); + System.out.println(model.getAttribute("redirect_uri")); + System.out.println(model.getAttribute("nonce")); + + + return "index"; + } + + /** + * Apple login page Controller (SSL - https) + * + * @param model + * @return + */ + @GetMapping(value = "/apple/login") + public String appleLogin(ModelMap model) { + + Map metaInfo = appleService.getLoginMetaInfo(); + + model.addAttribute("client_id", metaInfo.get("CLIENT_ID")); + model.addAttribute("redirect_uri", metaInfo.get("REDIRECT_URI")); + model.addAttribute("nonce", metaInfo.get("NONCE")); + model.addAttribute("response_type", "code id_token"); + model.addAttribute("scope", "name email"); + model.addAttribute("response_mode", "form_post"); + + System.out.println("=========================="); + System.out.println(model.getAttribute("client_id")); + System.out.println(model.getAttribute("redirect_uri")); + System.out.println(model.getAttribute("nonce")); + System.out.println(model.getAttribute("response_type")); + System.out.println(model.getAttribute("scope")); + System.out.println(model.getAttribute("response_mode")); + + + return "redirect:https://appleid.apple.com/auth/authorize"; + } + + /** + * Apple Login 유저 정보를 받은 후 권한 생성 + * + * @param serviceResponse + * @return + */ + @PostMapping(value = "/redirect") + @ResponseBody + public TokenResponse servicesRedirect(ServicesResponse serviceResponse) throws NoSuchAlgorithmException { + + System.out.println("1-------------"); + if (serviceResponse == null) { + return null; + } + System.out.println("2-------------"); + + + System.out.println(serviceResponse); + System.out.println("3-------------"); + + + String code = serviceResponse.getCode(); + System.out.println(code); + System.out.println("4-------------"); + + String id_token = serviceResponse.getId_token(); + System.out.println(id_token); + System.out.println("5-------------"); + + String client_secret = appleService.getAppleClientSecret(serviceResponse.getId_token()); + System.out.println(client_secret); + System.out.println("6-------------"); + + + logger.debug("================================"); + logger.debug("id_token ‣ " + serviceResponse.getId_token()); + logger.debug("payload ‣ " + appleService.getPayload(serviceResponse.getId_token())); + logger.debug("client_secret ‣ " + client_secret); + logger.debug("================================"); + + System.out.println("7-------------"); + + return appleService.requestCodeValidations(client_secret, code, null); + } + + /** + * refresh_token 유효성 검사 + * + * @param client_secret + * @param refresh_token + * @return + */ + @PostMapping(value = "/refresh") + @ResponseBody + public TokenResponse refreshRedirect(@RequestParam String client_secret, @RequestParam String refresh_token) { + return appleService.requestCodeValidations(client_secret, null, refresh_token); + } + + /** + * Apple 유저의 이메일 변경, 서비스 해지, 계정 탈퇴에 대한 Notifications을 받는 Controller (SSL - https (default: 443)) + * + * @param appsResponse + */ + @PostMapping(value = "/apps/to/endpoint") + @ResponseBody + public void appsToEndpoint(@RequestBody AppsResponse appsResponse) { + logger.debug("[/path/to/endpoint] RequestBody ‣ " + appsResponse.getPayload()); + } + +} diff --git a/server/src/main/java/com/yogit/server/applelogin/model/AppsResponse.java b/server/src/main/java/com/yogit/server/applelogin/model/AppsResponse.java new file mode 100644 index 0000000..13f6d21 --- /dev/null +++ b/server/src/main/java/com/yogit/server/applelogin/model/AppsResponse.java @@ -0,0 +1,18 @@ +package com.yogit.server.applelogin.model; + + +public class AppsResponse { + + private String payload; + + public String getPayload() { + return payload; + } + + public void setPayload(String payload) { + this.payload = payload; + } + + public AppsResponse() { + } +} diff --git a/server/src/main/java/com/yogit/server/applelogin/model/Key.java b/server/src/main/java/com/yogit/server/applelogin/model/Key.java new file mode 100644 index 0000000..81eecf6 --- /dev/null +++ b/server/src/main/java/com/yogit/server/applelogin/model/Key.java @@ -0,0 +1,62 @@ +package com.yogit.server.applelogin.model; + +public class Key { + + private String kty; + private String kid; + private String use; + private String alg; + private String n; + private String e; + + public String getKty() { + return kty; + } + + public void setKty(String kty) { + this.kty = kty; + } + + public String getKid() { + return kid; + } + + public void setKid(String kid) { + this.kid = kid; + } + + public String getUse() { + return use; + } + + public void setUse(String use) { + this.use = use; + } + + public String getAlg() { + return alg; + } + + public void setAlg(String alg) { + this.alg = alg; + } + + public String getN() { + return n; + } + + public void setN(String n) { + this.n = n; + } + + public String getE() { + return e; + } + + public void setE(String e) { + this.e = e; + } + + public Key() { + } +} diff --git a/server/src/main/java/com/yogit/server/applelogin/model/Keys.java b/server/src/main/java/com/yogit/server/applelogin/model/Keys.java new file mode 100644 index 0000000..37b0988 --- /dev/null +++ b/server/src/main/java/com/yogit/server/applelogin/model/Keys.java @@ -0,0 +1,19 @@ +package com.yogit.server.applelogin.model; + +import java.util.List; + +public class Keys { + + private List keys; + + public List getKeys() { + return keys; + } + + public void setKeys(List keys) { + this.keys = keys; + } + + public Keys() { + } +} diff --git a/server/src/main/java/com/yogit/server/applelogin/model/Payload.java b/server/src/main/java/com/yogit/server/applelogin/model/Payload.java new file mode 100644 index 0000000..6ef6f1a --- /dev/null +++ b/server/src/main/java/com/yogit/server/applelogin/model/Payload.java @@ -0,0 +1,144 @@ +package com.yogit.server.applelogin.model; + +public class Payload { + + private String iss; + private String aud; + private Long exp; + private Long iat; + private String sub; + private String nonce; + private String c_hash; + private String at_hash; + private String email; + private String email_verified; + private String is_private_email; + private Long auth_time; + private boolean nonce_supported; + + public String getIss() { + return iss; + } + + public void setIss(String iss) { + this.iss = iss; + } + + public String getAud() { + return aud; + } + + public void setAud(String aud) { + this.aud = aud; + } + + public Long getExp() { + return exp; + } + + public void setExp(Long exp) { + this.exp = exp; + } + + public Long getIat() { + return iat; + } + + public void setIat(Long iat) { + this.iat = iat; + } + + public String getSub() { + return sub; + } + + public void setSub(String sub) { + this.sub = sub; + } + + public String getNonce() { + return nonce; + } + + public void setNonce(String nonce) { + this.nonce = nonce; + } + + public String getC_hash() { + return c_hash; + } + + public void setC_hash(String c_hash) { + this.c_hash = c_hash; + } + + public String getAt_hash() { + return at_hash; + } + + public void setAt_hash(String at_hash) { + this.at_hash = at_hash; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getEmail_verified() { + return email_verified; + } + + public void setEmail_verified(String email_verified) { + this.email_verified = email_verified; + } + + public String getIs_private_email() { + return is_private_email; + } + + public void setIs_private_email(String is_private_email) { + this.is_private_email = is_private_email; + } + + public Long getAuth_time() { + return auth_time; + } + + public void setAuth_time(Long auth_time) { + this.auth_time = auth_time; + } + + public boolean isNonce_supported() { + return nonce_supported; + } + + public void setNonce_supported(boolean nonce_supported) { + this.nonce_supported = nonce_supported; + } + + @Override + public String toString() { + return "{" + + "iss='" + iss + '\'' + + ", aud='" + aud + '\'' + + ", exp=" + exp + + ", iat=" + iat + + ", sub='" + sub + '\'' + + ", nonce='" + nonce + '\'' + + ", c_hash='" + c_hash + '\'' + + ", at_hash='" + at_hash + '\'' + + ", email='" + email + '\'' + + ", email_verified='" + email_verified + '\'' + + ", is_private_email='" + is_private_email + '\'' + + ", auth_time=" + auth_time + + ", nonce_supported=" + nonce_supported + + '}'; + } + + public Payload() { + } +} diff --git a/server/src/main/java/com/yogit/server/applelogin/model/ServicesResponse.java b/server/src/main/java/com/yogit/server/applelogin/model/ServicesResponse.java new file mode 100644 index 0000000..fb4ca02 --- /dev/null +++ b/server/src/main/java/com/yogit/server/applelogin/model/ServicesResponse.java @@ -0,0 +1,44 @@ +package com.yogit.server.applelogin.model; + +public class ServicesResponse { + + private String state; + private String code; + private String id_token; + private String user; + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getId_token() { + return id_token; + } + + public void setId_token(String id_token) { + this.id_token = id_token; + } + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public ServicesResponse() { + } +} diff --git a/server/src/main/java/com/yogit/server/applelogin/model/TokenResponse.java b/server/src/main/java/com/yogit/server/applelogin/model/TokenResponse.java new file mode 100644 index 0000000..28738e4 --- /dev/null +++ b/server/src/main/java/com/yogit/server/applelogin/model/TokenResponse.java @@ -0,0 +1,56 @@ +package com.yogit.server.applelogin.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown=true) +public class TokenResponse { + + private String access_token; + private Long expires_in; + private String id_token; + private String refresh_token; + private String token_type; + + public String getAccess_token() { + return access_token; + } + + public void setAccess_token(String access_token) { + this.access_token = access_token; + } + + public Long getExpires_in() { + return expires_in; + } + + public void setExpires_in(Long expires_in) { + this.expires_in = expires_in; + } + + public String getId_token() { + return id_token; + } + + public void setId_token(String id_token) { + this.id_token = id_token; + } + + public String getRefresh_token() { + return refresh_token; + } + + public void setRefresh_token(String refresh_token) { + this.refresh_token = refresh_token; + } + + public String getToken_type() { + return token_type; + } + + public void setToken_type(String token_type) { + this.token_type = token_type; + } + + public TokenResponse() { + } +} diff --git a/server/src/main/java/com/yogit/server/applelogin/service/AppleService.java b/server/src/main/java/com/yogit/server/applelogin/service/AppleService.java new file mode 100644 index 0000000..078847f --- /dev/null +++ b/server/src/main/java/com/yogit/server/applelogin/service/AppleService.java @@ -0,0 +1,18 @@ +package com.yogit.server.applelogin.service; + +import com.yogit.server.applelogin.model.TokenResponse; + +import java.security.NoSuchAlgorithmException; +import java.util.Map; + +public interface AppleService { + + String getAppleClientSecret(String id_token) throws NoSuchAlgorithmException; + + TokenResponse requestCodeValidations(String client_secret, String code, String refresh_token); + + Map getLoginMetaInfo(); + + String getPayload(String id_token); + +} diff --git a/server/src/main/java/com/yogit/server/applelogin/service/AppleServiceImpl.java b/server/src/main/java/com/yogit/server/applelogin/service/AppleServiceImpl.java new file mode 100644 index 0000000..6cdd73f --- /dev/null +++ b/server/src/main/java/com/yogit/server/applelogin/service/AppleServiceImpl.java @@ -0,0 +1,77 @@ +package com.yogit.server.applelogin.service; + +import com.yogit.server.applelogin.model.TokenResponse; +import com.yogit.server.applelogin.util.AppleUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.security.NoSuchAlgorithmException; +import java.util.Map; + +@Service +public class AppleServiceImpl implements AppleService { + + @Autowired + AppleUtils appleUtils; + + /** + * 유효한 id_token인 경우 client_secret 생성 + * + * @param id_token + * @return + */ + @Override + public String getAppleClientSecret(String id_token) throws NoSuchAlgorithmException { + + if (appleUtils.verifyIdentityToken(id_token)) { + return appleUtils.createClientSecret(); + } + + return null; + } + + /** + * code 또는 refresh_token가 유효한지 Apple Server에 검증 요청 + * + * @param client_secret + * @param code + * @param refresh_token + * @return + */ + @Override + public TokenResponse requestCodeValidations(String client_secret, String code, String refresh_token) { + + TokenResponse tokenResponse = new TokenResponse(); + + // 만약 처음 인증하는 유저여서 refresh토큰 없으면 client_secret, authorization_code로 검증 + if (client_secret != null && code != null && refresh_token == null) { + tokenResponse = appleUtils.validateAuthorizationGrantCode(client_secret, code); + } + // 이미 refresh토큰잇는 유저면 client_secret, refresh_token로 검증 + else if (client_secret != null && code == null && refresh_token != null) { + tokenResponse = appleUtils.validateAnExistingRefreshToken(client_secret, refresh_token); + } + + return tokenResponse; + } + + /** + * Apple login page 호출을 위한 Meta 정보 가져오기 + * + * @return + */ + @Override + public Map getLoginMetaInfo() { + return appleUtils.getMetaInfo(); + } + + /** + * id_token에서 payload 데이터 가져오기 + * + * @return + */ + @Override + public String getPayload(String id_token) { + return appleUtils.decodeFromIdToken(id_token).toString(); + } +} diff --git a/server/src/main/java/com/yogit/server/applelogin/util/AppleUtils.java b/server/src/main/java/com/yogit/server/applelogin/util/AppleUtils.java new file mode 100644 index 0000000..6044a42 --- /dev/null +++ b/server/src/main/java/com/yogit/server/applelogin/util/AppleUtils.java @@ -0,0 +1,330 @@ +package com.yogit.server.applelogin.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.*; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.ReadOnlyJWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import com.yogit.server.applelogin.model.Key; +import com.yogit.server.applelogin.model.Keys; +import com.yogit.server.applelogin.model.TokenResponse; +import net.minidev.json.JSONObject; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.text.ParseException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Component +public class AppleUtils { + + @Value("${APPLE.PUBLICKEY.URL}") + private String APPLE_PUBLIC_KEYS_URL; + + @Value("${APPLE.ISS}") + private String ISS; + + @Value("${APPLE.AUD}") + private String AUD; + + @Value("${APPLE.TEAM.ID}") + private String TEAM_ID; + + @Value("${APPLE.KEY.ID}") + private String KEY_ID; + + @Value("${APPLE.KEY.PATH}") + private String KEY_PATH; + + @Value("${APPLE.AUTH.TOKEN.URL}") + private String AUTH_TOKEN_URL; + + @Value("${APPLE.WEBSITE.URL}") + private String APPLE_WEBSITE_URL; + + /** + * User가 Sign in with Apple 요청(https://appleid.apple.com/auth/authorize)으로 전달받은 id_token을 이용한 최초 검증 + * Apple Document URL ‣ https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/verifying_a_user + * + * @param id_token + * @return boolean + */ + public boolean verifyIdentityToken(String id_token) { + + try { + SignedJWT signedJWT = SignedJWT.parse(id_token); + ReadOnlyJWTClaimsSet payload = signedJWT.getJWTClaimsSet(); + + // EXP 만료시간 검증 + Date currentTime = new Date(System.currentTimeMillis()); + if (!currentTime.before(payload.getExpirationTime())) { + return false; + } + + // NONCE(Test value), ISS, AUD + if (!"20B20D-0S8-1K8".equals(payload.getClaim("nonce")) || !ISS.equals(payload.getIssuer()) || !AUD.equals(payload.getAudience().get(0))) { + return false; + } + + // RSA + if (verifyPublicKey(signedJWT)) { + return true; + } + } catch (ParseException e) { + e.printStackTrace(); + } + + return false; + } + + /** + * Apple Server에서 공개 키를 받아서 서명 확인 + * + * @param signedJWT + * @return + */ + private boolean verifyPublicKey(SignedJWT signedJWT) { + + try { + String publicKeys = HttpClientUtils.doGet(APPLE_PUBLIC_KEYS_URL); + ObjectMapper objectMapper = new ObjectMapper(); + Keys keys = objectMapper.readValue(publicKeys, Keys.class); + for (Key key : keys.getKeys()) { + RSAKey rsaKey = (RSAKey) JWK.parse(objectMapper.writeValueAsString(key)); + RSAPublicKey publicKey = rsaKey.toRSAPublicKey(); + JWSVerifier verifier = new RSASSAVerifier(publicKey); + + if (signedJWT.verify(verifier)) { + return true; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + + return false; + } + + /** + * client_secret 생성 + * Apple Document URL ‣ https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens + * + * @return client_secret(jwt) + */ + public String createClientSecret() { + + JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(KEY_ID).build(); + JWTClaimsSet claimsSet = new JWTClaimsSet(); + Date now = new Date(); + + claimsSet.setIssuer(TEAM_ID); + claimsSet.setIssueTime(now); + claimsSet.setExpirationTime(new Date(now.getTime() + 3600000)); + claimsSet.setAudience(ISS); + claimsSet.setSubject(AUD); + + SignedJWT jwt = new SignedJWT(header, claimsSet); + + try { +// ECPrivateKey ecPrivateKey = new ECPrivateKeyImpl2(readPrivateKey()); +// JWSSigner jwsSigner = new ECDSASigner(ecPrivateKey.getS()); +// +// jwt.sign(jwsSigner); + System.out.println("=====새로운 시도 1====="); + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(readPrivateKey()); + System.out.println("=====새로운 시도 5====="); + + + try{ + KeyFactory kf = KeyFactory.getInstance("EC"); + System.out.println("=====새로운 시도 6====="); + ECPrivateKey ecPrivateKey = (ECPrivateKey) kf.generatePrivate(spec); + System.out.println("=====새로운 시도 7====="); + System.out.println("=====새로운 시도 7.5====="+ecPrivateKey.getS()); + JWSSigner jwsSigner = new ECDSASigner(ecPrivateKey.getS()); + System.out.println("=====새로운 시도 8====="); + jwt.sign(jwsSigner); + System.out.println("=====새로운 시도 9====="); + }catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + }catch (InvalidKeySpecException e){ + e.printStackTrace(); + } + + } +// catch (InvalidKeyException e) { +// e.printStackTrace(); +// } + catch (JOSEException e) { + e.printStackTrace(); + } + + return jwt.serialize(); + } + + /** + * 파일에서 private key 획득 + * + * @return Private Key + */ + private byte[] readPrivateKey() { + + ClassPathResource resource = new ClassPathResource(KEY_PATH); + + +// Resource resource = new ClassPathResource(KEY_PATH); + byte[] content = null; + System.out.println("=====새로운 시도 2====="); + try (Reader keyReader = new InputStreamReader(resource.getInputStream()); + + PemReader pemReader = new PemReader(keyReader)) { + { + System.out.println("=====새로운 시도 3====="); + PemObject pemObject = pemReader.readPemObject(); + System.out.println("=====새로운 시도 3.5====="); + content = pemObject.getContent(); + System.out.println("=====새로운 시도 4====="); + System.out.println(content+"=====content====="); + } + } catch (IOException e) { + e.printStackTrace(); + } + + return content; + } + + /** + * 유효한 code 인지 Apple Server에 확인 요청 + * Apple Document URL ‣ https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens + * + * @return + */ + public TokenResponse validateAuthorizationGrantCode(String client_secret, String code) { + + Map tokenRequest = new HashMap<>(); + + tokenRequest.put("client_id", AUD); + tokenRequest.put("client_secret", client_secret); + tokenRequest.put("code", code); + tokenRequest.put("grant_type", "authorization_code"); + tokenRequest.put("redirect_uri", APPLE_WEBSITE_URL); + + return getTokenResponse(tokenRequest); + } + + /** + * 유효한 refresh_token 인지 Apple Server에 확인 요청 + * Apple Document URL ‣ https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens + * + * @param client_secret + * @param refresh_token + * @return + */ + public TokenResponse validateAnExistingRefreshToken(String client_secret, String refresh_token) { + + Map tokenRequest = new HashMap<>(); + + tokenRequest.put("client_id", AUD); + tokenRequest.put("client_secret", client_secret); + tokenRequest.put("grant_type", "refresh_token"); + tokenRequest.put("refresh_token", refresh_token); + + return getTokenResponse(tokenRequest); + } + + /** + * POST https://appleid.apple.com/auth/token + * + * @param tokenRequest + * @return + */ + private TokenResponse getTokenResponse(Map tokenRequest) { + + try { + System.out.println("======tokenRequest"+tokenRequest); + String response = HttpClientUtils.doPost(AUTH_TOKEN_URL, tokenRequest); + System.out.println("======response"+response); + + ObjectMapper objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false); // null허용 설정 추가 + System.out.println("======objectMapper"+objectMapper); + + TokenResponse tokenResponse = objectMapper.readValue(response, TokenResponse.class); + System.out.println("======tokenResponse"+tokenResponse); + + if (tokenRequest != null) { + return tokenResponse; + } + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + + return null; + } + + /** + * Apple Meta Value + * + * @return + */ + public Map getMetaInfo() { + + Map metaInfo = new HashMap<>(); + + metaInfo.put("CLIENT_ID", AUD); + metaInfo.put("REDIRECT_URI", APPLE_WEBSITE_URL); + metaInfo.put("NONCE", "20B20D-0S8-1K8"); // Test value + + return metaInfo; + } + + /** + * id_token을 decode해서 payload 값 가져오기 + * + * @param id_token + * @return + */ + public JSONObject decodeFromIdToken(String id_token) { + + try { + SignedJWT signedJWT = SignedJWT.parse(id_token); + System.out.println("=====payload1"+signedJWT ); + ReadOnlyJWTClaimsSet getPayload = signedJWT.getJWTClaimsSet(); + System.out.println("=====payload2"+getPayload); + ObjectMapper objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false); +// objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT); // null 값 허용 +// objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT); +// Payload payload = objectMapper.readValue(getPayload.toJSONObject().toJSONString(), Payload.class); + JSONObject payload = objectMapper.readValue(getPayload.toJSONObject().toJSONString(), JSONObject.class); + + System.out.println("=====payload3"+payload ); + if (payload != null) { + return payload; + } + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + +} diff --git a/server/src/main/java/com/yogit/server/applelogin/util/ECPrivateKeyImpl2.java b/server/src/main/java/com/yogit/server/applelogin/util/ECPrivateKeyImpl2.java new file mode 100644 index 0000000..ecb82e8 --- /dev/null +++ b/server/src/main/java/com/yogit/server/applelogin/util/ECPrivateKeyImpl2.java @@ -0,0 +1,185 @@ +package com.yogit.server.applelogin.util; + +import sun.security.pkcs.PKCS8Key; +import sun.security.util.*; +import sun.security.x509.AlgorithmId; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.AlgorithmParameters; +import java.security.InvalidKeyException; +import java.security.interfaces.ECPrivateKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.InvalidParameterSpecException; +import java.util.Arrays; + +/** + * Key implementation for EC private keys. + * + * ASN.1 syntax for EC private keys from SEC 1 v1.5 (draft): + * + *
+ * EXPLICIT TAGS
+ *
+ * ECPrivateKey ::= SEQUENCE {
+ *   version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
+ *   privateKey OCTET STRING,
+ *   parameters [0] ECDomainParameters {{ SECGCurveNames }} OPTIONAL,
+ *   publicKey [1] BIT STRING OPTIONAL
+ * }
+ * 
+ * + * We currently ignore the optional parameters and publicKey fields. We + * require that the parameters are encoded as part of the AlgorithmIdentifier, + * not in the private key structure. + * + * @since 1.6 + * @author Andreas Sterbenz + */ +public final class ECPrivateKeyImpl2 extends PKCS8Key implements ECPrivateKey { + + private static final long serialVersionUID = 88695385615075129L; + + private BigInteger s; // private value + private byte[] arrayS; // private value as a little-endian array + private ECParameterSpec params; + + /** + * Construct a key from its encoding. Called by the ECKeyFactory. + */ + ECPrivateKeyImpl2(byte[] encoded) throws InvalidKeyException { + super(encoded); + parseKeyBits(); + } + + /** + * Construct a key from its components. Used by the + * KeyFactory. + */ + ECPrivateKeyImpl2(BigInteger s, ECParameterSpec params) + throws InvalidKeyException { + this.s = s; + this.params = params; + makeEncoding(s); + + } + + ECPrivateKeyImpl2(byte[] s, ECParameterSpec params) + throws InvalidKeyException { + this.arrayS = s.clone(); + this.params = params; + makeEncoding(s); + } + + private void makeEncoding(byte[] s) throws InvalidKeyException { + algid = new AlgorithmId + (AlgorithmId.EC_oid, ECParameters.getAlgorithmParameters(params)); + try { + DerOutputStream out = new DerOutputStream(); + out.putInteger(1); // version 1 + byte[] privBytes = s.clone(); + ArrayUtil.reverse(privBytes); + out.putOctetString(privBytes); + Arrays.fill(privBytes, (byte)0); + DerValue val = DerValue.wrap(DerValue.tag_Sequence, out); + key = val.toByteArray(); + val.clear(); + } catch (IOException exc) { + // should never occur + throw new InvalidKeyException(exc); + } + } + + private void makeEncoding(BigInteger s) throws InvalidKeyException { + algid = new AlgorithmId(AlgorithmId.EC_oid, + ECParameters.getAlgorithmParameters(params)); + try { + byte[] sArr = s.toByteArray(); + // convert to fixed-length array + int numOctets = (params.getOrder().bitLength() + 7) / 8; + byte[] sOctets = new byte[numOctets]; + int inPos = Math.max(sArr.length - sOctets.length, 0); + int outPos = Math.max(sOctets.length - sArr.length, 0); + int length = Math.min(sArr.length, sOctets.length); + System.arraycopy(sArr, inPos, sOctets, outPos, length); + Arrays.fill(sArr, (byte)0); + + DerOutputStream out = new DerOutputStream(); + out.putInteger(1); // version 1 + out.putOctetString(sOctets); + Arrays.fill(sOctets, (byte)0); + DerValue val = DerValue.wrap(DerValue.tag_Sequence, out); + key = val.toByteArray(); + val.clear(); + } catch (IOException exc) { + throw new AssertionError("Should not happen", exc); + } + } + + // see JCA doc + public String getAlgorithm() { + return "EC"; + } + + // see JCA doc + public BigInteger getS() { + if (s == null) { + byte[] arrCopy = arrayS.clone(); + ArrayUtil.reverse(arrCopy); + s = new BigInteger(1, arrCopy); + Arrays.fill(arrCopy, (byte)0); + } + return s; + } + + public byte[] getArrayS() { + if (arrayS == null) { + arrayS = ECUtil.sArray(getS(), params); + } + return arrayS.clone(); + } + + // see JCA doc + public ECParameterSpec getParams() { + return params; + } + + private void parseKeyBits() throws InvalidKeyException { + try { + DerInputStream in = new DerInputStream(key); + DerValue derValue = in.getDerValue(); + if (derValue.tag != DerValue.tag_Sequence) { + throw new IOException("Not a SEQUENCE"); + } + DerInputStream data = derValue.data; + int version = data.getInteger(); + if (version != 1) { + throw new IOException("Version must be 1"); + } + byte[] privData = data.getOctetString(); + ArrayUtil.reverse(privData); + arrayS = privData; + while (data.available() != 0) { + DerValue value = data.getDerValue(); + if (value.isContextSpecific((byte) 0)) { + // ignore for now + } else if (value.isContextSpecific((byte) 1)) { + // ignore for now + } else { + throw new InvalidKeyException("Unexpected value: " + value); + } + } + AlgorithmParameters algParams = this.algid.getParameters(); + if (algParams == null) { + throw new InvalidKeyException("EC domain parameters must be " + + "encoded in the algorithm identifier"); + } + params = algParams.getParameterSpec(ECParameterSpec.class); + } catch (IOException e) { + throw new InvalidKeyException("Invalid EC private key", e); + } catch (InvalidParameterSpecException e) { + throw new InvalidKeyException("Invalid EC private key", e); + } + } +} + diff --git a/server/src/main/java/com/yogit/server/applelogin/util/HttpClientUtils.java b/server/src/main/java/com/yogit/server/applelogin/util/HttpClientUtils.java new file mode 100644 index 0000000..63efdf0 --- /dev/null +++ b/server/src/main/java/com/yogit/server/applelogin/util/HttpClientUtils.java @@ -0,0 +1,120 @@ +package com.yogit.server.applelogin.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.HttpEntity; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class HttpClientUtils { + + private static Logger logger = LoggerFactory.getLogger(HttpClientUtils.class); + private static ObjectMapper objectMapper = new ObjectMapper(); + + public static String doGet(String url) { + String result = null; + CloseableHttpClient httpclient = null; + CloseableHttpResponse response = null; + Integer statusCode = null; + String reasonPhrase = null; + + try { + httpclient = HttpClients.createDefault(); + HttpGet get = new HttpGet(url); + response = httpclient.execute(get); + statusCode = response.getStatusLine().getStatusCode(); + reasonPhrase = response.getStatusLine().getReasonPhrase(); + HttpEntity entity = response.getEntity(); + result = EntityUtils.toString(entity, "UTF-8"); + EntityUtils.consume(entity); + + if (statusCode != 200) { + logger.error(String.format("[doGet]http get url(%s) failed. status code:%s. reason:%s. result:%s", url, statusCode, reasonPhrase, result)); + } + } catch (Throwable t) { + logger.error(String.format("[doGet]http get url(%s) failed. status code:%s. reason:%s.", url, statusCode, reasonPhrase), t); + } finally { + try { + if (response != null) { + response.close(); + } + if (httpclient != null) { + httpclient.close(); + } + } catch (IOException e) { + logger.error(String.format("[doGet]release http get resource failed. url(%s). reason:%s.", url, e.getMessage())); + } + } + + return result; + } + + public static String doPost(String url, Map param) { + String result = null; + CloseableHttpClient httpclient = null; + CloseableHttpResponse response = null; + Integer statusCode = null; + String reasonPhrase = null; + try { + httpclient = HttpClients.createDefault(); + HttpPost httpPost = new HttpPost(url); + httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); + List nvps = new ArrayList<>(); + Set> entrySet = param.entrySet(); + for (Entry entry : entrySet) { + String fieldName = entry.getKey(); + String fieldValue = entry.getValue(); + nvps.add(new BasicNameValuePair(fieldName, fieldValue)); + } + UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nvps); + httpPost.setEntity(formEntity); + response = httpclient.execute(httpPost); + statusCode = response.getStatusLine().getStatusCode(); + reasonPhrase = response.getStatusLine().getReasonPhrase(); + HttpEntity entity = response.getEntity(); + result = EntityUtils.toString(entity, "UTF-8"); + + if (statusCode != 200) { + logger.error(String.format("[doPost]post url(%s) failed. status code:%s. reason:%s. param:%s. result:%s", url, statusCode, reasonPhrase, objectMapper.writeValueAsString(param), result)); + } + EntityUtils.consume(entity); + } catch (Throwable t) { + try { + logger.error(String.format("[doPost]post url(%s) failed. status code:%s. reason:%s. param:%s.", url, statusCode, reasonPhrase, objectMapper.writeValueAsString(param)), t); + } catch (JsonProcessingException e) { + } + } finally { + try { + if (response != null) { + response.close(); + } + if (httpclient != null) { + httpclient.close(); + } + } catch (IOException e) { + try { + logger.error(String.format("[doPost]release http post resource failed. url(%s). reason:%s, param:%s.", url, e.getMessage(), objectMapper.writeValueAsString(param))); + } catch (JsonProcessingException ex) { + } + } + } + return result; + } + +} diff --git a/server/src/main/resources/application.properties b/server/src/main/resources/application.properties new file mode 100644 index 0000000..b0b6d37 --- /dev/null +++ b/server/src/main/resources/application.properties @@ -0,0 +1,15 @@ +logging.level.com.whitepaek.demosigninwithapple=DEBUG + +APPLE.AUTH.TOKEN.URL=https://appleid.apple.com/auth/token +APPLE.PUBLICKEY.URL=https://appleid.apple.com/auth/keys +# redirect url 정보 +APPLE.WEBSITE.URL=https://yogit.world/redirect +APPLE.ISS=https://appleid.apple.com +# client_ID +APPLE.AUD=com.Branch.service +#Team_ID +APPLE.TEAM.ID=9487SKDZZB +# kid +APPLE.KEY.ID=QLHFNT37VK +# key id path : AuthKey_[key_id], 애플 사이트에서 다운 받아야 함 +APPLE.KEY.PATH=static/AuthKey_QLHFNT37VK.p8 \ No newline at end of file diff --git a/server/src/main/resources/static/AuthKey_QLHFNT37VK.p8 b/server/src/main/resources/static/AuthKey_QLHFNT37VK.p8 new file mode 100644 index 0000000..59a3419 --- /dev/null +++ b/server/src/main/resources/static/AuthKey_QLHFNT37VK.p8 @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg6bpEOxRZ0VuuNgJ6 +drfzrk9/9v0uNhEaXqpqpiEdT1OgCgYIKoZIzj0DAQehRANCAARB7v5hC6Pv68oB +H/gxjfHwjDrLTQBzXqWhjjVjK8UOf0vhGH2uGQ09KfFdt1JSfFJHycpkF4+VkwsQ +yDh9qWTX +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/server/src/main/resources/templates/index.html b/server/src/main/resources/templates/index.html new file mode 100644 index 0000000..15f03c3 --- /dev/null +++ b/server/src/main/resources/templates/index.html @@ -0,0 +1,22 @@ + + + + + Apple sign-in + + +

웰컴 페이지

+
+ + + + \ No newline at end of file