github.com/greenpau/go-authcrunch@v1.1.4/pkg/ids/ldap/authenticator.go (about) 1 // Copyright 2022 Paul Greenberg greenpau@outlook.com 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package ldap 16 17 import ( 18 "crypto/tls" 19 "crypto/x509" 20 "fmt" 21 ldap "github.com/go-ldap/ldap/v3" 22 "github.com/greenpau/go-authcrunch/pkg/errors" 23 "github.com/greenpau/go-authcrunch/pkg/requests" 24 "go.uber.org/zap" 25 "io/ioutil" 26 "net" 27 "net/url" 28 "os" 29 "strings" 30 "sync" 31 "time" 32 ) 33 34 // Authenticator represents database connector. 35 type Authenticator struct { 36 mux sync.Mutex 37 realm string 38 servers []*AuthServer 39 username string 40 password string 41 searchBaseDN string 42 searchUserFilter string 43 searchGroupFilter string 44 userAttributes UserAttributes 45 fallbackRoles []string 46 rootCAs *x509.CertPool 47 groups []*UserGroup 48 logger *zap.Logger 49 } 50 51 // NewAuthenticator returns an instance of Authenticator. 52 func NewAuthenticator() *Authenticator { 53 return &Authenticator{ 54 servers: []*AuthServer{}, 55 groups: []*UserGroup{}, 56 } 57 } 58 59 // ConfigureRealm configures a domain name (realm) associated with 60 // the instance of authenticator. 61 func (sa *Authenticator) ConfigureRealm(cfg *Config) error { 62 sa.mux.Lock() 63 defer sa.mux.Unlock() 64 if cfg.Realm == "" { 65 return fmt.Errorf("no realm found") 66 } 67 sa.realm = cfg.Realm 68 sa.logger.Info( 69 "LDAP plugin configuration", 70 zap.String("phase", "realm"), 71 zap.String("realm", cfg.Realm), 72 ) 73 return nil 74 } 75 76 // ConfigureServers configures the addresses of LDAP servers. 77 func (sa *Authenticator) ConfigureServers(cfg *Config) error { 78 sa.mux.Lock() 79 defer sa.mux.Unlock() 80 if len(cfg.Servers) == 0 { 81 return fmt.Errorf("no authentication servers found") 82 } 83 for _, entry := range cfg.Servers { 84 if !strings.HasPrefix(entry.Address, "ldaps://") && !strings.HasPrefix(entry.Address, "ldap://") { 85 return fmt.Errorf("the server address does not have neither ldaps:// nor ldap:// prefix, address: %s", entry.Address) 86 } 87 if entry.Timeout == 0 { 88 entry.Timeout = 5 89 } 90 if entry.Timeout > 10 { 91 return fmt.Errorf("invalid timeout value: %d, cannot exceed 10 seconds", entry.Timeout) 92 } 93 94 server := &AuthServer{ 95 Address: entry.Address, 96 IgnoreCertErrors: entry.IgnoreCertErrors, 97 Timeout: entry.Timeout, 98 PosixGroups: entry.PosixGroups, 99 } 100 101 url, err := url.Parse(entry.Address) 102 if err != nil { 103 return fmt.Errorf("failed parsing LDAP server address: %s, %s", entry.Address, err) 104 } 105 server.URL = url 106 107 switch { 108 case strings.HasPrefix(entry.Address, "ldaps://"): 109 server.Port = "636" 110 server.Encrypted = true 111 case strings.HasPrefix(entry.Address, "ldap://"): 112 server.Port = "389" 113 } 114 115 if server.URL.Port() != "" { 116 server.Port = server.URL.Port() 117 } 118 119 sa.logger.Info( 120 "LDAP plugin configuration", 121 zap.String("phase", "servers"), 122 zap.String("address", server.Address), 123 zap.String("url", server.URL.String()), 124 zap.String("port", server.Port), 125 zap.Bool("ignore_cert_errors", server.IgnoreCertErrors), 126 zap.Bool("posix_groups", server.PosixGroups), 127 zap.Int("timeout", server.Timeout), 128 ) 129 sa.servers = append(sa.servers, server) 130 } 131 return nil 132 } 133 134 // ConfigureBindCredentials configures user credentials for LDAP binding. 135 func (sa *Authenticator) ConfigureBindCredentials(cfg *Config) error { 136 username := cfg.BindUsername 137 password := cfg.BindPassword 138 sa.mux.Lock() 139 defer sa.mux.Unlock() 140 if username == "" { 141 return fmt.Errorf("no username found") 142 } 143 if password == "" { 144 password = os.Getenv("LDAP_USER_SECRET") 145 if password == "" { 146 return fmt.Errorf("no password found") 147 } 148 } 149 150 if strings.HasPrefix(password, "file:") { 151 secretFile := strings.TrimPrefix(password, "file:") 152 sa.logger.Info( 153 "LDAP plugin configuration", 154 zap.String("phase", "bind_credentials"), 155 zap.String("password_file", secretFile), 156 ) 157 fileContent, err := ioutil.ReadFile(secretFile) 158 if err != nil { 159 return fmt.Errorf("failed reading password file: %s, %s", secretFile, err) 160 } 161 password = strings.TrimSpace(string(fileContent)) 162 if password == "" { 163 return fmt.Errorf("no password found in file: %s", secretFile) 164 } 165 } 166 167 sa.username = username 168 sa.password = password 169 170 cfg.BindUsername = username 171 cfg.BindPassword = password 172 173 sa.logger.Info( 174 "LDAP plugin configuration", 175 zap.String("phase", "bind_credentials"), 176 zap.String("username", sa.username), 177 ) 178 return nil 179 } 180 181 // ConfigureSearch configures base DN, search filter, attributes for LDAP queries. 182 func (sa *Authenticator) ConfigureSearch(cfg *Config) error { 183 attr := cfg.Attributes 184 searchBaseDN := cfg.SearchBaseDN 185 searchUserFilter := cfg.SearchUserFilter 186 searchGroupFilter := cfg.SearchGroupFilter 187 188 sa.mux.Lock() 189 defer sa.mux.Unlock() 190 if searchBaseDN == "" { 191 return fmt.Errorf("no search_base_dn found") 192 } 193 if searchUserFilter == "" { 194 searchUserFilter = "(&(|(sAMAccountName=%s)(mail=%s))(objectclass=user))" 195 cfg.SearchUserFilter = searchUserFilter 196 } 197 if searchGroupFilter == "" { 198 searchGroupFilter = "(&(uniqueMember=%s)(objectClass=groupOfUniqueNames))" 199 cfg.SearchGroupFilter = searchGroupFilter 200 } 201 if attr.Name == "" { 202 attr.Name = "givenName" 203 cfg.Attributes.Name = attr.Name 204 } 205 if attr.Surname == "" { 206 attr.Surname = "sn" 207 cfg.Attributes.Surname = attr.Surname 208 } 209 if attr.Username == "" { 210 attr.Username = "sAMAccountName" 211 cfg.Attributes.Username = attr.Username 212 } 213 if attr.MemberOf == "" { 214 attr.MemberOf = "memberOf" 215 cfg.Attributes.MemberOf = attr.MemberOf 216 } 217 if attr.Email == "" { 218 attr.Email = "mail" 219 cfg.Attributes.Email = attr.Email 220 } 221 222 if len(cfg.FallbackRoles) > 0 { 223 sa.fallbackRoles = cfg.FallbackRoles 224 } 225 sa.logger.Info( 226 "LDAP plugin configuration", 227 zap.String("phase", "search"), 228 zap.String("search_base_dn", searchBaseDN), 229 zap.String("search_user_filter", searchUserFilter), 230 zap.String("search_group_filter", searchGroupFilter), 231 zap.String("attr.name", attr.Name), 232 zap.String("attr.surname", attr.Surname), 233 zap.String("attr.username", attr.Username), 234 zap.String("attr.member_of", attr.MemberOf), 235 zap.String("attr.email", attr.Email), 236 ) 237 sa.searchBaseDN = searchBaseDN 238 sa.searchUserFilter = searchUserFilter 239 sa.searchGroupFilter = searchGroupFilter 240 sa.userAttributes = attr 241 242 if len(cfg.FallbackRoles) > 0 { 243 sa.fallbackRoles = cfg.FallbackRoles 244 } 245 246 return nil 247 } 248 249 // ConfigureUserGroups configures user group bindings for LDAP searching. 250 func (sa *Authenticator) ConfigureUserGroups(cfg *Config) error { 251 groups := cfg.Groups 252 if len(groups) == 0 { 253 return fmt.Errorf("no groups found") 254 } 255 for i, group := range groups { 256 if group.GroupDN == "" { 257 return fmt.Errorf("Base DN for group %d is empty", i) 258 } 259 if len(group.Roles) == 0 { 260 return fmt.Errorf("Role assignments for group %d is empty", i) 261 } 262 for j, role := range group.Roles { 263 if role == "" { 264 return fmt.Errorf("Role assignment %d for group %d is empty", j, i) 265 } 266 } 267 saGroup := &UserGroup{ 268 GroupDN: group.GroupDN, 269 Roles: group.Roles, 270 } 271 sa.logger.Info( 272 "LDAP plugin configuration", 273 zap.String("phase", "user_groups"), 274 zap.String("roles", strings.Join(saGroup.Roles, ", ")), 275 zap.String("dn", saGroup.GroupDN), 276 ) 277 sa.groups = append(sa.groups, saGroup) 278 } 279 return nil 280 } 281 282 // IdentifyUser returns user challenges. 283 func (sa *Authenticator) IdentifyUser(r *requests.Request) error { 284 sa.mux.Lock() 285 defer sa.mux.Unlock() 286 287 for _, server := range sa.servers { 288 conn, err := sa.dial(server) 289 if err != nil { 290 continue 291 } 292 defer conn.Close() 293 if err := sa.findUser(conn, server, r); err != nil { 294 if err.Error() == errors.ErrIdentityStoreLdapAuthFailed.WithArgs("user not found").Error() { 295 r.User.Username = "nobody" 296 r.User.Email = "nobody@localhost" 297 r.User.Challenges = []string{"password"} 298 return nil 299 } 300 r.Response.Code = 401 301 return err 302 } 303 304 return nil 305 } 306 r.Response.Code = 500 307 return errors.ErrIdentityStoreLdapAuthFailed.WithArgs("LDAP servers are unavailable") 308 } 309 310 // AuthenticateUser checks the database for the presence of a username/email 311 // and password and returns user claims. 312 func (sa *Authenticator) AuthenticateUser(r *requests.Request) error { 313 sa.mux.Lock() 314 defer sa.mux.Unlock() 315 316 for _, server := range sa.servers { 317 ldapConnection, err := sa.dial(server) 318 if err != nil { 319 continue 320 } 321 defer ldapConnection.Close() 322 323 searchUserFilter := strings.ReplaceAll(sa.searchUserFilter, "%s", r.User.Username) 324 325 req := ldap.NewSearchRequest( 326 // group.GroupDN, 327 sa.searchBaseDN, 328 ldap.ScopeWholeSubtree, 329 ldap.NeverDerefAliases, 330 0, 331 server.Timeout, 332 false, 333 searchUserFilter, 334 []string{ 335 sa.userAttributes.Email, 336 }, 337 nil, // Controls 338 ) 339 340 if req == nil { 341 sa.logger.Error( 342 "LDAP request building failed, request is nil", 343 zap.String("server", server.Address), 344 zap.String("search_base_dn", sa.searchBaseDN), 345 zap.String("search_user_filter", searchUserFilter), 346 ) 347 continue 348 } 349 350 resp, err := ldapConnection.Search(req) 351 if err != nil { 352 sa.logger.Error( 353 "LDAP search failed", 354 zap.String("server", server.Address), 355 zap.String("search_base_dn", sa.searchBaseDN), 356 zap.String("search_user_filter", searchUserFilter), 357 zap.String("error", err.Error()), 358 ) 359 continue 360 } 361 362 switch len(resp.Entries) { 363 case 1: 364 case 0: 365 return errors.ErrIdentityStoreLdapAuthFailed.WithArgs("user not found") 366 default: 367 return errors.ErrIdentityStoreLdapAuthFailed.WithArgs("multiple users matched") 368 } 369 370 user := resp.Entries[0] 371 // Use the provided password to make an LDAP connection. 372 if err := ldapConnection.Bind(user.DN, r.User.Password); err != nil { 373 sa.logger.Error( 374 "LDAP auth binding failed", 375 zap.String("server", server.Address), 376 zap.String("dn", user.DN), 377 zap.String("username", r.User.Username), 378 zap.String("error", err.Error()), 379 ) 380 return errors.ErrIdentityStoreLdapAuthFailed.WithArgs(err) 381 } 382 383 sa.logger.Debug( 384 "LDAP auth succeeded", 385 zap.String("server", server.Address), 386 zap.String("dn", user.DN), 387 zap.String("username", r.User.Username), 388 ) 389 return nil 390 } 391 392 return errors.ErrIdentityStoreLdapAuthFailed.WithArgs("LDAP servers are unavailable") 393 } 394 395 // ConfigureTrustedAuthorities configured trusted certificate authorities, if any. 396 func (sa *Authenticator) ConfigureTrustedAuthorities(cfg *Config) error { 397 authorities := cfg.TrustedAuthorities 398 if len(authorities) == 0 { 399 return nil 400 } 401 for _, authority := range authorities { 402 pemCerts, err := ioutil.ReadFile(authority) 403 if err != nil { 404 return fmt.Errorf("failed reading trusted authority file: %s, %s", authority, err) 405 } 406 407 if sa.rootCAs == nil { 408 sa.rootCAs = x509.NewCertPool() 409 } 410 if ok := sa.rootCAs.AppendCertsFromPEM(pemCerts); !ok { 411 return fmt.Errorf("failed added trusted authority file contents to Root CA pool: %s", authority) 412 } 413 sa.logger.Debug( 414 "added trusted authority", 415 zap.String("pem_file", authority), 416 ) 417 } 418 return nil 419 } 420 421 func (sa *Authenticator) searchGroups(conn *ldap.Conn, reqData map[string]interface{}, roles map[string]bool) error { 422 if roles == nil { 423 roles = make(map[string]bool) 424 } 425 426 req := ldap.NewSearchRequest(reqData["base_dn"].(string), ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 427 reqData["timeout"].(int), false, reqData["search_group_filter"].(string), []string{"dn"}, nil, 428 ) 429 if req == nil { 430 return fmt.Errorf("failed building group search LDAP request") 431 } 432 433 resp, err := conn.Search(req) 434 if err != nil { 435 return err 436 } 437 438 if len(resp.Entries) < 1 { 439 return fmt.Errorf("no groups found for %s", reqData["user_dn"].(string)) 440 } 441 442 for _, entry := range resp.Entries { 443 for _, g := range sa.groups { 444 if g.GroupDN != entry.DN { 445 continue 446 } 447 for _, role := range g.Roles { 448 if role == "" { 449 continue 450 } 451 roles[role] = true 452 } 453 } 454 } 455 return nil 456 } 457 458 func (sa *Authenticator) dial(server *AuthServer) (*ldap.Conn, error) { 459 var ldapDialer net.Conn 460 var err error 461 timeout := time.Duration(server.Timeout) * time.Second 462 if server.Encrypted { 463 // Handle LDAPS servers. 464 tlsConfig := &tls.Config{ 465 InsecureSkipVerify: server.IgnoreCertErrors, 466 } 467 if sa.rootCAs != nil { 468 tlsConfig.RootCAs = sa.rootCAs 469 } 470 ldapDialer, err = tls.DialWithDialer( 471 &net.Dialer{ 472 Timeout: timeout, 473 }, 474 "tcp", 475 net.JoinHostPort(server.URL.Hostname(), server.Port), 476 tlsConfig, 477 ) 478 if err != nil { 479 sa.logger.Error( 480 "LDAP TLS dialer failed", 481 zap.String("server", server.Address), 482 zap.Error(err), 483 ) 484 return nil, err 485 } 486 sa.logger.Debug( 487 "LDAP TLS dialer setup succeeded", 488 zap.String("server", server.Address), 489 ) 490 } else { 491 // Handle LDAP servers. 492 ldapDialer, err = net.DialTimeout("tcp", net.JoinHostPort(server.URL.Hostname(), server.Port), timeout) 493 if err != nil { 494 sa.logger.Error( 495 "LDAP dialer failed", 496 zap.String("server", server.Address), 497 zap.Error(err), 498 ) 499 return nil, err 500 } 501 sa.logger.Debug( 502 "LDAP dialer setup succeeded", 503 zap.String("server", server.Address), 504 ) 505 } 506 507 ldapConnection := ldap.NewConn(ldapDialer, server.Encrypted) 508 if ldapConnection == nil { 509 err = fmt.Errorf("ldap connection is nil") 510 sa.logger.Error( 511 "LDAP connection failed", 512 zap.String("server", server.Address), 513 zap.Error(err), 514 ) 515 return nil, err 516 } 517 518 if server.Encrypted { 519 tlsState, ok := ldapConnection.TLSConnectionState() 520 if !ok { 521 err = fmt.Errorf("TLSConnectionState is not ok") 522 sa.logger.Error( 523 "LDAP connection TLS state polling failed", 524 zap.String("server", server.Address), 525 zap.Error(err), 526 ) 527 return nil, err 528 } 529 530 sa.logger.Debug( 531 "LDAP connection TLS state polling succeeded", 532 zap.String("server", server.Address), 533 zap.String("server_name", tlsState.ServerName), 534 zap.Bool("handshake_complete", tlsState.HandshakeComplete), 535 zap.String("version", fmt.Sprintf("%d", tlsState.Version)), 536 zap.String("negotiated_protocol", tlsState.NegotiatedProtocol), 537 ) 538 } 539 540 ldapConnection.Start() 541 542 if err := ldapConnection.Bind(sa.username, sa.password); err != nil { 543 sa.logger.Error( 544 "LDAP connection binding failed", 545 zap.String("server", server.Address), 546 zap.String("username", sa.username), 547 zap.String("error", err.Error()), 548 ) 549 return nil, err 550 } 551 sa.logger.Debug( 552 "LDAP binding succeeded", 553 zap.String("server", server.Address), 554 ) 555 return ldapConnection, nil 556 } 557 558 func (sa *Authenticator) findUser(ldapConnection *ldap.Conn, server *AuthServer, r *requests.Request) error { 559 searchUserFilter := strings.ReplaceAll(sa.searchUserFilter, "%s", r.User.Username) 560 561 req := ldap.NewSearchRequest( 562 // group.GroupDN, 563 sa.searchBaseDN, 564 ldap.ScopeWholeSubtree, 565 ldap.NeverDerefAliases, 566 0, 567 server.Timeout, 568 false, 569 searchUserFilter, 570 []string{ 571 sa.userAttributes.Name, 572 sa.userAttributes.Surname, 573 sa.userAttributes.Username, 574 sa.userAttributes.MemberOf, 575 sa.userAttributes.Email, 576 }, 577 nil, // Controls 578 ) 579 580 if req == nil { 581 sa.logger.Error( 582 "LDAP request building failed, request is nil", 583 zap.String("server", server.Address), 584 zap.String("search_base_dn", sa.searchBaseDN), 585 zap.String("search_user_filter", searchUserFilter), 586 ) 587 return errors.ErrIdentityStoreLdapAuthFailed.WithArgs("LDAP request building failed, request is nil") 588 } 589 590 resp, err := ldapConnection.Search(req) 591 if err != nil { 592 sa.logger.Error( 593 "LDAP search failed", 594 zap.String("server", server.Address), 595 zap.String("search_base_dn", sa.searchBaseDN), 596 zap.String("search_user_filter", searchUserFilter), 597 zap.String("error", err.Error()), 598 ) 599 return errors.ErrIdentityStoreLdapAuthFailed.WithArgs("LDAP search failed") 600 } 601 602 sa.logger.Debug( 603 "LDAP search succeeded", 604 zap.String("server", server.Address), 605 zap.Int("entry_count", len(resp.Entries)), 606 zap.String("search_base_dn", sa.searchBaseDN), 607 zap.String("search_user_filter", searchUserFilter), 608 zap.Any("users", resp.Entries), 609 ) 610 611 switch len(resp.Entries) { 612 case 1: 613 case 0: 614 return errors.ErrIdentityStoreLdapAuthFailed.WithArgs("user not found") 615 default: 616 return errors.ErrIdentityStoreLdapAuthFailed.WithArgs("multiple users matched") 617 } 618 619 user := resp.Entries[0] 620 var userFullName, userLastName, userFirstName, userAccountName, userMail string 621 userRoles := make(map[string]bool) 622 623 if server.PosixGroups { 624 // Handle POSIX group memberships. 625 searchGroupRequest := map[string]interface{}{ 626 "user_dn": user.DN, 627 "base_dn": sa.searchBaseDN, 628 "search_group_filter": strings.ReplaceAll(sa.searchGroupFilter, "%s", user.DN), 629 "timeout": server.Timeout, 630 } 631 if err := sa.searchGroups(ldapConnection, searchGroupRequest, userRoles); err != nil { 632 sa.logger.Error( 633 "LDAP group search failed, request", 634 zap.String("server", server.Address), 635 zap.String("base_dn", sa.searchBaseDN), 636 zap.String("search_group_filter", sa.searchGroupFilter), 637 zap.Error(err), 638 ) 639 return err 640 } 641 } 642 643 for _, attr := range user.Attributes { 644 if len(attr.Values) < 1 { 645 continue 646 } 647 if attr.Name == sa.userAttributes.Name { 648 userFirstName = attr.Values[0] 649 } 650 if attr.Name == sa.userAttributes.Surname { 651 userLastName = attr.Values[0] 652 } 653 if attr.Name == sa.userAttributes.Username { 654 userAccountName = attr.Values[0] 655 } 656 if attr.Name == sa.userAttributes.MemberOf { 657 for _, v := range attr.Values { 658 for _, g := range sa.groups { 659 if g.GroupDN != v { 660 continue 661 } 662 for _, role := range g.Roles { 663 if role == "" { 664 continue 665 } 666 userRoles[role] = true 667 } 668 } 669 } 670 } 671 if attr.Name == sa.userAttributes.Email { 672 userMail = attr.Values[0] 673 } 674 } 675 676 if userFirstName != "" { 677 userFullName = userFirstName 678 } 679 if userLastName != "" { 680 if userFullName == "" { 681 userFullName = userLastName 682 } else { 683 userFullName = userFullName + " " + userLastName 684 } 685 } 686 687 switch { 688 case len(userRoles) == 0 && len(sa.fallbackRoles) == 0: 689 return errors.ErrIdentityStoreLdapAuthFailed.WithArgs("no matched groups") 690 case len(userRoles) == 0: 691 for _, role := range sa.fallbackRoles { 692 r.User.Roles = append(r.User.Roles, role) 693 } 694 default: 695 for role := range userRoles { 696 r.User.Roles = append(r.User.Roles, role) 697 } 698 } 699 700 r.User.Username = userAccountName 701 r.User.Email = userMail 702 r.User.FullName = userFullName 703 704 sa.logger.Debug( 705 "LDAP user match", 706 zap.String("server", server.Address), 707 zap.String("name", r.User.FullName), 708 zap.String("username", r.User.Username), 709 zap.String("email", r.User.Email), 710 zap.Any("roles", r.User.Roles), 711 ) 712 713 r.User.Challenges = []string{"password"} 714 r.Response.Code = 200 715 return nil 716 }