Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pkg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Unit Tests
id: run-tests
continue-on-error: true
run: gotestsum --format standard-quiet --junitfile test-results.xml -- ./... -coverpkg=./... -coverprofile=coverage.txt
run: gotestsum --format standard-quiet --junitfile test-results.xml -- ./... -coverprofile=coverage.txt

- name: Analyze and upload test results
uses: smartcontractkit/.github/actions/branch-out-upload@branch-out-upload/v1
Expand Down
19 changes: 15 additions & 4 deletions pkg/nodeauth/jwt/node_jwt_authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,21 @@ func (v *NodeJWTAuthenticator) AuthenticateJWT(ctx context.Context, tokenString
// Public Key Validation: Verify node's CSA pubkey against the whitelisted registry via NodeAuthProvider.
isValid, err := v.nodeAuthProvider.IsNodePubKeyTrusted(ctx, publicKey)
if err != nil {
v.logger.Errorw("Node validation failed",
"csaPubKey", hex.EncodeToString(publicKey),
"error", err,
)
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
attrs := []any{
"csaPubKey", hex.EncodeToString(publicKey),
"error", err,
}
if ctxErr := ctx.Err(); ctxErr != nil {
attrs = append(attrs, "contextErr", ctxErr)
}
v.logger.Warnw("Node validation skipped: context canceled or deadline exceeded", attrs...)
} else {
v.logger.Errorw("Node validation failed",
"csaPubKey", hex.EncodeToString(publicKey),
"error", err,
)
}
return false, claims, fmt.Errorf("node validation failed: %w", err)
}

Expand Down
82 changes: 82 additions & 0 deletions pkg/nodeauth/jwt/node_jwt_authenticator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import (
"crypto/ed25519"
"crypto/rand"
"encoding/hex"
"errors"
"testing"
"time"

"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zapcore"

"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/nodeauth/jwt/mocks"
Expand Down Expand Up @@ -450,6 +452,86 @@ func TestNewNodeJWTAuthenticator_WithAndWithoutLeeway(t *testing.T) {
})
}

func TestNodeJWTAuthenticator_AuthenticateJWT_ProviderNonContextError(t *testing.T) {
Comment thread
elatoskinas marked this conversation as resolved.
Outdated
// Non-context provider errors must be logged at ERROR level.
privateKey, csaPubKey := createValidatorTestKeys()
providerErr := errors.New("database unavailable")
mockProvider := &mocks.NodeAuthProvider{}
mockProvider.On("IsNodePubKeyTrusted", mock.Anything, csaPubKey).Return(false, providerErr)

lggr, observedLogs := logger.TestObserved(t, zapcore.DebugLevel)
authenticator := NodeJWTAuthenticatorConfig{Logger: lggr}.New(mockProvider)

testRequest := testRequest{Field: "test-request"}
valid, claims, err := authenticator.AuthenticateJWT(context.Background(), createValidJWT(privateKey, csaPubKey), testRequest)

require.Error(t, err)
assert.False(t, valid)
assert.NotNil(t, claims)
assert.Contains(t, err.Error(), "node validation failed")

entries := observedLogs.FilterMessage("Node validation failed").All()
require.Len(t, entries, 1, "expected exactly one 'Node validation failed' log entry")
assert.Equal(t, zapcore.ErrorLevel, entries[0].Level, "non-context provider errors should log at ERROR")
mockProvider.AssertExpectations(t)
}

func TestNodeJWTAuthenticator_AuthenticateJWT_ProviderContextCancelledError(t *testing.T) {
// Context-cancellation errors from the provider must be logged at WARN, not ERROR,
// because they are caused by the caller cancelling the request — not a system fault.
privateKey, csaPubKey := createValidatorTestKeys()
mockProvider := &mocks.NodeAuthProvider{}
mockProvider.On("IsNodePubKeyTrusted", mock.Anything, csaPubKey).Return(false, context.Canceled)

lggr, observedLogs := logger.TestObserved(t, zapcore.DebugLevel)
authenticator := NodeJWTAuthenticatorConfig{Logger: lggr}.New(mockProvider)

ctx, cancel := context.WithCancel(context.Background())
cancel() // already cancelled

testRequest := testRequest{Field: "test-request"}
valid, claims, err := authenticator.AuthenticateJWT(ctx, createValidJWT(privateKey, csaPubKey), testRequest)

require.Error(t, err)
assert.False(t, valid)
assert.NotNil(t, claims)
assert.ErrorIs(t, err, context.Canceled)

entries := observedLogs.FilterMessage("Node validation skipped: context canceled or deadline exceeded").All()
require.Len(t, entries, 1, "expected exactly one context-cancellation log entry")
assert.Equal(t, zapcore.WarnLevel, entries[0].Level, "context cancellation from provider should log at WARN not ERROR")
assert.Equal(t, context.Canceled.Error(), entries[0].ContextMap()["contextErr"])
mockProvider.AssertExpectations(t)
}

func TestNodeJWTAuthenticator_AuthenticateJWT_ProviderDeadlineExceededError(t *testing.T) {
// context.DeadlineExceeded from the provider must also be logged at WARN, not ERROR,
// because it is an expected transient condition (e.g. slow upstream), not a system fault.
privateKey, csaPubKey := createValidatorTestKeys()
mockProvider := &mocks.NodeAuthProvider{}
mockProvider.On("IsNodePubKeyTrusted", mock.Anything, csaPubKey).Return(false, context.DeadlineExceeded)

lggr, observedLogs := logger.TestObserved(t, zapcore.DebugLevel)
authenticator := NodeJWTAuthenticatorConfig{Logger: lggr}.New(mockProvider)

ctx, cancel := context.WithTimeout(context.Background(), 0) // immediately expired
defer cancel()

testRequest := testRequest{Field: "test-request"}
valid, claims, err := authenticator.AuthenticateJWT(ctx, createValidJWT(privateKey, csaPubKey), testRequest)

require.Error(t, err)
assert.False(t, valid)
assert.NotNil(t, claims)
assert.ErrorIs(t, err, context.DeadlineExceeded)

entries := observedLogs.FilterMessage("Node validation skipped: context canceled or deadline exceeded").All()
require.Len(t, entries, 1, "expected exactly one context-cancellation log entry")
assert.Equal(t, zapcore.WarnLevel, entries[0].Level, "deadline exceeded from provider should log at WARN not ERROR")
assert.Equal(t, context.DeadlineExceeded.Error(), entries[0].ContextMap()["contextErr"])
mockProvider.AssertExpectations(t)
}

func TestNodeJWTAuthenticatorConfig_New(t *testing.T) {
t.Run("basic construction via config.New", func(t *testing.T) {
mockProvider := &mocks.NodeAuthProvider{}
Expand Down
Loading