📝

Go M2Mクライアント実装

2025/03/08に公開

はじめに

GoでOAuth2.0 M2M認証のクライアントコードを実装する際、どのように実装するかを調べました。

https://pkg.go.dev/golang.org/x/oauth2/clientcredentials

良さげなライブラリが存在しており、これで生成されたhttp.Clientを使うと、リソースサーバにリクエストを送る際に認証サーバからトークンの取得、セットをしてリクエストを送ってくれるようです。

SaaSを使用してM2M認証を実現する際、課金単位はトークンの発行回数である事が多いようですので、不必要にアクセストークンが発行される事のないよう挙動を確認しました。
http.Clientのインスタンスが同一である時、トークンの再発行が発生しない事を確認します。

確認_1インスタンスからのリクエスト

リソースサーバーの準備

最初に、ダミーのリソースサーバとしてトークンを出力するだけのwebサーバを準備します。

package main

import (
	"fmt"
	"log"
	"net/http"
	"strings"
)

func main() {
	http.HandleFunc("/resource", func(w http.ResponseWriter, r *http.Request) {
		authHeader := r.Header.Get("Authorization")
		if authHeader != "" {
			parts := strings.Split(authHeader, " ")
			if len(parts) == 2 && parts[0] == "Bearer" {
				token := parts[1]
				log.Printf("Received Access Token: %s\n", token)
			} else {
				log.Println("Invalid Authorization header format")
			}
		} else {
			log.Println("No Authorization header found")
		}

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		fmt.Fprintln(w, `{"message": "Request received"}`)
	})

	log.Println("Starting server on :5000")
	if err := http.ListenAndServe(":5000", nil); err != nil {
		log.Fatalf("Could not start server: %s\n", err)
	}
}

クライアントコード

続いて、クライアントコードです。

package main

import (
	"context"
	"log"
	"time"

	"golang.org/x/oauth2/clientcredentials"
)

func main() {
	// OAuth2 クライアントクレデンシャル設定
	conf := &clientcredentials.Config{
		ClientID:     "YOUR_CLIENTID",
		ClientSecret: "YOUR_CLIENTSECRET",
		TokenURL:     "YOUR_TOKENURL",
		EndpointParams: map[string][]string{
			"audience": {"YOUR_AUDIENCE"},
		},
	}

	// コンテキストの生成
	ctx := context.Background()

	// HTTP クライアントの生成
 	// *1つのクライアントを生成*
	client := conf.Client(ctx)

	// リソースサーバへのリクエスト
	resp, err := client.Get("http://localhost:5000/resource")
	if err != nil {
		log.Fatalf("Failed to get resource: %v", err)
	}
	defer resp.Body.Close()

	time.Sleep(3 * time.Second)

	// リソースサーバへのリクエスト
	resp2, err := client.Get("http://localhost:5000/resource")
	if err != nil {
		log.Fatalf("Failed to get resource: %v", err)
	}
	defer resp2.Body.Close()

}

実行

実行した際のリソースサーバのログです。
2回のリクエストで同一のトークンが使用されている(認証サーバへトークンの再発行が行われていない)事が確認できました。

2025/03/08 16:40:30 Received Access Token: eyJh*******lk4rDgoZzSbg
2025/03/08 16:40:33 Received Access Token: eyJh*******lk4rDgoZzSbg

確認_複数インスタンスからのリクエスト

http.Clientのインスタンスが複数となる場合の挙動を確認します。

クライアントコード

package main

import (
	"context"
	"log"
	"time"

	"golang.org/x/oauth2/clientcredentials"
)

func main() {
	// OAuth2 クライアントクレデンシャル設定
	conf := &clientcredentials.Config{
		ClientID:     "YOUR_CLIENTID",
		ClientSecret: "YOUR_CLIENTSECRET",
		TokenURL:     "YOUR_TOKENURL",
		EndpointParams: map[string][]string{
			"audience": {"YOUR_AUDIENCE"},
		},
	}

	// コンテキストの生成
	ctx := context.Background()

	// HTTP クライアントの生成
	// *2つのクライアントを生成*
	client := conf.Client(ctx)
	client2 := conf.Client(ctx)

	// リソースサーバへのリクエスト
	resp, err := client.Get("http://localhost:5000/resource")
	if err != nil {
		log.Fatalf("Failed to get resource: %v", err)
	}
	defer resp.Body.Close()

	time.Sleep(3 * time.Second)

	// リソースサーバへのリクエスト
	resp2, err := client2.Get("http://localhost:5000/resource")
	if err != nil {
		log.Fatalf("Failed to get resource: %v", err)
	}
	defer resp2.Body.Close()

}

実行

リソースサーバのログです。
トークンが別のものが設定されている事から、インスタンスがそれぞれトークンを発行した事が確認できます。

2025/03/08 16:47:07 Received Access Token: eyJh*******Ht2Tb7WAD6Rg
2025/03/08 16:47:10 Received Access Token: eyJh*******XHJZjfSe-pFA

まとめ

ライブラリでトークン取得してリクエストを送る処理は簡単に実現できることが確認できました。
同一のクライアントインスタンスを使用するように実装すれば、不必要なトークン発行も抑えることが可能そうです。

Discussion