Skip to content

Commit 45dce04

Browse files
committed
Let any MCP client connect; drop IdP-specific role and group mapping
- Remove AUTH_ALLOWED_REDIRECT_URIS: any client can register; only redirect URIs unsafe for OAuth are refused (non-loopback http, javascript:, data:, file:, user info, fragments); the consent page shows every new client - Remove OIDC_ROLE_CLAIM and OIDC_GROUPS_CLAIM, AppUser.Roles/Groups and the role-based admin policy; copy any IdP claim with OIDC_TOKEN_CLAIMS and use RequireClaim or AppUser.ClaimValues - Tests and docs updated
1 parent 0106062 commit 45dce04

30 files changed

Lines changed: 181 additions & 198 deletions

‎CHANGELOG.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ First release.
1515
errors the model can fix.
1616
- **Auth:** OIDC proxy for any OIDC identity provider (dynamic client registration, PKCE, ID token + userinfo merge,
1717
encrypted IdP tokens), consent page in the style of the FastMCP OAuth proxy, `jwt` resource-server provider,
18-
extensible `IAuthProvider`, dev mode. Scopes are defined in code (`Core/Auth/AppScopes.cs`); clients get only the
19-
scopes they request.
18+
extensible `IAuthProvider`, dev mode. Any MCP client can register: no redirect URI allow-list, only redirect URIs
19+
unsafe for OAuth are refused (non-loopback `http`, `javascript:`, `data:`, `file:`). Scopes are defined in code
20+
(`Core/Auth/AppScopes.cs`); clients get only the scopes they request. No IdP-specific role or group mapping: copy
21+
any IdP claim with `OIDC_TOKEN_CLAIMS` and use it in a policy.
2022
- **Users sign in once:** rotating refresh tokens renew expired access tokens; every refresh extends the session and
2123
the client registration; a 30-second reuse window (`AUTH_REFRESH_REUSE_SECONDS`) makes concurrent refreshes and
2224
retries safe; sign-ins are stored in files by default (`AUTH_STORE=file`, a `/data` volume in Docker) so restarts

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,14 @@ Add the server in Claude (Settings → Connectors → Add custom connector) and
6262
| Area | Included |
6363
| --- | --- |
6464
| **MCP** | Tools, resources and prompts discovered from attributes; stateless Streamable HTTP (scales horizontally); annotations; structured content with output schemas |
65-
| **Auth** | OIDC proxy for any OIDC provider (Entra ID, Google, Auth0, Okta, Keycloak, Zitadel, ...): DCR, PKCE, consent, ID token + userinfo merged into one `AppUser`. Or the `jwt` provider, or your own `IAuthProvider`. `[Authorize]` per item, scope and role policies |
65+
| **Auth** | OIDC proxy for any OIDC provider (Entra ID, Google, Auth0, Okta, Keycloak, Zitadel, ...): DCR, PKCE, consent, ID token + userinfo merged into one `AppUser`. **Any MCP client can connect**, no allow-list. Or the `jwt` provider, or your own `IAuthProvider`. `[Authorize]` per item, scope policies, any IdP claim when you need it |
6666
| **MCP Apps** | `ui/` workspace (React 19, Vite, Tailwind 4, ext-apps 2): one bundle per view, host theme, tool calls from the view, zod-validated outputs, sandbox preview, per-entry builds |
6767
| **Validation** | Data annotations and `IValidatableObject` on every tool and prompt call, nested objects included |
6868
| **Integrations** | Typed API clients from one line of registration; retries, circuit breaker, timeouts; `bearer`, `api_key`, `basic`, `client_credentials`, or the signed-in user's token |
6969
| **Redis** | Optional, for scale-out or your own caching: one shared StackExchange.Redis connection, `REDIS_URL` as URL or connection string (cluster, Sentinel, TLS), and one `IRedisConnectionFactory` to plug in any vendor (Azure Entra ID, AWS, GCP, certificates). Health-checked |
7070
| **Settings** | `.env` plus typed settings classes validated at startup, every problem listed by variable name |
7171
| **Dev loop** | `dotnet watch` with hot reload, `pnpm run watch` for views, dual-stack `localhost`, startup URL in the log |
72-
| **Tests** | Unit, integration (a real MCP client against the in-memory server, including the full OAuth flow against a fake IdP) and architecture rules: 80 tests out of the box |
72+
| **Tests** | Unit, integration (a real MCP client against the in-memory server, including the full OAuth flow against a fake IdP) and architecture rules: 92 tests out of the box |
7373
| **Ship** | Multi-stage Dockerfile, Compose, GitHub Actions (build, test, UI bundle check, Docker), pinned SDK, central package versions |
7474
| **Docs** | 11 guides organized by task, 4 architecture decision records |
7575

‎templates/DotnetMcpTemplate/.env.example‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,8 @@ OIDC_CLIENT_AUTH_METHOD=client_secret_basic
3838
OIDC_SCOPES=openid,profile,email
3939
# auto: call userinfo when the IdP advertises it | always | never
4040
OIDC_USERINFO=auto
41-
# Claim holding roles; dot paths work for nested claims, e.g. realm_access.roles
42-
OIDC_ROLE_CLAIM=roles
43-
OIDC_GROUPS_CLAIM=groups
44-
# Extra IdP claims to copy into the server token (comma-separated), e.g. tenant_id,department
41+
# Extra IdP claims to copy into the server token (comma-separated), e.g. roles,groups,tenant_id.
42+
# IdPs name these differently, so none are copied by default; use them with AppUser.Claims or a RequireClaim policy.
4543
OIDC_TOKEN_CLAIMS=
4644
# jwt provider only: accepted token audience(s). Empty = the MCP URL (APP_URL + MCP_PATH).
4745
OIDC_AUDIENCE=
@@ -55,8 +53,6 @@ AUTH_TOKEN_LIFETIME_MINUTES=60
5553
AUTH_REFRESH_TOKEN_LIFETIME_DAYS=30
5654
# Seconds an old refresh token still returns the same new tokens (concurrent refresh, retry). 0 = off.
5755
AUTH_REFRESH_REUSE_SECONDS=30
58-
# Redirect URIs MCP clients may register (comma-separated; * allowed in port and path).
59-
AUTH_ALLOWED_REDIRECT_URIS=http://localhost:*,http://127.0.0.1:*,https://claude.ai/api/mcp/auth_callback,https://claude.com/api/mcp/auth_callback
6056
# Remember a user's consent for a client for N days (0 = always ask).
6157
AUTH_CONSENT_REMEMBER_DAYS=30
6258
# Where sign-ins live. file: on disk, survives restarts (one instance) | redis: several instances | memory: lost on restart

‎templates/DotnetMcpTemplate/README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,9 @@ The server URL is `APP_URL` + `MCP_PATH`, for example `https://mcp.example.com/m
105105
| Cursor | `~/.cursor/mcp.json`: `{ "mcpServers": { "my-server": { "url": "…/mcp" } } }` |
106106
| MCP Inspector | `npx @modelcontextprotocol/inspector` → Streamable HTTP → the URL |
107107

108-
A client's redirect URI must match `AUTH_ALLOWED_REDIRECT_URIS`. Details: [connecting clients](docs/development/connecting-clients.md).
108+
Any MCP client can connect; there is no allow-list. Only unsafe redirect URIs are refused (`http` outside localhost,
109+
`javascript:`, `data:`, `file:`), and the consent page shows every new client. Details:
110+
[connecting clients](docs/development/connecting-clients.md).
109111

110112
## MCP App UI
111113

@@ -273,7 +275,7 @@ HTTP. In production, terminate TLS at a reverse proxy and set `APP_URL` to the p
273275
| `MCP_AUTH_MODE=none` is refused | Only allowed with `APP_ENV=dev` |
274276
| Login fails with `IDX20803: Unable to obtain configuration` | Open `OIDC_DISCOVERY_URL` in a browser: it must return JSON (note the dot in `/.well-known/`) |
275277
| Identity provider says the redirect URI is invalid | Register `${APP_URL}/oauth/callback` exactly, not `APP_URL` alone |
276-
| A client can't register (`invalid_redirect_uri`) | Add its redirect URI pattern to `AUTH_ALLOWED_REDIRECT_URIS` |
278+
| A client can't register (`invalid_redirect_uri`) | Its redirect URI is unsafe: it must be `https`, `http` on localhost, or an app scheme |
277279
| Users must sign in again after a restart | `AUTH_STORE=memory` loses sign-ins: use `file` (default) or `redis`; in Docker keep the `/data` volume |
278280
| `.env` changes have no effect under `dotnet watch` | Settings are read at startup: press Ctrl+R in the watch terminal |
279281
| `UI bundle not found at …/ui_dist/<entry>.html` | `cd ui && pnpm install && pnpm run build` |

‎templates/DotnetMcpTemplate/docs/development/README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Read top to bottom when you're new; jump to a section when you have a task.
3030

3131
| Guide | Covers |
3232
| --- | --- |
33-
| [Authentication and authorization](authentication-and-authorization.md) | Auth modes, the OIDC proxy, `jwt` provider, policies, scopes, roles, troubleshooting |
33+
| [Authentication and authorization](authentication-and-authorization.md) | Auth modes, the OIDC proxy, any MCP client, `jwt` provider, policies, scopes, IdP claims, troubleshooting |
3434
| [Custom auth providers](custom-auth-providers.md) | Implement `IAuthProvider` for anything else |
3535

3636
## 5. Keep it correct
@@ -47,7 +47,8 @@ Read top to bottom when you're new; jump to a section when you have a task.
4747
| Call an external API | [API clients](api-clients.md), then a service in `Services/` |
4848
| Share sign-ins across instances, or use Redis | [Redis](redis.md) |
4949
| Add a setting | [Configuration → Typed settings](configuration-and-environments.md#typed-settings) |
50-
| Require a scope or role | [Auth → Scopes](authentication-and-authorization.md#scopes) |
50+
| Require a scope | [Auth → Scopes](authentication-and-authorization.md#scopes) |
51+
| Use IdP roles or groups | [Auth → Roles, groups and other IdP claims](authentication-and-authorization.md#roles-groups-and-other-idp-claims) |
5152
| Show a UI for a tool | [MCP Apps](mcp-apps.md), then [Adding a view](../../ui/README.md#adding-a-view) |
5253
| Fix a login problem | [Auth → Default provider](authentication-and-authorization.md#default-provider-oidc-proxy-auth_provideroidc) |
5354
| Test a tool with auth | [Testing → Integration tests](testing.md#integration-tests-the-server-as-a-client-sees-it) |

‎templates/DotnetMcpTemplate/docs/development/authentication-and-authorization.md‎

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,16 @@ Flow: the MCP client gets `401` + `resource_metadata` → reads `/.well-known/oa
2929
IdP login → `/oauth/callback` (the server validates the ID token, calls userinfo, merges claims, stores the IdP tokens
3030
encrypted) → the client exchanges the code at `/oauth/token` for the **server's** token.
3131

32-
The server token contains `sub`, `name`, `email`, `picture`, `preferred_username`, `roles` (from `OIDC_ROLE_CLAIM`,
33-
dot paths allowed), `groups`, granted `scope`, and any `OIDC_TOKEN_CLAIMS`. Inject `AppUser` to read them.
32+
The server token contains `sub`, `name`, `email`, `picture`, `preferred_username`, the granted `scope`, and any IdP
33+
claims listed in `OIDC_TOKEN_CLAIMS`. Inject `AppUser` to read them.
3434

35-
Security properties: PKCE S256 on both legs, exact redirect URI match, registration limited to
36-
`AUTH_ALLOWED_REDIRECT_URIS`, consent per client (remembered `AUTH_CONSENT_REMEMBER_DAYS`), CSRF-protected consent,
35+
**Any MCP client can connect.** Clients register themselves (dynamic client registration); there is no allow-list.
36+
A redirect URI is refused only when it's unsafe for OAuth: it must be `https`, `http` on a loopback host
37+
(`localhost`, `127.0.0.1`, `[::1]`), or an app scheme such as `cursor://`, and never `javascript:`, `data:`, `file:`,
38+
user info or a fragment. The consent page shows each new client and exactly where its credentials will be sent.
39+
40+
Security properties: PKCE S256 on both legs, exact redirect URI match, safe redirect URIs only, consent per client
41+
(remembered `AUTH_CONSENT_REMEMBER_DAYS`), CSRF-protected consent,
3742
single-use codes (5 min), rotating single-use refresh tokens, audience-bound tokens (RFC 8707), `iss` in the
3843
authorization response (RFC 9207), codes/tokens stored by hash, IdP tokens encrypted (AES-GCM).
3944

@@ -67,12 +72,12 @@ The store holds client registrations, sessions (user claims, encrypted IdP token
6772
## Provider `jwt`
6873

6974
For IdPs that support MCP clients directly and issue JWT access tokens for your MCP URL: the server only validates
70-
tokens (`OIDC_DISCOVERY_URL`, `OIDC_AUDIENCE`, `OIDC_ROLE_CLAIM`).
75+
tokens (`OIDC_DISCOVERY_URL`, `OIDC_AUDIENCE`). The IdP's claims arrive as they are, in `AppUser.Claims`.
7176

7277
## Authorization
7378

7479
- `[Authorize]`: any signed-in user. `[AllowAnonymous]`: public (useful with `mixed`).
75-
- Policies in `Core/Auth/Policies.cs`: `Policies.ProjectsWrite` (scope), `Policies.Admin` (role). Add your own there.
80+
- Policies in `Core/Auth/Policies.cs`: `Policies.ProjectsWrite` (scope). Add your own there.
7681
- Scopes are read from `scope` and `scp` in any format, so policies work with every IdP.
7782

7883
## Scopes
@@ -85,5 +90,21 @@ permissions, so tools behind a scope policy refuse it. To add a scope:
8590
2. Add a policy for it in `Policies.cs`.
8691
3. Use it: `[Authorize(Policy = Policies.YourScope)]`.
8792

88-
Roles (`[Authorize(Roles = ...)]`, `Policies.Admin`) come from the IdP through `OIDC_ROLE_CLAIM`, not from scopes.
89-
With `MCP_AUTH_MODE=none` the developer user has every scope and the `admin` role.
93+
With `MCP_AUTH_MODE=none` the developer user has every scope.
94+
95+
## Roles, groups and other IdP claims
96+
97+
Every identity provider names and shapes these differently (`roles`, `groups`, `cognito:groups`, `realm_access`,
98+
app roles, ...), so the template maps none of them. Scopes above are the server's own and work the same with every
99+
IdP. When you do need an IdP claim, you decide which one:
100+
101+
1. Copy it into the server token: `OIDC_TOKEN_CLAIMS=roles` (comma-separated, top-level claim names).
102+
2. Require it in a policy in `Policies.cs`, or read it in code:
103+
104+
```csharp
105+
.AddPolicy("admin", policy => policy.RequireAuthenticatedUser().RequireClaim("roles", "admin"))
106+
107+
if (user.ClaimValues("roles").Contains("admin")) { ... }
108+
```
109+
110+
With the `jwt` provider the IdP's claims are already in the token; skip step 1.

‎templates/DotnetMcpTemplate/docs/development/connecting-clients.md‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@ URL: `APP_URL` + `MCP_PATH`, e.g. `https://mcp.example.com/mcp` (Streamable HTTP
44
automatically from the `401` response.
55

66
- **Claude** (claude.ai / Desktop): Settings → Connectors → Add custom connector → paste the URL.
7-
Allow `https://claude.ai/api/mcp/auth_callback` in `AUTH_ALLOWED_REDIRECT_URIS`.
87
- **VS Code**: `.vscode/mcp.json`: `{ "servers": { "my-server": { "type": "http", "url": "https://mcp.example.com/mcp" } } }`
98
- **Cursor**: `~/.cursor/mcp.json`: `{ "mcpServers": { "my-server": { "url": "https://mcp.example.com/mcp" } } }`
10-
(allow Cursor's redirect URI pattern if it uses a custom scheme).
119
- **MCP Inspector**: `npx @modelcontextprotocol/inspector` → Streamable HTTP → URL.
1210

13-
If a client fails to register, check the server log for the rejected redirect URI and add a pattern to
14-
`AUTH_ALLOWED_REDIRECT_URIS`.
11+
Any MCP client can connect: there is no allow-list to maintain. Registration refuses only redirect URIs that are
12+
unsafe for OAuth (`http` outside localhost, `javascript:`, `data:`, `file:`, fragments); the error names the URI.
13+
Each new client is shown on the consent page before the user signs in.
1514

1615
## Behind a reverse proxy
1716

‎templates/DotnetMcpTemplate/docs/development/custom-auth-providers.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ public sealed class ApiKeyAuthProvider : IAuthProvider
2222
```
2323

2424
The template adds the MCP 401 challenge, the protected resource metadata, policies and `AppUser` on top.
25-
Write claims with the names in `AppClaims` (`sub`, `name`, `email`, `roles`, `scope`) so `AppUser` and policies work.
25+
Write claims with the names in `AppClaims` (`sub`, `name`, `email`, `scope`) so `AppUser` and policies work; any
26+
other claim is available in `AppUser.Claims`.
2627

2728
Examples in the repository: `Core/Auth/Jwt/JwtAuthProvider.cs` (resource server) and
2829
`tests/.../TestInfrastructure/TestAuthProvider.cs` (header-based, for tests).

‎templates/DotnetMcpTemplate/docs/development/testing.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ public sealed class InvoiceToolTests(McpServerFactory factory) : IClassFixture<M
4949
}
5050
```
5151

52-
- **Auth:** `AUTH_PROVIDER=test` accepts `Bearer test|{userId}|{scopes}|{roles}`. Build tokens with
53-
`TestAuthProvider.Token("alice", scopes: "mcp:tools projects:write", roles: "admin")`; `null` connects anonymously.
52+
- **Auth:** `AUTH_PROVIDER=test` accepts `Bearer test|{userId}|{scopes}`. Build tokens with
53+
`TestAuthProvider.Token("alice", scopes: "mcp:tools projects:write")`; `null` connects anonymously.
5454
- **Fakes:** override `ConfigureTestServices` in a factory subclass to replace more services.
5555
- **Settings:** pass overrides to the protected `McpServerFactory(overrides)` constructor (see
5656
`OidcProxyServerFactory`, which runs the whole OAuth flow against `FakeIdentityProvider`).

‎templates/DotnetMcpTemplate/docs/development/tools-resources-prompts.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public sealed class InvoiceTools(IInvoiceService invoices)
3131
- Inject `AppUser` (or `ClaimsPrincipal`) for the caller; it is hidden from the schema.
3232
- Set annotations honestly: `ReadOnly`, `Destructive`, `Idempotent`, `OpenWorld` help clients decide on confirmations.
3333
- Throw `AppException.NotFound/Conflict/Rule(...)` for expected errors; the model receives the message.
34-
- Use `[Authorize(Policy = Policies.X)]` on a method for extra requirements (scopes, roles).
34+
- Use `[Authorize(Policy = Policies.X)]` on a method for extra requirements (scopes, IdP claims).
3535
- Return a record. Set `UseStructuredContent = true` when clients or an MCP App read the result as data: the tool
3636
then returns `structuredContent` and publishes an output schema. Without it the result is JSON text.
3737

0 commit comments

Comments
 (0)