fix: remove trailing slash from the issuer in oauth claims (#8028)

- Trim the ending slash '/' from the URL used in the OpenID Connect "well_known" endpoint and in the JWT tokens issued by Forgejo.
- This makes it compliant with the OpenID specification. https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
- Resolves #7941

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8028
Reviewed-by: Lucas <sclu1034@noreply.codeberg.org>
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: jmaasing <jmaasing@noreply.codeberg.org>
Co-committed-by: jmaasing <jmaasing@noreply.codeberg.org>
This commit is contained in:
jmaasing 2025-06-10 20:46:17 +02:00 committed by Gusted
parent 9b6e3b61cf
commit 5391f43888
4 changed files with 20 additions and 4 deletions

View file

@ -225,7 +225,7 @@ func newAccessTokenResponse(ctx go_context.Context, grant *auth.OAuth2Grant, ser
idToken := &oauth2.OIDCToken{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationDate.AsTime()),
Issuer: setting.AppURL,
Issuer: strings.TrimSuffix(setting.AppURL, "/"),
Audience: []string{app.ClientID},
Subject: fmt.Sprint(grant.UserID),
},
@ -409,7 +409,7 @@ func IntrospectOAuth(ctx *context.Context) {
if err == nil && app != nil {
response.Active = true
response.Scope = grant.Scope
response.Issuer = setting.AppURL
response.Issuer = strings.TrimSuffix(setting.AppURL, "/")
response.Audience = []string{app.ClientID}
response.Subject = fmt.Sprint(grant.UserID)
}
@ -669,6 +669,7 @@ func GrantApplicationOAuth(ctx *context.Context) {
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
func OIDCWellKnown(ctx *context.Context) {
ctx.Data["SigningKey"] = oauth2.DefaultSigningKey
ctx.Data["Issuer"] = strings.TrimSuffix(setting.AppURL, "/")
ctx.JSONTemplate("user/auth/oidc_wellknown")
}

View file

@ -51,6 +51,7 @@ func TestNewAccessTokenResponse_OIDCToken(t *testing.T) {
// Scopes: openid
oidcToken := createAndParseToken(t, grants[0])
assert.Equal(t, "https://try.gitea.io", oidcToken.RegisteredClaims.Issuer)
assert.Empty(t, oidcToken.Name)
assert.Empty(t, oidcToken.PreferredUsername)
assert.Empty(t, oidcToken.Profile)
@ -67,6 +68,7 @@ func TestNewAccessTokenResponse_OIDCToken(t *testing.T) {
// Scopes: openid profile email
oidcToken = createAndParseToken(t, grants[0])
assert.Equal(t, "https://try.gitea.io", oidcToken.RegisteredClaims.Issuer)
assert.Equal(t, "User Five", oidcToken.Name)
assert.Equal(t, "user5", oidcToken.PreferredUsername)
assert.Equal(t, "https://try.gitea.io/user5", oidcToken.Profile)

View file

@ -1,5 +1,5 @@
{
"issuer": "{{AppUrl | JSEscape}}",
"issuer": "{{.Issuer | JSEscape}}",
"authorization_endpoint": "{{AppUrl | JSEscape}}login/oauth/authorize",
"token_endpoint": "{{AppUrl | JSEscape}}login/oauth/access_token",
"jwks_uri": "{{AppUrl | JSEscape}}login/oauth/keys",

View file

@ -632,6 +632,19 @@ func TestSignInOAuthCallbackPKCE(t *testing.T) {
})
}
func TestWellKnownDocumentIssuerDoesNotEndWithASlash(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/.well-known/openid-configuration")
resp := MakeRequest(t, req, http.StatusOK)
type response struct {
Issuer string `json:"issuer"`
}
parsed := new(response)
DecodeJSON(t, resp, parsed)
assert.Equal(t, strings.TrimSuffix(setting.AppURL, "/"), parsed.Issuer)
}
func TestSignInOAuthCallbackRedirectToEscaping(t *testing.T) {
defer tests.PrepareTestEnv(t)()
@ -697,7 +710,7 @@ func setupMockOIDCServer() *httptest.Server {
case "/.well-known/openid-configuration":
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{
"issuer": "` + mockServer.URL + `",
"issuer": "` + strings.TrimSuffix(mockServer.URL, "/") + `",
"authorization_endpoint": "` + mockServer.URL + `/authorize",
"token_endpoint": "` + mockServer.URL + `/token",
"userinfo_endpoint": "` + mockServer.URL + `/userinfo"