RFC 6749

RFC 6749는 OAuth 2.0 권한 부여 프레임워크(The OAuth 2.0 Authorization Framework)를 정의하는 인터넷 표준 문서이다. 이 문서는 2012년 10월에 인터넷 표준화 기구인 IETF(Internet Engineering Task Force)에 의해 발행되었다. 주 저자는 Dick Hardt이며, 여러 기술 전문가들의 협업으로 만들어졌다.

OAuth 2.0은 이전 버전인 OAuth 1.0(RFC 5849)의 후속 버전으로, 다양한 웹 애플리케이션, 데스크톱 애플리케이션, 모바일 애플리케이션 및 IoT 장치에서 안전한 권한 위임을 가능하게 하는 프로토콜이다. 간단히 말해, OAuth 2.0은 사용자가 자신의 비밀번호를 공유하지 않고도 제3자 애플리케이션에 자신의 데이터에 대한 제한된 접근 권한을 부여할 수 있도록 해주는 프레임워크이다.

OAuth 1.0에서 OAuth 2.0으로의 전환은 단순한 버전 업그레이드가 아닌 완전한 재설계였다. OAuth 2.0은 OAuth 1.0보다 구현이 더 간단하고, 모바일 애플리케이션과 같은 다양한 클라이언트 유형을 더 잘 지원하며, 더 나은 확장성을 제공한다. 그러나 이런 단순화는 보안 모델의 변화도 수반했다.

OAuth 2.0의 기본 개념과 용어

OAuth 2.0을 이해하기 위해서는 먼저 다음과 같은 핵심 용어와 개념을 이해해야 한다:

주요 역할(Roles)

OAuth 2.0 프레임워크에는 네 가지 주요 역할이 있다:

  1. 리소스 소유자(Resource Owner): 보호된 리소스에 접근 권한을 부여할 수 있는 엔티티로, 일반적으로 최종 사용자이다. 예를 들어, 자신의 Google 계정 데이터에 접근 권한을 부여하는 사용자이다.
  2. 리소스 서버(Resource Server): 보호된 리소스를 호스팅하는 서버로, 액세스 토큰을 사용하여 보호된 리소스 요청을 수락하고 응답할 수 있다. 예를 들어, Google의 API 서버가 이에 해당한다.
  3. 클라이언트(Client): 리소스 소유자의 권한으로 보호된 리소스에 접근을 요청하는 애플리케이션이다. 예를 들어, 사용자의 Google 계정에 접근하려는 제3자 애플리케이션이 이에 해당한다.
  4. 권한 부여 서버(Authorization Server): 리소스 소유자를 인증하고 권한을 얻은 후 클라이언트에게 액세스 토큰을 발급하는 서버이다. 예를 들어, Google의 OAuth 서버가 이에 해당한다.

토큰(Tokens)

OAuth 2.0은 두 가지 주요 토큰 유형을 사용한다:

  1. 액세스 토큰(Access Token): 보호된 리소스에 접근하기 위한 자격 증명으로, 일반적으로 문자열 형태이다. 이 토큰은 리소스 소유자가 클라이언트에게 부여한 권한 범위, 기간 등의 정보를 나타낸다.
  2. 리프레시 토큰(Refresh Token): 액세스 토큰이 만료된 후 새 액세스 토큰을 얻기 위해 사용되는 자격 증명이다. 액세스 토큰보다 일반적으로 수명이 길고, 경우에 따라 발급되지 않을 수도 있다.

클라이언트 유형

OAuth 2.0은 다양한 클라이언트 유형을 지원한다:

  1. 기밀 클라이언트(Confidential Client): 클라이언트 자격 증명(비밀)을 안전하게 보관할 수 있는 클라이언트로, 일반적으로 웹 서버에서 실행되는 애플리케이션이다.
  2. 공개 클라이언트(Public Client): 클라이언트 자격 증명을 안전하게 보관할 수 없는 클라이언트로, 일반적으로 브라우저에서 실행되는 JavaScript 애플리케이션이나 모바일 앱이 이에 해당한다.

권한 부여 유형(Grant Types)

RFC 6749는 다양한 사용 사례를 지원하기 위해 네 가지 기본 권한 부여 유형을 정의한다:

  1. 권한 부여 코드(Authorization Code): 클라이언트가 권한 부여 서버로부터 권한 부여 코드를 얻은 다음, 이 코드를 액세스 토큰으로 교환하는 방식이다. 이는 서버 사이드 웹 애플리케이션에 가장 적합하다.
  2. 암시적 부여(Implicit): 간소화된 권한 부여 코드 흐름으로, 권한 부여 코드 없이 액세스 토큰을 직접 발급받는다. 주로 브라우저 내 JavaScript 애플리케이션에 사용된다.
  3. 리소스 소유자 비밀번호 자격 증명(Resource Owner Password Credentials): 리소스 소유자의 자격 증명(아이디/비밀번호)을 클라이언트가 직접 사용하여 액세스 토큰을 얻는 방식이다. 높은 수준의 신뢰가 필요하며, 다른 방식이 불가능할 때만 사용해야 한다.
  4. 클라이언트 자격 증명(Client Credentials): 클라이언트가 자신의 자격 증명만을 사용하여 액세스 토큰을 얻는 방식으로, 클라이언트가 리소스 소유자를 대신하지 않고 자체적으로 리소스에 접근할 때 사용한다.

OAuth 2.0 권한 부여 흐름 상세 분석

권한 부여 코드 부여(Authorization Code Grant)

이 흐름은 웹 애플리케이션과 같은 기밀 클라이언트에 가장 적합하며, 다음과 같은 단계로 진행된다.

  1. 권한 요청: 클라이언트는 사용자를 권한 부여 서버로 리다이렉트하여 권한을 요청한다.

    1
    2
    3
    4
    5
    
    GET /authorize?
    response_type=code&
    client_id=s6BhdRkqt3&
    state=xyz&
    redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
    
  2. 사용자 인증 및 권한 부여: 권한 부여 서버는 사용자를 인증하고 클라이언트 요청에 대한 허가를 요청한다.

  3. 권한 부여 코드 발급: 사용자가 권한을 부여하면, 권한 부여 서버는 클라이언트로 권한 부여 코드와 함께 사용자를 리다이렉트한다.

    1
    2
    
    HTTP/1.1 302 Found
    Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
    
  4. 액세스 토큰 요청: 클라이언트는 권한 부여 코드를 사용하여 액세스 토큰을 요청한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    POST /token HTTP/1.1
    Host: server.example.com
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=authorization_code&
    code=SplxlOBeZQQYbYS6WxSbIA&
    redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb&
    client_id=s6BhdRkqt3&
    client_secret=7Fjfp0ZBr1KtDRbnfVdmIw
    
  5. 액세스 토큰 발급: 권한 부여 서버는 클라이언트의 신원을 확인하고 권한 부여 코드를 검증한 후, 액세스 토큰과 선택적으로 리프레시 토큰을 발급한다.

     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":"bearer",
      "expires_in":3600,
      "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
      "example_parameter":"example_value"
    }
    

이 흐름의 주요 장점은 높은 보안성과 리프레시 토큰 지원이다. 클라이언트 비밀이 사용자에게 노출되지 않고, 액세스 토큰도 프론트 채널(브라우저)을 통해 전달되지 않는다.

암시적 부여(Implicit Grant)

이 흐름은 브라우저 기반 애플리케이션(SPA)과 같은 공개 클라이언트에 적합하며, 다음과 같은 단계로 진행된다:

  1. 권한 요청: 클라이언트는 사용자를 권한 부여 서버로 리다이렉트한다. 이때 response_type=token으로 설정한다.

    1
    2
    3
    4
    5
    
    GET /authorize?
    response_type=token&
    client_id=s6BhdRkqt3&
    state=xyz&
    redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
    
  2. 사용자 인증 및 권한 부여: 권한 부여 서버는 사용자를 인증하고 권한을 요청한다.

  3. 토큰 발급: 사용자가 권한을 부여하면, 권한 부여 서버는 액세스 토큰을 URI 프래그먼트에 포함시켜 클라이언트로 사용자를 리다이렉트한다.

    1
    2
    
    HTTP/1.1 302 Found
    Location: https://client.example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA&state=xyz&token_type=bearer&expires_in=3600
    
  4. 토큰 사용: 클라이언트는 URI 프래그먼트에서 액세스 토큰을 추출하여 보호된 리소스에 접근하는 데 사용한다.

이 흐름은 권한 부여 코드 흐름보다 단순하지만, 보안 측면에서 몇 가지 단점이 있다. 액세스 토큰이 브라우저를 통해 전달되므로 노출 위험이 있으며, 리프레시 토큰을 지원하지 않는다.

리소스 소유자 비밀번호 자격 증명 부여(Resource Owner Password Credentials Grant)

이 흐름은 클라이언트가 리소스 소유자의 자격 증명을 직접 수집하여 액세스 토큰을 얻는 방식이다:

  1. 자격 증명 수집: 클라이언트는 리소스 소유자로부터 직접 사용자 이름과 비밀번호를 획득한다.

  2. 토큰 요청: 클라이언트는 이 자격 증명을 사용하여 권한 부여 서버에 액세스 토큰을 요청한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    POST /token HTTP/1.1
    Host: server.example.com
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=password&
    username=johndoe&
    password=A3ddj3w&
    client_id=s6BhdRkqt3&
    client_secret=7Fjfp0ZBr1KtDRbnfVdmIw
    
  3. 토큰 발급: 권한 부여 서버는 자격 증명을 검증하고 액세스 토큰을 발급한다.

이 방식은 리소스 소유자와 클라이언트 간에 높은 신뢰가 있을 때만 사용해야 한다. 예를 들어, 운영 체제 내장 클라이언트나 높은 권한을 가진 애플리케이션에 적합하다.

클라이언트 자격 증명 부여(Client Credentials Grant)

이 흐름은 클라이언트가 자신의 자격 증명만을 사용하여 액세스 토큰을 얻는 방식이다:

  1. 토큰 요청: 클라이언트는 자신의 자격 증명을 사용하여 권한 부여 서버에 액세스 토큰을 요청한다.

    1
    2
    3
    4
    5
    6
    7
    
    POST /token HTTP/1.1
    Host: server.example.com
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=client_credentials&
    client_id=s6BhdRkqt3&
    client_secret=7Fjfp0ZBr1KtDRbnfVdmIw
    
  2. 토큰 발급: 권한 부여 서버는 클라이언트 자격 증명을 검증하고 액세스 토큰을 발급한다.

이 흐름은 리소스 소유자의 컨텍스트가 없는 클라이언트-서버 간 통신에 적합하다. 예를 들어, 백그라운드 서비스나 마이크로서비스 간 통신에 사용된다.

토큰과 범위(Scopes)

액세스 토큰(Access Token)

액세스 토큰은 보호된 리소스에 접근하기 위한 자격 증명으로, 다음과 같은 특성을 가진다:

  1. 형식: RFC 6749는 토큰 형식을 명시하지 않으며, 구현에 따라 다양한 형식(불투명 문자열, JWT 등)이 사용된다.

  2. 전달 방식: 주로 HTTP Authorization 헤더를 통해 전달되며, 형식은 일반적으로 “Bearer” 토큰 유형을 사용한(RFC 6750).

    1
    2
    3
    
    GET /resource HTTP/1.1
    Host: server.example.com
    Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA
    
  3. 수명: 액세스 토큰은 일반적으로 짧은 수명(몇 분에서 몇 시간)을 가지며, 이는 토큰 노출 시 위험을 줄이기 위함이다.

리프레시 토큰(Refresh Token)

리프레시 토큰은 새로운 액세스 토큰을 얻기 위해 사용되며, 다음과 같은 특성을 가진다:

  1. 용도: 액세스 토큰이 만료된 후 사용자를 다시 인증하지 않고도 새 액세스 토큰을 발급받을 수 있다.

  2. 보안: 리프레시 토큰은 액세스 토큰보다 더 민감하므로, 클라이언트는 이를 안전하게 저장해야 한다.

  3. 갱신 프로세스: 클라이언트는 리프레시 토큰을 사용하여 다음과 같이 새 액세스 토큰을 요청한다.

    1
    2
    3
    4
    5
    6
    7
    8
    
    POST /token HTTP/1.1
    Host: server.example.com
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=refresh_token&
    refresh_token=tGzv3JOkF0XG5Qx2TlKWIA&
    client_id=s6BhdRkqt3&
    client_secret=7Fjfp0ZBr1KtDRbnfVdmIw
    
  4. 갱신 토큰 순환(Rotation): 보안 강화를 위해 새 액세스 토큰을 발급할 때 새 리프레시 토큰도 함께 발급하는 방식으로, 이전 리프레시 토큰은 무효화된다.

범위(Scopes)

범위는 액세스 토큰이 부여하는 권한의 범위를 제한하는 메커니즘이다:

  1. 정의: 범위는 일반적으로 공백으로 구분된 문자열 목록으로 표현된다. 예: scope=read_profile edit_profile read_contacts

  2. 요청: 클라이언트는 권한 요청 시 필요한 범위를 지정할 수 있다.

    1
    2
    3
    4
    5
    6
    
    GET /authorize?
    response_type=code&
    client_id=s6BhdRkqt3&
    state=xyz&
    redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb&
    scope=read_profile%20read_contacts
    
  3. 동의 화면: 권한 부여 서버는 요청된 범위에 기반하여 사용자에게 동의 화면을 제시한다.

  4. 토큰 응답: 발급된 토큰의 범위는 응답에 포함될 수 있으며, 요청된 범위보다 좁을 수 있다.

    1
    2
    3
    4
    5
    6
    7
    
    {
      "access_token":"2YotnFZFEjr1zCsicMWpAA",
      "token_type":"bearer",
      "expires_in":3600,
      "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
      "scope":"read_profile read_contacts"
    }
    
  5. 범위 확인: 리소스 서버는 액세스 토큰과 관련된 범위를 확인하여 요청된 리소스에 접근할 권한이 있는지 판단한다.

OAuth 2.0의 보안 고려사항

RFC 6749는 OAuth 2.0 구현 시 고려해야 할 다양한 보안 측면을 다루고 있다.

클라이언트 인증

클라이언트 인증은 권한 부여 서버가 토큰을 요청하는 클라이언트의 신원을 확인하는 과정이다:

  1. 클라이언트 비밀(Client Secret): 기밀 클라이언트는 권한 부여 서버에 등록 시 받은 비밀을 사용하여 인증한다.

  2. 인증 방법:

    • HTTP Basic 인증
    1
    
    Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
    
    • 요청 본문 파라미터
    1
    
    client_id=s6BhdRkqt3&client_secret=gX1fBat3bV
    
  3. 공개 클라이언트: 공개 클라이언트는 클라이언트 비밀을 안전하게 보관할 수 없으므로, 대안적인 인증 메커니즘(예: PKCE)이 권장된다.

권한 코드와 액세스 토큰 보호

권한 코드와 액세스 토큰을 보호하기 위한 고려사항:

  1. 수명 제한: 권한 코드는 매우 짧은 수명(일반적으로 몇 분)을 가져야 하며, 한 번만 사용 가능해야 한다.
  2. 토큰 기밀성: 액세스 토큰은 권한이 없는 당사자에게 노출되지 않도록 해야 한다.
  3. TLS 사용: 모든 토큰 교환은 TLS(Transport Layer Security)를 통해 이루어져야 한다.
  4. 리다이렉션 URI 검증: 권한 부여 서버는 클라이언트로부터 받은 리다이렉션 URI가 등록된 URI와 정확히 일치하는지 확인해야 한다.

일반적인 보안 위협과 대응

OAuth 2.0 구현에서 고려해야 할 주요 보안 위협:

  1. CSRF(Cross-Site Request Forgery):

    • 위협: 공격자가 사용자를 속여 의도하지 않은 권한 부여 요청을 수행하게 할 수 있다.
    • 대응: 클라이언트는 권한 요청 시 state 파라미터를 사용하여 CSRF 공격을 방지해야 한다.
    1
    
    GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
    
  2. 리다이렉션 URI 조작:

    • 위협: 공격자가 권한 코드나 액세스 토큰을 자신의 서버로 리다이렉트할 수 있다.
    • 대응: 권한 부여 서버는 리다이렉션 URI를 엄격하게 검증해야 한다.
  3. 토큰 탈취:

    • 위협: 액세스 토큰이 노출되면 공격자가 보호된 리소스에 접근할 수 있다.
    • 대응: 짧은 토큰 수명, TLS 사용, 토큰 저장 시 보안 조치 적용 등
  4. 클라이언트 가장(Impersonation):

    • 위협: 공격자가 합법적인 클라이언트인 것처럼 가장할 수 있다.
    • 대응: 안전한 클라이언트 등록 및 인증 절차 구현

PKCE(Proof Key for Code Exchange)

PKCE는 RFC 7636에서 정의된 확장으로, 공개 클라이언트를 위한 보안 강화 메커니즘이다:

  1. 목적: 권한 코드 가로채기 공격으로부터 보호한다.

  2. 작동 방식:

    • 클라이언트는 랜덤 문자열(code_verifier)을 생성한다.
    • 이 문자열의 해시(code_challenge)를 계산한다.
    • 권한 요청 시 code_challenge와 code_challenge_method(S256 또는 plain)를 포함한다.
    • 토큰 교환 시 원래의 code_verifier를 제공한다.
    • 권한 부여 서버는 code_verifier의 해시가 이전에 받은 code_challenge와 일치하는지 확인한다.
  3. 구현 예:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    # 권한 요청
    GET /authorize?
    response_type=code&
    client_id=s6BhdRkqt3&
    state=xyz&
    redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb&
    code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
    code_challenge_method=S256
    
    # 토큰 요청
    POST /token HTTP/1.1
    Host: server.example.com
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=authorization_code&
    code=SplxlOBeZQQYbYS6WxSbIA&
    redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb&
    client_id=s6BhdRkqt3&
    code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
    

OAuth 2.0 확장과 후속 발전

RFC 6749 발표 이후, OAuth 2.0 생태계는 다양한 확장과 개선을 통해 발전해왔다.

주요 확장 표준

  1. Bearer Token Usage (RFC 6750):

    • 액세스 토큰을 HTTP 요청에 포함시키는 방법을 정의한다.
    • Authorization 헤더, 폼 인코딩 바디 파라미터, URI 쿼리 파라미터 등 방법을 명시한다.
  2. Token Revocation (RFC 7009):

    • 클라이언트가 더 이상 필요하지 않거나 손상된 것으로 의심되는 토큰을 취소하는 방법을 정의한다.
    1
    2
    3
    4
    5
    
    POST /revoke HTTP/1.1
    Host: server.example.com
    Content-Type: application/x-www-form-urlencoded
    
    token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token
    
  3. Dynamic Client Registration (RFC 7591):

    • 클라이언트가 권한 부여 서버에 자동으로 등록하고 필요한 정보를 얻는 방법을 정의한다.
  4. JWT for OAuth 2.0 Client Authentication (RFC 7523):

    • JSON Web Token(JWT)을 사용하여 OAuth 2.0 클라이언트를 인증하는 방법을 정의한다.
    • 클라이언트 비밀 대신 JWT를 사용하여 클라이언트 인증을 가능하게 한다.
  5. Token Introspection (RFC 7662):

    • 리소스 서버가 권한 부여 서버에 토큰의 상태와 메타데이터를 질의할 수 있는 방법을 정의한다.
    1
    2
    3
    4
    5
    6
    
    POST /introspect HTTP/1.1
    Host: server.example.com
    Accept: application/json
    Content-Type: application/x-www-form-urlencoded
    
    token=2YotnFZFEjr1zCsicMWpAA&token_type_hint=access_token
    
  6. JWT 프로필 및 액세스 토큰 유형(RFC 9068):

    • JWT를 OAuth 2.0 액세스 토큰으로 사용하기 위한 표준 프로필을 정의한다.
    • 액세스 토큰에 포함되어야 하는 클레임과 검증 규칙을 명시한다.

OAuth 2.1

OAuth 2.1은 현재 개발 중인 규격으로, OAuth 2.0의 개선 버전이다.

주요 변경 사항은 다음과 같다:

  1. 간소화: 덜 사용되거나 보안 이슈가 있는 권한 부여 흐름 제거(암시적 부여 등)
  2. PKCE 필수화: 모든 권한 코드 부여 흐름에서 PKCE 사용을 필수로 함
  3. 리다이렉션 URI 요구사항 강화: 더 엄격한 리다이렉션 URI 검증
  4. 모범 사례 통합: 여러 확장과 모범 사례를 핵심 사양에 통합
  5. 보안 권장사항 업데이트: 최신 위협 모델과 대응 방법 포함

OpenID Connect

OpenID Connect(OIDC)는 OAuth 2.0 위에 구축된 인증 레이어로, 다음과 같은 특징이 있다:

  1. ID 토큰: 사용자 인증 정보를 포함하는 JWT 형식의 토큰
  2. UserInfo 엔드포인트: 사용자 프로필 정보를 검색하기 위한 표준 API
  3. 표준화된 범위: email, profile, address 등의 표준화된 정보 범위
  4. 디스커버리: 서비스 구성 정보를 자동으로 발견할 수 있는 메커니즘
  5. 세션 관리: 로그아웃 및 세션 상태 확인을 위한 표준 방법

OpenID Connect는 OAuth 2.0의 권한 부여(Authorization) 기능과 함께 인증(Authentication) 기능을 제공하여, 통합 인증 및 접근 제어 솔루션을 구현할 수 있게 한다.

OAuth 2.0 구현 예제

다양한 프로그래밍 언어와 프레임워크에서 OAuth 2.0을 구현하는 방법을 살펴보면:

Node.js 구현 예제

Express.js와 Passport.js를 사용한 OAuth 2.0 클라이언트 구현:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
const express = require('express');
const passport = require('passport');
const OAuth2Strategy = require('passport-oauth2');
const session = require('express-session');

const app = express();

// 세션 설정
app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: false
}));

app.use(passport.initialize());
app.use(passport.session());

// OAuth 2.0 전략 설정
passport.use(new OAuth2Strategy({
    authorizationURL: 'https://provider.example.com/oauth2/authorize',
    tokenURL: 'https://provider.example.com/oauth2/token',
    clientID: 'your-client-id',
    clientSecret: 'your-client-secret',
    callbackURL: 'http://localhost:3000/auth/callback',
    state: true
  },
  function(accessToken, refreshToken, profile, cb) {
    // 액세스 토큰 저장 및 사용자 정보 검색
    console.log('Access Token:', accessToken);
    console.log('Refresh Token:', refreshToken);
    
    // 데이터베이스에서 사용자 찾기 또는 생성
    return cb(null, { id: '123', name: 'Example User' });
  }
));

// 사용자 직렬화/역직렬화
passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser((id, done) => {
  // 실제 구현에서는 데이터베이스에서 사용자 조회
  done(null, { id: id, name: 'Example User' });
});

// 로그인 라우트
app.get('/auth/login', passport.authenticate('oauth2'));

// 콜백 라우트
app.get('/auth/callback', 
  passport.authenticate('oauth2', { failureRedirect: '/login' }),
  function(req, res) {
    // 성공적인 인증 후 리다이렉트
    res.redirect('/profile');
  }
);

// 프로필 라우트
app.get('/profile', isAuthenticated, (req, res) => {
  res.send(`Hello, ${req.user.name}!`);
});

// 인증 미들웨어
function isAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  res.redirect('/auth/login');
}

app.listen(3000, () => {
  console.log('Server started on port 3000');
});

Spring Boot 구현 예제

Spring Security OAuth2를 사용한 리소스 서버 구현:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;

@SpringBootApplication
@EnableResourceServer
@EnableWebSecurity
public class ResourceServerApplication extends ResourceServerConfigurerAdapter {

    public static void main(String[] args) {
        SpringApplication.run(ResourceServerApplication.class, args);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/api/**").authenticated()
            .anyRequest().permitAll();
    }

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("your-signing-key");
        return converter;
    }
}

@RestController
class UserController {

    @GetMapping("/api/user")
    public Principal user(Principal principal) {
        return principal;
    }

    @GetMapping("/api/resource")
    public String resource() {
        return "Protected Resource";
    }
}

Python 구현 예제

Flask와 Authlib을 사용한 OAuth 2.0 클라이언트 구현:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from flask import Flask, url_for, redirect, session, request
from authlib.integrations.flask_client import OAuth
import os

app = Flask(__name__)
app.secret_key = os.urandom(24)

oauth = OAuth(app)
oauth.register(
    name='example',
    client_id='your-client-id',
    client_secret='your-client-secret',
    access_token_url='https://provider.example.com/oauth2/token',
    access_token_params=None,
    authorize_url='https://provider.example.com/oauth2/authorize',
    authorize_params=None,
    api_base_url='https://api.example.com/',
    client_kwargs={'scope': 'profile email'},
)

@app.route('/')
def homepage():
    user = session.get('user')
    if user:
        return f'Hello {user["name"]}!'
    return '<a href="/login">Login</a>'

@app.route('/login')
def login():
    redirect_uri = url_for('auth', _external=True)
    return oauth.example.authorize_redirect(redirect_uri)

@app.route('/auth')
def auth():
    token = oauth.example.authorize_access_token()
    resp = oauth.example.get('userinfo')
    user_info = resp.json()
    # 사용자 정보를 세션에 저장
    session['user'] = user_info
    return redirect('/')

@app.route('/logout')
def logout():
    session.pop('user', None)
    return redirect('/')

if __name__ == '__main__':
    app.run(debug=True)

OAuth 2.0의 모범 사례

OAuth 2.0을 효과적이고 안전하게 구현하기 위한 모범 사례.

일반적인 모범 사례

  1. 항상 TLS 사용: 모든 OAuth 2.0 통신은 TLS(HTTPS)를 통해 이루어져야 한다.
  2. 적절한 권한 부여 흐름 선택:
    • 웹 서버 애플리케이션: 권한 부여 코드 부여
    • 단일 페이지 애플리케이션(SPA): 권한 부여 코드 부여 + PKCE
    • 모바일/데스크톱 앱: 권한 부여 코드 부여 + PKCE
    • 서버 간 통신: 클라이언트 자격 증명 부여
  3. 토큰 수명 관리:
    • 액세스 토큰: 짧은 수명(15분-1시간)
    • 리프레시 토큰: 더 긴 수명, 필요 시 취소 가능
  4. 상태 파라미터 사용: 모든 권한 요청에 state 파라미터를 포함하여 CSRF 공격을 방지한다.
  5. 리다이렉션 URI 검증: 정확한 매칭을 통해 리다이렉션 URI를 검증한다.

클라이언트 측 모범 사례

  1. 안전한 토큰 저장:
    • 웹 애플리케이션: HttpOnly, Secure, SameSite 쿠키
    • SPA: 메모리 저장(JavaScript 변수) 또는 안전한 쿠키
    • 모바일 앱: 안전한 저장소(iOS Keychain, Android Keystore)
  2. PKCE 사용: 모든 공개 클라이언트는 PKCE를 사용해야 한다.
  3. 토큰 갱신 전략: 액세스 토큰 만료 전에 갱신하는 전략을 구현한다.
  4. 오류 처리: 토큰 만료, 취소 등의 오류 상황을 적절히 처리한다.

서버 측 모범 사례

  1. 클라이언트 검증: 클라이언트 등록 시 철저한 검증과 승인 절차를 수행한다.
  2. 범위 제한: 필요한 최소한의 범위만 허용한다.
  3. 토큰 서명 및 검증: JWT를 사용할 경우 강력한 서명 알고리즘(RS256 등)을 사용한다.
  4. 무차별 대입 공격 방지: 속도 제한, 계정 잠금 등의 방어 메커니즘을 구현한다.
  5. 토큰 취소 지원: 토큰 취소 엔드포인트와 취소 목록 관리를 구현한다.

OAuth 2.0의 한계와 도전 과제

OAuth 2.0의 주요 한계와 도전 과제를 살펴보면:

  1. 프레임워크의 복잡성
    1. 다양한 흐름: 여러 권한 부여 흐름이 존재하여 적절한 흐름을 선택하고 구현하는 데 복잡성이 증가한다.
    2. 확장의 파편화: 많은 확장과 프로필이 존재하여 상호 운용성 문제가 발생할 수 있다.
    3. 구현 차이: 다양한 OAuth 2.0 제공자 간의 구현 차이로 인해 통합이 어려울 수 있다.
  2. 보안 고려 사항
    1. 묵시적 흐름 문제: 원래 SPA용으로 설계된 묵시적 흐름은 여러 보안 취약점이 있어 현재는 사용이 권장되지 않는다.
    2. 피싱 취약성: 사용자가 가짜 권한 부여 화면을 구별하기 어려울 수 있다.
    3. 토큰 보안: 액세스 토큰이 노출될 경우 보호된 리소스에 무단 접근이 가능하다.
    4. 오픈 리다이렉터 문제: 부적절한 리다이렉션 URI 검증으로 인해 공격 가능성이 있다.
  3. 사용자 경험 문제
    1. 리다이렉션 기반: 리다이렉션 기반 흐름은 일부 환경(embedded 시스템, CLI 도구 등)에서 사용자 경험이 좋지 않을 수 있다.
    2. 동의 피로(Consent Fatigue): 사용자가 너무 많은 동의 화면에 노출되면 내용을 제대로 읽지 않고 승인하는 경향이 있다.
    3. 인증서버 종속성: 인증 서버가 다운되면 모든 클라이언트가 영향을 받는다.

용어 정리

용어설명

참고 및 출처