Skip to content

Commit 4251c45

Browse files
committed
Implement OIDC discovery and add Google OAuth demo guide (#9562)
1 parent bdcd205 commit 4251c45

8 files changed

Lines changed: 250 additions & 50 deletions

File tree

ai/mcp/server/knowledge-base/Server.mjs

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -105,33 +105,59 @@ class Server extends Base {
105105
const mcpServerUrl = getFullUrl(process.env.HOST || 'localhost', aiConfig.ssePort);
106106

107107
// Optional OIDC/OAuth Authorization
108-
if (aiConfig.auth.host) {
108+
if (aiConfig.auth.host || aiConfig.auth.issuerUrl) {
109109
const {mcpAuthMetadataRouter, getOAuthProtectedResourceMetadataUrl} = await import('@modelcontextprotocol/sdk/server/auth/router.js');
110110
const {requireBearerAuth} = await import('@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js');
111111
const {InvalidTokenError} = await import('@modelcontextprotocol/sdk/server/auth/errors.js');
112112
const {checkResourceAllowed} = await import('@modelcontextprotocol/sdk/shared/auth-utils.js');
113113

114-
const authBaseUrl = getFullUrl(aiConfig.auth.host, aiConfig.auth.port);
114+
let oauthUrls;
115115

116-
// Append Keycloak realm path if not already present
117-
if (!authBaseUrl.pathname.includes('/realms/')) {
118-
authBaseUrl.pathname = `/realms/${aiConfig.auth.realm}/`;
119-
}
116+
if (aiConfig.auth.issuerUrl) {
117+
let issuerUrl = aiConfig.auth.issuerUrl;
120118

121-
const oauthUrls = {
122-
issuer : authBaseUrl.toString(),
123-
introspection_endpoint: new URL('protocol/openid-connect/token/introspect', authBaseUrl).toString(),
124-
authorization_endpoint: new URL('protocol/openid-connect/auth', authBaseUrl).toString(),
125-
token_endpoint : new URL('protocol/openid-connect/token', authBaseUrl).toString(),
126-
};
119+
if (!issuerUrl.endsWith('/')) {
120+
issuerUrl += '/';
121+
}
122+
123+
const discoveryUrl = new URL('.well-known/openid-configuration', issuerUrl);
124+
const response = await fetch(discoveryUrl);
125+
126+
if (!response.ok) {
127+
throw new Error(`Failed to fetch OIDC discovery document from ${discoveryUrl}: ${response.statusText}`);
128+
}
129+
130+
oauthUrls = await response.json();
131+
logger.info(`[neo-knowledge-base MCP] OIDC Discovery successful for issuer: ${oauthUrls.issuer}`);
132+
} else {
133+
const authBaseUrl = getFullUrl(aiConfig.auth.host, aiConfig.auth.port);
134+
135+
// Append Keycloak realm path if not already present
136+
if (!authBaseUrl.pathname.includes('/realms/')) {
137+
authBaseUrl.pathname = `/realms/${aiConfig.auth.realm}/`;
138+
}
139+
140+
oauthUrls = {
141+
issuer : authBaseUrl.toString(),
142+
introspection_endpoint: new URL('protocol/openid-connect/token/introspect', authBaseUrl).toString(),
143+
authorization_endpoint: new URL('protocol/openid-connect/auth', authBaseUrl).toString(),
144+
token_endpoint : new URL('protocol/openid-connect/token', authBaseUrl).toString(),
145+
};
146+
}
127147

128148
const oauthMetadata = {
129149
...oauthUrls,
130-
response_types_supported: ['code'],
150+
response_types_supported: oauthUrls.response_types_supported || ['code'],
131151
};
132152

133153
const tokenVerifier = {
134154
verifyAccessToken: async (token) => {
155+
const introspectionEndpoint = oauthUrls.introspection_endpoint;
156+
157+
if (!introspectionEndpoint) {
158+
throw new Error('No introspection endpoint available in OIDC metadata');
159+
}
160+
135161
const params = new URLSearchParams({
136162
token : token,
137163
client_id: aiConfig.auth.clientId,
@@ -141,7 +167,7 @@ class Server extends Base {
141167
params.set('client_secret', aiConfig.auth.clientSecret);
142168
}
143169

144-
const response = await fetch(oauthUrls.introspection_endpoint, {
170+
const response = await fetch(introspectionEndpoint, {
145171
method : 'POST',
146172
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
147173
body : params.toString(),

ai/mcp/server/knowledge-base/config.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const defaultConfig = {
1313
* Automatically synchronize the knowledge base on startup.
1414
* @type {boolean}
1515
*/
16-
autoSync: true,
16+
autoSync: process.env.AUTO_SYNC !== 'false',
1717
/**
1818
* Global debug flag for all MCP servers.
1919
* @type {boolean}
@@ -43,6 +43,7 @@ const defaultConfig = {
4343
host : process.env.AUTH_HOST || null,
4444
port : Number(process.env.AUTH_PORT) || 8080,
4545
realm : process.env.AUTH_REALM || 'master',
46+
issuerUrl : process.env.AUTH_ISSUER_URL || null,
4647
clientId : process.env.OAUTH_CLIENT_ID || null,
4748
clientSecret: process.env.OAUTH_CLIENT_SECRET || '',
4849
},

ai/mcp/server/memory-core/Server.mjs

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -104,33 +104,59 @@ class Server extends Base {
104104
const mcpServerUrl = getFullUrl(process.env.HOST || 'localhost', aiConfig.ssePort);
105105

106106
// Optional OIDC/OAuth Authorization
107-
if (aiConfig.auth.host) {
107+
if (aiConfig.auth.host || aiConfig.auth.issuerUrl) {
108108
const {mcpAuthMetadataRouter, getOAuthProtectedResourceMetadataUrl} = await import('@modelcontextprotocol/sdk/server/auth/router.js');
109109
const {requireBearerAuth} = await import('@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js');
110110
const {InvalidTokenError} = await import('@modelcontextprotocol/sdk/server/auth/errors.js');
111111
const {checkResourceAllowed} = await import('@modelcontextprotocol/sdk/shared/auth-utils.js');
112112

113-
const authBaseUrl = getFullUrl(aiConfig.auth.host, aiConfig.auth.port);
113+
let oauthUrls;
114114

115-
// Append Keycloak realm path if not already present
116-
if (!authBaseUrl.pathname.includes('/realms/')) {
117-
authBaseUrl.pathname = `/realms/${aiConfig.auth.realm}/`;
118-
}
115+
if (aiConfig.auth.issuerUrl) {
116+
let issuerUrl = aiConfig.auth.issuerUrl;
119117

120-
const oauthUrls = {
121-
issuer : authBaseUrl.toString(),
122-
introspection_endpoint: new URL('protocol/openid-connect/token/introspect', authBaseUrl).toString(),
123-
authorization_endpoint: new URL('protocol/openid-connect/auth', authBaseUrl).toString(),
124-
token_endpoint : new URL('protocol/openid-connect/token', authBaseUrl).toString(),
125-
};
118+
if (!issuerUrl.endsWith('/')) {
119+
issuerUrl += '/';
120+
}
121+
122+
const discoveryUrl = new URL('.well-known/openid-configuration', issuerUrl);
123+
const response = await fetch(discoveryUrl);
124+
125+
if (!response.ok) {
126+
throw new Error(`Failed to fetch OIDC discovery document from ${discoveryUrl}: ${response.statusText}`);
127+
}
128+
129+
oauthUrls = await response.json();
130+
logger.info(`[neo-memory-core MCP] OIDC Discovery successful for issuer: ${oauthUrls.issuer}`);
131+
} else {
132+
const authBaseUrl = getFullUrl(aiConfig.auth.host, aiConfig.auth.port);
133+
134+
// Append Keycloak realm path if not already present
135+
if (!authBaseUrl.pathname.includes('/realms/')) {
136+
authBaseUrl.pathname = `/realms/${aiConfig.auth.realm}/`;
137+
}
138+
139+
oauthUrls = {
140+
issuer : authBaseUrl.toString(),
141+
introspection_endpoint: new URL('protocol/openid-connect/token/introspect', authBaseUrl).toString(),
142+
authorization_endpoint: new URL('protocol/openid-connect/auth', authBaseUrl).toString(),
143+
token_endpoint : new URL('protocol/openid-connect/token', authBaseUrl).toString(),
144+
};
145+
}
126146

127147
const oauthMetadata = {
128148
...oauthUrls,
129-
response_types_supported: ['code'],
149+
response_types_supported: oauthUrls.response_types_supported || ['code'],
130150
};
131151

132152
const tokenVerifier = {
133153
verifyAccessToken: async (token) => {
154+
const introspectionEndpoint = oauthUrls.introspection_endpoint;
155+
156+
if (!introspectionEndpoint) {
157+
throw new Error('No introspection endpoint available in OIDC metadata');
158+
}
159+
134160
const params = new URLSearchParams({
135161
token : token,
136162
client_id: aiConfig.auth.clientId,
@@ -140,7 +166,7 @@ class Server extends Base {
140166
params.set('client_secret', aiConfig.auth.clientSecret);
141167
}
142168

143-
const response = await fetch(oauthUrls.introspection_endpoint, {
169+
const response = await fetch(introspectionEndpoint, {
144170
method : 'POST',
145171
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
146172
body : params.toString(),

ai/mcp/server/memory-core/config.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const defaultConfig = {
1313
* Automatically trigger session summarization on startup.
1414
* @type {boolean}
1515
*/
16-
autoSummarize: true,
16+
autoSummarize: process.env.AUTO_SUMMARIZE !== 'false',
1717
/**
1818
* Global debug flag for all MCP servers.
1919
* @type {boolean}
@@ -43,6 +43,7 @@ const defaultConfig = {
4343
host : process.env.AUTH_HOST || null,
4444
port : Number(process.env.AUTH_PORT) || 8080,
4545
realm : process.env.AUTH_REALM || 'master',
46+
issuerUrl : process.env.AUTH_ISSUER_URL || null,
4647
clientId : process.env.OAUTH_CLIENT_ID || null,
4748
clientSecret: process.env.OAUTH_CLIENT_SECRET || '',
4849
},

learn/guides/mcp/Authorization.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ To enable authorization on your MCP server, you must use the **SSE transport** a
2222

2323
| Variable | Description | Example |
2424
| :--- | :--- | :--- |
25-
| `AUTH_HOST` | The hostname of your OIDC provider. | `keycloak.local` |
26-
| `AUTH_PORT` | The port of your OIDC provider. | `8080` |
27-
| `AUTH_REALM` | The OIDC realm name. | `master` |
25+
| `AUTH_ISSUER_URL` | The base URL of the OIDC provider (enables Discovery). | `https://accounts.google.com` |
26+
| `AUTH_HOST` | The hostname of your OIDC provider (Keycloak fallback). | `keycloak.local` |
27+
| `AUTH_PORT` | The port of your OIDC provider (Keycloak fallback). | `8080` |
28+
| `AUTH_REALM` | The OIDC realm name (Keycloak fallback). | `master` |
2829
| `OAUTH_CLIENT_ID` | The OAuth 2.1 client ID for the server. | `neo-mcp-server` |
2930
| `OAUTH_CLIENT_SECRET` | The OAuth 2.1 client secret. | `your-secret-here` |
3031
| `HOST` | The public hostname of the MCP server. | `mcp.neomjs.com` |
@@ -37,6 +38,13 @@ The server intelligently resolves URLs:
3738

3839
---
3940

41+
## Hands-on Examples
42+
43+
- **Keycloak:** See the [Local Keycloak Setup](#example-local-keycloak-setup) below.
44+
- **Google OAuth:** Follow the [Google OAuth 2.1 Demo](GoogleAuthDemo.md) guide.
45+
46+
---
47+
4048
## Example: Local Keycloak Setup
4149

4250
1. **Start Keycloak:** Ensure Keycloak is running at `localhost:8080`.

learn/guides/mcp/GoogleAuthDemo.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Google OAuth 2.1 Demo
2+
3+
This hands-on guide demonstrates how to secure your Neo.mjs MCP servers (Knowledge Base or Memory Core) using Google OAuth 2.1. This allows you to restrict access to your AI tools using your organization's Google workspace.
4+
5+
## Prerequisites
6+
7+
1. A Google Cloud Platform (GCP) project.
8+
2. The Neo.mjs project cloned and initialized.
9+
3. The MCP server deployed to a public URL (or using a tunnel like `ngrok` for local testing).
10+
11+
---
12+
13+
## Step 1: Configure Google Cloud Console
14+
15+
1. Go to the [GCP APIs & Services > Credentials](https://console.cloud.google.com/apis/credentials) page.
16+
2. Click **Create Credentials** > **OAuth client ID**.
17+
3. Select **Web application** as the application type.
18+
4. **Authorized JavaScript origins:** Add the public URL of your MCP server (e.g., `https://mcp.yourdomain.com`).
19+
5. **Authorized redirect URIs:** Since the MCP server acts as a **Protected Resource**, it doesn't handle the user login redirect itself. However, your **MCP Client** (e.g., VS Code or a custom UI) will need a redirect URI.
20+
6. Click **Create** and note your **Client ID** and **Client Secret**.
21+
22+
---
23+
24+
## Step 2: Configure the MCP Server
25+
26+
Update your `.env` file or IaC environment variables.
27+
28+
### Using OIDC Discovery (Recommended)
29+
Google provides a standard OIDC discovery endpoint. This is the easiest way to configure the server.
30+
31+
```env
32+
TRANSPORT=sse
33+
SSE_PORT=3000
34+
HOST=mcp.yourdomain.com
35+
36+
# OIDC Discovery
37+
AUTH_ISSUER_URL=https://accounts.google.com
38+
OAUTH_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
39+
OAUTH_CLIENT_SECRET=your-google-client-secret
40+
```
41+
42+
### Protocol Awareness
43+
Ensure `HOST` is set correctly. If your server is behind a load balancer with SSL termination, ensure `HOST` does not include the protocol unless you want to force one. The server will default to `https` for any host other than `localhost`.
44+
45+
---
46+
47+
## Step 3: Technical Nuances
48+
49+
### Token Introspection
50+
Unlike Keycloak, Google does not strictly adhere to the RFC 7662 introspection spec for all token types in the same way. However, for **OpenID Connect**, the MCP SDK handles the validation of the Bearer token.
51+
52+
### Audience (aud) Validation
53+
Google ID Tokens include the `aud` field, which matches your `OAUTH_CLIENT_ID`. The Neo.mjs MCP server will validate that the incoming token's audience is authorized to access the resource.
54+
55+
> **Note:** In complex scenarios where the client and server use different GCP projects, you may need to configure additional audience mappings in your GCP project.
56+
57+
---
58+
59+
## Step 4: Testing with a Client
60+
61+
When connecting an MCP client (like VS Code) to your server:
62+
63+
1. The client will hit `https://mcp.yourdomain.com/.well-known/oauth-protected-resource`.
64+
2. It will discover that the server requires authorization from `https://accounts.google.com`.
65+
3. The client will initiate the OAuth flow with Google.
66+
4. Once authorized, the client will send requests with the header:
67+
`Authorization: Bearer <google_access_token>`
68+
69+
---
70+
71+
## Troubleshooting
72+
73+
### 401 Unauthorized
74+
Verify that the `OAUTH_CLIENT_ID` in your server config matches the one used by the client to obtain the token.
75+
76+
### OIDC Discovery Failure
77+
If the server logs `Failed to fetch OIDC discovery document`, ensure the server has outbound internet access to reach `https://accounts.google.com/.well-known/openid-configuration`.

learn/tree.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@
128128
{"name": "The Neural Link Server", "parentId": "guides/mcp", "id": "guides/mcp/NeuralLink"},
129129
{"name": "The GitHub Workflow Server", "parentId": "guides/mcp", "id": "guides/mcp/GitHubWorkflow"},
130130
{"name": "Server Authorization", "parentId": "guides/mcp", "id": "guides/mcp/Authorization"},
131+
{"name": "Google OAuth Demo", "parentId": "guides/mcp", "id": "guides/mcp/GoogleAuthDemo"},
131132
{"name": "Code Execution", "parentId": "guides/mcp", "id": "guides/mcp/CodeExecution"},
132133
{"name": "AI", "parentId": "guides", "isLeaf": false, "id": "guides/ai", "collapsed": true},
133134
{"name": "Strategic Workflows", "parentId": "guides/ai", "id": "guides/ai/StrategicWorkflows"},

0 commit comments

Comments
 (0)