From 4f44317622c662391f17945886e36a07ff0115cc Mon Sep 17 00:00:00 2001 From: forgejo-backport-action Date: Thu, 30 Apr 2026 20:10:43 +0200 Subject: [PATCH] [v15.0/forgejo] fix(oauth): only accept refresh tokens as refresh tokens (#12354) **Backport:** https://codeberg.org/forgejo/forgejo/pulls/12291 `handleRefreshToken` never checked `token.Type == TypeRefreshToken`. When `InvalidateRefreshTokens` is disabled, an access token could be submitted as a `refresh_token` and exchanged for a new token pair. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes (can be removed for JavaScript changes) - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [x] `make pr-go` before pushing ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [x] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. *The decision if the pull request will be shown in the release notes is up to the mergers / release team.* The content of the `release-notes/.md` file will serve as the basis for the release notes. If the file does not exist, the title of the pull request will be used instead. Co-authored-by: jvoisin Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12354 Reviewed-by: Mathieu Fenniak --- routers/web/auth/oauth.go | 10 ++++++++++ tests/integration/oauth_test.go | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index eba2b441c4..83be4eb154 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -792,6 +792,16 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, server }) return } + + // Reject tokens that are not refresh tokens (e.g. access tokens submitted as refresh tokens) + if token.Type != oauth2.TypeRefreshToken { + handleAccessTokenError(ctx, AccessTokenErrorResponse{ + ErrorCode: AccessTokenErrorCodeUnauthorizedClient, + ErrorDescription: "token is not a refresh token", + }) + return + } + // get grant before increasing counter grant, err := auth.GetOAuth2GrantByID(ctx, token.GrantID) if err != nil || grant == nil { diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index 27c66f0e5a..3f14324cf7 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -455,6 +455,18 @@ func TestRefreshTokenInvalidation(t *testing.T) { assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode)) assert.Equal(t, "unable to parse refresh token", parsedError.ErrorDescription) + req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ + "grant_type": "refresh_token", + "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138", + "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=", + "redirect_uri": "a", + "refresh_token": parsed.AccessToken, + }) + resp = MakeRequest(t, req, http.StatusBadRequest) + require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &parsedError)) + assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode)) + assert.Equal(t, "token is not a refresh token", parsedError.ErrorDescription) + req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ "grant_type": "refresh_token", "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",