Skip to content

Commit 3cbf90d

Browse files
committed
implements #1007 - Option to
disable local login when OIDC is enabled - Add GOTIFY_LOCALAUTH_ENABLED, defaulting to true - Block local login and Basic Auth when disabled - Expose local auth state to the UI and hide local login flows - Skip default local admin creation when local auth is disabled - Require either local auth or OIDC at startup
1 parent c27a381 commit 3cbf90d

15 files changed

Lines changed: 170 additions & 89 deletions

app.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ func serve(vInfo *model.VersionInfo) int {
8585
conf, futureLogs := config.Get()
8686
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339, NoColor: noColor(conf.NoColor)}).Level(zerolog.Level(conf.LogLevel))
8787
log.Info().Str("version", vInfo.Version).Str("build_date", BuildDate).Msg("Gotify")
88+
if !conf.LocalAuthEnabled && !conf.OIDC.Enabled {
89+
log.Fatal().Msg("either local authentication or OIDC must be enabled")
90+
}
8891

8992
exit := false
9093
for _, futureLog := range futureLogs {
@@ -106,7 +109,7 @@ func serve(vInfo *model.VersionInfo) int {
106109
return 1
107110
}
108111

109-
db, err := database.New(conf.Database.Dialect, conf.Database.Connection, conf.DefaultUser.Name, conf.DefaultUser.Pass, conf.PassStrength, true, time.Now)
112+
db, err := database.New(conf.Database.Dialect, conf.Database.Connection, conf.DefaultUser.Name, conf.DefaultUser.Pass, conf.PassStrength, conf.LocalAuthEnabled, time.Now)
110113
if err != nil {
111114
log.Error().Err(err).Msg("Cannot initialize database")
112115
return 1

auth/authentication.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ type Database interface {
3939

4040
// Auth is the provider for authentication middleware.
4141
type Auth struct {
42-
DB Database
43-
SecureCookie bool
44-
CrossOrigin *http.CrossOriginProtection
42+
DB Database
43+
SecureCookie bool
44+
LocalAuthEnabled bool
45+
CrossOrigin *http.CrossOriginProtection
4546
}
4647

4748
// RequireAdmin requires an elevated client token or basic auth, the user must be an admin.
@@ -146,6 +147,9 @@ func (a *Auth) rejectForeignOrigin(ctx *gin.Context) bool {
146147

147148
func (a *Auth) handleUser(checks ...func(*model.User) (authState, error)) func(ctx *gin.Context) (authState, error) {
148149
return func(ctx *gin.Context) (authState, error) {
150+
if !a.LocalAuthEnabled {
151+
return authStateSkip, nil
152+
}
149153
if name, pass, ok := ctx.Request.BasicAuth(); ok {
150154
if user, err := a.DB.GetUserByName(name); err != nil {
151155
return authStateSkip, err

auth/authentication_test.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ type AuthenticationSuite struct {
2929
func (s *AuthenticationSuite) SetupSuite() {
3030
mode.Set(mode.TestDev)
3131
s.DB = testdb.NewDB(s.T())
32-
s.auth = &Auth{DB: s.DB, CrossOrigin: http.NewCrossOriginProtection()}
32+
s.auth = &Auth{DB: s.DB, LocalAuthEnabled: true, CrossOrigin: http.NewCrossOriginProtection()}
3333

3434
now := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
3535
timeNow = func() time.Time { return now }
@@ -270,6 +270,15 @@ func (s *AuthenticationSuite) TestBasicAuth() {
270270
s.assertHeaderRequest("Authorization", "Basic bm90ZXhpc3Rpbmc6cHc=", s.auth.RequireElevatedClient, 401)
271271
}
272272

273+
func (s *AuthenticationSuite) TestBasicAuthDisabled() {
274+
s.auth.LocalAuthEnabled = false
275+
defer func() { s.auth.LocalAuthEnabled = true }()
276+
277+
s.assertHeaderRequest("Authorization", "Basic YWRtaW46cHc=", s.auth.RequireClient, 401)
278+
s.assertHeaderRequest("Authorization", "Basic YWRtaW46cHc=", s.auth.RequireAdmin, 401)
279+
s.assertHeaderRequest("Authorization", "Basic YWRtaW46cHc=", s.auth.RequireElevatedClient, 401)
280+
}
281+
273282
func (s *AuthenticationSuite) TestOptionalAuth() {
274283
// various invalid users
275284
ctx := s.assertQueryRequest("token", "ergerogerg", s.auth.Optional, 200)

config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ type Configuration struct {
7979
UploadedImagesDir string
8080
PluginsDir string
8181
Registration bool
82+
LocalAuthEnabled bool
8283
OIDC OIDC
8384
NoColor string
8485
}
@@ -111,6 +112,7 @@ func Get() (*Configuration, []FutureLog) {
111112
PassStrength: 10,
112113
UploadedImagesDir: "data/images",
113114
PluginsDir: "data/plugins",
115+
LocalAuthEnabled: true,
114116
OIDC: OIDC{
115117
UsernameClaim: "preferred_username",
116118
AutoRegister: true,
@@ -167,6 +169,7 @@ func Get() (*Configuration, []FutureLog) {
167169
add(parseString(&c.UploadedImagesDir, EnvUploadedImagesDir))
168170
add(parseString(&c.PluginsDir, EnvPluginsDir))
169171
add(parseBool(&c.Registration, EnvRegistration))
172+
add(parseBool(&c.LocalAuthEnabled, EnvLocalAuthEnabled))
170173

171174
add(parseBool(&c.OIDC.Enabled, EnvOIDCEnabled))
172175
add(parseString(&c.OIDC.Issuer, EnvOIDCIssuer))

config/config_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func TestConfigEnv(t *testing.T) {
2020
os.Setenv("GOTIFY_SERVER_CORS_ALLOWMETHODS", "GET,POST")
2121
os.Setenv("GOTIFY_SERVER_CORS_ALLOWHEADERS", "Authorization,content-type")
2222
os.Setenv("GOTIFY_SERVER_STREAM_ALLOWEDORIGINS", ".+.example.com,otherdomain.com")
23+
t.Setenv(EnvLocalAuthEnabled, "false")
2324

2425
defer func() {
2526
os.Unsetenv("GOTIFY_DEFAULTUSER_NAME")
@@ -41,6 +42,7 @@ func TestConfigEnv(t *testing.T) {
4142
assert.Equal(t, []string{"GET", "POST"}, conf.Server.Cors.AllowMethods)
4243
assert.Equal(t, []string{"Authorization", "content-type"}, conf.Server.Cors.AllowHeaders)
4344
assert.Equal(t, []string{".+.example.com", "otherdomain.com"}, conf.Server.Stream.AllowedOrigins)
45+
assert.False(t, conf.LocalAuthEnabled)
4446
}
4547

4648
func TestFile(t *testing.T) {

config/keys.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const (
4040
EnvOIDCRedirectURL = "GOTIFY_OIDC_REDIRECTURL"
4141
EnvOIDCAutoRegister = "GOTIFY_OIDC_AUTOREGISTER"
4242
EnvOIDCLinkByUsername = "GOTIFY_OIDC_LINK_BY_USERNAME"
43+
EnvLocalAuthEnabled = "GOTIFY_LOCALAUTH_ENABLED"
4344
EnvOIDCScopes = "GOTIFY_OIDC_SCOPES"
4445
EnvNoColor = "NOCOLOR"
4546
)

docs/spec.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2940,9 +2940,16 @@
29402940
"required": [
29412941
"version",
29422942
"register",
2943+
"localauth",
29432944
"oidc"
29442945
],
29452946
"properties": {
2947+
"localauth": {
2948+
"description": "If local authentication is enabled.",
2949+
"type": "boolean",
2950+
"x-go-name": "LocalAuth",
2951+
"example": true
2952+
},
29462953
"oidc": {
29472954
"description": "If oidc is enabled.",
29482955
"type": "boolean",

gotify-server.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,13 @@
224224
# Type: text-list
225225
# GOTIFY_OIDC_SCOPES=openid,profile,email
226226

227+
# Enable authentication via username and password.
228+
# At least one of GOTIFY_LOCALAUTH_ENABLED or GOTIFY_OIDC_ENABLED must be set to
229+
# true to allow users to login. Otherwise the server will refuse to start.
230+
#
231+
# Type: boolean
232+
# GOTIFY_LOCALAUTH_ENABLED=true
233+
227234
# Database driver to use. For mysql and postgres the target database must
228235
# already exist and the configured user must have sufficient permissions.
229236
#

model/gotifyinfo.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ type GotifyInfo struct {
1414
// required: true
1515
// example: true
1616
Register bool `json:"register"`
17+
// If local authentication is enabled.
18+
//
19+
// required: true
20+
// example: true
21+
LocalAuth bool `json:"localauth"`
1722
// If oidc is enabled.
1823
//
1924
// required: true

router/router.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,10 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
8585
}
8686
}()
8787
authentication := auth.Auth{
88-
DB: db,
89-
SecureCookie: conf.Server.SecureCookie,
90-
CrossOrigin: http.NewCrossOriginProtection(),
88+
DB: db,
89+
SecureCookie: conf.Server.SecureCookie,
90+
LocalAuthEnabled: conf.LocalAuthEnabled,
91+
CrossOrigin: http.NewCrossOriginProtection(),
9192
}
9293
messageHandler := api.MessageAPI{Notifier: streamHandler, DB: db}
9394
healthHandler := api.HealthAPI{DB: db}
@@ -118,7 +119,7 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
118119
userChangeNotifier.OnUserDeleted(pluginManager.RemoveUser)
119120
userChangeNotifier.OnUserAdded(pluginManager.InitializeForUserID)
120121

121-
ui.Register(g, *vInfo, conf.Registration, conf.OIDC.Enabled)
122+
ui.Register(g, *vInfo, conf.Registration, conf.LocalAuthEnabled, conf.OIDC.Enabled)
122123

123124
if conf.OIDC.Enabled {
124125
oidcHandler := api.NewOIDC(conf, db, userChangeNotifier)
@@ -158,7 +159,9 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
158159

159160
g.Group("/user").Use(authentication.Optional).POST("", userHandler.CreateUser)
160161

161-
g.POST("/auth/local/login", sessionHandler.Login)
162+
if conf.LocalAuthEnabled {
163+
g.POST("/auth/local/login", sessionHandler.Login)
164+
}
162165

163166
g.OPTIONS("/*any")
164167

@@ -189,7 +192,7 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
189192
// schema:
190193
// $ref: "#/definitions/GotifyInfo"
191194
g.GET("gotifyinfo", func(ctx *gin.Context) {
192-
ctx.JSON(200, &model.GotifyInfo{Version: vInfo.Version, Oidc: conf.OIDC.Enabled, Register: conf.Registration})
195+
ctx.JSON(200, &model.GotifyInfo{Version: vInfo.Version, Oidc: conf.OIDC.Enabled, Register: conf.Registration, LocalAuth: conf.LocalAuthEnabled})
193196
})
194197

195198
g.Group("/").Use(authentication.RequireApplicationOrClient).POST("/message", messageHandler.CreateMessage)

0 commit comments

Comments
 (0)