Compare commits
	
		
			5 Commits
		
	
	
		
			v0.1.3
			...
			9c5865df00
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9c5865df00 | |||
| 9c05d2f38a | |||
| 67716a883d | |||
| 4afa9a01b6 | |||
| 2888905ab1 | 
							
								
								
									
										117
									
								
								authmw/token.go
									
									
									
									
									
								
							
							
						
						
									
										117
									
								
								authmw/token.go
									
									
									
									
									
								
							@@ -8,39 +8,26 @@ import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.t-juice.club/microfilm/auth"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
 | 
			
		||||
	"go.opentelemetry.io/otel"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ctxType string
 | 
			
		||||
 | 
			
		||||
var ctxKeyClaims ctxType = "claims"
 | 
			
		||||
 | 
			
		||||
var ErrNoClaimsInRequest = fmt.Errorf("no claims in request")
 | 
			
		||||
 | 
			
		||||
func VerifyToken(authURL string, permittedRoles []string) func(http.Handler) http.Handler {
 | 
			
		||||
	// Fetch current pubkey
 | 
			
		||||
	url := fmt.Sprintf("%s/key", authURL)
 | 
			
		||||
	req, err := http.NewRequest(http.MethodGet, url, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := http.DefaultClient.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	var authResponse auth.PubkeyResponse
 | 
			
		||||
	decoder := json.NewDecoder(resp.Body)
 | 
			
		||||
	if err := decoder.Decode(&authResponse); err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Parse pubkey
 | 
			
		||||
	pub, err := x509.ParsePKIXPublicKey(authResponse.PubKey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fn := func(next http.Handler) http.Handler {
 | 
			
		||||
		fn := func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
			ctx, span := otel.GetTracerProvider().Tracer("").Start(r.Context(), "verify-token")
 | 
			
		||||
			defer span.End()
 | 
			
		||||
 | 
			
		||||
			authHeader := r.Header.Get("Authorization")
 | 
			
		||||
			if !strings.Contains(authHeader, "Bearer ") {
 | 
			
		||||
				// No token, pass if unathorized in permitted
 | 
			
		||||
@@ -61,6 +48,74 @@ func VerifyToken(authURL string, permittedRoles []string) func(http.Handler) htt
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Fetch current pubkey
 | 
			
		||||
			url := fmt.Sprintf("%s/key", authURL)
 | 
			
		||||
			ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
 | 
			
		||||
			defer cancel()
 | 
			
		||||
 | 
			
		||||
			req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				// TODO: Should log
 | 
			
		||||
				w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
				errResp := &auth.ErrorResponse{
 | 
			
		||||
					Message: fmt.Sprintf("Error getting pubkey for token verification: %s", err),
 | 
			
		||||
					Status:  http.StatusUnauthorized,
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				encoder := json.NewEncoder(w)
 | 
			
		||||
				_ = encoder.Encode(&errResp)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			client := &http.Client{
 | 
			
		||||
				Transport: otelhttp.NewTransport(http.DefaultTransport),
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			resp, err := client.Do(req)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				// TODO: Should log
 | 
			
		||||
				w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
				errResp := &auth.ErrorResponse{
 | 
			
		||||
					Message: fmt.Sprintf("Error getting pubkey for token verification: %s", err),
 | 
			
		||||
					Status:  http.StatusUnauthorized,
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				encoder := json.NewEncoder(w)
 | 
			
		||||
				_ = encoder.Encode(&errResp)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
			var authResponse auth.PubkeyResponse
 | 
			
		||||
			decoder := json.NewDecoder(resp.Body)
 | 
			
		||||
			if err := decoder.Decode(&authResponse); err != nil {
 | 
			
		||||
				// TODO: Should log
 | 
			
		||||
				w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
				errResp := &auth.ErrorResponse{
 | 
			
		||||
					Message: fmt.Sprintf("Error getting pubkey for token verification: %s", err),
 | 
			
		||||
					Status:  http.StatusUnauthorized,
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				encoder := json.NewEncoder(w)
 | 
			
		||||
				_ = encoder.Encode(&errResp)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Parse pubkey
 | 
			
		||||
			pub, err := x509.ParsePKIXPublicKey(authResponse.PubKey)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				// TODO: Should log
 | 
			
		||||
				w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
				errResp := &auth.ErrorResponse{
 | 
			
		||||
					Message: fmt.Sprintf("Error getting pubkey for token verification: %s", err),
 | 
			
		||||
					Status:  http.StatusUnauthorized,
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				encoder := json.NewEncoder(w)
 | 
			
		||||
				_ = encoder.Encode(&errResp)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Validate token
 | 
			
		||||
			tokenString := strings.Split(authHeader, " ")[1]
 | 
			
		||||
			token, err := jwt.ParseWithClaims(tokenString, &auth.MicrofilmClaims{}, func(t *jwt.Token) (interface{}, error) { return pub, nil })
 | 
			
		||||
@@ -79,7 +134,7 @@ func VerifyToken(authURL string, permittedRoles []string) func(http.Handler) htt
 | 
			
		||||
 | 
			
		||||
			// Add claims to request context
 | 
			
		||||
			if claims, ok := token.Claims.(*auth.MicrofilmClaims); ok && token.Valid {
 | 
			
		||||
				ctx := context.WithValue(r.Context(), "claims", claims)
 | 
			
		||||
				ctx := context.WithValue(r.Context(), ctxKeyClaims, claims)
 | 
			
		||||
				next.ServeHTTP(w, r.WithContext(ctx))
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
@@ -91,3 +146,13 @@ func VerifyToken(authURL string, permittedRoles []string) func(http.Handler) htt
 | 
			
		||||
 | 
			
		||||
	return fn
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ClaimsFromCtx(ctx context.Context) (*auth.MicrofilmClaims, error) {
 | 
			
		||||
	rawValue := ctx.Value(ctxKeyClaims)
 | 
			
		||||
	value, ok := rawValue.(*auth.MicrofilmClaims)
 | 
			
		||||
	if ok {
 | 
			
		||||
		return value, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil, ErrNoClaimsInRequest
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								authmw/token_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								authmw/token_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
package authmw
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"git.t-juice.club/microfilm/auth"
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
	"github.com/google/go-cmp/cmp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestClaimsFromContext(t *testing.T) {
 | 
			
		||||
	claims := &auth.MicrofilmClaims{
 | 
			
		||||
		Role: "admin",
 | 
			
		||||
		RegisteredClaims: jwt.RegisteredClaims{
 | 
			
		||||
			Issuer:  "test",
 | 
			
		||||
			Subject: "subject",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	ctx := context.WithValue(context.Background(), ctxKeyClaims, claims)
 | 
			
		||||
 | 
			
		||||
	retrieved, err := ClaimsFromCtx(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Unable to retrieve claims")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if diff := cmp.Diff(claims, retrieved); diff != "" {
 | 
			
		||||
		t.Fatalf("Claims diff: %s", diff)
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								go.mod
									
									
									
									
									
								
							@@ -6,8 +6,10 @@ require (
 | 
			
		||||
	git.t-juice.club/microfilm/users v0.1.2
 | 
			
		||||
	github.com/go-chi/chi/v5 v5.0.10
 | 
			
		||||
	github.com/golang-jwt/jwt/v5 v5.0.0
 | 
			
		||||
	github.com/google/go-cmp v0.6.0
 | 
			
		||||
	github.com/google/uuid v1.3.1
 | 
			
		||||
	github.com/nats-io/nats.go v1.31.0
 | 
			
		||||
	github.com/nats-io/nkeys v0.4.5
 | 
			
		||||
	github.com/pelletier/go-toml/v2 v2.1.0
 | 
			
		||||
	github.com/urfave/cli/v2 v2.25.7
 | 
			
		||||
	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0
 | 
			
		||||
@@ -26,7 +28,6 @@ require (
 | 
			
		||||
	github.com/golang/protobuf v1.5.3 // indirect
 | 
			
		||||
	github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
 | 
			
		||||
	github.com/klauspost/compress v1.17.0 // indirect
 | 
			
		||||
	github.com/nats-io/nkeys v0.4.5 // indirect
 | 
			
		||||
	github.com/nats-io/nuid v1.0.1 // indirect
 | 
			
		||||
	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 | 
			
		||||
	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							@@ -24,8 +24,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
 | 
			
		||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 | 
			
		||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 | 
			
		||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 | 
			
		||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 | 
			
		||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 | 
			
		||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 | 
			
		||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
 | 
			
		||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
			
		||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,8 @@
 | 
			
		||||
ListenAddr = ":8082"
 | 
			
		||||
NATSAddr = "nats:4222"
 | 
			
		||||
BaseSubject = "microfilm.auth.v1"
 | 
			
		||||
 | 
			
		||||
UserServiceBaseURL = "http://mf-users:8080"
 | 
			
		||||
 | 
			
		||||
[NATS]
 | 
			
		||||
Enabled = true
 | 
			
		||||
Addr = "nats://nats1:4222,nats://nats2:4222,nats://nats3:4222"
 | 
			
		||||
NKeySeed = "SUAOUHJPINF4CK6TSNZMRR5G4DKGW5S76XRNIYURPEISNMWXJIXSVWIO7Y"
 | 
			
		||||
Subject = "microfilm.auth.v1"
 | 
			
		||||
@@ -8,12 +8,19 @@ import (
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	ListenAddr  string      `toml:"ListenAddr"`
 | 
			
		||||
	NATSAddr    string `toml:"NATSAddr"`
 | 
			
		||||
	NATS        *NATSConfig `toml:"NATS"`
 | 
			
		||||
	BaseSubject string      `toml:"BaseSubject"`
 | 
			
		||||
 | 
			
		||||
	UserServiceBaseURL string `toml:"UserServiceBaseURL"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NATSConfig struct {
 | 
			
		||||
	Enabled  bool   `toml:"Enabled"`
 | 
			
		||||
	NKeySeed string `toml:"NKeySeed"`
 | 
			
		||||
	Addr     string `toml:"Addr"`
 | 
			
		||||
	Subject  string `toml:"Subject"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ConfigFromReader(r io.Reader) (*Config, error) {
 | 
			
		||||
	decoder := toml.NewDecoder(r)
 | 
			
		||||
	var c Config
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ import (
 | 
			
		||||
	"github.com/golang-jwt/jwt/v5"
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	"github.com/nats-io/nats.go"
 | 
			
		||||
	"github.com/nats-io/nkeys"
 | 
			
		||||
	"go.opentelemetry.io/otel"
 | 
			
		||||
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
 | 
			
		||||
	"go.opentelemetry.io/otel/propagation"
 | 
			
		||||
@@ -72,7 +73,23 @@ func NewServer(config *Config) (*Server, error) {
 | 
			
		||||
 | 
			
		||||
	srv.store = store.NewMemoryAuthStore()
 | 
			
		||||
 | 
			
		||||
	conn, err := nats.Connect(config.NATSAddr)
 | 
			
		||||
	if config.NATS.Enabled {
 | 
			
		||||
		var opts []nats.Option
 | 
			
		||||
		if config.NATS.NKeySeed != "" {
 | 
			
		||||
			keys, err := nkeys.FromSeed([]byte(config.NATS.NKeySeed))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			pubkey, err := keys.PublicKey()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			srv.Logger.Debug("NATS enabled with NKeys", "pubkey", pubkey)
 | 
			
		||||
			creds := nats.Nkey(pubkey, keys.Sign)
 | 
			
		||||
			opts = append(opts, creds)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		conn, err := nats.Connect(config.NATS.Addr, opts...)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
@@ -82,6 +99,8 @@ func NewServer(config *Config) (*Server, error) {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		srv.nats = encoded
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	srv.userClient = NewUserClient(config.UserServiceBaseURL)
 | 
			
		||||
 | 
			
		||||
	// Generate keys
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,3 @@
 | 
			
		||||
package auth
 | 
			
		||||
 | 
			
		||||
const Version = "v0.1.3"
 | 
			
		||||
const Version = "v0.1.6"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user