🐙

Golangでchiを使ってAPIキーの認証を自作する

2023/07/30に公開

モチベーション

Goを使ったAPIを作成しており、APIキーの認証が必要となりました。AzureのAPI ManagementやAWSのAPI Gateway、OSSのKong Gatewayなどを利用して認証部分の実装をおまかせしても良いのですが、(お金や運用コストを鑑みて)自作してみることにしました。

Goにはecho, ginなどの軽量で素晴らしいwebフレームワークがありますが独自のcontextを使っているため多少慣れが必要です。今回はchiは標準のcontextに準拠している点を評価して採用しました。

https://github.com/go-chi/jwtauth を使っても良いのですが、クッキーからトークンを拾ってくる機能がついていたりするので最小限の機能だけで実装するために自作しました。https://github.com/lestrrat-go/jwx を信用しているので認証まわりで他のライブラリを噛ませたくないという気持ちもあります。

コード

package main

import (
	"fmt"
	"net/http"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/lestrrat-go/jwx/v2/jwa"
	"github.com/lestrrat-go/jwx/v2/jwt"
)

var key jwt.SignEncryptParseOption

func GenToken(id string) {
	t := jwt.New()
	t.Set("id", id)
	sampleToken, err := jwt.Sign(t, key)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(sampleToken))
}

func init() {
	key = jwt.WithKey(jwa.HS256, []byte("secret"))
	GenToken("123") // OK
	GenToken("456") // NG
}

func GetToken(r *http.Request) string {
	const bearer = "Bearer"
	token := r.Header.Get("Authorization")
	if len(token) > 7 && token[:7] != bearer { // 7 is len("Bearer ")
		return token[7:]
	}
	return ""
}

func MyMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		token := GetToken(r)
		fmt.Println("get token: ", token)
		jwtToken, err := jwt.Parse([]byte(token), key)
		if err != nil {
			fmt.Println(err)
			w.WriteHeader(http.StatusUnauthorized)
			return
		}

		// validate token
		err = jwt.Validate(jwtToken)
		if err != nil {
			fmt.Println(err)
			w.WriteHeader(http.StatusUnauthorized)
			return
		}

		// check id
		id, ok := jwtToken.Get("id")
		if !ok {
			w.WriteHeader(http.StatusUnauthorized)
			return
		}

		if id != "123" {
			w.WriteHeader(http.StatusUnauthorized)
			return
		}

		next.ServeHTTP(w, r)
	})
}

func main() {
	r := chi.NewRouter()
	r.Use(middleware.Logger)
	r.Use(MyMiddleware)
	r.Get("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("welcome"))
	})
	http.ListenAndServe(":3001", r)
}

id: "123"の方のJWTを使って認証するとうまくいきました。

% curl -H"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyJ9.HdEPYqE7fzGkBWBZyI-Cc8-gVQlNd2zBM7PwK47p67o" http://localhost:3001/
welcome

おわりに

APIキーをサーバー側で発行して、ユーザーはキーをそのまま認証に用いることを想定した実装ですが、かなり簡単にできました。実用的にはKVやDBと組み合わせた用途になると思うので次回はそちらをやってみようと思います。

お仕事のご連絡お待ちしています。contact@tychy.jp まで

Discussion