https://developer.apple.com/kr/news/?id=12m75xbj
2022년 6월 30일 부터
iOS앱은 탈퇴 시, 애플 연동해제를 필수로 넣게 됐다.
- 사족 ( 애플 연동해제를 넣게 된 과정 )
2022 관광공모전을 준비하면서
우리가 준비한건 iOS앱이기 때문에
당연하게도 애플로 로그인을 넣어야했다
나는 서버를 맡았기에
앱을 맡은 친구가 sdk를 써서 애플로 로그인 후,
정보를 나(서버)에게 넘기는 방식으로 진행했다.
근데 웬걸
이제는 애플 연동해제는 필수로 넣어야 한다는것이다.
여러방면으로 알아본 결과
앱에서도 할 수는 있는 것 같았지만
아무래도 서버에서 하는 것이 좀 더 편한 것 같아서
내가 진행하게 됐다.
언제나 그랬듯이
애플 관련된 것은 간다해 보이지만 간단하지 않은 과정이었다.
우선 순서를 살펴보도록 하자
Revoke Tokens
https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens
연동해제를 하려면 위 이미지처럼
해당 api를 사용하란다.
form data로
client_id, client_secret, token을 보내면 되는 간단한 방법이다.
그럼 하나씩 살펴보자
client_id
-> Apple App bundle ID. (ex: com.test.kedric) 간단하다.
client_secret
-> 연동한 애플 관련 개인정보와 개발자 계정정보를 이용해서 만든 JWT가 필요하단다.
그리곤 링크를 하나 던져준다. https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
링크를 들어가서 스크롤을 조금 내려보면
아래처럼 client_secret을 만드는 방법을 알려준다.
1. JWT header 를 만들고
2. JWT payload 를 만들고
3. JWT를 만들면 된다고 한다.
만드는데 필요한 정보들도 알려주고 예시도 알려준다.
{ // jwt header
"alg": "ES256",
"kid": "ABC123DEFG"
}
{ // jwt payload
"iss": "DEF123GHIJ",
"iat": 1437179036,
"exp": 1493298100,
"aud": "https://appleid.apple.com",
"sub": "com.mytest.app"
}
그럼 이제 어떻게 만드는지 알았으니
검색을 통해 이미 이 과정을 겪은 다른사람의 글을 참고해서
우선 예시 코드부터 작성해보자.
public 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", "ABC123DEFG"); // kid
jwtHeader.put("alg", "ES256"); // alg
return Jwts.builder()
.setHeaderParams(jwtHeader)
.setIssuer("DEF123GHIJ") // iss
.setIssuedAt(new Date(System.currentTimeMillis())) // 발행 시간
.setExpiration(expirationDate) // 만료 시간
.setAudience("https://appleid.apple.com") // aud
.setSubject("com.mytest.app") // sub
.signWith(SignatureAlgorithm.ES256, getPrivateKey())
.compact();
}
public PrivateKey getPrivateKey() throws IOException {
ClassPathResource resource = new ClassPathResource("static/AuthKey_1234ABCD.p8"); // .p8 key파일 위치
String privateKey = new String(Files.readAllBytes(Paths.get(resource.getURI())));
// File f = new File("C:/workspace/AuthKey_1234ABCD.p8");
// String privateKey = new String(Files.readAllBytes(f.toPath()));
Reader pemReader = new StringReader(privateKey);
PEMParser pemParser = new PEMParser(pemReader);
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject();
return converter.getPrivateKey(object);
}
위 소스처럼 작성하게되면 client_secret 을 얻을 수 있다.
그럼 각 필요한 정보들을 어디서 얻는지 알아보자
Key 파일 (.p8파일)
우선 getPrivateKey 를 보면 .p8 로 된 key파일(이후 key파일이라고 지칭하겠다)이 필요하다
https://developer.apple.com/account/resources/authkeys/list
애플 개발자 사이트 > Certificates, Identifiers & Profiles > Keys
처음에 만들어두고(처음 1번만 다운로드 가능) 잘 간직해두었던 key파일이 여기서 쓰인다.
이 key 파일을 프로젝트에 넣고 해당 경로를 명시해주면 되는데
소스를 보면 알다시피
ClassPathResource를 쓴 부분이 있고
주석 된 File 부분이 있다.
이 부분은 jar 내에서 찾는지 실제 파일시스템 내에서 찾는지 등의 차이가 있는데
따로 검색해서 알아보시길 바란다.
필자가 참고한 글을 같이 첨부한다.
kid
https://developer.apple.com/account/resources/authkeys/list
위에서 봤던 것처럼 생성했던 키파일에 들어가서
VIEW KEY DETAILS 의 KEY ID 를 입력해주면 된다.(A 10-character key identifier)
alg
토큰을 만들때 쓰는 알고리즘이다. 애플에서 ES256를 고정으로 사용하라고 한다.
iss
https://developer.apple.com/account/#!/membership
MEMBERSHIP 안의 Team ID를 입력해주면 된다.(10-character Team ID)
iat
client_secret 생성시간 - 현재시간
exp
client_secret 만료시간 - 현재시간으로 부터 15777000 초(6개월) 보다는 작은 시간이 설정되어야 한다.
(소스상에선 30일로 설정되었다.)
sub
App의 Bundle ID 값. (ex: com.test.kedric) client_id와 동일하다
이렇게 해서 client_secret도 생성하였다.
001111.12b2b666a41c473c9a942ca37bf7c123.1234 이런식으로 jwt가 생성된다.
그럼 우리가 기존에 연동해제에 필요했던
client_id, client_secret, token 3개중 2개가 만들어졌다
이번엔 token에 대해 알아보자
token
설명을 보면 refresh_token 혹은 access_token 을 입력하라고 한다.
그럼 refresh_token 혹은 access_token 은 어떻게 만드나
https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
client_secret을 만들 때 들어갔던 링크다
이번엔 하단으로 스크롤 하지말고 상단에 바로 보이는 api를 활용하면 된다.
여기서 필요한 form-data는
client_id, client_secret, grant_type이다.
그리고 자세히 읽어보면
authorization_code를 얻으려면 code 를
refres_token을 얻으려면 기존에 가지고 있던 refresh_token 을
같이 보내줘야 한다.
client_id
App의 Bundle ID 값. (ex: com.test.kedric) 계속 쓰고있는 값이다.
client_secret
방금 위에서 생성한 client_secret값이다.
grant_type
authorization_code 혹은 refresh_token을 입력하라고 한다.
-> authorization_code 인 경우, code 가 필요하고,
-> refresh_token 인 경우, 기존 가지고 있던 refresh_token 가 필요하다
refresh_token
우리는 refresh_token을 만들러 왔다. 당연히 가지고 있을리 없다.
(가지고 있을 수도 있다. 애플로 로그인을 진행했고, 만료시간 전에 계속 새로 발급받았다면..)
code
앱에서 보내준 authorization 응답값 안에 있는 authorization code 를 입력하라고 한다.
authorization code는 앱에서 애플로 로그인에 성공한 경우
응답값으로 authorizationCode 를 주게 되는데 이걸 넣어주면 된다.
{
"access_token": "adg61...67Or9",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rca7...lABoQ"
"id_token": "eyJra...96sZg"
}
해당 API를 호출하면 위와 같은 응답 값을 얻게 되는데
우리가 필요한 것은 access_token 혹은 refresh_token이다.
필자는 여기서 refresh_token을 사용하려고 한다.(이유는 나중에 설명하도록 하겠다.)
작성한 코드를 보자.
public String getRefreshToken(String clientSecret, String authCode){
String refreshToken = "";
String uriStr = "https://appleid.apple.com/auth/token";
Map<String, String> params = new HashMap<>();
params.put("client_secret", clientSecret); // 생성한 clientSecret
params.put("code", authCode); // 애플 로그인 시, 응답값으로 받은 authrizationCode
params.put("grant_type", "authorization_code");
params.put("client_id", "com.mytest.app"); // app bundle id
try {
HttpRequest getRequest = HttpRequest.newBuilder()
.uri(new URI(uriStr))
.POST(getParamsUrlEncoded(params))
.headers("Content-Type", "application/x-www-form-urlencoded")
.build();
HttpClient httpClient = HttpClient.newHttpClient();
HttpResponse<String> getResponse = httpClient.send(getRequest, HttpResponse.BodyHandlers.ofString());
JSONObject parseData = new JSONObject(getResponse.body());
refreshToken = parseData.get("refresh_token").toString();
} catch (Exception e) {
e.printStackTrace();
}
return refreshToken; // 생성된 refreshToken
}
private HttpRequest.BodyPublisher getParamsUrlEncoded(Map<String, String> parameters) {
String urlEncoded = parameters.entrySet()
.stream()
.map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
return HttpRequest.BodyPublishers.ofString(urlEncoded);
}
이렇게 해서 refresh_token마저도 얻게 되었다.
결국 우리가 기존에 연동해제에 필요했던
client_id, client_secret, token 모두가 만들어진 셈이다.
드디어 돌고 돌아 연동해제를 할 수 있게 됐다.
코드를 보자
public void withDrawApple(String clientSecret, String refreshToken){
String uriStr = "https://appleid.apple.com/auth/revoke";
Map<String, String> params = new HashMap<>();
params.put("client_secret", clientSecret); // 생성한 client_secret
params.put("token", refreshToken); // 생성한 refresh_token
params.put("client_id", "com.mytest.app"); // app bundle id
try {
HttpRequest getRequest = HttpRequest.newBuilder()
.uri(new URI(uriStr))
.POST(getParamsUrlEncoded(params))
.headers("Content-Type", "application/x-www-form-urlencoded")
.build();
HttpClient httpClient = HttpClient.newHttpClient();
httpClient.send(getRequest, HttpResponse.BodyHandlers.ofString());
} catch (Exception e) {
e.printStackTrace();
}
}
이렇게 하면 드디어 우리가 바라던 애플 연동해제가 완료된다.
어떤가? 간단하지 않은가? 하하하하하하하하하...
일련의 과정을 다시 짚어보자면
1. 애플 연동해제를 위해 https://appleid.apple.com/auth/revoke 를 사용해야 했고
2. client_secret(JWT)을 만들어주기 위해 key 파일이나 alg, iss 등을 활용했고
3. token을 만들기 위해 client_secret 및 authrization_code를 활용하여 https://appleid.apple.com/auth/token 를 사용했고
4. 결국 그 값들로 연동해제에 성공했다.
은근히 뭔가 많은 작업을 했지만
이 과정 속에서도 빠진 부분과 적당히(?) 넘어간 부분이 존재한다.
우선 마지막에 token을 만들 때, 필자는 access_token을 사용할 수도 있었는데 refresh_token을 사용했다.
access_token은 유효기간이 있는 반면
refresh_token은 유효기간이 없기 때문이다.
필자가 테스트 했을 때,
access_token은 유효기간이 지난 후 token api를 호출하면 에러를 응답했지만
refresh_token은 그렇지 않았다.
그렇다는 말은 access_token이 필요할때마다 해당 api를 호출해야하는데
차라리 refresh_token을 따로 저장해두고 필요할때 쓰는 것이 좀 더 나을 거라 생각했다.
(물론 직접 그때그때 하는게 더 맞을지도 모르겠다.)
또,
글 작성시에는 코드들에 third party 정보들(sub, iss 등등)을 그냥 입력한데 반해
해당 정보들도 앱에서 보내주는 정보들로 api 검증을 통해 받을 수 있는 부분이다.
심지어 authrization_code는 일회성이며, 유효기간이 5분이다...
로그인 시켜놓고 다른로직을 태우느라 기다렸다가 어영부영 진행할경우
유효기간이 지나 사용할수 없게 되버린다.
그럼 또 다시 로그인을 시키는 로직을 태워 얻어와서 다시하고의 반복순환이 계속된다.
참고로 필자는
애플 로그인할 때, 해당 사용자를 특정하여 refresh_token을 미리 만들어 DB에 저장해두었다.
authrization_code가 만료되는걸 방지하고, 유효기간이 없는 refresh_token을 저장하여, 연동해제 시, 불필요한 api 호출을 안하게끔 만들기 위함이었다.
그리고 연동 해제시에 저장해둔 refresh_token을 조회해서 연동해제를 진행했다.
필자가 작성한 해당 글이 무조건 정답이 아니거니와, 미숙한 부분이 많다고 생각된다.
그럼에도 애플 연동해제를 하려는 사람들에게 도움이 되었으면 싶다.
**참고한 사이트
https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens
https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
https://developer.apple.com/documentation/sign_in_with_apple/tokenresponse
https://hwannny.tistory.com/71
'개발자의 삶 > Apple' 카테고리의 다른 글
애플로 로그인 구현 (Sign in with Apple) - web (34) | 2020.12.24 |
---|