Home IRSA의 원리를 파헤쳐보자 3 - OAuth2.0
Post
Cancel

IRSA의 원리를 파헤쳐보자 3 - OAuth2.0

IRSA의 원리를 파헤쳐보자 시리즈의 세번째 글입니다. 지난 시간에는 service account token volume projection에 대해서 살펴보았습니다. 핵심은 projected service account token은 기본 service account token과 다르게 audience나 만료기간 등의 추가적인 정보를 삽입할 수 있으며 OIDC token의 표준적인 형식을 따르게 된다는 점이었습니다. IRSA를 위해서는 sub, aud 등의 정보, 즉 신뢰할만한 개체에 대한 정보를 미리 작성해 두는데 AWS 측에서 토큰의 유효성을 검증하기 위해 필요한 정보를 주입시킨다는 점에서 중요합니다.

한편 IRSA를 공부하다 보면 id token, JWT에서 사용되는 단어들이 매우 많이 등장함을 알 수 있습니다. 이는 OIDC(OpenID Connect)라는 프로토콜과 깊은 관련이 있는데요. 애석하게도 OIDC를 이해하기 위해서는 먼저 OAuth2.0를 이해해야 합니다… OIDC는 OAuth2.0 위에서 동작하는 프로토콜이기 때문입니다. 따라서 이번 글에서는 IRSA를 이해하기 위한 배경지식으로서 OAuth2.0을 (수박 겉핥기 식으로 🍉) 살펴보겠습니다

사실 공부하면서, IRSA가 완벽하게 OAuth2.0이나 OIDC를 따르는 것은 아니라는 생각이 들었습니다. 그래서 본 글을 정리할지 말지 고민을 많이 했는데, OAuth2.0과 OIDC에서 나오는 개념들을 계속 현업에서 부딪히다 보니 본 주제를 정리하기로 하였습니다. 머신러닝 엔지니어로서 언젠간 반드시 도움이 될 것이라고 생각하며… 😭

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


인증(authentication)과 인가(authorization)

authentication_and_authorization인증과 인가의 차이(출처: okta)

앞으로의 내용에 인증(authentication)과 인가(authorization)이라는 단어가 계속해서 등장할 것입니다. 인증과 인가는 한국어로 보면 큰 차이가 없어 보이는 단어인데요, 보안의 맥락에서는 엄연히 다른 개념입니다. 먼저 인증이란 사용자의 신원(identity)를 확인 및 검증 하는 행위입니다. 인증의 대표적인 예로는 아이디와 비밀번호를 통한 로그인이 있습니다. 한편 인가는 사용자에게 특정 리소스나 기능에 접근할 수 있는 권한 또는 권한의 범위(scope)를 부여하는 행위를 말합니다. 우리가 네이버에 로그인(인증)을 했다고 하더라도 다른 사람의 데이터에 접근할 수 있는 권한은 없습니다. 다시 말해 타인의 데이터에 접근하는 인가는 받지 못한 것입니다.

이어지는 글에서 더 자세히 설명하겠지만 인증과 인가는 각각 OIDC, OAuth2.0와 관련이 깊습니다. OIDC는 인증을 담당하는 프로토콜이며, OAuth2.0은 인가를 담당하는 프로토콜입니다. 그렇다고 해서 OIDC와 OAuth2.0이 아예 별개의 개념이진 않습니다. 오히려 서로는 매우 깊은 관련이 있습니다.


OAuth2.0, 왜 필요할까?

카카오, 페이스북, 구글, 애플 아이디로 다른 사이트에 로그인 할 수 있습니다.

만일 우리가 하나의 웹서비스를 만들었다고 하죠. 그리고 그 서비스에 접속하는 사용자가 구글 캘린더에 등록한 일정을 보여주는 화면을 만들고 싶습니다. 그렇다면 우리 서비스는 사용자를 대신해서 구글에 접속해서 캘린더 정보를 가져올 수 있어야겠죠. 가장 쉬운 방법은 무엇일까요? 바로 우리의 구글 아이디와 비밀번호를 웹서비스에 전달해준 다음, 웹서비스가 그 정보를 그대로 이용해서 구글 캘린더의 정보를 가져오면 됩니다. 그러나 직관적으로 생각해봐도 이는 그리 좋은 방법은 아닙니다. 이러면 웹서비스가 사용자의 아이디와 비밀번호를 관리해야하는 부담감이 있고, 구글에서는 우리 웹서비스를 신뢰하기가 어렵죠.

이러한 문제를 해결하기 위해 OAuth가 등장했습니다. OAuth는 최초 1.0 버전이 있은 뒤로, 현재 거론되는 OAuth는 대부분 OAuth2.0입니다(OAuth2.0은 하위 버전과 호환되지 않는다고 합니다). OAuth는 표준화된 방법과 절차에 따라 권한을 인가(authorization)하기 위한 프로토콜입니다. 혹시 ‘~로 로그인 하기’와 같은 소셜 로그인 기능을 사용해 보신적 있지 않으신가요? 사용자가 특정 웹사이트에 접속한 다음, ‘구글 아이디로 로그인 하기’ 버튼을 누르면 아이디와 비밀번호를 입력하고, 어떤 권한을 허용할 것인지 묻는 창이 나타납니다. 그리고 허용하면 구글 아이디를 통해서 다른 웹사이트에 로그인이 이루어집니다. 이러한 과정은 결론적으로 특정 웹사이트에게 구글 사용자로서의 권한을 ‘위임’해준 것입니다. 지금은 매우 단순하게 표현했지만, 사실 매우 복잡한 과정이 밑단에서 이루어져야 하며 그 과정을 표준화한 것이 바로 OAuth2.0이라고 이해하시면 됩니다.


OAuth2.0의 주체

지금까지 사용자, 웹서비스, 구글 등의 표현을 사용하였는데요, 이를 OAuth 진영에서 사용하는 용어로 바꿔보겠습니다. 총 4가지의 주체가 있습니다.

  • 리소스 소유자(resource owner):

    예시에서 ‘사용자’입니다. 리소스에 대한 접근 권한을 부여할 수 있는 주체입니다. 만일 리소스 소유자가 사람이라면 엔드 유저(end-user)라고도 불립니다.

  • 리소스 서버(resource sever):

    예시에서 ‘구글’입니다. 리소스를 호스팅 하는 주체입니다. 엑세스 토큰(access token)을 확인하여 리소스에 접근하려는 요청을 허가할지 거부할지 결정 할수 있습니다. 아래에서 설명할 인가 서버와 합쳐서 표현하기도 합니다.

    리소스 서버에게 클라이언트가 누구인지 미리 알려주는 과정이 선행되어야 OAuth 2.0 프로토콜이 제대로 동작합니다. 이는 리소스 서버에서 client id, client secret를 발급받고, redirect uri를 등록 해두는 행위를 말합니다. 아래 OAuth2.0 동작 흐름에서 더 자세히 설명하겠습니다.

  • 클라이언트(client):

    예시에서 ‘우리 웹서비스’입니다. 리소스 오너를 대신하여 리소스에 대한 접근을 요청 하는 주체입니다.

    단어 때문에 리소스 소유자와 클라이언트를 헷갈릴 수도 있습니다. 그러나 클라이언트라는 단어는 상대적인 개념입니다. 우리 웹서비스는 리소스 서버나 인가 서버 입장에서 보았을 때 클라이언트이므로 이러한 이름을 갖게 되었다고 생각할 수 있습니다.

  • 인가 서버(authorization server):

    예시에서 ‘구글’입니다. 클라이언트에서 엑세스 토큰(access token)을 발급하는 주체입니다. 리소스 오너가 자신의 신분을 성공적으로 증명했을 때 엑세스 토큰을 발급 해 줍니다. 위에서 설명한 리소스 서버와 합쳐서 표현하기도 합니다.


OAuth2.0의 동작 흐름(메커니즘)

먼저, OAuth2.0은 클라이언트가 리소스 소유자에게 인가를 얻어서 리소스 소유자 대신에 리소스 서버에 접근할 수 있는 방법이나 절차를 정의한 프로토콜이라는 사실을 잊지 말아야 합니다. 클라이언트는 인가를 잘 받았다는 징표로서 최종적으로 access token이라는 것을 얻을 수 있게되고, 이를 통해 정해진 권한 범위(scope) 내에서 리소스 서버의 자원을 마음껏 이용할 수 있게 되는 것입니다.

클라이언트가 access token, 즉 인가를 받기 위한 방법은 여러가지가 있습니다. 각 방식마다 장단점이 존재한다고 하는데, 더 자세한 내용은 관련 자료를 참고해주세요(구체적인 내용은 더 공부하고 정리해 보겠습니다). 본 글에서는 Authorization Code Grant 방식에 집중해서 살펴보겠습니다.

  • Authorization Code Grant: 클라이언트가 웹 서버인 경우 사용. 안정성이 높아 일반적으로 많이 사용하는 방식.
  • Implicit Grant: 인가 서버에서 클라이언트에 곧바로 access token을 발급함. 브라우저에 access token이 그대로 노출되기 때문에 안전하지 않음.
  • Resource Owner Password Credentials: 리소스 소유자의 인증 정보가 클라이언트에 전송된 다음 바로 인증 서버로 전송해도 되는 경우 사용
  • Client Credentials Grant: 클라이언트가 리소스 소유자와 동일할 경우 사용


OAuth2.0: Authorization Code Grant 흐름도


  1. 리소스 서버에 우리 웹서비스를 클라이언트로 등록

    client_id, client_secret을 발급받고, redirect_uri를 등록합니다.

    우리 웹서비스를 클라이언트를 등록하고 client_idclient_secret을 발급 받아둡니다. 그리고 리소스 소유자가 인증에 성공 했을 때 authorization code와 함께 사용자를 돌려 보낼 redirect_uri를 설정해야합니다. redirect_uri는 기본적으로 보안을 위해 https를 사용해야하지만 localhost의 경우는 예외적으로 http로 설정할 수 있습니다. client_idclient_secret 또한 인가를 받으려는 클라이언트가 사전에 승인된(등록된) 대상인지를 확인하고 access_token을 발급하는데 사용됩니다. client_id는 공개되어도 상관 없지만 client_secret은 절대 유출되면 안 되는 정보입니다.


  2. 로그인 요청 및 유저 로그인 진행

    리소스 소유자가 로그인 하게하고, 로그인 성공시 허용할 권한 동의 여부를 묻는다(인가).

    리소스 소유자가 클라이언트의 웹서비스에서 ‘구글 아이디로 로그인 하기’ 버튼을 클릭합니다. 그러면 클라이언트는 리소스 소유자를 인가 서버의 로그인을 담당하는 웹페이지로 보냅니다(예를 들어 구글 로그인 화면 등). 이 때 client_id, redirect_uri, response_type, scope 등의 파라미터를 쿼리 스트링을 URL에 포함합니다. 요청은 다음과 같은 형태입니다.

    1
    2
    3
    4
    5
    
    https://accounts.google.com/o/oauth2/v2/auth?
     scope=scope&
     response_type=code&
     redirect_uri=https%3A//oauth2.example.com/code
     client_id=client_id
    

    쿼리 스트링에는 보다 다양한 파라미터를 포함할 수 있지만, 필수적인 파라미터는 다음과 같습니다.

    • client_id: 리소스 서버에 애플리케이션(웹서비스)를 등록하고 발급 받은 클라이언트 id입니다.
    • redirect_uri: 리소스 소유자가 로그인에 성공할 경우 리소스 소유자를 리디렉션 하는 주소입니다. 애플리케이션을 등록할 때 설정해둔 값과 정확히 일치해야 합니다. 그렇지 않을 경우 redirect_uri_mismatch 에러가 발생하게 됩니다.
    • response_type: authorization code grant 방식으로 진행할 경우 code라고 지정합니다.
    • scope: 인가를 해줄 권한의 범위를 뜻합니다. 로그인에 성공한 후에 사용자에게 권한 동의 여부를 묻는데, 이와 관련된 파라미터입니다.


    클라이언트가 위 주소로 리소스 소유자를 이동시키면 유저는 자신의 아이디와 비밀번호를 입력하여 리소스 소유자임을 인증합니다. 최종적으로는 scope에 포함된 권한 인가를 동의 하겠냐는 안내 페이지가 보여지는데, 리소스 소유자가 이에 동의를 하면 클라이언트는 리소스 소유자를 대신하여 해당 권한을 가지고 리소스 서버에 자원을 요청할 수 있는 발판이 마련된 것입니다.


  3. authorization code 발급 2번 과정을 통해 리소스 소유자가 인증을 성공적으로 수행하였다면, 인증 서버는 사전에 등록해둔 redirect_uri로 사용자를 이동시킵니다. 이때 중요한 점은 쿼리 스트링에 authorization code가 포함된다는 것입니다. authorization code는 최종적으로 access token을 발급받기 위한 임시적 성격을 가지고 있으며, 일반적으로 수명이 매우 짧습니다. authorization code는 redirect_url 뒤에 code 라는 쿼리 스트링의 value로 전달됩니다. 다음과 같은 형태입니다.

    1
    
    https://oauth2.example.com/code?code=4/0AWgavdfkHODwoqv-iaNh4aoTlahhQIw6MzxKJ7B3liHY0U4c1tNxJ-Xaae-XwdVJkORZzg
    


  4. access token 교환 요청

    authorization code는 임시 코드라고 하였습니다. 클라이언트는 access token을 발급 받기 위해서 authorization code, client_id, client_secret를 (구글에 경우에) token endpoint에 전송합니다. 이러한 과정은 엑세스 토큰 교환(exchange)이라고도 합니다. 다음은 access token을 발급 받기 위한 요청의 예시입니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    POST /token HTTP/1.1
    Host: oauth2.googleapis.com
    Content-Type: application/x-www-form-urlencoded
       
    code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7&
    client_id=your_client_id&
    client_secret=your_client_secret&
    redirect_uri=https%3A//oauth2.example.com/code&
    grant_type=authorization_code
    
    • code: 인가 서버가 발급한 authorization code입니다.
    • client_id: 미리 설정해둔 client_id입니다.
    • client_secret: 미리 설정해둔 시크릿 값입니다. access token으로 교환하기 위해 처음으로 사용됩니다.
    • redirect_uri: 인가 서버 측에 등록해둔 redirect_uri와 같은 값인지 확인하기 위해 필요합니다.
    • grant_type: 반드시 ‘authorization_code‘라는 문자열이어야 합니다.


    위의 요청 예시를 보면 그간의 예시와 조금 형태가 달라졌음을 알 수 있는데요. access token의 경우 매우 중요한 토큰이다보니 기존처럼 브라우저를 통한 쿼리 스트링으로 전달할 경우 심각한 보안상 위협을 초래할 수 있습니다. 따라서 access_token을 다룰 때는 보통 클라이언트의 백엔드 서버가 이를 처리하는 것이 일반적인 프랙티스라고 할 수 있겠습니다. 백엔드가 처리하면 엔드 유저의 브라우저에는 엑세스 토큰이 직접적으로 노출되지 않기 때문에 훨씬 안전한 방법입니다.


  5. access token 발급

    인증 서버는 authorization code, client_id, client_secret을 검증하고 access_token을 발급해줍니다. 이 토큰은 매우 중요하므로 절대 유출이 되어서는 안됩니다. 클라이언트는 이를 자신들의 안전한 백엔드 DB에 저장해두어야 합니다. 요청이 정상적으로 처리되면 아래와 같은 응답을 받게 됩니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    HTTP/1.1 200 OK
    Content-Type: application/json;charset=UTF-8
    Cache-Control: no-store
    Pragma: no-cache
       
    {
    "access_token":"2YotnFZFEjr1zCsicMWpAA",
    "token_type":"example",
    "expires_in":3600,
    "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
    "example_parameter":"example_value"
    }
    
    • access_token: 클라이언트가 리소스 오너를 대신해서 특정 권한 범위(scope)내에서 리소스 서버의 자원에 접근할 수 있게 만드는 중요한 토큰입니다.
    • expires_in: access_token은 보안상 매우 중요하므로 만료 기간이 정해져있습니다. 만일 만료 기간이 지난 access_token으로 리소서 서버에 엑세스 한다면 HTTP 401 에러가 발생합니다.
    • refresh_token: access_token의 만료기간이 지났을 경우 유저를 다시 로그인 시키게 하는 것은 번거로울 수 있습니다. access_token 발급 시에 refresh token이라는 것도 같이 발급되는데, 클라이언트 이를 저장해 두었다가 401 에러가 발생할 경우 refresh_token을 전송하면됩니다. 이 경우 access_token이 바로 발급 됩니다.


  6. access token으로 리소스 요청 및 응답 클라이언트는 access token으로 리소스 서버에 엑세스 하여 리소스 오너의 자원을 가져올 수 있게 됩니다.


왜 귀찮게 authorization code를 access token으로 교환하는 과정이 있을까(참고1, 참고2)?

OAuth를 정리하면서 가장 의문이 생겼던 점입니다. 굳이 왜 귀찮게 authorization 코드를 받은 다음, 이를 다시 access token으로 교환하는 과정이 있는 걸까요? 결론부터 말하면 보안상의 안전성을 위한 것입니다. 일반적인 OAuth의 흐름을 따른다면, 브라우저라는 존재가 반드시 끼게 됩니다. 엔드 유저가 클라이언트(웹 서버)에 접근하는 가장 일반적인 방법이 브라우저로 접속하는 것이니까요. 따라서 flow를 쭉 진행하다보면 자연스럽게 authorization code는 redirect_uri에 쿼리스트링으로 전달됩니다. 만일 redirect_uri에 access token을 한번에 쿼리 스트링으로 전달한다면 브라우저에 쉽게 저장되고 노출되기 때문에 보안상 심각한 위협이 될 수 있습니다.

따라서 authorization code라는 임시 시크릿을 쿼리 스트링에 태워서 클라이언트의 프론트엔드로 전달한 뒤, 이를 안전하게 처리할 수 있는 백엔드 서버에서 access token으로 교환하는 과정이 아무래도 보안상 안전합니다. 브라우저에 그대로 노출되지 않기 때문입니다.


참고자료

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

IRSA의 원리를 파헤쳐보자 2 - K8S Sevice Account와 Service Account Token Volume Projection

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