OAuth 인증 되새김: Apple

Buddy·2023년 10월 29일
0

5종 간단 후기

최근 회사에서 OAuth 로그인 API 5종을 작업했다.
대충 후기를 남기면..

Kakao, Naver, Google 은 표준 방식과 유사하고 문서도 친절해서 적용하기 어렵지 않았다. (+ 개인적으로 진행해봤던 GitHub 도 마찬가지)

Facebook 은 구성이 어렵지는 않은데 문서 관리가 안된다는 느낌을 받았다. 항목을 찾기도 별로였고 깔끔하게 정리됐다는 느낌을 받기 힘들었다.

Apple 때문에 이글(독수리 아님ㅎ)을 쓰게 됐는데 방식이 다른 OAuth 와 달리 복잡했다. 정리가 잘 된 블로그들도 있지만 내가 잊지 않기 위해서 글을 적게 됐다. (조만간 또 작업을 해야해서..)

그래서 절차

이전에 썼던 글에서 재탕인 사진인데 원래 플로우라면 (웹 기준으로)

  1. SNS 인증 폼 요청
  2. 사용자 인증
  3. redirect uri 로 authorzation_code 응답받음
  4. authorization_code 로 해당 플랫폼에 access_token 요청
  5. 응답 받은 access_token 으로 사용자 프로필 요청

까지가 authroziation_server 라고 표기되어 있는 플랫폼과 상호 작용을 하고 이후로는 구현에 따라.. 뭐 프로필을 가지고 제공하는 서비스 내부에서 사용자 특정을 한다던가 하지만, 애플은 좀 달랐따.

  1. authorization_code 로 해당 플랫폼에 access_token 요청

에서 id_token 을 함께 돌려주며 이걸 해석하면 그 안에 apple 에서 유저에게 부여해주는 unique key 와 email 등의 정보가 들어있었다. access_token 요청하는 데이터 값도 좀 달랐음

첫 번째 난관, access_token 요청하기

보통 access_token 을 요청 할 때는 grant_type, code, redirect_uri 와 함께 요구사항에 따라 클라이언트의 고유 id 값이나 secret_key 를 요구한다.

애플에서는 client_id, client_secret 을 요구했다.

근데 그 client_secret 값이 일반적으로 앱 등록했을 때 주는 키값이 아니라 앱 등록 시 주는 암호화 키로 형식에 맞춰서 생성한 JWT(완전 tmi인데 JWT 토큰이라고 하면 JSON Web Token Token 이라 주의해야함)을 의미한다.

 private String createClientSecretToken() {
 		// 암호화 키 파일에서 값 가져오기 코드
        // 나도 남의 코드를 긁어온 부분이라 가져오기 좀 그래서 생략.. 출처에 넣을 생각임
        PrivateKey privateKey = getAppleIdPrivateKey();

        // JWT 클레임 설정
        long currentTimeMillis = System.currentTimeMillis();
        Date now = new Date(currentTimeMillis);
        Date expiration = new Date(currentTimeMillis + 3600000); // 1 hour

        // JWT 생성
        return Jwts.builder()
                .header()
                .keyId(clientSecret)
                .setAlgorithm(SignatureAlgorithm.ES256.toString())
                .and()
                .issuer(teamId)
                .issuedAt(now)
                .expiration(expiration)
                .audience()
                .add("https://appleid.apple.com")
                .and()
                .subject(clientId)
                .signWith(privateKey)
                .compact();
    }

요로코롬 만든 JWT 가 필요하다. jjwt 라이브러리를 썼고 기존에 블로그들에 있는 내용들이 deprecated 가 많아서.. 그대로 안가져오고 조금 수정했다. intellIJ 가 노란줄 뿜는게 좀 신경쓰였음.. 근데 지금 보니 setAlgorithm 도 deprecated 인데 미처 수정을 못했다.

두 번째 난관, id_token 해석하기

무튼 이렇게 하면 access_token 과 함께 id_tokoen 을 준다.
근데 이 id_token 을 해석하려면 apple 에서 제공하는 공개 키 값이 필요하다.
이 공개 키 값은 API 를 쏘면 3개를 주는데 id_token 에 쓰인 값이 거기서 뭔지 특정해서 검증에 사용해야함...ㅎㅎㅎ

대강 작성한 코드를 아래 첨부한다. 더 필요한 내용들은 (예외 처리 등) 알아서 추가 해야함!

private Claims verifyAppleIdToken(String idToken) {
        AppleAuthKeysResponseDTO appleAuthKeys = webClient.get()
                .uri(requestAuthKeysUri)
                .retrieve()
                .bodyToMono(AppleAuthKeysResponseDTO.class)
                .block()

        try {
            String headerOfIdentityToken = idToken.substring(0, idToken.indexOf("."));
            Decoder urlDecoder = java.util.Base64.getUrlDecoder();

            Map<String, String> header = objectMapper.readValue(new String(urlDecoder.decode(headerOfIdentityToken), Charsets.UTF_8), Map.class);
            AppleAuthKeysResponseDTO.AppleAuthKey key = appleAuthKeys.getMatchedKey(header.get("kid"), header.get("alg"))
                    .orElseThrow(() -> ...);

            byte[] nBytes = urlDecoder.decode(key.n());
            byte[] eBytes = urlDecoder.decode(key.e());

            BigInteger n = new BigInteger(1, nBytes);
            BigInteger e = new BigInteger(1, eBytes);

            RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e);
            KeyFactory keyFactory = KeyFactory.getInstance(key.kty());
            PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);

            JwtParser jwtParser = Jwts.parser()
                    .verifyWith(publicKey)
                    .build();
                    
            return jwtParser
                    .parseSignedClaims(idToken)
                    .getPayload();
        } catch (Exception e) {
	        ...
        }
    }

급 마무리와 reference

00시까지 20초 남아서 일단 끝!

profile
가볍게 쓰는 내용들이라 주관과 틀린 내용이 많습니다. 비판적인 시각으로 봐주시고 지적은 환영 :)

0개의 댓글