😸

GolangでOIDCを使った組織内認証を実験してみた!

2024/04/09に公開1

GoとOpenID Connectを使用したOAuth 2.0の実装

この記事では、Googleのアイデンティティサービスを使用して認証を行うOAuth 2.0およびOpenID Connectを利用したGoアプリケーションについて詳しく説明します。コードを一つずつ分解して、その動作を理解します。

自己紹介

某中堅?大学理学部情報科学化のryosukeです!

とある長期インターンでSaaSの作成を行っています!

残念ながらひとりで作成中です...

経験もSQLとGAS, Pythonのスクレイピングのみ...

とほほ...

概要

このコードは、GoogleのOAuth 2.0およびOpenID Connectを介してユーザーを認証する基本的なWebサーバーをGoで設定する方法を示しています。これを実現するために、go-oidcgodotenvoauth2などのパッケージを使用しています。

前提条件

  • Goのプログラミング知識
  • OAuth 2.0とOpenID Connectの基本的な理解
  • OAuth 2.0の資格情報を持つGoogle Cloudプロジェクト

ソースコード

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/coreos/go-oidc"
	"github.com/joho/godotenv"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
)

var (
    // 環境変数の定義
    clientID string
    clientSecret string
    // Google API Consoleに設定したリダイレクトURL
    redirectURL  = "http://localhost:8080/callback"
    provider *oidc.Provider // OIDCプロバイダー
    config   *oauth2.Config // OAuth 2.0設定
)

func init() {
    // .envファイルを読み込む
    if err := godotenv.Load(); err != nil {
        log.Fatalf("Error loading .env file: %v", err)
    }

    // 環境変数からクライアントIDとクライアントシークレットを取得
    clientID = os.Getenv("CLIENT_ID")
    clientSecret = os.Getenv("CLIENT_SECRET")

    // OIDCプロバイダーを初期化(Googleを指定)
    var err error
    provider, err = oidc.NewProvider(context.Background(), "https://accounts.google.com")
    if err != nil {
        log.Fatalf("failed to get provider: %v", err)
    }

    // OAuth 2.0設定を初期化
    config = &oauth2.Config{
        ClientID:     clientID,
        ClientSecret: clientSecret,
        RedirectURL:  redirectURL,
        Endpoint:     google.Endpoint, // GoogleのOAuth 2.0エンドポイント
        Scopes:       []string{oidc.ScopeOpenID, "profile", "email"}, // 要求するスコープ
    }
}


func main() {
    // ハンドラ関数をルートにマッピング
    http.HandleFunc("/", handleMain)
    http.HandleFunc("/login", handleLogin)
    http.HandleFunc("/callback", handleCallback)

    // サーバーを起動
    fmt.Println("Server started at http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}


// メインページのハンドラ
func handleMain(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello! Please <a href='/login'>log in</a>")
}


// ログイン処理のハンドラ
func handleLogin(w http.ResponseWriter, r *http.Request) {
    // 認証URLを生成し、そこへリダイレクト
    url := config.AuthCodeURL("state", oauth2.AccessTypeOffline)
    http.Redirect(w, r, url, http.StatusFound)
}


// OAuthコールバック処理のハンドラ
func handleCallback(w http.ResponseWriter, r *http.Request) {
    ctx := context.Background()

    // 認証コードを使ってトークンを交換
    oauth2Token, err := config.Exchange(ctx, r.URL.Query().Get("code"))
    if err != nil {
        http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
        return
    }

    // トークンからIDトークンを取得
    rawIDToken, ok := oauth2Token.Extra("id_token").(string)
    if !ok {
        http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError)
        return
    }

    // IDトークンを検証
    idToken, err := provider.Verifier(&oidc.Config{ClientID: clientID}).Verify(ctx, rawIDToken)
    if err != nil {
        http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
        return
    }

    // IDトークンからユーザー情報を取得
    var profile map[string]interface{}
    if err := idToken.Claims(&profile); err != nil {
        http.Error(w, "Failed to get user profile: "+err.Error(), http.StatusInternalServerError)
        return
    }

    // ユーザーのドメインが許可されているかを検証
    if domain, ok := profile["hd"].(string); !ok || domain != "dev-ryo.com" {
        http.Error(w, "Unauthorized domain", http.StatusUnauthorized)
        return
    }

    // ログイン成功メッセージを表示
    fmt.Fprintf(w, "Login successful: %+v", profile)
}

依存関係のインポート

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/coreos/go-oidc"
	"github.com/joho/godotenv"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
)

コードは必要なパッケージをインポートすることから始まります:

  • HTTPサーバー機能のためのcontexthttp
  • ログ記録のためのfmtlog
  • 環境変数アクセスのためのos
  • OAuthおよびOpenID Connectプロセスのためのgo-oidcgodotenvoauth2

グローバル変数

var (
    clientID string
    clientSecret string
    redirectURL = "http://localhost:8080/callback"
    provider *oidc.Provider
    config   *oauth2.Config
)

ここでは、クライアントID、クライアントシークレット、リダイレクトURL、OIDCプロバイダー、OAuth 2.0の設定を含むOAuth設定のためのグローバル変数を定義します。

初期化

func init() {
    if err := godotenv.Load(); err != nil {
        log.Fatalf("Error loading .env file: %v", err)
    }

    clientID = os.Getenv("CLIENT_ID")
    clientSecret = os.Getenv("CLIENT_SECRET")

    var err error
    provider, err = oidc.NewProvider(context.Background(), "https://accounts.google.com")
    if err != nil {
        log.Fatalf("failed to get provider: %v", err)
    }

    config = &oauth2.Config{
        ClientID:     clientID,
        ClientSecret: clientSecret,
        RedirectURL:  redirectURL,
        Endpoint:     google.Endpoint,
        Scopes:       []string{oidc.ScopeOpenID, "profile", "email"},
    }
}

init関数は環境変数をロードし、OIDCプロバイダーを初期化し、OAuth 2.0の設定を行います。

サーバーのセットアップとハンドラ

func main() {
    http.HandleFunc("/", handleMain)
    http.HandleFunc("/login", handleLogin)
    http.HandleFunc("/callback", handleCallback)
    fmt.Println("Server started at http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

mainでは、HTTPルートを定義し、サーバーを開始します。各ルートはOAuthフローの異なる部分を処理するハンドラ関数にマップされています。

ハンドラ関数

  • handleMainはログインリンクを表示します。
  • handleLoginはユーザーをGoogleのOAuth 2.0ログイン画面にリダイレクトします。
  • handleCallbackはOAuthコ

ールバックを処理し、認証コードをトークンと交換し、ユーザーのプロファイルを取得します。

GoにおけるOAuth 2.0のハンドラー関数に深く潜る

この記事では、Goアプリケーションで使用されるハンドラー関数を探り、OpenID Connectを用いたOAuth 2.0フローを管理する方法を解説します。各関数は認証プロセスにおいて重要な役割を果たし、ユーザーをログインへ導き、結果のデータを処理します。

ハンドラーの理解

アプリケーションは三つの主要なハンドラー関数を定義しています: handleMainhandleLoginhandleCallback。これらの関数はユーザーのブラウザとOAuth 2.0プロバイダーと対話し、認証を容易にします。

メインハンドラー: handleMain
func handleMain(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello! Please <a href='/login'>log in</a>")
}

目的: アプリケーションの入り口として機能し、ユーザーにログインリンクを提示します。
プロセス:
/loginルートへのリンクを含むシンプルなHTMLの挨拶を出力し、ログインプロセスを開始します。

ログインハンドラー: handleLogin
func handleLogin(w http.ResponseWriter, r *http.Request) {
    url := config.AuthCodeURL("state", oauth2.AccessTypeOffline)
    http.Redirect(w, r, url, http.StatusFound)
}

目的: ユーザーをプロバイダーのログインページにリダイレクトして、OAuth 2.0フローを開始します。
プロセス:
OAuth 2.0設定からAuthCodeURLを使用して認証URLを生成し、CSRF保護のためのstateパラメーターを含めます。
ユーザーを認証URLにリダイレクトし、そこでユーザーは自分の認証情報を使用してログインし、許可を与えます。

コールバックハンドラー: handleCallback
func handleCallback(w http.ResponseWriter, r *http.Request) {
    ctx := context.Background()
    oauth2Token, err := config.Exchange(ctx, r.URL.Query().Get("code"))
    if err != nil {
        http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
        return
    }

    rawIDToken, ok := oauth2Token.Extra("id_token").(string)
    if !ok {
        http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError)
        return
    }

    idToken, err := provider.Verifier(&oidc.Config{ClientID: clientID}).Verify(ctx, rawIDToken)
    if err != nil {
        http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
        return
    }

    var profile map[string]interface{}
    if err := idToken.Claims(&profile); err != nil {
        http.Error(w, "Failed to get user profile: "+err.Error(), http.StatusInternalServerError)
        return
    }

    if domain, ok := profile["hd"].(string); !ok || domain != "dev-ryo.com" {
        http.Error(w, "Unauthorized domain", http.StatusUnauthorized)
        return
    }

    fmt.Fprintf(w, "Login successful: %+v", profile)
}

目的: ユーザーがログインしてアプリケーションを認証した後、OAuth 2.0プロバイダーからのコールバックを処理します。
プロセス:
コールバックリクエストから認証コードを抽出し、config.Exchangeを使用してアクセストークンとIDトークンと交換します。
期待される発行者からのIDトークンを確認し、このアプリケーション向けで

あることを検証します。
必要に応じてドメインを検証しながら、ユーザーのプロファイル情報を抽出して表示します。

ハンドラー関数のまとめ

このGoアプリケーションのハンドラー関数は、ユーザーエクスペリエンスにOAuth 2.0フローをシームレスに統合します。これらはOAuth 2.0プロバイダー(この場合はGoogle)と対話し、ユーザーを認証し、アクセストークンを取得し、ユーザープロファイル情報を取得します。このプロセスはWebアプリケーションにおける典型的なOAuth 2.0フローを示し、Goを使用して安全かつ効率的な認証メカニズムを実装する方法を強調しています。

結論

このGoアプリケーションは、ユーザー認証のためのGoogleのOAuth 2.0およびOpenID Connectとの統合の基本的な例を提供します。Webサーバーを設定し、OAuthを構成し、認証フローを処理し、Goでこれらのプロトコルの実用的な実装を示しています。

アプリケーションでOAuth 2.0とOpenID Connectを理解し、実装することは、認証のための確立されたプロトコルとサービスを利用することで、セキュリティとユーザー体験を大幅に向上させることができます。

Discussion

りょうすけりょうすけ

echoフレームワークを使用したものに書き換えました!

package main

import (
	"context"  // コンテキストを管理するためのパッケージ
	"log"      // ログ出力のためのパッケージ
	"net/http" // HTTPサーバーとクライアントの実装
	"os"       // OS関連の操作として環境変数の取得

	"github.com/coreos/go-oidc"   // OpenID Connectのクライアント
	"github.com/joho/godotenv"    // .envファイルから環境変数を読み込むためのパッケージ
	"github.com/labstack/echo/v4" // Echoフレームワーク
	"golang.org/x/oauth2"         // OAuth 2.0のクライアント
	"golang.org/x/oauth2/google"  // GoogleのOAuth 2.0サービス
)

// グローバル変数の定義
var (
	clientID     string
	clientSecret string
	redirectURL  = "http://localhost:8081/callback"
	provider     *oidc.Provider
	config       *oauth2.Config
)

func init() {
	// .envファイルから環境変数をロード
	if err := godotenv.Load(); err != nil {
		log.Fatalf("Error loading .env file: %v", err)
	}

	// 環境変数からクライアントIDとクライアントシークレットを取得
	clientID = os.Getenv("CLIENT_ID")
	clientSecret = os.Getenv("CLIENT_SECRET")

	// GoogleのOpenID Connectプロバイダーを初期化
	var err error
	provider, err = oidc.NewProvider(context.Background(), "https://accounts.google.com")
	if err != nil {
		log.Fatalf("failed to get provider: %v", err)
	}

	// OAuth 2.0クライアント設定の初期化
	config = &oauth2.Config{
		ClientID:     clientID,
		ClientSecret: clientSecret,
		RedirectURL:  redirectURL,
		Endpoint:     google.Endpoint,
		Scopes:       []string{oidc.ScopeOpenID, "profile", "email"},
	}
}

func main() {
	e := echo.New() // Echoインスタンスの作成

	// ハンドラ関数のマッピング
	e.GET("/", handleMain)
	e.GET("/login", handleLogin)
	e.GET("/callback", handleCallback)

	// サーバーの起動
	log.Fatal(e.Start(":8081"))
}

// メインページのハンドラ関数
func handleMain(c echo.Context) error {
	return c.HTML(http.StatusOK, "Hello! Please <a href='/login'>log in</a>")
}

// ログインページのハンドラ関数
func handleLogin(c echo.Context) error {
	// 認証URLを生成しリダイレクト
	url := config.AuthCodeURL("state", oauth2.AccessTypeOffline)
	return c.Redirect(http.StatusFound, url)
}

// OAuthコールバックのハンドラ関数
func handleCallback(c echo.Context) error {
	ctx := context.Background()
	// 認証コードをトークンに交換
	oauth2Token, err := config.Exchange(ctx, c.QueryParam("code"))
	if err != nil {
		return c.String(http.StatusInternalServerError, "Failed to exchange token: "+err.Error())
	}

	// IDトークンの取得
	rawIDToken, ok := oauth2Token.Extra("id_token").(string)
	if !ok {
		return c.String(http.StatusInternalServerError, "No id_token field in oauth2 token.")
	}

	// IDトークンの検証
	idToken, err := provider.Verifier(&oidc.Config{ClientID: clientID}).Verify(ctx, rawIDToken)
	if err != nil {
		return c.String(http.StatusInternalServerError, "Failed to verify ID Token: "+err.Error())
	}

	// ユーザー情報の取得
	var profile map[string]interface{}
	if err := idToken.Claims(&profile); err != nil {
		return c.String(http.StatusInternalServerError, "Failed to get user profile: "+err.Error())
	}

	// ドメインの検証
	if domain, ok := profile["hd"].(string); !ok || domain != "dev-ryo.com" {
		return c.String(http.StatusUnauthorized, "Unauthorized domain")
	}

	// ログイン成功メッセージの表示
	return c.String(http.StatusOK, "Login successful: "+profile["email"].(string))
}