dev-miri

[Spring Boot][Apple OIDC] 애플로그인을 구현해보자(애플 공식문서 뜯어보기) 본문

Spring boot

[Spring Boot][Apple OIDC] 애플로그인을 구현해보자(애플 공식문서 뜯어보기)

miri-dev 2024. 6. 13. 03:57

애플 로그인 구현 방법

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

탈퇴 기능이 있는 경우에 authorization code 를 받는 방식으로 구현해야 하는 이유

탈퇴 문서
토큰 요청 문서
토큰 요청 Response 문서

  • 기존에 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 형태로 응답이 오게 되는데, 이 또한 살펴보자
  • 결론적으로, 탈퇴 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에서 가지고 있는 방법으로도 구현이 가능 하겠지만 굳이..? 싶다!
    • 처음부터 authorization code를 받아 처리한다면, client_secret이 오고갈 필요가 없고(당연히 보안상 좋지도 않다) 백엔드에서 authorization code를 로그인 시 받고(code는 유효기간이 5분 이다) -> token generate api를 호출해 Token들을 발급받아 DB에 저장하고, 탈퇴 시 사용하면 된다.
    • 만약 탈퇴 기능을 구현하지 않고, 애플 로그인 외 다른 apple에서 제공하는 api를 호출할 일이 없다면 Token이 필요없고, client_secret을 발급받을 필요가 없으므로 iOS sdk 로그인 시 idToken을 바로 제공하므로 그걸 사용하면 편리하다!

 

Code 받는 방식으로 구현

  • authorizationCode를 iOS로부터 받아서 구현해보자.
  • 순서는 다음과 같다
    1. code를 iOS로부터 입력받는다
    2. code로 token을 받는 api를 호출한다(refreshToken, accessToken, idToken)
    3. idToken으로 사용자 정보 추출해서, 회원가입 로직을 수행한다
    4. 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로 구분 된다
      1. code를 입력 받음
      2. code에서 발급 받은 idToken 내부 이메일 정보로 가입 되어있는 유저인지 검증
        1. 가입이 되어 있다면 ) 서버 access, refresh token 재발급으로 로그인 처리
        2. 가입이 되어있지 않다면 ) code 검증 과정에서 받은 정보 중 회원가입 시 필요한 정보(이메일과 appleRefreshToken)을 String으로 합쳐서 payload로 만들어 유효기간이 짧은 임시 토큰을 발급한다
      3. 추가정보 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