diff --git a/packages/api/src/mcp/oauth/tokens.test.ts b/packages/api/src/mcp/oauth/tokens.test.ts index 4a5944c8667..592cad7eeb5 100644 --- a/packages/api/src/mcp/oauth/tokens.test.ts +++ b/packages/api/src/mcp/oauth/tokens.test.ts @@ -88,4 +88,34 @@ describe('MCPTokenStorage.storeTokens expiry handling', () => { expect(stored.expiresIn).toBe(DEFAULT_TTL_SECONDS); }); + + it.each([ + { + source: 'refresh_token_expires_in', + expiry: { refresh_token_expires_in: 3600 }, + expected: 3600, + }, + { source: 'refresh_expires_in', expiry: { refresh_expires_in: 3600 }, expected: 3600 }, + { source: 'the default (neither field)', expiry: {}, expected: DEFAULT_TTL_SECONDS }, + ])('stores refresh token expiry from $source', async ({ expiry, expected }) => { + const createToken = jest.fn().mockResolvedValue({}); + await MCPTokenStorage.storeTokens({ + userId: 'user-1', + serverName: 'keycloak', + tokens: { + access_token: 'access-token', + token_type: 'Bearer', + refresh_token: 'refresh-token', + ...expiry, + }, + createToken: createToken as unknown as TokenMethods['createToken'], + }); + + const refreshTokenCall = createToken.mock.calls.find( + (call) => call[0]?.type === 'mcp_oauth_refresh', + ); + expect(refreshTokenCall).toBeDefined(); + expect(refreshTokenCall![0].expiresIn).toBeGreaterThanOrEqual(expected - 1); + expect(refreshTokenCall![0].expiresIn).toBeLessThanOrEqual(expected); + }); }); diff --git a/packages/api/src/mcp/oauth/tokens.ts b/packages/api/src/mcp/oauth/tokens.ts index d3f7bc7c8f9..ea4788ec0f1 100644 --- a/packages/api/src/mcp/oauth/tokens.ts +++ b/packages/api/src/mcp/oauth/tokens.ts @@ -855,9 +855,11 @@ export class MCPTokenStorage { `${logPrefix} New refresh token received from OAuth server, will store/update`, ); const encryptedRefreshToken = await encryptV2(tokens.refresh_token); - const extendedTokens = tokens as ExtendedOAuthTokens; - const refreshTokenExpiry = extendedTokens.refresh_token_expires_in - ? new Date(Date.now() + extendedTokens.refresh_token_expires_in * 1000) + const extendedTokens: ExtendedOAuthTokens = tokens; + const refreshTokenExpirySeconds = + extendedTokens.refresh_token_expires_in ?? extendedTokens.refresh_expires_in; + const refreshTokenExpiry = refreshTokenExpirySeconds + ? new Date(Date.now() + refreshTokenExpirySeconds * 1000) : new Date(Date.now() + 365 * 24 * 60 * 60 * 1000); // Default to 1 year /** Calculated expiresIn for refresh token */ diff --git a/packages/api/src/mcp/oauth/types.ts b/packages/api/src/mcp/oauth/types.ts index 000a21c8967..d04468ce0be 100644 --- a/packages/api/src/mcp/oauth/types.ts +++ b/packages/api/src/mcp/oauth/types.ts @@ -150,4 +150,6 @@ export interface MCPOAuthTokens extends OAuthTokens { export interface ExtendedOAuthTokens extends OAuthTokens { /** Refresh token expiry in seconds (non-standard, some providers include this) */ refresh_token_expires_in?: number; + /** Keycloak's refresh token expiry in seconds */ + refresh_expires_in?: number; }