WHITEPAEK Tech Docs

Total : 1,085,179 Today : 72 Yesterday : 446

[2] 스프링 프로젝트에 애플 로그인 API 연동하기

[1] 스프링 프로젝트에 애플 로그인 API 연동을 위한 Apple Developer 설정

[2] 스프링 프로젝트에 애플 로그인 API 연동하기 - (현재 글)

 

이전 포스트에서 "Sign in wtih Apple" 연동을 위한 Apple Developer 3가지 설정을 진행하였습니다.

설정을 통해서 필요한 데이터 준비가 끝났으므로 프로젝트에 설정하여 확인해보도록 하겠습니다.

 

먼저, 깃허브에 가서 코드를 다운로드하도록 합니다. (깃허브에서 코드를 다운로드하는 자세한 설명을 생략하도록 하겠습니다.)

깃허브 URL ‣ github.com/WHITEPAEK/demo-sign-in-with-apple 

 

macOS Catalina 버전 10.15.6 운영체제에서 IntelliJ IDEA Ultimate 환경에서 Spring Boot 프로젝트로 포스트를 설명하도록 하겠습니다.

 

JWT 관련 라이브러리는 "nimbus-jose-jwt(v3.10)"를 이용했습니다. 해당 버전의 라이브러리를 이용한 이유는 애플 로그인 API를 적용시킬 프로젝트에서 사용하고 있기 때문에 그대로 유지하여 사용하기로 결정하였기 때문입니다.


[그림 1] application.properties 설정

프로젝트 다운로드하여 실행하였다면 application.properties 파일로 이동해주세요.

이전 포스트에서 설정 후 얻은 값을 아래와 동일한 위치에 입력해주면 됩니다.

 

# [그림 1.8] Team ID 값 입력
APPLE.TEAM.ID=[Team ID]

# [그림 2.5] Return URLs 값 입력
APPLE.WEBSITE.URL=[Website URLs]

# [그림 2.6] Identifier 값 입력
APPLE.AUD=[Client ID]

# [그림 3.5] Key ID 값 입력
APPLE.KEY.ID=[Key ID]

# [그림 3.5]에서 다운로드 받은 Private Key 파일을 해당 위치로 이동 후, 파일명 입력
APPLE.KEY.PATH=static/AuthKey_[KeyID].p8

[그림 2] "localhost:8080/" 접속

프로젝트를 실행 후 "localhost:8080/"으로 접속하면 Sign in with Apple 로그인 화면이 정상적으로 실행됩니다.

그리고 앞서 application.properties에 설정이 정상적이라면 로그인을 진행하고 값을 반환받을 수 있습니다.

 

프로젝트 실행은 설정 값만 적용한다면 쉽게 진행할 수 있습니다.

간단하게 애플 로그인 프로세스와 코드에 대해서 설명드리도록 하겠습니다.


 

[1] 애플 로그인 버튼 페이지

[Apple Document]

‣ developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple

 

// AppleController.java - 30 라인

@GetMapping(value = "/")
public String appleLoginPage(ModelMap model) {

  Map<String, String> 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"));

  return "index";
}

["localhost:8080/" - Sign in with Apple] 애플 로그인 버튼이 보이는 화면입니다.

유저가 버튼을 클릭하면 로그인이 진행되는데 이때 메타정보와 유저 아이디, 비밀번호가 애플에게 요청됩니다.

 

필드 설명
ID 유저 아이디
Password 유저 비밀번호
appleid-signin-client-id Services ID - Identifier 값
appleid-signin-scope 애플에게 전달받을 유저 정보 - name email
appleid-signin-redirect-uri Services ID - Return URLs 값
appleid-signin-state 상태 값
appleid-signin-nonce 임시 값

 

[2] 유저 로그인 후 정보 받기

[Apple Document]

‣ developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple

 

// AppleController.java - 69 라인

@PostMapping(value = "/redirect")
@ResponseBody
public TokenResponse servicesRedirect(ServicesResponse serviceResponse) {

  if (serviceResponse == null) {
  	return null;
  }

  String code = serviceResponse.getCode();
  String client_secret = appleService.getAppleClientSecret(serviceResponse.getId_token());

  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("================================");

  return appleService.requestCodeValidations(client_secret, code, null);
}

정의된 7개의 데이터와 함께 "https://appleid.apple.com/auth/authorize" 호출되고

애플은 Services ID에 정의된 Return URLs로 JSON 데이터를 반환합니다.

 

 

{
  "state":"test",
  "code":"c50d317be38c742c0beb19d8743de014c.0.nruy.1NtQvAmp9uhyrsMj1mp7kg",
  "id_token":"eyJraWQiOiI4NkQ4OEtmIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLndoaXRlcGFlay5zZXJ2aWNlcyIsImV4cCI6MTU5ODgwMDEyOCwiaWF0IjoxNTk4Nzk5NTI4LCJzdWIiOiIwMDAxNDguZjA2ZDgyMmNlMGIyNDgzYWFhOTdkMjczYjA5NzgzMjUuMTcxNyIsIm5vbmNlIjoiMjBCMjBELTBTOC0xSzgiLCJjX2hhc2giOiJ1aFFiV0gzQUFWdEc1OUw4eEpTMldRIiwiZW1haWwiOiJpNzlmaWl0OWIzQHByaXZhdGVyZWxheS5hcHBsZWlkLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImlzX3ByaXZhdGVfZW1haWwiOiJ0cnVlIiwiYXV0aF90aW1lIjoxNTk4Nzk5NTI4LCJub25jZV9zdXBwb3J0ZWQiOnRydWV9.GQBCUHza0yttOfpQ-J5OvyZoGe5Zny8pI06sKVDIJaQY3bdiphllg1_pHMtPUp7FLv3ccthcmqmZn7NWVoIPkc9-_8squ_fp9F68XM-UsERKVzBvVR92TwQuKOPFr4lRn-2FlBzN4NegicMS-IV8Ad3AKTIRMIhvAXG4UgNxgPAuCpHwCwEAJijljfUfnRYO-_ywgTcF26szluBz9w0Y1nn_IIVCUzAwYiEMdLo53NoyJmWYFWu8pxmXRpunbMHl5nvFpf9nK-OGtMJrmZ4DlpTc2Gv64Zs2bwHDEvOyQ1WiRUB6_FWRH5FV10JSsccMlm6iOByOLYd03RRH2uYtFw",
  "user":"{
    \"email\":\"i79fiit9b3@privaterelay.appleid.com\",
    \"name\":{
      \"firstName\":\"SEUNGJOO\",
      \"lastName\":\"PAEK\"
    }
  }"
}

반환받은 JSON 데이터는 "state, code, id_token, user" 4개의 키로 이루어져 있습니다.

여기서 알고 있어야 할 부분은 user 키는 유저가 서비스 최초 가입할 때만 받을 수 있습니다.

또한, 유저는 자신의 email을 공유할 수도 있고, 하지 않을 수도 있습니다.

(JSON 데이터는 유저가 email을 공유하지 않은 데이터이며, "code" 키의 값은 5분 동안 유효합니다.)


 

[3] id_token 5가지 유효성 검증

[Apple Document]

‣ developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/verifying_a_user

‣ developer.apple.com/documentation/sign_in_with_apple/fetch_apple_s_public_key_for_verifying_token_signature

 

// AppleUtils.java - 69 라인

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;
}

애플에게 로그인 유저에 대한 정보를 JSON 데이터로 받은 후

"id_token" 값을 decode 하여 "RSA, exp, nonce, iss, aud" 5가지의 검증 절차를 진행합니다.

 

 

[JWT]

jwt.io

 

[그림 3] id_token을 Decoded

"exp, nonce, iss, aud"의 값은 "id_token" 값을 decode 하면 PAYLOAD 영역에 존재합니다.

 

 

 

[그림 4] id_token을 공개키 서명 확인

RSA 검증은 "[GET] https://appleid.apple.com/auth/keys"를 호출하여 공개키 리스트를 받은 후

"id_token" 값의 HEADER 영역의 kid와 동일한 공개키 데이터로 서명 확인을 진행합니다.

 

exp id_token 만료 시간 (10분)
iss https://appleid.apple.com
aud Services ID - Identifier 값
nonce 생성된 임의 값
RSA Apple에서 제공받은 Public Key

 

[4] client_secret 생성

[Apple Document]

developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens

 

// AppleUtils.java - 131 라인

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 ECPrivateKeyImpl(readPrivateKey());
            JWSSigner jwsSigner = new ECDSASigner(ecPrivateKey.getS());

            jwt.sign(jwsSigner);

        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (JOSEException e) {
            e.printStackTrace();
        }

        return jwt.serialize();
}

[3]에서 5가지의 검증 절차가 정상적으로 완료되었다면 client_secret을 생성해주도록 합니다.

client_sercret은 JWT로 생성되며 필요한 값은 아래와 같습니다.

 

kid 애플에서 생성한 Private Key에 대한 Key ID
alg ES256
iss App ID 생성에 사용된 Team ID
iat 생성 시간
exp 만료 시간
aud https://appleid.apple.com
sub Services ID - Identifier 값

위의 데이터로 client_secret의 JWT가 생성되었다면,

마지막으로 애플에서 다운로드한 Key 파일 안에 들어있는 Private Key로 서명을 해주면

client_secret이 정상적으로 생성 완료됩니다.


[5] 토큰 검증 및 발급

[Apple Document]

 developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens

developer.apple.com/documentation/sign_in_with_apple/tokenresponse

 

// AppleUtils.java - 189 라인

public TokenResponse validateAuthorizationGrantCode(String client_secret, String code) {

        Map<String, String> 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);
}

[2]에서 전달받은 code와 [4]에서 생성한 client_secret의 값 그리고  "client_id, grant_type, redirect_uri" 값으로

"[POST] https://appleid.apple.com/auth/token"을 호출하여 권한 부여를 위한 토큰 검증을 진행하도록 합니다. 

("code"는 5분간 유효한 값이므로 주의하도록 한다.)

 

client_id Services ID - Identifier 값
client_secret eyJraWQiOiJWTTJOOFMzN1RSIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJjb20ud2hpdGVwYWVrLnNlcnZpY2VzIiwiYXVkIjoiaHR0cHM6XC9cL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiODNNNlk1QllLVCIsImV4cCI6MTU5ODgwNTU2NSwiaWF0IjoxNTk4ODAxOTY1fQ.2HO_p7883orlgHS4GQ893haS8SLbRBGLhxNSCZl2i1bwc8uTZSEn4gCQcmvwCqs6lN7zRiUGE5iLQvqNlkJNPQ
code c3944a20072b7446b97633646556204f8.0.rruy.Gjgud84EqqpCvP31MrudDw
grant_type authorization_code
redirect_uri Services ID - Return URLs 값

"[POST] https://appleid.apple.com/auth/token" 호출이 정상적으로 완료되면 JSON 데이터를 반환받습니다. 

 

{
  "access_token":"a08c1600e80f84d44842ce3342abac413.0.mruy.IyMPSXmTYtMyUCDWDKKN3g",
  "expires_in":3600,
  "id_token":"eyJraWQiOiJlWGF1bm1MIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLndoaXRlcGFlay5zZXJ2aWNlcyIsImV4cCI6MTU5ODgwMjU2NiwiaWF0IjoxNTk4ODAxOTY2LCJzdWIiOiIwMDAxNDguZjA2ZDgyMmNlMGIyNDgzYWFhOTdkMjczYjA5NzgzMjUuMTcxNyIsIm5vbmNlIjoiMjBCMjBELTBTOC0xSzgiLCJhdF9oYXNoIjoiaFNMOFBrZWxoNWdFblNGeURISGNIQSIsImVtYWlsIjoiaTc5ZmlpdDliM0Bwcml2YXRlcmVsYXkuYXBwbGVpZC5jb20iLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJpc19wcml2YXRlX2VtYWlsIjoidHJ1ZSIsImF1dGhfdGltZSI6MTU5ODgwMTk2Miwibm9uY2Vfc3VwcG9ydGVkIjp0cnVlfQ.WqYWPuTi8apdqQnP9V6-yvVLBt84P48mYVbGa0e3io4sNKL919iIVZfNoE1GZ8F6WNOrXtcOQU_n3hclrfmNyYsidj-IH6R-0JwxwLobKJoFNH7lfKd067OyiYGxHJMFcleRaDoRWsBF4Wh_FUT3Nft_qy2CVd9pNEg-mFOruI6-5oUDdnPQwelywNCsqlkmECcKna4Psvs9eRn58ALfpke5SL-A762--peGzgp00RvrGMK4t26UWG9UN13LIXDvX3ydMCdg8gvmO7BizSoi4zJHgvKuYxLLT_heOkvNWxcT81h7dEdwqAczLTE3FBarTkAekxvtykVwlEBlfyoXcg",
  "refresh_token":"r8e88bc9f62bc496398b71117610c5aeb.0.mruy.UuuL5tpwnWaof86XPErqJg",
  "token_type":"Bearer"
}

반환받은 JSON 데이터에서 "id_token"을 decode 하여 필요한 유저 정보를 얻을 수 있습니다.


[6] refresh_token 검증 및 토근 재발급

[Apple Document]

 developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens

 developer.apple.com/documentation/sign_in_with_apple/tokenresponse

 

// AppleUtils.java - 210 라인

public TokenResponse validateAnExistingRefreshToken(String client_secret, String refresh_token) {

        Map<String, String> 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);
}

[5]에서 전달받은 "refresh_token"에 대한 유효성 검증을 하고 싶다면

"client_id, client_secret, grant_type, refresh_token"의 값으로

"[POST] https://appleid.apple.com/auth/token" 호출하여 검증을 진행합니다.

 

client_id Services ID - Identifier 값
client_secret eyJraWQiOiJWTTJOOFMzN1RSIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJjb20ud2hpdGVwYWVrLnNlcnZpY2VzIiwiYXVkIjoiaHR0cHM6XC9cL2FwcGxlaWQuYXBwbGUuY29tIiwiaXNzIjoiODNNNlk1QllLVCIsImV4cCI6MTU5ODgwNTU2NSwiaWF0IjoxNTk4ODAxOTY1fQ.2HO_p7883orlgHS4GQ893haS8SLbRBGLhxNSCZl2i1bwc8uTZSEn4gCQcmvwCqs6lN7zRiUGE5iLQvqNlkJNPQ
grant_type refresh_token
refresh_token r8e88bc9f62bc496398b71117610c5aeb.0.mruy.UuuL5tpwnWaof86XPErqJg

"refresh_token"에 대한 "[POST] https://appleid.apple.com/auth/token" 호출이 정상적으로 완료되면

JSON 데이터를 반환받습니다.

 

{
  "access_token":"aebbe3a8249d745d486af1573ac74b821.0.mruy.lamBfraOUXnmnhxb4NxjAA",
  "expires_in":3600,
  "id_token":"eyJraWQiOiI4NkQ4OEtmIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLndoaXRlcGFlay5zZXJ2aWNlcyIsImV4cCI6MTU5ODgwMzYxOSwiaWF0IjoxNTk4ODAzMDE5LCJzdWIiOiIwMDAxNDguZjA2ZDgyMmNlMGIyNDgzYWFhOTdkMjczYjA5NzgzMjUuMTcxNyIsImF0X2hhc2giOiJnN0N3WnhXSTBvOW5wdUhvSjE3azRBIiwiZW1haWwiOiJpNzlmaWl0OWIzQHByaXZhdGVyZWxheS5hcHBsZWlkLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImlzX3ByaXZhdGVfZW1haWwiOiJ0cnVlIn0.R77ost9PyivMguFyXYLyng-RZbH9lPq_GAA-35cr6DtanfInCg4UtlxFmoaGV6_euxK7vCVu_32vEiEkrktJwJF7H1RSOdQ8JBgBZt6Qdnr4hR_vfQTpb6D1JMoiLD_GCmjz_rfwvI0ityON1yjBCuYsJbV6RMasrNw6LvWueqAQ0v_nls68gxAqwGR0XOtl9SwK7CJK7Nj-BqJMcQT_H3sw8QM6zw1XopuEqSk7Ci8Qirh8Z36a9oSfilgXs7vT-H99CGk50HkYkLU9-DawoyaWc_iAMC3ROAo_WvxI_tlPs9CjtwZNXlBvz4ExLl9zVgAM9Rh8oE5R2evXeag3SQ",
  "token_type":"Bearer"
}

반환받은 JSON 데이터에서 "id_token"을 decode 하여 필요한 유저 정보를 얻을 수 있습니다.


대략적인 애플 로그인(Sign in with Apple) 연동에 대해서 설명했습니다.

코드와 같이 풀어서 쉽게 설명하려고 했으나 생각보다 더 복잡하고 헷갈릴 수 있을 거 같습니다..^^;;

Apple Developer Documentation을 참고하며 코드를 보면서 이해하신다면 포스트보다 이해하기가 좀 더 수월하실 거라 생각합니다.

 

[Sign in with Apple - Step by step !! (정리)]

[Step. 01] "Sign in with Apple" 버튼이 있는 애플 로그인 페이지

[Step. 02] "https://appleid.apple.com/auth/authorize" 호출 - (유저 로그인)

[Step. 03] "https://appleid.apple.com/auth/keys" 공개 키 호출 및 검증 - (rsa, exp, nonce, iss, aud)

[Step. 04] "client_secret" 생성 - (jwt + private key)

[Step. 05] "https://appleid.apple.com/auth/token" 호출 - (authorization_code)

[Step. 06] "https://appleid.apple.com/auth/token" 호출 - (refresh_token)

 


[추가 내용(1) - 이메일 변경, 서비스 해지, 애플 계정 탈퇴 이벤트가 발생한 경우]

// AppleController.java - 108 라인

@PostMapping(value = "/apps/to/endpoint")
    @ResponseBody
    public void appsToEndpoint(@RequestBody AppsResponse appsResponse) {
        logger.debug("[/path/to/endpoint] RequestBody ‣ " + appsResponse.getPayload());
}

유저의 애플 계정에 대한 이벤트가 발생하면 body안에 payload 키로 jwt 형태의 데이터가 담겨서

"App ID에 등록된 Endpoint URL"로 전송됩니다.

 

"payload" : "eyJraWQiOiI4NkQ4OEtmIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnNrdC5tYXNzaXZlYXJzaWduaW4iLCJleHAiOjE1OTg0MzU0MTIsImlhdCI6MTU5ODM0OTAxMiwianRpIjoiZDNvTUVfWE1tcjVqcC1KWlRMUHVIUSIsImV2ZW50cyI6IntcInR5cGVcIjpcImNvbnNlbnQtcmV2b2tlZFwiLFwic3ViXCI6XCIwMDAxNDguYzEyZjdlNmI4Yjk2NDExNGEzYzRiZTdmYzY5M2I0MzYuMDgxNlwiLFwiZXZlbnRfdGltZVwiOjE1OTgzNDg5ODQyMTJ9In0.EDWOfSnbdBDdNVGeSL7KBymsYV8NkcRz9XY1mCPIcWrOABK1tVLyLqdlclRwqD4lBKeJgGS74tE_YMAx1Z9iJcZVjL_56OuCbvKRge_-RXn8PLXbTJWONh8PBrsNSrbwZvykwLbKEcKfhNWcJWzoPNsuKLxVS5aPSQ59OjaegP8WNzYpRfDwO-f7prWSHPnkEO4vv6WQvYmKD3zQMl4DqkZwAIbWCv4TzAKP3h-vzy4RVNghF1WBrAG4MANdVY_Y6gJdvDnAy1tcghqpD5k13r04PaWpT3GU1QI--ps0EaNLekSJxrNUDZrD9bUxb1fLEthoQv4v4lcR_Y2xlp_Pbg"

payload의 값은 jwt이므로 decode 하면 HEADER와 PAYLOAD 데이터 영역으로 나뉩니다.

 

--HEADER--
{
  "kid": "86D88Kf",
  "alg": "RS256"
}

--PAYLOAD--
{
  "iss":"https:\/\/appleid.apple.com",
  "aud":"com.whitepaek.services",
  "exp":1598929977,
  "iat":1598843577,
  "events":"{
    \"type\":\"consent-revoked\",
    \"sub\":\"000148.f06d822ce0b2483aaa97d273b0978325.1717\",
    \"event_time\":1598843567475
  }",
  "jti":"uZmGepCUZyA_0by3Jh7JCQ"
}

유저가 서비스 해지를 한 경우,

전달된 payload의 값을 decode 한 결과입니다.

 


[추가 내용(2) - 애플 로그인 페이지]

// AppleController.java - 48 라인

@GetMapping(value = "/apple/login")
    public String appleLogin(ModelMap model) {

        Map<String, String> 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");

        return "redirect:https://appleid.apple.com/auth/authorize";
}

추가적으로 앱(App)에 Sign in with Apple 버튼이 존재하는 페이지가 아닌

애플 로그인 페이지 화면을 제공해야 하는 경우에는

"https://appleid.apple.com/auth/authorize"를 redirect 해주면

ID와 Password를 입력하는 화면으로 바로 이동됩니다.

 

[그림 5] Apple ID 로그인 화면으로 이동


이상으로 Sign in with Apple API를 Spring 프로젝트에 연동 방법에 대해 알아봤습니다.

잘못된 내용 혹은 궁금하신 점은 댓글 남겨주시면 확인 후 답변드리도록 하겠습니다.

감사합니다 :)

댓글(50)

  • 이전 댓글 더보기
  • 진짜초보개발자
    2020.10.14 18:08

    안녕하세요.
    좋은 포스팅 덕분에 무사히 로그인 회원가입 등을 구현하였습니다.

    추가적인 질문이 있어 글을 남깁니다. 포스팅 중에 [이메일 변경, 서비스 해지, 애플 계정 탈퇴 이벤트가 발생한 경우]
    이 Notification 관련 기능 구현이 있어 구현을 해보았는데요.
    기기에서 암호 보안->apple id를 사용하는 앱 -> apple id 사용중단
    식으로 진행을 하여두 noti가 들어오지 않아서요 .

    제가 찾아본결과 문서도 없는거 같은데 [이메일 변경, 서비스 해지, 애플 계정 탈퇴 이벤트가 발생한 경우] 이 기능관련하여 어디서 정보를 볼 수 있을까요 ?

    추가로 회원이 앱에서 탈퇴를 할때 서버에서 apple 쪽에 Unlink(apple id 사용중단 ) 요청을 보내는 기능이 존재하는지 아시나요 ? 문서쪽에서는 없는거 같아서요 ..

    • 2020.10.19 15:14 신고

      Server to Server Notification Endpoint에 대한 자세한 내용은 저도 좀 더 찾아봐야 할 거 같습니다.
      제가 확인한 부분은 Endpoint에 등록된 URL로 body안에 "payload" 키로 JWT 형태의 토큰의 값을 담아서 보내주는 건 확인했습니다.
      IAP 부분을 구현 중인데 notification 관련해서 같이 알아보도록 해야겠네요~

  • 개발
    2020.10.16 10:02

    안녕하세요~
    좋은 자료 감사드립니다.
    "/apple/login" 을 통해서 로그인을 한후에 redirect URL로 응답이 왔는데 ServicesResponse에 데이터가 모두 null 입니다.
    개발자 페이지를 보니 콜백쪽 request에 http body에 정보를 담아준다고 나와서 request에서도 확인해봤는데 아무런 데이터가 없습니다.
    RedirectURL 설정시 http로 했는데 그래서 그런걸까요??

    • 2020.10.19 15:20 신고

      SSL 적용이 되어 있어야 정상적으로 redirect가 되었던 걸로 기억합니다~

  • 초보자
    2020.10.18 23:43

    AppleUtils.java 파일에 146번 라인
    ECPrivateKey ecPrivateKey = new ECPrivateKeyImpl(readPrivateKey());
    에서 ECPrivateKeyImpl 이 import되지 않습니다.

    import sun.security.ec.ECPrivateKeyImpl; 가 찾질 못하는데 어떡해야 할까요..???

    • 2020.10.19 15:26 신고

      글쎄요.. 정확한 메시지가 없어서 말씀드리기가 힘드네요.
      Reload All Maven Projects를 해보시겠어요?

    • 2020.12.01 22:38

      저도 같은 오류입니다.

      java: package sun.security.ec does not exist
      라고 뜨네요

      패키지가 더는 사용되지 않는 것 같습니다ㅠ 어떤 라이브러리를 이용하여 수정할 수 있을까요?

  • 2020.10.29 18:23

    Apple OAuth 에서는 사용자 정보를 호출하는 api 가 별도로 없는건가요?
    페이스북이면 https://graph.facebook.com/v3.0/me 와 같은..

    • 2020.10.30 09:40 신고

      네, 애플 로그인 관련해서는 위 내용이 전부인 걸로 파악했습니다.

  • 2020.10.30 14:03

    안녕하세요. 포스팅 하신 내용 잘보았습니다. 궁금한점이 있어 여쭤봅니다

    1. 로그인 요청시(apple id server로) 특정 정보를 담아서 같이 요청/응답이 가능한가요?
    index.html 내 init 코드를 보면 state 요소에 "test" 를 입력하여 요청, apple id 서버 응답시 응답 요소중 state 에 대한 값은 요청시 입력한 값이 그대로 응답해주는거 같습니다.
    ([2] 유저 로그인 후 정보 받기, 두번째 스크린샷)
    그렇다면 state 요소에 apple id 서버로 요청시 필요한 특정 정보에 대해 보내고 받을수 있는건지요..

    2. localhost에서 return url을 받을수 있나요?
    return url은 https 프로토콜에서만 가능한지요?

    • 2020.11.01 16:07 신고

      1. state에 입력한 값이 넘어가기 때문에 적절한 데이터로 넣어서 활용할 수 있을 거 같습니다.
      2. Service ID 등록에서 "Return URLs"를 말씀하시는 거라면 자신의 IP로 받을 수 있습니다.

  • 초보
    2020.11.11 09:27

    안녕하세요. 포스트 내용이 정리가 잘 되어있어 참고하여 개발하고 있습니다. 개발중 궁금한게 있어 문의드립니다.

    1. nonce 는 임의 값이라고 하는데 정말 아무런 값이나 넣어도 상관없을까요? 이를테면 UUID 같은거요.
    2. 저는 단순히 사용자 인증 차원에서의 로그인만 필요하고 실제 세션관리는 서비스의 로그인 컨트롤로러 세션을 관리합니다. 이럴 경우 2번까지만 진행하고 3번 이후 작업(ID 토큰 검증) 은 하지 않아도 되는건가요? (이 경우 client_secret 생성도 필요 없는지요)
    3. 다른 SNS 로그인을 보면 해당 계정의 고유 식별자 값을 주는데요, 포스트의 예시를 보면 ID_TOKEN 값은 있지만 고유 식별자는 따로 보이지 않습니다, 혹 ID 토큰값이 유저의 고유식별값이 되는건가요?

    • 2020.11.11 10:03 신고

      1. UUID로 생성한 값을 설정하여 검증하는 방식이 안전합니다.
      2. 네, 제가 개발한 서비스도 id_token 검증 과정은 생략하고 [2]번에서 전달받은 유저 정보로 세션을 관리하고 있습니다.
      (client_secret 생성도 사용하지 않고 있습니다.)
      3. 유저의 고유한 식별 값은 전달받은 id_token을 디코딩 후 "sub"으로 식별하시면 됩니다.

  • 초보
    2020.11.11 10:50

    답변 감사합니다. 추가로 하나만 더 여쭤보겠습니다.
    1번 code 값을 받아오기 위하여 아래와 같은 url로 호출하였습니다. 결과로 403 error 가 발생하는데요, 권한문제인걸로 아는데
    아래 리턴url은 이전 포스팅에서 알려주신대로 개발자 계정에 추가한 상태입니다. 혹 다른 권한 설정같은 것이 있는건가요? 아니면 아래 uri 형식이 잘못된 걸까요?

    uri = "https://appleid.apple.com/auth/authorize?"
    + "client_id=서비스ID(IDENTIFIER) "
    + "&redirect_uri=http://127.0.0.1:port/test"
    + "&nonce="+UUID.randomUUID().toString()
    + "&response_type=code%20id_token"
    + "&scope=name%20email"
    + "&response_mode=form_post";

    • 2020.11.11 11:00 신고

      redirect_uri를 localhost(127.0.0.1)로 설정하면 안됩니다.

  • 초보
    2020.11.11 13:46

    그렇다면 개발 중에도 본인 실제 ip를 등록해야하는건가요?
    서버가 사내 내부 인터넷을 사용하고 있다면 내부망 아이피가 아니라 외부로 통하는 NAT IP로 설정을 해줘야하는것 같네요. 내부망 IP로도 접근이 안되는거보면... 이 경우 방화벽 문제가 있을 수도 있겠네요.
    아... 다른 SNS 로그인들은 localhost가 다 사용 가능해서 애플도 될 줄 알았는데 착각이었네요. 괜히 로컬호스트 등록이 안되는게 아니었군요... 확실히 다른 소셜 oauth 랑은 차이가 있네요.
    작성자님 덕분에 많이 배우고 갑니다.

  • 초보
    2020.12.02 00:14

    안녕하세요. 개발 중 많은 참고가 되었습니다. 정말 감사합니다.
    개발 중 궁금한게 있는데요, 2번에서 얻는 ID_TOKEN 값을 디코딩하면 5가지 값이 나온다고 하셨는데 게중에 sub 라는 항목이 애플에서 관리하는 유저 고유 키값으로 알고 있는데요,
    5번 client_secret 과 검증 과정을 통해서 얻은 ID_TOKEN 값과 차이가 있나요? 포스팅 설명을 보면 5번 과정을 거쳐서 얻게되는 ID_TOKEN 값으로 유저정보를 획득하면 된다고 하셨는데 실제로 데이터를 비교해보니 2번 과정 결과로 받은 id_token 값과 5번 과정을 통해서 반환받은 id_token 값은 큰 차이가 없었습니다.(만료시간 같은 시간 값만 다르고 sub 나 다른 값들은 동일 했습니다.)
    제가 궁금한건 애플에서 관리하는 유저의 고유 키값인 sub 값은 2번 과정에서 얻나 5번 과정에서 얻나 같은건가요? 그렇다면 굳이 리프레시 토큰이나 토큰을 유지할 필요가 없는 시스템에서는 2번 과정에서 얻는 유저의 정보(email 과 이름) 와 id_token을 디코딩 해서 얻는 sub(애플에서 관리하는 유저의 고유값) 만으로 프로세스를 종료해도 될까요?

    • 2020.12.02 09:28 신고

      refresh_token이 필요하지 않은 서비스에서 굳이 5번 과정까지 프로세스를 구현하지 않아도 됩니다. 말씀하신 대로 sub 값은 동일하며, 2번 과정까지만 프로세스를 진행하고 유저 정보를 획득 후에 프로세스를 종료시켜도 됩니다. (제가 적용한 서비스에서도 2번 과정까지의 프로세스를 진행하고 획득한 유저 정보로 서버에서 세션을 관리하고 있습니다.)

      ‣ 애플 로그인 프로세스 전체를 설명드린 이유는 혹시라도 필요하신 분이 있을까 하여 제가 파악하고 이해한 프로세스를 전부 작성하였기 때문에 필요한 부분만 이해하시고 응용하셔도 무방합니다.

  • 개발해야해
    2021.01.26 12:56

    감사합니다!!
    Apple Id로 로그인 구현이 필요한데 참조하면서 진행할께요!
    정리가 너무 잘되어있습니다.

  • 개발해야해
    2021.02.08 16:19

    안녕하세요 추가적으로 궁금한 사항이 있어서 문의드립니다.
    타 3party의 Social Login구현시에는 (Google, Kakao, Naver 등..) 은 로그인과정으로 A) access_Token을 획득, B) access_Token으로 Profile요청 이런 과정이 있는데.신기하게 Apple Id 로그인은 code 획득단계에서, 반환되는 id_token을 활용하면, id_token안에 profile정보가 가 들어있는것으로 보입니다.

    혹시 5)번 과정을 마치고 나온 결과물인 accessToken은 어떻게 활용하는지 궁금합니다. 위에서 B)access_Token으로 Profile요청과 같은 행위는 없는건가요?..

    • 2021.02.08 22:05 신고

      전달받은 결과에 대해 전부 작성해둔 걸로 기억하는데.. 해당 부분은 다시 확인해보셔야 할 거 같습니다^^; 기억이 흐릿하네요..ㅠ
      애플은 다른 소셜 로그인과 다르게 제한적으로 제공했던 걸로 기억합니다..

  • 은쩡
    2021.03.07 00:10

    애플로그인 개발시에 맥에서 개발 하였나요? 윈도우에서 개발시에 사파리에서 테스트 시
    애플버튼 실행해서 애플서버쪽 실행이 될때 소켓 에러가 나네요

  • 선인장
    2021.04.04 22:02

    로그인 완료 후 email에 대한 값을 추가로 가져오고 싶은데, scope을 줘도 email에 대한 값이 안넘어오네요. 다른 값을 추가로 줘야하나요??

    • 2021.04.05 02:16 신고

      전달받은 id_token 값을 decode 하여 email 값을 얻을 수 있습니다.

    • 2021.04.05 17:45

      답변 감사합니다. 제가 짠 프로젝트나 위 git 프로젝트를 가져와도 decode했을 때 email이 안 넘어와서 여쭙니다. 사용자가 email을 제공하지 않는 걸 선택했기 때문인 거 같은데 해당 부분에 대해서 어떻게 대처하고 계신 게 있으신가요?

    • 2021.04.06 09:30 신고

      사용자가 이메일 비공개로 설정한 경우에는 임의 이메일 주소를 받을 수 있습니다.
      해당 이메일 주소로 메일을 발송하면 사용자가 수신할 수 있습니다.
      현재 와서 이메일 주소를 제공하지 않는 옵션이 생겼는지는 확인 안 해봐서.. 문서 확인해보시는 게 빠를 듯 하네요 ^^

      [참고]
      https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/communicating_using_the_private_email_relay_service

  • 개발해야해
    2021.04.28 14:18

    안녕하세요, 추가적으로 궁금한 내용이 또 생겼네요.
    sign in with apple을 구현을 하였는데, 로그인한 에블 계정에 따라서 IdToken의 "email" , "email_verified" 필드가 포함되기도하고, 포함이 안되기도 합니다.
    최종적으로 email일 필드값을 활용해야하는데 이런 경우가있어서 난처하네요, 혹시 위와 같은 상황 있었을까요?..

    • 2021.05.07 00:49 신고

      공식 도큐먼트를 확인해보시는 게 정확할 거 같습니다 ^^;

    • 2021.09.02 15:01

      아마 최초 로그인시에 애플계정(name, email) 정보를 공유함, 공유안함 선택할 수 있는데 공유하지 않으면 값이 없을겁니다

  • 일꾼개미
    2021.05.27 23:50

    안녕하세요.
    다름이 아니라 질문 드릴것 있어서 이렇게 적게 되었습니다.
    Apple 로그인 페이지를 띄워서 id, password 입력해서 요청을 했는데... redirect callback 응답으로 오는 값에서 state, code는 값이 있는데 id_token, user 값이 들어오지 않습니다.
    왜 그런지 혹시 이유를 알수 있을까요?

    • 2021.09.02 15:00

      처음 요청시 response_type 에 쿼리로 code 만 넣으신거같은데 id_token 도 추가해주시면 되세요 아마도 user 는 최초로그인시에만 날라오는거아닐까요?

  • 2021.06.02 14:33 신고

    소셜 로그인 기능 개발하는데 애플만 달라서 정독했습니다.
    정리 및 소스제공까지 너무 감사합니다.
    타 서비스들이 accessToken으로 간단히 되는데 애플은 정말...ㅜㅜ 변태같내요
    다시 한번 감사하구요 몇번 더 보면서 실제 테스트 해봐야겠내요^^
    아직 mac도 없고 개발자계정 결제도 안했는데 회사에서 이런걸 시켜서...ㅜㅜ

  • 아크
    2021.08.04 11:30

    안녕하세요 문의 드립니다.

    ~/authoize 호출시

    &response_mode=form_post&scope=email 로 전송했는데

    payload값 찍어보면 "email":"[이메일]", 이부분이 통으로 안나오는네요.

    apple developer 사이트에서 email 관련 설정이 있어야 하는 걸까요?

    ~.p8 파일을 적용해야 볼수 있는 걸까요?

  • iphone app
    2021.08.09 10:01

    안녕하세요. 문의드립니다.
    첫 CallBack에서
    state 와 code만 return 되고 id_token은 안보여서요.
    어떻게 확인해야 할까요?

    • 2021.09.02 14:49

      처음 요청시 response_type 에 쿼리로 code 만 넣으신거같은데 id_token 도 추가해주시면 되세요

  • james
    2021.09.02 15:07

    저는 안드로이드 환경에서 애플로그인 지원하려고 참고했는데 많은 힌트가 되었습니다. 감사합니다

  • secret
    2022.04.13 15:00

    Caused by: java.lang.NoSuchFieldError: secp256r1

    저는 이런 에러가 나오는데 혹시 보신분 잇나요?