[GH-ISSUE #6423] Expired and Rotated OAuth Tokens Are Not Being Deleted in MCP Flow #27834

Closed
opened 2026-04-17 19:03:46 -05:00 by GiteaMirror · 6 comments
Owner

Originally created by @Ridhim-RR on GitHub (Nov 30, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/6423

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). Original GitHub issue: https://github.com/better-auth/better-auth/issues/6423 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 databasebugidentity labels 2026-04-17 19:03:46 -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

<!-- gh-comment-id:3592557782 --> @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

<!-- gh-comment-id:3592557921 --> @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

<!-- gh-comment-id:3592558086 --> @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

<!-- gh-comment-id:3592559651 --> @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 :)

<!-- gh-comment-id:3688642656 --> @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 :)
Author
Owner

@dosubot[bot] commented on GitHub (Mar 25, 2026):

Hi, @Ridhim-RR. I'm Dosu, and I'm helping the better-auth team manage their backlog and am marking this issue as stale.

Issue Summary:

  • You reported that expired refresh tokens were not deleted in the MCP token refresh flow, leading to token accumulation and security concerns.
  • It was confirmed this issue affected both MCP and OIDC flows.
  • Suggestions and example code were provided to implement a cleanup step deleting old tokens after issuing new ones.
  • An open issue (#5459) targeted OAuth 2.1 compliance, including one-time use refresh tokens and invalidation.
  • The maintainer released a new OAuth Provider Plugin that deletes revoked tokens, addressing the problem.

Next Steps:

  • Please confirm if this issue remains relevant with the latest OAuth Provider Plugin release.
  • If it is still a concern, feel free to comment to keep the discussion open; otherwise, I will close this issue automatically in 7 days.

Thank you for your understanding and contribution!

<!-- gh-comment-id:4127833877 --> @dosubot[bot] commented on GitHub (Mar 25, 2026): Hi, @Ridhim-RR. I'm [Dosu](https://dosu.dev), and I'm helping the better-auth team manage their backlog and am marking this issue as stale. **Issue Summary:** - You reported that expired refresh tokens were not deleted in the MCP token refresh flow, leading to token accumulation and security concerns. - It was confirmed this issue affected both MCP and OIDC flows. - Suggestions and example code were provided to implement a cleanup step deleting old tokens after issuing new ones. - An open issue (#5459) targeted OAuth 2.1 compliance, including one-time use refresh tokens and invalidation. - The maintainer released a new OAuth Provider Plugin that deletes revoked tokens, addressing the problem. **Next Steps:** - Please confirm if this issue remains relevant with the latest OAuth Provider Plugin release. - If it is still a concern, feel free to comment to keep the discussion open; otherwise, I will close this issue automatically in 7 days. Thank you for your understanding and contribution!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#27834