Skip to content

Commit

Permalink
JWT: Enable reading a JSON Web Key Set from a file (#180)
Browse files Browse the repository at this point in the history
* Add ForwardingSignatureValidator

ForwardingSignatureValidator holds a pointer to some other SignatureValidator
and will forward ValidateSignature requests to this validator.
The validator it references can be changed by calling Replace().

* Make JWKS a oneoff with either inline or file content

This changes the jwt.proto definition to take either an inline JWKS
struct, or a message containing a file path and a refresh interval.

The intention is that when a file path and refresh interval is provided,
we create a ForwardingSignatureValidator, and periodically update its internal
SignatureValidator with content from the referenced file.

When passing inline content, behavior should remain unchanged.

* Load JWKS from a file

When loading the JWT configuration, check if the config provides inline
JWKS content, or a reference to a file. If we get a reference to a file,
we set up a goroutine to periodically fetch the file and update a
ForwardingSignatureValidator.
  • Loading branch information
mortenmj authored Oct 30, 2023
1 parent ba53c0a commit 519a894
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 91 deletions.
14 changes: 4 additions & 10 deletions pkg/global/apply_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,10 @@ func (ls *LifecycleState) MarkReadyAndWait(group program.Group) {
router.Handle("/active_spans", httpHandler)
}

group.Go(func(ctx context.Context, siblingsGroup, dependenciesGroup program.Group) error {
if err := bb_http.NewServersFromConfigurationAndServe(
ls.config.HttpServers,
bb_http.NewMetricsHandler(router, "Diagnostics"),
group,
); err != nil {
return util.StatusWrap(err, "Failed to launch diagnostics HTTP server")
}
return nil
})
bb_http.NewServersFromConfigurationAndServe(
ls.config.HttpServers,
bb_http.NewMetricsHandler(router, "Diagnostics"),
group)
}
}

Expand Down
9 changes: 5 additions & 4 deletions pkg/grpc/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/buildbarn/bb-storage/pkg/auth"
"github.com/buildbarn/bb-storage/pkg/clock"
"github.com/buildbarn/bb-storage/pkg/jwt"
"github.com/buildbarn/bb-storage/pkg/program"
configuration "github.com/buildbarn/bb-storage/pkg/proto/configuration/grpc"
"github.com/buildbarn/bb-storage/pkg/util"
"github.com/jmespath/go-jmespath"
Expand All @@ -24,7 +25,7 @@ type Authenticator interface {

// NewAuthenticatorFromConfiguration creates a tree of Authenticator
// objects based on a configuration file.
func NewAuthenticatorFromConfiguration(policy *configuration.AuthenticationPolicy) (Authenticator, bool, error) {
func NewAuthenticatorFromConfiguration(policy *configuration.AuthenticationPolicy, group program.Group) (Authenticator, bool, error) {
if policy == nil {
return nil, false, status.Error(codes.InvalidArgument, "Authentication policy not specified")
}
Expand All @@ -39,7 +40,7 @@ func NewAuthenticatorFromConfiguration(policy *configuration.AuthenticationPolic
children := make([]Authenticator, 0, len(policyKind.Any.Policies))
needsPeerTransportCredentials := false
for _, childConfiguration := range policyKind.Any.Policies {
child, childNeedsPeerTransportCredentials, err := NewAuthenticatorFromConfiguration(childConfiguration)
child, childNeedsPeerTransportCredentials, err := NewAuthenticatorFromConfiguration(childConfiguration, group)
if err != nil {
return nil, false, err
}
Expand All @@ -51,7 +52,7 @@ func NewAuthenticatorFromConfiguration(policy *configuration.AuthenticationPolic
children := make([]Authenticator, 0, len(policyKind.All.Policies))
needsPeerTransportCredentials := false
for _, childConfiguration := range policyKind.All.Policies {
child, childNeedsPeerTransportCredentials, err := NewAuthenticatorFromConfiguration(childConfiguration)
child, childNeedsPeerTransportCredentials, err := NewAuthenticatorFromConfiguration(childConfiguration, group)
if err != nil {
return nil, false, err
}
Expand Down Expand Up @@ -81,7 +82,7 @@ func NewAuthenticatorFromConfiguration(policy *configuration.AuthenticationPolic
metadataExtractor,
), false, nil
case *configuration.AuthenticationPolicy_Jwt:
authorizationHeaderParser, err := jwt.NewAuthorizationHeaderParserFromConfiguration(policyKind.Jwt)
authorizationHeaderParser, err := jwt.NewAuthorizationHeaderParserFromConfiguration(policyKind.Jwt, group)
if err != nil {
return nil, false, util.StatusWrap(err, "Failed to create authorization header parser for JWT authentication policy")
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/grpc/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func init() {
func NewServersFromConfigurationAndServe(configurations []*configuration.ServerConfiguration, registrationFunc func(grpc.ServiceRegistrar), group program.Group) error {
for _, configuration := range configurations {
// Create an authenticator for requests.
authenticator, needsPeerTransportCredentials, err := NewAuthenticatorFromConfiguration(configuration.AuthenticationPolicy)
authenticator, needsPeerTransportCredentials, err := NewAuthenticatorFromConfiguration(configuration.AuthenticationPolicy, group)
if err != nil {
return err
}
Expand Down
9 changes: 5 additions & 4 deletions pkg/http/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/buildbarn/bb-storage/pkg/auth"
"github.com/buildbarn/bb-storage/pkg/clock"
"github.com/buildbarn/bb-storage/pkg/jwt"
"github.com/buildbarn/bb-storage/pkg/program"
configuration "github.com/buildbarn/bb-storage/pkg/proto/configuration/http"
"github.com/buildbarn/bb-storage/pkg/random"
"github.com/buildbarn/bb-storage/pkg/util"
Expand All @@ -30,7 +31,7 @@ type Authenticator interface {

// NewAuthenticatorFromConfiguration creates a tree of Authenticator
// objects based on a configuration file.
func NewAuthenticatorFromConfiguration(policy *configuration.AuthenticationPolicy) (Authenticator, error) {
func NewAuthenticatorFromConfiguration(policy *configuration.AuthenticationPolicy, group program.Group) (Authenticator, error) {
if policy == nil {
return nil, status.Error(codes.InvalidArgument, "Authentication policy not specified")
}
Expand All @@ -44,7 +45,7 @@ func NewAuthenticatorFromConfiguration(policy *configuration.AuthenticationPolic
case *configuration.AuthenticationPolicy_Any:
children := make([]Authenticator, 0, len(policyKind.Any.Policies))
for _, childConfiguration := range policyKind.Any.Policies {
child, err := NewAuthenticatorFromConfiguration(childConfiguration)
child, err := NewAuthenticatorFromConfiguration(childConfiguration, group)
if err != nil {
return nil, err
}
Expand All @@ -54,7 +55,7 @@ func NewAuthenticatorFromConfiguration(policy *configuration.AuthenticationPolic
case *configuration.AuthenticationPolicy_Deny:
return NewDenyAuthenticator(policyKind.Deny), nil
case *configuration.AuthenticationPolicy_Jwt:
authorizationHeaderParser, err := jwt.NewAuthorizationHeaderParserFromConfiguration(policyKind.Jwt)
authorizationHeaderParser, err := jwt.NewAuthorizationHeaderParserFromConfiguration(policyKind.Jwt, group)
if err != nil {
return nil, util.StatusWrap(err, "Failed to create authorization header parser for JWT authentication policy")
}
Expand Down Expand Up @@ -118,7 +119,7 @@ func NewAuthenticatorFromConfiguration(policy *configuration.AuthenticationPolic
cookieAEAD,
clock.SystemClock)
case *configuration.AuthenticationPolicy_AcceptHeader:
base, err := NewAuthenticatorFromConfiguration(policyKind.AcceptHeader.Policy)
base, err := NewAuthenticatorFromConfiguration(policyKind.AcceptHeader.Policy, group)
if err != nil {
return nil, err
}
Expand Down
46 changes: 24 additions & 22 deletions pkg/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,31 @@ import (
// program.Group, based on a configuration message. The web servers are
// automatically terminated if the context associated with the group is
// canceled.
func NewServersFromConfigurationAndServe(configurations []*configuration.ServerConfiguration, handler http.Handler, group program.Group) error {
for _, configuration := range configurations {
authenticator, err := NewAuthenticatorFromConfiguration(configuration.AuthenticationPolicy)
if err != nil {
return err
}
authenticatedHandler := NewAuthenticatingHandler(handler, authenticator)
for _, listenAddress := range configuration.ListenAddresses {
server := http.Server{
Addr: listenAddress,
Handler: authenticatedHandler,
func NewServersFromConfigurationAndServe(configurations []*configuration.ServerConfiguration, handler http.Handler, group program.Group) {
group.Go(func(ctx context.Context, siblingsGroup, dependenciesGroup program.Group) error {
for _, configuration := range configurations {
authenticator, err := NewAuthenticatorFromConfiguration(configuration.AuthenticationPolicy, dependenciesGroup)
if err != nil {
return err
}
group.Go(func(ctx context.Context, siblingsGroup, dependenciesGroup program.Group) error {
<-ctx.Done()
return server.Close()
})
group.Go(func(ctx context.Context, siblingsGroup, dependenciesGroup program.Group) error {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
return util.StatusWrapf(err, "Failed to launch HTTP server %#v", server.Addr)
authenticatedHandler := NewAuthenticatingHandler(handler, authenticator)
for _, listenAddress := range configuration.ListenAddresses {
server := http.Server{
Addr: listenAddress,
Handler: authenticatedHandler,
}
return nil
})
group.Go(func(ctx context.Context, siblingsGroup, dependenciesGroup program.Group) error {
<-ctx.Done()
return server.Close()
})
group.Go(func(ctx context.Context, siblingsGroup, dependenciesGroup program.Group) error {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
return util.StatusWrapf(err, "Failed to launch HTTP server %#v", server.Addr)
}
return nil
})
}
}
}
return nil
return nil
})
}
2 changes: 2 additions & 0 deletions pkg/jwt/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ go_library(
"ecdsa_sha_signature_generator.go",
"ecdsa_sha_signature_validator.go",
"ed25519_signature_validator.go",
"forwarding_signature_validator.go",
"generate_authorization_header.go",
"hmac_sha_signature_validator.go",
"rsa_sha_signature_validator.go",
Expand All @@ -21,6 +22,7 @@ go_library(
"//pkg/auth",
"//pkg/clock",
"//pkg/eviction",
"//pkg/program",
"//pkg/proto/configuration/jwt",
"//pkg/random",
"//pkg/util",
Expand Down
90 changes: 78 additions & 12 deletions pkg/jwt/configuration.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package jwt

import (
"context"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"encoding/json"
"log"
"os"
"reflect"
"time"

"github.com/buildbarn/bb-storage/pkg/clock"
"github.com/buildbarn/bb-storage/pkg/eviction"
"github.com/buildbarn/bb-storage/pkg/program"
configuration "github.com/buildbarn/bb-storage/pkg/proto/configuration/jwt"
"github.com/buildbarn/bb-storage/pkg/util"
jose "github.com/go-jose/go-jose/v3"
Expand All @@ -22,18 +27,31 @@ import (
// NewAuthorizationHeaderParserFromConfiguration creates a new HTTP
// "Authorization" header parser based on options stored in a
// configuration file.
func NewAuthorizationHeaderParserFromConfiguration(config *configuration.AuthorizationHeaderParserConfiguration) (*AuthorizationHeaderParser, error) {
jwksJSON, err := protojson.Marshal(config.JwksInline)
if err != nil {
return nil, util.StatusWrapWithCode(err, codes.InvalidArgument, "Failed to marshal JSON Web Key Set")
}
var jwks jose.JSONWebKeySet
if err := json.Unmarshal(jwksJSON, &jwks); err != nil {
return nil, util.StatusWrapWithCode(err, codes.InvalidArgument, "Failed to unmarshal JSON Web Key Set")
}
signatureValidator, err := NewSignatureValidatorFromJSONWebKeySet(&jwks)
if err != nil {
return nil, err
func NewAuthorizationHeaderParserFromConfiguration(config *configuration.AuthorizationHeaderParserConfiguration, group program.Group) (*AuthorizationHeaderParser, error) {
var signatureValidator SignatureValidator

switch key := config.Jwks.(type) {
case *configuration.AuthorizationHeaderParserConfiguration_JwksInline:
jwksJSON, err := protojson.Marshal(key.JwksInline)
if err != nil {
return nil, util.StatusWrapWithCode(err, codes.InvalidArgument, "Failed to marshal JSON Web Key Set")
}
var jwks jose.JSONWebKeySet
if err := json.Unmarshal(jwksJSON, &jwks); err != nil {
return nil, util.StatusWrapWithCode(err, codes.InvalidArgument, "Failed to unmarshal JSON Web Key Set")
}
signatureValidator, err = NewSignatureValidatorFromJSONWebKeySet(&jwks)
if err != nil {
return nil, err
}
case *configuration.AuthorizationHeaderParserConfiguration_JwksFile:
var err error
signatureValidator, err = NewSignatureValidatorFromJSONWebKeySetFile(key.JwksFile, group)
if err != nil {
return nil, err
}
default:
return nil, status.Error(codes.InvalidArgument, "No key type provided")
}

evictionSet, err := eviction.NewSetFromConfiguration[string](config.CacheReplacementPolicy)
Expand Down Expand Up @@ -102,3 +120,51 @@ func NewSignatureValidatorFromJSONWebKeySet(jwks *jose.JSONWebKeySet) (Signature

return NewDemultiplexingSignatureValidator(namedSignatureValidators, allSignatureValidators), nil
}

// NewSignatureValidatorFromJSONWebKeySetFile creates a new
// SignatureValidator capable of validating JWTs matching keys contained
// in a JSON Web Key Set read from a file. The content of the file is
// periodically refreshed.
func NewSignatureValidatorFromJSONWebKeySetFile(path string, group program.Group) (SignatureValidator, error) {
internalValidator, err := getJWKSFromFile(path)
if err != nil {
return nil, util.StatusWrapf(err, "Unable to read JWKS content from file at %#v", path)
}
forwardingValidator := NewForwardingSignatureValidator(internalValidator)

group.Go(func(ctx context.Context, siblingsGroup, dependenciesGroup program.Group) error {
t := time.NewTicker(300 * time.Second)
defer t.Stop()

for {
select {
case <-t.C:
internalValidator, err := getJWKSFromFile(path)
if err != nil {
log.Printf("Failed to read JWKS content from file at %#v: %s", path, err)
continue
}
forwardingValidator.Replace(internalValidator)

case <-ctx.Done():
return util.StatusFromContext(ctx)
}
}
})

return forwardingValidator, nil
}

func getJWKSFromFile(path string) (SignatureValidator, error) {
jwksJSON, err := os.ReadFile(path)
if err != nil {
return nil, err
}

var jwks jose.JSONWebKeySet
if err := json.Unmarshal(jwksJSON, &jwks); err != nil {
return nil, err
}

return NewSignatureValidatorFromJSONWebKeySet(&jwks)
}
33 changes: 33 additions & 0 deletions pkg/jwt/forwarding_signature_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package jwt

import (
"sync/atomic"
)

// ForwardingSignatureValidator wraps another SignatureValidator. It is
// used when the underlying SignatureValidator needs to be replaced at
// runtime.
type ForwardingSignatureValidator struct {
validator atomic.Pointer[SignatureValidator]
}

// NewForwardingSignatureValidator creates a SignatureValidator that simply forwards
// requests to another SignatureValidator.
// This returns a pointer to the new ForwardingSignatureValidator, so as not to
// copy the atomic.Pointer.
func NewForwardingSignatureValidator(validator SignatureValidator) *ForwardingSignatureValidator {
sv := ForwardingSignatureValidator{}
sv.validator.Store(&validator)

return &sv
}

// Replace replaces the registered SignatureValidator
func (sv *ForwardingSignatureValidator) Replace(validator SignatureValidator) {
sv.validator.Store(&validator)
}

// ValidateSignature validates a signature using the registered SignatureValidator
func (sv *ForwardingSignatureValidator) ValidateSignature(algorithm string, keyID *string, headerAndPayload string, signature []byte) bool {
return (*sv.validator.Load()).ValidateSignature(algorithm, keyID, headerAndPayload, signature)
}
1 change: 1 addition & 0 deletions pkg/proto/configuration/jwt/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ proto_library(
visibility = ["//visibility:public"],
deps = [
"//pkg/proto/configuration/eviction:eviction_proto",
"@com_google_protobuf//:duration_proto",
"@com_google_protobuf//:struct_proto",
],
)
Expand Down
Loading

0 comments on commit 519a894

Please sign in to comment.