Skip to content

Commit dfd66ee

Browse files
authored
fix(auth): separate Google and Generic MCP OAuth verification (#3341)
- enforce "active" and "issuer" field checks in generic OAuth - support MCP auth separately in "google" type authService because Google OAuth tokeninfo endpoint returns non-standard token validation response.
1 parent ddfd887 commit dfd66ee

19 files changed

Lines changed: 957 additions & 120 deletions

File tree

docs/en/documentation/configuration/authentication/generic.md

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ When a request is received in this mode, the service will:
4848
#### Example
4949

5050
```yaml
51-
kind: authServices
51+
kind: authService
5252
name: my-generic-auth
5353
type: generic
5454
audience: ${YOUR_OIDC_AUDIENCE}
@@ -112,7 +112,7 @@ When a request is received in this mode, the service will:
112112
#### Example
113113

114114
```yaml
115-
kind: authServices
115+
kind: authService
116116
name: my-generic-auth
117117
type: generic
118118
audience: ${YOUR_TOKEN_AUDIENCE}
@@ -123,28 +123,19 @@ scopesRequired:
123123
- write
124124
```
125125

126-
#### Google Opaque Access Token Validation Example
126+
#### Google Authentication Note
127127

128-
To use Google's `tokeninfo` endpoint for validating opaque access tokens, configure the service to use the `GET` method and `access_token` parameter name:
129-
130-
```yaml
131-
kind: authServices
132-
name: google-auth
133-
type: generic
134-
audience: "YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com"
135-
authorizationServer: https://accounts.google.com
136-
introspectionEndpoint: https://www.googleapis.com/oauth2/v3/tokeninfo
137-
introspectionMethod: GET
138-
introspectionParamName: access_token
139-
mcpEnabled: true
140-
```
128+
> [!WARNING]
129+
> Do not configure Google's tokeninfo endpoint (`https://oauth2.googleapis.com/tokeninfo`) using `type: generic`. Because the generic OIDC service strictly enforces the presence and validity of the `active` claim (RFC 7662), and Google's tokeninfo endpoint does not return this claim, validation will fail.
130+
>
131+
> To authenticate with Google tokens, use the native [Google Sign-In](./google.md) auth service (`type: google`) instead, which natively handles Google's endpoints and token formats.
141132

142133
#### Okta OIDC Configuration Example
143134

144135
To secure your MCP server or tools using Okta as the identity provider:
145136

146137
```yaml
147-
kind: authServices
138+
kind: authService
148139
name: okta-auth
149140
type: generic
150141
audience: api://default # Or your custom Okta audience
@@ -197,7 +188,7 @@ ${ENV_NAME} instead of hardcoding your secrets into the configuration file.
197188
| audience | string | true | The expected audience (`aud` claim) in the token. This ensures the token was minted specifically for your application. See [Getting Started](#getting-started) for details on OIDC audience matching. |
198189
| authorizationServer | string | true | The base URL of your OIDC provider. The service will append `/.well-known/openid-configuration` to discover the JWKS URI. HTTP is allowed but logs a warning. |
199190
| mcpEnabled | bool | false | Indicates if MCP endpoint authentication should be applied. Defaults to false. |
200-
| scopesRequired | []string | false | A list of required scopes that must be present in the token's `scope` claim to be considered valid. |
201-
| introspectionEndpoint | string | false | Optional override for the token introspection URL. Useful if the provider does not list it in OIDC discovery (e.g., Google). |
202-
| introspectionMethod | string | false | HTTP method to use for introspection. Defaults to "POST". Set to "GET" for providers like Google. |
203-
| introspectionParamName | string | false | Parameter name for the token in the introspection request. Defaults to "token". Set to "access_token" for Google. |
191+
| scopesRequired | []string | false | A list of required scopes that must be present in the token's `scope` claim to be considered valid. Disallowed if `mcpEnabled` is false. |
192+
| introspectionEndpoint | string | false | Optional override for the token introspection URL. Useful if the provider does not list it in OIDC discovery (e.g., Google). Disallowed if `mcpEnabled` is false. |
193+
| introspectionMethod | string | false | HTTP method to use for introspection. Defaults to "POST". Set to "GET" for providers like Google. Disallowed if `mcpEnabled` is false. |
194+
| introspectionParamName | string | false | Parameter name for the token in the introspection request. Defaults to "token". Set to "access_token" for Google. Disallowed if `mcpEnabled` is false. |

docs/en/documentation/configuration/authentication/google.md

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,57 +3,82 @@ title: "Google Sign-In"
33
type: docs
44
weight: 1
55
description: >
6-
Use Google Sign-In for Oauth 2.0 flow and token lifecycle.
6+
Use Google Sign-In for OAuth 2.0 flow and token lifecycle.
77
---
88

99
## Getting Started
1010

11-
Google Sign-In manages the OAuth 2.0 flow and token lifecycle. To integrate the
12-
Google Sign-In workflow to your web app [follow this guide][gsi-setup].
11+
Google Sign-In manages the OAuth 2.0 flow and token lifecycle. To integrate the Google Sign-In workflow to your web app, [follow this guide][gsi-setup].
1312

14-
After setting up the Google Sign-In workflow, you should have registered your
15-
application and retrieved a [Client ID][client-id]. Configure your auth service
16-
in with the `Client ID`.
13+
After setting up Google Sign-In, configure your auth service in the toolbox. The Google auth provider supports two distinct validation modes:
1714

18-
[gsi-setup]: https://developers.google.com/identity/sign-in/web/sign-in
19-
[client-id]: https://developers.google.com/identity/sign-in/web/sign-in#create_authorization_credentials
15+
1. **Web App Claims (OIDC)**: Used to authenticate user requests in web applications.
16+
2. **MCP Authorization**: Used to secure the entire MCP server transport (SSE or Stdio).
2017

21-
## Behavior
18+
[gsi-setup]: https://developers.google.com/identity/sign-in/web/sign-in
2219

23-
### Authorized Invocations
20+
## Configuration Modes
2421

25-
When using [Authorized Invocations][auth-invoke], a tool will be
26-
considered authorized if it has a valid Oauth 2.0 token that matches the Client
27-
ID.
22+
### 1. Web App OIDC Authentication
23+
If you are developing a web application using the Toolbox and need to retrieve user claims from Google ID tokens sent in custom request headers, configure the `clientId` field.
2824

29-
[auth-invoke]: ../tools/_index.md#authorized-invocations
25+
- **Header**: Expects the token in the `<name>_token` header (e.g. `my-google-auth_token`).
26+
- **Token Type**: Google OIDC ID tokens (JWT).
3027

31-
### Authenticated Parameters
28+
#### Example
29+
```yaml
30+
kind: authService
31+
name: my-google-auth
32+
type: google
33+
clientId: ${YOUR_GOOGLE_CLIENT_ID}
34+
```
3235
33-
When using [Authenticated Parameters][auth-params], any [claim provided by the
34-
id-token][provided-claims] can be used for the parameter.
36+
---
3537
36-
[auth-params]: ../tools/_index.md#authenticated-parameters
37-
[provided-claims]:
38-
https://developers.google.com/identity/openid-connect/openid-connect#obtaininguserprofileinformation
38+
### 2. MCP Authorization
39+
To secure all endpoints on your MCP server using Google OAuth tokens, enable `mcpEnabled` and specify the `audience` field.
3940

40-
## Example
41+
- **Header**: Expects the token in the standard `Authorization: Bearer <token>` header.
42+
- **Token Type**: Supports both Google **ID tokens (JWT)** and Google **opaque access tokens**.
4143

44+
#### Example
4245
```yaml
4346
kind: authService
4447
name: my-google-auth
4548
type: google
46-
clientId: ${YOUR_GOOGLE_CLIENT_ID}
49+
audience: ${YOUR_GOOGLE_CLIENT_ID}
50+
mcpEnabled: true
51+
scopesRequired:
52+
- https://www.googleapis.com/auth/userinfo.email
4753
```
4854

49-
{{< notice tip >}}
50-
Use environment variable replacement with the format ${ENV_NAME}
51-
instead of hardcoding your secrets into the configuration file.
52-
{{< /notice >}}
55+
> [!IMPORTANT]
56+
> - For **ID tokens (JWT)**: Local cryptographic signature verification is performed, which requires `audience` to be configured. If `audience` is not set, the provider will fall back to using `clientId`. If neither is configured, validation will fail.
57+
> - For **Opaque tokens**: The provider automatically queries Google's secure tokeninfo endpoint (`https://oauth2.googleapis.com/tokeninfo`) and validates the resulting audience against the configured `audience` field (falling back to `clientId` if `audience` is not set).
58+
59+
---
60+
61+
## Behavior
62+
63+
### Authorized Invocations
64+
When using [Authorized Invocations][auth-invoke], a tool will be considered authorized if it has a valid OAuth 2.0 token that matches the Client ID or Audience.
65+
66+
[auth-invoke]: ../tools/_index.md#authorized-invocations
67+
68+
### Authenticated Parameters
69+
When using [Authenticated Parameters][auth-params], any [claim provided by the id-token][provided-claims] can be used for the parameter.
70+
71+
[auth-params]: ../tools/_index.md#authenticated-parameters
72+
[provided-claims]: https://developers.google.com/identity/openid-connect/openid-connect#obtaininguserprofileinformation
73+
74+
---
5375

5476
## Reference
5577

56-
| **field** | **type** | **required** | **description** |
57-
|-----------|:--------:|:------------:|------------------------------------------------------------------|
58-
| type | string | true | Must be "google". |
59-
| clientId | string | true | Client ID of your application from registering your application. |
78+
| **field** | **type** | **required** | **description** |
79+
|----------------|:--------:|:------------:|----------------------------------------------------------------------------------------------------------------------------------------------|
80+
| type | string | true | Must be "google". |
81+
| clientId | string | false | Client ID of your application. Required for validating ID tokens in non-MCP web apps (`GetClaimsFromHeader`), and acts as a fallback for `audience` in MCP auth mode if `audience` is not configured. |
82+
| audience | string | false | Expected audience. Required for validating ID tokens in MCP Auth mode (unless `clientId` is configured as a fallback). If specified, also validates opaque token audiences. Disallowed if `mcpEnabled` is false. |
83+
| mcpEnabled | bool | false | Enforces global MCP transport authentication using the `Authorization: Bearer` header. Defaults to false. |
84+
| scopesRequired | []string | false | A list of required scopes that must be present in the token's claims/metadata to be considered valid. Disallowed if `mcpEnabled` is false. |

internal/auth/auth.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,21 @@ type AuthService interface {
3232
GetClaimsFromHeader(context.Context, http.Header) (map[string]any, error)
3333
ToConfig() AuthServiceConfig
3434
}
35+
36+
// MCPAuthError represents an error during MCP authentication validation.
37+
type MCPAuthError struct {
38+
Code int
39+
Message string
40+
ScopesRequired []string
41+
}
42+
43+
func (e *MCPAuthError) Error() string { return e.Message }
44+
45+
// MCPAuthService is the interface for authentication services that support MCP auth.
46+
type MCPAuthService interface {
47+
AuthService
48+
IsMCPEnabled() bool
49+
GetScopesRequired() []string
50+
GetAuthorizationServer() string
51+
ValidateMCPAuth(context.Context, http.Header) (map[string]any, error)
52+
}

internal/auth/generic/generic.go

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@ func (cfg Config) AuthServiceConfigType() string {
5757

5858
// Initialize a generic auth service
5959
func (cfg Config) Initialize() (auth.AuthService, error) {
60+
if !cfg.McpEnabled {
61+
if cfg.IntrospectionEndpoint != "" {
62+
return nil, fmt.Errorf("`introspectionEndpoint` is not allowed when `mcpEnabled` is false")
63+
}
64+
if cfg.IntrospectionMethod != "" {
65+
return nil, fmt.Errorf("`introspectionMethod` is not allowed when `mcpEnabled` is false")
66+
}
67+
if cfg.IntrospectionParamName != "" {
68+
return nil, fmt.Errorf("`introspectionParamName` is not allowed when `mcpEnabled` is false")
69+
}
70+
if len(cfg.ScopesRequired) > 0 {
71+
return nil, fmt.Errorf("`scopesRequired` is not allowed when `mcpEnabled` is false")
72+
}
73+
}
6074
httpClient := newSecureHTTPClient()
6175

6276
// Discover OIDC endpoints
@@ -157,7 +171,7 @@ func discoverOIDCConfig(client *http.Client, AuthorizationServer string) (jwksUR
157171
return config.JwksUri, config.IntrospectionEndpoint, config.Issuer, nil
158172
}
159173

160-
var _ auth.AuthService = AuthService{}
174+
var _ auth.MCPAuthService = AuthService{}
161175

162176
// struct used to store auth service info
163177
type AuthService struct {
@@ -182,6 +196,18 @@ func (a AuthService) GetName() string {
182196
return a.Name
183197
}
184198

199+
func (a AuthService) IsMCPEnabled() bool {
200+
return a.McpEnabled
201+
}
202+
203+
func (a AuthService) GetScopesRequired() []string {
204+
return a.ScopesRequired
205+
}
206+
207+
func (a AuthService) GetAuthorizationServer() string {
208+
return a.AuthorizationServer
209+
}
210+
185211
// Verifies generic JWT access token inside the Authorization header
186212
func (a AuthService) GetClaimsFromHeader(ctx context.Context, h http.Header) (map[string]any, error) {
187213
if a.McpEnabled {
@@ -230,13 +256,7 @@ func (a AuthService) GetClaimsFromHeader(ctx context.Context, h http.Header) (ma
230256
}
231257

232258
// MCPAuthError represents an error during MCP authentication validation.
233-
type MCPAuthError struct {
234-
Code int
235-
Message string
236-
ScopesRequired []string
237-
}
238-
239-
func (e *MCPAuthError) Error() string { return e.Message }
259+
type MCPAuthError = auth.MCPAuthError
240260

241261
// ValidateMCPAuth handles MCP auth token validation
242262
func (a AuthService) ValidateMCPAuth(ctx context.Context, h http.Header) (map[string]any, error) {
@@ -386,7 +406,7 @@ func (a AuthService) validateOpaqueToken(ctx context.Context, tokenStr string) (
386406
return nil, fmt.Errorf("failed to parse introspection response: %w", err)
387407
}
388408

389-
if introspectResp.Active != nil && !*introspectResp.Active {
409+
if introspectResp.Active == nil || !*introspectResp.Active {
390410
logger.InfoContext(ctx, "token is not active")
391411
return nil, &MCPAuthError{Code: http.StatusUnauthorized, Message: "token is not active", ScopesRequired: a.ScopesRequired}
392412
}
@@ -476,7 +496,7 @@ func (a AuthService) validateClaims(ctx context.Context, iss string, aud []strin
476496

477497
// Check scopes
478498
if len(a.ScopesRequired) > 0 {
479-
tokenScopes := strings.Split(scopeStr, " ")
499+
tokenScopes := strings.Fields(scopeStr)
480500
scopeMap := make(map[string]bool)
481501
for _, s := range tokenScopes {
482502
scopeMap[s] = true

0 commit comments

Comments
 (0)