OIDC 정리

2025. 10. 24. 18:29·개념잡기

OIDC

간단하게 요약하면, 기존 oauth2에서 액세스토큰을 받으면 유효한 사용자인지 인증하기 위해서 한번더 요청을 보냈는데, 이 과정을 줄이고자 사용자인증기능을 위한 JWT 토큰을 액세스토큰과 함께 보내 내부 검증할 수 있게 만든 구조입니다.

카카오 로그인에서는 ‘id_token’ 이 이에 해당됩니다.

💡

Id_toekn 구조

  1. 헤더(Header) - 공개키 ID(어떻게 암호화 되어있는지)
  2. 페이로드(Payload) - 사용자 인증 정보
  3. 서명(Signature) - 토큰의 유효성을 검증할 때 사용할 값

공개키 ID는 헤더의 ‘kid’ 필드, 키(key id) 에 해당됩니다. 우리는 이제 공개키목록에서 kid에 해당하는 공개키 값을 확인하고 그 값으로 ‘서명’ 부분을 검증하면 됩니다.

public class OidcIdTokenParser {

    public Claims parse(Locator<Key> locator, String issuer, String token) {
        try {
            JwtParser jwtParser = Jwts.parser()
                    .keyLocator(locator)
                    .requireIssuer(issuer)
                    .clock(() -> Date.from(Clock.systemUTC().instant()))
                    .build();
            return jwtParser.parseSignedClaims(token).getPayload();
        } catch (Exception e) {
            log.error("OIDC id_token 파싱 에러 = {}, origin={}", e.getMessage(), "OidcIdTokenParser.parse");
            throw new SocialLoginException();
        }
    }
}

파싱을 keyLocator라는 메서드로 진행한 모습입니다.

서버 내에서 사용하는 jwt를 verifyWith 메서드로 파싱해왔었는데 여기서는 그럴 수 없습니다. 동적으로 키를 찾아야 하기 때문입니다.

💡

서버 내에서는 고정된 단 하나의 비밀키를 통해서 생성하고 검증하는 대칭키 방식이므로 그렇습니다.

만약에 비밀키가 여러개거나, 하나여도 공개키를 외부에 제공하고 토큰 헤더에 kid 를 넣는 방식으로 구현(표준 OIDC 방식)했다면 이 역시도 keyLocator 방식으로 해야할 것입니다.

package io.jsonwebtoken;
public interface Locator<T> {
    T locate(Header var1);
}

public class KakaoJwkLocator implements Locator<Key> {

    private final KakaoOidcJwkSetProvider kakaoOidcJwkSetProvider;
    private final OidcPublicKeyProvider oidcPublicKeyProvider;

    @Override
    public Key locate(Header header) {
        String publicKeyId = (String) header.get("kid");
        if (publicKeyId == null) {
            log.error("공개키가 존재하지 않음, origin={}", "KakaoJwkLocator.locate");
            throw new SocialLoginException();
        }
        return oidcPublicKeyProvider.get(publicKeyId, kakaoOidcJwkSetProvider);
    }
}

keyLocator에 넘길 구현체입니다.

매개변수로 Header를 받고 원하는 키 타입 T 을 리턴하는 인터페이스네요.

어떤 키가 들어있을 줄 모르기 때문에 Key 또한 인터페이스로 리턴해주겠습니다.

public class KakaoOidcJwkSetProvider implements Supplier<JwkSet> {

    @Override
    public JwkSet get() {
        RestClient restClient = RestClient.create("https://kauth.kakao.com/.well-known/jwks.json");
        String response = restClient.get().retrieve().body(String.class);
        Parser<JwkSet> jwkParser = Jwks.setParser().build();
        return jwkParser.parse(response);
    }
}

해당 클래스에서는 카카오 jwks 엔드포인트에 HTTP 요청을 보내 공개키 목록들을 가져오고 파싱합니다.

그러나 이 클래스를 Locator에서 바로 호출하면 안됩니다.


https://developers.kakao.com/docs/latest/ko/kakaologin/utilize#oidc-id-token-verify

4번 항목

  • 공개 키를 일정 기간 캐싱하여 사용할 것을 권장하며
  • 지나친 요청은 차단될 수 있음을 유의

너무 많은 API 요청을 거부하겠다는 뜻인 것 같습니다. 사실 생각해보면 매 요청마다 공개키목록을 조회하는 API 를 보내면 결국에는 Oauth2에서 액세스토큰을 검증하는 요청을 줄여보자는 의의가 퇴색되기도 합니다.

다시 돌아와서 우리는 공개키를 캐싱해야합니다.

public class OidcPublicKeyProvider {

    private final Map<String, Key> jwks = new ConcurrentHashMap<>();
    private final ReentrantLock lock = new ReentrantLock();

    public Key get(String id, Supplier<JwkSet> supplier) {
        Key key = jwks.get(id);
        if (key != null) return key;
        try {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                try {
                    key = jwks.get(id);
                    if (key != null) return key;
                    JwkSet jwkSet = supplier.get();
                    jwkSet.forEach(jwk -> jwks.put(jwk.getId(), jwk.toKey()));
                    return jwks.get(id);
                } finally {
                    lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        throw new IllegalArgumentException();
    }
}

public interface Supplier<T> {
    T get();
}

kid에 해당하는 Key가 없을 때만 캐시를 갱신하도록했습니다.

동시성을 고려해 락을 사용하고 락을 기다리는 동안 다른 스레드에서 캐시를 갱신했을 가능성을 고려해 다시한번 캐시를 확인한 모습입니다.

💡

Supplier는 인자를 받지 않고 T 타입을 그대로 리턴하는 인터페이스 입니다.

이 특성 때문에 단순히 요청시점까지 실행을 미룰 수 있는데 특화되었습니다.

이런 설계 덕분에 OidcPublicKeyProvider 는 어떤 로그인 플랫폼과 관계없이 일관되게 공개키를 캐싱하고 리턴해줄 수 있게 되었습니다.

 

참고

https://velog.io/@glencode/OAuth2-%EB%8C%80%EC%8B%A0-OpenID-Connect%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%9D%B8%EC%A6%9D-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

'개념잡기' 카테고리의 다른 글

가비지 컬렉션  (1) 2025.07.16
웹 공격으로 생각해보는 JWT  (0) 2025.06.02
캐시  (1) 2025.05.28
'개념잡기' 카테고리의 다른 글
  • 가비지 컬렉션
  • 웹 공격으로 생각해보는 JWT
  • 캐시
gucoding
gucoding
gucoding 님의 블로그 입니다.
  • gucoding
    gucoding 님의 블로그
    gucoding
  • 전체
    오늘
    어제
    • 분류 전체보기 (14)
      • spring (1)
      • 회고 (1)
      • DB (3)
      • 개념잡기 (4)
      • Java (2)
      • 기타 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    opensearch
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
gucoding
OIDC 정리
상단으로

티스토리툴바