From ed407caadecd5504088d79fa6e913b7bd8a0157a Mon Sep 17 00:00:00 2001 From: "abbas.gheydi" Date: Fri, 31 May 2024 13:12:57 +0330 Subject: [PATCH] add ForceSearchForSamAccountName option to ldap query settings (issue #9) --- deploy/config/radiusd.conf | 34 +++-- go.mod | 1 + go.sum | 2 + pkgs/authentiate/ldap.go | 2 +- pkgs/confs/confs.go | 15 +- pkgs/confs/load.go | 3 +- radiusd.conf | 72 +-------- .../abbas-gheydi/go-ad-auth/v3/.gitignore | 4 + .../abbas-gheydi/go-ad-auth/v3/LICENSE | 9 ++ .../abbas-gheydi/go-ad-auth/v3/README.md | 89 +++++++++++ .../abbas-gheydi/go-ad-auth/v3/auth.go | 84 ++++++++++ .../abbas-gheydi/go-ad-auth/v3/config.go | 110 ++++++++++++++ .../abbas-gheydi/go-ad-auth/v3/conn.go | 81 ++++++++++ .../abbas-gheydi/go-ad-auth/v3/group.go | 88 +++++++++++ .../abbas-gheydi/go-ad-auth/v3/passwd.go | 80 ++++++++++ .../abbas-gheydi/go-ad-auth/v3/search.go | 82 ++++++++++ .../abbas-gheydi/go-ad-auth/v3/sid.go | 143 ++++++++++++++++++ vendor/modules.txt | 3 + 18 files changed, 809 insertions(+), 93 deletions(-) mode change 100644 => 120000 radiusd.conf create mode 100644 vendor/github.com/abbas-gheydi/go-ad-auth/v3/.gitignore create mode 100644 vendor/github.com/abbas-gheydi/go-ad-auth/v3/LICENSE create mode 100644 vendor/github.com/abbas-gheydi/go-ad-auth/v3/README.md create mode 100644 vendor/github.com/abbas-gheydi/go-ad-auth/v3/auth.go create mode 100644 vendor/github.com/abbas-gheydi/go-ad-auth/v3/config.go create mode 100644 vendor/github.com/abbas-gheydi/go-ad-auth/v3/conn.go create mode 100644 vendor/github.com/abbas-gheydi/go-ad-auth/v3/group.go create mode 100644 vendor/github.com/abbas-gheydi/go-ad-auth/v3/passwd.go create mode 100644 vendor/github.com/abbas-gheydi/go-ad-auth/v3/search.go create mode 100644 vendor/github.com/abbas-gheydi/go-ad-auth/v3/sid.go diff --git a/deploy/config/radiusd.conf b/deploy/config/radiusd.conf index 547da4d..8c31931 100644 --- a/deploy/config/radiusd.conf +++ b/deploy/config/radiusd.conf @@ -1,5 +1,5 @@ # radOTP configuratio file -# by default radOTP search for this file in current path, else it tries /etc/radotp/ (recommended location) +# by default radOTP search for this file in "/etc/radotp/", else it tries current path. [radius] # ListenAddress is Radius server address @@ -7,9 +7,12 @@ # Secret is common world bitween RadOTP and nas clients like Cisco or Fortinate firewalles to encrypt passwords. Secret = "secret" # Authentication_Mode is the key to set radius server authentication behavior - # Authentication_Mode = "only_password" , "only_otp" , "two_fa", "two_fa_optional_otp" - Authentication_Mode = "two_fa_optional_otp" - #Enable_Fortinet_Group_Name gets groups from ldap,it only works with "only_password" and "two_fa" authentication mode + #Authentication_Mode = "only_password" This mode authenticates users against an Active Directory LDAP/LDAPS server. Users only need to enter their AD password to log in. + #Authentication_Mode = "only_otp" This mode authenticates users with an OTP database only. Users only need to enter a one-time password (OTP) code to log in. + #Authentication_Mode = "two_fa" This mode enables two-factor authentication (2FA). Users need to enter both their AD password and an OTP code to log in. + #Authentication_Mode = "two_fa_optional_otp" This mode is similar to two_fa, but it only applies 2FA to users who have an OTP in the database. Users who do not have an OTP can log in with their AD password only. + Authentication_Mode = "two_fa" + # Enable_Fortinet_Group_Name gets groups from ldap,it only works with "only_password" and "two_fa" authentication mode Enable_Fortinet_Group_Name = false [web] @@ -17,7 +20,7 @@ ListenHTTPS = "0.0.0.0:8081" RedirectToHTTPS = true RedirectToHTTPSPortNumber = "443" - #Isuuer is qr code issueer name,it appears in google athenticator app + # Isuuer is qr code issueer name,it appears in google athenticator app Isuuer = "company.local" EnableRestApi = false Apikey = "test" @@ -44,24 +47,29 @@ # then Fortinet_Group_Name AVP sets in radius response. # FortiGroups = [ "vpnadmins", "vpnusers" ] - #LdapGroupsFilter = "vpn users" + # LdapGroupsFilter = "vpn users" # ldap server address (domain controller address) ldapServers = [ "127.0.0.1" , "192.168.1.12"] - #basedn is domain name in active directory, test.local is "DC=test,DC=local" for example + # basedn is domain name in active directory, test.local is "DC=test,DC=local" for example basedn = "DC=test,DC=local" port = 389 - #security is ldap security level settings + # security is ldap security level settings #### to enable tls on Active directory https://social.technet.microsoft.com/wiki/contents/articles/2980.ldap-over-ssl-ldaps-certificate.aspx - #security = 0 is SecurityNone - #security = 1 is SecurityTLS - #security = 2 is SecurityStartTLS - #security = 3 is SecurityInsecureTLS - #security = 4 is SecurityInsecureStartTLS + # security = 0 is SecurityNone + # security = 1 is SecurityTLS + # security = 2 is SecurityStartTLS + # security = 3 is SecurityInsecureTLS + # security = 4 is SecurityInsecureStartTLS security = 0 + + # When ForceSearchForSamAccountName is set to true, the LDAP query will forcefully search for SamAccountName. + # By default, this setting is false, which means that if set to false, the search will be conducted for userPrincipalName instead + ForceSearchForSamAccountName = false + [metrics] # prometheus exporter settings. diff --git a/go.mod b/go.mod index ed56fda..8e624f1 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( require ( github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect + github.com/abbas-gheydi/go-ad-auth/v3 v3.4.41 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect diff --git a/go.sum b/go.sum index 5a3b2fe..0798be7 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,8 @@ github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3 github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/abbas-gheydi/go-ad-auth/v3 v3.4.41 h1:5xm0QEIGGExRRR1UWHYlekmH3xlahcm4dsyODaQK5CM= +github.com/abbas-gheydi/go-ad-auth/v3 v3.4.41/go.mod h1:hEFwR0rdKWT0OpQPGztCV+eFXdRoaUfKB2LM0Kd84bw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= diff --git a/pkgs/authentiate/ldap.go b/pkgs/authentiate/ldap.go index 0b6eebf..f54adba 100644 --- a/pkgs/authentiate/ldap.go +++ b/pkgs/authentiate/ldap.go @@ -6,7 +6,7 @@ import ( "sync" "time" - ldapAuth "github.com/korylprince/go-ad-auth/v3" + ldapAuth "github.com/abbas-gheydi/go-ad-auth/v3" ) var ( diff --git a/pkgs/confs/confs.go b/pkgs/confs/confs.go index 1fbf857..20d0cbd 100644 --- a/pkgs/confs/confs.go +++ b/pkgs/confs/confs.go @@ -23,8 +23,8 @@ func (c *Configurations) Load() { viper.SetConfigName("radiusd.conf") viper.SetConfigType("toml") - viper.AddConfigPath("/etc/motp/") viper.AddConfigPath("/etc/radotp/") + viper.AddConfigPath("/etc/motp/") viper.AddConfigPath(".") err := viper.ReadInConfig() if err != nil { @@ -65,10 +65,11 @@ type databaseconf struct { } type LdapProvider struct { - FortiGroups []string - LdapGroupsFilter string - LdapServers []string - Basedn string - Port int - Security int + FortiGroups []string + LdapGroupsFilter string + LdapServers []string + Basedn string + Port int + Security int + ForceSearchForSamAccountName bool } diff --git a/pkgs/confs/load.go b/pkgs/confs/load.go index 7426cb4..4749375 100644 --- a/pkgs/confs/load.go +++ b/pkgs/confs/load.go @@ -7,7 +7,7 @@ import ( "github.com/Abbas-gheydi/radotp/pkgs/rad" "github.com/Abbas-gheydi/radotp/pkgs/storage" "github.com/Abbas-gheydi/radotp/pkgs/web" - ldapAuth "github.com/korylprince/go-ad-auth/v3" + ldapAuth "github.com/abbas-gheydi/go-ad-auth/v3" ) var Cfg Configurations @@ -41,6 +41,7 @@ func LoadConfigs() { rad.Auth_Provider.LdapConfig.Security = ldapAuth.SecurityType(Cfg.Ldap.Security) rad.Auth_Provider.LdapConfig.Server = Cfg.Ldap.LdapServers[0] rad.Auth_Provider.LdapServers = Cfg.Ldap.LdapServers + rad.Auth_Provider.LdapConfig.ForceSearchForSamAccountName = Cfg.Ldap.ForceSearchForSamAccountName //database configs storage.Dsn = fmt.Sprintf("host=%v user=%v password=%v dbname=%v port=%v sslmode=%v TimeZone=%v", Cfg.Database.Server, Cfg.Database.Username, Cfg.Database.Password, Cfg.Database.Dbname, Cfg.Database.Port, Cfg.Database.Sslmode, Cfg.Database.Timezone) diff --git a/radiusd.conf b/radiusd.conf deleted file mode 100644 index 547da4d..0000000 --- a/radiusd.conf +++ /dev/null @@ -1,71 +0,0 @@ -# radOTP configuratio file -# by default radOTP search for this file in current path, else it tries /etc/radotp/ (recommended location) - -[radius] - # ListenAddress is Radius server address - ListenAddress = "0.0.0.0:1812" - # Secret is common world bitween RadOTP and nas clients like Cisco or Fortinate firewalles to encrypt passwords. - Secret = "secret" - # Authentication_Mode is the key to set radius server authentication behavior - # Authentication_Mode = "only_password" , "only_otp" , "two_fa", "two_fa_optional_otp" - Authentication_Mode = "two_fa_optional_otp" - #Enable_Fortinet_Group_Name gets groups from ldap,it only works with "only_password" and "two_fa" authentication mode - Enable_Fortinet_Group_Name = false - -[web] - ListenHTTP = "0.0.0.0:8080" - ListenHTTPS = "0.0.0.0:8081" - RedirectToHTTPS = true - RedirectToHTTPSPortNumber = "443" - #Isuuer is qr code issueer name,it appears in google athenticator app - Isuuer = "company.local" - EnableRestApi = false - Apikey = "test" - - -[database] - - server = "db" - port = "5432" - username = "postgres" - password = "dbpassword" - dbname = "postgres" - sslmode = "disable" - timezone = "Asia/Tehran" - MaxOpenConns = 20 - MaxIdleConns = 20 - ConnMaxLifetimeInMiuntes = 5 - - - - -[ldap] - # FortiGroups is users group name in active directory.if user is in the groups And 'Enable_Fortinet_Group_Name = true' - # then Fortinet_Group_Name AVP sets in radius response. - # FortiGroups = [ "vpnadmins", "vpnusers" ] - - #LdapGroupsFilter = "vpn users" - - # ldap server address (domain controller address) - ldapServers = [ "127.0.0.1" , "192.168.1.12"] - - #basedn is domain name in active directory, test.local is "DC=test,DC=local" for example - basedn = "DC=test,DC=local" - - port = 389 - - #security is ldap security level settings - #### to enable tls on Active directory https://social.technet.microsoft.com/wiki/contents/articles/2980.ldap-over-ssl-ldaps-certificate.aspx - #security = 0 is SecurityNone - #security = 1 is SecurityTLS - #security = 2 is SecurityStartTLS - #security = 3 is SecurityInsecureTLS - #security = 4 is SecurityInsecureStartTLS - security = 0 - -[metrics] - # prometheus exporter settings. - Listen = "0.0.0.0:2111" - EnablePrometheusExporter = true - PromethuesAddress = "http://prometheus:9090" - diff --git a/radiusd.conf b/radiusd.conf new file mode 120000 index 0000000..c98dab1 --- /dev/null +++ b/radiusd.conf @@ -0,0 +1 @@ +deploy/config/radiusd.conf \ No newline at end of file diff --git a/vendor/github.com/abbas-gheydi/go-ad-auth/v3/.gitignore b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/.gitignore new file mode 100644 index 0000000..1f51e84 --- /dev/null +++ b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/.gitignore @@ -0,0 +1,4 @@ +*.swp +.env +tags +cmd diff --git a/vendor/github.com/abbas-gheydi/go-ad-auth/v3/LICENSE b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/LICENSE new file mode 100644 index 0000000..69e0bf7 --- /dev/null +++ b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2019 Kory Prince + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/abbas-gheydi/go-ad-auth/v3/README.md b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/README.md new file mode 100644 index 0000000..d090398 --- /dev/null +++ b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/README.md @@ -0,0 +1,89 @@ +[![pkg.go.dev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/korylprince/go-ad-auth/v3) + +# About + +`go-ad-auth` is a simple wrapper around the great [ldap](https://github.com/go-ldap/ldap) library to help with Active Directory authentication. + +# Installing + +Using Go Modules: + +`go get github.com/korylprince/go-ad-auth/v3` + +Using gopkg.in: + +`go get gopkg.in/korylprince/go-ad-auth.v3` + +**Dependencies:** + +* [github.com/go-ldap/ldap](https://github.com/go-ldap/ldap) +* [golang.org/x/text/encoding/unicode](https://pkg.go.dev/golang.org/x/text/encoding/unicode) + +If you have any issues or questions [create an issue](https://github.com/korylprince/go-ad-auth/issues). + +# API Versions + +You should update to the `v3` API when possible. The new API is cleaner, more idiomatic, exposes a lot more functionality, and is fully testable. + +`v3` was created to support Go Modules, so it is backwards compatible with `v2`. However, updates made to `v3` are not backported to `v2`. + +The `v3` API is almost a complete rewrite of the older [`gopkg.in/korylprince/go-ad-auth.v1`](https://pkg.go.dev/gopkg.in/korylprince/go-ad-auth.v1) API. There are similarities, but `v3` is not backwards-compatible. + + +One notable difference to be careful of is that while `v1`'s `Login` will return `false` if the user is not in the specified group, `v3`'s `AuthenticateExtended` will return `true` if the user authenticated successfully, regardless if they were in any of the specified groups or not. + +# Usage + +Example: + +```go +config := &auth.Config{ + Server: "ldap.example.com", + Port: 389, + BaseDN: "OU=Users,DC=example,DC=com", + Security: auth.SecurityStartTLS, +} + +username := "user" +password := "pass" + +status, err := auth.Authenticate(config, username, password) + +if err != nil { + //handle err + return +} + +if !status { + //handle failed authentication + return +} +``` + +See more advanced examples on [go.dev](https://pkg.go.dev/github.com/korylprince/go-ad-auth/v3?tab=doc#pkg-examples). + +# Testing + +`go test -v` + +Most tests will be skipped unless you supply the following environment variables to connect to an Active Directory server: + +| Name | Description | +| ----------------------- | ------------- | +| ADTEST_SERVER | Hostname or IP Address of an Active Directory server | +| ADTEST_PORT | Port to use - defaults to 389 | +| ADTEST_BIND_UPN | userPrincipalName (user@domain.tld) of admin user | +| ADTEST_BIND_PASS | Password of admin user | +| ADTEST_BIND_SECURITY | `NONE` \|\| `TLS` \|\| `STARTTLS` \|\| `INSECURETLS` \|\| `INSECURESTARTTLS` - defaults to `STARTTLS` | +| ADTEST_BASEDN | LDAP Base DN - for testing the root DN is recommended, e.g. `DC=example,DC=com` | +| ADTEST_PASSWORD_UPN | userPrincipalName of a test user that will be used to test password changing functions | + +# Nested Groups + +Since `v3.1.0`, [`AuthenticateExtended`](https://pkg.go.dev/github.com/korylprince/go-ad-auth/v3?tab=doc#AuthenticateExtended) and [`Conn.ObjectGroups`](https://pkg.go.dev/github.com/korylprince/go-ad-auth/v3?tab=doc#Conn.ObjectGroups) will automatically search for nested groups. For example, if `User A` is a member of `Group A`, and `Group A` is a member of `Group B`, using `Conn.ObjectGroups` on `User A` will return both `Group A` and `Group B`. + +# Security + +[SQL Injection](https://en.wikipedia.org/wiki/SQL_injection) is a well known attack vector, and most SQL libraries provide mitigations such as [prepared statements](https://en.wikipedia.org/wiki/Prepared_statement). Similarly, [LDAP Injection](https://www.owasp.org/index.php/Testing_for_LDAP_Injection_\(OTG-INPVAL-006\)), while not seen often in the wild, is something we should be concerned with. + +Since `v2.2.0`, this library sanitizes inputs (with [`ldap.EscapeFilter`](https://pkg.go.dev/github.com/go-ldap/ldap/v3?tab=doc#EscapeFilter)) that are used to create LDAP filters in library functions, namely [`GetDN`](https://pkg.go.dev/github.com/korylprince/go-ad-auth/v3#Conn.GetDN) and [`GetAttributes`](https://pkg.go.dev/github.com/korylprince/go-ad-auth/v3#Conn.GetAttributes). This means high level functions in this library are protected against malicious inputs. If you use [`Search`](https://pkg.go.dev/github.com/korylprince/go-ad-auth/v3#Conn.Search) or [`SearchOne`](https://pkg.go.dev/github.com/korylprince/go-ad-auth/v3#Conn.SearchOne), take care to sanitize any untrusted inputs you use in your LDAP filter. diff --git a/vendor/github.com/abbas-gheydi/go-ad-auth/v3/auth.go b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/auth.go new file mode 100644 index 0000000..b767f0e --- /dev/null +++ b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/auth.go @@ -0,0 +1,84 @@ +package auth + +import ( + ldap "github.com/go-ldap/ldap/v3" +) + +// Authenticate checks if the given credentials are valid, or returns an error if one occurred. +// username may be either the sAMAccountName or the userPrincipalName. +func Authenticate(config *Config, username, password string) (bool, error) { + user, _, err := config.ExtractUserName(username) + if err != nil { + return false, err + } + + conn, err := config.Connect() + if err != nil { + return false, err + } + defer conn.Conn.Close() + + return conn.Bind(user, password) +} + +// AuthenticateExtended checks if the given credentials are valid, or returns an error if one occurred. +// username may be either the sAMAccountName or the userPrincipalName. +// entry is the *ldap.Entry that holds the DN and any request attributes of the user. +// If groups is non-empty, userGroups will hold which of those groups the user is a member of. +// groups can be a list of groups referenced by DN or cn and the format provided will be the format returned. +func AuthenticateExtended(config *Config, username, password string, attrs, groups []string) (status bool, entry *ldap.Entry, userGroups []string, err error) { + + fullUserName, user, err := config.ExtractUserName(username) + if err != nil { + return false, nil, nil, err + } + + conn, err := config.Connect() + if err != nil { + return false, nil, nil, err + } + defer conn.Conn.Close() + + //bind + status, err = conn.Bind(fullUserName, password) + if err != nil { + return false, nil, nil, err + } + if !status { + return false, nil, nil, nil + } + + //get entry + attr := "userPrincipalName" + if config.ForceSearchForSamAccountName { + attr = "sAMAccountName" + } + entry, err = conn.GetAttributes(attr, user, attrs) + if err != nil { + return false, nil, nil, err + } + + if len(groups) > 0 { + //get all groups + foundGroups, err := conn.getGroups(entry.DN) + if err != nil { + return false, nil, nil, err + } + + for _, group := range groups { + groupDN, err := conn.GroupDN(group) + if err != nil { + return false, nil, nil, err + } + + for _, userGroup := range foundGroups { + if userGroup.DN == groupDN { + userGroups = append(userGroups, group) + break + } + } + } + } + + return status, entry, userGroups, nil +} diff --git a/vendor/github.com/abbas-gheydi/go-ad-auth/v3/config.go b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/config.go new file mode 100644 index 0000000..91d64e3 --- /dev/null +++ b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/config.go @@ -0,0 +1,110 @@ +package auth + +import ( + "crypto/x509" + "errors" + "fmt" + "net/mail" + "strings" +) + +// SecurityType specifies the type of security to use when connecting to an Active Directory Server. +type SecurityType int + +// Security will default to SecurityNone if not given. +const ( + SecurityNone SecurityType = iota + SecurityTLS + SecurityStartTLS + SecurityInsecureTLS + SecurityInsecureStartTLS +) + +// Config contains settings for connecting to an Active Directory server. +type Config struct { + Server string + Port int + BaseDN string + Security SecurityType + RootCAs *x509.CertPool + ForceSearchForSamAccountName bool +} + +// Domain returns the domain derived from BaseDN or an error if misconfigured. +func (c *Config) Domain() (string, error) { + domain := "" + for _, v := range strings.Split(strings.ToLower(c.BaseDN), ",") { + if trimmed := strings.TrimSpace(v); strings.HasPrefix(trimmed, "dc=") { + domain = domain + "." + trimmed[3:] + } + } + if len(domain) <= 1 { + return "", errors.New("Configuration error: invalid BaseDN") + } + return domain[1:], nil +} + +// UPN returns the userPrincipalName for the given username or an error if misconfigured. +func (c *Config) UPN(username string) (string, error) { + if _, err := mail.ParseAddress(username); err == nil { + return username, nil + } + + domain, err := c.Domain() + if err != nil { + return "", err + } + + return fmt.Sprintf("%s@%s", username, domain), nil +} + +func (c *Config) SamAccountName(username string) (fullUserName, user string, err error) { + // Split the username into user and domain parts + tmpList := strings.SplitN(username, "@", 2) + if len(tmpList) != 2 { + return "", "", errors.New("invalid username format") + } + + // Extract user and domain + user = tmpList[0] + domainParts := strings.Split(tmpList[1], ".") + if len(domainParts) < 2 { + return "", "", errors.New("invalid domain format") + } + domain := domainParts[0] + + // Construct the full user name using domain and username + fullUserName = domain + `\` + user + return fullUserName, user, nil +} + +func (c *Config) ExtractUserName(username string) (fullUserName, user string, err error) { + // Extract full user name using User Principal Name (UPN) + fullUserName, err = c.UPN(username) + if err != nil { + return "", "", err + } + + user = fullUserName + + // If forced to search for SamAccountName, extract it + if c.ForceSearchForSamAccountName { + var samFullUserName, samUser string + samFullUserName, samUser, err = c.SamAccountName(user) + if err != nil { + return "", "", err + } + + // Update values only if SamAccountName was successfully extracted + if samUser != "" && samFullUserName != "" { + fullUserName = samFullUserName + user = samUser + } + } + + if user == "" || fullUserName == "" { + err = errors.New("ldap error parsing username") + } + + return fullUserName, user, nil +} diff --git a/vendor/github.com/abbas-gheydi/go-ad-auth/v3/conn.go b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/conn.go new file mode 100644 index 0000000..5a43530 --- /dev/null +++ b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/conn.go @@ -0,0 +1,81 @@ +package auth + +import ( + "crypto/tls" + "errors" + "fmt" + + ldap "github.com/go-ldap/ldap/v3" +) + +//Conn represents an Active Directory connection. +type Conn struct { + Conn *ldap.Conn + Config *Config +} + +//Connect returns an open connection to an Active Directory server or an error if one occurred. +func (c *Config) Connect() (*Conn, error) { + switch c.Security { + case SecurityNone: + conn, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", c.Server, c.Port)) + if err != nil { + return nil, fmt.Errorf("Connection error: %w", err) + } + return &Conn{Conn: conn, Config: c}, nil + case SecurityTLS: + conn, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", c.Server, c.Port), &tls.Config{ServerName: c.Server, RootCAs: c.RootCAs}) + if err != nil { + return nil, fmt.Errorf("Connection error: %w", err) + } + return &Conn{Conn: conn, Config: c}, nil + case SecurityStartTLS: + conn, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", c.Server, c.Port)) + if err != nil { + return nil, fmt.Errorf("Connection error: %w", err) + } + err = conn.StartTLS(&tls.Config{ServerName: c.Server, RootCAs: c.RootCAs}) + if err != nil { + return nil, fmt.Errorf("Connection error: %w", err) + } + return &Conn{Conn: conn, Config: c}, nil + case SecurityInsecureTLS: + conn, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", c.Server, c.Port), &tls.Config{ServerName: c.Server, InsecureSkipVerify: true}) + if err != nil { + return nil, fmt.Errorf("Connection error: %w", err) + } + return &Conn{Conn: conn, Config: c}, nil + case SecurityInsecureStartTLS: + conn, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", c.Server, c.Port)) + if err != nil { + return nil, fmt.Errorf("Connection error: %w", err) + } + err = conn.StartTLS(&tls.Config{ServerName: c.Server, InsecureSkipVerify: true}) + if err != nil { + return nil, fmt.Errorf("Connection error: %w", err) + } + return &Conn{Conn: conn, Config: c}, nil + default: + return nil, errors.New("Configuration error: invalid SecurityType") + } +} + +//Bind authenticates the connection with the given userPrincipalName and password +//and returns the result or an error if one occurred. +func (c *Conn) Bind(upn, password string) (bool, error) { + if password == "" { + return false, nil + } + + err := c.Conn.Bind(upn, password) + if err != nil { + if e, ok := err.(*ldap.Error); ok { + if e.ResultCode == ldap.LDAPResultInvalidCredentials { + return false, nil + } + } + return false, fmt.Errorf("Bind error (%s): %w", upn, err) + } + + return true, nil +} diff --git a/vendor/github.com/abbas-gheydi/go-ad-auth/v3/group.go b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/group.go new file mode 100644 index 0000000..e10d4db --- /dev/null +++ b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/group.go @@ -0,0 +1,88 @@ +package auth + +import ( + "encoding/binary" + "errors" + "fmt" + "strconv" + "strings" +) + +const LDAPMatchingRuleInChain = "1.2.840.113556.1.4.1941" + +//GroupDN returns the DN of the group with the given cn or an error if one occurred. +func (c *Conn) GroupDN(group string) (string, error) { + if strings.HasSuffix(group, c.Config.BaseDN) { + return group, nil + } + + return c.GetDN("cn", group) +} + +//ObjectGroups returns which of the given groups (referenced by DN) the object with the given attribute value is in, +//if any, or an error if one occurred. +//Setting attr to "dn" and value to the DN of an object will avoid an extra LDAP search to get the object's DN. +func (c *Conn) ObjectGroups(attr, value string, groups []string) ([]string, error) { + dn := value + if attr != "dn" { + entry, err := c.GetAttributes(attr, value, []string{""}) + if err != nil { + return nil, err + } + dn = entry.DN + } + + objectGroups, err := c.getGroups(dn) + if err != nil { + return nil, err + } + + var matchedGroups []string + + for _, objectGroup := range objectGroups { + for _, parentGroup := range groups { + if objectGroup.DN == parentGroup { + matchedGroups = append(matchedGroups, parentGroup) + continue + } + } + } + + return matchedGroups, nil +} + +//ObjectPrimaryGroup returns the DN of the primary group of the object with the given attribute value +//or an error if one occurred. Not all LDAP objects have a primary group. +func (c *Conn) ObjectPrimaryGroup(attr, value string) (string, error) { + entry, err := c.GetAttributes(attr, value, []string{"objectSid", "primaryGroupID"}) + if err != nil { + return "", err + } + + gidStr := entry.GetAttributeValue("primaryGroupID") + if gidStr == "" { + return "", errors.New("Search error: primaryGroupID not found") + } + + gid, err := strconv.Atoi(entry.GetAttributeValue("primaryGroupID")) + if err != nil { + return "", fmt.Errorf(`Parse error: invalid primaryGroupID ("%s"): %w`, gidStr, err) + } + + uSID := entry.GetRawAttributeValue("objectSid") + gSID := make([]byte, len(uSID)) + copy(gSID, uSID) + binary.LittleEndian.PutUint32(gSID[len(gSID)-4:], uint32(gid)) + + encoded := "" + for _, b := range gSID { + encoded += fmt.Sprintf(`\%02x`, b) + } + + entry, err = c.SearchOne(fmt.Sprintf("(objectSid=%s)", encoded), nil) + if err != nil { + return "", fmt.Errorf("Search error: primary group not found: %w", err) + } + + return entry.DN, nil +} diff --git a/vendor/github.com/abbas-gheydi/go-ad-auth/v3/passwd.go b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/passwd.go new file mode 100644 index 0000000..9052930 --- /dev/null +++ b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/passwd.go @@ -0,0 +1,80 @@ +package auth + +import ( + "errors" + "fmt" + + ldap "github.com/go-ldap/ldap/v3" + "golang.org/x/text/encoding/unicode" +) + +//ModifyDNPassword sets a new password for the given user or returns an error if one occurred. +//ModifyDNPassword is used for resetting user passwords using administrative privileges. +func (c *Conn) ModifyDNPassword(dn, newPasswd string) error { + utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM) + encoded, err := utf16.NewEncoder().String(fmt.Sprintf(`"%s"`, newPasswd)) + if err != nil { + return fmt.Errorf("Password error: Unable to encode password: %w", err) + } + + req := ldap.NewModifyRequest(dn, nil) + req.Replace("unicodePwd", []string{encoded}) + + err = c.Conn.Modify(req) + if err != nil { + return fmt.Errorf("Password error: Unable to modify password: %w", err) + } + + return nil +} + +//UpdatePassword checks if the given credentials are valid and updates the password if they are, +//or returns an error if one occurred. UpdatePassword is used for users resetting their own password. +func UpdatePassword(config *Config, username, oldPasswd, newPasswd string) error { + utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM) + oldEncoded, err := utf16.NewEncoder().String(fmt.Sprintf(`"%s"`, oldPasswd)) + if err != nil { + return fmt.Errorf("Password error: Unable to encode old password: %w", err) + } + + newEncoded, err := utf16.NewEncoder().String(fmt.Sprintf(`"%s"`, newPasswd)) + if err != nil { + return fmt.Errorf("Password error: Unable to encode new password: %w", err) + } + + upn, err := config.UPN(username) + if err != nil { + return err + } + + conn, err := config.Connect() + if err != nil { + return err + } + defer conn.Conn.Close() + + //bind + status, err := conn.Bind(upn, oldPasswd) + if err != nil { + return err + } + if !status { + return errors.New("Password error: credentials not valid") + } + + dn, err := conn.GetDN("userPrincipalName", upn) + if err != nil { + return err + } + + req := ldap.NewModifyRequest(dn, nil) + req.Delete("unicodePwd", []string{oldEncoded}) + req.Add("unicodePwd", []string{newEncoded}) + + err = conn.Conn.Modify(req) + if err != nil { + return fmt.Errorf("Password error: Unable to modify password: %w", err) + } + + return nil +} diff --git a/vendor/github.com/abbas-gheydi/go-ad-auth/v3/search.go b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/search.go new file mode 100644 index 0000000..32fa63d --- /dev/null +++ b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/search.go @@ -0,0 +1,82 @@ +package auth + +import ( + "fmt" + + ldap "github.com/go-ldap/ldap/v3" +) + +//Search returns the entries for the given search criteria or an error if one occurred. +func (c *Conn) Search(filter string, attrs []string, sizeLimit int) ([]*ldap.Entry, error) { + search := ldap.NewSearchRequest( + c.Config.BaseDN, + ldap.ScopeWholeSubtree, + ldap.DerefAlways, + sizeLimit, + 0, + false, + filter, + attrs, + nil, + ) + result, err := c.Conn.Search(search) + if err != nil { + return nil, fmt.Errorf(`Search error "%s": %w`, filter, err) + } + + return result.Entries, nil +} + +//SearchOne returns the single entry for the given search criteria or an error if one occurred. +//An error is returned if exactly one entry is not returned. +func (c *Conn) SearchOne(filter string, attrs []string) (*ldap.Entry, error) { + search := ldap.NewSearchRequest( + c.Config.BaseDN, + ldap.ScopeWholeSubtree, + ldap.DerefAlways, + 1, + 0, + false, + filter, + attrs, + nil, + ) + + result, err := c.Conn.Search(search) + if err != nil { + if e, ok := err.(*ldap.Error); ok { + if e.ResultCode == ldap.LDAPResultSizeLimitExceeded { + return nil, fmt.Errorf(`Search error "%s": more than one entries returned`, filter) + } + } + + return nil, fmt.Errorf(`Search error "%s": %w`, filter, err) + } + + if len(result.Entries) == 0 { + return nil, fmt.Errorf(`Search error "%s": no entries returned`, filter) + } + + return result.Entries[0], nil +} + +//GetDN returns the DN for the object with the given attribute value or an error if one occurred. +//attr and value are sanitized. +func (c *Conn) GetDN(attr, value string) (string, error) { + entry, err := c.SearchOne(fmt.Sprintf("(%s=%s)", ldap.EscapeFilter(attr), ldap.EscapeFilter(value)), []string{""}) + if err != nil { + return "", err + } + + return entry.DN, nil +} + +//GetAttributes returns the *ldap.Entry with the given attributes for the object with the given attribute value or an error if one occurred. +//attr and value are sanitized. +func (c *Conn) GetAttributes(attr, value string, attrs []string) (*ldap.Entry, error) { + return c.SearchOne(fmt.Sprintf("(%s=%s)", ldap.EscapeFilter(attr), ldap.EscapeFilter(value)), attrs) +} + +func (c *Conn) getGroups(dn string) ([]*ldap.Entry, error) { + return c.Search(fmt.Sprintf("(member:%s:=%s)", LDAPMatchingRuleInChain, ldap.EscapeFilter(dn)), []string{""}, 1000) +} diff --git a/vendor/github.com/abbas-gheydi/go-ad-auth/v3/sid.go b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/sid.go new file mode 100644 index 0000000..575a757 --- /dev/null +++ b/vendor/github.com/abbas-gheydi/go-ad-auth/v3/sid.go @@ -0,0 +1,143 @@ +package auth + +import ( + "encoding/binary" + "errors" + "fmt" + "strconv" + "strings" +) + +// The only valid SID revision is 1 +const ( + SIDRevision = 1 + SIDRevisionStr = "1" +) + +var ( + ErrInvalidSIDHeader = errors.New("invalid sid header") + ErrInvalidSID = errors.New("invalid sid") +) + +// SID represents the structure of a security identifier, described at https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid +type SID struct { + Revision byte + SubAuthorityLength byte + IdentifierAuthority uint64 // 6 bytes, big endian + SubAuthoritys []uint32 // little endian +} + +// RID returns the relative identifier for sid. If RID returns 0, the caller should verify sid actually has sub authorities before using 0 as an actual RID +func (sid *SID) RID() uint32 { + if len(sid.SubAuthoritys) > 0 { + return sid.SubAuthoritys[len(sid.SubAuthoritys)-1] + } + return 0 +} + +// Equal returns true if sid == other +func (sid *SID) Equal(other *SID) bool { + if sid.Revision != other.Revision || + sid.SubAuthorityLength != other.SubAuthorityLength || + sid.IdentifierAuthority != other.IdentifierAuthority || + len(sid.SubAuthoritys) != len(other.SubAuthoritys) { + return false + } + for idx, sub := range sid.SubAuthoritys { + if sub != other.SubAuthoritys[idx] { + return false + } + } + return true +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface +func (sid *SID) UnmarshalBinary(buf []byte) error { + if l := len(buf); l < 8 || buf[0] != SIDRevision { // check static header + return ErrInvalidSIDHeader + } else if l != 8+(4*int(buf[1])) { // + return ErrInvalidSID + } + + sid.Revision = buf[0] + sid.SubAuthorityLength = buf[1] + sid.IdentifierAuthority = binary.BigEndian.Uint64(append([]byte{0, 0}, buf[2:8]...)) + sid.SubAuthoritys = make([]uint32, int(buf[1])) + + for idx := 0; idx < int(buf[1]); idx++ { + sid.SubAuthoritys[idx] = binary.LittleEndian.Uint32(buf[8+4*idx : 8+4*idx+4]) + } + + return nil +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface +func (sid *SID) MarshalBinary() ([]byte, error) { + return sid.marshalBinary(), nil +} + +func (sid *SID) marshalBinary() []byte { + buf := make([]byte, 8+4*int(sid.SubAuthorityLength)) + binary.BigEndian.PutUint64(buf, sid.IdentifierAuthority) + buf[0] = sid.Revision + buf[1] = sid.SubAuthorityLength + for idx := 0; idx < int(sid.SubAuthorityLength); idx++ { + binary.LittleEndian.PutUint32(buf[8+4*idx:8+4*idx+4], sid.SubAuthoritys[idx]) + } + + return buf +} + +// String returns the string representation of sid, e.g. "S-1-5-..." +func (sid *SID) String() string { + subs := make([]string, len(sid.SubAuthoritys)) + for idx, sub := range sid.SubAuthoritys { + subs[idx] = strconv.FormatUint(uint64(sub), 10) + } + header := fmt.Sprintf("S-%d-%d", sid.Revision, sid.IdentifierAuthority) + return strings.Join([]string{header, strings.Join(subs, "-")}, "-") +} + +// ParseSID parses a string representation of an SID, e.g. what *SID.String returns +func ParseSID(s string) (*SID, error) { + strs := strings.Split(s, "-") + if len(strs) < 3 || strs[0] != "S" || strs[1] != SIDRevisionStr { + return nil, ErrInvalidSIDHeader + } + + sid := &SID{ + Revision: 1, + } + var err error + sid.IdentifierAuthority, err = strconv.ParseUint(strs[2], 10, 48) + if err != nil { + return nil, fmt.Errorf("invalid identifier authority: %w", err) + } + + subs := make([]uint32, len(strs)-3) + for idx, str := range strs[3:] { + sub, err := strconv.ParseUint(str, 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid sub authority (%s): %w", str, err) + } + subs[idx] = uint32(sub) + } + + sid.SubAuthorityLength = byte(len(subs)) + sid.SubAuthoritys = subs + + return sid, nil +} + +// FilterString returns an escaped binary representation of sid suitable for use in ldap filters. +// e.g. filter := fmt.Sprintf("(objectSid=%s)", sid.FilterString()) +func (sid *SID) FilterString() string { + buf := sid.marshalBinary() + + var filter strings.Builder + for _, b := range buf { + filter.WriteString(fmt.Sprintf(`\%02x`, b)) + } + + return filter.String() +} diff --git a/vendor/modules.txt b/vendor/modules.txt index b6beb6e..db38fec 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,6 +1,9 @@ # github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e ## explicit github.com/Azure/go-ntlmssp +# github.com/abbas-gheydi/go-ad-auth/v3 v3.4.41 +## explicit; go 1.13 +github.com/abbas-gheydi/go-ad-auth/v3 # github.com/beorn7/perks v1.0.1 ## explicit; go 1.11 github.com/beorn7/perks/quantile