Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions docs/adr/0011-oauth-refresh-tokens-use-rotating-families.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
status: accepted
---

# OAuth Refresh Tokens Use Rotating Families

1MCP issues refresh tokens only to registered clients that request the `refresh_token` grant. Each approved authorization creates an independent, client-, resource-, and scope-bound Refresh Token Family that persists in its Runtime Scope for a fixed 30 days. Every successful refresh rotates a single-use opaque token; only token digests and family lineage are stored.

Because public PKCE clients are not sender-constrained, reuse of any consumed family member revokes its family and every access token issued from it. There is no retry grace period: concurrent refreshes permit one success, and later use is replay. A normal refresh may narrow the new access token's scopes and leaves earlier access tokens valid until their own expiry; explicit access-token revocation remains local to that token.

This chooses RFC 9700 rotation and strong replay containment over retry availability. A lost response or concurrent retry can force reauthorization, but the runtime never permits two valid successors or extends the family beyond its original lifetime.
6 changes: 6 additions & 0 deletions docs/en/guide/advanced/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ npx -y @1mcp/agent --config mcp.json --enable-auth

This will activate the OAuth 2.1 endpoints and require authentication for all incoming requests.

## Refresh Token Rotation

Registered clients receive a refresh token only when they request the `refresh_token` grant. Refresh tokens are single-use and rotate on every successful exchange. Each family has a fixed 30-day lifetime, remains bound to its original client and resource, and permits only equal or narrower scopes.

Reusing any consumed refresh token revokes the entire family and every access token issued from it. Concurrent refreshes therefore allow exactly one successful rotation; clients must reauthorize after a replay response or a lost response that causes a retry.

## OAuth Management Dashboard

Once authentication is enabled, you can use the OAuth Management Dashboard to manage the authorization flow with your backend services. The dashboard is available at the `/oauth` endpoint of your agent's URL (e.g., `http://localhost:3050/oauth`).
Expand Down
6 changes: 6 additions & 0 deletions docs/zh/guide/advanced/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ npx -y @1mcp/agent --config mcp.json --enable-auth

这将激活 OAuth 2.1 端点,并要求对所有传入请求进行身份验证。

## 刷新令牌轮换

只有请求 `refresh_token` 授权类型的已注册客户端才会收到刷新令牌。刷新令牌只能使用一次,每次成功交换都会轮换。每个令牌家族的生命周期固定为 30 天,并始终绑定到原始客户端和资源;后续请求只能保持或缩小原始作用域。

重复使用任何已消费的刷新令牌会撤销整个家族及其签发的所有访问令牌。因此,并发刷新只允许一次成功轮换;发生重放响应,或响应丢失后重试导致重放时,客户端必须重新授权。

## OAuth 管理仪表板

启用身份验证后,您可以使用 OAuth 管理仪表板来管理与后端服务的授权流程。该仪表板可在代理 URL 的 `/oauth` 端点处获得(例如,`http://localhost:3050/oauth`)。
Expand Down
22 changes: 18 additions & 4 deletions src/auth/oauthAuthorizationFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ export interface OAuthAuthorizationFlowStorage {
getClient(clientId: string): unknown | null | undefined;
processConsentApproval(authRequestId: string, selectedScopes: string[]): Promise<{ redirectUrl: URL }>;
processConsentDenial(authRequestId: string): Promise<URL>;
createSessionWithId(tokenId: string, clientId: string, resource: string, scopes: string[], ttlMs: number): string;
createSessionWithId(
tokenId: string,
clientId: string,
resource: string,
scopes: string[],
ttlMs: number,
refreshFamilyId?: string,
): string;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

export interface OAuthAuthorizationFlowStorageService {
Expand All @@ -26,7 +33,14 @@ export interface OAuthAuthorizationFlowStorageService {
processConsentApproval(authRequestId: string, selectedScopes: string[]): Promise<{ redirectUrl: URL }>;
processConsentDenial(authRequestId: string): Promise<URL>;
sessionRepository: {
createWithId(tokenId: string, clientId: string, resource: string, scopes: string[], ttlMs: number): string;
createWithId(
tokenId: string,
clientId: string,
resource: string,
scopes: string[],
ttlMs: number,
refreshFamilyId?: string,
): string;
};
}

Expand Down Expand Up @@ -382,8 +396,8 @@ export function createOAuthAuthorizationFlowFromStorage(
processConsentApproval: (authRequestId, selectedScopes) =>
storage.processConsentApproval(authRequestId, selectedScopes),
processConsentDenial: (authRequestId) => storage.processConsentDenial(authRequestId),
createSessionWithId: (tokenId, clientId, resource, scopes, ttlMs) =>
storage.sessionRepository.createWithId(tokenId, clientId, resource, scopes, ttlMs),
createSessionWithId: (tokenId, clientId, resource, scopes, ttlMs, refreshFamilyId) =>
storage.sessionRepository.createWithId(tokenId, clientId, resource, scopes, ttlMs, refreshFamilyId),
},
});
}
Expand Down
Loading