Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1648,23 +1648,24 @@ LEVEL = Info
;; - <username>.livejournal.com
;;
;; Whether to allow signin in via OpenID
;ENABLE_OPENID_SIGNIN = true
;ENABLE_OPENID_SIGNIN = false
;;
;; Whether to allow registering via OpenID
;; Do not include to rely on rhw DISABLE_REGISTRATION setting
;;ENABLE_OPENID_SIGNUP = true
;;
;; Allowed URI patterns (POSIX regexp).
;; Space separated.
;; Only these would be allowed if non-blank.
;; Example value: trusted.domain.org trusted.domain.net
;; Allowed OpenID provider hosts (host matcher).
;; Space or comma separated.
;; Only these would be allowed if non-blank. Matches the host portion only.
;; Example value: trusted.domain.org *.trusted.domain.net
;WHITELISTED_URIS =
;;
;; Forbidden URI patterns (POSIX regexp).
;; Space separated.
;; Blocked OpenID provider hosts (host matcher).
;; Space or comma separated.
;; Only used if WHITELISTED_URIS is blank.
;; Example value: loadaverage.org/badguy stackexchange.com/.*spammer
;BLACKLISTED_URIS =
;; Built-in: loopback (localhost), private (LAN/intranet), external (public hosts), * (all hosts)
;; Default value blocks localhost, loopback, private, and IPv6 link-local to avoid SSRF.
;BLACKLISTED_URIS = localhost loopback private 169.254.0.0/16 fe80::/10

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
63 changes: 63 additions & 0 deletions modules/setting/openid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package setting

import (
"strings"

"code.gitea.io/gitea/modules/hostmatcher"
)

// OpenID settings
var OpenID = struct {
EnableSignIn bool
EnableSignUp bool
Allowlist *hostmatcher.HostMatchList
Blocklist *hostmatcher.HostMatchList
}{
EnableSignIn: false,
EnableSignUp: false,
Allowlist: nil,
Blocklist: nil,
}

var defaultOpenIDBlocklist = []string{
"localhost",
hostmatcher.MatchBuiltinLoopback,
hostmatcher.MatchBuiltinPrivate,
"169.254.0.0/16",
"fe80::/10",
}

func splitOpenIDHostList(raw string) []string {
return strings.FieldsFunc(raw, func(r rune) bool {
switch r {
case ',', ' ', '\t', '\n', '\r':
return true
default:
return false
}
})
}

// loadOpenIDSetting loads OpenID settings from rootCfg, depends on service settings,
// so should be called after loadServiceSettings.
func loadOpenIDSetting(rootCfg ConfigProvider) {
sec := rootCfg.Section("openid")
OpenID.EnableSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(false)
OpenID.EnableSignUp = sec.Key("ENABLE_OPENID_SIGNUP").MustBool(!Service.DisableRegistration && OpenID.EnableSignIn)
OpenID.Allowlist = nil
OpenID.Blocklist = nil
allowlist := splitOpenIDHostList(sec.Key("WHITELISTED_URIS").String())
if len(allowlist) != 0 {
OpenID.Allowlist = hostmatcher.ParseHostMatchList("openid.WHITELISTED_URIS", strings.Join(allowlist, ","))
}
blocklist := splitOpenIDHostList(sec.Key("BLACKLISTED_URIS").String())
if len(blocklist) == 0 {
blocklist = defaultOpenIDBlocklist
}
Comment thread
lunny marked this conversation as resolved.
if len(blocklist) != 0 {
OpenID.Blocklist = hostmatcher.ParseHostMatchList("openid.BLACKLISTED_URIS", strings.Join(blocklist, ","))
}
}
73 changes: 73 additions & 0 deletions modules/setting/openid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package setting

import (
"testing"

"code.gitea.io/gitea/modules/test"

"github.com/stretchr/testify/assert"
)

func TestLoadOpenIDSettings(t *testing.T) {
defer test.MockVariableValue(&Service)()

t.Run("DefaultBlacklist", func(t *testing.T) {
cfg, err := NewConfigProviderFromData(`
[openid]
`)
assert.NoError(t, err)
loadServiceFrom(cfg)

assert.False(t, OpenID.EnableSignIn)
assert.False(t, OpenID.EnableSignUp)
assert.Nil(t, OpenID.Allowlist)
if assert.NotNil(t, OpenID.Blocklist) {
assert.True(t, OpenID.Blocklist.MatchHostName("localhost"))
assert.True(t, OpenID.Blocklist.MatchHostName("127.0.0.1"))
assert.True(t, OpenID.Blocklist.MatchHostName("192.168.0.1"))
assert.True(t, OpenID.Blocklist.MatchHostName("fe80::1"))
assert.False(t, OpenID.Blocklist.MatchHostName("example.com"))
}
})

t.Run("AllowlistParsing", func(t *testing.T) {
cfg, err := NewConfigProviderFromData(`
[openid]
ENABLE_OPENID_SIGNIN = true
WHITELISTED_URIS = example.com, *.trusted.example
`)
assert.NoError(t, err)
loadServiceFrom(cfg)

assert.True(t, OpenID.EnableSignIn)
assert.True(t, OpenID.EnableSignUp)
if assert.NotNil(t, OpenID.Allowlist) {
assert.True(t, OpenID.Allowlist.MatchHostName("example.com"))
assert.True(t, OpenID.Allowlist.MatchHostName("foo.trusted.example"))
assert.False(t, OpenID.Allowlist.MatchHostName("example.org"))
}
if assert.NotNil(t, OpenID.Blocklist) {
assert.True(t, OpenID.Blocklist.MatchHostName("localhost"))
}
})

t.Run("BlocklistParsing", func(t *testing.T) {
cfg, err := NewConfigProviderFromData(`
[openid]
BLACKLISTED_URIS = bad.example.com, 10.0.0.0/8
`)
assert.NoError(t, err)
loadServiceFrom(cfg)

assert.Nil(t, OpenID.Allowlist)
if assert.NotNil(t, OpenID.Blocklist) {
assert.True(t, OpenID.Blocklist.MatchHostName("bad.example.com"))
assert.True(t, OpenID.Blocklist.MatchHostName("10.1.1.1"))
assert.False(t, OpenID.Blocklist.MatchHostName("localhost"))
assert.False(t, OpenID.Blocklist.MatchHostName("good.example.com"))
}
})
}
27 changes: 0 additions & 27 deletions modules/setting/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package setting

import (
"regexp"
"runtime"
"strings"
"time"
Expand Down Expand Up @@ -85,12 +84,6 @@ var Service = struct {
UserDeleteWithCommentsMaxTime time.Duration
ValidSiteURLSchemes []string

// OpenID settings
EnableOpenIDSignIn bool
EnableOpenIDSignUp bool
OpenIDWhitelist []*regexp.Regexp
OpenIDBlacklist []*regexp.Regexp

// Explore page settings
Explore struct {
RequireSigninView bool `ini:"REQUIRE_SIGNIN_VIEW"`
Expand Down Expand Up @@ -265,26 +258,6 @@ func loadServiceFrom(rootCfg ConfigProvider) {
loadQosSetting(rootCfg)
}

func loadOpenIDSetting(rootCfg ConfigProvider) {
sec := rootCfg.Section("openid")
Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(!InstallLock)
Service.EnableOpenIDSignUp = sec.Key("ENABLE_OPENID_SIGNUP").MustBool(!Service.DisableRegistration && Service.EnableOpenIDSignIn)
pats := sec.Key("WHITELISTED_URIS").Strings(" ")
if len(pats) != 0 {
Service.OpenIDWhitelist = make([]*regexp.Regexp, len(pats))
for i, p := range pats {
Service.OpenIDWhitelist[i] = regexp.MustCompilePOSIX(p)
}
}
pats = sec.Key("BLACKLISTED_URIS").Strings(" ")
if len(pats) != 0 {
Service.OpenIDBlacklist = make([]*regexp.Regexp, len(pats))
for i, p := range pats {
Service.OpenIDBlacklist[i] = regexp.MustCompilePOSIX(p)
}
}
}

func loadQosSetting(rootCfg ConfigProvider) {
sec := rootCfg.Section("qos")
Service.QoS.Enabled = sec.Key("ENABLED").MustBool(false)
Expand Down
2 changes: 1 addition & 1 deletion modules/web/middleware/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func CommonTemplateContextData() reqctx.ContextData {
"DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives,

"EnableSwagger": setting.API.EnableSwagger,
"EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn,
"EnableOpenIDSignIn": setting.OpenID.EnableSignIn,
"PageStartTime": time.Now(),

"RunModeIsProd": setting.IsProd,
Expand Down
4 changes: 2 additions & 2 deletions routers/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ func Install(ctx *context.Context) {
form.RegisterConfirm = setting.Service.RegisterEmailConfirm
form.MailNotify = setting.Service.EnableNotifyMail

form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn
form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp
form.EnableOpenIDSignIn = setting.OpenID.EnableSignIn
form.EnableOpenIDSignUp = setting.OpenID.EnableSignUp
form.DisableRegistration = setting.Service.DisableRegistration
form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
form.EnableCaptcha = setting.Service.EnableCaptcha
Expand Down
1 change: 1 addition & 0 deletions routers/web/admin/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ func Config(ctx *context.Context) {
ctx.Data["LFS"] = setting.LFS

ctx.Data["Service"] = setting.Service
ctx.Data["OpenID"] = setting.OpenID
ctx.Data["DbCfg"] = setting.Database
ctx.Data["Webhook"] = setting.Webhook
ctx.Data["MailerEnabled"] = false
Expand Down
39 changes: 22 additions & 17 deletions routers/web/auth/openid.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,25 +44,30 @@ func SignInOpenID(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplSignInOpenID)
}

// Check if the given OpenID URI is allowed by blacklist/whitelist
// Check if the given OpenID URI is allowed by blocklist/allowlist
func allowedOpenIDURI(uri string) (err error) {
parsed, err := url.Parse(uri)
if err != nil {
return err
}
host := parsed.Hostname()
if host == "" {
return errors.New("invalid OpenID URI host")
}

// In case a Whitelist is present, URI must be in it
// in order to be accepted
if len(setting.Service.OpenIDWhitelist) != 0 {
for _, pat := range setting.Service.OpenIDWhitelist {
if pat.MatchString(uri) {
return nil // pass
}
if allowList := setting.OpenID.Allowlist; allowList != nil && !allowList.IsEmpty() {
if allowList.MatchHostName(host) {
return nil // pass
}
// must match one of this or be refused
return errors.New("URI not allowed by whitelist")
return errors.New("URI not allowed by allowlist")
}

// A blacklist match expliclty forbids
for _, pat := range setting.Service.OpenIDBlacklist {
if pat.MatchString(uri) {
return errors.New("URI forbidden by blacklist")
}
// A blocklist match expliclty forbids
if blockList := setting.OpenID.Blocklist; blockList != nil && blockList.MatchHostName(host) {
return errors.New("URI forbidden by blocklist")
}

return nil
Expand Down Expand Up @@ -222,7 +227,7 @@ func signInOpenIDVerify(ctx *context.Context) {
return
}

if u != nil || !setting.Service.EnableOpenIDSignUp || setting.Service.AllowOnlyInternalRegistration {
if u != nil || !setting.OpenID.EnableSignUp || setting.Service.AllowOnlyInternalRegistration {
ctx.Redirect(setting.AppSubURL + "/user/openid/connect")
} else {
ctx.Redirect(setting.AppSubURL + "/user/openid/register")
Expand All @@ -239,7 +244,7 @@ func ConnectOpenID(ctx *context.Context) {
ctx.Data["Title"] = "OpenID connect"
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsOpenIDConnect"] = true
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
ctx.Data["EnableOpenIDSignUp"] = setting.OpenID.EnableSignUp
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["OpenID"] = oid
userName, _ := ctx.Session.Get("openid_determined_username").(string)
Expand All @@ -260,7 +265,7 @@ func ConnectOpenIDPost(ctx *context.Context) {
ctx.Data["Title"] = "OpenID connect"
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsOpenIDConnect"] = true
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
ctx.Data["EnableOpenIDSignUp"] = setting.OpenID.EnableSignUp
ctx.Data["OpenID"] = oid

u, _, err := auth.UserSignIn(ctx, form.UserName, form.Password)
Expand Down Expand Up @@ -297,7 +302,7 @@ func RegisterOpenID(ctx *context.Context) {
ctx.Data["Title"] = "OpenID signup"
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsOpenIDRegister"] = true
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
ctx.Data["EnableOpenIDSignUp"] = setting.OpenID.EnableSignUp
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
ctx.Data["Captcha"] = context.GetImageCaptcha()
Expand Down Expand Up @@ -332,7 +337,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
ctx.Data["Title"] = "OpenID signup"
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsOpenIDRegister"] = true
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
ctx.Data["EnableOpenIDSignUp"] = setting.OpenID.EnableSignUp
context.SetCaptchaData(ctx)
ctx.Data["OpenID"] = oid

Expand Down
44 changes: 44 additions & 0 deletions routers/web/auth/openid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package auth

import (
"testing"

"code.gitea.io/gitea/modules/hostmatcher"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"

"github.com/stretchr/testify/assert"
)

func TestAllowedOpenIDURI(t *testing.T) {
defer test.MockVariableValue(&setting.Service)()

t.Run("Whitelist", func(t *testing.T) {
setting.OpenID.Allowlist = hostmatcher.ParseHostMatchList("openid.WHITELISTED_URIS", "trusted.example.com,*.trusted.net")
setting.OpenID.Blocklist = hostmatcher.ParseHostMatchList("openid.BLACKLISTED_URIS", "trusted.example.com,bad.example.com")

assert.NoError(t, allowedOpenIDURI("https://trusted.example.com/openid"))
assert.NoError(t, allowedOpenIDURI("https://sub.trusted.net"))
assert.Error(t, allowedOpenIDURI("https://trusted.example.com.evil.org"))
assert.Error(t, allowedOpenIDURI("https://bad.example.com"))
})

t.Run("Blacklist", func(t *testing.T) {
setting.OpenID.Allowlist = nil
setting.OpenID.Blocklist = hostmatcher.ParseHostMatchList("openid.BLACKLISTED_URIS", "bad.example.com,10.0.0.0/8")

assert.Error(t, allowedOpenIDURI("https://bad.example.com"))
assert.Error(t, allowedOpenIDURI("https://10.1.1.1"))
assert.NoError(t, allowedOpenIDURI("https://good.example.com"))
})

t.Run("InvalidURI", func(t *testing.T) {
setting.OpenID.Allowlist = nil
setting.OpenID.Blocklist = nil

assert.Error(t, allowedOpenIDURI("://bad"))
})
}
4 changes: 2 additions & 2 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,14 +299,14 @@ func registerWebRoutes(m *web.Router) {
validation.AddBindingRules()

openIDSignInEnabled := func(ctx *context.Context) {
if !setting.Service.EnableOpenIDSignIn {
if !setting.OpenID.EnableSignIn {
ctx.HTTPError(http.StatusForbidden)
return
}
}

openIDSignUpEnabled := func(ctx *context.Context) {
if !setting.Service.EnableOpenIDSignUp {
if !setting.OpenID.EnableSignUp {
ctx.HTTPError(http.StatusForbidden)
return
}
Expand Down
Loading