-
Notifications
You must be signed in to change notification settings - Fork 290
fix(auth): delegate JWT parsing to github.com/go-jose/go-jose #189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,14 +2,13 @@ package http | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "encoding/base64" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "encoding/json" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "net/http" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/coreos/go-oidc/v3/oidc" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/go-jose/go-jose/v4" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/go-jose/go-jose/v4/jwt" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "k8s.io/klog/v2" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/manusa/kubernetes-mcp-server/pkg/mcp" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -55,7 +54,10 @@ func AuthorizationMiddleware(requireOAuth bool, serverURL string, mcpServer *mcp | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Validate the token offline for simple sanity check | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Because missing expected audience and expired tokens must be | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // rejected already. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| claims, err := validateJWTToken(token, audience) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| claims, err := ParseJWTClaims(token) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err == nil && claims != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| err = claims.Validate(audience) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| klog.V(1).Infof("Authentication failed - JWT validation error: %s %s from %s, error: %v", r.Method, r.URL.Path, r.RemoteAddr, err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -118,11 +120,25 @@ func AuthorizationMiddleware(requireOAuth bool, serverURL string, mcpServer *mcp | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var allSignatureAlgorithms = []jose.SignatureAlgorithm{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jose.EdDSA, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jose.HS256, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like go-oidc does not easily support verifying symmetric tokens (the ones contain
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I copied the list from https://github.com/go-jose/go-jose/blob/3a80e136a96e747bf44049414eadc02828df4d33/jose-util/crypto.go#L48-L62 Since the original implementation was completely lenient on the signature I assumed we wanted to allow anything, hence reusing a list.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should have been clearer. I think, this list is correct and nice. The problem is in oidcProvider side which does not support symmetric algorithms such as |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jose.HS384, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jose.HS512, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jose.RS256, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jose.RS384, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jose.RS512, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jose.ES256, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jose.ES384, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jose.ES512, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jose.PS256, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jose.PS384, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jose.PS512, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type JWTClaims struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Issuer string `json:"iss"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Audience any `json:"aud"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ExpiresAt int64 `json:"exp"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Scope string `json:"scope,omitempty"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jwt.Claims | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Scope string `json:"scope,omitempty"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (c *JWTClaims) GetScopes() []string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -132,66 +148,21 @@ func (c *JWTClaims) GetScopes() []string { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return strings.Fields(c.Scope) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (c *JWTClaims) ContainsAudience(audience string) bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch aud := c.Audience.(type) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case string: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return aud == audience | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case []interface{}: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, a := range aud { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if str, ok := a.(string); ok && str == audience { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case []string: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, a := range aud { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if a == audience { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // validateJWTToken validates basic JWT claims without signature verification and returns the claims | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func validateJWTToken(token, audience string) (*JWTClaims, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts := strings.Split(token, ".") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(parts) != 3 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("invalid JWT token format") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| claims, err := parseJWTClaims(parts[1]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("failed to parse JWT claims: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if claims.ExpiresAt > 0 && time.Now().Unix() > claims.ExpiresAt { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("token expired") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !claims.ContainsAudience(audience) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("token audience mismatch: %v", claims.Audience) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return claims, nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Validate Checks if the JWT claims are valid and if the audience matches the expected one. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (c *JWTClaims) Validate(audience string) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer to keep JWTToken as a single entity for further refactoring |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Claims.Validate(jwt.Expected{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AnyAudience: jwt.Audience{audience}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func parseJWTClaims(payload string) (*JWTClaims, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Add padding if needed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(payload)%4 != 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| payload += strings.Repeat("=", 4-len(payload)%4) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| decoded, err := base64.URLEncoding.DecodeString(payload) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func ParseJWTClaims(token string) (*JWTClaims, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We won't need this, as validateJWTAndGetScopes handles it.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer to keep it separate for now |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tkn, err := jwt.ParseSigned(token, allSignatureAlgorithms) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("failed to decode JWT payload: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("failed to parse JWT token: %w", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var claims JWTClaims | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := json.Unmarshal(decoded, &claims); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("failed to unmarshal JWT claims: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return &claims, nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| claims := &JWTClaims{} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| err = tkn.UnsafeClaimsWithoutVerification(claims) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return claims, err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func validateTokenWithOIDC(ctx context.Context, provider *oidc.Provider, token, audience string) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about we only have one function that manages parsing, validating, extracting. This function returns scopes and error. Like this;
So that we don't need this;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that would be the next step.
We need a function or an entity that orchestrates all of the validation