Skip to content

Commit 775fa21

Browse files
authored
fix(auth): delegate JWT parsing to github.com/go-jose/go-jose (189)
fix(auth): delegate JWT parsing to github.com/golang-jwt/jwt Signed-off-by: Marc Nuri <[email protected]> --- fix(auth): delegate JWT parsing to go-jose Signed-off-by: Marc Nuri <[email protected]> --- fix(auth): delegate JWT parsing to go-jose - review comment Signed-off-by: Marc Nuri <[email protected]>
1 parent 73e9e84 commit 775fa21

File tree

3 files changed

+172
-248
lines changed

3 files changed

+172
-248
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/BurntSushi/toml v1.5.0
77
github.com/coreos/go-oidc/v3 v3.14.1
88
github.com/fsnotify/fsnotify v1.9.0
9+
github.com/go-jose/go-jose/v4 v4.0.5
910
github.com/mark3labs/mcp-go v0.34.0
1011
github.com/pkg/errors v0.9.1
1112
github.com/spf13/afero v1.14.0
@@ -52,7 +53,6 @@ require (
5253
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
5354
github.com/go-errors/errors v1.4.2 // indirect
5455
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
55-
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
5656
github.com/go-logr/logr v1.4.2 // indirect
5757
github.com/go-openapi/jsonpointer v0.21.0 // indirect
5858
github.com/go-openapi/jsonreference v0.20.2 // indirect

pkg/http/authorization.go

Lines changed: 35 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ package http
22

33
import (
44
"context"
5-
"encoding/base64"
6-
"encoding/json"
75
"fmt"
86
"net/http"
97
"strings"
10-
"time"
118

129
"github.com/coreos/go-oidc/v3/oidc"
10+
"github.com/go-jose/go-jose/v4"
11+
"github.com/go-jose/go-jose/v4/jwt"
1312
"k8s.io/klog/v2"
1413

1514
"github.com/manusa/kubernetes-mcp-server/pkg/mcp"
@@ -55,7 +54,10 @@ func AuthorizationMiddleware(requireOAuth bool, serverURL string, oidcProvider *
5554
// Validate the token offline for simple sanity check
5655
// Because missing expected audience and expired tokens must be
5756
// rejected already.
58-
claims, err := validateJWTToken(token, audience)
57+
claims, err := ParseJWTClaims(token)
58+
if err == nil && claims != nil {
59+
err = claims.Validate(audience)
60+
}
5961
if err != nil {
6062
klog.V(1).Infof("Authentication failed - JWT validation error: %s %s from %s, error: %v", r.Method, r.URL.Path, r.RemoteAddr, err)
6163

@@ -117,11 +119,25 @@ func AuthorizationMiddleware(requireOAuth bool, serverURL string, oidcProvider *
117119
}
118120
}
119121

122+
var allSignatureAlgorithms = []jose.SignatureAlgorithm{
123+
jose.EdDSA,
124+
jose.HS256,
125+
jose.HS384,
126+
jose.HS512,
127+
jose.RS256,
128+
jose.RS384,
129+
jose.RS512,
130+
jose.ES256,
131+
jose.ES384,
132+
jose.ES512,
133+
jose.PS256,
134+
jose.PS384,
135+
jose.PS512,
136+
}
137+
120138
type JWTClaims struct {
121-
Issuer string `json:"iss"`
122-
Audience any `json:"aud"`
123-
ExpiresAt int64 `json:"exp"`
124-
Scope string `json:"scope,omitempty"`
139+
jwt.Claims
140+
Scope string `json:"scope,omitempty"`
125141
}
126142

127143
func (c *JWTClaims) GetScopes() []string {
@@ -131,66 +147,21 @@ func (c *JWTClaims) GetScopes() []string {
131147
return strings.Fields(c.Scope)
132148
}
133149

134-
func (c *JWTClaims) ContainsAudience(audience string) bool {
135-
switch aud := c.Audience.(type) {
136-
case string:
137-
return aud == audience
138-
case []interface{}:
139-
for _, a := range aud {
140-
if str, ok := a.(string); ok && str == audience {
141-
return true
142-
}
143-
}
144-
case []string:
145-
for _, a := range aud {
146-
if a == audience {
147-
return true
148-
}
149-
}
150-
}
151-
return false
152-
}
153-
154-
// validateJWTToken validates basic JWT claims without signature verification and returns the claims
155-
func validateJWTToken(token, audience string) (*JWTClaims, error) {
156-
parts := strings.Split(token, ".")
157-
if len(parts) != 3 {
158-
return nil, fmt.Errorf("invalid JWT token format")
159-
}
160-
161-
claims, err := parseJWTClaims(parts[1])
162-
if err != nil {
163-
return nil, fmt.Errorf("failed to parse JWT claims: %v", err)
164-
}
165-
166-
if claims.ExpiresAt > 0 && time.Now().Unix() > claims.ExpiresAt {
167-
return nil, fmt.Errorf("token expired")
168-
}
169-
170-
if !claims.ContainsAudience(audience) {
171-
return nil, fmt.Errorf("token audience mismatch: %v", claims.Audience)
172-
}
173-
174-
return claims, nil
150+
// Validate Checks if the JWT claims are valid and if the audience matches the expected one.
151+
func (c *JWTClaims) Validate(audience string) error {
152+
return c.Claims.Validate(jwt.Expected{
153+
AnyAudience: jwt.Audience{audience},
154+
})
175155
}
176156

177-
func parseJWTClaims(payload string) (*JWTClaims, error) {
178-
// Add padding if needed
179-
if len(payload)%4 != 0 {
180-
payload += strings.Repeat("=", 4-len(payload)%4)
181-
}
182-
183-
decoded, err := base64.URLEncoding.DecodeString(payload)
157+
func ParseJWTClaims(token string) (*JWTClaims, error) {
158+
tkn, err := jwt.ParseSigned(token, allSignatureAlgorithms)
184159
if err != nil {
185-
return nil, fmt.Errorf("failed to decode JWT payload: %v", err)
160+
return nil, fmt.Errorf("failed to parse JWT token: %w", err)
186161
}
187-
188-
var claims JWTClaims
189-
if err := json.Unmarshal(decoded, &claims); err != nil {
190-
return nil, fmt.Errorf("failed to unmarshal JWT claims: %v", err)
191-
}
192-
193-
return &claims, nil
162+
claims := &JWTClaims{}
163+
err = tkn.UnsafeClaimsWithoutVerification(claims)
164+
return claims, err
194165
}
195166

196167
func validateTokenWithOIDC(ctx context.Context, provider *oidc.Provider, token, audience string) error {

0 commit comments

Comments
 (0)