@@ -16,11 +16,13 @@ const {
1616 mockCompleteManagedMcpOAuth,
1717 mockConsumeManagedAttempt,
1818 mockDiscoverServerTools,
19+ mockEnforceCallbackRateLimit,
1920} = vi . hoisted ( ( ) => ( {
2021 mockAuthenticateEnrollment : vi . fn ( ) ,
2122 mockCompleteManagedMcpOAuth : vi . fn ( ) ,
2223 mockConsumeManagedAttempt : vi . fn ( ) ,
2324 mockDiscoverServerTools : vi . fn ( ) ,
25+ mockEnforceCallbackRateLimit : vi . fn ( ) ,
2426} ) )
2527
2628vi . mock ( '@/lib/mcp/oauth' , ( ) => mcpOauthMock )
@@ -37,6 +39,9 @@ vi.mock('@/lib/credential-groups/mcp-oauth-state', () => ({
3739 consumeCredentialGroupMcpOAuthAttempt : mockConsumeManagedAttempt ,
3840 isCredentialGroupMcpOAuthState : ( state : string ) => state . startsWith ( 'mcp_cg_' ) ,
3941} ) )
42+ vi . mock ( '@/lib/credential-groups/rate-limit' , ( ) => ( {
43+ enforcePublicCredentialGroupIpRateLimit : mockEnforceCallbackRateLimit ,
44+ } ) )
4045
4146import { GET } from './route'
4247
@@ -82,6 +87,7 @@ describe('MCP OAuth callback route', () => {
8287 connectionId : 'mcp-cg-connection-1' ,
8388 mcpServerId : 'server-1' ,
8489 } )
90+ mockEnforceCallbackRateLimit . mockResolvedValue ( null )
8591 } )
8692
8793 it ( 'performs the token exchange through the SSRF-guarded mcpAuthGuarded wrapper' , async ( ) => {
@@ -151,6 +157,7 @@ describe('MCP OAuth callback route', () => {
151157
152158 const response = await GET ( request )
153159
160+ expect ( mockEnforceCallbackRateLimit ) . toHaveBeenCalledWith ( request , 'oauth-callback' )
154161 expect ( mockConsumeManagedAttempt ) . toHaveBeenCalledWith ( 'mcp_cg_state-1' )
155162 expect ( mockAuthenticateEnrollment ) . toHaveBeenCalledWith ( 'invitation-token' )
156163 expect ( mockCompleteManagedMcpOAuth ) . toHaveBeenCalledWith (
@@ -166,4 +173,18 @@ describe('MCP OAuth callback route', () => {
166173 '/credential-groups/enroll/invitation-token?mcp=connected&mcpServerId=server-1'
167174 )
168175 } )
176+
177+ it ( 'rate limits a managed callback before consuming its one-time state' , async ( ) => {
178+ const limitedResponse = new Response ( 'rate limited' , { status : 429 } )
179+ mockEnforceCallbackRateLimit . mockResolvedValueOnce ( limitedResponse )
180+ const request = new NextRequest (
181+ 'http://localhost:3000/api/mcp/oauth/callback?state=mcp_cg_state-1&code=auth-code-1'
182+ )
183+
184+ const response = await GET ( request )
185+
186+ expect ( response . status ) . toBe ( 429 )
187+ expect ( mockConsumeManagedAttempt ) . not . toHaveBeenCalled ( )
188+ expect ( mockCompleteManagedMcpOAuth ) . not . toHaveBeenCalled ( )
189+ } )
169190} )
0 commit comments