From 327f2207dce46c96d01e3109564ef61d2c87f008 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Tue, 11 Nov 2025 00:12:25 +0800 Subject: [PATCH] Make OAuth2 issuer configurable (#35915) (#35916) Backport #35915 by wxiaoguang --- custom/conf/app.example.ini | 5 ++++ modules/setting/oauth2.go | 1 + services/oauth2_provider/access_token.go | 6 +++- tests/integration/oauth_test.go | 36 ++++++++++++++++-------- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 5fee78af54..33bfe752a0 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -567,6 +567,11 @@ ENABLED = true ;; Alternative location to specify OAuth2 authentication secret. You cannot specify both this and JWT_SECRET, and must pick one ;JWT_SECRET_URI = file:/etc/gitea/oauth2_jwt_secret ;; +;; The "issuer" claim identifies the principal that issued the JWT. +;; Gitea 1.25 makes it default to "ROOT_URL without the last slash" to follow the standard. +;; If you have old logins from before 1.25, you may want to set it to the old (non-standard) value "ROOT_URL with the last slash". +;JWT_CLAIM_ISSUER = +;; ;; Lifetime of an OAuth2 access token in seconds ;ACCESS_TOKEN_EXPIRATION_TIME = 3600 ;; diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 1a88f3cb08..ae2a9d7bee 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -96,6 +96,7 @@ var OAuth2 = struct { InvalidateRefreshTokens bool JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"` JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"` + JWTClaimIssuer string `ini:"JWT_CLAIM_ISSUER"` MaxTokenLength int DefaultApplications []string }{ diff --git a/services/oauth2_provider/access_token.go b/services/oauth2_provider/access_token.go index dce4ac765b..3a77c86d9e 100644 --- a/services/oauth2_provider/access_token.go +++ b/services/oauth2_provider/access_token.go @@ -112,8 +112,12 @@ func NewJwtRegisteredClaimsFromUser(clientID string, grantUserID int64, exp *jwt // to retrieve the configuration information. This MUST also be identical to the "iss" Claim value in ID Tokens issued from this Issuer. // * https://accounts.google.com/.well-known/openid-configuration // * https://github.com/login/oauth/.well-known/openid-configuration + issuer := setting.OAuth2.JWTClaimIssuer + if issuer == "" { + issuer = strings.TrimSuffix(setting.AppURL, "/") + } return jwt.RegisteredClaims{ - Issuer: strings.TrimSuffix(setting.AppURL, "/"), + Issuer: issuer, Audience: []string{clientID}, Subject: strconv.FormatInt(grantUserID, 10), ExpiresAt: exp, diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index eab95ba688..e7edace653 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -919,20 +919,32 @@ func TestOAuth_GrantScopesClaimAllGroups(t *testing.T) { } func testOAuth2WellKnown(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")() urlOpenidConfiguration := "/.well-known/openid-configuration" - defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")() - req := NewRequest(t, "GET", urlOpenidConfiguration) - resp := MakeRequest(t, req, http.StatusOK) - var respMap map[string]any - DecodeJSON(t, resp, &respMap) - assert.Equal(t, "https://try.gitea.io", respMap["issuer"]) - assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"]) - assert.Equal(t, "https://try.gitea.io/login/oauth/access_token", respMap["token_endpoint"]) - assert.Equal(t, "https://try.gitea.io/login/oauth/keys", respMap["jwks_uri"]) - assert.Equal(t, "https://try.gitea.io/login/oauth/userinfo", respMap["userinfo_endpoint"]) - assert.Equal(t, "https://try.gitea.io/login/oauth/introspect", respMap["introspection_endpoint"]) - assert.Equal(t, []any{"RS256"}, respMap["id_token_signing_alg_values_supported"]) + t.Run("WellKnown", func(t *testing.T) { + req := NewRequest(t, "GET", urlOpenidConfiguration) + resp := MakeRequest(t, req, http.StatusOK) + var respMap map[string]any + DecodeJSON(t, resp, &respMap) + assert.Equal(t, "https://try.gitea.io", respMap["issuer"]) + assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"]) + assert.Equal(t, "https://try.gitea.io/login/oauth/access_token", respMap["token_endpoint"]) + assert.Equal(t, "https://try.gitea.io/login/oauth/keys", respMap["jwks_uri"]) + assert.Equal(t, "https://try.gitea.io/login/oauth/userinfo", respMap["userinfo_endpoint"]) + assert.Equal(t, "https://try.gitea.io/login/oauth/introspect", respMap["introspection_endpoint"]) + assert.Equal(t, []any{"RS256"}, respMap["id_token_signing_alg_values_supported"]) + }) + + t.Run("WellKnownWithIssuer", func(t *testing.T) { + defer test.MockVariableValue(&setting.OAuth2.JWTClaimIssuer, "https://try.gitea.io/")() + req := NewRequest(t, "GET", urlOpenidConfiguration) + resp := MakeRequest(t, req, http.StatusOK) + var respMap map[string]any + DecodeJSON(t, resp, &respMap) + assert.Equal(t, "https://try.gitea.io/", respMap["issuer"]) // has trailing by JWTClaimIssuer + assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"]) + }) defer test.MockVariableValue(&setting.OAuth2.Enabled, false)() MakeRequest(t, NewRequest(t, "GET", urlOpenidConfiguration), http.StatusNotFound)