Go로 사용자 인증


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

목표

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

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

비용

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

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

이 문서에 설명된 태스크를 완료했으면 만든 리소스를 삭제하여 청구가 계속되는 것을 방지할 수 있습니다. 자세한 내용은 삭제를 참조하세요.

시작하기 전에

  1. Google Cloud 계정에 로그인합니다. Google Cloud를 처음 사용하는 경우 계정을 만들고 Google 제품의 실제 성능을 평가해 보세요. 신규 고객에게는 워크로드를 실행, 테스트, 배포하는 데 사용할 수 있는 $300의 무료 크레딧이 제공됩니다.
  2. Google Cloud Console의 프로젝트 선택기 페이지에서 Google Cloud 프로젝트를 선택하거나 만듭니다.

    프로젝트 선택기로 이동

  3. Google Cloud CLI를 설치합니다.
  4. gcloud CLI를 초기화하려면 다음 명령어를 실행합니다.

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

    프로젝트 선택기로 이동

  6. Google Cloud CLI를 설치합니다.
  7. gcloud CLI를 초기화하려면 다음 명령어를 실행합니다.

    gcloud init
  8. 개발 환경을 준비합니다.

프로젝트 설정

  1. 터미널 창에서 샘플 앱 저장소를 로컬 머신에 클론합니다.

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git
  2. 샘플 코드가 있는 디렉터리로 변경합니다.

    cd golang-samples/getting-started/authenticating-users

배경

이 가이드에서는 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를 검색하는 것은 코드 한 줄로 충분합니다.

userID := r.Header.Get("X-Goog-Authenticated-User-ID")

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

코드 이해하기

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

  • go.mod 파일은 Go 모듈 및 종속된 모듈을 정의합니다

    module github.com/GoogleCloudPlatform/golang-samples/getting-started/authenticating-users
    
    go 1.19
    
    require (
    	cloud.google.com/go/compute/metadata v0.2.3
    	github.com/golang-jwt/jwt v3.2.2+incompatible
    )
    
    require cloud.google.com/go/compute v1.19.1 // indirect
    
  • app.yaml 파일은 코드에 필요한 언어 환경을 App Engine에 지정합니다.

    runtime: go112
  • 앱은 패키지를 가져오고 main 함수를 정의하면서 시작합니다. main 함수는 색인 핸들러를 등록하고 HTTP 서버를 시작합니다.

    
    // The authenticating-users program is a sample web server application that
    // extracts and verifies user identity data passed to it via Identity-Aware
    // Proxy.
    package main
    
    import (
    	"encoding/json"
    	"fmt"
    	"log"
    	"net/http"
    	"os"
    	"time"
    
    	"cloud.google.com/go/compute/metadata"
    	"github.com/golang-jwt/jwt"
    )
    
    // app holds the Cloud IAP certificates and audience field for this app, which
    // are needed to verify authentication headers set by Cloud IAP.
    type app struct {
    	certs map[string]string
    	aud   string
    }
    
    func main() {
    	a, err := newApp()
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	http.HandleFunc("/", a.index)
    
    	port := os.Getenv("PORT")
    	if port == "" {
    		port = "8080"
    		log.Printf("Defaulting to port %s", port)
    	}
    
    	log.Printf("Listening on port %s", port)
    	if err := http.ListenAndServe(":"+port, nil); err != nil {
    		log.Fatal(err)
    	}
    }
    
    // newApp creates a new app, returning an error if either the Cloud IAP
    // certificates or the app's audience field cannot be obtained.
    func newApp() (*app, error) {
    	certs, err := certificates()
    	if err != nil {
    		return nil, err
    	}
    
    	aud, err := audience()
    	if err != nil {
    		return nil, err
    	}
    
    	a := &app{
    		certs: certs,
    		aud:   aud,
    	}
    	return a, nil
    }
    
  • index 함수는 수신 요청에서 IAP가 추가한 JWT 어설션 헤더 값을 가져오고 validateAssertion 함수를 호출하여 해당 암호화 서명 값을 검증합니다. 그러면 이메일 주소는 최소 웹 응답에서 사용됩니다.

    
    // index responds to requests with our greeting.
    func (a *app) index(w http.ResponseWriter, r *http.Request) {
    	if r.URL.Path != "/" {
    		http.NotFound(w, r)
    		return
    	}
    
    	assertion := r.Header.Get("X-Goog-IAP-JWT-Assertion")
    	if assertion == "" {
    		fmt.Fprintln(w, "No Cloud IAP header found.")
    		return
    	}
    	email, _, err := validateAssertion(assertion, a.certs, a.aud)
    	if err != nil {
    		log.Println(err)
    		fmt.Fprintln(w, "Could not validate assertion. Check app logs.")
    		return
    	}
    
    	fmt.Fprintf(w, "Hello %s\n", email)
    }
    
  • validateAssertion 함수는 어설션이 정상적으로 서명되었는지 검증하고 연결된 이메일 주소와 사용자 ID를 반환합니다.

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

    
    // validateAssertion validates assertion was signed by Google and returns the
    // associated email and userID.
    func validateAssertion(assertion string, certs map[string]string, aud string) (email string, userID string, err error) {
    	token, err := jwt.Parse(assertion, func(token *jwt.Token) (interface{}, error) {
    		keyID := token.Header["kid"].(string)
    
    		_, ok := token.Method.(*jwt.SigningMethodECDSA)
    		if !ok {
    			return nil, fmt.Errorf("unexpected signing method: %q", token.Header["alg"])
    		}
    
    		cert := certs[keyID]
    		return jwt.ParseECPublicKeyFromPEM([]byte(cert))
    	})
    
    	if err != nil {
    		return "", "", err
    	}
    
    	claims, ok := token.Claims.(jwt.MapClaims)
    	if !ok {
    		return "", "", fmt.Errorf("could not extract claims (%T): %+v", token.Claims, token.Claims)
    	}
    
    	if claims["aud"].(string) != aud {
    		return "", "", fmt.Errorf("mismatched audience. aud field %q does not match %q", claims["aud"], aud)
    	}
    	return claims["email"].(string), claims["sub"].(string), nil
    }
    
  • 사용자가 Google Cloud 프로젝트의 숫자 ID와 이름을 찾아 직접 소스 코드에 넣을 수도 있지만, audience 함수가 모든 App Engine 앱에서 사용할 수 있는 표준 메타데이터 서비스를 쿼리하여 이를 자동으로 수행합니다. 메타데이터 서비스가 앱 코드 외부이기 때문에 후속 호출에서 메타데이터를 조회할 필요 없이 반환된 전역 변수에 결과가 저장됩니다.

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

    
    // audience returns the expected audience value for this service.
    func audience() (string, error) {
    	projectNumber, err := metadata.NumericProjectID()
    	if err != nil {
    		return "", fmt.Errorf("metadata.NumericProjectID: %w", err)
    	}
    
    	projectID, err := metadata.ProjectID()
    	if err != nil {
    		return "", fmt.Errorf("metadata.ProjectID: %w", err)
    	}
    
    	return "/projects/" + projectNumber + "/apps/" + projectID, nil
    }
    
  • 디지털 서명을 검증하려면 서명자의 공개 키 인증서가 필요합니다. Google에서는 현재 사용되는 모든 공개 키 인증서를 반환하는 웹사이트를 제공합니다. 이러한 결과는 동일한 앱 인스턴스에서 다시 필요한 경우를 위해 캐시 처리됩니다.

    
    // certificates returns Cloud IAP's cryptographic public keys.
    func certificates() (map[string]string, error) {
    	const url = "https://www.gstatic.com/iap/verify/public_key"
    	client := http.Client{
    		Timeout: 5 * time.Second,
    	}
    	resp, err := client.Get(url)
    	if err != nil {
    		return nil, fmt.Errorf("Get: %w", err)
    	}
    
    	var certs map[string]string
    	dec := json.NewDecoder(resp.Body)
    	if err := dec.Decode(&certs); err != nil {
    		return nil, fmt.Errorf("Decode: %w", err)
    	}
    
    	return certs, nil
    }
    

앱 배포

이제 앱을 배포한 다음 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 콘솔에서 IAP(Identity-Aware Proxy) 페이지로 이동합니다.

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

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

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

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

    • 계정이 Google Workspace 조직에 있는 경우 외부를 선택하고 만들기를 클릭합니다. 우선 명시적으로 허용한 사용자만 앱을 사용할 수 있습니다.

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

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

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

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

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

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

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

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

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

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

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

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

앱에 승인된 사용자 추가

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

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

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

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

  4. 저장을 클릭합니다.

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

  • Gmail 또는 Google Workspace 이메일 주소

  • Google 그룹스 이메일 주소

  • Google Workspace 도메인 이름

앱에 액세스하기

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

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

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

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

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

인증 개념

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

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

앱 관리 인증

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

애플리케이션 관리형 흐름

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

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

OAuth2를 사용한 외부 인증

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

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

OAuth2 흐름

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

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

IAP(Identity-Aware Proxy)

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

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

IAP 흐름

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

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

삭제

이 가이드에서 사용된 리소스 비용이 Google Cloud 계정에 청구되지 않도록 하려면 리소스가 포함된 프로젝트를 삭제하거나 프로젝트를 유지하고 개별 리소스를 삭제하세요.

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

    리소스 관리로 이동

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