Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 35 additions & 22 deletions .specs/mcp-gateway-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ when they appear in all capitals.
| `GET /.well-known/oauth-authorization-server/oauth/authorize` | App canonical route, Worker discovery alias | Metadata alias for clients that discover from the authorization route. The Worker alias redirects to the app canonical route. |
| `GET /.well-known/oauth-authorization-server/mcp-connect/...` | Worker discovery alias | Path-aware compatibility alias for clients that start discovery from one scoped connect URL; redirects to app canonical metadata. |
| `POST /api/mcp-gateway/oauth/register` | App | Dynamic client registration. |
| `POST /api/mcp-gateway/oauth/register/{scope}/{owner_id}/{config_id}/{route_key}` | App | Resource-specific registration after route eligibility discovery. |
| `POST /api/mcp-gateway/oauth/register/resource/{scope}/{owner_id}/{config_id}/{route_key}` | App | Resource-specific registration after route eligibility discovery. |
| `GET|PUT|DELETE /api/mcp-gateway/oauth/register/{client_id}` | App | Registration management authorized by registration token. |
| `GET /api/mcp-gateway/oauth/authorize` | App | Generic authorization-code flow; requires `resource`. |
| `GET /api/mcp-gateway/oauth/authorize/{scope}/{owner_id}/{config_id}/{route_key}` | App | Route-specific authorization-code flow. |
Expand Down Expand Up @@ -250,36 +250,50 @@ when they appear in all capitals.
configs.
2. `none` and `static_headers` configs complete first-level authorization without
provider OAuth.
3. A provider grant belongs to exactly one connection instance and MUST NOT be
3. Gateway client scopes and upstream provider scopes are separate scope systems.
Gateway client scopes come from the Kilo gateway vocabulary; upstream provider
scopes come from explicit admin override or the remote MCP server's
`WWW-Authenticate` challenge.
4. Upstream provider scopes MUST NOT be derived from `scopes_supported`.
5. When no upstream provider scope is known, the provider authorization request
MUST omit `scope` rather than inventing a gateway scope such as `profile`.
6. The remote protected-resource `resource` value, when available, MUST be
preserved in upstream provider authorization and token exchange requests.
7. A provider grant belongs to exactly one connection instance and MUST NOT be
shared across users, configs, owners, or scopes.
4. Provider access tokens, refresh tokens, provider client IDs, client secrets,
8. Provider access tokens, refresh tokens, provider client IDs, client secrets,
static header secrets, pending state, authorization codes, refresh tokens,
and PKCE verifiers are sensitive material.
5. Provider grants and pending provider authorization state MUST be encrypted at
9. Provider grants and pending provider authorization state MUST be encrypted at
rest.
6. Pending provider authorization MUST bind owner scope, owner ID, user ID,
config ID, config version, instance ID, canonical route, remote URL, auth
mode, provider credentials, authorization endpoint, token endpoint, redirect
URI, scopes, PKCE verifier, execution context, and first-level authorization
request ID when applicable.
7. Sensitive provider credentials, including provider client ID, MUST be inside
encrypted pending state rather than stored as plaintext pending columns.
8. Pending state is opaque, one-time, expires within 30 minutes, and MUST be
consumed atomically on success, provider error, expiry, or invalid callback.
9. Provider error callbacks MUST consume pending state and MUST NOT create a grant.
10. Provider callback success MUST persist the grant before the app issues a final
10. Pending provider authorization MUST bind owner scope, owner ID, user ID,
config ID, config version, instance ID, canonical route, remote URL, auth
mode, provider credentials, authorization endpoint, token endpoint, redirect
URI, upstream provider scopes, upstream provider resource when present, PKCE
verifier, execution context, and first-level authorization request ID when
applicable.
11. Sensitive provider credentials, including provider client ID, MUST be inside
encrypted pending state rather than stored as plaintext pending columns.
12. Pending state is opaque, one-time, expires within 30 minutes, and MUST be
consumed atomically on success, provider error, expiry, or invalid callback.
13. Provider error callbacks MUST consume pending state and MUST NOT create a grant.
14. Provider callback success MUST persist the grant before the app issues a final
authorization code.
11. Provider responses MUST be size-capped before JSON parsing and validated with
15. Provider responses MUST be size-capped before JSON parsing and validated with
the relevant schema.
12. Only bearer provider tokens are supported in v1. Non-bearer provider token
16. Only bearer provider tokens are supported in v1. Non-bearer provider token
types MUST be rejected.
13. Grant versioning is monotonic per instance. Creating, replacing, revoking, or
17. Grant versioning is monotonic per instance. Creating, replacing, revoking, or
deleting a grant MUST advance the version; replacement MUST NOT reset it.
14. Provider refresh is lazy and happens only during runtime proxying.
15. Refresh failure MUST move the instance to `needs_reauth` without overwriting a
18. Provider refresh is lazy and happens only during runtime proxying.
19. Refresh failure MUST move the instance to `needs_reauth` without overwriting a
newer app-side revoke/replacement.
16. A provider grant may be restored only by a successful provider authorization
20. A provider grant may be restored only by a successful provider authorization
for the same non-terminal instance.
21. Changing upstream provider scopes, provider scope source, provider resource,
provider authorization endpoint, provider token endpoint, or provider credentials
is a material config change and MUST revoke pending provider authorization state,
revoke active grants, and advance config version.

## Worker Runtime Proxy

Expand Down Expand Up @@ -371,4 +385,3 @@ when they appear in all capitals.
- Group/team assignment.
- External `/v0.1/servers` registry projection.
- A Worker-side provider token-exchange API.
- Dashboard UI or feature-flagged management pages.
79 changes: 79 additions & 0 deletions apps/web/src/app/(app)/cloud/mcp-gateway/ConnectionStatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { cn } from '@/lib/utils';

type ConnectionStatusInput = {
enabled: boolean;
authMode: string;
activeGrantCount: number;
assignmentCount: number;
};

type StatusTone = 'positive' | 'attention' | 'neutral';

type ConnectionStatus = {
label: string;
description: string;
tone: StatusTone;
};

export function getConnectionStatus(connection: ConnectionStatusInput): ConnectionStatus {
if (!connection.enabled) {
return { label: 'Disabled', description: 'Requests are blocked', tone: 'neutral' };
}
if (connection.authMode === 'none' || connection.authMode === 'static_headers') {
return { label: 'Ready', description: 'No provider sign-in required', tone: 'positive' };
}
if (connection.activeGrantCount > 0) {
if (connection.assignmentCount > connection.activeGrantCount) {
return {
label: 'Partially signed in',
description: `${connection.activeGrantCount} of ${connection.assignmentCount} assigned users have active grants`,
tone: 'attention',
};
}
return {
label: 'Signed in',
description:
connection.assignmentCount > 0
? `${connection.activeGrantCount} assigned users have active grants`
: 'A user has an active grant',
tone: 'positive',
};
}
return { label: 'Needs sign-in', description: 'No active provider grant yet', tone: 'attention' };
}

const toneDot: Record<StatusTone, string> = {
positive: 'bg-green-400',
attention: 'bg-yellow-400',
neutral: 'bg-muted-foreground',
};

const toneClassName: Record<StatusTone, string> = {
positive: 'bg-green-500/20 text-green-400 ring-green-500/20',
attention: 'bg-yellow-500/20 text-yellow-400 ring-yellow-500/20',
neutral: 'bg-secondary text-muted-foreground ring-border',
};

export function ConnectionStatusBadge({
connection,
className,
}: {
connection: ConnectionStatusInput;
className?: string;
}) {
const status = getConnectionStatus(connection);
return (
<span
className={cn(
'inline-flex h-6 items-center gap-1.5 rounded-md px-2 text-xs font-medium ring-1',
toneClassName[status.tone],
className
)}
aria-label={`${status.label}: ${status.description}`}
title={status.description}
>
<span aria-hidden className={cn('size-1.5 rounded-full', toneDot[status.tone])} />
{status.label}
</span>
);
}
Loading
Loading