
난이도 ⭐️⭐️
작성 날짜 2024.10.04
아이쿠 프로젝트는 Kakao 소셜 로그인을 사용하고 있다.
자체 로그인은 보안상 걱정될 뿐만 아니라 사용자 입장에서도 불편하기 때문이다.
서버 Rest API 로그인은 네이티브의 장점을 이용하지 못해 사용자 경험에도 좋지 않아 SDK 방식 사용을 결정했었다.
그런데 서버 팀원 분께서 다음과 같은 문제를 제시했다.
🤔
SDK 로그인 성공했다는 이유로 바로 Access Token을 발급하면 보안상 문제가 생기지 않을까요?

카카오 SDK로 로그인하면, 클라이언트에선 사진과 같은 과정을 거친다.
서버의 개입 없이, 모든 인증 과정을 처리할 수 있다.
현재의 방식은 다음과 같다.
문제가 되는 부분은 1과 2 사이였는데,
그래서 찾아본 방법이 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 로그인 절차가 끝나면 다음과 같은 방식으로 진행되도록 변경하였다.
보안에 관련해서는 항상 조심하는 것이 좋을 것 같다.
클라이언트에서 넘어오는 값에 대해 항상 검증하며 조심하는 자세를 가져야겠다.
돌 다리도 두들겨보고 건너자!