@@ -9,93 +9,131 @@ import (
99 "github.com/coreos/go-oidc/v3/oidc"
1010 "github.com/go-jose/go-jose/v4"
1111 "github.com/go-jose/go-jose/v4/jwt"
12+ "golang.org/x/oauth2"
13+ authenticationapiv1 "k8s.io/api/authentication/v1"
1214 "k8s.io/klog/v2"
15+ "k8s.io/utils/strings/slices"
1316
17+ "github.com/containers/kubernetes-mcp-server/pkg/config"
1418 "github.com/containers/kubernetes-mcp-server/pkg/mcp"
1519)
1620
17- const (
18- Audience = "kubernetes-mcp-server"
19- )
21+ type KubernetesApiTokenVerifier interface {
22+ // KubernetesApiVerifyToken TODO: clarify proper implementation
23+ KubernetesApiVerifyToken (ctx context.Context , token , audience string ) (* authenticationapiv1.UserInfo , []string , error )
24+ }
2025
21- // AuthorizationMiddleware validates the OAuth flow using Kubernetes TokenReview API
22- func AuthorizationMiddleware (requireOAuth bool , serverURL string , oidcProvider * oidc.Provider , mcpServer * mcp.Server ) func (http.Handler ) http.Handler {
26+ // AuthorizationMiddleware validates the OAuth flow for protected resources.
27+ //
28+ // The flow is skipped for unprotected resources, such as health checks and well-known endpoints.
29+ //
30+ // There are several auth scenarios supported by this middleware:
31+ //
32+ // 1. requireOAuth is false:
33+ //
34+ // - The OAuth flow is skipped, and the server is effectively unprotected.
35+ // - The request is passed to the next handler without any validation.
36+ //
37+ // see TestAuthorizationRequireOAuthFalse
38+ //
39+ // 2. requireOAuth is set to true, server is protected:
40+ //
41+ // 2.1. Raw Token Validation (oidcProvider is nil):
42+ // - The token is validated offline for basic sanity checks (expiration).
43+ // - If OAuthAudience is set, the token is validated against the audience.
44+ // - If ValidateToken is set, the token is then used against the Kubernetes API Server for TokenReview.
45+ //
46+ // see TestAuthorizationRawToken
47+ //
48+ // 2.2. OIDC Provider Validation (oidcProvider is not nil):
49+ // - The token is validated offline for basic sanity checks (audience and expiration).
50+ // - If OAuthAudience is set, the token is validated against the audience.
51+ // - The token is then validated against the OIDC Provider.
52+ // - If ValidateToken is set, the token is then used against the Kubernetes API Server for TokenReview.
53+ //
54+ // see TestAuthorizationOidcToken
55+ //
56+ // 2.3. OIDC Token Exchange (oidcProvider is not nil, StsClientId and StsAudience are set):
57+ // - The token is validated offline for basic sanity checks (audience and expiration).
58+ // - If OAuthAudience is set, the token is validated against the audience.
59+ // - The token is then validated against the OIDC Provider.
60+ // - If the token is valid, an external account token exchange is performed using
61+ // the OIDC Provider to obtain a new token with the specified audience and scopes.
62+ // - If ValidateToken is set, the exchanged token is then used against the Kubernetes API Server for TokenReview.
63+ //
64+ // see TestAuthorizationOidcTokenExchange
65+ func AuthorizationMiddleware (staticConfig * config.StaticConfig , oidcProvider * oidc.Provider , verifier KubernetesApiTokenVerifier ) func (http.Handler ) http.Handler {
2366 return func (next http.Handler ) http.Handler {
2467 return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
25- if r .URL .Path == healthEndpoint || r .URL .Path == oauthProtectedResourceEndpoint {
68+ if r .URL .Path == healthEndpoint || slices . Contains ( WellKnownEndpoints , r .URL .EscapedPath ()) {
2669 next .ServeHTTP (w , r )
2770 return
2871 }
29- if ! requireOAuth {
72+ if ! staticConfig . RequireOAuth {
3073 next .ServeHTTP (w , r )
3174 return
3275 }
3376
34- audience := Audience
35- if serverURL != "" {
36- audience = serverURL
77+ wwwAuthenticateHeader := "Bearer realm= \" Kubernetes MCP Server \" "
78+ if staticConfig . OAuthAudience != "" {
79+ wwwAuthenticateHeader += fmt . Sprintf ( `, audience="%s"` , staticConfig . OAuthAudience )
3780 }
3881
3982 authHeader := r .Header .Get ("Authorization" )
4083 if authHeader == "" || ! strings .HasPrefix (authHeader , "Bearer " ) {
4184 klog .V (1 ).Infof ("Authentication failed - missing or invalid bearer token: %s %s from %s" , r .Method , r .URL .Path , r .RemoteAddr )
4285
43- if serverURL == "" {
44- w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience="%s", error="missing_token"` , audience ))
45- } else {
46- w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience="%s"", resource_metadata="%s%s", error="missing_token"` , audience , serverURL , oauthProtectedResourceEndpoint ))
47- }
86+ w .Header ().Set ("WWW-Authenticate" , wwwAuthenticateHeader + ", error=\" missing_token\" " )
4887 http .Error (w , "Unauthorized: Bearer token required" , http .StatusUnauthorized )
4988 return
5089 }
5190
5291 token := strings .TrimPrefix (authHeader , "Bearer " )
5392
54- // Validate the token offline for simple sanity check
55- // Because missing expected audience and expired tokens must be
56- // rejected already.
5793 claims , err := ParseJWTClaims (token )
58- if err == nil && claims != nil {
59- err = claims .Validate (r .Context (), audience , oidcProvider )
94+ if err == nil && claims == nil {
95+ // Impossible case, but just in case
96+ err = fmt .Errorf ("failed to parse JWT claims from token" )
6097 }
61- if err != nil {
62- klog .V (1 ).Infof ("Authentication failed - JWT validation error: %s %s from %s, error: %v" , r .Method , r .URL .Path , r .RemoteAddr , err )
63-
64- if serverURL == "" {
65- w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience="%s", error="invalid_token"` , audience ))
66- } else {
67- w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience="%s"", resource_metadata="%s%s", error="invalid_token"` , audience , serverURL , oauthProtectedResourceEndpoint ))
98+ // Offline validation
99+ if err == nil {
100+ err = claims .ValidateOffline (staticConfig .OAuthAudience )
101+ }
102+ // Online OIDC provider validation
103+ if err == nil {
104+ err = claims .ValidateWithProvider (r .Context (), staticConfig .OAuthAudience , oidcProvider )
105+ }
106+ // Scopes propagation, they are likely to be used for authorization.
107+ if err == nil {
108+ scopes := claims .GetScopes ()
109+ klog .V (2 ).Infof ("JWT token validated - Scopes: %v" , scopes )
110+ r = r .WithContext (context .WithValue (r .Context (), mcp .TokenScopesContextKey , scopes ))
111+ }
112+ // Token exchange with OIDC provider
113+ sts := NewFromConfig (staticConfig , oidcProvider )
114+ // TODO: Maybe the token had already been exchanged, if it has the right audience and scopes, we can skip this step.
115+ if err == nil && sts .IsEnabled () {
116+ var exchangedToken * oauth2.Token
117+ // If the token is valid, we can exchange it for a new token with the specified audience and scopes.
118+ exchangedToken , err = sts .ExternalAccountTokenExchange (r .Context (), & oauth2.Token {
119+ AccessToken : claims .Token ,
120+ TokenType : "Bearer" ,
121+ })
122+ if err == nil {
123+ // Replace the original token with the exchanged token
124+ token = exchangedToken .AccessToken
125+ claims , err = ParseJWTClaims (token )
126+ r .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , token )) // TODO: Implement test to verify, THIS IS A CRITICAL PART
68127 }
69- http .Error (w , "Unauthorized: Invalid token" , http .StatusUnauthorized )
70- return
71128 }
72-
73- // Scopes are likely to be used for authorization.
74- scopes := claims .GetScopes ()
75- klog .V (2 ).Infof ("JWT token validated - Scopes: %v" , scopes )
76- r = r .WithContext (context .WithValue (r .Context (), mcp .TokenScopesContextKey , scopes ))
77-
78- // Now, there are a couple of options:
79- // 1. If there is no authorization url configured for this MCP Server,
80- // that means this token will be used against the Kubernetes API Server.
81- // So that we need to validate the token using Kubernetes TokenReview API beforehand.
82- // 2. If there is an authorization url configured for this MCP Server,
83- // that means up to this point, the token is validated against the OIDC Provider already.
84- // 2. a. If this is the only token in the headers, this validated token
85- // is supposed to be used against the Kubernetes API Server as well. Therefore,
86- // TokenReview request must succeed.
87- // 2. b. If this is not the only token in the headers, the token in here is used
88- // only for authentication and authorization. Therefore, we need to send TokenReview request
89- // with the other token in the headers (TODO: still need to validate aud and exp of this token separately).
90- _ , _ , err = mcpServer .VerifyTokenAPIServer (r .Context (), token , audience )
129+ // Kubernetes API Server TokenReview validation
130+ if err == nil && staticConfig .ValidateToken {
131+ err = claims .ValidateWithKubernetesApi (r .Context (), staticConfig .OAuthAudience , verifier )
132+ }
91133 if err != nil {
92- klog .V (1 ).Infof ("Authentication failed - API Server token validation error: %s %s from %s, error: %v" , r .Method , r .URL .Path , r .RemoteAddr , err )
134+ klog .V (1 ).Infof ("Authentication failed - JWT validation error: %s %s from %s, error: %v" , r .Method , r .URL .Path , r .RemoteAddr , err )
93135
94- if serverURL == "" {
95- w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience="%s", error="invalid_token"` , audience ))
96- } else {
97- w .Header ().Set ("WWW-Authenticate" , fmt .Sprintf (`Bearer realm="Kubernetes MCP Server", audience="%s"", resource_metadata="%s%s", error="invalid_token"` , audience , serverURL , oauthProtectedResourceEndpoint ))
98- }
136+ w .Header ().Set ("WWW-Authenticate" , wwwAuthenticateHeader + ", error=\" invalid_token\" " )
99137 http .Error (w , "Unauthorized: Invalid token" , http .StatusUnauthorized )
100138 return
101139 }
@@ -134,16 +172,24 @@ func (c *JWTClaims) GetScopes() []string {
134172 return strings .Fields (c .Scope )
135173}
136174
137- // Validate Checks if the JWT claims are valid and if the audience matches the expected one.
138- func (c * JWTClaims ) Validate (ctx context.Context , audience string , provider * oidc.Provider ) error {
139- if err := c .Claims .Validate (jwt.Expected {AnyAudience : jwt.Audience {audience }}); err != nil {
175+ // ValidateOffline Checks if the JWT claims are valid and if the audience matches the expected one.
176+ func (c * JWTClaims ) ValidateOffline (audience string ) error {
177+ expected := jwt.Expected {}
178+ if audience != "" {
179+ expected .AnyAudience = jwt.Audience {audience }
180+ }
181+ if err := c .Validate (expected ); err != nil {
140182 return fmt .Errorf ("JWT token validation error: %v" , err )
141183 }
184+ return nil
185+ }
186+
187+ // ValidateWithProvider validates the JWT claims against the OIDC provider.
188+ func (c * JWTClaims ) ValidateWithProvider (ctx context.Context , audience string , provider * oidc.Provider ) error {
142189 if provider != nil {
143190 verifier := provider .Verifier (& oidc.Config {
144191 ClientID : audience ,
145192 })
146-
147193 _ , err := verifier .Verify (ctx , c .Token )
148194 if err != nil {
149195 return fmt .Errorf ("OIDC token validation error: %v" , err )
@@ -152,6 +198,16 @@ func (c *JWTClaims) Validate(ctx context.Context, audience string, provider *oid
152198 return nil
153199}
154200
201+ func (c * JWTClaims ) ValidateWithKubernetesApi (ctx context.Context , audience string , verifier KubernetesApiTokenVerifier ) error {
202+ if verifier != nil {
203+ _ , _ , err := verifier .KubernetesApiVerifyToken (ctx , c .Token , audience )
204+ if err != nil {
205+ return fmt .Errorf ("kubernetes API token validation error: %v" , err )
206+ }
207+ }
208+ return nil
209+ }
210+
155211func ParseJWTClaims (token string ) (* JWTClaims , error ) {
156212 tkn , err := jwt .ParseSigned (token , allSignatureAlgorithms )
157213 if err != nil {
0 commit comments