ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [OIDC] 1. OIDC(OpenID Connect) 에 대한 설명
    ETC/OIDC 2025. 7. 12. 21:29

    OIDC란? 그리고 OAuth 2.0과의 차이

    OIDC란 OpenId Connect의 약어로서 OAuth 2.0 개념 상위에 구축된 인증 프로토콜이에요.

     

    왜 OIDC가 OAuth 2.0 상위 개념으로 생겼는지에 대해 짚고 가볼게요.

     

    기존 OAuth 2.0의 경우 AccessToken을 발급받고 추가적인 API를 통해 "유저 식별"이 필수적이었어요.

     

    "왜?"일까요?

     

    OAuth 2.0 은 "인가"라는 개념에 초점이 맞춰진 표준이에요.

     

    실제 OAuth 2.0의 공식 표준 문서 RFC 6749를 찾아보면

    https://datatracker.ietf.org/doc/html/rfc6749

     

    RFC 6749: The OAuth 2.0 Authorization Framework

    The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and the HTTP service, or by allowi

    datatracker.ietf.org

     

    그 어디에도 유저의 정보를 조회하는 userinfo와 같은 api가 표준으로 명시되어있지 않아요.

     

    우리가 알고 있던 /api/userinfo 는 사실상 암묵적인 룰로 잡혀온 거예요.

     

    그저 OAuth 2.0의 목적은 Access Token 발급시켜주는 플로우를 명시하고 결과적으론 "인가"의 기능만 수행하는 것인 거죠.

     

    그렇다면 OIDC는 뭐가 개선되었길래 상위 개념으로 나온 것일까요?

     

    OIDC는 OAuth 2.0에서 발급된 AccessToken을 통해 "인가"를 수행하는 방식은 동일하나, Access Token 이 아닌 ID Token(JWT Token)을 통해 "유저 식별"을 수행해요.

     

    즉, OAuth 2.0에서는 인가(Authorization)에 집중이 되었다면, OIDC는 인증(Authentication)을 확장시켜

    1. 사용자의 신원 확인 (=인증)

    2. 리소스 접근 권한 부여 (=인가)

    위 2가지를 동시에 처리할 수 있게 한 것이죠.

     

     

     

    실제 예시로 이해하는 OIDC vs OAuth 2.0 시나리오

    여기서 말하는 "인증"과 "인가"의 차이를 설명드리기 위해

    예를 들어 구글의 캘린더 서비스에 대해서 예시를 잡고 말씀드려 볼게요.

     

    기존 OAuth 2.0 방식으로는

     

    인증 플로우를 통해 Access Token을 발급받고

    -> Access Token을 통해 /api/userinfo를 통해 "유저 식별(= 인증)"을 수행한 뒤

    -> 캘린더의 일정 내용에 대해서 추가 정보를 요청하는 API 요청을 통해 추가적인 기능에 대한 "접근 권한(= 인가)"를 해요.

     

    여기서 OAuth 2.0 방식으로는 Access Token을 통해서 "구글의 캘린더 데이터"에 접근할 수 있는 권한만을 부여하게 된 것이에요.

    사용자가 누구인지 신원은 아직 알 수 없고, 유저의 식별이 필요하다면 별도의 /api/userinfo API를 추가로 호출해야 하는 것이죠.

     

    반면에 OIDC 방식으로는

     

    OAuth 2.0과 마찬가지의 인증 플로우를 통해 Access Token과 Id Token을 동시에 발급받아서

    -> Id Token 내의 Payload를 통해서 바로 "유저 식별(= 인증)"을 하고

    -> 캘린더의 일정 내용에 대해서 추가 정보를 요청하는 API 요청을 통해 추가적인 기능에 대한 "접근 권한(= 인가)"를 해요.

     

    여기서는 OAuth 2.0과 동일하게 캘린더에 접근하려면 Access Token을 통한 API 요청으로 "추가적인 기능에 접근" 하는 것은 동일해요.

    하지만 OIDC는 ID Token을 Access Token을 동시에 발급받고 추가적인 API 호출 없이 ID Token의 내부 데이터를 사용해서 유저를 신원을 알 수 있는 거죠.

     

    "인증"과 "인가"의 차이를 이해할 수 있겠죠?

     

     

    추가적으로 재밌는 사실은 RFC 6749에서는 공식적으로 "인증" 기능에 대한 명시는 없지만

    OIDC 공식 표준 문서에는 생겼어요.

     

    목차에서 5.3.1 UserInfo Request를 보면 돼요.

    https://openid.net/specs/openid-connect-core-1_0.html#UserInfoRequest

     

    ID Token 내부 데이터 구조

    그럼 일단 ID Token이 뭐길래 유저의 인증을 할 수 있다는 건지 궁금하죠?

    바로 아래 링크는 OIDC의 공식 표준 문서예요.

     

    목차에서 2. ID Token을 보면 돼요.

    https://openid.net/specs/openid-connect-core-1_0.html#IDToken

     

    Final: OpenID Connect Core 1.0 incorporating errata set 2

    OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol. It enables Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server, as well as to obtain basic profile information about

    openid.net

     

    {
       "iss": "https://server.example.com",
       "sub": "24400320",
       "aud": "s6BhdRkqt3",
       "nonce": "n-0S6_WzA2Mj",
       "exp": 1311281970,
       "iat": 1311280970,
       "auth_time": 1311280969,
       "acr": "urn:mace:incommon:iap:silver"
      }

     

    Claim 필수 여부 설명
    iss 필수 토큰 발급자(Authorization Server)의 식별자(URI)
    sub 필수 인증된 사용자의 고유 식별자
    aud 필수 토큰의 대상 Audience(일반적으로 클라이언트의 client_id)
    exp 필수 토큰 만료 시각(UTC 기준 초 단위)
    iat 필수 토큰 발급 시각(UTC 기준 초 단위)
    auth_time 선택 인증이 실제로 수행된 시각(특정 요청에서 필수)
    nonce 선택 재생 공격 방지를 위한 임의값(요청에 따라 필수)
    acr 선택 인증 컨텍스트 클래스 참조(인증 수준 등)
    amr 선택 인증에 사용된 방식(예: 비밀번호, OTP 등)
    azp 선택 토큰이 발급된 대상(Authorized party)

     

    표준 명세의 내용은 위와 같아요.

    OIDC가 표준이 되기 위한 최소한의 요소라고 보면 될 것 같아요.

     

    그럼 실제로 우리가 접하는 OIDC의 형태는 어떻게 생겼을까요?

    Kakao를 예시로 들어볼게요.

     

    https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#oidc-get-id-token-info-sample

     

    Kakao Developers

    카카오 API를 활용하여 다양한 어플리케이션을 개발해 보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

    developers.kakao.com

     

    {
        "iss": "https://kauth.kakao.com",
        "aud": "${APP_KEY}",
        "sub": "166959",
        "iat": 1647183250,
        "exp": 1647190450,
        "nonce": "${NONCE}",
        "auth_time": 1647183250
    }

     

    실제로 Kakao 개발자 공식 문서에서 제공되는 ID Token의 Payload의 내용이에요.

    "iss"부터 "exp"까지 5개의 필수 요소가 들어있고 추가적인 "nonce", "auth_time" 필드가 있는 것 을 볼 수 있어요.

     

    이렇게 표준이 지켜지기에 OIDC가 다양한 플랫폼에서 표준으로 사용될 수 있는 이유이기도 해요.

     

    공식 표준 지켜진다면 이메일(email), 이름(name)과 같은 정보가 추가적으로 포함되어있을 수 있어요.

     

    그럼 ID Token을 어떻게 활용해야 할까요?

    OIDC의 공식 표준을 기반으로 설명드리자면 ID Token을 사용하기 전에 필수적으로 검증이 필요해요.

     

    목차 3.1.3.7. ID Token Validation을 보면 돼요.

    https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation

     

    Final: OpenID Connect Core 1.0 incorporating errata set 2

    OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol. It enables Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server, as well as to obtain basic profile information about

    openid.net


    OIDC의 ID Token은 JWT 형태인데 비대칭키 방식으로 서명이 되어있어요.

    즉, 공개키를 통해 Payload의 유효성을 검증할 수 있는 것이죠.

     

    공개키(JWK)는 OIDC Provider의 ". well-known/openid-configuration"이라고 불리는 URI에서 필요 요소를 확인할 수 있어요.

     

    JWK(JSON Web Key)는 암호화에 사용되는 키(특히 공개키)를 JSON 포맷으로 표현하는 표준 데이터 구조예요.

     

    Kakao를 예시로 들자면

     

    https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#oidc-find-public-key

     

    Kakao Developers

    카카오 API를 활용하여 다양한 어플리케이션을 개발해 보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

    developers.kakao.com

    실제 공식 문서에서 제공하는 키의 값이에요.


    https://kauth.kakao.com/.well-known/jwks.json

     

    {
        keys: [
            {
                kid: "3f96980381e451efad0d2ddd30e3d3",
                kty: "RSA",
                alg: "RS256",
                use: "sig",
                n: "q8zZ0b_MNaLd6Ny8wd4cjFomilLfFIZcmhNSc1ttx_oQdJJZt5CDHB8WWwPGBUDUyY8AmfglS9Y1qA0_fxxs-ZUWdt45jSbUxghKNYgEwSutfM5sROh3srm5TiLW4YfOvKytGW1r9TQEdLe98ork8-rNRYPybRI3SKoqpci1m1QOcvUg4xEYRvbZIWku24DNMSeheytKUz6Ni4kKOVkzfGN11rUj1IrlRR-LNA9V9ZYmeoywy3k066rD5TaZHor5bM5gIzt1B4FmUuFITpXKGQZS5Hn_Ck8Bgc8kLWGAU8TzmOzLeROosqKE0eZJ4ESLMImTb2XSEZuN1wFyL0VtJw",
                e: "AQAB"
            },
            {
                kid: "9f252dadd5f233f93d2fa528d12fea",
                kty: "RSA",
                alg: "RS256",
                use: "sig",
                n: "qGWf6RVzV2pM8YqJ6by5exoixIlTvdXDfYj2v7E6xkoYmesAjp_1IYL7rzhpUYqIkWX0P4wOwAsg-Ud8PcMHggfwUNPOcqgSk1hAIHr63zSlG8xatQb17q9LrWny2HWkUVEU30PxxHsLcuzmfhbRx8kOrNfJEirIuqSyWF_OBHeEgBgYjydd_c8vPo7IiH-pijZn4ZouPsEg7wtdIX3-0ZcXXDbFkaDaqClfqmVCLNBhg3DKYDQOoyWXrpFKUXUFuk2FTCqWaQJ0GniO4p_ppkYIf4zhlwUYfXZEhm8cBo6H2EgukntDbTgnoha8kNunTPekxWTDhE5wGAt6YpT4Yw",
                e: "AQAB"
            }
        ]
    }

     

    위 Json 필드의 의미는 아래와 같아요.

     

    필드 설명
    kid Key ID, 키 식별자(임의 문자열)
    kty Key Type, RSA/EC 등
    alg 알고리즘(ex: RS256)
    use sig(서명용), enc(암호화용)
    n RSA 공개키의 modulus(Base64url 인코딩)
    e RSA 공개키의 exponent(Base64url 인코딩)

     

    결국 키를 바로 알려주지 않죠?

     

    키를 조합하는 방법이 있는데 해당 방식은 다음과 같아요.

     

    import java.math.BigInteger;
    import java.security.KeyFactory;
    import java.security.NoSuchAlgorithmException;
    import java.security.PublicKey;
    import java.security.spec.InvalidKeySpecException;
    import java.security.spec.RSAPublicKeySpec;
    
    private PublicKey getPublicKey(Map<String, Object> findKey) throws InvalidKeySpecException, NoSuchAlgorithmException {
            final byte[] decodeN = Base64.getUrlDecoder().decode((String) findKey.get("n"));
            final byte[] decodeE = Base64.getUrlDecoder().decode((String) findKey.get("e"));
    
            final BigInteger n = new BigInteger(1, decodeN);
            final BigInteger e = new BigInteger(1, decodeE);
    
            final RSAPublicKeySpec keySpec = new RSAPublicKeySpec(n, e);
    
            return KeyFactory.getInstance("RSA").generatePublic(keySpec);
    }

     

    이전 사이드 할 때의 코드인데 제공되는

     

    키의 수치 파라미터(: RSA n, e) 정수 → big-endian 바이트 배열 → Base64url 인코딩 순서로 변환된 값이므로 역순으로 변환하여 사용하면 돼요.

    "따라서 Base64url 디코딩 -> big-endian 바이트 배열정수 변환" 을 통해 RSA 공개키 생성 공식의

    modulus(n), exponent(e) 으로 사용하면 돼요.

     

    해당 글에서는 공개키 생성 방식에 대해서는 깊게 다루진 않고 실제 개발에서 사용된 내용을 간단히 정리해봤어요.

     

    이후에는 실제 JAVA로 구현했던 OIDC 전체 구현 플로우와 디자인 패턴에 대한 기록을 2번째 글에서 남겨보도록 해볼게요.

     

    반응형

    댓글

Designed by black7375.