mirror of
https://github.com/hanzo-fi/go-libs.git
synced 2026-10-30 20:53:44 +00:00
304 lines
7.7 KiB
Go
304 lines
7.7 KiB
Go
package jwt_test
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/go-jose/go-jose/v4"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/fx"
|
|
"go.uber.org/fx/fxtest"
|
|
|
|
auth "github.com/hanzo-fi/go-libs/v5/pkg/authn/jwt"
|
|
"github.com/hanzo-fi/go-libs/v5/pkg/authn/oidc"
|
|
"github.com/hanzo-fi/go-libs/v5/pkg/fx/authnfx"
|
|
logging "github.com/hanzo-fi/go-libs/v5/pkg/observe/log"
|
|
)
|
|
|
|
// setupTestOIDCServer creates an HTTP test server that simulates an OIDC provider
|
|
// Returns the server, the issuer URL, and a channel to track discovery requests
|
|
func setupTestOIDCServer(t *testing.T) (*httptest.Server, string, chan bool) {
|
|
discoveryCalled := make(chan bool, 1)
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
// Discovery endpoint
|
|
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
|
discoveryCalled <- true
|
|
issuer := "http://" + r.Host
|
|
|
|
config := oidc.DiscoveryConfiguration{
|
|
Issuer: issuer,
|
|
JwksURI: issuer + "/.well-known/jwks.json",
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(config)
|
|
})
|
|
|
|
// JWKS endpoint (not used in tests, but required for a valid OIDC server)
|
|
mux.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"keys": []interface{}{},
|
|
})
|
|
})
|
|
|
|
server := httptest.NewServer(mux)
|
|
t.Cleanup(server.Close)
|
|
|
|
issuer := server.URL
|
|
|
|
return server, issuer, discoveryCalled
|
|
}
|
|
|
|
func TestModule(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("module calls discovery endpoint when enabled", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, issuer, discoveryCalled := setupTestOIDCServer(t)
|
|
|
|
var authenticator auth.Authenticator
|
|
|
|
options := []fx.Option{
|
|
authnfx.JWTModule(auth.Config{
|
|
Enabled: true,
|
|
Issuers: []string{issuer},
|
|
Service: "test-service",
|
|
CheckScopes: false,
|
|
}),
|
|
fx.Provide(func() context.Context {
|
|
return context.Background()
|
|
}),
|
|
fx.Provide(func() logging.Logger {
|
|
return logging.Testing()
|
|
}),
|
|
fx.Populate(&authenticator),
|
|
}
|
|
|
|
if !testing.Verbose() {
|
|
options = append(options, fx.NopLogger)
|
|
}
|
|
|
|
app := fxtest.New(t, options...)
|
|
app.RequireStart()
|
|
defer app.RequireStop()
|
|
|
|
require.NotNil(t, authenticator)
|
|
|
|
// Verify that the discovery endpoint was called
|
|
select {
|
|
case called := <-discoveryCalled:
|
|
require.True(t, called, "Discovery endpoint should have been called")
|
|
default:
|
|
t.Fatal("Discovery endpoint was not called")
|
|
}
|
|
})
|
|
|
|
t.Run("module with additional checks calls discovery endpoint when enabled", func(t *testing.T) {
|
|
t.Parallel()
|
|
_, issuer, discoveryCalled := setupTestOIDCServer(t)
|
|
|
|
var authenticator auth.Authenticator
|
|
|
|
provider := func(*http.Request) (string, error) { return "dummy", nil }
|
|
additionalChecks := []auth.AdditionalCheck{
|
|
auth.CheckOrganizationIDClaim(provider),
|
|
}
|
|
|
|
options := []fx.Option{
|
|
authnfx.JWTModule(auth.Config{
|
|
Enabled: true,
|
|
Issuer: issuer,
|
|
Service: "test-service-with-orgId-aware-auth",
|
|
CheckScopes: false,
|
|
AdditionalChecks: additionalChecks,
|
|
}),
|
|
fx.Provide(func() context.Context {
|
|
return context.Background()
|
|
}),
|
|
fx.Provide(func() logging.Logger {
|
|
return logging.Testing()
|
|
}),
|
|
fx.Populate(&authenticator),
|
|
}
|
|
|
|
if !testing.Verbose() {
|
|
options = append(options, fx.NopLogger)
|
|
}
|
|
|
|
app := fxtest.New(t, options...)
|
|
app.RequireStart()
|
|
defer app.RequireStop()
|
|
|
|
require.NotNil(t, authenticator)
|
|
|
|
// Verify that the discovery endpoint was called
|
|
select {
|
|
case called := <-discoveryCalled:
|
|
require.True(t, called, "Discovery endpoint should have been called")
|
|
default:
|
|
t.Fatal("Discovery endpoint was not called")
|
|
}
|
|
})
|
|
|
|
t.Run("module can be overridden with fx.Decorate", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a custom KeySet with a different key
|
|
customPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
require.NoError(t, err)
|
|
|
|
customJWK := jose.JSONWebKey{
|
|
Key: &customPrivateKey.PublicKey,
|
|
KeyID: "custom-key-id",
|
|
Algorithm: string(jose.RS256),
|
|
Use: oidc.KeyUseSignature,
|
|
}
|
|
|
|
customKeySet := oidc.NewStaticKeySet(customJWK)
|
|
|
|
_, issuer, _ := setupTestOIDCServer(t)
|
|
|
|
var authenticator auth.Authenticator
|
|
|
|
// Use fx.Decorate to override the Authenticator.
|
|
// With multi-issuer support, discovery happens inside the Authenticator
|
|
// provider, so decorating the Authenticator itself is the correct pattern.
|
|
options := []fx.Option{
|
|
authnfx.JWTModule(auth.Config{
|
|
Enabled: true,
|
|
Issuers: []string{issuer},
|
|
Service: "test-service",
|
|
CheckScopes: false,
|
|
}),
|
|
fx.Provide(func() context.Context {
|
|
return context.Background()
|
|
}),
|
|
fx.Provide(func() logging.Logger {
|
|
return logging.Testing()
|
|
}),
|
|
fx.Decorate(func() auth.Authenticator {
|
|
return auth.NewJWTAuth(
|
|
map[string]oidc.KeySet{issuer: customKeySet},
|
|
"test-service",
|
|
false,
|
|
nil,
|
|
)
|
|
}),
|
|
fx.Populate(&authenticator),
|
|
}
|
|
|
|
if !testing.Verbose() {
|
|
options = append(options, fx.NopLogger)
|
|
}
|
|
|
|
app := fxtest.New(t, options...)
|
|
app.RequireStart()
|
|
defer app.RequireStop()
|
|
|
|
require.NotNil(t, authenticator)
|
|
|
|
// Verify that the authenticator is using our custom key set
|
|
// by checking it's a *JWTAuth (not NoAuth)
|
|
_, ok := authenticator.(*auth.JWTAuth)
|
|
require.True(t, ok, "Authenticator should be a JWTAuth")
|
|
})
|
|
|
|
t.Run("module with disabled auth does not call discovery", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, issuer, discoveryCalled := setupTestOIDCServer(t)
|
|
|
|
var authenticator auth.Authenticator
|
|
|
|
options := []fx.Option{
|
|
authnfx.JWTModule(auth.Config{
|
|
Enabled: false,
|
|
Issuer: issuer,
|
|
Service: "test-service",
|
|
CheckScopes: false,
|
|
}),
|
|
fx.Populate(&authenticator),
|
|
}
|
|
|
|
if !testing.Verbose() {
|
|
options = append(options, fx.NopLogger)
|
|
}
|
|
|
|
app := fxtest.New(t, options...)
|
|
app.RequireStart()
|
|
defer app.RequireStop()
|
|
|
|
require.NotNil(t, authenticator)
|
|
|
|
// Verify that the discovery endpoint was NOT called when auth is disabled
|
|
select {
|
|
case <-discoveryCalled:
|
|
t.Fatal("Discovery endpoint should NOT have been called when auth is disabled")
|
|
default:
|
|
// Good, discovery was not called
|
|
}
|
|
})
|
|
|
|
t.Run("module can be annotated and used distinctly", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, issuer1, discoveryCalled1 := setupTestOIDCServer(t)
|
|
_, issuer2, discoveryCalled2 := setupTestOIDCServer(t)
|
|
|
|
var authenticator auth.Authenticator
|
|
var authenticator2 auth.Authenticator
|
|
|
|
options := []fx.Option{
|
|
authnfx.AnnotatedJWTModule(auth.Config{
|
|
Enabled: true,
|
|
Issuer: issuer1,
|
|
Service: "test-service",
|
|
CheckScopes: false,
|
|
}, "one"),
|
|
authnfx.AnnotatedJWTModule(auth.Config{
|
|
Enabled: true,
|
|
Issuer: issuer2,
|
|
Service: "test-service",
|
|
CheckScopes: false,
|
|
}, "two"),
|
|
fx.Populate(
|
|
fx.Annotate(&authenticator, fx.ParamTags(`name:"one"`)),
|
|
fx.Annotate(&authenticator2, fx.ParamTags(`name:"two"`)),
|
|
),
|
|
}
|
|
|
|
if !testing.Verbose() {
|
|
options = append(options, fx.NopLogger)
|
|
}
|
|
|
|
app := fxtest.New(t, options...)
|
|
app.RequireStart()
|
|
defer app.RequireStop()
|
|
|
|
require.NotNil(t, authenticator)
|
|
require.NotNil(t, authenticator2)
|
|
|
|
select {
|
|
case called := <-discoveryCalled1:
|
|
require.True(t, called, "Discovery endpoint for issuer 1 should have been called")
|
|
default:
|
|
t.Fatal("Discovery endpoint for issuer 1 was not called")
|
|
}
|
|
|
|
select {
|
|
case called := <-discoveryCalled2:
|
|
require.True(t, called, "Discovery endpoint for issuer 2 should have been called")
|
|
default:
|
|
t.Fatal("Discovery endpoint for issuer 2 was not called")
|
|
}
|
|
})
|
|
}
|