Skip to content

Commit f6678f8

Browse files
authored
feat(auth): add generic authService type for MCP (#2619)
Add new generic `authService` type as part of the MCP auth implementation.
1 parent 9ebd93a commit f6678f8

12 files changed

Lines changed: 1001 additions & 44 deletions

File tree

cmd/internal/config.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
"github.com/goccy/go-yaml"
2929
"github.com/google/go-cmp/cmp"
30+
"github.com/googleapis/genai-toolbox/internal/auth/generic"
3031
"github.com/googleapis/genai-toolbox/internal/server"
3132
)
3233

@@ -309,6 +310,18 @@ func mergeConfigs(files ...Config) (Config, error) {
309310
return Config{}, fmt.Errorf("resource conflicts detected:\n - %s\n\nPlease ensure each source, authService, tool, toolset and prompt has a unique name across all files", strings.Join(conflicts, "\n - "))
310311
}
311312

313+
// Ensure only one authService has mcpEnabled = true
314+
var mcpEnabledAuthServers []string
315+
for name, authService := range merged.AuthServices {
316+
// Only generic type has McpEnabled right now
317+
if genericService, ok := authService.(generic.Config); ok && genericService.McpEnabled {
318+
mcpEnabledAuthServers = append(mcpEnabledAuthServers, name)
319+
}
320+
}
321+
if len(mcpEnabledAuthServers) > 1 {
322+
return Config{}, fmt.Errorf("multiple authServices with mcpEnabled=true detected: %s. Only one MCP authorization server is currently supported", strings.Join(mcpEnabledAuthServers, ", "))
323+
}
324+
312325
return merged, nil
313326
}
314327

cmd/internal/config_test.go

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"testing"
2121

2222
"github.com/google/go-cmp/cmp"
23+
"github.com/googleapis/genai-toolbox/internal/auth/generic"
2324
"github.com/googleapis/genai-toolbox/internal/auth/google"
2425
"github.com/googleapis/genai-toolbox/internal/embeddingmodels/gemini"
2526
"github.com/googleapis/genai-toolbox/internal/prebuiltconfigs"
@@ -616,38 +617,48 @@ func TestParseConfig(t *testing.T) {
616617
type: google
617618
clientId: testing-id
618619
---
619-
kind: embeddingModel
620-
name: gemini-model
621-
type: gemini
622-
model: gemini-embedding-001
623-
apiKey: some-key
624-
dimension: 768
620+
kind: authService
621+
name: my-generic-auth
622+
type: generic
623+
audience: testings
624+
authorizationServer: https://testings
625+
mcpEnabled: true
626+
scopesRequired:
627+
- read:files
628+
- write:files
625629
---
626-
kind: tool
627-
name: example_tool
628-
type: postgres-sql
629-
source: my-pg-instance
630-
description: some description
631-
statement: |
632-
SELECT * FROM SQL_STATEMENT;
633-
parameters:
634-
- name: country
635-
type: string
636-
description: some description
630+
kind: embeddingModel
631+
name: gemini-model
632+
type: gemini
633+
model: gemini-embedding-001
634+
apiKey: some-key
635+
dimension: 768
637636
---
638-
kind: toolset
639-
name: example_toolset
640-
tools:
641-
- example_tool
637+
kind: tool
638+
name: example_tool
639+
type: postgres-sql
640+
source: my-pg-instance
641+
description: some description
642+
statement: |
643+
SELECT * FROM SQL_STATEMENT;
644+
parameters:
645+
- name: country
646+
type: string
647+
description: some description
642648
---
643-
kind: prompt
644-
name: code_review
645-
description: ask llm to analyze code quality
646-
messages:
647-
- content: "please review the following code for quality: {{.code}}"
648-
arguments:
649-
- name: code
650-
description: the code to review
649+
kind: toolset
650+
name: example_toolset
651+
tools:
652+
- example_tool
653+
---
654+
kind: prompt
655+
name: code_review
656+
description: ask llm to analyze code quality
657+
messages:
658+
- content: "please review the following code for quality: {{.code}}"
659+
arguments:
660+
- name: code
661+
description: the code to review
651662
`,
652663
wantConfig: Config{
653664
Sources: server.SourceConfigs{
@@ -669,6 +680,14 @@ func TestParseConfig(t *testing.T) {
669680
Type: google.AuthServiceType,
670681
ClientID: "testing-id",
671682
},
683+
"my-generic-auth": generic.Config{
684+
Name: "my-generic-auth",
685+
Type: generic.AuthServiceType,
686+
Audience: "testings",
687+
McpEnabled: true,
688+
AuthorizationServer: "https://testings",
689+
ScopesRequired: []string{"read:files", "write:files"},
690+
},
672691
},
673692
EmbeddingModels: server.EmbeddingModelConfigs{
674693
"gemini-model": gemini.Config{
@@ -2029,12 +2048,19 @@ func TestMergeConfigs(t *testing.T) {
20292048
Sources: server.SourceConfigs{"source1": httpsrc.Config{Name: "source1"}},
20302049
Tools: server.ToolConfigs{"tool2": http.Config{Name: "tool2"}},
20312050
}
2051+
fileMcp1 := Config{
2052+
AuthServices: server.AuthServiceConfigs{"generic1": generic.Config{Name: "generic1", McpEnabled: true}},
2053+
}
2054+
fileMcp2 := Config{
2055+
AuthServices: server.AuthServiceConfigs{"generic2": generic.Config{Name: "generic2", McpEnabled: true}},
2056+
}
20322057

20332058
testCases := []struct {
2034-
name string
2035-
files []Config
2036-
want Config
2037-
wantErr bool
2059+
name string
2060+
files []Config
2061+
want Config
2062+
wantErr bool
2063+
errString string
20382064
}{
20392065
{
20402066
name: "merge two distinct files",
@@ -2054,6 +2080,12 @@ func TestMergeConfigs(t *testing.T) {
20542080
files: []Config{file1, file2, fileWithConflicts},
20552081
wantErr: true,
20562082
},
2083+
{
2084+
name: "merge multiple mcp enabled generic",
2085+
files: []Config{fileMcp1, fileMcp2},
2086+
wantErr: true,
2087+
errString: "multiple authServices with mcpEnabled=true detected",
2088+
},
20572089
{
20582090
name: "merge single file",
20592091
files: []Config{file1},
@@ -2094,7 +2126,9 @@ func TestMergeConfigs(t *testing.T) {
20942126
if err == nil {
20952127
t.Fatal("expected an error for conflicting files but got none")
20962128
}
2097-
if !strings.Contains(err.Error(), "resource conflicts detected") {
2129+
if tc.errString != "" && !strings.Contains(err.Error(), tc.errString) {
2130+
t.Errorf("expected error %q, but got: %v", tc.errString, err)
2131+
} else if tc.errString == "" && !strings.Contains(err.Error(), "resource conflicts detected") {
20982132
t.Errorf("expected conflict error, but got: %v", err)
20992133
}
21002134
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
title: "Generic OIDC Auth"
3+
type: docs
4+
weight: 2
5+
description: >
6+
Use a Generic OpenID Connect (OIDC) provider for OAuth 2.0 flow and token
7+
lifecycle.
8+
---
9+
10+
## Getting Started
11+
12+
The Generic Auth Service allows you to integrate with any OpenID Connect (OIDC)
13+
compliant identity provider (IDP). It discovers the JWKS (JSON Web Key Set) URL
14+
either through the provider's `/.well-known/openid-configuration` endpoint or
15+
directly via the provided `authorizationServer`.
16+
17+
To configure this auth service, you need to provide the `audience` (typically
18+
your client ID or the intended audience for the token), the
19+
`authorizationServer` of your identity provider, and optionally a list of
20+
`scopesRequired` that must be present in the token's claims.
21+
22+
## Behavior
23+
24+
### Token Validation
25+
26+
When a request is received, the service will:
27+
28+
1. Extract the token from the `<name>_token` header (e.g.,
29+
`my-generic-auth_token`).
30+
2. Fetch the JWKS from the configured `authorizationServer` (caching it in the
31+
background) to verify the token's signature.
32+
3. Validate that the token is not expired and its signature is valid.
33+
4. Verify that the `aud` (audience) claim matches the configured `audience`.
34+
claim contains all required scopes.
35+
5. Return the validated claims to be used for [Authenticated
36+
Parameters][auth-params] or [Authorized Invocations][auth-invoke].
37+
38+
[auth-invoke]: ../tools/#authorized-invocations
39+
[auth-params]: ../tools/#authenticated-parameters
40+
41+
## Example
42+
43+
```yaml
44+
kind: authServices
45+
name: my-generic-auth
46+
type: generic
47+
audience: ${YOUR_OIDC_AUDIENCE}
48+
authorizationServer: https://your-idp.example.com
49+
mcpEnabled: false
50+
scopesRequired:
51+
- read
52+
- write
53+
```
54+
55+
{{< notice tip >}} Use environment variable replacement with the format
56+
${ENV_NAME} instead of hardcoding your secrets into the configuration file.
57+
{{< /notice >}}
58+
59+
## Reference
60+
61+
| **field** | **type** | **required** | **description** |
62+
| ------------------- | :------: | :----------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
63+
| type | string | true | Must be "generic". |
64+
| audience | string | true | The expected audience (`aud` claim) in the JWT token. This ensures the token was minted specifically for your application. |
65+
| 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. |
66+
| mcpEnabled | bool | false | Indicates if MCP endpoint authentication should be applied. Defaults to false. |
67+
| scopesRequired | []string | false | A list of required scopes that must be present in the token's `scope` claim to be considered valid. |

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ require (
1919
github.com/ClickHouse/clickhouse-go/v2 v2.43.0
2020
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0
2121
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.31.0
22+
github.com/MicahParks/jwkset v0.11.0
23+
github.com/MicahParks/keyfunc/v3 v3.8.0
2224
github.com/apache/cassandra-gocql-driver/v2 v2.0.0
2325
github.com/cenkalti/backoff/v5 v5.0.3
2426
github.com/cockroachdb/cockroach-go/v2 v2.4.3
@@ -36,6 +38,7 @@ require (
3638
github.com/go-sql-driver/mysql v1.9.3
3739
github.com/goccy/go-yaml v1.19.2
3840
github.com/godror/godror v0.50.0
41+
github.com/golang-jwt/jwt/v5 v5.3.1
3942
github.com/google/go-cmp v0.7.0
4043
github.com/google/uuid v1.6.0
4144
github.com/jackc/pgx/v5 v5.9.1
@@ -172,7 +175,6 @@ require (
172175
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
173176
github.com/godror/knownpb v0.3.0 // indirect
174177
github.com/gofrs/flock v0.13.0 // indirect
175-
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
176178
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
177179
github.com/golang-sql/sqlexp v0.1.0 // indirect
178180
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0
8989
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
9090
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
9191
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
92+
github.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOhoyQ=
93+
github.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=
94+
github.com/MicahParks/keyfunc/v3 v3.8.0 h1:Hx2dgIjAXGk9slakM6rV9BOeaWDPEXXZ4Us8guNBfds=
95+
github.com/MicahParks/keyfunc/v3 v3.8.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=
9296
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
9397
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
9498
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=

0 commit comments

Comments
 (0)