Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
go mod verify
go mod download

LINT_VERSION=1.64.8
LINT_VERSION=2.7.2
curl -fsSL https://github.com/golangci/golangci-lint/releases/download/v${LINT_VERSION}/golangci-lint-${LINT_VERSION}-linux-amd64.tar.gz | \
tar xz --strip-components 1 --wildcards \*/golangci-lint
mkdir -p bin && mv golangci-lint bin/
Expand All @@ -45,6 +45,6 @@ jobs:
assert-nothing-changed go fmt ./...
assert-nothing-changed go mod tidy

bin/golangci-lint run --out-format=colored-line-number --timeout=3m || STATUS=$?
bin/golangci-lint run --timeout=3m || STATUS=$?

exit $STATUS
19 changes: 12 additions & 7 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
version: "2"

run:
timeout: 5m
tests: true
Expand All @@ -8,21 +10,24 @@ linters:
- govet
- errcheck
- staticcheck
- gofmt
- goimports
- revive
- ineffassign
- typecheck
- unused
- gosimple
- misspell
- nakedret
- bodyclose
- gocritic
- makezero
- gosec

formatters:
enable:
- gofmt
- goimports

output:
formats: colored-line-number
print-issued-lines: true
print-linter-name: true
formats:
text:
path: stdout
print-issued-lines: true
print-linter-name: true
Comment on lines +23 to +33
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The golangci-lint v2 configuration format has changed significantly from v1. The 'formatters' section is not a valid top-level key in v2. Additionally, gofmt and goimports are not formatters but linters in golangci-lint. The 'output.formats' structure also appears incorrect for v2 - it should be 'output.format' (singular). Please verify this configuration against the golangci-lint v2 documentation and consider testing it locally before merging, as it may cause the linter to fail or ignore these settings.

Copilot uses AI. Check for mistakes.
1 change: 1 addition & 0 deletions cmd/streamnative-mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// Package main is the entry point for the StreamNative MCP server.
package main

import (
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.24.4
require (
github.com/99designs/keyring v1.2.2
github.com/apache/pulsar-client-go v0.13.1
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/golang-jwt/jwt v3.2.1+incompatible
github.com/google/go-cmp v0.7.0
github.com/hamba/avro/v2 v2.28.0
github.com/mark3labs/mcp-go v0.43.2
Expand Down
3 changes: 1 addition & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dimfeld/httptreemux v5.0.1+incompatible h1:Qj3gVcDNoOthBAqftuD596rm4wg/adLLz5xh5CmpiCA=
github.com/dimfeld/httptreemux v5.0.1+incompatible/go.mod h1:rbUlSV+CCpv/SuqUTP/8Bk2O3LyUV436/yaRGkhP6Z0=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
Expand Down Expand Up @@ -85,6 +83,7 @@ github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
Expand Down
11 changes: 8 additions & 3 deletions pkg/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// Package auth provides authentication and authorization functionality for StreamNative MCP Server.
// It implements OAuth 2.0 flows including client credentials and device authorization grants.
package auth

import (
Expand All @@ -20,12 +22,13 @@ import (
"io"
"time"

"github.com/dgrijalva/jwt-go"
"github.com/golang-jwt/jwt"
"golang.org/x/oauth2"
"k8s.io/utils/clock"
)

const (
// ClaimNameUserName is the JWT claim name for the username.
ClaimNameUserName = "https://streamnative.io/username"
)

Expand All @@ -42,6 +45,7 @@ type AuthorizationGrantRefresher interface {
Refresh(grant *AuthorizationGrant) (*AuthorizationGrant, error)
}

// AuthorizationGrantType defines the supported OAuth2 grant types.
type AuthorizationGrantType string

const (
Expand Down Expand Up @@ -139,17 +143,18 @@ func ExtractUserName(token oauth2.Token) (string, error) {
return "", fmt.Errorf("access token doesn't contain a recognizable user claim")
}

// DumpToken outputs token information to the provided writer for debugging.
func DumpToken(out io.Writer, token oauth2.Token) {
p := jwt.Parser{}
claims := jwt.MapClaims{}
if _, _, err := p.ParseUnverified(token.AccessToken, claims); err != nil {
fmt.Fprintf(out, "Unable to parse token. Err: %v\n", err)
_, _ = fmt.Fprintf(out, "Unable to parse token. Err: %v\n", err)
return
}

text, err := json.MarshalIndent(claims, "", " ")
if err != nil {
fmt.Fprintf(out, "Unable to print token. Err: %v\n", err)
_, _ = fmt.Fprintf(out, "Unable to print token. Err: %v\n", err)
}
_, _ = out.Write(text)
_, _ = fmt.Fprintln(out, "")
Expand Down
7 changes: 6 additions & 1 deletion pkg/auth/authorization_tokenretriever.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ type TokenErrorResponse struct {
ErrorDescription string `json:"error_description"`
}

// TokenError represents an error response from the token endpoint.
type TokenError struct {
ErrorCode string
ErrorDescription string
Expand Down Expand Up @@ -222,6 +223,7 @@ func (ce *TokenRetriever) ExchangeCode(req AuthorizationCodeExchangeRequest) (*T
if err != nil {
return nil, err
}
defer func() { _ = response.Body.Close() }()

return ce.handleAuthTokensResponse(response)
}
Expand All @@ -230,7 +232,7 @@ func (ce *TokenRetriever) ExchangeCode(req AuthorizationCodeExchangeRequest) (*T
// auth tokens for errors and parsing the raw body to a TokenResult struct
func (ce *TokenRetriever) handleAuthTokensResponse(resp *http.Response) (*TokenResult, error) {
if resp.Body != nil {
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
}

if resp.StatusCode < 200 || resp.StatusCode > 299 {
Expand Down Expand Up @@ -272,6 +274,7 @@ func (ce *TokenRetriever) ExchangeDeviceCode(ctx context.Context, req DeviceCode
if err != nil {
return nil, err
}
defer func() { _ = response.Body.Close() }()
token, err := ce.handleAuthTokensResponse(response)
if err == nil {
return token, nil
Expand Down Expand Up @@ -314,6 +317,7 @@ func (ce *TokenRetriever) ExchangeRefreshToken(req RefreshTokenExchangeRequest)
if err != nil {
return nil, err
}
defer func() { _ = response.Body.Close() }()

return ce.handleAuthTokensResponse(response)
}
Expand All @@ -330,6 +334,7 @@ func (ce *TokenRetriever) ExchangeClientCredentials(req ClientCredentialsExchang
if err != nil {
return nil, err
}
defer func() { _ = response.Body.Close() }()

return ce.handleAuthTokensResponse(response)
}
2 changes: 2 additions & 0 deletions pkg/auth/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// Package cache provides cached token sources for authentication flows.
package cache

import (
Expand Down Expand Up @@ -51,6 +52,7 @@ type tokenCache struct {
token *oauth2.Token
}

// NewDefaultTokenCache creates a default token cache with the given store and refresher.
func NewDefaultTokenCache(store store.Store, audience string,
refresher auth.AuthorizationGrantRefresher) (CachingTokenSource, error) {
cache := &tokenCache{
Expand Down
5 changes: 5 additions & 0 deletions pkg/auth/client_credentials_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type ClientCredentialsExchanger interface {
ExchangeClientCredentials(req ClientCredentialsExchangeRequest) (*TokenResult, error)
}

// NewClientCredentialsFlow creates a new client credentials flow with the given components.
func NewClientCredentialsFlow(
issuerData Issuer,
provider ClientCredentialsProvider,
Expand Down Expand Up @@ -98,6 +99,7 @@ func NewDefaultClientCredentialsFlowWithKeyFileStruct(issuerData Issuer, keyFile

var _ Flow = &ClientCredentialsFlow{}

// Authorize requests an authorization grant using the client credentials flow.
func (c *ClientCredentialsFlow) Authorize() (*AuthorizationGrant, error) {
keyFile, err := c.provider.GetClientCredentials()
if err != nil {
Expand All @@ -121,12 +123,14 @@ func (c *ClientCredentialsFlow) Authorize() (*AuthorizationGrant, error) {
return grant, nil
}

// ClientCredentialsGrantRefresher refreshes client-credentials grants using the token endpoint.
type ClientCredentialsGrantRefresher struct {
issuerData Issuer
exchanger ClientCredentialsExchanger
clock clock.Clock
}

// NewDefaultClientCredentialsGrantRefresher creates a default client credentials grant refresher.
func NewDefaultClientCredentialsGrantRefresher(issuerData Issuer,
clock clock.Clock) (*ClientCredentialsGrantRefresher, error) {
wellKnownEndpoints, err := GetOIDCWellKnownEndpointsFromIssuerURL(issuerData.IssuerEndpoint)
Expand All @@ -147,6 +151,7 @@ func NewDefaultClientCredentialsGrantRefresher(issuerData Issuer,

var _ AuthorizationGrantRefresher = &ClientCredentialsGrantRefresher{}

// Refresh exchanges the client credentials for a fresh authorization grant.
func (g *ClientCredentialsGrantRefresher) Refresh(grant *AuthorizationGrant) (*AuthorizationGrant, error) {
if grant.Type != GrantTypeClientCredentials {
return nil, errors.New("unsupported grant type")
Expand Down
17 changes: 14 additions & 3 deletions pkg/auth/client_credentials_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,33 @@ import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)

const (
// KeyFileTypeServiceAccount identifies service account key files.
KeyFileTypeServiceAccount = "sn_service_account"
FILE = "file://"
DATA = "data://"
// FILE indicates a file:// key file reference.
FILE = "file://"
// DATA indicates a data:// inline key file reference.
DATA = "data://"
)

// KeyFileProvider provides client credentials from a key file path.
type KeyFileProvider struct {
KeyFile string
}

// KeyFile holds service account credentials from a JSON key file.
type KeyFile struct {
Type string `json:"type"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
ClientEmail string `json:"client_email"`
}

// NewClientCredentialsProviderFromKeyFile creates a provider from a key file path.
func NewClientCredentialsProviderFromKeyFile(keyFile string) *KeyFileProvider {
return &KeyFileProvider{
KeyFile: keyFile,
Expand All @@ -46,13 +53,14 @@ func NewClientCredentialsProviderFromKeyFile(keyFile string) *KeyFileProvider {

var _ ClientCredentialsProvider = &KeyFileProvider{}

// GetClientCredentials loads client credentials from the configured key file source.
func (k *KeyFileProvider) GetClientCredentials() (*KeyFile, error) {
var keyFile []byte
var err error
switch {
case strings.HasPrefix(k.KeyFile, FILE):
filename := strings.TrimPrefix(k.KeyFile, FILE)
keyFile, err = os.ReadFile(filename)
keyFile, err = os.ReadFile(filepath.Clean(filename))
case strings.HasPrefix(k.KeyFile, DATA):
keyFile = []byte(strings.TrimPrefix(k.KeyFile, DATA))
case strings.HasPrefix(k.KeyFile, "data:"):
Expand Down Expand Up @@ -80,17 +88,20 @@ func (k *KeyFileProvider) GetClientCredentials() (*KeyFile, error) {
return &v, nil
}

// KeyFileStructProvider provides client credentials from an in-memory KeyFile struct.
type KeyFileStructProvider struct {
KeyFile *KeyFile
}

// GetClientCredentials returns the client credentials from the in-memory KeyFile.
func (k *KeyFileStructProvider) GetClientCredentials() (*KeyFile, error) {
if k.KeyFile == nil {
return nil, fmt.Errorf("key file is nil")
}
return k.KeyFile, nil
}

// NewClientCredentialsProviderFromKeyFileStruct creates a provider from an in-memory KeyFile.
func NewClientCredentialsProviderFromKeyFileStruct(keyFile *KeyFile) *KeyFileStructProvider {
return &KeyFileStructProvider{
KeyFile: keyFile,
Expand Down
2 changes: 1 addition & 1 deletion pkg/auth/oidc_endpoint_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func GetOIDCWellKnownEndpointsFromIssuerURL(issuerURL string) (*OIDCWellKnownEnd
if err != nil {
return nil, errors.Wrapf(err, "could not get well known endpoints from url %s", u.String())
}
defer r.Body.Close()
defer func() { _ = r.Body.Close() }()

var wkEndpoints OIDCWellKnownEndpoints
err = json.NewDecoder(r.Body).Decode(&wkEndpoints)
Expand Down
7 changes: 7 additions & 0 deletions pkg/auth/store/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// Package store provides token storage implementations for authentication credentials.
// It includes a KeyringStore implementation that uses the system keyring for secure storage.
package store

import (
Expand All @@ -25,6 +27,7 @@ import (
"k8s.io/utils/clock"
)

// KeyringStore provides secure token storage using the system keyring.
type KeyringStore struct {
kr keyring.Keyring
clock clock.Clock
Expand All @@ -48,6 +51,7 @@ func NewKeyringStore(kr keyring.Keyring) (*KeyringStore, error) {

var _ Store = &KeyringStore{}

// SaveGrant saves an authorization grant to the keyring.
func (f *KeyringStore) SaveGrant(audience string, grant auth.AuthorizationGrant) error {
f.lock.Lock()
defer f.lock.Unlock()
Expand Down Expand Up @@ -75,6 +79,7 @@ func (f *KeyringStore) SaveGrant(audience string, grant auth.AuthorizationGrant)
return nil
}

// LoadGrant loads an authorization grant from the keyring.
func (f *KeyringStore) LoadGrant(audience string) (*auth.AuthorizationGrant, error) {
f.lock.Lock()
defer f.lock.Unlock()
Expand All @@ -97,6 +102,7 @@ func (f *KeyringStore) LoadGrant(audience string) (*auth.AuthorizationGrant, err
return &item.Grant, nil
}

// WhoAmI returns the username associated with the grant for the given audience.
func (f *KeyringStore) WhoAmI(audience string) (string, error) {
f.lock.Lock()
defer f.lock.Unlock()
Expand Down Expand Up @@ -132,6 +138,7 @@ func (f *KeyringStore) WhoAmI(audience string) (string, error) {
return label, err
}

// Logout removes all stored grants from the keyring.
func (f *KeyringStore) Logout() error {
f.lock.Lock()
defer f.lock.Unlock()
Expand Down
Loading
Loading