Go でのユーザーの認証


App Engine などの Google Cloud マネージド プラットフォームで実行するアプリは、Identity-Aware Proxy(IAP)を使用することにより、ユーザー認証やセッション管理の手間を省いてアプリへのアクセスを制御できます。IAP は、アプリへのアクセスを制御できるだけでなく、新しい HTTP ヘッダーの形式でメールアドレスや永続的な識別子などの認証済みユーザーに関する情報をアプリに提供することもできます。

目標

  • IAP を使用することにより、App Engine アプリのユーザーに自分自身を認証するように要求する。

  • アプリでユーザーの ID にアクセスして、現在のユーザーの認証済みメールアドレスを表示する。

料金

このドキュメントでは、Google Cloud の次の課金対象のコンポーネントを使用します。

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを生成できます。 新しい Google Cloud ユーザーは無料トライアルをご利用いただける場合があります。

このドキュメントに記載されているタスクの完了後、作成したリソースを削除すると、それ以上の請求は発生しません。詳細については、クリーンアップをご覧ください。

始める前に

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  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 を使用してユーザーを認証します。これは、いくつかのアプローチのうちの 1 つにすぎません。ユーザーのさまざまな認証方法の詳細については、認証のコンセプトセクションをご覧ください。

Hello user-email-address アプリ

このチュートリアル用のアプリは、最小の Hello world App Engine アプリで、「Hello world」の代わりに「Hello user-email-address」を表示する特別な機能を備えています。ここで、user-email-address は認証済みユーザーのメールアドレスです。

この機能は、IAP がアプリに渡す各ウェブ リクエストに追加する認証情報を調べることで可能になります。アプリに届くリクエストには 3 つの新しいリクエスト ヘッダーが追加されます。最初の 2 つのヘッダーは、ユーザーの識別に使用できる書式なしテキスト文字列です。3 つ目のヘッダーは、同じ情報を含む暗号署名付きオブジェクトです。

  • X-Goog-Authenticated-User-Email: ユーザーのメールアドレスでユーザーが識別されます。アプリで回避できる場合は、個人情報を保存しないようにしてください。このアプリはデータを保存しません。ユーザーにエコーバックするだけです。

  • X-Goog-Authenticated-User-Id: Google が割り当てたこのユーザー ID は、ユーザーに関する情報は表示しませんが、ログインしているユーザーが過去に表示されたユーザーと同じであることをアプリが認識できるようにします。

  • X-Goog-Iap-Jwt-Assertion: インターネット ウェブ リクエスト以外に、IAP を迂回してきた他のクラウドアプリからのウェブ リクエストを受け入れるように Google Cloud アプリを構成できます。アプリがこのように構成されている場合は、このようなリクエストのヘッダーが偽造されている可能性があります。上記のいずれかの書式なしテキスト ヘッダーを使用する代わりに、この暗号署名付きヘッダーを使用して、情報が Google から提供されたものであることを確認できます。ユーザーのメールアドレスと永続的なユーザー ID の両方をこの署名付きヘッダーの一部として使用できます。

アプリが、インターネット ウェブ リクエストのみを受け入れ、アプリに対する IAP サービスを無効にできないように構成されている場合は、一意のユーザー ID を取得するコードは 1 行で済みます。

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

ただし、回復性の高いアプリは予期せぬ構成や環境問題などの障害を想定しておく必要があるため、代わりに、暗号署名付きヘッダーを使用して確認する関数を作成することをおすすめします。ヘッダーの署名は偽造できないため、検証時に、識別を返すのに使用できます。

コードの説明

このセクションでは、コードの機能について説明します。アプリを実行する場合は、アプリをデプロイするセクションまでスキップできます。

  • go.modファイルは、Go モジュールと 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
    }
    

アプリのデプロイ

これで、アプリをデプロイして、次いで Cloud 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 で、[Identity-Aware Proxy] ページに移動します。

    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/ など)を入力します。

    • [Application privacy policy line] フィールドでは、テスト用にホームページ リンクと同じ URL を使用します。

  4. [保存] をクリックします。認証情報の作成が要求されたら、ウィンドウを閉じます。

  5. Google Cloud コンソールで、[Identity-Aware Proxy] ページに移動します。

    Identity-Aware Proxy ページに移動

  6. ページを更新するには、[Refresh](更新)をクリックします。ページに、保護可能なリソースのリストが表示されます。

  7. [IAP] 列でクリックしてアプリの IAP をオンにします。

  8. ブラウザで、再度 web-site-url にアクセスします。

  9. ウェブページの代わりに、自分を認証するためのログイン画面が表示されます。ログインしようとすると、アプリにアクセス可能なユーザーのリストが IAP にないため、アクセスが拒否されます。

アプリに承認済みユーザーを追加する

  1. Google Cloud コンソールで、[Identity-Aware Proxy] ページに移動します。

    Identity-Aware Proxy ページに移動

  2. App Engine アプリのチェックボックスをオンにしてから、[プリンシパルを追加] をクリックします。

  3. allAuthenticatedUsers と入力してから、[Cloud IAP/IAP で保護されたウェブアプリ ユーザー] 役割を選択します。

  4. [保存] をクリックします。

これで、Google で認証可能なすべてのユーザーがアプリにアクセスできます。必要に応じて、1 人または複数のユーザーやグループをプリンシパルとして追加するだけで、さらにアクセスを制限できます。

  • 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 サービスにリダイレクトされ、そこでユーザーが認証されてから、必要な認証情報と一緒にリクエストがアプリに返されます。詳しくは、Using OAuth 2.0 for Web Server Applications(英語)をご覧ください。

次の図は、OAuth2 方式を使用した外部認証を示しています。

OAuth2 のフロー

図のフローは、ユーザーがアプリにアクセスするためのリクエストを送信するところから始まります。直接応答する代わりに、アプリは、ユーザーのブラウザを Google の Identity Platform にリダイレクトします。そこで、Google にログインするためのページが表示されます。ログインに成功すると、ユーザーのブラウザがアプリに戻されます。このリクエストには、認証されたユーザーに関してアプリが検索可能な情報が含まれており、アプリはユーザーに応答できます。

この方法には、アプリにとっての多くのメリットがあります。アプリがすべてのアカウント管理機能とリスクを外部サービスに委任することで、アプリを変更せずにログインとアカウント セキュリティを向上させることができます。ただし、上の図に示すように、この方法を使用するには、アプリからインターネットにアクセスできる必要があります。また、アプリには、ユーザーの認証後にセッションを管理する責任があります。

Identity-Aware Proxy

このチュートリアルで扱う 3 つ目のアプローチは、IAP を使用して、認証とアプリに対するなんらかの変更を伴うセッション管理をすべて処理する方法です。IAP は、アプリへのすべてのウェブ リクエストを傍受して、認証されていないリクエストをブロックし、それ以外のリクエストはリクエストごとにユーザー ID データを追加してアプリに渡します。

リクエストの処理を次の図に示します。

IAP のフロー

ユーザーからの要求は、IAP によって傍受され、認証されていない要求がブロックされます。認証されたユーザーが許可されたユーザーのリストに含まれている場合は、認証されたリクエストがアプリに渡されます。IAP を介して渡されたリクエストには、要求を行ったユーザーを識別するヘッダーが追加されます。

アプリは、ユーザー アカウントやセッション情報を処理する必要がなくなります。ユーザーの一意の識別子を知る必要があるオペレーションでは、受信ウェブ リクエストから直接取得できます。ただし、App Engine やロードバランサなど、IAP をサポートするコンピューティング サービスでのみ使用できます。ローカル開発マシンで IAP を使用することはできません。

クリーンアップ

このチュートリアルで使用したリソースについて、Google Cloud アカウントに課金されないようにするには、リソースを含むプロジェクトを削除するか、プロジェクトを維持して個々のリソースを削除します。

  1. Google Cloud コンソールで、[リソースの管理] ページに移動します。

    [リソースの管理] に移動

  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。