Skip to content

Commit 340025b

Browse files
committed
chore: separate local auth, refactor
1 parent 36b9446 commit 340025b

20 files changed

Lines changed: 306 additions & 84 deletions

File tree

core/cli/run.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ type RunCMD struct {
132132
AuthBaseURL string `env:"LOCALAI_BASE_URL" help:"Base URL for OAuth callbacks (e.g. http://localhost:8080)" group:"auth"`
133133
AuthAdminEmail string `env:"LOCALAI_ADMIN_EMAIL" help:"Email address to auto-promote to admin role" group:"auth"`
134134
AuthRegistrationMode string `env:"LOCALAI_REGISTRATION_MODE" default:"open" help:"Registration mode: 'open' (default) or 'approval'" group:"auth"`
135+
DisableLocalAuth bool `env:"LOCALAI_DISABLE_LOCAL_AUTH" default:"false" help:"Disable local email/password registration and login (use with OAuth/OIDC-only setups)" group:"auth"`
135136

136137
Version bool
137138
}
@@ -352,6 +353,9 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error {
352353
if r.AuthRegistrationMode != "" {
353354
opts = append(opts, config.WithAuthRegistrationMode(r.AuthRegistrationMode))
354355
}
356+
if r.DisableLocalAuth {
357+
opts = append(opts, config.WithAuthDisableLocalAuth(true))
358+
}
355359
}
356360

357361
if idleWatchDog || busyWatchDog {

core/config/application_config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ type AuthConfig struct {
113113
BaseURL string // for OAuth callback URLs (e.g. "http://localhost:8080")
114114
AdminEmail string // auto-promote to admin on login
115115
RegistrationMode string // "open" (default), "approval", "invite"
116+
DisableLocalAuth bool // disable local email/password registration and login
116117
}
117118

118119
// AgentPoolConfig holds configuration for the LocalAGI agent pool integration.
@@ -774,6 +775,12 @@ func WithAuthRegistrationMode(mode string) AppOption {
774775
}
775776
}
776777

778+
func WithAuthDisableLocalAuth(disable bool) AppOption {
779+
return func(o *ApplicationConfig) {
780+
o.Auth.DisableLocalAuth = disable
781+
}
782+
}
783+
777784
func WithAuthOIDCIssuer(issuer string) AppOption {
778785
return func(o *ApplicationConfig) {
779786
o.Auth.OIDCIssuer = issuer

core/http/auth/apikeys_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ var _ = Describe("API Keys", func() {
1919

2020
BeforeEach(func() {
2121
db = testDB()
22-
user = createTestUser(db, "apikey@example.com", auth.RoleUser, "github")
22+
user = createTestUser(db, "apikey@example.com", auth.RoleUser, auth.ProviderGitHub)
2323
})
2424

2525
Describe("GenerateAPIKey", func() {
@@ -127,7 +127,7 @@ var _ = Describe("API Keys", func() {
127127
})
128128

129129
It("does not return other users' keys", func() {
130-
other := createTestUser(db, "other@example.com", auth.RoleUser, "github")
130+
other := createTestUser(db, "other@example.com", auth.RoleUser, auth.ProviderGitHub)
131131
auth.CreateAPIKey(db, user.ID, "my key", auth.RoleUser)
132132
auth.CreateAPIKey(db, other.ID, "other key", auth.RoleUser)
133133

@@ -154,7 +154,7 @@ var _ = Describe("API Keys", func() {
154154
_, record, err := auth.CreateAPIKey(db, user.ID, "mine", auth.RoleUser)
155155
Expect(err).ToNot(HaveOccurred())
156156

157-
other := createTestUser(db, "attacker@example.com", auth.RoleUser, "github")
157+
other := createTestUser(db, "attacker@example.com", auth.RoleUser, auth.ProviderGitHub)
158158
err = auth.RevokeAPIKey(db, record.ID, other.ID)
159159
Expect(err).To(HaveOccurred())
160160
})

core/http/auth/db_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ var _ = Describe("InitDB", func() {
3737
// Insert a user to verify the index doesn't prevent normal operations
3838
user := &auth.User{
3939
ID: "test-1",
40-
Provider: "github",
40+
Provider: auth.ProviderGitHub,
4141
Subject: "12345",
4242
Role: "admin",
4343
Status: auth.StatusActive,
@@ -46,7 +46,7 @@ var _ = Describe("InitDB", func() {
4646

4747
// Query using the indexed columns should work
4848
var found auth.User
49-
Expect(db.Where("provider = ? AND subject = ?", "github", "12345").First(&found).Error).ToNot(HaveOccurred())
49+
Expect(db.Where("provider = ? AND subject = ?", auth.ProviderGitHub, "12345").First(&found).Error).ToNot(HaveOccurred())
5050
Expect(found.ID).To(Equal("test-1"))
5151
})
5252
})

core/http/auth/middleware_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ var _ = Describe("Auth Middleware", func() {
8282
db = testDB()
8383
appConfig = config.NewApplicationConfig()
8484
app = newAuthTestApp(db, appConfig)
85-
user = createTestUser(db, "user@example.com", auth.RoleUser, "github")
85+
user = createTestUser(db, "user@example.com", auth.RoleUser, auth.ProviderGitHub)
8686
})
8787

8888
It("allows requests with valid session cookie", func() {
@@ -173,7 +173,7 @@ var _ = Describe("Auth Middleware", func() {
173173
})
174174

175175
It("passes for admin user", func() {
176-
admin := createTestUser(db, "admin@example.com", auth.RoleAdmin, "github")
176+
admin := createTestUser(db, "admin@example.com", auth.RoleAdmin, auth.ProviderGitHub)
177177
sessionID := createTestSession(db, admin.ID)
178178
app := newAdminTestApp(db, appConfig)
179179

@@ -182,7 +182,7 @@ var _ = Describe("Auth Middleware", func() {
182182
})
183183

184184
It("returns 403 for user role", func() {
185-
user := createTestUser(db, "user@example.com", auth.RoleUser, "github")
185+
user := createTestUser(db, "user@example.com", auth.RoleUser, auth.ProviderGitHub)
186186
sessionID := createTestSession(db, user.ID)
187187
app := newAdminTestApp(db, appConfig)
188188

@@ -198,7 +198,7 @@ var _ = Describe("Auth Middleware", func() {
198198
})
199199

200200
It("allows admin to access model management", func() {
201-
admin := createTestUser(db, "admin@example.com", auth.RoleAdmin, "github")
201+
admin := createTestUser(db, "admin@example.com", auth.RoleAdmin, auth.ProviderGitHub)
202202
sessionID := createTestSession(db, admin.ID)
203203
app := newAdminTestApp(db, appConfig)
204204

@@ -207,7 +207,7 @@ var _ = Describe("Auth Middleware", func() {
207207
})
208208

209209
It("blocks user from model management", func() {
210-
user := createTestUser(db, "user@example.com", auth.RoleUser, "github")
210+
user := createTestUser(db, "user@example.com", auth.RoleUser, auth.ProviderGitHub)
211211
sessionID := createTestSession(db, user.ID)
212212
app := newAdminTestApp(db, appConfig)
213213

@@ -216,7 +216,7 @@ var _ = Describe("Auth Middleware", func() {
216216
})
217217

218218
It("allows user to access regular inference endpoints", func() {
219-
user := createTestUser(db, "user@example.com", auth.RoleUser, "github")
219+
user := createTestUser(db, "user@example.com", auth.RoleUser, auth.ProviderGitHub)
220220
sessionID := createTestSession(db, user.ID)
221221
app := newAdminTestApp(db, appConfig)
222222

@@ -233,7 +233,7 @@ var _ = Describe("Auth Middleware", func() {
233233
})
234234

235235
It("allows admin to access trace endpoints", func() {
236-
admin := createTestUser(db, "admin2@example.com", auth.RoleAdmin, "github")
236+
admin := createTestUser(db, "admin2@example.com", auth.RoleAdmin, auth.ProviderGitHub)
237237
sessionID := createTestSession(db, admin.ID)
238238
app := newAdminTestApp(db, appConfig)
239239

@@ -245,7 +245,7 @@ var _ = Describe("Auth Middleware", func() {
245245
})
246246

247247
It("blocks non-admin from trace endpoints", func() {
248-
user := createTestUser(db, "user2@example.com", auth.RoleUser, "github")
248+
user := createTestUser(db, "user2@example.com", auth.RoleUser, auth.ProviderGitHub)
249249
sessionID := createTestSession(db, user.ID)
250250
app := newAdminTestApp(db, appConfig)
251251

@@ -257,7 +257,7 @@ var _ = Describe("Auth Middleware", func() {
257257
})
258258

259259
It("allows admin to access agent job endpoints", func() {
260-
admin := createTestUser(db, "admin3@example.com", auth.RoleAdmin, "github")
260+
admin := createTestUser(db, "admin3@example.com", auth.RoleAdmin, auth.ProviderGitHub)
261261
sessionID := createTestSession(db, admin.ID)
262262
app := newAdminTestApp(db, appConfig)
263263

@@ -269,7 +269,7 @@ var _ = Describe("Auth Middleware", func() {
269269
})
270270

271271
It("blocks non-admin from agent job endpoints", func() {
272-
user := createTestUser(db, "user3@example.com", auth.RoleUser, "github")
272+
user := createTestUser(db, "user3@example.com", auth.RoleUser, auth.ProviderGitHub)
273273
sessionID := createTestSession(db, user.ID)
274274
app := newAdminTestApp(db, appConfig)
275275

@@ -281,7 +281,7 @@ var _ = Describe("Auth Middleware", func() {
281281
})
282282

283283
It("blocks non-admin from system/management endpoints", func() {
284-
user := createTestUser(db, "user4@example.com", auth.RoleUser, "github")
284+
user := createTestUser(db, "user4@example.com", auth.RoleUser, auth.ProviderGitHub)
285285
sessionID := createTestSession(db, user.ID)
286286
app := newAdminTestApp(db, appConfig)
287287

@@ -292,7 +292,7 @@ var _ = Describe("Auth Middleware", func() {
292292
})
293293

294294
It("allows admin to access system/management endpoints", func() {
295-
admin := createTestUser(db, "admin4@example.com", auth.RoleAdmin, "github")
295+
admin := createTestUser(db, "admin4@example.com", auth.RoleAdmin, auth.ProviderGitHub)
296296
sessionID := createTestSession(db, admin.ID)
297297
app := newAdminTestApp(db, appConfig)
298298

core/http/auth/models.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@ import (
77
"time"
88
)
99

10+
// Auth provider constants.
11+
const (
12+
ProviderLocal = "local"
13+
ProviderGitHub = "github"
14+
ProviderOIDC = "oidc"
15+
)
16+
1017
// User represents an authenticated user.
1118
type User struct {
1219
ID string `gorm:"primaryKey;size:36"`
1320
Email string `gorm:"size:255;index"`
1421
Name string `gorm:"size:255"`
1522
AvatarURL string `gorm:"size:512"`
16-
Provider string `gorm:"size:50"` // "github", "oidc"
23+
Provider string `gorm:"size:50"` // ProviderLocal, ProviderGitHub, ProviderOIDC
1724
Subject string `gorm:"size:255"` // provider-specific user ID
1825
PasswordHash string `json:"-"` // bcrypt hash, empty for OAuth-only users
1926
Role string `gorm:"size:20;default:user"`

core/http/auth/oauth.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/coreos/go-oidc/v3/oidc"
1414
"github.com/google/uuid"
1515
"github.com/labstack/echo/v4"
16+
"github.com/mudler/xlog"
1617
"golang.org/x/oauth2"
1718
githubOAuth "golang.org/x/oauth2/github"
1819
"gorm.io/gorm"
@@ -53,8 +54,8 @@ func NewOAuthManager(baseURL string, params OAuthParams) (*OAuthManager, error)
5354
m := &OAuthManager{providers: make(map[string]*providerEntry)}
5455

5556
if params.GitHubClientID != "" {
56-
m.providers["github"] = &providerEntry{
57-
name: "github",
57+
m.providers[ProviderGitHub] = &providerEntry{
58+
name: ProviderGitHub,
5859
oauth2Config: oauth2.Config{
5960
ClientID: params.GitHubClientID,
6061
ClientSecret: params.GitHubClientSecret,
@@ -77,8 +78,8 @@ func NewOAuthManager(baseURL string, params OAuthParams) (*OAuthManager, error)
7778

7879
verifier := provider.Verifier(&oidc.Config{ClientID: params.OIDCClientID})
7980

80-
m.providers["oidc"] = &providerEntry{
81-
name: "oidc",
81+
m.providers[ProviderOIDC] = &providerEntry{
82+
name: ProviderOIDC,
8283
oauth2Config: oauth2.Config{
8384
ClientID: params.OIDCClientID,
8485
ClientSecret: params.OIDCClientSecret,
@@ -115,11 +116,13 @@ func (m *OAuthManager) LoginHandler(providerName string) echo.HandlerFunc {
115116
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate state"})
116117
}
117118

119+
secure := isSecure(c)
118120
c.SetCookie(&http.Cookie{
119121
Name: "oauth_state",
120122
Value: state,
121123
Path: "/",
122124
HttpOnly: true,
125+
Secure: secure,
123126
SameSite: http.SameSiteLaxMode,
124127
MaxAge: 600, // 10 minutes
125128
})
@@ -131,6 +134,7 @@ func (m *OAuthManager) LoginHandler(providerName string) echo.HandlerFunc {
131134
Value: inviteCode,
132135
Path: "/",
133136
HttpOnly: true,
137+
Secure: secure,
134138
SameSite: http.SameSiteLaxMode,
135139
MaxAge: 600,
136140
})
@@ -162,6 +166,7 @@ func (m *OAuthManager) CallbackHandler(providerName string, db *gorm.DB, adminEm
162166
Value: "",
163167
Path: "/",
164168
HttpOnly: true,
169+
Secure: isSecure(c),
165170
MaxAge: -1,
166171
})
167172

@@ -176,7 +181,8 @@ func (m *OAuthManager) CallbackHandler(providerName string, db *gorm.DB, adminEm
176181

177182
token, err := provider.oauth2Config.Exchange(ctx, code)
178183
if err != nil {
179-
return c.JSON(http.StatusBadRequest, map[string]string{"error": "failed to exchange code: " + err.Error()})
184+
xlog.Error("OAuth code exchange failed", "provider", providerName, "error", err)
185+
return c.JSON(http.StatusBadRequest, map[string]string{"error": "OAuth authentication failed"})
180186
}
181187

182188
// Fetch user info — branch based on provider type
@@ -200,6 +206,7 @@ func (m *OAuthManager) CallbackHandler(providerName string, db *gorm.DB, adminEm
200206
Value: "",
201207
Path: "/",
202208
HttpOnly: true,
209+
Secure: isSecure(c),
203210
MaxAge: -1,
204211
})
205212
}

core/http/auth/roles_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,28 +23,28 @@ var _ = Describe("Roles", func() {
2323
})
2424

2525
It("returns user for the second user", func() {
26-
createTestUser(db, "first@example.com", auth.RoleAdmin, "github")
26+
createTestUser(db, "first@example.com", auth.RoleAdmin, auth.ProviderGitHub)
2727

2828
role := auth.AssignRole(db, "second@example.com", "")
2929
Expect(role).To(Equal(auth.RoleUser))
3030
})
3131

3232
It("returns admin when email matches adminEmail", func() {
33-
createTestUser(db, "first@example.com", auth.RoleAdmin, "github")
33+
createTestUser(db, "first@example.com", auth.RoleAdmin, auth.ProviderGitHub)
3434

3535
role := auth.AssignRole(db, "admin@example.com", "admin@example.com")
3636
Expect(role).To(Equal(auth.RoleAdmin))
3737
})
3838

3939
It("is case-insensitive for admin email match", func() {
40-
createTestUser(db, "first@example.com", auth.RoleAdmin, "github")
40+
createTestUser(db, "first@example.com", auth.RoleAdmin, auth.ProviderGitHub)
4141

4242
role := auth.AssignRole(db, "Admin@Example.COM", "admin@example.com")
4343
Expect(role).To(Equal(auth.RoleAdmin))
4444
})
4545

4646
It("returns user when email does not match adminEmail", func() {
47-
createTestUser(db, "first@example.com", auth.RoleAdmin, "github")
47+
createTestUser(db, "first@example.com", auth.RoleAdmin, auth.ProviderGitHub)
4848

4949
role := auth.AssignRole(db, "other@example.com", "admin@example.com")
5050
Expect(role).To(Equal(auth.RoleUser))
@@ -53,7 +53,7 @@ var _ = Describe("Roles", func() {
5353

5454
Describe("MaybePromote", func() {
5555
It("promotes user to admin when email matches", func() {
56-
user := createTestUser(db, "promoted@example.com", auth.RoleUser, "github")
56+
user := createTestUser(db, "promoted@example.com", auth.RoleUser, auth.ProviderGitHub)
5757

5858
promoted := auth.MaybePromote(db, user, "promoted@example.com")
5959
Expect(promoted).To(BeTrue())
@@ -66,15 +66,15 @@ var _ = Describe("Roles", func() {
6666
})
6767

6868
It("does not promote when email does not match", func() {
69-
user := createTestUser(db, "user@example.com", auth.RoleUser, "github")
69+
user := createTestUser(db, "user@example.com", auth.RoleUser, auth.ProviderGitHub)
7070

7171
promoted := auth.MaybePromote(db, user, "admin@example.com")
7272
Expect(promoted).To(BeFalse())
7373
Expect(user.Role).To(Equal(auth.RoleUser))
7474
})
7575

7676
It("does not demote an existing admin", func() {
77-
user := createTestUser(db, "admin@example.com", auth.RoleAdmin, "github")
77+
user := createTestUser(db, "admin@example.com", auth.RoleAdmin, auth.ProviderGitHub)
7878

7979
promoted := auth.MaybePromote(db, user, "other@example.com")
8080
Expect(promoted).To(BeFalse())

core/http/auth/session.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,25 @@ func CleanExpiredSessions(db *gorm.DB) error {
6363
return db.Where("expires_at < ?", time.Now()).Delete(&Session{}).Error
6464
}
6565

66+
// isSecure returns true when the request arrived over HTTPS, either directly
67+
// or via a reverse proxy that sets X-Forwarded-Proto.
68+
func isSecure(c echo.Context) bool {
69+
return c.Scheme() == "https"
70+
}
71+
72+
// DeleteUserSessions removes all sessions for the given user.
73+
func DeleteUserSessions(db *gorm.DB, userID string) error {
74+
return db.Where("user_id = ?", userID).Delete(&Session{}).Error
75+
}
76+
6677
// SetSessionCookie sets the session cookie on the response.
6778
func SetSessionCookie(c echo.Context, sessionID string) {
6879
cookie := &http.Cookie{
6980
Name: sessionCookie,
7081
Value: sessionID,
7182
Path: "/",
7283
HttpOnly: true,
84+
Secure: isSecure(c),
7385
SameSite: http.SameSiteLaxMode,
7486
MaxAge: int(sessionDuration.Seconds()),
7587
}
@@ -83,6 +95,7 @@ func ClearSessionCookie(c echo.Context) {
8395
Value: "",
8496
Path: "/",
8597
HttpOnly: true,
98+
Secure: isSecure(c),
8699
SameSite: http.SameSiteLaxMode,
87100
MaxAge: -1,
88101
}

core/http/auth/session_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ var _ = Describe("Sessions", func() {
1919

2020
BeforeEach(func() {
2121
db = testDB()
22-
user = createTestUser(db, "session@example.com", auth.RoleUser, "github")
22+
user = createTestUser(db, "session@example.com", auth.RoleUser, auth.ProviderGitHub)
2323
})
2424

2525
Describe("CreateSession", func() {

0 commit comments

Comments
 (0)