애플 로그인 및 탈퇴 과정

givepro·2022년 6월 29일
2

apple

목록 보기
1/1

들어가면서


iOS APP 심사 중 아래와 같은 사유로 심사 리젝 응답을 받았다.

💡 Upcoming Requirement Reminder Note: This is a support message regarding upcoming requirements that may be relevant for your app.

Starting June 30, 2022, apps submitted to the App Store that support account creation must also include an option to initiate account deletion.

We noticed this app may support account creation. If it does not, you may disregard this message. If it already offers account deletion or you’re working to implement it, we appreciate your efforts to follow the App Store Review Guidelines. Apps submitted after June 30 that do not comply with the account deletion requirements in guideline 5.1.1(v) will not pass review.

Learn more about the account deletion requirements. If your app offers Sign in with Apple, use the Sign in with Apple REST API to revoke user tokens.

위 내용의 결론은?


Apple 로그인을 제공하는 경우 계정 탈퇴 (삭제) 요청 시 Apple REST API를 호출하여 사용자 토큰을 해지

문제 원인은?


  1. 현재 운영 중인 APP에서 회원 탈퇴의 방식은 Apple Server와 전혀 연동이 되어있지 않았다.

    → 탈퇴 진행 시 회원의 DB만 Update

  2. client_secret을 생성하기 위한 유효한 authorization_code의 부재

    → Apple Server를 호출하려면 Token을 생성해야하는데 client_secret이 필요함

문제 해결하려면?


  1. Apple 로그인 시 authorization_code를 포함하여 API Server에 전달
  2. client_secret을 생성 할 수 있도록 한다. → Apple Developer Key Info 필요
  3. Apple Server API를 접근 하기 위한 access_token 생성
  4. 애플 로그인 회원 탈퇴 시 Apple Server로 사용자 토큰 해지 처리 로직 개발

Apple 로그인 과정


💡 정상적인 애플 로그인 과정은 아래와 같습니다.

APP에서 Apple 로그인을 성공하면 Apple ID Server로부터 회원정보를 응답 받는다.
회원정보의 데이터는 아래 문서에서 확인이 가능하다.
Apple Developer Documentation

여기서 필요한 데이터 항목은 아래와 같다.

  • identityToken
  • authorizationCode

이후 과정은 Server로 위 데이터를 포함하여 회원가입 처리를 진행하고 응답받은 identityToken으로 Signature 검증을 하기 위한 public key를 요청 및 응답 결과를 받는다.

identityToken 서명 검증이 완료되면 apple 고유 account id를 획득 할 수 있다.

이후 로그인에 사용될 token을 요청하기 위해 client_secret를 생성 후 요청하여 token (access_token, refresh_token)을 응답 받는다.

응답받은 token은 APP 측으로 전달되도록 하여 사용한다.

아래는 애플 인증 과정이 정말 잘 정리되어있는 포스팅

Spring API서버에서 Apple 인증(로그인 , 회원가입) 처리하기

우선 현재 운영중인 앱에서의 애플 로그인 방식은 애플의 개발 가이드와 일치하지 않는 것을 확인 할 수 있습니다.
상세한 인증 과정은 아래와 같이 만들어봤습니다.

현재 운영중인 프로젝트의 애플 로그인 과정

정상적인 인증 과정과 비교하면 서명 검증, 토큰에 대한 부분이 누락되어 있습니다.
하지만 위와 같이 진행 한다고 해서 크게 문제가 되지는 않습니다.

기존의 로직에서는 authorization_code를 전달하지 않아서 API Server에서 데이터 사용 할 수가 없어서 이번에 업데이트를 하면서 APP 측에서 전달이 가능하도록 했습니다.

애플 계정 회원 탈퇴 과정


위 문제 해결을 다시 보도록 하겠습니다.

  1. Apple 로그인 시 authorization_code를 포함하여 API Server에 전달
  2. client_secret을 생성 할 수 있도록 한다. → Apple Developer Key Info 필요
  3. Apple Server API를 접근 하기 위한 access_token 생성
  4. 애플 로그인 회원 탈퇴 시 Apple Server로 사용자 토큰 해지 처리 로직 개발

1번의 경우 Apple 로그인 과정 에서 해결했습니다.

그렇다면 client_secret을 생성하려면 어떻게 하는지 먼저 보도록 하겠습니다.

Client_secret 생성 방법

Apple Developer Documentation

문서에 따르면 (해당 문서 하단에 내용 확인 가능)
client_secret은 다음과 같은 내용을 포함하여 JWT형식의 토큰을 생성해야 합니다.

{
    "alg": "ES256",
    "kid": "ABC123DEFG"
}
{
    "iss": "DEF123GHIJ",
    "iat": 1437179036,
    "exp": 1493298100,
    "aud": "https://appleid.apple.com",
    "sub": "com.mytest.app"
}

alg는 ES256를 사용한다.

kid는 Apple Developer 페이지에 명시되어있는 Key ID (10-character, Sign In with Apple)

https://developer.apple.com/account/resources/authkeys/list

iss는 Apple Developer 페이지에 명시되어있는 Team ID (10-character)

https://developer.apple.com/account/#!/membership

iat는 client secret이 생성된 일시를 입력. (현재시간을 주면 된다)

exp는 client secret이 만료될 일시를 입력. (현재시간으로 부터 15777000초, 즉 6개월을 초과하면 안된다.)

aud는 "https://appleid.apple.com" 값을 입력.

sub는 App의 Bundle ID 값을 입력. ex) com.xxx.xxx 와 같은 형식

private String createClientSecret() throws IOException {
    Date expirationDate = Date.from(LocalDateTime.now().plusDays(30).atZone(ZoneId.systemDefault()).toInstant());
    Map<String, Object> jwtHeader = new HashMap<>();
    jwtHeader.put("kid", appleSignKeyId);
    jwtHeader.put("alg", "ES256");

    return Jwts.builder()
            .setHeaderParams(jwtHeader)
            .setIssuer(appleTeamId)
            .setIssuedAt(new Date(System.currentTimeMillis())) // 발행 시간 - UNIX 시간
            .setExpiration(expirationDate) // 만료 시간
            .setAudience("https://appleid.apple.com")
            .setSubject(appleBundleId)
            .signWith(SignatureAlgorithm.ES256, getPrivateKey())
            .compact();
}

private PrivateKey getPrivateKey() throws IOException {
    ClassPathResource resource = new ClassPathResource(appleSignKeyFilePath);
    String privateKey = new String(Files.readAllBytes(Paths.get(resource.getURI())));

    Reader pemReader = new StringReader(privateKey);
    PEMParser pemParser = new PEMParser(pemReader);
    JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
    PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject();
    return converter.getPrivateKey(object);
}

Apple Developer Key

위에 작성된 코드를 보면 appleSignKeyFilePath를 확인 할 수 있습니다.

그렇다면 Key File은 어떻게 생성하는지 설명해보겠습니다. 먼저 아래 링크로 이동

https://developer.apple.com/account/resources/authkeys/list


Apple Developer → Certificates, Identifiers & Profiles → Keys → + click

기존에 등록된 키의 종류는 아래와 같았습니다.

  • Apple Push Notifications service (APNs)
  • Sign In with Apple

근데 여기서 문제가 발생. Key File은 최초 생성 시 1회만 다운로드가 가능한데 그 파일을 보관한 이력이 없음.

결국 Sign In with Apple 를 추가로 생성하여 등록했습니다. (최대 2개 생성 가능)

→ 기존 키와 문제되는지는 아직까지 이슈 없음.

생성하면 .p8 파일을 다운로드 가능하고, Key ID를 확인 할 수 있습니다.

Apple Server API를 접근 하기 위한 access_token 생성

위에서 client_secret 생성을 했으니 이제 token을 발급 받을 수 있도록 진행하면 된다.

Apple Developer Documentation

  1. URL

    POST https://appleid.apple.com/auth/token

  2. Content-Type

    application/x-www-form-urlencoded

  3. Parts

    client_idstringRequired
    client_secretstringRequired
    codestring
    grant_typestringRequired
    refresh_tokenstring
    redirect_uristring
  4. Response Codes

    • 200
    • 400

Token을 발급받는 방법은 2가지 케이스로 확인했습니다. (여기서는 authorization_code를 사용)

grant_type

  • authorization_code → code parameter use
  • refresh_token → refresh_toke parameter use
public AppleAuthTokenResponse GenerateAuthToken(User user) throws IOException {
    RestTemplate restTemplate = new RestTemplateBuilder().build();
    String authUrl = "https://appleid.apple.com/auth/token";

    MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("code", user.getThirdPartyCode());
    params.add("client_id", appleBundleId);
    params.add("client_secret", createClientSecret());
    params.add("grant_type", "authorization_code");

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
    headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
    HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers);

    try {
        ResponseEntity<AppleAuthTokenResponse> response = restTemplate.postForEntity(authUrl, httpEntity, AppleAuthTokenResponse.class);
        return response.getBody();
    } catch (HttpClientErrorException e) {
        throw new IllegalArgumentException("Apple Auth Token Error");
    }
}

애플 로그인 회원 탈퇴 시 Apple Server로 사용자 토큰 해지 처리

그러면 위에서 생성한 AppleAuthTokenResponse로 탈퇴 처리를 진행하면 되겠다.

공식문서는 아래와 같다.

Apple Developer Documentation

필요한 파라미터는 아래와 같다.

  • client_id → Apple App bundle ID
  • client_secret
  • token → access_token or refresh_token (/auth/token 응답 데이터에 포함되어있음)
💡 여기서 잠깐 중요한 포인트가 있어서 추가로 설명하겠습니다. **client_secret 생성 시 필요한 authorization_code는 유효기간이 5분입니다.** 이 말은 회원 탈퇴를 진행하는 경우 authorization_code를 재발급 받아야 가능하다는 말입니다.

그래서 회원 탈퇴를 진행하는 경우 APP에서 아래와 같이 처리했습니다.
1. 회원 탈퇴 버튼 클릭
2. 애플 APP 재로그인 진행 (앱스토어에서 앱 구매 시 비밀번호 입력하는 절차와 같음)
3. 재로그인이 완료되면 회원 탈퇴 API로 요청
4. 응답받은 결과 값에 따라서 로그아웃 처리

코드로 작성해보면 아래와 같다.

public void revoke(User user) throws IOException {

    AppleAuthTokenResponse appleAuthToken = GenerateAuthToken(user);

    if (appleAuthToken.getAccessToken() != null) {
        RestTemplate restTemplate = new RestTemplateBuilder().build();
        String revokeUrl = "https://appleid.apple.com/auth/revoke";

        LinkedMultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("client_id", appleBundleId);
        params.add("client_secret", createClientSecret());
        params.add("token", appleAuthToken.getAccessToken());

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers);

        restTemplate.postForEntity(revokeUrl, httpEntity, String.class);
    }

}

void로 처리한 이유는 애플에서 사용자 토큰 해지를 실패하는 경우 과연 회원 탈퇴까지 막아야되는지에 대해서 의문점이 있어서 타입을 이렇게 정했습니다. 만약 문제가 된다면 고치도록 할 예정입니다.

마무리

작성된 코드 내용으로만 봤을때는 난이도가 많이 높다고 생각하지 않는다고 생각합니다.

다만, 애플과 APP (client), API Server와의 인증 과정에 대해 이해를 하지 않고 진행한다면 이해하기 어려 울 수 있습니다. (처음에 애플 공식문서와 운영중인 앱의 로그인 과정이 달라서 혼란스러웠음 🤔 )

다음에 기회가 된다면 공식 가이드에 맞게 설계하여 로그인 처리를 해야겠다고 생각이 들었습니다.

profile
server developer

9개의 댓글

comment-user-thumbnail
2022년 7월 20일

AppleAuthTokenResponse <- 이 클래스가 혹시 구성이 어떻게 되는지 알 수 있을까요...?

2개의 답글
comment-user-thumbnail
2024년 1월 3일

안녕하세요. 소셜 로그인을 구현 중인데 authorization_code을 재발급 받을 수 있나요?
회원 탈퇴 후 재가입을 진행 중인데 .. authorization_code은 재발급 받을 수 없다는 글을 봐서요..

답글 달기