dev-miri
[Spring Boot][Apple OIDC] 애플로그인을 구현해보자(애플 공식문서 뜯어보기) 본문
애플 로그인 구현 방법
- 애플 로그인 구현 시 처리 방식에 따라 2가지로 구분 된다
- 프론트엔드에서 백엔드로 authorization code 전달
- 프론트엔드에서 백엔드로 id_token 전달
- 진행하고 있는 프로젝트 2개에서 각각 다른 방법을 사용하게 되어서, 두 방법 모두 정리해볼 예정이다.
- 어떤 방법을 사용할지의 여부는 백엔드와 프론트엔드가 상의하여 결정하는 것이 좋지만, 두 방법 모두 시도해보았을 때 애플 로그인 탈퇴를 구현하는 경우에는 1번으로 구현하는 방법이 더 나은 것 같다.
- 만약 탈퇴를 구현하지 않는 경우(iOS 앱 출시를 생각하지 않는 경우)에는 2번 방법이 1번 방법 보다는 백엔드에서 구현이 간단하다.
탈퇴 기능이 있는 경우에 authorization code 를 받는 방식으로 구현해야 하는 이유



- 기존에 2번 방법으로 구현을 하던 와중에, 탈퇴 기능을 구현하던 도중 1번 방법으로 로그인 구현 방식을 바꾸게 되었다.
- 애플 문서에서 탈퇴(revoke token) api 문서를 살펴보자.
- body로 보내야 하는 정보는 client_id, client_secret, token, token_type_hint이다.
- client_id는 com.application.app 으로, developer 계정에서 설정하는 것이고
- client_secret은 Jwt 토큰으로, 이게 authorization code를 받는 방식으로 구현해야 하는 주 원인이다.
- token_type_hint를 보면, refresh token과 access token 두 종류가 있는데, token 발급 받는 문서를 통해 좀 더 자세히 들여다보자.
- [토큰 관련 공식 문서][https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens]
- Use the authorization code to verify the token claims with Apple servers, and exchange them for refresh tokens. For more information, see Generate and validate tokens.
- 공식 문서에서는 프론트엔드로부터 authorization code를 받고, refresh token으로 교환하라고 한다.
- Generate and validate tokens의 Api docs를 살펴보자
- Generate and Validate Tokens
- 토큰을 generate 하고 validate 하는 api다. 애플은 다른 서비스의 api와 달리 하나의 api를 여러 용도로 사용하는 것 같다 .. 그래서 공식문서 가독성이 조금 떨어지는 것 같기도 하고.. 하지만 꼼꼼히 읽어보면 이해가 되기는 한다 ... : ) ..
- 필수로 요구되는 필드는
- client_id : app ID ... (app.XXX.YYY 형식의 appID를 의미한다.)
- client_secret
- grant_type
- 인데, client_secret은 서버에서 생성하는 Jwt 토큰으로 밑에서 다룰 것이다.
- grant_type 아래와 같이 채우면 된다고 한다.
- authorization_code : authorization code의 검증 요청에 사용
- refresh_token : refresh token 검증 요청에 사용
- authorization_code로 보낼 때 code 필드를 채워서 보내고, refresh_token 필드로 보낼 때 refresh_token 필드를 채워서 보내면 된다.
- TokenResponse
- id_token, access_token, refresh_token 세 종류의 토큰이 있다
위 api로 요청을 보냈을 때 성공 시 TokenReponse 형태로 응답이 오게 되는데, 이 또한 살펴보자
- id_token, access_token, refresh_token 세 종류의 토큰이 있다
- [토큰 관련 공식 문서][https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens]
- 결론적으로, 탈퇴 api는 client_id, client_secret, token(refresh token OR access token)을 보내야한다.
- 만약 iOS로부터 idToken을 받아서 구현하는 방식으로 개발한다면 -> 우리는 authorization code를 받지 못하므로, refresh token과 access token을 발급받는 api를 호출할 수 없다.
- 그럼 iOS에서 token을 발급 받아서 넘겨줘야 하는데, iOS에서 client_secret을 발급받는 과정(Jwt 헤더와 페이로드를 채우고, private-key로 서명하는 작업)을 매번 거치기도 어려운 부분이기 때문에, 서버에서 client_secret을 api를 통해 넘겨줘야 한다.
- 결론적으로 탈퇴를 요청하면 -> client_secret을 발급해서 iOS에 넘겨줌 -> 그 client_secret으로 iOS에서 Token 발급 -> 백엔드에 토큰 전달 -> 백엔드에서 토큰을 이용한 탈퇴 처리 ..... 라는 긴 과정을 거치게 된다
- 필자는 client_secret을 유효기간을 30일 정도로 해서 생성했는데, 30일동안 iOS에서 가지고 있는 방법으로도 구현이 가능 하겠지만 굳이..? 싶다!
- 그럼 iOS에서 token을 발급 받아서 넘겨줘야 하는데, iOS에서 client_secret을 발급받는 과정(Jwt 헤더와 페이로드를 채우고, private-key로 서명하는 작업)을 매번 거치기도 어려운 부분이기 때문에, 서버에서 client_secret을 api를 통해 넘겨줘야 한다.
- 처음부터 authorization code를 받아 처리한다면, client_secret이 오고갈 필요가 없고(당연히 보안상 좋지도 않다) 백엔드에서 authorization code를 로그인 시 받고(code는 유효기간이 5분 이다) -> token generate api를 호출해 Token들을 발급받아 DB에 저장하고, 탈퇴 시 사용하면 된다.
- 만약 탈퇴 기능을 구현하지 않고, 애플 로그인 외 다른 apple에서 제공하는 api를 호출할 일이 없다면 Token이 필요없고, client_secret을 발급받을 필요가 없으므로 iOS sdk 로그인 시 idToken을 바로 제공하므로 그걸 사용하면 편리하다!
- 만약 iOS로부터 idToken을 받아서 구현하는 방식으로 개발한다면 -> 우리는 authorization code를 받지 못하므로, refresh token과 access token을 발급받는 api를 호출할 수 없다.
Code 받는 방식으로 구현
- authorizationCode를 iOS로부터 받아서 구현해보자.
- 순서는 다음과 같다
- code를 iOS로부터 입력받는다
- code로 token을 받는 api를 호출한다(refreshToken, accessToken, idToken)
- idToken으로 사용자 정보 추출해서, 회원가입 로직을 수행한다
- refreshToken은 DB에 저장해두고, 탈퇴 시 사용한다.
Postman으로 요청 보내보기
- ResponseDto를 만들기 전에, Postman으로 요청을 보내보고 싶어서 보내는데 자꾸 "invalid_client" 오류가 떴다. 혹시 Body 인자가 잘못 되었을까봐 계속 확인해봤는데 틀린게 없었다 ㅠㅠ
- 공식문서를 보다 cURL에서 해답을 찾았는데 답은 content-type이었다..! 늘 application/json으로만 보냈는데, apple API는
x-www-form-urlencoded로 보내야 했다 ..! Body에서 x-www-form-urlencoded로 보내니 바로 성공했다!!- 관련 이슈는 뒤에서 한번 더 겪었다...익숙치 않은 x-www-form-urlencoded
authorization code로 Token 받는 Api 호출하기
- FeignClient를 이용해서 호출하였다.
- Record를 이용해서 설계하였고, ResponseDto는 다음과 같다.
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record AppleTokenResponse( String access_token, String token_type, String expires_in, String refresh_token, String id_token ){}
* RequestDto는 이렇게 설계했는데,,,!! RequestDto를 만들 필요가 없다.
# 만들지 않아도 됨!!!
@NoArgsConstructor
@AllArgsConstructor
public static class GenerateTokenRequest{
private String client_id;
private String client_secret;
private String code;
private String grant_type;
}
- 이것도 요청 타입과 관련된 문제인데, 아무 생각없이 RequestDto로 요청을 보냈는데 아래와 같은 오류가 생겼다.
- FeignClient를 사용하여 요청을 보낼 때, EncodeException이 발생 했는데, 이는 FeignClient가 요청 본문을 올바르게 직렬화할 수 없음을 의미한다.
- 이 오류가 생기는 이유는 우리는 application/x-www-form-urlencoded 형식으로 데이터를 보낼 때 @RequestParam 형식으로 데이터를 전송해야 한다!! (Body가 아닌). 즉 굳이 Dto를 만들 필요가 없고, 여러개의 파라미터를 @RequestParam 형식으로 수정해야 한다.
feign.codec.EncodeException: Could not write request: no suitable HttpMessageConverter found for request type \[com.example.nzgeneration.domain.auth.dto.AuthRequestDto$GenerateTokenRequest\]at org.springframework.cloud.openfeign.support.SpringEncoder.encodeWithMessageConverter(SpringEncoder.java:169) at org.
- 즉, 필자는 처음에 feignClient interface 함수를 아래와 같이 작성했는데
#잘못된 함수임
@PostMapping("/auth/token") AppleTokenResponse generateToken(@RequestBody GenerateTokenRequest generateTokenRequest);
- 요청을 이렇게 보내는 것이 올바르다.
@PostMapping("/auth/token")
AppleTokenResponse generateToken(@RequestParam("client_id") String clientId,
@RequestParam("client_secret") String clientSecret,
@RequestParam("code") String code,
@RequestParam("grant_type") String grantType);
- FeignClient Interface를 호출하는 함수이다.
public AppleTokenResponse getTokenRequest(String code) throws Exception {
return appleAuthApiClient.generateToken(clientId, jwtOIDCProvider.createSecretKey(), code, "authorization_code");
}
- 주요 로직을 수행하는 authService 함수이다.
- 우리 서비스는 소셜 서버에서 제공하는 정보 말고도 추가로 입력받아야 하는 정보가 필요한데, 이 경우 구현 방법이 여러 가지가 있는 것으로 알고 있다.
- ex) Db에 컬럼을 만들어 놓고, 추가 정보 및 가입 완료 여부 변경/임시 토큰 발급 등등..
- 필자는 임시 토큰 발급하는 방법으로 구현 했는데, 로그인 api와 추가정보 입력 api로 구분 된다
- code를 입력 받음
- code에서 발급 받은 idToken 내부 이메일 정보로 가입 되어있는 유저인지 검증
- 가입이 되어 있다면 ) 서버 access, refresh token 재발급으로 로그인 처리
- 가입이 되어있지 않다면 ) code 검증 과정에서 받은 정보 중 회원가입 시 필요한 정보(이메일과 appleRefreshToken)을 String으로 합쳐서 payload로 만들어 유효기간이 짧은 임시 토큰을 발급한다
- 추가정보 api를 호출한다 : 입력 받고자 하는 추가정보와 임시토큰을 입력 받고, 임시토큰을 파싱하여 정보를 빼내고, 이를 기반으로 회원가입을 완료한다
- 우리 서비스는 소셜 서버에서 제공하는 정보 말고도 추가로 입력받아야 하는 정보가 필요한데, 이 경우 구현 방법이 여러 가지가 있는 것으로 알고 있다.
public LoginSimpleInfo login(String code) throws Exception {
AppleTokenResponse appleTokenResponse = appleOauthHelper.getTokenRequest(code);
String idToken = appleTokenResponse.id_token();
OIDCDecodePayload oidcDecodePayload = appleOauthHelper.getOIDCDecodePayload(idToken);
String email = oidcDecodePayload.email();
String appleRefreshToken = appleTokenResponse.refresh_token();
String payload = email+"+"+appleRefreshToken;
Optional<User> optionalMember = userRepository.findByEmail(email);
String accessToken, refreshToken;
if(optionalMember.isPresent()){ //로그인 로직
accessToken = jwtTokenProvider.createAccessToken(optionalMember.get().getPayload());
refreshToken = jwtTokenProvider.createRefreshToken(optionalMember.get().getId());
optionalMember.get().updateToken(accessToken, refreshToken);
return LoginSimpleInfo.toDTO(accessToken, refreshToken, true);
}
accessToken = jwtTokenProvider.generateTempToken(payload);
return LoginSimpleInfo.toDTO(accessToken, null, false);
}
- appleOauthHelper.getTokenRequest 함수로는 code로부터 idToken, appleRefreshToken을 받아온다(위에서 설명)
- appleOauthHelper.getOIDCDecodePayload 함수는 idToken을 검증하여 사용자 정보를 얻어오는 함수인데, idToken을 프론트로부터 입력 받아 애플 로그인을 처리하는 방법은 여기서부터 진행하면 된다.
- 이는 다음 글에서 작성해볼 것이다.
- 지금까지 프론트엔드(iOS 개발자)로부터 code를 받고, idToken을 받아오는 과정까지 처리하는 과정을 진행했다.
- 다음 글에서는 idToken에서 사용자 정보를 얻어오는 방법을 작성해볼 예정이다.
'Spring boot' 카테고리의 다른 글
| [back-end]OAuth2.0/생활코딩 OAuth 2.0 (0) | 2022.09.25 |
|---|
Comments