Skip to content

Commit c8f945d

Browse files
alepane21Noroth
andauthored
feat: improve logging of authorization errors and allow use customization (#2431)
Co-authored-by: Ludwig Bedacht <ludwig.bedacht@gmail.com>
1 parent 45d65c4 commit c8f945d

File tree

12 files changed

+256
-34
lines changed

12 files changed

+256
-34
lines changed

router-tests/authentication_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4016,6 +4016,123 @@ func TestIntrospectionAuthentication(t *testing.T) {
40164016
})
40174017
}
40184018

4019+
func TestUseCustomization(t *testing.T) {
4020+
t.Parallel()
4021+
4022+
authHeader := func(token string) http.Header {
4023+
return http.Header{
4024+
"Authorization": []string{"Bearer " + token},
4025+
}
4026+
}
4027+
4028+
testRequest := func(t *testing.T, xEnv *testenv.Environment, header http.Header, expectSuccess bool) string {
4029+
t.Helper()
4030+
4031+
res, err := xEnv.MakeRequest(http.MethodPost, "/graphql", header, strings.NewReader(employeesQuery))
4032+
require.NoError(t, err)
4033+
defer res.Body.Close()
4034+
4035+
if expectSuccess {
4036+
require.Equal(t, http.StatusOK, res.StatusCode)
4037+
require.Equal(t, JwksName, res.Header.Get(xAuthenticatedByHeader))
4038+
} else {
4039+
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
4040+
}
4041+
4042+
data, err := io.ReadAll(res.Body)
4043+
require.NoError(t, err)
4044+
return string(data)
4045+
}
4046+
4047+
testSetup := func(t *testing.T, crypto jwks.Crypto, allowedUse ...string) (string, []authentication.Authenticator) {
4048+
t.Helper()
4049+
4050+
authServer, err := jwks.NewServerWithOptions(t, jwks.WithProviders(crypto), jwks.WithUse(""))
4051+
require.NoError(t, err)
4052+
t.Cleanup(authServer.Close)
4053+
4054+
cfg := toJWKSConfig(authServer.JWKSURL(), time.Second*5)
4055+
cfg.AllowedUse = allowedUse
4056+
4057+
tokenDecoder, err := authentication.NewJwksTokenDecoder(
4058+
NewContextWithCancel(t),
4059+
zap.NewNop(),
4060+
[]authentication.JWKSConfig{cfg},
4061+
)
4062+
require.NoError(t, err)
4063+
4064+
authOptions := authentication.HttpHeaderAuthenticatorOptions{
4065+
Name: JwksName,
4066+
TokenDecoder: tokenDecoder,
4067+
}
4068+
authenticator, err := authentication.NewHttpHeaderAuthenticator(authOptions)
4069+
require.NoError(t, err)
4070+
4071+
authenticators := []authentication.Authenticator{authenticator}
4072+
4073+
token, err := authServer.TokenForKID(crypto.KID(), nil, false)
4074+
require.NoError(t, err)
4075+
4076+
return token, authenticators
4077+
}
4078+
4079+
t.Run("Use option", func(t *testing.T) {
4080+
t.Parallel()
4081+
4082+
t.Run("Test authentication with empty use should fail by default", func(t *testing.T) {
4083+
t.Parallel()
4084+
4085+
rsaCrypto, err := jwks.NewRSACrypto("test", jwkset.AlgRS256, 2048)
4086+
require.NoError(t, err)
4087+
4088+
token, authenticators := testSetup(t, rsaCrypto)
4089+
4090+
accessController, err := core.NewAccessController(core.AccessControllerOptions{
4091+
Authenticators: authenticators,
4092+
AuthenticationRequired: true,
4093+
SkipIntrospectionQueries: false,
4094+
IntrospectionSkipSecret: "",
4095+
})
4096+
require.NoError(t, err)
4097+
4098+
testenv.Run(t, &testenv.Config{
4099+
RouterOptions: []core.Option{
4100+
core.WithAccessController(accessController),
4101+
},
4102+
}, func(t *testing.T, xEnv *testenv.Environment) {
4103+
body := testRequest(t, xEnv, authHeader(token), false)
4104+
require.Equal(t, unauthorizedExpectedData, string(body))
4105+
})
4106+
})
4107+
4108+
t.Run("Test authentication with empty use should succeed if allowed", func(t *testing.T) {
4109+
t.Parallel()
4110+
4111+
rsaCrypto, err := jwks.NewRSACrypto("test", jwkset.AlgRS256, 2048)
4112+
require.NoError(t, err)
4113+
4114+
token, authenticators := testSetup(t, rsaCrypto, "")
4115+
4116+
accessController, err := core.NewAccessController(core.AccessControllerOptions{
4117+
Authenticators: authenticators,
4118+
AuthenticationRequired: true,
4119+
SkipIntrospectionQueries: false,
4120+
IntrospectionSkipSecret: "",
4121+
})
4122+
require.NoError(t, err)
4123+
4124+
testenv.Run(t, &testenv.Config{
4125+
RouterOptions: []core.Option{
4126+
core.WithAccessController(accessController),
4127+
},
4128+
}, func(t *testing.T, xEnv *testenv.Environment) {
4129+
body := testRequest(t, xEnv, authHeader(token), true)
4130+
require.Equal(t, employeesExpectedData, string(body))
4131+
})
4132+
})
4133+
})
4134+
}
4135+
40194136
func toJWKSConfig(url string, refresh time.Duration, allowedAlgorithms ...string) authentication.JWKSConfig {
40204137
return authentication.JWKSConfig{
40214138
URL: url,

router-tests/cmd/jwks-server/main.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,17 @@ import (
4242
"errors"
4343
"flag"
4444
"fmt"
45-
"github.com/MicahParks/jwkset"
46-
"github.com/golang-jwt/jwt/v5"
47-
"github.com/wundergraph/cosmo/router-tests/jwks"
4845
"log"
4946
"net/http"
5047
"os"
5148
"os/signal"
5249
"strings"
5350
"syscall"
5451
"time"
52+
53+
"github.com/MicahParks/jwkset"
54+
"github.com/golang-jwt/jwt/v5"
55+
"github.com/wundergraph/cosmo/router-tests/jwks"
5556
)
5657

5758
var (

router-tests/jwks/crypto.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type Crypto interface {
1818
SigningMethod() jwt.SigningMethod
1919
PrivateKey() privateKey
2020
MarshalJWK() (jwkset.JWK, error)
21+
MarshalJWKWithUse(use jwkset.USE) (jwkset.JWK, error)
2122
KID() string
2223
}
2324

@@ -37,15 +38,15 @@ func (b *baseCrypto) SigningMethod() jwt.SigningMethod {
3738
return jwt.GetSigningMethod(b.alg.String())
3839
}
3940

40-
func (b *baseCrypto) MarshalJWK() (jwkset.JWK, error) {
41+
func (b *baseCrypto) MarshalJWKWithUse(use jwkset.USE) (jwkset.JWK, error) {
4142
marshalOptions := jwkset.JWKMarshalOptions{
4243
Private: false,
4344
}
4445

4546
meta := jwkset.JWKMetadataOptions{
4647
ALG: b.alg,
4748
KID: b.kID,
48-
USE: jwkset.UseSig,
49+
USE: use,
4950
}
5051

5152
options := jwkset.JWKOptions{
@@ -56,6 +57,11 @@ func (b *baseCrypto) MarshalJWK() (jwkset.JWK, error) {
5657
return jwkset.NewJWKFromKey(b.pk, options)
5758
}
5859

60+
func (b *baseCrypto) MarshalJWK() (jwkset.JWK, error) {
61+
// Delegate to the new method with default signature use.
62+
return b.MarshalJWKWithUse(jwkset.UseSig)
63+
}
64+
5965
func (b *baseCrypto) KID() string {
6066
return b.kID
6167
}
@@ -112,14 +118,18 @@ func NewHMACCrypto(kID string, alg jwkset.ALG) (Crypto, error) {
112118
}
113119

114120
func (b *hmacCrypto) MarshalJWK() (jwkset.JWK, error) {
121+
return b.MarshalJWKWithUse(jwkset.UseSig)
122+
}
123+
124+
func (b *hmacCrypto) MarshalJWKWithUse(use jwkset.USE) (jwkset.JWK, error) {
115125
marshalOptions := jwkset.JWKMarshalOptions{
116126
Private: true,
117127
}
118128

119129
meta := jwkset.JWKMetadataOptions{
120130
ALG: b.alg,
121131
KID: b.kID,
122-
USE: jwkset.UseSig,
132+
USE: use,
123133
}
124134

125135
options := jwkset.JWKOptions{

router-tests/jwks/jwks.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,46 @@ func (s *Server) SetRespondTime(d time.Duration) {
133133
s.respondTime = d
134134
}
135135

136+
// ServerOption represents a configuration option for the test JWKS server.
137+
type ServerOption func(*serverConfig)
138+
139+
// serverConfig holds configurable parameters for server initialization.
140+
type serverConfig struct {
141+
use jwkset.USE
142+
providers []Crypto
143+
}
144+
145+
// WithUse sets the JWK "use" metadata value for keys written to storage.
146+
func WithUse(use jwkset.USE) ServerOption {
147+
return func(cfg *serverConfig) {
148+
cfg.use = use
149+
}
150+
}
151+
152+
func WithProviders(providers ...Crypto) ServerOption {
153+
return func(cfg *serverConfig) {
154+
cfg.providers = providers
155+
}
156+
}
157+
136158
func NewServerWithCrypto(t *testing.T, providers ...Crypto) (*Server, error) {
159+
return NewServerWithOptions(t, WithProviders(providers...))
160+
}
161+
162+
func NewServerWithOptions(t *testing.T, opts ...ServerOption) (*Server, error) {
137163
t.Helper()
138-
if len(providers) == 0 {
164+
165+
// Default configuration
166+
cfg := &serverConfig{
167+
use: jwkset.UseSig,
168+
}
169+
170+
// Apply options
171+
for _, opt := range opts {
172+
opt(cfg)
173+
}
174+
175+
if len(cfg.providers) == 0 {
139176
t.Fatalf("At least one crypto provider is required.")
140177
}
141178

@@ -146,10 +183,10 @@ func NewServerWithCrypto(t *testing.T, providers ...Crypto) (*Server, error) {
146183

147184
ctx := context.Background()
148185

149-
for _, p := range providers {
186+
for _, p := range cfg.providers {
150187
kid := p.KID()
151188

152-
jwk, err := p.MarshalJWK()
189+
jwk, err := p.MarshalJWKWithUse(cfg.use)
153190
if err != nil {
154191
t.Fatalf("Failed to marshal the JWK.\nError: %s", err)
155192
}

router/core/access_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func NewAccessController(opts AccessControllerOptions) (*AccessController, error
4949
func (a *AccessController) Access(w http.ResponseWriter, r *http.Request) (*http.Request, error) {
5050
auth, err := authentication.AuthenticateHTTPRequest(r.Context(), a.authenticators, r)
5151
if err != nil {
52-
return nil, ErrUnauthorized
52+
return nil, errors.Join(err, ErrUnauthorized)
5353
}
5454
if auth != nil {
5555
w.Header().Set("X-Authenticated-By", auth.Authenticator())

router/core/graphql_prehandler.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1146,8 +1146,14 @@ func (h *PreHandler) handleAuthenticationFailure(requestContext *requestContext,
11461146
rtrace.AttachErrToSpan(routerSpan, err)
11471147
rtrace.AttachErrToSpan(authenticateSpan, err)
11481148

1149+
graphqlErr := err
1150+
// If the error is an unauthorized error, we want to hide details from the graphql error
1151+
if errors.Is(err, ErrUnauthorized) {
1152+
graphqlErr = ErrUnauthorized
1153+
}
1154+
11491155
writeOperationError(r, w, requestLogger, &httpGraphqlError{
1150-
message: err.Error(),
1156+
message: graphqlErr.Error(),
11511157
statusCode: http.StatusUnauthorized,
11521158
})
11531159
}

router/core/supervisor_instance.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ func setupAuthenticators(ctx context.Context, logger *zap.Logger, cfg *config.Co
292292
URL: jwks.URL,
293293
RefreshInterval: jwks.RefreshInterval,
294294
AllowedAlgorithms: jwks.Algorithms,
295+
AllowedUse: jwks.AllowedUse,
295296

296297
Secret: jwks.Secret,
297298
Algorithm: jwks.Algorithm,

router/core/websocket.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,11 +398,13 @@ func (h *WebsocketHandler) handleUpgradeRequest(w http.ResponseWriter, r *http.R
398398
handler.request, err = h.accessController.Access(w, r)
399399
if err != nil {
400400
statusCode := http.StatusForbidden
401+
errorMessage := err
401402
if errors.Is(err, ErrUnauthorized) {
402403
statusCode = http.StatusUnauthorized
404+
errorMessage = ErrUnauthorized
403405
}
404406
http.Error(handler.w, http.StatusText(statusCode), statusCode)
405-
_ = handler.writeErrorMessage(requestID, err)
407+
_ = handler.writeErrorMessage(requestID, errorMessage)
406408
handler.Close(false)
407409
return
408410
}

0 commit comments

Comments
 (0)