카카오 SDK 사용 시 유의 사항, OIDC 적용기

Choi Wontak·2025년 4월 14일

아이쿠MSA

목록 보기
3/12
post-thumbnail

난이도 ⭐️⭐️
작성 날짜 2024.10.04

고민 내용

아이쿠 프로젝트는 Kakao 소셜 로그인을 사용하고 있다.
자체 로그인은 보안상 걱정될 뿐만 아니라 사용자 입장에서도 불편하기 때문이다.
서버 Rest API 로그인은 네이티브의 장점을 이용하지 못해 사용자 경험에도 좋지 않아 SDK 방식 사용을 결정했었다.

그런데 서버 팀원 분께서 다음과 같은 문제를 제시했다.

🤔
SDK 로그인 성공했다는 이유로 바로 Access Token을 발급하면 보안상 문제가 생기지 않을까요?


찾아보기


카카오 SDK로 로그인하면, 클라이언트에선 사진과 같은 과정을 거친다.
서버의 개입 없이, 모든 인증 과정을 처리할 수 있다.

현재의 방식은 다음과 같다.

  1. 인증 과정이 종료된 후 클라이언트는 Kakao Server에서 받아온 Kakao 고유 ID 값을 서버에 전달
  2. 서버는 해당 ID 값이 DB에 존재하는지 확인한다.
    2-1. 존재하지 않는 경우 회원 정보가 없기 때문에 오류를 터뜨린다.
    클라이언트에선 해당 오류 발생 시 회원가입으로 이동
    2-2. 존재하는 경우 서비스 Access Token 발급

문제가 되는 부분은 1과 2 사이였는데,

  • SDK에서 정상적으로 로그인이 되었다고 하더라도, 서버에서는 이를 알 수 없다.
  • 단순히 KakaoID만으로 이를 판단하는 것은 탈취, 조작 위험에 열려있다.

👉 서버에서 인증할 방법은 없을까?

그래서 찾아본 방법이 OpenID Connect ID를 통한 추가적인 검증이다.

https://devtalk.kakao.com/t/sdk/139111

https://developers.kakao.com/docs/latest/ko/kakaologin/utilize#oidc

Kakao Dev에서 이 기능을 켜면, ID Token이라는 것을 발급받을 수 있다.
ID Token Payload에는 우리가 받고자 하는 값 (이메일, 카카오ID 등)이 담겨있으며, 이 토큰을 카카오 서버의 공개키를 통해 검증한다면, 해당 Payload의 유효성도 보장받을 수 있을 것이다!

코드는 해당 블로그를 참고하여 작성했습니다!
https://devnm.tistory.com/35

검증 방식은 다음의 값으로 진행한다.

페이로드 검증

  • iss: https://kauth.kakao.com와 일치해야 함
  • aud: 서비스 앱 키와 일치해야 함
  • exp: 현재 UNIX 타임스탬프(Timestamp)보다 큰 값 필요(ID 토큰의 만료 여부 확인)
  • nonce: 카카오 로그인 요청 시 전달한 값과 일치해야 함 (이 부분은 추후 추가 예정!!)
private Jwt<Header, Claims> getUnsignedTokenClaims(String token, String iss, String aud) {
        try {
            return Jwts.parserBuilder()
                    .requireAudience(aud)
                    .requireIssuer(iss)
                    .build()
                    .parseClaimsJwt(getUnsignedToken(token));
        } catch (ExpiredJwtException e) {
            throw new InvalidIdTokenException();
        } catch (Exception e) {
            log.error(e.toString());
            throw new InvalidIdTokenException();
        }
    }

서명 검증
1. 공개키 목록 조회하기 API로 카카오 인증 서버가 서명 시 사용하는 공개키 목록 조회
2. 공개키 목록에서 헤더의 kid에 해당하는 공개키 값 확인
공개키는 일정 기간 캐싱(Caching)하여 사용할 것을 권장하며, 지나치게 빈번한 요청 시 요청이 차단될 수 있으므로 유의
JWT 서명 검증을 지원하는 라이브러리를 사용해 공개키로 서명 검증

public Jws<Claims> getOIDCTokenJws(String token, String modulus, String exponent) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(getRSAPublicKey(modulus, exponent))
                    .build()
                    .parseClaimsJws(token);
        } catch (ExpiredJwtException e) {
            throw new InvalidIdTokenException();
        } catch (Exception e) {
            log.error(e.toString());
            throw new InvalidIdTokenException();
        }
    }
public OIDCDecodePayload getPayloadFromIdToken(
            String token, String iss, String aud, OIDCPublicKeysResponse oidcPublicKeysResponse) {
        String kid = getKidFromUnsignedIdToken(token, iss, aud);

        OIDCPublicKeyDto oidcPublicKeyDto =
                oidcPublicKeysResponse.getKeys().stream()
                        .filter(o -> o.getKid().equals(kid))
                        .findFirst()
                        .orElseThrow(() -> new InvalidIdTokenException());

        return jwtOIDCProvider.getOIDCTokenBody(
                token, oidcPublicKeyDto.getN(), oidcPublicKeyDto.getE());
    }

공개키 목록은 Redis를 이용해 캐싱하였다.

@FeignClient(
        name = "KakaoAuthClient",
        url = "https://kauth.kakao.com",
        configuration = KakaoOauthConfig.class)
public interface KakaoOauthClient {
        @Cacheable(cacheNames = "KakaoOICD", cacheManager = "oidcCacheManager")
        @GetMapping("/.well-known/jwks.json")
        OIDCPublicKeysResponse getKakaoOIDCOpenKeys();
}

이제 요청이 오면, 검증 로직을 호출한 후 ID Token에서 추출한 Oauth ID를 추출한다.

OauthInfo info = kakaoOauthHelper.getOauthInfoByIdToken(idToken);
String kakaoId = info.getOid();

정리하자면, 모바일 SDK 로그인 절차가 끝나면 다음과 같은 방식으로 진행되도록 변경하였다.

  1. 서버로 OIDC ID Token 전달
  2. 서버에서 ID Token 유효성 검증
  3. 서버에서 ID Token Payload 중 sub를 통해 인증 절차 진행 후 회원번호 추출
    3-1. 인증 정보 존재 -> 로그인 진행 (JWT 토큰 발행) & Refresh Token DB 저장
    3-2. 인증 정보 부재 -> 오류 메시지 전달 -> 회원가입 진행

결론

보안에 관련해서는 항상 조심하는 것이 좋을 것 같다.
클라이언트에서 넘어오는 값에 대해 항상 검증하며 조심하는 자세를 가져야겠다.

돌 다리도 두들겨보고 건너자!


profile
백엔드 주니어 주니어 개발자

0개의 댓글