11package  http
22
33import  (
4+ 	"context" 
45	"encoding/base64" 
56	"encoding/json" 
67	"fmt" 
78	"net/http" 
8- 	"slices" 
99	"strings" 
1010	"time" 
1111
12+ 	"github.com/coreos/go-oidc/v3/oidc" 
1213	"k8s.io/klog/v2" 
1314
1415	"github.com/manusa/kubernetes-mcp-server/pkg/mcp" 
@@ -31,37 +32,83 @@ func AuthorizationMiddleware(requireOAuth bool, serverURL string, mcpServer *mcp
3132				return 
3233			}
3334
35+ 			audience  :=  Audience 
36+ 			if  serverURL  !=  ""  {
37+ 				audience  =  serverURL 
38+ 			}
39+ 
3440			authHeader  :=  r .Header .Get ("Authorization" )
3541			if  authHeader  ==  ""  ||  ! strings .HasPrefix (authHeader , "Bearer " ) {
3642				klog .V (1 ).Infof ("Authentication failed - missing or invalid bearer token: %s %s from %s" , r .Method , r .URL .Path , r .RemoteAddr )
3743
38- 				w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience=%s, error="invalid_token"` , Audience ))
44+ 				if  serverURL  ==  ""  {
45+ 					w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience="%s", error="invalid_token"` , audience ))
46+ 				} else  {
47+ 					w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience="%s"", resource_metadata="%s%s", error="invalid_token"` , audience , serverURL , oauthProtectedResourceEndpoint ))
48+ 				}
3949				http .Error (w , "Unauthorized: Bearer token required" , http .StatusUnauthorized )
4050				return 
4151			}
4252
4353			token  :=  strings .TrimPrefix (authHeader , "Bearer " )
4454
45- 			audience  :=  Audience 
46- 			if  serverURL  !=  ""  {
47- 				audience  =  serverURL 
48- 			}
49- 
50- 			err  :=  validateJWTToken (token , audience )
55+ 			// Validate the token offline for simple sanity check 
56+ 			// Because missing expected audience and expired tokens must be 
57+ 			// rejected already. 
58+ 			claims , err  :=  validateJWTToken (token , audience )
5159			if  err  !=  nil  {
5260				klog .V (1 ).Infof ("Authentication failed - JWT validation error: %s %s from %s, error: %v" , r .Method , r .URL .Path , r .RemoteAddr , err )
5361
54- 				w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience=%s, error="invalid_token"` , Audience ))
62+ 				if  serverURL  ==  ""  {
63+ 					w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience="%s", error="invalid_token"` , audience ))
64+ 				} else  {
65+ 					w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience="%s"", resource_metadata="%s%s", error="invalid_token"` , audience , serverURL , oauthProtectedResourceEndpoint ))
66+ 				}
5567				http .Error (w , "Unauthorized: Invalid token" , http .StatusUnauthorized )
5668				return 
5769			}
5870
59- 			// Validate token using Kubernetes TokenReview API 
60- 			_ , _ , err  =  mcpServer .VerifyToken (r .Context (), token , Audience )
71+ 			oidcProvider  :=  mcpServer .GetOIDCProvider ()
72+ 			if  oidcProvider  !=  nil  {
73+ 				// If OIDC Provider is configured, this token must be validated against it. 
74+ 				if  err  :=  validateTokenWithOIDC (r .Context (), oidcProvider , token , audience ); err  !=  nil  {
75+ 					klog .V (1 ).Infof ("Authentication failed - OIDC token validation error: %s %s from %s, error: %v" , r .Method , r .URL .Path , r .RemoteAddr , err )
76+ 
77+ 					if  serverURL  ==  ""  {
78+ 						w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience="%s", error="invalid_token"` , audience ))
79+ 					} else  {
80+ 						w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience="%s"", resource_metadata="%s%s", error="invalid_token"` , audience , serverURL , oauthProtectedResourceEndpoint ))
81+ 					}
82+ 					http .Error (w , "Unauthorized: Invalid token" , http .StatusUnauthorized )
83+ 					return 
84+ 				}
85+ 			}
86+ 
87+ 			// Scopes are likely to be used for authorization. 
88+ 			scopes  :=  claims .GetScopes ()
89+ 			klog .V (2 ).Infof ("JWT token validated - Scopes: %v" , scopes )
90+ 
91+ 			// Now, there are a couple of options: 
92+ 			// 1. If there is no authorization url configured for this MCP Server, 
93+ 			// that means this token will be used against the Kubernetes API Server. 
94+ 			// So that we need to validate the token using Kubernetes TokenReview API beforehand. 
95+ 			// 2. If there is an authorization url configured for this MCP Server, 
96+ 			// that means up to this point, the token is validated against the OIDC Provider already. 
97+ 			// 2. a. If this is the only token in the headers, this validated token 
98+ 			// is supposed to be used against the Kubernetes API Server as well. Therefore, 
99+ 			// TokenReview request must succeed. 
100+ 			// 2. b. If this is not the only token in the headers, the token in here is used 
101+ 			// only for authentication and authorization. Therefore, we need to send TokenReview request 
102+ 			// with the other token in the headers (TODO: still need to validate aud and exp of this token separately). 
103+ 			_ , _ , err  =  mcpServer .VerifyTokenAPIServer (r .Context (), token , audience )
61104			if  err  !=  nil  {
62105				klog .V (1 ).Infof ("Authentication failed - token validation error: %s %s from %s, error: %v" , r .Method , r .URL .Path , r .RemoteAddr , err )
63106
64- 				w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience=%s, error="invalid_token"` , Audience ))
107+ 				if  serverURL  ==  ""  {
108+ 					w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience="%s", error="invalid_token"` , audience ))
109+ 				} else  {
110+ 					w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience="%s"", resource_metadata="%s%s", error="invalid_token"` , audience , serverURL , oauthProtectedResourceEndpoint ))
111+ 				}
65112				http .Error (w , "Unauthorized: Invalid token" , http .StatusUnauthorized )
66113				return 
67114			}
@@ -72,32 +119,60 @@ func AuthorizationMiddleware(requireOAuth bool, serverURL string, mcpServer *mcp
72119}
73120
74121type  JWTClaims  struct  {
75- 	Issuer     string    `json:"iss"` 
76- 	Audience   []string  `json:"aud"` 
77- 	ExpiresAt  int64     `json:"exp"` 
122+ 	Issuer     string  `json:"iss"` 
123+ 	Audience   any     `json:"aud"` 
124+ 	ExpiresAt  int64   `json:"exp"` 
125+ 	Scope      string  `json:"scope,omitempty"` 
126+ }
127+ 
128+ func  (c  * JWTClaims ) GetScopes () []string  {
129+ 	if  c .Scope  ==  ""  {
130+ 		return  nil 
131+ 	}
132+ 	return  strings .Fields (c .Scope )
78133}
79134
80- // validateJWTToken validates basic JWT claims without signature verification 
81- func  validateJWTToken (token , audience  string ) error  {
135+ func  (c  * JWTClaims ) ContainsAudience (audience  string ) bool  {
136+ 	switch  aud  :=  c .Audience .(type ) {
137+ 	case  string :
138+ 		return  aud  ==  audience 
139+ 	case  []interface {}:
140+ 		for  _ , a  :=  range  aud  {
141+ 			if  str , ok  :=  a .(string ); ok  &&  str  ==  audience  {
142+ 				return  true 
143+ 			}
144+ 		}
145+ 	case  []string :
146+ 		for  _ , a  :=  range  aud  {
147+ 			if  a  ==  audience  {
148+ 				return  true 
149+ 			}
150+ 		}
151+ 	}
152+ 	return  false 
153+ }
154+ 
155+ // validateJWTToken validates basic JWT claims without signature verification and returns the claims 
156+ func  validateJWTToken (token , audience  string ) (* JWTClaims , error ) {
82157	parts  :=  strings .Split (token , "." )
83158	if  len (parts ) !=  3  {
84- 		return  fmt .Errorf ("invalid JWT token format" )
159+ 		return  nil ,  fmt .Errorf ("invalid JWT token format" )
85160	}
86161
87162	claims , err  :=  parseJWTClaims (parts [1 ])
88163	if  err  !=  nil  {
89- 		return  fmt .Errorf ("failed to parse JWT claims: %v" , err )
164+ 		return  nil ,  fmt .Errorf ("failed to parse JWT claims: %v" , err )
90165	}
91166
92167	if  claims .ExpiresAt  >  0  &&  time .Now ().Unix () >  claims .ExpiresAt  {
93- 		return  fmt .Errorf ("token expired" )
168+ 		return  nil ,  fmt .Errorf ("token expired" )
94169	}
95170
96- 	if  ! slices . Contains ( claims .Audience ,  audience ) {
97- 		return  fmt .Errorf ("token audience mismatch: %v" , claims .Audience )
171+ 	if  ! claims .ContainsAudience ( audience ) {
172+ 		return  nil ,  fmt .Errorf ("token audience mismatch: %v" , claims .Audience )
98173	}
99174
100- 	return  nil 
175+ 	return  claims ,  nil 
101176}
102177
103178func  parseJWTClaims (payload  string ) (* JWTClaims , error ) {
@@ -118,3 +193,16 @@ func parseJWTClaims(payload string) (*JWTClaims, error) {
118193
119194	return  & claims , nil 
120195}
196+ 
197+ func  validateTokenWithOIDC (ctx  context.Context , provider  * oidc.Provider , token , audience  string ) error  {
198+ 	verifier  :=  provider .Verifier (& oidc.Config {
199+ 		ClientID : audience ,
200+ 	})
201+ 
202+ 	_ , err  :=  verifier .Verify (ctx , token )
203+ 	if  err  !=  nil  {
204+ 		return  fmt .Errorf ("JWT token verification failed: %v" , err )
205+ 	}
206+ 
207+ 	return  nil 
208+ }
0 commit comments