1
1
package http
2
2
3
3
import (
4
+ "context"
4
5
"encoding/base64"
5
6
"encoding/json"
6
7
"fmt"
7
8
"net/http"
8
- "slices"
9
9
"strings"
10
10
"time"
11
11
12
+ "github.com/coreos/go-oidc/v3/oidc"
12
13
"k8s.io/klog/v2"
13
14
14
15
"github.com/manusa/kubernetes-mcp-server/pkg/mcp"
@@ -31,37 +32,83 @@ func AuthorizationMiddleware(requireOAuth bool, serverURL string, mcpServer *mcp
31
32
return
32
33
}
33
34
35
+ audience := Audience
36
+ if serverURL != "" {
37
+ audience = serverURL
38
+ }
39
+
34
40
authHeader := r .Header .Get ("Authorization" )
35
41
if authHeader == "" || ! strings .HasPrefix (authHeader , "Bearer " ) {
36
42
klog .V (1 ).Infof ("Authentication failed - missing or invalid bearer token: %s %s from %s" , r .Method , r .URL .Path , r .RemoteAddr )
37
43
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
+ }
39
49
http .Error (w , "Unauthorized: Bearer token required" , http .StatusUnauthorized )
40
50
return
41
51
}
42
52
43
53
token := strings .TrimPrefix (authHeader , "Bearer " )
44
54
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 )
51
59
if err != nil {
52
60
klog .V (1 ).Infof ("Authentication failed - JWT validation error: %s %s from %s, error: %v" , r .Method , r .URL .Path , r .RemoteAddr , err )
53
61
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
+ }
55
67
http .Error (w , "Unauthorized: Invalid token" , http .StatusUnauthorized )
56
68
return
57
69
}
58
70
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 )
61
104
if err != nil {
62
105
klog .V (1 ).Infof ("Authentication failed - token validation error: %s %s from %s, error: %v" , r .Method , r .URL .Path , r .RemoteAddr , err )
63
106
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
+ }
65
112
http .Error (w , "Unauthorized: Invalid token" , http .StatusUnauthorized )
66
113
return
67
114
}
@@ -72,32 +119,60 @@ func AuthorizationMiddleware(requireOAuth bool, serverURL string, mcpServer *mcp
72
119
}
73
120
74
121
type 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 )
78
133
}
79
134
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 ) {
82
157
parts := strings .Split (token , "." )
83
158
if len (parts ) != 3 {
84
- return fmt .Errorf ("invalid JWT token format" )
159
+ return nil , fmt .Errorf ("invalid JWT token format" )
85
160
}
86
161
87
162
claims , err := parseJWTClaims (parts [1 ])
88
163
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 )
90
165
}
91
166
92
167
if claims .ExpiresAt > 0 && time .Now ().Unix () > claims .ExpiresAt {
93
- return fmt .Errorf ("token expired" )
168
+ return nil , fmt .Errorf ("token expired" )
94
169
}
95
170
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 )
98
173
}
99
174
100
- return nil
175
+ return claims , nil
101
176
}
102
177
103
178
func parseJWTClaims (payload string ) (* JWTClaims , error ) {
@@ -118,3 +193,16 @@ func parseJWTClaims(payload string) (*JWTClaims, error) {
118
193
119
194
return & claims , nil
120
195
}
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