mirror of
https://github.com/h44z/wg-portal.git
synced 2026-01-29 06:36:24 +00:00
feat: allow multiple auth sources per user (#500,#477) (#612)
* feat: allow multiple auth sources per user (#500,#477) * only override isAdmin flag if it is provided by the authentication source
This commit is contained in:
@@ -29,8 +29,8 @@ type UserManager interface {
|
||||
GetUser(context.Context, domain.UserIdentifier) (*domain.User, error)
|
||||
// RegisterUser creates a new user in the database.
|
||||
RegisterUser(ctx context.Context, user *domain.User) error
|
||||
// UpdateUser updates an existing user in the database.
|
||||
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
// UpdateUserInternal updates an existing user in the database.
|
||||
UpdateUserInternal(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
}
|
||||
|
||||
type EventBus interface {
|
||||
@@ -232,7 +232,7 @@ func (a *Authenticator) setupExternalAuthProviders(
|
||||
}
|
||||
for i := range ldap { // LDAP
|
||||
providerCfg := &ldap[i]
|
||||
providerId := strings.ToLower(providerCfg.URL)
|
||||
providerId := strings.ToLower(providerCfg.ProviderName)
|
||||
|
||||
if _, exists := a.ldapAuthenticators[providerId]; exists {
|
||||
// this is an unrecoverable error, we cannot register the same provider twice
|
||||
@@ -354,21 +354,45 @@ func (a *Authenticator) passwordAuthentication(
|
||||
var ldapProvider AuthenticatorLdap
|
||||
|
||||
var userInDatabase = false
|
||||
var userSource domain.UserSource
|
||||
existingUser, err := a.users.GetUser(ctx, identifier)
|
||||
if err == nil {
|
||||
userInDatabase = true
|
||||
userSource = existingUser.Source
|
||||
}
|
||||
if userInDatabase && (existingUser.IsLocked() || existingUser.IsDisabled()) {
|
||||
return nil, errors.New("user is locked")
|
||||
}
|
||||
|
||||
if !userInDatabase || userSource == domain.UserSourceLdap {
|
||||
// search user in ldap if registration is enabled
|
||||
authOK := false
|
||||
if userInDatabase {
|
||||
// User is already in db, search for authentication sources which support password authentication and
|
||||
// validate the password.
|
||||
for _, authentication := range existingUser.Authentications {
|
||||
if authentication.Source == domain.UserSourceDatabase {
|
||||
err := existingUser.CheckPassword(password)
|
||||
if err == nil {
|
||||
authOK = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if authentication.Source == domain.UserSourceLdap {
|
||||
ldapProvider, ok := a.ldapAuthenticators[strings.ToLower(authentication.ProviderName)]
|
||||
if !ok {
|
||||
continue // ldap provider not found, skip further checks
|
||||
}
|
||||
err := ldapProvider.PlaintextAuthentication(identifier, password)
|
||||
if err == nil {
|
||||
authOK = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// User is not yet in the db, check ldap providers which have registration enabled.
|
||||
// If the user is found, check the password - on success, sync it to the db.
|
||||
for _, ldapAuth := range a.ldapAuthenticators {
|
||||
if !userInDatabase && !ldapAuth.RegistrationEnabled() {
|
||||
continue
|
||||
if !ldapAuth.RegistrationEnabled() {
|
||||
continue // ldap provider does not support registration, skip further checks
|
||||
}
|
||||
|
||||
rawUserInfo, err := ldapAuth.GetUserInfo(context.Background(), identifier)
|
||||
@@ -379,55 +403,39 @@ func (a *Authenticator) passwordAuthentication(
|
||||
}
|
||||
continue // user not found / other ldap error
|
||||
}
|
||||
|
||||
// user found, check if the password is correct
|
||||
err = ldapAuth.PlaintextAuthentication(identifier, password)
|
||||
if err != nil {
|
||||
continue // password is incorrect, skip further checks
|
||||
}
|
||||
|
||||
// create a new user in the db
|
||||
ldapUserInfo, err = ldapAuth.ParseUserInfo(rawUserInfo)
|
||||
if err != nil {
|
||||
slog.Error("failed to parse ldap user info",
|
||||
"source", ldapAuth.GetName(), "identifier", identifier, "error", err)
|
||||
continue
|
||||
}
|
||||
user, err := a.processUserInfo(ctx, ldapUserInfo, domain.UserSourceLdap, ldapProvider.GetName(), true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to process user information: %w", err)
|
||||
}
|
||||
|
||||
// ldap user found
|
||||
userSource = domain.UserSourceLdap
|
||||
ldapProvider = ldapAuth
|
||||
existingUser = user
|
||||
slog.Debug("created new LDAP user in db",
|
||||
"identifier", user.Identifier, "provider", ldapProvider.GetName())
|
||||
|
||||
authOK = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if userSource == "" {
|
||||
slog.Warn("no user source found for user",
|
||||
"identifier", identifier, "ldapProviderCount", len(a.ldapAuthenticators), "inDb", userInDatabase)
|
||||
return nil, errors.New("user not found")
|
||||
if !authOK {
|
||||
return nil, errors.New("failed to authenticate user")
|
||||
}
|
||||
|
||||
if userSource == domain.UserSourceLdap && ldapProvider == nil {
|
||||
slog.Warn("no ldap provider found for user",
|
||||
"identifier", identifier, "ldapProviderCount", len(a.ldapAuthenticators), "inDb", userInDatabase)
|
||||
return nil, errors.New("ldap provider not found")
|
||||
}
|
||||
|
||||
switch userSource {
|
||||
case domain.UserSourceDatabase:
|
||||
err = existingUser.CheckPassword(password)
|
||||
case domain.UserSourceLdap:
|
||||
err = ldapProvider.PlaintextAuthentication(identifier, password)
|
||||
default:
|
||||
err = errors.New("no authentication backend available")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to authenticate: %w", err)
|
||||
}
|
||||
|
||||
if !userInDatabase {
|
||||
user, err := a.processUserInfo(ctx, ldapUserInfo, domain.UserSourceLdap, ldapProvider.GetName(),
|
||||
ldapProvider.RegistrationEnabled())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to process user information: %w", err)
|
||||
}
|
||||
return user, nil
|
||||
} else {
|
||||
return existingUser, nil
|
||||
}
|
||||
return existingUser, nil
|
||||
}
|
||||
|
||||
// endregion password authentication
|
||||
@@ -590,17 +598,34 @@ func (a *Authenticator) registerNewUser(
|
||||
source domain.UserSource,
|
||||
provider string,
|
||||
) (*domain.User, error) {
|
||||
ctxUserInfo := domain.GetUserInfo(ctx)
|
||||
now := time.Now()
|
||||
|
||||
// convert user info to domain.User
|
||||
user := &domain.User{
|
||||
Identifier: userInfo.Identifier,
|
||||
Email: userInfo.Email,
|
||||
Source: source,
|
||||
ProviderName: provider,
|
||||
IsAdmin: userInfo.IsAdmin,
|
||||
Firstname: userInfo.Firstname,
|
||||
Lastname: userInfo.Lastname,
|
||||
Phone: userInfo.Phone,
|
||||
Department: userInfo.Department,
|
||||
Identifier: userInfo.Identifier,
|
||||
Email: userInfo.Email,
|
||||
IsAdmin: false,
|
||||
Firstname: userInfo.Firstname,
|
||||
Lastname: userInfo.Lastname,
|
||||
Phone: userInfo.Phone,
|
||||
Department: userInfo.Department,
|
||||
Authentications: []domain.UserAuthentication{
|
||||
{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: ctxUserInfo.UserId(),
|
||||
UpdatedBy: ctxUserInfo.UserId(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
UserIdentifier: userInfo.Identifier,
|
||||
Source: source,
|
||||
ProviderName: provider,
|
||||
},
|
||||
},
|
||||
}
|
||||
if userInfo.AdminInfoAvailable && userInfo.IsAdmin {
|
||||
user.IsAdmin = true
|
||||
}
|
||||
|
||||
err := a.users.RegisterUser(ctx, user)
|
||||
@@ -610,6 +635,7 @@ func (a *Authenticator) registerNewUser(
|
||||
|
||||
slog.Debug("registered user from external authentication provider",
|
||||
"user", user.Identifier,
|
||||
"adminInfoAvailable", userInfo.AdminInfoAvailable,
|
||||
"isAdmin", user.IsAdmin,
|
||||
"provider", source)
|
||||
|
||||
@@ -643,6 +669,39 @@ func (a *Authenticator) updateExternalUser(
|
||||
return nil // user is locked or disabled, do not update
|
||||
}
|
||||
|
||||
// Update authentication sources
|
||||
foundAuthSource := false
|
||||
for _, auth := range existingUser.Authentications {
|
||||
if auth.Source == source && auth.ProviderName == provider {
|
||||
foundAuthSource = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundAuthSource {
|
||||
ctxUserInfo := domain.GetUserInfo(ctx)
|
||||
now := time.Now()
|
||||
existingUser.Authentications = append(existingUser.Authentications, domain.UserAuthentication{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: ctxUserInfo.UserId(),
|
||||
UpdatedBy: ctxUserInfo.UserId(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
UserIdentifier: existingUser.Identifier,
|
||||
Source: source,
|
||||
ProviderName: provider,
|
||||
})
|
||||
}
|
||||
|
||||
if existingUser.PersistLocalChanges {
|
||||
if !foundAuthSource {
|
||||
// Even if local changes are persisted, we need to save the new authentication source
|
||||
_, err := a.users.UpdateUserInternal(ctx, existingUser)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
isChanged := false
|
||||
if existingUser.Email != userInfo.Email {
|
||||
existingUser.Email = userInfo.Email
|
||||
@@ -664,33 +723,24 @@ func (a *Authenticator) updateExternalUser(
|
||||
existingUser.Department = userInfo.Department
|
||||
isChanged = true
|
||||
}
|
||||
if existingUser.IsAdmin != userInfo.IsAdmin {
|
||||
if userInfo.AdminInfoAvailable && existingUser.IsAdmin != userInfo.IsAdmin {
|
||||
existingUser.IsAdmin = userInfo.IsAdmin
|
||||
isChanged = true
|
||||
}
|
||||
if existingUser.Source != source {
|
||||
existingUser.Source = source
|
||||
isChanged = true
|
||||
}
|
||||
if existingUser.ProviderName != provider {
|
||||
existingUser.ProviderName = provider
|
||||
isChanged = true
|
||||
}
|
||||
|
||||
if !isChanged {
|
||||
return nil // nothing to update
|
||||
}
|
||||
if isChanged || !foundAuthSource {
|
||||
_, err := a.users.UpdateUserInternal(ctx, existingUser)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
|
||||
_, err := a.users.UpdateUser(ctx, existingUser)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update user: %w", err)
|
||||
slog.Debug("updated user with data from external authentication provider",
|
||||
"user", existingUser.Identifier,
|
||||
"adminInfoAvailable", userInfo.AdminInfoAvailable,
|
||||
"isAdmin", existingUser.IsAdmin,
|
||||
"provider", source)
|
||||
}
|
||||
|
||||
slog.Debug("updated user with data from external authentication provider",
|
||||
"user", existingUser.Identifier,
|
||||
"isAdmin", existingUser.IsAdmin,
|
||||
"provider", source)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -127,18 +127,26 @@ func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIden
|
||||
|
||||
// ParseUserInfo parses the user information from the LDAP server into a domain.AuthenticatorUserInfo struct.
|
||||
func (l LdapAuthenticator) ParseUserInfo(raw map[string]any) (*domain.AuthenticatorUserInfo, error) {
|
||||
isAdmin, err := internal.LdapIsMemberOf(raw[l.cfg.FieldMap.GroupMembership].([][]byte), l.cfg.ParsedAdminGroupDN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check admin group: %w", err)
|
||||
isAdmin := false
|
||||
adminInfoAvailable := false
|
||||
if l.cfg.FieldMap.GroupMembership != "" {
|
||||
adminInfoAvailable = true
|
||||
var err error
|
||||
isAdmin, err = internal.LdapIsMemberOf(raw[l.cfg.FieldMap.GroupMembership].([][]byte), l.cfg.ParsedAdminGroupDN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check admin group: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
userInfo := &domain.AuthenticatorUserInfo{
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, l.cfg.FieldMap.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, l.cfg.FieldMap.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, l.cfg.FieldMap.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, l.cfg.FieldMap.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, l.cfg.FieldMap.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, l.cfg.FieldMap.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, l.cfg.FieldMap.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, l.cfg.FieldMap.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, l.cfg.FieldMap.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, l.cfg.FieldMap.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, l.cfg.FieldMap.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, l.cfg.FieldMap.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
AdminInfoAvailable: adminInfoAvailable,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
|
||||
@@ -15,9 +15,11 @@ func parseOauthUserInfo(
|
||||
raw map[string]any,
|
||||
) (*domain.AuthenticatorUserInfo, error) {
|
||||
var isAdmin bool
|
||||
var adminInfoAvailable bool
|
||||
|
||||
// first try to match the is_admin field against the given regex
|
||||
if mapping.IsAdmin != "" {
|
||||
adminInfoAvailable = true
|
||||
re := adminMapping.GetAdminValueRegex()
|
||||
if re.MatchString(strings.TrimSpace(internal.MapDefaultString(raw, mapping.IsAdmin, ""))) {
|
||||
isAdmin = true
|
||||
@@ -26,6 +28,7 @@ func parseOauthUserInfo(
|
||||
|
||||
// next try to parse the user's groups
|
||||
if !isAdmin && mapping.UserGroups != "" && adminMapping.AdminGroupRegex != "" {
|
||||
adminInfoAvailable = true
|
||||
userGroups := internal.MapDefaultStringSlice(raw, mapping.UserGroups, nil)
|
||||
re := adminMapping.GetAdminGroupRegex()
|
||||
for _, group := range userGroups {
|
||||
@@ -37,13 +40,14 @@ func parseOauthUserInfo(
|
||||
}
|
||||
|
||||
userInfo := &domain.AuthenticatorUserInfo{
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, mapping.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, mapping.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, mapping.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, mapping.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, mapping.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, mapping.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, mapping.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, mapping.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, mapping.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, mapping.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, mapping.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, mapping.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
AdminInfoAvailable: adminInfoAvailable,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
|
||||
@@ -23,8 +23,8 @@ type WebAuthnUserManager interface {
|
||||
GetUser(context.Context, domain.UserIdentifier) (*domain.User, error)
|
||||
// GetUserByWebAuthnCredential returns a user by its WebAuthn ID.
|
||||
GetUserByWebAuthnCredential(ctx context.Context, credentialIdBase64 string) (*domain.User, error)
|
||||
// UpdateUser updates an existing user in the database.
|
||||
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
// UpdateUserInternal updates an existing user in the database.
|
||||
UpdateUserInternal(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
}
|
||||
|
||||
type WebAuthnAuthenticator struct {
|
||||
@@ -89,7 +89,7 @@ func (a *WebAuthnAuthenticator) StartWebAuthnRegistration(ctx context.Context, u
|
||||
|
||||
if user.WebAuthnId == "" {
|
||||
user.GenerateWebAuthnId()
|
||||
user, err = a.users.UpdateUser(ctx, user)
|
||||
user, err = a.users.UpdateUserInternal(ctx, user)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to store webauthn id to user: %w", err)
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func (a *WebAuthnAuthenticator) FinishWebAuthnRegistration(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err = a.users.UpdateUser(ctx, user)
|
||||
user, err = a.users.UpdateUserInternal(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -181,7 +181,7 @@ func (a *WebAuthnAuthenticator) RemoveCredential(
|
||||
}
|
||||
|
||||
user.RemoveCredential(credentialIdBase64)
|
||||
user, err = a.users.UpdateUser(ctx, user)
|
||||
user, err = a.users.UpdateUserInternal(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -205,7 +205,7 @@ func (a *WebAuthnAuthenticator) UpdateCredential(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err = a.users.UpdateUser(ctx, user)
|
||||
user, err = a.users.UpdateUserInternal(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user