Expired and Rotated OAuth Tokens Are Not Being Deleted in MCP Flow #2425

Open
opened 2026-03-13 09:53:22 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @Ridhim-RR on GitHub (Nov 30, 2025).

In mcp/token (inside index.ts), when handling the refresh_token flow, expired refresh tokens are not deleted from the database.

Current behavior:

if (token.refreshTokenExpiresAt < new Date()) {
    throw new APIError("UNAUTHORIZED", {
        error_description: "refresh token expired",
        error: "invalid_grant",
    });
}

If the token is expired, the function throws an error but does not remove the expired token from storage.

Later, when a new access token and refresh token are generated:

await ctx.context.adapter.create({
    model: modelName.oauthAccessToken,
    data: {
        accessToken,
        refreshToken: newRefreshToken,
        accessTokenExpiresAt,
        refreshTokenExpiresAt,
        clientId: client_id.toString(),
        userId: token.userId,
        scopes: token.scopes,
        createdAt: new Date(),
        updatedAt: new Date(),
    },
});

The code creates a new token, but the old token is never deleted.

Expected Behavior

After issuing a new access token + refresh token pair, the previous token record should be deleted to avoid:

  • Token accumulation
  • Possible security concerns
Originally created by @Ridhim-RR on GitHub (Nov 30, 2025). In mcp/token (inside index.ts), when handling the refresh_token flow, expired refresh tokens are not deleted from the database. **Current behavior:** ``` if (token.refreshTokenExpiresAt < new Date()) { throw new APIError("UNAUTHORIZED", { error_description: "refresh token expired", error: "invalid_grant", }); } ``` If the token is expired, the function throws an error but does not remove the expired token from storage. Later, when a new access token and refresh token are generated: ``` await ctx.context.adapter.create({ model: modelName.oauthAccessToken, data: { accessToken, refreshToken: newRefreshToken, accessTokenExpiresAt, refreshTokenExpiresAt, clientId: client_id.toString(), userId: token.userId, scopes: token.scopes, createdAt: new Date(), updatedAt: new Date(), }, }); ``` The code creates a new token, but the old token is never deleted. **Expected Behavior** After issuing a new access token + refresh token pair, the previous token record should be deleted to avoid: - Token accumulation - Possible security concerns
GiteaMirror added the bug label 2026-03-13 09:53:22 -05:00
Author
Owner

@better-auth-agent[bot] commented on GitHub (Nov 30, 2025):

(tag @better-auth-agent if you would like an answer from the Agent)

DiagramDiscordGitHub

Diagram Join Star

@better-auth-agent[bot] commented on GitHub (Nov 30, 2025): (tag @better-auth-agent if you would like an answer from the Agent) <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@Ridhim-RR commented on GitHub (Nov 30, 2025):

@better-auth-agent kindly look into this

@Ridhim-RR commented on GitHub (Nov 30, 2025): @better-auth-agent kindly look into this
Author
Owner

@better-auth-agent[bot] commented on GitHub (Nov 30, 2025):

Hi there! Thanks for reporting this. It looks like the current refresh token flow is designed to validate the token’s expiry, but it doesn’t automatically remove expired tokens when a new token pair is issued. As you’ve pointed out, this can lead to token accumulation and potential security concerns.

A good next step would be to implement a cleanup mechanism that deletes the expired token record once a new one is issued. In the meantime, you might consider setting up a scheduled job or database cleanup task to remove any tokens that have passed their expiration. This approach can help mitigate the issue until an official fix is provided.

Have you tried any temporary workarounds, or is this affecting your production deployment? tag @better-auth-agent if you would like an answer from the Agent

DiagramDiscordGitHub

Diagram Join Star

@better-auth-agent[bot] commented on GitHub (Nov 30, 2025): Hi there! Thanks for reporting this. It looks like the current refresh token flow is designed to validate the token’s expiry, but it doesn’t automatically remove expired tokens when a new token pair is issued. As you’ve pointed out, this can lead to token accumulation and potential security concerns. A good next step would be to implement a cleanup mechanism that deletes the expired token record once a new one is issued. In the meantime, you might consider setting up a scheduled job or database cleanup task to remove any tokens that have passed their expiration. This approach can help mitigate the issue until an official fix is provided. Have you tried any temporary workarounds, or is this affecting your production deployment? tag @better-auth-agent if you would like an answer from the Agent <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Author
Owner

@dosubot[bot] commented on GitHub (Nov 30, 2025):

You're correct—expired and rotated refresh tokens are not deleted in the MCP flow. The code checks expiry and throws if the token is expired, but leaves the old token in the database. When a new access/refresh token pair is issued, the previous token record remains, which can lead to token accumulation and security concerns. This behavior is consistent in both MCP and OIDC flows (source, source).

The adapter supports delete and deleteMany methods, so you can remove old tokens after issuing new ones. To implement the expected behavior, add a call to ctx.context.adapter.delete right after creating the new token:

await ctx.context.adapter.delete({
  model: modelName.oauthAccessToken,
  where: [{ field: "refreshToken", value: token.refreshToken }],
});

There's an open issue proposing OAuth 2.1 compliance, including one-time use refresh tokens and invalidation (deletion) of old tokens when a new one is issued (details). Before deploying, consider any audit or compliance requirements for token retention.

If you make this change, you may want to review other flows (like OIDC and session/logout) for consistency.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

@dosubot[bot] commented on GitHub (Nov 30, 2025): <!-- Answer --> You're correct—expired and rotated refresh tokens are not deleted in the MCP flow. The code checks expiry and throws if the token is expired, but leaves the old token in the database. When a new access/refresh token pair is issued, the previous token record remains, which can lead to token accumulation and security concerns. This behavior is consistent in both MCP and OIDC flows ([source](https://github.com/better-auth/better-auth/blob/2b699c5c9684a7bb117e9002861f163056a7df95/packages/better-auth/src/plugins/mcp/index.ts#L338-L386), [source](https://github.com/better-auth/better-auth/blob/2b699c5c9684a7bb117e9002861f163056a7df95/packages/better-auth/src/plugins/oidc-provider/index.ts#L581-L1716)). The adapter supports `delete` and `deleteMany` methods, so you can remove old tokens after issuing new ones. To implement the expected behavior, add a call to `ctx.context.adapter.delete` right after creating the new token: ```ts await ctx.context.adapter.delete({ model: modelName.oauthAccessToken, where: [{ field: "refreshToken", value: token.refreshToken }], }); ``` There's an open issue proposing OAuth 2.1 compliance, including one-time use refresh tokens and invalidation (deletion) of old tokens when a new one is issued ([details](https://github.com/better-auth/better-auth/issues/5459)). Before deploying, consider any audit or compliance requirements for token retention. If you make this change, you may want to review other flows (like OIDC and session/logout) for consistency. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/a34a6229-4425-4ca2-b1f3-377c45e208a3?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/a34a6229-4425-4ca2-b1f3-377c45e208a3?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/a34a6229-4425-4ca2-b1f3-377c45e208a3?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/a34a6229-4425-4ca2-b1f3-377c45e208a3?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/a34a6229-4425-4ca2-b1f3-377c45e208a3?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/a34a6229-4425-4ca2-b1f3-377c45e208a3?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/a34a6229-4425-4ca2-b1f3-377c45e208a3?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=You%27re%20correct%E2%80%94expired%20and%20rotated%20refresh%20tokens%20are%20not%20deleted%20in%20the%20MCP%20flow.%20The%20code%20checks%20expiry%20and%20throws%20if%20the%20token%20is%20expired%2C%20but%20leaves%20the%20old%20token%20in%20the%20database.%20When%20a%20new%20access/refresh%20token%20pair%20is%20issued%2C%20the%20previous%20token%20record%20remains%2C%20which%20can%20lead%20to%20token%20accumulation%20and%20security%20concerns.%20This%20behavior%20is%20consistent%20in%20both%20MCP%20and%20OIDC%20flows%C2%A0%28%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/blob/2b699c5c9684a7bb117e9002861f163056a7df95/packages/better-auth/src/plugins/mcp/index.ts%23L338-L386%29%2C%20%5Bsource%5D%28https%3A//github.com/better-auth/better-auth/blob/2b699c5c9684a7bb117e9002861f163056a7df95/packages/better-auth/src/plugins/oidc-provider/index.ts%23L581-L1716%29%29.%0A%0AThe%20adapter%20supports%20%60delete%60%20and%20%60deleteMany%60%20methods%2C%20so%20you%20can%20remove%20old%20tokens%20after%20issuing%20new%20ones.%20To%20implement%20the%20expected%20behavior%2C%20add%20a%20call%20to%20%60ctx.context.adapter.delete%60%20right%20after%20creating%20the%20new%20token%3A%0A%0A%60%60%60ts%0Aawait%20ctx.context.adapter.delete%28%7B%0A%20%20model%3A%20modelName.oauthAccessToken%2C%0A%20%20where%3A%20%5B%7B%20field%3A%20%22refreshToken%22%2C%20value%3A%20token.refreshToken%20%7D%5D%2C%0A%7D%29%3B%0A%60%60%60%0A%0AThere%27s%20an%20open%20issue%20proposing%20OAuth%202.1%20compliance%2C%20including%20one-time%20use%20refresh%20tokens%20and%20invalidation%20%28deletion%29%20of%20old%20tokens%20when%20a%20new%20one%20is%20issued%C2%A0%28%5Bdetails%5D%28https%3A//github.com/better-auth/better-auth/issues/5459%29%29.%20Before%20deploying%2C%20consider%20any%20audit%20or%20compliance%20requirements%20for%20token%20retention.%0A%0AIf%20you%20make%20this%20change%2C%20you%20may%20want%20to%20review%20other%20flows%20%28like%20OIDC%20and%20session/logout%29%20for%20consistency.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/6423)
Author
Owner

@dvanmali commented on GitHub (Dec 24, 2025):

Hi all, we released the new OAuth Provider Plugin which deletes revoked tokens. Feel free to let us know how it works :)

@dvanmali commented on GitHub (Dec 24, 2025): Hi all, we released the new [OAuth Provider Plugin](https://www.better-auth.com/docs/plugins/oauth-provider) which deletes revoked tokens. Feel free to let us know how it works :)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2425