Skip to content

Commit 3db12ea

Browse files
authored
fix(oauth/invite): do not register user (prending approval) without correct invite (#9189)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 8862e3c commit 3db12ea

1 file changed

Lines changed: 71 additions & 80 deletions

File tree

core/http/auth/oauth.go

Lines changed: 71 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -213,41 +213,84 @@ func (m *OAuthManager) CallbackHandler(providerName string, db *gorm.DB, adminEm
213213
})
214214
}
215215

216-
// In invite mode, block new OAuth users who don't have a valid invite code
217-
// to prevent phantom pending records from accumulating in the DB.
218-
// The first user (no users in DB yet) is always allowed through.
219-
if registrationMode == "invite" && inviteCode == "" {
220-
var existing User
221-
if err := db.Where("provider = ? AND subject = ?", providerName, userInfo.Subject).First(&existing).Error; err != nil {
222-
// Check if this would be the first user (always allowed)
223-
var userCount int64
224-
db.Model(&User{}).Count(&userCount)
225-
if userCount > 0 {
226-
// New user without invite code — reject without creating a DB record
227-
return c.Redirect(http.StatusTemporaryRedirect, "/login?error=invite_required")
228-
}
216+
// Check if user already exists
217+
var existingUser User
218+
userExists := db.Where("provider = ? AND subject = ?", providerName, userInfo.Subject).First(&existingUser).Error == nil
219+
220+
var user *User
221+
if userExists {
222+
// Existing user — update profile fields, no invite gating needed
223+
existingUser.Name = userInfo.Name
224+
existingUser.AvatarURL = userInfo.AvatarURL
225+
if userInfo.Email != "" {
226+
existingUser.Email = strings.ToLower(strings.TrimSpace(userInfo.Email))
229227
}
230-
}
228+
db.Save(&existingUser)
229+
user = &existingUser
230+
} else {
231+
// New user — validate invite BEFORE creating, inside a transaction
232+
var validInvite *InviteCode
233+
txErr := db.Transaction(func(tx *gorm.DB) error {
234+
email := ""
235+
if userInfo.Email != "" {
236+
email = strings.ToLower(strings.TrimSpace(userInfo.Email))
237+
}
231238

232-
// Upsert user (with invite code support)
233-
user, err := upsertOAuthUser(db, providerName, userInfo, adminEmail, registrationMode)
234-
if err != nil {
235-
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to create user"})
236-
}
239+
role := AssignRole(tx, email, adminEmail)
240+
status := StatusActive
241+
242+
if NeedsInviteOrApproval(tx, email, adminEmail, registrationMode) {
243+
if registrationMode == "invite" {
244+
if inviteCode == "" {
245+
return fmt.Errorf("invite_required")
246+
}
247+
invite, err := ValidateInvite(tx, inviteCode, hmacSecret)
248+
if err != nil {
249+
return fmt.Errorf("invalid_invite")
250+
}
251+
validInvite = invite
252+
status = StatusActive
253+
} else {
254+
// approval mode — create as pending
255+
status = StatusPending
256+
}
257+
}
237258

238-
// For new users that are pending, check if they have a valid invite
239-
if user.Status != StatusActive && inviteCode != "" {
240-
if invite, err := ValidateInvite(db, inviteCode, hmacSecret); err == nil {
241-
user.Status = StatusActive
242-
db.Model(user).Update("status", StatusActive)
243-
ConsumeInvite(db, invite, user.ID)
259+
newUser := User{
260+
ID: uuid.New().String(),
261+
Email: email,
262+
Name: userInfo.Name,
263+
AvatarURL: userInfo.AvatarURL,
264+
Provider: providerName,
265+
Subject: userInfo.Subject,
266+
Role: role,
267+
Status: status,
268+
}
269+
if err := tx.Create(&newUser).Error; err != nil {
270+
return fmt.Errorf("failed to create user: %w", err)
271+
}
272+
273+
if validInvite != nil {
274+
ConsumeInvite(tx, validInvite, newUser.ID)
275+
}
276+
277+
user = &newUser
278+
return nil
279+
})
280+
281+
if txErr != nil {
282+
msg := txErr.Error()
283+
if msg == "invite_required" {
284+
return c.Redirect(http.StatusTemporaryRedirect, "/login?error=invite_required")
285+
}
286+
if msg == "invalid_invite" {
287+
return c.Redirect(http.StatusTemporaryRedirect, "/login?error=invalid_invite")
288+
}
289+
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to create user"})
244290
}
245291
}
246292

247293
if user.Status != StatusActive {
248-
if registrationMode == "invite" {
249-
return c.JSON(http.StatusForbidden, map[string]string{"error": "a valid invite code is required to register"})
250-
}
251294
return c.JSON(http.StatusForbidden, map[string]string{"error": "account pending approval"})
252295
}
253296

@@ -393,58 +436,6 @@ func fetchGitHubPrimaryEmail(ctx context.Context, accessToken string) (string, e
393436
return "", fmt.Errorf("no verified email found")
394437
}
395438

396-
func upsertOAuthUser(db *gorm.DB, provider string, info *oauthUserInfo, adminEmail, registrationMode string) (*User, error) {
397-
// Normalize email from provider (#10)
398-
if info.Email != "" {
399-
info.Email = strings.ToLower(strings.TrimSpace(info.Email))
400-
}
401-
402-
var user User
403-
err := db.Where("provider = ? AND subject = ?", provider, info.Subject).First(&user).Error
404-
if err == nil {
405-
// Existing user — update profile fields
406-
user.Name = info.Name
407-
user.AvatarURL = info.AvatarURL
408-
if info.Email != "" {
409-
user.Email = info.Email
410-
}
411-
db.Save(&user)
412-
return &user, nil
413-
}
414-
415-
// New user — empty registration mode defaults to "approval"
416-
effectiveMode := registrationMode
417-
if effectiveMode == "" {
418-
effectiveMode = "approval"
419-
}
420-
status := StatusActive
421-
if effectiveMode == "approval" || effectiveMode == "invite" {
422-
status = StatusPending
423-
}
424-
425-
role := AssignRole(db, info.Email, adminEmail)
426-
// First user is always active regardless of registration mode
427-
if role == RoleAdmin {
428-
status = StatusActive
429-
}
430-
431-
user = User{
432-
ID: uuid.New().String(),
433-
Email: info.Email,
434-
Name: info.Name,
435-
AvatarURL: info.AvatarURL,
436-
Provider: provider,
437-
Subject: info.Subject,
438-
Role: role,
439-
Status: status,
440-
}
441-
442-
if err := db.Create(&user).Error; err != nil {
443-
return nil, err
444-
}
445-
446-
return &user, nil
447-
}
448439

449440
func generateState() (string, error) {
450441
b := make([]byte, 16)

0 commit comments

Comments
 (0)