Python용 Cloud Identity-Aware Proxy로 사용자 인증

App Engine과 같이 Google Cloud 관리형 플랫폼에서 실행되는 앱은 액세스 권한 제어를 위해 IAP(Identity-Aware Proxy)를 사용하여 사용자 인증 및 세션 관리를 피할 수 있습니다. IAP는 앱에 대한 액세스를 제어할 뿐만 아니라 이메일 주소, 앱에 대한 영구 ID와 같은 인증된 사용자에 대한 정보도 새로운 HTTP 헤더 형태로 제공합니다.

목표

  • IAP를 사용하여 App Engine 앱 사용자가 자신을 인증하도록 요구합니다.

  • 앱의 사용자 ID에 액세스하여 현재 사용자의 인증된 이메일 주소를 표시합니다.

비용

이 가이드에서는 비용이 청구될 수 있는 다음과 같은 Google Cloud 구성요소를 사용합니다.

프로젝트 사용량을 기준으로 예상 비용을 산출하려면 가격 계산기를 사용합니다. Google Cloud를 처음 사용하는 사용자는 무료 체험판을 사용할 수 있습니다.

이 가이드를 마치면 만든 리소스를 삭제하여 비용이 계속 청구되지 않게 할 수 있습니다. 자세한 내용은 삭제를 참조하세요.

시작하기 전에

  1. Google 계정으로 로그인합니다.

    아직 계정이 없으면 새 계정을 등록하세요.

  2. Cloud Console의 프로젝트 선택기 페이지에서 Cloud 프로젝트를 선택하거나 만듭니다.

    프로젝트 선택기 페이지로 이동

  3. Cloud SDK를 설치하고 초기화합니다.

배경

이 가이드에서는 IAP를 사용하여 사용자를 인증합니다. 이는 여러 가지 가능한 접근방법 중 하나일 뿐입니다. 사용자를 인증하는 다양한 방법에 대한 자세한 내용은 인증 개념 섹션을 참조하세요.

Hello user-email-address

이 가이드의 앱은 일반적이지 않은 기능 하나를 가진 App Engine의 최소 Hello World 앱으로, 'Hello World' 대신 'Hello user-email-address'를 표시합니다. 여기서 user-email-address는 인증된 사용자의 이메일 주소입니다.

이 기능은 IAP가 앱으로 전달하는 각 웹 요청에 추가하는 인증 정보를 검토하여 사용할 수 있습니다. 앱에 도달하는 각 웹 요청에는 3개의 새로운 요청 헤더가 추가됩니다. 처음 두 헤더는 사용자를 식별하는 데 사용할 수 있는 일반 텍스트 문자열입니다. 세 번째 헤더는 동일 정보를 포함하는 암호화 서명 객체입니다.

  • X-Goog-Authenticated-User-Email: 사용자의 이메일 주소로 식별합니다. 앱에서 사용하지 않을 수 있는 경우 개인정보를 저장하지 마세요. 이 앱은 어떠한 데이터도 저장하지 않고 사용자에게 다시 표시합니다.

  • X-Goog-Authenticated-User-Id: Google에서 할당한 이 사용자 ID는 사용자에 대한 정보를 표시하지 않지만, ID를 통해 앱은 로그인한 사용자가 이전에 로그인한 사용자와 동일한지 파악할 수 있습니다.

  • X-Goog-Iap-Jwt-Assertion: 인터넷 웹 요청 외에 IAP를 우회하여 다른 클라우드 앱의 웹 요청을 수락하도록 Google Cloud 앱을 구성할 수 있습니다. 앱이 이와 같이 구성된 경우 이러한 요청에는 위조된 헤더가 있을 수 있습니다. 앞에서 언급한 일반 텍스트 헤더 대신 이 암호화 서명 헤더를 사용하여 Google에서 정보를 제공했는지 확인할 수 있습니다. 사용자의 이메일 주소와 영구 사용자 ID 모두 서명된 헤더의 일부로 사용할 수 있습니다.

인터넷 웹 요청만 전달할 수 있도록 앱이 구성되어 있고 앱의 IAP 서비스를 사용 중지할 수 없는 경우, 순 사용자 ID를 검색하는 것은 코드 한 줄로 충분합니다.

user_id = request.headers.get('X-Goog-Authenticated-User-ID')

하지만 탄력적인 앱은 예기치 못한 구성 또는 환경 문제를 비롯한 문제가 발생할 것을 고려해야 하므로 암호화 서명 헤더를 사용하고 확인하는 함수를 만드는 것이 좋습니다. 헤더의 서명은 위조할 수 없고, 확인되면 ID를 반환하는 데 사용할 수 있습니다.

소스 코드 만들기

  1. 텍스트 편집기를 사용하여 main.py 파일을 만들고 다음 코드를 붙여넣습니다.

    import sys
    
    from flask import Flask
    app = Flask(__name__)
    
    CERTS = None
    AUDIENCE = None
    
    def certs():
        """Returns a dictionary of current Google public key certificates for
        validating Google-signed JWTs. Since these change rarely, the result
        is cached on first request for faster subsequent responses.
        """
        import requests
    
        global CERTS
        if CERTS is None:
            response = requests.get(
                'https://www.gstatic.com/iap/verify/public_key'
            )
            CERTS = response.json()
        return CERTS
    
    def get_metadata(item_name):
        """Returns a string with the project metadata value for the item_name.
        See https://cloud.google.com/compute/docs/storing-retrieving-metadata for
        possible item_name values.
        """
        import requests
    
        endpoint = 'http://metadata.google.internal'
        path = '/computeMetadata/v1/project/'
        path += item_name
        response = requests.get(
            '{}{}'.format(endpoint, path),
            headers={'Metadata-Flavor': 'Google'}
        )
        metadata = response.text
        return metadata
    
    def audience():
        """Returns the audience value (the JWT 'aud' property) for the current
        running instance. Since this involves a metadata lookup, the result is
        cached when first requested for faster future responses.
        """
        global AUDIENCE
        if AUDIENCE is None:
            project_number = get_metadata('numeric-project-id')
            project_id = get_metadata('project-id')
            AUDIENCE = '/projects/{}/apps/{}'.format(
                project_number, project_id
            )
        return AUDIENCE
    
    def validate_assertion(assertion):
        """Checks that the JWT assertion is valid (properly signed, for the
        correct audience) and if so, returns strings for the requesting user's
        email and a persistent user ID. If not valid, returns None for each field.
        """
        from jose import jwt
    
        try:
            info = jwt.decode(
                assertion,
                certs(),
                algorithms=['ES256'],
                audience=audience()
                )
            return info['email'], info['sub']
        except Exception as e:
            print('Failed to validate assertion: {}'.format(e), file=sys.stderr)
            return None, None
    
    @app.route('/', methods=['GET'])
    def say_hello():
        from flask import request
    
        assertion = request.headers.get('X-Goog-IAP-JWT-Assertion')
        email, id = validate_assertion(assertion)
        page = "<h1>Hello {}</h1>".format(email)
        return page

    main.py 파일에 대해서는 본 가이드 뒷부분의 코드 이해하기 섹션에서 자세히 설명합니다.

  2. requirements.txt이라는 다른 파일을 만들고 다음을 붙여넣습니다.

    Flask==1.1.1
    cryptography==2.8
    python-jose[cryptography]==3.0.1
    requests==2.22.0

    requirements.txt 파일에는 App Engine에서 로드하기 위해 앱에 필요한 모든 비표준 Python 라이브러리가 나열됩니다.

    • Flask는 앱에 사용되는 Python 웹 프레임워크입니다.

    • cryptography는 강력한 암호화 함수를 제공하는 모듈입니다.

    • python-jose[cryptography]는 JWT 검사 및 디코딩 함수를 제공합니다.

    • requests는 웹사이트에서 데이터를 검색합니다.

  3. app.yaml 파일을 만들고 다음 텍스트를 붙여넣습니다.

    runtime: python37

    app.yaml 파일은 코드에 필요한 언어 환경을 App Engine에 지정합니다.

코드 이해하기

이 섹션에서는 main.py의 코드가 작동하는 방식에 대해 설명합니다. 앱만 실행하려면 앱 배포 섹션으로 건너뛸 수 있습니다.

다음 코드는 main.py 파일에 있습니다. 앱에서 홈페이지에 대한 HTTP GET 요청을 수신하면 Flask 프레임워크가 say_hello 함수를 호출합니다.

@app.route('/', methods=['GET'])
def say_hello():
    from flask import request

    assertion = request.headers.get('X-Goog-IAP-JWT-Assertion')
    email, id = validate_assertion(assertion)
    page = "<h1>Hello {}</h1>".format(email)
    return page

say_hello 함수는 수신 요청에서 IAP가 추가한 JWT 어설션 헤더 값을 가져오고 함수를 호출하여 해당 암호화 서명 값을 검증합니다. 그러면 반환된 첫 번째 값(이메일)이 값을 만들고 반환하는 최소 웹페이지에서 사용됩니다.

def validate_assertion(assertion):
    """Checks that the JWT assertion is valid (properly signed, for the
    correct audience) and if so, returns strings for the requesting user's
    email and a persistent user ID. If not valid, returns None for each field.
    """
    from jose import jwt

    try:
        info = jwt.decode(
            assertion,
            certs(),
            algorithms=['ES256'],
            audience=audience()
            )
        return info['email'], info['sub']
    except Exception as e:
        print('Failed to validate assertion: {}'.format(e), file=sys.stderr)
        return None, None

validate_assertion 함수는 제3자 jose 라이브러리의 jwt.decode 함수를 사용하여 어설션이 적절하게 서명되어 있는지 확인하고 어설션에서 페이로드 정보를 추출합니다. 이 정보는 인증된 사용자의 이메일 주소이며 사용자의 영구 고유 ID입니다. 어설션을 디코딩할 수 없는 경우 이 함수는 각 값에 대해 None을 반환하고 메시지를 출력하여 오류를 로깅합니다.

JWT 어설션을 검증하려면 어설션에 서명한 개체(이 경우 Google)의 공개 키 인증서와 이 어설션의 대상인 잠재고객을 파악해야 합니다. App Engine 앱의 경우 잠재고객은 Google Cloud 프로젝트 식별 정보가 포함된 문자열입니다. 이 함수는 앞의 함수에서 잠재고객 문자열과 인증서를 가져옵니다.

def audience():
    """Returns the audience value (the JWT 'aud' property) for the current
    running instance. Since this involves a metadata lookup, the result is
    cached when first requested for faster future responses.
    """
    global AUDIENCE
    if AUDIENCE is None:
        project_number = get_metadata('numeric-project-id')
        project_id = get_metadata('project-id')
        AUDIENCE = '/projects/{}/apps/{}'.format(
            project_number, project_id
        )
    return AUDIENCE

Google Cloud 프로젝트의 숫자 ID와 이름을 찾아서 소스 코드에 직접 넣을 수 있지만 audience 함수는 모든 App Engine 앱에서 사용할 수 있는 표준 메타 데이터 서비스를 쿼리해 줍니다. 메타데이터 서비스는 앱 코드 외부에 있으므로 후속 호출에서 메타데이터를 조회할 필요 없이 반환되는 전역 변수에 저장됩니다.

def get_metadata(item_name):
    """Returns a string with the project metadata value for the item_name.
    See https://cloud.google.com/compute/docs/storing-retrieving-metadata for
    possible item_name values.
    """
    import requests

    endpoint = 'http://metadata.google.internal'
    path = '/computeMetadata/v1/project/'
    path += item_name
    response = requests.get(
        '{}{}'.format(endpoint, path),
        headers={'Metadata-Flavor': 'Google'}
    )
    metadata = response.text
    return metadata

App Engine 메타데이터 서비스 및 기타 Google Cloud 컴퓨팅 서비스의 메타데이터 서비스는 웹사이트와 유사하게 표시되며 표준 웹 쿼리로 쿼리합니다. 하지만 실제로는 외부 사이트가 아니라 실행 중인 앱에 대해 요청된 정보를 반환하는 내부 기능이므로 https 요청 대신 http를 사용하는 것이 안전합니다. 이는 JWT 어설션의 대상 잠재고객을 정의하는 데 필요한 현재 Google Cloud 식별자를 가져오는 데 사용됩니다.

def certs():
    """Returns a dictionary of current Google public key certificates for
    validating Google-signed JWTs. Since these change rarely, the result
    is cached on first request for faster subsequent responses.
    """
    import requests

    global CERTS
    if CERTS is None:
        response = requests.get(
            'https://www.gstatic.com/iap/verify/public_key'
        )
        CERTS = response.json()
    return CERTS

디지털 서명을 검증하려면 서명자의 공개 키 인증서가 필요합니다. Google에서는 현재 사용되는 모든 공개 키 인증서를 반환하는 웹사이트를 제공합니다. 이러한 결과는 동일한 앱 인스턴스에서 다시 필요한 경우를 위해 캐시 처리됩니다.

앱 배포

이제 앱을 배포한 다음 IAP를 사용 설정하여 사용자가 앱에 액세스하기 전에 인증하도록 합니다.

  1. 터미널 창에서 app.yaml 파일을 포함하는 디렉터리로 이동하여 앱을 App Engine에 배포합니다.

    gcloud app deploy
    
  2. 메시지가 표시되면 인근 리전을 선택합니다.

  3. 배포 작업을 계속할지 묻는 메시지가 표시되면 Y를 입력합니다.

    몇 분 내에 앱이 인터넷에 게시됩니다.

  4. 앱을 확인합니다.

    gcloud app browse
    

    출력에서 앱의 웹 주소인 web-site-url을 복사합니다.

  5. 브라우저 창에서 web-site-url을 붙여넣어 앱을 엽니다.

    아직 IAP를 사용하지 않아 사용자 정보가 앱으로 전송되지 않으므로 이메일이 표시되지 않습니다.

IAP 사용 설정

이제 App Engine 인스턴스가 생성되었으므로 IAP를 사용하여 보호할 수 있습니다.

  1. Google Cloud Console에서 IAP(Identity-Aware Proxy) 페이지로 이동합니다.

    IAP(Identity-Aware Proxy) 페이지로 이동

  2. 이 프로젝트에 인증 옵션을 처음 사용 설정했으므로 IAP를 사용하기 전에 OAuth 동의 화면을 구성해야 한다는 메시지가 표시됩니다.

    동의 화면 구성을 클릭합니다.

  3. 사용자 인증 정보 페이지의 OAuth 동의 화면 탭에서 다음 필드를 작성합니다.

    • 애플리케이션 이름 필드에 IAP Example을 입력합니다.

    • 지원 이메일 필드에 이메일 주소를 입력합니다.

    • 승인된 도메인 필드에 앱 URL의 호스트 이름 부분을 입력합니다(예: iap-example-999999.uc.r.appspot.com). 필드에 호스트 이름을 입력한 후에 Enter 키를 누릅니다.

    • 애플리케이션 홈페이지 링크 필드에 앱의 URL을 입력합니다(예: https://iap-example-999999.uc.r.appspot.com/).

    • 애플리케이션 개인정보처리방침 행 필드에 홈페이지 링크와 동일한 URL을 테스트용으로 사용합니다.

  4. 저장을 클릭합니다. 사용자 인증 정보를 만들라는 메시지가 표시되면 창을 닫을 수 있습니다.

  5. Cloud Console에서 IAP(Identity-Aware Proxy) 페이지로 이동합니다.

    IAP(Identity-Aware Proxy) 페이지로 이동

  6. 페이지를 새로고침하려면 새로고침 을 클릭합니다. 보호할 수 있는 리소스 목록이 페이지에 표시됩니다.

  7. IAP 열에서 앱의 IAP를 클릭하여 사용 설정합니다.

  8. 브라우저에서 web-site-url로 다시 이동합니다.

  9. 웹페이지 대신 자신을 인증하는 로그인 화면이 표시됩니다. IAP에 앱을 허용할 사용자 목록이 없으므로 로그인하면 액세스가 거부됩니다.

앱에 승인된 사용자 추가

  1. Cloud Console에서 IAP(Identity-Aware Proxy) 페이지로 이동합니다.

    IAP(Identity-Aware Proxy) 페이지로 이동

  2. App Engine 앱의 체크박스를 선택한 다음 구성원 추가를 클릭합니다.

  3. allAuthenticatedUsers를 입력한 다음 Cloud IAP/IAP 보안 웹 앱 사용자 역할을 선택합니다.

  4. 저장을 클릭합니다.

이제 Google에서 인증할 수 있는 모든 사용자가 앱에 액세스할 수 있습니다. 원하는 경우 하나 이상의 사용자 또는 그룹을 구성원으로 추가하여 액세스를 제한할 수 있습니다.

  • Gmail 또는 G Suite 이메일 주소

  • Google 그룹스 이메일 주소

  • G Suite 도메인 이름

앱에 액세스하기

  1. 브라우저에서 web-site-url로 이동합니다.

  2. 페이지를 새로고침하려면 새로고침을 클릭합니다.

  3. 로그인 화면에서 Google 사용자 인증 정보로 로그인합니다.

    페이지에 이메일 주소가 포함된 'Hello user-email-address' 페이지가 표시됩니다.

    이전과 동일한 페이지가 계속 표시되는 경우, 이제 IAP를 사용 설정했으므로 브라우저가 새 요청을 완전히 업데이트하지 않는 문제가 발생했을 수 있습니다. 브라우저 창을 모두 닫고 다시 연 다음 다시 시도하세요.

인증 개념

앱이 사용자를 인증하고 승인된 사용자로만 액세스를 제한할 수 있는 여러 방법이 있습니다. 일반적인 인증 방법은 앱에 덜 수고로운 순서대로 다음 섹션에 나열되어 있습니다.

옵션 장점 단점
앱 인증
  • 앱은 인터넷 연결 여부와 관계없이 모든 플랫폼에서 실행될 수 있습니다.
  • 사용자는 인증을 관리하기 위해 다른 서비스를 사용할 필요가 없습니다.
  • 앱은 사용자 인증 정보를 안전하게 관리하고 공개되지 않도록 보호해야 합니다.
  • 앱은 로그인한 사용자의 세션 데이터를 유지해야 합니다.
  • 앱은 사용자 등록, 비밀번호 변경, 비밀번호 복구 기능을 제공해야 합니다.
OAuth2
  • 앱은 개발자 워크스테이션을 포함하여 인터넷에 연결된 모든 플랫폼에서 실행될 수 있습니다.
  • 앱은 사용자 등록, 비밀번호 변경 또는 비밀번호 복구 기능이 필요하지 않습니다.
  • 사용자 정보 공개 위험은 다른 서비스에 위임됩니다.
  • 새로운 로그인 보안 조치는 앱 외부에서 처리됩니다.
  • 사용자는 ID 서비스에 등록해야 합니다.
  • 앱은 로그인한 사용자의 세션 데이터를 유지해야 합니다.
IAP
  • 앱은 사용자, 인증 또는 세션 상태를 관리하기 위한 코드가 필요하지 않습니다.
  • 앱에는 위반이 발생할 수 있는 사용자 인증 정보가 없습니다.
  • 앱은 서비스에서 지원하는 플랫폼에서만 실행될 수 있습니다. 특히 App Engine과 같이 IAP를 지원하는 특정 Google Cloud 서비스에서 실행될 수 있습니다.

앱 관리 인증

앱은 이 방법을 사용하여 사용자 인증의 모든 측면을 자체적으로 관리합니다. 앱은 사용자 인증 정보 데이터베이스를 자체적으로 유지하고 사용자 세션을 관리해야 합니다. 또한 사용자 계정 및 비밀번호를 관리하고, 사용자 인증 정보를 확인하며, 인증된 각 로그인으로 사용자 세션을 발행, 확인, 및 업데이트할 수 있는 함수를 제공해야 합니다. 다음 다이어그램은 앱 관리 인증 방법을 보여줍니다.

애플리케이션 관리형 흐름

다이어그램에 표시된 것처럼 사용자가 로그인한 후에 앱에서는 사용자 세션에 대한 정보를 만들고 유지해야 합니다. 사용자가 앱에 요청할 때 요청에는 앱이 확인해야 하는 세션 정보가 포함되어야 합니다.

이 접근방법의 주요 장점은 독립적으로 실행되고 앱의 관리를 받는다는 것입니다. 또한 앱이 인터넷 상에 존재하지 않아도 됩니다. 주요 단점은 앱에서 모든 계정 관리 기능을 제공하고 모든 민감한 사용자 인증 정보 데이터를 보호해야 한다는 점입니다.

OAuth2를 사용한 외부 인증

모든 것을 앱 내에서 처리하는 방식의 대안으로는 사용자 계정 정보와 기능을 처리하고 민감한 사용자 인증 정보를 보호하는 Google과 같은 외부 ID 서비스를 사용하는 것입니다. 사용자가 앱에 로그인하려고 하면 요청이 사용자를 인증하는 ID 서비스로 리디렉션된 후에 다시 필요한 인증 정보와 함께 앱으로 리디렉션됩니다. 자세한 내용은 최종 사용자로 인증을 참조하세요.

다음 다이어그램은 OAuth2 방법을 사용한 외부 인증을 보여줍니다.

OAuth2 흐름

다이어그램의 흐름은 사용자가 앱 액세스 요청을 전송하면 시작됩니다. 직접 응답하는 대신 앱은 사용자의 브라우저를 Google의 Identity Platform으로 리디렉션하여 Google에 로그인하는 페이지를 표시합니다. 로그인이 성공하면 사용자의 브라우저가 다시 앱으로 돌아갑니다. 이 요청에는 앱이 인증된 사용자에 대한 정보를 조회하는 데 사용할 수 있는 정보가 포함되며, 바로 앱이 사용자에게 응답합니다.

이 방법은 앱에 여러 가지 장점을 제공합니다. 모든 계정 관리 기능과 위험을 외부 서비스에 위임하므로 앱을 변경하지 않고도 로그인 및 계정 보안을 개선할 수 있습니다. 그러나 위의 다이어그램에 표시된 것처럼 이 방법을 사용하려면 앱에서 인터넷에 액세스할 수 있어야 합니다. 또한 사용자가 인증된 후에 앱에서 세션 관리를 담당합니다.

IAP(Identity-Aware Proxy)

이 가이드에서 다루는 세 번째 접근방법은 IAP를 사용하여 앱의 모든 변경사항과 함께 모든 인증 및 세션 관리를 처리하는 것입니다. IAP는 앱에 대한 모든 웹 요청을 가로채고 인증되지 않은 요청을 차단하며 각 요청에 사용자 ID 데이터를 추가하여 전달합니다.

요청 처리는 다음 다이어그램에 표시됩니다.

IAP 흐름

사용자 요청은 IAP가 가로채어 인증되지 않은 요청을 차단합니다. 인증된 요청은 앱으로 전달됩니다. 단, 인증된 사용자가 허용된 사용자 목록에 있어야 합니다. IAP를 통해 전달된 요청에는 헤더가 추가되어 요청한 사용자를 식별합니다.

앱은 더 이상 사용자 계정 또는 세션 정보를 처리할 필요가 없습니다. 사용자의 고유 ID를 파악하기 위해 필요한 모든 작업은 수신되는 각 웹 요청에서 직접 가져올 수 있습니다. 그러나 이는 App Engine 및 부하 분산기 같이 IAP를 지원하는 컴퓨팅 서비스에만 사용할 수 있습니다. 로컬 개발 머신에서는 IAP를 사용할 수 없습니다.

삭제

이 가이드에서 사용한 리소스 비용이 Google Cloud Platform 계정에 청구되지 않도록 하려면 다음 안내를 따르세요.

비용이 청구되지 않도록 하는 가장 쉬운 방법은 가이드에서 만든 프로젝트를 삭제하는 것입니다.

프로젝트를 삭제하는 방법은 다음과 같습니다.

  1. Cloud Console에서 리소스 관리 페이지로 이동합니다.

    리소스 관리 페이지로 이동

  2. 프로젝트 목록에서 삭제할 프로젝트를 선택하고 삭제 를 클릭합니다.
  3. 대화상자에서 프로젝트 ID를 입력한 후 종료를 클릭하여 프로젝트를 삭제합니다.