개발 알다가도 모르겠네요

[Spring] 애플 소셜 로그인을 구현해보자 본문

웹/Spring

[Spring] 애플 소셜 로그인을 구현해보자

이재빵 2024. 5. 1. 00:59
728x90

애플은 2019년 애플 로그인 기능을 발표한 동시에, App Store에 등록할 때 소셜 로그인이 하나라도 있다면 애플 로그인이 필수로 제공되어야 한다는 심사정책을 내놓았다.

 

5월에 MVP 출시 예정인 앱에는 소셜로그인 기능이 들어가기 때문에 애플로그인을 필수로 적용시켜야 하는 상황이었다.

 

문제는 애플로그인이 다른 소셜로그인들과 동작방식이 좀 다르다는 점이다.

카카오 소셜로그인 연동 로직

 

애플의 경우에는 아래처럼

1. 서버 내부에서 별도로 client_secret라는 값을 생성해준 뒤에

2. 애플서버에 여러 설정정보를 함께 전달해줘야 한다.

3. 여기서 끝이 아니라 애플서버로부터 전달받은 id_token 값을 파싱해야 비로소 이메일과 같은 사용자 정보를 얻을 수 있었다.

애플 소셜로그인 연동 로직

 

여러 레퍼런스를 살펴보니까, Identity Token값을 활용해서 구현하는 방법도 있었는데,

Authorization Code만 클라이언트로부터 전달받아서 구현하는 방법이 그나마 공수가 덜 들 것 같아서 이 방법으로 구현했다.

 

[구현코드]

    /**
     * Apple 토큰 인증 API 호출 -> 응답받은 ID 토큰을 JWT Decoding 처리 -> AppleUserInfoResponseDto로 반환
     * @param authorizationCode 사용자로부터 받은 인증 코드
     * @return 디코딩된 사용자 정보를 담고 있는 AppleUserInfoResponseDto 객체
     */
    public AppleUserInfoResponseDto getAppleUserProfile(String authorizationCode) throws IOException {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.valueOf(APPLICATION_FORM_URLENCODED_VALUE));
        HttpEntity<String> request = new HttpEntity<>("client_id=" + appleProperties.getClientId() +
                "&client_secret=" + generateClientSecret() +
                "&grant_type=" + appleProperties.getGrantType() +
                "&code=" + authorizationCode, headers);

        ResponseEntity<AppleSocialTokenInfoResponseDto> response = socialConfig.restTemplate().exchange(
                appleProperties.getAudience() + "/auth/token", HttpMethod.POST, request, AppleSocialTokenInfoResponseDto.class);

        DecodedJWT decodedJWT = JWT.decode(Objects.requireNonNull(response.getBody()).getIdToken());

        AppleUserInfoResponseDto appleUserInfoResponseDto = new AppleUserInfoResponseDto();

        appleUserInfoResponseDto.setSubject(decodedJWT.getClaim("sub").asString());
        appleUserInfoResponseDto.setEmail(decodedJWT.getClaim("email").asString());

        return appleUserInfoResponseDto;
    }
   /**
     * Apple의 인증 서버와의 통신에 사용될 JWT을 생성하기 위해 사용되는 ClientSecret
     * ClientSecret은 토큰 요청 시 서명 목적으로 사용되며, 공개키/비공개키 인증 메커니즘이 포함됨
     * @return 생성된 JWT ClientSecret
     */
    private String generateClientSecret() {
        LocalDateTime expiration = LocalDateTime.now().plusMinutes(5);

        return Jwts.builder()
                .setHeaderParam(JwsHeader.KEY_ID, appleProperties.getKeyId())
                .setIssuer(appleProperties.getTeamId())
                .setAudience(appleProperties.getAudience())
                .setSubject(appleProperties.getClientId())
                .setExpiration(Date.from(expiration.atZone(ZoneId.systemDefault()).toInstant()))
                .setIssuedAt(new Date())
                .signWith(getPrivateKey(), SignatureAlgorithm.ES256)
                .compact();
    }
    /**
     * 애플의 JWT 클라이언트 시크릿 생성을 위한 비공개 키 로드
     * @return 로드된 RSA 비공개 키
     */
    private PrivateKey getPrivateKey() {
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
        JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");

        try {
            byte[] privateKeyBytes = Base64.getDecoder().decode(appleProperties.getPrivateKey());
            PrivateKeyInfo privateKeyInfo = PrivateKeyInfo.getInstance(privateKeyBytes);
            return converter.getPrivateKey(privateKeyInfo);
        } catch (Exception e) {
            throw new RuntimeException("Error converting private key from String", e);
        }
    }
/**
 * 애플 유저 정보 DTO
 */
@Builder
@NoArgsConstructor(access = AccessLevel.PUBLIC)
@AllArgsConstructor
@Data
public class AppleUserInfoResponseDto {
    // 고유ID
    @JsonProperty("sub")
    private String subject;

    @JsonProperty("email")
    private String email;
}
/**
 * 애플 소셜 토큰 응답 정보 DTO
 */
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Data
public class AppleSocialTokenInfoResponseDto {
    @JsonProperty("access_token")
    private String accessToken;

    @JsonProperty("token_type")
    private String tokenType;

    @JsonProperty("expires_in")
    private Long expiresIn;

    @JsonProperty("refresh_token")
    private String refreshToken;

    @JsonProperty("id_token")
    private String idToken;
}

 

[트러블슈팅]

애플 서버 /auth/token로 토큰 발급 요청시 Invalid_grant 에러 발생

1. Authorization Code의 유효시간은 5분, 일회용으로 제한되기 때문에 생긴 문제였다. 새로 발급받은 코드로 요청해서 해결했다.

 

2. 토큰 발급 시 파라미터로 들어가는 client_id값이 App ID가 아닌 Service ID를 사용했기 때문에 발생한 문제였다.

client_id는 애플 개발자 계정에서 Certificates, Identifiers & Profiles - Identifiers에서 설정한 id인데,

아래의 그림을 보면 APP IDs값과 Service IDs값을 사용하는 경우가 나눠져 있었다.

 

간단하게 설명하자면 소셜로그인을 ios앱에서 연동한다면 client_id값에 App IDs가 들어가게 되고,

웹사이트나 웹뷰 형식을 통해 연동하면 client_id값에 Service IDs가 들어가게 되는 것이다.

client_id값에 애플 디벨로퍼에서 설정한 App IDs 값으로 수정하니까 해결됐다.

 

 

[레퍼런스]

 

Sign in with Apple REST API | Apple Developer Documentation

Communicate between your app servers and Apple’s authentication servers.

developer.apple.com

 

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

[1] 스프링 프로젝트에 애플 로그인 API 연동을 위한 Apple Developer 설정 - (현재 글) [2] 스프링 프로젝트에 애플 로그인 API 연동하기 Spring API Server에서 Apple Login API를 연동하여 앱(App)에 제공하기 위

whitepaek.tistory.com

 

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

들어가며 사이드 프로젝트를 진행하던 도중 APP에서 Apple 로그인을 적용해야 했다. https://apps.apple.com/kr/app/%EA%B8%80%EC%9D%84%EB%8B%B4%EB%8B%A4/id1517289762 ‎글을담다 ‎마음 속 와 닿은 글을 손쉽게 담는, '

hwannny.tistory.com

 

[Spring Boot] OAuth 2.0, JWT를 활용한 애플 로그인 구현 (2-구현편)

Spring Boot로 애플 로그인 구현하기

velog.io

 

Apple 로그인 구현하기

Spring Boot와 java security로 Apple 로그인 구현하기

velog.io

 

[Spring] 애플 로그인을 구현해보자

번거로운 회원가입 절차없이 버튼 클릭 하나만으로 서비스를 이용할 수 있는 소셜 로그인은 사용자 유입에 도움이 된다.하지만 iOS 앱은 카카오, 구글 로그인과 같은 소셜 로그인을 추가하려면

velog.io

 

TN3107: Resolving Sign in with Apple response errors | Apple Developer Documentation

Diagnose errors received by the Sign in with Apple client, or its server infrastructure, by identifying the underlying causes of common error codes and explore their potential solutions.

developer.apple.com

 

[IMAD] 애플 로그인 Invalid_grant 에러

기존 iOS 네이티브 앱에서 애플 로그인을 웹뷰에서 구현했었는데, 이번에 공식 라이브러리를 사용해 다시 구현하기로 했다. 이를 위해 앱 측에서 애플 인가 서버로부터 발급받은 사용자 정보와 a

velog.io

 

Apple SignIn invalid grant from iOS | Apple Developer Forums

Hi, I have implemented login/signup using apple login in my website and everything is working fine. I have created a service id, created the key, configured the redirect uri and generated the client secret. Now I'm trying to implement the login on my iOS a

forums.developer.apple.com