Home IRSA의 원리를 파헤쳐보자 4 - OIDC
Post
Cancel

IRSA의 원리를 파헤쳐보자 4 - OIDC

지난 글에서는 OAuth2.0를 살펴보았습니다. OAuth2.0을 이해해야 본 글에서 설명할 OIDC를 이해할 수 있습니다. OIDC는 인증(authentication)을 위한 프로토콜입니다.

IRSA의 원리를 파헤쳐보자 시리즈

  1. IRSA의 원리를 파헤쳐보자 1 - K8S Admission Webhook
  2. IRSA의 원리를 파헤쳐보자 2 - K8S Sevice Account와 Service Account Token Volume Projection
  3. IRSA의 원리를 파헤쳐보자 3 - OAuth2.0
  4. IRSA의 원리를 파헤쳐보자 4 - OIDC
  5. IRSA의 원리를 파헤쳐보자 5 - IRSA Process


OIDC(OpenID Connect)

OIDC란?

OIDC(OpenID Connect)란 OAuth 위에서 동작하는 사용하는 인증(authentication)을 위한 프로토콜입니다. 앞서 우리는 위에서 OAuth2.0에 대해서 배웠습니다. 인가를 담당하는 표준화된 프로토콜이라고 했죠. 그리고 인가를 하기 위해서 필연적으로 인증 과정이 수반되었습니다(구글 로그인, 페이스북 로그인 등) 이러한 사실 때문에 많은 사람들이 OAuth를 인가와 인증 두 가지 목적을 모두 담당하는 프로토콜이라고 오해하고 있습니다.(저도 포함입니다… 🙋‍♀️)


OIDC와 OAuth를 구분하자

사탕의 맛을 내기 위한 재료들과 사탕이 있습니다. 각각 인가와 인증으로 비유할 수 있습니다.

그러나 이는 엄청난 오해입니다. 비유를 들어서 쉽게 설명해보겠습니다(이해를 돕기 위한 비약이 있을 수 있습니다). 사탕의 맛을 내기 위한 재료들이 있습니다. 오렌지, 딸기, 초콜릿 등이 있습니다. 이런 재료들은 그 자체로도 충분히 우리의 먹거리가 되어줍니다. 그리고 사탕도 많은 종류가 있습니다. 오렌지 맛 사탕, 딸기 맛 사탕, 초콜릿 맛 사탕을 예시로 들 수 있습니다. 여기서 중요한 점은 초콜릿과 초콜릿 맛 사탕은 서로 깊은 연관이 있지만, 결국엔 서로 다르다는 것입니다.

이 비유에서 사탕의 맛을 내기 위한 재료들은 인가(authorization)를 구현하기 위한 여러가지 기술 또는 프로토콜이라고 할 수 있습니다. 그 중 가장 대중적으로 쓰이는 프로토콜이 바로 초콜릿, OAuth 2.0입니다. 그리고 초콜릿은 그 자체로 간식으로 먹는 것처럼 OAuth 2.0도 인가의 기능을 그 자체로 담당할 수 있습니다. 한편 사탕은 인증(authentication)을 구현하기 위한 기술 또는 프로토콜입니다. 특히 OIDC는 OAuth2.0에 기반을 둔 대표적인 프로토콜인데요, 일명 초콜릿 맛 사탕이라고 할 수 있겠습니다.

이처럼 OAuth(초콜릿)와 OIDC(초콜릿 맛 사탕)는 뗄려야 뗄 수 없는 관계입니다. 그러나 이 둘은 서로 엄연히 다른 목적을 가지고 있으며, OAuth와 OIDC를 혼동하는 것은 OAuth 공식 문서에서도 지적하고 있는 매우 중대한 오류입니다(그 이유에 대해서는 부록에 번역 및 작성해 두었습니다). OAuth는 인가를 위한 프로토콜인데, 특정 범위(scope)의 권한을 인가하기 위해서는 어쩔 수 없이 인증의 과정을 거치기는 합니다. 그러나 OAuth 흐름에서 발생하는 인증은 OAuth 프로토콜의 본래의 목적이 아니라 하나의 수단에 불과합니다. 한편 OIDC는 OAuth 위에서 동작하는 인증 프로토콜입니다. OAuth가 인가를 위해 어쩔 수 없이 인증 과정을 거치는데, 이 과정에서 OIDC는 인증을 위해 사용자의 신원을 식별하는데 활용 되는 id_token을 추가적으로 발급합니다(이때 OAuth는 access_token`을 발급합니다).


OIDC 주체

OIDC는 OAuth2.0 위에서 동작하는 프로토콜이지만, OIDC에 참여하는 주체들의 명칭은 OAuth2.0과 조금 다릅니다.

  • OpenID provider(IdP): ID token을 발급하는 인가 서버(authorization server)입니다. OAuth2.0에서의 인가 서버에 해당합니다.

  • End user: 인증하는 주체입니다. OAuth2.0에서의 리소스 소유자(Resource owner)에 해당합니다.

  • Relying party: OpenID provider에 id token을 요청하는 주체입니다. OAuth2.0에서의 클라이언트에 해당합니다.


OIDC에서 id token을 발급 받는 과정

OIDC는 OAuth 위에서 동작하는 프로토콜입니다. 따라서 이전 OAuth2.0 글에서 언급했던 플로우를 거의 그대로 따릅니다. 다만 다음과 같은 부분에서 차이가 있습니다:

  • 로그인 페이지 요청시 scope=openid를 반드시 명시합니다. 이 경우에만 추후 id_token이라는 사용자의 신원을 확인할 수 있는 토큰이 발급됩니다.
  • access_token 교환 요청이 성공적으로 수행되면 기존 OAuth2.0에서 발급되었던 access_tokenrefresh_token 이외에 id_token이 추가 발급됩니다.


발급된 id_token을 크게 2가지 용도로 사용될 수 있습니다.

  • 클라이언트(relying party, 애플리케이션)가 유저를 자체적으로 인증 용도(e.g., 카카오 아이디로 로그인 등)
  • 클라이언트가 다른 서비스에 인증하는 용도(e.g., k8s의 파드가 id_token을 이용하여 k8s api server에 인증)

이 중 클라이언트가 유저를 인증하는 용도로 사용되는 것이 더 일반적인 경우입니다. 그리고 이 과정은 id_token을 검증하고, id_token에 담겨 있는 정보를 조회해서 내부 DB와 교차 검증을 하는 순서로 진행됩니다. 이를 이해하기 위해서 id_token이 실제로 어떻게 생겼는지 살펴봅시다.


id_token

id_tokenaccess_token과 가장 크게 구별되는 점은 실제로 사용자와 관련된 정보를 담고 있다는 점입니다. id_token은 크게 헤더(Header), 페이로드(Payload), 서명(Signature)라는 총 3개의 파트가 온점(.)으로 구분된 스트링입니다. 원 형태는 JWT(Json Web Token)이지만, 이를 각 파트별로 base64로 인코딩한 후 온점(.)으로 구분합니다. 실제 id_token은 다음과 같은 형태입니다. 디코딩 된 실제 값은 여기를 참조해주세요.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2thdXRoLmtha2FvLmNvbSIsImF1ZCI6Imtvby1hcHAiLCJzdWIiOiJnd2tvbzgyQGdtYWlsLmNvbSIsImV4cCI6MTM1MzYwNDkyNiwiaWF0IjoxMzUzNjAxMDI2fQ.PEST2li71tPylFlh13cZDC_JqyCq13jLxIJt8vyNpLQ


Header에는 토큰의 타입과 이 토큰을 서명할 때 어떤 암호화 알고리즘을 사용하였는지, 토큰의 유형은 무엇인지 명시되어 있습니다. 주로 사용되는 암호화 알고리즘은 HS256(HMAC SHA256)1이나 RSA입니다. 참고로 HS256은 대칭키 암호화 방식, RSA는 비대칭키 암호화 방식입니다.

1
2
3
4
{
  "alg": "HS256", # 해시 알고리즘 (HMAC, SHA256, RSA)
  "typ": "JWT" # 토큰 유형
}


Payload는 JWT의 핵심 내용이 명시 되어 있는 부분으로 여러 종류의 claim2으로 구성되어 있습니다.

1
2
3
4
5
6
7
{
  'iss': 'https://kauth.kakao.com', 
  'aud': 'koo-app', 
  'sub': 'gwkoo82@gmail.com', 
  'exp': 1353604926,
  'iat': 1353601026
}

특히 중요한 claim들은 다음과 같습니다.

  • iss(issuer): id_token을 발급한 주체(e.g., google, facebook, kakao, …)
  • aud(audience): id_token을 활용하는 주체(e.g., 클라이언트, aws.sts)
  • sub(subject): 리소스 오너(resource owner), 엔드 유저(end-user) (e.g., 사용자, 쿠버네티스 pod)
  • exp: id_token 만료 시점
  • iat: id_token 발급 시점


마지막으로 Signature(서명)는 id_token이 중간에 변조되진 않았는지, 유효성을 검사하기 위해 필요한 파트입니다. 대략 아래와 같은 pseudo 코드를 거쳐서 만들어집니다(HS256 알고리즘 기준).

1
2
3
4
5
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  SECRET_VALUE
)

이렇게 해시값으로 암호화된 서명은 값은 (HS256 기준) 암호화에 사용된 SECRET_VALUE가 있다면 복호화가 가능합니다. 따라서 메시지에 실린 JWT의 header나 payload가 변조되었는지 여부를 서명을 복호화 한 다음, 서명시 같이 포함한 header와 payload 값과 비교해보면 곧바로 확인 가능합니다.


id_token 검증 과정

  1. 온점을 기준으로 id_token의 값을 헤더, 페이로드, 서명 부분으로 분리합니다.
  2. 페이로드를 base64 디코딩 합니다.
  3. iss가 실제 발급자와 일치하는지 확인합니다.
  4. aud가 현재 자기 자신 application의 키와 일치하는지 확인합니다.
  5. exp가 현재 유닉스 타임보다 큰 값인지, 즉 만료되었는지 확인합니다.
  6. 마지막으로 서명의 변조 여부를 확인합니다.


서명을 이해해 보자.

imgSHA-1은 쇄도효과의 좋은 예시입니다. 1비트만 바꿔도 결과 값이 완전히 달라집니다.

지금까지 글의 논지를 살펴보면 OIDC flow는 인증 서버에서 id_token이라는 것을 클라이언트(relying party)에게 발급 해주는 과정이라고 할 수 있습니다. 그리고 클라이언트는 이 토큰을 활용해서 다른 서비스에 인증을 하는데 활용할 수 있는데요. 이 과정에서 ‘다른 서비스’는 해당 토큰이 원 형태를 유지한 채 변조 되지 않았다는 사실을 검증해야 합니다. 그리고 토큰의 유효성을 검증 하기 위해서는 서명(signature) 파트의 값을 활용합니다. 그렇다면 도대체 서명은 어떻게 하고, 이에 대한 검증은 어떻게 한다는 걸까요? 이 부분이 궁금해서 조금 더 찾아보게 되었습니다.

먼저 서명은 암호화를 하는 것이 아님을 주의해야합니다. 어떤 데이터를 암호화 한다는 것은 그것을 사람이 쉽게 읽지 못하게 만든다는 것인데, 서명은 데이터를 읽지 못하게 하는 것이 아니라, 데이터가 ‘변조’ 되었는지 아닌지 여부만 체크하는데 활용됩니다. 변조 여부를 체크할 수 있는 것은 해시 함수의 쇄도 효과(산사태 효과, avalanche effect)의 원리에 기반합니다. 어떤 메시지가 아주 조금만 달라져도, 즉 변조가 아주 미세하게만 일어나도 해시값은 크게 바뀐다는 것입니다.

이와 같은 사실을 숙지하고, HS256 알고리즘을 서명 및 검증이 어떻게 이루어지는지 살펴봅시다. 생각보다 매우 간단합니다. 먼저 서명하는 쪽과 검증을 하는 쪽은 ‘대칭키’를 공유하고 있어야 합니다. 키라는 것은 일종의 문자열인데, 해시 함수는 결국 문자열을 비트로 변환한 후 특정 로직을 수행하므로 문자열을 그대로 사용하진 않습니다. 그리고 다음과 같이 헤더와 페이로드를 각각 base64로 인코딩 한뒤, 서로 공유 했던 대칭키와 함께 HS256을 이용해 메시지를 해시값으로 변환 합니다. 최종적으로 JWT의 ‘서명’ 부분에 이 해시값이 들어가서 전송됩니다.

1
2
3
4
5
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  SECRET_VALUE
)

이 메시지를 받는 쪽에서 해당 메시지가 변조 되었는지 어떻게 확인할 수 있을까요? 메시지를 받는 쪽은 기본적으로 JWT를 받았기 때문에 헤더와 페이로드 정보를 알 수 있습니다. 그리고 대칭키도 역시 사전에 공유되었으므로 알고 있는 정보입니다. 그럼 다시 위에 수도 코드를 실행할 수 있고, 해시값을 얻을 수 있을텐데요. 이를 전송 받았던 JWT의 서명 파트에 있는 해시와 비교 해보면 됩니다. 만일 내가 전달 받은 헤더나 페이로드가 원래 메시지와 달라졌다면 HS256의 결과인 해시값은 JWT의 서명 파트에 있는 값과 크게 차이가 날 것입니다.


Access token vs id token

 access tokenid token
audienceAccess tokens are meant to be read by the resource server(Access tokens must not be read or interpreted by the OAuth client. The OAuth client is not the intended audience of the token).ID tokens are meant to be read by the OAuth client
informationAccess tokens do not convey user identity or any other information about the user to the OAuth client.An ID token contains information about what happened when a user authenticated. The ID token may also contain information about the user such as their name or email address, although that is not a requirement of an ID token.
usageAccess tokens should only be used to make requests to the resource server.ID tokens must not be used to make requests to the resource server.
formcan be JWTs but may also be a random stringJWTs


부록

access_token을 유저 인증에 사용하면 안될까?

여기까지 오셨으면 OIDC와 OAuth와의 미묘한 차이를 이해하셨 것이라 생각합니다. 그런데 이렇게 생각해볼 수 있지 않을까요? OAuth는 어쨌든 인증 과정을 거쳐서 access_token을 발급해주었으니까 access_token을 인증 과정에 사용해도 되지 않을까요? 결론부터 말하면 이는 틀린 생각입니다. oauth.net에서는 OAuth로 인증을 하는 것의 위험성(Common pitfalls for authentication using OAuth)에 대해서 언급합니다.

access token에는 유저를 식별할 수 있는 정보가 없습니다

access token을 발급 받는 과정에 사용자 인증이 선행되기 때문에 OAuth 자체가 인증도 담당하고 있다고 생각할 수 있습니다. 그러나 access token은 근본적으로 client를 위한 개념이 아닙니다. 다시말해, access token의 audience(토근 발급 대상, 토근의 최종 사용 주체)는 client가 아니라 resource server(protected resource)입니다. client 입장에서 access token은 아무런 의미 없는 string에 불과합니다(the token is designed to be opaque to the client). 극단적으로 말하면, access token의 포맷이 바뀌었어도 authorization server와 resource server간의 로직만 이상없다면 client 입장에서는 신경 쓸 일이 전혀 없습니다. 그저 resource server로 access token을 전달하기만 하면 되니까요. 또한 access token에는 사용자와 관련된 데이터(user_idemail 등)는 전혀 없으므로 클라이언트가 사용자를 식별할 수 있는 방법은 없습니다. client가 access token으로 사용자를 식별한다는 행위는 애초에 불가능한 행위라는 것입니다.

유저의 인증 없이도 access token이 발급될 수 있습니다.

OAuth 과정에서 유저의 로그인을 통해 인증을 수행하고 최종적으로 access token을 발급 받을 수 있습니다. 그러나 이것이 access token을 발급 받을 수 있는 유일한 방법은 아닙니다. refresh token을 활용하여 유저의 인증 없이도 access token을 새로 발급 받을 수도 있습니다. 즉 access token을 발급 받았다는 사실 자체가 유저가 인증되었음을 완벽하게 보장할 수 없다는 뜻입니다.

인증 서버가 아닌 제 3자가 access token을 주입할 수도 있습니다.

만일 acces token이 URL 파라미터로 전달되는 implicit flow를 따르고, client가 access token의 유효성을 점검할 수 있는 메커니즘이 없다면 access token의 진위 여부를 판별하기 어렵습니다. 따라서 access token만으로 유저가 진짜로 인증했는지 여부를 판별하기 어렵습니다.

access token을 발급 해준 client인지 여부를 검증하는 메커니즘이 없습니다.

예를 들어, A라는 client가 정상적인 방법을 통해 access token을 발급 받은 후에 B라는 client가 해당 access token을 그대로 사용한다면, 일반적인 OAuth API들은 client를 검증하는 메커니즘이 없으므로 정상적으로 resource server와 통신이 일어날 것입니다. B라는 client가 A라는 client가 발급 받은 access token을 사용함에 있어서 유저의 인증은 개입되지 않았다는 점에서 access token으로 유저 인증을 하기에는 부족한 면이 있습니다.

access token에 대한 표준 형식이 없습니다.

OAuth 상에서 유저 인증 과정을 처리하려는 시도에 있어서 가장 큰 문제점은 access token의 표준 형식이 정해져있지 않다는 것입니다(access tokens can have different formats, structures, and methods of utilization) 예를 들어서 google에서는 유저의 아이디를 user_id 필드에 넘겨주는데, 페이스북에서는 subject 필드에 넘겨줄 수도 있습니다. 두 필드는 의미상으로는 동일하지만, 이를 처리하기 위해서는 서로 다른 방식으로 처리하는 코드를 짜야하기 때문에 번거롭습니다.


참고자료

  1. HMAC(Hash-based Message Authentication Code) + SHA(Secure Hash Algorithm)-256)를 뜻합니다. 

  2. JWT에서 claim이란 프로퍼티나 속성을 말합니다. "iss": "https://abc.com" 를 하나의 claim이라고 할 수 있으며, iss를 claim 이름, https://abc.com을 claim 값이라고 합니다. 

This post is licensed under CC BY 4.0 by the author.