🔐 fix: persist new MCP oauth tokens properly (#10439)

* fix: re-fetch OAuth flow state after completeOAuthFlow

* test: add tests for MCP OAuth flow state bugs
This commit is contained in:
Sean McGrath 2025-11-11 13:51:20 +13:00 committed by GitHub
parent 2524d33362
commit b443254151
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 127 additions and 8 deletions

View file

@ -498,6 +498,93 @@ describe('MCP Routes', () => {
expect(response.headers.location).toBe('/oauth/error?error=callback_failed');
expect(mockMcpManager.getUserConnection).not.toHaveBeenCalled();
});
it('should re-fetch flow state after completeOAuthFlow to capture DCR updates', async () => {
const { mcpServersRegistry } = require('@librechat/api');
const mockFlowManager = {
completeFlow: jest.fn().mockResolvedValue(),
deleteFlow: jest.fn().mockResolvedValue(true),
};
const initialClientInfo = {
client_id: 'initial123',
client_secret: 'initial_secret',
};
const updatedClientInfo = {
client_id: 'updated456',
client_secret: 'updated_secret',
};
const initialFlowState = {
serverName: 'test-server',
userId: 'test-user-id',
metadata: { toolFlowId: 'tool-flow-123', serverUrl: 'http://example.com' },
clientInfo: initialClientInfo,
codeVerifier: 'test-verifier',
};
const updatedFlowState = {
serverName: 'test-server',
userId: 'test-user-id',
metadata: { toolFlowId: 'tool-flow-123', serverUrl: 'http://example.com' },
clientInfo: updatedClientInfo, // DCR re-registration changed credentials
codeVerifier: 'test-verifier',
};
const mockTokens = {
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
};
// First call returns initial state, second call returns updated state
MCPOAuthHandler.getFlowState
.mockResolvedValueOnce(initialFlowState)
.mockResolvedValueOnce(updatedFlowState);
MCPOAuthHandler.completeOAuthFlow.mockResolvedValue(mockTokens);
MCPTokenStorage.storeTokens.mockResolvedValue();
mcpServersRegistry.getServerConfig.mockResolvedValue({});
getLogStores.mockReturnValue({});
require('~/config').getFlowStateManager.mockReturnValue(mockFlowManager);
const mockUserConnection = {
fetchTools: jest.fn().mockResolvedValue([]),
};
const mockMcpManager = {
getUserConnection: jest.fn().mockResolvedValue(mockUserConnection),
};
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
require('~/config').getOAuthReconnectionManager = jest.fn().mockReturnValue({
clearReconnection: jest.fn(),
});
const response = await request(app).get('/api/mcp/test-server/oauth/callback').query({
code: 'test-auth-code',
state: 'test-flow-id',
});
expect(response.status).toBe(302);
expect(response.headers.location).toBe('/oauth/success?serverName=test-server');
// Verify MCPOAuthHandler.getFlowState was called TWICE (before and after completion)
expect(MCPOAuthHandler.getFlowState).toHaveBeenCalledTimes(2);
expect(MCPOAuthHandler.getFlowState).toHaveBeenNthCalledWith(
1,
'test-flow-id',
mockFlowManager,
);
expect(MCPOAuthHandler.getFlowState).toHaveBeenNthCalledWith(
2,
'test-flow-id',
mockFlowManager,
);
// Verify storeTokens was called with UPDATED credentials, not initial ones
expect(MCPTokenStorage.storeTokens).toHaveBeenCalledWith(
expect.objectContaining({
userId: 'test-user-id',
serverName: 'test-server',
tokens: mockTokens,
clientInfo: updatedClientInfo, // Should use updated, not initial
metadata: updatedFlowState.metadata, // Should use updated metadata
}),
);
});
});
describe('GET /oauth/tokens/:flowId', () => {