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
30 changes: 30 additions & 0 deletions packages/api/src/mcp/oauth/tokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
8 changes: 5 additions & 3 deletions packages/api/src/mcp/oauth/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/mcp/oauth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}